- 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
268 lines
15 KiB
Markdown
268 lines
15 KiB
Markdown
# 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.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.1.0]: https://git.alk.dev/alkdev/alktty/releases/tag/v0.1.0 |