Files
alkhttp/AGENTS.md
T
deepseek-v4-flash 604b7c7c63 docs: document conventional commits style and unit-of-work commits in AGENTS.md
- 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
2026-08-25 08:51:33 +00:00

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:

  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 (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 handlingthiserror 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.capabilitiesOperationContext.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 → CapabilitiesHandlerRegistration.capabilitiesOperationContext.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 boundaryfrom_openapi/ from_jsonschema/to_openapi map call-protocol errors to HTTP status codes with HTTP_<status> 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.

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<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_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_<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_openapi gateway-spec versioning (info.version tracks 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_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.