- coverage audit recorded in the task file: every mapping.md row × every build it can appear in × both directions → the pinning test - gap rows landed in tests/dispatch_core.rs: the streamlocal_forward decline, the cancel-form refusions, the git-build pty decline - never-features pinned in every build both directions; the auth-agent open recorded pin-bound upstream (russh 0.65 declines AgentForward opens pre-handler; no client initiator at the pin) - sftp-only build verified no-op + declining via the matrix run - matrix runner landed: scripts/matrix.sh (cargo test × 6 builds, clippy ×2 -D warnings, fmt, doc), joined AGENTS.md verification list Verification: scripts/matrix.sh green end to end
13 KiB
AGENTS.md
Operating instructions for opencode agents working in this repo. opencode
auto-loads this file as instructions, overriding the built-in defaults for
this project. Custom agents in .opencode/agents/ inherit these rules
unless their own prompts say otherwise.
Git Workflow
Commit and push when reasonable. When a change is complete and
verified (build + lint + tests pass), commit and push to origin/main
without asking. This overrides the built-in default of "only commit when
explicitly asked."
Commit in small, focused units — one commit per unit of work (a fix, a feature, a doc change), not one large commit covering many topics. Push regularly so work is never stranded locally.
The workflow:
- Make the change
- Verify:
cargo test,cargo clippy --all-targets -- -D warnings,cargo fmt --check,cargo doc --no-depsif docs changed - Inspect
git statusandgit diffbefore staging — stage only the intended files, never secrets - Write a concise commit message in conventional commits style:
<type>(<scope>): <summary>— types arefeat,fix,docs,chore,refactor,test,perf,build,ci; the scope is optional. For multi-point changes, use a summary line plus a body with bullet points and a verification block. git push origin main- Report the commit hash and the verification summary
Exceptions — do not commit or push without asking:
- The change is exploratory / speculative (you're not sure the user wants it kept)
- The user is actively reviewing the diff and may ask for changes
- The change touches wire-relevant or semver-relevant public API (the
alk/sshALPN pin, the capability feature graph, and the mapping contracts become stable one-way doors once consumers exist; see the ADRs indocs/architecture/decisions/once they exist) - You'd be force-pushing, amending a published commit, creating an empty commit, or skipping hooks
Never commit secrets, keys, or credentials. If a commit fails or hooks reject it, fix the issue and create a new commit — do not amend the failed one.
Git identity is preconfigured (glm-5.3-flash <glm-5.3-flash@alk.dev>).
Do not change git config, skip hooks, or use git commit -i.
Project Conventions (Rust / SSH adapter crate)
This is alkssh — the family's SSH adapter (family ADR-003): a thin
wrapper around russh terminating the vanilla-ssh wire and bridging
to/from alkcall channels. The SSH counterpart of alkhttp's adapter
role — the vanilla-ssh front door for vanilla clients. No binary until
an assembly needs one; downstream apps embed the crate and bring their
own deployment. The conventions below apply to all work in src/ and
tests/. They mirror .opencode/agents/implementation-specialist.md
§Project Conventions and are repeated here so they apply to every
session, not just spawned implementation agents.
-
No comments in code unless the user explicitly asks. This is a project-wide convention. Doc comments (
///,//!) are fine and expected on public API. Inline//comments only when the user asks or when a non-obvious safety/correctness constraint would otherwise be missed (e.g., "the decline path must run before any per-channel state is allocated — a leaked channel alloc is a capability leak"). -
Error handling —
thiserrorfor library error types. No panics in library code. Nounwrap()orexpect()outside tests. If you reach forunwrap, the error path wasn't specified — stop and decide what should actually happen. For poisonedRwLock/Mutex, useunwrap_or_else(|e| e.into_inner())so a panic in one operation does not cascade to other operations. -
tokiois the async runtime — all I/O is async. SSH sessions are long-lived streaming conversations; do not introduce blocking I/O on the async path. Usetokio::syncprimitives (oneshot,mpsc) for correlation;parking_lotfor short-held internal locks. -
russh owns the SSH wire — no in-family SSH framing (family ADR-003 decision 1). All SSH protocol framing, key exchange, and channel multiplexing come from russh; the crate's substance is the glue to/from alkcall and the channel-type mappings. Third-party- upstream posture: file asks to russh upstream; never silently absorb a workaround.
-
Fail-closed capability features (the binding constraint) — every channel-type mapping is a cargo feature. A session opening an unfeatured or unimplemented channel type is declined with a clean error — never a half-capability, never a silent fallback (family ADR-003 decision 3). A default build (no features) has no capabilities; a git-only build cannot serve a shell, tunnels, or socks. This is alkforge's ssh endpoint shape. Every disabled feature's decline path is contract territory and gets tested too — feature-gated code that is never exercised in a given build rots silently otherwise.
-
Key reuse, no env vars — keys reuse the alknet
mainkey model (russh::keys, authorized-key/KeySource handling — see/workspace/@alkdev/alknet-main/crates/alknet-core/src/auth/) via alkvault-backed sources — keys held encrypted at rest, decrypted at use (the vault is a secret store on this path, not a key generator) (family ADR-003 decision 2). No secret material on the wire, in plaintext at rest, or in env vars — the standing family security posture (family ADR-002 decision 5). -
Visible-surface = authorized-surface — every channel type and request exposed on the wire is checked against alkcall's
AccessControl::check(peer_identity); ops withVisibility::Internalare never wire-callable. There is no unauthenticated capability; the fail-closed feature declines are part of this invariant. This is a spec-level invariant. -
Wire formats are stable one-way doors — the
alk/sshALPN pin is its own ADR (ledger rule 1, family ADR-003 decision 4) and, once published, does not change. The russh version pin is a one-way door in practice (SSH key exchange/kex algorithm negotiation crosses crate boundaries); bumps are deliberate, tested, and recorded. Protocol capability advertisement is honest: never advertise what we don't serve — an unfeatured channel type is declined, not advertised. -
Bounded resources — every session carries wall-clock, size, and channel limits (max concurrent channels, max channel window, session timeout, auth attempt limits). SSH servers are internet-facing; unbounded loops and unbounded buffers are bugs.
-
License hygiene — this project is MIT OR Apache-2.0. russh is MIT/Apache-2.0-compatible; check before adopting any new dependency, and never copy or derive code from incompatible-licensed reference projects. Reference code is for reading and facts, never copying.
-
Naming — Rust standard:
snake_casefor functions/variables/ modules,PascalCasefor types/traits,SCREAMING_SNAKE_CASEfor constants. -
Module structure — one module per file under
src/, re-exported from the crate root (the alktty crate-root pattern). Public API surface islib.rsre-exports. The mapping table (channel-type → family protocol) is the crate's spine; each mapping lives behind its feature and speaks only its family protocol's types. -
Composability boundary — alkssh is a door, not a backend: it terminates vanilla ssh and forwards mapped channel payloads to family protocol halves (alktty/alktunnels/alksocks/alkgit). It depends on alkcall types at the wire layer and the mapped crates' session/client surfaces, and carries no deployment policy — a downstream app (alkforge) embeds it, enables the features it wants, and brings identity/ACL configuration. No feature implies another; the feature graph is independent and flat until architecture says otherwise.
Verification Commands
Run these before committing. All must pass.
cargo test # full suite
cargo clippy --all-targets -- -D warnings
cargo fmt --check
cargo doc --no-deps # if docs changed
cargo publish --dry-run --allow-dirty # before a release
scripts/matrix.sh # the decline-matrix runner: the full suite per
# feature build (none / git / session / tunnel /
# sftp / --all-features), clippy ×2, fmt, doc
If feature flags are added, also run cargo test --all-features and
cargo clippy --all-features --all-targets -- -D warnings. Once the
feature graph lands, each disabled feature's decline path gets a test —
the fail-closed invariant is contract territory on both sides of every
feature.
Task files (taskgraph)
Any file under tasks/ is a taskgraph task. Frontmatter is required
(id, name) and its enum fields are closed sets (taskgraph
0.1.3): status: pending|in-progress|completed|failed|blocked (never
done or finished), scope: single|narrow|moderate|broad|system,
risk: trivial|low|medium|high|critical, impact: isolated|component|phase|project, level: planning|decomposition|implementation|review|research. A file without
frontmatter is silently dropped from the graph (validate does not flag
it) — prose-only trackers belong under docs/. Run taskgraph validate after writing or editing anything under tasks/; full field
semantics live in docs/sdd_process.md §"Task File Format" /
§"Categorical Estimates".
Lifecycle Status
The project is in SDD phase 0 (exploration; docs only — no
architecture committed, no implementation). Family ADR-003 (2026-10-25,
Accepted) records the structural decision already made: russh-wrapped
adapter, no in-family SSH framing, fail-closed capability features,
alknet-main key model via alkvault-backed sources; OQ-08's ordering
question is resolved (alkssh leads alkforge's phase 0) and OQ-08's
remaining scope — the channel-type → family-protocol mapping table
details, the auth design tying russh's authorized-key handling to
alkcall auth, and the alk/ssh ALPN pin (ledger rule 1) — is this
repo's phase 0 work. Phase 0 output lands in docs/research/ (the
phase 0 document is the current work item); docs/architecture/ +
ADR-001 (the alk/ssh ALPN pin) come with phase 1. POC code never
merges to main — two modes: branch mode (POC builds on repo code; git
branch, findings merged into research docs, branch dropped) or
standalone mode (POC independent of repo code; scratch project at
/workspace/<poc-name>). POCs have relaxed constraints
(comments/unwrap acceptable) — they are exploration tools, not
production code.
Architecture Context
docs/sdd_process.md— the SDD process (phases, roles, task format)docs/research/— phase 0 research (current source of truth for design direction):vision.md— vision, guiding principles, non-goals, the phase 0 scope questionsrussh.md— russh crate inventory (server/client Handler, Config knobs, keys module) and pin considerations (RC-pinned dep tree, MSRV, license) — no alknet-derived bias; the pin is verified against current russh at architecture timeauth-bridge.md— russh auth ↔ alkcallIdentityProvidermapping (server: verified-key fingerprint →resolve_from_fingerprint; client: alkvault-backed key sources — held keys encrypted at rest, at-rest/bearer-token-class storage, not key derivation)mapping-table.md— the crate's spine: SSH channel type / request → family protocol seam + cargo feature; the alktunnels absence note; thealk/sshALPN topology questionpocs.md— POC-1..3 plan (auth bridge, client-side at-rest key handling, mapping bake-off) gating phase 0 completion
- family docs:
/workspace/@alkdev/alkfamily/docs/—architecture/decisions/003-alkssh-russh-adapter.mdis the structural decision this repo builds from (and002-alkforge-gitea-replacement.mdexplains why: alkforge's git door, fail-closed posture);architecture/open-questions.mdOQ-08 holds the remaining scope - Sibling crates (published, MIT OR Apache-2.0, each with per-repo
AGENTS.md +
docs/architecture/decisions/):alkcall 0.10(call + channels RPC with ACL; thealk/sshALPN and the channel-wire notes are alkcall ADR-004 and ADR-034),alktty(session/pty mapping target),alktunnels(direct/forwarded-tcpip target),alksocks(-Dtarget),alkgit 0.1.0(git exec/subsystem target),alkvault 0.1(key/secret storage). The alkhttp adapters are the closest structural analog for the adapter shape. - alknet
mainkey model reference:/workspace/@alkdev/alknet-main/crates/alknet-core/src/auth/(russh 0.49,keys.rs/server_auth.rs/identity.rs) — the authorized-key/KeySource construction alkssh reuses; a reading aid and correctness reference, not a path dependency. - russh: pin per phase 0 research (the alknet pin — 0.49 — is a data point, not a decision).