4.3 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project Overview
Arbiter is a permissioned signing service for cryptocurrency wallets. It consists of:
server/— Rust gRPC daemon that holds encrypted keys and enforces policiesuseragent/— Flutter desktop app (macOS/Windows) with a Rust backend via Rinfprotobufs/— Protocol Buffer definitions shared between server and client
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: Rust 1.93.0 (with clippy), Flutter 3.38.9-stable, protoc 29.6, diesel_cli 2.3.6 (sqlite).
Server (Rust workspace at server/)
Crates
| Crate | Purpose |
|---|---|
arbiter-proto |
Generated gRPC stubs + protobuf types; compiled from protobufs/*.proto via tonic-prost-build |
arbiter-server |
Main daemon — actors, DB, EVM policy engine, gRPC service implementation |
arbiter-useragent |
Rust client library for the user agent side of the gRPC protocol |
arbiter-client |
Rust client library for SDK clients |
Common Commands
cd server
# Build
cargo build
# Run the server daemon
cargo run -p arbiter-server
# Run all tests (preferred over cargo test)
cargo nextest run
# Run a single test
cargo nextest run <test_name>
# Lint
cargo clippy
# Security audit
cargo audit
# Check unused dependencies
cargo shear
# Run snapshot tests and update snapshots
cargo insta review
Architecture
The server is actor-based using the kameo crate. All long-lived state lives in GlobalActors:
Bootstrapper— Manages the one-time bootstrap token written to~/.arbiter/bootstrap_tokenon first run.KeyHolder— Holds the encrypted root key and manages the Sealed/Unsealed vault state machine. On unseal, decrypts the root key into amemsafehardened memory cell.FlowCoordinator— Coordinates cross-connection flow between user agents and SDK clients.EvmActor— Handles EVM transaction policy enforcement and signing.
Per-connection actors live under actors/user_agent/ and actors/client/, each with auth (challenge-response authentication) and session (post-auth operations) sub-modules.
Database: SQLite via diesel-async + bb8 connection pool. Schema managed by embedded Diesel migrations in crates/arbiter-server/migrations/. DB file lives at ~/.arbiter/arbiter.sqlite. Tests use a temp-file DB via db::create_test_pool().
Cryptography:
- Authentication: ed25519 (challenge-response, nonce-tracked per peer)
- Encryption at rest: XChaCha20-Poly1305 (versioned via
schemefield for transparent migration on unseal) - Password KDF: Argon2
- Unseal transport: X25519 ephemeral key exchange
- TLS: self-signed certificate (aws-lc-rs backend), fingerprint distributed via
ArbiterUrl
Protocol: gRPC with Protocol Buffers. The ArbiterUrl type encodes host, port, CA cert, and bootstrap token into a single shareable string (printed to console on first run).
Proto Regeneration
When .proto files in protobufs/ change, rebuild to regenerate:
cd server && cargo build -p arbiter-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
User Agent (Flutter + Rinf at useragent/)
The Flutter app uses Rinf to call Rust code. The Rust logic lives in useragent/native/hub/ as a separate crate that uses arbiter-useragent for the gRPC client.
Communication between Dart and Rust uses typed signals defined in useragent/native/hub/src/signals/. After modifying signal structs, regenerate Dart bindings:
cd useragent && rinf gen
Common Commands
cd useragent
# Run the app (macOS or Windows)
flutter run
# Regenerate Rust↔Dart signal bindings
rinf gen
# Analyze Dart code
flutter analyze
The Rinf Rust entry point is useragent/native/hub/src/lib.rs. It spawns actors defined in useragent/native/hub/src/actors/ which handle Dart↔server communication via signals.