plan: keep default crate wasm-clean; split tokio features for local

The default crate (no features) must compile to wasm32-unknown-unknown
so the downstream TS/Python adapter story works — a wasm-compiled
alktty is the protocol layer for a sandboxed adapter. The local feature
is inherently non-wasm (portable-pty + tokio::process need a real OS)
and enabling it on wasm is a build error by design.

Cargo.toml:
- tokio: drop features = ["full"], use the wasm-clean subset alkcall
  uses (rt, sync, io-util, macros) with default-features = false
- local feature adds tokio/process + tokio/rt-multi-thread
- document the wasm constraint in the [features] comment

Plan:
- Decision 1: add WASM target subsection recording the constraint
- Phase 0: mark the tokio feature split as done
- Risks: add WASM-target-regression risk with a cargo-check CI mitigation
This commit is contained in:
2026-08-17 09:23:21 +00:00
parent 2086817a90
commit 66caa309ef
2 changed files with 62 additions and 6 deletions
+11 -2
View File
@@ -15,11 +15,20 @@ name = "alktty"
[features] [features]
default = [] 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] [dependencies]
alkcall = "0.1.1" 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" bytes = "1"
futures = "0.3" futures = "0.3"
futures-core = "0.3" futures-core = "0.3"
+51 -4
View File
@@ -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`, be a separate crate — those have real external deps (`bollard`,
`russh`) and their own resource models. `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` ### 2. Dependency: `alknet-core` → `alkcall::core`
All types previously from `alknet-core` now come from `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) ### Phase 0: Scaffold hygiene (small fixes before porting)
1. Bump `alkcall` in `Cargo.toml` from `"0.1.0"` → `"0.1.1"` (ALPN 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 2. Add `futures = "0.3"` to `[dependencies]` (the `BoxFuture` alias
uses `futures::future::BoxFuture`; alkcall pulls it transitively uses `futures::future::BoxFuture`; alkcall pulls it transitively
but alktty should declare it directly to not rely on a transitive but alktty should declare it directly to not rely on a transitive
dep for a public type alias). dep for a public type alias). *(Done.)*
3. Confirm the `local` feature already wires `portable-pty` + 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 `tokio-util` as optional deps (it does in the scaffold). The
`local` module's `pty.rs` additionally needs `libc` under `local` module's `pty.rs` additionally needs `libc` under
`cfg(unix)` — already in `[target.'cfg(unix)'.dependencies]`. `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). specific (Unix only for PTY, pipe mode works cross-platform).
**Mitigation**: Gate PTY behind `#[cfg(unix)]` as the existing code does. **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 ### Risk: BAST schema drift