From d1f755f2d92936fe6078328c8509c4c1929771a5 Mon Sep 17 00:00:00 2001 From: "glm-5.3-flash" Date: Wed, 28 Oct 2026 12:32:13 +0000 Subject: [PATCH] =?UTF-8?q?docs(tasks):=20decompose=20wave=203=20=E2=80=94?= =?UTF-8?q?=20the=20server=20mappings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Six implementation tasks + the gate under tasks/implementation/, chained sequentially (the mappings replace stubs in the same Handler impl and share the decline-matrix test binaries): - w3-seam-config: the serve-config's per-feature seam handles (alktty backends/ownership/selector; the alkgit quartet/pipeline/ push-options flag) + the optional sibling deps — wave 3's first course correction to the wave-1 config surface - w3-decline-tracing: the wave-2 gate's booked finding — the decline/serve-path tracing vocabulary, landed before the mappings - w3-git-mapping: the full exec grammar replaces the wave-2 probe + the serve_duplex_resolved dispatch + the store-and-take handoff - w3-tty-session: the translation bridge + the exit-derivation contract (rules 1-6, the signum table) - w3-tunnel-direct: the route-1 bridge (accept only after the fabric open succeeds) + set_identity propagation + the capacity-across- fabric-window mechanics - w3-tunnel-listen: the -R listen flow + the forwarded-tcpip data leg (the POC-untested leg; the stop-and-book provision) - review-wave-3: the gate The plan carries the wave-3 decomposition record with four interpretations flagged for the gate (the serve-config course correction, the already-discharged server-side sftp bullet, the w3-tunnel-listen open mechanics, the booked-items' wave-3 homes). taskgraph validate: 26 tasks, clean. Sibling pins verified against crates.io at decomposition time: alktty 0.8.1 / alkgit 0.1.0 / alktunnels 0.4.1 / alkcall 0.10.1. --- docs/plans/implementation.md | 62 ++++++++- tasks/implementation/review-wave-3.md | 92 +++++++++++++ tasks/implementation/w3-decline-tracing.md | 73 +++++++++++ tasks/implementation/w3-git-mapping.md | 124 ++++++++++++++++++ tasks/implementation/w3-seam-config.md | 96 ++++++++++++++ tasks/implementation/w3-tty-session.md | 143 +++++++++++++++++++++ tasks/implementation/w3-tunnel-direct.md | 125 ++++++++++++++++++ tasks/implementation/w3-tunnel-listen.md | 135 +++++++++++++++++++ 8 files changed, 849 insertions(+), 1 deletion(-) create mode 100644 tasks/implementation/review-wave-3.md create mode 100644 tasks/implementation/w3-decline-tracing.md create mode 100644 tasks/implementation/w3-git-mapping.md create mode 100644 tasks/implementation/w3-seam-config.md create mode 100644 tasks/implementation/w3-tty-session.md create mode 100644 tasks/implementation/w3-tunnel-direct.md create mode 100644 tasks/implementation/w3-tunnel-listen.md diff --git a/docs/plans/implementation.md b/docs/plans/implementation.md index 6a590a0..cd5424d 100644 --- a/docs/plans/implementation.md +++ b/docs/plans/implementation.md @@ -45,7 +45,7 @@ mappings ∥ client direction) → validation & release. |---|---|---|---| | [1](#wave-1--foundations) | Manifest landing (russh pin, feature graph); error model; config surface (serve-config + dial-config + `Limits`); vault-backed key sources; host-key policy seam; fingerprint; test doubles | — | decomposed | | [2](#wave-2--the-server-stack) | `SshAdapter::handle` mount + `Config` construction; the auth bridge; parse-only dispatch + fail-closed decline core; spawn-and-return plumbing; bounded resources; loopback harness | wave 1 | decomposed | -| [3](#wave-3--server-mappings) | session → alktty (translation bridge + exit derivation); git → alkgit (exec grammar + dispatch); tunnel → alktunnels route 1; sftp placeholder declines | wave 2 | pending | +| [3](#wave-3--server-mappings) | session → alktty (translation bridge + exit derivation); git → alkgit (exec grammar + dispatch); tunnel → alktunnels route 1; sftp placeholder declines | wave 2 | decomposed | | [4](#wave-4--client-direction) | Dial surface; op rows (exec/pty, direct-tcpip, git exec); the `-R` leg (listen + acceptor registry); lifecycle primitives | wave 1 | pending | | [5](#wave-5--validation--release) | Decline-matrix contract suite; real-tool e2e; fuzzing; docs; **the publish** | waves 3 + 4 | pending | @@ -288,6 +288,66 @@ wave 5's: Wave 3 decomposes after the wave-2 gate, absorbing whatever it corrects. +**Decomposition record** (2026-10-28, after the wave-2 gate closed +green): wave 3 decomposed into six task files under +`tasks/implementation/` plus the gate. Shape: `w3-seam-config` first +(the serve-config's per-feature seam handles + the optional +`alktty`/`alkgit` deps — see the interpretations — the mappings +dispatch against them, so it lands before the mappings); then +`w3-decline-tracing` (the wave-2 gate's booked finding: the decline/ +serve-path event vocabulary, landed **before** the mappings so they +inherit a uniform shape); then the three mapping branches +deliberately sequential — `w3-git-mapping` (the full grammar replaces +the wave-2 probe + the `serve_duplex_resolved` dispatch + the +store-and-take handoff it lands first for the session task to reuse), +`w3-tty-session` (the translation bridge + the exit-derivation +contract — the biggest single-row surface), `w3-tunnel-direct` (the +POC-proven route-1 bridge leg + identity `set_identity` propagation), +and `w3-tunnel-listen` (the `-R` listen flow + `forwarded-tcpip` +data leg — the POC-untested leg, sized with the stop-and-book +provision); and `review-wave-3` (the gate) depending on all six. As +with wave 2, the branch DAG is sequential: the three mappings replace +stubs in the same `Handler` impl and share the decline-matrix test +binaries, so no two tasks hold them in parallel — the branches are +parallel *conceptually* (independent seams), sequential +*logistically*. + +Interpretations recorded (flagged for the wave-3 gate): + +- **The serve-config extension is wave 3's first course correction to + the wave-1 config surface** — the assembly-supplied alktty/alkgit + seam handles the mappings' seam signatures take (the tty backends + map + selector + ownership provider; the alkgit backend quartet + + pipeline semaphore + push-options flag) were never carried by the + wave-1 config bullet, and `w3-seam-config` lands them feature-gated + with the optional sibling deps (`alktty = "0.8.1"`, `alkgit = + "0.1.0"` — crates.io-verified at decomposition) before the + mappings; `alktunnels = "0.4.1"` still lands with its first + consuming code (`w3-tunnel-direct`) to avoid a dead entry. The + unset-declines posture extends to the new handles (unset → the + owned rows decline even with the feature on). +- **The server-side sftp bullet is already discharged** — the OQ-13 + decline rows landed with `w2-dispatch-decline-core` (every subsystem + name declines in every build) and are already contract-tested per + build in `tests/dispatch_core.rs`; no wave-3 task exists for it — + the client-side placeholder folds into wave 4's acceptor/op-row + work and the wave-5 matrix covers both rows explicitly. +- **The `-R` listen flow carries the wave-3 gate's biggest open** — + the fabric-side inbound-forwarded mechanics (who dials the + forwarded op at the door, whether the assembly-supplied + `Arc` composition can carry the door-side accept or + the serve-config's tunnel handle must grow) is decided in + `w3-tunnel-listen` against alkcall 0.10.1 / alktunnels 0.4.1's + published surfaces; if the topology does not close, the task stops + and books it for the gate — nothing is silently forked or absorbed. +- **The wave-2 gate's three booked items all have wave-3 homes** — the + decline-path tracing task (`w3-decline-tracing`), the forwarded- + tcpip acceptor vocabulary (`w3-tunnel-listen`, with the client-side + accept hook landing in the harness), and the deferred identity- + landing assertion (each mapping asserts the resolved identity + reaches its seam; the tunnel branch additionally propagates it via + `set_identity` — auth.md §Server direction step 4). + ## Wave 4 — Client direction The door outward (independent of wave 3 — shares only wave 1's diff --git a/tasks/implementation/review-wave-3.md b/tasks/implementation/review-wave-3.md new file mode 100644 index 0000000..47d205f --- /dev/null +++ b/tasks/implementation/review-wave-3.md @@ -0,0 +1,92 @@ +--- +id: review-wave-3 +name: Wave 3 gate — the server-mappings review +status: pending +depends_on: [w3-seam-config, w3-decline-tracing, w3-git-mapping, w3-tty-session, w3-tunnel-direct, w3-tunnel-listen] +scope: narrow +risk: low +impact: phase +level: review +--- + +## Description + +The wave-3 review gate. The server mappings are the crate's spine +landed in wire-capable form — the wave-4 client direction reuses the +same grammar/rows pointed upstream, wave 5 builds the full matrix on +these rows, and the published crate freezes this surface. The gate +reads the landed mappings against the architecture line by line: + +- **mapping.md line-by-line** — each of the spine's server rows + (session/pty/shell/pipe; git exec; direct-tcpip; + direct-streamlocal; forwarded-tcpip; the global listen rows; the + request-level table) read against the landed code; the §exit + derivation contract rules 1–6 verified in code + rows; the decline + matrix green **per feature build** through the harness (including + the unset-field rows — feature on, handle unset). +- **ADR-006** — route-1 bridge (accept only after the fabric open + succeeds; `ConnectFailed` on failure), exec-only git (no subsystem + dispatch anywhere in the grammar), parse-only dispatch still + structural (no request-less serving), spawn-and-return over the + **real** serving paths (the wave-2 constraint re-pinned over + mappings, not stubs). +- **ADR-003** — the per-build matrices; honest advertisement + unchanged (an unfeatured type declines; the sftp feature still + gates nothing, its rows still decline in every build — OQ-13); + x11/auth-agent never-feature rows untouched. +- **Auth.md / identity** — the identity ride observable at every seam + (alktty/alkgit params; the channels connection for tunnels), + `set_identity` once-only mechanics recorded; the parked wave-2 + `auth_succeeded` item verified discharged; no username anywhere. +- **OQ resolutions in code** — OQ-08 (unset-declines on shell/wrapper + rows), OQ-09 (env declined), OQ-10 (derivation rules + the + PTY-path fidelity limit documented), OQ-11 (runtime-Unix gates + compile-everywhere + clean off-Unix declines), OQ-13 (sftp rows + unchanged). +- **Bounded resources** — no new unbounded shape: the cap across the + fabric window (release-on-failure pinned), the listener bookkeeping + bounded, the control-row surfaces bounded, the bridge adapters + bounded. +- **Tracing** — the uniform event vocabulary across all landed + serve/decline paths; the safety posture held (no secrets, no + username). +- **The smokes** — the three real-tool postures recorded honestly + (required/conditionally-skipped with reason); nothing silently + green. +- **Wave-2 gate's booked items** — every one discharged or explicitly + re-booked (decline tracing = landed; acceptor vocabulary = landed + in `w3-tunnel-listen`; identity landing = discharged; the + inherited-auth-agent-open watch item = unchanged or re-read at any + pin touch). +- **Docs honesty** — module docs name what each replaced stub now + is, what remains wave-4/5 material, and any new upstream notes + (portable-pty's PTY-path exit shape; the `take_call_connection` + semantics reading). + +Verdict: green (wave 4 decomposes, absorbing corrections) or findings +booked (remediation before wave 4 decomposes). + +## Acceptance Criteria + +- [ ] Every ADR/mapping surface above read and confirmed (or + corrected with the correction landed) +- [ ] The decline matrix verified per build through the harness; + the derivation contract verified against its rules; the + spawn-and-return constraint re-pinned over real serving paths +- [ ] Full verification set green (`cargo test`, `--all-features`, + clippy ×2 `-D warnings`, fmt, `cargo doc --no-deps`) +- [ ] `taskgraph validate` clean +- [ ] Verdict recorded; wave 4 decomposition unblocked or findings + booked + +## References + +- docs/plans/implementation.md (wave 3 section + this gate's role) +- docs/architecture/mapping.md, auth.md, client.md (the mirror), + overview.md, open-questions.md, ADRs 002/003/004/005/006 +- docs/research/poc-3-mapping-bakeoff.md (stages A/C/D — the evidence + the contract tests re-land) + +## Notes + +> To be filled at gate time. \ No newline at end of file diff --git a/tasks/implementation/w3-decline-tracing.md b/tasks/implementation/w3-decline-tracing.md new file mode 100644 index 0000000..a891b59 --- /dev/null +++ b/tasks/implementation/w3-decline-tracing.md @@ -0,0 +1,73 @@ +--- +id: w3-decline-tracing +name: Wave 3 — decline-path tracing (the wave-2 gate's booked finding) +status: pending +depends_on: [w3-seam-config] +scope: narrow +risk: low +impact: component +level: implementation +--- + +## Description + +Land the wave-2 review gate's booked finding: observability on the +decline paths. `tracing` rides the manifest per the ADR-002 toolkit but +so far nothing in `src/` emits — a request-level decline's "reason" is +today only the routing decision documented per build. The real serving +paths land in this wave's mapping tasks, where serve/decline tracing +matters — so the event **vocabulary** lands first and the mappings +inherit it. + +- **The family-convention check first** — before choosing the event + shape, read the family's established tracing posture (alktty / + alkgit / alktunnels / alkcall `src/`: levels, event/field naming, + what gets emitted on error and decline paths) and adopt it — no + alkssh-private dialect; record what was read and chosen. +- **The event vocabulary** — the deciding context on every decline: + row + feature build + channel id (+ the decline class — open-level + vs request-level vs serving-path, and where applicable the failure + shape carried). The mapping tasks then emit at their sites. +- **Instrument what exists now** — the open-level declines + (unfeatured / never-feature / cap-exceeded), the request-level + capability declines (`subsystem`/`env`/`x11`/`agent`, the + unfeatured-parse routes), the seam-stub serving declines, and the + serve-path skeleton (spawned path started / ended, failure) — the + stubs' events carry into the mapping tasks' replaced paths. +- **Safety posture** (conventions 6, OQ-03): no secret material ever + enters an event, never the username; identity/fingerprint exposure + on decline events is decided here against the family posture and + recorded (auth.md: log exposure is deployment policy — the crate's + events must be safe-by-default at every level). + +Small, cross-cutting unit — one pass over the landed handler/dispatch +sites with the vocabulary, so the three mapping branches never land +inconsistent events. + +## Acceptance Criteria + +- [ ] The family tracing posture read and recorded; the event + vocabulary established (row + build + channel + decline class) + and used at every decline site in `src/` +- [ ] Serve-path skeleton events in place (start/ended/failure) so the + mapping tasks' spawned paths inherit shaped rows +- [ ] The safety posture recorded (no secrets, no username; what + identity detail may appear decided) +- [ ] A test pins the event presence at the decline sites (or the + doc-test convention the family uses — decide + record) +- [ ] `cargo test`, `cargo clippy --all-targets -- -D warnings`, + `cargo fmt --check`, `cargo doc --no-deps` pass + +## References + +- docs/plans/implementation.md §Wave 2 (the booked finding, + review-wave-2 §Notes) +- docs/architecture/mapping.md §Wire decline reasons (the two decline + classes), §Decline matrix +- docs/architecture/auth.md §Dispositions (log exposure posture), + docs/architecture/decisions/002-russh-pin.md (the toolkit) +- sibling `src/` tracing usage — the convention read this task adopts + +## Notes + +> To be filled at implementation time. \ No newline at end of file diff --git a/tasks/implementation/w3-git-mapping.md b/tasks/implementation/w3-git-mapping.md new file mode 100644 index 0000000..47ceb1c --- /dev/null +++ b/tasks/implementation/w3-git-mapping.md @@ -0,0 +1,124 @@ +--- +id: w3-git-mapping +name: Wave 3 — the git mapping (exec grammar + alkgit dispatch) +status: pending +depends_on: [w3-decline-tracing] +scope: moderate +risk: medium +impact: phase +level: implementation +--- + +## Description + +Land the git branch of the spine (ADR-006 decision 2, exec-only) — the +alkforge primary purpose: the full exec grammar replaces the wave-2 +skeleton probe, and the git row becomes a real dispatch to the +published alkgit seam. Everything POC-3 stage A proved, re-landed as +contract. + +- **The full grammar** (replaces `dispatch::classify_exec` — the + wave-2 interpretation's named replacement; its 7 probe unit rows + migrate): `git-{upload,receive}-pack ''` — quoted/unquoted + paths; strip the quotes and the leading `/` (POC-3 stage A rows: + what real OpenSSH git sends); unknown services + (`git-send-pack`, `git-upload-archive`) → decline-with-reason, the + honest surface; service-only (no repo) → decline-with-reason — + POC-3 §A: SSH exec must carry the repo; per-length limits consumed + at last — `Limits::max_exec_command_len` enforced **before** any + parse or allocation (decline-first, alloc-never; the wave-2 + carried-not-enforced reservation). Decide the wire shapes per + decline class (open/reply vocabulary, whether "unknown service" and + "not git at all" share a reason shape) and record. +- **Repo path → repo_id, no existence oracle**: the parse validates + via `alkgit::validate_repo_id` (1–2 segments, charset, ≤ 100 bytes) + and hands the id through — alkssh never resolves; the + **collapsed generic denial before any protocol byte** is + `serve_duplex_resolved`'s `SubstrateError::AccessDenied` posture, + contract-pinned here: over the SSH channel, no pack/ref byte + precedes the denial band (the POC-3 §A.5 byte-order evidence, now a + test row). Decide the `.git`-suffix normalization against the + alkgit registry contract (what vanilla git sends vs + `validate_repo_id`'s grammar) and record. +- **The dispatch** — the git row becomes real: the taken channel + (the store-and-take handoff lands **here first** — the accepted + channel is stored at the open, `channels.remove(&id)` → + `into_stream()` in the spawned serving task; the session task + reuses the store) → `alkgit::serve_duplex_resolved(repo_id, + identity, service, duplex, limits, registry, refs, packgen, + ingest, push_options_negotiated, pipeline)` — the serve-config's + `git` seam handle (`w3-seam-config`), the resolved identity riding + the `identity` param. Process-mode dialect: alkgit speaks first + (the V0 advertisement, no service prefix) — nothing alkssh-side; + verify the dialect holds over the channel duplex with a byte-level + row. +- **Exit-status both directions** (POC-3 §A.5): clean serve end → + `exit-status(0)`... decide the code mapping (`SubstrateError` + outcomes → the code — POC evidence: denied → the client observed + `ExitStatus{1}`; vanilla git exits 0 on success) — sent before + close, from the spawned task via the cloned `Session::handle()` + (the wave-2 mechanics reused). Record the decision. +- **Identity** — the parked wave-2 deferred item discharges here: + the resolved identity (the bridge's `auth_succeeded` ride-along) + reaches the seam — assert it live (the authorize-path/witness row + the harness can observe; decide the cheapest row) so the + visible-surface invariant is observable through the git row. +- **Contract rows through the harness, per build** — the + `tests/dispatch_core.rs` skeleton rows upgraded: git-only serves + the git row through the doubles (clone/push semantics over the + in-mem composition), declines plain exec/pty/shell (parse-then-fail, + no resource alloc); session-only still declines git-parseable exec; + none-build still declines at open; under `--all-features` the + routing holds. The declined rows keep their wire shapes + (`channel_failure`, channel usable / serving-path failure+close per + class). +- **The real-tool smoke** — one `git clone` (2.43+) through the + harness against hand-rolled in-mem alkgit doubles (alkgit's `Mem*` + are test-private — alkssh hand-rolls the minimal quartet per + `alkgit/src/backend.rs:164-254`'s noop shapes; never sibling-checkout + paths, never copies of the doubles' code). Decide the tooling + posture (require git in the environment vs conditional-skip — + skipped-with-reason at most, never silently green) and record. + +## Acceptance Criteria + +- [ ] The full grammar replaces the probe (quoted/unquoted, unknown- + service vs non-git decline rows, service-only decline, the + `max_exec_command_len` pre-parse cap); the probe's unit rows + migrate/extend +- [ ] The git row dispatches to `serve_duplex_resolved` over the + channel duplex; process-mode dialect verified byte-level; + declined routes and the `AccessDenied`-before-protocol-bytes + order pinned by contract rows +- [ ] The store-and-take handoff lands (channel stored at open, taken + in the serving task), structured so the session task reuses it +- [ ] The exit-status mapping (clean end / terminal error) decided + + recorded; status before close from the spawned task +- [ ] The identity ride asserted live through the seam (the parked + wave-2 item discharged here) +- [ ] The decline-matrix rows per build green through the harness + (git-only / session-only / none / all-features), each with its + contract stamp; the real-tool clone smoke row +- [ ] `cargo test`, `cargo test --features git --test `, + `cargo test --all-features`, clippy ×2 (`-D warnings`), fmt, + `cargo doc --no-deps` pass + +## References + +- docs/architecture/mapping.md (the git channel-type row; §Session- + channel dispatch — decline-first-alloc-never; §the structural + constraint) +- docs/architecture/decisions/006-mapping-bridge-and-dispatch.md + (decision 2 — exec-only; decision 4 — spawn-and-return) +- docs/architecture/decisions/004-auth-dispositions.md (the identity + ride), docs/architecture/auth.md §Server direction (step 4) +- docs/research/poc-3-mapping-bakeoff.md §Stage A (the grammar rows, + the denial byte-order, the exit evidence, the spawn rule) +- docs/research/mapping-table.md (the validated research ancestor) +- alkgit 0.1.0: `serve_duplex_resolved` (`src/duplex.rs:1412`), + `SubstrateError` (`src/substrate.rs:116`), `validate_repo_id` + (`src/registry.rs:132`), the trait shapes (`src/backend.rs:164-254`) + +## Notes + +> To be filled at implementation time. \ No newline at end of file diff --git a/tasks/implementation/w3-seam-config.md b/tasks/implementation/w3-seam-config.md new file mode 100644 index 0000000..e1e929a --- /dev/null +++ b/tasks/implementation/w3-seam-config.md @@ -0,0 +1,96 @@ +--- +id: w3-seam-config +name: Wave 3 — serve-config seam handles (alktty/alkgit per-feature) +status: pending +depends_on: [review-wave-2, w1-config] +scope: narrow +risk: low +impact: component +level: implementation +--- + +## Description + +Extend [`crate::ServeConfig`] with the per-feature family-seam handles +the wave-3 mappings consume. This is wave 3's first *course +correction* to the wave-1 config surface — the assembly-supplied +composition inputs were never carried (mapping.md's seam-entries table +names them inside the seam signatures; the wave-1 config bullet +pre-dates that reading), and the mappings cannot call +`drive_session[_pre_negotiated]` / `serve_duplex_resolved` without +them. Landing them here, before the mapping tasks, keeps the +un-set-declines posture structural and the review surface unified: + +- **The `session` seam handle** (feature-gated), unset → the session + rows decline even with the feature on (the decline matrix's "an + unset field declines its row though the feature is on" note, + config-driven). Contents, per alktty 0.8.1's actual + `drive_session[_pre_negotiated]` signature — decide the exact final + shape (flat `Option` fields vs an `Option`-struct per seam; record): + - the alktty backends map (`Arc>>` — assembled by the assembly; alkssh never + touches a real backend, the `local` shapes stay alktty-side); + - the backend **selector** — the `NegotiateRequest.backend` string + alkssh writes (assembly-supplied, never wire-derived; decide and + record); + - the ownership provider `Option>` (an alkcall type — + no new dep; pass-through to `drive_session_pre_negotiated`, + family ACL layer riding along). +- **The `git` seam handle** (feature-gated), unset → the git rows + decline even with the feature on. Contents, per alkgit 0.1.0's + `serve_duplex_resolved` signature: the four backend traits + (`GitRegistry` / `GitRefs` / `GitPackGen` / `GitPackIngest`), the + caller-owned server-wide `pipeline: Arc`, + and `push_options_negotiated: bool` (the assembler's config flag; + false in the v1 default). +- **Manifest** (the wave-1 rule: per-feature sibling deps land with + their first consuming wave — the config fields consume them first, + so they are not dead entries): optional deps + `alktty = "0.8.1"`, `alkgit = "0.1.0"` wired feature-to-dep + (`session = ["dep:alktty"]`, `git = ["dep:alkgit"]`) so a git-build + never compiles alktty and vice versa; verify the pins against + crates.io before committing (verified at decomposition: 0.8.1 / + 0.1.0, both over the shared `alkcall 0.10` req → 0.10.1; MSRVs 1.88 + ≤ our 1.89; MIT OR Apache-2.0). **alktunnels is NOT landed here** — + the config carries no alktunnels types beyond the already-landed + `Arc`; its optional dep lands with its first + consuming code (`w3-tunnel-direct`). + +No construction-time validation (the unset-declines contract lives in +the type shape, checked at request time by the mappings); the tunnel +handle stays `Option>` as landed; `Debug` impls and +the config module docs updated (the direction list now states what +each seam handle carries). + +## Acceptance Criteria + +- [ ] `ServeConfig` gains the feature-gated seam handles (the final + shape decided and recorded); unset → the owned rows decline, + structurally carried, no construction-time validation +- [ ] The optional deps + `dep:` feature wiring land with crates.io + pins verified; a git-build never compiles alktty and vice versa + (no code path outside the cfg gate touches the types) +- [ ] The none-build's `ServeConfig::new` shape is unchanged; + feature-gated fields absent from featureless builds +- [ ] Tests: the per-feature construction shapes, the + unset-declines-preserved rows, the `Debug` posture +- [ ] `cargo test`, `cargo clippy --all-targets -- -D warnings`, + `cargo clippy --all-features --all-targets -- -D warnings`, + `cargo fmt --check`, `cargo doc --no-deps` pass + +## References + +- docs/plans/implementation.md §Wave 1 (config bullet), §Wave 3 +- docs/architecture/mapping.md §The seams (the signature shapes), + §Session-channel dispatch (the unset note), §Decline matrix +- docs/architecture/decisions/002-russh-pin.md (the manifest rule), + docs/architecture/decisions/003-feature-graph.md (the feature graph) +- alktty 0.8.1: `src/adapter.rs:205-241` (`drive_session[ + _pre_negotiated]`), `src/negotiation.rs:64-92` (`NegotiateRequest`) +- alkgit 0.1.0: `src/duplex.rs:1412-1426` (`serve_duplex_resolved`), + `src/backend.rs:31-162` (the four traits) + +## Notes + +> To be filled at implementation time. \ No newline at end of file diff --git a/tasks/implementation/w3-tty-session.md b/tasks/implementation/w3-tty-session.md new file mode 100644 index 0000000..f4b61b6 --- /dev/null +++ b/tasks/implementation/w3-tty-session.md @@ -0,0 +1,143 @@ +--- +id: w3-tty-session +name: Wave 3 — session → alktty (translation bridge + exit derivation) +status: pending +depends_on: [w3-git-mapping] +scope: moderate +risk: medium +impact: phase +level: implementation +--- + +## Description + +Land the session branch — the biggest single-row surface of the spine +(mapping.md's `session` rows): pty/shell/pipe-exec become a real +serving path through the published alktty seam, as a **translation +bridge, not a byte pump** (POC-3 §D.1). Everything POC-3 stage D +proved, re-landed as contract: + +- **The per-channel state + the store-and-take reuse** — the channel + store `w3-git-mapping` lands (`channels.remove(&id)` → + `into_stream()` in the spawned serving task) gains the session + rows' per-channel state: the PTY params a `pty_request` delivered + (consumed by the later `shell_request`/`exec_request` on the same + channel — `ssh -t host "cmd"` sends pty-then-exec and must serve + PTY-mode; a `shell_request` without a preceding pty serves + **pipe-mode** `tty = None` — decide whether such a shell serves or + declines and record). +- **Negotiation, built locally, never wire-derived** (OQ-08 — the + serve-config is the whole truth): `pty_request` args → + `TerminalParamsWire{term, cols, rows, pixel_w/h, modes}` (SSH + ttymodes → the reserved modes JSON — alktty OQ-44's reservation, + deferred there); `shell_request` → `NegotiateRequest{carriage: + "raw", cmd: shell_argv (the serve-config shell, verbatim), cwd: + session_cwd, env: session_env, backend: the seam selector}` → + **`drive_session_pre_negotiated(writer, reader, req, backends, + ownership, identity)`** spawned; `exec_request` (pipe row) → `cmd = + wrapper ++ [cmd]` (the OQ-08 argv append — vanilla quoting + semantics; **never naively split**), `tty = None` pipe mode. + **Unset-declines** (OQ-08): `shell_argv` unset → `shell_request` + declines; `exec_wrapper_argv` unset → the pipe row declines — never + a fallback. Decide the wire shape per row (capability decline — + channel stays usable — vs the serving-path failure+close shape) and + record. +- **The translation bridge** — two small adapters around a duplex the + serving task drives (the POC-3 §D.1 shape): SSH `data`/EOF → + `Chunk::stdin` (decide the EOF translation — the zero-length + stdin-chunk sentinel vs alktty's `ControlMessage::Eof` CTRL_IN — + verify what alktty's ChunkReader/backend actually consumes and + record); stdout chunks → SSH `data`; stderr chunks → + `extended_data(1)` (PTY mode merges upstream — the backend's + stderr stream is `None` there, so the translation is universal, no + mode-switch); the exit chunk (`STREAM_CTRL_OUT`) → the derivation + contract. No unbounded buffering in either adapter (convention 9). +- **Control rows** — `window_change_request` → `ControlMessage:: + Resize` CTRL_IN; `signal` → `ControlMessage::Signal` (alktty's + uppercase-name vocabulary; name accepted verbatim — decide the + decline shape for names alktty's backend semantics reject and + record). Mechanics: the requests arrive on the packet loop after + the serving task owns the wire — reach them into the spawned bridge + through a per-channel control surface (bounded mpsc — decide + + record; a silent unbounded proxy is a bounded-resources bug). +- **The exit derivation contract** (mapping.md rules 1–6, + normative): the alktty exit chunk (`{"type":"exit","code":i32}`, + exit-chunk-is-last) is the sole fidelity input, never widened: + `code ≥ 0` → `exit-status(N)`; negative `−N`, N ≥ 2 → + `exit-signal(name)` via the **server-side Unix signum→name table** + (crate-local; not derived from alktty's `signal_from_name` inverse + unless that is public and sound — decide the table's form and + record; `core_dumped` **never asserted**, the bool stays false); + `−1` → `exit-status(255)`, never a signal claim (rule 3 — the + SIGHUP collision accepted symmetrically, per the mapping.md + rationale); non-Unix adds no other negatives (rule 4); the PTY-path + rule-5 fidelity limit documented in the module docs (signal- + terminated PTY sessions arrive `exit-status(1)` — portable-pty's + exit surface, upstream); unmapped negatives → 255 (rule 6). Sent + **before** close, from the spawned task. +- **`env_request` stays declined** (OQ-09 — unchanged from wave 2); + `session_cwd`/`session_env` ride `NegotiateRequest` only. +- **Contract rows through the harness, per build** — the wave-2 stub + rows (`pty_row_stub`/`shell_row_stub`/ + `pipe_row_stub`) replaced; the decline matrix's unset-field rows — + feature on + handle unset → the rows decline; featured+set serve + through the alkssh-side **TtyBackend shim double** (alktty's + `MockBackend`/`testing.rs` are `pub(crate)` + `cfg(test)` — never + importable; alkssh hand-rolls the shim: a backend that records + `TtyParams`, emits scripted stdout/stderr/exit chunks and consumes + stdin/resizes/signals — per the `TtyHandle` field shapes). The + identity-landing item (the parked wave-2 note) asserted through the + row that observes the identity at `drive_session_pre_negotiated`. +- **The real-tool smoke** — one OpenSSH pty session through the + harness against the shim (or against a real PTY via a dev-dep + `alktty` `local` composition — decide which posture and record; the + real bash/`stty size` fidelity was POC stage D's, wave 5 re-lands it + as e2e): the smoke owns the wire rows a plain rustsh-client row + can't pin (echo paths, `stty size`-grade fidelity are wave-5's + matrix; here the pty lifecycle rows only). + +## Acceptance Criteria + +- [ ] 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 + 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 + 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 + 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- + 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 + (`-D warnings`), fmt, `cargo doc --no-deps` pass + +## References + +- docs/architecture/mapping.md (the `session` channel-type row, + request-level rows, §Exit chunk → SSH exit reporting — rules 1–6) +- docs/architecture/decisions/006-mapping-bridge-and-dispatch.md + (decision 4 — spawn-and-return: status before close from the spawned + task), docs/architecture/decisions/003-feature-graph.md +- docs/architecture/auth.md §Server direction step 4 (the identity + ride), §Dispositions (OQ-03) +- docs/research/poc-3-mapping-bakeoff.md §Stage D (the bridge shape, + control rows, the signal/exit fidelity findings, §D.3 mechanics) +- docs/research/reverse-runner.md (pipe-mode pid-only signal caveat — + the fidelity note) +- alktty 0.8.1: `adapter.rs:234-241` (`drive_session_pre_negotiated`), + `negotiation.rs` (`NegotiateRequest`/`TerminalParamsWire`), + `wire.rs` (chunks, readers/writers), `control.rs` (`ControlMessage`), + `backend.rs` (`TtyHandle`/`TtyParams`/`TtyBackend` shapes) + +## Notes + +> To be filled at implementation time. \ No newline at end of file diff --git a/tasks/implementation/w3-tunnel-direct.md b/tasks/implementation/w3-tunnel-direct.md new file mode 100644 index 0000000..05ad7be --- /dev/null +++ b/tasks/implementation/w3-tunnel-direct.md @@ -0,0 +1,125 @@ +--- +id: w3-tunnel-direct +name: Wave 3 — tunnel leg 1 (the route-1 bridge — direct-tcpip + streamlocal) +status: pending +depends_on: [w3-tty-session] +scope: moderate +risk: medium +impact: phase +level: implementation +--- + +## Description + +Land the tunnel branch's proven leg (ADR-006 decision 1) — the +adapter-side route-1 bridge for `direct-tcpip` and `direct-streamlocal` +(POC-3 stage C's shape, re-landed as contract): + +- **The route-1 bridge** — the `tunnel_row_stub` replaced with the + real flow: per accepted open, the target/substrate are constructed + from the SSH open arguments (`direct-tcpip`(host, port) → + `SubstrateAddr::HostPort` + `Substrate::Tcp`; `direct-streamlocal` + (socket path) → `SubstrateAddr::Path` + `Substrate::Unix`) and + spawn the flow POC-3 proved: **`TunnelSession::open_direct(& + channel_client, target, substrate)`** → **accept only after the + fabric open succeeds** (accept consumes the handle — the reject + shape is only possible pre-accept) → on fabric failure the explicit + `ConnectFailed` reject (the stub's reason survives as the real + decline; `tunnel_seam_stub_decline` retires into it) → on success + `take_halves()` pumped against the taken channel's + stream (`pump_bidi` — the alkcall primitive the POC recipe used) on + the spawned task, per spawn-and-return. The open hook returns fast + with the fabric open in flight. +- **The cap admission across the fabric window** — the wave-2 stub + declined without consuming a count; the real flow admits. Mechanics + to get right (record): the admission is taken at the hook (the + packet-loop single-writer site — no race window against the cap + check) and held across the fabric open; **a fabric failure must + release the count inside the spawned task** (the + `channel_close`-hook release point only fires for established + channels — a rejected open never established one), so a failed + bridge cannot leak budget. Decide the release mechanism and + record. +- **Identity propagation to the family fabric (auth.md §Server + direction step 4 — the visible-surface invariant's gate)**: the + resolved identity rides the channels connection so the fabric's + `AccessControl::check(peer_identity)` gates the mapped opens. The + parked wave-2 `auth_succeeded` item lands here: on auth completion + the handler propagates **once** via alkcall's `Connection:: + set_identity` — mechanics decided and recorded (the alkhub + `hub_leg.rs` one-time-set precedent: `ChannelClient:: + take_call_connection()` → `set_identity(identity)`; check the + `take` semantics against alkcall 0.10.1's docs — the relay note + says the detach is wrong for hubs; verify a door's reading and + hook placement, `auth_succeeded` vs the first seam call). +- **The runtime-Unix gate** (OQ-11 — universal compile, clean + open-decline off-Unix): `direct-streamlocal` compiles wherever + `tunnel` compiles; off-Unix a clean open-decline **before any + fabric call** (never a partial bridge); on-Unix the real bridge. +- **Contract rows through the harness, per build** (the wave-2 gate's + booked acceptor vocabulary note is the listen leg's — this task's + rows ride plain client opens): a git/session build with `tunnel` + off declines the tunnel opens (unfeatured, `AdministrativelyProhibited`); + the featured rows: a configured `tunnel` handle (feature + `Arc< + ChannelClient>`) serves, an **absent handle declined with a clean + error even under the feature** (the unchanged unset-declines + posture — the wave-1 config note), the cap rows (cap-gated before + spawn — never a leaked count over a failed bridge: the release + mechanism pinning row), and the `ConnectFailed` row over a refusing + fabric double. The **fabric double** is the POC-3 §C.4 in-process + recipe as test composition (ChannelsAdapter + InstallChannelZero + + the alktunnels openables + a hand-rolled dial/dial-fail seam) — + hand-rolled in `tests/`, never a sibling-checkout path. +- **The optional dep lands here** (`alktunnels = "0.4.1"`, wired + `tunnel = ["dep:alktunnels"]` — first consuming code; crates.io pin + re-verified before committing). +- **The real-tool smoke** — one `ssh -L` (+ curl) through the + harness against the fabric double (tooling posture decided like the + git task — required vs conditional-skip, recorded). + +## Acceptance Criteria + +- [ ] `direct-tcpip` serves through the route-1 bridge: fabric open → + accept → `take_halves()` pump on the spawned task; failure → the + explicit `ConnectFailed` reject, the stub retired; the open + hook returns fast +- [ ] The cap admission across the fabric window pinned (admit at the + hook, release on fabric failure inside the spawned task — no + leaked count, no double-admit) +- [ ] The identity propagates to the channels connection once + (`set_identity` mechanics + hook placement decided and + recorded); the wave-2 `auth_succeeded` item discharged +- [ ] `direct-streamlocal` carries the runtime-Unix gate + (compile-everywhere, clean open-decline off-Unix before any + fabric call) +- [ ] The per-build decline-matrix rows green through the harness + (unfeatured/unset/fabric-failure/cap/serve classes), each with + its contract stamp; the `ssh -L` smoke row +- [ ] The `alktunnels` optional dep lands, pin re-verified; + `tunnel`-off builds carry no alktunnels types in code paths +- [ ] `cargo test` (+ per-feature + `--all-features` rows), clippy ×2 + (`-D warnings`), fmt, `cargo doc --no-deps` pass + +## References + +- docs/architecture/mapping.md (the `direct-tcpip` / `direct- + streamlocal` channel-type rows; §the structural constraint) +- docs/architecture/decisions/006-mapping-bridge-and-dispatch.md + (decision 1 — route 1; the reject shape; the wiring recipe pointer) +- docs/architecture/decisions/003-feature-graph.md (the tunnel row, + the OQ-11 runtime-Unix gate) +- docs/architecture/auth.md §Server direction step 4 (set_identity, + once), §Open questions (all resolved) +- docs/research/poc-3-mapping-bakeoff.md §Stage C (the bridge + the + failure shapes + §C.4 the fabric recipe + the identity witness) +- alktunnels 0.4.1: `consumer.rs:89-148` (`TunnelSession::open`, + `open_direct`), `consumer.rs:235-260` (`take_halves`), + `params.rs:105-126` (`SubstrateAddr`/`TunnelDirectParams`), + `error.rs` (`TunnelOpenError`) +- alkcall 0.10.1: `channels/client.rs` (`take_call_connection`), + `core/types.rs:541` (`Connection::set_identity`), + `channels/hub_leg.rs:217-220` (the one-time-set precedent) + +## Notes + +> To be filled at implementation time. \ No newline at end of file diff --git a/tasks/implementation/w3-tunnel-listen.md b/tasks/implementation/w3-tunnel-listen.md new file mode 100644 index 0000000..1198130 --- /dev/null +++ b/tasks/implementation/w3-tunnel-listen.md @@ -0,0 +1,135 @@ +--- +id: w3-tunnel-listen +name: Wave 3 — tunnel leg 2 (the -R listen flow + the forwarded-tcpip data leg) +status: pending +depends_on: [w3-tunnel-direct] +scope: moderate +risk: high +impact: phase +level: implementation +--- + +## Description + +Land the `-R` leg's server direction (mapping.md §Global-request rows + +the `forwarded-tcpip` channel-type row) — the one leg the POC only +exercised as refusal ("only the refusal shape was exercised"), so this +task carries the wave-3 gate's biggest open mechanics. Shape per the +family door semantics (vanilla -R: the door's fabric listens, per +arrival the data flows back to the vanilla client, which dials its own +target): + +- **The listen request rows** — `tcpip_forward` / `streamlocal_forward` + (and their cancel forms) go from wave-2's unconditional decline to + the real flow under `tunnel` (+ the streamlocal runtime-Unix gate + riding its serving flow, OQ-11): the listen is mapped **family-ward** + through the alktunnels listen/forwarded shapes — the door owns + nothing local. The **exact composition is decided here** against + alktunnels 0.4.1's actual producer/consumer surfaces (`TunnelListener:: + open`'s listen dial + reply fields; the minted listen token in + `TunnelForwardedParams`; the bind-first accept dispatcher's + `ForwardedOpener` / `make_bindfirst_accept_handler`; + `register_tunnel_forwarded_openable`'s `ForwardedHandler`) with + POC-3 §C.4 as the template. Decide and record: who dials the + `tunnel/forwarded` op at the door, and whether the + assembly-supplied `Arc` composition can carry the + door-side acceptance of the fabric's inbound forwarded opens + (alkcall channel-manager openable registration on the door-side + connection) or the serve-config's tunnel handle must grow a + registration-seam handle — **if the door-side handle shape must be + amended, amend it here** (the `w3-seam-config` shape, recorded as + this wave's second config course-correction). If the topology does + not close over the published alkcall 0.10.1 / alktunnels 0.4.1 + surfaces, **stop and book it** for the gate — never silently + forked, worked around in-family, or re-litigating ADR-006's + resolution. +- **The `forwarded-tcpip` data leg** (mapping.md's row — "hand the + channel duplex to the handler"): per fabric arrival, the door opens + a `forwarded-tcpip` channel **toward the vanilla client** with the + SSH message's fields mapped per the vanilla -R convention + (`host_to_connect`/`port` echoing the target args the original + `tcpip_forward` request carried — recorded per pending listen; + originator fields carrying the arrival info — decide and record + what `TunnelForwardedParams.peer` yields) → accept-or-reject per + the in-crate cap (the **acceptor vocabulary** the wave-2 gate + booked — the first shape of what wave 4 grows into the client-side + acceptor registry) → pump the fabric `BiStream` ↔ the SSH channel + (`ForwardedHandler` ownership: the handler owns the pump per + alktunnels' shape; correlation-miss → teardown-at-birth, never a + half-open SSH channel). +- **Cancel forms + bounded bookkeeping** — a per-session listener map + (the bound-port handles) drives `cancel_tcpip_forward` / + `cancel_streamlocal_forward` 1:1 into the family listener's + teardown (`TunnelListener::close` posture); the listener count is a + bounded resource (decide the bound relation to + `Limits::max_concurrent_channels` or a dedicated cap — record; no + unbounded map). The bound-port out-param: `port = 0` → the + actually-bound number returned through the `&mut u32` (the + family-side listen reply's numbers; the mapping.md/client.md + lifecycle spec). +- **Identity**: the listen dial rides the propagated identity (the + fabric's `AccessControl` gates the listen + forwarded opens the + same as leg 1's — the visible-surface invariant across the whole + listen flow). +- **Contract rows through the harness, per build** — the wave-2 stub + rows replaced: featured + configured serve (the listen flow against + the fabric double); unfeatured/unset/streamlocal-off-Unix decline + cleanly (honest advertisement: a declined listen never left a half + state); the cancel rows; the arrival → forwarded-tcpip → pump row; + cap rows at the accept point. The **harness grows the client-side + accept hook** (russh client + `server_channel_open_forwarded_tcpip` — the server-initiated opens + the contract rows observe; the wave-2 note said exactly this). +- **The real-tool smoke** posture decided + recorded (a real `ssh -R` + through the harness's TCP loopback if tooling permits — else the + harness rows carry wave 3 and the real-tool `-R` smoke is named + wave-5 e2e territory explicitly; never silently dropped). + +## Acceptance Criteria + +- [ ] `tcpip_forward` / `streamlocal_forward` (+ cancel forms) carry + the real mapped flow under `tunnel` (+ the runtime-Unix gate + where it binds); decline classes keep their wire shapes +- [ ] The family-ward listen composition decided against alktunnels + 0.4.1's actual surfaces and recorded; the door-side inbound-open + mechanics closed (or a serve-config amendment landed + recorded; + or stopped-and-booked for the gate — nothing silent) +- [ ] The `forwarded-tcpip` data leg serves per the handler shape + (target-arg echo per pending listen, cap-gated acceptor, the + pump, teardown-at-birth on correlation-miss) +- [ ] The per-session listener bookkeeping bounded; the bound-port + out-param returns the actually-bound number +- [ ] The per-build decline-matrix rows green through the harness with + the client-side accept hook in place (the wave-2 gate's booked + harness item discharged) +- [ ] The real-tool smoke posture decided + recorded; the wave-2 stub + listen rows migrated +- [ ] `cargo test` (+ per-feature + `--all-features` rows), clippy ×2 + (`-D warnings`), fmt, `cargo doc --no-deps` pass + +## References + +- docs/architecture/mapping.md §Channel-type rows (`forwarded-tcpip` + row), §Global-request rows (the `-R` listen flow, the POC-3 note) +- docs/architecture/client.md §Op rows (the `-R` leg, the acceptor + framing — the client mirror this leg shares vocabulary with), + §Lifecycle primitives (the bound-port spec) +- docs/architecture/decisions/006-mapping-bridge-and-dispatch.md + (decision 1 — the fabric wiring recipe), docs/architecture/ + decisions/003-feature-graph.md (the tunnel row + §4 the + client-side acceptor symmetry) +- docs/research/poc-3-mapping-bakeoff.md §Stage C (the recipe; + §C.2's refusal shape this task goes past), OQ seed 4 +- docs/research/reverse-runner.md (the `-R` row — the client-side + mirror of the same wire vocabulary) +- alktunnels 0.4.1: `producer.rs:718-772` (`ForwardedOpener`, + `make_bindfirst_accept_handler`), `producer.rs:982-1082` + (`ForwardedHandler`, `register_tunnel_forwarded_openable`), + `consumer.rs:497-543` (`TunnelListener::open` + reply accessors), + `params.rs:381-401` (`TunnelForwardedParams`) +- alkcall 0.10.1: the channels openable/accept surfaces (the + door-side registration question this task resolves) + +## Notes + +> To be filled at implementation time. \ No newline at end of file