security: close the read-side authorization gap on handoff and decay sweeps - #99
Merged
Merged
Conversation
…sweeps POST /memories/handoff took source_agent_id from the request body with no auth context, and get_agent_memories_for_handoff applied no scope filter, so any project key could pull another agent's memories - agent-private included - as key_facts in one call. The W1 sweep missed it because the source-derived wiring test keyed on MemoryRepository.add, and a handoff writes nothing. - handoffs: resolve the source through effective_agent_id (bound keys may only hand off their own memories; unbound keys orchestrate as the application) and apply read_scope_restriction as the principal-trust ceiling, mirroring the query routes. - decay: POST /memories/decay/archive archived project-wide with no agent authz; a bound key's sweep is now confined to its own agent's rows. - test_authz_bypass: new source-derived sweep over every router that touches MemoryRepository at all (not just writers), plus HTTP-layer bypass tests for both routes. Validated against pre-fix main (cf74ec2) in a scratch worktree: the three adversarial tests fail there and the positive controls pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ollection Unrelated to this PR's change: mcp 2.0.0 shipped upstream and the dependency was unpinned, so any CI run today fails collecting tests/test_mcp_server.py and tests/test_mcp_local_mode.py (ModuleNotFoundError: mcp.server.fastmcp). The last green main run (2026-07-20) resolved 1.x. Pin below the major; migrating aegis_memory/mcp_server.py to the 2.0 API is a separate task. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Owner
Author
|
CI failure was unrelated to the security change: Pushed |
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.
What
The W1 authorization pass (#95, #98) covered every route that writes memories. Two routes that only read or sweep them inherited nothing:
POST /memories/handofftooksource_agent_idfrom the request body with noAuthContext, andget_agent_memories_for_handoffapplied no scope filter — so any holder of a project key (including one bound to a different agent) could pull all of another agent's memories,agent-privateincluded, askey_factsin a single call. This is the same "agent-private is not private" hole W1 closed on the query routes, still open on one route.POST /memories/decay/archiveran project-wide with no agent authorization — a key bound to one agent could soft-deprecate every other agent's memories.Why the sweep missed it
test_every_router_that_writes_memories_screens_and_authorizesenumerates routers from the source — but keys onMemoryRepository.add(. A handoff writes nothing, so a pure read sink was invisible to it. Same shape as the ace_delta miss (#98), one level up.Fix
effective_agent_id: a bound key may only hand off its own memories (handoff is a push by the source, never a pull by an arbitrary key); an unbound key represents the application and may orchestrate, per the documented W1 posture.read_scope_restrictionis applied as the principal-trust ceiling, mirroring the query routes.agent_idfilter onarchive_stale; unbound keys still sweep the project.get_agent_memories_for_handoffgainsrequested_scope,archive_stalegainsagent_id. Both optional, both default to prior behavior for unbound callers.Tests
test_every_router_that_touches_the_repository_authorizes_an_identity: enumerates every router touchingMemoryRepositoryat all — read, write, or sweep — and requires amemory_authzhelper or the admin gate. A new read-only router now fails on the day it is added.TestHandoffHttpBypass/TestDecayArchiveScoping: real HTTP requests through the FastAPI stack with a repository canary, per theTestHttpLayerBypassmethodology.main(cf74ec2) in a scratch worktree: the three adversarial tests fail there (spoofed handoff pull reaches retrieval; bound-key sweep runs project-wide; sweep test flags both routers) and the three positive controls pass.🤖 Generated with Claude Code