Commit Graph
29 Commits
Author SHA1 Message Date
glm-5.2 bf6ce957c0 docs(arch): tighten tls/endpoint/client specs — remove connect/connect_quic, move CallCredentials to core, shed alknet-call TLS deps
Review of the three new crates (alknet-tls, alknet-endpoint, alknet-client)
+ revised core found compile-blocking inconsistencies, stale claims, and
dep-graph contradictions. All resolved:

Critical:
- C1: CallClient::connect / ChannelClient::connect_quic REMOVED (not
  delegated) — keeping them as thin wrappers over AlknetClient::dial_quic
  would make protocol crates depend on alknet-client, contradicting the
  dep graph. Callers compose dial + take-over (2 lines).
- C2: alknet-client feature gates now pull alknet-core/quinn +
  alknet-core/iroh (for Connection::from_quinn_with_alpn / from_iroh).
- C3: rustls-native-certs + webpki-roots added to alknet-tls deps
  (always-present, not feature-gated — CA-verify path is transport-agnostic).

Warning:
- W1: CallCredentials/RemoteIdentity moved to alknet-core (from
  alknet-call) — the dial must not depend on the call protocol; not a
  two-way-door, it determines the dep graph.
- W2: webpki-roots fallback implemented in spec (ADR-088 §5 added) —
  the code claimed a fallback that never existed; now the store is never
  empty, NoRootAnchors unreachable, containerized deployments work.
- W3: EndpointError removed entirely (BindFailed + HandlerNotFound both
  vestigial after ADR-083); shutdown() is now infallible.
- W4: FingerprintPinVerifier moved to alknet-tls (from alknet-call) —
  alknet-call sheds quinn/rustls/rustls-pemfile/rustls-native-certs
  entirely; CallClient becomes a pure protocol crate.

Plus: ClientError removed (only produced by removed connect); S1
(CallCredentials → ClientVerifierContext mapping + auth_token stripped
at TLS boundary documented); amendment notes on ADR-017, ADR-069,
ADR-080, ADR-082, ADR-087, ADR-090; overview crate graph + README index
updated.

29 files, consistency-reviewed.
2026-07-16 11:33:18 +00:00
glm-5.2 34e3be2801 docs(arch): resolve OQ-67 — iroh proxy force-relay-only + HTTP-to-SOCKS5 bridge (ADR-090 §5 amended)
The iroh-proxy POC (/workspace/iroh-proxy-poc, 5/5 runs clean) settled
OQ-67: iroh does NOT expose a socket-injection hook for the IP/direct
transport (noq_endpoint() is pub(crate), the IP transport binds its own
netwatch::UdpSocket, CustomTransport operates on a separate CustomAddr
address space iroh's hole-punching doesn't route through). The quinn
POC's Socks5UdpSocket does not transfer to iroh. The decision: force
relay-only when a proxy is configured, via three stable public iroh
Builder knobs — clear_ip_transports() + addr_filter(relay_only) +
proxy_url. The peer sees the relay's IP; the relay sees the proxy's IP;
the client's real IP is hidden on both surfaces. No iroh fork required.

Because iroh's proxy_url expects an HTTP CONNECT proxy (not SOCKS5),
the integration runs a tiny local HTTP-to-SOCKS5 bridge (~80 lines) so
a single Socks5ProxyConfig covers all three dials uniformly: UDP
ASSOCIATE for dial_quic, CONNECT for dial_tcp_tls, force-relay-only +
HTTP-to-SOCKS5 bridge for dial_iroh.

The POC also corrected a factual error: iroh's proxy_url proxies the
relay WebSocket only, not pkarr/DoH (those use pkarr/hickory-resolver
directly). Acceptable for the force-relay-only config (QAD disabled);
spec text corrected.

Force relay-only forgoes iroh's direct-path latency advantage (negligible
for the hub deployment, which runs its own relay) and makes relay
availability a hard dependency — the intended privacy/availability
tradeoff; a caller that prefers availability over privacy for the iroh
path simply does not set the proxy.

- ADR-090 §5 amended: iroh force-relay-only decision + proxy_url
  coverage correction + HTTP-to-SOCKS5 bridge
- OQ-67: resolved (force relay-only)
- client README: iroh proxy row, bridge, limitations, ADR/OQ entries
- README/open-questions: OQ-67 resolved, Current State amendment
2026-07-16 09:18:56 +00:00
glm-5.2 b7d67e5a5f docs(arch): client-dial SOCKS5 proxy seam — ADR-090, OQ-67
AlknetClient gains an optional SOCKS5 proxy (with_socks5_proxy) so a
native client can hide its real IP from the hub. dial_quic routes QUIC
through SOCKS5 UDP ASSOCIATE (validated by the /workspace/quinn-proxy-poc
PoC — quinn's AsyncUdpSocket + new_with_abstract_socket is the extension
point, 5/5 runs clean); dial_tcp_tls routes through SOCKS5 CONNECT. The
proxy is invisible above the dial (Connection, dispatch, credentials,
TLS config all proxy-unaware) and the no-proxy path is the zero-cost
default (socks5 feature + fast-socks5 dep are opt-in).

SOCKS5 is the sole proxy protocol (covers both TCP and UDP, so no HTTP
CONNECT variant needed). The two distinct SOCKS5 concepts — the
client-dial proxy (ADR-090, transport-layer privacy) and the planned
alknet-socks5 channels data-channel handler (ADR-085 scope, a service
one side offers the other) — compose at the SOCKS5 protocol level
without alknet-type-level coupling.

iroh is the exception: dial_iroh does not consume Socks5ProxyConfig —
iroh's proxy_url covers the relay-exposure surface, but the
direct-connection peer-exposure case is OQ-67 (deferred(unclear) — the
pieces exist but the iroh socket-stack composition isn't clear; does
not block the first hub deployment, which uses QUIC/TCP+TLS).

