- AGENTS.md: full rewrite for the call+channels RPC crate (async/tokio, vendored core types, alktype dep, producer/consumer framing, the two stable wire formats, no-env-vars invariant, OperationEnv trait, abort cascade, AccessControl peer auth, ADR index for both halves) - implementation-specialist.md: replace vault crypto conventions (OsRng, zeroize, AES-GCM) with 15 alkcall protocol conventions matching AGENTS.md - docs/sdd_process.md: fix alkvault -> alkcall reference Verification: no code changed (docs-only) commit 0a031cd..HEAD
14 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:
- Make the change
- Verify:
cargo test,cargo clippy --all-targets -- -D warnings,cargo fmt --check,cargo doc --no-depsif docs changed - Inspect
git statusandgit diffbefore staging — stage only the intended files, never secrets - 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. git push origin main- 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
EventEnvelopeshape 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 <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
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.
-
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 corruptsPendingRequestMapcorrelation and the abort-cascade tree"). -
Error handling —
thiserrorfor library error types. No panics in library code. Nounwrap()orexpect()outside tests. If you reach forunwrap, the error path wasn't specified — stop and decide what should actually happen. For poisonedRwLock/Mutex, useunwrap_or_else(|e| e.into_inner())so a panic in one operation does not cascade to other operations. -
tokiois the async runtime — all I/O is async. The call protocol's stream handling, the channels demux/mux,from_calldiscovery, and the dispatch loop are all async. Do not introduce blocking I/O on the async path. Usetokio::syncprimitives (oneshot,mpsc) for request correlation and subscription channels;parking_lotfor short-held internal locks (PendingRequestMap). -
No secret material on the wire — the call protocol carries no private keys, API keys, mnemonics, or decrypted credentials in
call.requestedpayloads,call.respondedpayloads, orOperationContext.metadata. Outbound credentials flow throughCapabilitiesinjected at the assembly layer →HandlerRegistration.capabilities→OperationContext.capabilities→ handler. See the no-env-vars invariant below and ADR-014 (alknet source). -
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::varreads are unreachable because the assembly layer never callsDefault::default(). This is a spec-level invariant, not a runtime convention. -
OperationEnvmust remain a trait — the trait-based design enables registry layering (session overlays, connection overlays, peer-keyed composition). MakingOperationEnvconcrete 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 asIdentityProvider. See ADR-024, ADR-029 (alknet source). -
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 nostream_typeconcept — the handler owns its sub-stream multiplexing on theBiStreamit receives. See ADR-071, ADR-093 (alknet source).
-
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).
-
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 separatealkcoredependency. 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). -
alktypedependency — usealktypefor binary layout (the channels chunk header, future binary payload schemas) and JSON payload schema validation (OperationSpec'sinput_schema/output_schema). Do not roll your own offset map or validator. See the alktype crate at/workspace/@alkdev/alktype. -
Feature flags — transports may be feature-gated if the need arises. The base crate should compile lean (no
quinn, noirohunless the feature is on). The vendoredConnectiontype supportsConnection::from_stream/from_bidifor transport-agnostic construction — the call and channels protocols are transport-agnostic by construction. Verify bothcargo test(default) andcargo test --all-featurespass if features are added. -
Naming — Rust standard:
snake_casefor functions/variables/ modules,PascalCasefor types/traits,SCREAMING_SNAKE_CASEfor constants. -
Module structure — one module per file under
src/, re-exported fromsrc/lib.rs. Public API surface islib.rsre-exports. The crate has two subsystems:registry(operation specs, context, dispatch, registry) andprotocol(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). -
Abort cascades to descendants —
call.abortedfor a parent request cascades to all non-terminal descendants in the call tree. Default policy isabort-dependents;continue-runningis an opt-in for long-running work. The abort policy is set onOperationContextand propagated throughOperationEnv::invoke()— the composing handler decides the child's policy, not the wire caller. See ADR-016 (alknet source). -
Peer authorization via
AccessControl— a remote peer's call is authorized byAccessControl::check(peer_identity)against the op'sAccessControl— the same mechanism that gates every other call. Noremote_safeflag, notrusted_peerbypass. An op withAccessControl::default()is callable by any peer; an op withrequired_scopesis callable only by peers whoseIdentity.scopessatisfy them; an op withVisibility::Internalis never callable from the wire. See ADR-029 (alknet source).
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.
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-callandalknet-channelsfrom 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 intodocs/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
EventEnvelopeframing (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 (
CallClientspawn_dispatchtransport-agnostic;from_callimports remote ops; connection direction independent of call direction) - ADR-024 — operation registry layering (curated + session +
connection overlays;
OperationEnvas trait-object integration point) - ADR-029 — peer-graph routing model (peer-keyed overlays +
PeerRefrouting;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-runningopt-in) - ADR-022 — handler registration, provenance, and composition authority
- ADR-023 — operation error schemas (typed
detailsincall.error) - ADR-049 — streaming handler for subscriptions
(
StreamingHandlertype,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/subscribeon channel 0's call registry) - ADR-075 —
ChannelsAdapterandChannelManager(substrate-agnostic demux loop;ChannelManageris 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-agnosticfrom_connectionprimary; dial lives in the consumer) - ADR-094 — per-identity channel cap (256 per
PeerIdviaChannelLifecyclePolicy)
Shared (vendored core types):
- ADR-001 — ALPN-based protocol dispatch
- ADR-002 —
ProtocolHandlertrait - ADR-004 — auth as shared core (
IdentityProviderin core, handlers extract credentials) - ADR-006 — ALPN string convention (
alknet/prefix, one ALPN per connection) - ADR-007 —
BiStreamtype definition (handlers receiveConnection, notBiStream) - ADR-065 —
Connection::from_stream(generic single-stream connections — unblocks TCP+TLS, SSH, WebTransport, wasm) - ADR-070 —
BidiStreamSourcetrait (theConnectionextension pointChannelBidiStreamSourceimplements) - ADR-092 —
BiStreamas the handler leaf (accept_bireturnsBiStream)
- ADR-064 — hand-rolled
-
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
EventEnvelopeshape was derived from the@alkdev/pubsubEventEnvelope(/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.