Files
alknet/tasks/call/dispatch-peer-identity.md
glm-5.2 df355c53a9 tasks: decompose ADR-029/030/031/032/034/035 source sync into 17 tasks
Decompose the source-to-spec sync for the core and call crates into atomic,
dependency-ordered tasks for implementation agents:

Core (7 tasks + review):
- peer-entry-model: PeerEntry struct, AuthPolicy.peers (ADR-030 keystone)
- credential-store-trait: CredentialStore/InMemoryCredentialStore/StoreError (ADR-031/035)
- identity-store-trait: IdentityStore async write trait (ADR-035)
- config-identity-provider-peerentry: ConfigIdentityProvider PeerEntry resolution (ADR-030)
- fingerprint-normalization: ed25519:hex for raw keys across quinn/iroh (ADR-030 §6)
- three-remote-roles-docs: document ADR-034 roles and verifier selection
- review-core-sync: phase gate before call consumes new identity semantics

Call (9 tasks + review):
- retire-remote-safe: remove ADR-028 machinery, AccessControl is the gate (ADR-029 §3)
- operation-context-forwarded-for: forwarded_for field, wire-ingress only (ADR-032)
- peer-composite-env: PeerCompositeEnv, PeerId=Identity.id, remove UUID (ADR-029/030)
- operation-env-invoke-peer: invoke_peer/peer_contains/PeerRef (ADR-029 §2)
- services-list-accesscontrol-filtered: AccessControl filter, list-peers opt-in (ADR-029 §6)
- call-client-verifier-selection: TLS client-auth, verifier by PeerEntry (OQ-29, ADR-034)
- from-call-forwarded-for: populate forwarded_for, peer-keyed registration (ADR-029 §5, ADR-032)
- dispatch-peer-identity: AccessControl::check(peer_identity), PeerId from resolution (ADR-029 §3, ADR-030 §5)
- review-call-sync: phase gate for the call sync

Validated: 58 tasks, no cycles, logical topo order, two review checkpoints.
2026-06-28 21:08:41 +00:00

6.1 KiB

id, name, status, depends_on, scope, risk, impact, level
id name status depends_on scope risk impact level
call/dispatch-peer-identity Wire dispatch_requested to resolve peer Identity and run AccessControl::check (ADR-029 §3, ADR-030 §5) pending
call/peer-composite-env
call/retire-remote-safe
narrow medium component implementation

Description

Wire dispatch_requested to resolve the peer's Identity and run AccessControl::check(peer_identity) as the authorization mechanism, and ensure the PeerId for a connection comes from connection.identity().id (IdentityProvider resolution). Per ADR-029 §3 (AccessControl-based peer authorization) and ADR-030 §5 (PeerId from IdentityProvider resolution).

Current state

After call/retire-remote-safe, the RemoteFilter gate is removed. dispatch_requested resolves identity (connection-level + auth_token override) and OperationRegistry::invoke runs AccessControl::check. The remaining gap is ensuring the PeerId for the connection comes from IdentityProvider resolution (not a UUID), and that connections with no resolved identity get no PeerId (not added to PeerCompositeEnv).

Target state

1. PeerId from IdentityProvider resolution (ADR-030 §5)

The PeerId for a connection is connection.identity().id — the resolved Identity.id from IdentityProvider (= PeerEntry.peer_id, stable). The UUID workaround is removed (done in call/peer-composite-env). This task verifies the dispatch path reads connection.identity().id for the peer-keyed overlay and that a connection with no resolved identity is handled correctly.

2. AccessControl::check(peer_identity) is the authorization gate

dispatch_requested resolves the peer's Identity (from the connection's TLS fingerprint or the auth_token payload, via the existing IdentityProvider) and OperationRegistry::invoke runs AccessControl::check(peer_identity) against the op's AccessControl:

  • If the op's AccessControl is satisfied → dispatch (capabilities populated from the bundle).
  • If not → FORBIDDEN before the handler runs (capabilities never populated — the security property).
  • If the op is Visibility::InternalNOT_FOUND before ACL (existing behavior).

