Files
alktty/CHANGELOG.md
T
glm-5.3-flash 67cf253621 feat: alkcall 0.7.0 adoption (CF-006 identity alignment) — bump to 0.4.0
- BREAKING (ecosystem-coordination): alkcall bumped to 0.7.0. No type
  alktty touches changed shape (OpenEstablisher/OpenHandler/
  Establishment/ChannelPlan identical to 0.6.0); the minor bump is for
  downstream lockstep, since alktty's public signatures reference
  alkcall types.
- CF-006 alignment: the channels establisher and pump handler now take
  their identity from the per-call auth (alkcall 0.7 derives the
  dispatch-resolved opener — the same identity view the registry's ACL
  gate checked) instead of the captured install-time override.
  Behavior-identical in the per-connection-registry deployment; the
  ownership check and the ACL gate see the same subject in every
  deployment. make_tty_establisher / make_tty_open_handler drop their
  captured-identity parameter (both private).

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-07 17:57:36 +00:00

15 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.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.