Files
alkssh/AGENTS.md
T

14 KiB
Raw Blame History

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: <type>(<scope>): <summary> — 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 <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.

  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.

  14. 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 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).