docs(architecture): resolve all open questions (OQ-01..13), mark specs reviewed
- OQ-08: shell convention = assembly-supplied serve config (shell argv for shell_request; exec wrapper argv + command string for exec; no crate default; unset -> clean decline) - OQ-11: direct-streamlocal = universal compile + runtime Unix-gated decline (channel and listen flow) - OQ-10: closed as contract + upstream constraint; derivation rules formalized in mapping.md (Exit chunk -> SSH exit reporting), incl. the -1/genuine-SIGHUP fidelity tradeoff and an unmapped-negative fallback rule - OQ-01/04/09/13: resolved as fully-decided dispositions (ADR-001/ ADR-004/fixed decline postures); remainders were forecasts about consumers, not pending alkssh decisions; upstream deps now listed as watch items, not OQs - OQ-12 restored to the tracker (dropped in the rewrite) - review-agent criticals/warnings fixed: exec-unset + wrap mechanics specified, streamlocal listen gate added, stale deferred/tracked language removed, ADR-006 title fixed, 0.64.1-vs-0.65.0 crosswalk added in ADR-004, ADR frontmatter aligned to 'reviewed' All specs + ADRs 001-006 now reviewed; implementation may begin.
This commit is contained in:
1 parent
f36c268694
commit
ab6fb9de87
12 files changed
+248
-175
No files matched your search
+30
-19
@@ -1,29 +1,33 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-10-27
|
||||
status: reviewed
|
||||
last_updated: 2026-10-28
|
||||
---
|
||||
|
||||
# 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. The ADR set (001–006) is
|
||||
**Accepted** — including the post-review amendments (ADR-002's
|
||||
ed25519-only revert; ADR-003/006's `dynamic-forward` retirement) — and
|
||||
the four spec docs (overview/mapping/client/auth) are drafted; the
|
||||
architecture review pass is next, after which specs go to `reviewed`.
|
||||
Phase 1 (architecture) — complete pending implementation. Phase 0 is
|
||||
closed: all three POCs passed (121 checks;
|
||||
`docs/research/poc-{1,2,3}-*.md`) and the research docs record their
|
||||
corrections. The ADR set (001–006) is **Accepted** — including the
|
||||
post-review amendments (ADR-002's ed25519-only revert; ADR-003/006's
|
||||
`dynamic-forward` retirement) — and the four spec docs
|
||||
(overview/mapping/client/auth) are `reviewed` (2026-10-28): the OQ
|
||||
resolution pass closed all thirteen open questions (OQ-08/11 decided,
|
||||
OQ-10 converted to a derivation contract + upstream constraint,
|
||||
OQ-01/04/09/13 closed as fully-decided dispositions) and the review
|
||||
pass came back clean. Implementation may begin.
|
||||
|
||||
## Architecture Documents
|
||||
|
||||
| Document | Status | Scope |
|
||||
|---|---|---|
|
||||
| [overview.md](overview.md) | draft | Crate purpose, family position, surface categories, bounded-resources policy, non-goals |
|
||||
| [mapping.md](mapping.md) | draft | The spine — server-direction rows, dispatch, spawn-and-return, decline matrix |
|
||||
| [client.md](client.md) | draft | Client-direction surface: dial, op rows, the acceptor, lifecycle primitives |
|
||||
| [auth.md](auth.md) | draft | The auth bridge (server) + custody & host-key policy (client) |
|
||||
| [open-questions.md](open-questions.md) | draft | Centralized OQ tracker (seeded from phase 0 findings) |
|
||||
| [overview.md](overview.md) | reviewed | Crate purpose, family position, surface categories, bounded-resources policy, non-goals |
|
||||
| [mapping.md](mapping.md) | reviewed | The spine — server-direction rows, dispatch, spawn-and-return, decline matrix |
|
||||
| [client.md](client.md) | reviewed | Client-direction surface: dial, op rows, the acceptor, lifecycle primitives |
|
||||
| [auth.md](auth.md) | reviewed | The auth bridge (server) + custody & host-key policy (client) |
|
||||
| [open-questions.md](open-questions.md) | reviewed | Centralized OQ tracker (all OQ-01…13 resolved) |
|
||||
|
||||
## ADR Table
|
||||
|
||||
@@ -34,15 +38,22 @@ architecture review pass is next, after which specs go to `reviewed`.
|
||||
| [ADR-003](decisions/003-feature-graph.md) | The capability feature graph — flat, independent, two-direction | Accepted |
|
||||
| [ADR-004](decisions/004-auth-dispositions.md) | Auth dispositions — publickey-only, fail-closed, fingerprint identity | Accepted |
|
||||
| [ADR-005](decisions/005-at-rest-key-custody.md) | At-rest key custody — vault-wrapped OpenSSH-PEM, decrypt-at-use | Accepted |
|
||||
| [ADR-006](decisions/006-mapping-bridge-and-dispatch.md) | Mapping decisions — route-1 bridge, exec-only git, SOCKS dispatch | Accepted |
|
||||
| [ADR-006](decisions/006-mapping-bridge-and-dispatch.md) | Mapping decisions — route-1 bridge, exec-only git, session dispatch | Accepted |
|
||||
|
||||
## Open Questions
|
||||
|
||||
Tracked in [open-questions.md](open-questions.md) — OQ-01 … OQ-13,
|
||||
seeded from the phase 0 POC findings and research docs. Six resolved
|
||||
(OQ-02/03/05/06/07/12), three open (OQ-08/10/11 — mapping-detail
|
||||
decisions in the current review pass), four deferred with named
|
||||
blockers (OQ-01/04/09/13).
|
||||
seeded from the phase 0 POC findings and research docs. **All thirteen
|
||||
are resolved**: the three mapping-detail decisions settled this pass
|
||||
(OQ-08 shell convention → assembly-supplied serve-config field, no
|
||||
crate default; OQ-10 → closed as the derivation contract on
|
||||
`mapping.md`'s exit rows + the upstream PTY-path constraint; OQ-11 →
|
||||
universal compile + runtime Unix-gated decline), and the previously
|
||||
deferred entries closed as fully-decided dispositions (OQ-01/04/09/13
|
||||
— the decision surfaces were already fixed by ADR-001/ADR-004 and the
|
||||
decline postures; what remained was forecasts about consumers, not
|
||||
pending alkssh decisions). Upstream dependencies are listed as watch
|
||||
items in the tracker, not OQs.
|
||||
|
||||
## Document Lifecycle
|
||||
|
||||
|
||||
+11
-10
@@ -1,6 +1,6 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-10-27
|
||||
status: reviewed
|
||||
last_updated: 2026-10-28
|
||||
---
|
||||
|
||||
# Auth — identity is the family's, not SSH's
|
||||
@@ -101,8 +101,10 @@ the tunnels, not host pins).
|
||||
|
||||
- Password / keyboard-interactive anywhere (server: declined; client:
|
||||
non-goal until a use case).
|
||||
- OpenSSH certificate auth (`auth_openssh_certificate` declines;
|
||||
OQ-04 deferred on a CA use case).
|
||||
- OpenSSH certificate auth (`auth_openssh_certificate` declined
|
||||
unconditionally — OQ-04 resolved: no CA-trust story exists in the
|
||||
family identity model; future support is new-ADR territory with the
|
||||
upstream identity-model work as its prerequisite).
|
||||
- ssh-agent integration/forwarding (external signer processes are out
|
||||
of family scope; the vault is the custody story).
|
||||
- TOFU *as crate behavior* — policy lives at the assembly, the
|
||||
@@ -110,12 +112,11 @@ the tunnels, not host pins).
|
||||
|
||||
## Open Questions
|
||||
|
||||
- **OQ-04** (deferred — certificate auth, on a use case).
|
||||
- **OQ-07** (resolved at the adapter level — the vault `decrypt`
|
||||
plain-`String` gap: adapter-side `Zeroizing` wrap is the contract
|
||||
(ADR-005 decision 3); the vault-side improvement is an upstream
|
||||
alkvault ask).
|
||||
- **OQ-05** (resolved — recorded above).
|
||||
None open or deferred — see [open-questions.md](open-questions.md)
|
||||
(all of OQ-01…13 resolved). Doc-local contract anchors: OQ-03 (the
|
||||
username disposition — §Dispositions), OQ-04 (the certificate decline
|
||||
— §out-of-scope), OQ-05 (the host-key policy seam — §Host-key policy),
|
||||
OQ-06 / OQ-07 (custody boundaries — ADR-005).
|
||||
|
||||
## References
|
||||
|
||||
|
||||
+13
-14
@@ -1,6 +1,6 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-10-27
|
||||
status: reviewed
|
||||
last_updated: 2026-10-28
|
||||
---
|
||||
|
||||
# Client — client direction: driving vanilla SSH servers as family ops
|
||||
@@ -50,7 +50,7 @@ ours from the vault and the host's key is checked by the policy.
|
||||
| the `-D` behavior (local SOCKS door) | alksocks' local door **composed at the assembly**, fed a `DialFn` backed by row 2's open | the wire sees only `direct-tcpip`; alkssh *does not depend on alksocks* on this path (ADR-003 §1a) | `tunnel` |
|
||||
| listen + ingest (`-R` leg) | alktunnels consumer/`ForwardedHandler` shape | `tcpip_forward(address, port) -> bound` global request (no mpsc sink param) + **accept the server-initiated `forwarded-tcpip` opens** via `server_channel_open_forwarded_tcpip`, routing each to the family handler | `tunnel` |
|
||||
| upstream git consumption | alkgit client/service types | the same exec grammar the server side parses (`git-(upload,receive)-pack '/repo'` exec), pointed at an upstream `sshd` | `git` |
|
||||
| artifact/deploy transfer | alkfs client half (future) | `subsystem "sftp"` + the alkfs protocol | `sftp` (OQ-13) |
|
||||
| artifact/deploy transfer | alkfs client half (future) | `subsystem "sftp"` + the alkfs protocol | `sftp` (decline placeholder — OQ-13 resolved) |
|
||||
|
||||
## The acceptor: fail-closed client-side
|
||||
|
||||
@@ -86,15 +86,16 @@ half-capability, which the family forbids (ADR-003 symmetry +
|
||||
start/stop/logs job model, pid files, host-side ledgers) is
|
||||
assembly work composed over the exec rows; the crate gives the
|
||||
channels and the exit semantics (including the pipe-mode
|
||||
pid-only signal caveat — `mapping.md` / OQ-10).
|
||||
pid-only signal caveat — `mapping.md`'s derivation contract).
|
||||
- **Credential storage policy** — assembly (the vault handle is
|
||||
injected; ADR-005 / OQ-06).
|
||||
- **Trust decisions** — the host-key policy is assembly-provided
|
||||
(OQ-05; the crate default rejects; a permissive case-2 policy for
|
||||
ephemeral fleets is the assembly's to choose — the crate carries no
|
||||
trust model of its own).
|
||||
- **sftp** — until alkfs (OQ-13); the `sftp` feature exists as the
|
||||
tested decline on both directions.
|
||||
- **sftp** — the `sftp` feature is the tested decline placeholder on
|
||||
both directions (OQ-13 resolved); the alkfs-protocol mapping
|
||||
arrives with alkfs, as its own ADR.
|
||||
|
||||
## Correctness facts to carry (POC-recorded)
|
||||
|
||||
@@ -116,14 +117,12 @@ half-capability, which the family forbids (ADR-003 symmetry +
|
||||
|
||||
## Open Questions
|
||||
|
||||
- **OQ-05** (resolved): the host-key policy seam — the contract above;
|
||||
pin storage is assembly detail.
|
||||
- **OQ-12** (resolved): lifecycle primitives only — recorded above.
|
||||
- **OQ-13** (deferred, alkfs): the sftp client half.
|
||||
- **OQ-07** (resolved at the adapter level): the vault `decrypt`
|
||||
plain-`String` gap — the adapter-side `Zeroizing` wrap is the
|
||||
contract (ADR-005 decision 3); the vault-side improvement is an
|
||||
upstream alkvault ask, not alkssh scope.
|
||||
None open or deferred — see [open-questions.md](open-questions.md)
|
||||
(all of OQ-01…13 resolved). Doc-local contract anchors: OQ-05 (the
|
||||
host-key policy — §The dial surface), OQ-12 (lifecycle primitives only
|
||||
— §Lifecycle primitives), OQ-13 (the sftp decline placeholder — §op
|
||||
rows and §not-include), OQ-07 (the adapter-side `Zeroizing` wrap —
|
||||
ADR-005 decision 3).
|
||||
|
||||
## References
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
status: reviewed
|
||||
last_updated: 2026-10-26
|
||||
last_updated: 2026-10-28
|
||||
---
|
||||
|
||||
# ADR-001: The `alk/ssh` ALPN pin — edge-termination contract for `SshAdapter`
|
||||
@@ -72,7 +72,9 @@ later.
|
||||
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.
|
||||
*consumers* are a use-case question for assemblies/family work, not
|
||||
alkssh decisions (OQ-01 resolved: this ADR is the entire decision
|
||||
surface; nothing in the crate is gated on a consumer).
|
||||
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
|
||||
@@ -105,4 +107,5 @@ later.
|
||||
- `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)
|
||||
- OQ-01 (resolved — no consumer in scope; this ADR is the entire
|
||||
decision surface)
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-10-27
|
||||
status: reviewed
|
||||
last_updated: 2026-10-28
|
||||
---
|
||||
|
||||
# ADR-002: The russh pin — 0.65.0, aws-lc-rs, no compression, ed25519-only
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-10-26
|
||||
status: reviewed
|
||||
last_updated: 2026-10-28
|
||||
---
|
||||
|
||||
# ADR-003: The capability feature graph — flat, independent, two-direction
|
||||
@@ -40,8 +40,8 @@ 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 (which is what `-D` rides — see 1a) | alktunnels |
|
||||
| `sftp` | *placeholder — declines until alkfs lands (OQ-13)* | client sftp half, also deferred | alkfs (future) |
|
||||
| `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
|
||||
@@ -54,11 +54,12 @@ feature set except sftp (placeholder until alkfs lands).
|
||||
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 —
|
||||
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.
|
||||
|
||||
@@ -119,5 +120,5 @@ feature set except sftp (placeholder until alkfs lands).
|
||||
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 (sftp placeholder tracking), OQ-02 (dispatch precedence
|
||||
inside `session`)
|
||||
- OQ-13 (resolved — the placeholder contract stands), OQ-02 (resolved
|
||||
by ADR-006)
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-10-27
|
||||
status: reviewed
|
||||
last_updated: 2026-10-28
|
||||
---
|
||||
|
||||
# ADR-004: Auth dispositions — publickey-only, fail-closed, fingerprint identity
|
||||
@@ -22,11 +22,13 @@ every feature.
|
||||
The family identity model has exactly one SSH-shaped resolution path:
|
||||
a verified key's fingerprint → alkcall `IdentityProvider::
|
||||
resolve_from_fingerprint` → `Identity` (`auth-bridge.md`). russh 0.64.1
|
||||
performs the cryptographic verification itself — `auth_publickey`
|
||||
fires *after* signature and ownership verification (POC-1 observed),
|
||||
and invalid/malformed signatures produce `USERAUTH_FAILURE` before any
|
||||
handler hook fires (POC-2). alkssh therefore never touches signature
|
||||
material; its auth surface is dispositional.
|
||||
(the POC pin) performs the cryptographic verification itself —
|
||||
`auth_publickey` fires *after* signature and ownership verification
|
||||
(POC-1 observed), and invalid/malformed signatures produce
|
||||
`USERAUTH_FAILURE` before any handler hook fires (POC-2); the facts
|
||||
carry to the 0.65.0 pin per ADR-002's upgrade note, where the handler
|
||||
field shape `auth.md` cites is normative. alkssh therefore never
|
||||
touches signature material; its auth surface is dispositional.
|
||||
|
||||
The fail-closed posture (family ADR-003 decision 3) applies to auth
|
||||
twice: which methods exist, and what unauthenticated attempts receive.
|
||||
@@ -49,12 +51,14 @@ twice: which methods exist, and what unauthenticated attempts receive.
|
||||
at the pin.** `auth_none`, `auth_password`,
|
||||
`auth_keyboard_interactive`, `auth_openssh_certificate` each get
|
||||
explicit reject implementations. At russh 0.64.1 the defaults
|
||||
already reject (POC-1's correction of the 0.5x-era assumption) —
|
||||
the explicit implementations are family contract, not redundancy:
|
||||
the pin is a one-way door and a future bump could silently change
|
||||
defaults (an auth-posture change would otherwise be invisible).
|
||||
The certificate decline is the standing disposition (OQ-04 tracks
|
||||
whether a CA-trust use case ever arrives).
|
||||
already reject (POC-1's correction of the 0.5x-era assumption) —
|
||||
the explicit implementations are family contract, not redundancy:
|
||||
the pin is a one-way door and a future bump could silently change
|
||||
defaults (an auth-posture change would otherwise be invisible).
|
||||
The certificate decline is the standing disposition (OQ-04
|
||||
resolved: no CA-trust use case arrives in the family identity
|
||||
model; future support is new-ADR territory with the upstream
|
||||
identity-model work as its prerequisite).
|
||||
4. **Honest advertisement.** `Config::methods` advertises exactly what
|
||||
is implemented: `{publickey}` in every current configuration.
|
||||
OpenSSH honors the subset (POC-1: non-advertised methods are
|
||||
@@ -94,4 +98,4 @@ twice: which methods exist, and what unauthenticated attempts receive.
|
||||
- Family ADR-003 (decisions 2, 3), alkcall ADR-002/003/006 (the
|
||||
`IdentityProvider` surface and its single-backend posture; alkhttp
|
||||
ADR-073's one-backend-per-door analog)
|
||||
- OQ-03 (resolved), OQ-04 (deferred)
|
||||
- OQ-03 (resolved), OQ-04 (resolved)
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-10-27
|
||||
status: reviewed
|
||||
last_updated: 2026-10-28
|
||||
---
|
||||
|
||||
# ADR-005: At-rest key custody — vault-wrapped OpenSSH-PEM, decrypt-at-use
|
||||
@@ -81,7 +81,11 @@ both ends).
|
||||
6. **Host keys ride the same model.** Server host keys are
|
||||
decrypt-at-construction into `Config::keys` through the same
|
||||
load path; the client's host-*pin* is public config, never vault
|
||||
material (OQ-05's two-case policy handles pinning).
|
||||
material (OQ-05's two-case policy handles pinning). The two-case
|
||||
host-key policy *seam* is the crate's trust-decision contract —
|
||||
the crate is policy-blind (fail-closed default; a permissive
|
||||
case-2 policy is an assembly choice) — the contract lives in
|
||||
`auth.md`/`client.md`.
|
||||
7. **Plaintext never persists.** No disk spill on this path, ever;
|
||||
the vanilla-tool PEM compatibility is for the assembly layer to
|
||||
exploit if ever (e.g. handing a key to an external agent process —
|
||||
@@ -104,8 +108,9 @@ both ends).
|
||||
at-rest bytes are plain OpenSSH key files, so alkvault stores and
|
||||
rotates them with no SSH-specific logic.
|
||||
- **Negative**: decrypted plaintext has one unfenced window (before
|
||||
the adapter's wrap) until alkvault changes its API — the gap is
|
||||
recorded, owned, and tracked (OQ-07), not silently absorbed.
|
||||
the adapter's wrap) until alkvault changes its API — the gap is
|
||||
recorded, owned, and filed with its owner (the alkvault tracker;
|
||||
OQ-07 resolved at the adapter level), not silently absorbed.
|
||||
- **Neutral**: the seed-variant size optimization remains noted as a
|
||||
legitimate future option if blob size ever matters; adopting it
|
||||
would be an ADR amendment (it changes what a stored record is).
|
||||
@@ -119,4 +124,4 @@ both ends).
|
||||
- alkcall ADR-010 + alkhttp ADR-014 (the secret-material-flow family
|
||||
pattern — ADR-010 is alkhttp ADR-014's alkcall analog)
|
||||
- Family ADR-002 decision 5, family ADR-003 decision 2
|
||||
- OQ-05, OQ-06, OQ-07, OQ-13
|
||||
- OQ-05, OQ-06, OQ-07, OQ-13 (all resolved)
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-10-27
|
||||
status: reviewed
|
||||
last_updated: 2026-10-28
|
||||
---
|
||||
|
||||
# ADR-006: Mapping decisions — route-1 bridge, exec-only git, session dispatch
|
||||
@@ -76,10 +76,11 @@ alkgit/alksocks/alktunnels/alktty).
|
||||
handler deadlocks against the peer's follow-up bytes (POC-3
|
||||
proved by construction: receive-pack pushes bytes immediately
|
||||
after the exec request). Every serving path is
|
||||
`tokio::spawn` + handler-returns-fast; exit reporting from the
|
||||
spawned task rides the cloned `Session::handle()` —
|
||||
`exit-status`(then `close`) before channel close, `exit-signal`
|
||||
for signal termination (signal-name fidelity: OQ-10).
|
||||
`tokio::spawn` + handler-returns-fast; exit reporting from the
|
||||
spawned task rides the cloned `Session::handle()` —
|
||||
`exit-status`(then `close`) before channel close, `exit-signal`
|
||||
for signal termination (signal-name fidelity: the derivation
|
||||
contract on `mapping.md`'s exit rows — OQ-10 resolved).
|
||||
|
||||
## Consequences
|
||||
|
||||
@@ -103,4 +104,7 @@ alkgit/alksocks/alktunnels/alktty).
|
||||
- ADR-003 (the feature gates these dispatch decisions sit behind)
|
||||
- `docs/research/reverse-runner.md` (the client-direction mirror of
|
||||
leg 1 — the `-L`/`-R` legs the same route-1 bridge serves)
|
||||
- OQ-02 (resolved by this ADR), OQ-10 (exit-signal fidelity, open)
|
||||
- OQ-02 (resolved by this ADR), OQ-10 (resolved — the derivation
|
||||
contract lives on `mapping.md`'s exit rows; the PTY-path
|
||||
signal-identity limitation is recorded upstream, alktty/portable-pty
|
||||
side)
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-10-27
|
||||
status: reviewed
|
||||
last_updated: 2026-10-28
|
||||
---
|
||||
|
||||
# Mapping — server direction: SSH channels × family seams (the spine)
|
||||
@@ -29,13 +29,13 @@ bridge primitives every mapping uses (POC-1 §7).
|
||||
|
||||
| SSH channel type | Vanilla use | Feature | Family seam | Contract notes |
|
||||
|---|---|---|---|---|
|
||||
| `session` → `pty_request` + `shell_request` | `ssh host` interactive shell | `session` | alktty: `NegotiateRequest` constructed locally → `drive_session_pre_negotiated` | `pty_request` args → `TerminalParamsWire` (term/cols/rows/pixels; SSH modes → the reserved modes JSON); shell convention per OQ-08; the data plane is a **translation bridge**, not a byte pump — alktty is chunk-framed both directions (stdin/stdout/stderr/CTRL chunks): SSH data→stdin chunks; stdout chunks→SSH data; stderr chunks→`extended_data(1)`; exit chunk→`exit-status`/`exit-signal` + close (POC-3 §D.1) |
|
||||
| `session` → `exec_request` (non-git) | `ssh host "cmd"` | `session` | alktty pipe mode (`terminal: None`) — separate stderr | stderr rides `extended_data(1)` — only valid without PTY; PTY mode merges (kernel PTY, POC-3 §D.1 proven) |
|
||||
| `session` → `pty_request` + `shell_request` | `ssh host` interactive shell | `session` | alktty: `NegotiateRequest` constructed locally → `drive_session_pre_negotiated` | `pty_request` args → `TerminalParamsWire` (term/cols/rows/pixels; SSH modes → the reserved modes JSON); shell argv = the assembly-supplied serve-config shell (no crate default; unset → `shell_request` declines — OQ-08 resolved); the data plane is a **translation bridge**, not a byte pump — alktty is chunk-framed both directions (stdin/stdout/stderr/CTRL chunks): SSH data→stdin chunks; stdout chunks→SSH data; stderr chunks→`extended_data(1)`; exit chunk→`exit-status`/`exit-signal` + close (POC-3 §D.1) |
|
||||
| `session` → `exec_request` (non-git) | `ssh host "cmd"` | `session` | alktty pipe mode (`terminal: None`) — separate stderr | the wire's command string → the configured exec wrapper argv with the command appended as final element (`wrapper ++ [cmd]`, OQ-08 — never naively split); wrapper unset → clean decline; stderr rides `extended_data(1)` — only valid without PTY; PTY mode merges (kernel PTY, POC-3 §D.1 proven) |
|
||||
| `session` → `exec_request` git | `git clone/push ssh://` | `git` | alkgit: parse command → `Service` + repo → `serve_duplex_resolved(repo_id, identity, service, duplex, …)` | **exec-only** (ADR-006 decision 2): the repo rides inside the exec command string (`git-upload-pack '/repo'`); vanilla git never speaks subsystem for git; process-mode dialect (server-speaks-first advertisement); exit-status proven both directions |
|
||||
| `session` → `subsystem_request` | `sftp` clients; manual `ssh -s` | `sftp` (placeholder) / `git` rows | — | **`sftp` name**: declines until alkfs lands (OQ-13) — the feature exists, the route doesn't. **git service names**: decline with reason — a subsystem request carries only a name, never a repo id (POC-3 §A.3); never half-serves |
|
||||
| `session` → `subsystem_request` | `sftp` clients; manual `ssh -s` | `sftp` (placeholder) / `git` rows | — | **`sftp` name**: declines until alkfs lands — the feature exists, the route doesn't (OQ-13 resolved: the tested decline placeholder is the shipped behavior until an alkfs-mapping ADR changes it). **git service names**: decline with reason — a subsystem request carries only a name, never a repo id (POC-3 §A.3); never half-serves |
|
||||
| `direct-tcpip` | `ssh -L`; all `ssh -D` traffic | `tunnel` | alktunnels **route 1**: adapter-side bridge — `TunnelSession::open_direct` on a held `ChannelClient`, pump `take_halves()` vs the channel | target = `SubstrateAddr::HostPort`/`Path`; `Substrate` = Tcp/Unix; accept only **after** the fabric open succeeds — failure maps to an explicit `ConnectFailed` reject (POC-3 §C.2); vanilla `-D`'s dynamic targets dispatch here (POC-3 correction 2) |
|
||||
| `forwarded-tcpip` | `ssh -R` data channels | `tunnel` | the `register_tunnel_forwarded_openable`'s `ForwardedHandler` shape — hand the channel duplex to the handler | listen flow = the global-request rows below; the data leg is untested in POC-3 beyond refusal (see the POC-3 note below) |
|
||||
| `direct-streamlocal` | unix-socket forward | `tunnel` | `SubstrateAddr::Path` treatment | unix-only; decline shape per OQ-11 |
|
||||
| `direct-streamlocal` | unix-socket forward | `tunnel` | `SubstrateAddr::Path` treatment | compiles wherever `tunnel` compiles; serving is **runtime-Unix-gated** — on Unix serves, on other platforms a clean open-decline (OQ-11 resolved: universal compile + runtime decline, no per-platform feature fork) |
|
||||
| `x11` | X forwarding | — (never) | — | always declines; not feature-gated (ADR-003 §5) |
|
||||
| `auth-agent` | agent forwarding | — (never) | — | always declines; the anti-pattern of the alkvault custody posture (ADR-003 §5) |
|
||||
| `session` → SOCKS5-in-channel | (no vanilla use) | — (retired, ADR-003 §1a) | — | vanilla `-D` is client-side SOCKS + `direct-tcpip`; the family local door is alksocks composed at the assembly — see `client.md` |
|
||||
@@ -57,7 +57,9 @@ command → parse-then-fail).
|
||||
|
||||
`Handler::tcpip_forward(address, &mut port) -> bool` +
|
||||
`Handler::streamlocal_forward` — gate on `tunnel` (decline both the
|
||||
listen request and the data channels when absent). The bound-port
|
||||
listen request and the data channels when absent). `streamlocal_forward`
|
||||
additionally carries the same runtime-Unix gate as its channel row
|
||||
(OQ-11): off-Unix it declines. The bound-port
|
||||
out-param (`port = 0` → server picks) is a spec detail in
|
||||
`client.md`'s lifecycle primitives; disposition follows `tunnel`.
|
||||
*POC-3 note: only the refusal shape was exercised (egress-only build);
|
||||
@@ -68,15 +70,57 @@ the served listen flow is implementation-phase contract-test territory.*
|
||||
| SSH request | Family mapping |
|
||||
|---|---|
|
||||
| `pty_request` | `NegotiateRequest.tty = TerminalParamsWire{…}` (POC-3 §D.1: live `stty size` honored) |
|
||||
| `shell_request` | `NegotiateRequest.cmd` per OQ-08 (assembly convention, never wire-supplied) |
|
||||
| `exec_request` | parse → git dispatch (`git`) or alktty pipe `cmd` (`session`); unknown → `channel_failure`, no resource alloc |
|
||||
| `shell_request` | `NegotiateRequest.cmd` = the **assembly-supplied serve-config shell argv** (no crate default; unset → clean decline, never a fallback — OQ-08 resolved; never wire-supplied) |
|
||||
| `exec_request` | parse → git dispatch (`git`) or alktty pipe `cmd` = exec wrapper argv with the command string appended (`wrapper ++ [cmd]`, OQ-08 — vanilla quoting semantics, never naively split; wrapper unset → clean decline); unknown → `channel_failure`, no resource alloc |
|
||||
| `subsystem_request` | decline (per the rows table; honest, with reason) |
|
||||
| `env_request` | **decline by default** (OQ-09 deferred); wire env is not family config |
|
||||
| `env_request` | **declined unconditionally** (OQ-09 resolved; no allowlist ships) — wire env is not family config |
|
||||
| `window_change_request` | resize control (POC-3 §D.1: live resize proven) |
|
||||
| `signal` | signal control (alktty names verbatim); PTY mode reaches the process group, pipe mode is pid-only (backend semantics, recorded in OQ-10) |
|
||||
| `signal` | signal control (alktty names verbatim); PTY mode reaches the process group, pipe mode is pid-only (backend semantics — fidelity note, OQ-10) |
|
||||
| data / `extended_data(1)` | the translation bridge (rows table) |
|
||||
| `exit_status` | from the alktty exit chunk — sent **before** close, from the spawned task via `Session::handle()` (POC-3 §D: proven both legs). `−1` (alktty's could-not-determine sentinel — alktty ADR-004 §4) → `exit-status(255)`, never a signal claim |
|
||||
| `exit_signal` | signal-terminated exits on the **pipe** path: the exit chunk's negative code is the signal — `−N` → name via a Unix signal-number→name table (OQ-10 open — direction fixed, derivation details pending; POC-3 §D.2 observed `−15` → `exit-signal("TERM")` live). `core_dumped` never asserted (working disposition, v1 — alktty discards `ExitStatus::core_dumped()`). **PTY path**: signal-terminated children arrive as a plain code 1 (portable-pty's exit surface; the signal number is unrecoverable there today) → `exit-status(1)` — faithful `exit-signal` on the PTY row is an alktty-backend/portable-pty ask, upstream |
|
||||
| `exit_status` | from the alktty exit chunk — sent **before** close, from the spawned task via `Session::handle()` (POC-3 §D: proven both legs). Derivation contract rules 1, 3–6 below |
|
||||
| `exit_signal` | negative exit-chunk codes (the Unix pipe path's signal encoding) → `exit-signal(name)` — derivation contract rule 2; `−1` → `exit-status(255)`, never a signal claim (rule 3). PTY path: signal-terminated children arrive as a plain code 1 (portable-pty's exit surface) → `exit-status(1)` — the signal-identity loss is an upstream alktty/portable-pty constraint (rule 5) |
|
||||
|
||||
## Exit chunk → SSH exit reporting (the derivation contract)
|
||||
|
||||
The alktty exit chunk (`{"type":"exit","code":N}`, exit-chunk-is-last —
|
||||
alktty ADR-004, alknet ADR-055 lineage) is the sole fidelity input; the
|
||||
chunk is **not widened** (any widening needs its own alktty ADR — the
|
||||
convention is consumed as-is; OQ-10 resolved). Rules, normative for the
|
||||
`exit_status`/`exit_signal` rows above:
|
||||
|
||||
1. **`code ≥ 0`** → `exit-status(N)`.
|
||||
2. **Negative `−N`, N ≥ 2 (the Unix pipe path's signal encoding)** →
|
||||
`exit-signal(name)` via a Unix signal-number→name table (server-side
|
||||
work; signal semantics belong to the serving platform, where the
|
||||
backend reaps). `core_dumped` is **never asserted** — alktty discards
|
||||
`ExitStatus::core_dumped()`, so nothing better is knowable from the
|
||||
chunk, and SSH's `exit-signal` bool stays false. (POC-3 §D.2 observed
|
||||
the full derivation live: signal TERM → `−15` → `exit-signal("TERM")`.)
|
||||
3. **`−1` → `exit-status(255)`** (OpenSSH's conventional failure value).
|
||||
alktty's could-not-determine sentinel collides with the SIGHUP
|
||||
encoding on the Unix pipe path, and this rule accepts the fidelity
|
||||
cost symmetrically: neither a fabricated signal for a wait failure
|
||||
(a signal that never happened) nor a real one — a *genuine* SIGHUP
|
||||
death on the pipe path also reports 255, not `exit-signal("HUP")`.
|
||||
If the wait-failure vs SIGHUP distinction ever becomes load-bearing,
|
||||
alktty's additive `exit_error` control type is the extension seam
|
||||
(recorded in alktty's own ADR); alkssh derives from whatever that
|
||||
carries, with no contract change here.
|
||||
4. **Non-Unix**: no signal termination exists; `−1` is the only
|
||||
negative that can appear → rule 3.
|
||||
5. **The PTY path (fidelity limit, upstream)**: portable-pty 0.9 reaps
|
||||
a signal-terminated Unix child as a plain code 1 (the signal number
|
||||
is unrecoverable through its trait surface — verified against
|
||||
portable-pty 0.9.0's source) → PTY-row signal-terminated sessions
|
||||
arrive as `exit-status(1)`. Faithful `exit-signal` there is an
|
||||
alktty/portable-pty ask with tty/pty-grounded justification; alkssh's
|
||||
contract adapts automatically if the convention ever holds on that
|
||||
path. Pipe-mode pid-only vs PTY process-group *scope* (the `signal`
|
||||
row note) is backend semantics, not a mapping detail.
|
||||
6. **Unmapped negatives → rule 3.** The convention's producers are
|
||||
`−1` and `−signum` only; a negative value with no entry in the
|
||||
signal-name table (defensive case — the mapping never invents a
|
||||
name) reports `exit-status(255)`.
|
||||
|
||||
## The structural constraint: spawn-and-return
|
||||
|
||||
@@ -111,9 +155,9 @@ the whole spine, not an implementation choice.
|
||||
|---|---|---|
|
||||
| none (default) | — | everything |
|
||||
| `git` | git-parseable exec | pty/shell, plain exec, all other channel types, all `session` requests |
|
||||
| `session` | pty/shell/pipe-exec | git-parseable exec (the `git` parse is its gate), other channel types |
|
||||
| `session` | pty/shell/pipe-exec (requires the configured shell/exec wrapper — OQ-08; an unset field declines its row though the feature is on) | git-parseable exec (the `git` parse is its gate), other channel types |
|
||||
| `tunnel` | direct/forwarded-tcpip, streamlocal (unix), `-R` listens | session channels (auto-reject), all others |
|
||||
| any + `sftp` slot | — | `sftp` subsystems decline-with-reason until alkfs |
|
||||
| any build + `sftp` slot | unchanged from the underlying build | `sftp` subsystems decline-with-reason until alkfs |
|
||||
|
||||
Every row is tested in *every* build it can appear in — the decline
|
||||
paths are contract territory (AGENTS.md conventions 5–6), including
|
||||
@@ -121,11 +165,15 @@ under `--all-features`.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- **OQ-08** (shell convention — status: open): blocks this doc's
|
||||
`shell_request`/`NegotiateRequest.cmd` row detail.
|
||||
- **OQ-10** (exit-signal derivation — open: the negative code is the signal encoding on the pipe path [premise corrected]; the `−1` → 255 and core-dumped-never-asserted dispositions on the `exit_status`/`exit_signal` rows here are the working draft, formalized at OQ-10's close; the PTY path's signal-identity loss is recorded upstream, alktty/portable-pty-side).
|
||||
- **OQ-11** (streamlocal platform decline shape — status: open).
|
||||
- ~~OQ-02~~ (dispatch composition — resolved by ADR-006, as amended).
|
||||
None open or deferred — see [open-questions.md](open-questions.md)
|
||||
(all of OQ-01…13 resolved). Doc-local contract anchors: OQ-02 (session
|
||||
dispatch, ADR-006 as amended — §above), OQ-08 (the shell argv / exec
|
||||
wrapper serve config — `shell_request`/`exec_request` rows and the
|
||||
`session→pty+shell` channel row), OQ-09 (the `env_request` row),
|
||||
OQ-10 (§the derivation contract — formalized, upstream constraint
|
||||
noted there), OQ-11 (`direct-streamlocal` row + the listen-flow gate),
|
||||
OQ-13 (the `sftp` rows — the decline placeholder is the shipped
|
||||
behavior).
|
||||
|
||||
## References
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-10-27
|
||||
status: reviewed
|
||||
last_updated: 2026-10-28
|
||||
---
|
||||
|
||||
# Open questions
|
||||
@@ -10,17 +10,18 @@ Centralized tracker for alkssh. Seeded from the phase 0 POC findings
|
||||
the reverse-runner extraction. Numbers are repo-local (family and
|
||||
alkcall trackers have their own OQ spaces).
|
||||
|
||||
**All thirteen questions are resolved.** The tracker holds
|
||||
resolutions only; the genuinely-upstream items it surfaced (the PTY
|
||||
exit limit, the alkvault zeroization gap, the alkfs dependency) are
|
||||
noted at the end and carry their work where their owners are.
|
||||
|
||||
## Theme A — wire & family topology
|
||||
|
||||
### OQ-01: Does an `alk/ssh` relay/browser-leg consumer emerge?
|
||||
### OQ-01: ~~Does an `alk/ssh` relay/browser-leg consumer emerge?~~ — resolved
|
||||
|
||||
- **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.
|
||||
- **Status**: resolved — ADR-001 is the entire decision surface; what remained was a forecast about the world, not an alkssh decision.
|
||||
- **Resolution**: no relay/browser-leg consumer is in scope and no crate behavior is gated on one. ADR-001 decided everything decidable now: the `alk/ssh` pin's raw-stream contract (§1), payload-blind relay as part of the contract (§2), the phase-1 edge-termination-only scope (§3), and future-leg compatibility by construction (§4). Whether a consumer ever materializes is an assembly/family use-case question — a future leg arrives via its own ADR referencing ADR-001's already-fixed contract and would exercise `client.md`'s accept surface in a new role. Nothing in the crate waits on this. (Door-type note retained for the record: the pin is one-way; consumer *existence* was never an architecture decision.)
|
||||
- **Cross-references**: ADR-001, OQ-13
|
||||
|
||||
## Theme B — session-channel dispatch & mapping details
|
||||
@@ -37,15 +38,12 @@ alkcall trackers have their own OQ spaces).
|
||||
- **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?
|
||||
### OQ-04: ~~OpenSSH certificate auth — is there ever a CA-trust use case?~~ — resolved
|
||||
|
||||
- **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.
|
||||
- **Status**: resolved — the decline is the settled disposition; the "is there ever" remainder was a forecast.
|
||||
- **Resolution**: `auth_openssh_certificate` is declined unconditionally (ADR-004 decision 3: explicit reject implementation, contract-tested, POC-1-proven). There is no CA-trust story in the family identity model — exactly one SSH-shaped resolution path exists (verified key fingerprint → `IdentityProvider::resolve_from_fingerprint` → `Identity`). A certificate trust model would first require the family identity model to gain one upstream (alkcall identity territory) and would then enter alkssh as a new ADR — an auth-posture change is wire-visible. Nothing in the crate waits on this; no CA use case exists.
|
||||
- **Cross-references**: ADR-004, [auth.md](auth.md) §out-of-scope
|
||||
|
||||
## Theme C — auth, identity & key custody
|
||||
|
||||
@@ -71,75 +69,74 @@ alkcall trackers have their own OQ spaces).
|
||||
|
||||
- **Origin**: [poc-2-vault-keys.md](../research/poc-2-vault-keys.md) §2 + OQ seed 1
|
||||
- **Status**: resolved — the alkssh-level answer is the adapter-side wrap; the vault-side improvement belongs upstream, not here.
|
||||
- **Resolution**: **ADR-005 decision 3 is the contract on this path**: the adapter owns a `Zeroizing<String>` wrap at the seam (POC-2-proven; bounds the plaintext from adapter-takeover onward; ssh-key zeroizes the decoded key bytes on drop). The remaining unfenced pre-wrap window is **alkvault's API shape to close** — the ask is filed in alkvault's own tracker (downstream/family-space concern; alkforge and every secrets-bearing crate benefits, not just alkssh). alkssh does not carry it, does not gate on it, and ADR-005 records only the adapter obligation. This tracker entry stays closed; when alkvault ships a `Zeroizing`-out API, ADR-005's reference here updates as a one-line doc touch, nothing else.
|
||||
- **Resolution**: **ADR-005 decision 3 is the contract on this path**: the adapter owns a `Zeroizing<String>` wrap at the seam (POC-2-proven; bounds the plaintext from adapter-takeover onward; ssh-key zeroizes the decoded key bytes on drop). The remaining unfenced pre-wrap window is **alkvault's API shape to close** — the ask is filed in alkvault's own tracker (downstream/family-space concern; alkforge and every secrets-bearing crate benefits, not just alkssh). alkssh does not carry it, does not gate on it, and ADR-005 records only the adapter obligation. When alkvault ships a `Zeroizing`-out API, ADR-005's reference here updates as a one-line doc touch, nothing else.
|
||||
- **Cross-references**: ADR-005 §3, OQ-05
|
||||
|
||||
## Theme D — session-request mapping details
|
||||
|
||||
### OQ-08: Shell command convention for `shell_request`
|
||||
### OQ-08: ~~Shell command convention for `shell_request`~~ — resolved
|
||||
|
||||
- **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`.
|
||||
- **Status**: resolved — assembly-supplied serve-config field; no crate default.
|
||||
- **Resolution**: the shell convention is **assembly-supplied serve configuration** — the crate bakes no default, and each unset field means its request **declines with a clean error** (never a silent fallback). Grounding for the shape: alktty's contract requires a non-empty `cmd` vector (there is no pass-through-absence seam at `drive_session`), the wire supplies nothing (`shell_request` carries no command), and the crate is not an sshd — it has no account/passwd model from which "the user's shell" could be derived, so a crate-level default (`bash`, `sh`) would be deployment policy smuggled into the crate (the composability boundary, `overview.md`). Two fields, each governing its own row:
|
||||
|
||||
### OQ-09: `env_request` policy — is an allowlist ever wanted?
|
||||
1. **shell argv** — what `shell_request` runs verbatim as `NegotiateRequest.cmd`.
|
||||
2. **exec wrapper argv** (e.g. `["/bin/sh", "-c"]`) — what a non-git `exec_request` translates to: the wire's single command string is **appended as the wrapper argv's final element** (`wrapper ++ [cmd]`), preserving vanilla `ssh host "cmd"` quoting/expansion semantics — command strings are never naively split (that would break quoting). The wrapper form is platform-neutral: the assembly configures whatever its fleet's shell is.
|
||||
|
||||
Unset field → its row declines; a `session` build with no configured shell serves neither shell nor exec (fail-closed — no half-capability where the feature is on). The git row is unaffected (parse to `alkgit` first — it never needs a shell). `cwd`/`env` in `NegotiateRequest` are assembly-supplied serve-config values, never wire-derived (`env_request` stays declined — OQ-09). Recorded on `mapping.md`'s `shell_request`/`exec_request` rows.
|
||||
- **Cross-references**: OQ-09, [mapping.md](mapping.md)
|
||||
|
||||
### OQ-09: ~~`env_request` policy — is an allowlist ever wanted?~~ — resolved
|
||||
|
||||
- **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.
|
||||
- **Status**: resolved — decline-by-default is the fixed contract; no allowlist ships.
|
||||
- **Resolution**: `env_request` is declined **unconditionally**. Wire-supplied environment is not family config: session env comes from assembly serve configuration (`NegotiateRequest.env`), never from the wire. The only open shape was whether an allowlist ships — it does not; a future allowlist would be a new ADR (a serving capability is wire-visible) on a concrete consumer need, of which none exists.
|
||||
- **Cross-references**: OQ-08, [mapping.md](mapping.md) (`env_request` row)
|
||||
|
||||
### OQ-10: SSH `exit-signal` fidelity over the alktty exit-code convention
|
||||
### OQ-10: ~~SSH `exit-signal` fidelity over the alktty exit-code convention~~ — resolved (closed as contract + upstream constraint)
|
||||
|
||||
- **Origin**: `mapping-table.md` (exit_signal row), [poc-3-mapping-bakeoff.md](../research/poc-3-mapping-bakeoff.md) §D.2. **Premise corrected 2026-10-27**: the POC seed's "the exit chunk carries no signal identity — alktty might widen the chunk" framing was wrong; the negative code *is* the signal encoding (below), integrated into the seed here rather than argued after the fact.
|
||||
- **Status**: open — direction fixed, derivation details pending. Direction fixed by review: alkssh derives SSH fidelity from the exit chunk alktty already carries; widening the chunk (or `TtyHandle.exit_code → i32`) is not a justified candidate by default — "easier for alkssh" is not a tty/pty-grounded justification, and the change ripples `ControlMessage::Exit`'s parsed shape, the backend API, and the alknet ADR-055 lineage. Any widening needs its own alktty ADR with the full consequence analysis.
|
||||
- **The convention alkssh consumes**: `{"type":"exit","code":N}` on ctrl-out, exit-chunk-is-last (alktty ADR-004, ported from alknet ADR-055). `code` semantics: `ExitStatus::code()` on normal exit; on Unix, the negative signal number on signal termination — alktty's own convention (`ExitStatus::code()` returns `None` there), implemented per-backend (`pipe.rs::exit_code_from` via `ExitStatusExt::signal()`; POC-3 §D.2 observed it live: signal TERM → `−15` → SSH `exit-signal("TERM")` via the cloned `Session::handle()`).
|
||||
- **Derivation details** (mapping-contract territory; the working dispositions live on `mapping.md`'s exit rows until formalized there / as an ADR-006 amendment):
|
||||
1. **Name derivation**: SSH `exit-signal` requires a name; `−N → name` is alkssh server-side work, not a wire change (a Unix signal-number→name table — signal semantics belong to the serving platform, where the backend reaps).
|
||||
2. **Core-dumped**: alktty discards `ExitStatus::core_dumped()`, so nothing better is knowable from the chunk; SSH's `exit-signal` carries a bool. Working disposition: never asserted, v1.
|
||||
3. **`−1` is never a signal claim.** Per alktty ADR-004, `-1` is the backend could-not-determine sentinel — and on the Unix pipe path the same value is the SIGHUP encoding (signal 1). Working disposition: `exit-status(255)` (OpenSSH's conventional failure value) — fabricating `exit-signal("HUP")` for a wait failure would invent a signal that never happened. alktty's additive `exit_error` control type is the extension seam recorded in its own ADR if the distinction ever becomes load-bearing.
|
||||
4. **Non-Unix**: no signal termination; `−1` is the only negative that can appear → rule 3.
|
||||
5. **Pipe-mode pid-only vs PTY process-group signal scope** is backend semantics, not a mapping change — recorded as a fidelity note on `mapping.md`'s `signal` row.
|
||||
- **PTY path — the real fidelity limit, backend-side.** portable-pty 0.9 reaps a signal-terminated Unix child through `From<std::process::ExitStatus>`, which converts it to `code = code().unwrap_or(1)` → **1**, keeping the signal only as a locale-dependent `strsignal` string field (verified against portable-pty 0.9.0's source); the number is unrecoverable through its trait surface. alktty's PTY waiter (`pty.rs`) reads `exit_code()` only and discards that string, so a signal-killed PTY session arrives as a plain exit 1 → alkssh maps `exit-status(1)`. Faithful `exit-signal` for the `session`/PTY row needs the number — an alktty/portable-pty ask with tty/pty-grounded justification, not an alkssh convenience ask; alkssh adapts automatically with no contract change if the convention ever holds on that path.
|
||||
- **Cross-references**: OQ-08, [mapping.md](mapping.md) (`exit_signal`, `exit_status`, `signal` rows), ADR-006, alktty ADR-004 / alknet ADR-055
|
||||
- **Origin**: `mapping-table.md` (exit_signal row), [poc-3-mapping-bakeoff.md](../research/poc-3-mapping-bakeoff.md) §D.2. **Premise corrected 2026-10-27** (with the alktty side): the POC seed's "the exit chunk carries no signal identity — alktty might widen the chunk" framing was wrong; the negative code *is* the signal encoding. Integrated into the seed rather than argued after the fact.
|
||||
- **Status**: resolved — as asked, this was never a decision to make: the convention alkssh consumes does not change (alktty's exit-chunk convention, alktty ADR-004 / alknet ADR-055 lineage — no widening proposed or justified; "easier for alkssh" is not a tty/pty-grounded justification, and any widening ripples `ControlMessage::Exit`'s parsed shape, the backend API, and that ADR lineage, so it needs its own alktty ADR). What the entry surfaced was (a) a *derivation contract* on alkssh's side and (b) an upstream *fidelity limitation* — both now recorded where they constrain.
|
||||
- **Resolution**:
|
||||
- **The derivation contract** is formalized as `mapping.md` §"Exit chunk → SSH exit reporting" (the contract behind the `exit_status`/`exit_signal` rows): `code ≥ 0` → `exit-status(N)`; negative `−N` on the Unix pipe path → `exit-signal(name)` via a server-side Unix signal-number→name table; `−1` → `exit-status(255)` and never a signal claim (the sentinel collides with SIGHUP; fabricating `exit-signal("HUP")` for a wait failure invents a signal that never happened); non-Unix negatives → the `−1` rule; `core_dumped` never asserted. POC-3 §D.2 observed the pipe-path derivation live (`−15` → `exit-signal("TERM")`).
|
||||
- **The upstream constraint**: the PTY path loses the signal number (portable-pty 0.9 reaps a signal-terminated Unix child as a plain code 1 through its trait surface — verified against its source) → PTY-row signal-terminated sessions arrive as `exit-status(1)`. Faithful `exit-signal` on the PTY row is an **alktty/portable-pty ask with tty/pty-grounded justification**; alkssh's mapping adapts automatically with no contract change if the convention ever holds on that path. Recorded as rule 5 of `mapping.md`'s derivation contract (not tracked here — it constrains alktty, not alkssh).
|
||||
- **Cross-references**: OQ-08, [mapping.md](mapping.md) (derivation contract + `exit_signal`/`exit_status`/`signal` rows), ADR-006, alktty ADR-004 / alknet ADR-055
|
||||
|
||||
### OQ-11: `direct-streamlocal` platform surface — compile-time or runtime decline shape?
|
||||
### OQ-11: ~~`direct-streamlocal` platform surface — compile-time or runtime decline shape?~~ — resolved
|
||||
|
||||
- **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.
|
||||
- **Status**: resolved — universal compile, runtime decline.
|
||||
- **Resolution**: `direct-streamlocal` compiles wherever `tunnel` compiles; the serve path is **runtime-Unix-gated** — on Unix it serves (`SubstrateAddr::Path` treatment), on every other platform it declines with a clean SSH open-failure (never a half-open). This wins over a unix-only compile split: no per-platform feature fork (the feature graph stays flat and platform-independent — ADR-003), the decline matrix stays uniform across builds, and the decline path is contract-tested in every build like every other decline (fail-closed posture). The research row's "compile-time feature + runtime decline" hybrid collapses to the runtime-decline half — no platform-conditional feature is needed.
|
||||
- **Cross-references**: ADR-003, [mapping.md](mapping.md) (`direct-streamlocal` row + the `-R` listen flow)
|
||||
|
||||
### 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.
|
||||
- **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` §Lifecycle primitives; no reconnect policy is baked into the crate.
|
||||
- **Cross-references**: [client.md](client.md), `reverse-runner.md`
|
||||
|
||||
## Theme E — deferred features
|
||||
## Theme E — future features
|
||||
|
||||
### OQ-13: The `sftp` client+server halves when alkfs lands
|
||||
### OQ-13: ~~The `sftp` client+server halves when alkfs lands~~ — resolved
|
||||
|
||||
- **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
|
||||
- **Status**: resolved — the alkssh-side answer is fully decided; the remainder belongs to alkfs, not to this tracker.
|
||||
- **Resolution**: the `sftp` feature ships as the **tested decline placeholder in both directions** from the first build — the feature exists in the graph (ADR-003), advertisement is honest, and the decline contract is tested on both sides (`mapping.md`/`client.md`). That *is* the shipped behavior for as long as alkfs is pending, and it needs no further decision. What remained "pending" was the eventual alkfs-protocol mapping — a decision downstream of alkfs' design (family ADR-002; alkssh follows alkfs' maturity): alkssh writes that mapping and its ADR when the target protocol exists to map, the same way every other row's mapping was decided before implementation. No alkssh decision waits on this; there is no alkssh-side sftp work.
|
||||
- **Cross-references**: OQ-01, ADR-003, [client.md](client.md) §op rows, family ADR-002
|
||||
|
||||
## Deferred / Blocked (at a glance)
|
||||
## Upstream watch items (not OQs)
|
||||
|
||||
| OQ | Blocked on | Priority |
|
||||
|---|---|---|
|
||||
| OQ-01 | concrete relay/browser-leg consumer | medium |
|
||||
| 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) |
|
||||
Recorded so the dependency is visible, with the work carried where
|
||||
their owners are:
|
||||
|
||||
- **PTY-path exit fidelity** — needs alktty/portable-pty work (with
|
||||
tty/pty-grounded justification) before faithful `exit-signal` can
|
||||
hold on the PTY row; alkssh adapts with no contract change
|
||||
(`mapping.md` derivation contract, rule 5; OQ-10).
|
||||
- **alkfs landing** — the `sftp` placeholder's eventual replacement
|
||||
(family ADR-002); alkssh writes the sftp mapping + ADR when the
|
||||
protocol exists (OQ-13).
|
||||
- **alkvault `Zeroizing`-out `decrypt`** — closes the pre-wrap
|
||||
plaintext window upstream (OQ-07 / ADR-005 §3); a one-line doc touch
|
||||
for alkssh when it ships.
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-10-27
|
||||
status: reviewed
|
||||
last_updated: 2026-10-28
|
||||
---
|
||||
|
||||
# Overview — alkssh
|
||||
@@ -35,7 +35,7 @@ pin is ADR-002.
|
||||
| SSH channel-type → family-protocol mapping | **this crate** (the spine, `mapping.md` / `client.md`) |
|
||||
| Identity (fingerprint → `Identity`) | alkcall `IdentityProvider`, assembly-implemented; never the vault (vault = secrets *we* hold) |
|
||||
| Secrets we hold (client dial keys, host keys) | alkvault-backed sources (ADR-005) |
|
||||
| The `alk/ssh` ALPN | ADR-001 (edge-termination contract; relay/browser legs deferred, OQ-01) |
|
||||
| The `alk/ssh` ALPN | ADR-001 (edge-termination contract; relay/browser legs have no consumer in scope — OQ-01 resolved) |
|
||||
| Deployment policy (which features, which ACLs) | the embedding assembly (alkforge) |
|
||||
|
||||
Composability boundary: alkssh is a door, not a backend — it depends
|
||||
@@ -89,9 +89,9 @@ plus the knobs above are the whole posture.
|
||||
|
||||
Per `vision.md`: no in-family SSH protocol machinery (ever); not an
|
||||
sshd (no local accounts, no OpenSSH `authorized_keys` semantics, no
|
||||
PAM); no sftp serving until alkfs lands (OQ-13 — the feature exists as
|
||||
a declining placeholder); no binary/CLI; no password or
|
||||
keyboard-interactive auth in either direction; no x11 or agent
|
||||
PAM); no sftp serving until alkfs lands (OQ-13 resolved — the feature
|
||||
exists as a tested declining placeholder); no binary/CLI; no password
|
||||
or keyboard-interactive auth in either direction; no x11 or agent
|
||||
forwarding at all (not even feature-gated — ADR-003 §5).
|
||||
|
||||
## Lifecycle note
|
||||
|
||||
Reference in new issue
Block a user