The wave-2 gate's booked finding: observability on the decline paths.
`tracing` rides the manifest per the ADR-002 toolkit but nothing in
src/ emitted — a decline's "reason" was only the routing decision
documented per build.
- src/observe.rs: the event vocabulary, adopting the family posture
read from alktty/alktunnels/alkcall/alkhttp (free tracing fns,
message-last lowercase, flat fields; no info!/trace!, no macros,
no spans): feature_build + row + channel_id + decline_class
(open/request/serving/auth) + reason + exec_route (exec rows);
declines at debug, the serving-path failure arm the vocabulary's
one warn
- handler.rs: every decline site emits — auth dispositions
(non-ed25519 / fingerprint-unresolved / the four unimplemented
methods), open-level declines (unfeatured / never-feature /
cap-exceeded / tunnel connect-failed stub), request-level
capability declines (exec route-aware, subsystem, env, x11, agent,
the -R listen forms), and the seam-stub serving declines with the
spawned-path lifecycle (started / ended / failed — the shape the
wave-3 mappings inherit)
- tests/observe.rs: the event-presence pin — a hand-rolled global
capture subscriber (the recorded test convention; no event-capture
precedent exists in the family), field-matching rows across the
default/git/session/tunnel/--all-features builds, and the safety
scan (no username, no fingerprint ever in an event)
- helper-level exact shapes pinned in src/observe.rs unit rows
Verification: cargo test (default, --features git/session/tunnel,
--all-features all green), clippy x2 --all-targets -D warnings,
fmt --check, cargo doc --no-deps 0 warnings, taskgraph validate.
- ServeConfig gains cfg-gated SessionSeam / GitSeam Option fields
(whole-Option struct per seam — the shape decision recorded in the
task notes): unset → the owned rows decline even though the feature
is on, no construction-time validation
- SessionSeam carries the alktty backends map + backend selector +
ownership provider; GitSeam carries the alkgit backend quartet +
caller-owned pipeline semaphore + push_options_negotiated
- optional deps alktty 0.8.1 / alkgit 0.1.0 wired feature-to-dep
(pins verified against crates.io: licenses, MSRV, shared alkcall
resolution); gix-hash 0.28 as dev-dep only (alkgit does not
re-export ObjectId for the trait doubles)
- per-feature construction/unset-declines/Debug rows in config.rs;
accessors session_seam_configured / git_seam_configured; docs state
what each seam handle carries
Verified: cargo test (per-feature + --all-features), clippy ×2
(-D warnings), fmt, cargo doc --no-deps, cargo tree feature isolation
(git-build: no alktty; session-build: no alkgit), taskgraph validate
Six implementation tasks + the gate under tasks/implementation/,
chained sequentially (the mappings replace stubs in the same Handler
impl and share the decline-matrix test binaries):
- w3-seam-config: the serve-config's per-feature seam handles
(alktty backends/ownership/selector; the alkgit quartet/pipeline/
push-options flag) + the optional sibling deps — wave 3's first
course correction to the wave-1 config surface
- w3-decline-tracing: the wave-2 gate's booked finding — the
decline/serve-path tracing vocabulary, landed before the mappings
- w3-git-mapping: the full exec grammar replaces the wave-2 probe +
the serve_duplex_resolved dispatch + the store-and-take handoff
- w3-tty-session: the translation bridge + the exit-derivation
contract (rules 1-6, the signum table)
- w3-tunnel-direct: the route-1 bridge (accept only after the fabric
open succeeds) + set_identity propagation + the capacity-across-
fabric-window mechanics
- w3-tunnel-listen: the -R listen flow + the forwarded-tcpip data
leg (the POC-untested leg; the stop-and-book provision)
- review-wave-3: the gate
The plan carries the wave-3 decomposition record with four
interpretations flagged for the gate (the serve-config course
correction, the already-discharged server-side sftp bullet, the
w3-tunnel-listen open mechanics, the booked-items' wave-3 homes).
taskgraph validate: 26 tasks, clean.
Sibling pins verified against crates.io at decomposition time:
alktty 0.8.1 / alkgit 0.1.0 / alktunnels 0.4.1 / alkcall 0.10.1.
Gate reading (commits b90e89b..196289f) against the architecture line
by line: ADR-001 mount contract, ADR-002 Config construction,
ADR-003 feature gates, ADR-004 dispositions, ADR-006 constraints,
mapping.md skeleton decline matrix per build, bounded resources, and
the harness — all confirmed. All four flagged interpretations
confirmed as recorded.
Fixed inline before committing (task-file hygiene only):
- w2-loopback-harness, w2-dispatch-decline-core: acceptance boxes
checked once verified against the landed rows/runs
- w2-bounded-resources: stale placeholder lines removed
- review-wave-2: verdict recorded
Booked finding (next wave's decomposition): decline-path
tracing/observability — the reason a given wire decline carried is
currently docs-only (tracing unused in src/).
Verification: cargo test 104 rows green; --all-features green;
clippy ×2 -D warnings green; fmt clean; cargo doc 0 warnings;
taskgraph validate 19 tasks.
- new private channel_cap module: per-session active-channel count;
admit (check+increment at accept sites) / has_room / saturating
release, single-writer (inline packet-loop hooks), unit-tested
- handler wires the cap at every channel_open_* hook: the session
accept path admits-and-counts at the accept point; the tunnel rows
cap-gate before the seam stub (a counted-but-unopened stub would
leak the cap); x11 never consumes a count; the count releases
through the new channel_close hook
- dispatch::cap_exceeded_open_decline -> AdministrativelyProhibited
(the assembly's policy ceiling; OpenSSH MaxSessions parity)
- bounded-resources contract rows: cap reached -> clean decline,
close -> recovery open succeeds (wire-order deterministic),
zero cap refuses without poisoning, exhausted cap answers the
tunnel row with the cap reason; max_auth_attempts verified
behaviorally (within-ceiling failures keep the advertisement, the
past-ceiling attempt is the NoMoreAuthMethodsAvailable teardown);
window/packet/buffer/inactivity knobs recorded as covered by the
config-mapping tests
- max_exec_command_len stays carried, not enforced (wave-3 grammar)
Verification: cargo test (default), --features session / git /
tunnel / --all-features, clippy --all-targets (-D warnings, also
--all-features), fmt --check, cargo doc --no-deps - all green
- src/serve.rs (new): the ADR-006 decision-4 mechanics in code —
spawn_serving_path (the one choke point; nothing awaited inline)
and report_exit_status_then_close (exit-status before close via
the cloned Session::handle()), #[doc(hidden)] contract-test
re-exports; a unit row pins spawn-returns-while-pending.
- src/handler.rs: the four session-row seam stubs became spawned
serving paths (failure-then-close unchanged; stubs are plain fns —
an async stub holding &Session across the spawn is non-Send); open-
level declines and the tunnel seam stub stay inline by class, per
the new module-doc section.
- tests/spawn_return.rs (new): two end-to-end wire rows — the client
observes exit-status(7) strictly before close and after the handler
returned; the handler-returns-fast row routes follow-up traffic
while the serving path is still gated (POC-3 deadlock inversion).
- tasks/implementation/w2-spawn-return.md — completed with notes.
Verification (all green): cargo test; per-feature dispatch matrices
(--features git/session/tunnel/sftp, --all-features); cargo clippy
--all-targets/-all-features with -D warnings; cargo fmt --check;
cargo doc --no-deps (no warnings); taskgraph validate.
- auth_publickey: ed25519-only gate → single fingerprint path →
resolve_from_fingerprint → accept-with-identity (stored per-
connection, read by wave 3) / reject fail-closed; the gate is
explicit-over-inherited (ADR-002 decision 4) and precedes resolution
- auth_publickey_offered: explicit accept-and-delegate (counting
stays honest, POC-1)
- auth_none / auth_password / auth_keyboard_interactive /
auth_openssh_certificate: four explicit family-contract declines
- username carried by no hook — resolution is fingerprint-only (OQ-03)
- SessionHandler constructible via #[doc(hidden)] re-export (the
w2-server-mount note's widening); resolve_bridge structured out of
the #[non_exhaustive]-arg hooks so contract tests drive the bridge
core directly
- tests/auth_bridge.rs: 21 rows — wire rows over the real adapter
mount (accept, fail-closed reject, username independence, revoke
between attempts, none/password/certificate declines) + direct
rows over the wave-1 doubles
- cert fixture (throwaway ca + alpha-cert.pub) recorded in
fingerprints.txt; MemHostKeySource double in tests/common
Verification: cargo test (76 across the suite), --all-features,
clippy --all-targets (both feature sets, -D warnings), fmt --check,
doc --no-deps, taskgraph validate — all green
- SshAdapter:🆕 decrypt-at-construction host-key load (ADR-005
decision 6) — locked / absent / decryption / unknown-version /
decode all refuse with AdapterError::Construction(KeySourceError
preserved), no adapter; an empty key set refuses as Absent even
from a custom source.
- russh 0.65 server Config built from ServeConfig: methods exactly
{publickey} (MethodSet from [MethodKind::PublicKey] — honest
advertisement, ADR-004 decision 4); host keys ride Config::keys;
Limits knobs (window_size, maximum_packet_size,
channel_buffer_size, max_auth_attempts) one-for-one;
inactivity_timeout always Some (bounded-resources posture). All
other knobs stay russh's default posture: rekey Limits (RFC-4253
defaults, russh-enforced), auth_rejection_time, server_id
(russh default — recorded), server keepalive (client-direction
posture only).
- SshAdapter::handle: ADR-001 mount — generic duplex stream in,
run_stream per stream, run to completion (Result<(), AdapterError>;
no ALPN/relay code; the serving loop is spawned inside russh, so
ADR-006's spawn-and-return holds without double-spawning).
- SessionHandler skeleton (pub(crate)): per-connection
russh::server::Handler with type Error = AdapterError; every hook
at the inherited fail-closed default; carries Arc<ServeConfig> for
w2-auth-bridge / w2-dispatch-decline-core.
- AdapterError (error.rs): Construction + Wire (From<russh::Error>)
— the wave-1 error-model's reserved wave-2 shape.
- Tests: advertisement-exactly-publickey; knob mapping (tuned +
default postures); key-set parity; construction failure matrix;
EOF-on-mount; wire smoke (russh client over duplex: full KEX,
fail-closed auth decline, exactly the {publickey} method list on
the wire).
Verified: cargo test (61 across suites), cargo test --all-features,
clippy --all-targets -- -D warnings (default + all-features),
fmt --check, cargo doc --no-deps, taskgraph validate.
Six implementation tasks + the gate under tasks/implementation/,
chained sequentially (the auth hooks, dispatch, and channel cap all
edit the same per-connection Handler impl):
- w2-server-mount: SshAdapter::handle mount + russh Config
construction from the serve-config (honest advertisement, host-key
decrypt-at-construction, the Limits knob mapping) + the Handler
skeleton at fail-closed defaults
- w2-auth-bridge: the ADR-004 dispositions (ed25519-only explicit
decline, fingerprint-only resolution, explicit declines for every
non-publickey method)
- w2-loopback-harness: the in-process client/server contract-test
vehicle over the wave-1 doubles
- w2-dispatch-decline-core: parse-only dispatch + the fail-closed
decline core, contract-tested per feature build
- w2-spawn-return: the structural constraint pinned by test + the
exit-reporting mechanics via the cloned Session::handle()
- w2-bounded-resources: the in-crate concurrent-channel cap at the
handler surface
- review-wave-2: the gate
The plan carries the wave-2 decomposition record with four
interpretations flagged for the gate (skeleton-level git probe,
declining routing stubs, the adapter error surface, the harness
transport choice). taskgraph validate: 19 tasks, clean.
Review task record: the line-by-line ADR/OQ gate reading, the three
flagged interpretations confirmed (sibling-dep timing, vault-double
shape, policy verdict type), the minor inline fixes, and the green
verification summary. Wave 2 decomposition unblocked.
- resolve 2 rustdoc broken-intra-doc-link warnings (ambiguous
'async_trait' crate/attribute-macro name) in key_source.rs and
host_key.rs
- fix stray ')' typo in the HostKeySource docs
- rewrite the stale lib.rs architecture line (dependency surface and
feature graph are landed, not 'landing with implementation start')
- drop a discarded try_sign probe and a redundant None re-assignment
in tests
Verification: cargo test 47/47 (default + --all-features), clippy
-D warnings (both feature configs, all targets), fmt --check,
cargo doc --no-deps 0 warnings
The `w1-test-doubles` task: the in-memory seams every later wave's
contract tests run against, behind their production seams.
- tests/common/mod.rs — the doubles, test-only (nothing reachable from
lib.rs, no feature): MemIdentityProvider (registered fingerprints ->
alkcall Identity resolutions over the IdentityProvider trait; token
path resolves none per ADR-004), TestVault (a wrapper over alkvault
0.1's shipped in-memory construction — resolving the flagged
vault-double question: wrap, not re-impl), MemHostKeyPolicy
(exact (host, fingerprint) rules + explicit default verdict,
fail-closed by default, inspectable decision log), and the fixture
table. Module docs record the location/vault/fixture decisions and
the row/stamp discipline (greppable Contract stamps).
- tests/vectors/ — one unified fixture set: four ssh-keygen-minted
ed25519 keypairs (committed .key + .pub + fingerprints.txt recorded
from ssh-keygen -lf, OpenSSH_9.6p1). The previous inline PEM
constants in src/key_source.rs were rejected by OpenSSH itself
(russh-only decodable); all keys were re-minted and the overlapping
vector set retired — one fixture set now serves the parity tests,
the custody round-trip, and every later auth/mapping test
(src-unit tests share it via include_str!).
- tests/doubles.rs — the end-to-end rows: register -> resolve,
seal -> source -> decode (+ locked/absent/tampered postures),
host-key set in order, policy verdicts honored-and-logged.
- tests/fingerprint_parity.rs and src/key_source.rs tests moved onto
the shared fixture files; src changes are #cfg(test) only.
Verification: cargo test (33 + 9 + 5 green; --all-features also green),
cargo clippy --all-targets -- -D warnings clean, cargo fmt --check
clean, taskgraph validate ok (12 tasks).
- HostKeyPolicy trait: async, dyn-compatible (Arc<dyn> handle, the
alkcall injected-handle pattern) — decide(&self, &PresentedHostKey)
-> bool with host/port/fingerprint as the decision input
- Verdict type pinned to the russh 0.65 check_server_key shape:
bool is the trust decision; decline reasons stay with the policy
(declines are dispositions, not errors). Flagged for the wave-1 gate
- Built-in default RejectAll: fail-closed reject-all (russh's default,
kept); no accept-all option exists in the crate
- async-trait 0.1 added for the dyn-compatible async handle
(alkcall-precedented; tree-deduped). Flagged for the wave-1 gate
- Tests: default rejects; custom verdict honored; both through the dyn
handle; the whole presented context reaches the policy
Verified: cargo test (14), cargo test --all-features, cargo clippy
--all-targets -- -D warnings, cargo fmt --check, taskgraph validate
The format contract (ADR-004 decision 1) is SHA256: + unpadded
standard base64 — not url-safe. The ssh-key re-export and ssh-keygen
both emit the +/ alphabet (visible in the recorded fingerprints); the
'url-safe' wording in ADR-004/auth.md/plans/implementation.md and the
POC-1 §1 finding was a misnomer. Wording corrected; the parity
contract itself unchanged on both sides.
- KeySourceError with exactly five variants (Locked, Absent,
Decryption, UnknownKeyVersion, Decode) — ADR-005 decision 4's
failure matrix as distinct shapes, the vault taxonomy reshaped
never collapsed; no catch-all, no Other/Unknown
- payload shapes pinned: Locked/Absent payload-free;
Decryption/UnknownKeyVersion carry the vault's opaque messages;
Decode carries a message so the RC-pinned ssh-key error type
never enters the public API (flagged for the wave-1 gate)
- each variant doc-commented with ADR-005 semantics + fail-closed
consequence; module stays engine-blind (no vault/russh types)
- no From<VaultServiceError>: non-mappable vault variants would
force a collapse; w1-key-sources maps explicitly at its seam
- exhaustive-match + Display/Debug/Clone unit tests
Verified: cargo test (default + all-features), cargo clippy
--all-targets --all-features -- -D warnings, cargo fmt --check,
cargo doc --no-deps, taskgraph validate
- OQ-10 is not resolved: the mapping-contract derivation details (name
table, core-dumped disposition, −1 disposition) are working
dispositions on mapping.md's exit rows until formalized there / as an
ADR-006 amendment. The 1a40bfb commit prematurely closed them and left
ADR-006's reference (OQ-10 open) stale.
- Premise correction integrated into the seed, not argued after the
fact; the portable-pty 0.9.0 source now cited as verified
(From<std::process::ExitStatus> maps signal-terminated → code 1 +
locale-dependent strsignal string; the number is unrecoverable).
- mapping.md: exit_signal row and the Open Questions entry updated to
open OQ-10 with the working-draft framing.
Verification: cargo test (2/2 targets ok), cargo clippy --all-targets -- -D warnings, cargo fmt --check (docs-only change)
- OQ-10 resolved: alkssh derives SSH exit-signal from the alktty exit
chunk's negative code (−N = signal N, pipe path) server-side; −1
(alktty's could-not-determine sentinel) → exit-status(255), never a
signal claim; core_dumped never asserted in v1; no alktty wire change
- new verified backend finding recorded: portable-pty 0.9 reduces
PTY-mode signal termination to a plain exit code 1 (signal number
unrecoverable through its trait surface) — faithful exit-signal on
the PTY row is an alktty/portable-pty upstream ask, not an alkssh one
- research docs (poc-3 §D.2, pocs.md, mapping-table rows) restated
accurately instead of carrying inline correction scaffolding
Verification: cargo doc --no-deps earlier this session (docs-only change)
- OQ-10's seed overstated alktty's exit-chunk design as a 'signal-identity
gap': the negative code is the documented signal encoding (alktty ADR-004,
ported from alknet ADR-055; src/local/pipe.rs::exit_code_from — Unix
ExitStatus::signal() → -sig), a tty/pty-grounded convention
- 'alktty widens the chunk' demoted from co-equal candidate to gated
exception: it would widen ControlMessage::Exit's one-way parsed shape,
the backend-wide exit_code API, and the alknet ADR-055 lineage — requires
an alktty ADR with explicit tty/pty-grounded justification, never an
alkssh convenience ask
- OQ-10 re-scoped to the real remaining detail: −1 (wait failure vs SIGHUP)
collision, core-dumped disposition, non-Unix semantics, name derivation
- correction notes added in place: poc-3 §D.2, pocs.md, mapping-table.md
(both rows); mapping.md exit_signal row updated
Verification: cargo doc --no-deps (docs-only change)
- the alkssh-level contract is the adapter-side Zeroizing wrap
(ADR-005 decision 3, POC-2-proven); the vault-side fix (Zeroizing-out
API) is filed in alkvault's own tracker — not carried, not gated on
here; ADR-005 §3 wording updated accordingly
Vision principle 2 extended: server = door for vanilla clients (vanilla
in, family out; primary current purpose git), client = door to vanilla
servers (family in, vanilla out — reverse-runner hub dials vanilla
sshd; runner stays vanilla by design). Type symmetry at the seam
(family-protocol types both directions, russh the vanilla side); honest
asymmetry recorded (server serves seam entries, client consumes family
types)
Review untangle (three layers: local door / wire protocol / fabric):
- vanilla ssh -D binds its SOCKS5 proxy client-side (the local door —
alksocks' local-door shape) and opens plain direct-tcpip per
connection: the server needs nothing SOCKS-shaped (tunnel row +
route-1 bridge cover the leg; POC-3 correction 2 already said this)
- the family-native SOCKS5-in-channel server row served no consumer
of ours: family apps compose alksocks' local door locally over the
alkssh client's direct-tcpip DialFn — alkssh doesn't depend on
alksocks on this path
- amendments: ADR-003 §1a (feature removed), ADR-006 point 3 (SOCKS-
detect retired; session dispatch is parse-only; request-less
session channels decline), OQ-02 resolution updated with the
simplification, mapping-table.md rows annotated (research doc keeps
POC-3 stage B as the re-add feasibility record)
- ADR-006 (Accepted): direct-tcpip rides the adapter-side bridge
(alktunnels route 1; registry-side bridging rejected as door-posture
inversion; absence note resolved); git over SSH is exec-only with
subsystem declining (vanilla git's connection model is exec of
git-{upload,receive}-pack — POC-3 grammar table); session dispatch:
request-driven parse wins, request-less channels get the bounded
SOCKS-detect (first-byte 0x05, 500 ms default, assembly-configurable);
spawn-and-return recorded as a structural constraint
- OQ-02 resolved by ADR-006; README doc/ADr tables + OQ summary updated
- enabling the rsa feature is reverted: no other family location uses
RSA, ed25519 is the suggested path everywhere; the tree drops
rsa 0.10.0-rc.18 + pkcs1 0.8.0-rc.4 entirely (verified: aws-lc-rs-only
scratch build clean and RC-free except ssh-key =0.7.0-rc.11, which
russh exact-pins itself on the russh::keys path)
- RSA user keys decline fail-closed (russh fails RSA signing explicitly
when the feature is absent — 0.63.2 fix); assembly registration policy
(alkforge: register ed25519) noted as assembler territory
- README ADR table title updated
- ADR-002 (Accepted): pin russh 0.65 (0.65.0 published 2026-10-25
after the POCs' 0.64.1 verification — six security fixes incl.
three in internet-facing classes; breaks are handler-signature
shapes only, wire evidence transfers); aws-lc-rs backend confirmed
with a clean in-scratch build record (no provisioning; ring kept as
the recorded fallback); flate2 off (no compression); rsa on (front-
door compat — supersedes the research docs' earlier off-leaning;
RC-tree posture accepted with a recorded stable-release trigger);
des/dsa never; rust-version 1.89 lands with the dep
- russh.md: re-verification-at-architecture-time section added
(0.65.0 delta: PublicKeyOffer/VerifiedPublicKey, async-trait
removed, GHSA list, verified build + tree facts)
- ADR-003 (Accepted): five features (session/git/tunnel/
dynamic-forward/sftp-placeholder), flat + independent, default [],
two-direction symmetry rule (one name, both doors), client acceptor
fail-closed (not russh's accept-and-drop), x11/auth-agent never
features, honest advertisement per build
- OQ-05 resolved: known-beforehand hosts (assembly-minted/family runners)
-> pinned, hard-decline on mismatch; unknown-beforehand hosts (rental
fleets) -> assembly-provided HostKeyPolicy the crate calls — prompt-
record, provider-metadata match, or learn-and-record. Crate default
stays fail-closed reject-unknown; crate never decides trust; storage
of learned pins is assembly detail
- auth-bridge.md: refinement postscript — no-TOFU is a no-trust-decisions-
-in-crate rule, not pin-everything; alkminer's accept-all was the fleet-
churn collapse, and the real anchor for ephemeral fleets is hub-held
key material, not host pins
- reverse-runner.md §5 rewritten with the same nuance
- pocs.md: marked complete; corrected the stale 0.5x-era accept-defaults
hypothesis wording (0.64.1: only auth_publickey_offered defaults to
accept), stage D recorded as ran-in-full, completion gate updated with
per-question resolution status
- added client-side scope section: no dedicated POC needed (client API
exercised throughout POC-1/POC-3) + the acceptor-default fact (russh
client accept-and-drop for unhandled forwarded opens is ours to fix)
- new reverse-runner.md: alkminer pattern extraction (concepts only,
never code) — hub-dials-runner surface inventory, both tunnel legs,
the OAI-proxy secret-hygiene shape, and the client-direction lessons
(keepalive defaults, reconnect-restore, acceptor disposition)
- vision.md: scope-question resolution addendum + whitespace typo fix
All four stages ran against vanilla clients (OpenSSH -L/-R, real git,
real curl) and the published crates.io seams with zero seam fixups
(51/51 checks, reproduced twice; POC source /workspace/alkssh-poc3):
- stage A: vanilla git push/clone/second-push through russh exec
parse -> alkgit serve_duplex_resolved + alkgit-substrate mem
composition; exit-status proven both directions; denial is
repo-blind before any protocol byte
- stage B: alkgit serve_socks5_over over a session channel with the
shipped tcp_dial; real curl through the proxy; 8 MiB throughput
- stage C: direct-tcpip via alktunnels route 1 (TunnelSession::
open_direct over a real alkcall ChannelClient + local_dial);
vanilla ssh -L + curl; -R global request declines (honest
advertisement); opener identity rides the transport into the
establisher witness (CF-005)
- stage D: session/pty mapping via a translation bridge (SSH bytes
<-> alktty chunk plane) and drive_session_pre_negotiated over a
real PTY: pty params, window-change resize, pipe stderr as
extended_data, exit chunk -> exit-status/exit-signal
Corrections recorded in mapping-table.md (git subsystem over SSH is
exec-only; vanilla ssh -D speaks SOCKS client-side and opens
direct-tcpip; the alktty bridge is chunk-framed both ways with the
exit-chunk signal-identity gap), plus the load-bearing russh 0.64.1
runtime facts in russh.md (inline handler dispatch — serving paths
must spawn; Session::handle() exit reporting; client drain-to-target
message routing).
Verification: cargo test / clippy -D warnings / fmt --check / doc on
alkssh (docs-only change); POC crate builds warning-free and runs
overall=true.
tokio = "1.54.1" (full features) with its lockfile tree; version pin
and feature set to be revisited with phase 1 architecture (committed
now to reduce working-tree noise)
- OpenSSH-PEM inner + alkvault EncryptedData outer confirmed as the
at-rest shape (4-way inner-format comparison recorded; alkvault's
&str-only encrypt/decrypt surface is the decisive API constraint)
- decrypt-at-dial lifecycle proven: mid-session vault lock leaves live
sessions intact; locked/absent/tampered/rotate all fail closed with
distinct error shapes
- Signer seam dismissed with evidence: no provided impl in russh
0.64.1 (doc claim stale), splice convention hand-rolled, in-process
vault gains nothing over one-time decode
- host keys ride the same model end to end incl. vanilla-tool
compatibility; 39/39 checks pass
- OQ seeds: decrypt-returns-plain-String zeroization gap, opaque
EncryptedData handling, vault-per-role policy
Verification: POC run 39 checks / 5 groups all PASS (source at
/workspace/alkssh-poc2, standalone, never merges)
- new: docs/research/poc-1-auth-bridge.md (standalone POC at
/workspace/alkssh-poc1; russh 0.64.1 + OpenSSH 9.6p1; 31/31 checks)
- confirmed hypotheses: SHA256 fingerprint parity across russh/
ssh-keygen/vanilla known_hosts; auth_publickey -> IdentityProvider
resolve -> accept/decline; offered-hook accept-and-let-decide with
OpenSSH probe-then-sign; ChannelOpenHandle drop = auto-reject
(AdministrativelyProhibited) with clean vanilla diagnostics;
max_auth_attempts counts the initial none probe; into_stream echo
plumbing works both sides
- correction recorded to auth-bridge.md + russh.md: 0.64.1 default
auth impls reject (only publickey_offered defaults to accept)
- new API fact in russh.md: rejected channel opens surface as Err
from the typed open call
- OQ seeds: username-ignore direction, max_auth_attempts sizing,
pin as one-way door (reject-defaults could change on bump)
A conflation in the research set: alkvault's derive-and-custody surface
(derive_ed25519, SLIP-0010 paths) was being sketched as the SSH key
path. Corrected across the docs — held SSH keys (client outgoing keys
and the server's own host key) are bearer-token-class secrets: opaque
bytes the family holds, vault-encrypted at rest, decrypted at use (the
alkhttp ADR-014 posture). No derivation story, no vault-custody
design, and the Signer-injection idea is withdrawn and recorded with
its dismissal rationale.
- auth-bridge.md: client-side gap re-framed as at-rest storage — the
at-rest shape (OpenSSH encoding inside alkvault EncryptedData), the
at-use lifecycle (decrypt-at-dial, one-time decode_secret_key,
zeroized for the dial), the KeySource-like source shape
- pocs.md: POC-2 rewritten to at-rest key handling (formats comparison,
decrypt-at-use lifecycle, locked-vault fail-closed, Signer dismissal)
- vision.md: principle 5 scoped (secret store, not key generator/custodian)
- russh.md: Signer seam noted as not presumed to be the vault shape
- AGENTS.md/CHANGELOG: convention 6 + context updated to match
Verification: cargo doc --no-deps, cargo fmt --check, cargo test
(docs-only change)
The phase 0 document set (documented only; no POCs run yet):
- docs/research/vision.md — guiding principles (russh owns the wire,
door-not-backend, fail-closed everything, one seam per mapping,
family identity not SSH identity); non-goals (no sshd, no sftp until
alkfs, no password auth); the five scope questions phase 0/1 must
answer
- docs/research/russh.md — 0.64.1-era inventory: server/client
Handler method tables, Config knobs, keys module (ssh-key re-export,
fingerprint API), the RC-pinned dependency tree (ssh-key, rsa,
pkcs1), MSRV/edition/license posture; alknet-main referenced as a
rough example only, explicitly not decision input
- docs/research/auth-bridge.md — the alkcall mapping: no
resolve_from_signature exists; the actual surface is
IdentityProvider::resolve_from_fingerprint fed by russh-verified key
fingerprints in auth_publickey; auth-method disposition matrix
(only publickey implemented, all defaults overridden); client side
mirrors alkhttp ADR-014 (key sources, not static keys; vault
client-side only); the vault-to-russh-key bridge named as the
missing design piece
- docs/research/mapping-table.md — the spine: channel-type rows with
feature candidates and the verified sibling serve/consume entries
(alktty drive_session, alkgit serve_duplex_resolved, alksocks
serve_socks5_over, alktunnels consumer wrappers + the no-serve-over-
stream absence note); request-level rows; parse-then-route dispatch
inside the session channel; the alk/ssh ALPN topology question
(ADR-004 reserved / ADR-034 stream_types [0,1] / ADR-042 hub never
runs it); alktunnels route choice flagged for ADR
- docs/research/pocs.md — POC-1 (auth bridge + fail-closed declines
against vanilla OpenSSH), POC-2 (vault-to-russh key bridge:
PrivateKey extraction vs vault-backed Signer), POC-3 (mapping
bake-off stages A-D: git exec, socks -D, tunnel -L, pty); all
standalone mode; findings docs are the deliverables
Verification: cargo doc --no-deps, cargo fmt --check, cargo test
(docs-only change; lib untouched)
Phase 0 base for the russh-wrapped SSH adapter (family ADR-003):
- Cargo.toml: single-crate alkssh 0.1.0, MIT OR Apache-2.0, lib only,
publish exclude list matching the sibling-crate policy; deliberately
empty [dependencies] — the russh pin is phase 0 research output
(alknet main's 0.49 is a data point, not the decision). [features]
carries the fail-closed posture as a comment until the capability
feature graph lands with the committed architecture
- src/lib.rs: crate-root doc describing the adapter shape and the
fail-closed feature contract
- LICENSE-MIT / LICENSE-APACHE (alktty's standard texts)
- .gitignore (target/, node_modules/, .worktrees/)
- CHANGELOG.md: Keep a Changelog stub, [Unreleased] initialization
- AGENTS.md: full agent operating instructions — git workflow
(conventional commits, commit-and-push), 13 project conventions
(russh owns the wire, fail-closed capability features, key reuse via
alkvault-backed sources, visible-surface=authorized-surface,
ALPN as one-way door), verification commands, taskgraph
task rules, SDD phase 0 lifecycle status, architecture context
Verification: cargo build/test/clippy --all-targets -D warnings and
cargo fmt --check all pass (lib stub, zero deps)
The .opencode agent definitions and docs/sdd_process.md were copied
verbatim from alkgit; replace the stale project references with
alkssh-shaped ones:
- sdd_process.md: title line named @alkdev/alkgit
- coordinator.md: example spawn prompt named the project and carried
alkgit constraints (pure protocol crate, gix, feature list)
- implementation-specialist.md: conventions 7-9 were alkgit's
(gitoxide/no-MPL, sha256/acme features, repo-public auth wording);
now russh-owns-the-wire, fail-closed mapping features, key reuse
- architect.md: deferral examples named alkgit-admin and the alkhttp
git-feature shape
- poc-specialist.md: standalone-POC path example /workspace/alkgit-poc1
Verification: docs-only change