Files
alktty/CHANGELOG.md
T
glm-5.3-flash e95459c425 feat: alkcall 0.8.0 adoption — bump to 0.5.0
- BREAKING (ecosystem-coordination): alkcall bumped to 0.8.0. No type
  alktty touches changed shape (ChannelCore/openable machinery,
  OpenEstablisher/OpenHandler/Establishment/ChannelPlan,
  ChannelClient::open_channel, ChannelOpenError::CallFailed are
  identical to 0.7.1); the new reply-projection / relay / hub-leg
  surfaces are additive and alktty exercises none of them. alktty's own
  open op is the standard-shape name channels/tty/sub, so the new
  flavor-form discovery path stays on the byte-stable standard
  derivation, and with no establisher reply-fields configured the open
  reply is byte-identical to pre-0.8.0. The minor bump is for
  downstream lockstep, since alktty's public signatures reference
  alkcall types.
- Verification counts unchanged from the 0.7.1 baseline (113 default /
  156 all-features); the adoption required no source change in alktty.

Verification:
- cargo test: 113 passed (default), 156 passed (--all-features)
- cargo clippy --all-targets -- -D warnings: clean
- cargo fmt --check: clean
- cargo doc --no-deps: clean
- cargo check/clippy --target wasm32-unknown-unknown -D warnings: clean
- cargo publish --dry-run --allow-dirty: clean
2026-09-18 13:55:53 +00:00

18 KiB

Changelog

All notable changes to this crate are documented here. The format is based on Keep a Changelog, and this crate adheres to Semantic Versioning.

0.5.0 - 2026-09-18

The alkcall 0.8.0 adoption: a dependency bump only — no source change in alktty. alkcall 0.8.0 landed review 008's remediation (the establisher reply projection, flavor-form open-op ids in discovery, the in-tree ChannelRelay + HubLegImports/HubLegTemplate) plus a post-landing hardening sweep; all new surfaces are additive on the call-plane JSON and the registry seams, and the channels data plane is untouched. The wire format, the direct-ALPN path, the TtyBackend trait, and alktty's public API surface are unchanged.

Changed

  • BREAKING (ecosystem-coordination) — alkcall bumped to 0.8.0. alkcall 0.8.0 changed no type that alktty touches: the open-op machinery alktty registers through (ChannelCore/ register_openable_with_establisher/OpenEstablisher/OpenHandler /Establishment/ChannelPlan, ChannelClient::open_channel, ChannelOpenError::CallFailed) is shape-identical to 0.7.1 — the new open_channel_with_reply / reply-field / relay / hub-leg surfaces are additive and alktty exercises none of them. alktty's own open op is the standard-shape name channels/tty/sub, so the new flavor-form channel_open_alpn discovery path stays on the byte-stable standard derivation, and with no establisher reply-fields configured the open reply is byte-identical to pre-0.8.0. alktty's public signatures reference alkcall types, so downstream crates must now unify on alkcall 0.8.0. Minor bump per the 0.x ecosystem-coordination rule.
  • alkcall requirement updated to 0.8.0; lockfile bumped from 0.7.1 to 0.8.0. Verification counts are unchanged from the 0.7.1 baseline (113 default / 156 all-features) — the adoption required no source change in alktty.

0.4.1 - 2026-09-10

