diff --git a/docs/architecture/decisions/003-feature-graph.md b/docs/architecture/decisions/003-feature-graph.md index 4c8ca95..e483d40 100644 --- a/docs/architecture/decisions/003-feature-graph.md +++ b/docs/architecture/decisions/003-feature-graph.md @@ -13,18 +13,23 @@ decline paths are contract territory on every side of every feature. ## Context -`mapping-table.md` (POC-3-validated) split the SSH `session` channel -one channel type carries shell, exec, git, and SOCKS traffic, and -vanilla SSH multiplexes them all through session channels. The feature +`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, no -tunnels, no SOCKS — and, in the opposite direction, no capabilities a -client build didn't opt into. +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, -SOCKS-in-channel, direct-tcpip via the alktunnels bridge, session/pty, -all against published siblings) and the user's direction confirmed the -scope: full feature set except sftp (placeholder until alkfs lands). +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 @@ -35,18 +40,37 @@ scope: full feature set except sftp (placeholder until alkfs lands). |---|---|---|---| | `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 | client `direct-tcpip` opens; the client `-R` leg (listen request + forwarded-channel acceptor) | alktunnels | - | `dynamic-forward` | SOCKS5-in-channel on session channels (family-native client shape) | client-side SOCKS consumption | alksocks | + | `tunnel` | `direct-tcpip`, `forwarded-tcpip`, `direct-streamlocal`, the `tcpip_forward`/`streamlocal_forward` listen flow | client `direct-tcpip` opens (which is what `-D` rides — see 1a) | alktunnels | | `sftp` | *placeholder — declines until alkfs lands (OQ-13)* | client sftp half, also deferred | 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 deferred future + (OQ-01's relay/browser leg) ever produces a client that genuinely + wants a remote SOCKS door, 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); `dynamic-forward` does not imply - `session` (SOCKS-in-channel rides session channels but is gated at - dispatch, before any serve). + 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 diff --git a/docs/architecture/decisions/006-mapping-bridge-and-dispatch.md b/docs/architecture/decisions/006-mapping-bridge-and-dispatch.md index 2caf170..285fe4f 100644 --- a/docs/architecture/decisions/006-mapping-bridge-and-dispatch.md +++ b/docs/architecture/decisions/006-mapping-bridge-and-dispatch.md @@ -3,13 +3,22 @@ status: draft last_updated: 2026-10-26 --- -# ADR-006: Mapping decisions — route-1 bridge, exec-only git, SOCKS dispatch +# ADR-006: Mapping decisions — route-1 bridge, exec-only git, session dispatch ## Status **Accepted** — 2026-10-26. All three decisions are POC-3-validated against published sibling crates; amendments are new -ADRs. +ADRs. Amended same day (review untangle): point 3's **SOCKS-detect is +retired** — with the `dynamic-forward` feature removed (ADR-003 §1a), +session dispatch is parse-only and a request-less session channel +simply declines. The parse-vs-anything split was only ever needed if +a session channel could *be* a SOCKS stream; no consumer is that +client (vanilla `-D` rides `direct-tcpip`; the family `-D` door is +client-local, alksocks composed at the assembly). POC-3 stage B's +evidence survives as: (a) the load-bearing correction that vanilla +`-D` = client-side SOCKS + plain `direct-tcpip`, (b) proof that +SOCKS-in-channel is feasible if ever wanted back. ## Context @@ -49,21 +58,18 @@ alkgit/alksocks/alktunnels/alktty). fixups. Vanilla git behavior loses nothing — vanilla git never speaks subsystem for git; the decline only refuses a manual `ssh -s`-style invocation, with a reason. -3. **Session-channel dispatch: request-driven parse wins; detect - covers the request-less case.** The two POC-3 modes compose thus: - - a session channel that receives `exec_request` / - `pty_request` / `shell_request` takes the parse path first - (git parse under `git`; else pipe/shell under `session`); - - a session channel that receives **no request** (the - family-native SOCKS shape — a client speaking SOCKS5 in-band) - runs the **SOCKS-detect** under `dynamic-forward`: spawn, first- - byte sniff (`0x05`), bounded window — **500 ms default, - assembly-configurable**, silent or non-SOCKS → decline (logged); - - the detect never runs when the channel got a request, and the - parse never runs on a request-less channel. One channel, one - route, decided by what actually arrives. - This resolves OQ-02 (the composition was the unknown; the window - bound and configurability are now records, not questions). +3. **~~Session-channel dispatch: request-driven parse wins; detect + covers the request-less case~~ — amended: parse-only.** The + original decision composed the POC-3 modes as: requests take the + parse path (git under `git`, pipe/shell under `session`); + request-less session channels ran a SOCKS-detect + (`dynamic-forward`: spawn, first-byte `0x05` sniff, 500 ms + bounded window). With `dynamic-forward` removed from the feature + graph (ADR-003 §1a), the detect has no owner: **session dispatch + is parse-only** — requests parse to git (exec) or pipe/shell, + and a request-less session channel declines as unfeatured. This + still resolves OQ-02 (the composition was the unknown; the + resolution is now the simpler of the two candidates). 4. **The spawn-and-return rule is a structural constraint, not an implementation detail.** russh 0.64.1-era servers run handler callbacks inline inside the session's packet loop — a blocking @@ -81,10 +87,10 @@ alkgit/alksocks/alktunnels/alktty). recorded; the OQ list loses its highest-priority pending item; the alkforge assembly gets an exact wiring recipe (POC-3 §C.4's in-process fabric leg is the template, verbatim). -- **Negative**: the 500 ms detect window is a tunable that can be - misconfigured (too low: false declines on slow clients; too high: - slow-fail paths) — it is bounded and configurable by design, and - the decline is logged. +- **Negative**: the SOCKS-detect retirement removes a tunable the + POC proved workable — if a family client ever wants a remote SOCKS + door, the shape is re-addable via ADR (500 ms bounded window + preserved in the reference here). - **Neutral**: route 2 remains documented in `mapping-table.md` as the rejected alternative, with its rationale — useful if the posture is ever revisited (that revisit is an ADR, not a code flip). diff --git a/docs/architecture/open-questions.md b/docs/architecture/open-questions.md index aa33b54..bdf85f4 100644 --- a/docs/architecture/open-questions.md +++ b/docs/architecture/open-questions.md @@ -25,12 +25,11 @@ alkcall trackers have their own OQ spaces). ## Theme B — session-channel dispatch & mapping details -### OQ-02: ~~What is the session-channel dispatch shape (SOCKS-detect vs parse, and who bounds the window)?~~ — resolved +### OQ-02: ~~What is the session-channel dispatch shape (SOCKS-detect vs parse, and who bounds the window)?~~ — resolved (then simplified) - **Origin**: [poc-3-mapping-bakeoff.md](../research/poc-3-mapping-bakeoff.md) §Stage B.3, `mapping-table.md` §notes -- **Status**: resolved (ADR-006 point 3) -- **Resolution**: request-driven parse wins (exec/pty/shell → git-parse-else-pipe under their features); request-less session channels under `dynamic-forward` run the SOCKS-detect — spawn + first-byte sniff (`0x05`), bounded 500 ms default (assembly-configurable), silent/non-SOCKS → decline. One channel, one route, decided by what arrives. The composition — the actual unknown — was worked through against the POC-3 stage A/B constructions; the window bound and configurability record as values, not questions. -- **Cross-references**: ADR-006, ADR-003 +- **Status**: resolved — resolved twice over. First by ADR-006 point 3 (dispatch composition: request-driven parse wins, request-less channels get the detect). Then **simplified by the ADR-003 §1a amendment**: the `dynamic-forward` feature retired (no consumer is ever the family-native SOCKS-in-channel client; vanilla `-D` is client-side SOCKS + plain `direct-tcpip`; the family `-D` door is alksocks composed locally at the assembly), so the detect lost its owner. Final shape: **session dispatch is parse-only**; request-less session channels decline. POC-3 stage B survives as the feasibility record for re-adding a remote SOCKS door via explicit ADR. +- **Cross-references**: ADR-006, ADR-003 §1a ### OQ-03: ~~Username disposition~~ — resolved diff --git a/docs/research/mapping-table.md b/docs/research/mapping-table.md index 1d7c5c6..ebe0e84 100644 --- a/docs/research/mapping-table.md +++ b/docs/research/mapping-table.md @@ -39,7 +39,7 @@ request-level extras. | `session` → `pty_request`+`shell_request` | `ssh host` interactive shell | `session` | alktty producer: `NegotiateRequest` constructed locally → `drive_session` (or pre-allocated variant) | `pty_request` args → `TerminalParamsWire` (term/cols/rows/pixels; SSH modes → `modes` JSON reserved); shell case needs a cmd convention (OQ); PTY mode merges stderr into stdout (kernel PTY). **[POC-3 corrected]** the data-plane mapping is a *translation bridge*, not a byte pump: alktty's plane is chunk-framed both directions (stdin/stdout/stderr + CTRL_IN/CTRL_OUT chunks), so alkssh encodes SSH data→stdin chunks and decodes stdout→data, stderr→extended_data, exit chunk→exit-status/exit-signal. New OQ: the exit chunk carries no signal identity (negative code only) | | `session` → `exec_request` (non-git command) | `ssh host "cmd"` | `session` | alktty pipe mode: `terminal: None`, separate stderr | stderr flows back as SSH `extended_data` code 1 — only valid without PTY | | `session` → `exec_request`/`subsystem_request` git | `git push/clone ssh://` | `git` | alkgit: parse command → `Service` + repo (same grammar as `preamble.rs`, without `\0host=`) → `serve_duplex_resolved(repo_id, identity, service, duplex, ...)` | Process-mode dialect is correct (SSH supplies service+repo out of band; server speaks first = advertisement). **[POC-3 corrected]** git over SSH is exec-only: a `subsystem` request carries the service *name* but no repo id, so even a git-service subsystem must decline — vanilla git always executes `git-upload-pack '/repo'`. POC-3: vanilla clone/push/second-push through the published seam + substrate composition, exit-status in both directions | -| `session` → SOCKS5-in-channel | `ssh -D` | `dynamic-forward` | alksocks: `serve_socks5_over(channel_duplex, dial, egress, accept, can_bind)` | alkssh embeds `Socks5Producer`-shaped config from the assembly; `DialFn`/`EgressFactory` injected at assembly. **[POC-3 corrected]** this row is the family-native client shape — a vanilla `ssh -D` does its SOCKS5 client-side and opens plain `direct-tcpip` channels (the `tunnel` row above), so the two rows split by client shape, not just feature. POC-3: SOCKS-detect (bounded first-byte sniff) + real curl through the session channel | +| `session` → SOCKS5-in-channel | (n/a — retired) | ~~`dynamic-forward`~~ **retired (ADR-003 §1a)** | was alksocks: `serve_socks5_over` | **Post-architecture retirement:** no consumer is ever the family-native client shape this row served. Vanilla `ssh -D` does SOCKS client-side and opens plain `direct-tcpip` (POC-3 correction 2 → the `tunnel` row); the family `-D` behavior is a *client-local* alksocks door over the alkssh client's `direct-tcpip` opens (assembly composition). POC-3 stage B remains the evidence that SOCKS-in-channel is feasible if a family client ever wants a remote SOCKS door (back via explicit ADR). **[ADR-003 amended]** | | `direct-tcpip` | `ssh -L` | `tunnel` | alktunnels (two routes, below) | Target = `SubstrateAddr::HostPort`/`Path`; `Substrate` = Tcp/Unix | | `forwarded-tcpip` | `ssh -R` (data channel) | `tunnel` | alktunnels: `register_tunnel_forwarded_openable`'s `ForwardedHandler(params, BiStream)` shape — in the adapter, hand the russh channel duplex to the handler | listen flow via global-request rows below | | `direct-streamlocal` | unix-socket forward | `tunnel` | same `SubstrateAddr::Path` treatment | unix-only; decline on other platforms (compile-time feature + runtime decline) | @@ -60,12 +60,13 @@ a shell, a pipe-mode command. The failure matrix this produces: unfeatured channel type declines at open. Dispatch is parse-then-route by command shape: parsing to a git -service and repo requires `git`; SOCKS-in-channel requires -`dynamic-forward`; otherwise the alktty pipe/pty path requires -`session`. Parsing happens before dispatch and before any resource -allocation (a leaked channel alloc is a capability leak — decline -first). Exact grammar/precedence is a phase 1 spec question (OQ -candidate). +service and repo requires `git`; otherwise the alktty pipe/pty path +requires `session`. Parsing happens before dispatch and before any +resource allocation (a leaked channel alloc is a capability leak — +decline first). (Post-architecture: the former SOCKS-detect dispatch +arm is retired with the `dynamic-forward` feature — ADR-003 §1a / +ADR-006 amendment; request-less session channels decline.) Grammar/ +precedence per ADR-006. ## Global-request rows (`tcpip_forward` / `streamlocal_forward`) — the `-R` listen flow