--- status: reviewed last_updated: 2026-10-28 --- # ADR-003: The capability feature graph (flat, independent, two-direction) ## Status **Accepted** (2026-10-26). Builds the fail-closed capability contract (family ADR-003 decision 3) into the crate's own feature names; the decline paths are contract territory on every side of every feature. ## Context `mapping-table.md` (POC-3-validated) split the SSH `session` stream into capabilities by what rides it (shell/exec, git, pipes), and vanilla SSH multiplexes tunnel legs through `direct-tcpip`/`forwarded-tcpip` channels (a vanilla `ssh -D` is its own client-side SOCKS door plus `direct-tcpip`; the wire never sees SOCKS, POC-3 correction 2). The feature graph is how alkforge gets an endpoint that is *structurally incapable* of serving what it didn't enable: a git-only build accepts no shell and no tunnels; and, in the opposite direction, no capabilities a client build didn't opt into. Phase 0 validated the seams end to end (POC-3 stages A–D: git exec, direct-tcpip via the alktunnels bridge, session/pty, all against published sibling crates, with stage B additionally establishing, against real vanilla `ssh -D` + curl, that the wire-level SOCKS story is client-side; its SOCKS-in-channel serve path was subsequently retired, see §1a), and the user's direction confirmed the scope: full feature set except sftp (placeholder until alkfs lands). ## Decision 1. **The feature set** (cargo features on `alkssh`, all off by default): | Feature | Serves (inbound) | Consumes (outbound) | Family seam | |---|---|---|---| | `session` | session channels: pty/shell, pipe-mode exec | client pty/shell/exec/subsystem opens | alktty | | `git` | git-parseable exec (dispatched out of session channels) | client git consumption of upstream servers | alkgit | | `tunnel` | `direct-tcpip`, `forwarded-tcpip`, `direct-streamlocal`, the `tcpip_forward`/`streamlocal_forward` listen flow (the streamlocal rows, channel and listen, are runtime-Unix-gated, OQ-11) | client `direct-tcpip` opens (which is what `-D` rides; see 1a) | alktunnels | | `sftp` | *placeholder, declines until alkfs lands (OQ-13 resolved)* | client sftp half, same placeholder | alkfs (future) | **1a. No `dynamic-forward` feature.** Amended 2026-10-26 (review untangle): the earlier graph carried a `dynamic-forward` feature with a server-side SOCKS5-in-channel row, a family-native client shape ("speak SOCKS into a session channel, server dials through alksocks"). No consumer of ours is that client: vanilla `ssh -D` does its SOCKS5 handshake client-side and opens plain `direct-tcpip` channels (POC-3 correction 2), and the family behavior of `-D` is a **client-side local door**: alksocks' local door given a `DialFn` backed by the alkssh client's `direct-tcpip` opens, composed at the assembly. The server needs only the `tunnel` row for the whole `-D` leg, and alkssh does not depend on alksocks on this path. The SSH-detect dispatch from ADR-006 point 3 retires with it (session dispatch is parse-only; a request-less session channel declines). If a consumer ever genuinely wants a remote SOCKS door (the OQ-01 relay/browser leg, resolved out of scope: no such consumer exists today), adding it back is an explicit ADR. The two transports are "just byte streams", so nothing forbids it; nothing needs it. 2. **Flat and independent.** No feature implies another. `default = []`: a default build has no capabilities at all; every unfeatured channel type/open/request declines with a clean error. `git` does not imply `session` (a git-only build serves the git-parseable exec path by declining to parse to anything else, the parse-then-fail contract from POC-3 stage A). Note the `tunnel` client leg's SOCKS door is *local to the dialing host* (alksocks composed at the assembly), not a wire capability; the wire sees `direct-tcpip` only. 3. **Two-direction symmetry.** Each feature name gates *both* the server-direction (what an embedded alkssh serves vanilla clients) and the client-direction (what a family app drives when dialing an upstream SSH server, including the forwarded-channel acceptor rows). One name, one capability, both doors: this is the symmetry rule, now structural: a `git`-only build cannot dial a tunnel upstream any more than it can serve one. 4. **Client-side fail-closed at the acceptor.** Server-initiated channel opens (the real one is `forwarded-tcpip`, the `-R` data path) are served only under a registered feature's acceptor handler; unhandled/unfeatured accepted opens are **declined**, not accept-and-dropped (russh's client default is accept-then-drop, ours to fix; the reverse-runner extraction names it). 5. **Never features.** `x11` and `auth-agent` forwarding are not feature-gated at all and are always declined, at any configuration (agent forwarding is the *anti-pattern* of the alkvault custody model; x11 has no family seam). An ADR amendment is required to ever feature-gate them. 6. **Honest advertisement per build.** What the wire advertises mirrors what features are on: auth `methods` is always `{publickey}` (ADR-004) server-side; client-side capabilities are invoked only by the assembly, and unfeatured rows decline. There is no "advertise and see" path. ## Consequences - **Positive**: alkforge's ssh endpoint shape is a straight cargo selection; the failure matrix (git-only, session-only, none) is a tested decline matrix rather than a hope; the client and server surfaces can't drift apart on capability naming. - **Negative**: the split of `session` makes feature interplay a real test matrix (per-feature decline paths on both directions are contract tests; the AGENTS.md verification convention covers this via `--all-features` runs plus the decline-matrix suite). - **Neutral**: feature *names* are now contractual (downstream builds specify them); renames would be semver-visible, hence the ADR rather than incidental naming. ## References - `mapping-table.md` (the validated rows under these names + the feature-split notes + the three POC-3 corrections) - Family ADR-003 (decision 3, the fail-closed capability features), family ADR-002 (why exclusion matters for alkforge) - `docs/research/reverse-runner.md` (client-direction surface the symmetry rule serves; the acceptor default fact) - `docs/research/poc-3-mapping-bakeoff.md` (stages A–D: the seams at these names work end to end) - OQ-13 (resolved; the placeholder contract stands), OQ-02 (resolved by ADR-006)