# 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." 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 matching the repo style (see `git log --oneline -10`). 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 a public API surface that consumers implement or compile against (one-way doors — see "Public API shapes are one-way doors" below; once consumers exist, those signatures are stable contracts) - 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 `). Do not change `git config`, skip hooks, or use `git commit -i`. ## Project Conventions (Rust / TLS crate) This is the TLS crate — shared TLS setup types for the alk* stack: server and client `rustls` configs, cert resolvers, verifiers, and ACME state-machine wiring, transport-agnostic and shareable across transports (noq, `tokio-rustls` TCP+TLS, and anything else that consumes a `rustls` config). It is the extraction of the TLS handling from alknet (alknet ADR-082/087/088 and the `crates/tls` spec are the prior art). 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 security/correctness constraint would otherwise be missed (e.g., "the root store must never be empty — a container with no system CA bundle still needs to verify public X.509 remotes; see alknet ADR-088 §5"). 2. **Error handling** — `thiserror` for the library error type (`TlsError`, `#[non_exhaustive]`, one variant per failure category — the alknet ADR-088 shape). 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. The ACME state machine is a spawned task; cert loading is sync file I/O behind an `async fn` signature for API uniformity. Use the wasm-clean tokio subset (`rt`, `sync`, `macros`) — **do NOT use `features = ["full"]`** in `[dependencies]` (dev-dependencies may use `full`). 4. **The default crate should stay lean** — TLS setup and config types only. Transport-specific wrapping is feature-gated: `noq` (the `for_noq()` accessors), `tcp` (`tokio-rustls`), `acme` (the ACME state machine, a heavy dep). The `rustls` dep is always present — it is the core library. Unlike the sibling protocol crates (alktunnels, alktty), wasm is not a load-bearing target here: the crypto stacks (`aws-lc-rs`) and file I/O are platform code. If a design decision ever makes the default crate heavier than "rustls + cert material + tokio spawn," that is an ADR-worthy decision, not an accident. 5. **Behavior-preservation invariants are load-bearing** — the extraction from alknet must preserve these TLS behaviors exactly. An implementation that omits any of these compiles and passes type-checks but silently changes TLS behavior (alknet ADR-082 § Behavior-preservation invariants): - **`max_early_data_size = u32::MAX`** on all server config paths — enables 0-RTT / early data. Omitting it silently breaks 0-RTT clients. - **`rustls::crypto::aws_lc_rs::default_provider()`** as the crypto provider on all paths (alknet ADR-084). Do not switch to `ring` or the process-default provider without an ADR. - **`AcceptAnyCertVerifier`'s `supported_verify_schemes()`** returns ED25519 + ECDSA P-256/P-384 + RSA PSS/PKCS1 (SHA256/384/512), verbatim. - **`acme-tls/1` ALPN append** for the ACME path only, done by the TLS crate, not the caller (alknet ADR-027 §7). - **Non-empty root store** — the client CA path merges `webpki-roots` when the platform store is empty (alknet ADR-088 §5); a containerized deployment with no system CA bundle must still verify public X.509 remotes. 6. **Fail closed** — verifier selection (fingerprint pin / CA / fail closed) must never silently downgrade. An unknown raw-key remote fails at handshake, not to CA verification. Identity precedence and verifier selection follow the alknet ADR-034/ADR-091 shape: known peer + fingerprint → pin; unknown + X.509 → CA; unknown + raw key → fail closed. Client-auth cert presentation follows the local identity: raw key/X.509 present the cert, `None` presents nothing, `Acme` is a server-only identity (config error on the client path). 7. **One identity, N transports; one ACME state machine** — the central types are `TlsServerConfig` / `TlsClientConfig`, built once and shared (the inner `rustls` config is Clone — Arc-shared resolvers). `TlsServerConfig` is not `Clone` (it holds the ACME task's `JoinHandle`); share it via `Arc`. Never spawn a second ACME state machine for a domain already being served — duplicate orders risk Let's Encrypt rate limits and cert-cache divergence (alknet ADR-082 §The cert-reuse problem). This crate is the cert provider, not the accept loop — transport accept loops live in the consumers (assembly layer / endpoint crates). 8. **TLS-crate scope boundary** — this crate owns config *construction*. Handshake-time outcomes (a rejected cert, the unknown-raw-key fail-closed) flow through the transport's connector, not through `TlsError` — `TlsError` is the config-construction error type (alknet ADR-088 §6). ACME state-machine runtime errors are stream events, logged in the spawned task, not `TlsError` variants. Do not grow `TlsError` to cover handshake outcomes. 9. **Feature gates** — transport-specific deps are opt-in (`noq`, `tcp`, `acme`); `default = []` — the default crate compiles lean (ADR-003). Verify `cargo test` (default) and `cargo test --all-features` both pass whenever features are touched. 10. **Upstream posture** — this crate extracts working code from alknet (`crates/alknet-tls`, `crates/alknet-core` config/fingerprint). The alknet spec docs (ADR-082/083/084/086/087/088/089 and the `crates/tls` README) are the reference; where this crate deviates, record the deviation as an ADR here rather than silently diverging. Config types (`TlsIdentity`, `Ed25519SecretKey`) are expected to move here from `alknet-core` — this crate owns them after the rewrite; do not re-import them from alknet. 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 `src/lib.rs`. Public API surface is `lib.rs` re-exports. The alknet shape is the reference: `server.rs` (server config + resolvers), `client.rs` (client config + verifiers), `pem.rs` (cert/key loading), `signing.rs` (shared signing helpers). ## 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 test --all-features # if features are added/touched cargo publish --dry-run --allow-dirty # before a release ``` ## Architecture Context - `docs/research/phase-0.md` — the Phase 0 research findings (current phase). Phase 1 will produce `docs/architecture/` (overview, ADRs, open-questions tracker) per `docs/sdd_process.md`. - The prior art for this crate: - **alknet-tls** — `/workspace/@alkdev/alknet/crates/alknet-tls/` — the working extraction source (`server.rs`, `client.rs`, `pem.rs`, `signing.rs`); this crate supersedes it in the alknet rewrite. - **alknet docs** — `/workspace/@alkdev/alknet/docs/architecture/`: ADR-082 (extraction), ADR-083 (endpoint takes no TLS config), ADR-084 (aws-lc-rs provider), ADR-027 (identity model, `acme-tls/1`), ADR-086 (endpoint types / split ALPN lists), ADR-087 (`TlsClientConfig`), ADR-088 (`TlsError` shape, root-store fallback), ADR-089 (client dial seam), ADR-034 (verifier selection), ADR-091 (`ConnectionCredentials`), and `docs/architecture/crates/tls/README.md` (the full crate spec — the most complete reference for what this crate must do). - Key open threads carried from alknet (Phase 0 must resolve or defer them — see `docs/research/phase-0.md`): - The `TlsError` in the extracted code is the simplified 3-variant enum; the ADR-088 six-variant `#[non_exhaustive]` shape is the recorded target and was never implemented. - The config types (`TlsIdentity`, `Ed25519SecretKey`, `ConnectionCredentials`) still live in `alknet-core`; where they land for the rewrite is an alktls-side decision. - ADR-029's fingerprint question (`029-callclient-tls-client-auth...`) and the client-auth/remote-identity verification seams. - If a TODO references a design direction that an ADR has since decided against, the TODO is stale — remove it and align with the ADR. Do not implement the rejected design.