docs(tasks): decompose wave 2 — the server stack

Six implementation tasks + the gate under tasks/implementation/,
chained sequentially (the auth hooks, dispatch, and channel cap all
edit the same per-connection Handler impl):

- w2-server-mount: SshAdapter::handle mount + russh Config
  construction from the serve-config (honest advertisement, host-key
  decrypt-at-construction, the Limits knob mapping) + the Handler
  skeleton at fail-closed defaults
- w2-auth-bridge: the ADR-004 dispositions (ed25519-only explicit
  decline, fingerprint-only resolution, explicit declines for every
  non-publickey method)
- w2-loopback-harness: the in-process client/server contract-test
  vehicle over the wave-1 doubles
- w2-dispatch-decline-core: parse-only dispatch + the fail-closed
  decline core, contract-tested per feature build
- w2-spawn-return: the structural constraint pinned by test + the
  exit-reporting mechanics via the cloned Session::handle()
- w2-bounded-resources: the in-crate concurrent-channel cap at the
  handler surface
- review-wave-2: the gate

The plan carries the wave-2 decomposition record with four
interpretations flagged for the gate (skeleton-level git probe,
declining routing stubs, the adapter error surface, the harness
transport choice). taskgraph validate: 19 tasks, clean.
This commit is contained in:
glm-5.3-flash committed 2026-10-28 07:38:04 +00:00
1 parent a19cdc4832
commit 18ef55c60d
8 files changed
+612 -1

No files matched your search

