service

package
v1.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 29, 2026 License: AGPL-3.0 Imports: 38 Imported by: 0

Documentation

Index

Constants

View Source
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

View Source
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")
)
View Source
var ErrEmbeddingUnavailable = errors.New("embedding provider unavailable")

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).

View Source
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

func (*AWSEmbedder) Embed

func (e *AWSEmbedder) Embed(ctx context.Context, text string) (pgvector.Vector, error)

Embed generates an embedding, dispatching on the model family prefix. Upstream errors/empty results are logged server-side and returned as the tenant-safe ErrEmbeddingUnavailable sentinel (audit #18) — provider internals never leak.

type BatchEmbedder added in v1.1.0

type BatchEmbedder interface {
	EmbedBatch(ctx context.Context, texts []string) ([]pgvector.Vector, error)
}

BatchEmbedder is an optional capability: providers that can embed many texts in one upstream call implement it; callers fall back to looping Embed otherwise.

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

type DocSource func(emit func(path string, content []byte) error) error

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 EdgeResult added in v1.1.1

type EdgeResult struct {
	Edge           *models.Edge `json:"edge"`
	TargetArchived bool         `json:"target_archived"`
}

EdgeResult is the outcome of CreateEdge: the created (or, on an idempotent re-create, the existing) edge and whether THIS call archived the target.

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

func (*FakeEmbedder) Embed

func (e *FakeEmbedder) Embed(_ context.Context, text string) (pgvector.Vector, error)

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

func (*GCPEmbedder) Embed

func (e *GCPEmbedder) Embed(ctx context.Context, text string) (pgvector.Vector, error)

func (*GCPEmbedder) EmbedBatch added in v1.1.0

func (e *GCPEmbedder) EmbedBatch(ctx context.Context, texts []string) ([]pgvector.Vector, error)

EmbedBatch embeds texts in native Vertex batches of gcpMaxBatch. Predictions map 1:1 to instances, so results stay in input order; length equals len(texts).

type GlobalConfig added in v1.1.1

type GlobalConfig interface {
	MMRLambda() float64
	StalenessPenalty() float64
	CandidatePool() int
	SnippetChars() int
	StalenessDefault() string
	DuplicateGuardDefault() bool
	CleanupScanDefault() bool
	HistoryEnabled() bool
	DuplicateThreshold() float64
	SelfServicePolicy() string
}

GlobalConfig is the read-only slice of the global-config accessor the service reads live; *globalconfig.Accessor satisfies it.

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 HandoffRef added in v1.1.1

type HandoffRef struct {
	ID       uuid.UUID `json:"id"`
	Path     string    `json:"path"`
	Title    string    `json:"title"`
	Archived bool      `json:"archived"`
}

HandoffRef identifies one handoff in a resume chain (no section content).

type ImportResult

type ImportResult struct {
	Imported int
	Skipped  int
	Failed   int
}

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

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

func (s *MemoryService) CanManageTenant(ctx context.Context, tenantID uuid.UUID) bool

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) CreateEdge added in v1.1.1

func (s *MemoryService) CreateEdge(ctx context.Context, sourceID, targetID uuid.UUID, edgeType string, overrideID *uuid.UUID) (*EdgeResult, error)

CreateEdge records a directed typed edge from source to target. supersedes archives the target atomically with the insert (reason "superseded"); an idempotent re-create returns the existing edge and runs no second side effect.

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

func (s *MemoryService) DeleteAPIKey(ctx context.Context, id uuid.UUID) error

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) DeleteEdge added in v1.1.1

func (s *MemoryService) DeleteEdge(ctx context.Context, edgeID uuid.UUID, overrideID *uuid.UUID) error

DeleteEdge removes an edge, gated by editor on its source doc under the member write floor. Deleting a supersedes edge does NOT un-archive its former target.

func (*MemoryService) DeleteSection added in v1.1.1

func (s *MemoryService) DeleteSection(ctx context.Context, sectionID uuid.UUID, overrideID *uuid.UUID) error

DeleteSection removes one section by id, authorized exactly like UpdateSection. Deleting a document's last remaining section also deletes the now-empty parent document (same delete path as DeleteDocument). Writes no deletion_event.

func (*MemoryService) DeleteTenant

func (s *MemoryService) DeleteTenant(ctx context.Context, id uuid.UUID) error

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) GetDocumentHistory added in v1.1.1

func (s *MemoryService) GetDocumentHistory(ctx context.Context, docID uuid.UUID, overrideID *uuid.UUID) ([]models.MutationHistory, error)

GetDocumentHistory lists a doc's mutation history newest-first. Live doc: gated like GetDocumentByID (keeps per-doc guest access). Deleted doc: visible to readers of its owning tenant (audit survives). Neither readable ⇒ ErrNotFound, no leak.

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 (s *MemoryService) ListAPIKeys(ctx context.Context, tenantID uuid.UUID) ([]models.APIKey, error)

