Files
alkcall/AGENTS.md
T
glm-5.3-flash 1e20bb77d0 chore: prepublish review — bump to 0.3.0, changelog, doc alignment
0.2.0 is already on crates.io (2026-08-31, c16b069); the review
004/005 remediation work is unreleased on top of it and lands as
0.3.0.

- Bump version 0.2.0 -> 0.3.0. Two OperationRegistry methods changed
  borrowed returns to owned (registration, list_operations) —
  source-breaking for annotated call sites, minor bump per 0.x
  semver rules. cargo semver-checks passes (196 checks) against the
  published baseline; the return-type changes were caught by manual
  diff review.
- CHANGELOG 0.3.0: connect-side serving (from_connection_with_serving
  + ServingConfig), OperationRegistry::fork + builder from_registry,
  registry::op_register (bootstrap op, collision policy,
  ALREADY_EXISTS), install_bootstrap_discovery, spec_to_json_pub +
  resource_id_path round-trip, overlay accessors, concurrent serving
  loops, &self registration.
- README: serving-as-consumer section, consumer role table update,
  drop the stale `mut` on the registry example.
- AGENTS.md: ADR range 001..047 -> 001..048.
- Fix rustdoc private-intra-doc-link warning on StartedDispatch.

Verification: 589 default / 606 all-features tests, clippy
(all-targets, all-features, wasm32) clean, fmt clean, doc clean,
publish dry-run OK.
2026-09-04 14:14:22 +00:00

