A conflation in the research set: alkvault's derive-and-custody surface (derive_ed25519, SLIP-0010 paths) was being sketched as the SSH key path. Corrected across the docs — held SSH keys (client outgoing keys and the server's own host key) are bearer-token-class secrets: opaque bytes the family holds, vault-encrypted at rest, decrypted at use (the alkhttp ADR-014 posture). No derivation story, no vault-custody design, and the Signer-injection idea is withdrawn and recorded with its dismissal rationale. - auth-bridge.md: client-side gap re-framed as at-rest storage — the at-rest shape (OpenSSH encoding inside alkvault EncryptedData), the at-use lifecycle (decrypt-at-dial, one-time decode_secret_key, zeroized for the dial), the KeySource-like source shape - pocs.md: POC-2 rewritten to at-rest key handling (formats comparison, decrypt-at-use lifecycle, locked-vault fail-closed, Signer dismissal) - vision.md: principle 5 scoped (secret store, not key generator/custodian) - russh.md: Signer seam noted as not presumed to be the vault shape - AGENTS.md/CHANGELOG: convention 6 + context updated to match Verification: cargo doc --no-deps, cargo fmt --check, cargo test (docs-only change)
251 lines
13 KiB
Markdown
251 lines
13 KiB
Markdown
# 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
|
|
```
|
|
|
|
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). |