Compare commits

..

106 Commits

Author SHA1 Message Date
0677695b16 Merge pull request 'feat(proto)!: Shamir re-key, governance and bootstrapping vault state' (#102) from feat-shamir-protos into main
Reviewed-on: #102
Reviewed-by: Stas <business@jexter.tech>
2026-09-05 12:13:20 +00:00
Skipper
558910621b merge: rebase on main 2026-09-05 14:09:57 +02:00
CleverWild
d7fe3a6377 housekeeping: actualize AGENTS.md 2026-08-28 22:16:17 +02:00
Skipper
f97b8f9424 feat(grpc): governance contract 2026-08-26 13:01:44 +02:00
Skipper
27925a67ad merge: custom db ids for entities 2026-08-26 13:01:44 +02:00
Skipper
9ca2c4ed64 refactor(server::db): introduced newtype wrappers for entity id's in database 2026-08-26 13:01:44 +02:00
a0ac63f05c Merge pull request 'security: batch of fixes' (#95) from zeroized-bootstrap-token into main
Reviewed-on: #95
2026-08-26 13:01:44 +02:00
Skipper
ab79959dce housekeeping(server): deps upgrade + diesel migration to AsyncFnOnce 2026-08-26 13:01:44 +02:00
CleverWild
8dabec893e fix(user-agent): zombie sessions #74 2026-08-26 13:01:44 +02:00
Clippy Bot
3d4bba3584 test(client-auth): update metadata test to reflect frozen-metadata behavior 2026-08-26 13:01:44 +02:00
Skipper
853a038363 fix(server): MacOS build version 2026-08-26 13:01:44 +02:00
3ff0875c48 Merge pull request 'security(server): bind grant revocation state (revoked_at) to integrity hash' (#83) from security-hash-revoke_at into main
Reviewed-on: #83
2026-08-26 13:01:44 +02:00
Clippy Bot
7cc745182f Merge branch 'main' into zeroized-bootstrap-token 2026-08-26 13:01:44 +02:00
Skipper
a31ef99338 housekeeping(server): removed unused deps 2026-08-26 13:01:44 +02:00
CleverWild
7de235f144 fix(smlang::statemachine): macro invocation requires inner types to be public 2026-08-26 13:01:44 +02:00
CleverWild
2b8acad415 fix: lints 2026-08-26 13:01:44 +02:00
Clippy Bot
95799e5da8 fix: lints 2026-08-26 13:01:44 +02:00
Skipper
ec5a8143fb refactor: rename to to better reflect meaning 2026-08-26 13:01:44 +02:00
CleverWild
49e5f2c7f6 Merge branch 'main' into security-hash-revoke_at 2026-08-26 13:01:44 +02:00
CleverWild
00426e597d fix!: protect evm_wallet integrity and bind key ciphertext to wallet address
Three independent failures allowed an offline attacker with DB write access
to sign transactions using a different wallet's private key:
1. evm_wallet had no HMAC envelope — aead_encrypted_id could be swapped silently.
2. AEAD used a static tag as AAD — any valid ciphertext decrypted as any wallet key.
3. No post-decryption check that the derived address matched the requested wallet.

Fix: sign_entity covers (address, aead_encrypted_id) in a single transaction;
CreateNew/Decrypt take caller-provided AAD (wallet address bytes); after decryption
signer.address() is verified against the requested wallet address.
2026-08-26 13:01:44 +02:00
CleverWild
10486e6a32 fix(client-auth): freeze client metadata after initial operator approval 2026-08-26 13:01:44 +02:00
CleverWild
55b5b9361f fix(operator): bind sign-transaction to operator-approved client set 2026-08-26 13:01:44 +02:00
CleverWild
5824df16b1 fix(integrity): return AttestationStatus::Unavailable for key version mismatch 2026-08-26 13:01:44 +02:00
CleverWild
39d5dfd2e8 fix(wallet): delete access entries by entry ID instead of wallet ID 2026-08-26 13:01:44 +02:00
CleverWild
add058f1d5 security(bootstrap): use SafeCell for token storage instead of zeroize 2026-08-26 13:01:44 +02:00
CleverWild
5ecf3508d6 fix(UA): signing endpoint accepts arbitrary client_id 2026-08-26 13:01:44 +02:00
CleverWild
2b53023234 security: feat unseal and bootstrap handshake brute-force protection 2026-08-26 13:01:44 +02:00
CleverWild
850e2b8669 fix(bootstrap): token persists on disk with weak file permissions #59 2026-08-26 13:01:44 +02:00
CleverWild
4e11c27129 security(server): use SysRng directly for bootstrap token generation
Replace make_rng()/StdRng with UnwrapErr(SysRng) to eliminate the PRNG
intermediate layer and make OS entropy derivation explicit and unambiguous.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-26 13:01:44 +02:00
CleverWild
84e1ef7c85 security(memory): zeroization of bootstrap token 2026-08-26 13:01:44 +02:00
Skipper
e4db36f145 docs: move to folder and update to new challenge payload 2026-08-26 13:00:40 +02:00
CleverWild
7e23b3b2ec security(evm): remove client-controlled wallet_access_id from grant revocation 2026-08-26 13:00:40 +02:00
Skipper
602be3aa11 merge: feat-lints into main 2026-08-26 13:00:40 +02:00
CleverWild
bb28d311a1 feat(evm): add wallet_access_id to grant deletion requests and revocation logic 2026-08-26 13:00:40 +02:00
CleverWild
1d15e04a6b fix(lints): remove unstable ones 2026-08-26 13:00:40 +02:00
d8a03fcdbe Merge pull request 'refactor-integrity-check' (#90) from refactor-integrity-check into main
Reviewed-on: #90
2026-08-26 13:00:40 +02:00
CleverWild
3302bb8995 refactor(evm): implement revoke_grant method for grant revocation 2026-08-26 13:00:40 +02:00
CleverWild
d4084ded35 feat: rustc and clippy linting 2026-08-26 13:00:40 +02:00
6221debd84 Merge pull request 'housekeeping(server): dependencies upgrade' (#89) from push-zmvtzuwrnyyv into main
Reviewed-on: #89
2026-08-26 13:00:40 +02:00
Skipper
8392f27ce4 housekeeping(server): clippy warns fix 2026-08-26 13:00:40 +02:00
CleverWild
772936937b Merge branch 'security-hash-revoke_at' of ssh://git.markettakers.org:22222/MarketTakers/arbiter into security-hash-revoke_at 2026-08-26 13:00:40 +02:00
96b4eb1d98 Merge pull request 'refactor(hashing): introduce Hashable derive macro and migrate server types' (#82) from hashing-proc-macro into main
Reviewed-on: #82
Reviewed-by: Stas <business@jexter.tech>
2026-08-26 13:00:40 +02:00
Skipper
d7ca0b4fc8 housekeeping(server): dependencies upgrade 2026-08-26 13:00:40 +02:00
Skipper
e6724cb8d3 merge: refactor-integrity-check into main 2026-08-26 13:00:40 +02:00
CleverWild
7a64bcd1bf revert(server): bind grant revocation state (revoked_at) to integrity hash 2026-08-26 13:00:40 +02:00
CleverWild
943d0ee72f security(server): bind grant revocation state (revoked_at) to integrity hash 2026-08-26 13:00:40 +02:00
Skipper
6b572d0c86 housekeeping(server): clean too-broad visibility markers and organize imports 2026-08-26 13:00:40 +02:00
Skipper
bf63279f73 merge: refactor-integrity-check into main 2026-08-26 13:00:40 +02:00
Skipper
539265e3b1 docs: updated to new auth challenge format and removed stale TOCTOU race condition note 2026-08-26 13:00:40 +02:00
Skipper
cddf9e02ba fix(useragent): now using new challenge format 2026-08-26 13:00:40 +02:00
Skipper
270e843598 fix(server::tests): api surface of auth challenge changed 2026-08-26 13:00:40 +02:00
Skipper
0a48839321 refactor(server::{useragent::auth, client::auth}): use random based + timestamp nonce instead of monotonic counter in database 2026-08-26 13:00:40 +02:00
Skipper
f6151293d4 refactor(server): now keeps track of useragents, instead of 2026-08-26 13:00:40 +02:00
Skipper
3bbf713e67 refactor(server::grpc::vault_gate): standard approach using / traits 2026-08-26 13:00:40 +02:00
Skipper
810656702b fix(server): sending fixed vault state when on stage 2026-08-26 13:00:40 +02:00
hdbg
4823a291f8 WIP: kameo::messages wiring for transport generalization 2026-08-26 13:00:40 +02:00
hdbg
1d9b572565 feat(user-agent): add VaultGate for sealed vault authentication 2026-08-26 13:00:40 +02:00
hdbg
af7e90dcad fix(server::integrity): vault now differentias between expected/unexpected states for commands more granularly 2026-08-26 13:00:40 +02:00
hdbg
97e6b1950d fix(useragent): unsafe, but working implementation of ml-dsa 2026-08-26 13:00:40 +02:00
hdbg
d2f41e693c fix(server::user_agent): useragents now self-sign themselves on bootstrap 2026-08-26 13:00:40 +02:00
hdbg
9650407093 refactor(server): reorganized client/user_agent actors into separate module peers and added event MessageBus 2026-08-26 13:00:40 +02:00
100c257e95 Merge pull request 'Post-quantum crypto and better useragent security' (#80) from push-xrxykvkuxpsv into main
Reviewed-on: #80
2026-08-26 13:00:39 +02:00
CleverWild
247cb90fbf refactor(hashing): introduce Hashable derive macro and migrate server types 2026-08-26 13:00:39 +02:00
hdbg
13fb31f841 fix(server::bootsrapper): token compare is now constant-time 2026-08-26 13:00:39 +02:00
hdbg
a49baf7386 housekeeping(server): fixed clippy warns 2026-08-26 13:00:39 +02:00
hdbg
2df25b8641 tests(server::client::auth): integrity envelope insertion for valid paths 2026-08-26 13:00:39 +02:00
hdbg
6b814f4869 refactor(server): moved shared module crypto into arbiter-crypto 2026-08-26 13:00:39 +02:00
hdbg
11ad5df426 feat(server): add integrity verification for client keys 2026-08-26 13:00:39 +02:00
hdbg
97b7b483ff docs: ml-dsa scheme everywhere 2026-08-26 13:00:39 +02:00
307822187c Merge pull request 'fix(server): replaced postcard-based integrity fingerprint with custom trait providing order-independent hashing' (#77) from push-opwuyuwxknyo into main
Reviewed-on: #77
2026-08-26 13:00:39 +02:00
hdbg
4c2e00b56d refactor(server): migrated auth to ml-dsa 2026-08-26 13:00:39 +02:00
hdbg
dcf42b0e4e fix(server): added chain_id check and covered check_shared_constraints with unit tests 2026-08-26 13:00:39 +02:00
hdbg
bd0a8f3907 tests(server): property-based testing for ordering independency for hash 2026-08-26 13:00:39 +02:00
hdbg
38284d84b6 tests(server): initial cargo-mutants 2026-08-26 13:00:39 +02:00
hdbg
d482ed37aa fix(server): replaced postcard-based integrity fingerprint with custom trait providing order-independent hashing 2026-08-26 13:00:39 +02:00
hdbg
498e2e3145 fix(server): simplify hash function for debug profile 2026-08-26 13:00:39 +02:00
e1b32165e9 Merge pull request 'feat(server): integrity envelope engine for EVM grants with HMAC verification' (#51) from integrity-envelope into main
Reviewed-on: #51
2026-08-26 13:00:39 +02:00
hdbg
6f33d44697 docs: add recovery operators and multi-operator details 2026-08-26 13:00:39 +02:00
hdbg
9173be2920 tests(server): fixed for new integrity checks 2026-08-26 13:00:39 +02:00
hdbg
cdf2394963 docs: add multi-operator governance section 2026-08-26 13:00:39 +02:00
hdbg
d55e9880ec refactor(server): rework envelopes and integrity check 2026-08-26 13:00:39 +02:00
hdbg
37fa954933 merge: @main into client-integrity-verification 2026-08-26 13:00:39 +02:00
hdbg
ba7bfe9b6f fix(server): remove stale mentions of miette 2026-08-26 13:00:39 +02:00
hdbg
5c3421de1e housekeeping(server): fixed clippy warns 2026-08-26 13:00:39 +02:00
CleverWild
6a0c512773 chore: inline integrity proto types 2026-08-26 13:00:39 +02:00
hdbg
be23f87e67 refactor(user-agent): remove backfill pubkey integrity tags 2026-08-26 13:00:39 +02:00
CleverWild
4bd8a2df33 feat(server): integrity envelope engine for EVM grants with HMAC verification 2026-08-26 13:00:39 +02:00
hdbg
29f2174dcf fix(server): previously, user agent auth accepted invalid signatures 2026-08-26 13:00:39 +02:00
hdbg
149035ecf5 refactor(server): separate crypto by purpose and moved outside of actor into separate module 2026-08-26 13:00:39 +02:00
CleverWild
17ea8d8dc9 feat(auth): implement attestation status verification for public keys 2026-08-26 13:00:39 +02:00
CleverWild
e9e2888e95 fix(keyholder): comment drift 2026-08-26 13:00:39 +02:00
CleverWild
0b30416a7b refactor(keyholder): generalize derive_useragent_integrity_key and compute_useragent_pubkey_integrity_tag corespondenly to derive_integrity_key and compute_integrity_tag 2026-08-26 13:00:39 +02:00
CleverWild
e2e2c4fc07 feat(auth): add seal-key-derived pubkey integrity tags with auth enforcement and unseal backfill 2026-08-26 13:00:39 +02:00
hdbg
2b7d3257da style(dashboard): format code and add title margin 2026-08-26 13:00:39 +02:00
hdbg
ed6e102c96 feat(grants-create): add configurable grant authorization fields 2026-08-26 13:00:39 +02:00
hdbg
a7e031faca refactor(useragent::evm::grants): split into more files & flutter_form_builder usage 2026-08-26 13:00:39 +02:00
hdbg
8736a01e1f fix(useragent::dashboard): screen pushed twice due to improper listen hook 2026-08-26 13:00:39 +02:00
hdbg
11c45ca4e1 refactor(grants): wrap grant list in SingleChildScrollView 2026-08-26 13:00:39 +02:00
hdbg
5323eea129 style(dashboard): remove const from _CalloutBell and add title to nav rail 2026-08-26 13:00:39 +02:00
hdbg
238a9206e1 refactor(useragent): moved shared CreamPanel and StatePanel into generic widgets 2026-08-26 13:00:39 +02:00
hdbg
0048d627eb feat(evm): add EVM grants screen with create UI and list 2026-08-26 13:00:39 +02:00
hdbg
8aaeec078b refactor(proto): restructure wallet access messages for improved data organization 2026-08-26 13:00:39 +02:00
hdbg
3ca2554468 refactor(server::evm): removed repetetive errors and error variants 2026-08-26 13:00:39 +02:00
hdbg
d982954a11 refactor(useragent::evm::table): broke down into more widgets 2026-08-26 13:00:39 +02:00
hdbg
3b75d146a9 refactor(useragent::evm): moved out header into general widget 2026-08-26 13:00:39 +02:00
hdbg
27230f975f feat(useragent): vibe-coded access list 2026-08-26 13:00:39 +02:00
17 changed files with 292 additions and 2801 deletions

109
AGENTS.md
View File

@@ -1,13 +1,16 @@
# AGENTS.md # AGENTS.md
This file provides guidance to Codex (Codex.ai/code) when working with code in this repository. Guidance for coding agents (Claude Code, Codex, …) working in this repository.
## Project Overview ## Project Overview
Arbiter is a **permissioned signing service** for cryptocurrency wallets. It consists of: Arbiter is a **permissioned signing service** for cryptocurrency wallets:
- **`server/`** — Rust gRPC daemon that holds encrypted keys and enforces policies - **`server/`** — Rust gRPC daemon that holds encrypted keys and enforces policies
- **`operator/`** — Flutter desktop app (macOS/Windows) with a Rust backend via Rinf - **`useragent/`** — Flutter app (desktop + mobile + web targets) with a Rust core via `flutter_rust_bridge`
- **`protobufs/`** — Protocol Buffer definitions shared between server and client - **`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. The vault never exposes key material; it only produces signatures when requests satisfy configured policies.
@@ -18,7 +21,7 @@ Tools are managed via [mise](https://mise.jdx.dev/). Install all required tools:
mise install mise install
``` ```
Key versions: Rust 1.93.0 (with clippy), Flutter 3.38.9-stable, protoc 29.6, diesel_cli 2.3.6 (sqlite). 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/`) ## Server (Rust workspace at `server/`)
@@ -26,10 +29,14 @@ Key versions: Rust 1.93.0 (with clippy), Flutter 3.38.9-stable, protoc 29.6, die
| Crate | Purpose | | Crate | Purpose |
|---|---| |---|---|
| `arbiter-proto` | Generated gRPC stubs + protobuf types; compiled from `protobufs/*.proto` via `tonic-prost-build` | | `arbiter-proto` | Generated gRPC stubs + protobuf types (`tonic-prost-build`); also `ArbiterUrl`, `home_path()`, `BOOTSTRAP_PATH` |
| `arbiter-server` | Main daemon — actors, DB, EVM policy engine, gRPC service implementation | | `arbiter-crypto` | Shared crypto primitives: `authn` (ML-DSA), `safecell` (hardened memory), `hashing::Hashable`, re-exported `x-wing` |
| `arbiter-operator` | Rust client library for the operator side of the gRPC protocol | | `arbiter-macros` | `#[derive(Hashable)]` — canonical hashing of structs for the DB integrity layer |
| `arbiter-client` | Rust client library for SDK clients | | `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 ### Common Commands
@@ -42,54 +49,78 @@ cargo build
# Run the server daemon # Run the server daemon
cargo run -p arbiter-server cargo run -p arbiter-server
# Run all tests (preferred over cargo test) # Run all tests (preferred over cargo test; CI uses --all-features)
cargo nextest run cargo nextest run
# Run a single test # Run a single test
cargo nextest run <test_name> cargo nextest run <test_name>
# Lint # Lint (CI runs it with -D warnings)
cargo clippy cargo clippy --all -- -D warnings
# Security audit # Security audit
cargo audit cargo audit
# Supply-chain review (config in server/supply-chain/)
cargo vet
# Check unused dependencies # Check unused dependencies
cargo shear cargo shear
# Run snapshot tests and update snapshots # Mutation testing
cargo insta review 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 ### Architecture
The server is actor-based using the **kameo** crate. All long-lived state lives in `GlobalActors`: The server is actor-based using the **kameo** crate. Long-lived state lives in `GlobalActors` (`src/actors/mod.rs`):
- **`Bootstrapper`** — Manages the one-time bootstrap token written to `~/.arbiter/bootstrap_token` on first run. - **`Bootstrapper`** — one-time bootstrap token, written to `~/.arbiter/bootstrap_token` on first run
- **`Vault`** — Holds the encrypted root key and manages the Sealed/Unsealed vault state machine. On unseal, decrypts the root key into a `memsafe` hardened memory cell. - **`Vault`** — encrypted root key and the Sealed/Unsealed state machine; on unseal decrypts the root key into a `memsafe`-backed `SafeCell`
- **`FlowCoordinator`** — Coordinates cross-connection flow between operators and SDK clients. - **`FlowCoordinator`** — cross-connection flow between operators and SDK clients
- **`EvmActor`** — Handles EVM transaction policy enforcement and signing. - **`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 actors live under `actors/operator/` and `actors/client/`, each with `auth` (challenge-response authentication) and `session` (post-auth operations) sub-modules. 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.
**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()`. 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:** **Cryptography:**
- Authentication: ed25519 (challenge-response, nonce-tracked per peer) - Authentication: **ML-DSA-87** (post-quantum, `arbiter-crypto::authn::v1`), challenge-response with per-peer nonce tracking
- Encryption at rest: XChaCha20-Poly1305 (versioned via `scheme` field for transparent migration on unseal) - Encryption at rest: XChaCha20-Poly1305, versioned modules (`crypto/encryption/v1.rs`) with a `schema_version` column for transparent migration on unseal
- Password KDF: Argon2 - Password KDF: Argon2
- Unseal transport: X25519 ephemeral key exchange - Unseal transport: X25519 ephemeral key exchange (`peers/operator/vault_gate/`); `x-wing` (hybrid PQ KEM) is available via `arbiter-crypto`
- TLS: self-signed certificate (aws-lc-rs backend), fingerprint distributed via `ArbiterUrl` - TLS: self-signed certificate (rustls + aws-lc-rs, `prefer-post-quantum`), 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). 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 ### Proto Regeneration
When `.proto` files in `protobufs/` change, rebuild to regenerate: `arbiter-proto/build.rs` compiles `arbiter.proto`, `operator.proto`, `client.proto` and `evm.proto` (with their `shared/`, `operator/`, `client/` includes) on build:
```sh ```sh
cd server && cargo build -p arbiter-proto 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 ### Database Migrations
```sh ```sh
@@ -100,6 +131,8 @@ diesel migration generate <name> --migration-dir crates/arbiter-server/migration
diesel migration run --migration-dir crates/arbiter-server/migrations 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 ### Code Conventions
**`#[must_use]` Attribute:** **`#[must_use]` Attribute:**
@@ -121,29 +154,23 @@ pub fn verify(&self, nonce: i32, context: &[u8], signature: &Signature) -> bool
This forces callers to either use the return value or explicitly ignore it with `let _ = ...;`, preventing silent failures. This forces callers to either use the return value or explicitly ignore it with `let _ = ...;`, preventing silent failures.
## Operator (Flutter + Rinf at `operator/`) ## User Agent (Flutter + flutter_rust_bridge at `useragent/`)
The Flutter app uses [Rinf](https://rinf.cunarist.org) to call Rust code. The Rust logic lives in `operator/native/hub/` as a separate crate that uses `arbiter-operator` for the gRPC client. 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).
Communication between Dart and Rust uses typed **signals** defined in `operator/native/hub/src/signals/`. After modifying signal structs, regenerate Dart bindings:
```sh
cd operator && rinf gen
```
### Common Commands ### Common Commands
```sh ```sh
cd operator cd useragent
# Run the app (macOS or Windows) # Run the app
flutter run flutter run
# Regenerate Rust↔Dart signal bindings # Regenerate Rust↔Dart bindings after editing rust/src/api/
rinf gen mise run codegen # flutter_rust_bridge_codegen generate
# Analyze Dart code # Analyze Dart code (also run in CI)
flutter analyze flutter analyze
``` ```
The Rinf Rust entry point is `operator/native/hub/src/lib.rs`. It spawns actors defined in `operator/native/hub/src/actors/` which handle Dart↔server communication via signals. Note: `app/` contains only stale generated Flutter artifacts and is not the application source.

View File

@@ -1,31 +0,0 @@
Extension Discovery Cache
=========================
This folder is used by `package:extension_discovery` to cache lists of
packages that contains extensions for other packages.
DO NOT USE THIS FOLDER
----------------------
* Do not read (or rely) the contents of this folder.
* Do write to this folder.
If you're interested in the lists of extensions stored in this folder use the
API offered by package `extension_discovery` to get this information.
If this package doesn't work for your use-case, then don't try to read the
contents of this folder. It may change, and will not remain stable.
Use package `extension_discovery`
---------------------------------
If you want to access information from this folder.
Feel free to delete this folder
-------------------------------
Files in this folder act as a cache, and the cache is discarded if the files
are older than the modification time of `.dart_tool/package_config.json`.
Hence, it should never be necessary to clear this cache manually, if you find a
need to do please file a bug.

View File

@@ -1 +0,0 @@
{"version":2,"entries":[{"package":"app","rootUri":"../","packageUri":"lib/"}]}

View File

@@ -1,178 +0,0 @@
{
"configVersion": 2,
"packages": [
{
"name": "async",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/async-2.13.0",
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "boolean_selector",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/boolean_selector-2.1.2",
"packageUri": "lib/",
"languageVersion": "3.1"
},
{
"name": "characters",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/characters-1.4.0",
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "clock",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/clock-1.1.2",
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "collection",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/collection-1.19.1",
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "cupertino_icons",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/cupertino_icons-1.0.8",
"packageUri": "lib/",
"languageVersion": "3.1"
},
{
"name": "fake_async",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/fake_async-1.3.3",
"packageUri": "lib/",
"languageVersion": "3.3"
},
{
"name": "flutter",
"rootUri": "file:///Users/kaska/.local/share/mise/installs/flutter/3.38.9-stable/packages/flutter",
"packageUri": "lib/",
"languageVersion": "3.8"
},
{
"name": "flutter_lints",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/flutter_lints-6.0.0",
"packageUri": "lib/",
"languageVersion": "3.8"
},
{
"name": "flutter_test",
"rootUri": "file:///Users/kaska/.local/share/mise/installs/flutter/3.38.9-stable/packages/flutter_test",
"packageUri": "lib/",
"languageVersion": "3.8"
},
{
"name": "leak_tracker",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/leak_tracker-11.0.2",
"packageUri": "lib/",
"languageVersion": "3.2"
},
{
"name": "leak_tracker_flutter_testing",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/leak_tracker_flutter_testing-3.0.10",
"packageUri": "lib/",
"languageVersion": "3.2"
},
{
"name": "leak_tracker_testing",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/leak_tracker_testing-3.0.2",
"packageUri": "lib/",
"languageVersion": "3.2"
},
{
"name": "lints",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/lints-6.1.0",
"packageUri": "lib/",
"languageVersion": "3.8"
},
{
"name": "matcher",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/matcher-0.12.17",
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "material_color_utilities",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/material_color_utilities-0.11.1",
"packageUri": "lib/",
"languageVersion": "2.17"
},
{
"name": "meta",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/meta-1.17.0",
"packageUri": "lib/",
"languageVersion": "3.5"
},
{
"name": "path",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/path-1.9.1",
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "sky_engine",
"rootUri": "file:///Users/kaska/.local/share/mise/installs/flutter/3.38.9-stable/bin/cache/pkg/sky_engine",
"packageUri": "lib/",
"languageVersion": "3.8"
},
{
"name": "source_span",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/source_span-1.10.2",
"packageUri": "lib/",
"languageVersion": "3.1"
},
{
"name": "stack_trace",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/stack_trace-1.12.1",
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "stream_channel",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/stream_channel-2.1.4",
"packageUri": "lib/",
"languageVersion": "3.3"
},
{
"name": "string_scanner",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/string_scanner-1.4.1",
"packageUri": "lib/",
"languageVersion": "3.1"
},
{
"name": "term_glyph",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/term_glyph-1.2.2",
"packageUri": "lib/",
"languageVersion": "3.1"
},
{
"name": "test_api",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/test_api-0.7.7",
"packageUri": "lib/",
"languageVersion": "3.5"
},
{
"name": "vector_math",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/vector_math-2.2.0",
"packageUri": "lib/",
"languageVersion": "3.1"
},
{
"name": "vm_service",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/vm_service-15.0.2",
"packageUri": "lib/",
"languageVersion": "3.5"
},
{
"name": "app",
"rootUri": "../",
"packageUri": "lib/",
"languageVersion": "3.10"
}
],
"generator": "pub",
"generatorVersion": "3.10.8",
"flutterRoot": "file:///Users/kaska/.local/share/mise/installs/flutter/3.38.9-stable",
"flutterVersion": "3.38.9",
"pubCache": "file:///Users/kaska/.pub-cache"
}

View File

@@ -1,230 +0,0 @@
{
"roots": [
"app"
],
"packages": [
{
"name": "app",
"version": "1.0.0+1",
"dependencies": [
"cupertino_icons",
"flutter"
],
"devDependencies": [
"flutter_lints",
"flutter_test"
]
},
{
"name": "flutter_lints",
"version": "6.0.0",
"dependencies": [
"lints"
]
},
{
"name": "flutter_test",
"version": "0.0.0",
"dependencies": [
"clock",
"collection",
"fake_async",
"flutter",
"leak_tracker_flutter_testing",
"matcher",
"meta",
"path",
"stack_trace",
"stream_channel",
"test_api",
"vector_math"
]
},
{
"name": "cupertino_icons",
"version": "1.0.8",
"dependencies": []
},
{
"name": "flutter",
"version": "0.0.0",
"dependencies": [
"characters",
"collection",
"material_color_utilities",
"meta",
"sky_engine",
"vector_math"
]
},
{
"name": "lints",
"version": "6.1.0",
"dependencies": []
},
{
"name": "stream_channel",
"version": "2.1.4",
"dependencies": [
"async"
]
},
{
"name": "meta",
"version": "1.17.0",
"dependencies": []
},
{
"name": "collection",
"version": "1.19.1",
"dependencies": []
},
{
"name": "leak_tracker_flutter_testing",
"version": "3.0.10",
"dependencies": [
"flutter",
"leak_tracker",
"leak_tracker_testing",
"matcher",
"meta"
]
},
{
"name": "vector_math",
"version": "2.2.0",
"dependencies": []
},
{
"name": "stack_trace",
"version": "1.12.1",
"dependencies": [
"path"
]
},
{
"name": "clock",
"version": "1.1.2",
"dependencies": []
},
{
"name": "fake_async",
"version": "1.3.3",
"dependencies": [
"clock",
"collection"
]
},
{
"name": "path",
"version": "1.9.1",
"dependencies": []
},
{
"name": "matcher",
"version": "0.12.17",
"dependencies": [
"async",
"meta",
"stack_trace",
"term_glyph",
"test_api"
]
},
{
"name": "test_api",
"version": "0.7.7",
"dependencies": [
"async",
"boolean_selector",
"collection",
"meta",
"source_span",
"stack_trace",
"stream_channel",
"string_scanner",
"term_glyph"
]
},
{
"name": "sky_engine",
"version": "0.0.0",
"dependencies": []
},
{
"name": "material_color_utilities",
"version": "0.11.1",
"dependencies": [
"collection"
]
},
{
"name": "characters",
"version": "1.4.0",
"dependencies": []
},
{
"name": "async",
"version": "2.13.0",
"dependencies": [
"collection",
"meta"
]
},
{
"name": "leak_tracker_testing",
"version": "3.0.2",
"dependencies": [
"leak_tracker",
"matcher",
"meta"
]
},
{
"name": "leak_tracker",
"version": "11.0.2",
"dependencies": [
"clock",
"collection",
"meta",
"path",
"vm_service"
]
},
{
"name": "term_glyph",
"version": "1.2.2",
"dependencies": []
},
{
"name": "string_scanner",
"version": "1.4.1",
"dependencies": [
"source_span"
]
},
{
"name": "source_span",
"version": "1.10.2",
"dependencies": [
"collection",
"path",
"term_glyph"
]
},
{
"name": "boolean_selector",
"version": "2.1.2",
"dependencies": [
"source_span",
"string_scanner"
]
},
{
"name": "vm_service",
"version": "15.0.2",
"dependencies": []
}
],
"configVersion": 1
}

View File

@@ -1 +0,0 @@
3.38.9

File diff suppressed because it is too large Load Diff

View File

@@ -1,821 +0,0 @@
# Grant Grid View Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Add an "EVM Grants" dashboard tab that displays all grants as enriched cards (type, chain, wallet address, client name) with per-card revoke support.
**Architecture:** A new `walletAccessListProvider` fetches wallet accesses with their DB row IDs. The screen (`grants.dart`) watches only `evmGrantsProvider` for top-level state. Each `GrantCard` widget (its own file) watches enrichment providers (`walletAccessListProvider`, `evmProvider`, `sdkClientsProvider`) and the revoke mutation directly — keeping rebuilds scoped to the card. The screen is registered as a dashboard tab in `AdaptiveScaffold`.
**Tech Stack:** Flutter, Riverpod (`riverpod_annotation` + `build_runner` codegen), `sizer` (adaptive sizing), `auto_route`, Protocol Buffers (Dart), `Palette` design tokens.
---
## File Map
| File | Action | Responsibility |
|---|---|---|
| `operator/lib/theme/palette.dart` | Modify | Add `Palette.token` (indigo accent for token-transfer cards) |
| `operator/lib/features/connection/evm/wallet_access.dart` | Modify | Add `listAllWalletAccesses()` function |
| `operator/lib/providers/sdk_clients/wallet_access_list.dart` | Create | `WalletAccessListProvider` — fetches full wallet access list with IDs |
| `operator/lib/screens/dashboard/evm/grants/widgets/grant_card.dart` | Create | `GrantCard` widget — watches enrichment providers + revoke mutation; one card per grant |
| `operator/lib/screens/dashboard/evm/grants/grants.dart` | Create | `EvmGrantsScreen` — watches `evmGrantsProvider`; handles loading/error/empty/data states; renders `GrantCard` list |
| `operator/lib/router.dart` | Modify | Register `EvmGrantsRoute` in dashboard children |
| `operator/lib/screens/dashboard.dart` | Modify | Add Grants entry to `routes` list and `NavigationDestination` list |
---
## Task 1: Add `Palette.token`
**Files:**
- Modify: `operator/lib/theme/palette.dart`
- [ ] **Step 1: Add the color**
Replace the contents of `operator/lib/theme/palette.dart` with:
```dart
import 'package:flutter/material.dart';
class Palette {
static const ink = Color(0xFF15263C);
static const coral = Color(0xFFE26254);
static const cream = Color(0xFFFFFAF4);
static const line = Color(0x1A15263C);
static const token = Color(0xFF5C6BC0);
}
```
- [ ] **Step 2: Verify**
```sh
cd operator && flutter analyze lib/theme/palette.dart
```
Expected: no issues.
- [ ] **Step 3: Commit**
```sh
jj describe -m "feat(theme): add Palette.token for token-transfer grant cards"
jj new
```
---
## Task 2: Add `listAllWalletAccesses` feature function
**Files:**
- Modify: `operator/lib/features/connection/evm/wallet_access.dart`
`readClientWalletAccess` (existing) filters the list to one client's wallet IDs and returns `Set<int>`. This new function returns the complete unfiltered list with row IDs so the grant cards can resolve wallet_access_id → wallet + client.
- [ ] **Step 1: Append function**
Add at the bottom of `operator/lib/features/connection/evm/wallet_access.dart`:
```dart
Future<List<SdkClientWalletAccess>> listAllWalletAccesses(
Connection connection,
) async {
final response = await connection.ask(
OperatorRequest(listWalletAccess: Empty()),
);
if (!response.hasListWalletAccessResponse()) {
throw Exception(
'Expected list wallet access response, got ${response.whichPayload()}',
);
}
return response.listWalletAccessResponse.accesses.toList(growable: false);
}
```
Each returned `SdkClientWalletAccess` has:
- `.id` — the `evm_wallet_access` row ID (same value as `wallet_access_id` in a `GrantEntry`)
- `.access.walletId` — the EVM wallet DB ID
- `.access.sdkClientId` — the SDK client DB ID
- [ ] **Step 2: Verify**
```sh
cd operator && flutter analyze lib/features/connection/evm/wallet_access.dart
```
Expected: no issues.
- [ ] **Step 3: Commit**
```sh
jj describe -m "feat(evm): add listAllWalletAccesses feature function"
jj new
```
---
## Task 3: Create `WalletAccessListProvider`
**Files:**
- Create: `operator/lib/providers/sdk_clients/wallet_access_list.dart`
- Generated: `operator/lib/providers/sdk_clients/wallet_access_list.g.dart`
Mirrors the structure of `EvmGrants` in `providers/evm/evm_grants.dart` — class-based `@riverpod` with a `refresh()` method.
- [ ] **Step 1: Write the provider**
Create `operator/lib/providers/sdk_clients/wallet_access_list.dart`:
```dart
import 'package:arbiter/features/connection/evm/wallet_access.dart';
import 'package:arbiter/proto/operator.pb.dart';
import 'package:arbiter/providers/connection/connection_manager.dart';
import 'package:mtcore/markettakers.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'wallet_access_list.g.dart';
@riverpod
class WalletAccessList extends _$WalletAccessList {
@override
Future<List<SdkClientWalletAccess>?> build() async {
final connection = await ref.watch(connectionManagerProvider.future);
if (connection == null) {
return null;
}
try {
return await listAllWalletAccesses(connection);
} catch (e, st) {
talker.handle(e, st);
rethrow;
}
}
Future<void> refresh() async {
final connection = await ref.read(connectionManagerProvider.future);
if (connection == null) {
state = const AsyncData(null);
return;
}
state = const AsyncLoading();
state = await AsyncValue.guard(() => listAllWalletAccesses(connection));
}
}
```
- [ ] **Step 2: Run code generation**
```sh
cd operator && dart run build_runner build --delete-conflicting-outputs
```
Expected: `operator/lib/providers/sdk_clients/wallet_access_list.g.dart` created. No errors.
- [ ] **Step 3: Verify**
```sh
cd operator && flutter analyze lib/providers/sdk_clients/
```
Expected: no issues.
- [ ] **Step 4: Commit**
```sh
jj describe -m "feat(providers): add WalletAccessListProvider"
jj new
```
---
## Task 4: Create `GrantCard` widget
**Files:**
- Create: `operator/lib/screens/dashboard/evm/grants/widgets/grant_card.dart`
This widget owns all per-card logic: enrichment lookups, revoke action, and rebuild scope. The screen only passes it a `GrantEntry` — the card fetches everything else itself.
**Key types:**
- `GrantEntry` (from `proto/evm.pb.dart`): `.id`, `.shared.walletAccessId`, `.shared.chainId`, `.specific.whichGrant()`
- `SpecificGrant_Grant.etherTransfer` / `.tokenTransfer` — enum values for the oneof
- `SdkClientWalletAccess` (from `proto/operator.pb.dart`): `.id`, `.access.walletId`, `.access.sdkClientId`
- `WalletEntry` (from `proto/evm.pb.dart`): `.id`, `.address` (List<int>)
- `SdkClientEntry` (from `proto/operator.pb.dart`): `.id`, `.info.name`
- `revokeEvmGrantMutation``Mutation<void>` (global; all revoke buttons disable together while any revoke is in flight)
- `executeRevokeEvmGrant(ref, grantId: int)``Future<void>`
- [ ] **Step 1: Write the widget**
Create `operator/lib/screens/dashboard/evm/grants/widgets/grant_card.dart`:
```dart
import 'package:arbiter/proto/evm.pb.dart';
import 'package:arbiter/proto/operator.pb.dart';
import 'package:arbiter/providers/evm/evm.dart';
import 'package:arbiter/providers/evm/evm_grants.dart';
import 'package:arbiter/providers/sdk_clients/list.dart';
import 'package:arbiter/providers/sdk_clients/wallet_access_list.dart';
import 'package:arbiter/theme/palette.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/experimental/mutation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:sizer/sizer.dart';
String _shortAddress(List<int> bytes) {
final hex = bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
return '0x${hex.substring(0, 6)}...${hex.substring(hex.length - 4)}';
}
String _formatError(Object error) {
final message = error.toString();
if (message.startsWith('Exception: ')) {
return message.substring('Exception: '.length);
}
return message;
}
class GrantCard extends ConsumerWidget {
const GrantCard({super.key, required this.grant});
final GrantEntry grant;
@override
Widget build(BuildContext context, WidgetRef ref) {
// Enrichment lookups — each watch scopes rebuilds to this card only
final walletAccesses =
ref.watch(walletAccessListProvider).asData?.value ?? const [];
final wallets = ref.watch(evmProvider).asData?.value ?? const [];
final clients = ref.watch(sdkClientsProvider).asData?.value ?? const [];
final revoking = ref.watch(revokeEvmGrantMutation) is MutationPending;
final isEther =
grant.specific.whichGrant() == SpecificGrant_Grant.etherTransfer;
final accent = isEther ? Palette.coral : Palette.token;
final typeLabel = isEther ? 'Ether' : 'Token';
final theme = Theme.of(context);
final muted = Palette.ink.withValues(alpha: 0.62);
// Resolve wallet_access_id → wallet address + client name
final accessById = <int, SdkClientWalletAccess>{
for (final a in walletAccesses) a.id: a,
};
final walletById = <int, WalletEntry>{
for (final w in wallets) w.id: w,
};
final clientNameById = <int, String>{
for (final c in clients) c.id: c.info.name,
};
final accessId = grant.shared.walletAccessId;
final access = accessById[accessId];
final wallet = access != null ? walletById[access.access.walletId] : null;
final walletLabel = wallet != null
? _shortAddress(wallet.address)
: 'Access #$accessId';
final clientLabel = () {
if (access == null) return '';
final name = clientNameById[access.access.sdkClientId] ?? '';
return name.isEmpty ? 'Client #${access.access.sdkClientId}' : name;
}();
void showError(String message) {
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message), behavior: SnackBarBehavior.floating),
);
}
Future<void> revoke() async {
try {
await executeRevokeEvmGrant(ref, grantId: grant.id);
} catch (e) {
showError(_formatError(e));
}
}
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(24),
color: Palette.cream.withValues(alpha: 0.92),
border: Border.all(color: Palette.line),
),
child: IntrinsicHeight(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Accent strip
Container(
width: 0.8.w,
decoration: BoxDecoration(
color: accent,
borderRadius: const BorderRadius.horizontal(
left: Radius.circular(24),
),
),
),
// Card body
Expanded(
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: 1.6.w,
vertical: 1.4.h,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Row 1: type badge · chain · spacer · revoke button
Row(
children: [
Container(
padding: EdgeInsets.symmetric(
horizontal: 1.w,
vertical: 0.4.h,
),
decoration: BoxDecoration(
color: accent.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(8),
),
child: Text(
typeLabel,
style: theme.textTheme.labelSmall?.copyWith(
color: accent,
fontWeight: FontWeight.w800,
),
),
),
SizedBox(width: 1.w),
Container(
padding: EdgeInsets.symmetric(
horizontal: 1.w,
vertical: 0.4.h,
),
decoration: BoxDecoration(
color: Palette.ink.withValues(alpha: 0.06),
borderRadius: BorderRadius.circular(8),
),
child: Text(
'Chain ${grant.shared.chainId}',
style: theme.textTheme.labelSmall?.copyWith(
color: muted,
fontWeight: FontWeight.w700,
),
),
),
const Spacer(),
if (revoking)
SizedBox(
width: 1.8.h,
height: 1.8.h,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Palette.coral,
),
)
else
OutlinedButton.icon(
onPressed: revoke,
style: OutlinedButton.styleFrom(
foregroundColor: Palette.coral,
side: BorderSide(
color: Palette.coral.withValues(alpha: 0.4),
),
padding: EdgeInsets.symmetric(
horizontal: 1.w,
vertical: 0.6.h,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
icon: const Icon(Icons.block_rounded, size: 16),
label: const Text('Revoke'),
),
],
),
SizedBox(height: 0.8.h),
// Row 2: wallet address · client name
Row(
children: [
Text(
walletLabel,
style: theme.textTheme.bodySmall?.copyWith(
color: Palette.ink,
fontFamily: 'monospace',
),
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 0.8.w),
child: Text(
'·',
style: theme.textTheme.bodySmall
?.copyWith(color: muted),
),
),
Expanded(
child: Text(
clientLabel,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.bodySmall
?.copyWith(color: muted),
),
),
],
),
],
),
),
),
],
),
),
);
}
}
```
- [ ] **Step 2: Verify**
```sh
cd operator && flutter analyze lib/screens/dashboard/evm/grants/widgets/grant_card.dart
```
Expected: no issues.
- [ ] **Step 3: Commit**
```sh
jj describe -m "feat(grants): add GrantCard widget with self-contained enrichment"
jj new
```
---
## Task 5: Create `EvmGrantsScreen`
**Files:**
- Create: `operator/lib/screens/dashboard/evm/grants/grants.dart`
The screen watches only `evmGrantsProvider` for top-level state (loading / error / no connection / empty / data). When there is data it renders a list of `GrantCard` widgets — each card manages its own enrichment subscriptions.
- [ ] **Step 1: Write the screen**
Create `operator/lib/screens/dashboard/evm/grants/grants.dart`:
```dart
import 'package:arbiter/proto/evm.pb.dart';
import 'package:arbiter/providers/evm/evm_grants.dart';
import 'package:arbiter/providers/sdk_clients/wallet_access_list.dart';
import 'package:arbiter/router.gr.dart';
import 'package:arbiter/screens/dashboard/evm/grants/widgets/grant_card.dart';
import 'package:arbiter/theme/palette.dart';
import 'package:arbiter/widgets/page_header.dart';
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:sizer/sizer.dart';
String _formatError(Object error) {
final message = error.toString();
if (message.startsWith('Exception: ')) {
return message.substring('Exception: '.length);
}
return message;
}
// ─── State panel ──────────────────────────────────────────────────────────────
class _StatePanel extends StatelessWidget {
const _StatePanel({
required this.icon,
required this.title,
required this.body,
this.actionLabel,
this.onAction,
this.busy = false,
});
final IconData icon;
final String title;
final String body;
final String? actionLabel;
final Future<void> Function()? onAction;
final bool busy;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(24),
color: Palette.cream.withValues(alpha: 0.92),
border: Border.all(color: Palette.line),
),
child: Padding(
padding: EdgeInsets.all(2.8.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (busy)
SizedBox(
width: 2.8.h,
height: 2.8.h,
child: const CircularProgressIndicator(strokeWidth: 2.5),
)
else
Icon(icon, size: 34, color: Palette.coral),
SizedBox(height: 1.8.h),
Text(
title,
style: theme.textTheme.headlineSmall?.copyWith(
color: Palette.ink,
fontWeight: FontWeight.w800,
),
),
SizedBox(height: 1.h),
Text(
body,
style: theme.textTheme.bodyLarge?.copyWith(
color: Palette.ink.withValues(alpha: 0.72),
height: 1.5,
),
),
if (actionLabel != null && onAction != null) ...[
SizedBox(height: 2.h),
OutlinedButton.icon(
onPressed: () => onAction!(),
icon: const Icon(Icons.refresh),
label: Text(actionLabel!),
),
],
],
),
),
);
}
}
// ─── Grant list ───────────────────────────────────────────────────────────────
class _GrantList extends StatelessWidget {
const _GrantList({required this.grants});
final List<GrantEntry> grants;
@override
Widget build(BuildContext context) {
return Column(
children: [
for (var i = 0; i < grants.length; i++)
Padding(
padding: EdgeInsets.only(
bottom: i == grants.length - 1 ? 0 : 1.8.h,
),
child: GrantCard(grant: grants[i]),
),
],
);
}
}
// ─── Screen ───────────────────────────────────────────────────────────────────
@RoutePage()
class EvmGrantsScreen extends ConsumerWidget {
const EvmGrantsScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// Screen watches only the grant list for top-level state decisions
final grantsAsync = ref.watch(evmGrantsProvider);
Future<void> refresh() async {
await Future.wait([
ref.read(evmGrantsProvider.notifier).refresh(),
ref.read(walletAccessListProvider.notifier).refresh(),
]);
}
void showMessage(String message) {
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message), behavior: SnackBarBehavior.floating),
);
}
Future<void> safeRefresh() async {
try {
await refresh();
} catch (e) {
showMessage(_formatError(e));
}
}
final grantsState = grantsAsync.asData?.value;
final grants = grantsState?.grants;
final content = switch (grantsAsync) {
AsyncLoading() when grantsState == null => const _StatePanel(
icon: Icons.hourglass_top,
title: 'Loading grants',
body: 'Pulling grant registry from Arbiter.',
busy: true,
),
AsyncError(:final error) => _StatePanel(
icon: Icons.sync_problem,
title: 'Grant registry unavailable',
body: _formatError(error),
actionLabel: 'Retry',
onAction: safeRefresh,
),
AsyncData(:final value) when value == null => _StatePanel(
icon: Icons.portable_wifi_off,
title: 'No active server connection',
body: 'Reconnect to Arbiter to list EVM grants.',
actionLabel: 'Refresh',
onAction: safeRefresh,
),
_ when grants != null && grants.isEmpty => _StatePanel(
icon: Icons.policy_outlined,
title: 'No grants yet',
body: 'Create a grant to allow SDK clients to sign transactions.',
actionLabel: 'Create grant',
onAction: () => context.router.push(const CreateEvmGrantRoute()),
),
_ => _GrantList(grants: grants ?? const []),
};
return Scaffold(
body: SafeArea(
child: RefreshIndicator.adaptive(
color: Palette.ink,
backgroundColor: Colors.white,
onRefresh: safeRefresh,
child: ListView(
physics: const BouncingScrollPhysics(
parent: AlwaysScrollableScrollPhysics(),
),
padding: EdgeInsets.fromLTRB(2.4.w, 2.4.h, 2.4.w, 3.2.h),
children: [
PageHeader(
title: 'EVM Grants',
isBusy: grantsAsync.isLoading,
actions: [
FilledButton.icon(
onPressed: () =>
context.router.push(const CreateEvmGrantRoute()),
icon: const Icon(Icons.add_rounded),
label: const Text('Create grant'),
),
SizedBox(width: 1.w),
OutlinedButton.icon(
onPressed: safeRefresh,
style: OutlinedButton.styleFrom(
foregroundColor: Palette.ink,
side: BorderSide(color: Palette.line),
padding: EdgeInsets.symmetric(
horizontal: 1.4.w,
vertical: 1.2.h,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
),
icon: const Icon(Icons.refresh, size: 18),
label: const Text('Refresh'),
),
],
),
SizedBox(height: 1.8.h),
content,
],
),
),
),
);
}
}
```
- [ ] **Step 2: Verify**
```sh
cd operator && flutter analyze lib/screens/dashboard/evm/grants/
```
Expected: no issues.
- [ ] **Step 3: Commit**
```sh
jj describe -m "feat(grants): add EvmGrantsScreen"
jj new
```
---
## Task 6: Wire router and dashboard tab
**Files:**
- Modify: `operator/lib/router.dart`
- Modify: `operator/lib/screens/dashboard.dart`
- Regenerated: `operator/lib/router.gr.dart`
- [ ] **Step 1: Add route to `router.dart`**
Replace the contents of `operator/lib/router.dart` with:
```dart
import 'package:auto_route/auto_route.dart';
import 'router.gr.dart';
@AutoRouterConfig(generateForDir: ['lib/screens'])
class Router extends RootStackRouter {
@override
List<AutoRoute> get routes => [
AutoRoute(page: Bootstrap.page, path: '/bootstrap', initial: true),
AutoRoute(page: ServerInfoSetupRoute.page, path: '/server-info'),
AutoRoute(page: ServerConnectionRoute.page, path: '/server-connection'),
AutoRoute(page: VaultSetupRoute.page, path: '/vault'),
AutoRoute(page: ClientDetailsRoute.page, path: '/clients/:clientId'),
AutoRoute(page: CreateEvmGrantRoute.page, path: '/evm-grants/create'),
AutoRoute(
page: DashboardRouter.page,
path: '/dashboard',
children: [
AutoRoute(page: EvmRoute.page, path: 'evm'),
AutoRoute(page: ClientsRoute.page, path: 'clients'),
AutoRoute(page: EvmGrantsRoute.page, path: 'grants'),
AutoRoute(page: AboutRoute.page, path: 'about'),
],
),
];
}
```
- [ ] **Step 2: Update `dashboard.dart`**
In `operator/lib/screens/dashboard.dart`, replace the `routes` constant:
```dart
final routes = [
const EvmRoute(),
const ClientsRoute(),
const EvmGrantsRoute(),
const AboutRoute(),
];
```
And replace the `destinations` list inside `AdaptiveScaffold`:
```dart
destinations: const [
NavigationDestination(
icon: Icon(Icons.account_balance_wallet_outlined),
selectedIcon: Icon(Icons.account_balance_wallet),
label: 'Wallets',
),
NavigationDestination(
icon: Icon(Icons.devices_other_outlined),
selectedIcon: Icon(Icons.devices_other),
label: 'Clients',
),
NavigationDestination(
icon: Icon(Icons.policy_outlined),
selectedIcon: Icon(Icons.policy),
label: 'Grants',
),
NavigationDestination(
icon: Icon(Icons.info_outline),
selectedIcon: Icon(Icons.info),
label: 'About',
),
],
```
- [ ] **Step 3: Regenerate router**
```sh
cd operator && dart run build_runner build --delete-conflicting-outputs
```
Expected: `lib/router.gr.dart` updated, `EvmGrantsRoute` now available, no errors.
- [ ] **Step 4: Full project verify**
```sh
cd operator && flutter analyze
```
Expected: no issues.
- [ ] **Step 5: Commit**
```sh
jj describe -m "feat(nav): add Grants dashboard tab"
jj new
```

View File

@@ -1,170 +0,0 @@
# Grant Grid View — Design Spec
**Date:** 2026-03-28
## Overview
Add a "Grants" dashboard tab to the Flutter operator app that displays all EVM grants as a card-based grid. Each card shows a compact summary (type, chain, wallet address, client name) with a revoke action. The tab integrates into the existing `AdaptiveScaffold` navigation alongside Wallets, Clients, and About.
## Scope
- New `walletAccessListProvider` for fetching wallet access entries with their DB row IDs
- New `EvmGrantsScreen` as a dashboard tab
- Grant card widget with enriched display (type, chain, wallet, client)
- Revoke action wired to existing `executeRevokeEvmGrant` mutation
- Dashboard tab bar and router updated
- New token-transfer accent color added to `Palette`
**Out of scope:** Fixing grant creation (separate task).
---
## Data Layer
### `walletAccessListProvider`
**File:** `operator/lib/providers/sdk_clients/wallet_access_list.dart`
- `@riverpod` class, watches `connectionManagerProvider.future`
- Returns `List<SdkClientWalletAccess>?` (null when not connected)
- Each entry: `.id` (wallet_access_id), `.access.walletId`, `.access.sdkClientId`
- Exposes a `refresh()` method following the same pattern as `EvmGrants.refresh()`
### Enrichment at render time (Approach A)
The `EvmGrantsScreen` watches four providers:
1. `evmGrantsProvider` — the grant list
2. `walletAccessListProvider` — to resolve wallet_access_id → (wallet_id, sdk_client_id)
3. `evmProvider` — to resolve wallet_id → wallet address
4. `sdkClientsProvider` — to resolve sdk_client_id → client name
All lookups are in-memory Maps built inside the build method; no extra model class needed.
Fallbacks:
- Wallet address not found → `"Access #N"` where N is the wallet_access_id
- Client name not found → `"Client #N"` where N is the sdk_client_id
---
## Route Structure
```
/dashboard
/evm ← existing (Wallets tab)
/clients ← existing (Clients tab)
/grants ← NEW (Grants tab)
/about ← existing
/evm-grants/create ← existing push route (unchanged)
```
### Changes to `router.dart`
Add inside dashboard children:
```dart
AutoRoute(page: EvmGrantsRoute.page, path: 'grants'),
```
### Changes to `dashboard.dart`
Add to `routes` list:
```dart
const EvmGrantsRoute()
```
Add `NavigationDestination`:
```dart
NavigationDestination(
icon: Icon(Icons.policy_outlined),
selectedIcon: Icon(Icons.policy),
label: 'Grants',
),
```
---
## Screen: `EvmGrantsScreen`
**File:** `operator/lib/screens/dashboard/evm/grants/grants.dart`
```
Scaffold
└─ SafeArea
└─ RefreshIndicator.adaptive (refreshes evmGrantsProvider + walletAccessListProvider)
└─ ListView (BouncingScrollPhysics + AlwaysScrollableScrollPhysics)
├─ PageHeader
│ title: 'EVM Grants'
│ isBusy: evmGrantsProvider.isLoading
│ actions: [CreateGrantButton, RefreshButton]
├─ SizedBox(height: 1.8.h)
└─ <content>
```
### State handling
Matches the pattern from `EvmScreen` and `ClientsScreen`:
| State | Display |
|---|---|
| Loading (no data yet) | `_StatePanel` with spinner, "Loading grants" |
| Error | `_StatePanel` with coral icon, error message, Retry button |
| No connection | `_StatePanel`, "No active server connection" |
| Empty list | `_StatePanel`, "No grants yet", with Create Grant shortcut |
| Data | Column of `_GrantCard` widgets |
### Header actions
**CreateGrantButton:** `FilledButton.icon` with `Icons.add_rounded`, pushes `CreateEvmGrantRoute()` via `context.router.push(...)`.
**RefreshButton:** `OutlinedButton.icon` with `Icons.refresh`, calls `ref.read(evmGrantsProvider.notifier).refresh()`.
---
## Grant Card: `_GrantCard`
**Layout:**
```
Container (rounded 24, Palette.cream bg, Palette.line border)
└─ IntrinsicHeight > Row
├─ Accent strip (0.8.w wide, full height, rounded left)
└─ Padding > Column
├─ Row 1: TypeBadge + ChainChip + Spacer + RevokeButton
└─ Row 2: WalletText + "·" + ClientText
```
**Accent color by grant type:**
- Ether transfer → `Palette.coral`
- Token transfer → `Palette.token` (new entry in `Palette` — indigo, e.g. `Color(0xFF5C6BC0)`)
**TypeBadge:** Small pill container with accent color background at 15% opacity, accent-colored text. Label: `'Ether'` or `'Token'`.
**ChainChip:** Small container: `'Chain ${grant.shared.chainId}'`, muted ink color.
**WalletText:** Short hex address (`0xabc...def`) from wallet lookup, `bodySmall`, monospace font family.
**ClientText:** Client name from `sdkClientsProvider` lookup, or fallback string. `bodySmall`, muted ink.
**RevokeButton:**
- `OutlinedButton` with `Icons.block_rounded` icon, label `'Revoke'`
- `foregroundColor: Palette.coral`, `side: BorderSide(color: Palette.coral.withValues(alpha: 0.4))`
- Disabled (replaced with `CircularProgressIndicator`) while `revokeEvmGrantMutation` is pending — note: this is a single global mutation, so all revoke buttons disable while any revoke is in flight
- On press: calls `executeRevokeEvmGrant(ref, grantId: grant.id)`; shows `SnackBar` on error
---
## Adaptive Sizing
All sizing uses `sizer` units (`1.h`, `1.w`, etc.). No hardcoded pixel values.
---
## Files to Create / Modify
| File | Action |
|---|---|
| `lib/theme/palette.dart` | Modify — add `Palette.token` color |
| `lib/providers/sdk_clients/wallet_access_list.dart` | Create |
| `lib/screens/dashboard/evm/grants/grants.dart` | Create |
| `lib/router.dart` | Modify — add grants route to dashboard children |
| `lib/screens/dashboard.dart` | Modify — add tab to routes list and NavigationDestinations |

View File

@@ -4,25 +4,28 @@ package arbiter.operator;
import "operator/auth.proto"; import "operator/auth.proto";
import "operator/evm.proto"; import "operator/evm.proto";
import "operator/governance.proto";
import "operator/sdk_client.proto"; import "operator/sdk_client.proto";
import "operator/vault/vault.proto"; import "operator/vault/vault.proto";
message OperatorRequest { message OperatorRequest {
int32 id = 16; int32 id = 16;
oneof payload { oneof payload {
auth.Request auth = 1; auth.Request auth = 1;
vault.Request vault = 2; vault.Request vault = 2;
evm.Request evm = 3; evm.Request evm = 3;
sdk_client.Request sdk_client = 4; sdk_client.Request sdk_client = 4;
governance.Request governance = 5;
} }
} }
message OperatorResponse { message OperatorResponse {
optional int32 id = 16; optional int32 id = 16;
oneof payload { oneof payload {
auth.Response auth = 1; auth.Response auth = 1;
vault.Response vault = 2; vault.Response vault = 2;
evm.Response evm = 3; evm.Response evm = 3;
sdk_client.Response sdk_client = 4; sdk_client.Response sdk_client = 4;
governance.Response governance = 5;
} }
} }

View File

@@ -0,0 +1,136 @@
syntax = "proto3";
package arbiter.operator.governance;
message Request {
oneof payload {
CreateProposalRequest create = 1;
CastVoteRequest vote = 2;
QueryPendingRequest query = 3;
}
}
message CreateProposalRequest {
oneof kind {
ApproveSdkClientPayload approve_sdk_client = 1;
GrantWalletAccessPayload grant_wallet_access = 3;
ApproveServerUpdatePayload approve_server_update = 4;
ReplaceOperatorPayload replace_operator = 5;
UpdateShamirParametersPayload update_shamir_parameters = 6;
ApprovePersistentGrantPayload approve_persistent_grant = 7;
ApproveOneOffTransactionPayload approve_one_off_transaction = 8;
}
optional uint32 ttl_secs = 2;
}
message ReplaceOperatorPayload {
int32 old_operator_id = 1;
bytes new_pubkey = 2;
}
message UpdateShamirParametersPayload {
uint32 new_n = 1;
}
message ApproveServerUpdatePayload {}
message ApproveSdkClientPayload {
int32 client_id = 1;
}
message GrantWalletAccessPayload {
int32 wallet_id = 1;
int32 client_id = 2;
}
message CastVoteRequest {
int32 proposal_id = 1;
bool approve = 2;
bytes signature = 3;
}
message QueryPendingRequest {}
message Response {
oneof payload {
CreateProposalResponse created = 1;
VoteResponse voted = 2;
QueryPendingResponse pending = 3;
}
}
message CreateProposalResponse {
int32 proposal_id = 1;
}
message VoteResponse {
VoteOutcome outcome = 1;
}
enum VoteOutcome {
VOTE_OUTCOME_UNSPECIFIED = 0;
VOTE_OUTCOME_PENDING = 1;
VOTE_OUTCOME_APPROVED = 2;
VOTE_OUTCOME_REJECTED = 3;
}
message ProposalSummary {
int32 id = 1;
string kind = 2;
int32 initiator_id = 3;
int64 expires_at = 4;
int64 approve_count = 5;
int64 reject_count = 6;
}
message QueryPendingResponse {
repeated ProposalSummary proposals = 1;
}
message TransactionRateLimitProto {
uint32 count = 1;
int64 window_secs = 2;
}
message VolumeLimitProto {
bytes max_volume = 1;
int64 window_secs = 2;
}
message EtherTransferSpecProto {
repeated bytes targets = 1;
VolumeLimitProto limit = 2;
}
message TokenTransferSpecProto {
bytes token_contract = 1;
optional bytes target = 2;
repeated VolumeLimitProto volume_limits = 3;
}
message ApproveOneOffTransactionPayload {
int32 client_id = 1;
bytes wallet_address = 2;
uint64 chain_id = 3;
uint64 nonce = 4;
uint64 gas_limit = 5;
bytes max_fee_per_gas = 6;
bytes max_priority_fee_per_gas = 7;
bytes to = 8;
bytes value = 9;
bytes input = 10;
}
message ApprovePersistentGrantPayload {
int32 wallet_access_id = 1;
uint64 chain_id = 2;
optional int64 valid_from_secs = 3;
optional int64 valid_until_secs = 4;
optional bytes max_gas_fee_per_gas = 5;
optional bytes max_priority_fee_per_gas = 6;
optional TransactionRateLimitProto rate_limit = 7;
oneof specific {
EtherTransferSpecProto ether_transfer = 8;
TokenTransferSpecProto token_transfer = 9;
}
}

View File

@@ -8,15 +8,35 @@ message BootstrapEncryptedKey {
bytes associated_data = 3; bytes associated_data = 3;
} }
message DeclareCommittee {
uint32 count = 1;
uint32 recovery_count = 2;
}
message ContributePassphrase {
bytes passphrase = 1;
}
message ContributeRecoveryPassphrase {
int32 recovery_operator_id = 1;
bytes passphrase = 2;
}
enum BootstrapResult { enum BootstrapResult {
BOOTSTRAP_RESULT_UNSPECIFIED = 0; BOOTSTRAP_RESULT_UNSPECIFIED = 0;
BOOTSTRAP_RESULT_SUCCESS = 1; BOOTSTRAP_RESULT_SUCCESS = 1;
BOOTSTRAP_RESULT_ALREADY_BOOTSTRAPPED = 2; BOOTSTRAP_RESULT_ALREADY_BOOTSTRAPPED = 2;
BOOTSTRAP_RESULT_INVALID_KEY = 3; BOOTSTRAP_RESULT_INVALID_KEY = 3;
BOOTSTRAP_RESULT_AWAITING_CONTRIBUTIONS = 4;
} }
message Request { message Request {
BootstrapEncryptedKey encrypted_key = 2; oneof payload {
BootstrapEncryptedKey encrypted_key = 2;
DeclareCommittee declare_committee = 3;
ContributePassphrase contribute_passphrase = 4;
ContributeRecoveryPassphrase contribute_recovery_passphrase = 5;
}
} }
message Response { message Response {

View File

@@ -0,0 +1,30 @@
syntax = "proto3";
package arbiter.operator.vault.rekey;
message ContributePassphrase {
bytes passphrase = 1;
}
message ContributeRecoveryPassphrase {
int32 recovery_operator_id = 1;
bytes passphrase = 2;
}
enum RekeyResult {
REKEY_RESULT_UNSPECIFIED = 0;
REKEY_RESULT_SUCCESS = 1;
REKEY_RESULT_AWAITING_CONTRIBUTIONS = 2;
REKEY_RESULT_NOT_IN_PROGRESS = 3;
}
message Request {
oneof payload {
ContributePassphrase contribute_passphrase = 1;
ContributeRecoveryPassphrase contribute_recovery_passphrase = 2;
}
}
message Response {
RekeyResult result = 1;
}

View File

@@ -15,18 +15,29 @@ message UnsealEncryptedKey {
bytes associated_data = 3; bytes associated_data = 3;
} }
message ContributePassphrase {
bytes passphrase = 1;
}
message ContributeRecoveryPassphrase {
int32 recovery_operator_id = 1;
bytes passphrase = 2;
}
enum UnsealResult { enum UnsealResult {
UNSEAL_RESULT_UNSPECIFIED = 0; UNSEAL_RESULT_UNSPECIFIED = 0;
UNSEAL_RESULT_SUCCESS = 1; UNSEAL_RESULT_SUCCESS = 1;
UNSEAL_RESULT_INVALID_KEY = 2; UNSEAL_RESULT_INVALID_KEY = 2;
UNSEAL_RESULT_UNBOOTSTRAPPED = 3; UNSEAL_RESULT_UNBOOTSTRAPPED = 3;
UNSEAL_RESULT_LOCKED_OUT = 4; UNSEAL_RESULT_AWAITING_CONTRIBUTIONS = 4;
} }
message Request { message Request {
oneof payload { oneof payload {
UnsealStart start = 1; UnsealStart start = 1;
UnsealEncryptedKey encrypted_key = 2; UnsealEncryptedKey encrypted_key = 2;
ContributePassphrase contribute_passphrase = 3;
ContributeRecoveryPassphrase contribute_recovery_passphrase = 4;
} }
} }

View File

@@ -5,20 +5,23 @@ package arbiter.operator.vault;
import "google/protobuf/empty.proto"; import "google/protobuf/empty.proto";
import "shared/vault.proto"; import "shared/vault.proto";
import "operator/vault/bootstrap.proto"; import "operator/vault/bootstrap.proto";
import "operator/vault/rekey.proto";
import "operator/vault/unseal.proto"; import "operator/vault/unseal.proto";
message Request { message Request {
oneof payload { oneof payload {
google.protobuf.Empty query_state = 1; google.protobuf.Empty query_state = 1;
unseal.Request unseal = 2; unseal.Request unseal = 2;
bootstrap.Request bootstrap = 3; bootstrap.Request bootstrap = 3;
rekey.Request rekey = 4;
} }
} }
message Response { message Response {
oneof payload { oneof payload {
arbiter.shared.VaultState state = 1; arbiter.shared.VaultState state = 1;
unseal.Response unseal = 2; unseal.Response unseal = 2;
bootstrap.Response bootstrap = 3; bootstrap.Response bootstrap = 3;
rekey.Response rekey = 4;
} }
} }

View File

@@ -5,7 +5,8 @@ package arbiter.shared;
enum VaultState { enum VaultState {
VAULT_STATE_UNSPECIFIED = 0; VAULT_STATE_UNSPECIFIED = 0;
VAULT_STATE_UNBOOTSTRAPPED = 1; VAULT_STATE_UNBOOTSTRAPPED = 1;
VAULT_STATE_SEALED = 2; VAULT_STATE_BOOSTRAPPING = 2;
VAULT_STATE_UNSEALED = 3; VAULT_STATE_SEALED = 3;
VAULT_STATE_ERROR = 4; VAULT_STATE_UNSEALED = 4;
VAULT_STATE_ERROR = 5;
} }