W1 (call/protocol/abort-cascade-wiring): wire AbortCascade into CallAdapter handle_stream for EVENT_ABORTED. Cascades with AbortPolicy::AbortDependents, aborts root, no descendant frames on wire (ADR-016 Decision 2). Two integration tests added. W2 (core/endpoint-client-fingerprint): extract TLS client cert fingerprint in dispatch_quinn (SHA256:<hex> of leaf cert DER via peer_identity) and dispatch_iroh (ed25519:<hex> of peer NodeId). Fingerprint format documented in auth.md. Server config change (with_no_client_auth → request-but-don't-require) deferred to new follow-up task core/endpoint-request-client-cert. W3 (vault/mnemonic-debug-redaction): replace Mnemonic derive(Debug) with manual redacting impl (phrase: "[REDACTED]"). Seed confirmed no Debug impl. Redaction test added. W4 (core/auth-apikey-resources): Option B — drop entry.resources from spec. External identities (token/fingerprint) grant scopes only; resource-scoped ACLs are composition-internal (ADR-015/022). auth.md corrected + limitation documented. Two tests confirm empty resources. review-post-impl-fixes: all 4 verified, workspace green (326 tests, 0 failures, 0 clippy warnings). Review #004 status → resolved. Graph: 34 tasks, 12 gens.
6.4 KiB
id, name, status, depends_on, scope, risk, impact, level
| id | name | status | depends_on | scope | risk | impact | level | |
|---|---|---|---|---|---|---|---|---|
| call/protocol/abort-cascade-wiring | Wire AbortCascade into CallAdapter inbound event path (ADR-016) | completed |
|
narrow | medium | component | implementation |
Description
AbortCascade::cascade_abort is implemented and unit-tested in
crates/alknet-call/src/protocol/abort.rs (20 tests cover depth-3 cascades,
mixed Call/Subscribe entries, both AbortPolicy variants, and the
"unknown root silently discarded" rule), but no caller invokes it.
CallAdapter::handle_stream (adapter.rs:210–213) only dispatches
EVENT_REQUESTED; an inbound call.aborted event is logged at debug!
and dropped. As a result, ADR-016's cascade is a documented property
that does not hold at runtime — when a wire client aborts a parent
request, the parent's composed descendants keep running to completion,
which is exactly the wasted-work / unwanted-side-effects case ADR-016
was written to prevent.
This task adds the missing bolt between the two halves.
Wire visibility (read before implementing)
ADR-016 Decision 2 (decisions/016-...:220–224) and the abort-cascade
task (tasks/call/protocol/abort-cascade.md:68–74) are explicit:
composed child request_ids are internal. The client only sees
call.aborted for the root ID it sent; the server cascades internally.
So the wiring should:
- Receive the inbound
call.abortedforroot_request_id. - Call
AbortCascade::cascade_abort(root_request_id, AbortPolicy::AbortDependents)— the wire caller does not choose the policy (ADR-016 Decision 6: the root gets the defaultAbortDependents;ContinueRunningis a handler-level opt-in for children, not a wire field). - Drop each descendant's pending entry (which cancels its future via
Rust's async drop semantics — no
call.abortedis sent on the wire for descendants). - Drop the root's pending entry (the trigger event already came from the wire; the server mirrors the abort locally).
Do not send call.aborted frames back to the client for descendant
IDs — that would leak internal composition structure to the wire.
Implementation sketch
In CallAdapter::handle_stream (adapter.rs:200–228), replace the
continue branch with a match on event type:
match envelope.r#type.as_str() {
EVENT_REQUESTED => { /* existing dispatch path */ }
EVENT_ABORTED => {
let request_id = envelope.id.clone();
let mut pending = connection.pending().lock();
let mut cascade = AbortCascade::new(&mut pending);
let aborted = cascade.cascade_abort(&request_id, AbortPolicy::AbortDependents);
// also abort the root itself (cascade_abort does not touch the root)
pending.handle_aborted(&request_id);
if !aborted.is_empty() {
tracing::debug!(count = aborted.len(), "abort cascade evicted descendants");
}
}
other => {
debug!(event_type = %other, id = %envelope.id, "ignoring non-requested/non-aborted event on inbound stream");
}
}
AbortCascade already takes &mut PendingRequestMap; CallConnection::pending()
returns &Arc<Mutex<PendingRequestMap>> (connection.rs:53–55). Import
AbortCascade and AbortPolicy from super::abort and
crate::registry::context respectively.
Integration test (the test that would have caught the gap)
Add an integration test in adapter.rs's tests module that exercises
the full path:
- Build a
CallAdapterwith a registry containing one parent op (parent/run) whose handler callsenv.invoke("child", "run", ...). - Register
child/runin the same registry. - Open a stub
Connection, construct aCallConnection, manually register a pending entry for the parent's request_id, and simulate a composed child by registering a second pending entry withparent_request_id: Some(parent_id). - Send an
EventEnvelope::aborted(parent_id)frame throughhandle_stream. - Assert both the parent and child entries are gone from
PendingRequestMap.
The existing unit tests on AbortCascade cover the tree-walking logic;
this test only needs to confirm the wiring — that an inbound abort
frame actually reaches cascade_abort.
Acceptance Criteria
CallAdapter::handle_streamhandlesEVENT_ABORTED(not justEVENT_REQUESTED)- Inbound
call.abortedtriggersAbortCascade::cascade_abortwithAbortPolicy::AbortDependents - Root request's pending entry is also removed (cascade_abort skips the root)
- No
call.abortedframes are sent on the wire for descendant IDs (internal-only cascade) - Non-requested, non-aborted events still log at
debug!and continue - Integration test: parent abort removes parent + child from
PendingRequestMap - Integration test: abort for unknown request_id is a no-op (no panic, no removal)
cargo test -p alknet-callsucceedscargo clippy -p alknet-call --all-targetssucceeds with no warnings
References
- docs/reviews/004-post-implementation-sanity-check.md — W1 (full finding)
- docs/architecture/decisions/016-abort-cascade-for-nested-calls.md — ADR-016 (Decision 2: server-side cascade; Decision 6: policy is handler-set, not wire-set)
- docs/architecture/crates/call/call-protocol.md:497 — spec requiring the CallAdapter to walk the tree
- tasks/call/protocol/abort-cascade.md — completed task that built
AbortCascadein isolation - crates/alknet-call/src/protocol/abort.rs — existing
AbortCascadeimpl - crates/alknet-call/src/protocol/adapter.rs:200–228 —
handle_stream(the site to modify)
Notes
The abort-cascade task (call/protocol/abort-cascade) built and tested
AbortCascadebut its acceptance criteria did not include wiring it intoCallAdapter::handle_stream. The call-adapter task's acceptance criteria likewise omitted "inboundcall.abortedtriggers cascade." This task closes that integration gap — all the hard logic already exists and is tested; this task adds the ~30-line bolt and the one integration test that would have caught the gap.
Summary
handle_stream now matches EVENT_ABORTED → invokes
AbortCascade::cascade_abort with AbortPolicy::AbortDependents, then
aborts the root. Non-requested/non-aborted events still log at debug!.
No descendant call.aborted frames sent on the wire. Two integration
tests: cascade removes parent + child from PendingRequestMap; unknown
request_id is a no-op. cargo test -p alknet-call (161 tests) and
clippy clean.