diff --git a/Cargo.toml b/Cargo.toml index 0d13201..446ae2c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,9 +18,10 @@ default = [] local = ["dep:portable-pty", "dep:tokio-util"] [dependencies] -alkcall = "0.1.0" +alkcall = "0.1.1" tokio = { version = "1", features = ["full"] } bytes = "1" +futures = "0.3" futures-core = "0.3" tokio-stream = "0.1" serde = { version = "1", features = ["derive"] } diff --git a/docs/plans/project-setup.md b/docs/plans/project-setup.md index bc83f0e..e778216 100644 --- a/docs/plans/project-setup.md +++ b/docs/plans/project-setup.md @@ -1,7 +1,7 @@ # Project Setup Plan -Status: draft -Last updated: 2026-08-14 +Status: draft (revised 2026-08-17 to reflect landed upstream changes) +Last updated: 2026-08-17 ## Overview @@ -10,10 +10,90 @@ standalone `alktty` crate — a producer/consumer protocol crate on top of alkcall channels. This is the first protocol crate built on alkcall and establishes patterns that `alktunnels` and others will follow. +The local backend (`alknet-tty-local`) is folded into `alktty` as a +feature-gated `local` submodule rather than kept as a sibling crate. +Both the original `alknet-tty` (the wire/adapter/backend-trait half) and +`alknet-tty-local` (the `portable_pty` + `tokio::process` half) are +ported together, so the single crate carries the full stack: the +`alk/tty` protocol + the one backend implementation that ships with it. +A docker or SSH backend would be a separate crate; the local backend is +small and the cyclic-dependency workaround it required in the alknet +mono-repo (see ADR-054) doesn't apply to a single crate with a feature +flag. + +## Upstream changes that landed since the first draft + +These were "open questions" or "upstream changes needed" in the +2026-08-14 draft and are now resolved. The plan reflects the as-built +shape, not the original intent. + +### alkcall 0.1.1 — ALPN rename done + +- `CHANNELS_ALPN`: `b"alknet/channels"` → `b"alk/channels"` (in + `alkcall/src/channels/adapter.rs`). +- Test at `channels/client.rs:767` uses `"alk/tty"` (was + `"alknet/tty"`). +- Semver showed no public API change → alkcall bumped 0.1.0 → 0.1.1. +- `alkcall` `Cargo.toml` dependency needs updating from `0.1.0` to + `0.1.1` (the scaffold still pins `0.1.0`). + +### alkcall — `Identity.resources` shape + +The alknet-core `Identity` had `resources: HashMap` (a +flat resource-type → resource-id map). alkcall's `Identity` (in +`alkcall::core::auth`) has `resources: HashMap>` — +one resource type can carry multiple resource ids. The ported adapter +tests that build an `Identity` by hand need this shape; the production +code only reads `identity.scopes` (the scope-gate at negotiation), so +the change is test-side. + +### alkcall — `OwnershipProvider` / `OwnershipStore` signature + +- `OwnershipProvider::owns(&self, identity, resource_type, + resource_id, action: &str)` — added an `action` parameter (was 3 + args, now 4). The adapter's call site + (`provider.owns(id_ref, kind, &id, "tty")`) already passes an + `"tty"` action string, which is the new arity. No change to the + production adapter; the mock and test fixtures need the 4-arg shape. +- `OwnershipStore::record(&self, identity, resource_type, + resource_id)` — **dropped** the old `action` parameter that + alknet-core's variant took. The adapter test's + `store.record(&owner, "container", "c1").await` matches the new + arity. (The scaffold's plan previously listed an `OwnershipStore` + import that no longer needs an action arg.) + +### alkcall — `AuthContext::anonymous(alpn)` + +alkcall added a convenience constructor `AuthContext::anonymous(alpn)` +that builds an `AuthContext` with no identity, no fingerprint, no +remote address — only the ALPN. Useful in adapter tests; not used by +the production adapter (which receives the `AuthContext` from the +dispatch path). + +### alktype 0.2.0 — BAST format pivot + +alktype was reworked: the v0.1.0 `AlkType:*` custom-keyword JSON Schema +backend was dropped in favor of a straightforward `kind`-based BAST +(Binary Abstract Syntax Tree) format inspired by unist ASTs. A BAST +document is a plain JSON file (conforming to a JSON Schema meta-schema) +that describes binary layouts using `kind` strings (`"uint8"`, +`"uint32"`, `"struct"`, etc.) and `$defs`/`$ref` for composition. + +`alktty` will **not** depend on alktype — the hand-rolled +`ChunkReader`/`ChunkWriter` in `wire.rs` already works and is +straightforward. The BAST format matters here only as a normative +**schema document** for the `alk/tty` wire format: a JSON file under +`docs/architecture/` that downstream consumers (and alktype, if +desired) can consume to generate validators, layout maps, or readers +in any language. The schema is a documentation artifact, not a runtime +dependency. + ## Crate Structure Single crate (`alktty`) with the local backend gated behind a `local` -feature. This is a "tty-specific monorepo" — one crate, one concern. +feature. This is a "tty-specific monorepo" — one crate, one concern, +both halves of the original alknet split (`alknet-tty` + +`alknet-tty-local`) folded in. ``` alktty/ @@ -26,20 +106,19 @@ alktty/ │ ├── adapter.rs # TtyAdapter (ProtocolHandler), drive_session │ ├── session.rs # TtySession — typed consumer client │ ├── channels.rs # OpenHandler factory, register_openable helper -│ └── local/ # (behind `local` feature) -│ ├── mod.rs -│ ├── backend.rs # LocalTtyBackend -│ ├── pty.rs # PTY mode (portable_pty) -│ └── pipe.rs # Pipe/runner mode (tokio::process::Command) +│ └── local/ # (behind `local` feature — the folded alknet-tty-local) +│ ├── mod.rs # LocalTtyBackend re-export +│ ├── backend.rs # LocalTtyBackend (dispatches terminal: Some→pty, None→pipe) +│ ├── pty.rs # PTY mode (portable_pty, 3 std threads → tokio channels, #[cfg(unix)]) +│ └── pipe.rs # Pipe/runner mode (tokio::process::Command, cross-platform) ├── docs/ │ ├── architecture/ │ │ ├── README.md # architecture index -│ │ ├── crates/ -│ │ │ ├── overview.md -│ │ │ ├── tty-wire.md -│ │ │ ├── tty-backend.md -│ │ │ ├── tty-adapter.md -│ │ │ └── tty-local.md +│ │ ├── tty-wire.md # wire format spec (5-byte chunk + 4-byte neg frame) +│ │ ├── tty-bast.md # BAST JSON document for the alk/tty wire format +│ │ ├── tty-backend.md # TtyBackend trait + LocalTtyBackend +│ │ ├── tty-adapter.md # TtyAdapter + drive_session +│ │ ├── tty-local.md # PTY + pipe modes, REQ-TTY-01/02 │ │ └── decisions/ # renumbered ADRs │ └── plans/ │ └── project-setup.md # this document @@ -58,24 +137,47 @@ ADR-054 wanted `alknet-tty` to re-export `LocalTtyBackend` behind a `local` feature — which cargo rejects. The assembly-layer workaround (consumer depends on both crates directly) works but is awkward. -Single crate with `local` feature solves this cleanly. The `portable-pty` -dep is optional. A docker-only deployment doesn't pull in PTY code. +Single crate with `local` feature solves this cleanly. The +`portable-pty` and `tokio-util` deps are optional. A docker-only +deployment doesn't pull in PTY code. -### 2. Dependency: `alknet-core` → `alkcall` +**Folded-in scope.** The single crate carries both halves of the +original alknet split: the wire/adapter/backend-trait half (from +`alknet-tty`) and the local backend (from `alknet-tty-local`). The +local backend is small (~1.4k lines), tightly coupled to the trait, +and the cyclic-dep workaround it required in the alknet mono-repo +doesn't exist in a single crate. A docker or SSH backend would still +be a separate crate — those have real external deps (`bollard`, +`russh`) and their own resource models. + +### 2. Dependency: `alknet-core` → `alkcall::core` All types previously from `alknet-core` now come from `alkcall::core`: - `ProtocolHandler`, `Connection`, `BiStream`, `BidiStreamSource` - `AuthContext`, `Identity`, `IdentityProvider` -- `AccessControl`, `OwnershipProvider` +- `AccessControl`, `OwnershipProvider`, `OwnershipStore`, + `InMemoryOwnershipStore` - `HandlerError`, `StreamError` The `TtyAdapter`'s `impl ProtocolHandler` changes which crate the trait -comes from. The trait shape is identical. +comes from. The trait shape is identical. The `Identity` struct's +`resources` field is `HashMap>` in alkcall (was +`HashMap` in alknet-core); the production adapter only +reads `identity.scopes`, so the change is test-side. + +`Cargo.toml` pins `alkcall = "0.1.1"` (the scaffold's `0.1.0` pin is +stale — 0.1.1 is the ALPN-rename release; same public API per semver). ### 3. Channels integration (the new part) This is where alktty becomes a proper producer/consumer on alkcall. +**ALPN strings (settled).** `alk/tty` (direct) and `alk/channels` +(multiplexed). The `alknet/` → `alk/` rename landed in +alkcall 0.1.1 — `CHANNELS_ALPN` is `b"alk/channels"`, the channels +client test uses `"alk/tty"`. No upstream work remains; we just use +the new strings. + **Producer half** — two paths to expose TTY: - **Direct ALPN** (`alk/tty`): `TtyAdapter` implements `ProtocolHandler`. @@ -94,12 +196,17 @@ This is where alktty becomes a proper producer/consumer on alkcall. The channels integration code is small: a `register_openable` helper function that takes a `ChannelCore`, `OperationRegistry`, and backend - map, and registers the per-ALPN open op. This lives in `src/channels.rs`. + map, and registers the per-ALPN open op (`channels/tty/sub` — + consumer-opens). This lives in `src/channels.rs`. The op spec carries + the `channel_open` marker (`ChannelOpenSpec::new("alk/tty")`) and the + `AccessControl` for the scope-gate + ownership check; the channels + wrapper does `check_open` → `open_channel` → spawn the `OpenHandler` + → respond with `{ channel_id }`. **Consumer half** — `TtySession`: A typed client that wraps the wire protocol. Two constructors: -- `TtySession::connect_direct(connection)` — for direct `alknet/tty` +- `TtySession::connect_direct(connection)` — for direct `alk/tty` - `TtySession::open_via_channels(client, params)` — opens a channel via `ChannelClient`, negotiates, returns a session handle @@ -142,79 +249,189 @@ The old code had minimal access control (a scope-gate at negotiation). With alkcall, we get: - **Operation-level ACL**: `AccessControl` on `OperationSpec` — scopes, - resource ownership checks + resource ownership checks (`required_scopes`, `required_scopes_any`, + `resource_type` + `resource_id_path`, `resource_action`) - **Channel lifecycle policy**: `ChannelLifecyclePolicy` — per-identity - channel caps, open/close hooks + channel caps, open/close hooks (consulted by the `ChannelCore` + wrapper, not by alktty directly) - **Ownership**: `OwnershipProvider` — runtime-spawned resource tracking - (terminal sessions are resources per ADR-050) + (terminal sessions are resources per ADR-050). alkcall's + `OwnershipProvider::owns` takes an `action: &str` arg (the adapter + passes `"tty"`); `OwnershipStore::record` takes no action arg. -The `TtyAdapter`'s access control is reworked to use alkcall's primitives -rather than the ad-hoc scope check. The channels path gets this for free -via `ChannelCore::register_openable` which wires `AccessControl` into the -operation spec. +### 7. BAST schema as a documentation artifact + +The `alk/tty` wire format gets a BAST (Binary Abstract Syntax Tree) +document under `docs/architecture/tty-bast.md`. BAST is alktype's +JSON-based binary-layout vocabulary (alktype 0.2.0 dropped the old +`AlkType:*` custom-keyword backend in favor of this `kind`-based +format). The document is a normative spec: downstream consumers (and +alktype, if desired) can consume it to generate validators, layout +maps, or readers in any language with a JSON parser. + +**alktty does not depend on alktype.** The hand-rolled +`ChunkReader`/`ChunkWriter` in `wire.rs` is the runtime codec; the BAST +document is the human-readable contract that describes what those +types round-trip. Keeping them as separate artifacts avoids pulling in +alktype as a runtime dep for a codec that's already ~200 lines and +works. If we later want runtime validation against the BAST (e.g., to +reject malformed chunks at the framing boundary with a generated +validator), alktype becomes an optional dep — the BAST document is +already there to feed it. + +The `TtyAdapter`'s direct-ALPN path keeps the existing ad-hoc scope +check (`has_scope(identity, TTY_OPEN_SCOPE)`) for the scope gate and +optionally consults `OwnershipProvider` for the resource-ownership +check. The channels path gets both for free via +`ChannelCore::register_openable`, which wires `AccessControl` into the +operation spec — the registry runs the ACL before the wrapper, so the +`OpenHandler` only needs to validate params and spawn the protocol. ## Migration Strategy -### Phase 1: Scaffold and core types (no new logic) +### Phase 0: Scaffold hygiene (small fixes before porting) -1. Create `Cargo.toml` with alkcall dependency -2. Port `wire.rs` — change `alknet-core` imports to `alkcall::core`, rename - crate references in docs -3. Port `control.rs` — same -4. Port `negotiation.rs` — same -5. Port `backend.rs` — same; `BoxFuture` type alias can use - `futures::future::BoxFuture` since alkcall pulls in `futures` -6. Port `adapter.rs` — change `ProtocolHandler` import, `Connection` import, - `AuthContext` import, `HandlerError` import. The `handle()` method - signature is identical. +1. Bump `alkcall` in `Cargo.toml` from `"0.1.0"` → `"0.1.1"` (ALPN + rename release). +2. Add `futures = "0.3"` to `[dependencies]` (the `BoxFuture` alias + uses `futures::future::BoxFuture`; alkcall pulls it transitively + but alktty should declare it directly to not rely on a transitive + dep for a public type alias). +3. Confirm the `local` feature already wires `portable-pty` + + `tokio-util` as optional deps (it does in the scaffold). The + `local` module's `pty.rs` additionally needs `libc` under + `cfg(unix)` — already in `[target.'cfg(unix)'.dependencies]`. + +### Phase 1: Port core types from `alknet-tty` (no new logic) + +1. Port `wire.rs` — no `alknet-core` imports; just `std::io`, `tokio::io`, + `bytes`, `thiserror`. Rename crate references in doc comments + (`alknet/tty` → `alk/tty`, `alknet-tty` → `alktty`). +2. Port `control.rs` — no `alknet-core` imports; `serde`, `bytes`, + `libc` (under `cfg(unix)`). Rename `alknet/tty` → `alk/tty` in + doc comments. +3. Port `negotiation.rs` — no `alknet-core` imports; `std::io`, + `bytes`, `tokio::io`, `thiserror`, `serde_json`. Rename doc + references. +4. Port `backend.rs` — no `alknet-core` imports; `std`, `async_trait`, + `bytes`, `futures_core`, `tokio::sync`, `tokio_stream`. Change the + `BoxFuture` type alias from `Pin>` to + `futures::future::BoxFuture<'static, T>` (functionally identical; + the alias matches the alkcall convention and lets us drop the + `Pin`/`Future` imports). +5. Port `adapter.rs` — change imports: + - `alknet_core::auth::{AuthContext, Identity}` → + `alkcall::core::{AuthContext, Identity}` + - `alknet_core::ownership::OwnershipProvider` → + `alkcall::core::OwnershipProvider` + - `alknet_core::types::{Connection, HandlerError, ProtocolHandler, + StreamError}` → `alkcall::core::{Connection, HandlerError, + ProtocolHandler, StreamError}` + - Change `b"alknet/tty"` → `b"alk/tty"` in `TtyAdapter::alpn()`. + - The `handle()` method signature is identical (alkcall's + `ProtocolHandler` shape matches alknet-core's). +6. Port the unit tests inside each module alongside the production + code. The adapter tests need: + - `Identity.resources: HashMap>` (was + `HashMap` in alknet-core). + - `OwnershipStore::record(&self, identity, resource_type, + resource_id)` — 3 args, no `action` (alkcall dropped it). + - `OwnershipProvider::owns(identity, rt, rid, action)` — 4 args + (alkcall added `action`). ### Phase 2: Channels integration (new code) 1. Create `src/channels.rs`: - `register_openable` helper — takes `ChannelCore`, backend map, - `OperationRegistry`, registers the `channels/tty/sub` op - - `TtyOpenHandler` — the `OpenHandler` that receives a channel - `Connection`, calls `accept_bi()`, runs `drive_session` + `OperationRegistry`, `AuthContext`; builds the `OperationSpec` + for `channels/tty/sub` with `ChannelOpenSpec::new("alk/tty")`, + `AccessControl` carrying `TTY_OPEN_SCOPE` + resource-type/ownership + if a backend declares a `resource_id`; calls + `ChannelCore::register_openable(spec, open_handler, registry, auth)`. + - `TtyOpenHandler` — the `OpenHandler` (an + `Arc JoinHandle<()>`) + that receives a channel `Connection`, calls `accept_bi()`, runs + `drive_session` on the resulting `BiStream`. 2. Create `src/session.rs`: - `TtySession` struct — typed consumer client - - `TtySession::open_via_channels(client, params)` constructor - - Methods: `send_stdin`, `recv_stdout`, `resize`, `signal`, `wait` + - `TtySession::connect_direct(connection)` — direct `alk/tty` + - `TtySession::open_via_channels(client, params)` — opens a channel + via `ChannelClient::open_channel("channels/tty/sub", params, + "alk/tty")`, negotiates, returns a session handle + - Methods: `send_stdin`, `recv_stdout`, `recv_stderr`, `resize`, + `signal`, `wait` -### Phase 3: Local backend (behind `local` feature) +### Phase 3: Local backend (behind `local` feature) — folded `alknet-tty-local` -1. Create `src/local/mod.rs`, `src/local/backend.rs`, `src/local/pty.rs`, - `src/local/pipe.rs` -2. Port from `alknet-tty-local` — change `alknet-tty` imports to `crate` -3. `LocalTtyBackend` implements `crate::backend::TtyBackend` +1. Create `src/local/mod.rs` (re-exports `LocalTtyBackend`), + `src/local/backend.rs`, `src/local/pty.rs`, `src/local/pipe.rs`. +2. Port from `alknet-tty-local/src/`: + - `backend.rs` — change `alknet_tty::backend::{...}` → + `crate::backend::{...}`. `LocalTtyBackend::allocate` dispatches + on `params.terminal`: `Some` → `pty::allocate_pty`, `None` → + `pipe::allocate_pipe`. + - `pty.rs` — `portable_pty` + 3 std threads (reader/writer/waiter) + feeding tokio mpsc/oneshot. `#[cfg(unix)]`-only: PTY mode uses + `libc::kill(-pgid, sig)` for process-group signal forwarding. + Update `alknet_tty::backend::{BoxFuture, TtyControl, ...}` → + `crate::backend::{...}` and `alknet_tty::control::signal_from_name` + → `crate::control::signal_from_name`. + - `pipe.rs` — `tokio::process::Command` + `tokio_util::io::ReaderStream` + for stdout/stderr. Cross-platform: signal forwarding uses + `libc::kill(pid, sig)` under `cfg(unix)`, falls back to + `Child::kill()` on non-Unix. Update `alknet_tty::...` → `crate::...`. +3. `LocalTtyBackend` implements `crate::backend::TtyBackend`. Wire the + `local` feature in `src/lib.rs`: + `#[cfg(feature = "local")] pub mod local;` -### Phase 4: Architecture docs +### Phase 4: Architecture docs + BAST schema 1. Port the 5 spec docs from `alknet/docs/architecture/crates/tty/` + into `docs/architecture/` (flat layout, not the `crates/tty/` + subpath — this is a single-crate repo now): + `tty-wire.md`, `tty-backend.md`, `tty-adapter.md`, `tty-local.md`, + plus an `overview.md` index. 2. Port relevant ADRs (052, 053, 054, 055, 056, 057, 077, 093) — - renumber into alktty's ADR range, update cross-references -3. Write `docs/architecture/README.md` index + renumber into alktty's ADR range (001..008), update cross-references + (`alknet/tty` → `alk/tty`, `alknet-tty-local` → `alktty`'s `local` + feature, `alknet-core` → `alkcall::core`). +3. Write `docs/architecture/tty-bast.md` — the BAST JSON document for + the `alk/tty` wire format. Covers: + - The 5-byte chunk header (`struct` with `endian: "big"`: + `stream_type: uint8`, `length: uint32`). + - The four stream-type channels as an `enum` (`Stdin=0`, + `Stdout=1`, `Stderr=2`, `CtrlIn=3`, `CtrlOut=4`). + - The control-message `union` (field-name discriminator on `type`: + `resize`, `signal`, `eof`, `exit`). + - The negotiation frame as a separate `struct` (4-byte BE length + prefix + UTF-8 JSON `NegotiateRequest` body) — annotated as + out-of-band for the chunk codec but documented for completeness. + - Conforms to the BAST meta-schema at + `https://alk.dev/bast/v1/schema`; validatable by any JSON Schema + Draft 2020-12 validator. +4. Write `docs/architecture/README.md` index. ### Phase 5: Tests -1. Port existing unit tests from `alknet-tty` (wire, negotiation, control, - adapter) -2. Port integration tests from `alknet-tty-local` (negotiation, pipe, pty) +1. Port existing unit tests from `alknet-tty` (wire, negotiation, + control, adapter) — these live inline in each `src/*.rs` module's + `#[cfg(test)] mod tests`. +2. Port integration tests from `alknet-tty-local/tests/` (negotiation, + pipe, pty) into `tests/` at the crate root. The pty test stays + `#[cfg(unix)]`. 3. Add channels integration tests — end-to-end `register_openable` + - `ChannelClient::open_channel` + `drive_session` round-trip + `ChannelClient::open_channel` + `drive_session` round-trip. Use + the in-memory `MockBackend` from `backend.rs` for the producer + side so the test doesn't need a real PTY. ## Open Questions -### OQ-1: ALPN strings (RESOLVED) +### OQ-1: ALPN strings (RESOLVED — landed in alkcall 0.1.1) -**Decision**: Use `alk/tty`. The `alknet/` convention is being -shortened to `alk/` across all crates. This is a wire-format change -but alkcall is v0.1.0 and this is the first protocol crate built on it — -the right time to make the change. - -**Upstream change needed**: alkcall's test at `channels/client.rs:767` uses -`ChannelOpenSpec::new("alknet/tty")` — needs updating to `"alk/tty"`. -Also any docs/ADRs in alkcall that reference `alknet/` as the ALPN -convention. +**Decision**: `alk/tty` (direct) and `alk/channels` (multiplexed). The +`alknet/` → `alk/` rename landed in alkcall 0.1.1: +`CHANNELS_ALPN` is `b"alk/channels"`, the channels client test uses +`"alk/tty"`. Semver showed no public API change → 0.1.0 → 0.1.1. ### OQ-2: Channel open operation name (RESOLVED) @@ -239,38 +456,51 @@ code runs in both direct and channels modes. ### OQ-5: `BoxFuture` type alias (RESOLVED) -**Decision**: Use `futures::future::BoxFuture`. alkcall already pulls in -`futures`, so no new dependency. +**Decision**: Use `futures::future::BoxFuture<'static, T>`. alkcall +already pulls in `futures`, so no new transitive dep; alktty declares +`futures = "0.3"` directly so the public alias doesn't ride on a +transitive dep. + +### OQ-6: BAST schema scope (RESOLVED) + +**Decision**: Documentation artifact only — `docs/architecture/tty-bast.md`. +alktty does not depend on alktype. The hand-rolled `wire.rs` codec is +the runtime; the BAST document is the normative contract for downstream +consumers. If runtime validation against the BAST becomes desirable +later, alktype becomes an optional dep and the BAST document is already +there to feed it. ## Upstream Changes Needed in alkcall -These are changes we need to push to alkcall before or alongside this work: +All four landed in alkcall 0.1.1 (no public API change per semver). +Nothing is blocking alktty; this section is kept as a historical record +of what was pushed. 1. **ALPN convention**: `alknet/` → `alk/` across docs, ADRs, - and test code. The test at `channels/client.rs:767` uses - `ChannelOpenSpec::new("alknet/tty")` — needs `"alk/tty"`. + and test code. The test at `channels/client.rs:767` now uses + `ChannelOpenSpec::new("alk/tty")` (was `"alknet/tty"`). -2. **`CHANNELS_ALPN` constant**: `b"alknet/channels"` → `b"alk/channels"`. - This is in `alkcall/src/channels/adapter.rs`. +2. **`CHANNELS_ALPN` constant**: `b"alknet/channels"` → `b"alk/channels"` + in `alkcall/src/channels/adapter.rs`. -3. **ADR-004** (ALPN convention): Update from `alknet/` prefix to `alk/`. +3. **ADR-004** (ALPN convention): Updated from `alknet/` prefix to `alk/`. 4. **Architecture README**: The dependency layering diagram and pattern - section reference `alknet/tty` and `alknet/channels` — update to - `alk/tty` and `alk/channels`. + section reference `alk/tty` and `alk/channels`. ## Risks and Concerns -### Risk: alkcall is v0.1.0 — API stability +### Risk: alkcall is v0.1.x — API stability -alkcall is published at 0.1.0. Breaking changes are expected. The -integration surface we use (`ChannelCore::register_openable`, -`ChannelClient`, `ProtocolHandler`, `Connection`) is the core API and +alkcall is published at 0.1.1. Breaking changes are expected at this +major-zero stage. The integration surface we use +(`ChannelCore::register_openable`, `ChannelClient`, `ProtocolHandler`, +`Connection`, `Identity`, `OwnershipProvider`) is the core API and likely stable, but the channels module in particular may evolve. -**Mitigation**: We own alkcall and can push changes as needed. This is the -first real consumer, so we'll find and fix issues upstream rather than -working around them. +**Mitigation**: We own alkcall and can push changes as needed. This is +the first real consumer, so we'll find and fix issues upstream rather +than working around them. Pin `alkcall = "0.1.1"` and bump deliberately. ### Risk: `drive_session` complexity @@ -290,10 +520,29 @@ specific (Unix only for PTY, pipe mode works cross-platform). **Mitigation**: Gate PTY behind `#[cfg(unix)]` as the existing code does. Pipe mode works on all platforms. +### Risk: BAST schema drift + +The BAST document in `docs/architecture/tty-bast.md` is a documentation +artifact; the runtime codec is the hand-rolled `wire.rs`. If the wire +format evolves (new stream_type, control message variant, negotiation +field) and the BAST doc isn't updated, downstream consumers generated +from the BAST will be wrong. + +**Mitigation**: Add a test that parses the BAST document with +`serde_json` and asserts the stream-type enum values match +`wire.rs`'s `STREAM_STDIN`/`STREAM_STDOUT`/`STREAM_STDERR`/ +`STREAM_CTRL_IN`/`STREAM_CTRL_OUT` constants. Cheap; catches the +common drift case. (Doesn't require alktype as a dep — just +`serde_json`, which we already have.) + ## References - [alkcall architecture README](/workspace/@alkdev/alkcall/docs/architecture/README.md) -- [alknet-tty architecture docs](/workspace/@alkdev/alknet/docs/architecture/crates/tty/) +- [alkcall channels operations](/workspace/@alkdev/alkcall/src/channels/operations.rs) — `ChannelCore::register_openable`, `OpenHandler` +- [alkcall channels client](/workspace/@alkdev/alkcall/src/channels/client.rs) — `ChannelClient::open_channel`, reference integration pattern +- [alktype BAST format](/workspace/@alkdev/alktype/docs/architecture/bast-format.md) — normative format spec for the `tty-bast.md` document +- [alknet-tty source](/workspace/@alkdev/alknet/crates/alknet-tty/src/) — port origin (wire, control, negotiation, backend, adapter) +- [alknet-tty-local source](/workspace/@alkdev/alknet/crates/alknet-tty-local/src/) — port origin (local backend, pty, pipe) +- [alknet-tty architecture docs](/workspace/@alkdev/alknet/docs/architecture/crates/tty/) — port origin for the spec docs - [ADR-093](/workspace/@alkdev/alknet/docs/architecture/decisions/093-channels-pure-channel-multiplexing.md) — TTY always uses 5-byte format - [ADR-077](/workspace/@alkdev/alknet/docs/architecture/decisions/077-tty-inside-channels.md) — reversed, historical context -- [alkcall channels client test](/workspace/@alkdev/alkcall/src/channels/client.rs) — reference integration pattern