Files
alkssh/AGENTS.md
T
glm-5.3-flash e0805cd438 chore: initialize crate base (manifest, lib stub, licensing, agents)
Phase 0 base for the russh-wrapped SSH adapter (family ADR-003):

- Cargo.toml: single-crate alkssh 0.1.0, MIT OR Apache-2.0, lib only,
  publish exclude list matching the sibling-crate policy; deliberately
  empty [dependencies] — the russh pin is phase 0 research output
  (alknet main's 0.49 is a data point, not the decision). [features]
  carries the fail-closed posture as a comment until the capability
  feature graph lands with the committed architecture
- src/lib.rs: crate-root doc describing the adapter shape and the
  fail-closed feature contract
- LICENSE-MIT / LICENSE-APACHE (alktty's standard texts)
- .gitignore (target/, node_modules/, .worktrees/)
- CHANGELOG.md: Keep a Changelog stub, [Unreleased] initialization
- AGENTS.md: full agent operating instructions — git workflow
  (conventional commits, commit-and-push), 13 project conventions
  (russh owns the wire, fail-closed capability features, key reuse via
  alkvault-backed sources, visible-surface=authorized-surface,
   ALPN as one-way door), verification commands, taskgraph
  task rules, SDD phase 0 lifecycle status, architecture context

Verification: cargo build/test/clippy --all-targets -D warnings and
cargo fmt --check all pass (lib stub, zero deps)
2026-10-25 05:32:29 +00:00

12 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:

  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 (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

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