The MSRV floor becomes honest: rust-version raises from 1.85 to 1.88. The 1.85 claim was already false at the dependency level — the lockfile pins alkcall 0.7.x, and alkcall 0.7.1 raised its MSRV floor to 1.88 (the ecosystem MSRV audit: noq's 1.88 matching the QUIC path; iroh sits above at 1.91), so no 1.85 toolchain could build 0.4.0 regardless of what Cargo.toml declares. The raise breaks no downstream that could build the crate before. No API or wire-format change; downstreams on caret requirements pick this up on their next cargo update.

Changed

  • rust-version 1.85 → 1.88. Verified on a real 1.88 toolchain (full test suite, clippy). alktty's own code is 1.85-clean — cargo check under a 1.85 toolchain passes with the pre-bump lockfile — so the raise is dependency-driven only and needed no clippy fixes. Behavior-identical.
  • alkcall requirement updated to 0.7.1; lockfile bumped from 0.7.0 to 0.7.1. Version-requirement-only (0.7.x is already the resolved family); alkcall 0.7.1 changed no type alktty touches.

0.4.0 - 2026-09-07

The alkcall 0.7.0 adoption (the connect-side caller-identity seam, CF-005/006/007): a dependency bump plus one behavior-alignment change on the channels path. The wire format, the direct-ALPN path, the TtyBackend trait, and the public API surface are unchanged.

Changed

  • BREAKING (ecosystem-coordination) — alkcall bumped to 0.7.0. alkcall 0.7.0 changed no type that alktty touches (OpenEstablisher/OpenHandler/Establishment/ChannelPlan are identical to 0.6.0; alktty never constructs ServingConfig), but alktty's public signatures reference alkcall types, so downstream crates must now unify on alkcall 0.7.0. Minor bump per the 0.x ecosystem-coordination rule.
  • The channels establisher and pump handler take their identity from the per-call auth (CF-006 alignment). register_openable no longer captures the install-time identity and overrides the establisher/handler auth.identity with it — post-alkcall-0.7 the per-call context already carries the dispatch-resolved opener (the same identity view the registry's ACL gate checked; the end client, not the hub, on hub-forwarded opens). Behavior-identical in the per-connection-registry deployment (install-time identity was the caller's); the ownership check and the ACL gate now see the same subject in every deployment. Two factory parameters dropped (make_tty_establisher/make_tty_open_handler are private; no public-surface change).

0.3.0 - 2026-09-07

The alkcall 0.6.0 adoption (review 007 R-01 — the Establishment plan payload): backend allocation moves into the channels establisher. Breaking on the channels path's allocation-failure shape; the wire format, the direct-ALPN path, and the TtyBackend trait are unchanged.

Changed

  • BREAKING — alkcall bumped to 0.6.0; channels-path allocation failure changes shape (ADR-010 §2A). alkcall 0.6.0 filled the reserved Establishment field (plan: Option<ChannelPlan>, review 007 R-01), so backend.allocate moved from the pump handler into the establisher — the allocated TtyHandle crosses to the handler via the plan payload (a private per-open one-shot slot; the handle is not Sync). Allocation failure now fails the open op itself: TtySessionError::ChannelsOpen(CallFailed{ channel:open_failed, reason: "dial_failed" }) instead of the post-open in-band NegotiationRejected{ "allocate_failed" }. The SSH contract now covers every failure class: no channel ever exists opener-side, no channel_id, no 0x00-prefixed in-band frame. The direct-ALPN path's in-band vocabulary (including allocate_failed) is unchanged.
  • tty_open_spec()'s channel:open_failed ErrorDefinition declares dial_failed in the details.reason enum (four reachable reasons: dial_failed / unknown_resource / handler_error / timeout).
  • Handler closures in make_tty_open_handler gain the Option<ChannelPlan> parameter (alkcall 0.6 OpenHandler shape); a None plan (no-establisher registration) falls back to the pre-0.3.0 inline validate-and-allocate — defense-in-depth, keeping the in-band error-frame vocabulary for that registration shape.
  • New tests: establisher unit gate for allocate-in-establisher (plan slot carries the live handle; failure maps to dial_failed), the end-to-end dial_failed open-failure surface (flipped from the pinned in-band test), and the handler's plan: None fallback arm.

0.2.0 - 2026-09-06

The alkcall 0.5.0 adoption (ADR-049 — review 006 E-01/N-1): the channels-path establishment phase. Breaking on the channels path's error surface; the wire format, the direct-ALPN path, and the TtyBackend trait are unchanged.

Added

  • Channels establishment phase (ADR-010; alkcall 0.5.0 ADR-049 / review 006 E-01). register_openable now registers channels/tty/sub with an establisher (ChannelCore::register_openable_with_establisher): the semantic validation that used to be post-open in-band error frames — the full NegotiateRequest parse of schema-valid input, carriage == "raw", non-empty cmd, backend lookup, and the ADR-050 ownership check — runs before the open reply. Rejections resolve the open op with channel:open_failed carrying details: { reason, message } (unknown_resource for an unknown backend, handler_error for malformed-negotiation/ownership-denial, timeout on the establishment deadline) and no channel ever exists opener-side — the SSH contract. The phantom-channel workaround (allocate → succeed → in-band-fail) is retired.
  • channel:open_failed ErrorDefinition on tty_open_spec() (ADR-016). The establishment-failure contract (the reachable details.reason enum) is disclosed via services/schema; the spec also gains a description (review 006 E-02) disclosed via services/list.
  • New tests: the establisher unit gates (parse/carriage/cmd/backend/ ownership mappings), the end-to-end channel:open_failed surfaces (the review-006 verification gate), and a pinned test guarding the one failure class that stays in-band (below).

Changed

  • BREAKING — TtySessionError::ChannelsOpen carries alkcall's typed ChannelOpenError (#[from]) instead of a flattened String (alkcall 0.5.0 ADR-049 §4 / review 006 N-1, applied at alktty's layer). Consumers branch on ChannelOpenError::establishment_reason() instead of parsing a debug string.
  • BREAKING — channels-path semantic failures change shape. An unknown backend (and schema-valid-but-unparseable params, ownership denial) previously surfaced as TtySessionError::NegotiationRejected (an in-band 0x00-prefixed error frame on a channel the open op had already reported as succeeding); they now fail the open op itself as TtySessionError::ChannelsOpen(CallFailed{ channel:open_failed }) (ADR-010). The one failure class that still arrives in-band is allocate_failed (still NegotiationRejected): the establisher cannot carry the allocated TtyHandle across to the pump handler (Establishment is payloadless), and re-allocating would violate ADR-005's kill-on-Drop contract — revisit when alkcall gives Establishment a payload. The direct-ALPN path's failure surface is unchanged (two transports, two contracts).
  • alkcall = "0.5.0".

Fixed

  • No more phantom channels on the channels path. Ledger, policy count, and manager state balance on every establishment rejection (the wrapper's teardown runs before the reply), instead of allocating a channel that immediately EOFs with an error frame.

0.1.0 - 2026-09-05

Initial crates.io release: the alk/tty terminal-session protocol — producer/consumer protocol crate on top of alkcall channels, ported and consolidated from the alknet-tty + alknet-tty-local crates of the alknet mono-repo.

Added

  • Wire format (ADR-001). Two-carriage protocol: a 4-byte big-endian length-prefixed JSON negotiation frame (NegotiateRequest — carriage/backend/tty/cmd/cwd/env plus opaque serde(flatten) backend params), then raw chunks ([stream_type: u8][length: u32 be][payload]) with five stream types (STREAM_STDIN=0, STREAM_STDOUT=1, STREAM_STDERR=2, STREAM_CTRL_IN=3, STREAM_CTRL_OUT=4). Zero-length data chunks are sentinels (zero-length stdin = client EOF; zero-length stdout = server drained); control chunks are never zero-length. Payloads are capped at 16 MiB (MAX_CHUNK_LEN) — this keeps the length prefix's high byte 0x00, which is the disambiguation invariant between an error frame and a raw chunk (ADR-001 §5). The framing is self-contained (ADR-006): not reused from alkcall's EventEnvelope framing. The control channel is split into STREAM_CTRL_IN/STREAM_CTRL_OUT so it is genuinely bidirectional on the wire (the Phase 7 amendment inside ADR-001). TTY always uses its own 5-byte chunk format inside channels — the channels layer carries it transparently in the channel payload (ADR-008, reversing ADR-007). The machine-readable contract is the BAST document (docs/architecture/tty-bast.md); a unit test guards the BAST's StreamType enum against drift from the wire constants.
  • TtyBackend trait (ADR-002). The inversion point between the wire-format adapter and backend crates. allocate(&TtyParams) -> TtyHandle (stdin AsyncWrite, stdout/stderr Stream<Item = Bytes>, exit_code: BoxFuture, TtyControlHandle for resize/signal); resource_id(&TtyParams) -> Option<(kind, id)> feeds the ADR-050 ownership check. TtyError is #[non_exhaustive]. The trait shape is a one-way door once backends exist.
  • Local backend behind the local feature (ADR-003). The alknet-tty-local crate folded in as a feature-gated module: PTY mode via portable_pty (real terminal semantics — resize, process-group signal forwarding, merged stdout/stderr) and pipe mode via tokio::process::Command (the runner case — separate stdout/stderr, no-op resize, pid-only signal), selected by TtyParams::terminal. The blocking→async bridge uses three dedicated std threads feeding tokio mpsc/oneshot channels. Non-wasm by design; the default crate (no features) stays wasm32-unknown-unknown-clean.
  • TtyAdapter producer (direct alk/tty ALPN). A ProtocolHandler holding a HashMap<String, Arc<dyn TtyBackend>> keyed by the negotiation frame's backend string. Accepts a connection, loops accept_bi, and dispatches each bidi stream to a session. Per-stream flow (ADR-001/002/005): parse + validate the negotiation frame (carriage == "raw", non-empty cmd), scope-gate via tty:open, optional OwnershipProvider resource check, backend.allocate(), then three concurrent pumps (stdin→backend, stdout/stderr→client, exit→exit chunk) with the exit-chunk-is-last invariant — {"type":"exit","code":N} (-1 on wait failure, ADR-004; negative = signal-terminated) is enqueued only after both stream pumps complete and exit_code resolves. A concurrent drainer task owns the client write half, so a burst of >64 chunks from the backend cannot deadlock the session (the drainer starts before the pumps join; the single FIFO preserves exit-chunk-last). On session cancel the TtyHandle drops without driving exit_code to completion, which triggers the backend's kill-on-Drop guard — the adapter has no separate kill path (the session-cancel contract, ADR-005: dropping the future kills the session target).
  • channels integration (ADR-008, ADR-009). register_openable + tty_open_spec register the channels/tty/sub open op on a per-connection OperationRegistry (per ADR-047's per-connection amendment): the registry's AccessControl carries the tty:open scope gate, the op's input schema validates the shared NegotiateRequest fields (carriage/backend/cmd required; alkcall 0.4 enforces at dispatch), and the OpenHandler spawns the session driver pre-negotiated — the open op's params ARE the negotiation (ADR-009), no second frame on the channel stream. Access control is enforced by the registry before the handler runs; the handler validates the backend exists and drives the session.
  • TtySession consumer. The typed client handle with two constructors — connect_direct (direct alk/tty connection: writes the negotiation frame, then typed methods) and open_via_channels (opens channels/tty/sub on a ChannelClient, adopts the channel, starts in raw-chunk mode). Typed methods: send_stdin/close_stdin (stdin chunks / EOF sentinel), recv_stdout/recv_stderr (Stream<Item = Bytes>), resize, signal, and wait (awaits the Exit control chunk; watch-based, no lost wakeup). recv_stdout ends ON the zero-length drained sentinel (it is not yielded as an item). Both constructors disambiguate the first response frame (0x00 prefix = negotiation error frame → NegotiationRejected with the server's error code; raw chunk = session proceeds); a refused session open surfaces as TtySessionError::Open carrying the upstream error type. TtySessionError is #[non_exhaustive]. Dropping the session aborts the read pump and closes the write half.
  • Control messages. ControlMessage (Resize/Signal/Eof on ctrl_in; Exit on ctrl_out) — JSON, tagged by "type", unknown types ignored by policy. signal_from_name maps the common signal set (HUP/INT/QUIT/TERM/KILL/USR1/USR2/TSTP/CONT) to libc numbers (unix).
  • Crate-root re-exports. The primary types (TtyAdapter, TtySession, TtyBackend, TtyHandle, TtyParams, Chunk, NegotiateRequest, ControlMessage, the wire constants, and LocalTtyBackend under local) are re-exported at the crate root; the module paths remain the full surface.
  • Wire write-path validation. All ChunkWriter methods validate before touching the transport — a stream_type > 4 or a payload over MAX_CHUNK_LEN fails locally instead of writing a header the peer cannot frame (the length is validated before the u32 cast, so an oversized payload cannot truncate past the check and corrupt the peer's framing). Empty-payload writes are one shape: length == 0 with no payload bytes.
  • Peek-safe ChunkReader. The reader tracks peeked state: read_chunk() after peek_stream_type() completes the peeked chunk instead of consuming a second header byte (which silently desynchronized framing), and a second peek returns the peeked byte without reading. read_chunk_after_peek remains the explicit form.
  • Backpressure regression test. >64-chunk sessions are driven through the real adapter with a per-read timeout so a deadlock regression fails the test instead of hanging it, plus exact-boundary (MAX_CHUNK_LEN) round-trip and server-sends-client-direction- stream-types discard tests.
  • PTY bridge robustness (local feature). The stdin sink's EOF (poll_shutdown) parks an in-flight send on a full channel so the waker is registered — a stdin blast followed by EOF always delivers the EOF to the child. Lock-poisoning no longer cascades (the five lock sites adopt into_inner()), and thread-spawn failure maps to TtyError::AllocFailed instead of panicking.
  • Producer hardening. The tty:open scope gate runs before the backend lookup (an unscoped identity gets forbidden regardless of the request body — no backend-name enumeration differential), and the client→backend pump is aborted at session end instead of lingering until the client disconnects.