Skip to content

#147: bind memory namespace into the signed cap service (approach B)#150

Merged
hanwencheng merged 6 commits into
mainfrom
claude/memory-signed-namespace
Jun 2, 2026
Merged

#147: bind memory namespace into the signed cap service (approach B)#150
hanwencheng merged 6 commits into
mainfrom
claude/memory-signed-namespace

Conversation

@hanwencheng
Copy link
Copy Markdown
Member

Summary

First real increment of #147 (the memory system), using approach B (chosen with the maintainer): fold the memory namespace into the signed service field of the cap-token (service = "memory:<namespace>") instead of adding a new signed cap claim.

Why approach B

The cap-token already signs service, and both the broker (isServiceInScope) and the memory worker (key derivation + AAD + scope re-check) already operate entirely off cap.payload.service. So making the memory service carry the namespace gives, for free:

  • Tamper-proofing — the namespace is in the signed payload; a memory:travel cap can't be edited into memory:personal.
  • Authorization — the existing on-chain isServiceInScope(operator, actor, keccak("memory:travel")) gate now authorizes the namespace. No new mechanism.
  • Storage segregation — the worker keys S3 off the service → bots/<actor>/memory/memory:travel.enc, physically distinct from memory:personal.
  • AAD binding — the envelope AAD already binds the service, so decryption is namespace-bound too.

No CapPayload change, no broker change, no byte-exact broker↔worker signature risk, no breaking cap-format. It also fixes a latent bug: today every namespace collides at the single memory.enc key (the worker ignored the body namespace).

Changes

  • crates/agentkeys-mcp-server/src/tools/memory.rsmemory.put/memory.get now mint the cap with service = "memory:<namespace>" (was a static "memory" + a redundant body field the worker ignored).
  • crates/agentkeys-worker-memory/src/handlers.rs — added a test proving namespace-folded services segregate storage (memory:travelmemory:personal key). No behavior change in the worker — it already keys/scopes/AADs off the signed service.

Verification

  • cargo test -p agentkeys-mcp-server — green (35 tests).
  • cargo test -p agentkeys-worker-memory — green (incl. the new segregation test).

Remaining for #147 (follow-up commits on this PR)

  1. Demo / scope grantharness/phase1-wire-demo.sh + the heima-scope-set step grant memory:travel (and seed in the travel namespace) instead of memory, so the --real flow authorizes the namespaced service. (--light is unaffected — the in-memory backend keys off the body namespace.)
  2. arch.md §15.2 / §17 — document that the memory service is memory:<namespace> (canonical-names + bucket-layout note).
  3. Optional — strip the redundant memory: prefix in the worker S3 key (bots/<actor>/memory/travel.enc) and a cross-namespace-denial integration test in the stage-3/wire harness.

Notes

…h B)

memory.put/get now mint the cap with service="memory:<namespace>" instead
of a static "memory". Because the broker signs `service` and the worker
already derives the S3 key, AAD, and on-chain scope check from
cap.payload.service, this makes the namespace:
  - tamper-proof (signed into the cap),
  - authorized via the existing isServiceInScope gate,
  - storage-segregated (bots/<actor>/memory/memory:<ns>.enc),
  - AAD-bound,
with NO CapPayload change, NO broker change, and no byte-exact
broker<->worker signature risk. Also fixes a latent bug where every
namespace collided at the single memory.enc key.

No worker behavior change (it already keys/scopes/AADs off the signed
service); added a test proving namespace-folded services segregate
storage.

Verified: cargo test -p agentkeys-mcp-server (35) + -p agentkeys-worker-memory green.
@hanwencheng hanwencheng force-pushed the claude/memory-signed-namespace branch from 733b929 to 8b57201 Compare June 2, 2026 08:14
Answers 'Hermes lists many memory providers — which to pick, how to stay
compatible' and folds the strategy into the source of truth.