299 lines
15 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."
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 matching the repo style (see `git log
--oneline -10`). 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 `EventEnvelope` shape and the
channels 8-byte chunk header are wire-format-stable — see ADR-014 and
ADR-034)
- 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.
Git identity is preconfigured (`glm-5.2 <glm-5.2@alk.dev>`). Do not
change `git config`, skip hooks, or use `git commit -i`.
## Project Conventions (Rust / RPC protocol crate)
This is the call + channels RPC crate — the unification of
`alknet-call` (structured JSON RPC: operations, streaming subscriptions,
service discovery) and `alknet-channels` (multiplexing proxy: N logical
channels over one transport stream, channel 0 pre-negotiated as
`alk/call`). 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 (e.g., "fresh per call — ID reuse under the same connection
corrupts `PendingRequestMap` correlation and the abort-cascade
tree").
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 call
protocol's stream handling, the channels demux/mux, `from_call`
discovery, and the dispatch loop 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
(`PendingRequestMap`).
4. **No secret material on the wire** — the call protocol carries no
private keys, API keys, mnemonics, or decrypted credentials in
`call.requested` payloads, `call.responded` payloads, or
`OperationContext.metadata`. Outbound credentials flow through
`Capabilities` injected at the assembly layer →
`HandlerRegistration.capabilities` → `OperationContext.capabilities`
→ handler. See the no-env-vars invariant below and ADR-010.
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`. See ADR-019, ADR-024
7. **Wire formats are stable** — two wire formats live in this crate:
- **`EventEnvelope`** (`{ type, id, payload }` with length-prefixed
JSON framing) — the call protocol's wire format. Cross-language
consumable (TypeScript, Python, any language). The envelope shape
and the five event types (`call.requested`, `call.responded`,
`call.completed`, `call.aborted`, `call.error`) are stable. New
event types may be added; existing ones must not change shape. See
ADR-014.
- **Channels 8-byte chunk header** (`[channel_id:u32 BE][length:u32
BE][payload]`) — the channels wire format. This is a one-way door:
changing the header format breaks all peers. The channels layer has
no `stream_type` concept — the handler owns its sub-stream
multiplexing on the `BiStream` it receives. See ADR-034, ADR-035
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. See ADR-022,
ADR-037 §direction semantics.
9. **Vendored core types** — the types formerly in `alknet-core`
(`Connection`, `ProtocolHandler`, `BiStream`, `BidiStreamSource`,
`AuthContext`, `IdentityProvider`, `Identity`, `AuthToken`,
`Capabilities`, `OwnershipProvider`, `HandlerError`, `StreamError`)
live in this crate. They are the home for these types going forward —
do not add a separate `alkcore` dependency. When the alknet mono-repo
is reworked, it will consume alkcall's versions. Keep these types
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. See ADR-007, ADR-008, ADR-009.
10. **BAST documents for wire formats** — every binary wire format in
this crate carries a BAST (Binary Abstract Syntax Tree) document as
its machine-readable spec (e.g. the channels chunk header's
`docs/architecture/chunk-header.bast.json`, embedded as
`CHUNK_HEADER_BAST`). BAST is plain JSON — no dependency required
to author or consume it. The `alktype` crate compiles BAST into
readers/writers/validators; future codegen derives
language-specific implementations. Trivial or hot-path formats
(chunk header, tty framing) stay hand-rolled with the BAST doc as
the contract; complex formats (sftp) use the alktype engine or
codegen. Do not roll your own offset map or validator for complex
formats. See the alktype crate at `/workspace/@alkdev/alktype`.
11. **Feature flags** — transports may be feature-gated if the need
arises. The base crate should compile lean (no `quinn`, no `iroh`
unless the feature is on). The vendored `Connection` type supports
`Connection::from_stream` / `from_bidi` for transport-agnostic
construction — the call and channels protocols are
transport-agnostic by construction. Verify both `cargo test`
(default) and `cargo test --all-features` pass if features are
added.
12. **Naming** — Rust standard: `snake_case` for functions/variables/
modules, `PascalCase` for types/traits, `SCREAMING_SNAKE_CASE` for
constants.
13. **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 two subsystems: `registry` (operation specs, context,
dispatch, registry) and `protocol` (wire format, streams, adapter,
dispatch loop, pending requests, abort cascade) for the call half;
and the channels half (wire format, `ChannelsAdapter`,
`ChannelManager`, `ChannelBidiStreamSource`, channel lifecycle
operations, `ChannelClient`).
14. **Abort cascades to descendants** — `call.aborted` for a parent
request cascades to all non-terminal descendants in the call tree.
Default policy is `abort-dependents`; `continue-running` is an
opt-in for long-running work. The abort policy is set on
`OperationContext` and propagated through `OperationEnv::invoke()`
— the composing handler decides the child's policy, not the wire
caller. See ADR-020.
15. **Peer authorization via `AccessControl`** — a remote peer's call is
authorized by `AccessControl::check(peer_identity)` against the op's
`AccessControl` — the same mechanism that gates every other call. No
`remote_safe` flag, no `trusted_peer` bypass. An op with
`AccessControl::default()` is callable by any peer; an op with
`required_scopes` is callable only by peers whose `Identity.scopes`
satisfy them; an op with `Visibility::Internal` is never callable
from the wire. See ADR-024.
## 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`.
Before a release, also verify the wasm target (the crate is
WASM-clean by construction — no net/fs/process tokio features):
```bash
cargo check --target wasm32-unknown-unknown
cargo clippy --target wasm32-unknown-unknown -- -D warnings
cargo semver-checks check-release # before a release
```
## Architecture Context
- `docs/architecture/` — the authoritative spec. Read it before
non-trivial changes. ADRs are numbered 001..048; OQs (open questions)
track resolved/deferred decisions.
- This crate unifies `alknet-call` and `alknet-channels` from the
alknet mono-repo (`/workspace/@alkdev/alknet`). The source
architecture docs were ported from
`/workspace/@alkdev/alknet/docs/architecture/` and renumbered as
alkcall ADRs (001..048; ADR-048 was authored in this crate). The ALPN
strings (`alk/call`,
`alk/channels`) are wire-stable going forward (renamed from
`alknet/` to `alk/` in v0.1.1, before the first published consumer).
- Key ADRs that inform this crate's design:
**Call protocol:**
- ADR-014 — hand-rolled `EventEnvelope` framing (irpc never
integrated; supersedes ADR-013)
- ADR-015 — call protocol stream model (bidirectional streams,
ID-based correlation)
- ADR-022 — call protocol client and adapter contract (`CallClient`
`spawn_dispatch` transport-agnostic; `from_call` imports remote ops;
connection direction independent of call direction)
- ADR-019 — operation registry layering (curated + session +
connection overlays; `OperationEnv` as trait-object integration
point)
- ADR-024 — peer-graph routing model (peer-keyed overlays +
`PeerRef` routing; `AccessControl`-based peer authorization)
- ADR-010 — secret material flow and capability injection (no secret
material on the wire; capabilities injected at assembly layer)
- ADR-017 — privilege model and authority context (`internal` =
authority switch not ACL skip; External/Internal visibility)
- ADR-020 — abort cascade for nested calls (default
`abort-dependents`, `continue-running` opt-in)
- ADR-018 — handler registration, provenance, and composition
authority
- ADR-016 — operation error schemas (typed `details` in `call.error`)
- ADR-021 — streaming handler for subscriptions
(`StreamingHandler` type, `invoke_streaming()` dispatch path)
- ADR-026 — forwarded-for identity (metadata only, never used by
`AccessControl::check`)
**Channels:**
- ADR-034 — channels wire format (8-byte chunk header; one-way door)
- ADR-035 — channels pure channel multiplexing (no `stream_type`,
`BiStream`-only, handler owns sub-stream multiplexing)
- ADR-036 — channel 0 pre-negotiated as `alk/call`
- ADR-037 — channel lifecycle operations (`channel/open`/`close`/
`control`/`resources/subscribe` on channel 0's call registry)
- ADR-039 — `ChannelsAdapter` and `ChannelManager` (substrate-agnostic
demux loop; `ChannelManager` is ALPN-blind, auth-blind,
transport-blind)
- ADR-040 — backpressure, channel limits, ID reuse (bounded-buffer,
256-channel per-connection memory bound, monotonic IDs)
- ADR-042 — hub relay (translate channel 0, byte-forward data
channels with ID rewrite)
- ADR-043 — `ChannelClient` (transport-agnostic
`from_connection` primary; dial lives in the consumer)
- ADR-041 — per-identity channel cap (256 per `PeerId` via
`ChannelLifecyclePolicy`)
**Shared (vendored core types):**
- ADR-001 — ALPN-based protocol dispatch
- ADR-002 — `ProtocolHandler` trait
- ADR-003 — auth as shared core (`IdentityProvider` in core,
handlers extract credentials)
- ADR-004 — ALPN string convention (`alk/` prefix, one ALPN per
connection)
- ADR-005 — `BiStream` type definition (handlers receive
`Connection`, not `BiStream`)
- ADR-007 — `Connection::from_stream` (generic single-stream
connections — unblocks TCP+TLS, SSH, WebTransport, wasm)
- ADR-008 — `BidiStreamSource` trait (the `Connection` extension
point `ChannelBidiStreamSource` implements)
- ADR-009 — `BiStream` as the handler leaf (`accept_bi` returns
`BiStream`)
- 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 was derived from the
`@alkdev/pubsub` `EventEnvelope`
(`/workspace/@alkdev/pubsub/src/types.ts`), which has a working
WebSocket client/server implementation. The call protocol refined it
with typed event names and structured payloads.