From 4490bc251fe77bb6085e6c595b6b49bd1c604fbd Mon Sep 17 00:00:00 2001 From: "glm-5.2" Date: Sun, 28 Jun 2026 21:52:40 +0000 Subject: [PATCH] feat(call): retire remote_safe/trusted_peer/RemoteFilter (call/retire-remote-safe) --- crates/alknet-call/src/client/call_client.rs | 183 ++---------------- crates/alknet-call/src/client/from_call.rs | 9 +- .../alknet-call/src/client/from_jsonschema.rs | 10 +- crates/alknet-call/src/protocol/adapter.rs | 9 +- crates/alknet-call/src/protocol/dispatch.rs | 57 +----- crates/alknet-call/src/registry/discovery.rs | 130 ------------- .../alknet-call/src/registry/registration.rs | 168 ---------------- crates/alknet-call/tests/two_node_call.rs | 84 ++++---- 8 files changed, 66 insertions(+), 584 deletions(-) diff --git a/crates/alknet-call/src/client/call_client.rs b/crates/alknet-call/src/client/call_client.rs index e3dbd20..82f8db5 100644 --- a/crates/alknet-call/src/client/call_client.rs +++ b/crates/alknet-call/src/client/call_client.rs @@ -1,4 +1,4 @@ -//! `CallClient`: the outbound connection opener (ADR-017 §1, ADR-028). +//! `CallClient`: the outbound connection opener (ADR-017 §1). //! //! Opens a QUIC connection to a remote node on ALPN `alknet/call`, performs //! credential setup, and produces a [`CallConnection`] running the shared @@ -10,8 +10,7 @@ //! After establishment the connection is symmetric (ADR-017 §2): both sides //! can send and receive `call.requested`. The `CallClient` is both a caller //! (initiates outgoing calls via `CallConnection::call()`/`subscribe()`/ -//! `abort()`) and a callee (dispatches incoming calls against its -//! peer-scoped view of the registry). +//! `abort()`) and a callee (dispatches incoming calls against its registry). //! //! See `docs/architecture/crates/call/client-and-adapters.md` for the spec. @@ -23,7 +22,7 @@ use alknet_core::config::TlsIdentity; use alknet_core::types::Connection; use crate::protocol::connection::CallConnection; -use crate::protocol::dispatch::{Dispatcher, RemoteFilter}; +use crate::protocol::dispatch::Dispatcher; use crate::registry::registration::OperationRegistry; /// Expected identity of the remote node (ADR-017 §7). The concrete shape is @@ -87,21 +86,15 @@ pub enum ClientError { /// Outbound `alknet/call` connection opener (the #1 gap, ADR-017 §1). /// -/// The peer-scoped registry view is a dispatch-time read over the single -/// Layer-0 registry (ADR-028 §5) — not a copy. In default mode -/// (`trusted_peer: false`) only registrations with `remote_safe: true` -/// dispatch to the remote peer, and `services/list` hides non-remote-safe -/// ops (ADR-028 Assumption 2). In trusted-peer mode (`trusted_peer: true`, -/// explicit opt-in per ADR-028 §3) all `External` ops dispatch and list. +/// Peer authorization flows through the existing `AccessControl::check` gate +/// in `OperationRegistry::invoke` (ADR-029 §3) — no parallel `remote_safe`/ +/// `trusted_peer` gate. pub struct CallClient { registry: Arc, identity_provider: Arc, - trusted_peer: bool, } impl CallClient { - /// Default-deny mode: only `remote_safe: true` ops dispatch/list to the - /// remote peer (ADR-028). pub fn new( registry: Arc, identity_provider: Arc, @@ -109,20 +102,6 @@ impl CallClient { Self { registry, identity_provider, - trusted_peer: false, - } - } - - /// Trusted-peer mode: expose all `External` ops to the remote peer, - /// ignoring the `remote_safe` marking. Explicit opt-in per ADR-028 §3. - pub fn trusted_peer( - registry: Arc, - identity_provider: Arc, - ) -> Self { - Self { - registry, - identity_provider, - trusted_peer: true, } } @@ -134,10 +113,6 @@ impl CallClient { &self.identity_provider } - pub fn is_trusted_peer(&self) -> bool { - self.trusted_peer - } - /// Open a QUIC connection to `addr` on ALPN `alknet/call`, perform /// credential handshake, and return a `CallConnection` running the shared /// dispatch loop. Credentials come from `Capabilities` (ADR-014), not env @@ -147,8 +122,7 @@ impl CallClient { /// is live until the remote closes the connection or the caller drops it. /// The caller can immediately use `call()`/`subscribe()`/`abort()` on the /// returned connection, and the remote peer can call back into this - /// `CallClient`'s peer-scoped registry view (connection symmetry, - /// ADR-017 §2). + /// `CallClient`'s registry (connection symmetry, ADR-017 §2). #[cfg(feature = "quinn")] pub async fn connect( &self, @@ -188,11 +162,6 @@ impl CallClient { let dispatcher = Dispatcher::new( Arc::clone(&self.registry), Arc::clone(&self.identity_provider), - if self.trusted_peer { - RemoteFilter::trusted() - } else { - RemoteFilter::default_deny() - }, ); let run_conn = Arc::clone(&call_connection); tokio::spawn(async move { @@ -297,7 +266,6 @@ impl rustls::client::danger::ServerCertVerifier for AcceptAnyServerCertVerifier mod tests { use super::*; use crate::protocol::connection::CallConnection; - use crate::protocol::dispatch::{Dispatcher, RemoteFilter}; use crate::protocol::wire::ResponseEnvelope; use crate::registry::registration::{ make_handler, Handler, HandlerRegistration, OperationProvenance, @@ -366,42 +334,21 @@ mod tests { } } - fn registry_with_remote_safe_and_caps() -> Arc { + fn registry_with_caps() -> Arc { let mut registry = OperationRegistry::new(); - // remote_safe: false, carries a google api-key capability registry.register(HandlerRegistration::new( - external_spec("secret/run"), + external_spec("pub/run"), caps_inspect_handler(), OperationProvenance::Local, None, None, - Capabilities::new().with_api_key("google", "secret-key".to_string()), + Capabilities::new().with_api_key("google", "pub-key".to_string()), )); - // remote_safe: true, carries a google api-key capability - registry.register( - HandlerRegistration::new( - external_spec("pub/run"), - caps_inspect_handler(), - OperationProvenance::Local, - None, - None, - Capabilities::new().with_api_key("google", "pub-key".to_string()), - ) - .remote_safe(true), - ); Arc::new(registry) } - fn dispatcher(registry: &Arc, trusted_peer: bool) -> Dispatcher { - Dispatcher::new( - Arc::clone(registry), - Arc::new(NoopIdentityProvider), - if trusted_peer { - RemoteFilter::trusted() - } else { - RemoteFilter::default_deny() - }, - ) + fn dispatcher(registry: &Arc) -> Dispatcher { + Dispatcher::new(Arc::clone(registry), Arc::new(NoopIdentityProvider)) } async fn dispatch(d: &Dispatcher, conn: &Arc, op: &str) -> ResponseEnvelope { @@ -413,24 +360,6 @@ mod tests { .await } - #[test] - fn call_client_new_is_default_deny() { - let registry = Arc::new(OperationRegistry::new()); - let client = CallClient::new(Arc::clone(®istry), Arc::new(NoopIdentityProvider)); - assert!(!client.is_trusted_peer(), "new() is default-deny"); - } - - #[test] - fn call_client_trusted_peer_is_trusted() { - let registry = Arc::new(OperationRegistry::new()); - let client = - CallClient::trusted_peer(Arc::clone(®istry), Arc::new(NoopIdentityProvider)); - assert!( - client.is_trusted_peer(), - "trusted_peer() is trusted-peer mode" - ); - } - #[test] fn call_credentials_builder_methods() { let creds = CallCredentials::new().with_remote_identity(RemoteIdentity { @@ -445,94 +374,23 @@ mod tests { } #[tokio::test] - async fn default_deny_non_remote_safe_op_returns_not_found() { - let registry = registry_with_remote_safe_and_caps(); - let d = dispatcher(®istry, false); - let conn = Arc::new(CallConnection::new(stub_connection())); - let response = dispatch(&d, &conn, "secret/run").await; - match response.result { - Err(e) => assert_eq!(e.code, "NOT_FOUND"), - other => panic!("expected NOT_FOUND for non-remote-safe op, got {other:?}"), - } - } - - #[tokio::test] - async fn default_deny_remote_safe_op_dispatches() { - let registry = registry_with_remote_safe_and_caps(); - let d = dispatcher(®istry, false); - let conn = Arc::new(CallConnection::new(stub_connection())); - let response = dispatch(&d, &conn, "pub/run").await; - assert!( - response.result.is_ok(), - "remote_safe op must dispatch in default-deny mode" - ); - } - - #[tokio::test] - async fn trusted_peer_dispatches_non_remote_safe_op() { - let registry = registry_with_remote_safe_and_caps(); - let d = dispatcher(®istry, true); - let conn = Arc::new(CallConnection::new(stub_connection())); - let response = dispatch(&d, &conn, "secret/run").await; - assert!( - response.result.is_ok(), - "trusted-peer mode dispatches non-remote-safe ops" - ); - } - - /// The load-bearing security invariant (ADR-028 Context): a remote - /// peer's call to a non-remote-safe op must NOT populate - /// `OperationContext.capabilities` from the local registration bundle. - /// This test asserts the handler is never reached for non-remote-safe - /// ops in default-deny mode (NOT_FOUND before dispatch), so capabilities - /// are never populated — verified by the handler not running. - #[tokio::test] - async fn default_deny_non_remote_safe_does_not_populate_capabilities() { - let registry = registry_with_remote_safe_and_caps(); - let d = dispatcher(®istry, false); - let conn = Arc::new(CallConnection::new(stub_connection())); - let response = dispatch(&d, &conn, "secret/run").await; - match response.result { - Err(e) => assert_eq!(e.code, "NOT_FOUND"), - Ok(_) => panic!("non-remote-safe op must not dispatch (would populate capabilities)"), - } - } - - /// A remote-safe op's call DOES populate capabilities (the security - /// argument is about *non-remote-safe* ops, not all ops). The handler - /// inspects capabilities and reports whether the google key was injected. - #[tokio::test] - async fn remote_safe_op_populates_capabilities_for_handler() { - let registry = registry_with_remote_safe_and_caps(); - let d = dispatcher(®istry, false); + async fn external_op_dispatches_and_populates_capabilities() { + let registry = registry_with_caps(); + let d = dispatcher(®istry); let conn = Arc::new(CallConnection::new(stub_connection())); let response = dispatch(&d, &conn, "pub/run").await; let out = response.result.expect("ok"); assert_eq!( out["has_google_capability"], serde_json::json!(true), - "remote_safe op must have its capabilities populated" + "an External op's call must populate capabilities for the handler" ); } #[tokio::test] - async fn trusted_peer_populates_capabilities_for_non_remote_safe() { - let registry = registry_with_remote_safe_and_caps(); - let d = dispatcher(®istry, true); - let conn = Arc::new(CallConnection::new(stub_connection())); - let response = dispatch(&d, &conn, "secret/run").await; - let out = response.result.expect("ok"); - assert_eq!( - out["has_google_capability"], - serde_json::json!(true), - "trusted-peer mode populates capabilities for all External ops" - ); - } - - #[tokio::test] - async fn default_deny_unknown_op_returns_not_found() { + async fn unknown_op_returns_not_found() { let registry = Arc::new(OperationRegistry::new()); - let d = dispatcher(®istry, false); + let d = dispatcher(®istry); let conn = Arc::new(CallConnection::new(stub_connection())); let response = dispatch(&d, &conn, "no/such").await; match response.result { @@ -543,13 +401,10 @@ mod tests { #[tokio::test] async fn spawn_dispatch_returns_live_call_connection() { - let registry = registry_with_remote_safe_and_caps(); + let registry = registry_with_caps(); let client = CallClient::new(Arc::clone(®istry), Arc::new(NoopIdentityProvider)); let conn = client.spawn_dispatch(stub_connection()); - // The returned CallConnection is usable: it has an empty overlay and - // the underlying connection reports the alknet/call ALPN. assert_eq!(conn.connection().remote_alpn(), b"alknet/call"); - // The dispatch task is spawned; dropping the connection closes it. std::mem::drop(conn); } diff --git a/crates/alknet-call/src/client/from_call.rs b/crates/alknet-call/src/client/from_call.rs index 449f724..9fe34d6 100644 --- a/crates/alknet-call/src/client/from_call.rs +++ b/crates/alknet-call/src/client/from_call.rs @@ -441,9 +441,8 @@ mod tests { #[test] fn from_call_provenance_is_from_call_and_leaf_fields() { // Verify the registration shape produced by from_call: provenance - // FromCall, no composition authority, no scoped_env, empty caps, - // remote_safe false (default). Uses a synthetic spec to avoid the - // transport round-trip. + // FromCall, no composition authority, no scoped_env, empty caps. + // Uses a synthetic spec to avoid the transport round-trip. let spec = OperationSpec::new( "worker/echo", OperationType::Query, @@ -468,9 +467,5 @@ mod tests { assert_eq!(reg.provenance, OperationProvenance::FromCall); assert!(reg.composition_authority.is_none()); assert!(reg.scoped_env.is_none()); - assert!( - !reg.remote_safe, - "FromCall leaves default remote_safe false (ADR-028 §4)" - ); } } diff --git a/crates/alknet-call/src/client/from_jsonschema.rs b/crates/alknet-call/src/client/from_jsonschema.rs index ce3ad02..2c5bae6 100644 --- a/crates/alknet-call/src/client/from_jsonschema.rs +++ b/crates/alknet-call/src/client/from_jsonschema.rs @@ -17,11 +17,10 @@ use crate::registry::spec::OperationSpec; /// Build a [`HandlerRegistration`] from a JSON Schema-described operation. /// /// Schema-only: no real handler is attached — a placeholder returns a -/// `NOT_FOUND`-style error if ever invoked (schema-only ops are `Internal` -/// and `remote_safe: false`, so dispatch should never reach them; the -/// placeholder fails loudly on bugs). `provenance` is `FromJsonSchema`; -/// `composition_authority` and `scoped_env` are `None`; `capabilities` is -/// empty. +/// `NOT_FOUND`-style error if ever invoked (schema-only ops are `Internal`, +/// so dispatch should never reach them; the placeholder fails loudly on +/// bugs). `provenance` is `FromJsonSchema`; `composition_authority` and +/// `scoped_env` are `None`; `capabilities` is empty. pub fn from_jsonschema(spec: OperationSpec, _schema: Value) -> HandlerRegistration { let handler = make_handler(|_input: Value, context: OperationContext| async move { ResponseEnvelope::error( @@ -132,7 +131,6 @@ mod tests { assert_eq!(bundle.provenance, OperationProvenance::FromJsonSchema); assert!(bundle.composition_authority.is_none()); assert!(bundle.scoped_env.is_none()); - assert!(!bundle.remote_safe); } #[tokio::test] diff --git a/crates/alknet-call/src/protocol/adapter.rs b/crates/alknet-call/src/protocol/adapter.rs index 6cb6b0b..d57c6c3 100644 --- a/crates/alknet-call/src/protocol/adapter.rs +++ b/crates/alknet-call/src/protocol/adapter.rs @@ -18,12 +18,10 @@ use alknet_core::types::{Connection, HandlerError, ProtocolHandler}; use async_trait::async_trait; use super::connection::CallConnection; -use super::dispatch::{Dispatcher, RemoteFilter}; +use super::dispatch::Dispatcher; use crate::registry::context::OperationContext; use crate::registry::registration::OperationRegistry; -pub use super::dispatch::RemoteFilter as AdapterRemoteFilter; - #[cfg(test)] use super::wire::ResponseEnvelope; #[cfg(test)] @@ -47,11 +45,8 @@ impl CallAdapter { registry: Arc, identity_provider: Arc, ) -> Self { - // The accept path is not peer-scoped-filtered: a direct QUIC client is - // not a CallClient peer in the ADR-028 filtered sense, so the accept - // path lists/dispatches all External ops (trusted-peer posture). Self { - dispatcher: Dispatcher::new(registry, identity_provider, RemoteFilter::trusted()), + dispatcher: Dispatcher::new(registry, identity_provider), } } diff --git a/crates/alknet-call/src/protocol/dispatch.rs b/crates/alknet-call/src/protocol/dispatch.rs index 7c25077..ecea0cb 100644 --- a/crates/alknet-call/src/protocol/dispatch.rs +++ b/crates/alknet-call/src/protocol/dispatch.rs @@ -4,9 +4,9 @@ //! connect path produce a [`CallConnection`] and hand it to the same dispatch //! loop here (ADR-017 §1): the loop reads `EventEnvelope` frames off accepted //! bidirectional streams, dispatches `call.requested` events against the -//! operation registry (with optional peer-scoped filtering per ADR-028), and -//! writes the response back on the same stream. The connection-establishment -//! half differs (accept vs dial); the dispatch half is shared. +//! operation registry, and writes the response back on the same stream. The +//! connection-establishment half differs (accept vs dial); the dispatch half +//! is shared. //! //! See `docs/architecture/crates/call/call-protocol.md` and //! `docs/architecture/crates/call/client-and-adapters.md` for the spec. @@ -35,40 +35,6 @@ use crate::registry::registration::OperationRegistry; const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30); const SWEEPER_INTERVAL: Duration = Duration::from_secs(10); -/// Peer-scoped registry filter state (ADR-028). When `trusted_peer` is false -/// (the default-deny mode for a `CallClient`), incoming dispatch hides ops -/// whose `HandlerRegistration.remote_safe` is false, and `services/list` hides -/// them too. When `trusted_peer` is true (the explicit opt-in for trusted -/// peers), the filter is bypassed: all `External` ops dispatch and list. -/// -/// For the `CallAdapter` (local accept path), `trusted_peer` is `true` by -/// convention — a direct QUIC client is not a filtered `CallClient` peer in -/// the ADR-028 sense; the accept path keeps listing all `External` ops. -#[derive(Clone, Copy)] -pub struct RemoteFilter { - pub trusted_peer: bool, -} - -impl RemoteFilter { - /// Default-deny mode: only `remote_safe: true` ops dispatch/list. - pub fn default_deny() -> Self { - Self { - trusted_peer: false, - } - } - - /// Trusted-peer mode: all `External` ops dispatch/list regardless of - /// `remote_safe`. - pub fn trusted() -> Self { - Self { trusted_peer: true } - } - - /// Returns whether `registration` is dispatchable to the remote peer. - pub fn allows(&self, remote_safe: bool) -> bool { - self.trusted_peer || remote_safe - } -} - /// Shared dispatcher for an established `CallConnection`. Constructed by /// both `CallAdapter` (accept path) and `CallClient` (connect path) and used /// to run the dispatch loop. Holds no per-connection state; the @@ -78,21 +44,18 @@ pub struct Dispatcher { pub identity_provider: Arc, pub session_source: Option>, pub default_timeout: Duration, - pub remote_filter: RemoteFilter, } impl Dispatcher { pub fn new( registry: Arc, identity_provider: Arc, - remote_filter: RemoteFilter, ) -> Self { Self { registry, identity_provider, session_source: None, default_timeout: DEFAULT_TIMEOUT, - remote_filter, } } @@ -206,19 +169,6 @@ impl Dispatcher { .unwrap_or(""); let operation_name = Self::strip_leading_slash(operation_id).to_string(); - // Peer-scoped default-deny filter (ADR-028). When the caller is a - // remote peer (default-deny mode), an op marked `remote_safe: false` - // is hidden from dispatch — return NOT_FOUND, same posture as - // `Visibility::Internal` per ADR-015. Critically, this returns *before* - // any capability material reaches the handler, so a non-remote-safe - // op's `Capabilities` are never populated for a remote peer's call - // (ADR-028 Context — the security argument for default-deny). - if let Some(registration) = self.registry.registration(&operation_name) { - if !self.remote_filter.allows(registration.remote_safe) { - return ResponseEnvelope::not_found(request_id, &operation_name); - } - } - let connection_identity = connection.connection().identity().cloned(); let identity = self.resolve_identity(connection_identity, &payload); @@ -345,7 +295,6 @@ impl Clone for Dispatcher { identity_provider: Arc::clone(&self.identity_provider), session_source: self.session_source.clone(), default_timeout: self.default_timeout, - remote_filter: self.remote_filter, } } } diff --git a/crates/alknet-call/src/registry/discovery.rs b/crates/alknet-call/src/registry/discovery.rs index c4f4626..e8b25da 100644 --- a/crates/alknet-call/src/registry/discovery.rs +++ b/crates/alknet-call/src/registry/discovery.rs @@ -193,36 +193,6 @@ pub fn services_list_handler(registry: Arc) -> Handler { }) } -/// Peer-scoped `services/list` handler (ADR-028 Assumption 2). When -/// `trusted_peer` is false (default-deny mode for a `CallClient`), ops with -/// `remote_safe: false` are hidden from the remote peer in addition to the -/// existing `Visibility::External` filter — a peer should not see ops it -/// cannot call, so discovery and dispatch filters agree. When `trusted_peer` -/// is true, all `External` ops are listed regardless of `remote_safe`. -pub fn services_list_handler_peer_scoped( - registry: Arc, - trusted_peer: bool, -) -> Handler { - Arc::new(move |input: Value, ctx: OperationContext| { - let registry = Arc::clone(®istry); - Box::pin(async move { - let _ = input; - let ops: Vec = registry - .list_operations_peer_scoped(trusted_peer) - .into_iter() - .map(|s| { - json!({ - "name": s.name, - "namespace": s.namespace, - "op_type": op_type_str(s.op_type), - }) - }) - .collect(); - ResponseEnvelope::ok(ctx.request_id, json!({ "operations": ops })) - }) - }) -} - pub fn services_schema_handler(registry: Arc) -> Handler { Arc::new(move |input: Value, ctx: OperationContext| { let registry = Arc::clone(®istry); @@ -535,106 +505,6 @@ mod tests { assert!(output.get("operations").is_some()); } - fn registry_with_remote_safe_ops() -> Arc { - let mut registry = OperationRegistry::new(); - registry.register(HandlerRegistration::new( - external_spec("fs/readFile"), - echo_handler(), - OperationProvenance::Local, - None, - None, - Capabilities::new(), - )); - // remote_safe: false (default) - registry.register(HandlerRegistration::new( - external_spec("admin/run"), - echo_handler(), - OperationProvenance::Local, - None, - None, - Capabilities::new(), - )); - // remote_safe: true - registry.register( - HandlerRegistration::new( - external_spec("pub/status"), - echo_handler(), - OperationProvenance::Local, - None, - None, - Capabilities::new(), - ) - .remote_safe(true), - ); - Arc::new(registry) - } - - #[tokio::test] - async fn services_list_peer_scoped_default_deny_hides_non_remote_safe() { - let registry = registry_with_remote_safe_ops(); - let handler = services_list_handler_peer_scoped(Arc::clone(®istry), false); - let ctx = root_context("req-ps1"); - let response = handler(serde_json::json!({}), ctx).await; - let output = response.result.expect("ok"); - let ops = output - .get("operations") - .and_then(|v| v.as_array()) - .expect("operations array"); - let names: Vec<&str> = ops - .iter() - .filter_map(|o| o.get("name").and_then(|n| n.as_str())) - .collect(); - assert!( - names.contains(&"pub/status"), - "remote_safe ops must be listed in default-deny mode" - ); - assert!( - !names.contains(&"fs/readFile"), - "non-remote-safe ops must be hidden in default-deny mode (ADR-028 Assumption 2)" - ); - assert!( - !names.contains(&"admin/run"), - "non-remote-safe ops must be hidden in default-deny mode" - ); - } - - #[tokio::test] - async fn services_list_peer_scoped_trusted_peer_lists_all_external() { - let registry = registry_with_remote_safe_ops(); - let handler = services_list_handler_peer_scoped(Arc::clone(®istry), true); - let ctx = root_context("req-ps2"); - let response = handler(serde_json::json!({}), ctx).await; - let output = response.result.expect("ok"); - let ops = output - .get("operations") - .and_then(|v| v.as_array()) - .expect("operations array"); - let names: Vec<&str> = ops - .iter() - .filter_map(|o| o.get("name").and_then(|n| n.as_str())) - .collect(); - assert!(names.contains(&"fs/readFile")); - assert!(names.contains(&"admin/run")); - assert!(names.contains(&"pub/status")); - } - - #[tokio::test] - async fn services_list_peer_scoped_default_deny_with_no_remote_safe_returns_empty() { - let registry = registry_with_ops(); // no remote_safe ops - let handler = services_list_handler_peer_scoped(Arc::clone(®istry), false); - let ctx = root_context("req-ps3"); - let response = handler(serde_json::json!({}), ctx).await; - let output = response.result.expect("ok"); - let ops = output - .get("operations") - .and_then(|v| v.as_array()) - .expect("operations array"); - assert!( - ops.is_empty(), - "default-deny with no remote_safe ops lists nothing" - ); - } - #[test] fn normalize_name_strips_leading_slash() { assert_eq!(normalize_name("/fs/readFile"), "fs/readFile"); diff --git a/crates/alknet-call/src/registry/registration.rs b/crates/alknet-call/src/registry/registration.rs index e35fb5a..c045c1d 100644 --- a/crates/alknet-call/src/registry/registration.rs +++ b/crates/alknet-call/src/registry/registration.rs @@ -33,12 +33,6 @@ 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 { @@ -57,16 +51,8 @@ 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 { @@ -97,18 +83,6 @@ impl OperationRegistry { .collect() } - /// List `External` op specs, additionally filtered by `remote_safe` for - /// peer-scoped serving (ADR-028 Assumption 2). When `trusted_peer` is true, - /// the `remote_safe` filter is bypassed (all `External` ops listed). - pub fn list_operations_peer_scoped(&self, trusted_peer: bool) -> Vec<&OperationSpec> { - self.operations - .values() - .filter(|r| r.spec.visibility == Visibility::External) - .filter(|r| trusted_peer || r.remote_safe) - .map(|r| &r.spec) - .collect() - } - pub async fn invoke( &self, name: &str, @@ -152,19 +126,16 @@ 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 @@ -219,23 +190,6 @@ impl OperationRegistryBuilder { 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 - } - pub fn build(self) -> OperationRegistry { OperationRegistry { operations: self.operations, @@ -776,126 +730,4 @@ 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/crates/alknet-call/tests/two_node_call.rs b/crates/alknet-call/tests/two_node_call.rs index 42711a2..1af99f5 100644 --- a/crates/alknet-call/tests/two_node_call.rs +++ b/crates/alknet-call/tests/two_node_call.rs @@ -1,9 +1,7 @@ //! Integration test: two-node `alknet/call` round-trip over a real QUIC //! loopback. A `CallAdapter` server accepts, a `CallClient` connects, and //! the client calls back into the server (connection symmetry, ADR-017 §2). -//! Verifies the shared dispatch loop works end-to-end and that the -//! peer-scoped default-deny filter (ADR-028) is enforced over a real -//! connection. +//! Verifies the shared dispatch loop works end-to-end. #![cfg(feature = "quinn")] @@ -117,21 +115,18 @@ async fn build_raw_quinn_server( (bound_addr, join) } -/// Build the server's registry: a remote_safe echo op, a non-remote-safe -/// secret op, and the services/list + services/schema discovery handlers. +/// Build the server's registry: an echo op, a secret op, and the +/// services/list + services/schema discovery handlers. fn build_server_registry() -> Arc { let mut registry = OperationRegistry::new(); - registry.register( - HandlerRegistration::new( - external_spec("server/echo"), - echo_handler(), - OperationProvenance::Local, - None, - None, - Capabilities::new(), - ) - .remote_safe(true), - ); + registry.register(HandlerRegistration::new( + external_spec("server/echo"), + echo_handler(), + OperationProvenance::Local, + None, + None, + Capabilities::new(), + )); registry.register(HandlerRegistration::new( external_spec("server/secret"), echo_handler(), @@ -144,17 +139,14 @@ fn build_server_registry() -> Arc { let list_handler = services_list_handler(Arc::clone(&discovery_registry)); let schema_handler = services_schema_handler(Arc::clone(&discovery_registry)); let mut full = OperationRegistry::new(); - full.register( - HandlerRegistration::new( - external_spec("server/echo"), - echo_handler(), - OperationProvenance::Local, - None, - None, - Capabilities::new(), - ) - .remote_safe(true), - ); + full.register(HandlerRegistration::new( + external_spec("server/echo"), + echo_handler(), + OperationProvenance::Local, + None, + None, + Capabilities::new(), + )); full.register(HandlerRegistration::new( external_spec("server/secret"), echo_handler(), @@ -187,20 +179,17 @@ async fn two_node_call_round_trip() { let server_registry = build_server_registry(); let (server_addr, _server_join) = build_raw_quinn_server(Arc::clone(&server_registry)).await; - // Client side: a CallClient in default-deny mode with its own ops so the - // server can call back (connection symmetry). + // Client side: a CallClient with its own ops so the server can call back + // (connection symmetry). let mut client_registry = OperationRegistry::new(); - client_registry.register( - HandlerRegistration::new( - external_spec("client/echo"), - echo_handler(), - OperationProvenance::Local, - None, - None, - Capabilities::new(), - ) - .remote_safe(true), - ); + client_registry.register(HandlerRegistration::new( + external_spec("client/echo"), + echo_handler(), + OperationProvenance::Local, + None, + None, + Capabilities::new(), + )); let client_registry = Arc::new(client_registry); let client = CallClient::new(Arc::clone(&client_registry), Arc::new(NoopIdentityProvider)); @@ -212,7 +201,7 @@ async fn two_node_call_round_trip() { .expect("connect did not time out") .expect("connect succeeds"); - // Outbound call: client -> server's remote_safe op. + // Outbound call: client -> server's echo op. let response = tokio::time::timeout( Duration::from_secs(5), conn.call("server/echo", serde_json::json!({"hi": 1})), @@ -221,13 +210,12 @@ async fn two_node_call_round_trip() { .expect("call did not time out"); assert_eq!(response.result, Ok(serde_json::json!({"hi": 1}))); - // The peer-scoped default-deny behavior (a CallClient hiding its - // non-remote-safe ops from a remote peer that calls back) is exercised by - // the unit tests in `client/call_client.rs` against the shared - // `Dispatcher`. This integration test focuses on the QUIC connect path + - // shared dispatch loop working end-to-end (the call above proves the - // CallClient opened a real connection, the shared loop dispatched, and the - // CallConnection::call() round-tripped). + // Peer authorization is enforced by the AccessControl gate in + // OperationRegistry::invoke (ADR-029 §3) — exercised by the unit tests in + // `registry/registration.rs`. This integration test focuses on the QUIC + // connect path + shared dispatch loop working end-to-end (the call above + // proves the CallClient opened a real connection, the shared loop + // dispatched, and the CallConnection::call() round-tripped). } #[tokio::test(flavor = "multi_thread", worker_threads = 4)]