Files
alkssh/AGENTS.md
T
glm-5.3-flash d37cbc0c8a test(fuzz): the four-target fuzz sidecar (w5-fuzz-harness)
The family standing rule (anything worth fuzzing gets fuzzed) on the
crate's ingress surfaces, per the alkgit convention:

- fuzz_targets/{exec_parse,fingerprint,key_decode,terminal_params} —
  thin fuzz_target! wrappers over four ingress surfaces:
  - exec_parse: the production parse path (from_utf8-fallback ingress
    shape), differentially paired against a from-spec grammar oracle;
    typed routing classes + determinism.
  - fingerprint: blob parse + the single fingerprint path; the
    SHA256:/unpadded-base64 contract shape via a from-spec
    base64 round-trip oracle; the committed ssh-keygen -lf vectors
    replayed inside the gate.
  - key_decode: the production decode arm (the #[doc(hidden)]
    decode_key_arm exposure, routed through itself); typed-class +
    bounded-fixed-message invariants; passphrase-shape robustness
    drive on the underlying decode.
  - terminal_params: the pty_request conversion via the #[doc(hidden)]
    terminal_params exposure; exact clamp, verbatim term, the
    reserved-modes {} JSON shape, serialized-shape determinism.
- shared/ — the invariant logic as a stable-clean library; the
  corpus gate (cargo test --manifest-path fuzz/shared/Cargo.toml)
  replays every committed seed through the batteries and joins
  AGENTS.md's verification list.
- corpus/<target>/ committed seeds + the deterministic
  gen_fuzz_seeds.py (no-op-diff verified).
- run-detached.sh + nightly pin; the sidecar excluded from the root
  workspace (workspace + package excludes; publish dry-run clean).

Exposure shape decided + recorded per target (README + lib.rs doc
comments): fingerprint needs none (fully public ingress); the rest
are #[doc(hidden)] re-exports of the production surfaces, never
forks. Detached campaigns ran per target (600 s default budget, fork
mode): all four clean; the two findings of the first round are
recorded in the task file (the harness oracle's double-/-strip bug;
the PPK echo fixed in the preceding commit).

Verification: corpus gate green (11 tests); scripts/matrix.sh green;
taskgraph validate clean.
2026-10-30 11:44:21 +00:00

13 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.

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