8.6 KiB
AGENTS.md
Guidance for coding agents (Claude Code, Codex, …) working in this repository.
Project Overview
Arbiter is a permissioned signing service for cryptocurrency wallets:
server/— Rust gRPC daemon that holds encrypted keys and enforces policiesuseragent/— Flutter app (desktop + mobile + web targets) with a Rust core viaflutter_rust_bridgeprotobufs/— Protocol Buffer definitions shared between server and clientsdocs/—ARCHITECTURE.md(peer types, flows, threat model) andIMPLEMENTATION.md; treat them as the design source of truth and update them when behaviour changesscripts/— helper scripts, e.g.gen_erc20_registry.py
The vault never exposes key material; it only produces signatures when requests satisfy configured policies.
Toolchain Setup
Tools are managed via mise. Install all required tools:
mise install
Key versions live in mise.toml (currently Rust 1.95.0 with clippy, Flutter 3.41.7-stable, protoc 29.6, diesel_cli 2.3.7 with sqlite-bundled, Python 3.14). Also provided there: cargo-nextest, cargo-audit, cargo-vet, cargo-shear, cargo-mutants, cargo-features-manager, cargo-edit, ast-grep, flutter_rust_bridge_codegen.
Server (Rust workspace at server/)
Crates
| Crate | Purpose |
|---|---|
arbiter-proto |
Generated gRPC stubs + protobuf types (tonic-prost-build); also ArbiterUrl, home_path(), BOOTSTRAP_PATH |
arbiter-crypto |
Shared crypto primitives: authn (ML-DSA), safecell (hardened memory), hashing::Hashable, re-exported x-wing |
arbiter-macros |
#[derive(Hashable)] — canonical hashing of structs for the DB integrity layer |
arbiter-server |
Main daemon — actors, peers, DB, EVM policy engine, gRPC service implementation |
arbiter-client |
Rust client library for SDK clients (ArbiterClient, EVM wallet, key storage) |
arbiter-tokens-registry |
Generated ERC-20 token registry used by token-transfer policies |
Workspace lints (server/Cargo.toml) are strict: most of clippy pedantic/nursery plus a large restriction set. as casts, indexing/slicing, dbg!, float arithmetic and undocumented unsafe are denied or warned — expect to add an #[expect(..., reason = "...")] rather than to silence a lint globally.
Common Commands
cd server
# Build
cargo build
# Run the server daemon
cargo run -p arbiter-server
# Run all tests (preferred over cargo test; CI uses --all-features)
cargo nextest run
# Run a single test
cargo nextest run <test_name>
# Lint (CI runs it with -D warnings)
cargo clippy --all -- -D warnings
# Security audit
cargo audit
# Supply-chain review (config in server/supply-chain/)
cargo vet
# Check unused dependencies
cargo shear
# Mutation testing
cargo mutants
CI
Woodpecker pipelines in .woodpecker/ run on server/** changes: server-lint (clippy), server-test (nextest, --all-features), server-audit, server-vet, plus useragent-analyze for the Flutter app.
Architecture
The server is actor-based using the kameo crate. Long-lived state lives in GlobalActors (src/actors/mod.rs):
Bootstrapper— one-time bootstrap token, written to~/.arbiter/bootstrap_tokenon first runVault— encrypted root key and the Sealed/Unsealed state machine; on unseal decrypts the root key into amemsafe-backedSafeCellFlowCoordinator— cross-connection flow between operators and SDK clientsOperatorRegistry— tracks currently connected operatorsEvmActor— EVM transaction policy enforcement and signingevents— akameo_actors::MessageBus(DeliveryStrategy::Guaranteed) for cross-actor notifications
Per-connection state lives under src/peers/, not actors/: peers/client/ and peers/operator/, each with auth (challenge-response) and session (post-auth) sub-modules; the operator side additionally has vault_gate/ for the unseal handshake.
The gRPC surface lives in src/grpc/, split per peer (client/, operator/, common/) and per direction (inbound.rs — requests to the daemon, outbound.rs — server-initiated streams), with request_tracker.rs correlating the two.
EVM logic is in src/evm/: policies/ether_transfer/, policies/token_transfers/, abi.rs, safe_signer.rs.
Database: SQLite via diesel-async + bb8. Schema in src/db/schema.rs, models in src/db/models.rs, embedded migrations in crates/arbiter-server/migrations/. DB file lives at ~/.arbiter/arbiter.sqlite; tests use a temp-file DB via db::create_test_pool().
Entity ids are newtypes generated by the declare_id! macro in db::models (OperatorId, ChainId, …), each a #[repr(transparent)] wrapper over i32 with to_raw/from_raw. Pass these around instead of bare i32.
Row integrity: sensitive rows are covered by an HMAC-SHA256 envelope (src/crypto/integrity/, table integrity_envelope), keyed from the vault root key. A struct becomes coverable by deriving arbiter_macros::Hashable and implementing Integrable (KIND + VERSION). When adding or changing a covered entity, keep the derive and the payload version in sync — a mismatch surfaces as PayloadVersionMismatch or MacMismatch at runtime.
Cryptography:
- Authentication: ML-DSA-87 (post-quantum,
arbiter-crypto::authn::v1), challenge-response with per-peer nonce tracking - Encryption at rest: XChaCha20-Poly1305, versioned modules (
crypto/encryption/v1.rs) with aschema_versioncolumn for transparent migration on unseal - Password KDF: Argon2
- Unseal transport: X25519 ephemeral key exchange (
peers/operator/vault_gate/);x-wing(hybrid PQ KEM) is available viaarbiter-crypto - TLS: self-signed certificate (rustls + aws-lc-rs,
prefer-post-quantum), fingerprint distributed viaArbiterUrl
Crypto modules are versioned by convention: mod.rs re-exports the current vN. Add a v(N+1) rather than editing an existing version in place.
Protocol: gRPC with Protocol Buffers. ArbiterUrl encodes host, port, CA cert and bootstrap token into a single shareable string (printed to console on first run).
Proto Regeneration
arbiter-proto/build.rs compiles arbiter.proto, operator.proto, client.proto and evm.proto (with their shared/, operator/, client/ includes) on build:
cd server && cargo build -p arbiter-proto
Dart protobuf stubs are generated separately, from the repo root:
mise run codegen # protoc --dart_out=grpc:useragent/lib/proto
Database Migrations
# Create a new migration
diesel migration generate <name> --migration-dir crates/arbiter-server/migrations
# Run migrations manually (server also runs them on startup)
diesel migration run --migration-dir crates/arbiter-server/migrations
Pre-release policy: there is a single init migration and no deployed databases yet, so schema changes are made by editing that migration directly instead of stacking new ones. Regenerate src/db/schema.rs after changing it.
Code Conventions
#[must_use] Attribute:
Apply the #[must_use] attribute to return types of functions where the return value is critical and should not be accidentally ignored. This is commonly used for:
- Methods that return
boolindicating success/failure or validation state - Any function where ignoring the return value indicates a logic error
Do not apply #[must_use] redundantly to items (types or functions) that are already annotated with #[must_use].
Example:
#[must_use]
pub fn verify(&self, nonce: i32, context: &[u8], signature: &Signature) -> bool {
// verification logic
}
This forces callers to either use the return value or explicitly ignore it with let _ = ...;, preventing silent failures.
User Agent (Flutter + flutter_rust_bridge at useragent/)
The Flutter app calls Rust through flutter_rust_bridge 2.12.0. The Rust side is the rust_lib_arbiter crate at useragent/rust/; everything exposed to Dart is declared in useragent/rust/src/api/ and lands in useragent/lib/src/rust/ (see useragent/flutter_rust_bridge.yaml). Dart UI code is organised as lib/features/, lib/screens/, lib/widgets/, lib/providers/, lib/theme/, with routing in lib/router.dart (router.gr.dart is generated).
Common Commands
cd useragent
# Run the app
flutter run
# Regenerate Rust↔Dart bindings after editing rust/src/api/
mise run codegen # flutter_rust_bridge_codegen generate
# Analyze Dart code (also run in CI)
flutter analyze
Note: app/ contains only stale generated Flutter artifacts and is not the application source.