# Authentication and tenant isolation

Team mode uses opaque API keys. A key is shown once at creation; only its SHA-256 hash is stored. Tokens contain 192 bits of random material, so offline guessing is not practical even though lookup uses a fast hash.

## One-click local console boundary

The v0.13.2 `console` launcher creates a separate random local Runtime credential and instance ID. The credential is passed to the child Runtime through its environment, not through MemPlumb command arguments, browser-launch arguments, a URL, command output, or Runtime logs. For browser bootstrap, the authenticated launcher mints a different 256-bit token that expires after 60 seconds and is atomically consumed on first exchange. Only this disposable token enters the URL fragment and the process arguments used to open the browser. With `--no-open` or when browser startup fails, the same short-lived URL is returned in command output for manual opening; that output is sensitive until the token is consumed or expires. A successful automatic launch returns only the clean `/console` URL. The console validates and removes the fragment before state initialization, exchanges the token over same-origin HTTP, and stores the returned credential in `sessionStorage`.

Launcher reuse requires a sensitive state file at `%USERPROFILE%\.memplumb\run\<profile-id>.json`. It contains the credential in recoverable form so a later double-click can reconnect to the same local Runtime. On Windows, confidentiality therefore depends on the current user's profile ACL; the portable executable cannot turn a shared or compromised Windows account into a separate security boundary. Do not sync, upload, attach, or collect this state file as diagnostics. The ordinary CLI does not allow relocating this state to a shared path.

The launcher binds control to more than a port or process ID. Reuse and stop authenticate with the saved credential and instance ID, then verify the Runtime identity, process ID, Workspace, database profile, durability, version, and Build ID as applicable. An occupied port with unknown ownership is skipped and its process is never killed. `console-stop` removes state only after the exact launcher-owned instance stops; an unverifiable live process is left untouched.

Default launcher logs live at `%USERPROFILE%\.memplumb\logs\<profile-id>.log`. They exclude the local credential, rotate at 2 MiB, and retain one previous `.log.1` file, limiting accidental growth but not replacing normal endpoint security and retention controls. The advanced `serve` authentication and deployment model is unchanged. Docker is not part of the SQLite console trust boundary and is required only for deployments or tests that choose containerized dependencies such as PostgreSQL.

The current persistence and release boundary is Memory Store Contract v20, SQLite schema v26, PostgreSQL migration v32, Workspace Snapshot v17, Actor export schema v14, and Release Artifact v12. Webhook secrets are bound to a global master-key fingerprint and generation; rotation is a single Store transaction, delivery completion is lease-version fenced, and Actor purge cannot race a guarded outbound dispatch. Artifacts v8-v11 remain supported for integrity and deployment verification.

```powershell
memplumb key-create --workspace support-prod --name agent-runtime --scope memory:read --scope memory:write
memplumb key-create --workspace support-prod --name production-judge --scope evaluation:write --evaluator-id judge-2026-07 --evaluator-type model
memplumb key-create --workspace support-prod --name quality-exporter --scope evaluation:export
memplumb key-create --workspace support-prod --name replay-operator --scope evaluation:read --scope evaluation:execute
memplumb key-create --workspace support-prod --name memory-auditor --scope causality:read
memplumb key-list --workspace support-prod
memplumb key-revoke --workspace support-prod --key key_...
memplumb serve --require-auth --rate-limit 600
```

The authenticated key selects the Workspace. Client headers and request bodies cannot override it. Supported scopes are `memory:read`, `memory:write`, `evaluation:read`, `evaluation:write`, `evaluation:export`, `evaluation:execute`, `review:work`, `review:resolve`, `review:audit`, `causality:read`, and `admin`.

`causality:read` grants only the typed Memory Causality Ledger traversal; it does not grant raw Memory or Outcome evidence reads. `admin` also satisfies the scope. The endpoint is Workspace-bound and projects a strict attribute allowlist, omitting Actor/Workspace IDs, Memory values, prompts, Context queries, Outcome evidence, Evidence Control reasons, credentials, and arbitrary stored metadata. It does expose subject IDs plus optional trace/span IDs, which can correlate activity across an observability backend, so the scope should be reserved for auditors and release operators rather than ordinary agents.

