# 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: 1. Make the change 2. Verify: `cargo test`, `cargo clippy --all-targets -- -D warnings`, `cargo fmt --check`, `cargo doc --no-deps` if docs changed 3. Inspect `git status` and `git diff` before staging — stage only the intended files, never secrets 4. Write a concise commit message in **conventional commits** style: `(): ` — types are `feat`, `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. 5. `git push origin main` 6. 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/ssh` ALPN pin, the capability feature graph, and the mapping contracts become stable one-way doors once consumers exist; see the ADRs in `docs/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 `). 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. 1. **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"). 2. **Error handling** — `thiserror` for library error types. No panics in library code. No `unwrap()` or `expect()` outside tests. If you reach for `unwrap`, the error path wasn't specified — stop and decide what should actually happen. For poisoned `RwLock`/`Mutex`, use `unwrap_or_else(|e| e.into_inner())` so a panic in one operation does not cascade to other operations. 3. **`tokio` is 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. Use `tokio::sync` primitives (`oneshot`, `mpsc`) for correlation; `parking_lot` for short-held internal locks. 4. **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. 5. **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. 6. **Key reuse, no env vars** — keys reuse the alknet `main` key 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). 7. **Visible-surface = authorized-surface** — every channel type and request exposed on the wire is checked against alkcall's `AccessControl::check(peer_identity)`; ops with `Visibility::Internal` are never wire-callable. There is no unauthenticated capability; the fail-closed feature declines are part of this invariant. This is a spec-level invariant. 8. **Wire formats are stable one-way doors** — the `alk/ssh` ALPN 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. 9. **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. 10. **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. 11. **Naming** — Rust standard: `snake_case` for functions/variables/ modules, `PascalCase` for types/traits, `SCREAMING_SNAKE_CASE` for constants. 12. **Module structure** — one module per file under `src/`, re-exported from the crate root (the alktty crate-root pattern). Public API surface is `lib.rs` re-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. 13. **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. ```bash 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/`). 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 questions - `russh.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 time - `auth-bridge.md` — russh auth ↔ alkcall `IdentityProvider` mapping (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; the `alk/ssh` ALPN topology question - `pocs.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.md` is the structural decision this repo builds from (and `002-alkforge-gitea-replacement.md` explains why: alkforge's git door, fail-closed posture); `architecture/open-questions.md` OQ-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; the `alk/ssh` ALPN and the channel-wire notes are alkcall ADR-004 and ADR-034), `alktty` (session/pty mapping target), `alktunnels` (direct/forwarded-tcpip target), `alksocks` (`-D` target), `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 `main` key 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).