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:
1 parent
ab6fb9de87
commit
0b1bc284fa
9 files changed
+882
No files matched your search
@@ -0,0 +1,322 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-10-28 (initial plan — five waves committed; wave 1
|
||||
decomposed same day, see its decomposition record)
|
||||
---
|
||||
|
||||
# alkssh — Implementation plan
|
||||
|
||||
Wave-based decomposition of the architecture (`docs/architecture/`)
|
||||
into units of implementable work. The sibling pattern proven on
|
||||
alkgit/alkstore/alkblobs, adopted deliberately as a deviation from the
|
||||
SDD process's decompose-everything-upfront step: early waves change the
|
||||
shape of later ones (wave 1's config/error/key-source types are what
|
||||
every later task implements against; wave 2's server stack is what
|
||||
every mapping composes into), and decomposing only the next wave at a
|
||||
time keeps each session's task set reviewable and lets later
|
||||
decompositions absorb earlier waves' course corrections — reviews
|
||||
sometimes change things, and tasks downstream then have to be edited,
|
||||
so decomposing too far ahead is paid rework.
|
||||
|
||||
Rhythm: decompose a wave → implement it → review gate → decompose the
|
||||
next wave. Task files live in `tasks/implementation/` (frontmatter
|
||||
carries the categorical estimates; the DAG ordering keeps parallel
|
||||
branches from stepping on each other). Wave boundaries are also review
|
||||
boundaries. Each wave's section here carries a **decomposition record**
|
||||
added when that wave actually decomposes (not written upfront).
|
||||
|
||||
Scope authority: **v0.1.0 publishes the crate** — the complete
|
||||
two-direction door (server stack + all mappings, client stack + op
|
||||
rows, key sources, the decline matrix contract-tested per build). The
|
||||
sftp decline placeholder *is* shipped behavior (OQ-13 resolved). **The
|
||||
assembly stays out of these waves**: alkforge's mounting of
|
||||
`SshAdapter::handle` at an alkcall `channel/open` boundary, its
|
||||
feature/ACL selection, its `IdentityProvider` and serve-config values
|
||||
are downstream composition work — the crate ships the door, not the
|
||||
deployment. No binary, no workspace split (single crate, the alktty
|
||||
root pattern; no tracks — one crate, one publish).
|
||||
|
||||
## The waves
|
||||
|
||||
Dependency logic in one line: foundations → server stack → (server
|
||||
mappings ∥ client direction) → validation & release.
|
||||
|
||||
| Wave | Contents | Depends on | Status |
|
||||
|---|---|---|---|
|
||||
| [1](#wave-1--foundations) | Manifest landing (russh pin, feature graph); error model; config surface (serve-config + dial-config + `Limits`); vault-backed key sources; host-key policy seam; fingerprint; test doubles | — | decomposed |
|
||||
| [2](#wave-2--the-server-stack) | `SshAdapter::handle` mount + `Config` construction; the auth bridge; parse-only dispatch + fail-closed decline core; spawn-and-return plumbing; bounded resources; loopback harness | wave 1 | pending |
|
||||
| [3](#wave-3--server-mappings) | session → alktty (translation bridge + exit derivation); git → alkgit (exec grammar + dispatch); tunnel → alktunnels route 1; sftp placeholder declines | wave 2 | pending |
|
||||
| [4](#wave-4--client-direction) | Dial surface; op rows (exec/pty, direct-tcpip, git exec); the `-R` leg (listen + acceptor registry); lifecycle primitives | wave 1 | pending |
|
||||
| [5](#wave-5--validation--release) | Decline-matrix contract suite; real-tool e2e; fuzzing; docs; **the publish** | waves 3 + 4 | pending |
|
||||
|
||||
## Wave 1 — Foundations
|
||||
|
||||
The contract surface in code, engine-blind: everything later waves
|
||||
implement *against*, and nothing that needs a live SSH session to
|
||||
test. The alkssh analog of alkgit's seam wave.
|
||||
|
||||
- **Manifest landing** (ADR-002) — the dependency set lands with
|
||||
implementation start: `russh = "0.65"` with `aws-lc-rs` (no `flate2`,
|
||||
no `rsa` — the tree RC-free except russh's own `ssh-key` pin),
|
||||
`alkcall 0.10` (identity/ACL types at the wire layer), `alkvault 0.1`
|
||||
(the custody model), plus the standing toolkit (`tokio`, `thiserror`,
|
||||
`tracing`, `parking_lot`, `zeroize`). `edition = "2024"`,
|
||||
`rust-version = "1.89"`. The ADR-003 feature graph is declared from
|
||||
the first commit: `session`, `git`, `tunnel`, `sftp`, `default = []`
|
||||
— features gate nothing yet (the decline core references them from
|
||||
wave 2); 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. All family deps
|
||||
pinned by version, never sibling-checkout paths; pins verified
|
||||
against crates.io before committing.
|
||||
- **Error model** — the crate's `thiserror` taxonomy, fail-closed with
|
||||
no catch-alls: the key-source failure matrix as distinct variants
|
||||
(ADR-005 decision 4 — locked / absent / decryption failure / unknown
|
||||
`key_version`, plus decode failure; the vault's taxonomy reshaped,
|
||||
never collapsed). Auth declines are *dispositions* (`Auth::Reject`),
|
||||
not errors; wire decline reasons are wave-2 shaped. What the config
|
||||
surface needs beyond this lands with it.
|
||||
- **Config surface** — the pure configuration types both directions
|
||||
implement against: the **serve-config** (feature wiring; OQ-08's two
|
||||
assembly-supplied fields — shell argv and exec wrapper argv, each
|
||||
`Option`, unset → its row declines, never a fallback; the
|
||||
`IdentityProvider` handle; the host-key source handle; the tunnel
|
||||
`ChannelClient` handle; the `Limits`), the **dial-config** (address,
|
||||
key source, `HostKeyPolicy` handle, keepalive/inactivity/window
|
||||
knobs), and **`Limits`** — the bounded-resources budget model
|
||||
(`overview.md`'s table: in-crate max-concurrent-channels cap,
|
||||
exec-parse length caps, and the russh knob mapping with the
|
||||
documented default postures — `max_auth_attempts` 10 sized for the
|
||||
counting `none`-probe, keepalives 15 s / 3 client-side per the
|
||||
reverse-runner evidence).
|
||||
- **Vault-backed key sources** (ADR-005) — the custody model in code:
|
||||
decrypt-at-use (`vault.decrypt` → `Zeroizing<String>` wrap at the
|
||||
adapter seam → one-time `decode_secret_key(plain, None)`), the
|
||||
client dial-key source and the server host-key load path
|
||||
(decrypt-at-construction; locked vault → no server constructed),
|
||||
`EncryptedData` opaque to the crate, plaintext never persists, every
|
||||
failure mode mapped to its distinct error variant.
|
||||
- **Host-key policy seam** (OQ-05) — the `HostKeyPolicy` trait the
|
||||
client dial calls with (host, port, presented fingerprint): case-1
|
||||
pins (hard-decline on mismatch) and case-2 assembly policies are the
|
||||
implementor's; the crate ships the fail-closed default (reject
|
||||
unknown) and is otherwise policy-blind — no accept-all built-in.
|
||||
- **Fingerprint** (ADR-004) — the `SHA256:` + unpadded url-safe base64
|
||||
computation via the `ssh-key` re-export (`HashAlg::Sha256`); the
|
||||
string format is a contract invariant, proven against independent
|
||||
issuers (`ssh-keygen -lf` vectors, the POC-1 §1 checkpoint shape).
|
||||
- **Test doubles** — the in-memory seams every later wave's contract
|
||||
tests run against: an in-memory `IdentityProvider` (registered
|
||||
fingerprints → identities), an in-memory vault (encrypt/decrypt
|
||||
round-trip + the failure postures), an in-memory `HostKeyPolicy`,
|
||||
and ed25519 test-key fixtures. The convention (adapted from the
|
||||
sibling crates) is established here even though the harness that
|
||||
exercises them end to end lands with wave 2's server stack.
|
||||
|
||||
The wave-1 review gate is 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; the published crate freezes
|
||||
them). A shape error found at this gate 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
|
||||
ADR-002/003/004/005 and the OQ-08/05 resolutions line by line.
|
||||
|
||||
**Decomposition record** (2026-10-28): wave 1 decomposed into eight
|
||||
task files under `tasks/implementation/` (flat — one track, no
|
||||
per-track subdir). Shape: `w1-manifest` first (the ADR-002 dependency
|
||||
landing + the ADR-003 feature graph declaration everything lands
|
||||
inside); then four independent foundations in parallel —
|
||||
`w1-error-model` (the `KeySourceError` five-variant taxonomy),
|
||||
`w1-fingerprint` (the contract-invariant computation + parity
|
||||
vectors), `w1-key-sources` (the ADR-005 custody chain, over manifest +
|
||||
error model), and `w1-host-key-policy` (the OQ-05 seam + fail-closed
|
||||
default); then `w1-config` (serve-config + dial-config + `Limits`)
|
||||
over the key-source and policy seams it carries handles for;
|
||||
`w1-test-doubles` (the in-memory seams + ed25519 fixtures) over error
|
||||
model + key sources + policy; and `review-wave-1` (the cheap-place
|
||||
gate) depending on all seven.
|
||||
|
||||
Interpretations recorded (flagged for the wave-1 gate):
|
||||
|
||||
- **Per-feature sibling deps land with their first consuming wave**,
|
||||
not in the wave-1 manifest — `alktty`/`alktunnels`/`alkgit` would be
|
||||
dead manifest entries in a build where the features gate nothing
|
||||
yet; the wave-1 manifest lands russh + alkcall + alkvault + the
|
||||
toolkit only. The feature *names* are declared from the first
|
||||
commit; the wave-2 decline core is their first `cfg` consumer.
|
||||
- **No config-error type ships in wave 1** unless the config task
|
||||
finds one needed: the OQ-08 fields are `Option` and decline at
|
||||
request time (never construction time), so the error model carries
|
||||
only the key-source taxonomy; wire decline reasons are wave-2
|
||||
shaped.
|
||||
- **The policy verdict type** (bool vs a richer reason-carrying
|
||||
verdict) is pinned in `w1-host-key-policy` against the russh
|
||||
`check_server_key` hook shape at this pin — flagged for the gate.
|
||||
- **The vault-double shape** (wrap alkvault's in-memory construction
|
||||
if it ships one, else a local double behind the source trait) is
|
||||
pinned in `w1-test-doubles` — flagged for the gate.
|
||||
|
||||
## Wave 2 — The server stack
|
||||
|
||||
The russh server substrate every mapping composes into — the door
|
||||
frame, before any door is hung:
|
||||
|
||||
- **`SshAdapter::handle` + `Config` construction** (ADR-001/002) — the
|
||||
mount point ADR-001 pins as the `alk/ssh` receiving endpoint: a
|
||||
generic stream in, the full russh server stack per stream (edge
|
||||
termination only — no ALPN code, no relay leg; the pin is a ledger
|
||||
entry, already decided). `Config` built from the serve-config:
|
||||
honest advertisement (`methods = {publickey}` exactly, ADR-004
|
||||
decision 4), host keys from the vault-backed source (locked → no
|
||||
server constructed), the bounded knobs from `Limits`.
|
||||
- **The auth bridge** (ADR-004) — the dispositional handler surface:
|
||||
`auth_publickey` fingerprint → `resolve_from_fingerprint` → accept
|
||||
with the resolved identity / reject fail-closed; ed25519-only
|
||||
(non-ed25519 keys declined explicitly); `auth_publickey_offered`
|
||||
accept-and-delegate (counting stays honest); `auth_none` /
|
||||
`auth_password` / `auth_keyboard_interactive` /
|
||||
`auth_openssh_certificate` explicit declines (family contract, not
|
||||
inherited defaults); the username carries no semantics (OQ-03).
|
||||
- **Parse-only dispatch + the fail-closed decline core** (ADR-006 as
|
||||
amended, ADR-003) — session-channel dispatch skeleton (parse path:
|
||||
git-parseable exec → `git`, else → `session`; request-less session
|
||||
channels decline), unfeatured channel types decline at open, x11 /
|
||||
`auth-agent` always decline (never feature-gated), the sftp
|
||||
subsystem decline-with-reason placeholder (OQ-13 — folded here,
|
||||
server side), **decline first, alloc never** (the POC-3 stage-A
|
||||
matrix: unknown repo → collapsed generic denial before any protocol
|
||||
byte).
|
||||
- **Spawn-and-return plumbing** (ADR-006 decision 4) — the structural
|
||||
constraint in code: every serving path `tokio::spawn`ed, handlers
|
||||
return fast, exit/status reporting from spawned tasks rides the
|
||||
cloned `Session::handle()`. The reporting *mechanics* land here; the
|
||||
derivation contract's rules are wave-3 session-mapping material.
|
||||
- **Bounded resources** — the in-crate concurrent-channel cap enforced
|
||||
at the handler surface, `inactivity_timeout`/`max_auth_attempts`
|
||||
wired from `Limits`, per-length exec-parse limits reserved for the
|
||||
grammar's consumption (wave 3).
|
||||
- **Loopback harness** — the in-process client/server test pair the
|
||||
decline matrix and every mapping's contract tests run through
|
||||
(in-memory doubles from wave 1; real-tool smokes ride the mapping
|
||||
waves).
|
||||
|
||||
Wave 2 decomposes after the wave-1 gate, absorbing whatever it
|
||||
corrects.
|
||||
|
||||
## Wave 3 — Server mappings
|
||||
|
||||
Three parallel branches over the stack (plus the folded sftp row),
|
||||
each carrying a minimal real-tool smoke (one clone, one `-L`, one pty
|
||||
session — the POC-3 gates re-landed cheaply); the full matrix is
|
||||
wave 5's:
|
||||
|
||||
- **session → alktty** — the translation bridge, not a byte pump
|
||||
(POC-3 §D.1): `pty_request` args → `TerminalParamsWire` (SSH modes →
|
||||
the reserved modes JSON); shell argv / exec wrapper from the
|
||||
serve-config (`wrapper ++ [cmd]`, never naively split; unset → clean
|
||||
decline — OQ-08); stdin/stdout/stderr chunk translation
|
||||
(`extended_data(1)` for stderr); `window_change_request`/`signal`
|
||||
rows; `env_request` declined unconditionally (OQ-09); the **exit
|
||||
derivation contract** (`mapping.md` rules 1–6: `code ≥ 0` →
|
||||
`exit-status(N)`, negative `−N` → `exit-signal(name)` via the
|
||||
server-side Unix signal-number→name table, `−1` → `exit-status(255)`
|
||||
never a signal claim, `core_dumped` never asserted, PTY-path rule-5
|
||||
fidelity limit) — exit-status sent before close, from the spawned
|
||||
task.
|
||||
- **git → alkgit** — exec-only (ADR-006 decision 2): the parse grammar
|
||||
(`git-{upload,receive}-pack '/repo'`, quoted/unquoted paths,
|
||||
service-only → decline-with-reason, non-git → parse-then-fail,
|
||||
per-length limits), unknown-repo → collapsed generic denial before
|
||||
any protocol byte (no existence oracle), dispatch to
|
||||
`serve_duplex_resolved(repo_id, identity, service, duplex, limits,
|
||||
…)` over the channel duplex, process-mode dialect, exit-status both
|
||||
directions.
|
||||
- **tunnel → alktunnels route 1** (ADR-006 decision 1) — the
|
||||
adapter-side bridge: the serve-config's `ChannelClient` into the
|
||||
family fabric (identity propagated once via `set_identity` so
|
||||
`AccessControl::check` gates the mapped opens — the visible-surface
|
||||
invariant), `TunnelSession::open_direct` + `take_halves()` pumped
|
||||
against the russh channel, accept only after the fabric open
|
||||
succeeds (failure → explicit `ConnectFailed` reject),
|
||||
`direct-streamlocal` runtime-Unix-gated (OQ-11: universal compile,
|
||||
clean open-decline off-Unix), the `forwarded-tcpip` handler shape,
|
||||
the `-R` listen flow (`tcpip_forward`/`streamlocal_forward` + the
|
||||
same runtime-Unix gate).
|
||||
- **sftp placeholder** — the decline-with-reason rows (folded into the
|
||||
dispatch task server-side; the client-side placeholder folds into
|
||||
wave 4's acceptor/op-row work; the wave-5 matrix covers both
|
||||
explicitly).
|
||||
|
||||
Wave 3 decomposes after the wave-2 gate, absorbing whatever it
|
||||
corrects.
|
||||
|
||||
## Wave 4 — Client direction
|
||||
|
||||
The door outward (independent of wave 3 — shares only wave 1's
|
||||
foundations), each branch carrying its real-tool smoke where cheap:
|
||||
|
||||
- **Dial surface** (`client.md`) — connect with address, key source
|
||||
(decrypt-at-dial, fail-closed on locked/absent — never env vars),
|
||||
host-key policy at `check_server_key` (the wave-1 seam consumed:
|
||||
case-1 pins hard-decline on mismatch, case-2 policies decide), the
|
||||
bounded config (keepalives 15 s / 3, inactivity, window knobs). The
|
||||
authenticated connection handle everything else rides.
|
||||
- **Op rows** — family op → vanilla translation per `client.md`'s
|
||||
table: session exec/pty opens (the POC-3 §D.3 mechanics:
|
||||
`Channel::exec()` not `request_exec`, Success/Failure via
|
||||
`wait()`-drain not `Err`, `window_change`/`signal` before
|
||||
`into_stream()`, never consume the queue before conversion);
|
||||
`direct-tcpip` typed dial (reject → `Err(ChannelOpenFailure)`, the
|
||||
connection stays usable); git exec consumption (the same grammar the
|
||||
server parses, pointed upstream); the sftp subsystem placeholder
|
||||
decline.
|
||||
- **The `-R` leg** — `tcpip_forward(address, port) -> bound` (the
|
||||
actually-bound port returned), server-initiated `forwarded-tcpip`
|
||||
opens routed to the **acceptor registry**: served only under a
|
||||
registered feature's acceptor, unregistered/unfeatured accepted
|
||||
opens declined cleanly (never russh's accept-and-drop default),
|
||||
late-install across a live connection (the alkminer dynamic),
|
||||
`streamlocal_forward` with the runtime-Unix gate.
|
||||
- **Lifecycle primitives** (OQ-12) — explicitly cancellable
|
||||
listener/session handles, clean `Disconnect::ByApplication`; **no
|
||||
reconnect or restore policy owned by the crate** (assembly policy;
|
||||
the primitives make it possible).
|
||||
|
||||
Wave 4 decomposes after the wave-3 gate (it may start any time after
|
||||
wave 1 in principle, but decomposing it after wave 3's gate keeps the
|
||||
session task sets in rhythm and lets it absorb any seam corrections
|
||||
the mappings surfaced).
|
||||
|
||||
## Wave 5 — Validation & release
|
||||
|
||||
- **The decline-matrix contract suite** (`mapping.md`'s table) — every
|
||||
build (none / `git` / `session` / `tunnel` / `--all-features`) ×
|
||||
every row it can appear in × both directions, including the sftp
|
||||
placeholder rows and the never-features (x11, `auth-agent`). The
|
||||
fail-closed invariant is contract territory on both sides of every
|
||||
feature (AGENTS.md conventions 5–6).
|
||||
- **Real-tool e2e** — the POC-3 gates re-landed against the shipped
|
||||
crate: OpenSSH (pty/shell/exec, `-L`, `-R`, `-D`'s `direct-tcpip`
|
||||
leg, decline observability), git 2.43+ (clone/push through the git
|
||||
mapping), curl through a `-D` proxy; client direction against a real
|
||||
`sshd` (the reverse-runner shape: exec, direct-tcpip dial, `-R`
|
||||
listen + acceptor).
|
||||
- **Fuzzing** — the family standing rule (anything worth fuzzing gets
|
||||
fuzzed) on the crate's ingress surfaces: the exec parse grammar, the
|
||||
fingerprint format path, the key-decode path, the TerminalParamsWire
|
||||
conversion. The `fuzz/` sidecar convention (detached runner, corpus
|
||||
gate joining the verification list).
|
||||
- **Release readiness** — crate docs (public-API rustdoc
|
||||
warning-free; docs honesty), the feature matrix (`default`, each
|
||||
feature over it, `--all-features`), CHANGELOG/README, publish
|
||||
dry-run, then **the publish of `alkssh 0.1`** — the gate review that
|
||||
closes the plan.
|
||||
|
||||
## References
|
||||
|
||||
- `docs/architecture/` — overview, mapping, client, auth,
|
||||
open-questions (all reviewed 2026-10-28), ADR-001…006
|
||||
- `docs/research/` — vision, russh inventory, auth bridge, mapping
|
||||
table, POC-1/2/3 findings, reverse-runner extraction
|
||||
- Family ADR-003 (the structural decision); the alkgit implementation
|
||||
plan (the pattern this follows)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in new issue
Block a user