Outcome, Retrieval Adjudication, and Write Adjudication creation require `evaluation:write`; summary evidence, review queues, and comparisons require `evaluation:read`. Full Write Adjudication notes/evidence require `review:audit`. None of these scopes grants memory access. Creating an `evaluation:write` key requires an evaluator ID and type. Under managed authentication the Runtime derives that identity from the key, rejects conflicting body fields, records the non-secret Key ID as `evaluator_principal_id`, and marks the record `authenticated`. Retrieval/Outcome workload attribution comes from the referenced Context Run; Write Adjudication workload attribution and observed action come from the exact Store-held Write Decision Observation. Thus a scoped evaluator cannot forge either workload attribution or a trusted judge identity.

Retrieval evaluator identity defaults to `unverified`. Direct Store writes, local `quality-adjudicate`, legacy authentication, and otherwise unmanaged HTTP adjudication remain auditable but cannot enter an authenticated-only production quality dataset. An unmanaged HTTP path may use a domain-separated hash to create a non-secret pseudonymous `evaluator_principal_id` for principal-bound idempotency; that identifier is correlation material, not proof of identity. Consumers must authorize on explicit authentication state and credential scope, never on principal-ID presence alone.

Quality review assignment names a logical evaluator identity; it is not authority
by itself. Claim derives that identity and the physical lease-owner principal
from the managed evaluator-bound key. This separation permits safe key rotation
without allowing a caller to impersonate the assigned reviewer through request
fields. Panel configuration requires `admin`. Slot work requires `review:work`
or legacy `evaluation:write` plus an evaluator binding. Resolution requires an
explicit evaluator-bound `review:resolve` key; admin alone cannot bypass it, and
a round participant cannot resolve the same round. Full blind identities require
`review:audit` or admin.

Every claim/takeover uses a client-generated uniformly random 256-bit token. The
Runtime persists only its SHA-256 hash and compares the presented token together
with monotonic `lease_version`, owner principal, and database-clock expiry while
revalidating the stored task generation and signal basis. The raw token is a
short-lived operational credential: clients must place it in neither URLs,
telemetry, exceptions, nor general secret stores. CLI automation should prefer
`MEMPLUMB_REVIEW_LEASE_TOKEN` to a command-line option. MemPlumb excludes both
raw token and hash from public task responses, logs, spans, metrics, Change Feed
payloads, Causality facts, Actor exports, and Snapshot v17. Restored active task,
slot, and resolution leases become pending with independent higher fences.

Managed adjudication cannot bypass an active stable task. Only task `complete`
may atomically append authenticated evidence while checking the current lease
and exact signal basis. Rejected completion creates durable authenticated audit
evidence and `needs_escalation`; an administrator can resume with a reason and
new evaluator assignment but cannot silently resolve the task. `resolved`
requires authoritative Feedback or
Outcome correction. See
[quality-review-task-leases.md](./quality-review-task-leases.md).

`evaluation:execute` is deliberately separate from evaluator write and data export. It can curate a Memory Case from an already accepted authenticated adjudication and create, cancel, or resume Replay Runs and Replay Cohort Plans. It cannot submit an adjudication, read Case content fingerprints/query/Memory values/source references, control evidence, or impersonate a reviewer. Case summaries expose a random opaque ID rather than stable content correlators. Case, Run, Cohort summary, and Cohort freshness reads accept `evaluation:read`, `evaluation:execute`, or `evaluation:export` so an execution-only job can observe its own durable work without receiving restricted payload access.

Replay Cohort Plan creation is server-controlled. The caller supplies rubric, policies, thresholds, and shard size but cannot supply Case IDs or the population cutoff. The Store chooses one cutoff, resolves the complete latest-authenticated consensus population, and pins member/source eligibility evidence atomically. Memory Store Contract v20 preserves v19's freshness rule: `replayCohortPlanFreshness` chooses a new database cutoff and reconstructs the complete current population atomically rather than trusting a client-provided member list.

HTTP, typed SDK, and Console projections expose only Plan metadata, population counts, progress, shard states, opaque Run IDs, integrity hashes, and freshness counts for added, removed, changed, and unchanged Cases. They never return the freshness Case IDs, member list, eligibility snapshots, Case sources, private payloads, or evaluator provenance. `cohort-plan-check` and `GET /v1/replay-cohort-plans/{planId}/freshness` therefore do not widen `evaluation:read` into restricted Case access. Release Artifact v12 consumes the full graph and current population only through trusted Store interfaces and emits a privacy-safe Plan/population/shard/result/freshness binding.

Freshness is also an authorization boundary on time. The default maximum evidence age is seven days and is bound into Artifact v12 with `valid_until`; changing the maximum produces different release evidence. Release construction fails closed when the current population differs or cannot be proven. `release-sign` refuses an expired v12 artifact, Runtime and rollout loading reject expired deployment evidence, and every long-running canary assignment rechecks candidate validity. When validity expires after startup, the shared rollout state atomically records `evidence_expired`, disables candidate routing, and serves stable. An operator resume cannot bypass the same check. These controls are clock-dependent, so production Stores and hosts require trustworthy time synchronization.

`evaluation:export` is deliberately separate from both evaluation read and write. It permits materializing datasets and reading complete Memory Cases. Case v2 may include the full captured Actor retrieval state, query, Memory key/value content, State/Context/index fingerprints, and source references even though global identifiers are replaced with local aliases. Grant it only to a release or dataset job with a restricted output location. Export responses set `Cache-Control: no-store`; the result is `restricted_memory_content` and requires encryption, log/artifact exclusion, retention, and deletion controls. `admin` satisfies this scope and carries the same disclosure risk.

Outcome guardrails and production retrieval datasets consume only rows whose `evaluator_authentication` is `authenticated`, in addition to their exact rubric/evaluator rules. Unverified rows remain audit evidence but cannot control rollout or release state. API key revocation stops future submissions but does not rewrite historical evidence. If a key was compromised, revoke it and treat its historical evidence window as an incident requiring operator review.

Evidence Control endpoints require `admin`; evaluation scopes cannot quarantine or restore their own evidence. Outcome and Retrieval Adjudication controls are independent ledgers: changing one evidence kind never changes the other. A retrieval control may optionally target one rubric or all rubrics for an evaluator principal. Windows are inclusive at `since` and exclusive at `until`; any matching overlapping quarantine wins. A quarantine may revoke the referenced evaluator key atomically. Controls require an evaluator-bound key in the same Workspace and target its non-secret principal ID, preventing one tenant or a second key with the same evaluator name from suppressing unrelated evidence. Restore appends an exact-window revision, never revives an older adjudication revision, and never unrevokes credentials. Control reasons and operator principal IDs are durable audit data and should use incident references without secrets. See [retrieval-adjudication-evidence-controls.md](./retrieval-adjudication-evidence-controls.md).

Retrieval quarantine also advances the affected task generation and lease fence.
Restore can automatically complete the task from only the same latest
authenticated accepted revision when the exact signal basis remained unchanged;
otherwise a new lease and completion are required. Neither transition makes an
old raw token, principal, or lease version current again.

Outcome and Retrieval Adjudication idempotency keys are SHA-256 hashed before persistence and never returned through APIs, exports, or snapshots. Clients should still treat keys as opaque non-secret identifiers. Request hashes bind a key to Context ID, canonical payload, and the effective managed or domain-separated principal, preventing cross-task and cross-evaluator replay without turning an unmanaged principal into trusted evidence.

Memory Case and Cohort Plan idempotency use separate principal-scoped boundaries keyed by Workspace and SHA-256 of the key. They store request hashes and resolved opaque object references so an exact retry returns the same result and changed input conflicts. Raw keys are never stored or exported. Memory Case retry hashes/references remain in Workspace Snapshot v17 as sensitive correlation metadata; Cohort Plan idempotency and request hashes are deliberately stripped from Snapshot v17 and Actor export v14 even though the immutable Plan graph is preserved. Actor purge deletes matching Case entries and removes affected Cohort Plan graphs whose pinned population depends on the purged Actor.

Event ingestion uses a separate Workspace-scoped, Actor-bound idempotency cache whose opaque raw key and completed response are retained for exact HTTP replay. Each processing owner receives an unguessable 256-bit claim token; only its SHA-256 hash is stored, and Actor export v14 deliberately excludes that hash. The five-minute lease allows the same Actor and request to take over an abandoned claim with a new token. Complete and fail are compare-and-set operations over the current token and `processing` state, so a stale worker cannot overwrite a takeover. Completion also requires an Event belonging to the claimed Actor. Hard purge deletes processing, failed, and completed rows by `actor_id` before deleting Events; forced Workspace replacement counts and clears the same ledger. The HTTP layer accepts only a strict `true` completion result, maps a missing Event/Actor binding to `409 EVENT_WRITE_FENCE_CONFLICT`, and does not run a late failure update after a completed transaction cleanup error. Treat Actor exports as restricted personal data because a completed cached response can contain the original Memory result and the opaque key remains correlation material.

