docs(tasks): decompose wave 3 — the server mappings

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.
This commit is contained in:
glm-5.3-flash committed 2026-10-28 12:32:13 +00:00
1 parent 5b2654f492
commit d1f755f2d9
8 files changed
+849 -1

No files matched your search

+61 -1
View File
@@ -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<ChannelClient>` 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
+92
View File
@@ -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.
@@ -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.
+124
View File
@@ -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 '<path>'` — 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 <git rows>`,
`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.
+96
View File
@@ -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<HashMap<String,
Arc<dyn TtyBackend>>>` — 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<Arc<dyn
alkcall::core::ownership::OwnershipProvider>>` (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<tokio::sync::Semaphore>`,
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<ChannelClient>`; 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<Arc<ChannelClient>>` 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.
+143
View File
@@ -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.
+125
View File
@@ -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.
+135
View File
@@ -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<ChannelClient>` 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.