Files
alkssh/AGENTS.md
T
glm-5.3-flash d5be18baed test(mapping): complete the decline-matrix contract suite (w5-decline-matrix)
- coverage audit recorded in the task file: every mapping.md row ×
  every build it can appear in × both directions → the pinning test
- gap rows landed in tests/dispatch_core.rs: the streamlocal_forward
  decline, the cancel-form refusions, the git-build pty decline
- never-features pinned in every build both directions; the auth-agent
  open recorded pin-bound upstream (russh 0.65 declines AgentForward
  opens pre-handler; no client initiator at the pin)
- sftp-only build verified no-op + declining via the matrix run
- matrix runner landed: scripts/matrix.sh (cargo test × 6 builds,
  clippy ×2 -D warnings, fmt, doc), joined AGENTS.md verification list

Verification: scripts/matrix.sh green end to end
2026-10-30 07:02:07 +00:00

254 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.
```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/<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).