Files
alkhttp/AGENTS.md
T
glm-5.3-flash 5971c0a0d6 fuzz: adopt fuzzing (wave 1) + fix the SSE dribble quadratic (FWD-20)
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
2026-09-29 08:32:29 +00:00

261 lines
13 KiB
Markdown

# 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 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 crate is feature-sided (ADR-039
Amendment 1): `server` (axum host, gateway, WS upgrade, `to_*`)
and `client` (outbound client host, `from_*` adapters) are both
default features; `openapi` is the shared spec model (implied by
both); `mcp` gates the MCP adapters, `wss` the tungstenite
transport, `h2`/`http1` the hyper protocol features (imply
`server`). A lean build takes `default-features = false` plus one
side. 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_<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.
```bash
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-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.