Story-1726 AgentCore Memory wiring — 4 sealed ledgers + forgetting
RFD-AGT-002 D-13. This Story wires Atlas's durable memory onto
AWS Bedrock AgentCore Memory (the v1 substrate D-13 locked — not mem0, not a
hand-rolled store). It is unblocked by the seal-proof Spike (#1725 / pramaan-infra PR #69), which
proved the per-matter namespace seal is service-enforced: a caller scoped to
matter A is denied at the AgentCore service when it asks for matter B's namespace, via the
bedrock-agentcore:namespace IAM condition key. Everything here lives under
agents/atlas/memory/ and ships behind a default-OFF flag — v1 wiring,
live enablement is a deploy concern.
The seal — namespace built only from authenticated identity
memory/scope.py is the one place that turns an authenticated identity
(firm_id / user_id / matter_id — from the broker invoke,
never a model output or browser field) into a namespace. It matches the IAM seal
exactly, including the records leaf the spike read live.
firms/{firm_id}/users/{user_id}/sessions/{session_id}/records session
firms/{firm_id}/users/{user_id}/preferences/records user-preference
firms/{firm_id}/style/records firm-style (firm-shared)
firms/{firm_id}/matters/{matter_id}/records matter
Per-matter isolation is the load-bearing property: matterA and matterB under the same firm/user
produce distinct, non-overlapping namespaces — neither a prefix of the other.
Segments carrying /, *, :, whitespace, or ..
are rejected so an id can never widen the boundary (defence-in-depth behind the service seal).
The four ledgers — read / write / forget
memory/ledgers.py exposes AtlasMemory over the boto3
bedrock-agentcore data-plane client (the one egress D-13 adds beyond inference; the
brain-egress CI gate does not ban it, so no allowlist widening). Each op takes a
MemoryScope and runs under its sealed namespace:
- recall →
RetrieveMemoryRecordsscoped by the sealed namespace. - remember →
CreateEvent(session · user-preference · matter). - forget →
DeleteMemoryRecord(correction / deletion / retention).
The other 3 of the 7 memory kinds are not here by design: page-state is ephemeral, product is the Product Catalog (D-2), and audit stays OUT of memory (D-13).
firm-style writes are explicit + audited (D-13 "Never")
The firm-style ledger is firm-shared: one member's correction changes what
every colleague reads next. So the generic remember() refuses
firm-style — callers must use remember_firm_style(), which requires a non-empty
author_user_id and an AuditSink. The audit event is emitted to the
tamper-evident log (pramaan-functions, ADR-124) carrying a content digest, never the raw
note — audit is never written into memory. No silent cross-member mutation.
Memory-poisoning defence at write (D-13 item 5, ties D-5)
Durable memory is a re-injection surface. memory/sanitize.py bounds size, strips
control characters, and wraps stored content in a <<memory:data>> DATA
fence so a planted "instruction" reads back as quoted data, never a command (RFD §8: injected =
DATA). This is the write-side floor; the read side treating recalled memory as data is the
matching control — both required.
Default-OFF + fail-closed
ATLAS_MEMORY_ENABLEDdefaultsfalse— byte-identical to no-memory.- Disabled reads return empty (a missing read is safe).
- Disabled writes / forgets fail LOUD (
AtlasMemoryDisabledError) — a mis-wired durable write can never silently vanish.
Verification
ruff clean on agents/; brain-egress gate clean (the
bedrock-agentcore client is not a banned egress); 30 new unit tests across namespace
isolation, sanitization, and the ledger read/write/forget/firm-style paths with the AgentCore
client mocked — no live AWS in unit tests. CI is the real gate.
Key finding carried from #1725: AgentCore reads via a Forward Access Session
under the caller's identity, so the reader principal needs kms:Decrypt +
kms:DescribeKey on the CMK alongside the namespace seal, or even an own-namespace
read fails "Missing permission for encryption key". That grant is infra (pramaan-infra), not code.