Adopt the alk*/fuzzing posture (alkcall 0.8.1 precedent) — the dep
bump this ride-along builds on was 4005517:
- docs/plans/fuzzing.md: the four-target plan (sse_parser,
ws_chunk_framing this session; spec_import + the stateful wave
later), the infrastructure blueprint, and the corpus/campaign
policies (§3's table tracks progress)
- fuzz/ workspace (the sibling layout): nightly-pinned subtree,
stable fuzz/shared invariant library (corpus replay = the standing
fuzz gate), detached runner (never a foreground child of an agent
session), JSON dict, deterministic seed generator; 80 committed seeds
- two wave-1 targets: sse_parser (SseParser::feed + the FWD-17
envelope mapping — the parser every subscription forwards through)
and ws_chunk_framing (WriteChunkWalk, the write pump's chunk walk)
- crate-side surfacing: SseParser/SseEvent/sse_event_envelope are pub
behind the adapters::forward::fuzzing re-export hub; the write
pump's inline chunk loop extracted as WriteChunkWalk (pump byte
behavior unchanged — 441 tests green); retained_bytes() diagnostic
(the alktunnels/alksocks posture)
- root Cargo.toml gains the explicit [workspace] table with
fuzz/ excluded (auto-discovery would pull the nightly-consumed
dev-deps into the stable build — the alkcall trap); fuzz/ joins the
publish exclude list
- AGENTS.md verification checklist gains the corpus-replay line
FWD-20 fix, found by driving the new sse_parser harness: feed()
rescanned the whole reassembly buffer per feed — a dribble-fed
subscription (one unterminated line over many small chunks) cost
quadratic CPU (1 MiB dribble ≈ minutes per connection, unbounded
total). Line assembly now walks only the arriving chunk: the buffer
holds just the current line, the scan never revisits drained bytes.
1 MiB dribble: ~5 min (quadratic) → 21 ms (linear). Regression test
sse_parser_dribble_feeds_do_not_rescan_the_buffer.
Verification: cargo test 441 passed; cargo test --all-features ok;
clippy --all-targets -- -D warnings (main + fuzz/shared) clean;
fmt --check (main, fuzz, fuzz/shared) clean; cargo fuzz build clean;
corpus replay (cargo test --manifest-path fuzz/shared/Cargo.toml) 80
seeds green.
Refs: alkcall docs/research/fuzzing.md (§7.9 posture), alksocks
08a3694 (the sibling wave-1 shape), docs/plans/fuzzing.md §2
13 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 crate is feature-sided (ADR-039 Amendment 1):
server(axum host, gateway, WS upgrade,to_*) andclient(outbound client host,from_*adapters) are both default features;openapiis the shared spec model (implied by both);mcpgates the MCP adapters,wssthe tungstenite transport,h2/http1the hyper protocol features (implyserver). A lean build takesdefault-features = falseplus one side. 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 test --manifest-path fuzz/shared/Cargo.toml # fuzz corpus replay (the fuzz gate)
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.
The corpus replay is the standing fuzz gate (the alkcall
docs/research/fuzzing.md §7.9 posture): it replays every committed
seed through the same invariant functions the fuzz targets run, on
stable, without nightly. Campaigns (nightly, cargo-fuzz) run manually
via fuzz/run-detached.sh — never as a foreground child of an agent
session — after touching the SSE parser, the WS byte adapter, or the
import adapters, and before releases. See docs/plans/fuzzing.md and
fuzz/README.md.
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.