All six tasks are done:
- core/connection-credentials: ConnectionCredentials + RemoteIdentity in alknet-core
- tls/crate-init: alknet-tls crate skeleton with deps and feature flags
- tls/server-extract: server-side TLS code extracted into alknet-tls
- tls/client-extract: client-side TLS code extracted into alknet-tls
- tls/tests: 34 TLS tests moved and adapted into alknet-tls
- tls/review-tls: spec conformance review passed, all feature combos green
The BidiStreamSource trait (ADR-070) made Connection hold Box<dyn
BidiStreamSource> so downstream crates can add connection shapes without
editing core — but the public constructor for that path was missing.
The source field is private; the channels crate (or any future crate)
had no way to build a Connection from its own BidiStreamSource impl.
The trait was unusable from outside core.
Add Connection::from_source(source: impl BidiStreamSource, alpn: Vec<u8>)
-> Self: the extension point. Takes impl BidiStreamSource (not Box<dyn>)
to match the ergonomic style of from_stream / from_bidi; boxes the
source internally. No feature gate (always available, like from_stream).
Initializes alpn and identity: OnceLock::new() the same as the other
constructors.
Test: a custom RecordingSource BidiStreamSource impl (not a built-in)
constructed via from_source, verifying remote_alpn / remote_addr /
accept_bi (round-trips real bytes via tokio::io::duplex) / open_bi
(StreamClosed) / close (records code+reason) all delegate to the custom
impl.
Verified: cargo test + clippy clean across default, --no-default-features,
--features iroh, --all-features; cargo check -p alknet-call succeeds
unchanged; cargo fmt --check passes.
ADR-070 made Connection hold Box<dyn BidiStreamSource> so downstream
crates can add connection shapes without editing core. The trait and
three built-in impls landed, but the public constructor that lets a
downstream crate construct a Connection from its own BidiStreamSource
impl was never added — the source field is private with no from_source
constructor. The channels POC update surfaced this when it tried to
use the trait directly and found no way to build a Connection from a
ChannelBidiStreamSource.
This is a gap in the ADR-070 implementation, not a new decision. ADR-070
§Consequences says 'the channels crate implements ChannelBidiStreamSource
in its own crate and constructs Connection from it' — the constructor for
that path is what was missing.
- ADR-070: add from_source to the Constructors table + reword the
downstream-crates paragraph to make the two paths explicit
(from_source for custom impls; from_quinn/from_iroh/from_stream for
built-in impls)
- core-types.md: add from_source to the impl Connection block, the
built-in implementations table, and the design-decisions table entry
- tasks/core/connection-from-source-constructor.md: the implementation
task (one constructor + one test, scope narrow, risk low)
The implementer flipped the frontmatter status to completed but left the
acceptance checkboxes empty and omitted the Summary section. Fill both
to match the convention every other completed task in tasks/core/ follows
(core-types.md, fingerprint-normalization.md, review-core.md — all check
boxes [x] and add a ## Summary section recording what landed).
Extract stream-yield ops from Connection into a BidiStreamSource trait;
Connection now holds Box<dyn BidiStreamSource> instead of the closed
ConnectionKind enum. Three crate-private impls wrap the existing
constructors: QuinnBidiStreamSource (feature quinn), IrohBidiStreamSource
(feature iroh), StreamBidiStreamSource (no gate, yield-once per ADR-065).
Public Connection API is preserved verbatim — handlers dispatch through
the trait object transparently.
The StreamBidiStreamSource::close impl prefixes code/reason with _ and
documents why they're ignored (the drop is the close — ADR-065). This
resolves the ADR-065 leftover clippy warning under --no-default-features
(REQ-CORE-02).
Add AuthContext::anonymous(alpn) convenience constructor (REQ-CORE-03):
sets identity/fingerprint/remote_addr to None, only alpn is populated.
Removes the four-None-field literal that recurred in handler POCs/tests.
Verified: cargo test + clippy clean across default, --no-default-features,
--features iroh, and --all-features; cargo check -p alknet-call succeeds
unchanged; cargo fmt --check passes.
Three core-crate changes surfaced by the alknet-channels POC:
- ADR-070: extract BidiStreamSource trait so Connection holds
Box<dyn BidiStreamSource> instead of a closed ConnectionKind enum;
downstream crates (channels, future transports) implement the trait
to add connection shapes without editing core. Public Connection API
preserved verbatim. REQ-CORE-02 (close() params clippy warning under
--no-default-features) folded in — the signature stays on the trait,
non-QUIC impls ignore the args.
- OQ-55: AlknetClient / client establishment extraction deferred(scope).
Blocked on a second *transport's* real client (not a second QUIC
client) — extracting a QUIC-shaped connector now would bake QUIC in
as the establishment shape, the same welding ADR-065 unwound on the
server side. Each crate builds its own client standalone for now.
- core-types.md / auth.md / overview.md / README indexes updated to
reflect ADR-070 and OQ-55. Architecture review: zero critical issues.
- tasks/core/bidistreamsource-trait.md: the implementation task (Parts
1-3: BidiStreamSource refactor, close() fix, AuthContext::anonymous).
Establishes the tasks/architecture/ blocker-task half of the Safe Exit
protocol. Each deferred OQ now has an external-trigger tracker task
representing its unblocking condition, and the OQ's Blocked on field names
the tracker task ID — closing the gap where the deferral field existed but
the task-graph half was unenforced.
Changes:
- Add 6 external-trigger tracker tasks for the deferred OQs (OQ-09, 10, 32,
41, 44, 46) under tasks/architecture/, tagged [external-trigger, deferred-oq]
- Add structured Blocked on field to OQ-09 and OQ-10 (previously used legacy
'deferred' status with the reason in the Resolution prose); index now
surfaces all 6 deferred OQs with concrete conditions, no placeholders
- Document the architecture-task level mapping (level: implementation for
ADRs/specs, decomposition for spec-to-ADR breakdown, planning for backlog
seeding, research/review direct fits) and the deferred-OQ blocker-task
pattern in docs/sdd_process.md
- Mark architecture/safe-exit-blocker-task-mechanism and
architecture/oq-09-10-blocking-conditions completed (implemented by this pass)
- Fix 4 pre-existing ADR-050 implementation tasks: status: done → completed
(taskgraph enum is 'completed', not 'done') — taskgraph validate now passes
for all 100 tasks
Implements the 4-task DAG for runtime-spawned resource ownership so
AccessControl::check can answer "does this identity own this specific
container/TTY/process" instead of relying only on static
Identity.resources grants.
1. core/ownership-store-trait: OwnershipProvider (sync read) +
OwnershipStore (async write) traits + InMemoryOwnershipStore +
OwnershipError in alknet-core. Fourth instance of the repo/adapter
pattern (ADR-033), mirroring CredentialStore/store.rs.
2. call/registry/operation-spec-resource-id-path: resource_id_path
field on OperationSpec — JSON pointer into input for resource ID
extraction. Single field addition, all ~40 construction sites across
alknet-call + alknet-http updated to pass None (no semantic change).
3. call/registry/access-control-ownership-check: check() signature
gains resource_id + Option<&dyn OwnershipProvider>. 3-case decision
tree: ownership Some + resource_id Some -> owns(); ownership Some +
resource_id None -> owns_any() (list scope-gate); ownership None ->
static Identity.resources fallback (backward compat). 7 call sites
updated to (None, None) — including a 7th in alknet-http/gateway_routes
not listed in the original spec.
4. call/registry/dispatch-resource-id-extraction: wire the dispatch
path. OperationContext gains ownership field; Dispatcher/CallAdapter
gain with_ownership_provider builders; invoke/invoke_streaming/
invoke_with_policy extract resource_id via spec.resource_id_path
and thread context.ownership to check(). extract_json_pointer helper
handles $.field syntax (graceful None on missing/non-string). 20
OperationContext literals updated across both crates.
Backward compatibility is load-bearing throughout: ownership=None
falls back to the existing static resource-check path. Deployments
without runtime-spawned resources wire nothing and behave identically.
821 tests pass workspace-wide (was ~770); clippy clean; fmt clean.
Task specs marked done.
Four tasks forming a DAG for the dynamic resource ownership model (ADR-050):
1. core/ownership-store-trait (no deps) — OwnershipProvider (sync read) +
OwnershipStore (async write) traits + InMemoryOwnershipStore + OwnershipError
in alknet-core; fourth instance of the repo/adapter pattern (ADR-033)
2. call/registry/operation-spec-resource-id-path (no deps) — add
resource_id_path: Option<String> to OperationSpec (JSON pointer into
input for resource ID extraction)
3. call/registry/access-control-ownership-check (depends on 1) — update
AccessControl::check signature to accept resource_id + OwnershipProvider;
backward compatible (ownership=None falls back to static Identity.resources)
4. call/registry/dispatch-resource-id-extraction (depends on 2, 3) — wire
dispatch path to extract resource_id from input via spec.resource_id_path
and thread OwnershipProvider to check(); OperationContext gains ownership
field
Tasks 1 and 2 can run in parallel (different crates, no deps). Task 3
depends on 1. Task 4 depends on 2 and 3. Validated: no cycles, 90 tasks total.
Three tasks implementing ADR-027:
1. core/rawkey-decouple-from-iroh: TlsIdentity::RawKey now uses
Ed25519SecretKey (alknet-core-owned wrapper over ed25519_dalek)
instead of iroh::SecretKey. RawKeyCertResolver and Ed25519SigningKey
un-gated from #[cfg(all(quinn, iroh))] to #[cfg(quinn)] only.
Quinn-only builds (default) now support RFC 7250 raw-key identity.
iroh transport converts via iroh::SecretKey::from_bytes.
2. core/endpoint-request-client-cert: replaced with_no_client_auth()
with AcceptAnyCertVerifier — a custom ClientCertVerifier that
requests client certs but doesn't require them or verify against
a CA. alknet's identity model is fingerprint-based (the
authorized_fingerprints set is the trust anchor), not PKI-based.
Peer certs are extracted at the TLS layer for fingerprinting;
peers without certs connect normally.
3. core/acme-integration: TlsIdentity::Acme variant (domains,
cache_dir, directory, contact) + AcmeDirectory enum. TlsSetup
two-phase construction: synchronous for X509/RawKey/SelfSigned,
async for Acme (spawns AcmeState event loop, builds ServerConfig
with ResolvesServerCertAcme). acme-tls/1 ALPN added when ACME is
active; dispatch_quinn guard closes challenge connections
gracefully (challenge is TLS-layer-handled). acme feature gate
keeps rustls-acme out of non-ACME builds.
Workspace: build/test/clippy green across all 3 feature configs
(quinn-only, quinn+iroh, quinn+acme, all-features). 331 tests, 0
failures, 0 warnings.
ADR-027 resolves the architectural gap surfaced when ACME integration
became a concrete target:
1. TlsIdentity::Acme variant — static config data (domains, cache_dir,
directory, contact) with async AcmeState constructed at endpoint
setup via two-phase TlsSetup (not stuffed into the Clone-able enum).
2. TlsIdentity::RawKey decoupled from the iroh feature — uses
Ed25519SecretKey (alknet-core-owned wrapper over ed25519_dalek)
instead of iroh::SecretKey. Raw-key TLS identity (RFC 7250, the
default for most alknet nodes) now works in quinn-only builds.
iroh transport converts via SecretKey::from_bytes.
3. ACME feature-gated behind new acme feature (rustls-acme optional
dep). Non-ACME builds don't compile it.
4. dispatch_quinn guard for acme-tls/1 challenge connections — TLS-ALPN-01
is handled at the rustls cert resolver layer during the handshake;
the guard closes challenge connections gracefully instead of logging
a misleading "no handler" warning.
Research confirmed QUIC (quinn) handles ACME challenges differently than
TCP (reverse-proxy): quinn gives no ClientHello peek hook, but the
challenge is fully answered at the cert resolution step before the
connection surfaces to the application. No handler registration needed.
Spec updates: config.md, endpoint.md, open-questions.md (OQ-12),
overview.md + README.md (ADR index), ADR-010 (cross-ref).
Tasks: core/rawkey-decouple-from-iroh (gen 1, no deps),
core/acme-integration (gen 2, depends on rawkey). Graph: 36 tasks.
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.
W1 (call/protocol/abort-cascade-wiring): wire AbortCascade into CallAdapter
handle_stream for EVENT_ABORTED. W2 (core/endpoint-client-fingerprint):
extract TLS client cert fingerprint in dispatch_quinn/dispatch_iroh.
W3 (vault/mnemonic-debug-redaction): replace Mnemonic derive(Debug) with
redacting impl. W4 (core/auth-apikey-resources, level: research): decide
whether ApiKeyEntry should carry resources, then implement or drop from
spec. review-post-impl-fixes gates on all four. Graph: 33 tasks, 12 gens.
Break down the three initial crates (alknet-vault, alknet-core, alknet-call)
into dependency-ordered task files for implementation agents.
Structure:
- tasks/vault/ (10 tasks) — drift fixes from ADR-025/026 refactor, review,
spec sync. Vault is independent and can run fully in parallel with core/call.
- tasks/core/ (6 tasks) — crate init, core types, config, auth, endpoint,
review. Core is foundational; call depends on it.
- tasks/call/ (12 tasks) — split into registry/ and protocol/ topic subdirs
reflecting the two subsystems. CallAdapter is the merge point.
Key decisions:
- Drifts 3+9+10 grouped as one task (key-versioning-rotation) — the complete
ADR-021 rotation feature that doesn't compile in pieces
- Reviews injected at end of each crate phase (vault, core, call)
- Vault spec-sync task removes the drift table and bumps doc status to stable
- ACME deferred in core/endpoint (noted as TODO; X509 manual certs for now)
- OperationEnv kept as a trait (load-bearing for ADR-024 layering)
Validated: 28 tasks, no cycles, 11 generations of parallel work.
Critical path runs through call (11 tasks). Vault completes by generation 4.
6 high-risk tasks identified (21%): irpc-removal, endpoint, operation-context,
operation-env, call-adapter, abort-cascade.
Phase 1 of the integration plan modifies alknet-core to support the
architectural changes from Phase 0 ADRs and specs. Decomposed into
dependency-ordered tasks across config split, identity, forwarding
policy, OperationEnv, interface abstraction, and NAPI reload API.
Critical path: config-split → identity → forwarding → wire-into-handler
→ interface-trait → ssh-interface-extraction → review.
Two highest-risk tasks (interface-trait-definition, ssh-interface-extraction)
are split from §1.8 per the integration plan's note that it may need
sub-phases. OperationEnv is split into types and runtime per Phase 1
local-dispatch-only constraint.