Documentation
¶
Index ¶
- Constants
- Variables
- type AWSEmbedder
- type BootstrapSpec
- type DocSource
- type DocumentView
- type EmbeddingConfig
- type EmbeddingProvider
- type FakeEmbedder
- type GCPEmbedder
- type Grant
- type ImportResult
- type ImportWorker
- type MemoryService
- func (s *MemoryService) Bootstrap(ctx context.Context, token string, spec BootstrapSpec) (string, *models.APIKey, error)
- func (s *MemoryService) CanManageTenant(ctx context.Context, tenantID uuid.UUID) bool
- func (s *MemoryService) CreateAPIKey(ctx context.Context, tenantID uuid.UUID, label string, subjectID *string, ...) (string, *models.APIKey, error)
- func (s *MemoryService) CreateTenant(ctx context.Context, name, email string, tenantType ...string) (*models.Tenant, error)
- func (s *MemoryService) DeleteAPIKey(ctx context.Context, id uuid.UUID) error
- func (s *MemoryService) DeleteDocument(ctx context.Context, category string, subcategory *string, slug string, ...) error
- func (s *MemoryService) DeleteDocumentByID(ctx context.Context, id uuid.UUID, overrideID *uuid.UUID) error
- func (s *MemoryService) DeleteTenant(ctx context.Context, id uuid.UUID) error
- func (s *MemoryService) GenerateIndex(ctx context.Context, depth string, category *string, overrideID *uuid.UUID) ([]repository.IndexEntry, error)
- func (s *MemoryService) GetCleanupQueue(ctx context.Context, limit int, includeResolved bool, overrideID *uuid.UUID) ([]models.CleanupQueue, error)
- func (s *MemoryService) GetDocument(ctx context.Context, category string, subcategory *string, slug string, ...) (*DocumentView, error)
- func (s *MemoryService) GetDocumentByID(ctx context.Context, id uuid.UUID, forceRead bool, reason string, ...) (*DocumentView, error)
- func (s *MemoryService) GetRelated(ctx context.Context, documentID uuid.UUID, limit int, overrideID *uuid.UUID) ([]repository.RelatedResult, error)
- func (s *MemoryService) GrantDocumentAccess(ctx context.Context, docID uuid.UUID, email, relation string) error
- func (s *MemoryService) GrantTenantAccess(ctx context.Context, tenantID uuid.UUID, email, relation string) error
- func (s *MemoryService) GrantTenantUser(ctx context.Context, email string, tenantID uuid.UUID, role string) (*models.TenantUser, error)
- func (s *MemoryService) HasAnyAdmin(ctx context.Context) (bool, error)
- func (s *MemoryService) ImportDocuments(ctx context.Context, tenantID uuid.UUID, src DocSource) (ImportResult, error)
- func (s *MemoryService) IsAdmin(ctx context.Context) bool
- func (s *MemoryService) LintMemory(ctx context.Context, checks []string, thresholds *repository.LintThresholds, ...) ([]repository.LintFinding, error)
- func (s *MemoryService) ListAPIKeys(ctx context.Context, tenantID uuid.UUID) ([]models.APIKey, error)
- func (s *MemoryService) ListDocumentGrants(ctx context.Context, docID uuid.UUID) ([]Grant, error)
- func (s *MemoryService) ListDocuments(ctx context.Context, category, subcategory *string, overrideID *uuid.UUID, ...) ([]models.Document, error)
- func (s *MemoryService) ListTenantGrants(ctx context.Context, tenantID uuid.UUID) ([]Grant, error)
- func (s *MemoryService) ListTenantUsers(ctx context.Context, tenantID uuid.UUID) ([]models.TenantUser, error)
- func (s *MemoryService) ListTenants(ctx context.Context) ([]models.Tenant, error)
- func (s *MemoryService) ListTenantsByType(ctx context.Context, tenantType, q string) ([]TenantAccess, error)
- func (s *MemoryService) MarkCleanupDone(ctx context.Context, queueID uuid.UUID, resolution, note string, ...) error
- func (s *MemoryService) MarkVerified(ctx context.Context, sectionID uuid.UUID, overrideID *uuid.UUID) error
- func (s *MemoryService) MergeDocuments(ctx context.Context, winnerID, loserID uuid.UUID, sectionsToKeep []uuid.UUID, ...) (*MergeResult, error)
- func (s *MemoryService) ProvisionPersonalTenant(ctx context.Context, email, displayName, hostedDomain string, ...) (string, error)
- func (s *MemoryService) PurgeDeadKeys(ctx context.Context, ttl time.Duration) (int64, error)
- func (s *MemoryService) ResetBootstrap(ctx context.Context) error
- func (s *MemoryService) RevokeAPIKey(ctx context.Context, id uuid.UUID) error
- func (s *MemoryService) RevokeDocumentAccess(ctx context.Context, docID uuid.UUID, email, relation string) error
- func (s *MemoryService) RevokeTenantAccess(ctx context.Context, tenantID uuid.UUID, email, relation string) error
- func (s *MemoryService) RevokeTenantUser(ctx context.Context, email string) error
- func (s *MemoryService) RotateAPIKey(ctx context.Context, keyID uuid.UUID, grace time.Duration) (string, *models.APIKey, error)
- func (s *MemoryService) Search(ctx context.Context, query string, category, subcategory *string, limit int, ...) ([]repository.SearchResult, error)
- func (s *MemoryService) StoreDocument(ctx context.Context, category string, subcategory *string, ...) (*StoreResult, error)
- func (s *MemoryService) UpdateDocumentTitle(ctx context.Context, docID uuid.UUID, title string, overrideID *uuid.UUID) (*models.Document, error)
- func (s *MemoryService) UpdateMyTenantSettings(ctx context.Context, stalenessMode *string, duplicateGuard *bool, ...) (*models.Tenant, error)
- func (s *MemoryService) UpdateSection(ctx context.Context, sectionID uuid.UUID, content *string, heading *string, ...) (*models.Section, error)
- func (s *MemoryService) UpdateTenant(ctx context.Context, id uuid.UUID, fields UpdateTenantFields) (*models.Tenant, error)
- func (s *MemoryService) UpdateTenantSettings(ctx context.Context, tenantID uuid.UUID, stalenessMode *string, ...) (*models.Tenant, error)
- func (s *MemoryService) UpdateTenantUserRole(ctx context.Context, email, role string) (*models.TenantUser, error)
- func (s *MemoryService) WritableTenants(ctx context.Context) ([]TenantAccess, error)
- type MergeResult
- type OllamaEmbedder
- type OpenAIEmbedder
- type SectionView
- type StoreResult
- type TenantAccess
- type UpdateTenantFields
Constants ¶
const ( // MaxSearchLimit caps the number of search results a caller may request. MaxSearchLimit = 100 // MaxQueryLen caps the length of a search query string. MaxQueryLen = 10_000 // DefaultListLimit is the document-list page size GET /api/documents applies // when limit is absent or non-positive. DefaultListLimit = 50 // MaxListLimit caps the document-list page size a caller may request. MaxListLimit = 200 )
Search input bounds shared by every read surface (MCP search_memory and the HTTP GET /api/search handler) so the clamp/reject limits live in one place.
Variables ¶
var ( // ErrBootstrapForbidden is returned on the network path when the caller token is // empty, the generated BootstrapToken is empty, or the two do not match. The // instance fails closed and never provisions. Front-ends map this to HTTP 403. // (The local-admin CLI path bypasses the gate and never returns this.) ErrBootstrapForbidden = errors.New("bootstrap forbidden: missing or invalid token") // ErrAlreadyBootstrapped is returned when an admin already exists; bootstrap is // one-shot. Front-ends map this to HTTP 409. ErrAlreadyBootstrapped = errors.New("already bootstrapped: an admin already exists") )
ErrEmbeddingUnavailable is the tenant-safe error for a non-2xx upstream embedding response. Full status/body is logged server-side (audit #18), never propagated to callers (which would leak provider internals into MCP responses).
var ErrSignupNotAllowed = errors.New("signup not allowed: email domain is not permitted")
ErrSignupNotAllowed is returned by ProvisionPersonalTenant when the signup domain gate blocks an email. The auth adapter maps it to HTTP 403; no tenant is created. Distinct from apperr.ErrInvalidInput so front-ends can tell a gate rejection apart from a malformed request.
Functions ¶
This section is empty.
Types ¶
type AWSEmbedder ¶
type AWSEmbedder struct {
// contains filtered or unexported fields
}
AWSEmbedder generates embeddings via Amazon Bedrock Runtime InvokeModel.
func NewAWSEmbedder ¶
func NewAWSEmbedder(region, model string, dimensions int) (*AWSEmbedder, error)
NewAWSEmbedder resolves config + credentials from the standard AWS chain (env, shared config, or assumed role — never from config) and builds the embedder.
func (*AWSEmbedder) Dimensions ¶
func (e *AWSEmbedder) Dimensions() int
type BootstrapSpec ¶
type BootstrapSpec struct {
TenantName string // default "admin"
TenantEmail string // optional
KeyLabel string // default "admin"
// AdminEmail, when set AND the OAuth login path is configured, is mapped to the
// new tenant as admin (design D4) so the operator can log in via /ui without a
// race-to-claim. Ignored when empty or when OAuth is not configured.
AdminEmail string
}
BootstrapSpec describes the first tenant and admin API key to provision. Empty fields fall back to sensible defaults so HTTP/CLI front-ends can stay thin.
type DocSource ¶
DocSource is a push-style iterator over documents to ingest: it walks its own source — a filesystem directory, an in-memory unzipped archive, ... — and calls emit once per document found, as (relative path, raw content). A non-nil error returned by the DocSource itself aborts the whole import (e.g. the root path does not exist); per-document problems are handled inside the emit callback ImportDocuments passes in and never abort the walk. The CLI (Task 7.5) wraps filepath.Walk and the HTTP worker (Task 7.4) wraps an unzipped-archive walk; both drive the same ImportDocuments core through this one shape.
type DocumentView ¶
type DocumentView struct {
ID uuid.UUID `json:"id"`
TenantID uuid.UUID `json:"tenant_id"`
TenantName string `json:"tenant_name,omitempty"`
TenantType string `json:"tenant_type,omitempty"`
Category string `json:"category"`
Subcategory *string `json:"subcategory,omitempty"`
Slug string `json:"slug"`
Title string `json:"title"`
DocType string `json:"doc_type"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Sections []SectionView `json:"sections,omitempty"`
}
DocumentView is the API-facing projection of a document with filtered sections.
type EmbeddingConfig ¶
type EmbeddingConfig struct {
Dimensions int
// Ollama
OllamaURL string
OllamaModel string
// GCP
GCPProject string
GCPLocation string
GCPModel string
// OpenAI-compatible (/v1/embeddings)
OpenAIBaseURL string
OpenAIAPIKey string
OpenAIModel string
// AWS Bedrock
AWSRegion string
AWSModel string
}
EmbeddingConfig holds all provider-agnostic and provider-specific config.
type EmbeddingProvider ¶
type EmbeddingProvider interface {
Embed(ctx context.Context, text string) (pgvector.Vector, error)
Dimensions() int
}
EmbeddingProvider generates vector embeddings from text.
func NewEmbeddingProvider ¶
func NewEmbeddingProvider(provider string, cfg EmbeddingConfig) (EmbeddingProvider, error)
NewEmbeddingProvider creates an EmbeddingProvider based on the provider name.
type FakeEmbedder ¶
type FakeEmbedder struct {
// contains filtered or unexported fields
}
FakeEmbedder produces deterministic sha256-derived vectors: reproducible but semantically meaningless (tests needing similarity must seed by id, not ranking). Wired via EMBEDDING_PROVIDER=fake or injected by integration tests; never in production.
func NewFakeEmbedder ¶
func NewFakeEmbedder(dim int) *FakeEmbedder
func (*FakeEmbedder) Dimensions ¶
func (e *FakeEmbedder) Dimensions() int
type GCPEmbedder ¶
type GCPEmbedder struct {
// contains filtered or unexported fields
}
func NewGCPEmbedder ¶
func NewGCPEmbedder(project, location, model string, dimensions int) (*GCPEmbedder, error)
func (*GCPEmbedder) Dimensions ¶
func (e *GCPEmbedder) Dimensions() int
type Grant ¶
type Grant struct {
Email string `json:"email"`
SubjectID string `json:"subject_id"`
Relation string `json:"relation"`
}
Grant is one relation-tuple rendered for display: SubjectID resolved to Email via tenant_users (design.md §5).
type ImportResult ¶
ImportResult tallies a bulk import: Imported counts documents successfully parsed and stored, Skipped counts items whose path did not parse into a category/slug (not fatal), and Failed counts parseable items whose StoreDocument call errored (also not fatal — one bad file must not abort the batch, matching cmd/import's current behavior).
type ImportWorker ¶
type ImportWorker struct {
// contains filtered or unexported fields
}
ImportWorker drains the import_jobs queue in-process (design D7). It is started beside the cleanup scanner in cmd/server/main.go and bound to the root context; cancelling that context stops all of its goroutines.
func NewImportWorker ¶
func NewImportWorker(jobs importJobQueue, importDocs importFunc, concurrency int, interval time.Duration, logger *slog.Logger) *ImportWorker
NewImportWorker builds a worker. concurrency < 1 is treated as 1; interval <= 0 falls back to a sane default poll cadence.
func (*ImportWorker) Start ¶
func (w *ImportWorker) Start(ctx context.Context)
Start sweeps interrupted jobs (D9) then launches `concurrency` polling goroutines. Non-blocking, mirroring cleanup.Scanner.Start.
type MemoryService ¶
type MemoryService struct {
// BootstrapToken is the generated first-run token the HTTP Bootstrap path
// compares the caller's token against (constant-time). Set once at startup in
// cmd/server/main.go when the instance has no admin yet (design D1); empty means
// the HTTP path refuses to bootstrap. The offline CLI bypasses it via a
// local-admin context (design D2), so it need not be set for that path.
BootstrapToken string
// OAuthConfigured mirrors cfg.AuthletEnabled(): whether the authlet OAuth login
// path is wired. Set once at construction in cmd/server/main.go. Bootstrap uses
// it to decide admin-email seeding (design D4) — an operator email is only
// useful when logins can actually resolve via OAuth. The offline CLI, which
// skips config.Load, leaves it false.
OAuthConfigured bool
// TenantDefaults are the operator-chosen toggle defaults (staleness_mode,
// duplicate_guard, cleanup_scan_enabled) stamped onto every tenant created
// through the service. Set once at startup from config.TenantDefaults; a zero
// value (unset — offline CLI / tests) leaves creation to the model/DB default.
TenantDefaults models.TenantDefaults
// SelfServicePolicyDefault is the operator-chosen global default self-service
// policy ("open" | "admin_only"); a per-tenant override resolves against it.
// Set once at startup from config.SelfServicePolicy. Empty (offline CLI /
// tests) resolves to "open" — no lockout.
SelfServicePolicyDefault string
// contains filtered or unexported fields
}
func NewMemoryService ¶
func NewMemoryService( db *gorm.DB, docs *repository.DocumentRepository, sections *repository.SectionRepository, embedder EmbeddingProvider, tenants *repository.TenantRepository, keys *repository.APIKeyRepository, lint *repository.LintRepository, thresholds *staleness.ThresholdStore, overrides *repository.OverrideLogRepository, cleanup *repository.CleanupQueueRepository, authzStore authz.Store, ) *MemoryService
NewMemoryService constructs the service. Optional deps may be nil outside the MCP server (e.g. import CLI), disabling their features; a nil authzStore also disables tuple seeding and fails every authorization Check closed.
func (*MemoryService) Bootstrap ¶
func (s *MemoryService) Bootstrap(ctx context.Context, token string, spec BootstrapSpec) (string, *models.APIKey, error)
Bootstrap performs token-gated, one-shot first-run provisioning. On the network path it verifies the caller token against the generated BootstrapToken in constant time (failing closed when either is empty); a local-admin context (the offline CLI) bypasses the token gate. It then — inside ONE transaction guarded by a Postgres advisory lock so concurrent callers yield exactly one admin — confirms no admin exists yet and provisions the first tenant + admin API key, seeding the system:memory#admin tuple via authzseed. The plaintext key is returned exactly once and is deliberately never logged.
func (*MemoryService) CanManageTenant ¶
CanManageTenant reports whether the caller may administer tenantID's membership: system admin OR tenant#manager (which itself includes tenant#admin, since the manager relation's rewrite is this ∪ computed(admin)).
func (*MemoryService) CreateAPIKey ¶
func (s *MemoryService) CreateAPIKey(ctx context.Context, tenantID uuid.UUID, label string, subjectID *string, expiresAt *time.Time) (string, *models.APIKey, error)
CreateAPIKey mints a key for a tenant. subjectID pins the key to an authorization subject; nil/empty defaults to the tenant service principal ("svc:<tenant_id>"). The subject is granted tenant membership (idempotent).
func (*MemoryService) CreateTenant ¶
func (s *MemoryService) CreateTenant(ctx context.Context, name, email string, tenantType ...string) (*models.Tenant, error)
CreateTenant provisions a tenant. tenantType is optional (variadic so existing callers stay source-compatible): the first non-empty value classifies the tenant (models.TenantType*), defaulting to shared. The type is a DISPLAY-ONLY classifier — it is validated here and persisted but MUST NEVER be read by authz.
func (*MemoryService) DeleteAPIKey ¶
DeleteAPIKey permanently removes a key row (hard delete, no audit trace). It is admin-only and restricted to dead keys — already revoked or past expiry — since the UI surfaces it only on those rows, for cleanup. An active key must be revoked (or expire) first.
func (*MemoryService) DeleteDocument ¶
func (s *MemoryService) DeleteDocument(ctx context.Context, category string, subcategory *string, slug string, overrideID *uuid.UUID) error
DeleteDocument removes a document and all its sections in a transaction. Explicitly deletes sections first (FK-safe order), does not rely on CASCADE.
func (*MemoryService) DeleteDocumentByID ¶
func (s *MemoryService) DeleteDocumentByID(ctx context.Context, id uuid.UUID, overrideID *uuid.UUID) error
DeleteDocumentByID removes a document and all its sections, addressed by UUID and deleted from its OWNING tenant. It exists because DeleteDocument re-resolves a (category, subcategory, slug) path against the caller's home tenant, so an id that resolves to a foreign (common-pool or granted) doc would otherwise delete a same-path home-tenant doc instead. Here the doc is located across the caller's read scope, and a doc outside the caller's home tenant requires document#editor.
func (*MemoryService) DeleteTenant ¶
func (*MemoryService) GenerateIndex ¶
func (s *MemoryService) GenerateIndex(ctx context.Context, depth string, category *string, overrideID *uuid.UUID) ([]repository.IndexEntry, error)
GenerateIndex builds the browse catalog aggregated across the caller's readable tenant SET (home + common pool + directly-granted tenants), the same no-leak scope as search/list/get/get_related. The optional tenant_id filter narrows to one readable tenant; a non-readable filter yields an empty scope -> empty index (no existence leak).
func (*MemoryService) GetCleanupQueue ¶
func (s *MemoryService) GetCleanupQueue(ctx context.Context, limit int, includeResolved bool, overrideID *uuid.UUID) ([]models.CleanupQueue, error)
GetCleanupQueue returns unresolved cleanup queue entries for the tenant. When includeResolved is true, all rows are returned (most recent first).
func (*MemoryService) GetDocument ¶
func (s *MemoryService) GetDocument(ctx context.Context, category string, subcategory *string, slug string, forceRead bool, reason string, overrideID *uuid.UUID) (*DocumentView, error)
GetDocument fetches a document with all sections by path, applying the staleness filter. forceRead + reason override it and audit to override_log.
func (*MemoryService) GetDocumentByID ¶
func (s *MemoryService) GetDocumentByID(ctx context.Context, id uuid.UUID, forceRead bool, reason string, overrideID *uuid.UUID) (*DocumentView, error)
GetDocumentByID mirrors GetDocument (staleness filter + force_read audit) but addresses by UUID. Needed to reach a shadow doc that shares a path with another (e.g. cleanup_queue doc_a_id/doc_b_id), which path-keyed GetDocument can't disambiguate.
func (*MemoryService) GetRelated ¶
func (s *MemoryService) GetRelated(ctx context.Context, documentID uuid.UUID, limit int, overrideID *uuid.UUID) ([]repository.RelatedResult, error)
GetRelated returns documents semantically related to the target, aggregated across the caller's readable tenant SET (home + common pool + directly-granted tenants) exactly like search/list/get, and labels each result by its owning tenant. A related document is returned only when its owning tenant is in that set — the same no-leak guarantee as the other reads. The optional tenant_id filter narrows to one readable tenant; a non-readable filter yields an empty scope -> empty result (no existence leak), consistent with the other reads.
The viewer Check on the caller-supplied target (finding #9, IDOR) still blocks probing another tenant's docs; viewer-level is deliberate — denies private cross-tenant targets but allows relating over world-readable common-pool docs.
func (*MemoryService) GrantDocumentAccess ¶
func (s *MemoryService) GrantDocumentAccess(ctx context.Context, docID uuid.UUID, email, relation string) error
GrantDocumentAccess grants email per-document guest access (viewer or editor) to docID. The caller must manage the document's owning tenant (CanManageTenant(doc.TenantID)) — document guest sharing is bounded by tenant management, not by holding a grant on the document itself.
func (*MemoryService) GrantTenantAccess ¶
func (s *MemoryService) GrantTenantAccess(ctx context.Context, tenantID uuid.UUID, email, relation string) error
GrantTenantAccess grants email the given relation (viewer, member, or manager) on tenantID, enforcing the grant-ceiling matrix (design.md §6). email must already have a tenant_users row; the ACL surface does not auto-create one.
func (*MemoryService) GrantTenantUser ¶
func (s *MemoryService) GrantTenantUser(ctx context.Context, email string, tenantID uuid.UUID, role string) (*models.TenantUser, error)
GrantTenantUser maps a verified email to a tenant+role, creating the tenant_users row and seeding membership tuples (+ admin when role==admin, + owner when role==owner). Admin-gated; the lifecycle seam for user grants (no in-band tool writes tuples).
func (*MemoryService) HasAnyAdmin ¶
func (s *MemoryService) HasAnyAdmin(ctx context.Context) (bool, error)
HasAnyAdmin reports whether any subject holds system:memory#admin — the derived "is this instance bootstrapped?" signal (design D1: bootstrap state IS the admin tuple, not a separate state column). A nil authz store yields false.
func (*MemoryService) ImportDocuments ¶
func (s *MemoryService) ImportDocuments(ctx context.Context, tenantID uuid.UUID, src DocSource) (ImportResult, error)
ImportDocuments is the shared ingest core (design D8; spec: *Shared ingest core*). It establishes tenantID on the context, then drains src and for each (path, content) pair parses category/subcategory/slug and stores the document with the duplicate guard bypassed (force=true) — StoreDocument itself seeds the document's document#tenant authz tuple (lifecycle seeding), so no separate seed step is needed here. This is exactly the loop cmd/import/main.go currently inlines, lifted so both the CLI and the HTTP import worker can drive it via different DocSource implementations.
func (*MemoryService) IsAdmin ¶
func (s *MemoryService) IsAdmin(ctx context.Context) bool
IsAdmin exposes the admin gate for the admin HTTP middleware (which decides 403-vs-proceed before dispatch; service methods re-check, so not the sole enforcement point). Admin = system:memory#admin via tuple Check, not tenant.Email.
func (*MemoryService) LintMemory ¶
func (s *MemoryService) LintMemory(ctx context.Context, checks []string, thresholds *repository.LintThresholds, overrideID *uuid.UUID) ([]repository.LintFinding, error)
func (*MemoryService) ListAPIKeys ¶
func (*MemoryService) ListDocumentGrants ¶
ListDocumentGrants lists every direct per-document guest viewer/editor grant on docID (tenant-inherited access is out of scope — only explicit guest shares). Caller must CanManageTenant(doc.TenantID).
func (*MemoryService) ListDocuments ¶
func (s *MemoryService) ListDocuments(ctx context.Context, category, subcategory *string, overrideID *uuid.UUID, limit, offset int) ([]models.Document, error)
ListDocuments lists documents across the caller's readable tenant set, optionally filtered. A positive limit paginates; limit <= 0 returns the full list unpaginated (design D2), so the MCP list tool and CLI are unaffected. Each returned document already carries its owning TenantID; a nil overrideID aggregates, a set overrideID narrows to one readable tenant (empty result if not readable — never a leak).
func (*MemoryService) ListTenantGrants ¶
ListTenantGrants lists every direct viewer/member/manager grant on tenantID (tenant#admin is out of scope here — that's the GrantTenantUser admin flow). Caller must CanManageTenant(tenantID). A public wildcard (user:*) grant is included as an auditable entry; usersets and subjects with no resolvable email (stale tenant_users, service principals) are skipped rather than failing the whole list.
func (*MemoryService) ListTenantUsers ¶
func (s *MemoryService) ListTenantUsers(ctx context.Context, tenantID uuid.UUID) ([]models.TenantUser, error)
ListTenantUsers returns the email->tenant mappings for a tenant. Admin-gated.
func (*MemoryService) ListTenants ¶
func (*MemoryService) ListTenantsByType ¶
func (s *MemoryService) ListTenantsByType(ctx context.Context, tenantType, q string) ([]TenantAccess, error)
ListTenantsByType backs GET /api/tenants?type=<t>&q=<filter> (design D5). It reuses the WritableTenants authz shape: a system admin sees all tenants (labeled admin), a non-admin sees only the tenants they manage (ReadBySubject candidates confirmed with Check(tenant, id, manager)). The result is then filtered by type and the optional q. An empty type means "all types"; a non-empty type must be valid (personal|shared) or ErrInvalidInput is returned so the handler can map it to 400. Not admin-only — managers use it.
func (*MemoryService) MarkCleanupDone ¶
func (s *MemoryService) MarkCleanupDone(ctx context.Context, queueID uuid.UUID, resolution, note string, mergedInto *uuid.UUID, overrideID *uuid.UUID) error
MarkCleanupDone resolves a queue entry; mergedInto records the survivor when resolution is "merged". Editor Check on the referenced doc (finding #17): denies resolving a common-pool/cross-tenant entry without the write right.
func (*MemoryService) MarkVerified ¶
func (s *MemoryService) MarkVerified(ctx context.Context, sectionID uuid.UUID, overrideID *uuid.UUID) error
MarkVerified stamps verified_at=NOW() on a section (after an agent confirms a claim). Editor Check on the parent doc (finding #8): else any caller could verify a shared common-pool section it has no write right to.
func (*MemoryService) MergeDocuments ¶
func (s *MemoryService) MergeDocuments( ctx context.Context, winnerID, loserID uuid.UUID, sectionsToKeep []uuid.UUID, overrideID *uuid.UUID, ) (*MergeResult, error)
MergeDocuments moves the sections in sections_to_keep (in the caller's desired final order) into the winner, deletes the rest, and deletes the loser. A keep ID not belonging to winner or loser rejects the whole operation.
func (*MemoryService) ProvisionPersonalTenant ¶
func (s *MemoryService) ProvisionPersonalTenant(ctx context.Context, email, displayName, hostedDomain string, allowedDomains []string) (string, error)
ProvisionPersonalTenant auto-provisions a personal tenant for a verified email on first login (design D1-D3). It gate-checks the email domain against allowedDomains (honoring the Google `hd` hosted-domain claim when non-empty); on block it returns ErrSignupNotAllowed and creates nothing. On pass it runs, in ONE transaction, CreateTenant(type=personal, name=<base>) then GrantTenantUser(email, tenant, owner) — the same pair the bootstrap path uses, so the resolved subject (tenant_users.id) and tuples match a hand-provisioned owner (personal-owner-role). It returns the tenant id (UUID string).
The name base is displayName when non-empty (the OIDC `name` claim, the product's primary source), else the email local-part.
Returning user: if the email already maps to a tenant, that tenant id is returned immediately — no create, no disambiguation.
Same-email race: two concurrent first-logins of the SAME email collide on the globally-unique tenant_users.email; the loser re-resolves and returns the winning tenant — never a second tenant.
Distinct-email name collision: two different emails whose base name is equal (e.g. two "John Smith", or alice@a.com vs alice@b.com) must BOTH provision. tenants.name is globally unique, so on a Name unique-violation the name is disambiguated (domain-qualified, then numeric) and creation retried within a bounded loop, yielding distinct tenants.
Auto-provisioned personal tenants get NO public-read wildcard (that is default-pool-only); they are private to their owner.
func (*MemoryService) PurgeDeadKeys ¶
PurgeDeadKeys hard-deletes keys that have been dead — revoked or expired — for longer than ttl. It runs from the scheduled system sweep with no user context, so it performs no per-request authz. ttl <= 0 is a no-op. Returns the count removed.
func (*MemoryService) ResetBootstrap ¶
func (s *MemoryService) ResetBootstrap(ctx context.Context) error
ResetBootstrap is the operator-only break-glass reset (design D5, spec: *Break-glass reset*). In ONE transaction it deletes every system:memory#admin tuple and the API key(s) behind each one's subject — nothing else. Tenants, documents, sections, and any non-admin API key are left untouched. After it commits, HasAnyAdmin is false again and the instance re-arms for Bootstrap (the next server boot generates and logs a fresh bootstrap token).
This mirrors Bootstrap's seeding in reverse: Bootstrap mints an admin key and writes authzseed.SystemAdmin(authzseed.APIKeySubjectID(key)); this reads those tuples back, uses APIKeySubjectID's resolution (via the repo's FindBySubjectID) to find the key(s) that produced each subject, deletes the key(s), then deletes the tuple.
Security-critical: this function has no HTTP route and must never gain one (spec: *Reset cannot be triggered over the network*) — the only caller is the boot-time MEMORY_RESET check in cmd/server/main.go (task 6.1).
func (*MemoryService) RevokeAPIKey ¶
func (*MemoryService) RevokeDocumentAccess ¶
func (s *MemoryService) RevokeDocumentAccess(ctx context.Context, docID uuid.UUID, email, relation string) error
RevokeDocumentAccess removes email's per-document guest grant on docID, subject to the same tenant-management requirement as GrantDocumentAccess.
func (*MemoryService) RevokeTenantAccess ¶
func (s *MemoryService) RevokeTenantAccess(ctx context.Context, tenantID uuid.UUID, email, relation string) error
RevokeTenantAccess removes email's relation grant on tenantID, subject to the same grant-ceiling matrix as GrantTenantAccess.
func (*MemoryService) RevokeTenantUser ¶
func (s *MemoryService) RevokeTenantUser(ctx context.Context, email string) error
RevokeTenantUser removes a user's email->tenant mapping and its membership tuples (member, plus admin or owner when applicable). Admin-gated. Email is the key.
func (*MemoryService) RotateAPIKey ¶
func (s *MemoryService) RotateAPIKey(ctx context.Context, keyID uuid.UUID, grace time.Duration) (string, *models.APIKey, error)
RotateAPIKey issues a replacement for an existing key's tenant/label/subject and retires the predecessor: grace==0 revokes it now, grace>0 sets expiry to now+grace for a zero-downtime swap. Returns the new plaintext exactly once.
func (*MemoryService) Search ¶
func (s *MemoryService) Search(ctx context.Context, query string, category, subcategory *string, limit int, forceRead bool, reason string, overrideID *uuid.UUID) ([]repository.SearchResult, error)
Search performs hybrid semantic + keyword search, applying staleness filter. When forceRead is true, Reason is required and the override is audited.
func (*MemoryService) StoreDocument ¶
func (s *MemoryService) StoreDocument( ctx context.Context, category string, subcategory *string, slug, content string, force bool, reason string, overrideID *uuid.UUID, ) (*StoreResult, error)
StoreDocument parses markdown into sections, embeds them (before any DB write, to avoid partial state), runs the duplicate guard, and stores. When the guard trips and force is false, no write happens and the result carries candidates.
func (*MemoryService) UpdateDocumentTitle ¶
func (s *MemoryService) UpdateDocumentTitle(ctx context.Context, docID uuid.UUID, title string, overrideID *uuid.UUID) (*models.Document, error)
UpdateDocumentTitle sets a document's title. Blank titles are rejected (Title is NOT NULL). Refuses common-pool docs for non-admins.
func (*MemoryService) UpdateMyTenantSettings ¶
func (s *MemoryService) UpdateMyTenantSettings(ctx context.Context, stalenessMode *string, duplicateGuard *bool, cleanupScanEnabled *bool) (*models.Tenant, error)
UpdateMyTenantSettings edits the caller's OWN tenant's toggles (staleness, duplicate guard, cleanup scan); name/email stay admin-only. A field-less call is a status read and is always allowed. Writes require MANAGE rights (manager) via requireSelfService, NOT bare membership: these toggles arm destructive behavior — staleness_mode="hard" arms the retention sweep that archives then hard-deletes documents — so they are not member-level self-service. A personal tenant's owner still passes (owner ⇒ manager), keeping personal self-service intact; a shared tenant's plain member is refused. This matches the by-id sibling UpdateTenantSettings (also manager). Every call is audited to override_log (compromised-key trail).
func (*MemoryService) UpdateSection ¶
func (s *MemoryService) UpdateSection(ctx context.Context, sectionID uuid.UUID, content *string, heading *string, overrideID *uuid.UUID) (*models.Section, error)
UpdateSection partially updates a section: content!=nil re-embeds and sets content; heading!=nil sets heading (blank -> NULL). Both nil is a no-op.
func (*MemoryService) UpdateTenant ¶
func (s *MemoryService) UpdateTenant(ctx context.Context, id uuid.UUID, fields UpdateTenantFields) (*models.Tenant, error)
UpdateTenant is the admin-only patcher. It can touch any field.
func (*MemoryService) UpdateTenantSettings ¶
func (s *MemoryService) UpdateTenantSettings(ctx context.Context, tenantID uuid.UUID, stalenessMode *string, duplicateGuard *bool, cleanupScanEnabled *bool) (*models.Tenant, error)
UpdateTenantSettings is the tenant-targeted analogue of UpdateMyTenantSettings: it reads or edits ANOTHER tenant's toggles by id (the ctx-only sibling always targets the caller's home tenant). With all three field pointers nil it is a READ, gated by CanManageTenant (system admin OR tenant#manager). Otherwise it is a WRITE, gated by the tenant's self-service policy at manager level (requireSelfService: open ⇒ manager, admin_only ⇒ admin, system-admin bypass) — managers manage settings, the lock escalates to admins. Writes are audited.
func (*MemoryService) UpdateTenantUserRole ¶
func (s *MemoryService) UpdateTenantUserRole(ctx context.Context, email, role string) (*models.TenantUser, error)
UpdateTenantUserRole changes a role and syncs the role tuple: the target role gets its tuple written (admin -> tenant#admin, owner -> tenant#owner) and the other role tuple removed, so the stored tuples always match exactly one role (member tuple untouched — every role is a member). Admin-gated; email is the unique key.
func (*MemoryService) WritableTenants ¶
func (s *MemoryService) WritableTenants(ctx context.Context) ([]TenantAccess, error)
WritableTenants lists the tenants the caller may administer: every tenant for a system admin (labeled RelAdmin), else the tenants where a direct tuple on the caller's subject resolves to tenant#manager once confirmed by Check. ReadBySubject returns only DIRECT tuples, so a member/viewer tuple on a tenant the caller does not otherwise manage is a candidate that Check then correctly excludes, while a direct tenant#admin or tenant#owner tuple is correctly included via the manager<-admin / manager<-owner rewrites. A directly-owned personal tenant is labeled RelOwner; other managed tenants RelManager (design.md §4).
type MergeResult ¶
type MergeResult struct {
Winner *DocumentView `json:"winner"`
}
MergeResult holds the surviving doc's post-merge view.
type OllamaEmbedder ¶
type OllamaEmbedder struct {
// contains filtered or unexported fields
}
func NewOllamaEmbedder ¶
func NewOllamaEmbedder(ollamaURL, model string, dimensions int) *OllamaEmbedder
func (*OllamaEmbedder) Dimensions ¶
func (e *OllamaEmbedder) Dimensions() int
type OpenAIEmbedder ¶
type OpenAIEmbedder struct {
// contains filtered or unexported fields
}
OpenAIEmbedder calls any OpenAI-compatible /v1/embeddings endpoint. The wire format is shared across OpenAI, Azure, vLLM, LM Studio, LocalAI, and HF TEI, so one client (base URL + optional API key + model) covers all of them.
func NewOpenAIEmbedder ¶
func NewOpenAIEmbedder(baseURL, apiKey, model string, dimensions int) *OpenAIEmbedder
NewOpenAIEmbedder builds an OpenAI-compatible embedder. baseURL is the API root exposing /embeddings (trailing slash tolerated); apiKey may be empty for local servers. dimensions>0 requests output truncation (text-embedding-3-*) and is the dimension the corpus is pinned to by the startup guard.
func (*OpenAIEmbedder) Dimensions ¶
func (e *OpenAIEmbedder) Dimensions() int
type SectionView ¶
type SectionView struct {
ID uuid.UUID `json:"id"`
DocumentID uuid.UUID `json:"document_id"`
Ordinal int `json:"ordinal"`
Heading *string `json:"heading,omitempty"`
Content string `json:"content,omitempty"`
VerifiedAt *time.Time `json:"verified_at,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Status string `json:"status,omitempty"`
Preview string `json:"preview,omitempty"`
VerifyHints []string `json:"verify_hints,omitempty"`
StaleDays int `json:"age_days,omitempty"`
ThresholdDays int `json:"threshold_days,omitempty"`
}
SectionView is the API-facing projection of a section. When the section is stale and mentions code paths, Content is replaced with Preview + verify hints and Status is set to "needs_verification".
type StoreResult ¶
type StoreResult struct {
Status string `json:"status"`
Document *models.Document `json:"document,omitempty"`
Path string `json:"path,omitempty"`
Sections int `json:"sections,omitempty"`
Candidates []repository.SimilarityCandidate `json:"candidates,omitempty"`
}
StoreResult is the outcome of StoreDocument. Status "similar_exists" means the save was skipped and Candidates lists the colliders (Document nil); "ok" on success.
type TenantAccess ¶
TenantAccess pairs a tenant with the caller's effective relation on it, so the UI can label the caller's role (design.md §5).
type UpdateTenantFields ¶
type UpdateTenantFields struct {
Name *string
Email *string
Type *string
StalenessMode *string
DuplicateGuard *bool
CleanupScanEnabled *bool
// SelfServicePolicy accepts "open" | "admin_only" | "inherit" (the last clears
// the per-tenant override to NULL). Admin-only — never wired to self-service.
SelfServicePolicy *string
}
UpdateTenantFields bundles the optional patches admin/self tools may apply. Any nil pointer leaves the corresponding column unchanged.