alktty 0.5.0

Terminal session protocol: wire format, TtyBackend trait, TtyAdapter, and typed consumer client. Producer/consumer protocol crate on top of alkcall channels.
Documentation
# alktty

Terminal session protocol for the `alk/tty` ALPN: a JSON negotiation
frame followed by raw chunks (`[stream_type: u8][length: u32
be][payload]`), a backend-agnostic `TtyBackend` trait, a
backend-agnostic producer adapter, and a typed consumer client.

A producer/consumer protocol crate on top of
[alkcall](https://crates.io/crates/alkcall) channels — no networking, no
transport dependencies. Downstream crates (a `alknet-docker` or
`alknet-ssh` backend, a sandboxed TS/Python adapter) compose on top of
it: implement [`TtyBackend`](https://docs.rs/alktty/latest/alktty/trait.TtyBackend.html)
for your execution environment, and the adapter handles the wire.

## Quick start

### Producer — register backends and run the adapter

```rust
use std::collections::HashMap;
use std::sync::Arc;

// `LocalTtyBackend` needs the `local` feature; swap in any `TtyBackend`
// implementation (docker, ssh, ...) compiled for your target.
let mut backends: HashMap<String, Arc<dyn alktty::TtyBackend>> = HashMap::new();
backends.insert("local".into(), Arc::new(alktty::LocalTtyBackend::new()));

let tty_adapter = alktty::TtyAdapter::new(backends);
// tty_adapter implements alkcall::core::ProtocolHandler on the `alk/tty`
// ALPN — hand it to your transport accept loop:
//   tty_adapter.handle(connection, &auth).await
```

### Producer — via `alk/channels` instead of a dedicated ALPN

```rust
use std::collections::HashMap;
use std::sync::Arc;

let backends: HashMap<String, Arc<dyn alktty::TtyBackend>> =
    std::iter::once(("local".to_string(), Arc::new(alktty::LocalTtyBackend::new()) as _))
        .collect();

// In your channels acceptor's `install_channel_zero` hook (per-connection):
//   alktty::register_openable(&channel_core, Arc::new(backends), None, &mut registry, auth)
// Consumers open `channels/tty/sub`; the open op's params ARE the
// negotiation (ADR-009) — no second frame on the channel stream.
```

### Consumer — open a session over the direct ALPN

```rust
use alkcall::core::Connection;
use alktty::{NegotiateRequest, TtySession};

let connection: Connection = /* dial the transport, negotiate `alk/tty` */;
let session = TtySession::connect_direct(
    connection,
    NegotiateRequest {
        carriage: "raw".into(),
        backend: "local".into(),
        tty: None,
        cmd: vec!["bash".into()],
        cwd: None,
        env: Default::default(),
        backend_params: Default::default(),
    },
).await?;

session.resize(120, 40, 0, 0).await?;
session.send_stdin("ls -l\n".into()).await?;
session.close_stdin().await?;

let mut stdout = session.recv_stdout().await;
while let Some(bytes) = futures::StreamExt::next(&mut stdout).await {
    // The stream ends on the zero-length stdout sentinel ("drained") —
    // it is never yielded as an item.
    // ... render bytes ...
}
let code = session.wait().await?;
```

### Consumer — open a session via `alk/channels`

```rust
use alkcall::channels::client::ChannelClient;
use alktty::TtySession;

let client: ChannelClient = /* from_connection on an `alk/channels` connection */;
let session = TtySession::open_via_channels(
    &client,
    serde_json::json!({
        "carriage": "raw",
        "backend": "local",
        "cmd": ["bash"],
        "tty": {"term": "xterm-256color", "cols": 120, "rows": 40,
                "pixel_width": 0, "pixel_height": 0}
    }),
).await?;
```

## Architecture

Two roles, symmetric per the alk convention (avoid "server"/"client":
both sides of a connection can be either):

| Role | Direct `alk/tty` | Via `alk/channels` |
|------|------------------|--------------------|
| **Producer** (runs backends, emits terminal bytes) | [`TtyAdapter`]https://docs.rs/alktty/latest/alktty/adapter/struct.TtyAdapter.html as a `ProtocolHandler` — negotiation frame → access control → backend allocation → three-pump session driver | [`register_openable`]https://docs.rs/alktty/latest/alktty/channels/fn.register_openable.html — the registry's ACL gates the open op; the handler spawns the same session driver pre-negotiated |
| **Consumer** (types input, renders output) | [`TtySession::connect_direct`]https://docs.rs/alktty/latest/alktty/struct.TtySession.html#method.connect_direct — writes the negotiation frame, exposes typed stdin/stdout/stderr/control/exit methods | [`TtySession::open_via_channels`]https://docs.rs/alktty/latest/alktty/struct.TtySession.html#method.open_via_channels — the open op's params are the negotiation |

One connection hosts many sessions (one per bidi stream on the direct
path, one per channel on the channels path); sessions are independent.

The wire format (ADR-001) is two carriages: a 4-byte
length-prefixed JSON negotiation frame, then raw chunks —
`[stream_type: u8][length: u32 be][payload]` with five stream types
(`STREAM_STDIN`=0 … `STREAM_CTRL_OUT`=4; see
[`alktty::wire`](https://docs.rs/alktty/latest/alktty/wire/index.html)).
Zero-length data chunks are sentinels (stdin EOF from the client,
stdout drained from the server); the exit code rides as the final
`ctrl_out` control chunk (ADR-005). The codec is
[`alktty::wire`](https://docs.rs/alktty/latest/alktty/wire/index.html)'s
`ChunkReader`/`ChunkWriter`; the machine-readable contract is the BAST
document at `docs/architecture/tty-bast.md`.

### WASM target

The default crate (no features) compiles to `wasm32-unknown-unknown` —
this is what makes the downstream TS/Python adapter story work: a
wasm-compiled alktty is the protocol layer for a sandboxed adapter. The
`local` feature (the [`LocalTtyBackend`](https://docs.rs/alktty/latest/alktty/struct.LocalTtyBackend.html)
reference implementation over `portable_pty` + `tokio::process`) needs a
real OS and is non-wasm by design.

## Documentation

- [Architecture docs]docs/architecture/README.md — the authoritative
  spec: ADRs (001–009), the wire format, the backend trait contract,
  and the local backend's cancel-cleanup semantics.
- [API docs]https://docs.rs/alktty — full crate documentation on
  docs.rs.

## License

MIT OR Apache-2.0