From a779dd0d0df4d9ce5beffae25220518df57b4427 Mon Sep 17 00:00:00 2001 From: "glm-5.2" Date: Wed, 12 Aug 2026 05:47:59 +0000 Subject: [PATCH] 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 --- .opencode/agents/implementation-specialist.md | 72 +++-- AGENTS.md | 263 ++++++++++++++---- docs/sdd_process.md | 2 +- 3 files changed, 263 insertions(+), 74 deletions(-) diff --git a/.opencode/agents/implementation-specialist.md b/.opencode/agents/implementation-specialist.md index bfc1fa1..62b5216 100644 --- a/.opencode/agents/implementation-specialist.md +++ b/.opencode/agents/implementation-specialist.md @@ -209,28 +209,56 @@ This is especially important for complex tasks that span many file operations. Read `AGENTS.md` at project root for full details. Key rules: -1. **No comments in code** — Per project convention. -2. **Error handling** — Use `anyhow::Result` for application code, `thiserror` for - library error types. Never panic in library code. -3. **No `unwrap()` or `expect()` outside tests** — These are debug signals that - something wasn't clear. If you reach for `unwrap()`, it means the error - handling path wasn't specified — stop and think about what should actually - happen on that error. For poisoned locks, use - `unwrap_or_else(|e| e.into_inner())` or explicit error propagation. A panic - in one operation must not cascade to other operations. -4. **Cryptographic nonces use `OsRng`** — AES-GCM IVs and any other cryptographic - nonces must use `OsRng` (or equivalent CSPRNG), never `rand::random()`. IV - reuse under the same key is catastrophic for GCM. -5. **Secret material is zeroized on drop** — Any type holding derived keys, - decrypted credentials, or other secret material must derive `Zeroize` and - `ZeroizeOnDrop`. Secrets must not linger in freed heap memory. -6. **Feature flags** — Transports are feature-gated (`tls`, `iroh`, `acme`). Base - crate should compile lean. -7. **Async runtime** — `tokio` is the async runtime. All I/O is async. -8. **Naming conventions** — Rust standard: `snake_case` for functions/variables/ - modules, `PascalCase` for types/traits, `SCREAMING_SNAKE_CASE` for constants. -9. **Module structure** — One module per component under `src/`. Re-export via - `mod.rs` or `lib.rs` as appropriate. +1. **No comments in code** — Per project convention. Doc comments (`///`, `//!`) + are fine and expected on public API. +2. **Error handling** — `thiserror` for library error types. No panics in + library code. No `unwrap()` or `expect()` outside tests. 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. 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** — `call.requested`/`call.responded` + payloads and `OperationContext.metadata` carry no private keys, API keys, or + decrypted credentials. Outbound credentials flow through `Capabilities` + injected at the assembly layer → `HandlerRegistration.capabilities` → + `OperationContext.capabilities` → handler. +5. **No-env-vars invariant** — no handler reads outbound credentials from any + source other than `OperationContext.capabilities`. 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). Do not make it concrete or hardcode the global registry. +7. **Wire formats are stable** — `EventEnvelope` (`{ type, id, payload }` + + length-prefixed JSON framing) and the channels 8-byte chunk header + (`[channel_id:u32 BE][length:u32 BE][payload]`) are one-way doors. New event + types may be added; existing shapes must not change. +8. **Producer/consumer, not server/client** — both sides of a call or channels + connection can initiate. Use "producer"/"consumer" or "accept side"/"connect + side," not "server"/"client." +9. **Vendored core types** — `Connection`, `ProtocolHandler`, `BiStream`, + `BidiStreamSource`, `AuthContext`, `IdentityProvider`, `Identity`, + `AuthToken`, `Capabilities`, `OwnershipProvider`, `HandlerError`, + `StreamError` live in this crate. Do not add a separate `alkcore` dependency. + Keep them lean (no TLS, no transport coupling, no endpoint/accept-loop). +10. **`alktype` dependency** — use `alktype` for binary layout (channels chunk + header, future binary payload schemas) and JSON payload schema validation + (`OperationSpec`'s `input_schema`/`output_schema`). Do not roll your own. +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). Verify both `cargo test` (default) and `cargo test --all-features` pass + if features are added. +12. **Abort cascades to descendants** — `call.aborted` for a parent cascades to + all non-terminal descendants. Default `abort-dependents`; + `continue-running` opt-in. The composing handler decides the child's policy, + not the wire caller. +13. **Peer authorization via `AccessControl`** — a remote peer's call is + authorized by `AccessControl::check(peer_identity)`. No `remote_safe` flag, + no `trusted_peer` bypass. `Visibility::Internal` ops are never wire-callable. +14. **Naming conventions** — Rust standard: `snake_case` for functions/variables/ + modules, `PascalCase` for types/traits, `SCREAMING_SNAKE_CASE` for constants. +15. **Module structure** — one module per file under `src/`, re-exported from + `src/lib.rs`. Public API surface is `lib.rs` re-exports. ## Key Principles diff --git a/AGENTS.md b/AGENTS.md index 28bb1f0..1926d69 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,8 +15,8 @@ explicitly asked." The workflow: 1. Make the change -2. Verify: `cargo test --all-features`, `cargo clippy --all-features - --all-targets`, `cargo doc --no-deps --all-features` if docs changed +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 @@ -31,7 +31,9 @@ Exceptions — **do not** commit or push without asking: 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 repo is on crates.io; `EncryptedData` is frozen per ADR-018) + (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 @@ -42,85 +44,244 @@ failed one. Git identity is preconfigured (`glm-5.2 `). Do not change `git config`, skip hooks, or use `git commit -i`. -## Project Conventions (Rust / cryptographic library) +## Project Conventions (Rust / RPC protocol crate) -This is a cryptographic vault crate. 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. +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 — IV reuse under the same key is - catastrophic"). + 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, `anyhow` for - application code (this crate is a library; use `thiserror`). No panics +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. **Cryptographic nonces use `OsRng`** — AES-GCM IVs and any other - cryptographic nonces must use `SysRng`/`OsRng` (or equivalent CSPRNG), - never `rand::random()`. IV reuse under the same key is catastrophic - for GCM (authenticity breaks, two-time-pad on plaintext). See - `docs/architecture/encryption.md` §Security Constraints. +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. **Secret material is zeroized on drop** — any type holding derived - keys, decrypted credentials, or other secret material must derive - `Zeroize` and `ZeroizeOnDrop`. Secrets must not linger in freed heap - memory. See `EncryptionKey` and `Seed` for the pattern. +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. **Feature flags** — `secp256k1` is a feature gate for the Ethereum - BIP-0032 derivation path. The base crate compiles lean (no networking, - no `tokio`, no `secp256k1` unless the feature is on). Verify both - `cargo test` (default) and `cargo test --all-features` pass. +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. **No `async`** — this crate is synchronous (`std::sync::RwLock`, - direct method calls). No `tokio` dependency (ADR-025). Do not - introduce `async`/`.await` or async-sync primitives. +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. **Naming** — Rust standard: `snake_case` for functions/variables/ - modules, `PascalCase` for types/traits, `SCREAMING_SNAKE_CASE` for - constants. +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. **Module structure** — one module per file under `src/`, re-exported - from `src/lib.rs`. Public API surface is `lib.rs` re-exports. +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. **Wire format is frozen** — `EncryptedData` is a stable wire format - shared with `alknet-storage` and the TypeScript `@alkdev/storage` - consumer by type-level agreement (ADR-018). Fields, encoding, and - semantics are locked. No field may be removed or renamed; new fields - must be optional (default on deserialization) and must not change the - meaning of existing fields. The `salt` field is unused in v2 key - derivation (ADR-020) but retained for wire-format compatibility — do - not remove it. +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 --all-features # full suite (~108 tests) -cargo test # default features (~101 tests) -cargo clippy --all-features --all-targets -cargo doc --no-deps --all-features # if docs changed -cargo publish --dry-run --all-features --allow-dirty # before a release +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. -- ADR-018 — vault standalone, `EncryptedData` wire format lock -- ADR-020 — HD derivation for encryption keys, salt unused in v2 -- ADR-021 — key rotation via version-indexed derivation paths -- ADR-025 — local-only dispatch, no `async`/`tokio`/irpc +- 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. \ No newline at end of file + 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. \ No newline at end of file diff --git a/docs/sdd_process.md b/docs/sdd_process.md index 245cfaf..0ee8a31 100644 --- a/docs/sdd_process.md +++ b/docs/sdd_process.md @@ -2,7 +2,7 @@ ## Overview -This document defines the SDD process for the @alkdev/alkvault package. It +This document defines the SDD process for the @alkdev/alkcall package. It leverages: - **OpenCode CLI** as the agent execution environment