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

15 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."

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.

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):

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.