+39 -1
View File
@@ -44,7 +44,7 @@ mappings ∥ client direction) → validation & release.
| Wave | Contents | Depends on | Status |
|---|---|---|---|
| [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 | pending |
| [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 |
| [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 |
@@ -203,6 +203,44 @@ frame, before any door is hung:
Wave 2 decomposes after the wave-1 gate, absorbing whatever it
corrects.
**Decomposition record** (2026-10-28, after the wave-1 gate closed
green): wave 2 decomposed into six task files under
`tasks/implementation/` plus the gate. Shape: `w2-server-mount` first
(the `SshAdapter` mount + russh `Config` construction from the
serve-config — the substrate, with the Handler skeletoned at its
fail-closed defaults); then `w2-auth-bridge` (the ADR-004
dispositions fill the auth hooks, over the mount + the wave-1
fingerprint path and doubles); then `w2-loopback-harness` (the
in-process client/server pair — the contract-test vehicle — proving
the doubles live through the stack); then `w2-dispatch-decline-core`
(parse-only dispatch + the fail-closed decline core, contract-tested
per feature build through the harness); then `w2-spawn-return` (the
structural constraint pinned by test + the exit-reporting mechanics
via the cloned `Session::handle()`); then `w2-bounded-resources` (the
in-crate channel cap at the handler surface); and `review-wave-2`
(the gate) depending on all six. The chain is deliberately sequential
— the auth hooks, the dispatch, and the cap all edit the same
per-connection Handler impl, so no two tasks hold it in parallel.
Interpretations recorded (flagged for the wave-2 gate):
- **The git-parseability probe in wave 2 is skeleton-level** (service
prefix + path shape) — enough to contract-test the routing decision
and the session-build's git-gated decline; the full grammar
(quoted/unquoted paths, service-only → decline-with-reason,
per-length limits) is wave-3's git task, which replaces the probe.
- **Wave-2 routing stubs decline cleanly at the family-seam boundary**
— the seams land in wave 3; the stub behavior is documented as
skeleton so the routing decisions are contract-tested now. The
spawn-and-return mechanics are proven on a stub serving path.
- **The adapter error surface is shaped in `w2-server-mount`** — the
construction error wrapping `KeySourceError` (locked → no server)
plus the Handler `type Error` (`From<russh::Error>`); this is where
the wave-1 note's "wire decline reasons are wave-2 shaped" lands.
- **The harness transport and placement are pinned in
`w2-loopback-harness`** (duplex pair vs TCP loopback; module
placement beside the wave-1 doubles) — flagged for the gate.
## Wave 3 — Server mappings
Three parallel branches over the stack (plus the folded sftp row),
+76
View File
@@ -0,0 +1,76 @@
---
id: review-wave-2
name: Wave 2 gate — the server-stack review
status: pending
depends_on: [w2-server-mount, w2-auth-bridge, w2-loopback-harness, w2-dispatch-decline-core, w2-spawn-return, w2-bounded-resources]
scope: narrow
risk: low
impact: phase
level: review
---
## Description
The wave-2 review gate. The server stack is the substrate every
mapping composes into (wave 3 hangs three mappings and the sftp row on
it; wave 4 shares its foundations) — a shape error in the mount
signature, the auth-bridge state, or the dispatch routing costs churn
across all of them. The gate reads the landed stack against the
architecture line by line:
- **ADR-001** — the mount contract: `SshAdapter::handle` takes a
generic stream, mounts the full russh stack per stream, edge
termination only; no ALPN/relay code exists; the per-stream limits
posture holds.
- **ADR-002** — the Config construction (honest advertisement
`{publickey}` exactly; the knob mapping; no `des`/`dsa` posture
drift; the pin untouched).
- **ADR-003** — the feature gates as the dispatch's first `cfg`
consumers: flat, independent, `default = []` declines everything;
x11/auth-agent never feature-gated; honest advertisement per build.
- **ADR-004** — the dispositions in code (explicit declines for every
non-publickey method; offered-hook accept-and-delegate; ed25519-only
explicit decline; fingerprint-only resolution; the identity rides
the session).
- **ADR-006** — parse-only dispatch (no detect residue); decline-first-
alloc-never structurally; **the spawn-and-return constraint pinned
by test, not convention**.
- **`mapping.md`** — the skeleton decline-matrix rows green per build
through the harness; the wire decline reasons vanilla-observable.
- **Bounded resources** — the cap enforced and recovering; the knobs
behaviorally verified or recorded; no unbounded shape anywhere.
- **The harness** — the contract-test vehicle convention established;
the wave-1 doubles exercised live.
- **Docs honesty** — module docs state what is deliberately not here
yet (the family seams, the client direction); the skeleton-stub
behavior (declining routes awaiting wave 3) documented as such.
- **The wave-1 gate's lesson absorbed** — `cargo doc --no-deps` runs
clean on this gate too.
Verdict: green (wave 3 decomposes, absorbing corrections) or findings
booked (remediation before wave 3 decomposes).
## Acceptance Criteria
- [ ] Every ADR/mapping surface above read and confirmed (or
corrected with the correction landed)
- [ ] The spawn-and-return constraint verified as test-pinned;
decline-first-alloc-never verified structurally
- [ ] Full verification set green (`cargo test`, `--all-features`,
clippy ×2, fmt, `cargo doc --no-deps`)
- [ ] Verdict recorded; wave 3 decomposition unblocked or findings
booked
## References
- docs/plans/implementation.md (wave 2 section + this gate's role)
- docs/architecture/decisions/001…006, mapping.md, auth.md,
overview.md, open-questions.md
## Notes
> To be filled by implementation agent
## Summary
> To be filled on completion
+80
View File
@@ -0,0 +1,80 @@
---
id: w2-auth-bridge
name: Wave 2 — the auth bridge (dispositional handler surface)
status: pending
depends_on: [w2-server-mount, w1-fingerprint, w1-test-doubles]
scope: moderate
risk: low
impact: component
level: implementation
---
## Description
Fill the Handler skeleton's auth hooks with the ADR-004 dispositions —
the fingerprint → identity bridge, fail-closed on every path:
- **`auth_publickey`** (fires post-verification; the key arrives as
`VerifiedPublicKey.public_key` with the signature `.algorithm`
exposed): non-ed25519 algorithm → **explicit reject** (ADR-002
decision 4 — the explicit-over-inherited pattern, so the posture
survives a pin bump); ed25519 → fingerprint via the crate's single
fingerprint path (`src/fingerprint.rs`, the ADR-004 contract
invariant) → `IdentityProvider::resolve_from_fingerprint` →
`Some(identity)` → `Auth::Accept` with the resolved identity stored
in the per-connection handler state (it rides the session for the
wave-3 seam propagation — `auth_succeeded` is the ride-along hook);
`None` → reject fail-closed. The resolved-identity-`None` path never
creates a weakly authorized session.
- **`auth_publickey_offered`** — accept-and-delegate (the russh
default shape at this pin; keep it explicit so the counting stays
honest — the hook does not consume an attempt, POC-1 verified).
- **Explicit declines** — `auth_none`, `auth_password`,
`auth_keyboard_interactive`, `auth_openssh_certificate` each get an
explicit `Auth::reject()` implementation (family contract, not
inherited defaults — a pin bump could silently change them).
- **The username carries no semantics** (OQ-03) — resolution is
fingerprint-only; the user string is never an identity input.
Auth declines are *dispositions* (`Auth::Reject`), not errors — the
error model stays key-source-only. Tests run against the wave-1
doubles (`MemIdentityProvider` + the ed25519 fixture keys) at the
handler level; the loopback harness proves the bridge end to end over
the wire in `w2-loopback-harness`.
## Acceptance Criteria
- [ ] `auth_publickey`: non-ed25519 → explicit reject; ed25519 →
fingerprint → `resolve_from_fingerprint` → accept-with-identity
/ reject fail-closed; the resolved identity lands in
per-connection handler state
- [ ] `auth_publickey_offered` accepts-and-delegates explicitly
- [ ] `auth_none` / `auth_password` / `auth_keyboard_interactive` /
`auth_openssh_certificate` each explicitly reject
- [ ] The username is never an identity input (fingerprint-only
resolution; OQ-03)
- [ ] Handler-level tests: known-fingerprint accept (fixture key +
`MemIdentityProvider`), unknown-fingerprint reject, non-ed25519
explicit reject, every non-publickey method rejects
- [ ] `cargo test`, `cargo clippy --all-targets -- -D warnings`,
`cargo fmt --check` pass
## References
- docs/architecture/decisions/004-auth-dispositions.md (the
dispositions, decisions 1–5)
- docs/architecture/auth.md §Server direction (the bridge steps; the
0.65.0 `VerifiedPublicKey` shape)
- docs/architecture/decisions/002-russh-pin.md (decision 4 —
ed25519-only, the explicit-decline rationale)
- docs/plans/implementation.md §Wave 2 (the auth-bridge bullet)
- russh 0.65.0 in-source: `Auth`, `VerifiedPublicKey`,
`PublicKeyOffer`, the auth hook signatures
## Notes
> To be filled by implementation agent
## Summary
> To be filled on completion
@@ -0,0 +1,68 @@
---
id: w2-bounded-resources
name: Wave 2 — bounded resources (the in-crate channel cap at the handler surface)
status: pending
depends_on: [w2-spawn-return]
scope: narrow
risk: low
impact: component
level: implementation
---
## Description
Enforce the in-crate bounded-resources posture at the handler surface
(`overview.md` §Bounded-resources policy; AGENTS.md convention 9 —
SSH servers are internet-facing; unbounded loops and unbounded buffers
are bugs):
- **The in-crate concurrent-channel cap** — a count of active channels
enforced at every `channel_open_*` hook: opens beyond
`Limits::max_concurrent_channels` are declined fail-closed (a clean
refuse, never queued, never a half-open channel). The count
decrements on channel close so the cap recovers. Short-held
internal state (`parking_lot`, the project convention).
- **The russh knobs are already wired** (`w2-server-mount` mapped
`inactivity_timeout` always-`Some`, `max_auth_attempts`,
`window_size`, `maximum_packet_size`, `channel_buffer_size` from
`Limits`) — this task verifies them behaviorally where cheap
(e.g., the auth-attempt ceiling disconnects an exceeding client
through the harness) and records anything not cheaply observable
as covered by the config-mapping tests.
- **Per-length exec-parse limits stay reserved** for the wave-3
grammar's consumption (`Limits::max_exec_command_len` is carried,
not yet enforced — the grammar owns it).
The per-identity policy layer (when the alkcall bridge carries it) is
additional and arrives with the bridge itself (wave 3's tunnel leg) —
not from this task.
## Acceptance Criteria
- [ ] The concurrent-channel cap is enforced at every
`channel_open_*` hook: beyond `Limits::max_concurrent_channels`
→ clean decline; the count recovers on close
- [ ] Harness tests: cap reached → next open declines; a close → an
open succeeds again; the cap is `Limits`-tunable
- [ ] The wired russh knobs verified behaviorally where cheap
(auth-attempt ceiling through the harness) or recorded as
covered by the config-mapping tests
- [ ] `cargo test`, `cargo clippy --all-targets -- -D warnings`,
`cargo fmt --check` pass
## References
- docs/architecture/overview.md §Bounded-resources policy (the knob
table)
- docs/architecture/mapping.md §Data-plane / limits
- docs/plans/implementation.md §Wave 2 (the bounded-resources bullet)
- src/config.rs (`Limits` — the budget model and its documented
postures)
## Notes
> To be filled by implementation agent
## Summary
> To be filled on completion
@@ -0,0 +1,100 @@
---
id: w2-dispatch-decline-core
name: Wave 2 — parse-only dispatch + the fail-closed decline core
status: pending
depends_on: [w2-loopback-harness]
scope: moderate
risk: medium
impact: phase
level: implementation
---
## Description
Land the dispatch skeleton and the fail-closed decline core (ADR-006
as amended, ADR-003) — the routing decisions and the declines; the
family-seam serving paths are wave-3 material and arrive as clean
declining stubs this task shapes the routes for.
- **Channel-open gates** (the `channel_open_*` hooks): `session`
channels open only where a session-serving feature exists
(`session` ∨ `git` — the parse path lives inside session channels);
`direct-tcpip` / `forwarded-tcpip` / `direct-streamlocal` gate on
the `tunnel` feature; **`x11` and `auth-agent` always decline**
(never feature-gated — ADR-003 §5); every other/unimplemented
channel type declines. Unfeatured → decline at open with a clean
wire reason — never a half-open channel.
- **Parse-only session dispatch** (ADR-006 point 3 as amended):
`exec_request` parses first — a git-parseable command routes to the
git row, anything else to the session pipe row; `pty_request` /
`shell_request` route to the session rows. **The git-parseability
probe here is skeleton-level** (service prefix + path shape); the
full grammar (quoted/unquoted paths, service-only →
decline-with-reason, per-length limits) is wave-3's git task, which
replaces this probe. Parsing happens before dispatch and before any
resource allocation — **decline first, alloc never** (a leaked
channel alloc is a capability leak; the POC-3 stage-A rule).
- **Request-level declines**: `subsystem_request` — the `sftp` name
declines-with-reason (the OQ-13 placeholder, folded here server
side); git service names decline-with-reason (a subsystem request
carries only a name, never a repo id); everything else declines.
`env_request` declines unconditionally (OQ-09 — no allowlist ships).
- **Routing stubs**: the git row and the session rows (pty/shell,
pipe-exec) route to stubs that decline cleanly at the family-seam
boundary — the seams land in wave 3; the stub shape is documented
as skeleton behavior so the routing decisions are contract-tested
now, not after the mappings exist.
- **Wire decline reasons are shaped here** (the wave-1 error-model
note's reservation): channel-open failure codes vs
`channel_failure` per decline class, chosen for vanilla-client
observability (POC-1 §5: channel-scoped decline, auth intact, no
hangs).
- **Contract tests through the loopback harness, per feature build**:
the skeleton rows of the decline matrix (`mapping.md` §Decline
matrix) — none-build declines everything; git-build routes
git-parseable exec (stub at the seam) and declines pty/shell and
plain exec; session-build declines git-parseable exec (the git
parse is its gate) and serves its rows only to the stub; tunnel
rows decline (the route-1 bridge is wave 3) even with the feature
on; x11/auth-agent decline in every build. The full per-build
matrix suite is wave-5's; this task tests the rows it lands.
## Acceptance Criteria
- [ ] Every `channel_open_*` hook gates per the feature graph;
x11/auth-agent decline in every build; unfeatured types decline
at open with clean wire reasons
- [ ] `exec_request` takes the parse path (git-parseable → git row,
else → session pipe row) before any allocation; request-less
session channels decline as unfeatured per ADR-006
- [ ] `subsystem_request` declines-with-reason for `sftp` (OQ-13
placeholder) and git service names; `env_request` declines
unconditionally (OQ-09)
- [ ] Decline-first-alloc-never holds structurally (declines precede
any channel/resource allocation on every path)
- [ ] Feature-gated code paths exercised per build (`cargo test
--features git/session/tunnel` rows + `--all-features`); the
skeleton decline-matrix rows green through the harness
- [ ] `cargo test`, `cargo clippy --all-targets -- -D warnings`,
`cargo clippy --all-features --all-targets -- -D warnings`,
`cargo fmt --check` pass
## References
- docs/architecture/decisions/006-mapping-bridge-and-dispatch.md
(points 2–3; the amendment — parse-only)
- docs/architecture/decisions/003-feature-graph.md (the feature graph;
§5 never-features)
- docs/architecture/mapping.md §Session-channel dispatch, §Request-level
rows, §Decline matrix
- docs/plans/implementation.md §Wave 2 (the dispatch bullet)
- docs/research/poc-3-mapping-bakeoff.md §stage A (the decline matrix
evidence; decline-first-alloc-never)
## Notes
> To be filled by implementation agent
## Summary
> To be filled on completion
@@ -0,0 +1,77 @@
---
id: w2-loopback-harness
name: Wave 2 — the loopback harness (in-process client/server test pair)
status: pending
depends_on: [w2-auth-bridge, w1-test-doubles]
scope: moderate
risk: medium
impact: phase
level: implementation
---
## Description
Land the in-process client/server test pair every later contract test
runs through — the decline matrix, every mapping's contract tests
(wave 3), and the client-direction smokes all drive the crate through
this harness. In-memory doubles from wave 1; real-tool smokes ride the
mapping waves (wave 3+) and the e2e wave (wave 5).
- **Server side** — the `SshAdapter` from `w2-server-mount` over an
in-process transport. The transport choice (tokio duplex pair vs
127.0.0.1:0 TCP loopback) is pinned here — both work with
`run_stream`/russh's client connect; decide on determinism and
portability and record the choice.
- **Client side** — a russh client driven directly (the wave-4 dial
surface does not exist yet; the harness builds the russh client
config from the doubles): in-memory key from the ed25519 fixtures
(`FIXTURE_KEYS`), the `MemHostKeyPolicy` double with the permissive
test posture (`with_default(true)` — the accept-all posture exists
only as a test choice, never a crate built-in), the keepalive
posture from `Limits`.
- **Harness helpers** — the vocabulary the contract tests speak:
authenticate with a fixture key (and observe auth failure shapes),
open a channel type (and observe the open-failure reason), send a
session request (and observe success/failure), await exit-status /
close. Helpers stay thin — the russh client API is the vocabulary;
the harness removes boilerplate, not semantics.
- **Convention** — lives with the wave-1 doubles under `tests/common/`
(or a sibling harness module — same convention, decide and record);
documented as *the* contract-test vehicle for the crate.
The harness's first consumer is its own acceptance test: a full
in-process connect → authenticate (fixture key through
`MemIdentityProvider`) → authenticated-session observation, proving
the wave-1 doubles work live through the wave-2 stack.
## Acceptance Criteria
- [ ] In-process server/client pair connects and authenticates end to
end (fixture key → `MemIdentityProvider` → authenticated
session) with no TCP port dependency (or the TCP choice
recorded, if taken)
- [ ] Harness helpers cover: auth (success + failure observation),
channel open (success + open-failure reason), session requests
(success/failure), exit-status/close observation
- [ ] The transport choice and module placement are recorded
- [ ] The harness is documented as the contract-test vehicle; the
wave-1 doubles are exercised live through it
- [ ] `cargo test`, `cargo clippy --all-targets -- -D warnings`,
`cargo fmt --check` pass
## References
- docs/plans/implementation.md §Wave 2 (the loopback-harness bullet)
- docs/architecture/mapping.md §Decline matrix (the rows the harness
will drive)
- docs/research/poc-1-auth-bridge.md (the decline-observability facts
the helpers must be able to observe)
- tests/common/mod.rs (the wave-1 doubles this harness drives)
## Notes
> To be filled by implementation agent
## Summary
> To be filled on completion
+98
View File
@@ -0,0 +1,98 @@
---
id: w2-server-mount
name: Wave 2 — SshAdapter::handle mount + russh Config construction
status: pending
depends_on: [review-wave-1, w1-config, w1-key-sources, w1-error-model]
scope: moderate
risk: medium
impact: project
level: implementation
---
## Description
Land the door frame: the `SshAdapter` type — the ADR-001 mount point —
and the russh server `Config` construction from the wave-1
`ServeConfig`. This is the substrate every mapping composes into; the
auth bridge and dispatch fill the Handler this task skeletons.
- **`SshAdapter`** — holds the `ServeConfig`; construction
(`SshAdapter::new`) loads the host keys decrypt-at-construction via
the injected `HostKeySource` (ADR-005 decision 6): a locked /
absent / decryption-failed / unknown-version load means **no server
is constructed** — the error surfaces as a distinct adapter
construction error wrapping the `KeySourceError` variant (never a
half-configured adapter, never a keyless server that fails later).
- **russh `Config` construction** (ADR-002 surface notes) — built from
the serve-config: `methods` = `{publickey}` **exactly**
(`MethodSet` from `[MethodKind::Publickey]` — honest advertisement,
ADR-004 decision 4); `keys` from the loaded host-key set;
`window_size` / `maximum_packet_size` / `channel_buffer_size` /
`max_auth_attempts` mapped verbatim from `Limits`;
`inactivity_timeout` mapped **always-`Some`** (the bounded-resources
posture — the crate never constructs a session with no wall clock);
russh's rekey `Limits` stay russh-enforced at their RFC-4253
defaults (recorded in the wave-1 config notes — keep that
recording). `server_id` stays russh's default posture unless the
task finds a family reason otherwise (decide and record).
- **The mount** — `SshAdapter::handle(stream)`: a generic stream in,
the full russh server stack per stream via `russh::server::run_stream`
(edge termination only — no ALPN code, no relay leg; the `alk/ssh`
pin is a ledger entry, ADR-001 — nothing here speaks it). The exact
return shape (awaited `RunningSession`, an internal spawn + join
handle, …) is decided here against the spawn-and-return posture
(ADR-006 decision 4) and the assembly's mounting needs — decide and
record; it is semver-visible once consumers exist.
- **The Handler skeleton** — the per-connection `russh::server::Handler`
impl with the adapter's `type Error` (`From<russh::Error>`, thiserror
— the wave-2-shaped error surface the wave-1 error-model note
reserved); every hook left at its fail-closed russh default
(decline/reject). Later wave-2 tasks fill the auth hooks
(`w2-auth-bridge`) and the channel/request dispatch
(`w2-dispatch-decline-core`).
No mapping seams, no feature-gated serving (wave 3); no client dial
(wave 4). The feature gates stay untouched — nothing consumes them
yet (the dispatch task is their first `cfg` consumer).
## Acceptance Criteria
- [ ] `SshAdapter::new(ServeConfig)` loads host keys
decrypt-at-construction; locked / absent / decryption-failure /
unknown-version → `Err` with the `KeySourceError` variant
preserved, no adapter constructed
- [ ] russh `Config` built from the serve-config: `methods` is exactly
`{publickey}`; host keys ride `Config::keys`; the `Limits` knobs
map one-for-one with `inactivity_timeout` always `Some`
- [ ] `SshAdapter::handle` mounts the full russh server stack per
generic stream (`run_stream`); no ALPN/relay code exists
- [ ] The Handler skeleton compiles with an adapter error type
(`From<russh::Error>`); every hook at its fail-closed default
- [ ] Tests: honest advertisement (methods exactly `{publickey}`); the
knob mapping; construction failure postures (locked vault → no
server, absent host keys → no server)
- [ ] `cargo test`, `cargo clippy --all-targets -- -D warnings`,
`cargo fmt --check` pass
## References
- docs/architecture/decisions/001-alkssh-alpn-pin.md (the mount-point
contract: `SshAdapter::handle`, raw vanilla-SSH stream in)
- docs/architecture/decisions/002-russh-pin.md (the Config surface
notes; honest advertisement)
- docs/architecture/decisions/004-auth-dispositions.md (decision 4 —
`methods = {publickey}`)
- docs/architecture/decisions/005-at-rest-key-custody.md
(decrypt-at-construction; locked → no server)
- docs/architecture/overview.md §Bounded-resources policy
- docs/plans/implementation.md §Wave 2 (first bullet)
- russh 0.65.0 in-source: `server::Config`, `server::run_stream`,
`auth::MethodSet`
## Notes
> To be filled by implementation agent
## Summary
> To be filled on completion
+74
View File
@@ -0,0 +1,74 @@
---
id: w2-spawn-return
name: Wave 2 — spawn-and-return plumbing + exit/status reporting mechanics
status: pending
depends_on: [w2-dispatch-decline-core]
scope: narrow
risk: medium
impact: phase
level: implementation
---
## Description
Land the structural constraint in code (ADR-006 decision 4): russh at
this pin runs server handler callbacks **inline inside the session's
packet loop** — a blocking handler suspends all packet routing for the
connection and deadlocks against the peer's immediate follow-up bytes
(POC-3 proved by construction with receive-pack). Therefore:
- **Every serving path is `tokio::spawn`ed; handlers return fast.**
The dispatch's routing stubs become spawned serving paths (still
declining stubs — the seams are wave 3), so the pattern is in place
before any real mapping hangs on it.
- **Exit/status reporting mechanics** — reporting from spawned tasks
rides the cloned `Session::handle()`: `exit-status` sent **before**
channel close, from the spawned task. The *derivation contract's
rules* (which code → which SSH exit shape) are wave-3
session-mapping material (`mapping.md` rules 1–6); this task lands
the mechanics — the handle clone, the exit-status send, the
ordering-before-close — proven end to end.
- **The proof** — a stub serving path that spawns, completes
asynchronously (after the handler has returned), and reports
`exit-status(N)` + close through the cloned handle; the loopback
harness client observes the exit-status arriving after the handler
already returned. A handler-returns-fast assertion (the handler
completes while the spawned task is still running) pins the
constraint as a test, not a convention.
This is a load-bearing correctness constraint of the whole spine, not
an implementation choice — the wave-3 mappings inherit the pattern
mechanically from the stubs this task leaves behind.
## Acceptance Criteria
- [ ] Every serving path (the dispatch stubs) runs in a
`tokio::spawn`; no handler hook awaits a serving path inline
- [ ] Exit/status reporting from spawned tasks rides the cloned
`Session::handle()`; `exit-status` is sent before channel close
- [ ] A harness test proves the mechanics end to end: the client
observes `exit-status(N)` from a spawned stub that completed
after the handler returned
- [ ] A handler-returns-fast assertion pins the structural constraint
as a test
- [ ] `cargo test`, `cargo clippy --all-targets -- -D warnings`,
`cargo fmt --check` pass
## References
- docs/architecture/decisions/006-mapping-bridge-and-dispatch.md
(decision 4 — the structural constraint)
- docs/architecture/mapping.md §The structural constraint:
spawn-and-return; §exit_status row (sent before close, from the
spawned task)
- docs/plans/implementation.md §Wave 2 (the spawn-and-return bullet)
- docs/research/poc-3-mapping-bakeoff.md (the deadlock-by-construction
evidence)
## Notes
> To be filled by implementation agent
## Summary
> To be filled on completion