plan/agentkeys-memory-design.md:
- New §6a 'Engine integration — Hermes providers + the adapter seam':
  - Reframe: the ~9 Hermes providers bundle engine+store+delivery; they
    slot into AgentKeys' pluggable ENGINE axis, not as peers.
  - Delivery stays at the pre_llm_call hook (#141), NOT the runtime
    memory.provider interface (its lifecycle step 6 hands the LLM
    memory-enumeration tools — breaks the no-whole-context invariant).
  - Canonical engine = OpenViking (self-hosted, deterministic, zero
    third-party egress); Holographic second; cloud providers = tier 3.
  - Adapter seam = one MemoryEngine trait (extract/rank/synthesize);
    compatibility = one conformance test with the engine swapped and
    store+gate+delivery held constant.
  - Two compatibility tiers, one gate: local=own-store+gate-read,
    cloud=gate-egress+audit.
- New engine stage E0 (the recommended start): MemoryEngine trait +
  OpenViking reference adapter + swap-the-engine conformance test.

arch.md:
- §22 pluggable surfaces: add 'Memory engine' axis row (fix stale
  'six'->'eight' axes count).
- §15.2 memory-service: document namespace = signed service
  'memory:<namespace>' (#147) and the pluggable-engine posture; add the
  previously-missing outward links to the plan + research docs.
…es (#147)

Starts implementing plan §6a / arch.md §22 'Memory engine' axis: the
caller-side, deterministic engine seam that ranks/selects gate-authorized
memory lines before injection (never in the worker, never an LLM in the gate).

crates/agentkeys-core/src/memory_engine.rs (new):
- MemoryEngine trait: select(query, lines, budget) -> lines.
- PassthroughEngine: identity when unbounded (today's full-blob inject);
  recency-trim when a budget is set.
- LexicalEngine: deterministic term-overlap ranking with a query (stopword
  filtered), recency fallback without one. Real reference engine, no LLM,
  no external service.
- select_blob(): the blob->blob seam contract — swapping the engine never
  changes the signature, only the selected subset (plan §6a.5 conformance).
- SelectionBudget + env config (AGENTKEYS_MEMORY_ENGINE / _MAX_LINES /
  _MAX_BYTES). 8 unit + conformance tests.

crates/agentkeys-cli/src/hook.rs:
- memory-inject (pre_llm_call) now runs the configured engine over each
  namespace blob. Default passthrough + unbounded budget = byte-identical
  to prior behavior (the Chengdu single-line fixture is unchanged).

OpenViking/Holographic adapters implement the SAME trait and are the next
adapters — deferred pending the API spike (plan §6a.3); not fabricated here.

Tests: cargo test -p agentkeys-core -p agentkeys-cli green (core 38 incl. 8
new engine tests; cli unchanged-green).
…orker (#147)

Task 2 — make phase1-wire-demo prove memory against the REAL provider, now
through the pluggable engine (plan §6a). The engine runs caller-side in the
wired pre_llm_call hook, so it must be baked at wire time (Hermes invokes the
hook in the Phase-4 chat, where the demo's env can't reach).

agentkeys wire (main.rs + wire.rs):
- New --memory-engine (passthrough|lexical, default passthrough) and
  --memory-max-lines flags. They bake AGENTKEYS_MEMORY_ENGINE /
  AGENTKEYS_MEMORY_MAX_LINES into the generated memory-inject script.
- Default passthrough + no budget emits NO engine env → the generated
  script is byte-identical to before (idempotency + existing tests hold).
- 2 new tests: omitted-by-default, and baked-when-set (env precedes exec).

harness/phase1-wire-demo.sh:
- MEMORY_ENGINE / MEMORY_MAX_LINES config (default passthrough = unchanged).
- Passes them to , so in --real the wired hook runs the
  engine over the REAL memory worker's lines.
- 3.1 now surfaces engine + source ('engine=lexical via REAL worker → …').
- bash -n clean.

docs/operator-runbook-wire.md: document the MEMORY_ENGINE / MEMORY_MAX_LINES
knobs in the env-override list (runbook-fix-fold-back).

Note: the full --real run needs the operator's live stack (broker + Heima +
WebAuthn + sandbox), not runnable from this worktree; verified via cargo test
(core+cli green) + bash -n. To SEE selection, run --real with a multi-line
SEED_MEMORY_CONTENT and MEMORY_ENGINE=lexical MEMORY_MAX_LINES=N.
harness/storage-test.sh — proves the memory STORAGE solution from a fresh
checkout with NO external infra (no AWS, no chain, no broker, no network):

  step 0 env+cache   resolves CARGO_TARGET_DIR/CARGO_HOME (build cache → fast
                     re-runs); unsets AGENTKEYS_BROKER_URL/_DATA_ROLE_ARN so
                     tests can never reach a live broker (the env leak that
                     makes cli provision tests hit prod).
  step 1 prereqs     cargo / jq / curl, fail-loud with install hints.
  step 2 build       cargo build CLI + MCP server; reports cache-hit vs compiled.
  step 3 suites      runs the REAL storage code paths — envelope AES-256-GCM
                     (encrypt-at-rest), per-actor S3 key derivation, namespace
                     isolation (#147), pluggable engine. cli uses --lib to skip
                     the env-dependent provision integration tests.
  step 4 roundtrip   starts an in-process in-memory MCP server and drives
                     put → get → inject end-to-end, plus an engine-selection
                     check (lexical + max_lines budget → 1 of N lines).

Idempotent: cargo no-op when unchanged; the MCP server is killed + restarted
fresh each run (ephemeral state). Verified: two consecutive runs both ALL GREEN,
2nd reports build cache-hit.

NOT a real-S3 proof (in-memory backend = plumbing only). For the authoritative
live-worker proof, harness/phase1-wire-demo.sh --real remains the path; the
header documents this.
…#147)

CI (harness-ci.yml + mcp-server.yml) failed on the new memory_engine.rs:
- cargo fmt --all -- --check: long test-call lines + env_usize chain needed
  rustfmt wrapping.
- cargo clippy --workspace -- -D warnings: clippy::unnecessary_sort_by on the
  PassthroughEngine recency sort →
  sort_by(|a,b| b.seq.cmp(&a.seq))  →  sort_by_key(|l| std::cmp::Reverse(l.seq)).

Verified locally against the exact CI commands:
- cargo fmt --all -- --check                              (exit 0)
- cargo clippy --workspace --all-targets -- -D warnings   (exit 0)
- cargo test --workspace -- --test-threads=1              (735 passed)
- cargo test -p agentkeys-mcp-server --all-features       (35 passed)
@hanwencheng hanwencheng merged commit 9ad8563 into main Jun 2, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant