# 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. This keeps each unit of work isolated and easy to revert or review if something goes wrong. 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: `(): ` — types are `feat`, `fix`, `docs`, `chore`, `refactor`, `test`, `perf`, `build`, `ci`; the scope is optional (e.g. `docs(review 002): ...`). 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 published wire formats or semver-relevant public API (this crate will be on crates.io; the HTTP surface and the HTTP-backed call-protocol adapters are the stable contract — see the ADRs in `docs/architecture/decisions/`) - 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. ## Project Conventions (Rust / HTTP interface crate) This is the HTTP interface crate — the extraction of `alknet-http` from the alknet mono-repo (`/workspace/@alkdev/alknet`). It serves HTTP/1.1 + HTTP/2 on standard ALPNs (with WebSocket upgrade for browser bidirectional access to the call protocol) and hosts the HTTP-backed call-protocol adapters (`from_openapi`, `to_openapi`, `from_mcp`, `to_mcp`). 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. 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. The HTTP server, the WebSocket upgrade path, and the reqwest-backed adapters are all async. Do not introduce blocking I/O on the async path. Use `tokio::sync` primitives (`oneshot`, `mpsc`) for request correlation and subscription channels; `parking_lot` for short-held internal locks. 4. **No secret material on the wire** — the HTTP surface carries no private keys, API keys, mnemonics, or decrypted credentials in request/response payloads or headers. Outbound credentials flow through `Capabilities` injected at the assembly layer → `HandlerRegistration.capabilities` → `OperationContext.capabilities` → handler. The `from_openapi`/`from_mcp` adapters are the credential injection point. See the no-env-vars invariant below. 5. **No-env-vars invariant** — no handler reads outbound credentials from any source other than `OperationContext.capabilities`. The credential injection path is vault → assembly layer → `Capabilities` → `HandlerRegistration.capabilities` → `OperationContext.capabilities` → handler. Downstream consumers' `std::env::var` reads are unreachable because the assembly layer never calls `Default::default()`. This is a spec-level invariant, not a runtime convention. 6. **`OperationEnv` must remain a trait** — the trait-based design enables registry layering (session overlays, connection overlays, peer-keyed composition). Making `OperationEnv` concrete or hardcoding the global registry into the dispatch path would close the session-overlay and connection-overlay patterns. This is the same integration-point pattern as `IdentityProvider`. 7. **The HTTP surface is the stable contract** — the server serves REST APIs, the `to_openapi`/`to_mcp` projections of local call-protocol operations, the `/healthz` operational endpoint, and the decoy surface for stealth mode. The gateway endpoints (`/search`/`/schema`/`/call`/`/batch`/`/subscribe`) are the sole invoke path for HTTP callers; the WebSocket path carries the native call-protocol session, not the gateway shape. HTTP/3 + WebTransport (`h3`) is deferred — browsers use WebSocket. See ADR-044, ADR-047, ADR-048. 8. **Producer/consumer, not server/client** — both sides of a call or channels connection can initiate. A producer exposes operations (call) or opens data channels (channels); a consumer calls operations or opens channels. Both sides can be both simultaneously — connection direction (who opened it) is independent of call/channel direction (who calls/opens). Avoid "server" and "client" framing in docs and API names; use "producer" and "consumer," or "accept side" / "connect side" for the connection-establishment half specifically. 9. **Dependency on the call crate** — this crate consumes the call protocol from the alkcall crate (`/workspace/@alkdev/alkcall`), which owns the vendored core types (`Connection`, `ProtocolHandler`, `BiStream`, `BidiStreamSource`, `AuthContext`, `IdentityProvider`, `Identity`, `AuthToken`, `Capabilities`, `OwnershipProvider`, `HandlerError`, `StreamError`) and the `EventEnvelope` wire format. Do not re-implement or fork those types here; do not add a separate `alkcore` dependency. Keep this crate lean (no TLS, no transport coupling, no endpoint/accept-loop); the dial and the TLS config are concerns of the consumer, not of this crate. 10. **Feature flags** — the HTTP transports are feature-gated: `h2` and `http1` are default features (hyper), `mcp` gates the `from_mcp`/`to_mcp` adapters (rmcp). The base crate should compile lean (no `rmcp` unless the `mcp` feature is on). Verify both `cargo test` (default) and `cargo test --all-features` pass if features are added. 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 crate has five subsystems: `server` (HttpAdapter, auth, stealth, `/healthz`, gateway routes), `websocket` (upgrade, native session overlay), `adapters` (`from_openapi`, `to_openapi`, `from_mcp`, `to_mcp`, `from_jsonschema`), `client` (reqwest-backed HTTP client host), and `gateway` (dispatch, error mapping). 13. **Adapter-registered ops are `Internal` by default** — operations registered by the adapters are `Visibility::Internal` unless explicitly marked otherwise. Peer authorization is via `AccessControl::check(peer_identity)` — no `remote_safe` flag, no `trusted_peer` bypass. See ADR-015, ADR-024. 14. **Error fidelity across the HTTP boundary** — `from_openapi`/ `from_jsonschema`/`to_openapi` map call-protocol errors to HTTP status codes with `HTTP_` error codes. The gateway is the sole invoke path; per-caller `AccessControl`-filtered `/search` is the discovery. See ADR-023, ADR-047. ## 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`. ## Architecture Context - `docs/architecture/` — the authoritative spec. Read it before non-trivial changes. ADRs are numbered 001..066; OQs (open questions) track resolved/deferred decisions. - This crate is the extraction of `alknet-http` from the alknet mono-repo (`/workspace/@alkdev/alknet`). The source architecture docs were ported from `/workspace/@alkdev/alknet/docs/architecture/crates/http/` and renumbered as alkhttp ADRs. The ALPN strings (`h2`, `http/1.1`) and the gateway endpoint contract are wire-stable going forward. - Key ADRs that inform this crate's design: **Server:** - ADR-001 — ALPN-based protocol dispatch (`HttpAdapter` registers on standard HTTP ALPNs) - ADR-002 — `ProtocolHandler` trait (`HttpAdapter` implements it) - ADR-004 — auth as shared core (Bearer → `resolve_from_token`) - ADR-010 — ALPN router and endpoint (stealth mode = HTTP handler on standard ALPNs) - ADR-039 — HTTP server and client host colocated in one crate - ADR-046 — assembly-layer custom HTTP routes on `HttpAdapter` (`extra_routes: Option`) - ADR-047 — remove the direct-call HTTP surface; the 5 gateway endpoints are the sole invoke path - ADR-048 — WebSocket carries the native call-protocol session, not the gateway shape **WebSocket:** - ADR-044 — defer h3/WebTransport; browsers use WebSocket ("browser is not a peer" rationale) - ADR-048 — WebSocket native session (framing, dispatch, bidirectionality, connection-local Layer 2 overlay) **Adapters:** - ADR-014 — secret material flow (`from_openapi`/`from_mcp` are the credential injection point) - ADR-015 — privilege model (adapter-registered ops are `Internal` by default) - ADR-017 — call protocol client and adapter contract (`OperationAdapter` trait; `to_*` are projections) - ADR-022 — handler registration, provenance, and composition authority (adapters produce leaf bundles) - ADR-023 — operation error schemas (`HTTP_` error codes) - ADR-041 — MCP tool-gateway pattern for `to_mcp` (4 fixed gateway tools, not one tool per operation) - ADR-042 — OpenAPI gateway pattern for `to_openapi` (5 fixed gateway endpoints, not one path per operation) - ADR-045 — `to_openapi` gateway-spec versioning (`info.version` tracks the gateway endpoint contract) - ADR-049 — streaming handler for subscription operations (`HandlerKind::Stream`; SSE → `BoxStream`) - ADR-051 — YAML input format for `from_openapi` (`yaml_serde` 0.10.x is YAML 1.2) - ADR-066 — `from_jsonschema` as HTTP-backed single-endpoint adapter **Client:** - ADR-039 — one crate for server + client host (shared HTTP deps, shared mapping) - If a TODO references a "Phase B" or a design direction that an ADR has since decided against, the TODO is stale — remove it and align the comments with the ADR. Do not implement the rejected design. - The call protocol's `EventEnvelope` shape is owned by the alkcall crate (`/workspace/@alkdev/alkcall`), which was extracted from the alknet mono-repo alongside this crate. The WebSocket path carries the native `EventEnvelope` session.