From e4a25947d6daa066b200bde4695e9b24fc71e51b Mon Sep 17 00:00:00 2001 From: "glm-5.2" Date: Fri, 26 Jun 2026 12:51:18 +0000 Subject: [PATCH] feat(call): remote_safe field on HandlerRegistration (ADR-028) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the v1 data shape for peer-scoped default-deny registry filtering, the one-way-door piece of the call-completion batch (ADR-028): - HandlerRegistration gains pub remote_safe: bool, defaulting false across all provenance (Local, Session, FromOpenAPI, FromMCP, FromCall, FromJsonSchema) per ADR-028 §4. HandlerRegistration::new() keeps its existing 6-arg signature (defaults remote_safe: false), so all current call sites compile unchanged. - Chainable HandlerRegistration::remote_safe(bool) setter + a OperationRegistryBuilder::remote_safe() helper that marks the most-recently-registered op (tracked via last_name, not HashMap iteration order which is unspecified). - Field is data-only here — the filtering behavior (dispatch path + services/list hide) is wired in call/client/call-client, not this task. services/list is unchanged. - Tests: default false, setter flips field, all six provenance variants default false, builder setter marks last op, existing call sites unchanged. 178 tests pass, clippy clean. Refs: tasks/call/registry/remote-safe-marking.md Refs: docs/architecture/decisions/028-callclient-peer-scoped-registry-filtering.md --- .../alknet-call/src/registry/registration.rs | 174 ++++++++++++++++-- tasks/call/registry/remote-safe-marking.md | 2 +- 2 files changed, 164 insertions(+), 12 deletions(-) diff --git a/crates/alknet-call/src/registry/registration.rs b/crates/alknet-call/src/registry/registration.rs index c6e00d5..92b391f 100644 --- a/crates/alknet-call/src/registry/registration.rs +++ b/crates/alknet-call/src/registry/registration.rs @@ -33,6 +33,12 @@ pub struct HandlerRegistration { pub composition_authority: Option, pub scoped_env: Option, pub capabilities: Capabilities, + /// Whether this op is exposed to `CallClient` peers. Defaults to `false` + /// across all provenance (ADR-028 §4) — default-deny. The `CallClient` + /// dispatch path filters on this field (trusted-peer mode bypasses it, + /// ADR-028 §3). Data-only here; the filtering behavior is wired in the + /// `CallClient` task, not here. + pub remote_safe: bool, } impl HandlerRegistration { @@ -51,8 +57,16 @@ impl HandlerRegistration { composition_authority, scoped_env, capabilities, + remote_safe: false, } } + + /// Chainable setter for `remote_safe` (ADR-028). Flips this op to be + /// exposed to `CallClient` peers in default-deny mode. + pub fn remote_safe(mut self, remote_safe: bool) -> Self { + self.remote_safe = remote_safe; + self + } } pub struct OperationRegistry { @@ -126,17 +140,26 @@ impl Default for OperationRegistry { pub struct OperationRegistryBuilder { operations: HashMap, + last_name: Option, } impl OperationRegistryBuilder { pub fn new() -> Self { Self { operations: HashMap::new(), + last_name: None, } } + fn store(mut self, registration: HandlerRegistration) -> Self { + self.last_name = Some(registration.spec.name.clone()); + self.operations + .insert(registration.spec.name.clone(), registration); + self + } + pub fn with_local( - mut self, + self, spec: OperationSpec, handler: Handler, composition_authority: Option, @@ -151,9 +174,7 @@ impl OperationRegistryBuilder { scoped_env, capabilities, ); - self.operations - .insert(registration.spec.name.clone(), registration); - self + self.store(registration) } pub fn with_leaf( @@ -179,15 +200,27 @@ impl OperationRegistryBuilder { ) -> Self { let registration = HandlerRegistration::new(spec, handler, provenance, None, None, capabilities); - let mut this = self; - this.operations - .insert(registration.spec.name.clone(), registration); - this + self.store(registration) } - pub fn with(mut self, registration: HandlerRegistration) -> Self { - self.operations - .insert(registration.spec.name.clone(), registration); + pub fn with(self, registration: HandlerRegistration) -> Self { + self.store(registration) + } + + /// Mark the most-recently-registered op as `remote_safe: true` (ADR-028). + /// Chainable right after a `with_local` / `with_leaf` / `with_leaf_provenance` + /// / `with` call. Panics if nothing has been registered yet (programming + /// error, not a runtime condition). + pub fn remote_safe(mut self) -> Self { + let name = self + .last_name + .clone() + .expect("remote_safe() called before any registration"); + let last = self + .operations + .get_mut(&name) + .expect("last-registered op must be present"); + last.remote_safe = true; self } @@ -731,4 +764,123 @@ mod tests { assert_eq!(reg.composition_authority.as_ref().unwrap().label, "agent"); assert!(reg.scoped_env.as_ref().unwrap().allows("fs/readFile")); } + + #[test] + fn handler_registration_default_remote_safe_is_false() { + let reg = HandlerRegistration::new( + external_spec("echo", AccessControl::default()), + echo_handler(), + OperationProvenance::Local, + None, + None, + Capabilities::new(), + ); + assert!(!reg.remote_safe, "remote_safe must default to false (ADR-028 §4)"); + } + + #[test] + fn handler_registration_remote_safe_setter_flips_field() { + let reg = HandlerRegistration::new( + external_spec("echo", AccessControl::default()), + echo_handler(), + OperationProvenance::Local, + None, + None, + Capabilities::new(), + ) + .remote_safe(true); + assert!(reg.remote_safe); + } + + #[test] + fn all_provenance_variants_default_remote_safe_false() { + for provenance in [ + OperationProvenance::Local, + OperationProvenance::Session, + OperationProvenance::FromOpenAPI, + OperationProvenance::FromMCP, + OperationProvenance::FromCall, + OperationProvenance::FromJsonSchema, + ] { + let reg = HandlerRegistration::new( + external_spec("op", AccessControl::default()), + echo_handler(), + provenance, + None, + None, + Capabilities::new(), + ); + assert!( + !reg.remote_safe, + "provenance {provenance:?} must default remote_safe to false (ADR-028 §4)" + ); + } + } + + #[test] + fn builder_remote_safe_marks_last_inserted() { + let registry = OperationRegistryBuilder::new() + .with_local( + external_spec("first", AccessControl::default()), + echo_handler(), + None, + None, + Capabilities::new(), + ) + .with_leaf( + external_spec("second", AccessControl::default()), + echo_handler(), + Capabilities::new(), + ) + .remote_safe() + .build(); + assert!( + !registry.registration("first").unwrap().remote_safe, + "first op untouched by remote_safe()" + ); + assert!( + registry.registration("second").unwrap().remote_safe, + "remote_safe() marks the most-recently-registered op" + ); + } + + #[test] + fn builder_existing_call_sites_compile_unchanged() { + // Verifies the field addition does not require changes to existing + // call-site shapes (defaults to false). + let registry = OperationRegistryBuilder::new() + .with_local( + external_spec("a", AccessControl::default()), + echo_handler(), + Some(CompositionAuthority::new("agent", ["fs:read".to_string()])), + Some(ScopedOperationEnv::new(["fs/readFile"])), + Capabilities::new(), + ) + .with_leaf( + external_spec("b", AccessControl::default()), + echo_handler(), + Capabilities::new(), + ) + .with_leaf_provenance( + external_spec("c", AccessControl::default()), + echo_handler(), + OperationProvenance::FromCall, + Capabilities::new(), + ) + .with(HandlerRegistration::new( + external_spec("d", AccessControl::default()), + echo_handler(), + OperationProvenance::Session, + None, + None, + Capabilities::new(), + )) + .build(); + for name in ["a", "b", "c", "d"] { + assert!( + !registry.registration(name).unwrap().remote_safe, + "{name} should default remote_safe false without explicit setter" + ); + } + } } diff --git a/tasks/call/registry/remote-safe-marking.md b/tasks/call/registry/remote-safe-marking.md index e483c8e..36038f2 100644 --- a/tasks/call/registry/remote-safe-marking.md +++ b/tasks/call/registry/remote-safe-marking.md @@ -1,7 +1,7 @@ --- id: call/registry/remote-safe-marking name: Add remote_safe field to HandlerRegistration for CallClient peer-scoped filtering (ADR-028) -status: pending +status: completed depends_on: [] scope: narrow risk: medium