diff --git a/crates/agentkeys-mcp-server/src/tools/memory.rs b/crates/agentkeys-mcp-server/src/tools/memory.rs index 91f735f5..dbf85385 100644 --- a/crates/agentkeys-mcp-server/src/tools/memory.rs +++ b/crates/agentkeys-mcp-server/src/tools/memory.rs @@ -63,11 +63,14 @@ pub async fn put( "device_key_hash", config.default_device_key_hash.as_deref(), )?; - let service = params - .get("service") - .and_then(|v| v.as_str()) - .unwrap_or("memory") - .to_string(); + // Issue #147 (approach B): fold the namespace into the SIGNED `service`, + // so the cap is cryptographically bound to exactly one namespace and + // authorized via the existing on-chain `isServiceInScope` check. A + // `memory:travel` cap cannot touch `memory:personal` — different service + // ⇒ different scope entry, different S3 key, different AAD. No CapPayload + // change, no broker change: the broker already signs whatever `service` + // it's given and the worker already keys storage + scope + AAD off it. + let service = format!("memory:{namespace}"); let ttl_seconds = params .get("ttl_seconds") .and_then(|v| v.as_u64()) @@ -140,11 +143,14 @@ pub async fn get( "device_key_hash", config.default_device_key_hash.as_deref(), )?; - let service = params - .get("service") - .and_then(|v| v.as_str()) - .unwrap_or("memory") - .to_string(); + // Issue #147 (approach B): fold the namespace into the SIGNED `service`, + // so the cap is cryptographically bound to exactly one namespace and + // authorized via the existing on-chain `isServiceInScope` check. A + // `memory:travel` cap cannot touch `memory:personal` — different service + // ⇒ different scope entry, different S3 key, different AAD. No CapPayload + // change, no broker change: the broker already signs whatever `service` + // it's given and the worker already keys storage + scope + AAD off it. + let service = format!("memory:{namespace}"); let ttl_seconds = params .get("ttl_seconds") .and_then(|v| v.as_u64()) diff --git a/crates/agentkeys-worker-memory/src/handlers.rs b/crates/agentkeys-worker-memory/src/handlers.rs index b11997b9..6fb5eb63 100644 --- a/crates/agentkeys-worker-memory/src/handlers.rs +++ b/crates/agentkeys-worker-memory/src/handlers.rs @@ -281,4 +281,19 @@ mod tests { fn s3_prefix_uses_memory_path() { assert_eq!(s3_prefix("0xABCDEF"), "bots/abcdef/memory/"); } + + #[test] + fn namespace_folded_service_segregates_storage() { + // Issue #147 (approach B): the MCP mints memory caps with + // service="memory:". Because the worker keys S3 off the + // SIGNED service, two namespaces land at distinct keys — a + // `memory:travel` cap physically cannot read/write the + // `memory:personal` object. This is the namespace-isolation gate, + // enforced by construction (signed service ⇒ key + scope + AAD). + let travel = s3_key("0xabc", "memory:travel"); + let personal = s3_key("0xabc", "memory:personal"); + assert_ne!(travel, personal); + assert_eq!(travel, "bots/abc/memory/memory:travel.enc"); + assert!(personal.contains("memory:personal")); + } }