Rate limiting is a Store Capability keyed by Workspace and a SHA-256 hash of the authenticated principal. SQLite persists windows for local/multi-process use. PostgreSQL uses an atomic `INSERT ... ON CONFLICT DO UPDATE` counter, so every daemon replica shares the same per-minute budget. Raw API tokens, IP addresses, and principal IDs are not stored in the limiter table. Old windows are pruned at daemon startup.

`--rate-limit 0` disables the limiter. A positive limit adds one atomic Store write per authenticated request. Limiter backend failures fail closed with `503 DEPENDENCY_UNAVAILABLE`; the public response does not expose database details. This favors quota integrity over availability during partial storage failures.

`/health` and `/ready` remain unauthenticated for orchestration probes and do not disclose a Workspace. Readiness includes the active Context Policy's semantic profile state and exposes only bounded backend/state/coverage counts, never provider credentials, query vectors, Memory IDs, or database details. Failures use a stable `NOT_READY` error and never return the underlying database error or connection string.

`GET /v1/session` is authenticated and returns final action capabilities rather than asking the browser to infer authority from scopes. It does not turn `admin` into a reviewer or resolver and does not return the local socket address. `GET /v1/operations/overview` requires `admin`; its Runtime projection excludes executable paths, and its exact aggregate contains no recent rows, queries, Memory values, or raw evidence. Event and Context Run list responses require `memory:read`, use opaque resource-bound cursors, and omit full event text/metadata and assembled Context evidence respectively.

Legacy `--api-key` remains available for single-workspace local compatibility. Team deployments should use `--require-auth` and managed keys.

Webhook configuration, signing-secret rotation, delivery payload inspection, redrive, and deletion require `admin`; `memory:read` is insufficient because delivery records can contain personal memory values. Per-endpoint signing secrets are encrypted with the runtime `MEMPLUMB_WEBHOOK_MASTER_KEY`, never logged, and returned only on create/rotation. Webhook egress defaults to HTTPS public addresses and revalidates DNS at delivery time. Private network and HTTP targets require separate explicit runtime flags; enabling them expands the SSRF boundary and should be restricted to isolated self-hosted networks.

SQLite Webhook lifecycle fencing is process-local. A SQLite-backed Webhook deployment must run exactly one Runtime process; a deployment with multiple Runtime processes or hosts must use PostgreSQL. Independent root `MemoryStore` instances inside one process share the coordinator by canonical database path, so path aliases to the same file remain inside one fence, while different files remain independent. This is a Webhook dispatch, purge, and Workspace-replacement boundary, not a blanket single-process limitation on unrelated SQLite capabilities.

Embedding vectors are derived personal data, not disposable indexes. MemPlumb isolates canonical cache rows and PostgreSQL semantic projections by Workspace and Actor, binds them to exact Memory revision/content hash and provider dimensions, includes canonical rows in Actor export manifests and deletion receipts, and deletes both canonical and projected rows on forget or hard purge. Provider descriptions never contain API keys; embedding credentials are read from `MEMPLUMB_EMBEDDING_API_KEY` and redacted from logs.

`semantic-index-provision` is a privileged DDL boundary. PostgreSQL migration v15 does not automatically install `pgvector`; explicit provisioning attempts `CREATE EXTENSION vector`, verifies version 0.8 or newer, and creates a fixed-dimension provider table plus HNSW/scope indexes. Production Runtime roles should not retain extension/DDL privilege after an administrator-owned provisioning job. A provider key cannot be reprovisioned at a different dimension.

The ANN query joins projection rows to current Memories and applies Workspace, exact Actor, active status, exact Memory revision, `valid_from`, and `expires_at` before returning candidates. Global vector search followed by application-side tenant filtering is forbidden. Candidate signals contain only bounded scalar `semantic_similarity`; raw query or Memory vectors are rejected by the result validator and are not persisted in Context evidence, returned by HTTP, or attached to telemetry.

Portable posting terms are also derived personal data because whole terms and CJK 2/3-grams can reveal Memory content. They remain inside the active Store, are isolated by Workspace and Actor, are transactionally replaced with Memory revisions, and are deleted on forget or Actor purge. Posting rows carry the tokenizer/index version so upgrades rebuild stale derived data. Workspace Snapshot rebuilds this index from restored memories rather than treating physical postings as portable evidence.

