Files
arbiter/AGENTS.md
2026-08-28 22:16:17 +02:00

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 policies
  • useragent/ — Flutter app (desktop + mobile + web targets) with a Rust core via flutter_rust_bridge
  • protobufs/ — Protocol Buffer definitions shared between server and clients
  • docs/ARCHITECTURE.md (peer types, flows, threat model) and IMPLEMENTATION.md; treat them as the design source of truth and update them when behaviour changes
  • scripts/ — 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_token on first run
  • Vault — encrypted root key and the Sealed/Unsealed state machine; on unseal decrypts the root key into a memsafe-backed SafeCell
  • FlowCoordinator — cross-connection flow between operators and SDK clients
  • OperatorRegistry — tracks currently connected operators
  • EvmActor — EVM transaction policy enforcement and signing
  • events — a kameo_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 a schema_version column 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 via arbiter-crypto
  • TLS: self-signed certificate (rustls + aws-lc-rs, prefer-post-quantum), fingerprint distributed via ArbiterUrl

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