- ADR-090: Client-Dial SOCKS5 Proxy Seam
- OQ-67: iroh Proxy Support (Direct-Connection Peer Exposure)
- client README: proxy section, struct/builder, Proxy error variant,
  socks5 feature gate, deps, assembly example, decisions/open-questions
- README/open-questions: index entries, Current State, OQ count 67/20
2026-07-16 08:42:33 +00:00
glm-5.2 ce7de57973 docs(arch): AlknetClient native dial seam — resolves OQ-55 (ADR-089)
Extract the deferred AlknetClient as a new crate alknet-client — the
client-side analogue of AlknetEndpoint. Three dial methods (QUIC +
TCP+TLS via TlsClientConfig, iroh via key) produce a Connection for
CallClient::spawn_dispatch / ChannelClient::from_connection to consume.
The deferral collapsed because ADR-086 gave the native endpoint type
three dial shapes within one endpoint type, ADR-087 broke the circular
hedge, and ADR-083 gave the server-side shape to mirror by symmetry.

Names the three concept layers that were tangled throughout the initial
development (deployment role / establishment side / ALPN-level category)
so the fix is legible. Names alknet/register as a dialable entry-point
ALPN (native registration, parallel to HTTP registration in OQ-58); its
wire protocol is deferred to OQ-66 (blocked on OQ-58's token model).

Cross-references updated across 11 existing docs (README, overview,
open-questions, OQ-55, tls, hub, core, channels README/overview/
channel-client, call client-and-adapters) to reflect OQ-55 resolved and
the new alknet-client crate. Architecture review passed (2 critical, 7
warnings — all addressed).
2026-07-15 12:50:13 +00:00
glm-5.2 43b8179304 docs(arch): TlsError shape — single enum, owned by alknet-tls (ADR-088, resolves OQ-63)
Grounded in the actual error-producing call sites (endpoint.rs server
side, call_client.rs client side) and the dependency-crate sources read
from the cargo cache (rustls 0.23.41, rustls-pemfile 2.2.0, rcgen 0.13.2,
quinn-proto 0.11.15, rustls-acme 0.12.1).

Decision: single #[non_exhaustive] enum, one variant per failure
category, owned by alknet-tls (not re-exported from core). Six variants:
CertLoad(io::Error), SelfSigned(rcgen::Error), Rustls(rustls::Error),
VerifierBuild(VerifierBuilderError), QuinnWrap(NoInitialCipherSuite)
[quinn-gated], AcmeConfig(String).

Three findings drove single-enum over thin wrapper: (1) for_quinn()
fails with NoInitialCipherSuite, not rustls::Error — a rustls::Error
wrapper cannot represent the for_quinn() failure; (2) rustls_pemfile::Error
is not a std::error::Error (no Display, no Error impl) so #[from] would
not compile — pemfile BufRead APIs return io::Error; (3)
WebPkiServerVerifier::build() returns VerifierBuilderError, not
rustls::Error — a thin wrapper cannot represent empty-CA-root-store as
a first-class failure.

Deliberately NOT variants: ACME EventError/OrderError (stream events,
logged not returned from new); unknown-raw-key fail-closed (handshake-
time rejection at dial time, not a config-construction error —
corrects OQ-63's original framing); provider init (infallible); resolver
construction (infallible).
2026-07-15 09:22:24 +00:00
glm-5.2 5941280bca fix(agents): break the hedging-at-the-root pattern — deferred(unclear), impacts field, reviewer detection
Address the root cause of rework-causing hedging: the architect was
put in a logical bind where it couldn't express justified uncertainty
('the pieces exist but the shape isn't clear yet'). The only options
were 'decide now' (premature) or 'deferred(scope)' (false — the
information isn't missing, it's un-synthesized). The agent picked
deferred(scope) with a circular blocking condition (OQ-64 blocked on
OQ-55, OQ-55 needs OQ-64) because there was no honest way to say 'I
can see the pieces but I can't see the shape.'

Changes to the architect role spec:
- Add deferred(unclear) state: the pieces exist but the composition
  isn't clear; resolution requires investigation (work through
  examples, POC), not waiting. Has an investigation target and an
  impacts field.
- Add 'Impacts' field to the OQ format: what does this block
  downstream? Be specific ('blocks the first hub deployment because
  the hub dials workers' not 'blocks the hub crate'). The triage
  signal that makes deferral urgency visible — the field that would
  have made the AlknetClient circular hedge visible.
- Add circular-reasoning guard to self-review: 'check that your
  blocking condition isn't a prerequisite of the thing you're
  deferring.'
- Trim anti-patterns #9-#11 (hedging synonyms catalog, ~40 lines):
  detection belongs in the reviewer, not the architect's self-review.
  The architect is too close to its own reasoning to see its own
  circular hedges.
- Trim door-types section (30→10 lines): keep the one-paragraph
  summary, cut the elaboration.

Changes to the architecture-reviewer role spec:
- Add Decision Quality (F) category: false-deferral check
  distinguishing three cases — (1) hedging on a resolved decision, (2)
  false deferral / circular hedge (the blocking condition is a
  prerequisite of the thing being deferred), (3) legitimate deferral.
- Add Impacts Field Coverage (G) category: check that unresolved OQs
  have specific impacts fields.
- Note: the Decision Quality category is often the highest-value
  check on poorly-defined projects — the architect cannot self-review
  it (circular reasoning is invisible from inside the circle).

Retrofit existing OQs:
- Add Impacts field to all 16 unresolved OQs (10 deferred, 6 open).
- Update OQ-63 (TlsError shape) to reflect ADR-087's client-side
  addition — the error type now covers both server and client
  variants.
- Move OQ-65 (WebSocket carrying channels) to alknet-http theme
  (done in prior commit; this commit adds its impacts field).
- Verified: no circular reasoning found in existing deferrals. The
  AlknetClient hedge (OQ-64) was the circular one; it's already
  resolved by ADR-087.
2026-07-15 07:35:55 +00:00
glm-5.2 77321a7e84 docs(arch): break the AlknetClient circular hedge — TlsClientConfig not blocked on dial (ADR-087, resolves OQ-64)
OQ-64 and OQ-55 were linked in a circular dependency: the client-side
TLS config was deferred behind the dial seam (OQ-55), but the dial
needs the TLS config. No second transport can dial until it has a TLS
config; the TLS config was deferred until a second transport dials.
Schrödinger's code — required and not required until observed.

ADR-087 breaks the circle by separating two concerns that were
conflated as 'the same seam':

1. TlsClientConfig — rustls::ClientConfig + ADR-034 verifier selection
   + ADR-084 crypto provider. Transport-agnostic. All decisions made.
   Buildable today. A PREREQUISITE for any dial, not a consequence of it.

2. The dial (AlknetClient::dial()) — transport-specific connection
   establishment. Extracting a transport-polymorphic dial from one
   shape (QUIC) would bake QUIC in. Legitimate deferral (OQ-55,
   unchanged).

The hub makes this non-optional: a hub dials out to workers it
supervises and to other hubs (hub-as-client). The first hub deployment
(web + native) dials workers over QUIC with the worker's fingerprint
pinned. There is no 'later' for the TLS config — it is on the critical
path for the first hub and for alknet-worker.

Changes:
- ADR-087: TlsClientConfig in alknet-tls, not blocked on OQ-55
- OQ-64: resolved (yes, alknet-tls provides TlsClientConfig)
- OQ-55: amended — only the dial seam is deferred; TLS client config
  is explicitly NOT part of the deferral
- TLS README: 'Server-only (for now)' section replaced with
  TlsClientConfig section; crate is no longer server-only
- Hub README: dial/supervision section references TlsClientConfig for
  outbound connections
2026-07-15 06:05:59 +00:00
glm-5.2 7d9b1ebad9 docs(arch): endpoint types and entry points (ADR-086) — resolves OQ-62
Name the three-endpoint-type model (web/native/iroh) and the
entry-point vs. endpoint ALPN distinction. This untangles the
hub/endpoint/ALPN-config confusion that OQ-62 hinted at:

- A hub composes a SUBSET of three endpoint types (web, native, iroh),
  each with its own identity model, auth model, and transport(s). A
  full hub runs all three; a minimal hub runs iroh alone (no public
  IP required). The first real use case is web + native. Corrects the
  hub README's 'must support TCP+TLS and QUIC' framing.

- ALPNs split into entry points (accepted without identity — h2,
  http/1.1, future alknet/register) and endpoints (identity required
  before dispatch — alknet/channels, alknet/call, alknet/ssh). This
  resolves OQ-62: split ALPN lists by endpoint type (Option B), because
  each endpoint type serves a different client class with different
  negotiable ALPNs. The assembly-layer wiring pattern is now guessable.

- Foundational handlers are two categories, not one: channels
  data-channel ALPNs (tunnel, socks5, fs, sftp — gated by channels,
  not in any TLS ALPN list) vs. SSH (an endpoint ALPN that wraps
  channels inside it, RFC 7250 keys, legacy compat, comes later).

Files OQ-65 (WebSocket carrying channels — the browser story update)
filed as a one-way-door question, open. ADR-048 is not superseded;
OQ-65 may extend it. The web config advertises alknet/channels by
default so the hub is ready if OQ-65 resolves to 'WebSocket carries
channels.'
2026-07-15 05:19:11 +00:00
glm-5.2 7610ec1f31 docs(arch): workspace scope correction (ADR-085) + tls spec review fixes
ADR-085 records the actual workspace scope: the mono-repo is the core
networking toolkit (substrate: core, tls, call, channels; deployment
shapes: hub, worker; foundational handlers: tty, http, ssh, tunnel,
socks5, fs, sftp; vault). Consumer repos (docker, agent) are separate
repos depending on the published core crates. The overview's crate
graph had been describing the wrong scope since ADR-003 — a flat
~12-crate workspace including DNS/messaging/NAPI while omitting
channels, hub, worker, and tls. This stale scope was a causal factor
in the 'assembly layer' hedging pattern: when the overview implies
everything lives in one repo but the architecture needs a hub/worker
composition layer not in the graph, the gap gets filled with
'assembly layer' as an escape hatch. The overview is rewritten to
match the real boundary.

TLS spec review fixes (from architecture review):
- C3: hub/worker/hub-worker terminology pointers (tls README + endpoint.md)
- W1: server-only statement + OQ-64 (client-side TLS helper, deferred)
- W2: ACME task lifecycle semantics (returns immediately, no first-cert await)
- W3: remove stale EndpointError::TlsConfig variant
- W4: update stale ALPN section for two-config hub
- W5: add alknet-tls to hub dep graph (assembly-layer dep)
- W6: trim inline rationale -> point to ADR-084
- W7: ADR-084 status dependency note

New open questions:
- OQ-62: ALPN list sharing for two-config hub (open, high)
- OQ-63: TlsError shape (open, high)
- OQ-64: client-side TLS helper (deferred, blocked on OQ-55)
2026-07-14 12:46:16 +00:00
glm-5.2 34729c7846 docs(arch): resolve OQ-59 (fingerprint stays in core) + ADR-084 (aws-lc-rs crypto provider)
OQ-59 resolved to Option A: fingerprint.rs stays in alknet-core. The
client-side FingerprintPinVerifier in alknet-call uses fingerprint
functions and must not depend on alknet-tls (which would pull TLS setup
infra into client-only deployments). The rustls dep in core is narrow —
production fingerprint code uses only sha2 + manual DER parsing; the
rustls::sign usage is a test helper only. alknet-tls re-exports the
fingerprint functions for convenience.

ADR-084: aws-lc-rs as the TLS crypto provider on all server + client
config paths. Records the decision that was already in the code (to
match iroh's tls-aws-lc-rs feature) but had no ADR. FIPS-capable, broad
platform support, consistent across quinn/iroh/TCP+TLS/client. Switching
to ring or process-default requires a new ADR. ADR-082's
behavior-preservation invariant now references ADR-084 for the decision
record.
2026-07-14 09:53:43 +00:00
glm-5.2 2abe8f1872 docs(arch): TCP+TLS as first-class owned transport — resolves OQ-60, dissolves OQ-61
ADR-083 revised: TCP+TLS moves from an external sibling loop calling
public dispatch to a first-class owned transport via
with_tcp_tls(listener, acceptor), running inside run() alongside the
quinn and iroh accept loops. The endpoint owns all its accept loops;
shutdown() stops them all. The multi-owner shutdown problem (OQ-61)
does not arise — dissolved.

The reason TCP+TLS was structurally excluded (ADR-010 Am. 1: the
endpoint built transports internally, TCP+TLS couldn't fit) is gone
after ADR-083 — the endpoint no longer builds transports; it runs
accept loops on whatever it's given. TCP+TLS is a listener transport,
same shape as quinn and iroh. ADR-010 Amendment 2 supersedes Am. 1's
struct-level exclusion.

dispatch stays public — but for genuinely external shapes (SSH channels,
future WebTransport streams), which are connection-internal multiplexing,
not listener transports. The listener-vs-multiplexing distinction is now
explicit.

OQ-60 resolved: the TCP+TLS loop lives in alknet-core behind a tcp
feature (owned by the endpoint); builder functions are inlined by the
assembly layer. A alknet-transport crate was rejected — it would contain
only trivial builders; the real component (the loop) is in core. Hub-
specific composition lives in the hub crate; transport runtimes that any
node might need live in core.

Updated: ADR-010 (Amendment 2), ADR-082 (TCP+TLS loop location), ADR-083
(revised), core/endpoint.md (struct + dispatch + shutdown), hub/README.md
(transport table + assembly example + stale sibling references),
tls/README.md (endpoint section + TCP+TLS loop location + references),
open-questions.md (OQ-60 resolved, OQ-61 dissolved).

Review: zero critical issues, five warnings fixed (stale hub README
prose, stale core endpoint.md struct/dispatch listings, stale ADR-082
TCP+TLS loop location, stale TLS README reference entry, hub front-matter
date).
2026-07-14 08:58:50 +00:00
glm-5.2 81bde6f28f docs(arch): endpoint as pure accept-loop runner + acme-tls/1 guard relocation (ADR-083, OQ-60/61)
ADR-083: AlknetEndpoint becomes a pure accept-loop runner with a public
dispatch method. Transport construction moves out of the endpoint — the
assembly layer reads StaticConfig, builds transports from
TlsServerConfig(s), and hands pre-built quinn/iroh endpoints to the
endpoint via with_quinn/with_iroh. TCP+TLS dispatch is first-class (same
dispatch path as quinn/iroh); ADR-010 Amendment 1's duplicated-dispatch
workaround is retired. StaticConfig stays in core as the assembly-layer
config; the endpoint takes only drain_timeout.

The acme-tls/1 guard moves from dispatch_quinn to the shared dispatch
method — ACME TLS-ALPN-01 challenges arrive over TCP (CAs validate via
TCP to port 443, not QUIC), so the guard's quinn-specific location was a
latent bug once TCP+TLS exists. The guard is transport-agnostic; the
rationale (no handler, silent close) is unchanged. ADR-027 §5 amended.

OQ-60: where build_iroh_endpoint lives (assembly layer / alknet-tls
helper / transport module). build_quinn_server_config_from_rustls is
decided — it moves to alknet-tls as for_quinn() per ADR-082; only
build_iroh_endpoint is genuinely undecided.

OQ-61: multi-owner shutdown coordination. Boundary committed (endpoint
owns dispatched handlers; assembly layer owns spawned accept loops);
mechanism open.

ADR-082 amended: drops the Arc<TlsServerConfig> endpoint signature
(superseded by ADR-083); keeps its scope as the alknet-tls crate's
TlsServerConfig + accessors.

Review: zero critical issues, five warnings fixed (build_iroh_endpoint
destination contradiction, undeclared shutdown_sender, missing ADR-027
amendment marker, underspecified dispatch no-match behavior, stale
door-type timing clause).
2026-07-14 07:59:04 +00:00
glm-5.2 192bd0a5a2 docs(arch): spec alknet-tls crate — shared TLS config across quinn + TCP+TLS + iroh (ADR-082, OQ-59)
The TLS setup in alknet-core is welded to quinn: build_rustls_server_config
produces a rustls::ServerConfig, then build_quinn_server_config_from_rustls
consumes it into quinn::ServerConfig — the rustls config is moved, not
shareable. ACME is worse: the AcmeState task is spawned inside the quinn
endpoint, so a TCP+TLS listener would need a second ACME state machine
(two orders for the same domain, two cert caches, Let's Encrypt
rate-limit risk).

alknet-tls extracts the TLS setup into a shareable TlsServerConfig:
- TlsServerConfig::new(identity, alpns) builds the rustls::ServerConfig
  once (including the ACME state machine if ACME)
- for_quinn() clones it for quinn (QuicServerConfig::try_from)
- for_tcp_tls() clones it for tokio-rustls (TlsAcceptor::from)
- rustls_config() borrows it for any other consumer
- One cert, one ACME state machine, N transports

TlsIdentity and Ed25519SecretKey stay in core (config types).
fingerprint.rs stays in core (shared by server endpoint + client
FingerprintPinVerifier in alknet-call — OQ-59 tracks whether it should
move; likely stays because moving would force alknet-call to depend on
alknet-tls, pulling TLS infra into client-only deployments).

iroh shares the key, not the rustls config — iroh has its own TLS built
into the Endpoint (takes iroh::SecretKey, not rustls::ServerConfig).
alknet-tls has no for_iroh() method; the assembly layer passes
Ed25519SecretKey to iroh directly.

Feature gates: quinn / tcp / acme as independent opt-ins. rustls always
present. tokio-rustls only when tcp. quinn only when quinn. rustls-acme
only when acme.

Files:
- crates/tls/README.md: crate spec (TlsServerConfig API, what moves/
  stays, feature gates, deps, behavior-preservation invariants)
- decisions/082-alknet-tls-extraction.md: the ADR (Proposed)
- questions/059-fingerprint-module-location.md: OQ-59 (should
  fingerprint.rs stay in core or move to alknet-tls? open, two-way,
  likely stays)
- open-questions.md: OQ-59 added to alknet-core table
- README.md: tls doc table row + ADR-082 row added

Review (architecture-reviewer): 3 critical (missing
build_quinn_server_config_from_rustls in README table, missing futures
dep, two TlsServerConfig code blocks disagreed on feature gates), 7
warnings (behavior-preservation invariants: max_early_data_size,
aws_lc_rs provider, verifier scheme list, acme-tls/1 ALPN;
rustls-pki-types dep; fingerprint.rs rustls-usage imprecision; ADR
missing acme-tls/1), 4 suggestions. All criticals and warnings addressed.
2026-07-13 10:58:45 +00:00
glm-5.2 b0cc0a01b7 docs(arch): unweld CallClient from QUIC — spawn_dispatch primary, connect convenience (ADR-017 Am. 2026-07-13)
Same fix as ADR-080 (ChannelClient) applied to the call crate. The
existing code already had the right structure — spawn_dispatch is not
feature-gated (transport-agnostic), connect is #[cfg(feature = "quinn")]
— but the docs inverted the framing: connect was 'primary,' spawn_dispatch
was 'lower-level.' That welding masked the call protocol's
transport-agnosticism (ADR-012 EventEnvelope; ADR-065 from_stream/from_bidi
accept any AsyncRead + AsyncWrite).

Changes:
- client-and-adapters.md: reframe spawn_dispatch as the transport-
  agnostic primary constructor (one-way door), connect as the QUIC
  convenience (two-way door, feature-gated on quinn). Mirror the
  from_connection / connect_quic pattern from ADR-080/channel-client.md.
  Fix all 'QUIC-backed' / 'over QUIC' / 'opens a QUIC connection' framing
  to be transport-agnostic. Fix from_call description, adapter location
  map, exchange-of-operations example, Constraints section.
- call-protocol.md: 'runs over QUIC bidirectional streams' → 'runs over
  any ordered, reliable bidirectional stream.' Fix stream model, stream
  lifecycle (connection drop / stream reset now list all transports,
  not just QUIC). Fix CallConnection.connection doc comment.
- operation-registry.md: FromCall provenance comment 'QUIC forwarding
  stub' → 'call-protocol forwarding stub.' Fix from_call description.
- call README.md: fix client-and-adapters.md description, fix design
  principle #11 framing.
- ADR-017: add Amendment (2026-07-13) documenting the spawn_dispatch /
  connect reframe, mirroring ADR-080's amendment.
- overview.md, architecture README: update ADR-017 and
  client-and-adapters.md table summaries.
- OQ-015, OQ-007: fix 'opens QUIC connections' / 'bidirectional QUIC
  streams' framing in resolution text.

No code changes — spawn_dispatch and connect already exist with the
right feature-gate structure. This is a documentation reframe.
2026-07-13 09:53:43 +00:00
glm-5.2 a26401aadd docs(arch): rewrite hub README for multi-transport + channels substrate; add OQ-58 (worker registration)
The hub README predates channels (2026-07-09) and was built on the
call-protocol-directly-over-QUIC model: 'One QUIC connection per peer
carrying the call protocol,' CallClient::connect as the dial path,
QUIC as the only transport. The channels crate replaced that substrate
(ADR-071/079/080), and the hub's primary use case (worker provisioning
on docker/vast.ai/runpod) requires TCP+TLS for registration and QUIC
or TCP+TLS for the ongoing session — coexisting on one hub, not as
alternatives.

Rewrite:
- Multi-transport stated as decided: 'A hub MUST support TCP+TLS and
  QUIC endpoints simultaneously.' TCP+TLS accept loop (ADR-010 Am. 1)
  lives in the hub, shares the HandlerRegistry with the quinn endpoint.
- Channels substrate: the hub uses ChannelsAdapter/ChannelClient
  (from_connection primary, connect_quic convenience — ADR-080),
  not CallClient/CallAdapter directly. One channels connection per
  peer; CallAdapter runs on channel 0 (ADR-072).
- Transport-agnostic dial/accept: dial_worker_connection takes a
  Connection (one-way door); connect_quic_worker is a two-way-door
  convenience. supervise_worker takes a dial closure, not a SocketAddr.
- Identity over transports: fingerprint path (QUIC+raw-key,
  X.509 client cert) via resolve_from_fingerprint; bearer-token path
  (TCP+TLS no client cert, WebTransport, WebSocket) via
  resolve_from_token on the call first frame. Both resolve to the
  same PeerEntry (ADR-030/034).
- Worker registration flow (6 steps): provision → token → download →
  key gen → HTTP POST over TCP+TLS → channels connect. Step 4 is HTTP
  on HttpAdapter; step 6 is channels over QUIC or TCP+TLS. The hub
  creates a mixed-fingerprint PeerEntry (ADR-034 §3) at registration.
- OQ-58: worker registration flow — enrollment-token model,
  endpoint shape, register_worker API. Open (decision-ready, not
  blocked), one-way door, high priority.
- ADR-081: fix stale references to channels-hub/channels-worker
  sub-crates (the ADR's Decision says they don't exist; the References
  section said they did).

Review (architecture-reviewer): 3 critical (ADR-081 stale refs;
bearer-token extraction flow; assembly example QUIC-only +
unspecced into_connection), 7 warnings (registration 'or'→'both',
channel/control translation, OQ-52 interim, HubError variant naming,
RegistrationError definition, X.509-client-cert prose, adapter
ownership phrasing), 5 suggestions. All criticals and warnings #4/#7/#8
addressed; #5/#6/#9/#10 addressed; #11-15 noted as optional.
2026-07-13 09:22:32 +00:00
glm-5.2 73c621bf21 docs(arch): unweld ChannelClient from QUIC — from_connection primary, connect_quic convenience (ADR-080 amendment)
The channels protocol is transport-agnostic by design (ADR-071 substrate
modes; ADR-065 unwound the server-side QUIC-welding via
Connection::from_stream/from_bidi). ADR-080 had welded the client-side
one-way-door API to QUIC and masked it as a deferral ('QUIC-only
initially', 'can be generalized later') — anti-patterns #8/#9/#11
(door-type-as-deferral + resolved-with-escape-hatch). 'Can be
generalized later' meant 'can be rewritten later' — the expensive
reversal the one-way-door classification exists to prevent.

Fix: split the constructor surface.
- from_connection(connection: Connection) — transport-agnostic primary,
  one-way door. Mirrors server-side ChannelsAdapter::handle(Connection)
  and the existing CallClient::spawn_dispatch pattern.
- connect_quic(addr, credentials) — QUIC convenience, two-way door,
  additive. Future connect_tcp_tls / connect_webtransport join it
  without touching the one-way-door surface.

OQ-55 reframed: the deferred thing is the shared dial+TLS seam
(AlknetClient), not a QUIC-welded client API. The client take-over APIs
(CallClient::spawn_dispatch, ChannelClient::from_connection) are
transport-agnostic and decided; only the shared dial across transports
is blocked on a second transport's dial existing.

Files:
- decisions/080-channelclient.md: amendment section, Decision code
  block, Transport-agnostic by construction (replaces QUIC-only
  initially), Consequences, Door type, subscribe_resources added to
  Decision block (was referenced by Door type but missing)
- crates/channels/channel-client.md: from_connection primary API,
  Transport-agnostic by construction section, OQ-55 relationship
- crates/channels/overview.md, crates/channels/README.md,
  crates/core/README.md, README.md, open-questions.md,
  questions/055-...md: cross-reference summaries aligned
2026-07-12 19:33:36 +00:00
glm-5.2 2313c51f12 docs(arch): add alknet-channels specs — ADRs 071-080, 7 spec docs, OQ-56/57
Phase 1 architecture for alknet-channels (multiplexing proxy on
alknet/channels). Grounded in the completed de-risk POC (28 tests) and the
landed ADR-070 (BidiStreamSource trait + Connection::from_source).

ADRs:
- 071: 9-byte chunk wire format (generalizes TTY's 5-byte)
- 072: channel 0 pre-negotiated as alknet/call (no special control plane)
- 073: channel lifecycle operations on the call protocol — channel/open,
  close, control, resources/subscribe; direction field pinned; subscribe
  from day one (not poll-for-v1 — StreamingHandler machinery exists)
- 074: ChannelBidiStreamSource implements BidiStreamSource (ADR-070);
  into_sub_streams() typed accessor for TTY; accept_bi() generic path
- 075: ChannelsAdapter + ChannelManager; REQ-CH-01..04 wire invariants
- 076: bounded-buffer backpressure (1 MiB), 256-channel cap, monotonic IDs
- 077: TTY inside channels uses sub-streams, not own wire format;
  amends ADR-052 scope to direct-connect TTY; channels feature on tty
- 078: two-pump shutdown-on-completion contract (handler-level)
- 079: hub relay translates channel 0, byte-forwards data channels
- 080: ChannelClient (QUIC-only); AlknetClient core extraction deferred (OQ-55)

Spec docs: overview, channels-wire, channels-connection, channels-adapter,
channel-operations, channel-client.

OQ-56 (full windowing) and OQ-57 (two-pump helper extraction) are genuine
deferred(scope) deferrals with concrete blocking conditions; the contracts
are decided, only the extensions are deferred.

Hedging audit converted three research hedges into decisions:
resources/subscribe (not poll), server-assigned IDs (not if-zero-RTT),
bounded-buffer (not if-HOL-becomes-a-problem).
2026-07-12 12:57:18 +00:00
glm-5.2 f8f5f27ce0 docs(arch): land ADR-070 BidiStreamSource + OQ-55 AlknetClient deferral + impl task
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).
2026-07-12 09:43:39 +00:00
deepseek-v4-pro deea6de38a docs(arch): remove channels from hub spec — channels are research-phase, not specced
The channels concept (docs/research/alknet-channels/phase-0-findings.md)
was developed in a separate session and is research-phase, not
architecture. The hub spec was inadvertently built assuming a channel
model that doesn't exist yet.

Changes:
- Remove all channels references from hub README (subtitle, channel
  model section, channel management bullet, channel proxying section,
  'does NOT do' bullet, references)
- Remove OQ-55 (channel/open operation) — channels research has its
  own question tracking (OQ-CH-01 through OQ-CH-07)
- Hub spec now describes what it actually provides: peer lifecycle,
  aggregated operation env, service discovery. One QUIC connection
  per peer carrying the call protocol. No channels, no future
  multiplexing, no research references.
2026-07-11 07:52:02 +00:00
deepseek-v4-pro 5d56bae1ba docs(arch): fix inbound worker hook — callback inside handle(), not post-hoc
- Replace on_worker_connected() post-hoc call with WorkerConnectedCallback
  that fires inside CallAdapter::handle(). handle() blocks until disconnect
  — there is no 'after handle accepts' point for the assembly layer to
  hook into. The callback carries both on_connected (from_call + attach_peer)
  and on_disconnected (detach_peer + drop channels).

- Add CallAdapter::with_worker_connected_callback(callback) builder method.

- Consolidate duplicate WorkerConnectedCallback struct definitions.

- Fix channel role: 'channel proxying' → 'channel management'. The hub
  tracks channels; it does not proxy streams. What the caller does with
  the resulting channel is the assembly layer's business.

- Resolve OQ-54: callback is the committed design. Update OQ file and
  open-questions.md table.
2026-07-11 07:46:00 +00:00
deepseek-v4-pro b38b1d28a7 docs(arch): channel model — call protocol as control plane, any ALPN as data plane
Replace the 'future strategies' section with the channel model:

- Channel 0 is the call protocol — the universal control plane. Handles
  operation discovery (from_call), operation routing (invoke_peer),
  service discovery, and channel negotiation (channel/open, channel/close,
  channel/list).

- Channels 1..N carry any ALPN as data planes. Opened via channel/open on
  the call protocol. Each channel is a bidirectional QUIC stream, wrapped
  as Connection::from_bidi (ADR-065), and handed to the same
  ProtocolHandler::handle() that handles dedicated connections. The
  handler does not know it's on a multiplexed channel.

- Channel negotiation is symmetric — either side can open a channel.
  Same pattern as from_call: bidirectional, symmetric, negotiated over
  the call protocol.

- The hub's role for channels 1..N is transparent stream proxying. The
  hub does not interpret the protocol; the client and worker speak the
  ALPN directly.

- Hub struct gains channel tracking (PeerId → ChannelId → ChannelInfo).
  HubError gains ChannelAlreadyOpen, ChannelNotFound, ChannelOpenFailed.

- New OQ-55: channel/open operation spec (deferred to call-protocol
  implementation phase).
2026-07-11 07:41:07 +00:00
glm-5.2 ab963d0e46 docs(arch): resolve OQ-050 — include docker/system/events in v1
Resolve the deferred docker system events subscription question.
docker/system/events is now a v1 Subscription operation using the
same StreamingHandler pattern already wired for logs, exec, and
image/pull. The internal ownership-store subscription for stale-entry
cleanup on destroy events is a follow-up refinement.

Scrub hedging language from ADR-060 and docker specs:
- Remove 'marginal gain' / 'future feature is additive' framing
- Replace 'no reaper' / 'not promptly cleaned up' with clean
  statement that events subscription provides the prompt-cleanup path
- Remove stale-entry policy from ADR-060's two-way door classification
2026-07-10 07:31:55 +00:00
glm-5.2 87b2c2a5ef docs(arch): hub-wiring cluster — aggregated env, peer_operations, from_call cleanup, alknet-hub crate spec
Three ADRs addressing gaps surfaced by the first hub consumer (alkapi):

- ADR-067: Aggregated peer-env wiring — Dispatcher::with_aggregated_env hook
  so compose_root_env reads a shared PeerCompositeEnv across all calls,
  not a fresh per-call one. The hub-defining gap (alkapi OQ-08 / G.1).

- ADR-068: PeerCompositeEnv::peer_operations override — adds
  list_operation_names() to OperationEnv, overrides on OverlayOperationEnv
  and PeerCompositeEnv. Fixes services/list-peers returning empty operation
  lists for non-local peers (alkapi G.6).

- ADR-069: from_call is a manual free function, not auto-wired — reverses
  the aspirational OQ-27 resolution to match the implementation. Cleans
  the 'v1 default' hedging language in ADR-017 and client-and-adapters.md
  (alkapi G.4).

New crate spec: alknet-hub — reusable hub pattern (aggregated env,
connection lifecycle, worker supervision with backoff, service discovery).

Spec fixes: builder API drift (with_local/with_local_streaming separation),
ScopedOperationEnv → ScopedPeerEnv type name, OQ count 51→54.

Three new OQs: OQ-52 (wait_for_close), OQ-53 (backoff defaults),
OQ-54 (inbound hook placement).
2026-07-09 16:01:30 +00:00
glm-5.2 2aa6363e57 docs(arch): sync specs to transport generalization sweep (ADR-064, ADR-065)
Three code commits landed a clean sweep discovered when building an
external app against the crates. This syncs the architecture specs to
match the codebase and amends the affected decisions.

ADR-064 (supersedes ADR-005): irpc was never integrated — no .rs file in
the workspace ever imported it. The wire protocol (wire.rs) is hand-rolled
length-prefixed JSON; the EventEnvelope shape was derived from the
@alkdev/pubsub TypeScript prior art (ADR-013), not from irpc. ADR-005's
premise ('irpc as the call protocol foundation') was never implemented as
stated. Superseded with a clear header; the body is kept as historical
record.

ADR-065: Connection::from_stream / from_bidi — Connection now accepts any
AsyncRead + AsyncWrite pair via ConnectionKind::Stream (yield-once
accept_bi contract: QUIC yields many streams, everything else yields one
then ConnectionClosed). Unblocks TCP+TLS, SSH channel dispatch,
WebTransport streams, and wasm streams through the same HandlerRegistry
as QUIC connections, with zero handler code changes. MockConnection /
ConnectionKind::Mock removed (tests use from_stream with sink/empty).
Stream-level Mock variants renamed to Stream (they were already generic).

Amended ADRs: ADR-003 (irpc removed from dep table), ADR-007 (from_stream
opens the server-side door; MockConnection removed), ADR-010 (TCP+TLS can
now dispatch through the registry via from_bidi; iroh 1.0 migration noted).

Updated specs: core-types.md (Connection/SendStream/RecvStream sections),
endpoint.md (TCP section, iroh note), call-protocol.md, operation-registry
(irpc Integration section replaced), overview.md, http-server.md,
webtransport.md, call README. OQ-09 (WASM) resolution amended: the
Connection door is now open via from_stream; the accept-loop runtime door
remains closed (tokio doesn't run on WASM).

See docs/research/transport-generalization/findings.md for the full trace.
2026-07-09 07:40:51 +00:00
glm-5.2 1ce11717d8 docs(arch): draft alknet-docker architecture specs (ADRs 058-063, OQs 048-051)
Greenfield architecture spec set for the alknet-docker crate — a thin,
single-host bollard wrapper exposing docker container/image operations
as call-protocol ops on the shared alknet/call ALPN, plus a
DockerTtyBackend (impl TtyBackend) behind a tty feature for interactive
terminal sessions into containers over alknet/tty.

Six ADRs:
- 058: docker ops on alknet/call (no separate ALPN; raw-carriage handoff
  dissolved by alknet-tty extraction — interactive attach moved to
  alknet/tty via DockerTtyBackend, no carriage field on call.requested)
- 059: bollard 0.21 (verified current on crates.io) + feature selection
  (http+pipe+time; no ssl/ssh/websocket/buildkit)
- 060: container resource model (ADR-050 application) — alknet.managed/
  alknet.owner labels, list owned_only flag, hosted-services operator
  role via static-resource fallback, handler-driven revoke with
  autonomous-death tolerance, resource-action vocabulary
- 061: DockerTtyBackend in alknet-docker behind tty feature (attach vs
  exec mode; POC drive_attach_raw as reference)
- 062: Docker client + OwnershipStore injection via closure capture
  (not Capabilities, not OperationContext — matches from_openapi pattern)
- 063: exit code on terminal call.responded for non-interactive exec
  (call.completed stays empty, ADR-012 unchanged)

Four spec docs (crates/docker/): README, overview, docker-operations,
docker-tty-backend. Four deferred-scope OQs (048-051): network/volume
ops, buildkit, system events subscription, create options surface.

Updates the tty-backend.md Backend implementations table (DockerTtyBackend
row now specced) and the architecture README (doc table, ADR table,
current-state paragraph, OQ count).

Grounded in the alknet-docker POC (docs/research/alknet-docker/poc-summary.md)
which validated the hard parts; the remaining lifecycle ops are mechanical
bollard wrapping. Reviewed by architecture-reviewer subagent; criticals
(the docker_client injection model conflicting with the Capabilities
contract) resolved via ADR-062/063 before commit.
2026-07-08 09:29:22 +00:00
glm-5.2 25f0f55592 docs(arch): alknet-tty spec sanity-check fixes — TtyControlHandle newtype (C1), backend cleanup contract (W1, ADR-056), clarifications (S1-S5)
C1: Box<dyn TtyControl + Clone> does not compile (Clone is not object-safe).
Replace with a TtyControlHandle newtype: #[derive(Clone)] wrapping
Arc<dyn TtyControl + Send + Sync>. The trait stays object-safe; the
newtype carries the Clone-ability. Updated across tty-backend.md,
ADR-053, OQ-43, and the tty README.

W1: 'Drop is the cleanup, threads exit on channel close' was false for
the local PTY waiter thread (blocked in Child::wait(), does not observe
channel close) — a child that ignores stdin EOF would be orphaned on
session cancel. New ADR-056 commits a backend cleanup contract: dropping
the exit_code future MUST kill the session target. The local backend
implements it via a ChildKiller held in the exit_code future's Drop
guard (disarmed on resolve). Corrected the lifecycle section in
tty-adapter.md, added the mechanism + constraint to tty-local.md, and
referenced ADR-056 from tty-backend.md, overview.md, both READMEs.

S1: error-frame < 16 MiB stated as a wire-format invariant (not an
assumption) so the 0x00-first-byte disambiguation trick is sound by
construction.
S2: carriage field validation (MUST be "raw" in v1, else
malformed_negotiation) specified.
S3: NegotiateRequest + TerminalParamsWire Rust structs added with
serde defaults and validation rules.
S4: modes field annotated 'backends MUST ignore content in v1' in both
tty-backend.md and ADR-053.
S5: AsyncWrite/Stream qualified with crate origins (tokio::io,
futures_core) in the TtyHandle struct definitions.
2026-07-07 10:38:49 +00:00
glm-5.2 db4da1e6f0 docs(arch): resolve OQ-45 — no application-level windowing; QUIC per-stream flow control suffices
The backpressure chain is complete by construction: QUIC flow control
→ bounded drainer channel → bounded stdout channel → OS pipe/PTY buffer
→ process write() blocks. Every link awaits its producer; no unbounded
buffer breaks the chain. The reversal path (an additive ControlMessage
variant, not a wire-format header change) is noted in ADR-052's
consequences as a two-way door, not the expected path.

Tightened ADR-052 assumption 1 from a hedge ('expected to work; a POC
would confirm') to a decided constraint. Updated all OQ-45 references
across the tty spec docs from 'open (low risk)' to 'resolved'.
2026-07-07 09:36:18 +00:00
glm-5.2 df5d04af1d tasks(arch): adopt taskgraph for architecture work — blocker tasks for deferred OQs, level mapping, status fixes
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
2026-07-06 16:34:01 +00:00
glm-5.2 1baa619ce9 docs(arch): decompose open-questions.md into per-OQ files under questions/
The monolithic open-questions.md (1310 lines, 47 OQs) was large enough to be
unmanageable, with high size variance (OQ-42 at 220 lines next to OQ-06 at 8).
Decomposed into one file per OQ under docs/architecture/questions/ (NNN-slug.md,
mirroring the ADR convention), with open-questions.md retained as the index:
theme-grouped tables plus a cross-theme Deferred/Blocked section that surfaces
the 6 deferred OQs with their Blocked-on conditions inline (the safe-exit
visibility surface). Per-OQ content moved verbatim; all 62 inbound links stay
valid (none used anchors). README's curated OQ summary dropped (now redundant
with the index tables).

Also seeds tasks/architecture/ with this task plus two follow-ups found during
the decompose: OQ-09/10 missing structured Blocked-on fields, and the
tasks/architecture/ blocker-task half of the Safe Exit protocol being
unenforced.
2026-07-06 16:07:59 +00:00