diff --git a/docs/architecture/README.md b/docs/architecture/README.md new file mode 100644 index 0000000..241357f --- /dev/null +++ b/docs/architecture/README.md @@ -0,0 +1,61 @@ +--- +status: draft +last_updated: 2026-10-26 +--- + +# Architecture index + +## Current State + +Phase 1 (architecture) — in progress. Phase 0 is closed: all three +POCs passed (121 checks; `docs/research/poc-{1,2,3}-*.md`) and the +research docs record their corrections. This directory is being +populated: ADR-001 (the `alk/ssh` ALPN pin) is drafted for review, the +OQ tracker is seeded from the POC findings, and the spec documents +below are being written. No implementation until specs reach +`reviewed` (per `docs/sdd_process.md`). + +## Architecture Documents + +| Document | Status | Scope | +|---|---|---| +| [open-questions.md](open-questions.md) | draft | Centralized OQ tracker (seeded from phase 0 findings) | +| [decisions/001](decisions/001-alkssh-alpn-pin.md) | Proposed | The `alk/ssh` ALPN pin (ledger rule 1) | + +### Planned documents (not yet written) + +| Planned doc | Scope | +|---|---| +| `overview.md` | Crate purpose, exports, dependency posture, bounded-resources policy | +| `mapping.md` | The spine — server-direction channel-type/request→family-seam rows (absorbs the validated `mapping-table.md`) | +| `client.md` | Client-direction surface: dial rows, the forwarded-channel acceptor, reverse-runner shape | +| `auth.md` | Server auth bridge + client-side vault key custody | + +## ADR Table + +| ADR | Title | Status | +|---|---|---| +| [ADR-001](decisions/001-alkssh-alpn-pin.md) | The `alk/ssh` ALPN pin — edge-termination contract for `SshAdapter` | Proposed | + +## Open Questions + +Tracked in [open-questions.md](open-questions.md) — OQ-01 … OQ-13, +seeded from the phase 0 POC findings and research docs. + +## Document Lifecycle + +| Status | Meaning | Transitions | +|--------|---------|-------------| +| `draft` | Under active development; may change significantly | → `reviewed` when its open questions are resolved | +| `reviewed` | Final; implementation may begin; changes require review | → `stable` when implementation is complete and verified | +| `stable` | Locked; changes require review and may warrant an ADR | → `deprecated` when superseded | +| `deprecated` | Superseded; kept for reference | Removed when unreferenced | + +## References + +- `docs/sdd_process.md` — the SDD process +- `docs/research/` — phase 0 (vision, POC plans + findings, russh + inventory, auth bridge, mapping table, reverse-runner extraction) +- Family: `/workspace/@alkdev/alkfamily/docs/architecture/decisions/001..003` + (ADR-003 is the alkssh structural decision) +- Sibling: alkcall ADRs 002/004/009/034/042/051, alkhttp ADR-014/016/073-analogs \ No newline at end of file diff --git a/docs/architecture/decisions/001-alkssh-alpn-pin.md b/docs/architecture/decisions/001-alkssh-alpn-pin.md new file mode 100644 index 0000000..5e26b7e --- /dev/null +++ b/docs/architecture/decisions/001-alkssh-alpn-pin.md @@ -0,0 +1,109 @@ +--- +status: draft +last_updated: 2026-10-26 +--- + +# ADR-001: The `alk/ssh` ALPN pin — edge-termination contract for `SshAdapter` + +## Status + +**Proposed** — 2026-10-26. Pending review sign-off; once Accepted this +is a one-way door (ledger rule 1: ALPN registry entries are stable — +"once published, does not change"). Acceptance marks the pin for the +family ledger; the first consumer is alkforge (family ADR-003 +decision 4: the pin lands before alkforge becomes the consumer). + +## Context + +Family ADR-003 (decision 4) requires alkssh to pin exactly one +`alk/ssh` ALPN via its own ADR. Four alkcall ADRs constrain what that +pin means: + +- **ADR-004** (ALPN convention + connection model) reserves + `alk/ssh` → `SshAdapter` as a custom protocol. +- **ADR-034** (channels wire format) grants `alk/ssh` stream_types + `[0, 1]` — "SSH multiplexes internally, including its own control". + The channels layer is byte-blind to SSH; it must never parse SSH + channel data. +- **ADR-042 / ADR-051** (hub relay): "The hub never runs a handler for + `alk/tty`, `alk/ssh`, or `alk/tunnel`" — SSH channels are relayed + payload-blind between spokes (translate + byte-forward with id + rewrite, ACL fired at `channel/open` by the hub's `CallAdapter`). +- **ADR-009** (BiStream handler leaf) sketches the browser route: a + browser opens a WebSocket/WebTransport session to `/alk/ssh`; the + h3/WS handler hands each bidi stream to `SshAdapter::handle` as a + `Connection`; a WASM SSH parser speaks vanilla SSH over the stream. + +Phase 0 left the topology open (`mapping-table.md` §topology; vision +scope question 1): whether `alk/ssh` carries raw vanilla-SSH bytes +between alkssh instances (a relay leg), serves the browser route, or +both — and how that coexists with ADR-042's relay ruling. POC-3 +settled what could be settled by evidence: the served topology is +**edge termination** (russh terminates vanilla SSH at the alkssh +endpoint; family seams are consumed in-process or via `alk/channels` +bridge legs), worked end to end with vanilla clients through published +sibling crates — and no leg of it requires the hub to parse SSH. + +The decision that remains is the *pin*: what contract `alk/ssh` +commits to on the ledger, so the ALPN string cannot be reinterpreted +later. + +## Decision + +1. **The pin.** `alk/ssh` is pinned for exactly one contract: a + connection opened under `alk/ssh` carries a **raw vanilla-SSH byte + stream**, unchanged and uninterpreted, and the receiving endpoint + is `SshAdapter::handle(Connection)` — alkssh's russh server stack + mounted at an alkcall `channel/open` boundary. Each bidi stream is + one independent SSH transport with its own KEX, auth, and channel + multiplexing (hence ADR-034's `[0, 1]` — SSH owns its own + multiplexing, and the channels layer stays byte-blind, per + ADR-042's relay ruling). +2. **Payload-blind relay is part of the contract.** Any intermediate + (the hub in ADR-042's topology, or a spoke forwarding to a spoke) + relays `alk/ssh` streams as opaque bytes; it runs ACL at + `channel/open` and never an SSH handler. This is ADR-042's ruling, + restated as the ALPN's own contract so it cannot drift. +3. **Phase 1 serves edge termination only.** The first deployment + shape — russh terminating vanilla SSH at the alkssh edge, family + seams consumed in-process — does not speak `alk/ssh` itself: it is + a door, not a relay party. No pin behavior in this ADR requires the + relay leg to exist. +4. **The relay leg and the browser-WASM route remain compatible by + construction.** Because the contract is a transport-blind raw byte + stream, either future leg (hub-relayed SSH, browser WS → + `SshAdapter::handle`) satisfies it without a pin change. Their + *consumers* are deferred — see OQ-01 — on a concrete use case. +5. **What the pin is not.** It is not a carrier for alkcall's framed + channels traffic (that has its own protocol); it is not a second + SSH-flavored control channel; and honesty cuts both ways — an + endpoint that declines to serve `alk/ssh` must refuse the open, + never accept-and-stall (the same honest-advertisement rule that + runs through every alkssh decision). + +## Consequences + +- **Positive**: the ALPN decouples the *endpoint-adjacency contract* + (frozen here) from the *served edge topology* (which evolves with + the family seams); the browser and relay routes are pre-enabled at + zero pin cost; the ledger gets a single unambiguous entry. +- **Negative**: one-way door — the raw-vanilla-SSH-stream contract + binds every future consumer (alkforge first). If a consumer ever + needed framed or pre-parsed SSH over `alk/ssh`, that would be a new + ALPN + new ADR, not a reinterpretation. +- **Neutral**: `SshAdapter::handle` mounts the full russh server + stack per stream, so endpoints pay per-connection bounded-resource + limits (window, auth-attempt, inactivity ceilings) — the alkssh + limits contract applies per `alk/ssh` stream, not per transport. + +## References + +- Family ADR-003 (`003-alkssh-russh-adapter.md`) — decisions 1 and 4 +- alkcall ADR-002 (ProtocolHandler ALPN claim), ADR-004 + (the `alk/ssh` → `SshAdapter` reservation), ADR-009 (browser-WASM + sketch), ADR-034 (`alk/ssh` stream_types `[0, 1]`), ADR-042/051 + (relay: payload-blind, hub handler-free) +- `docs/research/mapping-table.md` §topology, + `docs/research/vision.md` scope Q1, + `docs/research/poc-3-mapping-bakeoff.md` (edge termination, proven) +- OQ-01 (relay/browser-leg consumers, deferred) \ No newline at end of file diff --git a/docs/architecture/open-questions.md b/docs/architecture/open-questions.md new file mode 100644 index 0000000..2a8f74a --- /dev/null +++ b/docs/architecture/open-questions.md @@ -0,0 +1,145 @@ +--- +status: draft +last_updated: 2026-10-26 +--- + +# Open questions + +Centralized tracker for alkssh. Seeded from the phase 0 POC findings +(`docs/research/poc-{1,2,3}-*.md`), the research docs' OQ seeds, and +the reverse-runner extraction. Numbers are repo-local (family and +alkcall trackers have their own OQ spaces). + +## Theme A — wire & family topology + +### OQ-01: Does an `alk/ssh` relay/browser-leg consumer emerge? + +- **Origin**: [mapping-table.md](../research/mapping-table.md) §topology, `vision.md` scope Q1 +- **Status**: deferred(scope) +- **Door type**: two-way (the pin is one-way; consumer *existence* is not) +- **Priority**: medium +- **Impacts**: none for the phase 1 scope — ADR-001 pins the contract so relay/browser legs don't foreclose it; the phase 1 docs serve edge termination only. A future relay leg would exercise `docs/architecture/client.md`'s accept surface in a new role. +- **Blocked on**: a concrete consumer — either the browser-WASM route (alkcall ADR-009 sketch) materializing in an assembly, or a hub deployment needing SSH-in-relay. No such use case exists yet. +- **Resolution**: Not yet decidable — no consumer use case exists. ADR-001 decides everything decidable now: the ALPN pin's raw-stream contract, and its transport-blindness. +- **Cross-references**: ADR-001, OQ-13 + +## 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)? + +- **Origin**: [poc-3-mapping-bakeoff.md](../research/poc-3-mapping-bakeoff.md) §Stage B.3, `mapping-table.md` §notes +- **Status**: deferred(unclear) +- **Door type**: two-way +- **Priority**: high +- **Impacts**: blocks the exact dispatch contract detail in the `mapping.md` spec (ordering: exec-parse vs SOCKS-detect vs pipe; the detect window's bound; crate default vs assembly config). +- **Investigation**: the pieces exist — POC-3 proven both modes (stage A parse-then-route for git; stage B bounded first-byte sniff for SOCKS) and proven both serve "session" channels behind different features. Unclear is the composition: walk 2+ concrete session-channel flows (git exec, family-native SOCKS client, plain pipe exec) and pin dispatch precedence, the detect window's default bound, and the configuration seam. Do during `mapping.md` writing. +- **Resolution**: Not yet decidable by inspection — needs the composition worked through, not new facts. +- **Cross-references**: OQ-08 + +### OQ-03: ~~Username disposition~~ — resolved + +- **Origin**: [poc-1-auth-bridge.md](../research/poc-1-auth-bridge.md) §2, `auth-bridge.md` §The mapping +- **Status**: resolved +- **Resolution**: usernames are never the identity model and carry no routing semantics — the fingerprint is the sole resolution path (POC-1: `ssh anything@host` works with the same identity). Whether a username string appears in logs is deployment policy, outside the crate. Rides as the recorded contract in the `auth.md` spec / ADR-004. + +### OQ-04: OpenSSH certificate auth — is there ever a CA-trust use case? + +- **Origin**: `mapping-table.md` (auth rows), [poc-1-auth-bridge.md](../research/poc-1-auth-bridge.md) OQ seed 4 +- **Status**: deferred(scope) +- **Door type**: two-way +- **Priority**: low +- **Impacts**: none — `auth_openssh_certificate` is declined (the ADR-004 disposition), and POC-1 established the decline exists. A revisit only matters if alkforge ever wants CA-issued certificates. +- **Blocked on**: a concrete OpenSSH-CA use case arriving in an embedding assembly. +- **Resolution**: Not yet decidable — decline initial; no CA-trust story in the family identity model today. + +## Theme C — auth, identity & key custody + +### OQ-05: What is the client-side host-key pin store? + +- **Origin**: [auth-bridge.md](../research/auth-bridge.md) §Host keys, `reverse-runner.md` §5 +- **Status**: open +- **Priority**: high +- **Impacts**: blocks the `client.md` dial surface (the `check_server_key` policy and its configuration seam) and any alkforge reverse-runner deployment (every dial needs a pinned host). +- **Resolution**: pending. The no-TOFU rule is fixed; candidates are the family identity registry vs an assembly-supplied pin set vs `check_known_hosts`. Decide when `client.md` is written — the reverse-runner evidence (chalk-full accept-all in old code) is the anti-pattern witness, not the precedent. +- **Cross-references**: ADR-004, OQ-07 + +### OQ-06: ~~One vault, many roles?~~ — resolved + +- **Origin**: [poc-2-vault-keys.md](../research/poc-2-vault-keys.md) OQ seed 3 +- **Status**: resolved +- **Resolution**: not an alkssh constraint. The crate takes vault handles the assembly injects; whether the assembly runs separate vault instances per role (client keys vs host keys) is assembly policy. ADR-005 records the boundary: vault handle in, key material out, version semantics owned by the vault. + +### OQ-07: Is the alkvault `decrypt` plain-`String` zeroization gap closed at the vault or at the adapter? + +- **Origin**: [poc-2-vault-keys.md](../research/poc-2-vault-keys.md) §2 + OQ seed 1 +- **Status**: open +- **Priority**: medium +- **Impacts**: an in-family cross-crate ask (alkvault API change vs alkssh-side convention). Blocks nothing functionally — POC-2 proves the adapter-side `Zeroizing` wrap correct — but decides where the security convention lives for the whole family (alkhttp secrets ride the same `&str`-out surface). +- **Resolution**: pending. Candidates: alkvault returns `Zeroizing` (family-wide fix, touches a published crate), or the adapter-side wrap is the recorded family convention (ADR-005). Bring to alkvault; if upstream accepts, ADR-005 cites the follow-up; if not, ADR-005 records the adapter obligation as the convention. +- **Cross-references**: OQ-05, ADR-005 + +## Theme D — session-request mapping details + +### OQ-08: Shell command convention for `shell_request` + +- **Origin**: `mapping-table.md` (shell row), [poc-3-mapping-bakeoff.md](../research/poc-3-mapping-bakeoff.md) OQ seed 3 +- **Status**: open +- **Priority**: medium +- **Impacts**: blocks the `mapping.md` session-row detail (what alktty `drives` when a vanilla `ssh host` lands). The POC's stand-in (`bash`/`sh -c`) was explicit assembly policy, not wire-derived. +- **Resolution**: pending. Direction known (assembly policy, nothing wire-supplied); the decision is the concrete convention surface: a config field on the assembly vs a crate-level default that the assembly may override. Decide when writing `mapping.md`. + +### OQ-09: `env_request` policy — is an allowlist ever wanted? + +- **Origin**: `mapping-table.md` (env_request row) +- **Status**: deferred(scope) +- **Door type**: two-way +- **Priority**: low +- **Impacts**: none — decline-by-default is the fixed posture (wire env is not family config). An allowlist (`LANG`, …) only matters if a consuming assembly has a concrete need. +- **Blocked on**: a concrete consumer use case requiring wire-supplied environment variables. +- **Resolution**: Not yet decidable — no use case; decline stands. + +### OQ-10: The alktty exit chunk carries no signal identity — widen the chunk or map by assembly policy? + +- **Origin**: [poc-3-mapping-bakeoff.md](../research/poc-3-mapping-bakeoff.md) §D.2, `mapping-table.md` (exit_signal row) +- **Status**: open +- **Priority**: medium +- **Impacts**: blocks the `exit-signal` fidelity detail in `mapping.md`; an in-family upstream ask (alktty wire change, third-party-upstream posture applies family-internally too). +- **Resolution**: pending. Candidates (per POC-3): alktty widens the exit chunk with the signal name (filed as an ask — alkssh never silently works around), or alkssh maps negative exit codes via assembly policy (fidelity compromise: name inferred/coarse). Also in scope for the same OQ: pipe-mode signals are pid-only vs PTY process-group scope (backend-semantics fidelity note for the session rows). Decide the *fallback* during `mapping.md`; the upstream ask stays open until alktty answers. +- **Cross-references**: OQ-08 + +### OQ-11: `direct-streamlocal` platform surface — compile-time or runtime decline shape? + +- **Origin**: `mapping-table.md` (streamlocal row) +- **Status**: open +- **Priority**: low +- **Impacts**: blocks the `tunnel` feature's platform matrix detail (unix-only compile-out vs universal-compile runtime decline), a small contract test question. +- **Resolution**: pending — decide during `mapping.md`; default direction is runtime decline (matches fail-closed and windows-on-ssh portability) unless the unix-only compile split proves simpler. + +### OQ-12: ~~Client reconnect: does the crate own forward/session restoration?~~ — resolved + +- **Origin**: `reverse-runner.md` §4 +- **Status**: resolved +- **Resolution**: the crate owns lifecycle primitives only — bound-port handles (forward listeners return the actually-bound port, per russh's `tcpip_forward -> u32`), explicit cancellable forward/session handles, and clean disconnects. Reconnect-and-restore policy (rebinding persisted tunnel ports per alkminer's flow) is assembly-owned. Recorded as a constraint in `client.md`; no reconnect policy is baked into the crate. + +## Theme E — deferred features + +### OQ-13: The `sftp` client+server halves when alkfs lands + +- **Origin**: `mapping-table.md` (sftp rows), `reverse-runner.md`, POC-2 scope notes +- **Status**: deferred(scope) +- **Door type**: two-way +- **Priority**: low (until alkforge phase 0 resumes) +- **Impacts**: the `sftp` feature ships declining from the first build (honest advertisement, decline contract tested on both sides); the alkforge CI deploy path (binary/artifact push, alkminer-precedent evidence) and the alkfs server seam are the eventual consumers. +- **Blocked on**: alkfs — family ADR-002, paused so alkforge can land; alkssh's sftp implementation follows alkfs' maturity. +- **Resolution**: Not yet decidable — the target protocol is pending. The placeholder contract (decline, tested) is in scope for the first architecture pass. +- **Cross-references**: OQ-01 + +## Deferred / Blocked (at a glance) + +| OQ | Blocked on | Priority | +|---|---|---| +| OQ-01 | concrete relay/browser-leg consumer | medium | +| OQ-02 | (deferred(unclear)) investigation during `mapping.md` | high | +| OQ-04 | concrete OpenSSH-CA use case | low | +| OQ-09 | concrete wire-env use case | low | +| OQ-13 | alkfs landing (family ADR-002) | low (for now) | \ No newline at end of file diff --git a/tasks/architecture/oq-01-relay-leg-consumer.md b/tasks/architecture/oq-01-relay-leg-consumer.md new file mode 100644 index 0000000..99fd746 --- /dev/null +++ b/tasks/architecture/oq-01-relay-leg-consumer.md @@ -0,0 +1,36 @@ +--- +id: architecture/oq-01-relay-leg-consumer +name: "OQ-01 tracker — alk/ssh relay/browser-leg consumer" +status: pending +depends_on: [] +scope: single +risk: trivial +impact: isolated +level: research +tags: [deferred-oq, external-trigger] +--- + +## Description + +External-trigger tracker for [OQ-01](docs/architecture/open-questions.md) — "does an `alk/ssh` relay/browser-leg consumer emerge?" (deferred(scope)). Not actionable work; tracks whether the unblocking condition has arrived. + +## Work + +Transition check only: when a concrete consumer use case exists — a browser-WASM route landing in an assembly (alkcall ADR-009 sketch) or a hub deployment needing SSH-in-relay — mark this task `completed` and move OQ-01 from `deferred(scope)` to `open` in `open-questions.md` (directly to `resolved` if no decision remains, which would be an ADR-001 amendment since the pin is one-way). + +## Verification + +- [ ] OQ-01 status in `open-questions.md` matches this task's state. + +## References + +- `docs/architecture/decisions/001-alkssh-alpn-pin.md` +- `docs/architecture/open-questions.md` (Theme A) + +## Out of scope + +Any actual relay/browser-leg design — that work would be new ADR/spec territory once the use case exists. + +## Summary + +> Filled on completion. \ No newline at end of file diff --git a/tasks/architecture/oq-04-openssh-cert-auth.md b/tasks/architecture/oq-04-openssh-cert-auth.md new file mode 100644 index 0000000..1dc492e --- /dev/null +++ b/tasks/architecture/oq-04-openssh-cert-auth.md @@ -0,0 +1,35 @@ +--- +id: architecture/oq-04-openssh-cert-auth +name: "OQ-04 tracker — OpenSSH certificate auth use case" +status: pending +depends_on: [] +scope: single +risk: trivial +impact: isolated +level: research +tags: [deferred-oq, external-trigger] +--- + +## Description + +External-trigger tracker for [OQ-04](docs/architecture/open-questions.md) — whether an OpenSSH-CA-trust use case ever arrives for `auth_openssh_certificate` (deferred(scope)). Not actionable work; decline is the standing disposition (ADR-004 territory). + +## Work + +Transition check only: when a consuming assembly presents a concrete CA-issued-certificate requirement, mark this task `completed` and move OQ-04 to `open` for a design pass (the decline disposition itself would be revisited by ADR amendment). + +## Verification + +- [ ] OQ-04 status in `open-questions.md` matches this task's state. + +## References + +- `docs/architecture/open-questions.md` (Theme B) + +## Out of scope + +Any certificate-auth design. + +## Summary + +> Filled on completion. \ No newline at end of file diff --git a/tasks/architecture/oq-09-env-request-allowlist.md b/tasks/architecture/oq-09-env-request-allowlist.md new file mode 100644 index 0000000..20aa22d --- /dev/null +++ b/tasks/architecture/oq-09-env-request-allowlist.md @@ -0,0 +1,35 @@ +--- +id: architecture/oq-09-env-request-allowlist +name: "OQ-09 tracker — env_request allowlist use case" +status: pending +depends_on: [] +scope: single +risk: trivial +impact: isolated +level: research +tags: [deferred-oq, external-trigger] +--- + +## Description + +External-trigger tracker for [OQ-09](docs/architecture/open-questions.md) — whether a consuming assembly ever wants a wire-supplied env allowlist on session channels (deferred(scope)). Decline-by-default is standing posture. + +## Work + +Transition check only: when a concrete use case requiring wire-supplied environment variables arrives, mark this task `completed` and move OQ-09 to `open` in `open-questions.md` for an allowlist-shape design pass. + +## Verification + +- [ ] OQ-09 status in `open-questions.md` matches this task's state. + +## References + +- `docs/architecture/open-questions.md` (Theme D) + +## Out of scope + +Any allowlist design. + +## Summary + +> Filled on completion. \ No newline at end of file diff --git a/tasks/architecture/oq-13-alkfs-sftp.md b/tasks/architecture/oq-13-alkfs-sftp.md new file mode 100644 index 0000000..f13b1ef --- /dev/null +++ b/tasks/architecture/oq-13-alkfs-sftp.md @@ -0,0 +1,37 @@ +--- +id: architecture/oq-13-alkfs-sftp +name: "OQ-13 tracker — alkfs landed (sftp implementation unblocks)" +status: pending +depends_on: [] +scope: single +risk: trivial +impact: isolated +level: research +tags: [deferred-oq, external-trigger] +--- + +## Description + +External-trigger tracker for [OQ-13](docs/architecture/open-questions.md) — the `sftp` feature's implementation waits for alkfs (family ADR-002, paused for alkforge). The `sftp` feature ships declining (tested decline contract on both directions) until this unblocks. + +## Work + +Transition check only: when alkfs reaches an implementation-ready state (its spec/implementation exists), mark this task `completed` and move OQ-13 to `open` for the sftp mapping design (client+server halves; alkforge CI artifact deploy is the consumer). + +## Verification + +- [ ] OQ-13 status in `open-questions.md` matches this task's state. +- [ ] The `sftp` decline contract remains tested until that point. + +## References + +- `docs/architecture/open-questions.md` (Theme E) +- `docs/research/reverse-runner.md` (the deploy use case evidence) + +## Out of scope + +Any sftp mapping design. + +## Summary + +> Filled on completion. \ No newline at end of file