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.
This commit is contained in:
187
tasks/call/peer-composite-env.md
Normal file
187
tasks/call/peer-composite-env.md
Normal file
@@ -0,0 +1,187 @@
|
||||
---
|
||||
id: call/peer-composite-env
|
||||
name: Replace CompositeOperationEnv with PeerCompositeEnv (peer-keyed overlays) and PeerId from Identity.id (ADR-029/030)
|
||||
status: pending
|
||||
depends_on: [call/retire-remote-safe]
|
||||
scope: broad
|
||||
risk: high
|
||||
impact: phase
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Replace `CompositeOperationEnv` (singular `connection: Option<Arc<dyn
|
||||
OperationEnv>>`) with `PeerCompositeEnv` (peer-keyed `HashMap<PeerId,
|
||||
connection_overlay>`), and change `PeerId` from a connection-assigned UUID to
|
||||
`Identity.id` from `IdentityProvider` resolution (= `PeerEntry.peer_id`,
|
||||
stable across key rotation). Per ADR-029 §1 and ADR-030 §4-5.
|
||||
|
||||
This is the highest-risk call task — a structural rewrite of the composition
|
||||
env that aggregates multiple connections. The singular-connection case (one
|
||||
peer) is the degenerate case with a single-entry map.
|
||||
|
||||
### PeerCompositeEnv struct
|
||||
|
||||
```rust
|
||||
pub struct PeerCompositeEnv {
|
||||
pub base: Arc<dyn OperationEnv + Send + Sync>, // Layer 0 curated
|
||||
pub session: Option<Arc<dyn OperationEnv + Send + Sync>>, // Layer 1
|
||||
pub connections: HashMap<PeerId, Arc<dyn OperationEnv + Send + Sync>>, // Layer 2, peer-keyed
|
||||
connection_order: Vec<PeerId>, // insertion order for PeerRef::Any first-match
|
||||
}
|
||||
|
||||
pub type PeerId = String; // = Identity.id from IdentityProvider resolution
|
||||
// = PeerEntry.peer_id (stable, not crypto material — ADR-030)
|
||||
```
|
||||
|
||||
### PeerCompositeEnv methods
|
||||
|
||||
```rust
|
||||
impl PeerCompositeEnv {
|
||||
pub fn new(base: Arc<dyn OperationEnv + Send + Sync>) -> Self;
|
||||
|
||||
pub fn with_session(mut self, session: Arc<dyn OperationEnv + Send + Sync>) -> Self;
|
||||
|
||||
/// Attach a peer's connection overlay. The `peer_id` comes from
|
||||
/// `connection.identity().id` (IdentityProvider resolution). A connection
|
||||
/// with no resolved identity has no PeerId and is NOT attached (ADR-030 §5).
|
||||
pub fn attach_peer(&mut self, peer_id: PeerId, overlay: Arc<dyn OperationEnv + Send + Sync>);
|
||||
|
||||
/// Detach a peer's overlay (on disconnect). The peer's sub-overlay drops;
|
||||
/// in-flight PeerRef::Specific(that_peer) gets NOT_FOUND.
|
||||
pub fn detach_peer(&mut self, peer_id: &PeerId);
|
||||
}
|
||||
```
|
||||
|
||||
### PeerCompositeEnv::invoke_with_policy
|
||||
|
||||
```rust
|
||||
async fn invoke_with_policy(&self, namespace: &str, operation: &str, input: Value,
|
||||
parent: &OperationContext, policy: AbortPolicy) -> ResponseEnvelope {
|
||||
let name = format!("{namespace}/{operation}");
|
||||
if !parent.scoped_env.allows(&name) {
|
||||
return ResponseEnvelope::not_found(parent.request_id.clone(), &name);
|
||||
}
|
||||
// PeerRef::Any routing (ADR-029 §2): session → peers in insertion
|
||||
// order → curated base. First overlay that contains the op wins.
|
||||
if let Some(session) = &self.session {
|
||||
if session.contains(&name) {
|
||||
return session.invoke_with_policy(namespace, operation, input, parent, policy).await;
|
||||
}
|
||||
}
|
||||
for peer_id in &self.connection_order {
|
||||
if let Some(conn_env) = self.connections.get(peer_id) {
|
||||
if conn_env.contains(&name) {
|
||||
return conn_env.invoke_with_policy(namespace, operation, input, parent, policy).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.base.invoke_with_policy(namespace, operation, input, parent, policy).await
|
||||
}
|
||||
|
||||
fn contains(&self, name: &str) -> bool {
|
||||
self.session.as_ref().map_or(false, |s| s.contains(name))
|
||||
|| self.connections.values().any(|c| c.contains(name))
|
||||
|| self.base.contains(name)
|
||||
}
|
||||
```
|
||||
|
||||
The `invoke_peer` / `peer_contains` methods (PeerRef routing) are
|
||||
`call/operation-env-invoke-peer` — this task builds the struct and the
|
||||
`invoke_with_policy` / `contains` methods; the peer-routing methods are the
|
||||
next task.
|
||||
|
||||
### compose_root_env rewrite
|
||||
|
||||
`Dispatcher::compose_root_env` builds a `PeerCompositeEnv` per incoming call:
|
||||
|
||||
```rust
|
||||
fn compose_root_env(&self, connection: &CallConnection, context: &OperationContext)
|
||||
-> Arc<dyn OperationEnv + Send + Sync>
|
||||
{
|
||||
let base = Arc::new(LocalOperationEnv::new(Arc::clone(&self.registry)));
|
||||
let session = self.session_source.as_ref()
|
||||
.and_then(|s| s.overlay_for(context));
|
||||
|
||||
let mut env = PeerCompositeEnv::new(base);
|
||||
if let Some(session) = session {
|
||||
env = env.with_session(session);
|
||||
}
|
||||
// Attach this connection's overlay, keyed by the peer's PeerId.
|
||||
// PeerId = connection.identity().id (IdentityProvider resolution).
|
||||
// A connection with no resolved identity is NOT attached to the
|
||||
// peer-keyed overlay (ADR-030 §5) — its ops are invoked through the
|
||||
// CallConnection handle directly, not via PeerRef::Specific.
|
||||
if let Some(peer_id) = connection.connection().identity().map(|id| id.id.clone()) {
|
||||
env.attach_peer(peer_id, connection.overlay_env());
|
||||
}
|
||||
Arc::new(env)
|
||||
}
|
||||
```
|
||||
|
||||
### PeerId source: Identity.id (remove UUID workaround)
|
||||
|
||||
`PeerId` is `Identity.id` from `IdentityProvider` resolution — the stable
|
||||
`PeerEntry.peer_id` (ADR-030 §4). The UUID workaround (ADR-029 Assumption 1's
|
||||
connection-assigned UUID) is removed. A connection with no resolved identity
|
||||
has no `PeerId` and is not added to `PeerCompositeEnv` (ADR-030 §5).
|
||||
|
||||
### Migration: CompositeOperationEnv → PeerCompositeEnv
|
||||
|
||||
All call sites that construct `CompositeOperationEnv::new(base, Some(conn),
|
||||
session)` migrate to `PeerCompositeEnv::new(base).with_session(session).
|
||||
attach_peer(peer_id, conn)`. The singular-connection case (one peer) is the
|
||||
degenerate case (`connections` with one entry).
|
||||
|
||||
### What this task does NOT do
|
||||
|
||||
- Does NOT add `invoke_peer` / `peer_contains` / `PeerRef` — that's
|
||||
`call/operation-env-invoke-peer`. This task builds the struct and the
|
||||
`invoke_with_policy` (PeerRef::Any equivalent) + `contains` methods.
|
||||
- Does NOT change `from_call` registration — that's
|
||||
`call/from-call-forwarded-for` (peer-keyed registration, forwarded_for).
|
||||
- Does NOT change `services/list` — that's
|
||||
`call/services-list-accesscontrol-filtered`.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `PeerCompositeEnv` struct with `base`, `session`, `connections: HashMap<PeerId, ...>`, `connection_order: Vec<PeerId>`
|
||||
- [ ] `PeerId = String` (= `Identity.id`, not UUID)
|
||||
- [ ] `PeerCompositeEnv::new(base)`, `with_session(session)`, `attach_peer(peer_id, overlay)`, `detach_peer(peer_id)`
|
||||
- [ ] `invoke_with_policy` routes: session → peers in insertion order → base (first `contains` wins)
|
||||
- [ ] `contains` checks session + all connections + base
|
||||
- [ ] `compose_root_env` builds `PeerCompositeEnv` per call, attaches this connection's overlay keyed by `connection.identity().id`
|
||||
- [ ] Connection with no resolved identity → not attached to peer-keyed overlay (no PeerId)
|
||||
- [ ] `CompositeOperationEnv` removed (all call sites migrated)
|
||||
- [ ] UUID workaround removed (no connection-assigned UUID for PeerId)
|
||||
- [ ] Singular-connection case works (degenerate single-entry map)
|
||||
- [ ] Unit test: PeerCompositeEnv routes to session when it contains the op
|
||||
- [ ] Unit test: PeerCompositeEnv routes to first peer (insertion order) that contains the op
|
||||
- [ ] Unit test: PeerCompositeEnv falls through to base when no overlay contains the op
|
||||
- [ ] Unit test: attach_peer + detach_peer (detach → NOT_FOUND for that peer)
|
||||
- [ ] Unit test: connection with no identity → not attached
|
||||
- [ ] Unit test: reachability check (scoped_env.allows) still gates before routing
|
||||
- [ ] `cargo test -p alknet-call` succeeds
|
||||
- [ ] `cargo clippy -p alknet-call` succeeds with no warnings
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/crates/call/operation-registry.md — PeerCompositeEnv, OperationEnv
|
||||
- docs/architecture/crates/call/call-protocol.md — compose_root_env, build_root_context
|
||||
- docs/architecture/decisions/029-peer-graph-routing-model.md — ADR-029 §1 (peer-keyed overlays)
|
||||
- docs/architecture/decisions/030-peerentry-and-identity-id-decoupling.md — ADR-030 §4-5 (PeerId source)
|
||||
|
||||
## Notes
|
||||
|
||||
> Highest-risk call task — structural rewrite of the composition env. The
|
||||
> singular-connection case is the degenerate case (one-entry map). PeerId is
|
||||
> `Identity.id` (stable `peer_id`), not a UUID — the UUID workaround is removed.
|
||||
> A connection with no resolved identity gets no PeerId and is not attached
|
||||
> (ADR-030 §5). The `invoke_peer` / `PeerRef` routing methods are the next
|
||||
> task (`call/operation-env-invoke-peer`); this task builds the struct and the
|
||||
> PeerRef::Any-equivalent routing (`invoke_with_policy`).
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
Reference in New Issue
Block a user