Repository navigation
fix(mcp): the server decides whose memory and what provenance; untrusted writes are quarantined - #62
Merged
Merged
Conversation
…against main Thirteen scenarios in features/memory-scope-and-provenance.feature, one test each in tests/test_memory_scope.py. All thirteen fail on origin/main c8708c5: the scope, user-claim, external-default and fact-deletion tests on the defect itself (DID NOT RAISE, 'tool' == 'external'), the rest because the quarantine mark, the sharing opt-in and the server settings do not exist yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…untrusted writes (F-007) engram-mcp took the agent scope and the trust label of every write from the tool call: any client could read or write another agent's memory by naming its agent_id, label any text provenance="user", and delete any fact, since facts were shared by every agent and forget checked only the id. - The scope is the identity the server is started with (--agent-id). A call naming another agent_id is refused with PermissionError, never served from that scope; naming its own, or nothing, is served. A server with no identity serves the whole file as one tenant, warns, and refuses any named agent. The pool now holds one Engram instance. - The server sets provenance: --provenance / ENGRAM_MCP_PROVENANCE (default external, since an MCP write is an agent's own output) is the ceiling; a caller may lower it to tool or external, and "user" is refused. - Quarantine: the session turns untrusted when recall or why returns a memory that is not the user's own or is quarantined, when the caller writes content it labels tool or external or passes read_untrusted, or from the start with --session-untrusted. Later writes are stored quarantined, never as user, and recall, why, library results and the adapters return the mark. compress() keeps it on a summary of quarantined episodes. - Facts record their writer and whether they are shared. remember(kind= "semantic") keeps a fact private unless shared=true; assert_fact keeps its shared default. Scoped fact reads leave out other agents' private facts, and forget_fact deletes only the instance's own (KeyError for an invisible fact, PermissionError for a visible one); an unscoped instance may delete any. - Migration: four nullable columns, no default, nothing rewritten. Rows written before them read as the least trusted (episode quarantined with provenance unknown; fact quarantined, visible to all, deletable by no agent). Export and import carry the new fields. Invariant 12 and an approved decision in CLAUDE.md; 14 scenarios in features/memory-scope-and-provenance.feature bound to tests by the new gate scripts/features-are-bound.sh, with four teeth cases, wired into CI and the release workflow. Tests that asserted the old behaviour (per-call agent_id on one pool, a caller-claimed user provenance) now open one server per agent and take the source from the server's configuration. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ed by every agent Facts can now be private to their writer, so 'shared by every agent' no longer describes them; the count still covers every agent's facts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d quarantined why() already read the mark off the source episodes; storing it on the fact makes timeline(), export and a later reader see it too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of the agent-security programme (
@decided 2026-10-17: close the review's findings). Memory poisoning is the persistence layer 2026 attacks use, and the server took the scope and the provenance from the caller.--agent-id); a call naming anotheragent_idis refused, never served.--provenance/ENGRAM_MCP_PROVENANCE, defaultexternal); a caller may only lower it (tool,external) and can never claimuser.read_untrusted, or--session-untrusted), its writes are stored quarantined and recall returns that mark.forgetand fact deletion are limited to the caller's own scope; a shared fact needs an explicit opt-in at write time. Existing rows migrate to a conservative provenance.Breaking: a client that passes another agent's
agent_idis refused. The console (genaryx) spawns the server with no--agent-idand passesagent_idper call, so it must change first: do not merge before the genaryx connector change lands, because the console image takes engram from main at its next build.Not proven: content the agent read outside engram is not seen unless the caller or launcher labels the session; identity is configuration, not a verified token (wave 1 later).
🤖 Generated with Claude Code