Commit Graph
603 Commits
Author SHA1 Message Date
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