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:
1 parent
7aac2d11a2
commit
b1e569d1a8
13 files changed
+3018
-155
No files matched your search
@@ -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.
|
||||
Reference in new issue
Block a user