scaffold: Cargo.toml and project setup plan
This commit is contained in:
+43
@@ -0,0 +1,43 @@
|
|||||||
|
[package]
|
||||||
|
name = "alktty"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
rust-version = "1.85"
|
||||||
|
license = "MIT OR Apache-2.0"
|
||||||
|
description = "Terminal session protocol: wire format, TtyBackend trait, TtyAdapter, and typed consumer client. Producer/consumer protocol crate on top of alkcall channels."
|
||||||
|
repository = "https://git.alk.dev/alkdev/alktty"
|
||||||
|
keywords = ["tty", "terminal", "pty", "channels", "alkcall"]
|
||||||
|
categories = ["network-programming", "asynchronous"]
|
||||||
|
exclude = [".opencode/", "docs/reviews/", "docs/research/", "docs/sdd_process.md", "Cargo.lock"]
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "alktty"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = []
|
||||||
|
local = ["dep:portable-pty", "dep:tokio-util"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
alkcall = "0.1.0"
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
bytes = "1"
|
||||||
|
futures-core = "0.3"
|
||||||
|
tokio-stream = "0.1"
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
async-trait = "0.1"
|
||||||
|
tracing = "0.1"
|
||||||
|
thiserror = "2"
|
||||||
|
|
||||||
|
portable-pty = { version = "0.9", optional = true }
|
||||||
|
tokio-util = { version = "0.7", features = ["io"], optional = true }
|
||||||
|
|
||||||
|
[target.'cfg(unix)'.dependencies]
|
||||||
|
libc = "0.2"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tokio = { version = "1", features = ["full", "test-util", "macros"] }
|
||||||
|
tokio-stream = "0.1"
|
||||||
|
serde_json = "1"
|
||||||
|
tempfile = "3"
|
||||||
|
bytes = "1"
|
||||||
@@ -0,0 +1,299 @@
|
|||||||
|
# Project Setup Plan
|
||||||
|
|
||||||
|
Status: draft
|
||||||
|
Last updated: 2026-08-14
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Extract `alknet-tty` + `alknet-tty-local` from the alknet mono-repo into a
|
||||||
|
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.
|
||||||
|
|
||||||
|
## Crate Structure
|
||||||
|
|
||||||
|
Single crate (`alktty`) with the local backend gated behind a `local`
|
||||||
|
feature. This is a "tty-specific monorepo" — one crate, one concern.
|
||||||
|
|
||||||
|
```
|
||||||
|
alktty/
|
||||||
|
├── src/
|
||||||
|
│ ├── lib.rs # crate root, re-exports
|
||||||
|
│ ├── wire.rs # ChunkReader/ChunkWriter (5-byte format)
|
||||||
|
│ ├── negotiation.rs # NegotiateRequest, framing reader/writer
|
||||||
|
│ ├── control.rs # ControlMessage enum, signal_from_name
|
||||||
|
│ ├── backend.rs # TtyBackend trait, TtyHandle, TtyParams, TtyError
|
||||||
|
│ ├── 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)
|
||||||
|
├── docs/
|
||||||
|
│ ├── architecture/
|
||||||
|
│ │ ├── README.md # architecture index
|
||||||
|
│ │ ├── crates/
|
||||||
|
│ │ │ ├── overview.md
|
||||||
|
│ │ │ ├── tty-wire.md
|
||||||
|
│ │ │ ├── tty-backend.md
|
||||||
|
│ │ │ ├── tty-adapter.md
|
||||||
|
│ │ │ └── tty-local.md
|
||||||
|
│ │ └── decisions/ # renumbered ADRs
|
||||||
|
│ └── plans/
|
||||||
|
│ └── project-setup.md # this document
|
||||||
|
├── Cargo.toml
|
||||||
|
├── LICENSE-APACHE
|
||||||
|
└── LICENSE-MIT
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Design Decisions
|
||||||
|
|
||||||
|
### 1. Single crate, feature-gated local backend
|
||||||
|
|
||||||
|
The old split (`alknet-tty` + `alknet-tty-local`) had a cyclic dependency
|
||||||
|
problem: `alknet-tty-local` depends on `alknet-tty` for the trait, and
|
||||||
|
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.
|
||||||
|
|
||||||
|
### 2. Dependency: `alknet-core` → `alkcall`
|
||||||
|
|
||||||
|
All types previously from `alknet-core` now come from `alkcall::core`:
|
||||||
|
- `ProtocolHandler`, `Connection`, `BiStream`, `BidiStreamSource`
|
||||||
|
- `AuthContext`, `Identity`, `IdentityProvider`
|
||||||
|
- `AccessControl`, `OwnershipProvider`
|
||||||
|
- `HandlerError`, `StreamError`
|
||||||
|
|
||||||
|
The `TtyAdapter`'s `impl ProtocolHandler` changes which crate the trait
|
||||||
|
comes from. The trait shape is identical.
|
||||||
|
|
||||||
|
### 3. Channels integration (the new part)
|
||||||
|
|
||||||
|
This is where alktty becomes a proper producer/consumer on alkcall.
|
||||||
|
|
||||||
|
**Producer half** — two paths to expose TTY:
|
||||||
|
|
||||||
|
- **Direct ALPN** (`alk/tty`): `TtyAdapter` implements `ProtocolHandler`.
|
||||||
|
Existing code, just import migration. Used for direct-connect scenarios
|
||||||
|
(browser terminal over WebTransport, standalone TTY endpoint).
|
||||||
|
|
||||||
|
- **Through channels** (`alk/channels`): Register an `OpenHandler` via
|
||||||
|
`ChannelCore::register_openable()`. The handler receives a `Connection`
|
||||||
|
(the channel's data plane with ALPN `alk/tty`), calls `accept_bi()`,
|
||||||
|
and runs the same `drive_session` on the resulting `BiStream`.
|
||||||
|
|
||||||
|
Per ADR-093 (reversal of ADR-077): TTY always uses its 5-byte format.
|
||||||
|
The channels layer strips its 8-byte header and hands TTY the payload
|
||||||
|
transparently. The same `wire.rs` code runs in both modes — only the
|
||||||
|
`BiStream` source differs.
|
||||||
|
|
||||||
|
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`.
|
||||||
|
|
||||||
|
**Consumer half** — `TtySession`:
|
||||||
|
|
||||||
|
A typed client that wraps the wire protocol. Two constructors:
|
||||||
|
- `TtySession::connect_direct(connection)` — for direct `alknet/tty`
|
||||||
|
- `TtySession::open_via_channels(client, params)` — opens a channel via
|
||||||
|
`ChannelClient`, negotiates, returns a session handle
|
||||||
|
|
||||||
|
The session handle exposes:
|
||||||
|
- `send_stdin(bytes)` / `close_stdin()`
|
||||||
|
- `recv_stdout()` / `recv_stderr()` — streams
|
||||||
|
- `resize(cols, rows)`, `signal(name)`
|
||||||
|
- `wait()` — awaits exit code
|
||||||
|
|
||||||
|
### 4. Negotiation framing — keep self-contained
|
||||||
|
|
||||||
|
ADR-057 says the negotiation framing is self-contained (~30 lines, format
|
||||||
|
coincides with alkcall's by convention). Even though alkcall is now a
|
||||||
|
dependency, we keep the self-contained framing because:
|
||||||
|
|
||||||
|
- The payload is `NegotiateRequest`, not `EventEnvelope` — alkcall's
|
||||||
|
`FrameFramedReader` is hardcoded to deserialize `EventEnvelope`
|
||||||
|
- The framing is trivial (4-byte BE length prefix + body)
|
||||||
|
- Avoids coupling to alkcall's internal wire types for a TTY-specific payload
|
||||||
|
|
||||||
|
### 5. Producer/consumer framing
|
||||||
|
|
||||||
|
The architecture README in alkcall describes protocol crates as having two
|
||||||
|
halves:
|
||||||
|
|
||||||
|
1. **Producer half** — `register_*()` functions that take an
|
||||||
|
`&mut OperationRegistry` and register ops with handlers. For channels,
|
||||||
|
an `OpenHandler` factory.
|
||||||
|
|
||||||
|
2. **Consumer half** — a typed client wrapper around `CallConnection` or
|
||||||
|
`ChannelClient` that exposes the crate's ops as async methods.
|
||||||
|
|
||||||
|
We make this explicit in the module structure:
|
||||||
|
- `src/adapter.rs` + `src/channels.rs` = producer half
|
||||||
|
- `src/session.rs` = consumer half
|
||||||
|
|
||||||
|
### 6. Access control and auth
|
||||||
|
|
||||||
|
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
|
||||||
|
- **Channel lifecycle policy**: `ChannelLifecyclePolicy` — per-identity
|
||||||
|
channel caps, open/close hooks
|
||||||
|
- **Ownership**: `OwnershipProvider` — runtime-spawned resource tracking
|
||||||
|
(terminal sessions are resources per ADR-050)
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## Migration Strategy
|
||||||
|
|
||||||
|
### Phase 1: Scaffold and core types (no new logic)
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
### 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`
|
||||||
|
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`
|
||||||
|
|
||||||
|
### Phase 3: Local backend (behind `local` feature)
|
||||||
|
|
||||||
|
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`
|
||||||
|
|
||||||
|
### Phase 4: Architecture docs
|
||||||
|
|
||||||
|
1. Port the 5 spec docs from `alknet/docs/architecture/crates/tty/`
|
||||||
|
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
|
||||||
|
|
||||||
|
### 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)
|
||||||
|
3. Add channels integration tests — end-to-end `register_openable` +
|
||||||
|
`ChannelClient::open_channel` + `drive_session` round-trip
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
### OQ-1: ALPN strings (RESOLVED)
|
||||||
|
|
||||||
|
**Decision**: Use `alk/tty`. The `alknet/<name>` convention is being
|
||||||
|
shortened to `alk/<name>` 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/<name>` as the ALPN
|
||||||
|
convention.
|
||||||
|
|
||||||
|
### OQ-2: Channel open operation name (RESOLVED)
|
||||||
|
|
||||||
|
The alkcall test uses `channels/tty/sub` as the operation name. The
|
||||||
|
convention from ADR-047 is `channels/<alpn>/sub` for subscribe (consumer
|
||||||
|
opens) and `channels/<alpn>/pub` for publish (producer opens). Since TTY
|
||||||
|
is consumer-opens (the client requests a shell), `channels/tty/sub` is
|
||||||
|
correct.
|
||||||
|
|
||||||
|
**Decision**: `channels/tty/sub` for consumer-opens, `channels/tty/pub`
|
||||||
|
reserved for future producer-opens use case.
|
||||||
|
|
||||||
|
### OQ-3: `TtySession` API — stream vs callback (RESOLVED)
|
||||||
|
|
||||||
|
**Decision**: Stream-based (`Stream<Item = Bytes>`). Consistent with the
|
||||||
|
backend trait's `TtyHandle` shape.
|
||||||
|
|
||||||
|
### OQ-4: `drive_session` — keep the three-pump pattern? (RESOLVED)
|
||||||
|
|
||||||
|
**Decision**: Keep it. It works, it's tested, and ADR-093 means the same
|
||||||
|
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.
|
||||||
|
|
||||||
|
## Upstream Changes Needed in alkcall
|
||||||
|
|
||||||
|
These are changes we need to push to alkcall before or alongside this work:
|
||||||
|
|
||||||
|
1. **ALPN convention**: `alknet/<name>` → `alk/<name>` across docs, ADRs,
|
||||||
|
and test code. The test at `channels/client.rs:767` uses
|
||||||
|
`ChannelOpenSpec::new("alknet/tty")` — needs `"alk/tty"`.
|
||||||
|
|
||||||
|
2. **`CHANNELS_ALPN` constant**: `b"alknet/channels"` → `b"alk/channels"`.
|
||||||
|
This is in `alkcall/src/channels/adapter.rs`.
|
||||||
|
|
||||||
|
3. **ADR-004** (ALPN convention): Update 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`.
|
||||||
|
|
||||||
|
## Risks and Concerns
|
||||||
|
|
||||||
|
### Risk: alkcall is v0.1.0 — 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
|
||||||
|
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.
|
||||||
|
|
||||||
|
### Risk: `drive_session` complexity
|
||||||
|
|
||||||
|
The three-pump driver is ~1500 lines with edge cases around exit ordering,
|
||||||
|
cancel cleanup, and bidirectional control. Porting it is mechanical but
|
||||||
|
needs careful review.
|
||||||
|
|
||||||
|
**Mitigation**: The existing code has extensive tests. Port tests first
|
||||||
|
or alongside.
|
||||||
|
|
||||||
|
### Risk: Local backend PTY bridge
|
||||||
|
|
||||||
|
The PTY mode uses 3 std threads feeding tokio channels. This is a
|
||||||
|
well-understood pattern (wezterm uses it) but is inherently platform-
|
||||||
|
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.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [alkcall architecture README](/workspace/@alkdev/alkcall/docs/architecture/README.md)
|
||||||
|
- [alknet-tty architecture docs](/workspace/@alkdev/alknet/docs/architecture/crates/tty/)
|
||||||
|
- [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
|
||||||
Reference in New Issue
Block a user