177 lines
8.6 KiB
Markdown
177 lines
8.6 KiB
Markdown
# 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](https://mise.jdx.dev/). Install all required tools:
|
|
```sh
|
|
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
|
|
|
|
```sh
|
|
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:
|
|
```sh
|
|
cd server && cargo build -p arbiter-proto
|
|
```
|
|
|
|
Dart protobuf stubs are generated separately, from the repo root:
|
|
```sh
|
|
mise run codegen # protoc --dart_out=grpc:useragent/lib/proto
|
|
```
|
|
|
|
### Database Migrations
|
|
|
|
```sh
|
|
# 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:
|
|
|
|
```rust
|
|
#[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](https://cjycode.com/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
|
|
|
|
```sh
|
|
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.
|