docs(plans): add wave-based implementation plan; decompose wave 1

- docs/plans/implementation.md: five-wave decomposition of the
  architecture (foundations -> server stack -> server mappings ||
  client direction -> validation & release), following the alkgit
  sibling pattern (decompose-implement-gate rhythm, per-wave
  decomposition records, review gates at wave boundaries)
- scope authority: v0.1.0 publishes the crate; assembly wiring stays
  out; sftp decline placeholder is shipped behavior
- wave 1 decomposed into eight task files under tasks/implementation/
  (manifest, error model, key sources, host-key policy, fingerprint,
  config surface, test doubles, review gate) with recorded
  interpretations flagged for the gate

Verification: taskgraph validate (12 tasks, no cycles); cargo test,
clippy -D warnings, fmt --check green
This commit is contained in:
glm-5.3-flash committed 2026-10-28 06:13:26 +00:00
1 parent ab6fb9de87
commit 0b1bc284fa
9 files changed
+882

No files matched your search

+69
View File
@@ -0,0 +1,69 @@
---
id: review-wave-1
name: Wave 1 gate — the foundations review
status: pending
depends_on: [w1-manifest, w1-error-model, w1-key-sources, w1-host-key-policy, w1-fingerprint, w1-config, w1-test-doubles]
scope: narrow
risk: low
impact: phase
level: review
---
## Description
The wave-1 review gate — the cheap-place gate. The config types, the
error taxonomy, and the key-source seam are one-way doors in practice:
every later task implements against them, and the published crate
freezes them. A shape error found here costs a task revision; found
after the stack and mappings implement against them it costs churn
across both directions.
The gate reads the landed surface against the architecture line by
line:
- ADR-002 — the pin as declared (russh 0.65, `aws-lc-rs` only, no
`flate2`/`rsa` in the tree, edition 2024, MSRV 1.89); the recorded
`ssh-key` RC exception verified in `cargo tree`.
- ADR-003 — the feature graph shape (flat, independent, `default =
[]`); the recorded interpretation (per-feature sibling deps land
with their first consuming wave) confirmed or corrected.
- ADR-004 — the fingerprint format invariant (parity vectors green);
the `max_auth_attempts` default posture in `Limits`.
- ADR-005 — the custody chain (decrypt-at-use, `Zeroizing` wrap,
distinct failure variants, `EncryptedData` opaque, host-key load
path, plaintext never persists).
- OQ-05 — the policy seam (fail-closed default, no accept-all
built-in, policy-blind crate).
- OQ-08/09 — the serve-config `Option`-field semantics (unset →
decline, no crate default, no construction-time validation drift).
- The error model — no catch-alls; auth declines are dispositions, not
errors.
- Docs honesty — module docs state what is deliberately not here yet
(the stack, the mappings, the client direction).
Verdict: green (wave 2 decomposes, absorbing corrections) or findings
booked (remediation before wave 2 decomposes).
## Acceptance Criteria
- [ ] Every ADR/OQ surface above read and confirmed (or corrected
with the correction landed)
- [ ] The recorded interpretations confirmed or fixed (sibling-dep
timing; the vault-double shape; the policy verdict type)
- [ ] Full verification set green (`cargo test`, `--all-features`,
clippy, fmt)
- [ ] Verdict recorded; wave 2 decomposition unblocked or findings
booked
## References
- docs/plans/implementation.md (wave 1 section + this gate's role)
- docs/architecture/decisions/001…006, open-questions.md
## Notes
> To be filled by implementation agent
## Summary
> To be filled on completion
+83
View File
@@ -0,0 +1,83 @@
---
id: w1-config
name: Wave 1 — the config surface (serve-config, dial-config, Limits)
status: pending
depends_on: [w1-manifest, w1-key-sources, w1-host-key-policy]
scope: moderate
risk: low
impact: project
level: implementation
---
## Description
Land the pure configuration types both directions implement against —
the shapes every later wave's constructors take:
- **Serve-config** (server direction, `mapping.md`/`overview.md`) —
the assembly-supplied serving configuration: the OQ-08 fields
(**shell argv** and **exec wrapper argv**, each `Option<Vec<String>>`
— unset → its row declines with a clean error, never a fallback, no
crate default; the wrapper form is platform-neutral), the
`IdentityProvider` handle (`Arc<dyn IdentityProvider>` — the
assembly injects it; the crate never resolves identities itself),
the host-key source handle (the wave-1 key-source seam), the tunnel
`ChannelClient` handle (the route-1 bridge's family-fabric entry —
`Option`, absent → tunnel rows decline even under the feature), and
the `Limits`. `cwd`/`env` in `NegotiateRequest` are assembly-supplied
serve-config values, never wire-derived (OQ-09 posture).
- **Dial-config** (client direction, `client.md` §The dial surface) —
address, key source (the wave-1 seam — never static keys, never env
vars), the `HostKeyPolicy` handle, and the bounded-resource knobs:
`keepalive_interval`/`keepalive_max` (15 s / 3 default posture per
the reverse-runner evidence), `inactivity_timeout`, window/packet
knobs.
- **`Limits`** — the bounded-resources budget model (`overview.md`'s
table): the in-crate max-concurrent-channels cap, exec-parse length
caps (consumed by the wave-3 grammar), and the russh knob mapping
with documented default postures — `max_auth_attempts` default 10
(sized for the counting `none`-probe, ADR-004 decision 6),
`window_size`/`maximum_packet_size`/`channel_buffer_size` set,
`inactivity_timeout` set (assembly-tunable). Every session carries
it; no unbounded loops, no unbounded buffers.
Types only — no russh `Config` construction (wave 2), no dialing
(wave 4). The OQ-08 unset-declines semantics live in the type shape
(`Option` fields + doc contracts), not in construction-time
validation.
## Acceptance Criteria
- [ ] Serve-config carries the OQ-08 fields as `Option` with
unset-declines doc contracts; no crate default shell/wrapper
- [ ] `IdentityProvider`, host-key source, and `ChannelClient` are
injected handles; the crate holds no identity/trust policy of
its own
- [ ] Dial-config carries key source + `HostKeyPolicy` + the
keepalive/inactivity/window knobs with the documented defaults
- [ ] `Limits` carries the in-crate caps (channels, exec-parse) and
the russh knob defaults (`max_auth_attempts` 10, keepalive
15 s / 3 client posture)
- [ ] Unit tests cover default construction and the `Option`-field
semantics
- [ ] `cargo test`, `cargo clippy --all-targets -- -D warnings`,
`cargo fmt --check` pass
## References
- docs/architecture/overview.md §Public surface, §Bounded-resources
policy
- docs/architecture/mapping.md (OQ-08 rows, the data-plane/limits
section)
- docs/architecture/client.md §The dial surface
- docs/architecture/open-questions.md (OQ-08, OQ-09 resolutions)
- docs/architecture/decisions/004-auth-dispositions.md (decision 6 —
the `max_auth_attempts` default)
## Notes
> To be filled by implementation agent
## Summary
> To be filled on completion
+63
View File
@@ -0,0 +1,63 @@
---
id: w1-error-model
name: Wave 1 — the error model (fail-closed taxonomy)
status: pending
depends_on: [w1-manifest]
scope: narrow
risk: low
impact: project
level: implementation
---
## Description
Land the crate's error taxonomy in `src/error.rs` (one module per file,
re-exported from the crate root — the alktty pattern), `thiserror`
throughout, no catch-alls anywhere (AGENTS.md convention 2):
- **`KeySourceError`** — the ADR-005 decision-4 failure matrix as
distinct variants, the vault's error taxonomy reshaped by the
adapter, **never collapsed**: `Locked` (vault locked → no dial, and
no server: a host key that can't load prevents server
construction), `Absent` (no blob at the requested path),
`Decryption` (wrong mnemonic / tampered ciphertext — the
refuse-at-crypto class), `UnknownKeyVersion` (the vault's
refuse-before-crypto `InvalidPath` class), `Decode` (plaintext
arrived but is not decodable key material — the one-time
`decode_secret_key` failure). Doc comments carry the ADR semantics
per variant, including the fail-closed consequence of each.
- Deliberately **not** error types: auth declines (dispositions —
`Auth::Reject`, ADR-004; never errors), wire decline reasons
(SSH-shaped responses — wave-2 material), config validation (the
OQ-08 fields are `Option` and decline at request time, not
construction time — no config-error type ships unless the config
task finds one needed, flagged for the gate).
The exact variant payload shapes are the task's to pin (minimal per
the sibling-crate posture; stringly-typed payloads only where a
message is the honest content), flagged for the wave-1 gate.
## Acceptance Criteria
- [ ] `KeySourceError` has exactly the five pinned variants; no
catch-all, no `Other`/`Unknown`
- [ ] Doc comments state each variant's ADR-005 semantics and its
fail-closed consequence
- [ ] Unit tests construct every variant and assert `Display`/`Debug`
paths compile clean
- [ ] `cargo test`, `cargo clippy --all-targets -- -D warnings`,
`cargo fmt --check` pass
## References
- docs/architecture/decisions/005-at-rest-key-custody.md (decision 4)
- docs/architecture/auth.md §Key custody (the failure-mode list)
- docs/research/poc-2-vault-keys.md §3 (the failure matrix, 39/39)
## Notes
> To be filled by implementation agent
## Summary
> To be filled on completion
+57
View File
@@ -0,0 +1,57 @@
---
id: w1-fingerprint
name: Wave 1 — fingerprint computation (the contract invariant)
status: pending
depends_on: [w1-manifest]
scope: narrow
risk: low
impact: project
level: implementation
---
## Description
Land the fingerprint computation the auth bridge (server) and the
host-key policy path (client) both consume — ADR-004 decision 1's
contract invariant in code:
- Compute a key's fingerprint via the `ssh-key` re-export
(`HashAlg::Sha256`): the `SHA256:` + **unpadded url-safe base64**
OpenSSH format. The string format is a **contract invariant** —
registration strings from any standard issuer (`ssh-keygen -lf`,
alkcall-side computation) must compare equal to what this crate
computes; parity is proven, not assumed (POC-1 §1).
- The function takes the russh/ssh-key public-key type and returns the
string; it is the single fingerprint path in the crate (no
second formatting anywhere — a drift here breaks every identity
resolution silently).
- Parity tests against independent-issuer vectors: static ed25519
test keys with their `ssh-keygen -lf`-recorded expected fingerprints
(the POC-1 §1 checkpoint shape). The vectors are committed test
fixtures (throwaway test keys — no secret material).
## Acceptance Criteria
- [ ] Single fingerprint function via `HashAlg::Sha256`; `SHA256:` +
unpadded url-safe base64 output
- [ ] Parity tests against `ssh-keygen -lf`-recorded vectors pass
(independent-issuer format parity proven)
- [ ] No second fingerprint-formatting path exists in `src/`
- [ ] `cargo test`, `cargo clippy --all-targets -- -D warnings`,
`cargo fmt --check` pass
## References
- docs/architecture/decisions/004-auth-dispositions.md (decision 1 —
the format invariant)
- docs/architecture/auth.md §The bridge (step 2)
- docs/research/poc-1-auth-bridge.md §1 (the parity checkpoint)
- docs/research/auth-bridge.md §The mapping (step 2)
## Notes
> To be filled by implementation agent
## Summary
> To be filled on completion
@@ -0,0 +1,65 @@
---
id: w1-host-key-policy
name: Wave 1 — the HostKeyPolicy seam (OQ-05, policy-blind)
status: pending
depends_on: [w1-manifest]
scope: narrow
risk: low
impact: project
level: implementation
---
## Description
Land the client-side host-key policy seam (`auth.md` §Host-key policy,
OQ-05 resolution) — the trust-decision contract the dial surface calls:
- **The `HostKeyPolicy` trait** — the crate surfaces the presented host
key (host, port, fingerprint) to an assembly-provided policy that
decides accept/reject. The two OQ-05 cases are the *implementor's*:
case-1 pins (known beforehand — hard-decline on mismatch) and case-2
policies (unknown beforehand — prompt-and-record, learn-and-record,
provider-metadata match, accept-and-continue for ephemeral fleets).
The crate is **policy-blind**: it never decides trust, never
hard-codes a trust model.
- **The crate default is fail-closed**: reject unknown (russh's own
reject-all default, kept as the built-in default policy). **No
accept-all built-in crate option** — a permissive case-2 policy is
the assembly's to write.
- The trait receives the fingerprint string (the wave-1 fingerprint
computation is the caller's input path, wave-4 dial); the exact
decision type (bool vs a richer verdict carrying a reason) is the
task's to pin against the russh `check_server_key` hook shape at
this pin — flagged for the wave-1 gate.
- The russh client-handler consumption (`check_server_key` wiring) is
wave-4 dial-surface material — this task lands the seam and its
default only.
## Acceptance Criteria
- [ ] `HostKeyPolicy` trait in place; the presented-key context
(host, port, fingerprint) is the decision input
- [ ] Built-in default policy rejects unknown keys (fail-closed);
no accept-all option exists in the crate
- [ ] Doc comments carry the two-case model and the policy-blind rule
- [ ] Unit tests: default policy rejects; a custom policy's verdict
is honored
- [ ] `cargo test`, `cargo clippy --all-targets -- -D warnings`,
`cargo fmt --check` pass
## References
- docs/architecture/auth.md §Host-key policy (OQ-05 resolution)
- docs/architecture/client.md §The dial surface, §What the crate does
not include (trust decisions)
- docs/architecture/decisions/005-at-rest-key-custody.md (decision 6 —
the client host-*pin* is public config, never vault material)
- docs/research/auth-bridge.md §Host keys and `check_server_key`
## Notes
> To be filled by implementation agent
## Summary
> To be filled on completion
+74
View File
@@ -0,0 +1,74 @@
---
id: w1-key-sources
name: Wave 1 — vault-backed key sources (ADR-005 custody in code)
status: pending
depends_on: [w1-manifest, w1-error-model]
scope: moderate
risk: medium
impact: project
level: implementation
---
## Description
Land the ADR-005 custody model in code — the key-source seam both
directions consume (client dial keys, server host keys):
- **The source seam** — the adapter takes *key sources*, not static
keys (the alkhttp ADR-014 pattern): a source trait the assembly
implements over an injected alkvault handle, `EncryptedData` opaque
to the crate (ADR-005 decision 5 — the crate never interprets
`key_version` or record internals).
- **At-use lifecycle** (decision 2) — `vault.decrypt(blob)` →
**`Zeroizing<String>` wrap at the adapter seam** (decision 3 — the
known-gap contract; the vault-side improvement is upstream, not
here) → one-time `decode_secret_key(plain, None)` → the decoded
`PrivateKey` lives for the dial's lifetime. The vault is needed only
at dial time; locking mid-session keeps live sessions streaming.
- **Host-key load path** (decision 6) — decrypt-at-construction
returning the `PrivateKey` set the server `Config::keys` will
consume (the `Config` construction itself is wave 2; locked vault →
the load fails → **no server is constructed**, POC-2 §5).
- **Failure mapping** — every vault failure mode maps to its distinct
`KeySourceError` variant (locked / absent / decryption / unknown
version / decode); never collapsed, never a fallback dial.
- **Plaintext never persists** (decision 7) — no disk spill on this
path, ever; the `Zeroizing` wrap bounds the plaintext from the
adapter seam onward.
Inner format is OpenSSH private-key PEM, unencrypted (decision 1) —
the source contract assumes it; the decode call is the single
conversion.
## Acceptance Criteria
- [ ] Source trait(s) take the vault handle injected; `EncryptedData`
opaque (no `key_version` interpretation anywhere in `src/`)
- [ ] Decrypt → `Zeroizing<String>` → one-time `decode_secret_key`
chain in place; the plaintext never outlives the wrap
- [ ] Host-key load path returns the decoded key set; locked vault →
`KeySourceError::Locked` (no server construction possible)
- [ ] Round-trip test: encrypt(PEM) → source → decoded ed25519 key
usable by russh
- [ ] Failure-mode tests: locked / absent / tampered ciphertext /
unknown version / undecodable plaintext each map to their
distinct variant
- [ ] `cargo test`, `cargo clippy --all-targets -- -D warnings`,
`cargo fmt --check` pass
## References
- docs/architecture/decisions/005-at-rest-key-custody.md (decisions
1–7)
- docs/architecture/auth.md §Key custody
- docs/research/poc-2-vault-keys.md (§1 the 4-way table, §2 lifecycle
+ zeroization bounds, §3 failure matrix, §5 host keys end to end)
- docs/research/auth-bridge.md §The gap (the source-shape reasoning)
## Notes
> To be filled by implementation agent
## Summary
> To be filled on completion
+78
View File
@@ -0,0 +1,78 @@
---
id: w1-manifest
name: Wave 1 — manifest landing (russh pin + the feature graph)
status: pending
depends_on: []
scope: moderate
risk: medium
impact: project
level: implementation
---
## Description
Land the implementation dependency set and the ADR-003 feature graph —
the first commit of wave 1; everything else lands inside it. Per
ADR-002 decision 6 ("manifest lands with the dependency") and
`overview.md` §lifecycle note:
- **`russh = "0.65"`** (caret stops at the minor), `default-features =
false, features = ["aws-lc-rs"]` — the family crypto posture
(decision 2), **no `flate2`** (decision 3), **no `rsa`** (decision 4:
ed25519-only; the tree must drop `rsa`/`pkcs1` RC deps entirely —
`ssh-key =0.7.0-rc.11` riding the russh keys path is the recorded
exception).
- **`alkcall 0.10`** (identity/ACL types at the wire layer —
`IdentityProvider`, `Identity`, `AccessControl`), **`alkvault 0.1`**
(the ADR-005 custody model). Both pinned by version, never
sibling-checkout paths.
- Standing toolkit: `tokio`, `thiserror`, `tracing`, `parking_lot`,
`zeroize` (the ADR-005 `Zeroizing` wrap).
- **`edition = "2024"`, `rust-version = "1.89"`** (supersedes the
current 1.88/2021 manifest).
- **The ADR-003 feature graph declared from the first commit**:
`session`, `git`, `tunnel`, `sftp`, `default = []` — flat,
independent, no feature implies another. Features gate nothing yet
(the wave-2 decline core is their first consumer); the per-feature
sibling deps (`alktty` under `session`, `alktunnels` under `tunnel`,
`alkgit` under `git`) land with their first consuming wave, not as
dead manifest entries — recorded interpretation, flagged for the
wave-1 gate.
- Update the manifest comments (the current "deliberately empty until
implementation start" notes are superseded); keep comments to the
non-obvious-constraint class only.
Pre-flight: verify every pin exists on crates.io at the declared
version (`russh 0.65`, `alkcall 0.10`, `alkvault 0.1`); if a
crate/version is not published, stop and escalate — a blocking issue,
not something to work around.
## Acceptance Criteria
- [ ] Manifest declares russh 0.65 with `aws-lc-rs` only;
`cargo tree` shows no `rsa`/`pkcs1` packages and no `flate2`
(the `ssh-key` RC pin excepted, recorded)
- [ ] `alkcall 0.10` and `alkvault 0.1` pinned by version; no path
deps anywhere
- [ ] Features `session`/`git`/`tunnel`/`sftp` declared, `default =
[]`; `cargo test --all-features` passes (features gate nothing
yet)
- [ ] `edition = "2024"`, `rust-version = "1.89"` set
- [ ] `cargo test`, `cargo clippy --all-targets -- -D warnings`,
`cargo fmt --check` pass
## References
- docs/architecture/decisions/002-russh-pin.md (decisions 1–6)
- docs/architecture/decisions/003-feature-graph.md (decision 1, §2)
- docs/architecture/overview.md §lifecycle note, §bounded-resources
- docs/research/russh.md (the 2026-10-26 re-verification section —
feature names, tree inventory, MSRV)
## Notes
> To be filled by implementation agent
## Summary
> To be filled on completion
+71
View File
@@ -0,0 +1,71 @@
---
id: w1-test-doubles
name: Wave 1 — test doubles (the in-memory seams)
status: pending
depends_on: [w1-error-model, w1-key-sources, w1-host-key-policy]
scope: narrow
risk: low
impact: component
level: implementation
---
## Description
Establish the test-double convention (adapted from the sibling crates)
and land the in-memory seams every later wave's contract tests run
against:
- **In-memory `IdentityProvider`** — registered fingerprints →
`Identity` resolutions over alkcall's trait; the double the auth
bridge and dispatch tests resolve against.
- **In-memory vault seam** — the encrypt/decrypt round-trip plus the
failure postures (locked / absent / tampered) behind the same seam
the key sources consume, so the custody tests run without a live
vault service. If alkvault 0.1 ships an in-memory construction, wrap
it; otherwise a local test double behind the source trait — the
task pins which, flagged for the wave-1 gate.
- **In-memory `HostKeyPolicy`** — a configurable record/verdict double
(accept/reject per host, decision log inspectable) for the dial and
acceptor tests.
- **ed25519 test-key fixtures** — throwaway test keys (generated at
test time or committed static PEMs — the task pins which; committed
keys are throwaway fixtures, not secrets) with recorded
`ssh-keygen -lf` fingerprints, shared with the fingerprint parity
tests and every later auth/mapping test.
The convention: doubles live in-crate (test module or `#[cfg(test)]`
utility module per the sibling posture), every protocol wave's gate
runs its machines against them, and no double leaks into `src/`
non-test code.
## Acceptance Criteria
- [ ] In-memory `IdentityProvider`, vault seam, and `HostKeyPolicy`
doubles in place behind their production seams
- [ ] ed25519 test-key fixtures with recorded fingerprints available
to the test suite
- [ ] The doubles exercise the wave-1 seams end to end (a round-trip
test: register fingerprint → resolve; encrypt → source → decode;
policy verdict honored)
- [ ] No double code outside test-gated compilation
- [ ] `cargo test`, `cargo clippy --all-targets -- -D warnings`,
`cargo fmt --check` pass
## References
- docs/architecture/decisions/004-auth-dispositions.md (the
`IdentityProvider` surface)
- docs/architecture/decisions/005-at-rest-key-custody.md (the custody
seam the vault double backs)
- docs/research/poc-2-vault-keys.md (the failure postures the vault
double reproduces)
- The alkgit test-double convention (tasks/implementation/core/
w1-test-doubles.md — reading reference, never copied code)
## Notes
> To be filled by implementation agent
## Summary
> To be filled on completion