diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ed2d55c --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +target/ +node_modules/ +.worktrees/ \ No newline at end of file diff --git a/.opencode/agents/implementation-specialist.md b/.opencode/agents/implementation-specialist.md index 62b5216..746c7e2 100644 --- a/.opencode/agents/implementation-specialist.md +++ b/.opencode/agents/implementation-specialist.md @@ -210,55 +210,101 @@ This is especially important for complex tasks that span many file operations. Read `AGENTS.md` at project root for full details. Key rules: 1. **No comments in code** — Per project convention. Doc comments (`///`, `//!`) - are fine and expected on public API. -2. **Error handling** — `thiserror` for library error types. No panics in - library code. No `unwrap()` or `expect()` outside tests. For poisoned - `RwLock`/`Mutex`, use `unwrap_or_else(|e| e.into_inner())` so a panic in one - operation does not cascade to other operations. -3. **`tokio` is the async runtime** — all I/O is async. Use `tokio::sync` - primitives (`oneshot`, `mpsc`) for request correlation and subscription - channels; `parking_lot` for short-held internal locks (`PendingRequestMap`). -4. **No secret material on the wire** — `call.requested`/`call.responded` - payloads and `OperationContext.metadata` carry no private keys, API keys, or - decrypted credentials. Outbound credentials flow through `Capabilities` - injected at the assembly layer → `HandlerRegistration.capabilities` → - `OperationContext.capabilities` → handler. -5. **No-env-vars invariant** — no handler reads outbound credentials from any - source other than `OperationContext.capabilities`. This is a spec-level - invariant, not a runtime convention. -6. **`OperationEnv` must remain a trait** — the trait-based design enables - registry layering (session overlays, connection overlays, peer-keyed - composition). Do not make it concrete or hardcode the global registry. -7. **Wire formats are stable** — `EventEnvelope` (`{ type, id, payload }` + - length-prefixed JSON framing) and the channels 8-byte chunk header - (`[channel_id:u32 BE][length:u32 BE][payload]`) are one-way doors. New event - types may be added; existing shapes must not change. -8. **Producer/consumer, not server/client** — both sides of a call or channels - connection can initiate. Use "producer"/"consumer" or "accept side"/"connect - side," not "server"/"client." -9. **Vendored core types** — `Connection`, `ProtocolHandler`, `BiStream`, - `BidiStreamSource`, `AuthContext`, `IdentityProvider`, `Identity`, - `AuthToken`, `Capabilities`, `OwnershipProvider`, `HandlerError`, - `StreamError` live in this crate. Do not add a separate `alkcore` dependency. - Keep them lean (no TLS, no transport coupling, no endpoint/accept-loop). -10. **`alktype` dependency** — use `alktype` for binary layout (channels chunk - header, future binary payload schemas) and JSON payload schema validation - (`OperationSpec`'s `input_schema`/`output_schema`). Do not roll your own. -11. **Feature flags** — transports may be feature-gated if the need arises. The - base crate should compile lean (no `quinn`, no `iroh` unless the feature is - on). Verify both `cargo test` (default) and `cargo test --all-features` pass - if features are added. -12. **Abort cascades to descendants** — `call.aborted` for a parent cascades to - all non-terminal descendants. Default `abort-dependents`; - `continue-running` opt-in. The composing handler decides the child's policy, - not the wire caller. -13. **Peer authorization via `AccessControl`** — a remote peer's call is - authorized by `AccessControl::check(peer_identity)`. No `remote_safe` flag, - no `trusted_peer` bypass. `Visibility::Internal` ops are never wire-callable. -14. **Naming conventions** — Rust standard: `snake_case` for functions/variables/ - modules, `PascalCase` for types/traits, `SCREAMING_SNAKE_CASE` for constants. -15. **Module structure** — one module per file under `src/`, re-exported from - `src/lib.rs`. Public API surface is `lib.rs` re-exports. + are fine and expected on public API. Inline `//` comments only when the user + asks or when a non-obvious safety/correctness constraint would otherwise be + missed (e.g., "zero-length stdin is the EOF sentinel — the codec does not + special-case it; the adapter interprets `length == 0` chunks"). +2. **Error handling** — `thiserror` for library error types (`TtyError`, + `NegotiateError`, `WireError`; `HandlerError`/`StreamError` come from + alkcall::core). No panics in library code. No `unwrap()` or `expect()` + outside tests. For poisoned `RwLock`/`Mutex`, use + `unwrap_or_else(|e| e.into_inner())`. +3. **`tokio` is the async runtime** — all I/O is async. The adapter's + per-session pumps, the channels integration, and `TtySession` are all async. + Use `tokio::sync` primitives (`oneshot`, `mpsc`) for request/exit + correlation and stdout/stderr streams. The PTY bridge in `src/local/pty.rs` + is the one exception: three dedicated std threads feeding tokio mpsc/oneshot + channels (the `portable_pty` API is blocking) — behind the `local` feature. +4. **WASM target is load-bearing** — the default crate (no `local` feature) + MUST compile to `wasm32-unknown-unknown`. This is what makes the downstream + TS/Python adapter story work. `Cargo.toml` uses + `tokio = { default-features = false, features = ["rt", "sync", "io-util", "macros"] }` + (the wasm-clean subset alkcall uses); `local` adds `tokio/process` + + `tokio/rt-multi-thread`. **Do NOT use `features = ["full"]`** — it pulls in + `signal`/`fs`/`net` which break `wasm32-unknown-unknown`. The adapter's + `tokio::spawn` is fine on wasm; the `local` module's std threads, + `tokio::process::Command`, and `libc::kill` are the non-wasm parts, and + they're feature-gated. +5. **Wire formats are stable** — two one-way doors: + - **5-byte chunk header** (`[stream_type: u8][length: u32 BE][payload]`) — + the raw chunk codec in `wire.rs`. 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 = EOF from client; zero-length stdout = "drained" from server); + control chunks are never zero-length. Changing the header, the + stream-type values, or the sentinel semantics breaks all peers. + - **Negotiation frame** (4-byte BE length prefix + UTF-8 JSON + `NegotiateRequest` body) — self-contained, not reused from alkcall's + `EventEnvelope` framing. The `NegotiateRequest` JSON shape is + wire-stable once consumers exist. + Per ADR-093: TTY always uses its 5-byte format, even inside channels. The + channels layer strips its 8-byte header transparently — the same `wire.rs` + code runs in both direct (`alk/tty`) and channels (`alk/channels`) modes; + only the `BiStream` source differs. +6. **`TtyBackend` trait shape is a one-way door** — the trait in + `src/backend.rs` is the inversion point between the wire-format adapter and + the backend crates (alktty's own `local` feature module, future + `alknet-docker`, `alknet-ssh`). alktty defines the trait; the backends + implement it. The adapter holds a `HashMap>` + keyed by the negotiation frame's `backend` string; backends produce + `TtyHandle`s, they do not write to the wire. `TtyError` is + `#[non_exhaustive]` so new variants are additive. +7. **Producer/consumer, not server/client** — both sides of a `alk/tty` or + `alk/channels` connection can initiate. Use "producer"/"consumer" or + "accept side"/"connect side," not "server"/"client." +8. **`local` feature isolation** — the `local` module is the only non-wasm + part of the crate. The shared/producer/consumer modules (`wire`, `control`, + `negotiation`, `backend`, `adapter`, `channels`, `session`) MUST NOT import + from `crate::local` or reference any `local`-only type (`LocalTtyBackend`, + `portable_pty`, `tokio::process::Command`). The local backend is injected at + the assembly layer, not wired in by the adapter. A regression where a + non-`local` module pulls in `portable_pty`, `tokio::process`, + `std::thread`, or `libc` (outside the existing `cfg(unix)` calls in + `control.rs`/`pipe.rs`) silently breaks the downstream TS/Python adapter + story. +9. **Module structure** — one module per file under `src/`, re-exported from + `src/lib.rs`. Producer half: `src/adapter.rs` + `src/channels.rs`. Consumer + half: `src/session.rs`. Shared: `src/wire.rs`, `src/control.rs`, + `src/negotiation.rs`, `src/backend.rs`. The `src/local/` module is behind + the `local` feature and never imported from the shared/producer/consumer + modules — only re-exported from `lib.rs` under `#[cfg(feature = "local")]`. +10. **BAST document, not alktype dependency** — `docs/architecture/tty-bast.md` + (Phase 4, not yet written) will be the machine-readable spec for the + `alk/tty` wire format, conforming to the BAST meta-schema at + `https://alk.dev/bast/v1/schema`. 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. If runtime + validation against the BAST becomes desirable later, alktype becomes an + optional dep — do not roll your own offset map or validator for complex + formats. +11. **Feature flags** — the only feature is `local` (default = `[]`). `local` + is inherently non-wasm; enabling it on wasm is a build error by design. + Verify both `cargo test` (default) and `cargo test --all-features` pass. + Do not add a feature flag that pulls non-wasm deps into the default crate. +12. **Access control** — the direct-ALPN path keeps the ad-hoc scope check + (`has_scope(identity, TTY_OPEN_SCOPE)`) and optionally consults + `OwnershipProvider` (`provider.owns(id_ref, kind, &id, "tty")` — the 4-arg + shape). The channels path gets both for free via + `ChannelCore::register_openable`, which wires `AccessControl` into the + operation spec. `OwnershipStore::record(&self, identity, resource_type, + resource_id)` is the 3-arg shape (alkcall dropped the old `action` arg). +13. **Naming conventions** — Rust standard: `snake_case` for functions/variables/ + modules, `PascalCase` for types/traits, `SCREAMING_SNAKE_CASE` for + constants (`STREAM_STDIN`, `TTY_OPEN_SCOPE`, `CHUNK_HEADER_LEN`). +14. **No `unsafe`** — the crate has zero `unsafe` blocks. The PTY bridge's + `libc::kill(-pgid, sig)` and `libc::kill(pid, sig)` calls are safe `libc` + crate APIs (not `unsafe` blocks in this crate). Do not introduce `unsafe` + for performance. ## Key Principles diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..805e20b --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,292 @@ +# AGENTS.md + +Operating instructions for opencode agents working in this repo. opencode +auto-loads this file as instructions, overriding the built-in defaults for +this project. Custom agents in `.opencode/agents/` inherit these rules +unless their own prompts say otherwise. + +## Git Workflow + +**Commit and push when reasonable.** When a change is complete and +verified (build + lint + tests pass), commit and push to `origin/main` +without asking. This overrides the built-in default of "only commit when +explicitly asked." + +The workflow: + +1. Make the change +2. Verify: `cargo test`, `cargo clippy --all-targets -- -D warnings`, + `cargo fmt --check`, `cargo doc --no-deps` if docs changed +3. Inspect `git status` and `git diff` before staging — stage only the + intended files, never secrets +4. Write a concise commit message matching the repo style (see `git log + --oneline -10`). For multi-point changes, use a summary line plus a + body with bullet points and a verification block. +5. `git push origin main` +6. Report the commit hash and the verification summary + +Exceptions — **do not** commit or push without asking: + +- The change is exploratory / speculative (you're not sure the user wants + it kept) +- The user is actively reviewing the diff and may ask for changes +- The change touches the wire format or the `TtyBackend` trait shape + (one-way doors — see "Wire formats are stable" and "`TtyBackend` trait + shape is a one-way door" below; the 5-byte chunk header, the + negotiation-frame layout, the `NegotiateRequest` JSON shape, and the + trait signature are wire-stable contracts once consumers exist) +- You'd be force-pushing, amending a published commit, creating an empty + commit, or skipping hooks + +Never commit secrets, keys, or credentials. If a commit fails or hooks +reject it, fix the issue and create a new commit — do not amend the +failed one. + +Git identity is preconfigured (`glm-5.2 `). Do not +change `git config`, skip hooks, or use `git commit -i`. + +## Project Conventions (Rust / TTY protocol crate) + +This is the `alk/tty` terminal-session protocol crate — a +producer/consumer protocol crate on top of alkcall channels. It folds +the old `alknet-tty` (wire/adapter/backend-trait half) and +`alknet-tty-local` (`portable_pty` + `tokio::process` half) from the +alknet mono-repo into a single crate with a `local` feature. The +conventions below apply to all work in `src/` and `tests/`. They mirror +`.opencode/agents/implementation-specialist.md` §Project Conventions +and are repeated here so they apply to every session, not just spawned +implementation agents. + +1. **No comments in code** unless the user explicitly asks. This is a + project-wide convention. Doc comments (`///`, `//!`) are fine and + expected on public API. Inline `//` comments only when the user asks + or when a non-obvious safety/correctness constraint would otherwise be + missed (e.g., "zero-length stdin is the EOF sentinel — the codec does + not special-case it; the adapter interprets `length == 0` chunks"). + +2. **Error handling** — `thiserror` for library error types + (`TtyError`, `NegotiateError`, `WireError`, `HandlerError`/`StreamError` + come from alkcall::core). No panics in library code. No `unwrap()` or + `expect()` outside tests. If you reach for `unwrap`, the error path + wasn't specified — stop and decide what should actually happen. For + poisoned `RwLock`/`Mutex`, use + `unwrap_or_else(|e| e.into_inner())` so a panic in one operation does + not cascade to other operations. + +3. **`tokio` is the async runtime** — all I/O is async. The adapter's + per-session pumps, the channels integration, and `TtySession` are all + async. Use `tokio::sync` primitives (`oneshot`, `mpsc`) for + request/exit correlation and stdout/stderr streams. The PTY bridge in + `src/local/pty.rs` is the one exception: it uses three dedicated std + threads feeding tokio mpsc/oneshot channels (the `portable_pty` API + is blocking) — and it's behind the `local` feature. + +4. **WASM target is load-bearing** — the default crate (no `local` + feature) MUST compile 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). `Cargo.toml` uses + `tokio = { default-features = false, features = ["rt", "sync", + "io-util", "macros"] }` (the wasm-clean subset alkcall uses) and + `local` adds `tokio/process` + `tokio/rt-multi-thread`. **Do NOT use + `features = ["full"]`** — it pulls in `signal`/`fs`/`net` which break + `wasm32-unknown-unknown`. The adapter's `tokio::spawn` for per-session + pumps is fine on wasm (the wasm tokio runtime supports `spawn`); the + `local` module's std threads, `tokio::process::Command`, and + `libc::kill` are the non-wasm parts, and they're feature-gated. + `libc` stays under `cfg(unix)` (it's already there for + `signal_from_name` and the pipe-mode `kill` path — neither runs on + wasm because the only callers are in `local`). + +5. **Wire formats are stable** — two wire formats live in this crate, + both one-way doors: + - **5-byte chunk header** (`[stream_type: u8][length: u32 BE] + [payload]`) — the raw chunk codec in `wire.rs`. 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 = EOF from client; zero-length stdout + = "drained" from server); control chunks are never zero-length. + Changing the header, the stream-type values, or the sentinel + semantics breaks all peers. See ADR-052 (ported as alktty ADR-001). + - **Negotiation frame** (4-byte BE length prefix + UTF-8 JSON + `NegotiateRequest` body) — self-contained per ADR-057 (ported as + alktty ADR-008), not reused from alkcall's `EventEnvelope` framing + (the payload is `NegotiateRequest`, not `EventEnvelope`; alkcall's + `FrameFramedReader` is hardcoded to deserialize `EventEnvelope`). + The `NegotiateRequest` JSON shape is wire-stable once consumers + exist. + + Per ADR-093 (ported as alktty ADR-007): TTY always uses its 5-byte + format, even inside channels. The channels layer strips its 8-byte + header and hands TTY the payload transparently — the same `wire.rs` + code runs in both direct (`alk/tty`) and channels (`alk/channels`) + modes; only the `BiStream` source differs. + +6. **`TtyBackend` trait shape is a one-way door** — the trait + (`src/backend.rs`) is the inversion point between the wire-format + adapter and the backend crates (alktty's own `local` feature module, + future `alknet-docker`, `alknet-ssh`). alktty defines the trait; the + backends implement it. Changing the trait shape after backends exist + is a rewrite across crates. The adapter holds a + `HashMap>` keyed by the negotiation + frame's `backend` string and pumps the `TtyHandle` fields + bidirectionally; backends produce handles, they do not write to the + wire. `TtyError` is `#[non_exhaustive]` so new variants are additive + (two-way-door extension within the one-way trait shape). See ADR-053 + (ported as alktty ADR-002). + +7. **Producer/consumer, not server/client** — inherited from alkcall. + Both sides of a `alk/tty` or `alk/channels` connection can initiate. + A producer exposes TTY (direct ALPN via `TtyAdapter::ProtocolHandler`, + or via `channels::register_openable`); a consumer opens a session + (`TtySession::connect_direct` / `open_via_channels`). Both sides can + be both simultaneously — connection direction (who opened it) is + independent of TTY direction (who's the shell, who's the client). + Avoid "server" and "client" framing in docs and API names; use + "producer" and "consumer," or "accept side" / "connect side" for the + connection-establishment half specifically. + +8. **Module structure** — one module per file under `src/`, re-exported + from `src/lib.rs`. Public API surface is `lib.rs` re-exports. The + crate has three concerns: + - **Producer half** — `src/adapter.rs` (`TtyAdapter` + `drive_session` + for direct `alk/tty`) and `src/channels.rs` (`register_openable` + helper + `TtyOpenHandler` for the `alk/channels` multiplexed path). + - **Consumer half** — `src/session.rs` (`TtySession` typed client + with `connect_direct` and `open_via_channels` constructors). + - **Shared** — `src/wire.rs` (chunk codec), `src/control.rs` + (control-message enum + `signal_from_name`), `src/negotiation.rs` + (negotiation frame + `NegotiateRequest`), `src/backend.rs` + (`TtyBackend` trait, `TtyHandle`, `TtyParams`, `TtyError`). + The `src/local/` module (the folded `alknet-tty-local`) is behind the + `local` feature and never imported from the shared/producer/consumer + modules — only re-exported from `lib.rs` under + `#[cfg(feature = "local")]`. + +9. **`local` feature isolation** — the `local` module is the only + non-wasm part of the crate. The shared/producer/consumer modules + (`wire`, `control`, `negotiation`, `backend`, `adapter`, `channels`, + `session`) MUST NOT import from `crate::local` or reference any + `local`-only type (`LocalTtyBackend`, `portable_pty`, + `tokio::process::Command`). The producer's `TtyAdapter` takes a + `HashMap>` — the local backend is + injected at the assembly layer, not wired in by the adapter. A + regression where a non-`local` module pulls in `portable_pty`, + `tokio::process`, `std::thread`, or `libc` (outside the existing + `cfg(unix)` calls in `control.rs`/`pipe.rs`) silently breaks the + downstream TS/Python adapter story. + +10. **BAST document for the wire format** — `docs/architecture/tty-bast.md` + (Phase 4, not yet written) will be the machine-readable spec for the + `alk/tty` wire format, conforming to the BAST meta-schema at + `https://alk.dev/bast/v1/schema`. BAST is plain JSON — no + dependency required to author or consume it. 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. + If runtime validation against the BAST becomes desirable later, + alktype becomes an optional dep and the BAST document is already + there to feed it. Do not roll your own offset map or validator for + complex formats — use alktype (as an optional dep) when that + surfaces. + +11. **Access control** — the producer'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 (`provider.owns(id_ref, kind, &id, + "tty")` — the 4-arg shape alkcall adopted). 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. Terminal sessions are resources per + ADR-050 (ported as alktty ADR-003); `OwnershipStore::record(&self, + identity, resource_type, resource_id)` is the 3-arg shape (alkcall + dropped the old `action` arg). + +12. **Feature flags** — the only feature is `local` (default = `[]`). + `local` is inherently non-wasm (`portable-pty` + `tokio::process` + need a real OS); enabling `local` on wasm is a build error by + design. Verify both `cargo test` (default) and + `cargo test --all-features` pass. Do not add a feature flag that + pulls non-wasm deps into the default crate. + +13. **Naming** — Rust standard: `snake_case` for functions/variables/ + modules, `PascalCase` for types/traits, `SCREAMING_SNAKE_CASE` for + constants (`STREAM_STDIN`, `TTY_OPEN_SCOPE`, `CHUNK_HEADER_LEN`). + +14. **No `unsafe`** — the crate has zero `unsafe` blocks. The PTY + bridge's `libc::kill(-pgid, sig)` and `libc::kill(pid, sig)` calls + are safe `libc` crate APIs (not `unsafe` blocks in this crate); + bounds-checked slice access via `get(..)`/`ok_or_else` is the + pattern. Do not introduce `unsafe` for performance. + +## Verification Commands + +Run these before committing. All must pass. + +```bash +cargo test +cargo clippy --all-targets -- -D warnings +cargo fmt --check +cargo doc --no-deps +cargo test --all-features +cargo check --target wasm32-unknown-unknown # the default crate stays wasm-clean +cargo clippy --target wasm32-unknown-unknown -- -D warnings +cargo publish --dry-run --allow-dirty # before a release +``` + +The wasm check is the structural guard for the "default crate is +wasm-clean" invariant — run it whenever a non-`local` module changes. +`cargo test --all-features` exercises the `local` backend (PTY mode is +`#[cfg(unix)]`; pipe mode is cross-platform). + +## Architecture Context + +- `docs/plans/project-setup.md` — the current plan (phases 0–5). Phase 0 + (scaffold hygiene), Phase 1 (port core types), Phase 2 (channels + integration + `TtySession`), and Phase 3 (`local` backend) are landed. + Phase 4 (architecture docs + BAST schema + renumbered ADRs) and + Phase 5 (tests, including integration tests in `tests/` at the crate + root) are not yet done. +- `docs/architecture/` does not exist yet — it's created in Phase 4. + The ADRs referenced below (alknet ADR-052, 053, 054, 055, 056, 057, + 077, 093) will be ported and renumbered into alktty's ADR range + (001..008) at that time. Until then, the alknet originals at + `/workspace/@alkdev/alknet/docs/architecture/decisions/` are the + authoritative source — read them before non-trivial changes to the + wire format, trait, or adapter. +- Key ADRs that inform this crate's design (alknet numbers → planned + alktty numbers): + - ADR-052 → 001 — two-carriage wire format (JSON negotiation + raw + chunks); the 5-byte chunk header + - ADR-053 → 002 — `TtyBackend` trait (the inversion point between + wire-format adapter and backend crates) + - ADR-050 → 003 — dynamic resource ownership for runtime-spawned + terminal sessions + - ADR-054 → 004 — single crate with `local` feature (resolves the + alknet cyclic-dep workaround; the local backend is folded in) + - ADR-055 → 005 — exit-code reporting (`{"type":"exit","code":N}` on + `ctrl_out`; `code: -1` on wait-failure) + - ADR-056 → 006 — control-message split (`ctrl_in` client→server, + `ctrl_out` server→client; the bidirectionality fix) + - ADR-093 → 007 — TTY always uses its 5-byte format inside channels + (reverses ADR-077; channels strips its 8-byte header transparently) + - ADR-057 → 008 — negotiation framing is self-contained (not reused + from alkcall's `EventEnvelope` framing) +- If a TODO references a "Phase 7" note or a design direction that an + ADR has since decided against, the TODO is stale — remove it and + align with the ADR. Do not implement the rejected design. +- The crate was ported from `/workspace/@alkdev/alknet/crates/ + alknet-tty/src/` (wire, control, negotiation, backend, adapter) and + `/workspace/@alkdev/alknet/crates/alknet-tty-local/src/` (local + backend, pty, pipe). The source architecture docs at + `/workspace/@alkdev/alknet/docs/architecture/crates/tty/` are the + port origin for the Phase 4 spec docs. +- alkcall (`/workspace/@alkdev/alkcall`) is the upstream dependency. + All types formerly in `alknet-core` (`Connection`, `ProtocolHandler`, + `BiStream`, `BidiStreamSource`, `AuthContext`, `Identity`, + `IdentityProvider`, `AccessControl`, `OwnershipProvider`, + `OwnershipStore`, `InMemoryOwnershipStore`, `HandlerError`, + `StreamError`) come from `alkcall::core`. alkcall is v0.1.x — + breaking changes are expected at this major-zero stage; this is the + first real consumer, so we find and fix issues upstream rather than + working around them. Pin `alkcall = "0.1.1"` and bump deliberately. \ No newline at end of file