Skip to content

ADR-0007: Consent gate on agent retrieval (not user browsing)

  • Status: Accepted
  • Date: 2026-05-03
  • Affected: search_memories_semantic, match_memories, backend/mcp/arcive-memory-mcp/, apps/web/app/api/chat/, apps/web/lib/search.ts

Context

memory_participants.person_id references people, which carries a consent_status column (granted / pending / revoked). The Caregiver role’s prompt promises to refuse to draw conclusions about people whose consent is pending or revoked. Reviewer / Tutor / Brainstorm have the same need — quoting someone who hasn’t consented is problematic regardless of the role.

But the data owner — the user — should still see everything they recorded. They’re the source of truth; the gate is about what agents expose downstream, not what the user can read.

Options considered

Option A — Per-role enforcement in the system prompt only

  • Pros: Zero schema work; LLM follows the rule.
  • Cons: Prompt-only enforcement leaks. The model sees the un-consented data in retrieval and may still reference it. Not auditable.

Option B — Filter at the application layer (/api/chat)

  • Pros: Centralized in the agent surface.
  • Cons: Each agent surface (chat, voice, MCP) has to remember to filter. Easy to miss when a new surface ships. The MCP server exposes the same retrieval to external clients (V1.0); they wouldn’t inherit the filter.

Option C — Filter at retrieval, opt-in via parameter (chosen)

  • Pros: One place enforces it. Every agent path passes respect_consent: true and inherits the gate. User UI passes the default false and sees everything they own. The MCP server tool always passes true, so external clients get the gate too.
  • Cons: Requires passing the flag everywhere agent retrieval happens. Rest of the system reads the schema directly (e.g. delete cascades); those paths aren’t affected.

Option D — Two separate RPCs (…_for_owner and …_for_agent)

  • Pros: Type-level distinction; impossible to forget the gate on the agent path because the function name says it.
  • Cons: Duplication; two functions to keep in sync. The boolean parameter is a clearer one-knob design.

Decision

Add respect_consent boolean default false to search_memories_semantic and match_memories. When true, exclude memories with any participant whose consent_status <> 'granted'. When false (the default), behave as before. The default ensures the user’s own /today browsing is unaffected.

The MCP server’s search_memories tool always passes respect_consent: true. /api/chat’s inline-fallback path also passes true. lib/search.ts (user-facing search box) does not pass it; default false applies.

Consequences

  • Caregiver, Reviewer, Tutor, Brainstorm now share the same retrieval consent gate without per-role code.
  • A memory with one un-consented participant disappears from agent retrieval entirely — there’s no partial redaction. Per-segment redaction is harder and not in scope for V0.3.
  • The owner can grant consent in the UI (people.consent_status) to bring a memory back into agent retrieval.
  • Voice service (Pipecat) inherits the gate when MCP wiring lands (next slice).
  • compute-edges-step uses match_memories. Edges currently get computed without respect_consent. We could flip it on there too to avoid hint-by-edge, but for V0.3 the gate on retrieval is sufficient: edges are surfaced through Universe (owner-visible surface) only.

Notes

People rows are user-scoped; consent_status defaults to pending for newly auto-created people (e.g. an “Unknown speaker” from re-ID). That means re-ID’d people are gated from agent retrieval until the owner explicitly grants consent — failure-closed by default. The “Self” person is granted on signup.