Files
alkssh/tasks/implementation/w2-dispatch-decline-core.md
glm-5.3-flash f4b5f896b4 docs: sweep em dashes from all markdown prose
Renames the written-usage rule into practice across the contract surface:
docs/ (architecture, research, plans, sdd), tasks/ (all 43 task files,
taskgraph validate passes), AGENTS.md, README.md, CHANGELOG.md, and the
.opencode/agents/ prompts. Every replacement is a sense-preserving
rewrite (colons, semicolons, commas, parentheses, sentence splits, varied),
never a mechanical swap; no spaced hyphens were substituted. Enum fields,
frontmatter, tables, wire data, and cited identifiers survive verbatim;
en dashes inside numeric ranges keep their meaning.

Verification: taskgraph validate (43 tasks), repo grep clean (single
exempt hit: the verification command's own search pattern in AGENTS.md),
cargo test, clippy --all-targets -D warnings, fmt --check, doc all green.
2026-11-04 20:28:00 +00:00

218 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
id: w2-dispatch-decline-core
name: "Wave 2: parse-only dispatch + the fail-closed decline core"
status: completed
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), which is to say 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`, since 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, per 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`, where 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, since 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), where 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
- [x] 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
- [x] `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
- [x] `subsystem_request` declines-with-reason for `sftp` (OQ-13
placeholder) and git service names; `env_request` declines
unconditionally (OQ-09)
- [x] Decline-first-alloc-never holds structurally (declines precede
any channel/resource allocation on every path)
- [x] 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
- [x] `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 is 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 with 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 per 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, so 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, an 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`), which are 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.