Why
The wire protocol already carries an opaque namespace (spec 7.3) and the substrate has a catalog seam for it (3.2 lance-chokepoints-catalog), but a personal pond has exactly one namespace and nothing enforces separation. Two concrete needs:
- Work vs personal. One person's client sessions must not mix with their personal ones in the same store, and an agent working on a client repo must not be able to recall personal sessions (or the reverse). Today the only answer is two separate stores with two config files and two
serve processes - it works, but it is undocumented and every surface (schedule, status, MCP registration) assumes one.
- Per-agent scoping. A fleet of agents sharing one store (e.g. several assistants behind one
pond serve) where each agent should see only its own tenant.
Design (agreed 2026-09-01)
Full write-up, findings, alternatives, and edge rules: docs/plans/2609-01-scoped-access-tenants-design.md.
- Token gate + token-to-tenant map on
serve. [serve.tokens.<name>] (token_file / token_command, tenants = [...]) applied on /v1/* and /mcp; 401 on unknown or missing token; required for a non-loopback bind; stdio takes --tenant. Spec 2.3 keeps "no identity" and drops "no authentication".
- Session-level editable
tenant label, not denormalized. Typed column on sessions or an append-only session_tenants sidecar (B vs C, decided after the benchmark below). Reads resolve the visible session set and push session_id IN (...) through the existing BTree indexes on messages / parts; pond_sql views get the same semi-join. Assigned at ingest from [adapters.<x>].tenant or the token; edited with pond tenant set / pond tenant assign; child sessions follow the parent.
- Fail-closed. Unassigned sessions are visible only to unscoped callers; scoped responses report in-scope counts only; the wire
namespace narrows within the token's allowlist, anything outside is namespace_unknown. Scoped tokens can ingest (tenant stamped on Session events, mismatches rejected per row).
One store, all visible by default, scoped when tokens are configured. No child Lance namespaces or partitions now (deferred to a physical trigger); no MemWAL needed for live writes; storage credentials ([creds.<name>].scope) remain the hard boundary.
Open
- B (mutable column) vs C (append-only sidecar): same read path, differ only in the write. Decide after
pond search --explain confirms an index-served IN prefilter and serve_mem_bench --io-trace at 100 / 1k / 5k visible sessions, warm and cold. Denormalizing onto messages is the fallback only if the tail degrades.
- Optional per-token
projects / source_agents allowlists at the same injection point (the former "should project allowlists come first?" question) - not in the first cut.
Acceptance
- Spec: 2.3 amendment, a 5.x section for the tenant label and its write model, 7.3 (
namespace resolves to a tenant within the token scope), 7.5 ingest stamping, 7.8 verbs.
- An MCP or HTTP connection bound to tenant A cannot see tenant B across search, get, and sql (tested); unknown token is 401; unassigned sessions are hidden from scoped callers; scoped ingest rejects a mismatching Session event.
pond copy / .pond archives / JSONL carry the tenant; pond erase cascades; pond tenant assign backfills an existing store.
bench-gate rows on both sides before landing (schema change).
- Independently of the above: a docs page for the fully separate-store pattern and
pond init --profile remain the hard-isolation answer.
Why
The wire protocol already carries an opaque
namespace(spec 7.3) and the substrate has a catalog seam for it (3.2lance-chokepoints-catalog), but a personal pond has exactly one namespace and nothing enforces separation. Two concrete needs:serveprocesses - it works, but it is undocumented and every surface (schedule, status, MCP registration) assumes one.pond serve) where each agent should see only its own tenant.Design (agreed 2026-09-01)
Full write-up, findings, alternatives, and edge rules: docs/plans/2609-01-scoped-access-tenants-design.md.
serve.[serve.tokens.<name>](token_file/token_command,tenants = [...]) applied on/v1/*and/mcp; 401 on unknown or missing token; required for a non-loopback bind; stdio takes--tenant. Spec 2.3 keeps "no identity" and drops "no authentication".tenantlabel, not denormalized. Typed column onsessionsor an append-onlysession_tenantssidecar (B vs C, decided after the benchmark below). Reads resolve the visible session set and pushsession_id IN (...)through the existing BTree indexes onmessages/parts;pond_sqlviews get the same semi-join. Assigned at ingest from[adapters.<x>].tenantor the token; edited withpond tenant set/pond tenant assign; child sessions follow the parent.namespacenarrows within the token's allowlist, anything outside isnamespace_unknown. Scoped tokens can ingest (tenant stamped on Session events, mismatches rejected per row).One store, all visible by default, scoped when tokens are configured. No child Lance namespaces or partitions now (deferred to a physical trigger); no MemWAL needed for live writes; storage credentials (
[creds.<name>].scope) remain the hard boundary.Open
pond search --explainconfirms an index-served IN prefilter andserve_mem_bench --io-traceat 100 / 1k / 5k visible sessions, warm and cold. Denormalizing ontomessagesis the fallback only if the tail degrades.projects/source_agentsallowlists at the same injection point (the former "should project allowlists come first?" question) - not in the first cut.Acceptance
namespaceresolves to a tenant within the token scope), 7.5 ingest stamping, 7.8 verbs.pond copy/.pondarchives / JSONL carry the tenant;pond erasecascades;pond tenant assignbackfills an existing store.bench-gaterows on both sides before landing (schema change).pond init --profileremain the hard-isolation answer.