func (*MemoryService) ListDocumentEdges added in v1.1.1

func (s *MemoryService) ListDocumentEdges(ctx context.Context, docID uuid.UUID, overrideID *uuid.UUID) ([]repository.EdgeListItem, error)

ListDocumentEdges returns a document's edges in both directions, gated by read access on the doc (same gate as GetRelated). Includes edges to archived endpoints so the supersede trail stays visible from the live source.

func (*MemoryService) ListDocumentGrants

func (s *MemoryService) ListDocumentGrants(ctx context.Context, docID uuid.UUID) ([]Grant, error)

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

func (s *MemoryService) ListTenantGrants(ctx context.Context, tenantID uuid.UUID) ([]Grant, error)

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 (s *MemoryService) ListTenants(ctx context.Context) ([]models.Tenant, error)

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

func (s *MemoryService) PurgeDeadKeys(ctx context.Context, ttl time.Duration) (int64, error)

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) Resume added in v1.1.1

func (s *MemoryService) Resume(ctx context.Context, subcategory *string, overrideID *uuid.UUID, depth int) (ResumeResult, error)

Resume returns the latest handoff for a project plus, when depth > 1, the ordered continues_from chain of prior handoffs (bounded). Read-scope gated: an empty scope or a project with no handoff yields an empty result, no error.

func (*MemoryService) RevokeAPIKey

func (s *MemoryService) RevokeAPIKey(ctx context.Context, id uuid.UUID) error

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, docType *string, limit int, forceRead bool, reason string, overrideID *uuid.UUID, snippet bool) ([]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,
	pin *bool,
) (*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. pin distinguishes unset (nil, keep current on upsert / default false on create) from an explicit true/false. It sets documents.pinned, which exempts the doc from access-recency eviction (D4).

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, duplicateThreshold *float64, clearDuplicateThreshold 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, duplicateThreshold *float64, clearDuplicateThreshold 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

func (*OllamaEmbedder) Embed

func (e *OllamaEmbedder) Embed(ctx context.Context, text string) (pgvector.Vector, error)

Embed generates an embedding vector for the given text.

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

func (*OpenAIEmbedder) Embed

func (e *OpenAIEmbedder) Embed(ctx context.Context, text string) (pgvector.Vector, error)

Embed generates an embedding vector for text via the OpenAI-compatible API.

type Option added in v1.1.1

type Option func(*MemoryService)

Option configures optional MemoryService behavior at construction time.

func WithCandidatePool added in v1.1.1

func WithCandidatePool(n int) Option

WithCandidatePool sets the per-list HybridSearch candidate LIMIT applied to every Search call. Without it candidatePool keeps its default.

func WithEdgeRepository added in v1.1.1

func WithEdgeRepository(edges *repository.EdgeRepository) Option

WithEdgeRepository injects the typed-edge repo. Without it edges stays nil and the edge methods refuse (offline CLI / tests); wired only in the MCP server.

func WithGlobalConfig added in v1.1.1

func WithGlobalConfig(gc GlobalConfig) Option

WithGlobalConfig injects the global-config accessor. Without it globalCfg stays nil and the write guard's threshold falls back to defaultDuplicateThreshold.

func WithMMRLambda added in v1.1.1

func WithMMRLambda(lambda float64) Option

WithMMRLambda sets the default MMR diversity lambda applied to every Search call. Without this option mmrLambda stays nil and MMR re-ranking is off.

func WithSnippetChars added in v1.1.1

func WithSnippetChars(chars int) Option

WithSnippetChars sets the match-centered snippet window size (chars) applied when Search runs in snippet mode. Without it snippetChars keeps its default.

type ResumeResult added in v1.1.1

type ResumeResult struct {
	Latest *models.Document `json:"latest"`
	Chain  []HandoffRef     `json:"chain,omitempty"`
}

ResumeResult is the outcome of Resume: the latest handoff (full content) and the ordered prior-handoff chain it continues from (newest first, bounded).

type SearchResponse added in v1.1.1

type SearchResponse struct {
	Results []repository.SearchResult `json:"results"`
}

SearchResponse is the search envelope shared by the MCP and HTTP surfaces: results is always a JSON array, never null.

func NewSearchResponse added in v1.1.1

func NewSearchResponse(results []repository.SearchResult) SearchResponse

NewSearchResponse builds the envelope from Search's results: a nil slice becomes [] so the wire shape is always a JSON array.

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"`
	// Warnings carries non-fatal issues (e.g. a best-effort handoff auto-chain
	// link that failed) so a valuable write still succeeds while surfacing them.
	Warnings []string `json:"warnings,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

type TenantAccess struct {
	Tenant   models.Tenant `json:"tenant"`
	Relation string        `json:"relation"`
}

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
	DuplicateThreshold *float64
	// ClearDuplicateThreshold clears the per-tenant override to NULL (inherit the
	// global default); it wins over DuplicateThreshold when both are set.
	ClearDuplicateThreshold 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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL