# Changelog
All notable changes to this crate are documented here. The format is
based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
this crate adheres to [Semantic Versioning](https://semver.org/).
## [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.
[0.5.0]: https://git.alk.dev/alkdev/alktty/releases/tag/v0.5.0
[0.4.1]: https://git.alk.dev/alkdev/alktty/releases/tag/v0.4.1
[0.4.0]: https://git.alk.dev/alkdev/alktty/releases/tag/v0.4.0
[0.3.0]: https://git.alk.dev/alkdev/alktty/releases/tag/v0.3.0
[0.2.0]: https://git.alk.dev/alkdev/alktty/releases/tag/v0.2.0
[0.1.0]: https://git.alk.dev/alkdev/alktty/releases/tag/v0.1.0