Skip to content

tenants: keep work and personal sessions apart, scope what each agent can recall #166

Description

@tenequm

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.

  1. 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".
  2. 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.
  3. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

enhancementNew feature or requestroadmapOn the public roadmap project

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions