- 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