# 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-064 and ADR-071 in the alknet source docs, to be renumbered as alkcall ADRs) - 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 `). 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 `alknet/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-014 (alknet source). 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-024, ADR-029 (alknet source). 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-064 (alknet source). - **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-071, ADR-093 (alknet source). 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-017, ADR-073 §direction semantics (alknet source). 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-065, ADR-070, ADR-092 (alknet source). 10. **`alktype` dependency** — use `alktype` for binary layout (the channels chunk header, future binary payload schemas) and JSON payload schema validation (`OperationSpec`'s `input_schema`/ `output_schema`). Do not roll your own offset map or validator. 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-016 (alknet source). 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-029 (alknet source). ## 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; 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 are at `/workspace/@alkdev/alknet/docs/architecture/crates/call/` and `/workspace/@alkdev/alknet/docs/architecture/crates/channels/`. They will be ported into `docs/architecture/` here and renumbered as alkcall ADRs (starting at ADR-001). - The source ADRs are in `/workspace/@alkdev/alknet/docs/architecture/decisions/`. Key ADRs that inform this crate's design: **Call protocol:** - ADR-064 — hand-rolled `EventEnvelope` framing (irpc never integrated; supersedes ADR-005) - ADR-012 — call protocol stream model (bidirectional streams, ID-based correlation) - ADR-017 — call protocol client and adapter contract (`CallClient` `spawn_dispatch` transport-agnostic; `from_call` imports remote ops; connection direction independent of call direction) - ADR-024 — operation registry layering (curated + session + connection overlays; `OperationEnv` as trait-object integration point) - ADR-029 — peer-graph routing model (peer-keyed overlays + `PeerRef` routing; `AccessControl`-based peer authorization) - ADR-014 — secret material flow and capability injection (no secret material on the wire; capabilities injected at assembly layer) - ADR-015 — privilege model and authority context (`internal` = authority switch not ACL skip; External/Internal visibility) - ADR-016 — abort cascade for nested calls (default `abort-dependents`, `continue-running` opt-in) - ADR-022 — handler registration, provenance, and composition authority - ADR-023 — operation error schemas (typed `details` in `call.error`) - ADR-049 — streaming handler for subscriptions (`StreamingHandler` type, `invoke_streaming()` dispatch path) - ADR-032 — forwarded-for identity (metadata only, never used by `AccessControl::check`) **Channels:** - ADR-071 — channels wire format (8-byte chunk header; one-way door) - ADR-093 — channels pure channel multiplexing (no `stream_type`, `BiStream`-only, handler owns sub-stream multiplexing) - ADR-072 — channel 0 pre-negotiated as `alknet/call` - ADR-073 — channel lifecycle operations (`channel/open`/`close`/ `control`/`resources/subscribe` on channel 0's call registry) - ADR-075 — `ChannelsAdapter` and `ChannelManager` (substrate-agnostic demux loop; `ChannelManager` is ALPN-blind, auth-blind, transport-blind) - ADR-076 — backpressure, channel limits, ID reuse (bounded-buffer, 256-channel per-connection memory bound, monotonic IDs) - ADR-079 — hub relay (translate channel 0, byte-forward data channels with ID rewrite) - ADR-080 — `ChannelClient` (transport-agnostic `from_connection` primary; dial lives in the consumer) - ADR-094 — per-identity channel cap (256 per `PeerId` via `ChannelLifecyclePolicy`) **Shared (vendored core types):** - ADR-001 — ALPN-based protocol dispatch - ADR-002 — `ProtocolHandler` trait - ADR-004 — auth as shared core (`IdentityProvider` in core, handlers extract credentials) - ADR-006 — ALPN string convention (`alknet/` prefix, one ALPN per connection) - ADR-007 — `BiStream` type definition (handlers receive `Connection`, not `BiStream`) - ADR-065 — `Connection::from_stream` (generic single-stream connections — unblocks TCP+TLS, SSH, WebTransport, wasm) - ADR-070 — `BidiStreamSource` trait (the `Connection` extension point `ChannelBidiStreamSource` implements) - ADR-092 — `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.