Documentation
¶
Overview ¶
Package store owns Ken's embedded SQLite database: schema migrations plus the search/get queries. Writes serialize through a single-writer pool; reads use a separate pool. WAL lets readers run concurrently with the writer.
Index ¶
- Constants
- Variables
- func LangForeign(contentLang string, curationLangs []string) bool
- func VerifySnapshot(ctx context.Context, path string) (int, error)
- type ActorCandidate
- type BrowseFilter
- type BrowseRow
- type CodeData
- type Content
- type DiffResult
- type DirectoryEntry
- type EmbedTarget
- type ErrTransferCollision
- type FieldDiff
- type HumanCred
- type HumanUser
- type ImportInput
- type LinkInput
- type NewAuthCode
- type OAuthClient
- type OAuthGrantRow
- type OAuthPrincipal
- type Patch
- type PromoteInput
- type ProposalRow
- type ProposeInput
- type ProposeResult
- type RecentEntry
- type RefreshResult
- type ReviewData
- type SaveInput
- type SaveResult
- type SearchOpts
- type Session
- type Station
- type StationKey
- type StationLink
- type StationLockerEntry
- type StationLockerLimits
- type StationNote
- type StationNoteLimits
- type StationPrincipal
- type StationRequestRow
- type StationTask
- type StationTaskLimits
- type StationUsage
- type Store
- func (s *Store) ActorExists(ctx context.Context, id int64) (bool, error)
- func (s *Store) ActorsWithCommStatus(ctx context.Context) ([]ActorCandidate, error)
- func (s *Store) AddStationTask(ctx context.Context, lim StationTaskLimits, t StationTask, tokenID string, ...) (*StationTask, []StationTask, error)
- func (s *Store) ApproveLinkRequest(ctx context.Context, requestID string, actorID int64) (*StationLink, error)
- func (s *Store) ApproveStationRequest(ctx context.Context, requestID, name string, actorID int64) (*Station, error)
- func (s *Store) ArchiveStation(ctx context.Context, stationID string, archived bool) error
- func (s *Store) AreStationsLinked(ctx context.Context, x, y string) (bool, error)
- func (s *Store) AuthenticateStationKey(ctx context.Context, presented string) (*StationPrincipal, error)
- func (s *Store) BriefStationTasks(ctx context.Context, lim StationTaskLimits, stationID string) (*TaskBriefing, error)
- func (s *Store) Close() error
- func (s *Store) CloseStationTasks(ctx context.Context, stationID string, taskIDs []string, ...) (int, error)
- func (s *Store) CountActiveOAuthGrants(ctx context.Context) (int, error)
- func (s *Store) CountActiveTokens(ctx context.Context) (int, error)
- func (s *Store) CountEntries(ctx context.Context) (int, error)
- func (s *Store) CountHumanUsers(ctx context.Context) (int, error)
- func (s *Store) CountProposals(ctx context.Context) (int, error)
- func (s *Store) CountVersions(ctx context.Context) (int, error)
- func (s *Store) CreateFirstAdmin(ctx context.Context, name, pwHash string) (bool, error)
- func (s *Store) CreateHumanUser(ctx context.Context, name, pwHash string) (int64, error)
- func (s *Store) CreateOAuthGrantAndCode(ctx context.Context, in NewAuthCode, codeTTL time.Duration) (string, error)
- func (s *Store) CreateSession(ctx context.Context, actorID int64, ttl time.Duration) (*Session, error)
- func (s *Store) CreateStation(ctx context.Context, spaceID int64, name, purpose string, actorID int64) (*Station, error)
- func (s *Store) CreateStationLinkRequest(ctx context.Context, spaceID int64, ...) (string, error)
- func (s *Store) CreateStationRequest(ctx context.Context, spaceID int64, ...) (string, error)
- func (s *Store) CrossStationHumanTasks(ctx context.Context, spaceID int64, blockedOn string, limit int) ([]StationTask, error)
- func (s *Store) DeferStationTask(ctx context.Context, stationID, taskID, until, reason string) error
- func (s *Store) DeleteExpiredSessions(ctx context.Context) (int64, error)
- func (s *Store) DeleteSession(ctx context.Context, id string) error
- func (s *Store) DeleteStationLockerBlob(ctx context.Context, stationID, name string) error
- func (s *Store) DenyStationRequest(ctx context.Context, requestID, reason string, actorID int64) error
- func (s *Store) DistinctCategories(ctx context.Context) ([]string, error)
- func (s *Store) DropStationTasks(ctx context.Context, stationID string, taskIDs []string, reason string, ...) (int, error)
- func (s *Store) EmbeddingStats(ctx context.Context) (embedded, total int, err error)
- func (s *Store) ExchangeOAuthCode(ctx context.Context, code string, grantID int64, ...) (access, refresh string, err error)
- func (s *Store) FindActor(ctx context.Context, kind, name string) (int64, error)
- func (s *Store) FindOrCreateActor(ctx context.Context, kind, name string) (int64, error)
- func (s *Store) FirstHumanActor(ctx context.Context) (int64, error)
- func (s *Store) FlagStale(ctx context.Context, slug, reason string, actorID int64, actorKind string) (string, error)
- func (s *Store) Get(ctx context.Context, slugs []string, detailed bool) (entries []model.Entry, missing []string, err error)
- func (s *Store) GetEntry(ctx context.Context, slug string) (*model.Entry, error)
- func (s *Store) GetSettings(ctx context.Context) (map[string]string, error)
- func (s *Store) GetStationLockerBlob(ctx context.Context, stationID, name string) (*StationLockerEntry, error)
- func (s *Store) HandoffStaleness(ctx context.Context, stationID string) (writtenAt string, activitiesSince int, err error)
- func (s *Store) History(ctx context.Context, slug string) ([]VersionRow, error)
- func (s *Store) HumanByName(ctx context.Context, name string) (*HumanCred, error)
- func (s *Store) ImportEntry(ctx context.Context, in ImportInput) (created bool, err error)
- func (s *Store) IsStationKeyRevoked(ctx context.Context, tokenID string) (bool, error)
- func (s *Store) IssueBindingVoucher(ctx context.Context, stationID, tokenID, forEndpoint string, ...) (string, error)
- func (s *Store) IssueStationKey(ctx context.Context, actorID int64, stationID, label string, scopes []string) (string, error)
- func (s *Store) IssueToken(ctx context.Context, actorID int64, scopes []string, label string) (string, error)
- func (s *Store) ListEntries(ctx context.Context, f BrowseFilter) ([]BrowseRow, bool, error)
- func (s *Store) ListHumanUsers(ctx context.Context) ([]HumanUser, error)
- func (s *Store) ListOAuthGrants(ctx context.Context) ([]OAuthGrantRow, error)
- func (s *Store) ListProposals(ctx context.Context) ([]ProposalRow, error)
- func (s *Store) ListStationKeys(ctx context.Context, stationID string) ([]StationKey, error)
- func (s *Store) ListStationLinks(ctx context.Context, spaceID int64) ([]StationLink, error)
- func (s *Store) ListStationLocker(ctx context.Context, stationID string) ([]StationLockerEntry, error)
- func (s *Store) ListStationNotes(ctx context.Context, stationID string) ([]StationNote, error)
- func (s *Store) ListStationTasks(ctx context.Context, lim StationTaskLimits, stationID, state, blockedOn string, ...) ([]StationTask, int, error)
- func (s *Store) ListStations(ctx context.Context, spaceID int64) ([]Station, error)
- func (s *Store) ListStationsVisibleTo(ctx context.Context, spaceID int64, fromStation string) ([]DirectoryEntry, error)
- func (s *Store) ListTokens(ctx context.Context) ([]TokenRow, error)
- func (s *Store) Migrate() error
- func (s *Store) OAuthClientByID(ctx context.Context, clientID string) (*OAuthClient, error)
- func (s *Store) PeekOAuthCode(ctx context.Context, code string) (*CodeData, error)
- func (s *Store) PendingStationRequests(ctx context.Context, spaceID int64) ([]StationRequestRow, error)
- func (s *Store) Promote(ctx context.Context, in PromoteInput) error
- func (s *Store) PromoteStationNote(ctx context.Context, stationID, key string) (string, error)
- func (s *Store) ProposalReview(ctx context.Context, versionID int64) (*ReviewData, error)
- func (s *Store) ProposeEnhancement(ctx context.Context, in ProposeInput) (ProposeResult, error)
- func (s *Store) ProvisionalReview(ctx context.Context, slug string) (*ReviewData, error)
- func (s *Store) PurgeExpiredOAuth(ctx context.Context) error
- func (s *Store) PutStationLockerBlob(ctx context.Context, lim StationLockerLimits, stationID, name string, ...) (*StationLockerEntry, error)
- func (s *Store) ReadStationNote(ctx context.Context, stationID, key string) (*StationNote, error)
- func (s *Store) RecentContext(ctx context.Context, sinceDays, limit int, kind string) ([]RecentEntry, error)
- func (s *Store) RecordOutcome(ctx context.Context, slug, outcome string, actorID int64, ...) (staleness string, err error)
- func (s *Store) RedeemBindingVoucher(ctx context.Context, voucher, endpointID string, byActor int64) (stationID, tokenID string, err error)
- func (s *Store) RegisterOAuthClient(ctx context.Context, name string, redirectURIs []string) (string, error)
- func (s *Store) Reject(ctx context.Context, slug string, versionID, actorID int64, ...) error
- func (s *Store) RenameStation(ctx context.Context, stationID, name string) error
- func (s *Store) ReopenStationTasks(ctx context.Context, stationID string, taskIDs []string, reason string) (int, error)
- func (s *Store) Repromote(ctx context.Context, in PromoteInput) error
- func (s *Store) RetireStationKey(ctx context.Context, tokenID string) error
- func (s *Store) RevokeOAuthGrant(ctx context.Context, id int64) error
- func (s *Store) RevokeStationLink(ctx context.Context, linkID string) error
- func (s *Store) RevokeToken(ctx context.Context, tokenID string) error
- func (s *Store) RotateOAuthRefresh(ctx context.Context, refresh string, accessTTL, refreshTTL time.Duration) (access, newRefresh string, rr *RefreshResult, err error)
- func (s *Store) Save(ctx context.Context, in SaveInput) (SaveResult, error)
- func (s *Store) Search(ctx context.Context, query string, opt SearchOpts) ([]model.SearchResult, error)
- func (s *Store) SearchPage(ctx context.Context, query string, opt SearchOpts) ([]model.SearchResult, bool, error)
- func (s *Store) SeedDemo(ctx context.Context) (string, error)
- func (s *Store) SessionByID(ctx context.Context, id string) (*Session, error)
- func (s *Store) SetDetector(d lang.Detector)
- func (s *Store) SetSettings(ctx context.Context, upsert map[string]string, remove []string, updater string) error
- func (s *Store) SetStationPublished(ctx context.Context, stationID string, published bool) error
- func (s *Store) SetStationSelfDescription(ctx context.Context, stationID, about string, tags []string) error
- func (s *Store) Snapshot(ctx context.Context, dest string) error
- func (s *Store) StationAssetUsage(ctx context.Context, stationID string) (*StationUsage, error)
- func (s *Store) StationByID(ctx context.Context, stationID string) (*Station, error)
- func (s *Store) StationByName(ctx context.Context, spaceID int64, name string) (*Station, error)
- func (s *Store) StationKeyOwner(ctx context.Context, tokenID string) (string, error)
- func (s *Store) StationLinkByID(ctx context.Context, linkID string) (StationLink, error)
- func (s *Store) StationTaskByID(ctx context.Context, taskID string) (*StationTask, error)
- func (s *Store) SweepBindingVouchers(ctx context.Context) (int, error)
- func (s *Store) TouchStationActivity(ctx context.Context, stationID string) error
- func (s *Store) TouchToken(ctx context.Context, tokenID string)
- func (s *Store) TransferStationAssets(ctx context.Context, fromID, toID string, notes, tasks, locker bool) (*TransferResult, error)
- func (s *Store) UpsertEmbedding(ctx context.Context, versionID int64, modelID string, vec []float32) error
- func (s *Store) ValidateOAuthAccessToken(ctx context.Context, token string) (*OAuthPrincipal, error)
- func (s *Store) VersionDiff(ctx context.Context, slug string, revA, revB int) (*DiffResult, error)
- func (s *Store) VersionsNeedingEmbedding(ctx context.Context, modelID string, limit int) ([]EmbedTarget, error)
- func (s *Store) WriteStationNote(ctx context.Context, lim StationNoteLimits, stationID, key, title, body string, ...) (*StationNote, error)
- type TaskBriefing
- type TokenRow
- type TransferResult
- type VersionRow
Constants ¶
const HandoffKey = "handoff"
HandoffKey is the reserved page the briefing reads first, transfer collides on, and every station is expected to keep. A handoff written only on the way out is never written, so maintaining it is a duty of the current session.
const MaxSelfDescriptionBytes = 4 << 10
MaxSelfDescriptionBytes caps the one field an agent may write about itself.
It was uncapped, and slice 2 made that dangerous rather than merely untidy: the directory tools now carry this text verbatim into every peer's context, so an unbounded field became an unbounded write into other agents' working memory. A 700 KiB self-description was accepted and returned byte-identical to a different station. Every sibling agent-writable payload is bounded — notebook pages at 64 KiB, locker blobs at 256 KiB, task text at 512 B — and this one was reachable only by the MCP body limit.
4 KiB is generous for "what I know and am responsible for" and small enough that a directory listing many stations stays readable.
const MaxSelfDescriptionTags = 24
MaxSelfDescriptionTags bounds the tag list separately: the byte cap alone would permit thousands of one-character tags.
const VoucherTTL = 5 * time.Minute
VoucherTTL is deliberately short. A voucher is redeemed by the same session that asked for it, in its very next tool call, so minutes is generous — and every additional minute is time a value sitting in a transcript stays live.
Variables ¶
var ( ErrSlugConflict = errors.New("an entry with that slug already exists") ErrNotFound = errors.New("entry not found") ErrBadVersion = errors.New("version not found or not in a promotable state") // ErrInvalid wraps user-facing validation failures (safe to surface to clients). ErrInvalid = errors.New("invalid input") // ErrForeignLang blocks promoting a version whose detected content language is // not one the curator declared they can read — the "can't promote what you // can't read" comprehension gate. It is enforced in the store (Promote / // Repromote), not just the UI, so no server-side path reaches the head with // unreadable content. ErrForeignLang = errors.New("proposal is not in a curation language") // OAuth authorization-server errors (see internal/store/oauth.go). ErrOAuthNoClient = errors.New("oauth client not found") ErrOAuthBadCode = errors.New("authorization code invalid, expired, or already used") ErrOAuthBadToken = errors.New("oauth token invalid, expired, or revoked") ErrOAuthReuseKill = errors.New("refresh token reuse detected — grant revoked") )
Sentinel errors surfaced to callers (MCP tools, web handlers).
var ErrLockerCapReached = errors.New("locker cap reached")
ErrLockerCapReached refuses rather than evicting (S12).
var ErrNoteRevConflict = errors.New("notebook page changed underneath this write")
ErrNoteRevConflict is returned when an `if_rev` precondition fails. Two sessions may staff one station (S4), so a blind write would silently destroy the other's page.
var ErrNotebookCapReached = errors.New("notebook cap reached")
ErrNotebookCapReached refuses rather than evicting (S12): silent eviction of a working note is data loss the session cannot see, a refusal is an error the model reacts to.
var ErrRequestNotPending = errors.New("this request is no longer pending — it was already decided")
ErrRequestNotPending is returned when a request has already been decided. It is distinct from "not found" on purpose: the console shows it as "someone already handled this" rather than as a broken link, which is the likely truth when two browser tabs are open on the same queue.
var ErrStationKeyRevoked = errors.New("the station key that bound this endpoint has been revoked — tell your human; you cannot reconnect with it, and a new key must be minted from the console")
ErrStationKeyRevoked is returned to a caller whose station key has been revoked. It is DISTINGUISHABLE from an ordinary auth failure on purpose (S6): a model that is told its key was revoked reports that to its human, while one that merely sees "invalid" retries in a loop. This does not weaken §5's unprobeability, because it is returned only AFTER the endpoint's own secret has verified — it informs a proven holder and tells a prober nothing.
var ErrStationNameTaken = errors.New("station name already in use in this space")
ErrStationNameTaken is returned when a name collides within a space. Names are display-only and unique per space; routing is always by the opaque station_id, so a collision is a human-facing inconvenience rather than an addressing failure.
var ErrTaskCapReached = errors.New("open task cap reached")
ErrTaskCapReached is returned instead of evicting. S12: refuse, never evict — silent eviction of a working note is data loss the session cannot see, while a refusal is an error the model reads and reacts to.
var ErrVoucherInvalid = errors.New("binding voucher is not valid — it may be unknown, already used, or expired (they last a few minutes; ask /station for a fresh one)")
ErrVoucherInvalid covers unknown, expired and already-redeemed vouchers with ONE error. The three cases are never distinguished to a caller: the redeemer is the comm endpoint, which is authenticated but is NOT the station's credential holder, so telling it which vouchers exist would leak across the very boundary the voucher indirection was introduced to protect.
var ErrVoucherNotForThisEndpoint = errors.New("this binding voucher names a different endpoint than the one redeeming it. " +
"A voucher is minted FOR one endpoint and only that endpoint can use it, so a voucher that leaked or was meant " +
"for another session is useless here. Ask /station for a voucher naming THIS endpoint_id and redeem it with comm_bind")
ErrVoucherNotYours is returned when a voucher is real and live but was issued to a different actor than the one now presenting it.
This is DISTINGUISHED from ErrVoucherInvalid, which deliberately collapses unknown, used and expired into one string — so the difference needs defending, or a later reader will "fix" it back by merging them.
Collapsing those three protects a secret an attacker might GUESS. This one cannot be reached by guessing: the caller must already hold a live 32-character voucher, which means it was handed to them. Against that caller the distinction reveals one bit — "the voucher is real, and you are not who it was for" — and they cannot act on it, because the actor is what the check requires and it is not something they can present.
What the distinction buys is the entire diagnosis. An actor mismatch is a SETUP error, not an attack: the station key was minted under a different actor than the one holding the comm token, which is a configuration a deployment can sit in for months without symptom. Reported as "voucher is not valid" it looks like an expiry race, and the operator issues fresh vouchers forever, each failing identically. ErrVoucherNotForThisEndpoint is returned when a voucher is real and live but names a different endpoint than the one redeeming it.
Distinguished from ErrVoucherNotYours for a practical reason: the two demand OPPOSITE responses. This one is fixed by asking for a voucher that names your endpoint — a retry that works. An actor mismatch is fixed only by re-minting a station key from the console, and retrying it forever is exactly what a collapsed error causes.
Safe to distinguish on the same reasoning as ErrVoucherNotYours: reaching this requires already holding a live 32-character voucher, so it cannot be reached by guessing, and the one bit it reveals — "real, but not for you" — is not actionable by someone who cannot produce the named endpoint's secret.
var ErrVoucherNotYours = errors.New("this binding voucher was issued to a different identity than the one presenting it — " +
"the station key that minted it belongs to a different actor than the comm token this endpoint registered under. " +
"Nothing is wrong with the voucher. Mint a station key under the actor that holds this endpoint's comm token " +
"(the /stations console lists which actor each key belongs to and whether it has a comm token) and try again")
Functions ¶
func LangForeign ¶
LangForeign reports whether a detected content language would be BLOCKED for promotion under the given curation languages — the inverse of the internal gate. The web layer uses it to flag out-of-language proposals on the review queue with the exact same rule the store enforces (no drift between the badge and the gate).
func VerifySnapshot ¶
VerifySnapshot opens a database file and runs the backup's mandatory checks: PRAGMA integrity_check, foreign-key integrity, the FTS5 internal integrity-check on both indexes, a functional MATCH canary, embedding vector-length parity, and returns the entry count (for the caller to reconcile against the source). Opened read-write so the FTS integrity-check can run; a VACUUM-INTO snapshot is in rollback-journal mode, so no -wal/-shm persists.
Types ¶
type ActorCandidate ¶ added in v1.5.2
type ActorCandidate struct {
ID int64
Kind string
Name string
HasComm bool
CommTags string // labels of its comm tokens, for a human to recognise the machine
}
ActorCandidate is an actor that could own a station key, with whether it already holds a comm token — which is the thing that has to match (S5).
type BrowseFilter ¶
type BrowseFilter struct {
Category string // exact match; "" = any
Kind string // user|feedback|project|reference; "" = any
Staleness string // fresh|aging|stale|refuted; "" = any
Lifecycle string // draft|active|deprecated; "" = any non-archived
Sort string // updated (default) | title | used | created | kind
Limit int // default 50, max 200
Offset int
}
BrowseFilter parameterizes ListEntries. Every field is optional; the zero value lists all non-archived entries, newest-updated first.
type BrowseRow ¶
type BrowseRow struct {
Slug string
Title string
Summary string
Kind string
Category string
Staleness string
Lifecycle string
CuratedRev int
UseCount int
HasProvisional bool
UpdatedAt string
}
BrowseRow is one entry as shown in the browse listing. Every field is read straight from the denormalized entry row (the curated head's title/summary/ category are kept in sync on promotion), so browsing never joins entry_version.
type CodeData ¶
type CodeData struct {
GrantID int64
ClientID string
RedirectURI string
CodeChallenge string
CodeChallengeMethod string
Scope string
Resource string
}
CodeData is the authorization-code record needed to validate a token exchange.
type Content ¶
type Content struct {
Title, Summary, Problem, Solution, Rationale, Caveats string
Code []model.CodeSnippet
Tags, Triggers, AppliesTo []string
VerifiedAgainst []model.VerifiedRef
}
Content is the writable content of an entry version.
type DiffResult ¶
DiffResult is the field-by-field diff of two revisions of an entry.
type DirectoryEntry ¶ added in v1.6.0
type DirectoryEntry struct {
Station
Linked bool // an active link with the asking station exists RIGHT NOW
}
DirectoryEntry is one row of the station directory: a station another station is allowed to know exists, plus whether the two may currently talk.
Linked is separate from visibility on purpose. Discovery and permission are different questions, and collapsing them makes the directory useless for the case it exists to serve — you cannot ask your human for a link to a station you were never allowed to see the name of.
type EmbedTarget ¶
EmbedTarget is a version that needs an embedding computed.
type ErrTransferCollision ¶ added in v1.5.0
ErrTransferCollision reports asset names that exist on BOTH stations. It carries the names because the human cannot act on a bare refusal: they have to rename or drop something, and they need to know what.
func (*ErrTransferCollision) Error ¶ added in v1.5.0
func (e *ErrTransferCollision) Error() string
type ImportInput ¶
type ImportInput struct {
Slug string
Kind string
Content Content
ChangeNote string
Links []LinkInput
}
ImportInput migrates a flat-memory file into Ken as a curated rev-1 entry.
type LinkInput ¶
type LinkInput struct{ ToSlug, LinkType string }
LinkInput is a [[wikilink]] to create from the new entry.
type NewAuthCode ¶
type NewAuthCode struct {
ClientID string
ConnectorActorID int64
HumanActorID int64
RedirectURI string
CodeChallenge string
CodeChallengeMethod string
Scope string
Resource string
}
NewAuthCode is the input to CreateOAuthGrantAndCode: everything captured at the consent step. ConnectorActorID is the 'ai' actor that will author MCP writes; HumanActorID is the curator who approved.
type OAuthClient ¶
OAuthClient is a registered public client (PKCE; no secret).
type OAuthGrantRow ¶
type OAuthGrantRow struct {
ID int64
ClientName string
ApprovedBy string
Scope string
CreatedAt string
ActiveTokens int
}
OAuthGrantRow is one live connector grant, for the curator UI.
type OAuthPrincipal ¶
OAuthPrincipal is the resolved identity behind a valid OAuth access token.
type Patch ¶
type Patch struct {
Title, Summary, Problem, Solution, Rationale, Caveats *string
Code *[]model.CodeSnippet
Tags, Triggers, AppliesTo *[]string
VerifiedAgainst *[]model.VerifiedRef
}
Patch carries the fields to change in an enhancement; nil fields inherit from the based-on version (so an enhancement need only send what it changes).
type PromoteInput ¶
type PromoteInput struct {
Slug string
VersionID int64
ActorID int64
ActorKind string
Note string
// CurationLangs, when non-empty, enforces the comprehension gate: a version
// whose detected content language is not one of these is refused with
// ErrForeignLang. Empty ⇒ no language restriction (feature off). The web
// handler fills it from the live settings snapshot.
CurationLangs []string
}
PromoteInput promotes a proposed version to the curated head (the curation gate).
type ProposalRow ¶
type ProposalRow struct {
Slug string
Title string
Kind string
NProposals int
LatestRev int
LatestVersionID int64
LatestConfidence float64
LatestChangeNote string
LatestLang string // detected content language of the latest proposal ("" ⇒ undetected/legacy)
// LatestViaComm marks the latest proposal as possibly SECOND-HAND: the token
// that authored it had recently received an inter-session message
// (docs/COMM.md §7). False means "no signal", never "known first-hand" — it is
// a prompt to ask for a citation, not a verdict.
LatestViaComm bool
}
ProposalRow summarizes an entry that has pending proposals (the review queue).
type ProposeInput ¶
type ProposeInput struct {
Slug string
BasedOnRev int // 0 => base on the current curated head
ChangeNote string
Confidence float64
AuthorActorID int64
AuthorKind string
SessionID string
Patch Patch
// ViaComm — see SaveInput.ViaComm.
ViaComm bool
}
ProposeInput appends an enhancement to an existing entry.
type ProposeResult ¶
ProposeResult reports the appended version and any rebase warning.
type RecentEntry ¶
type RecentEntry struct {
Slug, Title, Summary, Kind, LastEvent, LastAt string
}
RecentEntry is one row of the recent-activity briefing.
type RefreshResult ¶
RefreshResult reports the grant a rotated refresh belongs to.
type ReviewData ¶
type ReviewData struct {
Slug string
EntryTitle string
ProposalVID int64
ProposalRev int
ProposalState string
ChangeNote string
// ViaComm marks this proposal as possibly second-hand (docs/COMM.md §7). It
// belongs here, not only on the review-queue listing, because THIS is the view
// that carries the Promote button — a hearsay warning that never reaches the
// moment of promotion is not a mitigation.
ViaComm bool
Proposal Content
HasCurated bool
CuratedRev int
Curated Content
}
ReviewData is the material for a proposal diff view: the proposal content alongside the current curated head content.
type SaveInput ¶
type SaveInput struct {
Slug string // optional; derived from the title if empty
Kind string
Category string
Content Content
Confidence float64
AuthorActorID int64 // 0 => NULL (e.g. the dev token)
AuthorKind string
SessionID string
Links []LinkInput
// ViaComm marks the version as possibly second-hand: the authoring token had
// recently RECEIVED an inter-session message (docs/COMM.md §7). It is a prompt
// for the curator's judgement, not a verdict — false means "no signal", never
// "known first-hand".
ViaComm bool
}
SaveInput creates a new draft entry with its first (proposed) version.
type SaveResult ¶
type SaveResult struct {
Slug string
EntryID int64
VersionID int64
RevNo int
Lifecycle string
State string
}
SaveResult reports the created entry.
type SearchOpts ¶
type SearchOpts struct {
Kind string
Category string
Scope string // curated (default) | proposals | history | all
K int
Offset int
QueryVec []float32 // optional; when set, adds a semantic (vector) arm
EmbedModel string // model id of QueryVec; only vectors from this model are compared
}
SearchOpts filters a kb_search query.
type Station ¶ added in v1.4.2
type Station struct {
StationID string
SpaceID int64
Name string // human-typed; never agent-supplied
Purpose string
SelfDescribedAbout string // a CLAIM (S8) — the field name carries that
SelfDescribedTags []string // ditto
Published bool
State string // active | archived
CreatedAt string
AdvertisedAt string
LastActivityAt string
}
Station is a durable working identity.
type StationKey ¶ added in v1.4.2
type StationKey struct {
TokenID, StationID, Label, CreatedAt, LastUsedAt, RetiredAt, RevokedAt string
// The actor this key was minted under, and whether that same actor holds a live
// COMM token. Carried for DISPLAY, because the operator cannot otherwise see the
// one property that decides whether the key can bind an endpoint at all.
//
// A mismatch has no symptom until someone tries to bind: the key authenticates
// perfectly, every station tool works, and only redemption refuses — which is
// months later and in a different surface. Showing it next to the key turns an
// invisible misconfiguration into a visible one.
ActorKind, ActorName string
ActorHasComm bool
}
StationKey is a row for the console's key list.
type StationLink ¶ added in v1.5.0
type StationLink struct {
LinkID string
StationA string
StationB string
NameA string
NameB string
State string // active | dormant | revoked
ApprovedAt string
}
StationLink is an approved peer relationship. Materializing channels from one is slice 4; the console lists them now so approving a link is not a write into a void.
type StationLockerEntry ¶ added in v1.4.2
type StationLockerEntry struct {
Name string
SizeBytes int
SHA256 string
ContentType string
UpdatedAt string
Bytes []byte // only populated by GetStationLockerBlob
}
StationLockerEntry is one stored file. Bytes are omitted from listings.
type StationLockerLimits ¶ added in v1.4.2
type StationLockerLimits struct {
MaxBlobBytes int // 256 KiB
MaxTotalBytes int // 2 MiB per station
}
StationLockerLimits are §9's numbers.
func DefaultStationLockerLimits ¶ added in v1.4.2
func DefaultStationLockerLimits() StationLockerLimits
DefaultStationLockerLimits are §9's numbers.
type StationNote ¶ added in v1.4.2
type StationNote struct {
Key string
Title string
Tags []string
Body string
Rev int
Bytes int
UpdatedAt string
UpdatedByToken string
HearsayAtWrite bool
}
StationNote is one page. Provenance is ken.db facts only — never an endpoint id, which is guaranteed to dangle once the COMM sweep runs and does not exist with COMM off (S7).
type StationNoteLimits ¶ added in v1.4.2
type StationNoteLimits struct {
MaxPageBytes int // 64 KiB — larger than this is a document, not a note
MaxRevisionBytes int // 256 KiB per page of history: an undo buffer, not an archive
MaxNotebookBytes int // 4 MiB of HEAD revisions; history is bounded separately
}
StationNoteLimits are §9's numbers. Each is a BACKUP decision: every byte lands in the live database plus fourteen nightlies plus Litestream, so a cap is really cap × ~15.
func DefaultStationNoteLimits ¶ added in v1.4.2
func DefaultStationNoteLimits() StationNoteLimits
DefaultStationNoteLimits are §9's numbers.
type StationPrincipal ¶ added in v1.4.2
type StationPrincipal struct {
TokenID string
ActorID int64
StationID string // empty = a station-less key: station_request and nothing else
Scopes []string
}
StationPrincipal is what a verified station key resolves to.
type StationRequestRow ¶ added in v1.4.2
type StationRequestRow struct {
RequestID, Kind, NameHint, Purpose, Reason, CreatedAt string
PromptedByPeerTraffic bool
}
StationRequestRow is a pending ask awaiting a human decision.
type StationTask ¶ added in v1.4.2
type StationTask struct {
TaskID string
StationID string
StationName string // filled by the cross-station view
Text string
Detail string
Context string
BlockedOn string // self | human | peer
BlockedOnStation string
RemindAfter string
State string // open | done | dropped
Resolution string
ResolutionLink string
CreatedAt string
HearsayAtWrite bool
LastBriefedAt string
BriefedCount int
DeferredUntil string
DeferCount int
LastDeferReason string
ClosedAt string
}
StationTask is one row. `blocked_on` is the field that earns its place: it turns the end-of-session guess ("two things waiting on you") into a query.
type StationTaskLimits ¶ added in v1.4.2
type StationTaskLimits struct {
MaxOpen int // refuse a new task past this (§9: 500)
MaxTextBytes int // one line, by construction (§9: 512)
MaxDetailBytes int // detail + context (§9: 4 KiB)
ListLimit int // default AND hard ceiling (§11.5: 50)
// BriefStampThrottleSec approximates "at most once per staffing session" without a
// session table: a task the briefing displayed within this window is shown again but
// NOT re-stamped, so `station_me` called repeatedly cannot advance the aging clock.
BriefStampThrottleSec int
}
StationTaskLimits bounds the list. Values are settings in the shipped product; the defaults here match §9, whose reason is a BACKUP argument: every byte lands in the live database plus fourteen nightlies plus Litestream.
func DefaultStationTaskLimits ¶ added in v1.4.2
func DefaultStationTaskLimits() StationTaskLimits
DefaultStationTaskLimits are §9's numbers.
type StationUsage ¶ added in v1.5.0
type StationUsage struct {
StationID string
Notes int
NoteBytes int64
OpenTasks int
TotalTasks int
LockerFiles int
LockerBytes int64
Keys int // live keys: neither retired nor revoked
}
StationUsage is what the console shows against the configured caps.
type Store ¶
type Store struct {
W *sql.DB // single-writer pool (MaxOpenConns == 1) — serializes all writes
R *sql.DB // reader pool
// contains filtered or unexported fields
}
Store holds the writer and reader connection pools over one SQLite file.
func (*Store) ActorExists ¶ added in v1.7.0
ActorExists reports whether an actor id names a live row.
Used where an id arrives from a FORM rather than from a lookup: a connector's authoring actor is chosen by an operator at consent time, and authorship is the field a human reads when deciding whether to promote a proposal. An unvalidated id there would attribute one connector's writes to another identity, which is the one kind of wrong the curation gate cannot repair afterwards.
func (*Store) ActorsWithCommStatus ¶ added in v1.7.0
func (s *Store) ActorsWithCommStatus(ctx context.Context) ([]ActorCandidate, error)
ActorsWithCommStatus lists actors that could sensibly author an agent's writes, comm-token holders first.
Two callers, one question. A STATION KEY must be minted under the actor holding that machine's comm token, or the hearsay marker silently never fires. A CONNECTOR must be pointed at one for exactly the same reason — its writes are authored by the grant's actor, and an actor invented from a client's display name can never match COMM traffic. Named for the property it reports rather than for the first caller.
This exists because the previous default was actively wrong in a way nothing surfaced. A station key was minted under a HUMAN actor — the CLI hardcoded the kind, the console used the logged-in curator's — while COMM tokens default to an `ai` actor, and `(kind, display_name)` is unique, so the two were different rows with different ids. The hearsay window joins on the actor, so it could never match: `hearsay_at_write` was permanently false on any deployment that followed the documented setup, and the only remedy the shipped commands offered was to deliberately mislabel an AI session's token as human — repairing one provenance signal by corrupting the one the whole curation model rests on.
The marker is biased toward over-reporting precisely because a false negative silently launders hearsay into the knowledge base. A mismatched actor produced exactly that false negative, on every station write, with no symptom.
func (*Store) AddStationTask ¶ added in v1.4.2
func (s *Store) AddStationTask(ctx context.Context, lim StationTaskLimits, t StationTask, tokenID string, actorID int64, hearsay bool) (*StationTask, []StationTask, error)
AddStationTask records a commitment. `blockedOn` is REQUIRED: it is a three-value enum costing one token, and making it optional would put an unstated default into the human's only cross-station view (§11.3).
It returns the new task plus NEAR-MATCHES from the open set, in the same result and at zero extra call cost — a model that just re-created something is told immediately rather than discovering it three weeks later (§11.5).
func (*Store) ApproveLinkRequest ¶ added in v1.5.0
func (s *Store) ApproveLinkRequest(ctx context.Context, requestID string, actorID int64) (*StationLink, error)
ApproveLinkRequest turns a pending link request into an active link. Either side may then materialize a channel without a fresh pairing code.
func (*Store) ApproveStationRequest ¶ added in v1.5.0
func (s *Store) ApproveStationRequest(ctx context.Context, requestID, name string, actorID int64) (*Station, error)
ApproveStationRequest turns a pending `station` request into a real station, with the name the HUMAN typed. The name_hint the agent supplied is not consulted here at all — the console may show it, but this function takes only what the operator entered (S3).
Atomic: the station is created and the request resolved in one transaction, so the queue can never show a pending request whose station already exists.
func (*Store) ArchiveStation ¶ added in v1.4.2
ArchiveStation is reversible (S3/§10): assets are kept, links go dormant rather than revoked so unarchiving restores them, and the NAME is held — releasing it is a separate act that makes the archive irreversible, because a released name can be taken by a new station.
func (*Store) AreStationsLinked ¶ added in v1.5.0
AreStationsLinked reports whether an ACTIVE link joins two stations. This is the authorization check for materializing a channel without a pairing code: a dormant link (either station archived) does not authorize anything until it is restored.
func (*Store) AuthenticateStationKey ¶ added in v1.4.2
func (s *Store) AuthenticateStationKey(ctx context.Context, presented string) (*StationPrincipal, error)
AuthenticateStationKey verifies a `kens_<id>_<secret>` credential.
Retired and revoked keys are both refused here, and indistinguishably from an unknown one — extending COMM's unprobeability house rule (§5). The one place a caller learns WHY it was cut off is after its endpoint secret has already verified (S6), which informs a proven holder and tells a prober nothing.
func (*Store) BriefStationTasks ¶ added in v1.4.2
func (s *Store) BriefStationTasks(ctx context.Context, lim StationTaskLimits, stationID string) (*TaskBriefing, error)
BriefStationTasks builds the briefing AND performs the only stamping in the system.
The head has FIXED SLOTS — up to 2 due, 2 human-blocked, 3 aging — because classes 1 and 2 are monotonic (a passed date never un-passes; the human-blocked pile is by definition the one not being cleared), so a pure rank order lets them hold the head forever and the aging clause never runs. Silence, the cheapest human response, must not be able to pin an item at rank 1 and freeze everything beneath it (§11.5).
Only the rows actually returned are stamped, and only if they were not stamped inside the throttle window — so `station_me` called repeatedly cannot advance the clock (§11.4).
func (*Store) CloseStationTasks ¶ added in v1.4.2
func (s *Store) CloseStationTasks(ctx context.Context, stationID string, taskIDs []string, resolution, link string, actorID int64) (int, error)
CloseStationTasks is the cheapest verb, and takes several ids because closing a batch after a release is the common case — five calls is five chances not to bother (§11.6).
func (*Store) CountActiveOAuthGrants ¶
CountActiveOAuthGrants counts live connector grants (for the dashboard stat).
func (*Store) CountActiveTokens ¶
CountActiveTokens returns the number of non-revoked agent API tokens.
func (*Store) CountEntries ¶
CountEntries returns the number of knowledge-base entries (curated or draft).
func (*Store) CountHumanUsers ¶
CountHumanUsers returns the number of human (login) actors; 0 drives the first-run setup wizard.
func (*Store) CountProposals ¶ added in v1.2.0
CountProposals returns how many entries have at least one proposed version — the same population ListProposals returns, counted cheaply. It backs the Proposals page's live auto-refresh: a curator who keeps that page open should see a new proposal appear without a manual reload, and polling a bare count is far lighter than re-running the full listing query on a timer.
func (*Store) CountVersions ¶
CountVersions returns the number of entry versions (the append-only history, which only ever grows).
func (*Store) CreateFirstAdmin ¶
CreateFirstAdmin atomically creates the initial human admin ONLY if no human user exists yet (the first-run wizard). Returns created=false (no error) if one already exists — a single INSERT...WHERE NOT EXISTS that closes the check-then- insert TOCTOU a separate SELECT+INSERT would leave open (two concurrent /setup posts can't both create an admin).
func (*Store) CreateHumanUser ¶
CreateHumanUser creates a human actor with an Argon2id password hash.
func (*Store) CreateOAuthGrantAndCode ¶
func (s *Store) CreateOAuthGrantAndCode(ctx context.Context, in NewAuthCode, codeTTL time.Duration) (string, error)
CreateOAuthGrantAndCode records the human's approval as a durable grant and a single-use authorization code (hashed), returning the plaintext code once.
func (*Store) CreateSession ¶
func (s *Store) CreateSession(ctx context.Context, actorID int64, ttl time.Duration) (*Session, error)
CreateSession creates a session for actorID with the given TTL and a fresh CSRF token (rotated per login).
func (*Store) CreateStation ¶ added in v1.4.2
func (s *Store) CreateStation(ctx context.Context, spaceID int64, name, purpose string, actorID int64) (*Station, error)
CreateStation creates a station with a HUMAN-supplied name. There is deliberately no agent-reachable path to this function: an agent files a station request and a human approves it, typing the name at that moment (S3).
func (*Store) CreateStationLinkRequest ¶ added in v1.5.0
func (s *Store) CreateStationLinkRequest(ctx context.Context, spaceID int64, tokenID, fromStation, toStation, reason string, hearsay bool) (string, error)
CreateStationLinkRequest files a request for a peer relationship.
Three properties, and each is load-bearing:
- THE REASON IS NEVER DELIVERED TO THE TARGET before approval. It is stored for the human and shown only in the console. Without that rule, every request is a one-shot unauthorized message channel: A cannot talk to B, but A could put a paragraph in front of B merely by asking to.
- A MUTED PAIR IS SILENTLY DROPPED, and the caller receives the ordinary "submitted, pending review" answer. Telling the caller it was muted would let a persistent session PROBE the human's past decisions, one request at a time. The mute is on the UNORDERED pair, because muting an ordered one would let the same relationship be re-asked from the other side.
- hearsay MARKS THE TRANSITIVE PATH. A cannot create a channel, but A can talk B into requesting one to C, and B's request then reaches the human looking like B's own idea. Recording whether the requester was mid-conversation is the only signal the human gets that the idea may not be B's.
Returns the request id, or ("", nil) when it was silently dropped — the caller must report success either way.
func (*Store) CreateStationRequest ¶ added in v1.4.2
func (s *Store) CreateStationRequest(ctx context.Context, spaceID int64, tokenID, fromStation, nameHint, purpose string) (string, error)
CreateStationRequest files an agent's ask for a human decision (S3/S9). The reason and purpose are shown ONLY to the human: nothing here is delivered to a target station before approval, because a request that reached its target would be a one-shot unauthorized message channel.
func (*Store) CrossStationHumanTasks ¶ added in v1.4.2
func (s *Store) CrossStationHumanTasks(ctx context.Context, spaceID int64, blockedOn string, limit int) ([]StationTask, error)
CrossStationHumanTasks answers the HUMAN's question — "what is everyone waiting on me for?" — which per-station lists do not (§11.8). Ordered by the same §11.5 contract applied across stations, never by recent station activity: ordering the whole-pile view by recency would sink the old items on the one surface built to stop that.
func (*Store) DeferStationTask ¶ added in v1.4.2
func (s *Store) DeferStationTask(ctx context.Context, stationID, taskID, until, reason string) error
DeferStationTask is deliberately the wordiest verb: a date AND a reason, and it leaves a counted trace. Deferring is legitimate; deferring silently and repeatedly is the failure mode (§11.6).
func (*Store) DeleteExpiredSessions ¶
DeleteExpiredSessions purges sessions past their expiry; returns the count.
func (*Store) DeleteSession ¶
DeleteSession removes a session (logout). Takes the RAW cookie value.
func (*Store) DeleteStationLockerBlob ¶ added in v1.4.2
DeleteStationLockerBlob removes one file.
func (*Store) DenyStationRequest ¶ added in v1.5.0
func (s *Store) DenyStationRequest(ctx context.Context, requestID, reason string, actorID int64) error
DenyStationRequest records a refusal. A reason is REQUIRED for the same purpose a task's resolution line is required: the next request from the same station arrives to a human who can see what was already said no to, instead of re-deciding blind.
For a link request this also feeds the denial ledger, whose mute window is what stops a persistent session from re-asking until a tired human says yes.
func (*Store) DistinctCategories ¶
DistinctCategories returns the non-empty categories present on non-archived entries, alphabetically — the option list for the browse category filter.
func (*Store) DropStationTasks ¶ added in v1.4.2
func (s *Store) DropStationTasks(ctx context.Context, stationID string, taskIDs []string, reason string, humanDecided bool, actorID int64) (int, error)
DropStationTasks abandons tasks. It REFUSES a `blocked_on: human` task unless the caller carries the human's own decision — without that guard the nag would aim the model's one destructive verb squarely at the pile the feature exists to preserve (§11.6).
func (*Store) EmbeddingStats ¶
EmbeddingStats reports embedded vs total version counts.
func (*Store) ExchangeOAuthCode ¶
func (s *Store) ExchangeOAuthCode(ctx context.Context, code string, grantID int64, accessTTL, refreshTTL time.Duration) (access, refresh string, err error)
ExchangeOAuthCode atomically consumes the code (single-use) and issues a new access + refresh token pair under its grant, returning both plaintexts once. The DELETE row-count is the double-spend guard: if the code was already used (or expired) between Peek and here, RowsAffected is 0 and this returns ErrOAuthBadCode without issuing anything.
func (*Store) FindActor ¶ added in v1.5.2
FindActor resolves an existing actor by kind and name WITHOUT creating one. Distinct from FindOrCreateActor on purpose: minting a station key should never invent an actor, because a typo would then produce a key that authenticates perfectly and marks nothing.
func (*Store) FindOrCreateActor ¶
FindOrCreateActor returns the id of an actor with the given kind + display name, creating it if absent.
func (*Store) FirstHumanActor ¶ added in v1.4.2
FirstHumanActor returns the earliest human actor, so CLI commands have a sane default.
func (*Store) FlagStale ¶
func (s *Store) FlagStale(ctx context.Context, slug, reason string, actorID int64, actorKind string) (string, error)
FlagStale marks an entry stale (still authoritative, ranks lower) and records the concern. Raising a concern is safe/additive; asserting freshness is not.
func (*Store) Get ¶
func (s *Store) Get(ctx context.Context, slugs []string, detailed bool) (entries []model.Entry, missing []string, err error)
Get returns full entries for the given slugs (curated head, or the provisional version for an uncurated draft). Unknown slugs are returned in missing. Each found entry bumps use_count. detailed adds provenance.
func (*Store) GetEntry ¶
GetEntry returns one entry without bumping use_count (for the human web UI).
func (*Store) GetSettings ¶
GetSettings returns all operator-set setting overrides (key -> value). An empty map means "all defaults".
func (*Store) GetStationLockerBlob ¶ added in v1.4.2
func (s *Store) GetStationLockerBlob(ctx context.Context, stationID, name string) (*StationLockerEntry, error)
GetStationLockerBlob returns one file's bytes.
func (*Store) HandoffStaleness ¶ added in v1.4.2
func (s *Store) HandoffStaleness(ctx context.Context, stationID string) (writtenAt string, activitiesSince int, err error)
HandoffStaleness reports how stale the handoff page is, measured in STATION ACTIVITY rather than the wall clock (§4): an idle station is never stale, a busy one goes stale fast. Activity is counted from ken.db facts only — tasks touched and pages edited since the handoff was last written — never messages, which live in the expendable file and may be absent entirely.
func (*Store) HumanByName ¶
HumanByName returns the login credential for a human actor (web login).
func (*Store) ImportEntry ¶
ImportEntry inserts an imported entry directly as curated (lifecycle 'active', one 'curated' version rev 1, author_kind 'import') — imported memories are already curated knowledge, so they bypass the proposal queue. Idempotent: returns created=false without changes if the slug already exists.
func (*Store) IsStationKeyRevoked ¶ added in v1.5.0
IsStationKeyRevoked reports whether a station key has been revoked.
This exists because severing cannot be made reliable at the REVOKING end. Both revoke paths — the /tokens console and `ken token revoke` — go through RevokeToken, and the CLI runs in a SEPARATE PROCESS with no comm.db handle at all, so a revocation issued there can never reach into the message database to mark endpoints. Making the check happen at USE instead means every revocation path works, including ones added later that forget about stations: it fails closed by construction rather than by remembering.
The eager sweep (comm.SeverEndpointsBoundBy) still runs where a comm handle exists, because it also RELEASES CLAIMS — a severed reader is never coming back to ack, and leaving its claims to expire would hide those messages from the station's remaining readers for the rest of the lease.
func (*Store) IssueBindingVoucher ¶ added in v1.5.0
func (s *Store) IssueBindingVoucher(ctx context.Context, stationID, tokenID, forEndpoint string, actorID, spaceID int64) (string, error)
IssueBindingVoucher mints a single-use voucher for a station. Called from the station endpoint, where the caller has already proven possession of a station key.
tokenID is recorded so revoking that key can later sever every endpoint it bound (S6). Without it, revocation would stop future bindings but leave the leaked capability running — which S6 calls theatre, correctly.
forEndpoint is the ONE comm endpoint that may redeem this voucher, and it is what stops the voucher being a bearer capability (migration 0015). actorID and spaceID record the issuing identity and are checked too, but they are the SETUP guard, not the security property — see RedeemBindingVoucher, which explains why both remain.
forEndpoint is not validated here and cannot be: this is the durable store, the endpoint lives in the expendable one (S7), and a foreign key across that boundary is exactly what S7 forbids. A voucher naming an endpoint that does not exist is simply a voucher nobody can redeem, which fails closed.
func (*Store) IssueStationKey ¶ added in v1.4.2
func (s *Store) IssueStationKey(ctx context.Context, actorID int64, stationID, label string, scopes []string) (string, error)
IssueStationKey mints a `kens_`-prefixed key. stationID may be empty: such a key can call exactly one tool, station_request, which is how a session with no station asks for one (S3).
actorID must be the SAME actor as that machine's comm token, because the hearsay window is keyed on the actor — a different actor silently defeats prompted_by_peer_traffic, and a marker that fails open without saying so is worse than no marker (S5).
THIS FUNCTION STILL DOES NOT ENFORCE THAT. It records what it is told. An earlier version of this comment said "the caller enforces that", naming an enforcer that never existed — which cost a production operator real time, because a contract comment asserting a guarantee is worse than silence: it stops the reader looking.
What changed in 0014: a mismatch is no longer silent everywhere. BINDING now enforces it — RedeemBindingVoucher requires the redeeming endpoint's actor to be the one the voucher was issued to, so a key minted under the wrong actor cannot bind an endpoint and says so by name. That is a real check, and it is deliberately NOT this function's: refusing at mint time would block the legitimate case of a deployment that has no comm token yet, and stations run with COMM off by design.
So the hearsay consequence above remains unenforced and silent — a mismatched key authenticates perfectly and marks nothing — while the binding consequence is now loud. Do not read the new check as covering both. What the callers still do is make the right actor the DEFAULT: `ken station key` resolves the actor holding this deployment's comm token and says which one it picked, the console offers a picker that marks them, and the /stations key table now shows each key's actor and whether it holds a comm token.
func (*Store) IssueToken ¶
func (s *Store) IssueToken(ctx context.Context, actorID int64, scopes []string, label string) (string, error)
IssueToken creates an API token for actorID and returns the full token string (ken_<id>_<secret>) exactly once; only SHA-256(secret) is persisted.
func (*Store) ListEntries ¶
ListEntries returns a filtered, sorted, paginated page of entries plus a has-more flag (it over-fetches one row so an exact-limit final page is not reported as "more" — the same technique as SearchPage). Archived entries are always excluded; a blank Lifecycle filter still shows draft/active/deprecated.
func (*Store) ListHumanUsers ¶
ListHumanUsers lists human (login) actors.
func (*Store) ListOAuthGrants ¶
func (s *Store) ListOAuthGrants(ctx context.Context) ([]OAuthGrantRow, error)
ListOAuthGrants returns live (non-revoked) grants, newest first.
func (*Store) ListProposals ¶
func (s *Store) ListProposals(ctx context.Context) ([]ProposalRow, error)
ListProposals returns entries with at least one proposed version, newest first.
func (*Store) ListStationKeys ¶ added in v1.4.2
ListStationKeys lists a station's keys for the console, including retired and revoked ones — a key nobody uses should be visible before it is a problem (§8).
func (*Store) ListStationLinks ¶ added in v1.5.0
ListStationLinks returns the space's links with both station names resolved, so the console never has to show an opaque id to a human.
func (*Store) ListStationLocker ¶ added in v1.4.2
func (s *Store) ListStationLocker(ctx context.Context, stationID string) ([]StationLockerEntry, error)
ListStationLocker returns metadata only — names, sizes and digests. A caller that wants bytes asks for one file.
func (*Store) ListStationNotes ¶ added in v1.4.2
ListStationNotes returns page metadata and SIZES but never bodies — the AI pays to read, so the list exists to let it choose what is worth a second call.
func (*Store) ListStationTasks ¶ added in v1.4.2
func (s *Store) ListStationTasks(ctx context.Context, lim StationTaskLimits, stationID, state, blockedOn string, limit int) ([]StationTask, int, error)
ListStationTasks is a PURE QUERY and stamps nothing (§11.4). A model checking its own list three times must not silently demote items nobody was told about.
func (*Store) ListStations ¶ added in v1.4.2
ListStations returns every station in a space, newest activity first. Console-facing: what an AGENT may see is narrower (published stations plus its own links, §5).
func (*Store) ListStationsVisibleTo ¶ added in v1.6.0
func (s *Store) ListStationsVisibleTo(ctx context.Context, spaceID int64, fromStation string) ([]DirectoryEntry, error)
ListStationsVisibleTo returns the stations `fromStation` may know about, newest activity first.
VISIBILITY RULE, and this is what finally gives `published` a reader: a station is listed when it is PUBLISHED, or when the asking station already holds an active link to it. Until now `published` was a human-settable flag that gated nothing — writable from the console, read by no query — which is the same unfinished shape as a column nothing selects. It now means exactly one thing: listed in the directory.
The link clause is not redundant with publication. A station may be deliberately unpublished and still be someone's established peer; hiding an existing relationship from the party that holds it would be a lie by omission, and would make the directory disagree with what comm_open_channel will actually do.
Archived stations are excluded: the directory answers "who is available", and a station nobody is staffing by design is not. Self is excluded for the same reason — a session does not need to discover itself.
The caller supplies liveness separately (comm.StaffingByStation); this package must not reach into the expendable database (S7).
func (*Store) ListTokens ¶
ListTokens lists all API tokens, newest first.
func (*Store) Migrate ¶
Migrate applies embedded migrations in lexical order, skipping versions already recorded in schema_migration. It is idempotent. All migrations are plain SQL (no loadable extensions), so all apply unconditionally; the embeddings table is created empty and only populated when a provider is configured.
func (*Store) OAuthClientByID ¶
OAuthClientByID returns the registered client or ErrOAuthNoClient.
func (*Store) PeekOAuthCode ¶
PeekOAuthCode reads (without consuming) a non-expired authorization code so the token endpoint can verify client_id, redirect_uri, and PKCE before committing. ErrOAuthBadCode if missing or expired. Consumption happens in ExchangeOAuthCode.
func (*Store) PendingStationRequests ¶ added in v1.4.2
func (s *Store) PendingStationRequests(ctx context.Context, spaceID int64) ([]StationRequestRow, error)
PendingStationRequests lists what is waiting on the human.
func (*Store) Promote ¶
func (s *Store) Promote(ctx context.Context, in PromoteInput) error
Promote is the only operation that moves the curated head. In one IMMEDIATE transaction, guarded by the proposal's state='proposed' check (see below), it supersedes the old head, marks the proposal curated, advances the head, resets staleness, and refreshes the denormalized ranking/browse surface. A duplicate or stale promote returns ErrBadVersion from that state check — reconcile, don't clobber. (lock_version is still bumped for auditing but is no longer a guard.)
func (*Store) PromoteStationNote ¶ added in v1.4.2
PromoteStationNote opens a PENDING PROMOTION for the human to convert. It writes no curated row, calls no kb_* tool, and requires no knowledge-base scope (S10).
func (*Store) ProposalReview ¶
ProposalReview loads a version and its entry's curated head for a diff view.
func (*Store) ProposeEnhancement ¶
func (s *Store) ProposeEnhancement(ctx context.Context, in ProposeInput) (ProposeResult, error)
ProposeEnhancement appends an immutable 'proposed' version. It never moves the curated head — knowledge is persisted the instant it is proposed.
func (*Store) ProvisionalReview ¶
ProvisionalReview returns the review material for an entry's pending proposal (its provisional version), or (nil, nil) when the entry has none.
func (*Store) PurgeExpiredOAuth ¶
PurgeExpiredOAuth deletes spent authorization codes and long-expired tokens. Best-effort housekeeping; safe to call periodically.
func (*Store) PutStationLockerBlob ¶ added in v1.4.2
func (s *Store) PutStationLockerBlob(ctx context.Context, lim StationLockerLimits, stationID, name string, body []byte, contentType, tokenID string, actorID int64) (*StationLockerEntry, error)
PutStationLockerBlob stores or replaces a file.
func (*Store) ReadStationNote ¶ added in v1.4.2
ReadStationNote fetches one page, body included.
func (*Store) RecentContext ¶
func (s *Store) RecentContext(ctx context.Context, sinceDays, limit int, kind string) ([]RecentEntry, error)
RecentContext returns entries with curation activity in the last sinceDays, newest first — a compact "what the KB learned recently" briefing.
func (*Store) RecordOutcome ¶
func (s *Store) RecordOutcome(ctx context.Context, slug, outcome string, actorID int64, actorKind, sessionID, note string) (staleness string, err error)
RecordOutcome records an agent's outcome report for an entry. 'was-wrong' also flags the entry stale (for human review). Returns the entry's staleness after.
func (*Store) RedeemBindingVoucher ¶ added in v1.5.0
func (s *Store) RedeemBindingVoucher(ctx context.Context, voucher, endpointID string, byActor int64) (stationID, tokenID string, err error)
RedeemBindingVoucher consumes a voucher and reports which station it binds to. Called from comm_register on the OTHER endpoint, which is why it takes no station argument: the voucher is the only thing that decides, so a caller cannot ask to be bound to a station it was not given a voucher for.
endpointID is the comm.db endpoint being bound. It is stored for the operator trail only and is never dereferenced — it points into the expendable database and is expected to dangle once the COMM sweep runs (S7).
Redemption is a conditional UPDATE rather than a read-then-write, so two concurrent registrations racing on one voucher cannot both succeed: exactly one UPDATE reports a row.
TWO checks, and they are not redundant. Do not remove either believing the other covers it — they answer different questions and only one of them is security.
endpointID must be the endpoint the voucher NAMED. This is the security property. Redeeming therefore requires that endpoint's own secret, which the voucher does not carry, so a leaked voucher is inert in anyone else's hands.
byActor must be the actor the voucher was issued to. This is the SETUP guard. It catches a station key minted under a different actor than the machine's comm token — a misconfiguration that otherwise has no symptom at all until it silently defeats the hearsay marker (see IssueStationKey). It is defence in depth for (1), never a substitute.
The history matters, because check (2) shipped alone and was described as closing the hole. It did not. As first written, redemption checked the hash, the single-use flag, the expiry and the station's state, and nothing about the holder — anything possessing the string could bind its own endpoint to the station's inbox, and the only control was a human remembering "never send a voucher over COMM". Adding the actor check narrowed that to "same actor", and the accompanying claim — that a leaked voucher then grants nothing the comm token does not already grant — was FALSE. A comm token alone registers an UNBOUND endpoint; it confers no station's mail. Binding is precisely the capability it does not give.
ken-prod-ops found the consequence by measuring rather than reading: six of their eight stations share one actor, because the actor is per MACHINE. So the check narrowed the credential to six sessions on one workstation, and the voucher held a WEAKER binding than the per-station key that minted it. Check (1) is the fix, and it is the reason binding no longer happens inside comm_register: registration has no endpoint id yet, so a voucher passed there could never name one.
func (*Store) RegisterOAuthClient ¶
func (s *Store) RegisterOAuthClient(ctx context.Context, name string, redirectURIs []string) (string, error)
RegisterOAuthClient mints a new public client_id for the given name + redirect-URI allowlist and returns it. The caller validates the redirect URIs (https or loopback) before calling.
func (*Store) Reject ¶
func (s *Store) Reject(ctx context.Context, slug string, versionID, actorID int64, actorKind, note string) error
Reject marks a proposed version rejected (retained + searchable as a dead-end).
func (*Store) RenameStation ¶ added in v1.4.2
RenameStation / SetStationPublished / ArchiveStation are HUMAN-only operations, reachable from the console and the CLI and from no tool.
func (*Store) ReopenStationTasks ¶ added in v1.4.2
func (s *Store) ReopenStationTasks(ctx context.Context, stationID string, taskIDs []string, reason string) (int, error)
ReopenStationTasks exists because a decision to drop is sometimes wrong, and because a terminal state nobody can leave makes the record a dead end (§11.3).
func (*Store) Repromote ¶
func (s *Store) Repromote(ctx context.Context, in PromoteInput) error
Repromote sets an EXISTING historical version (superseded/rejected/withdrawn — anything but the current head or a still-'proposed' version) back as the curated head. It is the human recovery path when promotions were applied in the wrong order and regressed the head; unlike Promote it does not require state='proposed' (proposed versions go through the normal Promote review). One IMMEDIATE tx.
func (*Store) RetireStationKey ¶ added in v1.4.2
RetireStationKey stops the key binding NEW endpoints and leaves live ones alone — the graceful "I moved machines" path. Revocation is the other verb and it SEVERS; see RevokeToken plus the endpoint-severing pass in the comm layer (S6).
func (*Store) RevokeOAuthGrant ¶
RevokeOAuthGrant revokes a grant and all of its outstanding tokens in one tx. Idempotent: revoking an already-revoked grant is a no-op success.
func (*Store) RevokeStationLink ¶ added in v1.5.0
RevokeStationLink ends a relationship. S9's trade-off is that approval widens from per-conversation to per-relationship, and this is the other half of that bargain: revocation is one click. Killing the live channel is the CALLER's job, because the channel lives in the expendable database this package must not reach into.
func (*Store) RevokeToken ¶
RevokeToken soft-revokes a token by id.
func (*Store) RotateOAuthRefresh ¶
func (s *Store) RotateOAuthRefresh(ctx context.Context, refresh string, accessTTL, refreshTTL time.Duration) (access, newRefresh string, rr *RefreshResult, err error)
RotateOAuthRefresh validates a refresh token and, on success, revokes it and issues a fresh access + refresh pair (rotation). If a refresh token that has ALREADY been rotated (revoked) is presented, that is a theft signal: the whole grant is revoked and ErrOAuthReuseKill is returned. ErrOAuthBadToken for a missing/expired token or a revoked grant.
func (*Store) Search ¶
func (s *Store) Search(ctx context.Context, query string, opt SearchOpts) ([]model.SearchResult, error)
Search runs the hybrid keyword+vector search and returns the clamped page.
func (*Store) SearchPage ¶
func (s *Store) SearchPage(ctx context.Context, query string, opt SearchOpts) ([]model.SearchResult, bool, error)
SearchPage is Search plus an accurate has-more flag. It over-fetches one row (K+1) so an exact-K final page isn't reported as "more" (a false positive).
func (*Store) SeedDemo ¶
SeedDemo inserts (idempotently) one curated demo entry so the skeleton has something to return from kb_search / kb_get. Dev/smoke use only.
func (*Store) SessionByID ¶
SessionByID returns a live (unexpired) session for the RAW cookie value, or ErrNotFound. The lookup is by hash (see sessionKey); the returned Session carries the raw id the caller passed in, so callers are unaffected by the storage change.
func (*Store) SetDetector ¶
SetDetector overrides the content-language detector (used by tests to force a deterministic language without exercising whatlanggo).
func (*Store) SetSettings ¶
func (s *Store) SetSettings(ctx context.Context, upsert map[string]string, remove []string, updater string) error
SetSettings applies setting changes in one transaction: it upserts each key/value in upsert (an empty value is a legitimate override, stored verbatim) and deletes each key in remove (reverting it to the default). Deletion is an explicit list, never inferred from an empty value.
func (*Store) SetStationPublished ¶ added in v1.4.2
func (*Store) SetStationSelfDescription ¶ added in v1.4.2
func (s *Store) SetStationSelfDescription(ctx context.Context, stationID, about string, tags []string) error
SetStationSelfDescription is the ONE station field an agent may write. It is stored in columns whose names say the value is a claim, so a reader that flattens the result still sees it marked (S8).
func (*Store) Snapshot ¶
Snapshot writes a consistent copy of the database to dest via VACUUM INTO — safe on a live WAL database (no torn file). dest must not already exist.
The result is chmod'd 0600 HERE, not left to the caller. A snapshot is a byte-complete copy of the knowledge base — every entry, the full curation history, curator accounts and token records — so its mode is part of writing it correctly, not a courtesy the caller may forget. SQLite creates the file at the process umask (0644 under the usual 0022), and callers that only *document* a safe umask leave every hand-run `ken backup snapshot --out …` world-readable: the shell wrappers cannot protect an operator following the runbook by hand.
The umask is also narrowed for the duration of the write (see the CLI), so the file is 0600 from creation rather than only once VACUUM INTO returns; this chmod is the backstop that holds no matter which caller invoked us.
func (*Store) StationAssetUsage ¶ added in v1.5.0
StationAssetUsage counts what a station is holding. Reported per station rather than per space because the caps are per station, and a total across stations would hide the one that is actually full.
func (*Store) StationByID ¶ added in v1.4.2
StationByID resolves the opaque routing id — the only identifier anything outside this package should hold.
func (*Store) StationByName ¶ added in v1.4.2
StationByName resolves a display name within a space. For CONSOLE and CLI use only: a name is not an address, and no agent-facing path may route by it (S3).
func (*Store) StationKeyOwner ¶ added in v1.5.0
StationKeyOwner reports the station a key is bound to, or "" for a station-less key. Used when severing: the console needs to know what a revocation will hit before it happens, and S6 requires stating the count before the click.
func (*Store) StationLinkByID ¶ added in v1.6.0
StationLinkByID resolves one link with both station names, so the caller can name the pair in a confirmation and hand the two station ids to COMM.
Separate from RevokeStationLink on purpose: the revoke is a write and this is the read that must happen BEFORE it — once the row says 'revoked' the console still needs to say whose relationship just ended.
func (*Store) StationTaskByID ¶ added in v1.4.2
StationTaskByID fetches one task.
func (*Store) SweepBindingVouchers ¶ added in v1.5.0
SweepBindingVouchers drops expired unredeemed vouchers. Redeemed ones are KEPT: they are the trail answering "which key bound this endpoint", which is the first question asked when a station key turns out to have leaked.
func (*Store) TouchStationActivity ¶ added in v1.4.2
TouchStationActivity stamps last_activity_at from ken.db facts only — a task touched or a page edited. Deliberately NOT messages: those live in the expendable file, and the console's cross-station ordering must not depend on a database that may be absent (S7, §11.8).
func (*Store) TouchToken ¶
TouchToken updates last_used_at, throttled to at most ~once per minute per token, so the read path doesn't amplify into a write on every request.
func (*Store) TransferStationAssets ¶ added in v1.5.0
func (s *Store) TransferStationAssets(ctx context.Context, fromID, toID string, notes, tasks, locker bool) (*TransferResult, error)
TransferStationAssets moves assets from one station to another — the answer to "a session is gone and its work should not be", and to "this machine is being replaced".
Three properties, each load-bearing:
- ATOMIC across every class in one transaction. A half-moved station is worse than an unmoved one: the human would have to reconstruct which classes went.
- REFUSED ENTIRELY on any name collision, with the colliding names returned. Merging silently would let a `handoff` page overwrite a `handoff` page — and since every station is expected to keep one, that collision is the COMMON case, not the edge. The human renames or drops, then retries.
- The MESSAGE QUEUE NEVER MOVES. It lives in comm.db, it is expendable by design, and pointing it at a new station would drag an expendable pointer into a durable move (S7).
Tasks cannot collide — they are keyed by an opaque id, not a name — so they always move cleanly. Notes and locker blobs are keyed by human-chosen names and can.
func (*Store) UpsertEmbedding ¶
func (s *Store) UpsertEmbedding(ctx context.Context, versionID int64, modelID string, vec []float32) error
UpsertEmbedding stores (or replaces) a version's embedding for a given model. The (version_id, model_id) primary key lets multiple models coexist per version; OR REPLACE upserts the row for this exact (version, model) pair only.
func (*Store) ValidateOAuthAccessToken ¶
func (s *Store) ValidateOAuthAccessToken(ctx context.Context, token string) (*OAuthPrincipal, error)
ValidateOAuthAccessToken resolves an opaque access token to its principal, or ErrOAuthBadToken if it is unknown, expired, revoked, or its grant was revoked. Read-only (reader pool) so it never contends for the single writer.
func (*Store) VersionDiff ¶
VersionDiff diffs two revisions of an entry, field by field.
func (*Store) VersionsNeedingEmbedding ¶
func (s *Store) VersionsNeedingEmbedding(ctx context.Context, modelID string, limit int) ([]EmbedTarget, error)
VersionsNeedingEmbedding returns versions lacking an embedding for modelID (limit<=0 means all). Text is what the query vector is compared against.
func (*Store) WriteStationNote ¶ added in v1.4.2
func (s *Store) WriteStationNote(ctx context.Context, lim StationNoteLimits, stationID, key, title, body string, tags []string, mode string, ifRev int, tokenID string, actorID int64, hearsay bool) (*StationNote, error)
WriteStationNote appends to or replaces a page, creating a revision.
ifRev > 0 is an optimistic-concurrency precondition: the write is refused if the page moved underneath it, naming the current revision. Two sessions may staff one station, and without a precondition the second writer silently destroys the first's page.
type TaskBriefing ¶ added in v1.4.2
type TaskBriefing struct {
Head []StationTask // the fixed-slot head, already stamped
OpenTotal int
BlockedOnHuman int
Overdue int
AgingCount int // not briefed in the last N sessions
StuckCount int // briefed repeatedly, unchanged and never deferred
RepeatedlyDefer int
Remainder int
}
TaskBriefing is what a session is handed when it staffs a station: named rows, not only counts. A task the human never hears named is a task that decayed.
type TokenRow ¶
type TokenRow struct {
TokenID, ActorName, Kind, Scopes, Label, CreatedAt, LastUsedAt, RevokedAt string
}
TokenRow is a row for `ken token list`.
type TransferResult ¶ added in v1.5.0
TransferResult reports what an asset transfer actually moved.