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.
218 lines
12 KiB
Markdown
218 lines
12 KiB
Markdown
---
|
||
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. |