- 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
248 lines
12 KiB
Markdown
248 lines
12 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 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_<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 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.
|