feat(session): land the session mapping — the translation bridge + the exit derivation (w3-tty-session)

The biggest single-row surface of the spine: pty/shell/pipe-exec serve
through the published alktty seam (drive_session_pre_negotiated) over a
translation bridge, re-landing POC-3 stage D as contract:

- src/tty_map.rs (session feature): the bridge (SSH data → Chunk::stdin
  through a bounded mpsc + framing task; SSH EOF → the zero-length
  stdin sentinel — verified against alktty's pump semantics; stdout →
  data; stderr → extended_data(1); exit chunk → the derivation
  contract), the locally-built negotiation (OQ-08: shell argv verbatim,
  wrapper ++ [cmd], cwd/env session-side, ttymodes → the reserved {}
  modes JSON), and the spawned serving path.
- serve_tty_row: driven chunks→SSH inline so the exit report is the
  path's final act (status before close, ADR-006 decision 4); two
  cancel signals (per-channel watch fired by channel_close, connection
  teardown fired by SessionHandler::Drop) abort the alktty session
  task — the ADR-056 kill-on-drop contract is the cancelled session's
  cleanup.
- Derivation contract rules 1–6 in derive_exit + the crate-local
  signum→name table: −N → exit-signal(name), −1 → exit-status(255),
  core_dumped never, unmapped negatives → 255, the PTY-path rule-5
  fidelity limit documented; wire rows pin the shapes.
- Negotiation-error frames (alktty's 0x00-length-prefixed error
  framing) → negotiation-failed serving decline + exit-status(255) +
  close — the one could-not-start claim this door makes (nothing ran).
- Control rows: window_change/signal → the per-channel BridgeSurface
  (bounded mpsc, try_send, never awaited on the packet loop); signal
  names verbatim (no decline shape — fire-and-forget); pre-serve
  resizes update the stored pty params (final size wins).
- Unset-declines (OQ-08) keep the git row's serving-path shape
  (failure then close, spawned): shell-unconfigured /
  wrapper-unconfigured / seam-unconfigured; pipe-less shell SERVES
  pipe-mode (the recorded vanilla-sshd posture); env_request decline
  unchanged (OQ-09).
- tests: the hand-rolled TtyBackend shim + ownership witness doubles;
  serving rows through the harness, the unset-field decline matrix,
  derivation rows on the wire, negotiation-failure rows, the identity
  ride asserted live at the seam, the kill witness; the real-tool
  smoke — OpenSSH -tt against a real PTY via dev-dep alktty local
  (test graph only), skip-with-reason posture. Wave-2 stub rows
  migrated to the new decline reasons (dispatch_core, observe).

Verification: cargo test (113) / --features session (149) / git (142) /
tunnel (115) / --all-features (161) — all green; cargo clippy
--all-targets -- -D warnings ×5 feature configs; cargo fmt --check;
cargo doc --no-deps ×3 builds clean; taskgraph validate.
This commit is contained in:
glm-5.3-flash committed 2026-10-28 20:29:59 +00:00
1 parent 7aac2d11a2
commit b1e569d1a8
13 files changed
+3018 -155

No files matched your search

