- Bump alkcall 0.5.0 -> 0.6.0 (Establishment plan payload, ADR-049 amendment 2); alktty 0.2.0 -> 0.3.0 (breaking channels-path allocation-failure shape) - make_tty_establisher runs backend.allocate; failure maps to EstablishmentError::DialFailed (channel:open_failed reason dial_failed); the TtyHandle crosses to the pump handler via a private per-open AllocatedHandle one-shot slot (the handle is not Sync — it cannot be the ChannelPlan directly) - make_tty_open_handler gains the Option<ChannelPlan> parameter; a plan drives the new drive_session_pre_allocated (pumps only), a None plan falls back to drive_session_pre_negotiated (inline validate-and-allocate — defense-in-depth for no-establisher registrations) - tty_open_spec's channel:open_failed ErrorDefinition declares dial_failed (four reachable reasons) - Pinned in-band allocate test flipped: allocate_failure_fails_open_as_dial_failed (end-to-end, no channel survives); establisher unit gate for the plan slot + failure mapping - ADR-010 amended (§2A; §2 kept as historical record), tty-adapter.md, session/channels/adapter docs, CHANGELOG, AGENTS.md alkcall pin updated Verification: cargo test (113) + --all-features (137), clippy -D warnings (native + wasm32, default + all-features), fmt, doc, wasm32 check — all clean
407 lines
23 KiB
Markdown
407 lines
23 KiB
Markdown
---
|
|
status: draft (ported from alknet 2026-08-17; alknet/tty → alk/tty,
|
|
alknet-core → alkcall::core, alknet-call → alkcall, ADRs renumbered
|
|
052..093 → 001..009)
|
|
last_updated: 2026-09-05
|
|
---
|
|
|
|
# alktty — TtyAdapter and Session Lifecycle
|
|
|
|
The `TtyAdapter` is the `ProtocolHandler` for `alk/tty`: it receives a
|
|
`Connection`, accepts bidi streams, reads the negotiation frame, selects
|
|
a `TtyBackend` (ADR-002), and pumps bytes bidirectionally for the life of
|
|
the session using the wire format (ADR-001). This document specifies the
|
|
session lifecycle, the three-pump driver, negotiation errors, the
|
|
exit-chunk ordering (ADR-004), session-cancel cleanup (ADR-005), and
|
|
access control.
|
|
|
|
## What
|
|
|
|
`TtyAdapter` implements `ProtocolHandler` (alknet ADR-002, revised by
|
|
alknet ADR-007 to receive a `Connection`) on ALPN `alk/tty` (alknet
|
|
ADR-006). It holds a `HashMap<String, Arc<dyn TtyBackend>>` populated at
|
|
construction (ADR-002 §5). Its `handle()` method accepts the connection
|
|
and loops `connection.accept_bi()`, dispatching each bidi stream to a
|
|
session. One `alk/tty` connection hosts multiple terminal sessions — one
|
|
session per bidi stream (DP-6, decided in the alknet research; matches
|
|
the call protocol's one-operation-per-stream model).
|
|
|
|
```rust
|
|
pub struct TtyAdapter {
|
|
/// Backends keyed by the negotiation frame's `backend` string
|
|
/// ("local", "docker", "ssh"). Populated at construction.
|
|
backends: Arc<HashMap<String, Arc<dyn TtyBackend>>>,
|
|
/// Optional ownership provider (alknet ADR-050) for terminal sessions as
|
|
/// runtime-spawned resources. None = no resource-level ACL (scope-
|
|
/// gate only). Wired by the assembly layer.
|
|
ownership: Option<Arc<dyn OwnershipProvider>>,
|
|
}
|
|
|
|
#[async_trait]
|
|
impl ProtocolHandler for TtyAdapter {
|
|
fn alpn(&self) -> &'static [u8] { b"alk/tty" }
|
|
|
|
async fn handle(&self, connection: Connection, auth: &AuthContext)
|
|
-> Result<(), HandlerError>
|
|
{
|
|
// One connection → many sessions (one bidi stream each).
|
|
while let Ok((send, recv)) = connection.accept_bi().await {
|
|
let backends = self.backends.clone();
|
|
let ownership = self.ownership.clone();
|
|
let identity = auth.identity.clone();
|
|
tokio::spawn(async move {
|
|
let _ = drive_session(send, recv, backends, ownership, identity).await;
|
|
});
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
```
|
|
|
|
The `drive_session` function is the per-stream session driver — the
|
|
counterpart to the POC's `session::drive_session`, generalized from the
|
|
local PTY backend to the `TtyBackend` trait. It is also the function the
|
|
channels path reuses — per [ADR-008](decisions/008-channels-pure-channel-multiplexing.md),
|
|
TTY always uses its 5-byte format, so the same driver runs in
|
|
both direct `alk/tty` and channels `alk/channels` modes; only the
|
|
`BiStream` source differs. The two paths differ in where the
|
|
`NegotiateRequest` comes from and where semantic failures go: the
|
|
direct path reads the wire-frame negotiation (ADR-001 §"Negotiation
|
|
Frame") and answers failures with in-band error frames; the channels
|
|
path parses the open op's registry-validated `input` and runs
|
|
`drive_session_pre_negotiated` (ADR-009 — no second negotiation
|
|
frame), with the establisher rejecting the semantic-failure classes
|
|
as `channel:open_failed` before the handler spawns (ADR-010).
|
|
|
|
## Why
|
|
|
|
The adapter is the place where the wire format (ADR-001), the backend
|
|
trait (ADR-002), and the exit-chunk ordering (ADR-004) come together.
|
|
Keeping these in one place — the adapter — is what makes the invariants
|
|
enforceable: the wire format's "exit chunk is last" invariant is enforced
|
|
here, not in the backends (which produce handles, not wire bytes); the
|
|
backend's `exit_code` future is awaited here, not in the backend; the
|
|
negotiation frame is parsed here, not in the backend. The adapter is
|
|
backend-agnostic; the backends are wire-format-agnostic. The inversion is
|
|
the `TtyBackend` trait.
|
|
|
|
## Architecture
|
|
|
|
### Session Lifecycle
|
|
|
|
A `alk/tty` session on one bidi stream proceeds in three phases:
|
|
|
|
1. **Negotiation.** The adapter reads the single length-prefixed JSON
|
|
negotiation frame from the client (ADR-001 §"Negotiation Frame"),
|
|
parses it into `NegotiateRequest`, extracts the `backend` string,
|
|
looks up the `TtyBackend`, and constructs `TtyParams`. If the backend
|
|
is not registered, or the negotiation frame is malformed, the adapter
|
|
sends a JSON error response and closes the stream (see §"Negotiation
|
|
errors" below).
|
|
|
|
2. **Allocation.** The adapter calls `backend.allocate(¶ms)`, which
|
|
returns a `TtyHandle` (ADR-002). If allocation fails (PTY couldn't be
|
|
allocated, docker exec failed, SSH channel request rejected), the
|
|
adapter sends a JSON error response and closes the stream.
|
|
|
|
3. **Raw carriage — the bidirectional pump.** The adapter switches to
|
|
the raw chunk format and pumps three concurrent tasks:
|
|
|
|
- **A. stdout → client**: backend stdout (`TtyHandle.stdout`) → stdout
|
|
chunks (stream_type 1) to the client. If `TtyHandle.stderr` is
|
|
`Some`, a concurrent stderr pump emits stderr chunks (stream_type 2).
|
|
On backend stdout EOF, emit a zero-length stdout sentinel.
|
|
- **B. client → backend**: client chunks → backend. stdin chunks
|
|
(stream_type 0) → `TtyHandle.stdin` (via `AsyncWrite`).
|
|
Client→server control chunks (`STREAM_CTRL_IN`, stream_type 3) →
|
|
`ControlMessage` dispatch: `Resize` → `TtyControl::resize`, `Signal`
|
|
→ `TtyControl::signal`, `Eof` → close stdin. `Exit` on
|
|
`STREAM_CTRL_IN` is a protocol violation (it's server→client only)
|
|
and is ignored. `STREAM_CTRL_OUT` (stream_type 4) from the client is
|
|
a protocol violation (it's the server→client half) and is ignored.
|
|
On client read-half close or a zero-length stdin chunk, signal EOF
|
|
to the backend's stdin.
|
|
- **C. exit → exit chunk**: await `TtyHandle.exit_code`; on resolve,
|
|
enqueue `{"type":"exit","code":N}` as a server→client control
|
|
chunk (`STREAM_CTRL_OUT`, stream_type 4).
|
|
|
|
A drainer task writes chunks to the client in arrival order. After the
|
|
exit chunk is written (task C resolves and the exit chunk drains),
|
|
the adapter closes the write half — the session ends.
|
|
|
|
This is the POC's `session::drive_session` pattern, generalized: the POC
|
|
hardcoded the local PTY backend; the adapter dispatches to any
|
|
`TtyBackend`.
|
|
|
|
### Bidirectional Control Channel (Phase 7)
|
|
|
|
The control channel is split into two halves so it is genuinely
|
|
bidirectional on the wire:
|
|
|
|
- **`STREAM_CTRL_IN = 3`** — client→server control (`Resize`, `Signal`,
|
|
`Eof`).
|
|
- **`STREAM_CTRL_OUT = 4`** — server→client control (`Exit`).
|
|
|
|
The adapter enforces the direction:
|
|
|
|
- An `Exit` arriving on `STREAM_CTRL_IN` is a protocol violation
|
|
(server→client message on the client→server half) — the adapter
|
|
ignores it (the previous single `STREAM_CONTROL = 3` could not
|
|
distinguish the two directions, so `Exit` from the client was always
|
|
ignored; the split makes the rejection explicit).
|
|
- A `Resize`/`Signal`/`Eof` arriving on `STREAM_CTRL_OUT` is a protocol
|
|
violation (client→server message on the server→client half) — the
|
|
adapter ignores it (the server never dispatches control messages it
|
|
receives on the server→client half).
|
|
- `STREAM_CTRL_OUT` (stream_type 4) chunks written by the client are a
|
|
protocol violation (the client should not write on the server→client
|
|
half) — the adapter ignores them.
|
|
|
|
The exit chunk (`Exit`) is emitted on `STREAM_CTRL_OUT` (stream_type
|
|
4), not on the previous `STREAM_CONTROL = 3`. A client distinguishing
|
|
the two halves can route exit vs. control without parsing the JSON
|
|
`type` tag first. See ADR-001 §"Control channel split" and
|
|
`tty-wire.md` §"Control Channel".
|
|
|
|
### Negotiation Errors
|
|
|
|
If the server cannot allocate the session, it sends a JSON error response
|
|
in the same length-prefixed framing as the negotiation frame (the JSON
|
|
carriage, not the raw chunk format) and closes the stream without
|
|
entering raw mode. The error response shape:
|
|
|
|
```json
|
|
{ "error": "unknown_backend", "backend": "kubernetes" }
|
|
```
|
|
|
|
On the **direct-ALPN path** this is the failure surface for every
|
|
negotiation/allocation failure class (below). On the **channels path**
|
|
the semantic classes (unknown backend, malformed negotiation,
|
|
ownership denial) **and allocation** are rejected by the establisher
|
|
before the open reply and surface as `channel:open_failed` call
|
|
errors (alkcall ADR-049 / alktty ADR-010, as amended for alkcall 0.6's
|
|
`Establishment` plan payload — ADR-010 §2A) — the in-band frames on
|
|
that path are defense-in-depth arms only (reachable from
|
|
no-establisher registrations).
|
|
|
|
| Error | When | Shape |
|
|
|-------|------|------|
|
|
| `unknown_backend` | the `backend` string is not in the adapter's backend map — direct path, or the channels path's defense-in-depth arm (the establisher rejects it as `channel:open_failed` / `unknown_resource` first) | `{"error":"unknown_backend","backend":"..."}` |
|
|
| `malformed_negotiation` | the negotiation frame failed to parse as JSON or failed `NegotiateRequest` validation — on the direct path the wire frame; on the channels path the open op's `input` (schema-valid values can still fail the typed parse, e.g. `cwd` typed as a number, because the schema is deliberately partial — the establisher rejects it as `channel:open_failed` / `handler_error` first; the handler-side frame is defense-in-depth, ADR-010 §2) | `{"error":"malformed_negotiation","message":"..."}` |
|
|
| `allocate_failed` | `backend.allocate()` returned a `TtyError` — the failure surface of the **direct path**. On the channels path the establisher runs `allocate` (the handle crosses to the pump handler via alkcall 0.6's `Establishment` plan payload — ADR-010 §2A) and its failure is a `channel:open_failed` call error (`details.reason == "dial_failed"`) | `{"error":"allocate_failed","message":"..."}` |
|
|
|
|
After sending the error response, the adapter closes the write half of
|
|
the bidi stream. The client reads the error frame and treats stream close
|
|
as the failure signal. There is no `call.error` — this is not the call
|
|
protocol; the error is a JSON response in the negotiation framing.
|
|
**Framing disambiguation (success vs error).** Both a successful
|
|
allocation (raw chunks) and a failed allocation (JSON error frame) begin
|
|
with bytes the client must read before knowing which framing applies.
|
|
The disambiguation is by the first byte: a JSON error frame's 4-byte
|
|
big-endian length prefix always starts with `0x00` (error frames MUST
|
|
be under 16 MiB — `MAX_CHUNK_LEN` — so the high byte is zero; this is
|
|
a wire-format invariant, not an assumption), while a raw chunk's first
|
|
byte is a `stream_type`. The server never sends `0` (stdin —
|
|
client→server only) or `3` (`STREAM_CTRL_IN` — client→server only), so
|
|
the server-sent set is `{1, 2, 4}` (stdout, stderr, `STREAM_CTRL_OUT`);
|
|
`0x00` is unambiguous. The client distinguishes: read the first byte; if
|
|
it is `0x00`, interpret the next 4 bytes as a big-endian length prefix
|
|
and read that many bytes as a JSON error frame; otherwise interpret it
|
|
as a `stream_type` byte and continue reading the raw chunk header. This
|
|
is a one-way-door wire-format invariant (ADR-001): error frames use the
|
|
negotiation framing (length prefix) and MUST be under 16 MiB; success
|
|
uses the raw chunk framing (stream_type byte first); the
|
|
`0x00`-as-length-prefix vs `0x00`-as-invalid-stream_type disambiguation
|
|
is what makes the two distinguishable on the wire.
|
|
|
|
### Exit-Chunk Ordering (ADR-004)
|
|
|
|
The "exit chunk is last" invariant (ADR-004) is enforced here, in the
|
|
adapter's session driver, not in the backend. The ordering:
|
|
|
|
1. The stdout pump (task A) drains the backend's stdout to EOF. The
|
|
backend's stdout ends when the process exits and the PTY/pipe buffer
|
|
drains (Unix `Child::wait()` blocks until the child is reaped, which
|
|
happens after the child exits and its stdout drains — ADR-004
|
|
assumption 1).
|
|
2. The exit task (task C) awaits `TtyHandle.exit_code`. The exit resolves
|
|
after the child is reaped (the local backend's waiter thread calls
|
|
`Child::wait()`; docker's `inspect_exec` after the output stream ends;
|
|
SSH's channel close after the process exits).
|
|
3. **The adapter waits for *both* the stdout pump to complete (EOF)
|
|
*and* `exit_code` to resolve** before enqueueing the exit chunk. If a
|
|
backend's stdout outlives the exit resolve (a hypothetical backend
|
|
where the process exits but a buffer flush is still in flight), the
|
|
adapter waits for the stdout pump; the `TtyHandle.stderr` (if `Some`)
|
|
is pumped concurrently and also drains before the exit chunk. (ADR-004
|
|
assumption 2.)
|
|
4. After both resolve, the exit chunk (`{"type":"exit","code":N}`) is
|
|
enqueued on the writer channel.
|
|
5. The drainer writes the exit chunk to the client.
|
|
6. The adapter closes the write half — the session ends.
|
|
|
|
A client reads stdout/stderr/control chunks until it sees the exit
|
|
chunk, records the exit code, and treats subsequent stream close as the
|
|
session end. The exit chunk is the deterministic completion signal —
|
|
the same stopgap property the docker POC validated for logs subscriptions,
|
|
now for any backend.
|
|
|
|
If `exit_code` resolves with a `TtyError` (the backend couldn't determine
|
|
the exit code), the adapter sends `{"type":"exit","code":-1}` (ADR-004
|
|
§4). The client treats `-1` as "the backend reported an exit error, not
|
|
a real exit code."
|
|
|
|
### Access Control
|
|
|
|
Terminal sessions are runtime-spawned resources per alknet ADR-050. A
|
|
`alk/tty` session is a resource the caller owns: the caller that
|
|
opened the session owns it; proxy to share; teardown (stream close)
|
|
revokes. The adapter's access control declares against the ADR-050 model:
|
|
|
|
- **Scope-gate at negotiation.** The adapter checks the caller's
|
|
`identity.scopes` for the `tty:open` scope (or a deployment-configured
|
|
scope) before allocating the session. A caller without the scope gets
|
|
a negotiation error (`{"error":"forbidden"}`) and the stream closes.
|
|
- **Resource ownership for backend-specific resources.** Some
|
|
backends target a pre-existing resource (a docker backend targets a
|
|
specific container); others create their own (a local backend's
|
|
process, an SSH backend's channel). The adapter delegates the
|
|
resource-id extraction to the backend via
|
|
`TtyBackend::resource_id(¶ms)` (ADR-002), which returns
|
|
`None` (no pre-existing resource — the session creates its own) or
|
|
`Some((kind, id))` (the caller must own this resource). The adapter
|
|
checks `OwnershipProvider::owns(identity, kind, id, "tty")` if an
|
|
ownership provider is wired and the backend returns `Some`. The
|
|
adapter does not parse backend-specific JSON itself — the extraction
|
|
is backend-driven, so adding a backend with a new resource shape (e.g.,
|
|
a Kubernetes backend targeting a pod) requires no adapter change.
|
|
- **`forwarded_for` for proxied sessions.** A hub that proxies a
|
|
terminal session to a worker carries the end user's identity as
|
|
`forwarded_for` (alknet ADR-032); the worker authorizes the hub (its
|
|
direct caller), not the end user. The hub's end-user ACL is its own
|
|
layer.
|
|
|
|
The tty adapter is a `ProtocolHandler`, not an `OperationSpec`-registered
|
|
operation — it doesn't go through the call protocol's
|
|
`OperationRegistry::invoke()`. The access-control shape is the adapter's
|
|
own (scope-gate + backend-driven ownership check at negotiation),
|
|
declaring against the ADR-050 model but not consuming
|
|
`OperationSpec.resource_id_path` (that field is for call-protocol
|
|
operations; the tty adapter is its own ALPN, and the resource-id
|
|
extraction is delegated to the backend via `resource_id()` rather than
|
|
a path expression). See alknet ADR-050 §"Specifics" for the model this
|
|
declares against.
|
|
|
|
The concrete choice — the scope name (`tty:open`) and the
|
|
check-at-negotiation timing — is a **two-way-door** choice within the
|
|
one-way `TtyAdapter` shape. The scope name can be renamed (a
|
|
deployment-configured scope, not a wire-format constant). The
|
|
resource-id extraction is backend-driven via `resource_id()`, so new
|
|
backends with new resource shapes require no adapter change — the
|
|
generalization is already in place (ADR-002). No ADR is warranted for
|
|
the scope name; it is a reversible implementation choice, not an
|
|
architectural commitment.
|
|
|
|
### Connection and Stream Lifecycle
|
|
|
|
- **Connection drop**: when the QUIC connection closes, all in-flight
|
|
sessions on that connection are cancelled. Each session's pump tasks
|
|
are dropped (Rust `Drop`); the `TtyHandle` is dropped; the
|
|
`exit_code` future is dropped without being driven to completion,
|
|
which triggers the backend's cancel-cleanup — the session target is
|
|
killed (ADR-005). For the local backend, the `exit_code` future's
|
|
`Drop` calls `ChildKiller::kill(SIGHUP)`, the child exits, the
|
|
waiter thread's `wait()` reaps it and exits, and the reader/writer
|
|
threads exit on channel close. For docker/SSH backends (future), the
|
|
`Drop` issues the backend's kill (container kill / channel close).
|
|
See ADR-005 for the contract and the mechanism.
|
|
- **Stream reset**: when a bidi stream is reset mid-session, the
|
|
`ChunkReader` returns a `RawError` (ConnectionClosed or Io). The pump
|
|
tasks exit; the `TtyHandle` is dropped; the cancel-cleanup runs
|
|
(ADR-005). No exit chunk is sent — the stream is gone, the client
|
|
that reset it already knows.
|
|
- **Client cancel**: when the client closes the write half (or sends a
|
|
zero-length stdin chunk / `eof` control chunk), the adapter signals
|
|
EOF to the backend's stdin and keeps pumping stdout until the backend's
|
|
stdout ends and the exit resolves. The session completes normally —
|
|
the exit chunk is sent — the client just stopped sending input. This
|
|
is NOT a cancel from the adapter's perspective (the session runs to
|
|
completion); the cancel-cleanup (ADR-005) is not triggered. The
|
|
cancel-cleanup is triggered only when the *adapter* drops the handle
|
|
(connection drop, stream reset, panic), not when the client closes
|
|
the write half.
|
|
|
|
## Constraints
|
|
|
|
- **The adapter, not the backend, owns the wire format.** Backends
|
|
produce handles; the adapter pumps. A backend that wrote to the wire
|
|
directly would break the "exit chunk is last" invariant (ADR-004) and
|
|
the negotiation-error framing.
|
|
- **One session per bidi stream, multiple streams per connection.** A
|
|
connection hosts multiple sessions (one stream each); the adapter
|
|
spawns a `drive_session` task per accepted stream. Sessions are
|
|
independent — one session's exit doesn't affect another.
|
|
- **Negotiation errors are JSON, not raw chunks.** The error response
|
|
uses the negotiation framing (length-prefixed JSON), not the raw chunk
|
|
format. The stream enters raw mode only after a successful allocation.
|
|
- **The exit chunk is the deterministic completion signal.** A client
|
|
reading to completion sees the exit chunk and knows the process exited
|
|
with code N. A client that cancels mid-stream (closes the write half)
|
|
won't see the exit chunk — that's correct; a cancelled stream doesn't
|
|
have a deterministic exit.
|
|
- **The adapter triggers backend cleanup by dropping the `TtyHandle`
|
|
(ADR-005).** On connection drop, stream reset, or pump-task panic, the
|
|
adapter's pump tasks are dropped, which drops the `TtyHandle`, which
|
|
drops the `exit_code` future without driving it to completion. The
|
|
`exit_code` future's `Drop` is the backend's cancel-cleanup path
|
|
(kill the session target). The adapter has no separate kill method to
|
|
call; the cleanup is wired into the `exit_code` future's `Drop` by the
|
|
backend. A backend that returns an `exit_code` future without a
|
|
kill-on-`Drop` guard violates the contract and will orphan processes
|
|
on cancel. See ADR-005.
|
|
|
|
## Design Decisions
|
|
|
|
| Decision | ADR | Summary |
|
|
|----------|-----|---------|
|
|
| Wire format and two-carriage model | [ADR-001](decisions/001-wire-format-and-two-carriage.md) | The chunk codec + control channel the adapter pumps |
|
|
| `TtyBackend` trait and `TtyHandle` | [ADR-002](decisions/002-ttybackend-trait-and-ttyhandle.md) | The backend the adapter dispatches to; the handles the adapter pumps |
|
|
| Exit code on a control chunk | [ADR-004](decisions/004-exit-code-on-control-chunk.md) | The "exit chunk is last" invariant the adapter enforces |
|
|
| Backend cleanup on session cancel | [ADR-005](decisions/005-backend-cleanup-on-session-cancel.md) | Dropping `exit_code` future kills the session target; the adapter triggers it by dropping the `TtyHandle` on cancel |
|
|
| Channels pure channel multiplexing | [ADR-008](decisions/008-channels-pure-channel-multiplexing.md) | The same session driver runs in both direct and channels modes; only the `BiStream` source differs |
|
|
| Negotiation carried in the open op | [ADR-009](decisions/009-channels-open-op-is-the-negotiation.md) | The channels path carries no second negotiation frame; the open op's registry-validated `input` is the negotiation (`drive_session_pre_negotiated`) |
|
|
| Channels establishment failures are call errors | [ADR-010](decisions/010-channels-establisher-migration.md) | The channels path's semantic failures and allocation are rejected/run in the establisher (`channel:open_failed`, alkcall ADR-049; alkcall 0.6 plan payload carries the `TtyHandle`); no in-band frame from a registered producer |
|
|
| Dynamic resource ownership | alknet ADR-050 | Terminal sessions as runtime-spawned resources; the adapter's access-control shape |
|
|
|
|
## Open Questions
|
|
|
|
- **OQ-45** (resolved): Flow control for high-throughput stdout — no application-level windowing; QUIC per-stream flow control is the backpressure mechanism.
|
|
|
|
## References
|
|
|
|
- [ADR-001](decisions/001-wire-format-and-two-carriage.md) — the wire
|
|
format the adapter pumps
|
|
- [ADR-002](decisions/002-ttybackend-trait-and-ttyhandle.md) — the
|
|
backend trait the adapter dispatches to
|
|
- [ADR-004](decisions/004-exit-code-on-control-chunk.md) — the
|
|
exit-chunk ordering the adapter enforces
|
|
- [ADR-005](decisions/005-backend-cleanup-on-session-cancel.md) — the
|
|
cancel-cleanup contract the adapter triggers by dropping the
|
|
`TtyHandle` on session cancel
|
|
- [ADR-008](decisions/008-channels-pure-channel-multiplexing.md) — why
|
|
the same session driver runs in channels mode
|
|
- [ADR-009](decisions/009-channels-open-op-is-the-negotiation.md) — the
|
|
channels path carries no second negotiation frame (the open op's
|
|
registry-validated `input` is the negotiation)
|
|
- [ADR-010](decisions/010-channels-establisher-migration.md) — the
|
|
channels path's semantic failures are establisher call errors
|
|
(`channel:open_failed`); since alkcall 0.6 the establisher also
|
|
allocates (the `TtyHandle` crosses via the plan payload), so no
|
|
failure class arrives in-band from a registered producer
|
|
- alknet ADR-050 — the ownership model the adapter's access control
|
|
declares against
|
|
- alknet ADR-007 — `Connection`, `accept_bi`, the handler-receives-
|
|
Connection pattern
|
|
- `src/adapter.rs` — the Rust source this spec documents
|
|
- [tty-wire.md](tty-wire.md) — the wire format details
|
|
- [tty-backend.md](tty-backend.md) — the backend trait details |