This is the existing OperationRegistry::invoke path — the RemoteFilter gate (removed in call/retire-remote-safe) was a parallel gate. This task verifies the AccessControl::check path is the sole authorization mechanism and that no remote_safe/trusted_peer remnants remain.

3. Connections with no resolved identity

A connection with no resolved identity (no client cert, unrecognized fingerprint) has no PeerId and is not added to PeerCompositeEnv (ADR-030 §5). The handler either rejects the connection or falls back to a connection-without-peer-identity path. The dispatch path must handle this case:

  • connection.identity() returns None → no PeerId
  • The connection's ops (if any discovered via from_call) are invoked through the CallConnection handle directly, not via PeerRef::Specific

dispatch_requested flow (post-sync)

async fn dispatch_requested(&self, connection: &Arc<CallConnection>,
    request_id: String, payload: Value) -> ResponseEnvelope {
    let operation_id = payload.get("operationId").and_then(|v| v.as_str()).unwrap_or("");
    let operation_name = Self::strip_leading_slash(operation_id).to_string();

    // No RemoteFilter gate (removed). AccessControl::check in invoke is the gate.

    let connection_identity = connection.connection().identity().cloned();
    let identity = self.resolve_identity(connection_identity, &payload);
    let forwarded_for = payload.get("forwarded_for")
        .and_then(|v| serde_json::from_value::<Identity>(v.clone()).ok());

    let input = payload.get("input").cloned().unwrap_or(Value::Null);
    let context = self.build_root_context(
        request_id.clone(), &operation_name, identity, forwarded_for, connection);

    // OperationRegistry::invoke runs AccessControl::check(identity) —
    // the sole authorization mechanism (ADR-029 §3).
    self.registry.invoke(&operation_name, input, context).await
}

Acceptance Criteria

  • dispatch_requested resolves peer Identity (connection-level + auth_token override)
  • OperationRegistry::invoke runs AccessControl::check(peer_identity) as the sole authorization gate
  • No RemoteFilter/remote_safe/trusted_peer remnants in dispatch
  • PeerId for connection comes from connection.identity().id (not UUID)
  • Connection with no resolved identity → no PeerId, not added to PeerCompositeEnv
  • Op with AccessControl::default() dispatches to any peer
  • Op with required_scopesFORBIDDEN for unauthorized peers (capabilities never populated)
  • Op with Visibility::InternalNOT_FOUND before ACL
  • forwarded_for extracted from payload and passed to build_root_context
  • Unit test: authorized peer → dispatch (capabilities populated)
  • Unit test: unauthorized peer → FORBIDDEN (capabilities never populated)
  • Unit test: Internal op → NOT_FOUND from wire
  • Unit test: connection with no identity → no PeerId
  • cargo test -p alknet-call succeeds
  • cargo clippy -p alknet-call succeeds with no warnings

References

  • docs/architecture/crates/call/call-protocol.md — dispatch_requested, AuthContext and Identity Resolution
  • docs/architecture/crates/call/client-and-adapters.md — peer authorization via AccessControl
  • docs/architecture/decisions/029-peer-graph-routing-model.md — ADR-029 §3 (AccessControl-based peer auth)
  • docs/architecture/decisions/030-peerentry-and-identity-id-decoupling.md — ADR-030 §5 (PeerId from IdentityProvider)

Notes

The RemoteFilter gate (removed in call/retire-remote-safe) was a parallel authorization system. This task verifies AccessControl::check in OperationRegistry::invoke is the sole gate and that the PeerId comes from IdentityProvider resolution (not UUID). Connections with no resolved identity get no PeerId and are not in the peer-keyed overlay — their ops are invoked through the CallConnection handle directly.

Summary

To be filled on completion