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:

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

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.