P0 — make conversations, context, and memory strictly principal-scoped for multi-user daemon operation #131

Closed
opened 2026-08-21 23:41:45 +00:00 by lost-rob0t · 0 comments
lost-rob0t commented 2026-08-21 23:41:45 +00:00 (Migrated from github.com)

Parent epic: #127
Depends on: #130
Integrates with: #51, existing MemoryManager, existing conversation persistence
Research: docs/research/001-daemon-zeromq-voice-service.md

Goal

Make authenticated multi-user operation a structural property of Zara rather than a UI convention. A principal must never be able to read, mutate, enumerate, infer the existence of, or subscribe to another principal's private conversation/context/memory state.

Problem

The current MemoryManager is process-oriented: it owns an in-memory session dictionary, one collection name, and current_session_id. That is acceptable for a single-user process but unsafe as the ownership model of a shared daemon.

Required ownership model

Introduce one explicit server-side principal scope used by every private state operation:

Principal
  +-- sessions/devices
  +-- conversations/messages
  +-- ContextManager state
  +-- learned memories/summaries
  +-- vector namespace
  +-- user configuration/preferences
  +-- tool grants/approval state
  +-- audit/export/delete ownership

Authenticated principal identity comes from #130 and is injected by the daemon. Callers do not choose another owner by sending a different user_id.

Conversation/context persistence

  • every durable conversation/message row carries an immutable owner/principal key;
  • every read/update/delete/list/search API requires principal scope;
  • conversation IDs are not global bearer capabilities: knowing another ID does not grant access;
  • current #51 ContextManager work remains the canonical context assembly boundary;
  • transient retrieved memory/skills/tool state remain transient under the same principal;
  • stale/cancelled turns cannot append context after ownership changes or a newer turn takes over.

Memory

Refactor/wrap MemoryManager so daemon state does not depend on process-global current_session_id.

Requirements:

  • memory operations require an owning principal context;
  • session IDs are scoped to a principal;
  • summaries/facts/transcripts store owner metadata at the persistence boundary;
  • retrieval can never execute an unscoped query in daemon mode;
  • Chroma persistence must have a hard owner boundary: initially prefer a separate collection/database namespace per principal if that is simpler to prove; a shared collection is allowed only if every operation structurally requires an owner filter and tests prove no bypass exists;
  • local in-memory fallback is also partitioned per principal;
  • guest/ephemeral principals cannot see durable user memory and are destroyed explicitly;
  • per-principal export/delete is deterministic and cannot affect another principal.

Configuration and providers

  • model/provider/voice/context preferences may be per principal where already supported;
  • provider credentials need an explicit ownership policy: daemon-global service credential vs principal-owned credential must never be inferred from arbitrary request payloads;
  • do not duplicate #124 provider implementation.

Event isolation

  • private runtime/turn/tool/memory events are routed only to authenticated sessions belonging to the owning principal;
  • reconnecting one principal cannot subscribe to another principal's conversation by ID;
  • server diagnostics may aggregate counts only where authorized and must not leak private content/identifiers unnecessarily.

Tests — mandatory cross-user matrix

Create at least principals A and B and prove:

  • A cannot get/list/search B conversation by known ID;
  • A cannot append/rename/delete B conversation;
  • A cannot retrieve B learned fact/summary even with an exact matching query;
  • A cannot infer B memory existence from error differences where practical;
  • A cannot subscribe to B turn/tool/voice events;
  • identical session/conversation labels remain independent;
  • reconnect/resume preserves the correct owner;
  • guest session sees no durable memory;
  • deletion/export affects only the selected principal;
  • vector/local-fallback paths have the same isolation guarantees;
  • concurrent A/B turns do not share current_session or mutable context;
  • focused non-interactive isolation script plus full repo/Nix gate.

Acceptance

Multi-user privacy is regression-proven at persistence, runtime, event, and memory boundaries. There is no unscoped daemon API for private state and no global mutable current-user/current-session shortcut.

