Files
alknet/tasks/call/registry/remote-safe-marking.md
glm-5.2 e4a25947d6 feat(call): remote_safe field on HandlerRegistration (ADR-028)
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
2026-06-26 12:51:18 +00:00

5.8 KiB

id, name, status, depends_on, scope, risk, impact, level
id name status depends_on scope risk impact level
call/registry/remote-safe-marking Add remote_safe field to HandlerRegistration for CallClient peer-scoped filtering (ADR-028) completed
narrow medium isolated implementation

Description

Add the remote_safe: bool field to HandlerRegistration (and its builder) so that a CallClient can default-deny which operations it exposes to a remote peer. This is the v1 shape of the peer-scoped filtering mechanism locked by ADR-028. It is the prerequisite for call/client/call-client (the CallClient's dispatch path reads this field) and is the only one-way-door piece of the call-completion work, so it goes first.

Field

pub struct HandlerRegistration {
    pub spec: OperationSpec,
    pub handler: Handler,
    pub provenance: OperationProvenance,
    pub composition_authority: Option<CompositionAuthority>, // None for leaves
    pub scoped_env: Option<ScopedOperationEnv>,               // None for leaves
    pub capabilities: Capabilities,
    pub remote_safe: bool,   // default false; ADR-028 — exposes this op to
                             // CallClient peers (trusted-peer mode bypasses)
}

remote_safe defaults to false across all provenance (Local, Session, and the leaf provenances — see ADR-028 §4). The operator flips specific operations to true when they want a peer to reach them. This mirrors the default-deny posture of ADR-015 (visibility Internal by default) and ADR-022 (composition authority None for leaves by default).

Builder

OperationRegistryBuilder needs a way to set remote_safe:

  • .with_local(...) / .with_leaf(...) / .with(...) should default remote_safe: false (current call sites stay valid unchanged).
  • Add a chainable setter, e.g. .remote_safe(true) on the builder or a with_local_remote(...) / explicit-arg variant. The exact builder API shape is a two-way door — pick the least invasive (an optional trailing arg or a builder setter method); do not over-engineer per-peer allowlists (that's OQ-25's two-way-door remainder, explicitly out of scope here).

services/list interaction (ADR-028 Assumption 2)

services/list already filters by Visibility::External (ADR-015). Per ADR-028 Assumption 2, when served to a CallClient peer, services/list must additionally hide non-remote-safe ops — a peer should not see ops it cannot call, so discovery and dispatch filters agree. The services_list_handler in registry/discovery.rs currently filters only on visibility.

Scoping note: the services/list handler doesn't know whether the caller is a CallClient peer or a local process. The v1 implementation: the filter applied by services/list is the registry's filter, and the peer-scoped view a CallClient exposes is built atop this. The cleanest v1 split is:

  • services/list keeps filtering by Visibility::External (unchanged).
  • The CallClient's peer-scoped view (task call/client/call-client) is a dispatch-time read that additionally filters by remote_safe, and the CallClient's own services/list serving (when it receives services/list from the remote peer) hides non-remote-safe ops.

So this task adds the field + builder setter + provenance defaults, and the filtering behavior that consumes the field is wired in call/client/call-client (the dispatch path) and — for the services/list-hides-non-remote-safe behavior — in the CallClient's serving path. This task only adds the data and defaults, plus a unit test that the field defaults to false and that the setter flips it. Keep this task tightly scoped: adding the field must not change any existing dispatch behavior (the field is read-only by the CallClient layer added later).

Acceptance Criteria

  • HandlerRegistration has pub remote_safe: bool field
  • All existing with_local / with_leaf / with builder call sites compile unchanged (default remote_safe: false)
  • A builder setter exists to set remote_safe: true (e.g. .remote_safe(true) or an explicit-arg variant)
  • Provenance-aware defaults: Local, Session, FromOpenAPI, FromMCP, FromCall, FromJsonSchema all default to false (ADR-028 §4)
  • No existing dispatch path behavior changes (field is data-only here; the CallClient filter that reads it is a later task)
  • services/list handler is unchanged in this task (filtering wired later)
  • Unit test: HandlerRegistration default has remote_safe == false
  • Unit test: builder setter produces remote_safe == true
  • Unit test: all six provenance variants default remote_safe == false
  • cargo test -p alknet-call succeeds
  • cargo clippy -p alknet-call --all-targets succeeds with no warnings

References

  • docs/architecture/decisions/028-callclient-peer-scoped-registry-filtering.md — ADR-028 (the one-way door; §2 field location, §4 provenance defaults, Assumption 2 services/list hide)
  • docs/architecture/crates/call/operation-registry.md — HandlerRegistration struct sketch (now shows remote_safe)
  • docs/architecture/crates/call/client-and-adapters.md — CallClient § (consumes the field; trusted-peer bypass)
  • docs/research/alknet-call-completion/gap-analysis.md — DC-1

Notes

This is the one-way-door piece of the call-completion work: the existence of default-deny filtering is locked by ADR-028; this task adds only the v1 data shape (remote_safe: bool) and defaults. The richer per-peer shape (allowlist, capability-class tag) is explicitly out of scope — it's the two-way-door remainder tracked as OQ-25. Do not implement per-peer logic here. The filtering behavior (dispatch path + services/list hide) is wired in call/client/call-client, not here — this task is data + defaults only so it can land first and unblock the CallClient task.