Files
alkcall/AGENTS.md
glm-5.2 a779dd0d0d docs: replace alkvault scaffold with alkcall project conventions
- 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
2026-08-12 05:47:59 +00:00

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:

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

  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 handlingthiserror 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.capabilitiesOperationContext.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 → CapabilitiesHandlerRegistration.capabilitiesOperationContext.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 descendantscall.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.

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.