Commit Graph
100 Commits
Author SHA1 Message Date
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 9823c6e4ab docs(research): TCP+TLS first-class dispatch, not sibling afterthought
The endpoint refactor findings doc relegated TCP+TLS to a 'sibling
accept loop' that duplicated the endpoint's dispatch logic at the
assembly layer. That was ADR-010 Amendment 1's workaround for the
endpoint being welded to quinn — not a deliberate design.

The endpoint now exposes a public dispatch() method so every transport
(quinn, iroh, TCP+TLS, future SSH, future WebTransport) calls the same
dispatch path: handler lookup, build_auth_context, spawn. The
transport-specific parts (ALPN extraction, fingerprint extraction,
Connection construction, no-handler close) happen before dispatch is
called. No duplicated logic at the assembly layer.

Also: the acme-tls/1 guard is quinn-specific (ACME challenges arrive
over QUIC); a TCP+TLS listener serving HTTPS does not advertise
acme-tls/1. build_auth_context becomes a private helper called by
dispatch, not a standalone function.
2026-07-13 13:54:57 +00:00
glm-5.2 e06446ae9d docs(research): endpoint-as-pure-ALPN-dispatcher findings — transport construction moves to assembly layer
Captures the full picture of the AlknetEndpoint refactor interleaved
with the alknet-tls extraction:

- AlknetEndpoint becomes a pure accept-loop runner (no transport
  construction, no StaticConfig, no tls_identity reading)
- A hub holds one or two TlsServerConfigs (raw key + X.509), not one;
  cert-reuse is within each identity, shared across that identity's
  transports
- TCP fallback for native clients uses the raw-key config (no X.509
  needed); raw-key/X.509 mixing works in the TLS handshake
- iroh and SSH share the Ed25519SecretKey, not the TlsServerConfig
- The reverse-proxy as reference for ACME lifecycle and live renewal
- The remaining tls spec review items (C2-C6, W3-W5) carried forward
2026-07-13 13:41:00 +00:00
glm-5.2 837f94f2aa docs(arch): alknet-tls review fixes — dep claim, Clone clarity, dedup, ADR refs
C1: Correct dep-change claim — only rustls-pemfile, rcgen, rustls-acme
leave core; quinn, iroh, ed25519-dalek stay (endpoint struct, accept
loops, and Ed25519SecretKey remain in core).

W1: Trim duplicated 'Why' section in README to summary + ADR-082 link;
keep the three-use-cases table as reference.

W2: Clarify TlsServerConfig is not Clone (holds JoinHandle); share via
Arc, accessors clone the inner rustls::ServerConfig. Fix in README and
ADR-082.

W6: Resolve futures dep inconsistency — acme-gated in both README and ADR.

S1: Behavior-preservation invariants now reference ADR-027 as their
origin.

S2: Document for_quinn fallibility (QuicServerConfig::try_from can fail)
vs for_tcp_tls infallibility (TlsAcceptor::new cannot fail).
2026-07-13 11:46:58 +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 3006e29afc docs(arch): control format is ALPN-specific (not JSON-binding); hub/worker are consumers not sub-crates
Two refinements from the review:

1. Control stream_types (3/4/5) carry ALPN-specific payloads, not
   JSON. The channels layer is blind to what control stream_types carry —
   it reassembles bytes and delivers them to the handler. TTY happens to
   use JSON for its control channel because its control messages map
   cleanly to JSON; another ALPN might use a binary format. The channels
   layer does not mandate JSON on control stream_types, the same way it
   doesn't mandate a format for data stream_types. This prevents the
   TTY/PTY JSON constraint from becoming a binding constraint on all
   future channel types. (ADR-071, channels-wire.md)

2. Hub and worker are consumers of channels, not sub-crates. The existing
   alknet-hub crate IS the channels hub — it depends on channels-call and
   uses channels as its substrate, with the relay logic (ADR-079) living
   in alknet-hub. A worker is any crate that uses ChannelClient to dial.
   There are no channels-hub or channels-worker sub-crates. The dependency
   direction is: alknet-hub → channels-call → channels-core → alknet-core;
   worker → channels-call → channels-core → alknet-core. The channels
   crate has no dependency on alknet-hub or any worker crate. (ADR-081,
   overview.md)
2026-07-12 17:36:24 +00:00
glm-5.2 fd83fc1685 docs(arch): channels substrate simplification + stream_type decomposition + sub-crate split
Three simplifications to the channels spec, all flowing from the review:

1. Substrate simplification (ADR-071 revised): the 9-byte chunk header is
   used in ALL substrates — in-line (TCP+TLS, WebTransport), native (QUIC
   bidi streams), and multi-connection. The ChannelsAdapter reads headers
   off every bidi stream it accepts; the transport's native multiplexing
   is a performance optimization (independent flow-control windows), not a
   protocol change. One wire format, one code path, one handler experience.
   The channel_id in the header is the correlation key across substrates.

2. Stream_type decomposition (ADR-071 revised): every stream_type is
   unidirectional. Bidirectionality is two stream_types (write + read), not
   one 'bidirectional' stream_type. Grouped in threes: 0/1/2 = data
   write/read/err, 3/4/5 = control write/read/err, % 3 formula. This
   resolves the TTY control channel's 'not actually bidirectional' flaw —
   control is now 3 (write, client→server) + 4 (read, server→client), each
   with its own reassembly buffer and EOF. Channel 0 uses [0,1] (call frames
   bidirectional via 0=in, 1=out). TTY uses [0,1,2,3,4]. ADR-072, 073, 074,
   077 updated for the new stream_type assignments.

3. Sub-crate decomposition (ADR-081 new): channels-core (pure multiplexer —
   wire format, demux/mux, ChannelBidiStreamSource, ChannelManager; depends
   on alknet-core only, no call dependency, ALPN-blind) / channels-call
   (channel 0 pre-negotiation + lifecycle op registrations; depends on
   channels-core + alknet-call) / channels-hub (relay) / channels-worker
   (ChannelClient). Isolates the call-protocol coupling from the pure
   multiplexer so the dependency graph is honest.

ADR-077 (TTY inside channels) updated: TTY now uses 5 sub-streams [0,1,2,3,4]
with control properly bidirectional via 3/4; amends ADR-052's stream_type
assignments for direct mode too (direct alknet/tty now uses 0-4, not 0-3).

Spec docs updated: channels-wire, channels-connection, channels-adapter,
channel-operations, overview, README.
2026-07-12 15:47:42 +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 f997c81d2f docs(research): note Connection::from_source gap closed (e8bbc74); POC deliberately not retrofitted
The from_source constructor gap surfaced by this POC has been resolved by
commit e8bbc74 (pub fn from_source(impl BidiStreamSource, alpn) at types.rs:574).
Updated the POC scope note on issue #1 to record this and explain why the POC
was deliberately not retrofitted to use from_source + a ChannelBidiStreamSource:
the POC's de-risk objective was reached, the surfaced issues are resolved, and
rewiring to the N-stream shape now would be rework the Phase 1 crate will do
authoritatively anyway. POC stands as the de-risk artifact; Phase 1 builds on
the now-unblocked trait and constructor.
2026-07-12 11:11:53 +00:00
glm-5.2 e8bbc7465f refactor(core): add Connection::from_source public constructor (ADR-070 gap)
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.
2026-07-12 11:07:17 +00:00
glm-5.2 02c5b9e039 docs(arch): add Connection::from_source to ADR-070 — the missing extension point
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)
2026-07-12 10:55:25 +00:00
glm-5.2 3954c788d4 docs(research): mark channels POC issues #1-#3 resolved by ADR-070; adopt AuthContext::anonymous in POC
Re-verified the POC against the post-refactor alknet-core (BidiStreamSource
trait, ADR-070): all 28 tests still pass, clippy fully clean (the upstream
Connection::close unused-arg warnings are gone). Updated the POC's handler
tests to use AuthContext::anonymous(alpn) (REQ-CORE-03), removing the
four-None-field literal that recurred across echo_handler and tunnel_handler.