Context retrieval is always Workspace- and Actor-scoped. The Pipeline rejects missing or blank Actor IDs. Both built-in Stores apply Workspace, exact Actor, active status, `valid_from`, and `expires_at` predicates in lexical, protected, recent, semantic, and final row lookups. The Pipeline validates every returned candidate, and both Stores repeat Actor and Memory-revision validation before atomically persisting a Context Run. Query embedding happens before ANN lookup, but no candidate Memory text is sent to the provider until after the Store has enforced Actor scope. A faulty or future indexed adapter therefore fails closed instead of exposing cross-Actor memory text. Workspace-only retrieval is available to explicit administrative list/export operations, never to Context assembly.

`portable_fallback` is an availability choice, not silent success: Context evidence records degraded state, reason, backend, coverage, and reallocated budgets. Regulated deployments that cannot accept reduced semantic recall should use `ann_failure_mode: "fail_closed"` and alert on readiness/coverage. SQLite has no production ANN backend and correctly rejects that mode.

Full candidate evidence is sensitive even when a memory was excluded from the prompt. It records Memory IDs/revisions, candidate membership, source channels, eligibility, scores, explanations, and exclusion reasons. `GET /v1/context/:contextId` therefore remains under `memory:read`; Actor export, purge, backup encryption, and retention controls cover selected and non-selected candidates alike.

Retrieval Adjudications are sensitive audit records. The Store validates candidate membership and exact Memory revision before commit, resolves missed positives only within the Context Actor at `evaluated_at`, binds managed authenticated evidence to the evaluator-key principal, and derives release/Context Policy attribution from the run. Dataset materialization selects the latest revision first and then applies effective controls; it does not expose an older revision when the latest is quarantined. Adjudications and labels participate in Actor export and hard purge. Exported quality datasets are external derived assets, however, so a later Runtime purge cannot erase copies already written to CI or object storage; deletion receipts must be connected to those retention systems.

Memory Cases persist restricted source evidence inside the Store instead of requiring an external dataset file. The server revalidates canonical accepted authentication and current control eligibility before materializing content. Case v2 projects raw state into local aliases and recursively rejects raw vectors/global identifier keys. Lists and create responses omit payload, sources, and scenario/Oracle/payload/State fingerprints; full detail places them behind `evaluation:export`. Per-Case Replay results use opaque Case identity and aliases while omitting query/Memory values and internal fingerprints. State blobs never enter ordinary evaluation reads, Change Feed payloads, Causality facts, logs, or telemetry.

Replay worker tokens are stored only as hashes and are never returned by HTTP, snapshots, exports, logs, traces, or errors. Long evaluations heartbeat the current lease. A renewal mismatch or expiry aborts evaluation and prevents the stale worker from committing partial Case results, terminal state, or a misleading failure over a new owner. Cohort resume reuses this fence and advances only unfinished current shard Runs. Actor purge removes Actor-owned State Snapshots/Context bindings, sources, matching Case idempotency entries, orphan Cases, affected Cohort Plans, and dependent Run evidence so no result or Release v12 eligibility/freshness claim outlives its governed source.

Memory Causality records and links retain Workspace/Actor ownership internally so tenant isolation, Actor export, and hard purge remain enforceable even though those identifiers are not returned by Explain. They and the restricted Memory State and Cohort member collections are included in current Workspace Snapshot v17 with candidate, review, Case, Run, and Plan evidence; snapshot encryption, access control, deletion retention, and restore authorization must cover them. Snapshot v1-v16 remains readable but naturally omits collections introduced later. Cross-Workspace guardrail records are projected into each contributing Workspace; Actor-linked evidence edges and affected Cohort Plans are deleted on purge while privacy-safe aggregate release artifacts may remain under their separate retention policy.

OpenTelemetry exports metadata only through attribute allowlists. MemPlumb never adds Actor or Workspace identifiers, Memory/Context/Event IDs, raw text, queries, evidence, credentials, database URLs, or SQL parameters to span or Link attributes. Span Links necessarily carry trace and span identifiers; their only MemPlumb attributes are causal relation and source type. W3C baggage is not accepted or forwarded. Links are capped, deduplicated, and best-effort, and a sampled-out or expired source trace may be unavailable in the backend even while the durable Ledger remains intact. OTLP endpoint credentials are removed from `doctor` output, and only a boolean `headers_present` state is exposed. Operators remain responsible for securing the Collector and applying retention controls to trace IDs and policy/release metadata.
