- Explicitly require small, focused commits (one per unit of work) so work is easy to revert and review - Specify conventional commits format: <type>(<scope>): <summary> with feat/fix/docs/chore/refactor/test/perf/build/ci types, matching the alkcall repo's established pattern
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. 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:
- Make the change
- Verify:
cargo test,cargo clippy --all-targets -- -D warnings,cargo fmt --check,cargo doc --no-depsif docs changed - Inspect
git statusandgit diffbefore staging — stage only the intended files, never secrets - Write a concise commit message in conventional commits style:
<type>(<scope>): <summary>— types arefeat,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. git push origin main- 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.
-
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. -
Error handling —
thiserrorfor library error types. No panics in library code. Nounwrap()orexpect()outside tests. If you reach forunwrap, the error path wasn't specified — stop and decide what should actually happen. For poisonedRwLock/Mutex, useunwrap_or_else(|e| e.into_inner())so a panic in one operation does not cascade to other operations. -
tokiois 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. Usetokio::syncprimitives (oneshot,mpsc) for request correlation and subscription channels;parking_lotfor short-held internal locks. -
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
Capabilitiesinjected at the assembly layer →HandlerRegistration.capabilities→OperationContext.capabilities→ handler. Thefrom_openapi/from_mcpadapters are the credential injection point. See the no-env-vars invariant below. -
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::varreads are unreachable because the assembly layer never callsDefault::default(). This is a spec-level invariant, not a runtime convention. -
OperationEnvmust remain a trait — the trait-based design enables registry layering (session overlays, connection overlays, peer-keyed composition). MakingOperationEnvconcrete 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 asIdentityProvider. -
The HTTP surface is the stable contract — the server serves REST APIs, the
to_openapi/to_mcpprojections of local call-protocol operations, the/healthzoperational 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. -
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.
-
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 theEventEnvelopewire format. Do not re-implement or fork those types here; do not add a separatealkcoredependency. 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. -
Feature flags — the HTTP transports are feature-gated:
h2andhttp1are default features (hyper),mcpgates thefrom_mcp/to_mcpadapters (rmcp). The base crate should compile lean (normcpunless themcpfeature is on). Verify bothcargo test(default) andcargo test --all-featurespass if features are added. -
Naming — Rust standard:
snake_casefor functions/variables/ modules,PascalCasefor types/traits,SCREAMING_SNAKE_CASEfor constants. -
Module structure — one module per file under
src/, re-exported fromsrc/lib.rs. Public API surface islib.rsre-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), andgateway(dispatch, error mapping). -
Adapter-registered ops are
Internalby default — operations registered by the adapters areVisibility::Internalunless explicitly marked otherwise. Peer authorization is viaAccessControl::check(peer_identity)— noremote_safeflag, notrusted_peerbypass. See ADR-015, ADR-024. -
Error fidelity across the HTTP boundary —
from_openapi/from_jsonschema/to_openapimap call-protocol errors to HTTP status codes withHTTP_<status>error codes. The gateway is the sole invoke path; per-callerAccessControl-filtered/searchis the discovery. See ADR-023, ADR-047.
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.
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-httpfrom 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 (
HttpAdapterregisters on standard HTTP ALPNs) - ADR-002 —
ProtocolHandlertrait (HttpAdapterimplements 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<Router>) - 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_mcpare the credential injection point) - ADR-015 — privilege model (adapter-registered ops are
Internalby default) - ADR-017 — call protocol client and adapter contract
(
OperationAdaptertrait;to_*are projections) - ADR-022 — handler registration, provenance, and composition authority (adapters produce leaf bundles)
- ADR-023 — operation error schemas (
HTTP_<status>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_openapigateway-spec versioning (info.versiontracks the gateway endpoint contract) - ADR-049 — streaming handler for subscription operations
(
HandlerKind::Stream; SSE →BoxStream<ResponseEnvelope>) - ADR-051 — YAML input format for
from_openapi(yaml_serde0.10.x is YAML 1.2) - ADR-066 —
from_jsonschemaas HTTP-backed single-endpoint adapter
Client:
- ADR-039 — one crate for server + client host (shared HTTP deps, shared mapping)
- ADR-001 — ALPN-based protocol dispatch (
-
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
EventEnvelopeshape 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 nativeEventEnvelopesession.