Issues #1 (BidiStreamSource), #2 (Connection::close unused args), and #3
(AuthContext verbosity) in the POC summary are now marked RESOLVED with
pointers to ADR-070 / commit 60cce22. Issues #4-#7 (channels-side: zero-length
sentinel on shutdown, dynamic mux registration, demux EOF teardown, two-pump
shutdown-on-completion) remain for Phase 1. POC scope note added: the POC
correctly keeps Connection::from_stream (yield-once) — building
ChannelBidiStreamSource is Phase 1's job, now unblocked by the trait.
2026-07-12 10:51:31 +00:00
glm-5.2 1318eee451 docs(task): mark bidistreamsource-trait acceptance criteria complete + add summary
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).
2026-07-12 10:06:02 +00:00
glm-5.2 60cce228d2 refactor(core): implement BidiStreamSource trait + AuthContext::anonymous (ADR-070, REQ-CORE-01/02/03)
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.
2026-07-12 09:52:08 +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
glm-5.2 9ea69efba6 docs(research): graduate POC findings into phase-0 — REQ-CH/REQ-CORE, resolved OQs, AlknetClient clarification
Incorporates the completed de-risk POC (poc-summary.md, 28 tests passing)
into phase-0-findings.md:

- Marks OQ-CH-12 RESOLVED (lenient drop, validated by POC), OQ-CH-13
  CONFIRMED +EV (do the BidiStreamSource refactor), and clarifies OQ-CH-14
  with the AlknetClient context: a general downstream-facing client in
  alknet-core paired with whichever ALPN handler a crate provides, with
  server/client bidirectionality preserved (hub/worker can act as both,
  each side fills its registry with the other's resources).

- Adds §POC-Validated Requirements carrying the POC's surfaced invariants
  into Phase 1 as requirements (not open questions):
  - REQ-CH-01..07: wire-level invariants for the channels crate spec
    (shutdown emits zero-length sentinel, transport close drops all
    senders, mux handle/runner split, lenient unknown channel_id, bounded
    backpressure, two-pump shutdown-on-completion, PollSender adapter).
  - REQ-CORE-01..04: the light alknet-core refactor to land alongside the
    channels crate (BidiStreamSource trait, Connection::close fix,
    AuthContext test helper, AlknetClient).

- Updates the De-risk POC section to COMPLETE status with what was
  validated, what was surfaced, and what remains out of scope.

- Updates the opening revision note to reflect the POC pushed the core
  mechanics beyond speculation into validated territory.
2026-07-12 08:08:51 +00:00
glm-5.2 6f1663a391 docs(research): add alknet-channels POC summary — 3 targets validated, core refactor notes 2026-07-12 07:52:43 +00:00
glm-5.2 5e07203b86 docs(research): add detailed channels POC plan; add BidiStreamSource + client endpoint open questions
Adds docs/research/alknet-channels/poc-plan.md — a standalone three-step POC
plan that derisks the channels layer in isolation (no call protocol, no real
transport, no real adapters):

- Step 1: chunk format + N-channel demux/mux — generalizes TTY's 5-byte
  ChunkReader/ChunkWriter to the 9-byte format, validates decompose→stream→
  recompose for 3+ concurrent channels with the sync-core/async-shell split
  pattern from TTY's REQ-TTY-01.
- Step 2: per-channel Connection presentation — wraps each reassembled
  channel as Connection::from_stream and runs a minimal echo ProtocolHandler
  through the full path. Validates the existing Connection abstraction is
  sufficient; no core changes needed for the POC.
- Step 3: tunnel handler — opens a TcpStream and pumps bidirectionally,
  reusing TTY's pump_session shape with two pumps. Validates the same
  concepts behind the TTY crate work as a generic port proxy.

Stretch goals: WASM build of the sync core, mixed channel types on one
connection, hub relay sketch with channel_id remapping.

Adds three open questions to phase-0-findings.md:

- OQ-CH-12: unknown channel_id on demux (lenient drop vs strict error).
- OQ-CH-13: core BidiStreamSource trait — additive refactor to make
  ChannelConnection a first-class peer of QUIC (many bidi streams) rather
  than a bag of yield-once Connections. Likely +EV; not needed for the POC
  but should be evaluated in Phase 1.
- OQ-CH-14: client-side channels endpoint — the symmetric ChannelClient
  type (analogue of AlknetEndpoint vs CallClient). After the POC we're
  probably going to need some light refactoring to the core to make these
  easier; mostly additive and not breaking in major/pita ways.

Updates phase-0-findings.md De-risk POC section to point at the detailed
plan and adds the poc-plan to References.
2026-07-12 06:35:50 +00:00
glm-5.2 12cf8aa0bd docs(research): add single-stream throughput ceiling to less-straightforward parts
Captures the single-stream flow-control ceiling as a known constraint with
the recursive-composition escape hatch: parallel throughput comes from N
independent channels connections (client optimization), not cross-connection
tokens or channels-layer coordination. Pins that the channels layer does no
cross-connection channels, no cross-connection primitives, and no parallel-
transfer optimization — those are downstream client concerns.
2026-07-11 12:42:49 +00:00
glm-5.2 8692f9748e docs(research): flesh out alknet-channels — hub motivation, channel open negotiation, channel manager internals
Adds three sections filling the conceptual gaps in the phase-0 findings:

- Hub Motivation: The Multi-Transport Collapse — diagnoses the
  O(protocols × transports × spokes) mess the hub crate faces and shows how
  channels collapses it to one connection per leg with channel-by-channel
  byte forwarding, reusing the call protocol's auth/forwarded-for model.

- Channel Open Negotiation — concrete channel/open, channel/close,
  channel/control, channel/resources operation payloads with field tables,
  error codes as CallError strings, bidirectional open semantics, and the
  end-to-end ACL flow for browser→hub→spoke. No new wire framing; all four
  operations register on the call protocol's existing OperationRegistry.

- Channel Manager and Connection Internals — the ChannelsAdapter/
  ChannelManager split, ChannelManager state sketch, ChannelsAdapter::handle
  read/demux loop with channel 0 preinstall, the channel/open handler
  closure threading into OperationRegistry, ChannelConnection as a
  Connection via the existing from_stream path, the hub relay as
  ChannelManager-to-ChannelManager byte pumping, and the boundary
  (ChannelManager holds no handlers, no ALPN parsing, no auth, no transport
  coupling).

Also adds four new open questions (OQ-CH-08 through OQ-CH-11) on resource
staleness, responder-to-initiator lifecycle, typed destructure ownership,
and hub relay channel_id remapping; two new POC stretch goals (hub relay,
channel/resources); and updates the opening to note the revision scope.
2026-07-11 08:26:56 +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 11f531131c docs(arch): clean unwrap, remove stale ownership, protocol-agnostic hub core
- Replace .unwrap() with .expect() in spec pseudocode; remove .unwrap()
  from ADR-067 prose. Specs describe intent, not implementation details.

- Remove 'ownership reaping' from hub's 'does NOT do' list. Ownership
  is an alknet-docker concern, not a hub concern. The other agent's
  hedging leaked into the spec; removed.

- Restructure hub README: separate core peer lifecycle (protocol-agnostic)
  from call-protocol strategy (first strategy). Add extension points
  section for future tty/blobs/custom strategies. A QUIC connection can
  multiplex multiple protocols over separate streams; the hub's core
  manages the connection, each strategy manages its own protocol-specific
  state. The call-protocol strategy handles the majority of real-world
  use cases; the architecture keeps the door open for others.
2026-07-10 06:26:56 +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 2282647f8b style(core): fmt fix for SendStreamKind::Stream match arm 2026-07-09 12:06:16 +00:00
glm-5.2 d9fcd18a01 feat(http): move from_jsonschema to alknet-http as real HTTP-backed adapter (ADR-066)
- Add FromJsonSchema adapter in alknet-http with reqwest forwarding handler
  reusing from_openapi's build_request/forward/forward_stream logic
- Delete broken placeholder from alknet-call (NOT_FOUND-returning handler)
- Keep FromJsonSchema variant in OperationProvenance (alknet-call)
- Make forwarding functions pub(crate) in from_openapi.rs for reuse
- 11 tests: unit (provenance, handler kind, path/query, bearer injection,
  no-env-vars) + integration (echo server, non-2xx errors, SSE streaming)
2026-07-09 12:04:17 +00:00
glm-5.2 97456bc608 docs(arch): ADR-066 — move from_jsonschema to alknet-http as HTTP-backed single-endpoint adapter
from_jsonschema was in alknet-call as a schema-only placeholder with a
NOT_FOUND handler — broken (an op in the registry needs a real handler)
and in the wrong crate (alknet-call has no HTTP client; a useful
from_jsonschema needs reqwest like from_openapi). ADR-066 moves it to
alknet-http as a real reqwest-backed single-endpoint adapter for
non-standard/non-OpenAPI REST endpoints, functionally similar to
from_openapi but one endpoint at a time. FromJsonSchema provenance
stays in alknet-call (now a handler-bearing leaf).

- New ADR-066 (supersedes ADR-017 §5 from_jsonschema clause + ADR-022
  FromJsonSchema row; both amended with strikethrough + pointer)
- Updated specs: call/client-and-adapters, call/README,
  call/operation-registry, http/http-adapters (new from_jsonschema
  section), http/overview, http/README, arch README + overview
- New task: tasks/http/adapters/from-jsonschema.md (depends on
  from-openapi; includes alknet-call cleanup of the broken placeholder)
- Old task tasks/call/client/from-jsonschema.md marked superseded

taskgraph validate: 116 tasks valid
2026-07-09 11:42:56 +00:00
glm-5.2 06ab5db459 test(http): fix from_mcp_integration — unwrap HandlerKind before invoking handlers
The from_mcp_integration test invoked bundle.handler directly as a
function, but HandlerRegistration.handler is a HandlerKind enum
(Once/Stream), not a callable. This caused E0618 (expected function,
found HandlerKind) and a downstream E0282 on the response match arm.

Pattern-match HandlerKind::Once to extract the inner Handler before
calling, mirroring the pattern used in protocol/connection.rs and
from_openapi.rs tests.
2026-07-09 07:46:00 +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 865fef6210 feat(core): Connection::from_stream — generic single-stream connections
Add ConnectionKind::Stream: a single read/write pair behind a
Mutex<Option<...>> that accept_bi yields once (then ConnectionClosed).
Add Connection::from_stream(send, recv, alpn, remote_addr) and
Connection::from_bidi(stream, alpn, remote_addr) constructors.

Rename the stream-level SendStreamKind::Mock / RecvStreamKind::Mock to
::Stream (they were already generic Box<dyn AsyncRead/Write> — the
wrong name). Rename from_mock to from_stream.

Remove MockConnection trait + ConnectionKind::Mock entirely. Migrate
all alknet-call test StubConnections to use from_stream with
tokio::io::sink() + tokio::io::empty() (immediate EOF on the read side
causes handle_stream to exit cleanly, then accept_bi returns
ConnectionClosed and the run_loop exits).

The yield-once accept_bi contract: QUIC yields many streams, everything
else yields one. Handlers that loop (TtyAdapter) get one iteration per
single-stream connection; handlers that call once (HttpAdapter) get the
stream directly. Both correct, no branching on transport.

This unblocks TCP+TLS listeners, SSH channel dispatch, WebTransport
streams, and wasm — all via from_stream, all through the same
HandlerRegistry, zero handler code changes.

See docs/research/transport-generalization/findings.md for the full
trace and the yield-once contract.
2026-07-09 07:06:12 +00:00
glm-5.2 acd049e8b2 feat(core): migrate iroh 0.35 -> 1.0.2
Cargo.toml: iroh = { version = "1.0", optional = true, default-features = false, features = ["tls-aws-lc-rs"] }

6 API changes in endpoint.rs + types.rs:
- Endpoint::builder() -> builder(presets::Minimal) (mandatory crypto_provider)
- SecretKey::from_bytes() now returns SecretKey directly, not Result
- SecretKey::generate() takes no rng arg (OsRng internal)
- Connection::remote_node_id() -> remote_id() (returns PublicKey, not Result)
- Connection::alpn() returns &[u8] directly, not Option<&[u8]>
- tls-aws-lc-rs feature matches quinn path's existing crypto provider

Unblocks alknet-blobs (depends on iroh-blobs 0.103 which pulls iroh 1.0
transitively). See docs/research/iroh-update/findings.md for the full trace.
2026-07-09 06:58:07 +00:00
glm-5.2 668d777ef0 chore: drop dead irpc/irpc-derive workspace deps
irpc 0.16 was declared in the workspace Cargo.toml and consumed by
alknet-call, but no .rs file in the workspace imports it. alknet-call's
wire protocol (src/protocol/wire.rs) is hand-rolled length-prefixed JSON.
The dep was dead weight.

Re-add as 0.17 when alknet-blobs lands (it depends on iroh-blobs 0.103
which pulls irpc 0.17 transitively).
2026-07-09 06:51:13 +00:00
glm-5.2 d9487c519a docs(research): transport generalization — from_stream + iroh 1.0 + dead irpc sweep
Three-commit plan to unblock api.alk.dev (standard TCP+TLS HTTP), alknet-ssh
(per-channel dispatch over russh), and alknet-blobs (iroh 1.0 prerequisite):

1. Drop dead irpc/irpc-derive workspace deps (never imported by any .rs file)
2. Migrate alknet-core iroh 0.35 -> 1.0 (3 edits in build_iroh_endpoint + Cargo.toml)
3. Add Connection::from_stream / from_bidi — a generic single-stream connection
   kind that makes every ProtocolHandler work over TCP+TLS, SSH channels,
   WebTransport streams, and wasm without handler code changes. Rename the
   existing stream-level Mock variants to Stream (they were already generic
   stream holders, just misnamed).

The yield-once accept_bi contract: QUIC yields many streams, everything else
yields one. Handlers that loop (tty) get one iteration; handlers that call
once (http) get the stream directly. Both correct, no branching on transport.

No ProtocolHandler trait shape change (one-way door per ADR-009). SSH's
per-channel dispatch is handler-internal, same pattern as TtyAdapter's
per-stream dispatch. Incorporates ../iroh-update/findings.md by reference.
2026-07-08 15:57:33 +00:00
glm-5.2 e024b50b1e docs(research): iroh/irpc version update findings + alknet-blobs external-store probe
Traces the version gap deferred by the alknet-blobs probe: verifies latest
stable iroh 1.0.2 / iroh-blobs 0.103 / irpc 0.17 against crates.io, rules out
the 'iroh 0.35 is a stale pin' hypothesis (it's load-bearing — 3 APIs broke),
identifies the real cheap win (irpc 0.16 is a dead dep in alknet-call, never
imported), and traces the iroh 1.0.x Preset trait so the migration is a
concrete 3-edit diff rather than a TODO.

Includes the sibling probe doc (alknet-blobs-external-store-probe.md) which
the findings cross-references.
2026-07-08 13:32:57 +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 5873fa22d6 chore: mark tty/review-tty-final as completed — all 15 tasks done
Final review verified:
- 103 tests pass (61 alknet-tty + 42 alknet-tty-local), clippy clean, fmt clean
- Cross-crate seam: portable_pty only in alknet-tty-local, not in alknet-tty default
- ADR-055 exit-chunk-is-last enforced in adapter, validated in integration tests
- ADR-056 cancel-cleanup guards in both PTY and pipe exit futures, integration tests confirm no orphans
- REQ-TTY-01 three-thread bridge, REQ-TTY-02 process-group signal forwarding
- No bollard/russh/alknet-call deps; alknet-tty depends on alknet-core only
- Deviation: local feature re-export deferred to assembly layer (cargo cycle constraint)
2026-07-07 23:49:40 +00:00
glm-5.2 affcf14986 chore: mark tty/integration-test as completed 2026-07-07 23:49:08 +00:00
glm-5.2 45506ed77b test(tty): end-to-end integration tests for LocalTtyBackend + drive_session
Add 18 integration tests in crates/alknet-tty-local/tests/ exercising
the full stack (LocalTtyBackend + TtyAdapter::drive_session over
tokio::io::duplex, real commands), validating the two crates work
together through the TtyBackend trait seam.

Tests live in alknet-tty-local (not alknet-tty) per the feature-gate
deviation: the cyclic dependency alknet-tty → alknet-tty-local →
alknet-tty prevents alknet-tty from depending on alknet-tty-local, so
the integration tests live in the crate that depends on both. Unix-only
tests (signal, process-group) use #[cfg(unix)].

PTY mode (8): happy path echo, interactive cat round-trip, resize,
SIGINT signal, process-group signal (REQ-TTY-02), stdin EOF sentinel,
cancel cleanup (ADR-056), exit-chunk-is-last (ADR-055).

Pipe mode (6): happy path echo, separate stderr, SIGTERM signal,
cancel cleanup, resize no-op, stdout chunk + sentinel.

Negotiation errors (5): unknown_backend, malformed_negotiation (bad
JSON, carriage != raw, empty cmd), allocate_failed (nonexistent
binary).

A shared test harness (tests/common/mod.rs) provides the client-side
wire protocol helpers: write_negotiation, write_chunk, write_control,
try_read_chunk, read_chunk_timeout, read_error_frame (asserts the
0x00 first-byte framing-disambiguation invariant), read_until_exit,
assert_no_more_chunks, close_write_half, plus spawn_session which
wires drive_session over a duplex pair with a test identity carrying
tty:open.

Verification: cargo test -p alknet-tty-local (42 tests pass), cargo
clippy -p alknet-tty-local -- -D warnings (clean), cargo fmt --check
-p alknet-tty-local (clean).
2026-07-07 23:48:12 +00:00
glm-5.2 558800c2dd chore: mark tty/local-feature-reexport as completed 2026-07-07 23:25:28 +00:00
glm-5.2 a20efb4d06 feat(tty): wire local feature gate with assembly-layer re-export pattern
ADR-054 prescribes  with a re-export
module, but cargo rejects the cyclic dependency (alknet-tty →
alknet-tty-local → alknet-tty) even with an optional dep. The re-export
is deferred to the assembly layer: consumers depend on alknet-tty-local
directly and register LocalTtyBackend in the TtyAdapter backend map.
The  feature gate stays declared (empty) for forward compatibility.
Assembly pattern documented in the crate root doc comment.
2026-07-07 23:25:08 +00:00
glm-5.2 6d7adf5911 chore: mark tty-local/review-tty-local as completed (coordinator review — REQ-TTY-01/02, ADR-056 verified) 2026-07-07 23:03:57 +00:00
glm-5.2 4d7b189c2b chore: mark tty-local/backend-impl as completed 2026-07-07 23:03:34 +00:00
glm-5.2 dfb1379026 Merge remote-tracking branch 'origin/feat/tty-local/backend-impl' into develop 2026-07-07 23:03:20 +00:00
glm-5.2 0ff0ceeac3 feat(tty-local): implement LocalTtyBackend (TtyBackend) branching on terminal Some/None
Implements the wiring task: LocalTtyBackend::allocate() dispatches to
pty::allocate_pty when TtyParams.terminal is Some (PTY mode, stderr None)
and pipe::allocate_pipe when None (runner mode, stderr Some). Validates
non-empty cmd, ignores backend_params (local backend has no backend-
specific selector fields), and resource_id() returns None (local backend
creates its own resource — process). Adds async-trait dependency.
2026-07-07 23:03:05 +00:00
glm-5.2 0e76e6044e chore: mark tty/review-tty as completed (coordinator review — all criteria verified) 2026-07-07 22:59:49 +00:00
glm-5.2 ad9afab145 chore: mark tty/adapter, tty-local/pty-mode, tty-local/pipe-mode as completed 2026-07-07 22:58:52 +00:00
glm-5.2 6dce987ed4 Merge feat/tty-local/pipe-mode: resolve Cargo.toml/Cargo.lock conflicts with pty-mode (both tokio-stream and tokio-util needed) 2026-07-07 22:58:40 +00:00
glm-5.2 f817376723 Merge remote-tracking branch 'origin/feat/tty-local/pty-mode' into develop 2026-07-07 22:57:40 +00:00
glm-5.2 d5612f1a0e feat(tty): implement TtyAdapter (ProtocolHandler) and three-pump session driver (ADR-052, 053, 055, 056)
Implement TtyAdapter (ProtocolHandler on alknet/tty) and the
drive_session three-pump bidirectional driver in adapter.rs.

TtyAdapter holds a HashMap<String, Arc<dyn TtyBackend>> keyed by the
negotiation frame's backend string and an optional OwnershipProvider.
handle() loops accept_bi and spawns drive_session per stream.

drive_session proceeds in three phases:
1. Negotiation — read length-prefixed JSON, parse NegotiateRequest,
   validate (carriage == raw, cmd non-empty), look up backend, scope-gate
   (tty:open), resource ownership check via backend.resource_id() +
   OwnershipProvider::owns(), construct TtyParams. Errors sent as JSON
   in negotiation framing, stream closed.
2. Allocation — backend.allocate(&params). On failure send
   allocate_failed error, close.
3. Raw carriage — three concurrent pumps:
   A. stdout/stderr → client (stream_type 1/2), zero-length sentinel on EOF
   B. client → backend: stdin chunks → TtyHandle.stdin, control chunks →
      Resize/Signal/Eof dispatch (Exit ignored, unknown types ignored)
   C. exit → exit chunk: await exit_code; on resolve enqueue exit chunk

Exit-chunk-is-last invariant (ADR-055): adapter waits for BOTH
stdout/stderr pumps to complete AND exit_code to resolve before
enqueueing the exit chunk (tokio::join!). On TtyError → code -1.

Cancel cleanup (ADR-056): on connection drop/stream reset, pump tasks
drop, TtyHandle drops, exit_code future drops without resolve →
backend kill-on-Drop fires. Client write-half close is NOT a cancel —
session runs to completion.

Access control (ADR-050): scope gate at negotiation (tty:open scope,
configurable constant), resource ownership via backend.resource_id() +
OwnershipProvider::owns() when wired.

18 integration tests with a TestBackend fixture over tokio::io::duplex:
happy path, exit-chunk-is-last, stdin EOF, resize/signal control,
unknown control ignored, exit control ignored, unknown_backend,
malformed_negotiation (bad JSON, carriage != raw, empty cmd),
allocate_failed, exit error (-1), cancel cleanup, scope gate forbidden,
ownership check deny/allow, stderr pump, eof control closes stdin.

Also fix two clippy unused_mut warnings in negotiation.rs tests.
2026-07-07 15:34:09 +00:00
glm-5.2 c76b6aab7a feat(tty-local): implement pipe mode — tokio::process spawn, PipeControl, ADR-056 kill guard
Implements the runner case (terminal: None, no PTY) in pipe.rs:
- allocate_pipe spawns via tokio::process::Command with Stdio::piped(),
  returning a TtyHandle with separate stdout/stderr (stderr=Some).
- stdin: ChildStdin boxed as Box<dyn AsyncWrite + Send + Unpin>.
- stdout/stderr: ChildStdout/ChildStderr wrapped via tokio_util::io::ReaderStream
  into Pin<Box<dyn Stream<Item = Bytes> + Send>> (errors map to stream EOF).
- exit_code: PipeExitFuture holding the Child; poll drives Child::wait() and
  disarms the guard on resolve; Drop calls Child::start_kill() on cancel
  (ADR-056 spec-compliant kill-on-Drop, not kill_on_drop(true) alone).
- control: PipeControl (no-op resize, libc::kill(pid, sig) signal on Unix,
  SIGKILL fallback for unknown names; non-Unix logs and relies on Drop guard).
  Doc comment documents the no-process-group limitation of pipe mode.

Adds tokio-util (io) and tokio-stream (dev) deps. 9 unit tests cover the
happy path, stdin round-trip, separate stderr, SIGTERM signal, cancel
cleanup (asserts child killed via kill(pid,0)), resize no-op, unknown
signal SIGKILL fallback, pid recording, and empty-command error.
2026-07-07 15:04:30 +00:00
glm-5.2 9f8d07fc25 feat(tty-local): implement PTY mode — three-thread bridge, PtyControl, REQ-TTY-02, ADR-056 kill guard
Implement allocate_pty in src/pty.rs: portable_pty openpty + spawn as
session leader (set_controlling_tty(true)), with the three-thread
blocking→async bridge (REQ-TTY-01): reader → mpsc<Bytes> with
zero-length EOF sentinel, writer draining mpsc<StdinCmd> (Bytes/Eof),
waiter → oneshot<i32>. TtyHandle.stdout is ReceiverStream<Bytes>,
stdin is a StdinSink AsyncWrite wrapper over mpsc::Sender<StdinCmd>
(parks a reserve+send future when the channel is full), stderr is None
(PTY merges), exit_code is LocalExitFuture.

PtyControl implements TtyControl: resize via MasterPty::resize; signal
via libc::kill(-pgid, sig) with kill(pid, sig) fallback (Unix, REQ-TTY-02)
using alknet_tty::signal_from_name; unknown names and non-Unix fall back
to ChildKiller::kill (SIGHUP).

LocalExitFuture wraps the oneshot::Receiver<i32> + a ChildKiller kill
guard (ADR-056). poll delegates to the receiver and disarms the guard
on Ready (no-op Drop on happy path); Drop on cancel calls
ChildKiller::kill (SIGHUP) — best-effort; the waiter thread reaps.

Bump portable-pty 0.8 → 0.9 to match the POC API (MasterPty: Send).
Add tokio-stream, serde_json (dev), tempfile (dev) deps.

Tests: happy path (echo), stdin round-trip (cat), resize, signal INT
kills child, signal reaches process group (bash -c "sleep 60"),
cancel cleanup on drop, unknown signal falls back to ChildKiller.
2026-07-07 14:56:35 +00:00
glm-5.2 42643be338 chore: mark tty/negotiation as completed 2026-07-07 14:20:56 +00:00
glm-5.2 be344d2f0f Merge remote-tracking branch 'origin/feat/tty/negotiation' into develop 2026-07-07 14:20:20 +00:00
glm-5.2 491ff1e6f8 feat(tty): implement negotiation frame — NegotiateRequest, framing reader/writer, error response (ADR-052, ADR-057)
Replace placeholder negotiation types with the full Phase 1 wire shape:
- NegotiateRequest/TerminalParamsWire with serde(flatten) capturing
  backend-specific fields into backend_params
- NegotiationReader/Writer: self-contained 4-byte BE length-prefixed
  framing on tokio AsyncRead/AsyncWrite, bounds-checked against MAX_CHUNK_LEN
- NegotiationError (Io, ConnectionClosed, FrameTooLarge, Json) via thiserror
- error_response_bytes helper for server-side JSON error frames
- into_inner reclaims the stream for raw-chunk use
- Error frames stay under 16 MiB so the high byte of the length prefix is
  0x00, making the framing-disambiguation trick sound (ADR-052 §5)

Unit tests cover round-trip, serde(flatten) capture, FrameTooLarge,
ConnectionClosed on truncated header/body, error-response shape, and the
0x00-first-byte framing-disambiguation invariant. backend.rs's
From<NegotiateRequest> for TtyParams still compiles (same field shapes).
2026-07-07 14:19:49 +00:00
glm-5.2 83f20f5196 chore: mark tty-local/crate-init as completed 2026-07-07 14:17:52 +00:00
glm-5.2 2022a5496a feat(tty-local): initialize alknet-tty-local sibling crate (ADR-054)
Create crates/alknet-tty-local with Cargo.toml (alknet-tty workspace dep,
portable-pty, libc unix-only, tokio, bytes, futures-core, tracing,
thiserror), src/lib.rs with module declarations and LocalTtyBackend
re-export, and skeleton module files (pty, pipe, backend) with doc
comments and TODO markers. Add the crate to the workspace members list.
2026-07-07 14:16:10 +00:00
glm-5.2 00320c2943 chore: mark tty/backend-trait as completed 2026-07-07 14:10:03 +00:00
glm-5.2 f72b80c451 Merge remote-tracking branch 'origin/feat/tty/backend-trait' into develop 2026-07-07 14:08:45 +00:00
glm-5.2 2074a71f5f chore(tty): drop unused futures crate dep
backend.rs defines its own BoxFuture type alias
(Pin<Box<dyn Future + Send>>) using std::future::Future + futures_core,
so the futures crate is unused. Removes the dead dependency.
2026-07-07 13:55:32 +00:00
glm-5.2 b5834d3385 feat(tty): implement TtyBackend trait, TtyHandle, TtyControl, TtyParams, TtyError (ADR-053)
Implements the one-way-door backend inversion point in crates/alknet-tty/src/backend.rs:

- TtyBackend trait (#[async_trait]): allocate() + resource_id() default None
- TtyError enum (#[non_exhaustive]): AllocFailed, WaitFailed, Io, Backend
- TtyParams + TerminalParams (allocation request shapes)
- TtyHandle: stdin (AsyncWrite), stdout/stderr (Stream<Bytes>),
  exit_code (BoxFuture<Result<i32,TtyError>>), control (Option<TtyControlHandle>)
- TtyControl trait (object-safe, NOT Clone) + TtyControlHandle newtype
  (#[derive(Clone)] wrapping Arc<dyn TtyControl + Send + Sync>)
- From<NegotiateRequest> for TtyParams and From<TerminalParamsWire> for
  TerminalParams (adapter uses these; no hand-rolled mapping)
- MockBackend (in-memory mpsc pipes + oneshot exit + MockControl) for
  compile-check and adapter tests
- Unit tests: TtyControlHandle clone+delegation, MockBackend allocate/exit,
  resource_id default, From<NegotiateRequest> mapping (pty + pipe modes)

TtyBackend trait doc carries REQ-TTY-01 (backends need not be natively
async); TtyHandle.exit_code doc carries the ADR-056 kill-on-Drop contract.

negotiation.rs carries minimal NegotiateRequest/TerminalParamsWire
placeholders (field set per tty-wire.md) so the From conversion compiles
before the negotiation task lands; that task replaces them with the full
framing machinery.
2026-07-07 13:54:36 +00:00
glm-5.2 39450e69da chore: mark tty/wire-codec and tty/control-messages as completed 2026-07-07 13:52:34 +00:00
glm-5.2 ae75a61722 Merge remote-tracking branch 'origin/feat/tty/control-messages' into develop 2026-07-07 13:52:01 +00:00
glm-5.2 8b8de61186 feat(tty): implement ControlMessage enum and signal_from_name
Port the POC's control channel schema (alknet-tty-poc/src/control.rs)
into crates/alknet-tty/src/control.rs: the ControlMessage tagged enum
(Resize/Signal/Eof/Exit) with snake_case type tag, to_json/from_slice
helpers, and the Unix-only signal_from_name mapping the 9 supported
signal names to libc numbers. Unknown type tags return a serde_json
error; the adapter (later task) ignores that error per the wire spec's
extensibility policy. Adds libc as a target.'cfg(unix)' dependency.

Refs: docs/architecture/crates/tty/tty-wire.md §"Control Channel"
Refs: docs/architecture/decisions/055-exit-code-on-control-chunk.md
2026-07-07 13:51:32 +00:00
glm-5.2 17713a5c27 feat(tty): implement raw chunk codec (ChunkReader/ChunkWriter, RawError)
Port of the alknet-tty POC's raw.rs into crates/alknet-tty/src/wire.rs.
The Phase 2 'raw carriage' wire format (ADR-052):
[stream_type: u8][length: u32 be][payload bytes].

- STREAM_STDIN/STDOUT/STDERR/CONTROL constants (0-3)
- CHUNK_HEADER_LEN (5) and MAX_CHUNK_LEN (16 MiB)
- RawError: Io / ConnectionClosed / InvalidStreamType / ChunkTooLarge
- Chunk struct with stdin/stdout/stderr/control constructors
- ChunkReader::read_chunk validates stream_type and length, returns
  ConnectionClosed on clean UnexpectedEof (header or payload)
- ChunkWriter::write_chunk/write_stdin/write_control_json write
  header + payload + flush
- Round-trip tests for all four stream types, InvalidStreamType (4),
  ChunkTooLarge (> MAX_CHUNK_LEN), ConnectionClosed on truncated
  header and payload, via tokio::io::duplex
2026-07-07 13:51:31 +00:00
glm-5.2 8c2fc42280 chore: update task tty/crate-init status to completed 2026-07-07 13:46:59 +00:00
glm-5.2 da6333c206 feat(tty): initialize alknet-tty crate skeleton
Create crates/alknet-tty with Cargo.toml (alknet-core path dep, tokio,
bytes, futures-core, tokio-stream, serde, serde_json, async-trait,
tracing, thiserror — no portable_pty/bollard/russh/alknet-call per
ADR-057), the `local` feature gate (dep wiring deferred to
tty/local-feature-reexport), src/lib.rs with the crate doc comment and
module declarations, and skeleton modules (wire, control, negotiation,
backend, adapter) with doc comments and TODO markers. Add the crate to
the workspace members list.

cargo check, clippy -D warnings, and fmt --check all pass.
2026-07-07 13:46:11 +00:00
glm-5.2 8d20f89902 tasks(tty): decompose alknet-tty + alknet-tty-local into 15 implementation tasks
Break the tty spec set (docs/architecture/crates/tty/) into atomic,
dependency-ordered tasks across two crates:

alknet-tty (lean core, depends on alknet-core only — ADR-057):
  - crate-init, wire-codec, control-messages, negotiation, backend-trait,
    adapter, review-tty

alknet-tty-local (sibling, portable_pty + libc — ADR-054):
  - crate-init, pty-mode, pipe-mode, backend-impl, review-tty-local

integration:
  - local-feature-reexport, integration-test, review-tty-final

Three review checkpoints at critical points: after the core crate (before
the local backend builds on the one-way-door TtyBackend trait — ADR-053),
after the local backend (validating REQ-TTY-01/02 and ADR-056), and a final
merge-readiness review. The high-risk tasks (backend-trait, adapter,
pty-mode) are flagged; the ADR-056 kill-on-Drop guard the POC lacked is
called out in both pty-mode and pipe-mode.

Validated with taskgraph: 115 tasks, no cycles, 10-generation parallel
structure with 3-way parallelism after crate-init and 2-way after the
backend trait.
2026-07-07 12:22:58 +00:00
glm-5.2 dbac972aaa docs(arch): ADR-057 — alknet-tty does not depend on alknet-call (self-contained negotiation framing)
The earlier specs claimed alknet-tty reuses alknet-call's
FrameFramedReader/FrameFramedWriter 'framing utility' (ADR-052 §6,
ADR-003 Am. 1). A pre-implementation sanity check found this was
unsound: FrameFramedReader::read_frame() is hardcoded to deserialize
EventEnvelope — the length-prefix read and the type-specific deserialize
are one entangled call, not a separable utility. alknet-tty's
negotiation payload is a NegotiateRequest, not an EventEnvelope, so the
claimed reuse did not exist in a usable form.

ADR-057: alknet-tty implements its own ~30-line length-prefixed
framing directly on tokio AsyncRead/AsyncWrite. The format coincides
with alknet-call's framing by convention (both are length-prefixed
JSON); the implementations are independent. alknet-tty depends on
alknet-core only — no alknet-call dependency. The ~30 lines of framing
is an idiom, not a domain abstraction worth a cross-crate dependency.

Updates:
- ADR-003 Amendment 2: clarifies alknet-tty does not depend on
  alknet-call; the Am. 1 protocol-foundation exception remains for
  alknet-http/agent/napi (actual type reuse), not alknet-tty.
- ADR-052 §6: framing is self-contained in alknet-tty; format coincides
  by convention, not by code reuse.
- overview.md: dependency block drops alknet-call; new 'Why no
  alknet-call dependency' subsection explains the unsound-reuse finding.
- tty-wire.md, tty-backend.md, tty README, crates/tty/README.md,
  top-level README: ADR tables, Design Decisions tables, and dependency
  edges updated to reflect alknet-core-only dependency.
- ADR-053, ADR-054: References updated to ADR-003 Am. 2 + ADR-057.
2026-07-07 11:15:52 +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 b90d717ac2 docs(arch): amend ADR-053 — opaque backend params + resource_id(), complete the inversion
The BackendParams typed enum was a spec bug: it created a partial
inversion (trait inverted, params not), modeled the SSH channel as an
input when it is an output (the backend opens it in allocate()), and
created a dependency contradiction — SshChannelRef could not live in
alknet-ssh (alknet-tty would depend on it) nor in alknet-tty (would pull
in russh types) without violating the inversion ADR-053 establishes.

Replaced with an opaque serde_json::Map: the adapter passes the
negotiation frame's backend-specific fields through verbatim; each
backend deserializes its own strongly-typed params struct. alknet-tty
has zero knowledge of any backend's params shape. Added a resource_id()
default method to TtyBackend so the ownership check (ADR-050) is
backend-driven rather than the adapter hardcoding docker's container
field extraction. The inversion is now complete — both the trait and
the params are inverted; adding SSH (or any future backend) is purely
additive: zero changes to alknet-tty, zero changes to existing call
sites.
2026-07-07 09:36:25 +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 e6062230f9 tasks: fix taskgraph enum values for architecture tasks (level, impact, scope)
The three architecture tasks I seeded in the previous commit used invalid
enum values for the taskgraph frontmatter (level: architecture, impact: local,
scope: small). Corrected to the valid taskgraph enums: level: implementation/
decomposition/planning, impact: project/component, scope: moderate/narrow.
taskgraph validate now passes for tasks/architecture/.

(Pre-existing tasks/call/registry/*.md and tasks/core/ownership-store-trait.md
use status: done instead of completed — out of scope for this fix.)
2026-07-06 16:23:59 +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
glm-5.2 e4e6ccfb14 docs(arch): alknet-tty Phase 1 specs — wire format, backend trait, local backend, exit-chunk ordering
Grounded in the alknet-docker POC (seed codec) and the alknet-tty POC
(2026-07-05, validated the control channel, the local-PTY blocking→async
bridge, and the signal-delivery contract).

Four ADRs:
- ADR-052: wire format — alknet/tty ALPN, two-carriage (JSON negotiation
  then raw chunks), fixed channel set 0-3, control as JSON, negotiation-
  error framing disambiguation
- ADR-053: TtyBackend trait + TtyHandle — the backend inversion point;
  exit_code as a Future; REQ-TTY-01 (backends need not be natively async)
- ADR-054: local backend as alknet-tty-local sibling crate behind a
  feature re-export; PTY vs pipe per-session; runner pattern preserved
- ADR-055: exit code on a stream_type 3 control chunk; "exit chunk is
  last" invariant; adapter owns the ordering

Five component specs (crates/tty/): README, overview, tty-wire, tty-
backend, tty-adapter, tty-local. The docker/SSH backends are future
crates (out of scope); the trait shape is committed so they can be
built against it.

Five OQs (OQ-43…47): two resolved (TtyControl Clone, stdin closure),
two deferred(scope) (terminal modes, runner API surface), one open
low-risk (flow control).

Zero critical issues from a general-subagent architecture review;
warnings addressed (inline rationale trimmed to ADR refs, TtyError and
StdinCmd defined, framing disambiguation documented, missing ADRs added
to index tables).
2026-07-06 15:05:36 +00:00
glm-5.2 14c8340380 docs(arch): amend ADR-051 §2 — yaml_serde 0.10.x is YAML 1.2, not 1.1
Implementation verified yaml_serde 0.10.4 (and serde_yaml 0.9) implement the
YAML 1.2 core schema, where bare yes/no/on/off are strings, not booleans. The
original §2 rationale cited YAML 1.1 boolean-coercion as a present hazard
justifying JSON-first detection; it does not exist with this dependency
version.

The JSON-first decision is retained (Accepted ADR) — reframed as a defensive
default that locks the contract against a future YAML-parser swap, not a
guard against a present hazard. §2 heading, Consequences, and Assumption #2
amended; an in-place amendment note records what changed and when. The two
coercion-related tests already document yaml_serde 0.10.x's actual behavior
with explicit comments. Source doc-comments on from_yaml/from_str corrected
to stop propagating the flawed assumption. 256 tests pass; clippy clean.
2026-07-06 13:30:59 +00:00
glm-5.2 21c4f7d252 feat(http): implement ADR-051 YAML input for from_openapi — from_yaml + JSON-first from_str
Adds OpenAPISpec::from_yaml and OpenAPISpec::from_str alongside the
existing from_json. from_str is JSON-first, YAML-fallback (ADR-051 §2):
a valid JSON doc parses through serde_json (stricter grammar, immune to
YAML-specific coercion); only on JSON failure does it fall back to the
YAML parser. yaml_serde 0.10 (resolves to 0.10.4, latest stable) is the
maintained official-YAML-org fork of the deprecated serde_yaml; not
feature-gated per ADR-051 §3 (YAML OpenAPI is first-class).

Note: ADR-051 §2's stated YAML 1.1 boolean-coercion rationale does not
hold for yaml_serde 0.10 (YAML 1.2 core schema — yes/no/on/off are
strings). The JSON-first rule is retained per the Accepted ADR as a
defensive default; tests document the actual 0.10.x behavior with
explicit comments. The ADR rationale should be amended separately.

256 tests pass; clippy + fmt clean.
2026-07-06 13:15:53 +00:00
glm-5.2 193f3a0e24 docs(arch): ADR-051 — YAML input format for from_openapi (gap-fill; JSON-first detection, yaml_serde dep)
from_openapi's spec already stated the contract as "JSON/YAML doc" but the
implementation only delivered the JSON half. vast.ai publishes a YAML
OpenAPI schema, which surfaces the gap. This is gap-filling against an
existing constraint — no architecture invariants touched (OpenAPISpec
stays serde_json::Value-based, forwarding handler / no-env-vars / error
fidelity / to_openapi output unchanged).

ADR-051 records: (1) from_yaml + format-detecting from_str constructors;
(2) JSON-first/YAML-fallback detection as a correctness rule (YAML 1.1
coerces yes/no/on/off to booleans — running JSON through YAML would
silently corrupt string fields); (3) yaml_serde dependency (maintained
official-YAML-org fork of deprecated serde_yaml, not feature-gated);
(4) to_openapi output stays JSON (scope boundary).

Specs updated to reference ADR-051 by number (http-adapters, overview,
both README ADR tables). Implementation task added at
tasks/http/adapters/from-openapi-yaml-input.md.
2026-07-06 12:41:46 +00:00
glm-5.2 d0e3711b46 docs(research): record alknet-tty local-PTY POC findings — REQ-TTY-01 (blocking-backend trait accommodation) and REQ-TTY-02 (process-group signal forwarding)
Built /workspace/alknet-tty-poc against portable_pty 0.9 to validate the
local-PTY path (Step 2 of the build order) before Phase 1 specs. The POC
surfaced two constraints that were not knowable from reading the
portable_pty docs alone and that the architect must carry into the
tty-backend.md and tty-local.md specs:

- REQ-TTY-01: portable_pty is a blocking std::io API; the TtyBackend
  trait must accommodate blocking backends that bridge to async via std
  threads + tokio mpsc. exit_code resolves to a Future the adapter
  awaits (resolves the load-bearing half of OQ-TTY-01).
- REQ-TTY-02: signal forwarding must target the process group
  (kill(-pgid, sig)), which depends on the child being a session leader
  (portable_pty's controlling_tty=true default).

The POC also validated the control channel (stream_type 3), JSON control
messages (DP-3), and exit-code-on-control-chunk (DP-5). OQ-TTY-01 is
marked resolved with the control-as-Clone-trait-object sub-question left
open with a POC-informed recommendation. The POC itself lives in the dev
workspace, not the repo; this doc is the durable record.
2026-07-05 14:43:28 +00:00
glm-5.2 45fb7efbfc docs(research): remove hedging language from alknet-ssh and alknet-tty research
- alknet-ssh: remove paragraphs describing old hedging problem in channel
  decomposition section and DP-4 — state the insight directly
- alknet-tty: replace 'defer for v1' / 'reserved for future use' with
  'not needed for the current scope' in OQ-TTY-02
2026-07-05 13:08:29 +00:00
glm-5.2 73724b8a1d feat(core,call): implement ADR-050 dynamic resource ownership — ownership store, resource_id_path, check signature, dispatch wiring
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.
2026-07-05 12:57:33 +00:00
glm-5.2 de536b82e1 tasks: add ADR-050 implementation tasks — ownership store, resource_id_path, check signature, dispatch wiring
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.
2026-07-05 11:12:44 +00:00
glm-5.2 ad167aa470 docs(arch): update core/call specs for ADR-050 — ownership provider + resource_id_path
operation-registry.md:
- OperationSpec gains resource_id_path: Option<String> (JSON pointer
  into the input for runtime-spawned resource ID extraction)
- AccessControl::check signature updated: consults an OwnershipProvider
  for dynamic resource ownership; falls back to static Identity.resources
  when no provider is wired (backward compatible)
- Dispatch flow updated: step 3 extracts resource_id via
  spec.resource_id_path before the ACL check
- Added composition + dynamic ownership interaction (ADR-050 §4d):
  two orthogonal checks, ADR-015/022 unchanged
- Design Decisions table + Open Questions + References updated

auth.md:
- New 'Ownership Provider and Store (ADR-050)' section: OwnershipProvider
  (sync read trait) + OwnershipStore (async write trait) + InMemoryOwnershipStore
  default adapter; fourth instance of the repo/adapter pattern (ADR-033)
- How it integrates with AccessControl::check
- Access pattern: proxy-only (spawner owns, proxy to share, teardown
  revokes; no grant mechanism in core)
- Per-node ownership (no cross-node propagation in the base model)
- Resource-scoped ACLs table gains the dynamic ownership path
- Design Decisions table + Open Questions updated
2026-07-05 08:50:04 +00:00
glm-5.2 f6ddd37433 docs(arch): add ADR-050 — dynamic resource ownership for runtime-spawned resources
Writes OQ-42's five decisions into ADR format:

1. Storage: reuse the repo/adapter pattern (ADR-033, fourth instance
   alongside IdentityProvider/IdentityStore/CredentialStore). New traits:
   OwnershipProvider (sync read, hot-path) + OwnershipStore (async write,
   handler lifecycle). In-memory default; persistence adapter additive.
2. Integration: AccessControl::check consults the ownership provider
   directly (Option 2). OperationSpec gains resource_id_path (JSON pointer
   into the input). Backward-compatible — ownership=None falls back to
   the static Identity.resources path.
3. Access pattern: proxy-only. Spawner owns, proxy to share via from_call
   + forwarded_for (ADR-032), teardown revokes. No grant mechanism in
   core. Future grant is additive (new trait method), stated as
   reversal-cost classification, not deferral.
4. Four edge specifics: list = scope-gate + result-filter; teardown =
   automatic, handler-driven; fleet = per-node ownership, downstream app
   tracks 'who is this for'; composition = two orthogonal checks,
   ADR-015/022 unchanged.

Reviewed: zero critical issues. Two warnings fixed (None-handling in the
check sketch, missing ADR-004 cross-ref). One suggestion applied
('v1 mechanism' → 'initial mechanism' to avoid hedging misread).
2026-07-04 16:08:04 +00:00
glm-5.2 13dd15ab0b docs(arch): resolve OQ-42 — proxy-only ownership model for runtime-spawned resources
Lock in the dynamic resource ownership model for runtime-spawned
resources (containers, TTYs, runner workspaces). Three decisions:

1. Storage: reuse the repo/adapter pattern (ADR-033, fourth instance
   alongside IdentityProvider/IdentityStore/CredentialStore) with an
   in-memory default adapter; sync read on the dispatch hot path.
2. Integration: AccessControl::check consults an ownership provider
   directly (Option 2); OperationSpec gains resource_id_path (JSON
   pointer into the input) so the spec is fully self-describing for
   authorization.
3. Access pattern: proxy-only — spawner owns, proxy to share via
   from_call + forwarded_for (ADR-032), teardown revokes. No grant
   mechanism in core; 'poking holes' is a downstream-app concern. A
   future grant is additive (new trait method), stated as reversal-cost
   classification, not deferral.

Four edge specifics pinned: list = scope-gate + result-filter; teardown
= automatic, handler-driven; fleet = per-node ownership, downstream app
tracks 'who is this for'; composition = two orthogonal checks, ADR-015/022
unchanged.

Removes the prior hedging language ('decision direction set', 'open for
the ADR') and the contingent qualifiers from specifics 3/4 now that the
proxy-vs-grant call is made. The dependent crate specs (docker, tty,
runner, fleet) can declare their AccessControl shapes against this model.
2026-07-04 16:02:38 +00:00
glm-5.2 3daecd7ab2 fix(process): add architect Safe Exit for deferred decisions, clean hedging language
- Add Safe Exit section to architect spec: when a decision genuinely can't
  be made, mark OQ as deferred(scope) with concrete blocking condition,
  create blocker task, move on
- Add anti-patterns #10-#11 covering hedging synonyms (feature extension,
  additive, not a v1 blocker, for now, resolved with escape hatch)
- Add hedging audit to architect self-review checklist
- Clean hedging language from resolved OQs (OQ-04, OQ-13, OQ-14, OQ-16)
- Reclassify OQ-32 and OQ-41 as deferred(scope) with blocking conditions
- Add deferred(scope) status to OQ format in sdd_process.md
2026-07-04 15:31:04 +00:00
glm-5.2 f390550a06 docs(arch): set OQ-42 decision direction — repo/adapter storage + Option 2 integration
Two structural decisions for dynamic resource ownership (OQ-42), recorded
in the OQ so ADR drafting starts from a clear position:

1. Storage side reuses the repo/adapter pattern (ADR-033) — a fourth
   instance alongside IdentityProvider/IdentityStore/CredentialStore.
   Trait in alknet-core with an in-memory default adapter; persistence
   adapter separable. Sync read with ArcSwap + honker-NOTIFY cache
   invalidation, same shape as ConfigIdentityProvider (ADR-035). No new
   shape invented; no Phase 0 needed for the storage side.

2. Integration point is Option 2 — AccessControl::check consults the
   ownership provider directly. Rejected Option 1 (augment identity with
   a per-request snapshot) because its purity was theatrical — the
   question 'can X exec into container C' was never purely a function of
   identity, it just looked that way because the resource set was static.
   Option 2 makes check's signature honest about what ACL checking is in
   the presence of dynamic resources. Cost is a check signature change
   (one-way door, every call site updates) — implementation cost, not
   semantic cost, per the project's decision principle.

Refinement that makes Option 2 clean: OperationSpec gains resource_id_path
(JSON pointer into the input, e.g. '$.containerId'). Fits naturally with
the existing JSON-Schema-backed input_schema — the pointer is within an
existing schema on the same spec. OperationSpec becomes fully
self-describing for authorization: resource type, action, and which input
field drives the resource lookup, all declared on the spec.

Four specifics remain open for the ADR: the no-specific-resource (list)
case, teardown coupling, fleet representation (spoke resources on the
hub), and composition interaction with dynamic ownership. These were
surfaced by choosing Option 2 rather than by leaving the integration point
undecided.
2026-07-04 13:04:14 +00:00
glm-5.2 e29672942c docs(arch): record OQ-42 — dynamic resource ownership for runtime-spawned resources
The alknet-docker POC research surfaced that containers are a natural
AccessControl resource, but the resource set is dynamic (containers are
created at runtime) and ownership is derived from creation — which the
current static Identity.resources model (config-sourced via
PeerEntry/CompositionAuthority) doesn't fit. The issue generalizes to
every crate that spawns a thing at runtime and exposes it over the call
protocol (docker, tty, opencode-runner wrapper, alknet-container fleet
layer); solving it per-crate would diverge. Recording as OQ-42 in the
centralized tracker with the generalized framing so the architecture
workflow sees it: one-way door at the model level (core/call), two-way
at the mechanism level, high priority, blocks the dependent crate specs.
A Phase 0 research/POC pass is likely warranted before the ADR.
2026-07-04 11:38:23 +00:00
glm-5.2 8c7443c7c6 docs(research): fix alknet-docker POC normalization crate boundary — alknet-compute is a workload not the fleet layer; add head-worker/machine-node model and dispatch reverse-runner prior art 2026-07-03 10:28:53 +00:00
glm-5.2 4500338384 docs(research): add alknet-tty phase-0 findings — terminal session protocol as separate ALPN, TtyBackend trait, dissolves alknet-ssh PTY hedge 2026-07-03 07:48:21 +00:00
glm-5.2 157f1dfb18 docs(research): add alknet-docker POC summary — validates two-carriage model (JSON + raw) for bollard docker ops over framed bidi streams 2026-07-02 17:08:52 +00:00
glm-5.2 e258ce0523 docs(review): mark review-streaming-impl completed — ADR-049 streaming handler review passes all 12 checklist points 2026-07-02 10:12:19 +00:00
glm-5.2 ab610730c0 docs(http): mark http/server/subscribe-sse-streaming completed — /subscribe pipes BoxStream to SSE 2026-07-02 10:10:55 +00:00
glm-5.2 c77024cdf5 fix(http): update websocket subscription tests to expect call.responded (dispatch_requested now routes Subscription via invoke_streaming) 2026-07-02 10:10:42 +00:00
glm-5.2 9e4d17b1c5 feat(http/server/subscribe-sse-streaming): wire /subscribe to invoke_streaming and pipe BoxStream to SSE
Replace the one-event placeholder (subscribe_stream_from_envelope +
envelope_to_sse_stream, which called invoke() and wrapped the single
ResponseEnvelope) with the real streaming path: subscribe_handler now
calls GatewayDispatch::invoke_streaming() and pipes the
BoxStream<ResponseEnvelope> to SSE via subscribe_stream_from_envelope_stream
(futures::StreamExt::map). Each Ok(output) becomes a data: frame; each
Err becomes an event:error frame (terminal — stream ends after it);
natural stream end closes the SSE. Internal ops still return a single
NOT_FOUND error event via subscribe_stream_internal_error (kept). Client
disconnect drops the stream via Rust's Drop (abort cascade per ADR-016).
2026-07-02 10:04:27 +00:00
glm-5.2 c34b4d2df4 docs(call): mark call/protocol/dispatch-streaming-branch completed — server-side streaming dispatch 2026-07-02 09:59:00 +00:00