Non-goals

  • no second memory subsystem unrelated to MemoryManager/#51;
  • no reliance on UI filtering as a security boundary;
  • no client-authoritative owner IDs;
  • no requirement to expose administrative cross-user browsing APIs in this slice.
Parent epic: #127 Depends on: #130 Integrates with: #51, existing `MemoryManager`, existing conversation persistence Research: `docs/research/001-daemon-zeromq-voice-service.md` ## Goal Make authenticated multi-user operation a structural property of Zara rather than a UI convention. A principal must never be able to read, mutate, enumerate, infer the existence of, or subscribe to another principal's private conversation/context/memory state. ## Problem The current `MemoryManager` is process-oriented: it owns an in-memory session dictionary, one collection name, and `current_session_id`. That is acceptable for a single-user process but unsafe as the ownership model of a shared daemon. ## Required ownership model Introduce one explicit server-side principal scope used by every private state operation: ```text Principal +-- sessions/devices +-- conversations/messages +-- ContextManager state +-- learned memories/summaries +-- vector namespace +-- user configuration/preferences +-- tool grants/approval state +-- audit/export/delete ownership ``` Authenticated principal identity comes from #130 and is injected by the daemon. Callers do not choose another owner by sending a different `user_id`. ## Conversation/context persistence - every durable conversation/message row carries an immutable owner/principal key; - every read/update/delete/list/search API requires principal scope; - conversation IDs are not global bearer capabilities: knowing another ID does not grant access; - current #51 `ContextManager` work remains the canonical context assembly boundary; - transient retrieved memory/skills/tool state remain transient under the same principal; - stale/cancelled turns cannot append context after ownership changes or a newer turn takes over. ## Memory Refactor/wrap `MemoryManager` so daemon state does not depend on process-global `current_session_id`. Requirements: - memory operations require an owning principal context; - session IDs are scoped to a principal; - summaries/facts/transcripts store owner metadata at the persistence boundary; - retrieval can never execute an unscoped query in daemon mode; - Chroma persistence must have a hard owner boundary: initially prefer a separate collection/database namespace per principal if that is simpler to prove; a shared collection is allowed only if every operation structurally requires an owner filter and tests prove no bypass exists; - local in-memory fallback is also partitioned per principal; - guest/ephemeral principals cannot see durable user memory and are destroyed explicitly; - per-principal export/delete is deterministic and cannot affect another principal. ## Configuration and providers - model/provider/voice/context preferences may be per principal where already supported; - provider credentials need an explicit ownership policy: daemon-global service credential vs principal-owned credential must never be inferred from arbitrary request payloads; - do not duplicate #124 provider implementation. ## Event isolation - private runtime/turn/tool/memory events are routed only to authenticated sessions belonging to the owning principal; - reconnecting one principal cannot subscribe to another principal's conversation by ID; - server diagnostics may aggregate counts only where authorized and must not leak private content/identifiers unnecessarily. ## Tests — mandatory cross-user matrix Create at least principals A and B and prove: - A cannot get/list/search B conversation by known ID; - A cannot append/rename/delete B conversation; - A cannot retrieve B learned fact/summary even with an exact matching query; - A cannot infer B memory existence from error differences where practical; - A cannot subscribe to B turn/tool/voice events; - identical session/conversation labels remain independent; - reconnect/resume preserves the correct owner; - guest session sees no durable memory; - deletion/export affects only the selected principal; - vector/local-fallback paths have the same isolation guarantees; - concurrent A/B turns do not share `current_session` or mutable context; - focused non-interactive isolation script plus full repo/Nix gate. ## Acceptance Multi-user privacy is regression-proven at persistence, runtime, event, and memory boundaries. There is no unscoped daemon API for private state and no global mutable current-user/current-session shortcut. ## Non-goals - no second memory subsystem unrelated to `MemoryManager`/#51; - no reliance on UI filtering as a security boundary; - no client-authoritative owner IDs; - no requirement to expose administrative cross-user browsing APIs in this slice.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
nsaspy/zara#131
No description provided.