From af92f3463efe02a963ad5e40315276dafe5f4976 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Thu, 8 Oct 2026 22:29:38 +0530 Subject: [PATCH 1/3] feat(namespace): add Reach::within to test reach containment Add a within method that reports whether one reach reads nothing an outer reach does not, so a host can confine a caller-supplied reach to the subtree it allows. It checks the anchor, inherited ancestors, and descendant coverage, with tests covering containment and escapes. Auto-committed-on: macbook Co-authored-by: Medulla --- crates/tinymemory-api/src/namespace/mod.rs | 28 ++++++++++++++ .../tinymemory-api/src/namespace/mod_tests.rs | 38 +++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/crates/tinymemory-api/src/namespace/mod.rs b/crates/tinymemory-api/src/namespace/mod.rs index 29ece082..37a4f7be 100644 --- a/crates/tinymemory-api/src/namespace/mod.rs +++ b/crates/tinymemory-api/src/namespace/mod.rs @@ -401,6 +401,34 @@ impl Reach { } } + /// Whether `self` reads nothing `outer` does not: every namespace `self` + /// admits, `outer` admits too. + /// + /// This is how a host confines a caller-supplied reach to the one it + /// allows (an agent's or an identity's subtree): a reach `within` the + /// allowed one may be used as given; any other would widen it. `at` must + /// be in `outer`; with `inherit`, so must every ancestor of `at` (a reach + /// inheriting above a subtree's top is not within it); with + /// `descendants`, `outer` must read descendants too and `at` must lie at + /// or below `outer.at` (the descendants of an ancestor of `outer.at` + /// include its siblings). + #[must_use] + pub fn within(&self, outer: &Self) -> bool { + if !outer.admits(&self.at) { + return false; + } + if self.inherit + && !self + .at + .ancestors_and_self() + .iter() + .all(|node| outer.admits(node)) + { + return false; + } + !self.descendants || (outer.descendants && self.at.is_within(&outer.at)) + } + /// The nodes read exactly, root first: `at` and, when `inherit`, its /// ancestors. Descendants are not enumerable here; an engine reads them /// as one subtree below `at`. diff --git a/crates/tinymemory-api/src/namespace/mod_tests.rs b/crates/tinymemory-api/src/namespace/mod_tests.rs index 8e1787fa..544d5c2f 100644 --- a/crates/tinymemory-api/src/namespace/mod_tests.rs +++ b/crates/tinymemory-api/src/namespace/mod_tests.rs @@ -191,3 +191,41 @@ fn refuses_a_child_past_the_depth_limit() { .unwrap_err(); assert!(matches!(error, Error::InvalidRequest(_)), "{error}"); } + +#[test] +fn a_reach_inside_a_subtree_is_within_it() { + let team = Reach::subtree(ns("team:acme")); + assert!(team.within(&team)); + assert!(Reach::subtree(ns("team:acme/agent:writer")).within(&team)); + assert!(Reach::exact(ns("team:acme/agent:writer")).within(&team)); + assert!(Reach::exact(ns("team:acme")).within(&team)); + // Everything is within the root's subtree, except a service sandbox. + let all = Reach::subtree(Namespace::ROOT); + assert!(Reach::of(ns("team:acme/agent:writer")).within(&all)); + assert!(Reach::subtree(ns("team:acme")).within(&all)); + assert!(!Reach::exact(ns("service:flows")).within(&all)); +} + +#[test] +fn a_reach_leaving_a_subtree_is_not_within_it() { + let team = Reach::subtree(ns("team:acme")); + // A sibling, the root, an ancestor's subtree. + assert!(!Reach::subtree(ns("team:other")).within(&team)); + assert!(!Reach::exact(Namespace::ROOT).within(&team)); + assert!(!Reach::subtree(Namespace::ROOT).within(&team)); + // Inheriting reads the root above the subtree's top. + assert!(!Reach::of(ns("team:acme/agent:writer")).within(&team)); + // A service sandbox below the subtree is not read by it. + assert!(!Reach::exact(ns("team:acme/service:flows")).within(&team)); +} + +#[test] +fn descendants_need_descendants_and_a_node_at_or_below() { + let agent = Reach::of(ns("team:acme/agent:writer")); + assert!(Reach::exact(ns("team:acme")).within(&agent)); + assert!(Reach::of(ns("team:acme")).within(&agent)); + // The team's subtree holds every other member's memory. + assert!(!Reach::subtree(ns("team:acme")).within(&agent)); + assert!(!Reach::subtree(ns("team:acme/agent:writer")).within(&agent)); + assert!(!Reach::exact(ns("team:acme/agent:editor")).within(&agent)); +} From e5652d33c670a5ba56c9415111d4b16143cb4634 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Thu, 8 Oct 2026 23:19:21 +0530 Subject: [PATCH 2/3] fix(namespace): treat a reach at max depth as exact A reach with `descendants` at the deepest legal node now counts as exact when tested with `within`, since no descendants can exist below it. The confinement rules are documented in the namespaces architecture note and the memory-v2 spec. Auto-committed-on: macbook Co-authored-by: Medulla --- crates/tinymemory-api/src/namespace/mod.rs | 6 ++++-- docs/architecture/namespaces.md | 11 +++++++++++ docs/specs/memory-v2.md | 2 +- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/crates/tinymemory-api/src/namespace/mod.rs b/crates/tinymemory-api/src/namespace/mod.rs index 37a4f7be..d06feb2b 100644 --- a/crates/tinymemory-api/src/namespace/mod.rs +++ b/crates/tinymemory-api/src/namespace/mod.rs @@ -411,7 +411,8 @@ impl Reach { /// inheriting above a subtree's top is not within it); with /// `descendants`, `outer` must read descendants too and `at` must lie at /// or below `outer.at` (the descendants of an ancestor of `outer.at` - /// include its siblings). + /// include its siblings). A reach at the deepest legal node reads no + /// descendants (none can exist), so it counts as exact. #[must_use] pub fn within(&self, outer: &Self) -> bool { if !outer.admits(&self.at) { @@ -426,7 +427,8 @@ impl Reach { { return false; } - !self.descendants || (outer.descendants && self.at.is_within(&outer.at)) + let reads_below = self.descendants && self.at.depth() < MAX_DEPTH; + !reads_below || (outer.descendants && self.at.is_within(&outer.at)) } /// The nodes read exactly, root first: `at` and, when `inherit`, its diff --git a/docs/architecture/namespaces.md b/docs/architecture/namespaces.md index 8db93722..de07b330 100644 --- a/docs/architecture/namespaces.md +++ b/docs/architecture/namespaces.md @@ -138,6 +138,17 @@ or `GetRequest::reach` left `None`) reads as `Reach::subtree(Namespace::ROOT)` | `Reach::subtree(at)` | no | yes | `at` and everything below it, no ancestors | | `Reach::default()` | yes | no | `Reach::of(root)`: **only the root** | +`Reach::within(outer)` is the confinement test a host applies to a +caller-supplied reach: true when every namespace `self` admits, `outer` +admits too (so `self` may be used as given; any other would widen `outer`). +`at` must be in `outer`; with `inherit`, every ancestor of `at` must be in +`outer` too (inheriting above a subtree's top escapes it); with +`descendants`, `outer` must read descendants and `at` must lie at or below +`outer.at`, because the descendants of an ancestor include its siblings. A +`service:` sandbox below `outer.at` stays out of `outer`'s descendants, as in +`admits`. A reach at the deepest legal node (depth 8) has no descendants, so +`descendants` there counts as exact. + `Reach::nodes()` lists the nodes read exactly, root first (`at` and, when `inherit`, its ancestors). Descendants cannot be enumerated from the reach; an engine reads them as one subtree below `at`. diff --git a/docs/specs/memory-v2.md b/docs/specs/memory-v2.md index 43f7255e..2c85cd08 100644 --- a/docs/specs/memory-v2.md +++ b/docs/specs/memory-v2.md @@ -154,7 +154,7 @@ listing can be ordered by it. An item's id is its `StoreItem::fingerprint()`. Memory is a tree of nodes (`Namespace`, written `team:acme/agent:writer`; the empty path is the root, written `root`). The root holds what every agent shares; each agent, sub-agent (nested under its spawner), team, user, workspace, project, knowledge source or service (one automation, such as a workflow) has its own node (`SegmentKind`, non-exhaustive). Segment ids are `[A-Za-z0-9_-]{1,128}`; `Segment::sanitized` maps any host id onto that charset without collisions; depth is at most 8. - **Placement.** `MemoryMeta.namespace` (default root, omitted on the wire when root) puts an item at one node, and is part of its fingerprint: the same text at two nodes is two items. Old envelopes read as root. -- **Reach.** `MetaFilter.reach: Option` confines every filtered read (recall, fetch, list, explore, forget by filter). `Reach { at, inherit, descendants }` admits `at`, its ancestors when `inherit` (the default, so an agent reads what its team and the root share), and everything below it when `descendants`. A sibling is never admitted, and a descendant read never enters a `service:` sandbox below `at`. `None` reads as `Reach::subtree(Namespace::ROOT)`: every node except a sandbox. +- **Reach.** `MetaFilter.reach: Option` confines every filtered read (recall, fetch, list, explore, forget by filter). `Reach { at, inherit, descendants }` admits `at`, its ancestors when `inherit` (the default, so an agent reads what its team and the root share), and everything below it when `descendants`. A sibling is never admitted, and a descendant read never enters a `service:` sandbox below `at`. `None` reads as `Reach::subtree(Namespace::ROOT)`: every node except a sandbox. `Reach::within(outer)` tests confinement: it is true when every namespace the reach admits, `outer` admits too (`at` in `outer`; every ancestor in `outer` when `inherit`; `outer.descendants` and `at` at or below `outer.at` when `descendants`, except at maximum depth where no descendant exists), so a host can accept a caller's reach only when it does not widen the one it allows. - **Get and forget by id.** `GetRequest.reach` leaves out ids beyond it. `ForgetTarget::Ids` is not scoped; a confined caller reads the ids with `get` and its reach first. - **Explore.** `Facet::Namespace` groups by node; narrowing a value reads exactly that node. - **Context.** `ContextSpec.reach` compiles a document from one node's reach. From 67f62b6e9d160cdfeacb4ff40b8e949d3d61c65e Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Thu, 8 Oct 2026 23:19:36 +0530 Subject: [PATCH 3/3] test(namespace): cover subtree reach at the depth limit Add a test asserting that a subtree rooted at the maximum namespace depth behaves as a singleton, since no node can exist below it, while a sibling node at the same depth still falls outside. Auto-committed-on: macbook Co-authored-by: Medulla --- crates/tinymemory-api/src/namespace/mod_tests.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/crates/tinymemory-api/src/namespace/mod_tests.rs b/crates/tinymemory-api/src/namespace/mod_tests.rs index 544d5c2f..0839931c 100644 --- a/crates/tinymemory-api/src/namespace/mod_tests.rs +++ b/crates/tinymemory-api/src/namespace/mod_tests.rs @@ -229,3 +229,13 @@ fn descendants_need_descendants_and_a_node_at_or_below() { assert!(!Reach::subtree(ns("team:acme/agent:writer")).within(&agent)); assert!(!Reach::exact(ns("team:acme/agent:editor")).within(&agent)); } + +#[test] +fn a_subtree_at_the_depth_limit_is_a_singleton() { + let deep = ns(&["agent:a"; MAX_DEPTH].join("/")); + // Nothing can exist below the deepest node, so its subtree is exact. + assert!(Reach::subtree(deep.clone()).within(&Reach::exact(deep.clone()))); + // Still confined: a different node of that depth is outside. + let other = ns(&["agent:b"; MAX_DEPTH].join("/")); + assert!(!Reach::subtree(other).within(&Reach::exact(deep))); +}