diff --git a/Cargo.toml b/Cargo.toml index 446ae2c..5d360bd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,11 +15,20 @@ name = "alktty" [features] default = [] -local = ["dep:portable-pty", "dep:tokio-util"] +# `local` is inherently non-wasm (portable-pty + tokio::process need a real +# OS). The default crate (no features) targets `wasm32-unknown-unknown`; +# enabling `local` on wasm is a build error by design — use a real OS for +# the local-process backend. The protocol crate (wire, negotiation, +# control, backend trait, adapter, session, channels) stays wasm-clean so +# downstream TS/Python adapters can compile it in a sandbox. +local = ["dep:portable-pty", "dep:tokio-util", "tokio/process", "tokio/rt-multi-thread"] [dependencies] alkcall = "0.1.1" -tokio = { version = "1", features = ["full"] } +# Minimal, wasm-clean tokio features. `local` adds `process` + +# `rt-multi-thread` (non-wasm). Do NOT use `features = ["full"]` — it +# pulls in `signal`/`fs`/`net` which break `wasm32-unknown-unknown`. +tokio = { version = "1", default-features = false, features = ["rt", "sync", "io-util", "macros"] } bytes = "1" futures = "0.3" futures-core = "0.3" diff --git a/docs/plans/project-setup.md b/docs/plans/project-setup.md index e778216..fb7d4f7 100644 --- a/docs/plans/project-setup.md +++ b/docs/plans/project-setup.md @@ -150,6 +150,29 @@ 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. +**WASM target.** The default crate (no `local` feature) MUST compile +to `wasm32-unknown-unknown`. alkcall and alktype both target wasm; +keeping alktty wasm-clean is what makes the downstream TS/Python +adapter story work — a wasm-compiled alktty is the protocol layer for +a sandboxed adapter (browser terminal over WebTransport, a Python +wheel that shells out to a wasm module, etc.). The `local` feature is +inherently non-wasm (`portable-pty` needs a real OS, `tokio::process` +needs `spawn`), so enabling `local` on wasm is a build error by +design — the local-process backend runs on a real OS (Linux, macOS, +Windows), never in a sandbox. + +Concretely, `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 and `tokio::process::Command` 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`). + ### 2. Dependency: `alknet-core` → `alkcall::core` All types previously from `alknet-core` now come from `alkcall::core`: @@ -292,12 +315,20 @@ operation spec — the registry runs the ACL before the wrapper, so the ### Phase 0: Scaffold hygiene (small fixes before porting) 1. Bump `alkcall` in `Cargo.toml` from `"0.1.0"` → `"0.1.1"` (ALPN - rename release). + rename release). *(Done in this revision.)* 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` + + dep for a public type alias). *(Done.)* +3. **Split tokio features for the WASM target.** *(Done.* `Cargo.toml` + *now uses `tokio = { default-features = false, features = ["rt", + "sync", "io-util", "macros"] }` for the default wasm-clean build, + and `local` adds `tokio/process` + `tokio/rt-multi-thread`.)* The + scaffold's `features = ["full"]` pulled in `signal`/`fs`/`net` + which break `wasm32-unknown-unknown`. The dev-deps that need + `tokio/process` (the pipe/pty integration tests) get it via the + `local` feature on the crate itself, not by re-declaring `full`. +4. 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]`. @@ -518,7 +549,23 @@ 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. +Pipe mode works on all platforms. The whole `local` module is behind +the `local` feature, which is non-wasm by design. + +### Risk: WASM target regression + +The default crate MUST stay `wasm32-unknown-unknown`-clean. A future +change that pulls in `tokio::process`, `std::thread`, `libc`, or any +OS-specific dep into a non-`local` module silently breaks the +downstream TS/Python adapter story (the wasm-compiled protocol layer +is what makes those adapters cheap to build). + +**Mitigation**: Add a CI job that runs `cargo check --target +wasm32-unknown-unknown` (no features) on every PR. Cheap, catches the +regression at the boundary. The tokio feature split in `Cargo.toml` +(the wasm-clean `["rt", "sync", "io-util", "macros"]` subset, with +`local` adding `process`/`rt-multi-thread`) is the structural guard; +the CI job is the enforcement. ### Risk: BAST schema drift