Files
alkssh/tasks/implementation/w2-dispatch-decline-core.md
T
glm-5.3-flash 5b2654f492 docs(tasks): close the wave-2 review gate — verdict green
Gate reading (commits b90e89b..196289f) against the architecture line
by line: ADR-001 mount contract, ADR-002 Config construction,
ADR-003 feature gates, ADR-004 dispositions, ADR-006 constraints,
mapping.md skeleton decline matrix per build, bounded resources, and
the harness — all confirmed. All four flagged interpretations
confirmed as recorded.

Fixed inline before committing (task-file hygiene only):

- w2-loopback-harness, w2-dispatch-decline-core: acceptance boxes
  checked once verified against the landed rows/runs
- w2-bounded-resources: stale placeholder lines removed
- review-wave-2: verdict recorded

Booked finding (next wave's decomposition): decline-path
tracing/observability — the reason a given wire decline carried is
currently docs-only (tracing unused in src/).

Verification: cargo test 104 rows green; --all-features green;
clippy ×2 -D warnings green; fmt clean; cargo doc 0 warnings;
taskgraph validate 19 tasks.
2026-10-28 11:53:51 +00:00

12 KiB
Raw Blame History

id, name, status, depends_on, scope, risk, impact, level
id name status depends_on scope risk impact level
w2-dispatch-decline-core Wave 2 — parse-only dispatch + the fail-closed decline core completed
w2-loopback-harness
moderate medium phase 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

  • src/dispatch.rs (new): the parse-only dispatch core — the ExecRoute decision (Git / Pipe), the skeleton exec-parse probe (classify_exec: exact git-upload-pack/git-receive-pack service prefix + space + path-shaped argument — leading ' or /; everything else → the pipe row), and the two wire-decline reason shapers (unfeatured_open_decline → AdministrativelyProhibited, tunnel_seam_stub_decline → ConnectFailed). Seven probe unit rows pin the POC-3 stage-A grammar facts the skeleton carries. The full grammar (quoted/ unquoted paths, per-length limits, service-only decline-with-reason unknown-service rows) is wave-3's to replace it with.
  • src/handler.rs: every channel_open_* hook gates per the feature graph (session open: session ∨ git; the three tunnel opens: tunnel; x11: always declines). Request hooks: exec_request parses first then routes on the feature graph; pty/shell route under session; subsystem_request declines every name (sftp = the OQ-13 placeholder, git-* = no repository id in a name-only request — ADR-006 decision 2); env_request declines unconditionally (OQ-09); x11_request/agent_request decline (ADR-003 §5); the four listen requests decline (the flow is wave 3; never advertise a listen that cannot serve). Decline-first- alloc-never holds structurally: no hook allocates before its decline; a confirmed session open accepts and drops the channel handle (nothing is held this wave — the store-and-take handoff is wave-3 mapping material, and the routing decisions around it are already pinned by the seam stubs).
  • Wire decline reasons (the wave-1 error-model reservation, now spent): open-level declines carry channel-open failure codes — unfeatured and never-feature rows answer AdministrativelyProhibited (1) is the honest class: without the feature there is no capability to distinguish; the tunnel seam stub answers ConnectFailed (2), the explicit reject shape ADR-006 decision 1 pins for failed fabric opens (accept only after the fabric open succeeds; the skeleton never opens the fabric). Request-level declines carry channel_failure — the SSH request vocabulary has no failure text, so the "reason" is the routing decision, documented per build. Within request declines, two shapes: a capability decline answers failure and leaves the channel usable (POC-1 §5 posture, proven live by the channel-alive rows); a serving-path decline (the seam stubs) answers failure then closes the channel (vanilla clients never hang on a row that will spawn next wave).
  • russh 0.65 facts coded around: only agent_request and the global requests auto-reply from the handler's bool; every other request hook must call session.channel_failure itself. The bare auth-agent@openssh.com (and forwarded-streamlocal@openssh.com) channel types have no handler hook — russh rejects those opens inline (administratively prohibited); that is the decline the never-feature contract needs but it is an inherited posture flagged in the handler docs (pin-bump change would appear as accepted-then-dropped — upstream note). forwarded-tcpip has the same gate/stub code shape as its tcp sibling but no client-side open helper (the server opens it toward the client), so its wire rows are wave-3 acceptor material.
  • Harness: the helper vocabulary extended (tunnel/x11 opens; pty/ env/x11/agent-forward requests; tcpip_forward), discharging the loopback-harness task's recorded expectation that the request helpers' live rows arrive here. One vehicle row (authenticated_session_serves_and_declines_with_an_observed_reason) recorded the skeleton decline-only behavior; the dispatch core makes session opens serve under their features, so that row is now cfg-pinned to the featureless builds it speaks about (its ChannelOpenFailure-reason observation remains the none-build's proof).
  • Request-less session channels (ADR-006 as amended): parse-only means nothing serves before a request arrives (the SOCKS-detect is retired); a request-less channel is served by nothing and dies at the wall clock. The wire-observable rows are the unfeatured opens (none/tunnel builds decline session channels at the open); the no-detect posture is structural (no serving path exists outside the request hooks).

Summary

Landed the parse-only dispatch + the fail-closed decline core (w2-dispatch-decline-core) in three files plus the harness:

  • src/dispatch.rs — the ExecRoute decision, the skeleton exec-parse probe (service prefix + path shape; wave-3 grammar replaces), the wire-reason shapers (AdministrativelyProhibited for unfeatured/never-feature opens, ConnectFailed for the tunnel seam stub), 7 probe unit rows.
  • src/handler.rs — the channel_open_* gates (session ∨ git; tunnel; x11/auth-agent never), the parse-only exec_request dispatch, the session-row routes, the request-level declines (subsystem all-names with the sftp/git named classes, env, x11, agent), the four listen-request declines, and the named seam stubs (git_row_stub, pipe_row_stub, pty_row_stub, shell_row_stub, tunnel_row_stub) — the wave-3 replacement points, each documented with its family seam. Module docs now carry the decline-core contract (gates, decline classes, wire reasons, decline-first-alloc-never, the parse-only posture, the inherited-auth-agent-open upstream note).
  • tests/dispatch_core.rs — the skeleton decline-matrix rows per feature build, 26 rows total, each with a Contract stamp:: shared rows in every build (x11 open, tcpip-forward decline, x11/agent requests where sessions open); none-build (everything declines at open); git-only (sessions open, git exec → seam-stub failure+close, plain exec/unknown services → pipe-row decline with channel alive, pty/shell/subsystem/env/capability declines, tunnel unfeatured); session-only (git-parseable exec declines as gate, stub rows failure+close, capability declines, tunnel unfeatured); tunnel-only (session opens decline, featured tunnel rows → ConnectFailed stubs); all-features (routes hold with the full graph — git row, pipe row, pty/shell stubs, ConnectFailed stubs, capability declines).
  • tests/common/harness.rs — the declined-vocabulary helpers (tunnel/x11 opens, pty/env/x11/agent requests, listen requests), module doc updated to record the discharge of the w2-loopback-harness note.

Verification (all green): cargo test (91 rows across 7 binaries) · cargo test --features git/session/tunnel/sftp --test dispatch_core (each build's rows) · cargo test --all-features · cargo clippy --all-targets -- -D warnings · cargo clippy --all-features --all-targets -- -D warnings · cargo fmt --check · cargo doc --no-deps (no warnings). One pre-existing harness row pinned to the skeleton's decline-only open behavior was cfg-gated to the featureless builds it describes.