14 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.
-
No em dashes in prose. Do not use the em dash (U+2014) in source doc comments, markdown docs (
docs/,README.md,CHANGELOG.md,AGENTS.md), agent prompts under.opencode/agents/, or commit messages. Restructure instead: split the sentence, use parentheses, commas, a colon, or a semicolon, whichever reads naturally, and vary the choice rather than converting every instance the same way. A spaced hyphen (-) is not an acceptable substitute; it reads as a different but equally recognizable writing tell. The same rule covers en dashes used as clause separators, though en dashes inside numeric ranges keep their normal meaning. Hyphens in compound words (fail-closed,client-side) are untouched. In test fixtures and scripts the rule applies to prose-y content only: comment lines are rewritten too, while fixture data, wire examples, and tool transcripts survive verbatim.
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
grep -rn '—' src/ docs/ README.md CHANGELOG.md AGENTS.md \
.opencode/agents/ tests/ scripts/ # must be zero hits;
# see convention 14
scripts/matrix.sh # the decline-matrix runner: the full suite per
# feature build (none / git / session / tunnel /
# sftp / --all-features), clippy ×2, fmt, doc
cargo test --manifest-path fuzz/shared/Cargo.toml # the fuzz corpus gate: the committed seeds replay
# the `fuzz/` sidecar's invariant batteries
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).