0023 — Governed User-Agent Memory Requires Explicit Consent and Exact Erasure¶
Status: Proposed
Implementation: issue #193. This proposal does not enable memory or authorize implementation issues. A maintainer must first accept, amend, or reject the privacy stance.
Context¶
Fgentic already has conversation memory: the bridge reuses one A2A contextId per (room, ghost), and kagent persists the corresponding session. Issue #100 added !forget <agent> and maxSessionAge for that session. Those controls do not govern kagent's separate long-term semantic memory store.
Pinned kagent v0.9.11 can extract and retain facts across sessions. Its memory design and exact source establish the current boundary:
- Memory is keyed by
agent_nameanduser_id, stored as plaintext content plus a 768-dimensional embedding in kagent's PostgreSQL database. The bridge currently supplies the complete Matrix sender MXID asX-User-Id, but D11 classifies that value as attribution rather than global authentication. - The runtime automatically summarizes and saves a session every fifth user turn, may save facts through
save_memory, and prefetches relevant facts into a later session. Enabling the Agent-level setting therefore enables collection for every admitted user; it is not a per-user consent switch. GET /api/memoriesreturns every memory and its content for one(agent_name, user_id).DELETE /api/memorieshard-deletes only the complete set for that pair. The pinned handler has no per-entry delete operation.ttl_daysis not a strict maximum retention period. The pinned database queries extend an expired, frequently accessed memory by 15 days and reset its access count; only less-accessed expired rows are deleted.- Summarization, embeddings, prefetch, and explicit load/save consume model or embedding capacity outside the bridge's current invocation budget. D7's chat-token controls do not by themselves bound those background operations.
- The pinned Python runtime logs raw memory search queries and malformed summarizer output, while the pinned Go runtime logs raw memory search queries. Failed model, embedding-provider, or controller responses may likewise contain memory content. The current log path is therefore not a content-free governance boundary.
A raw (agent, MXID) store also crosses Matrix disclosure boundaries. The same person may invoke an Agent in a private room, a team room, and a federated partner room. Retrieving a private preference into a plaintext group reply exposes it to every current room reader and participating homeserver, even if the original fact was collected elsewhere. A localpart, email address, or unverified cross-platform handle cannot safely merge those identities.
The placement choices are:
- Expose kagent memory directly. This is the smallest implementation, but it makes consent Agent-wide, cannot erase one fact, does not enforce a strict retention ceiling, and can reuse facts across rooms. Rejected.
- Store memory content in the bridge database. This permits exact policy but makes the Matrix transport own sensitive semantic content, embedding/search, and a new data lifecycle. It violates the surface budget in ADR 0012 and duplicates kagent. Rejected.
- Keep kagent as the content store, add the missing governed contract, and let the bridge own only Matrix consent/control UX. Selected. Required kagent capabilities may be contributed upstream. If upstream cannot provide them, a separately reviewed sibling memory service is the escape hatch; it is not silently folded into the bridge.
Decision¶
-
Long-term semantic memory remains disabled by default. No default, demo, federation, or production-shaped profile enables kagent
memoryuntil every gate in this decision is implemented and tested. A Proposed ADR is not permission to collect memory. -
The authorization key is a typed triple: complete user principal, exact Agent identity, and memory scope.
- A Matrix principal is the complete MXID plus its origin kind; a localpart alone is invalid. An external-appservice identity remains distinct from a native local identity.
- The default memory scope is one explicitly managed single-subject control room. It never inherits an ambient room, Space, homeserver, or localpart namespace.
- Cross-room personal memory is a later opt-in scope. It requires an authenticated canonical-person binding and an output-audience policy at least as strict as every source scope. Matching email, display name, localpart, or Fediverse handle is not sufficient.
-
A memory-enabled delegation fails closed when the current room, principal, Agent, classification, or output audience does not exactly match the consented scope.
-
Consent precedes collection and retrieval.
- Consent is explicit, purpose-specific, versioned, and off by default for each typed scope. Room membership, invoking an Agent, or accepting general terms is not memory consent.
- Before consent, neither automatic save,
save_memory, prefetch, nor load may run. Because pinned kagent enables these at the Agent level, an implementation must add a per-request/per-scope disable gate or route non-consenting requests to a memory-disabled runtime; prompts alone are not enforcement. - Every memory read and write carries the current consent generation. The content-store boundary transactionally compares that generation with a durable scope record and rejects a missing, disabled, expired, or stale generation. This fence applies to automatic background saves and explicit tools, not only requests initiated by the bridge.
- Withdrawal atomically disables the scope and advances its generation before waiting for or rejecting in-flight work. Erasure then follows a fixed disable/fence → quiesce-or-reject stale work → delete → read-after-delete verify order. A background save scheduled under the old generation can never recreate memory after verification.
-
If fencing, erasure, or verification fails, the scope remains disabled and reports a content-free operator failure; it never resumes on a best-effort assumption.
-
User controls are exact and private. The initial Matrix contract is:
!memory status <agent>shows whether the sender's exact scope is enabled, its purpose, hard expiry, entry cap, and last verified erasure state.!memory inspect <agent>returns bounded, paginated entries with stable opaque ID, stored text, source scope, creation time, and hard expiry.!memory forget <agent> <entry-id>deletes exactly one inspected entry and verifies that it is no longer readable.!memory forget <agent> alland!memory off <agent>require an explicit confirmation;offdisables future collection before clearing all entries.- Only the exact subject may inspect or erase personal memory. A room moderator or platform operator cannot retrieve its content through the Matrix UX.
- Commands execute and reply only in a room-v12, plaintext, invite-only, joined-history control room whose exact allowed state is the subject, the bridge-owned bot, and the selected local Agent ghost, with no other joined or invited identity and a server ACL admitting only the local homeserver. The bridge checks the complete current membership, join rule, history visibility, encryption absence, and ACL at command admission.
-
The bridge recomputes that state immediately before every content-bearing response, retry, and paginated inspect page. Any state drift suppresses the content and fails closed with a content-free notice. A previously delivered Matrix event remains subject to normal history, device-cache, retention, and backup limits.
-
kagent remains the content store; the bridge stores control state only.
- Stored memory records, including fact text and vectors, are persisted only in kagent's scoped PostgreSQL database. Fact and query text, derived summaries, and model, embedding-provider, or controller responses may transit only the explicitly governed kagent, agentgateway, and self-hosted model/embedding processing path authorized for the consented scope. Those processors are part of the memory data boundary; transient values are not persisted outside the scoped store.
- Neither persisted nor transient memory material enters bridge tables, logs, metrics, traces, audit events, exception text, or Matrix room state.
- The bridge may persist a content-free consent receipt, typed scope, policy version, entry/operation identifiers, timestamps, and terminal result. That record is governance evidence, not memory content.
- Before implementation, the kagent boundary must support a scope distinct from attribution, consent-generation fencing at every read/write, per-entry hard deletion with read-after-delete verification, read-time expiry enforcement, an immutable maximum expiry, a hard per-scope entry cap, and content scrubbing before every log, trace, metric label, exception, and audit sink. Pinned
v0.9.11does not satisfy that contract. - Log and error records use only stable content-free reason codes, operation types, and bounded opaque identifiers. They never include raw queries, facts, summaries, prompts, embeddings, or model/provider/controller response bodies, including on malformed or failed responses.
- Expiry is an authorization boundary, not a pruning hint. Every list, search, prefetch, inspect, and Agent retrieval query requires
expires_at > nowat the content-store boundary. At the instant that predicate becomes false the entry is unreadable, regardless of cleanup state or access count. Physical deletion follows within 24 hours and cannot extend the expiry; cleanup lag is not permission to return content. -
The controller API remains cluster-internal behind exact workload and NetworkPolicy authorization. D11's unauthenticated kagent endpoint is never exposed to Matrix clients or federation peers.
-
Federation is denied by default.
- The first implementation admits only a native local MXID in the local-only single-subject control room defined above.
- A remote Matrix user, bridged identity, OAuth client, or Fediverse actor cannot store or retrieve personal memory until a reviewed bilateral policy names the data controller/processor roles, purpose, classification, residency, retention, erasure owner, and canonical principal binding.
- Memory is never injected into a federated room merely because the subject is local. Matrix replication makes every participating homeserver an output recipient; the scope must authorize that audience explicitly.
-
Offboarding one partner disables affected scopes before trust removal and records the remaining backup/federated-copy limits. Trust revocation does not retract previously replicated Matrix history.
-
Conversation reset, semantic-memory erasure, and identity offboarding remain separate operations.
!forget <agent>from #100 resets the room/Agent kagent session. It does not imply semantic-memory deletion.!memory forgetand!memory offgovern semantic memory. They do not redact Matrix events, media, model-provider records, or backups.-
The offboarding reconciler in #153 must disable memory before MAS deactivation, enumerate every consented Agent/scope for the exact principal, clear it, and retain a content-free result. GDPR erasure still composes with the wider retention runbook and its stated residuals.
-
The first prototype is deliberately narrow and cost-bounded.
- One sample Agent, one native local test user, and one local-only single-subject control room are enabled on the disposable demo profile only. No remote identity, group room, default profile, or production claim is included.
- Embeddings use the opt-in self-hosted BGE-M3 profile through the governed agentgateway path. No paid provider or credential-bearing direct model route is introduced.
- The candidate policy uses a strict seven-day maximum, at most 100 entries per scope, and at most 512 UTF-8 bytes per stored fact.
- Cost admission is enforced before model or embedding work. One new session may issue at most eight prefetch subqueries of at most 512 UTF-8 bytes each. Explicit load/save operations share the bridge's six-per-minute, burst-three sender/Agent ceiling and also have a 60-operation per-scope hourly ceiling. One automatic save may run per five user turns; its input is capped at 16 KiB and its output at ten facts. Every summarization, prefetch subquery, explicit load/save, and embedding item consumes the hourly ceiling. Over-limit memory work fails closed without blocking an otherwise memory-free chat reply.
- The implementation meters admitted embedding, summarization, save, and retrieval operations separately from chat tokens and exports bounded aggregate counters only. Metering is evidence after admission, not the limit itself; reservations and operation counts are never reported as model-token consumption.
-
Acceptance proves no collection before consent; private inspect; exact one-entry erase; disable-and-clear; hard expiry; entry-cap refusal; no cross-room, cross-user, cross-agent, bridged, or federated reuse; and offboarding enumeration. Negative log-sink tests inject distinct sentinels into a search query, malformed summarizer output, and failed model, embedding-provider, and controller response bodies, then prove those sentinels are absent from logs, traces, metrics, audits, and returned errors. A test prompt that asks the Agent to ignore these rules must not bypass them.
-
Implementation work is cut only after human privacy approval. The approved sequence is expected to be:
- upstream or adapter capability: typed scope, consent-generation fencing, read-time expiry, per-entry delete, immutable expiry, cap, per-request disable, and content-free log/error sanitization;
- bridge consent/control UX and content-free evidence;
- one disabled-by-default sample Agent/profile with self-hosted embeddings and operation metering;
- deterministic negative/runtime proofs, including federation denial; and
-
153 offboarding integration.¶
These are design slices, not pre-authorized issues. The maintainer approves the privacy stance before implementation issues are created.
Consequences¶
- Fgentic does not claim that an Agent "remembers the user" until the user can see, bound, and erase what is retained. The competitive feature ships later, but with an auditable privacy contract rather than an irreversible default.
- The bridge owns the interaction and consent boundary without becoming a vector database or semantic-content custodian. kagent remains replaceable behind a narrow governed contract.
- Pinned kagent needs upstream work or a separately reviewed adapter before the proposal is implementable. That dependency is intentional: unfenced background writes, whole-store deletion, read-visible expired rows, popularity-extended TTL, and raw query/summary logging cannot be presented as reliable
off, exact "forget X", maximum retention, or content-free operations. - The first release does not provide cross-platform or federated personal memory. Those features require canonical identity and output-audience decisions that do not exist today.
- Inspection necessarily returns sensitive content to Matrix. Restricting it to a managed single-subject control room reduces exposure but does not make Matrix history erasable; the normal retention, backup, device-cache, and federation caveats still apply.
- The escape hatch is a self-contained sibling memory service with its own database, API, NetworkPolicy, and lifecycle. Choosing it requires a follow-up ADR and explicit path ownership; it is not an excuse to grow the bridge or bypass upstream governance.