docs(architecture): close the OQ-07/OQ-10 upstream watch items; amend ADR-005 §3 + the mapping rows
- ADR-005 decision 3 amended (the gap is closed upstream: alkvault 0.2.0's Zeroizing<String> return on every path incl. the error one; the adapter wrap dropped — the double wrap would be dead posture); the Negative consequence amended; the vault-surface note updated. - mapping.md: the forwarded-tcpip row rides the gate shape (typed call-plane miss, residual race re-scoped); derivation-contract rule 5 amended (the PTY flattening constraint retired, alktty 0.8.2 — no contract change); the exit_signal row + OQ anchors updated. - open-questions.md: OQ-07 + OQ-10 amended with the upstream closures; the two closed watch items struck with pointers. - client.md: the OQ-07 anchor updated.
This commit is contained in:
1 parent
4668061b19
commit
9f443e982b
4 files changed
+61
-43
No files matched your search
@@ -200,8 +200,9 @@ 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).
|
||||
rows and §not-include), OQ-07 (closed upstream — the vault returns a
|
||||
`Zeroizing<String>` from alkvault 0.2.0; ADR-005 decision 3 amended,
|
||||
the adapter-side wrap dropped).
|
||||
|
||||
## References
|
||||
|
||||
|
||||
@@ -28,8 +28,9 @@ the server direction it holds the host key. Family rules in force:
|
||||
— that path is the assembly-layer `IdentityProvider` (scope note in
|
||||
`pocs.md` §POC-2).
|
||||
|
||||
alkvault 0.1.0's `encrypt`/`decrypt` surface is `&str` in, `String`
|
||||
out (`EncryptedData` as the wrapper) — a decisive constraint on any
|
||||
alkvault's `encrypt`/`decrypt` surface is `&str` in, `Zeroizing<String>`
|
||||
out since 0.2.0 (0.1.0 returned a plain `String`; the `EncryptedData`
|
||||
wrapper unchanged) — a decisive constraint on any
|
||||
binary inner format (POC-2 measured: it would force base64 layers on
|
||||
both ends).
|
||||
|
||||
@@ -55,17 +56,25 @@ both ends).
|
||||
only at dial time: locking it mid-session keeps live sessions
|
||||
streaming while new dials fail (POC-2 verified); unlock is
|
||||
side-effect-free on key identity.
|
||||
3. **Zeroization ownership (the known gap).** ssh-key zeroizes
|
||||
ed25519 key bytes on `Drop` (decoded keys, and their clones, clean
|
||||
themselves). The *decrypted plaintext* is the gap: alkvault's
|
||||
`decrypt` returns a plain `String`, so from the vault's return to
|
||||
the adapter's wrap the plaintext sits in unfenced heap. The
|
||||
adapter owns the wrap (proven in POC-2) — **that is the contract on
|
||||
this path**; the vault-side improvement (a `Zeroizing<String>`-out
|
||||
API) is an upstream ask filed in alkvault's own tracker, not an
|
||||
alkssh obligation — when it lands, this ADR's pointer updates as a
|
||||
one-line doc touch (alkssh OQ-07 is closed at the
|
||||
adapter-convention level).
|
||||
3. **Zeroization ownership (the gap — closed upstream).** ssh-key
|
||||
zeroizes ed25519 key bytes on `Drop` (decoded keys, and their
|
||||
clones, clean themselves). The *decrypted plaintext* was the gap:
|
||||
alkvault's `decrypt` once returned a plain `String`, leaving the
|
||||
plaintext in unfenced heap between the vault's return and the
|
||||
adapter's wrap — the adapter owned the wrap then (proven in POC-2;
|
||||
**that was the contract on this path**) and the vault-side
|
||||
improvement (a `Zeroizing<String>`-out API) was an upstream ask
|
||||
filed in alkvault's own tracker. The ask shipped in **alkvault
|
||||
0.2.0** (2026-11-04, the tracker's review 001 — this ADR is the
|
||||
origin it cites): `decrypt` returns `Zeroizing<String>`, the buffer
|
||||
zeroizes on every path (including the UTF-8 error path), and
|
||||
`rotate` transits zeroized output too. The adapter seam wrap is
|
||||
thereby **dropped** (the alkssh 0.2.0 adoption): `decrypt_and_decode`
|
||||
consumes the vault's zeroized return directly — the double wrap
|
||||
would be dead posture. Decision 3's ownership now sits entirely
|
||||
with alkvault; the adapter's obligation shrinks to consuming the
|
||||
zeroized value in one pass (the `decode_key_arm` signature keeps
|
||||
riding the `Zeroizing<String>` type as the proof).
|
||||
4. **Failure modes are all fail-closed, with distinct error shapes.**
|
||||
Locked vault → no dial (and no server: a host key that can't load
|
||||
prevents server construction — verified at bootstrap). Absent
|
||||
@@ -107,10 +116,11 @@ both ends).
|
||||
future held material; every failure mode is proven fail-closed; the
|
||||
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 filed with its owner (the alkvault tracker;
|
||||
OQ-07 resolved at the adapter level), not silently absorbed.
|
||||
- **Negative**: ~~decrypted plaintext has one unfenced window (before
|
||||
the adapter's wrap) until alkvault changes its API~~ — the gap is
|
||||
resolved (alkvault 0.2.0's `Zeroizing<String>` return; the adapter
|
||||
wrap dropped, OQ-07 amended). No plaintext window remains on this
|
||||
ADR's path.
|
||||
- **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).
|
||||
|
||||
@@ -34,7 +34,7 @@ bridge primitives every mapping uses (POC-1 §7).
|
||||
| `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 — 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 | per fabric arrival the door opens a `forwarded-tcpip` channel **toward the vanilla client** (a vanilla client never initiates this type — sshd parity; an inbound open is the unvanilla class, declined `ConnectFailed`); echo args = the original `tcpip_forward` request's wire string verbatim + the actually-bound port (the sshd `c->path` + `get_local_port` parity — a `port = 0` mint echoes the allocated number); originator = `TunnelForwardedParams.peer`; correlation-miss → teardown-at-birth (never a half-open channel); the accept is the crate's per-session channel cap (the acceptor vocabulary) |
|
||||
| `forwarded-tcpip` | `ssh -R` data channels | `tunnel` | the `register_tunnel_forwarded_openable_with_establisher` gate shape (alktunnels 0.5.0, review-003 W2 adoption): the router's correlation lookup rides the establisher's `ForwardedGate`; the handler's `ForwardedHandler` shape hands the channel duplex to the pump | per fabric arrival the door opens a `forwarded-tcpip` channel **toward the vanilla client** (a vanilla client never initiates this type — sshd parity; an inbound open is the unvanilla class, declined `ConnectFailed`); echo args = the original `tcpip_forward` request's wire string verbatim + the actually-bound port (the sshd `c->path` + `get_local_port` parity — a `port = 0` mint echoes the allocated number); originator = `TunnelForwardedParams.peer`; a stale/missing correlation token fails the open **typed at the call plane** (`channel:open_failed`, `unknown_resource`) before any channel is born (the gate's miss class); the narrow gate-pass→handler-run residual race and the cap-admission declines still teardown-at-birth (never a half-open channel); the accept is the crate's per-session channel cap (the acceptor vocabulary) |
|
||||
| `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) |
|
||||
@@ -58,8 +58,10 @@ command → parse-then-fail).
|
||||
`Handler::tcpip_forward(address, &mut port) -> bool` +
|
||||
`Handler::streamlocal_forward` (+ both cancel forms) — gate on
|
||||
`tunnel` (unfeatured declines both the listen requests and the data
|
||||
channels). The served flow (`w3-tunnel-listen`, decidable against
|
||||
alktunnels 0.4.1's actual surfaces with POC-3 §C.4 as the template):
|
||||
channels). The served flow (`w3-tunnel-listen`; the route-1 classes
|
||||
decided against alktunnels 0.4.1's recorded surfaces, the
|
||||
establisher shape against 0.5.0 — the review-003 W2 adoption — with
|
||||
POC-3 §C.4 as the template):
|
||||
|
||||
- **The listen dial** — family-ward through the consumer surface:
|
||||
`TunnelListener::open(channel_client, TunnelParams {resource: <the
|
||||
@@ -135,7 +137,7 @@ alktunnels 0.4.1's actual surfaces with POC-3 §C.4 as the template):
|
||||
| `signal` | signal control (alktty names verbatim — no door-side filter; PTY mode reaches the process group, pipe mode is pid-only — backend semantics, fidelity note, OQ-10); fire-and-forget, no decline shape |
|
||||
| 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). 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_signal` | negative exit-chunk codes → `exit-signal(name)` — derivation contract rule 2; `−1` → `exit-status(255)`, never a signal claim (rule 3). Both the pipe **and** the PTY path report signal deaths as `−N` (alktty 0.8.2 reaps the PTY child directly — the former portable-pty flattening constraint retired; rule 5) |
|
||||
|
||||
## Exit chunk → SSH exit reporting (the derivation contract)
|
||||
|
||||
@@ -165,14 +167,17 @@ convention is consumed as-is; OQ-10 resolved). Rules, normative for the
|
||||
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`
|
||||
5. **The PTY path (constraint retired, alktty 0.8.2)**: portable-pty
|
||||
0.9's `ExitStatus` flattens a signal-terminated Unix child to a
|
||||
plain code 1, so PTY-row signal deaths once arrived as
|
||||
`exit-status(1)`. alktty 0.8.2 retires the constraint (its Unix
|
||||
PTY waiter skips portable-pty's conversion and reaps the direct
|
||||
child via `waitpid`, decoding signal deaths as `−N`); the PTY path
|
||||
now reports signal termination exactly like the pipe path, and the
|
||||
derivation here needed **no contract change** — the negative-code
|
||||
convention is consumed verbatim (OQ-10's upstream constraint
|
||||
closed). 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
|
||||
@@ -227,8 +232,8 @@ None open or deferred — see [open-questions.md](open-questions.md)
|
||||
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-10 (§the derivation contract — formalized; the former upstream PTY
|
||||
constraint is retired as of alktty 0.8.2, rule 5), OQ-11 (`direct-streamlocal` row + the listen-flow gate),
|
||||
OQ-13 (the `sftp` rows — the decline placeholder is the shipped
|
||||
behavior).
|
||||
|
||||
|
||||
@@ -68,8 +68,8 @@ noted at the end and carry their work where their owners are.
|
||||
### OQ-07: ~~Is the alkvault `decrypt` plain-`String` zeroization gap closed at the vault or at the adapter?~~ — resolved (at this level)
|
||||
|
||||
- **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. When alkvault ships a `Zeroizing`-out API, ADR-005's reference here updates as a one-line doc touch, nothing else.
|
||||
- **Status**: resolved — the alkssh-level answer was the adapter-side wrap; the vault-side improvement belongs upstream, not here. **Superseded upstream 2026-11-04 (alkvault 0.2.0)**: the ask shipped — `decrypt` returns `Zeroizing<String>`, zeroizing the buffer on every path including the error one, and the `rotate` path never holds a bare plaintext either.
|
||||
- **Resolution**: **ADR-005 decision 3 was the contract on this path**: the adapter owned 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 was **alkvault's API shape to close** — the ask was filed in alkvault's own tracker (downstream/family-space concern; alkforge and every secrets-bearing crate benefits, not just alkssh). alkssh did not carry it, did not gate on it, and ADR-005 recorded only the adapter obligation. With alkvault 0.2.0 the window is closed upstream: the adapter-side wrap is dropped (a double wrap would be dead posture), `decrypt_and_decode` consumes the vault's zeroized return directly, and ADR-005's decision-3 pointer is amended — the predicted one-line doc touch, exactly.
|
||||
- **Cross-references**: ADR-005 §3, OQ-05
|
||||
|
||||
## Theme D — session-request mapping details
|
||||
@@ -99,7 +99,7 @@ noted at the end and carry their work where their owners are.
|
||||
- **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).
|
||||
- **The upstream constraint** (superseded 2026-11-04, alktty 0.8.2): the PTY path used to lose 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). alktty 0.8.2 retired that constraint consumer-input-driven (alktty review 004, this entry's bookkeeping): its Unix PTY waiter now reaps the direct child via `waitpid` and decodes signal deaths as `−N`, so the convention holds on both paths and the PTY-row signal-terminated session reports `exit-signal(name)` with **no alkssh contract change** (the negative code is consumed verbatim; `mapping.md` rule 5 amended). The `-1` sentinel collision (SIGHUP vs wait-failure) remains on the pipe path and gains the cancel path — the rule-3 reading stands unchanged.
|
||||
- **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?~~ — resolved
|
||||
@@ -130,13 +130,15 @@ noted at the end and carry their work where their owners are.
|
||||
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.
|
||||
- ~~**PTY-path exit fidelity**~~ — closed upstream (alktty 0.8.2,
|
||||
review 004's resolution): the PTY waiter now reports signal deaths
|
||||
as `−N`; the derivation contract needed no alkssh change
|
||||
(`mapping.md` rule 5 amended; OQ-10).
|
||||
- ~~**alkvault `Zeroizing`-out `decrypt`**~~ — closed upstream
|
||||
(alkvault 0.2.0, review 001's resolution; also closes the `rotate`
|
||||
window and zeroizes the error path): the adapter seam wrap is
|
||||
dropped, ADR-005 decision 3 + OQ-07 amended as the predicted
|
||||
one-line doc touch.
|
||||
Reference in new issue
Block a user