+142 -9
View File
@@ -1,7 +1,7 @@
---
id: w3-tty-session
name: Wave 3 — session → alktty (translation bridge + exit derivation)
status: pending
status: completed
depends_on: [w3-git-mapping]
scope: moderate
risk: medium
@@ -98,26 +98,26 @@ proved, re-landed as contract:
## Acceptance Criteria
- [ ] pty/shell/pipe-exec rows serve through
- [x] pty/shell/pipe-exec rows serve through
`drive_session_pre_negotiated` (spawned, handler fast); the
store-and-take + per-channel PTY state reused from the git
task's handoff; pipe-less shell decided + recorded
- [ ] `pty_request` → `TerminalParamsWire` (ttymodes → the reserved
- [x] `pty_request` → `TerminalParamsWire` (ttymodes → the reserved
JSON), `shell_request` → shell argv, exec → `wrapper ++ [cmd]`,
cwd/env session-side; unset-declines on both OQ-08 rows with
recorded wire shapes
- [ ] The bridge translated per row (stdin chunks + EOF shape
- [x] The bridge translated per row (stdin chunks + EOF shape
verified against alktty; stdout→data; stderr→`extended_data(1)`;
resize/signal CTRL_IN reach the serving path through a bounded
per-channel surface)
- [ ] The derivation contract rules 1–6 in code with the signum→name
- [x] The derivation contract rules 1–6 in code with the signum→name
table; `−1`→255; `core_dumped` never; PTY-path rule-5 documented;
exit-status before close from the spawned task — pinned by rows
- [ ] The decline matrix per build green through the harness (unset-
- [x] The decline matrix per build green through the harness (unset-
field rows included); identity ride asserted; the `env_request`
decline unchanged; skeleton stub rows migrated
- [ ] The real-tool pty smoke row (posture decided + recorded)
- [ ] `cargo test` (+ per-feature + `--all-features` rows), clippy ×2
- [x] The real-tool pty smoke row (posture decided + recorded)
- [x] `cargo test` (+ per-feature + `--all-features` rows), clippy ×2
(`-D warnings`), fmt, `cargo doc --no-deps` pass
## References
@@ -140,4 +140,137 @@ proved, re-landed as contract:
## Notes
> To be filled at implementation time.
### Decisions recorded (implementation, 2026-10-28)
- **The module** (`src/tty_map.rs`, feature `session`): the session
rows live in one module following the `git_map` pattern — the
bridge, the locally-built negotiation, the derivation contract, and
the serving path. `serde_json` (the OQ-44 modes JSON + the exit
chunk/error-frame reads) and `bytes` (the chunk plane) joined as
session-gated optional deps at alktty's own pins.
- **The negotiation** (locally built, never wire-derived — OQ-08):
`pty_request` args → `TerminalParamsWire` with SSH ttymodes → the
**reserved `{}` modes JSON** (alktty OQ-44's reservation, deferred
there; the map is honest — nothing is carried). Shell argv rides the
serve-config **verbatim**; exec rides `wrapper ++ [cmd]` (never
naively split); cwd/env come from `session_cwd`/`session_env` only.
`backend_params` stays empty (no backend-specific selector fields on
this door; the seam selector is the `backend` string).
**Pipe-less shell decided: a `shell_request` without a preceding
pty SERVES pipe-mode (`tty = None`)** — vanilla sshd serves a shell
without a terminal too (`ssh -T` is legitimate vanilla).
- **Unset-declines (OQ-08) — wire shape decided + recorded**: the
**serving-path shape (`channel_failure` then close, from a spawned
task)** for `shell_argv`-unset (`shell-unconfigured`),
`exec_wrapper_argv`-unset (`wrapper-unconfigured`), and the
`session_seam`-unset rows (`seam-unconfigured` — the git row's
recorded shape); the pty row checks the seam first (a doomed
allocation is declined before its params are stored — decline first,
alloc never). Field checks precede the seam check. The unvanilla
guards keep the git row's shapes: `channel-not-held` → inline
request-level failure; `no-identity` → the spawned serving-path
decline. A routed request answers `channel_success` inline (the
POC-3 §A.5 shape).
- **The translation bridge** (the POC-3 §D.1 shape, two adapters
around a duplex pair): SSH `data` → `Chunk::stdin` through a bounded
mpsc (bound 64) feeding a framing task; SSH channel EOF → the
**zero-length stdin-chunk sentinel** — decided and verified against
alktty 0.8.1's `pump_client_to_backend`: a zero-length stdin chunk
shuts the backend's stdin, identical in effect to
`ControlMessage::Eof`, and rides the data path the SSH EOF already
chose (no extra control frame). stdout chunks → SSH `data`; stderr
chunks → `extended_data(1)`; the exit chunk → the derivation
contract. All buffers bounded (convention 9): mpsc 64, duplexes
64 KiB, frames bounds-checked against alktty's `MAX_CHUNK_LEN`.
- **The negotiation-error frame is a serving-path decline class**
(decided + recorded): alktty re-runs validation/allocation/ownership
inline even on the pre-negotiated path and answers failures with a
length-prefixed error frame (first byte `0x00` — alktty's ADR-052 §5
disambiguation invariant). The bridge reads it via a 4-byte
first-window disambiguation and maps it to `negotiation-failed` +
**`exit-status(255)` + close — the one place this door claims 255**:
the session never started, nothing ran, so 255 is the honest
could-not-start semantic (contrast the git row, whose service runs
and honestly reports `1`; `255` is never fabricated for a real
outcome). A bridge read ending **without** an exit chunk reports
nothing (sole-fidelity-input posture: absence is neither 0 nor 255)
— plain close.
- **The control rows**: `window_change_request` →
`ControlMessage::Resize`, `signal` → `ControlMessage::Signal`,
forwarded through the **per-channel `BridgeSurface`** (handler
state while the row serves): a bounded mpsc whose `try_send` is
never awaited on the packet loop — a full/closed surface drops the
control event (fire-and-forget rows carry no SSH reply to decline
with; bounded resources, convention 9). **Signal names are accepted
verbatim — no decline shape**: the backend owns the rejection
semantics, nothing on the wire can answer. A pre-serve resize
updates the stored pty params instead (the final size wins —
vanilla sshd's ordering).
- **The derivation contract** (mapping.md rules 1–6, in code + unit
rows + wire rows): `derive_exit` — `code ≥ 0` → `exit-status(N)`;
negative `−N`, N ≥ 2 → `exit-signal(name)` via the crate-local
[`SIGNAL_NAMES`] signum→name table (1–31, server-side work,
deliberately not alktty's `signal_from_name` inverse); `−1` →
`exit-status(255)` (rule 3, the SIGHUP collision accepted
symmetrically); `core_dumped` **never asserted** (the bool stays
false on every exit-signal row); unmapped negatives (and
`i32::MIN`'s checked-abs failure) → 255 (rule 6); the PTY-path
rule-5 fidelity limit documented in the module docs. Report sent
**before** close from the spawned task; names ride russh's `Sig`
(known variants verbatim, `Sig::Custom` for the rest — RFC 4254
§6.10).
- **Cancellation** (bounded resources + alktty's kill-on-drop): the
serving path watches two signals — the per-channel cancel (fired by
`channel_close` via the surface's watch) and the connection
teardown (fired by the handler's `Drop`; the russh session ended
however it ended). On either: the serving task **aborts the alktty
session task**, dropping its not-yet-resolved `exit_code` future —
alktty's ADR-056 kill-on-drop contract is the cancelled session's
cleanup — then closes the channel and ends. Witnessed live by the
shim's kill log row.
- **The contract tests** (`tests/tty_mapping.rs`, session-gated;
`tests/common/tty_doubles.rs`): the shim double (hand-rolled per
the `TtyHandle` field shapes — alktty's `MockBackend` is
`pub(crate)` and never importable) records TtyParams, consumes
stdin/EOF/resizes/signals, emits scripted chunks and exit codes,
and carries the kill-on-drop witness; the ownership witness double
observes the identity at the seam. The wave-2 stub rows migrated:
`dispatch_core.rs`'s session-build rows now pin the
unset-decline reasons (`seam-unconfigured` / `wrapper-unconfigured`,
same serving-path wire shape); `observe.rs`'s two session rows
updated (and a one-line pre-existing clippy lint in its git-build
cap row fixed). `env_request` decline unchanged (OQ-09).
- **The identity ride** (the parked wave-2 item, discharged): asserted
through alktty's ADR-050 ownership check — the shim's
`resource_id` guarantees the check evaluates, and the ownership
double records the resolved identity; the deny shape also pins the
negotiation-error wire shape.
- **Real-tool smoke posture (recorded)**: one OpenSSH `-tt` pty
session (`ssh -tt … smoke-rider@127.0.0.1` with piped stdin) through
a TCP-mounted adapter against a **real PTY via a dev-dep alktty
`local` composition** — the decided posture: a shim row cannot show
what vanilla OpenSSH really sends, and POC-3 stage D proved exactly
this composition. The dev-dep `features = ["local"]` unification
applies to the test graph only (the published lib never enables it;
`src/` never names a local shape). Requires ssh + sh; absent
tooling is skipped **with a printed reason** — never silently green.
The smoke pins the pty lifecycle (serve, stdin ride, clean exit →
exit-status(0) before close); the `stty size`-grade fidelity matrix
is wave 5's.
- **Verification**: `cargo test` (default), `cargo test --features
session` (incl. the smoke + shim rows), `cargo test --features git`,
`cargo test --features tunnel`, `cargo test --all-features`;
`cargo clippy --all-targets -- -D warnings` ×5 (default / session /
git / tunnel / all-features); `cargo fmt --check`; `cargo doc
--no-deps` (default + session + all-features) — all green.