refactor: rename to to better reflect meaning
Some checks failed
ci/woodpecker/push/server-audit Pipeline was successful
ci/woodpecker/push/server-vet Pipeline failed
ci/woodpecker/push/server-lint Pipeline failed
ci/woodpecker/push/server-test Pipeline was successful

This commit is contained in:
Skipper
2026-04-19 13:35:18 +02:00
parent fd25de32a1
commit a1c3ffd2d1
58 changed files with 437 additions and 437 deletions

View File

@@ -6,7 +6,7 @@ This file provides guidance to Codex (Codex.ai/code) when working with code in t
Arbiter is a **permissioned signing service** for cryptocurrency wallets. It consists of: Arbiter is a **permissioned signing service** for cryptocurrency wallets. It consists of:
- **`server/`** — Rust gRPC daemon that holds encrypted keys and enforces policies - **`server/`** — Rust gRPC daemon that holds encrypted keys and enforces policies
- **`useragent/`** — Flutter desktop app (macOS/Windows) with a Rust backend via Rinf - **`operator/`** — Flutter desktop app (macOS/Windows) with a Rust backend via Rinf
- **`protobufs/`** — Protocol Buffer definitions shared between server and client - **`protobufs/`** — Protocol Buffer definitions shared between server and client
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.
@@ -28,7 +28,7 @@ Key versions: Rust 1.93.0 (with clippy), Flutter 3.38.9-stable, protoc 29.6, die
|---|---| |---|---|
| `arbiter-proto` | Generated gRPC stubs + protobuf types; compiled from `protobufs/*.proto` via `tonic-prost-build` | | `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-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-operator` | Rust client library for the operator side of the gRPC protocol |
| `arbiter-client` | Rust client library for SDK clients | | `arbiter-client` | Rust client library for SDK clients |
### Common Commands ### Common Commands
@@ -67,10 +67,10 @@ The server is actor-based using the **kameo** crate. All long-lived state lives
- **`Bootstrapper`** — Manages the one-time bootstrap token written to `~/.arbiter/bootstrap_token` on first run. - **`Bootstrapper`** — Manages the 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`** — 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.
- **`FlowCoordinator`** — Coordinates cross-connection flow between user agents and SDK clients. - **`FlowCoordinator`** — Coordinates cross-connection flow between operators and SDK clients.
- **`EvmActor`** — Handles EVM transaction policy enforcement and signing. - **`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. Per-connection actors live under `actors/operator/` 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()`. **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()`.
@@ -121,20 +121,20 @@ 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.
## User Agent (Flutter + Rinf at `useragent/`) ## Operator (Flutter + Rinf at `operator/`)
The Flutter app uses [Rinf](https://rinf.cunarist.org) to call Rust code. The Rust logic lives in `useragent/native/hub/` as a separate crate that uses `arbiter-useragent` for the gRPC client. 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.
Communication between Dart and Rust uses typed **signals** defined in `useragent/native/hub/src/signals/`. After modifying signal structs, regenerate Dart bindings: Communication between Dart and Rust uses typed **signals** defined in `operator/native/hub/src/signals/`. After modifying signal structs, regenerate Dart bindings:
```sh ```sh
cd useragent && rinf gen cd operator && rinf gen
``` ```
### Common Commands ### Common Commands
```sh ```sh
cd useragent cd operator
# Run the app (macOS or Windows) # Run the app (macOS or Windows)
flutter run flutter run
@@ -146,4 +146,4 @@ rinf gen
flutter analyze 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. 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.

View File

@@ -4,7 +4,7 @@
## Security warning ## Security warning
Arbiter can't meaningfully protect against host compromise. Potential attack flow: Arbiter can't meaningfully protect against host compromise. Potential attack flow:
- Attacker steals TLS keys from database - Attacker steals TLS keys from database
- Pretends to be server; just accepts user agent challenge solutions - Pretends to be server; just accepts operator challenge solutions
- Pretend to be in sealed state and performing DH with client - Pretend to be in sealed state and performing DH with client
- Steals user password and derives seal key - Steals user password and derives seal key

View File

@@ -9,7 +9,7 @@ Arbiter is a permissioned signing service for cryptocurrency wallets. It runs as
Arbiter distinguishes two kinds of peers: Arbiter distinguishes two kinds of peers:
- **User Agent** — A client application used by the owner to manage the vault (create wallets, approve SDK clients, configure policies). - **Operator** — A client application used by the owner to manage the vault (create wallets, approve SDK clients, configure policies).
- **SDK Client** — A consumer of signing capabilities, typically an automation tool. In the future, this could include a browser-based wallet. - **SDK Client** — A consumer of signing capabilities, typically an automation tool. In the future, this could include a browser-based wallet.
- **Recovery Operator** — A dormant recovery participant with narrowly scoped authority used only for custody recovery and operator replacement. - **Recovery Operator** — A dormant recovery participant with narrowly scoped authority used only for custody recovery and operator replacement.
@@ -30,24 +30,24 @@ All peers authenticate via public-key cryptography using a challenge-response pr
Authentication challenges are per-connection, ephemeral values. They are not persisted in the peer tables, and peer records store no challenge state. Authentication challenges are per-connection, ephemeral values. They are not persisted in the peer tables, and peer records store no challenge state.
### 2.2 User Agent Bootstrap ### 2.2 Operator Bootstrap
On first run — when no User Agents are registered — the server generates a one-time bootstrap token. It is made available in two ways: On first run — when no Operators are registered — the server generates a one-time bootstrap token. It is made available in two ways:
- **Local setup:** Written to `~/.arbiter/bootstrap_token` for automatic discovery by a co-located User Agent. - **Local setup:** Written to `~/.arbiter/bootstrap_token` for automatic discovery by a co-located Operator.
- **Remote setup:** Printed to the server's console output. - **Remote setup:** Printed to the server's console output.
The first User Agent must present this token alongside the standard challenge-response to complete registration. The first Operator must present this token alongside the standard challenge-response to complete registration.
### 2.3 SDK Client Registration ### 2.3 SDK Client Registration
There is no bootstrap mechanism for SDK clients. They must be explicitly approved by an already-registered User Agent. There is no bootstrap mechanism for SDK clients. They must be explicitly approved by an already-registered Operator.
--- ---
## 3. Multi-Operator Governance ## 3. Multi-Operator Governance
When more than one User Agent is registered, the vault is treated as having multiple operators. In that mode, sensitive actions are governed by voting rather than by a single operator decision. When more than one Operator is registered, the vault is treated as having multiple operators. In that mode, sensitive actions are governed by voting rather than by a single operator decision.
### 3.1 Voting Rules ### 3.1 Voting Rules
@@ -165,13 +165,13 @@ In both cases, committee formation is a coordinated process. Arbiter does not al
When an unbootstrapped vault is initialized as a multi-operator vault, the setup proceeds as follows: When an unbootstrapped vault is initialized as a multi-operator vault, the setup proceeds as follows:
1. An operator connects to the unbootstrapped vault using a User Agent and the bootstrap token. 1. An operator connects to the unbootstrapped vault using an Operator and the bootstrap token.
2. During bootstrap setup, that operator declares: 2. During bootstrap setup, that operator declares:
- the total number of ordinary operators - the total number of ordinary operators
- the total number of Recovery Operators - the total number of Recovery Operators
3. The vault enters **multi-bootstrap mode**. 3. The vault enters **multi-bootstrap mode**.
4. While in multi-bootstrap mode: 4. While in multi-bootstrap mode:
- every ordinary operator must connect with a User Agent using the bootstrap token - every ordinary operator must connect with an Operator using the bootstrap token
- every Recovery Operator must also connect using the bootstrap token - every Recovery Operator must also connect using the bootstrap token
- each participant is registered individually - each participant is registered individually
- each participant's share is created and protected with that participant's credentials - each participant's share is created and protected with that participant's credentials
@@ -193,8 +193,8 @@ The server proves its identity using TLS with a self-signed certificate. The TLS
Peers verify the server by its **public key fingerprint**: Peers verify the server by its **public key fingerprint**:
- **User Agent (local):** Receives the fingerprint automatically through the bootstrap token. - **Operator (local):** Receives the fingerprint automatically through the bootstrap token.
- **User Agent (remote) / SDK Client:** Must receive the fingerprint out-of-band. - **Operator (remote) / SDK Client:** Must receive the fingerprint out-of-band.
> A streamlined setup mechanism using a single connection string is planned but not yet implemented. > A streamlined setup mechanism using a single connection string is planned but not yet implemented.
@@ -231,11 +231,11 @@ On boot, the root key is encrypted and the server cannot perform any signing ope
### 6.2 Unseal Flow ### 6.2 Unseal Flow
To transition to the **Unsealed** state, a User Agent must provide the password: To transition to the **Unsealed** state, an Operator must provide the password:
1. The User Agent initiates an unseal request. 1. The Operator initiates an unseal request.
2. The server generates a one-time key pair and returns the public key. 2. The server generates a one-time key pair and returns the public key.
3. The User Agent encrypts the user's password with this one-time public key and sends the ciphertext to the server. 3. The Operator encrypts the user's password with this one-time public key and sends the ciphertext to the server.
4. The server decrypts and verifies the password: 4. The server decrypts and verifies the password:
- **Success:** The root key is decrypted and placed into a hardened memory cell. The server transitions to `Unsealed`. Any entries pending encryption scheme migration are re-encrypted. - **Success:** The root key is decrypted and placed into a hardened memory cell. The server transitions to `Unsealed`. Any entries pending encryption scheme migration are re-encrypted.
- **Failure:** The server returns an error indicating the password is incorrect. - **Failure:** The server returns an error indicating the password is incorrect.
@@ -257,7 +257,7 @@ See [IMPLEMENTATION.md](IMPLEMENTATION.md) for the current and planned memory pr
### 7.1 Fundamental Rules ### 7.1 Fundamental Rules
- SDK clients have **no access by default**. - SDK clients have **no access by default**.
- Access is granted **explicitly** by a User Agent. - Access is granted **explicitly** by an Operator.
- Grants are scoped to **specific wallets** and governed by **policies**. - Grants are scoped to **specific wallets** and governed by **policies**.
Each blockchain requires its own policy system due to differences in static transaction analysis. Currently, only EVM is supported; Solana support is planned. Each blockchain requires its own policy system due to differences in static transaction analysis. Currently, only EVM is supported; Solana support is planned.
@@ -277,19 +277,19 @@ sequenceDiagram
autonumber autonumber
actor SDK as SDK Client actor SDK as SDK Client
participant Server participant Server
participant UA as User Agent participant operator as Operator
SDK->>Server: SignTransactionRequest SDK->>Server: SignTransactionRequest
Server->>Server: Resolve wallet and wallet visibility Server->>Server: Resolve wallet and wallet visibility
alt Visibility approval required alt Visibility approval required
Server->>UA: Ask for wallet visibility approval Server->>operator: Ask for wallet visibility approval
UA-->>Server: Vote result operator-->>Server: Vote result
end end
Server->>Server: Evaluate transaction Server->>Server: Evaluate transaction
Server->>Server: Load grant and limits context Server->>Server: Load grant and limits context
alt Grant approval required alt Grant approval required
Server->>UA: Ask for execution / grant approval Server->>operator: Ask for execution / grant approval
UA-->>Server: Vote result operator-->>Server: Vote result
opt Create persistent grant opt Create persistent grant
Server->>Server: Create and store grant Server->>Server: Create and store grant
end end

View File

@@ -8,10 +8,10 @@ This document covers concrete technology choices and dependencies. For the archi
### Authentication Result Semantics ### Authentication Result Semantics
Authentication no longer uses an implicit success-only response shape. Both `client` and `user-agent` return explicit auth status enums over the wire. Authentication no longer uses an implicit success-only response shape. Both `client` and `operator` return explicit auth status enums over the wire.
- **Client:** `AuthResult` may return `SUCCESS`, `INVALID_KEY`, `INVALID_SIGNATURE`, `APPROVAL_DENIED`, `NO_USER_AGENTS_ONLINE`, or `INTERNAL` - **Client:** `AuthResult` may return `SUCCESS`, `INVALID_KEY`, `INVALID_SIGNATURE`, `APPROVAL_DENIED`, `NO_OPERATORS_ONLINE`, or `INTERNAL`
- **User-agent:** `AuthResult` may return `SUCCESS`, `INVALID_KEY`, `INVALID_SIGNATURE`, `BOOTSTRAP_REQUIRED`, `TOKEN_INVALID`, or `INTERNAL` - **Operator:** `AuthResult` may return `SUCCESS`, `INVALID_KEY`, `INVALID_SIGNATURE`, `BOOTSTRAP_REQUIRED`, `TOKEN_INVALID`, or `INTERNAL`
This makes transport-level failures and actor/domain-level auth failures distinct: This makes transport-level failures and actor/domain-level auth failures distinct:
@@ -22,7 +22,7 @@ Clients are expected to handle these status codes directly and present the concr
### New Client Approval ### New Client Approval
When a client whose public key is not yet in the database connects, all connected user agents are asked to approve the connection. The first agent to respond determines the outcome; remaining requests are cancelled via a watch channel. When a client whose public key is not yet in the database connects, all connected operators are asked to approve the connection. The first operator to respond determines the outcome; remaining requests are cancelled via a watch channel.
```mermaid ```mermaid
flowchart TD flowchart TD
@@ -31,10 +31,10 @@ flowchart TD
C -- yes --> G[Generate AuthChallenge] C -- yes --> G[Generate AuthChallenge]
C -- no --> E[Ask all UserAgents:\nClientConnectionRequest] C -- no --> E[Ask all Operators:\nClientConnectionRequest]
E --> F{First response} E --> F{First response}
F -- denied --> Z([Reject connection]) F -- denied --> Z([Reject connection])
F -- approved --> F2[Cancel remaining\nUserAgent requests] F -- approved --> F2[Cancel remaining\nOperator requests]
F2 --> F3[INSERT client] F2 --> F3[INSERT client]
F3 --> G F3 --> G
@@ -50,7 +50,7 @@ Auth challenges are generated from fresh random bytes plus a nanosecond timestam
The authentication schema stores peer identity, not replay counters: The authentication schema stores peer identity, not replay counters:
- `program_client` stores the SDK client's public key, metadata binding, and timestamps. - `program_client` stores the SDK client's public key, metadata binding, and timestamps.
- `useragent_client` stores the User Agent public key and timestamps. - `operator_client` stores the Operator public key and timestamps.
- Neither table stores an authentication nonce, and challenge generation does not update either table. - Neither table stores an authentication nonce, and challenge generation does not update either table.
--- ---
@@ -62,7 +62,7 @@ The authentication schema stores peer identity, not replay counters:
### User-Agent Authentication ### User-Agent Authentication
User-agent authentication supports multiple signature schemes because platform-provided "hardware-bound" keys do not expose a uniform algorithm across operating systems and hardware. Operator authentication supports multiple signature schemes because platform-provided "hardware-bound" keys do not expose a uniform algorithm across operating systems and hardware.
- **Supported schemes:** ML-DSA - **Supported schemes:** ML-DSA
- **Why:** Secure Enclave (MacOS) support them natively, on other platforms we could emulate while they roll-out - **Why:** Secure Enclave (MacOS) support them natively, on other platforms we could emulate while they roll-out
@@ -86,7 +86,7 @@ User-agent authentication supports multiple signature schemes because platform-p
### Request Multiplexing ### Request Multiplexing
Both `client` and `user-agent` connections support multiple in-flight requests over one gRPC bidi stream. Both `client` and `operator` connections support multiple in-flight requests over one gRPC bidi stream.
- Every request carries a monotonically increasing request ID - Every request carries a monotonically increasing request ID
- Every normal response echoes the request ID it corresponds to - Every normal response echoes the request ID it corresponds to
@@ -141,7 +141,7 @@ flowchart TD
L -- Yes --> M[Check grant limits] L -- Yes --> M[Check grant limits]
L -- No --> N[Start execution or grant voting flow] L -- No --> N[Start execution or grant voting flow]
N --> O{User-agent decision} N --> O{Operator decision}
O -- Reject --> Z4[Return no matching grant error] O -- Reject --> Z4[Return no matching grant error]
O -- Allow once --> M O -- Allow once --> M
O -- Create grant --> P[Create grant with user-selected limits] O -- Create grant --> P[Create grant with user-selected limits]

View File

@@ -111,7 +111,7 @@ String shortAddress(List<int> bytes) {
- [ ] **Step 2: Verify** - [ ] **Step 2: Verify**
```sh ```sh
cd useragent && dart analyze lib/screens/dashboard/evm/grants/create/utils.dart cd operator && dart analyze lib/screens/dashboard/evm/grants/create/utils.dart
``` ```
Expected: no errors. Expected: no errors.
@@ -168,7 +168,7 @@ class GrantCreation extends _$GrantCreation {
- [ ] **Step 2: Run code generator** - [ ] **Step 2: Run code generator**
```sh ```sh
cd useragent && dart run build_runner build --delete-conflicting-outputs cd operator && dart run build_runner build --delete-conflicting-outputs
``` ```
Expected: generates `provider.freezed.dart` and `provider.g.dart`, no errors. Expected: generates `provider.freezed.dart` and `provider.g.dart`, no errors.
@@ -176,7 +176,7 @@ Expected: generates `provider.freezed.dart` and `provider.g.dart`, no errors.
- [ ] **Step 3: Verify** - [ ] **Step 3: Verify**
```sh ```sh
cd useragent && dart analyze lib/screens/dashboard/evm/grants/create/provider.dart cd operator && dart analyze lib/screens/dashboard/evm/grants/create/provider.dart
``` ```
Expected: no errors. Expected: no errors.
@@ -204,7 +204,7 @@ jj describe -m "feat(grants): add GrantCreation provider (client selection + gra
```dart ```dart
// lib/screens/dashboard/evm/grants/create/fields/client_picker_field.dart // lib/screens/dashboard/evm/grants/create/fields/client_picker_field.dart
import 'package:arbiter/proto/user_agent.pb.dart'; import 'package:arbiter/proto/operator.pb.dart';
import 'package:arbiter/providers/sdk_clients/list.dart'; import 'package:arbiter/providers/sdk_clients/list.dart';
import 'package:arbiter/screens/dashboard/evm/grants/create/provider.dart'; import 'package:arbiter/screens/dashboard/evm/grants/create/provider.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -246,7 +246,7 @@ class ClientPickerField extends ConsumerWidget {
```dart ```dart
// lib/screens/dashboard/evm/grants/create/fields/wallet_access_picker_field.dart // lib/screens/dashboard/evm/grants/create/fields/wallet_access_picker_field.dart
import 'package:arbiter/proto/evm.pb.dart'; import 'package:arbiter/proto/evm.pb.dart';
import 'package:arbiter/proto/user_agent.pb.dart'; import 'package:arbiter/proto/operator.pb.dart';
import 'package:arbiter/providers/evm/evm.dart'; import 'package:arbiter/providers/evm/evm.dart';
import 'package:arbiter/providers/sdk_clients/wallet_access_list.dart'; import 'package:arbiter/providers/sdk_clients/wallet_access_list.dart';
import 'package:arbiter/screens/dashboard/evm/grants/create/provider.dart'; import 'package:arbiter/screens/dashboard/evm/grants/create/provider.dart';
@@ -522,7 +522,7 @@ class TransactionRateLimitField extends StatelessWidget {
- [ ] **Step 8: Verify all field widgets** - [ ] **Step 8: Verify all field widgets**
```sh ```sh
cd useragent && dart analyze lib/screens/dashboard/evm/grants/create/fields/ cd operator && dart analyze lib/screens/dashboard/evm/grants/create/fields/
``` ```
Expected: no errors. Expected: no errors.
@@ -585,7 +585,7 @@ class SharedGrantFields extends StatelessWidget {
- [ ] **Step 2: Verify** - [ ] **Step 2: Verify**
```sh ```sh
cd useragent && dart analyze lib/screens/dashboard/evm/grants/create/shared_grant_fields.dart cd operator && dart analyze lib/screens/dashboard/evm/grants/create/shared_grant_fields.dart
``` ```
Expected: no errors. Expected: no errors.
@@ -978,7 +978,7 @@ class _TokenVolumeLimitRow extends HookWidget {
- [ ] **Step 4: Run code generator for token_transfer_grant.g.dart** - [ ] **Step 4: Run code generator for token_transfer_grant.g.dart**
```sh ```sh
cd useragent && dart run build_runner build --delete-conflicting-outputs cd operator && dart run build_runner build --delete-conflicting-outputs
``` ```
Expected: generates `token_transfer_grant.g.dart`, no errors. Expected: generates `token_transfer_grant.g.dart`, no errors.
@@ -986,7 +986,7 @@ Expected: generates `token_transfer_grant.g.dart`, no errors.
- [ ] **Step 5: Verify** - [ ] **Step 5: Verify**
```sh ```sh
cd useragent && dart analyze lib/screens/dashboard/evm/grants/create/grants/ cd operator && dart analyze lib/screens/dashboard/evm/grants/create/grants/
``` ```
Expected: no errors. Expected: no errors.
@@ -1265,7 +1265,7 @@ String _formatError(Object error) {
- [ ] **Step 2: Verify the full create/ directory** - [ ] **Step 2: Verify the full create/ directory**
```sh ```sh
cd useragent && dart analyze lib/screens/dashboard/evm/grants/create/ cd operator && dart analyze lib/screens/dashboard/evm/grants/create/
``` ```
Expected: no errors. Expected: no errors.

View File

@@ -14,24 +14,24 @@
| File | Action | Responsibility | | File | Action | Responsibility |
|---|---|---| |---|---|---|
| `useragent/lib/theme/palette.dart` | Modify | Add `Palette.token` (indigo accent for token-transfer cards) | | `operator/lib/theme/palette.dart` | Modify | Add `Palette.token` (indigo accent for token-transfer cards) |
| `useragent/lib/features/connection/evm/wallet_access.dart` | Modify | Add `listAllWalletAccesses()` function | | `operator/lib/features/connection/evm/wallet_access.dart` | Modify | Add `listAllWalletAccesses()` function |
| `useragent/lib/providers/sdk_clients/wallet_access_list.dart` | Create | `WalletAccessListProvider` — fetches full wallet access list with IDs | | `operator/lib/providers/sdk_clients/wallet_access_list.dart` | Create | `WalletAccessListProvider` — fetches full wallet access list with IDs |
| `useragent/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/widgets/grant_card.dart` | Create | `GrantCard` widget — watches enrichment providers + revoke mutation; one card per grant |
| `useragent/lib/screens/dashboard/evm/grants/grants.dart` | Create | `EvmGrantsScreen` — watches `evmGrantsProvider`; handles loading/error/empty/data states; renders `GrantCard` list | | `operator/lib/screens/dashboard/evm/grants/grants.dart` | Create | `EvmGrantsScreen` — watches `evmGrantsProvider`; handles loading/error/empty/data states; renders `GrantCard` list |
| `useragent/lib/router.dart` | Modify | Register `EvmGrantsRoute` in dashboard children | | `operator/lib/router.dart` | Modify | Register `EvmGrantsRoute` in dashboard children |
| `useragent/lib/screens/dashboard.dart` | Modify | Add Grants entry to `routes` list and `NavigationDestination` list | | `operator/lib/screens/dashboard.dart` | Modify | Add Grants entry to `routes` list and `NavigationDestination` list |
--- ---
## Task 1: Add `Palette.token` ## Task 1: Add `Palette.token`
**Files:** **Files:**
- Modify: `useragent/lib/theme/palette.dart` - Modify: `operator/lib/theme/palette.dart`
- [ ] **Step 1: Add the color** - [ ] **Step 1: Add the color**
Replace the contents of `useragent/lib/theme/palette.dart` with: Replace the contents of `operator/lib/theme/palette.dart` with:
```dart ```dart
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -48,7 +48,7 @@ class Palette {
- [ ] **Step 2: Verify** - [ ] **Step 2: Verify**
```sh ```sh
cd useragent && flutter analyze lib/theme/palette.dart cd operator && flutter analyze lib/theme/palette.dart
``` ```
Expected: no issues. Expected: no issues.
@@ -65,20 +65,20 @@ jj new
## Task 2: Add `listAllWalletAccesses` feature function ## Task 2: Add `listAllWalletAccesses` feature function
**Files:** **Files:**
- Modify: `useragent/lib/features/connection/evm/wallet_access.dart` - 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. `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** - [ ] **Step 1: Append function**
Add at the bottom of `useragent/lib/features/connection/evm/wallet_access.dart`: Add at the bottom of `operator/lib/features/connection/evm/wallet_access.dart`:
```dart ```dart
Future<List<SdkClientWalletAccess>> listAllWalletAccesses( Future<List<SdkClientWalletAccess>> listAllWalletAccesses(
Connection connection, Connection connection,
) async { ) async {
final response = await connection.ask( final response = await connection.ask(
UserAgentRequest(listWalletAccess: Empty()), OperatorRequest(listWalletAccess: Empty()),
); );
if (!response.hasListWalletAccessResponse()) { if (!response.hasListWalletAccessResponse()) {
throw Exception( throw Exception(
@@ -97,7 +97,7 @@ Each returned `SdkClientWalletAccess` has:
- [ ] **Step 2: Verify** - [ ] **Step 2: Verify**
```sh ```sh
cd useragent && flutter analyze lib/features/connection/evm/wallet_access.dart cd operator && flutter analyze lib/features/connection/evm/wallet_access.dart
``` ```
Expected: no issues. Expected: no issues.
@@ -114,18 +114,18 @@ jj new
## Task 3: Create `WalletAccessListProvider` ## Task 3: Create `WalletAccessListProvider`
**Files:** **Files:**
- Create: `useragent/lib/providers/sdk_clients/wallet_access_list.dart` - Create: `operator/lib/providers/sdk_clients/wallet_access_list.dart`
- Generated: `useragent/lib/providers/sdk_clients/wallet_access_list.g.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. Mirrors the structure of `EvmGrants` in `providers/evm/evm_grants.dart` — class-based `@riverpod` with a `refresh()` method.
- [ ] **Step 1: Write the provider** - [ ] **Step 1: Write the provider**
Create `useragent/lib/providers/sdk_clients/wallet_access_list.dart`: Create `operator/lib/providers/sdk_clients/wallet_access_list.dart`:
```dart ```dart
import 'package:arbiter/features/connection/evm/wallet_access.dart'; import 'package:arbiter/features/connection/evm/wallet_access.dart';
import 'package:arbiter/proto/user_agent.pb.dart'; import 'package:arbiter/proto/operator.pb.dart';
import 'package:arbiter/providers/connection/connection_manager.dart'; import 'package:arbiter/providers/connection/connection_manager.dart';
import 'package:mtcore/markettakers.dart'; import 'package:mtcore/markettakers.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
@@ -165,15 +165,15 @@ class WalletAccessList extends _$WalletAccessList {
- [ ] **Step 2: Run code generation** - [ ] **Step 2: Run code generation**
```sh ```sh
cd useragent && dart run build_runner build --delete-conflicting-outputs cd operator && dart run build_runner build --delete-conflicting-outputs
``` ```
Expected: `useragent/lib/providers/sdk_clients/wallet_access_list.g.dart` created. No errors. Expected: `operator/lib/providers/sdk_clients/wallet_access_list.g.dart` created. No errors.
- [ ] **Step 3: Verify** - [ ] **Step 3: Verify**
```sh ```sh
cd useragent && flutter analyze lib/providers/sdk_clients/ cd operator && flutter analyze lib/providers/sdk_clients/
``` ```
Expected: no issues. Expected: no issues.
@@ -190,26 +190,26 @@ jj new
## Task 4: Create `GrantCard` widget ## Task 4: Create `GrantCard` widget
**Files:** **Files:**
- Create: `useragent/lib/screens/dashboard/evm/grants/widgets/grant_card.dart` - 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. 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:** **Key types:**
- `GrantEntry` (from `proto/evm.pb.dart`): `.id`, `.shared.walletAccessId`, `.shared.chainId`, `.specific.whichGrant()` - `GrantEntry` (from `proto/evm.pb.dart`): `.id`, `.shared.walletAccessId`, `.shared.chainId`, `.specific.whichGrant()`
- `SpecificGrant_Grant.etherTransfer` / `.tokenTransfer` — enum values for the oneof - `SpecificGrant_Grant.etherTransfer` / `.tokenTransfer` — enum values for the oneof
- `SdkClientWalletAccess` (from `proto/user_agent.pb.dart`): `.id`, `.access.walletId`, `.access.sdkClientId` - `SdkClientWalletAccess` (from `proto/operator.pb.dart`): `.id`, `.access.walletId`, `.access.sdkClientId`
- `WalletEntry` (from `proto/evm.pb.dart`): `.id`, `.address` (List<int>) - `WalletEntry` (from `proto/evm.pb.dart`): `.id`, `.address` (List<int>)
- `SdkClientEntry` (from `proto/user_agent.pb.dart`): `.id`, `.info.name` - `SdkClientEntry` (from `proto/operator.pb.dart`): `.id`, `.info.name`
- `revokeEvmGrantMutation``Mutation<void>` (global; all revoke buttons disable together while any revoke is in flight) - `revokeEvmGrantMutation``Mutation<void>` (global; all revoke buttons disable together while any revoke is in flight)
- `executeRevokeEvmGrant(ref, grantId: int)``Future<void>` - `executeRevokeEvmGrant(ref, grantId: int)``Future<void>`
- [ ] **Step 1: Write the widget** - [ ] **Step 1: Write the widget**
Create `useragent/lib/screens/dashboard/evm/grants/widgets/grant_card.dart`: Create `operator/lib/screens/dashboard/evm/grants/widgets/grant_card.dart`:
```dart ```dart
import 'package:arbiter/proto/evm.pb.dart'; import 'package:arbiter/proto/evm.pb.dart';
import 'package:arbiter/proto/user_agent.pb.dart'; import 'package:arbiter/proto/operator.pb.dart';
import 'package:arbiter/providers/evm/evm.dart'; import 'package:arbiter/providers/evm/evm.dart';
import 'package:arbiter/providers/evm/evm_grants.dart'; import 'package:arbiter/providers/evm/evm_grants.dart';
import 'package:arbiter/providers/sdk_clients/list.dart'; import 'package:arbiter/providers/sdk_clients/list.dart';
@@ -438,7 +438,7 @@ class GrantCard extends ConsumerWidget {
- [ ] **Step 2: Verify** - [ ] **Step 2: Verify**
```sh ```sh
cd useragent && flutter analyze lib/screens/dashboard/evm/grants/widgets/grant_card.dart cd operator && flutter analyze lib/screens/dashboard/evm/grants/widgets/grant_card.dart
``` ```
Expected: no issues. Expected: no issues.
@@ -455,13 +455,13 @@ jj new
## Task 5: Create `EvmGrantsScreen` ## Task 5: Create `EvmGrantsScreen`
**Files:** **Files:**
- Create: `useragent/lib/screens/dashboard/evm/grants/grants.dart` - 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. 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** - [ ] **Step 1: Write the screen**
Create `useragent/lib/screens/dashboard/evm/grants/grants.dart`: Create `operator/lib/screens/dashboard/evm/grants/grants.dart`:
```dart ```dart
import 'package:arbiter/proto/evm.pb.dart'; import 'package:arbiter/proto/evm.pb.dart';
@@ -702,7 +702,7 @@ class EvmGrantsScreen extends ConsumerWidget {
- [ ] **Step 2: Verify** - [ ] **Step 2: Verify**
```sh ```sh
cd useragent && flutter analyze lib/screens/dashboard/evm/grants/ cd operator && flutter analyze lib/screens/dashboard/evm/grants/
``` ```
Expected: no issues. Expected: no issues.
@@ -719,13 +719,13 @@ jj new
## Task 6: Wire router and dashboard tab ## Task 6: Wire router and dashboard tab
**Files:** **Files:**
- Modify: `useragent/lib/router.dart` - Modify: `operator/lib/router.dart`
- Modify: `useragent/lib/screens/dashboard.dart` - Modify: `operator/lib/screens/dashboard.dart`
- Regenerated: `useragent/lib/router.gr.dart` - Regenerated: `operator/lib/router.gr.dart`
- [ ] **Step 1: Add route to `router.dart`** - [ ] **Step 1: Add route to `router.dart`**
Replace the contents of `useragent/lib/router.dart` with: Replace the contents of `operator/lib/router.dart` with:
```dart ```dart
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
@@ -759,7 +759,7 @@ class Router extends RootStackRouter {
- [ ] **Step 2: Update `dashboard.dart`** - [ ] **Step 2: Update `dashboard.dart`**
In `useragent/lib/screens/dashboard.dart`, replace the `routes` constant: In `operator/lib/screens/dashboard.dart`, replace the `routes` constant:
```dart ```dart
final routes = [ final routes = [
@@ -800,7 +800,7 @@ destinations: const [
- [ ] **Step 3: Regenerate router** - [ ] **Step 3: Regenerate router**
```sh ```sh
cd useragent && dart run build_runner build --delete-conflicting-outputs cd operator && dart run build_runner build --delete-conflicting-outputs
``` ```
Expected: `lib/router.gr.dart` updated, `EvmGrantsRoute` now available, no errors. Expected: `lib/router.gr.dart` updated, `EvmGrantsRoute` now available, no errors.
@@ -808,7 +808,7 @@ Expected: `lib/router.gr.dart` updated, `EvmGrantsRoute` now available, no error
- [ ] **Step 4: Full project verify** - [ ] **Step 4: Full project verify**
```sh ```sh
cd useragent && flutter analyze cd operator && flutter analyze
``` ```
Expected: no issues. Expected: no issues.

View File

@@ -4,7 +4,7 @@
## Overview ## Overview
Add a "Grants" dashboard tab to the Flutter user-agent 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. 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 ## Scope
@@ -23,7 +23,7 @@ Add a "Grants" dashboard tab to the Flutter user-agent app that displays all EVM
### `walletAccessListProvider` ### `walletAccessListProvider`
**File:** `useragent/lib/providers/sdk_clients/wallet_access_list.dart` **File:** `operator/lib/providers/sdk_clients/wallet_access_list.dart`
- `@riverpod` class, watches `connectionManagerProvider.future` - `@riverpod` class, watches `connectionManagerProvider.future`
- Returns `List<SdkClientWalletAccess>?` (null when not connected) - Returns `List<SdkClientWalletAccess>?` (null when not connected)
@@ -85,7 +85,7 @@ NavigationDestination(
## Screen: `EvmGrantsScreen` ## Screen: `EvmGrantsScreen`
**File:** `useragent/lib/screens/dashboard/evm/grants/grants.dart` **File:** `operator/lib/screens/dashboard/evm/grants/grants.dart`
``` ```
Scaffold Scaffold

View File

@@ -1,51 +1,51 @@
# @generated - this file is auto-generated by `mise lock` https://mise.jdx.dev/dev-tools/mise-lock.html # @generated - this file is auto-generated by `mise lock` https://mise.jdx.dev/dev-tools/mise-lock.html
[[tools.ast-grep]] [[tools.ast-grep]]
version = "0.42.0" version = "0.42.1"
backend = "aqua:ast-grep/ast-grep" backend = "aqua:ast-grep/ast-grep"
[tools.ast-grep."platforms.linux-arm64"] [tools.ast-grep."platforms.linux-arm64"]
checksum = "sha256:5c830eae8456569e2f7212434ed9c238f58dca412d76045418ed6d394a755836" checksum = "sha256:3ba383839044cf9817929435f5ce0027f91d06931e8efb32d942e58d73d92be5"
url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.0/app-aarch64-unknown-linux-gnu.zip" url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.1/app-aarch64-unknown-linux-gnu.zip"
[tools.ast-grep."platforms.linux-arm64-musl"] [tools.ast-grep."platforms.linux-arm64-musl"]
checksum = "sha256:5c830eae8456569e2f7212434ed9c238f58dca412d76045418ed6d394a755836" checksum = "sha256:3ba383839044cf9817929435f5ce0027f91d06931e8efb32d942e58d73d92be5"
url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.0/app-aarch64-unknown-linux-gnu.zip" url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.1/app-aarch64-unknown-linux-gnu.zip"
[tools.ast-grep."platforms.linux-x64"] [tools.ast-grep."platforms.linux-x64"]
checksum = "sha256:e825a05603f0bcc4cd9076c4cc8c9abd6d008b7cd07d9aa3cc323ba4b8606651" checksum = "sha256:5de8b87cba67fc8dc3e239d54b6484802ad745a7ae3de76be4fe89661dc52657"
url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.0/app-x86_64-unknown-linux-gnu.zip" url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.1/app-x86_64-unknown-linux-gnu.zip"
[tools.ast-grep."platforms.linux-x64-musl"] [tools.ast-grep."platforms.linux-x64-musl"]
checksum = "sha256:e825a05603f0bcc4cd9076c4cc8c9abd6d008b7cd07d9aa3cc323ba4b8606651" checksum = "sha256:5de8b87cba67fc8dc3e239d54b6484802ad745a7ae3de76be4fe89661dc52657"
url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.0/app-x86_64-unknown-linux-gnu.zip" url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.1/app-x86_64-unknown-linux-gnu.zip"
[tools.ast-grep."platforms.macos-arm64"] [tools.ast-grep."platforms.macos-arm64"]
checksum = "sha256:fc300d5293b1c770a5aece03a8a193b92e71e87cec726c28096990691a582620" checksum = "sha256:c3961d8e8a4ee0ce2d0d98c7beeb168bb331cdc766b53630118a7b6c4fd39015"
url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.0/app-aarch64-apple-darwin.zip" url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.1/app-aarch64-apple-darwin.zip"
[tools.ast-grep."platforms.macos-x64"] [tools.ast-grep."platforms.macos-x64"]
checksum = "sha256:979ffe611327056f4730a1ae71b0209b3b830f58b22c6ed194cda34f55400db2" checksum = "sha256:a038965bfd7fe44257c771cdf8918dc3467dd8ec0eef673b8b14f639b144cdbd"
url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.0/app-x86_64-apple-darwin.zip" url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.1/app-x86_64-apple-darwin.zip"
[tools.ast-grep."platforms.windows-x64"] [tools.ast-grep."platforms.windows-x64"]
checksum = "sha256:55836fa1b2c65dc7d61615a4d9368622a0d2371a76d28b9a165e5a3ab6ae32a4" checksum = "sha256:fe34f631bb24c08ad146f92ca2a92971a53d179461b509fd8d32dc863bff9f83"
url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.0/app-x86_64-pc-windows-msvc.zip" url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.1/app-x86_64-pc-windows-msvc.zip"
[[tools."cargo:cargo-audit"]] [[tools."cargo:cargo-audit"]]
version = "0.22.1" version = "0.22.1"
backend = "cargo:cargo-audit" backend = "cargo:cargo-audit"
[[tools."cargo:cargo-edit"]] [[tools."cargo:cargo-edit"]]
version = "0.13.9" version = "0.13.10"
backend = "cargo:cargo-edit" backend = "cargo:cargo-edit"
[[tools."cargo:cargo-features-manager"]] [[tools."cargo:cargo-features-manager"]]
version = "0.11.1" version = "0.12.0"
backend = "cargo:cargo-features-manager" backend = "cargo:cargo-features-manager"
[[tools."cargo:cargo-insta"]] [[tools."cargo:cargo-insta"]]
version = "1.46.3" version = "1.47.2"
backend = "cargo:cargo-insta" backend = "cargo:cargo-insta"
[[tools."cargo:cargo-mutants"]] [[tools."cargo:cargo-mutants"]]
@@ -53,7 +53,7 @@ version = "27.0.0"
backend = "cargo:cargo-mutants" backend = "cargo:cargo-mutants"
[[tools."cargo:cargo-nextest"]] [[tools."cargo:cargo-nextest"]]
version = "0.9.126" version = "0.9.133"
backend = "cargo:cargo-nextest" backend = "cargo:cargo-nextest"
[[tools."cargo:cargo-shear"]] [[tools."cargo:cargo-shear"]]
@@ -65,7 +65,7 @@ version = "0.10.2"
backend = "cargo:cargo-vet" backend = "cargo:cargo-vet"
[[tools."cargo:diesel_cli"]] [[tools."cargo:diesel_cli"]]
version = "2.3.6" version = "2.3.7"
backend = "cargo:diesel_cli" backend = "cargo:diesel_cli"
[tools."cargo:diesel_cli".options] [tools."cargo:diesel_cli".options]
@@ -77,7 +77,7 @@ version = "2.12.0"
backend = "cargo:flutter_rust_bridge_codegen" backend = "cargo:flutter_rust_bridge_codegen"
[[tools.flutter]] [[tools.flutter]]
version = "3.38.9-stable" version = "3.41.7-stable"
backend = "asdf:flutter" backend = "asdf:flutter"
[[tools.protoc]] [[tools.protoc]]
@@ -113,44 +113,44 @@ checksum = "sha256:1ebd7c87baffb9f1c47169b640872bf5fb1e4408079c691af527be9561d8f
url = "https://github.com/protocolbuffers/protobuf/releases/download/v29.6/protoc-29.6-win64.zip" url = "https://github.com/protocolbuffers/protobuf/releases/download/v29.6/protoc-29.6-win64.zip"
[[tools.python]] [[tools.python]]
version = "3.14.3" version = "3.14.4"
backend = "core:python" backend = "core:python"
[tools.python."platforms.linux-arm64"] [tools.python."platforms.linux-arm64"]
checksum = "sha256:53700338695e402a1a1fe22be4a41fbdacc70e22bb308a48eca8ed67cb7992be" checksum = "sha256:b8b597fdb2f8dccdc502c11947b60a4b65eb6bce79cfa60c7ccf9b6e8352c60a"
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260324/cpython-3.14.3+20260324-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz" url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260414/cpython-3.14.4+20260414-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz"
provenance = "github-attestations" provenance = "github-attestations"
[tools.python."platforms.linux-arm64-musl"] [tools.python."platforms.linux-arm64-musl"]
checksum = "sha256:53700338695e402a1a1fe22be4a41fbdacc70e22bb308a48eca8ed67cb7992be" checksum = "sha256:b8b597fdb2f8dccdc502c11947b60a4b65eb6bce79cfa60c7ccf9b6e8352c60a"
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260324/cpython-3.14.3+20260324-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz" url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260414/cpython-3.14.4+20260414-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz"
provenance = "github-attestations" provenance = "github-attestations"
[tools.python."platforms.linux-x64"] [tools.python."platforms.linux-x64"]
checksum = "sha256:d7a9f970914bb4c88756fe3bdcc186d4feb90e9500e54f1db47dae4dc9687e39" checksum = "sha256:fe9a9c32d13870af632cbac3dfc7528ae53597e94472aa4c7d6a42e8166136cd"
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260324/cpython-3.14.3+20260324-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz" url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260414/cpython-3.14.4+20260414-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz"
provenance = "github-attestations" provenance = "github-attestations"
[tools.python."platforms.linux-x64-musl"] [tools.python."platforms.linux-x64-musl"]
checksum = "sha256:d7a9f970914bb4c88756fe3bdcc186d4feb90e9500e54f1db47dae4dc9687e39" checksum = "sha256:fe9a9c32d13870af632cbac3dfc7528ae53597e94472aa4c7d6a42e8166136cd"
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260324/cpython-3.14.3+20260324-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz" url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260414/cpython-3.14.4+20260414-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz"
provenance = "github-attestations" provenance = "github-attestations"
[tools.python."platforms.macos-arm64"] [tools.python."platforms.macos-arm64"]
checksum = "sha256:c43aecde4a663aebff99b9b83da0efec506479f1c3f98331442f33d2c43501f9" checksum = "blake3:0314ec66e0f33ec04959583b5900bc8edae371a396aa96b8874e750d1fe936e6"
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260324/cpython-3.14.3+20260324-aarch64-apple-darwin-install_only_stripped.tar.gz" url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260414/cpython-3.14.4+20260414-aarch64-apple-darwin-install_only_stripped.tar.gz"
provenance = "github-attestations" provenance = "github-attestations"
[tools.python."platforms.macos-x64"] [tools.python."platforms.macos-x64"]
checksum = "sha256:9ab41dbc2f100a2a45d1833b9c11165f51051c558b5213eda9a9731d5948a0c0" checksum = "sha256:d51250a32fa5d9f0799c7bcb71720c27b10a3afd4a7de288120f96085d508a5a"
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260324/cpython-3.14.3+20260324-x86_64-apple-darwin-install_only_stripped.tar.gz" url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260414/cpython-3.14.4+20260414-x86_64-apple-darwin-install_only_stripped.tar.gz"
provenance = "github-attestations" provenance = "github-attestations"
[tools.python."platforms.windows-x64"] [tools.python."platforms.windows-x64"]
checksum = "sha256:bbe19034b35b0267176a7442575ae7dc6343480fd4d35598cb7700173d431e09" checksum = "sha256:a976991dcd085c1bb5d9a8084823a6bc8b7f9b079d8c432574a6ddd68c3a6fe1"
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260324/cpython-3.14.3+20260324-x86_64-pc-windows-msvc-install_only_stripped.tar.gz" url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260414/cpython-3.14.4+20260414-x86_64-pc-windows-msvc-install_only_stripped.tar.gz"
provenance = "github-attestations" provenance = "github-attestations"
[[tools.rust]] [[tools.rust]]
version = "1.93.0" version = "1.95.0"
backend = "core:rust" backend = "core:rust"

View File

@@ -1,17 +1,17 @@
[tools] [tools]
"cargo:diesel_cli" = { version = "2.3.6", features = "sqlite,sqlite-bundled", default-features = false } "cargo:diesel_cli" = { version = "2.3.7", features = "sqlite,sqlite-bundled", default-features = "false" }
"cargo:cargo-audit" = "0.22.1" "cargo:cargo-audit" = "0.22.1"
"cargo:cargo-vet" = "0.10.2" "cargo:cargo-vet" = "0.10.2"
flutter = "3.38.9-stable" flutter = "3.41.7-stable"
protoc = "29.6" protoc = "29.6"
"rust" = {version = "1.93.0", components = "clippy,rust-analyzer"} rust = { version = "1.95.0", components = "clippy,rust-analyzer" }
"cargo:cargo-features-manager" = "0.11.1" "cargo:cargo-features-manager" = "0.12.0"
"cargo:cargo-nextest" = "0.9.126" "cargo:cargo-nextest" = "0.9.133"
"cargo:cargo-shear" = "latest" "cargo:cargo-shear" = "latest"
"cargo:cargo-insta" = "1.46.3" "cargo:cargo-insta" = "1.47.2"
python = "3.14.3" python = "3.14.4"
ast-grep = "0.42.0" ast-grep = "0.42.1"
"cargo:cargo-edit" = "0.13.9" "cargo:cargo-edit" = "0.13.10"
"cargo:cargo-mutants" = "27.0.0" "cargo:cargo-mutants" = "27.0.0"
"cargo:flutter_rust_bridge_codegen" = "2.12.0" "cargo:flutter_rust_bridge_codegen" = "2.12.0"

View File

@@ -3,7 +3,7 @@ syntax = "proto3";
package arbiter; package arbiter;
import "client.proto"; import "client.proto";
import "user_agent.proto"; import "operator.proto";
message ServerInfo { message ServerInfo {
string version = 1; string version = 1;
@@ -12,5 +12,5 @@ message ServerInfo {
service ArbiterService { service ArbiterService {
rpc Client(stream arbiter.client.ClientRequest) returns (stream arbiter.client.ClientResponse); rpc Client(stream arbiter.client.ClientRequest) returns (stream arbiter.client.ClientResponse);
rpc UserAgent(stream arbiter.user_agent.UserAgentRequest) returns (stream arbiter.user_agent.UserAgentResponse); rpc Operator(stream arbiter.operator.OperatorRequest) returns (stream arbiter.operator.OperatorResponse);
} }

View File

@@ -24,7 +24,7 @@ enum AuthResult {
AUTH_RESULT_INVALID_KEY = 2; AUTH_RESULT_INVALID_KEY = 2;
AUTH_RESULT_INVALID_SIGNATURE = 3; AUTH_RESULT_INVALID_SIGNATURE = 3;
AUTH_RESULT_APPROVAL_DENIED = 4; AUTH_RESULT_APPROVAL_DENIED = 4;
AUTH_RESULT_NO_USER_AGENTS_ONLINE = 5; AUTH_RESULT_NO_OPERATORS_ONLINE = 5;
AUTH_RESULT_INTERNAL = 6; AUTH_RESULT_INTERNAL = 6;
} }

View File

@@ -75,7 +75,7 @@ message SpecificGrant {
} }
} }
// --- UserAgent grant management --- // --- Operator grant management ---
message EvmGrantCreateRequest { message EvmGrantCreateRequest {
SharedSettings shared = 1; SharedSettings shared = 1;
SpecificGrant specific = 2; SpecificGrant specific = 2;

View File

@@ -1,13 +1,13 @@
syntax = "proto3"; syntax = "proto3";
package arbiter.user_agent; package arbiter.operator;
import "user_agent/auth.proto"; import "operator/auth.proto";
import "user_agent/evm.proto"; import "operator/evm.proto";
import "user_agent/sdk_client.proto"; import "operator/sdk_client.proto";
import "user_agent/vault/vault.proto"; import "operator/vault/vault.proto";
message UserAgentRequest { message OperatorRequest {
int32 id = 16; int32 id = 16;
oneof payload { oneof payload {
auth.Request auth = 1; auth.Request auth = 1;
@@ -17,7 +17,7 @@ message UserAgentRequest {
} }
} }
message UserAgentResponse { message OperatorResponse {
optional int32 id = 16; optional int32 id = 16;
oneof payload { oneof payload {
auth.Response auth = 1; auth.Response auth = 1;

View File

@@ -1,6 +1,6 @@
syntax = "proto3"; syntax = "proto3";
package arbiter.user_agent.auth; package arbiter.operator.auth;
message AuthChallengeRequest { message AuthChallengeRequest {
bytes pubkey = 1; bytes pubkey = 1;

View File

@@ -1,6 +1,6 @@
syntax = "proto3"; syntax = "proto3";
package arbiter.user_agent.evm; package arbiter.operator.evm;
import "evm.proto"; import "evm.proto";
import "google/protobuf/empty.proto"; import "google/protobuf/empty.proto";

View File

@@ -1,6 +1,6 @@
syntax = "proto3"; syntax = "proto3";
package arbiter.user_agent.sdk_client; package arbiter.operator.sdk_client;
import "shared/client.proto"; import "shared/client.proto";
import "google/protobuf/empty.proto"; import "google/protobuf/empty.proto";

View File

@@ -1,6 +1,6 @@
syntax = "proto3"; syntax = "proto3";
package arbiter.user_agent.vault.bootstrap; package arbiter.operator.vault.bootstrap;
message BootstrapEncryptedKey { message BootstrapEncryptedKey {
bytes nonce = 1; bytes nonce = 1;

View File

@@ -1,6 +1,6 @@
syntax = "proto3"; syntax = "proto3";
package arbiter.user_agent.vault.unseal; package arbiter.operator.vault.unseal;
message UnsealStart { message UnsealStart {
bytes client_pubkey = 1; bytes client_pubkey = 1;

View File

@@ -1,11 +1,11 @@
syntax = "proto3"; syntax = "proto3";
package arbiter.user_agent.vault; package arbiter.operator.vault;
import "google/protobuf/empty.proto"; import "google/protobuf/empty.proto";
import "shared/vault.proto"; import "shared/vault.proto";
import "user_agent/vault/bootstrap.proto"; import "operator/vault/bootstrap.proto";
import "user_agent/vault/unseal.proto"; import "operator/vault/unseal.proto";
message Request { message Request {
oneof payload { oneof payload {

View File

@@ -26,13 +26,13 @@ use chrono::DateTime;
pub enum AuthError { pub enum AuthError {
#[error("Server sent invalid auth challenge")] #[error("Server sent invalid auth challenge")]
InvalidChallenge, InvalidChallenge,
#[error("Client approval denied by User Agent")] #[error("Client approval denied by Operator")]
ApprovalDenied, ApprovalDenied,
#[error("Auth challenge was not returned by server")] #[error("Auth challenge was not returned by server")]
MissingAuthChallenge, MissingAuthChallenge,
#[error("No User Agents online to approve client")] #[error("No Operators online to approve client")]
NoUserAgentsOnline, NoOperatorsOnline,
#[error("Signing key storage error")] #[error("Signing key storage error")]
Storage(#[from] StorageError), Storage(#[from] StorageError),
@@ -44,7 +44,7 @@ pub enum AuthError {
fn map_auth_result(code: i32) -> AuthError { fn map_auth_result(code: i32) -> AuthError {
match AuthResult::try_from(code).unwrap_or(AuthResult::Unspecified) { match AuthResult::try_from(code).unwrap_or(AuthResult::Unspecified) {
AuthResult::ApprovalDenied => AuthError::ApprovalDenied, AuthResult::ApprovalDenied => AuthError::ApprovalDenied,
AuthResult::NoUserAgentsOnline => AuthError::NoUserAgentsOnline, AuthResult::NoOperatorsOnline => AuthError::NoOperatorsOnline,
AuthResult::Unspecified AuthResult::Unspecified
| AuthResult::Success | AuthResult::Success
| AuthResult::InvalidKey | AuthResult::InvalidKey

View File

@@ -7,7 +7,7 @@ use ml_dsa::{
use rand::RngExt; use rand::RngExt;
pub static CLIENT_CONTEXT: &[u8] = b"arbiter_client"; pub static CLIENT_CONTEXT: &[u8] = b"arbiter_client";
pub static USERAGENT_CONTEXT: &[u8] = b"arbiter_user_agent"; pub static OPERATOR_CONTEXT: &[u8] = b"arbiter_operator";
const NONCE_SIZE: usize = 32; const NONCE_SIZE: usize = 32;
@@ -192,7 +192,7 @@ mod tests {
use crate::authn::AuthChallenge; use crate::authn::AuthChallenge;
use super::{CLIENT_CONTEXT, PublicKey, Signature, SigningKey, USERAGENT_CONTEXT}; use super::{CLIENT_CONTEXT, PublicKey, Signature, SigningKey, OPERATOR_CONTEXT};
#[test] #[test]
fn public_key_round_trip_decodes() { fn public_key_round_trip_decodes() {
@@ -227,7 +227,7 @@ mod tests {
.expect("signature should be created"); .expect("signature should be created");
assert!(public_key.verify(&challenge, CLIENT_CONTEXT, &signature)); assert!(public_key.verify(&challenge, CLIENT_CONTEXT, &signature));
assert!(!public_key.verify(&challenge, USERAGENT_CONTEXT, &signature)); assert!(!public_key.verify(&challenge, OPERATOR_CONTEXT, &signature));
} }
#[test] #[test]

View File

@@ -10,7 +10,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
.compile_protos( .compile_protos(
&[ &[
format!("{}/arbiter.proto", PROTOBUF_DIR), format!("{}/arbiter.proto", PROTOBUF_DIR),
format!("{}/user_agent.proto", PROTOBUF_DIR), format!("{}/operator.proto", PROTOBUF_DIR),
format!("{}/client.proto", PROTOBUF_DIR), format!("{}/client.proto", PROTOBUF_DIR),
format!("{}/evm.proto", PROTOBUF_DIR), format!("{}/evm.proto", PROTOBUF_DIR),
], ],

View File

@@ -12,30 +12,30 @@ pub mod proto {
} }
} }
pub mod user_agent { pub mod operator {
tonic::include_proto!("arbiter.user_agent"); tonic::include_proto!("arbiter.operator");
pub mod auth { pub mod auth {
tonic::include_proto!("arbiter.user_agent.auth"); tonic::include_proto!("arbiter.operator.auth");
} }
pub mod evm { pub mod evm {
tonic::include_proto!("arbiter.user_agent.evm"); tonic::include_proto!("arbiter.operator.evm");
} }
pub mod sdk_client { pub mod sdk_client {
tonic::include_proto!("arbiter.user_agent.sdk_client"); tonic::include_proto!("arbiter.operator.sdk_client");
} }
pub mod vault { pub mod vault {
tonic::include_proto!("arbiter.user_agent.vault"); tonic::include_proto!("arbiter.operator.vault");
pub mod bootstrap { pub mod bootstrap {
tonic::include_proto!("arbiter.user_agent.vault.bootstrap"); tonic::include_proto!("arbiter.operator.vault.bootstrap");
} }
pub mod unseal { pub mod unseal {
tonic::include_proto!("arbiter.user_agent.vault.unseal"); tonic::include_proto!("arbiter.operator.vault.unseal");
} }
} }
} }

View File

@@ -43,13 +43,13 @@ create table if not exists arbiter_settings (
insert into arbiter_settings (id) values (1) on conflict do nothing; insert into arbiter_settings (id) values (1) on conflict do nothing;
-- ensure singleton row exists -- ensure singleton row exists
create table if not exists useragent_client ( create table if not exists operator_client (
id integer not null primary key, id integer not null primary key,
public_key blob not null, public_key blob not null,
created_at integer not null default(unixepoch ('now')), created_at integer not null default(unixepoch ('now')),
updated_at integer not null default(unixepoch ('now')) updated_at integer not null default(unixepoch ('now'))
) STRICT; ) STRICT;
create unique index if not exists uniq_useragent_client_public_key on useragent_client (public_key); create unique index if not exists uniq_operator_client_public_key on operator_client (public_key);
create table if not exists client_metadata ( create table if not exists client_metadata (
id integer not null primary key, id integer not null primary key,

View File

@@ -48,7 +48,7 @@ impl Bootstrapper {
let row_count: i64 = { let row_count: i64 = {
let mut conn = db.get().await?; let mut conn = db.get().await?;
schema::useragent_client::table schema::operator_client::table
.count() .count()
.get_result(&mut conn) .get_result(&mut conn)
.await? .await?

View File

@@ -134,7 +134,7 @@ impl EvmActor {
#[messages] #[messages]
impl EvmActor { impl EvmActor {
#[message] #[message]
pub async fn useragent_create_grant( pub async fn operator_create_grant(
&mut self, &mut self,
basic: SharedGrantSettings, basic: SharedGrantSettings,
grant: SpecificGrant, grant: SpecificGrant,
@@ -161,7 +161,7 @@ impl EvmActor {
#[message] #[message]
#[expect(clippy::unused_async, reason = "reserved for impl")] #[expect(clippy::unused_async, reason = "reserved for impl")]
pub async fn useragent_delete_grant(&mut self, _grant_id: i32) -> Result<(), Error> { pub async fn operator_delete_grant(&mut self, _grant_id: i32) -> Result<(), Error> {
// let mut conn = self.db.get().await.map_err(DatabaseError::from)?; // let mut conn = self.db.get().await.map_err(DatabaseError::from)?;
// let vault = self.vault.clone(); // let vault = self.vault.clone();
@@ -186,7 +186,7 @@ impl EvmActor {
} }
#[message] #[message]
pub async fn useragent_list_grants(&mut self) -> Result<Vec<Grant<SpecificGrant>>, Error> { pub async fn operator_list_grants(&mut self) -> Result<Vec<Grant<SpecificGrant>>, Error> {
match self.engine.list_all_grants().await { match self.engine.list_all_grants().await {
Ok(grants) => Ok(grants), Ok(grants) => Ok(grants),
Err(ListError::Database(db_err)) => Err(Error::Database(db_err)), Err(ListError::Database(db_err)) => Err(Error::Database(db_err)),

View File

@@ -2,7 +2,7 @@ use crate::{
actors::flow_coordinator::ApprovalError, actors::flow_coordinator::ApprovalError,
peers::{ peers::{
client::ClientProfile, client::ClientProfile,
user_agent::{UserAgentSession, session::BeginNewClientApproval}, operator::{OperatorSession, session::BeginNewClientApproval},
}, },
}; };
@@ -15,12 +15,12 @@ use std::ops::ControlFlow;
pub struct Args { pub struct Args {
pub client: ClientProfile, pub client: ClientProfile,
pub user_agents: Vec<ActorRef<UserAgentSession>>, pub operators: Vec<ActorRef<OperatorSession>>,
pub reply: ReplySender<Result<bool, ApprovalError>>, pub reply: ReplySender<Result<bool, ApprovalError>>,
} }
pub struct ClientApprovalController { pub struct ClientApprovalController {
/// Number of UAs that have not yet responded (approval or denial) or died. /// Number of operators that have not yet responded (approval or denial) or died.
pending: usize, pending: usize,
/// Number of approvals received so far. /// Number of approvals received so far.
approved: usize, approved: usize,
@@ -42,21 +42,21 @@ impl Actor for ClientApprovalController {
async fn on_start( async fn on_start(
Args { Args {
client, client,
user_agents, operators,
reply, reply,
}: Self::Args, }: Self::Args,
actor_ref: ActorRef<Self>, actor_ref: ActorRef<Self>,
) -> Result<Self, Self::Error> { ) -> Result<Self, Self::Error> {
let this = Self { let this = Self {
pending: user_agents.len(), pending: operators.len(),
approved: 0, approved: 0,
reply: Some(reply), reply: Some(reply),
}; };
for user_agent in user_agents { for operator in operators {
actor_ref.link(&user_agent).await; actor_ref.link(&operator).await;
let _ = user_agent let _ = operator
.tell(BeginNewClientApproval { .tell(BeginNewClientApproval {
client: client.clone(), client: client.clone(),
controller: actor_ref.clone(), controller: actor_ref.clone(),
@@ -73,10 +73,10 @@ impl Actor for ClientApprovalController {
_: ActorId, _: ActorId,
_: ActorStopReason, _: ActorStopReason,
) -> Result<ControlFlow<ActorStopReason>, Self::Error> { ) -> Result<ControlFlow<ActorStopReason>, Self::Error> {
// A linked UA died before responding — counts as a non-approval. // A linked operator died before responding — counts as a non-approval.
self.pending = self.pending.saturating_sub(1); self.pending = self.pending.saturating_sub(1);
if self.pending == 0 { if self.pending == 0 {
// At least one UA didn't approve: deny. // At least one operator didn't approve: deny.
self.send_reply(Ok(false)); self.send_reply(Ok(false));
return Ok(ControlFlow::Break(ActorStopReason::Normal)); return Ok(ControlFlow::Break(ActorStopReason::Normal));
} }
@@ -99,7 +99,7 @@ impl ClientApprovalController {
self.pending = self.pending.saturating_sub(1); self.pending = self.pending.saturating_sub(1);
if self.pending == 0 { if self.pending == 0 {
// Every connected UA approved. // Every connected operator approved.
self.send_reply(Ok(true)); self.send_reply(Ok(true));
ctx.stop(); ctx.stop();
} }

View File

@@ -1,7 +1,7 @@
use crate::{ use crate::{
actors::{ actors::{
flow_coordinator::client_connect_approval::ClientApprovalController, flow_coordinator::client_connect_approval::ClientApprovalController,
useragent_registry::{GetConnected, UserAgentRegistry}, operator_registry::{GetConnected, OperatorRegistry},
}, },
peers::client::{ClientProfile, session::ClientSession}, peers::client::{ClientProfile, session::ClientSession},
}; };
@@ -20,14 +20,14 @@ pub mod client_connect_approval;
pub struct FlowCoordinator { pub struct FlowCoordinator {
pub clients: HashMap<ActorId, ActorRef<ClientSession>>, pub clients: HashMap<ActorId, ActorRef<ClientSession>>,
useragent_registry: ActorRef<UserAgentRegistry>, operator_registry: ActorRef<OperatorRegistry>,
} }
impl FlowCoordinator { impl FlowCoordinator {
pub fn new(useragent_registry: ActorRef<UserAgentRegistry>) -> Self { pub fn new(operator_registry: ActorRef<OperatorRegistry>) -> Self {
Self { Self {
clients: HashMap::default(), clients: HashMap::default(),
useragent_registry, operator_registry,
} }
} }
} }
@@ -66,8 +66,8 @@ impl Actor for FlowCoordinator {
#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq, Hash)] #[derive(Debug, thiserror::Error, Clone, PartialEq, Eq, Hash)]
pub enum ApprovalError { pub enum ApprovalError {
#[error("No user agents connected")] #[error("No operators connected")]
NoUserAgentsConnected, NoOperatorsConnected,
} }
#[messages] #[messages]
@@ -93,19 +93,19 @@ impl FlowCoordinator {
unreachable!("Expected `request_client_approval` to have callback channel"); unreachable!("Expected `request_client_approval` to have callback channel");
}; };
let Ok(refs) = self.useragent_registry.ask(GetConnected).await else { let Ok(refs) = self.operator_registry.ask(GetConnected).await else {
reply_sender.send(Err(ApprovalError::NoUserAgentsConnected)); reply_sender.send(Err(ApprovalError::NoOperatorsConnected));
return reply; return reply;
}; };
if refs.is_empty() { if refs.is_empty() {
reply_sender.send(Err(ApprovalError::NoUserAgentsConnected)); reply_sender.send(Err(ApprovalError::NoOperatorsConnected));
return reply; return reply;
} }
ClientApprovalController::spawn(client_connect_approval::Args { ClientApprovalController::spawn(client_connect_approval::Args {
client, client,
user_agents: refs, operators: refs,
reply: reply_sender, reply: reply_sender,
}); });

View File

@@ -1,7 +1,7 @@
use crate::{ use crate::{
actors::{ actors::{
bootstrap::Bootstrapper, evm::EvmActor, flow_coordinator::FlowCoordinator, bootstrap::Bootstrapper, evm::EvmActor, flow_coordinator::FlowCoordinator,
useragent_registry::UserAgentRegistry, vault::Vault, operator_registry::OperatorRegistry, vault::Vault,
}, },
db, db,
}; };
@@ -13,7 +13,7 @@ use thiserror::Error;
pub mod bootstrap; pub mod bootstrap;
pub mod evm; pub mod evm;
pub mod flow_coordinator; pub mod flow_coordinator;
pub mod useragent_registry; pub mod operator_registry;
pub mod vault; pub mod vault;
#[derive(Error, Debug)] #[derive(Error, Debug)]
@@ -31,7 +31,7 @@ pub struct GlobalActors {
pub vault: ActorRef<Vault>, pub vault: ActorRef<Vault>,
pub bootstrapper: ActorRef<Bootstrapper>, pub bootstrapper: ActorRef<Bootstrapper>,
pub flow_coordinator: ActorRef<FlowCoordinator>, pub flow_coordinator: ActorRef<FlowCoordinator>,
pub useragent_registry: ActorRef<UserAgentRegistry>, pub operator_registry: ActorRef<OperatorRegistry>,
pub evm: ActorRef<EvmActor>, pub evm: ActorRef<EvmActor>,
pub events: ActorRef<MessageBus>, pub events: ActorRef<MessageBus>,
} }
@@ -44,15 +44,15 @@ impl GlobalActors {
pub async fn spawn(db: db::DatabasePool) -> Result<Self, SpawnError> { pub async fn spawn(db: db::DatabasePool) -> Result<Self, SpawnError> {
let message_bus = Self::spawn_message_bus(); let message_bus = Self::spawn_message_bus();
let key_holder = Vault::spawn(Vault::new(db.clone(), message_bus.clone()).await?); let key_holder = Vault::spawn(Vault::new(db.clone(), message_bus.clone()).await?);
let useragent_registry = UserAgentRegistry::spawn(UserAgentRegistry::default()); let operator_registry = OperatorRegistry::spawn(OperatorRegistry::default());
Ok(Self { Ok(Self {
bootstrapper: Bootstrapper::spawn(Bootstrapper::new(&db).await?), bootstrapper: Bootstrapper::spawn(Bootstrapper::new(&db).await?),
evm: EvmActor::spawn(EvmActor::new(key_holder.clone(), db)), evm: EvmActor::spawn(EvmActor::new(key_holder.clone(), db)),
vault: key_holder, vault: key_holder,
flow_coordinator: FlowCoordinator::spawn(FlowCoordinator::new( flow_coordinator: FlowCoordinator::spawn(FlowCoordinator::new(
useragent_registry.clone(), operator_registry.clone(),
)), )),
useragent_registry, operator_registry,
events: message_bus, events: message_bus,
}) })
} }

View File

@@ -1,4 +1,4 @@
use crate::peers::user_agent::UserAgentSession; use crate::peers::operator::OperatorSession;
use kameo::{ use kameo::{
Actor, Actor,
@@ -11,11 +11,11 @@ use std::{collections::HashMap, ops::ControlFlow};
use tracing::info; use tracing::info;
#[derive(Default)] #[derive(Default)]
pub struct UserAgentRegistry { pub struct OperatorRegistry {
connected: HashMap<ActorId, ActorRef<UserAgentSession>>, connected: HashMap<ActorId, ActorRef<OperatorSession>>,
} }
impl Actor for UserAgentRegistry { impl Actor for OperatorRegistry {
type Args = Self; type Args = Self;
type Error = Infallible; type Error = Infallible;
@@ -33,8 +33,8 @@ impl Actor for UserAgentRegistry {
if self.connected.remove(&id).is_some() { if self.connected.remove(&id).is_some() {
info!( info!(
?id, ?id,
actor = "UserAgentRegistry", actor = "OperatorRegistry",
event = "useragent.disconnected" event = "operator.disconnected"
); );
} }
Ok(ControlFlow::Continue(())) Ok(ControlFlow::Continue(()))
@@ -42,20 +42,20 @@ impl Actor for UserAgentRegistry {
} }
#[messages] #[messages]
impl UserAgentRegistry { impl OperatorRegistry {
#[message(ctx)] #[message(ctx)]
pub async fn connect_useragent( pub async fn connect_operator(
&mut self, &mut self,
actor: ActorRef<UserAgentSession>, actor: ActorRef<OperatorSession>,
ctx: &mut Context<Self, ()>, ctx: &mut Context<Self, ()>,
) { ) {
info!(id = %actor.id(), actor = "UserAgentRegistry", event = "useragent.connected"); info!(id = %actor.id(), actor = "OperatorRegistry", event = "operator.connected");
ctx.actor_ref().link(&actor).await; ctx.actor_ref().link(&actor).await;
self.connected.insert(actor.id(), actor); self.connected.insert(actor.id(), actor);
} }
#[message] #[message]
pub fn get_connected(&self) -> Vec<ActorRef<UserAgentSession>> { pub fn get_connected(&self) -> Vec<ActorRef<OperatorSession>> {
self.connected.values().cloned().collect() self.connected.values().cloned().collect()
} }
} }

View File

@@ -248,8 +248,8 @@ pub struct ProgramClient {
} }
#[derive(Queryable, Debug)] #[derive(Queryable, Debug)]
#[diesel(table_name = schema::useragent_client, check_for_backend(Sqlite))] #[diesel(table_name = schema::operator_client, check_for_backend(Sqlite))]
pub struct UseragentClient { pub struct OperatorClient {
pub id: i32, pub id: i32,
pub public_key: Vec<u8>, pub public_key: Vec<u8>,
pub created_at: SqliteTimestamp, pub created_at: SqliteTimestamp,

View File

@@ -186,7 +186,7 @@ diesel::table! {
} }
diesel::table! { diesel::table! {
useragent_client (id) { operator_client (id) {
id -> Integer, id -> Integer,
public_key -> Binary, public_key -> Binary,
created_at -> Integer, created_at -> Integer,
@@ -233,5 +233,5 @@ diesel::allow_tables_to_appear_in_same_query!(
program_client, program_client,
root_key_history, root_key_history,
tls_history, tls_history,
useragent_client, operator_client,
); );

View File

@@ -176,8 +176,8 @@ impl Convert for auth::Error {
InvalidChallengeSolution => ProtoAuthResult::InvalidSignature, InvalidChallengeSolution => ProtoAuthResult::InvalidSignature,
ApproveError(auth::ApproveError::Denied) => ProtoAuthResult::ApprovalDenied, ApproveError(auth::ApproveError::Denied) => ProtoAuthResult::ApprovalDenied,
ApproveError(auth::ApproveError::Upstream( ApproveError(auth::ApproveError::Upstream(
crate::actors::flow_coordinator::ApprovalError::NoUserAgentsConnected, crate::actors::flow_coordinator::ApprovalError::NoOperatorsConnected,
)) => ProtoAuthResult::NoUserAgentsOnline, )) => ProtoAuthResult::NoOperatorsOnline,
ApproveError(auth::ApproveError::Internal) ApproveError(auth::ApproveError::Internal)
| DatabasePoolUnavailable | DatabasePoolUnavailable
| DatabaseOperationFailed | DatabaseOperationFailed

View File

@@ -1,8 +1,8 @@
use crate::peers::{client::ClientConnection, user_agent::UserAgentConnection}; use crate::peers::{client::ClientConnection, operator::OperatorConnection};
use arbiter_proto::{ use arbiter_proto::{
proto::{ proto::{
client::{ClientRequest, ClientResponse}, client::{ClientRequest, ClientResponse},
user_agent::{UserAgentRequest, UserAgentResponse}, operator::{OperatorRequest, OperatorResponse},
}, },
transport::grpc::GrpcBi, transport::grpc::GrpcBi,
}; };
@@ -14,7 +14,7 @@ use tracing::info;
mod request_tracker; mod request_tracker;
pub mod client; pub mod client;
pub mod user_agent; pub mod operator;
mod common; mod common;
@@ -33,7 +33,7 @@ pub trait TryConvert {
#[async_trait] #[async_trait]
impl arbiter_proto::proto::arbiter_service_server::ArbiterService for super::Server { impl arbiter_proto::proto::arbiter_service_server::ArbiterService for super::Server {
type UserAgentStream = ReceiverStream<Result<UserAgentResponse, Status>>; type OperatorStream = ReceiverStream<Result<OperatorResponse, Status>>;
type ClientStream = ReceiverStream<Result<ClientResponse, Status>>; type ClientStream = ReceiverStream<Result<ClientResponse, Status>>;
#[tracing::instrument(level = "debug", skip(self))] #[tracing::instrument(level = "debug", skip(self))]
@@ -52,23 +52,23 @@ impl arbiter_proto::proto::arbiter_service_server::ArbiterService for super::Ser
} }
#[tracing::instrument(level = "debug", skip(self))] #[tracing::instrument(level = "debug", skip(self))]
async fn user_agent( async fn operator(
&self, &self,
request: Request<tonic::Streaming<UserAgentRequest>>, request: Request<tonic::Streaming<OperatorRequest>>,
) -> Result<Response<Self::UserAgentStream>, Status> { ) -> Result<Response<Self::OperatorStream>, Status> {
let req_stream = request.into_inner(); let req_stream = request.into_inner();
let (bi, rx) = GrpcBi::from_bi_stream(req_stream); let (bi, rx) = GrpcBi::from_bi_stream(req_stream);
tokio::spawn(user_agent::start( tokio::spawn(operator::start(
UserAgentConnection { OperatorConnection {
db: self.context.db.clone(), db: self.context.db.clone(),
actors: self.context.actors.clone(), actors: self.context.actors.clone(),
}, },
bi, bi,
)); ));
info!(event = "connection established", "grpc.user_agent"); info!(event = "connection established", "grpc.operator");
Ok(Response::new(rx)) Ok(Response::new(rx))
} }

View File

@@ -1,12 +1,12 @@
use crate::{ use crate::{
grpc::request_tracker::RequestTracker, grpc::request_tracker::RequestTracker,
peers::user_agent::{OutOfBand, UserAgentConnection, UserAgentSession}, peers::operator::{OutOfBand, OperatorConnection, OperatorSession},
}; };
use arbiter_proto::{ use arbiter_proto::{
proto::user_agent::{ proto::operator::{
UserAgentRequest, UserAgentResponse, OperatorRequest, OperatorResponse,
user_agent_request::Payload as UserAgentRequestPayload, operator_request::Payload as OperatorRequestPayload,
user_agent_response::Payload as UserAgentResponsePayload, operator_response::Payload as OperatorResponsePayload,
}, },
transport::{Error as TransportError, Receiver, Sender, grpc::GrpcBi}, transport::{Error as TransportError, Receiver, Sender, grpc::GrpcBi},
}; };
@@ -38,8 +38,8 @@ impl Sender<OutOfBand> for OutOfBandAdapter {
} }
async fn dispatch_loop( async fn dispatch_loop(
mut bi: GrpcBi<UserAgentRequest, UserAgentResponse>, mut bi: GrpcBi<OperatorRequest, OperatorResponse>,
actor: ActorRef<UserAgentSession>, actor: ActorRef<OperatorSession>,
mut receiver: mpsc::Receiver<OutOfBand>, mut receiver: mpsc::Receiver<OutOfBand>,
mut request_tracker: RequestTracker, mut request_tracker: RequestTracker,
) { ) {
@@ -53,7 +53,7 @@ async fn dispatch_loop(
let payload = sdk_client::out_of_band_payload(oob); let payload = sdk_client::out_of_band_payload(oob);
if bi.send(Ok(UserAgentResponse { id: None, payload: Some(payload) })).await.is_err() { if bi.send(Ok(OperatorResponse { id: None, payload: Some(payload) })).await.is_err() {
return; return;
} }
} }
@@ -64,7 +64,7 @@ async fn dispatch_loop(
let conn = match message { let conn = match message {
Ok(conn) => conn, Ok(conn) => conn,
Err(err) => { Err(err) => {
warn!(error = ?err, "Failed to receive user agent request"); warn!(error = ?err, "Failed to receive operator request");
return; return;
} }
}; };
@@ -78,13 +78,13 @@ async fn dispatch_loop(
}; };
let Some(payload) = conn.payload else { let Some(payload) = conn.payload else {
let _ = bi.send(Err(Status::invalid_argument("Missing user-agent request payload"))).await; let _ = bi.send(Err(Status::invalid_argument("Missing operator request payload"))).await;
return; return;
}; };
match dispatch_inner(&actor, payload).await { match dispatch_inner(&actor, payload).await {
Ok(Some(response)) => { Ok(Some(response)) => {
if bi.send(Ok(UserAgentResponse { if bi.send(Ok(OperatorResponse {
id: Some(request_id), id: Some(request_id),
payload: Some(response), payload: Some(response),
})).await.is_err() { })).await.is_err() {
@@ -93,7 +93,7 @@ async fn dispatch_loop(
} }
Ok(None) => {} Ok(None) => {}
Err(status) => { Err(status) => {
error!(?status, "Failed to process user agent request"); error!(?status, "Failed to process operator request");
let _ = bi.send(Err(status)).await; let _ = bi.send(Err(status)).await;
return; return;
} }
@@ -104,23 +104,23 @@ async fn dispatch_loop(
} }
async fn dispatch_inner( async fn dispatch_inner(
actor: &ActorRef<UserAgentSession>, actor: &ActorRef<OperatorSession>,
payload: UserAgentRequestPayload, payload: OperatorRequestPayload,
) -> Result<Option<UserAgentResponsePayload>, Status> { ) -> Result<Option<OperatorResponsePayload>, Status> {
match payload { match payload {
UserAgentRequestPayload::Vault(req) => vault::dispatch(actor, req).await, OperatorRequestPayload::Vault(req) => vault::dispatch(actor, req).await,
UserAgentRequestPayload::Evm(req) => evm::dispatch(actor, req).await, OperatorRequestPayload::Evm(req) => evm::dispatch(actor, req).await,
UserAgentRequestPayload::SdkClient(req) => sdk_client::dispatch(actor, req).await, OperatorRequestPayload::SdkClient(req) => sdk_client::dispatch(actor, req).await,
UserAgentRequestPayload::Auth(..) => { OperatorRequestPayload::Auth(..) => {
warn!("Unsupported post-auth user agent auth request"); warn!("Unsupported post-auth operator auth request");
Err(Status::invalid_argument("Unsupported user-agent request")) Err(Status::invalid_argument("Unsupported operator request"))
} }
} }
} }
pub async fn start( pub async fn start(
mut conn: UserAgentConnection, mut conn: OperatorConnection,
mut bi: GrpcBi<UserAgentRequest, UserAgentResponse>, mut bi: GrpcBi<OperatorRequest, OperatorResponse>,
) { ) {
let mut request_tracker = RequestTracker::default(); let mut request_tracker = RequestTracker::default();
@@ -129,16 +129,16 @@ pub async fn start(
let actor = { let actor = {
let transport = auth::AuthTransportAdapter::new(&mut bi, &mut request_tracker); let transport = auth::AuthTransportAdapter::new(&mut bi, &mut request_tracker);
match crate::peers::user_agent::start(&mut conn, transport, Box::new(oob_adapter)).await { match crate::peers::operator::start(&mut conn, transport, Box::new(oob_adapter)).await {
Ok(actor) => actor, Ok(actor) => actor,
Err(e) => { Err(e) => {
warn!(error = ?e, "User agent connection failed"); warn!(error = ?e, "Operator connection failed");
return; return;
} }
} }
}; };
info!("User agent session established"); info!("Operator session established");
dispatch_loop(bi, actor.clone(), oob_receiver, request_tracker).await; dispatch_loop(bi, actor.clone(), oob_receiver, request_tracker).await;
actor.kill(); actor.kill();

View File

@@ -1,16 +1,16 @@
use crate::{grpc::request_tracker::RequestTracker, peers::user_agent::auth}; use crate::{grpc::request_tracker::RequestTracker, peers::operator::auth};
use arbiter_crypto::authn; use arbiter_crypto::authn;
use arbiter_proto::{ use arbiter_proto::{
proto::user_agent::{ proto::operator::{
UserAgentRequest, UserAgentResponse, OperatorRequest, OperatorResponse,
auth::{ auth::{
self as proto_auth, AuthChallenge as ProtoAuthChallenge, self as proto_auth, AuthChallenge as ProtoAuthChallenge,
AuthChallengeRequest as ProtoAuthChallengeRequest, AuthChallengeRequest as ProtoAuthChallengeRequest,
AuthChallengeSolution as ProtoAuthChallengeSolution, AuthResult as ProtoAuthResult, AuthChallengeSolution as ProtoAuthChallengeSolution, AuthResult as ProtoAuthResult,
request::Payload as AuthRequestPayload, response::Payload as AuthResponsePayload, request::Payload as AuthRequestPayload, response::Payload as AuthResponsePayload,
}, },
user_agent_request::Payload as UserAgentRequestPayload, operator_request::Payload as OperatorRequestPayload,
user_agent_response::Payload as UserAgentResponsePayload, operator_response::Payload as OperatorResponsePayload,
}, },
transport::{Bi, Error as TransportError, Receiver, Sender, grpc::GrpcBi}, transport::{Bi, Error as TransportError, Receiver, Sender, grpc::GrpcBi},
}; };
@@ -20,13 +20,13 @@ use tonic::Status;
use tracing::warn; use tracing::warn;
pub(super) struct AuthTransportAdapter<'a> { pub(super) struct AuthTransportAdapter<'a> {
pub(super) bi: &'a mut GrpcBi<UserAgentRequest, UserAgentResponse>, pub(super) bi: &'a mut GrpcBi<OperatorRequest, OperatorResponse>,
pub(super) request_tracker: &'a mut RequestTracker, pub(super) request_tracker: &'a mut RequestTracker,
} }
impl<'a> AuthTransportAdapter<'a> { impl<'a> AuthTransportAdapter<'a> {
pub(super) const fn new( pub(super) const fn new(
bi: &'a mut GrpcBi<UserAgentRequest, UserAgentResponse>, bi: &'a mut GrpcBi<OperatorRequest, OperatorResponse>,
request_tracker: &'a mut RequestTracker, request_tracker: &'a mut RequestTracker,
) -> Self { ) -> Self {
Self { Self {
@@ -35,7 +35,7 @@ impl<'a> AuthTransportAdapter<'a> {
} }
} }
pub(super) const fn bi_mut(&mut self) -> &mut GrpcBi<UserAgentRequest, UserAgentResponse> { pub(super) const fn bi_mut(&mut self) -> &mut GrpcBi<OperatorRequest, OperatorResponse> {
self.bi self.bi
} }
@@ -45,21 +45,21 @@ impl<'a> AuthTransportAdapter<'a> {
pub(super) async fn send_response_payload( pub(super) async fn send_response_payload(
&mut self, &mut self,
payload: UserAgentResponsePayload, payload: OperatorResponsePayload,
) -> Result<(), TransportError> { ) -> Result<(), TransportError> {
self.bi self.bi
.send(Ok(UserAgentResponse { .send(Ok(OperatorResponse {
id: Some(self.request_tracker.current_request_id()), id: Some(self.request_tracker.current_request_id()),
payload: Some(payload), payload: Some(payload),
})) }))
.await .await
} }
async fn send_user_agent_response( async fn send_operator_response(
&mut self, &mut self,
payload: AuthResponsePayload, payload: AuthResponsePayload,
) -> Result<(), TransportError> { ) -> Result<(), TransportError> {
self.send_response_payload(UserAgentResponsePayload::Auth(proto_auth::Response { self.send_response_payload(OperatorResponsePayload::Auth(proto_auth::Response {
payload: Some(payload), payload: Some(payload),
})) }))
.await .await
@@ -107,7 +107,7 @@ impl Sender<Result<auth::Outbound, auth::Error>> for AuthTransportAdapter<'_> {
} }
}; };
self.send_user_agent_response(payload).await self.send_operator_response(payload).await
} }
} }
@@ -117,7 +117,7 @@ impl Receiver<auth::Inbound> for AuthTransportAdapter<'_> {
let request = match self.bi.recv().await? { let request = match self.bi.recv().await? {
Ok(request) => request, Ok(request) => request,
Err(error) => { Err(error) => {
warn!(error = ?error, "Failed to receive user agent auth request"); warn!(error = ?error, "Failed to receive operator auth request");
return None; return None;
} }
}; };
@@ -133,16 +133,16 @@ impl Receiver<auth::Inbound> for AuthTransportAdapter<'_> {
let Some(payload) = request.payload else { let Some(payload) = request.payload else {
warn!( warn!(
event = "received request with empty payload", event = "received request with empty payload",
"grpc.useragent.auth_adapter" "grpc.operator.auth_adapter"
); );
return None; return None;
}; };
let UserAgentRequestPayload::Auth(auth_request) = payload else { let OperatorRequestPayload::Auth(auth_request) = payload else {
let _ = self let _ = self
.bi .bi
.send(Err(Status::invalid_argument( .send(Err(Status::invalid_argument(
"Unsupported user-agent auth request", "Unsupported operator auth request",
))) )))
.await; .await;
return None; return None;
@@ -151,7 +151,7 @@ impl Receiver<auth::Inbound> for AuthTransportAdapter<'_> {
let Some(payload) = auth_request.payload else { let Some(payload) = auth_request.payload else {
warn!( warn!(
event = "received auth request with empty payload", event = "received auth request with empty payload",
"grpc.useragent.auth_adapter" "grpc.operator.auth_adapter"
); );
return None; return None;
}; };
@@ -164,7 +164,7 @@ impl Receiver<auth::Inbound> for AuthTransportAdapter<'_> {
let Ok(pubkey) = authn::PublicKey::try_from(pubkey.as_slice()) else { let Ok(pubkey) = authn::PublicKey::try_from(pubkey.as_slice()) else {
warn!( warn!(
event = "received request with invalid public key", event = "received request with invalid public key",
"grpc.useragent.auth_adapter" "grpc.operator.auth_adapter"
); );
return None; return None;
}; };

View File

@@ -3,8 +3,8 @@ use crate::{
Convert, TryConvert, Convert, TryConvert,
common::inbound::{RawEvmAddress, RawEvmTransaction}, common::inbound::{RawEvmAddress, RawEvmTransaction},
}, },
peers::user_agent::{ peers::operator::{
UserAgentSession, OperatorSession,
session::handlers::{ session::handlers::{
GrantMutationError, HandleEvmWalletCreate, HandleEvmWalletList, HandleGrantCreate, GrantMutationError, HandleEvmWalletCreate, HandleEvmWalletList, HandleGrantCreate,
HandleGrantDelete, HandleGrantList, HandleSignTransaction, HandleGrantDelete, HandleGrantList, HandleSignTransaction,
@@ -24,12 +24,12 @@ use arbiter_proto::proto::{
wallet_create_response::Result as WalletCreateResult, wallet_create_response::Result as WalletCreateResult,
wallet_list_response::Result as WalletListResult, wallet_list_response::Result as WalletListResult,
}, },
user_agent::{ operator::{
evm::{ evm::{
self as proto_evm, SignTransactionRequest as ProtoSignTransactionRequest, self as proto_evm, SignTransactionRequest as ProtoSignTransactionRequest,
request::Payload as EvmRequestPayload, response::Payload as EvmResponsePayload, request::Payload as EvmRequestPayload, response::Payload as EvmResponsePayload,
}, },
user_agent_response::Payload as UserAgentResponsePayload, operator_response::Payload as OperatorResponsePayload,
}, },
}; };
@@ -37,16 +37,16 @@ use kameo::actor::ActorRef;
use tonic::Status; use tonic::Status;
use tracing::warn; use tracing::warn;
const fn wrap_evm_response(payload: EvmResponsePayload) -> UserAgentResponsePayload { const fn wrap_evm_response(payload: EvmResponsePayload) -> OperatorResponsePayload {
UserAgentResponsePayload::Evm(proto_evm::Response { OperatorResponsePayload::Evm(proto_evm::Response {
payload: Some(payload), payload: Some(payload),
}) })
} }
pub(super) async fn dispatch( pub(super) async fn dispatch(
actor: &ActorRef<UserAgentSession>, actor: &ActorRef<OperatorSession>,
req: proto_evm::Request, req: proto_evm::Request,
) -> Result<Option<UserAgentResponsePayload>, Status> { ) -> Result<Option<OperatorResponsePayload>, Status> {
let Some(payload) = req.payload else { let Some(payload) = req.payload else {
return Err(Status::invalid_argument("Missing EVM request payload")); return Err(Status::invalid_argument("Missing EVM request payload"));
}; };
@@ -62,8 +62,8 @@ pub(super) async fn dispatch(
} }
async fn handle_wallet_create( async fn handle_wallet_create(
actor: &ActorRef<UserAgentSession>, actor: &ActorRef<OperatorSession>,
) -> Result<Option<UserAgentResponsePayload>, Status> { ) -> Result<Option<OperatorResponsePayload>, Status> {
let result = match actor.ask(HandleEvmWalletCreate {}).await { let result = match actor.ask(HandleEvmWalletCreate {}).await {
Ok((wallet_id, address)) => WalletCreateResult::Wallet(WalletEntry { Ok((wallet_id, address)) => WalletCreateResult::Wallet(WalletEntry {
id: wallet_id, id: wallet_id,
@@ -82,8 +82,8 @@ async fn handle_wallet_create(
} }
async fn handle_wallet_list( async fn handle_wallet_list(
actor: &ActorRef<UserAgentSession>, actor: &ActorRef<OperatorSession>,
) -> Result<Option<UserAgentResponsePayload>, Status> { ) -> Result<Option<OperatorResponsePayload>, Status> {
let result = match actor.ask(HandleEvmWalletList {}).await { let result = match actor.ask(HandleEvmWalletList {}).await {
Ok(wallets) => WalletListResult::Wallets(WalletList { Ok(wallets) => WalletListResult::Wallets(WalletList {
wallets: wallets wallets: wallets
@@ -107,8 +107,8 @@ async fn handle_wallet_list(
} }
async fn handle_grant_list( async fn handle_grant_list(
actor: &ActorRef<UserAgentSession>, actor: &ActorRef<OperatorSession>,
) -> Result<Option<UserAgentResponsePayload>, Status> { ) -> Result<Option<OperatorResponsePayload>, Status> {
let result = match actor.ask(HandleGrantList {}).await { let result = match actor.ask(HandleGrantList {}).await {
Ok(grants) => EvmGrantListResult::Grants(EvmGrantList { Ok(grants) => EvmGrantListResult::Grants(EvmGrantList {
grants: grants grants: grants
@@ -134,9 +134,9 @@ async fn handle_grant_list(
} }
async fn handle_grant_create( async fn handle_grant_create(
actor: &ActorRef<UserAgentSession>, actor: &ActorRef<OperatorSession>,
req: EvmGrantCreateRequest, req: EvmGrantCreateRequest,
) -> Result<Option<UserAgentResponsePayload>, Status> { ) -> Result<Option<OperatorResponsePayload>, Status> {
let basic = req let basic = req
.shared .shared
.ok_or_else(|| Status::invalid_argument("Missing shared grant settings"))? .ok_or_else(|| Status::invalid_argument("Missing shared grant settings"))?
@@ -164,9 +164,9 @@ async fn handle_grant_create(
} }
async fn handle_grant_delete( async fn handle_grant_delete(
actor: &ActorRef<UserAgentSession>, actor: &ActorRef<OperatorSession>,
req: EvmGrantDeleteRequest, req: EvmGrantDeleteRequest,
) -> Result<Option<UserAgentResponsePayload>, Status> { ) -> Result<Option<OperatorResponsePayload>, Status> {
let result = match actor let result = match actor
.ask(HandleGrantDelete { .ask(HandleGrantDelete {
grant_id: req.grant_id, grant_id: req.grant_id,
@@ -190,9 +190,9 @@ async fn handle_grant_delete(
} }
async fn handle_sign_transaction( async fn handle_sign_transaction(
actor: &ActorRef<UserAgentSession>, actor: &ActorRef<OperatorSession>,
req: ProtoSignTransactionRequest, req: ProtoSignTransactionRequest,
) -> Result<Option<UserAgentResponsePayload>, Status> { ) -> Result<Option<OperatorResponsePayload>, Status> {
let request = req let request = req
.request .request
.ok_or_else(|| Status::invalid_argument("Missing sign transaction request"))?; .ok_or_else(|| Status::invalid_argument("Missing sign transaction request"))?;

View File

@@ -14,7 +14,7 @@ use arbiter_proto::{
TransactionRateLimit as ProtoTransactionRateLimit, VolumeRateLimit as ProtoVolumeRateLimit, TransactionRateLimit as ProtoTransactionRateLimit, VolumeRateLimit as ProtoVolumeRateLimit,
specific_grant::Grant as ProtoSpecificGrantType, specific_grant::Grant as ProtoSpecificGrantType,
}, },
proto::user_agent::sdk_client::{WalletAccess, WalletAccessEntry as SdkClientWalletAccess}, proto::operator::sdk_client::{WalletAccess, WalletAccessEntry as SdkClientWalletAccess},
}; };
use alloy::primitives::{Address, U256}; use alloy::primitives::{Address, U256};

View File

@@ -10,7 +10,7 @@ use arbiter_proto::proto::{
TransactionRateLimit as ProtoTransactionRateLimit, VolumeRateLimit as ProtoVolumeRateLimit, TransactionRateLimit as ProtoTransactionRateLimit, VolumeRateLimit as ProtoVolumeRateLimit,
specific_grant::Grant as ProtoSpecificGrantType, specific_grant::Grant as ProtoSpecificGrantType,
}, },
user_agent::sdk_client::{WalletAccess, WalletAccessEntry as ProtoSdkClientWalletAccess}, operator::sdk_client::{WalletAccess, WalletAccessEntry as ProtoSdkClientWalletAccess},
}; };
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};

View File

@@ -1,8 +1,8 @@
use crate::{ use crate::{
db::models::NewEvmWalletAccess, db::models::NewEvmWalletAccess,
grpc::Convert, grpc::Convert,
peers::user_agent::{ peers::operator::{
OutOfBand, UserAgentSession, OutOfBand, OperatorSession,
session::handlers::{ session::handlers::{
HandleGrantEvmWalletAccess, HandleListWalletAccess, HandleNewClientApprove, HandleGrantEvmWalletAccess, HandleListWalletAccess, HandleNewClientApprove,
HandleRevokeEvmWalletAccess, HandleSdkClientList, HandleRevokeEvmWalletAccess, HandleSdkClientList,
@@ -12,7 +12,7 @@ use crate::{
use arbiter_crypto::authn; use arbiter_crypto::authn;
use arbiter_proto::proto::{ use arbiter_proto::proto::{
shared::ClientInfo as ProtoClientMetadata, shared::ClientInfo as ProtoClientMetadata,
user_agent::{ operator::{
sdk_client::{ sdk_client::{
self as proto_sdk_client, ConnectionCancel as ProtoSdkClientConnectionCancel, self as proto_sdk_client, ConnectionCancel as ProtoSdkClientConnectionCancel,
ConnectionRequest as ProtoSdkClientConnectionRequest, ConnectionRequest as ProtoSdkClientConnectionRequest,
@@ -24,7 +24,7 @@ use arbiter_proto::proto::{
request::Payload as SdkClientRequestPayload, request::Payload as SdkClientRequestPayload,
response::Payload as SdkClientResponsePayload, response::Payload as SdkClientResponsePayload,
}, },
user_agent_response::Payload as UserAgentResponsePayload, operator_response::Payload as OperatorResponsePayload,
}, },
}; };
@@ -32,13 +32,13 @@ use kameo::actor::ActorRef;
use tonic::Status; use tonic::Status;
use tracing::{info, warn}; use tracing::{info, warn};
const fn wrap_sdk_client_response(payload: SdkClientResponsePayload) -> UserAgentResponsePayload { const fn wrap_sdk_client_response(payload: SdkClientResponsePayload) -> OperatorResponsePayload {
UserAgentResponsePayload::SdkClient(proto_sdk_client::Response { OperatorResponsePayload::SdkClient(proto_sdk_client::Response {
payload: Some(payload), payload: Some(payload),
}) })
} }
pub(super) fn out_of_band_payload(oob: OutOfBand) -> UserAgentResponsePayload { pub(super) fn out_of_band_payload(oob: OutOfBand) -> OperatorResponsePayload {
match oob { match oob {
OutOfBand::ClientConnectionRequest { profile } => wrap_sdk_client_response( OutOfBand::ClientConnectionRequest { profile } => wrap_sdk_client_response(
SdkClientResponsePayload::ConnectionRequest(ProtoSdkClientConnectionRequest { SdkClientResponsePayload::ConnectionRequest(ProtoSdkClientConnectionRequest {
@@ -59,9 +59,9 @@ pub(super) fn out_of_band_payload(oob: OutOfBand) -> UserAgentResponsePayload {
} }
pub(super) async fn dispatch( pub(super) async fn dispatch(
actor: &ActorRef<UserAgentSession>, actor: &ActorRef<OperatorSession>,
req: proto_sdk_client::Request, req: proto_sdk_client::Request,
) -> Result<Option<UserAgentResponsePayload>, Status> { ) -> Result<Option<OperatorResponsePayload>, Status> {
let Some(payload) = req.payload else { let Some(payload) = req.payload else {
return Err(Status::invalid_argument( return Err(Status::invalid_argument(
"Missing SDK client request payload", "Missing SDK client request payload",
@@ -87,9 +87,9 @@ pub(super) async fn dispatch(
} }
async fn handle_connection_response( async fn handle_connection_response(
actor: &ActorRef<UserAgentSession>, actor: &ActorRef<OperatorSession>,
resp: ProtoSdkClientConnectionResponse, resp: ProtoSdkClientConnectionResponse,
) -> Result<Option<UserAgentResponsePayload>, Status> { ) -> Result<Option<OperatorResponsePayload>, Status> {
let pubkey = authn::PublicKey::try_from(resp.pubkey.as_slice()) let pubkey = authn::PublicKey::try_from(resp.pubkey.as_slice())
.map_err(|()| Status::invalid_argument("Invalid ML-DSA public key"))?; .map_err(|()| Status::invalid_argument("Invalid ML-DSA public key"))?;
@@ -108,8 +108,8 @@ async fn handle_connection_response(
} }
async fn handle_list( async fn handle_list(
actor: &ActorRef<UserAgentSession>, actor: &ActorRef<OperatorSession>,
) -> Result<Option<UserAgentResponsePayload>, Status> { ) -> Result<Option<OperatorResponsePayload>, Status> {
let result = match actor.ask(HandleSdkClientList {}).await { let result = match actor.ask(HandleSdkClientList {}).await {
Ok(clients) => ProtoSdkClientListResult::Clients(ProtoSdkClientList { Ok(clients) => ProtoSdkClientListResult::Clients(ProtoSdkClientList {
clients: clients clients: clients
@@ -144,9 +144,9 @@ async fn handle_list(
} }
async fn handle_grant_wallet_access( async fn handle_grant_wallet_access(
actor: &ActorRef<UserAgentSession>, actor: &ActorRef<OperatorSession>,
req: ProtoSdkClientGrantWalletAccess, req: ProtoSdkClientGrantWalletAccess,
) -> Result<Option<UserAgentResponsePayload>, Status> { ) -> Result<Option<OperatorResponsePayload>, Status> {
let entries: Vec<NewEvmWalletAccess> = req.accesses.into_iter().map(Convert::convert).collect(); let entries: Vec<NewEvmWalletAccess> = req.accesses.into_iter().map(Convert::convert).collect();
match actor.ask(HandleGrantEvmWalletAccess { entries }).await { match actor.ask(HandleGrantEvmWalletAccess { entries }).await {
Ok(()) => { Ok(()) => {
@@ -161,9 +161,9 @@ async fn handle_grant_wallet_access(
} }
async fn handle_revoke_wallet_access( async fn handle_revoke_wallet_access(
actor: &ActorRef<UserAgentSession>, actor: &ActorRef<OperatorSession>,
req: ProtoSdkClientRevokeWalletAccess, req: ProtoSdkClientRevokeWalletAccess,
) -> Result<Option<UserAgentResponsePayload>, Status> { ) -> Result<Option<OperatorResponsePayload>, Status> {
match actor match actor
.ask(HandleRevokeEvmWalletAccess { .ask(HandleRevokeEvmWalletAccess {
entries: req.accesses, entries: req.accesses,
@@ -182,8 +182,8 @@ async fn handle_revoke_wallet_access(
} }
async fn handle_list_wallet_access( async fn handle_list_wallet_access(
actor: &ActorRef<UserAgentSession>, actor: &ActorRef<OperatorSession>,
) -> Result<Option<UserAgentResponsePayload>, Status> { ) -> Result<Option<OperatorResponsePayload>, Status> {
match actor.ask(HandleListWalletAccess {}).await { match actor.ask(HandleListWalletAccess {}).await {
Ok(accesses) => Ok(Some(wrap_sdk_client_response( Ok(accesses) => Ok(Some(wrap_sdk_client_response(
SdkClientResponsePayload::ListWalletAccess(ListWalletAccessResponse { SdkClientResponsePayload::ListWalletAccess(ListWalletAccessResponse {

View File

@@ -1,11 +1,11 @@
use crate::{ use crate::{
actors::vault::VaultState, actors::vault::VaultState,
peers::user_agent::{UserAgentSession, session::handlers::HandleQueryVaultState}, peers::operator::{OperatorSession, session::handlers::HandleQueryVaultState},
}; };
use arbiter_proto::{ use arbiter_proto::{
proto::shared::VaultState as ProtoVaultState, proto::shared::VaultState as ProtoVaultState,
proto::user_agent::{ proto::operator::{
user_agent_response::Payload as UserAgentResponsePayload, operator_response::Payload as OperatorResponsePayload,
vault::{ vault::{
self as proto_vault, request::Payload as VaultRequestPayload, self as proto_vault, request::Payload as VaultRequestPayload,
response::Payload as VaultResponsePayload, response::Payload as VaultResponsePayload,
@@ -17,16 +17,16 @@ use kameo::actor::ActorRef;
use tonic::Status; use tonic::Status;
use tracing::warn; use tracing::warn;
const fn wrap_vault_response(payload: VaultResponsePayload) -> UserAgentResponsePayload { const fn wrap_vault_response(payload: VaultResponsePayload) -> OperatorResponsePayload {
UserAgentResponsePayload::Vault(proto_vault::Response { OperatorResponsePayload::Vault(proto_vault::Response {
payload: Some(payload), payload: Some(payload),
}) })
} }
pub(super) async fn dispatch( pub(super) async fn dispatch(
actor: &ActorRef<UserAgentSession>, actor: &ActorRef<OperatorSession>,
req: proto_vault::Request, req: proto_vault::Request,
) -> Result<Option<UserAgentResponsePayload>, Status> { ) -> Result<Option<OperatorResponsePayload>, Status> {
let Some(payload) = req.payload else { let Some(payload) = req.payload else {
return Err(Status::invalid_argument("Missing vault request payload")); return Err(Status::invalid_argument("Missing vault request payload"));
}; };
@@ -42,8 +42,8 @@ pub(super) async fn dispatch(
} }
async fn handle_query_vault_state( async fn handle_query_vault_state(
actor: &ActorRef<UserAgentSession>, actor: &ActorRef<OperatorSession>,
) -> Result<Option<UserAgentResponsePayload>, Status> { ) -> Result<Option<OperatorResponsePayload>, Status> {
let state = match actor.ask(HandleQueryVaultState {}).await { let state = match actor.ask(HandleQueryVaultState {}).await {
Ok(VaultState::Unbootstrapped) => ProtoVaultState::Unbootstrapped, Ok(VaultState::Unbootstrapped) => ProtoVaultState::Unbootstrapped,
Ok(VaultState::Sealed) => ProtoVaultState::Sealed, Ok(VaultState::Sealed) => ProtoVaultState::Sealed,

View File

@@ -1,7 +1,7 @@
use super::auth::AuthTransportAdapter; use super::auth::AuthTransportAdapter;
use crate::{ use crate::{
grpc::TryConvert, grpc::TryConvert,
peers::user_agent::vault_gate::{self as vault_gate}, peers::operator::vault_gate::{self as vault_gate},
}; };
use arbiter_proto::transport::{Bi, Error as TransportError, Receiver, Sender}; use arbiter_proto::transport::{Bi, Error as TransportError, Receiver, Sender};
@@ -20,7 +20,7 @@ impl Receiver<vault_gate::Inbound> for AuthTransportAdapter<'_> {
Err(error) => { Err(error) => {
warn!( warn!(
?error, ?error,
"Failed to receive user agent request during vault gate" "Failed to receive operator request during vault gate"
); );
return None; return None;
} }

View File

@@ -1,11 +1,11 @@
use crate::{ use crate::{
grpc::{Convert, TryConvert}, grpc::{Convert, TryConvert},
peers::user_agent::vault_gate::{ peers::operator::vault_gate::{
self as vault_gate, HandleBootstrapEncryptedKey, HandleHandshake, HandleUnsealEncryptedKey, self as vault_gate, HandleBootstrapEncryptedKey, HandleHandshake, HandleUnsealEncryptedKey,
}, },
}; };
use arbiter_proto::proto::user_agent::{ use arbiter_proto::proto::operator::{
user_agent_request::Payload as UserAgentRequestPayload, operator_request::Payload as OperatorRequestPayload,
vault::{ vault::{
self as proto_vault, self as proto_vault,
bootstrap::{self as proto_bootstrap}, bootstrap::{self as proto_bootstrap},
@@ -16,7 +16,7 @@ use arbiter_proto::proto::user_agent::{
use tonic::Status; use tonic::Status;
impl TryConvert for UserAgentRequestPayload { impl TryConvert for OperatorRequestPayload {
type Output = vault_gate::Inbound; type Output = vault_gate::Inbound;
type Error = Status; type Error = Status;

View File

@@ -1,12 +1,12 @@
use crate::{ use crate::{
actors::vault::VaultState, actors::vault::VaultState,
grpc::{Convert, TryConvert}, grpc::{Convert, TryConvert},
peers::user_agent::vault_gate::{self as vault_gate}, peers::operator::vault_gate::{self as vault_gate},
}; };
use arbiter_proto::proto::{ use arbiter_proto::proto::{
shared::VaultState as ProtoVaultState, shared::VaultState as ProtoVaultState,
user_agent::{ operator::{
user_agent_response::Payload as UserAgentResponsePayload, operator_response::Payload as OperatorResponsePayload,
vault::{ vault::{
self as proto_vault, self as proto_vault,
bootstrap::{self as proto_bootstrap, BootstrapResult as ProtoBootstrapResult}, bootstrap::{self as proto_bootstrap, BootstrapResult as ProtoBootstrapResult},
@@ -22,28 +22,28 @@ use arbiter_proto::proto::{
use tonic::Status; use tonic::Status;
use tracing::warn; use tracing::warn;
const fn wrap_vault_response(payload: VaultResponsePayload) -> UserAgentResponsePayload { const fn wrap_vault_response(payload: VaultResponsePayload) -> OperatorResponsePayload {
UserAgentResponsePayload::Vault(proto_vault::Response { OperatorResponsePayload::Vault(proto_vault::Response {
payload: Some(payload), payload: Some(payload),
}) })
} }
const fn wrap_unseal_response(payload: UnsealResponsePayload) -> UserAgentResponsePayload { const fn wrap_unseal_response(payload: UnsealResponsePayload) -> OperatorResponsePayload {
wrap_vault_response(VaultResponsePayload::Unseal(proto_unseal::Response { wrap_vault_response(VaultResponsePayload::Unseal(proto_unseal::Response {
payload: Some(payload), payload: Some(payload),
})) }))
} }
fn wrap_bootstrap_response(result: ProtoBootstrapResult) -> UserAgentResponsePayload { fn wrap_bootstrap_response(result: ProtoBootstrapResult) -> OperatorResponsePayload {
wrap_vault_response(VaultResponsePayload::Bootstrap(proto_bootstrap::Response { wrap_vault_response(VaultResponsePayload::Bootstrap(proto_bootstrap::Response {
result: result.into(), result: result.into(),
})) }))
} }
impl Convert for VaultState { impl Convert for VaultState {
type Output = UserAgentResponsePayload; type Output = OperatorResponsePayload;
fn convert(self) -> UserAgentResponsePayload { fn convert(self) -> OperatorResponsePayload {
let proto_state = match self { let proto_state = match self {
Self::Unbootstrapped => ProtoVaultState::Unbootstrapped, Self::Unbootstrapped => ProtoVaultState::Unbootstrapped,
Self::Sealed => ProtoVaultState::Sealed, Self::Sealed => ProtoVaultState::Sealed,
@@ -54,9 +54,9 @@ impl Convert for VaultState {
} }
impl Convert for vault_gate::HandshakeResponse { impl Convert for vault_gate::HandshakeResponse {
type Output = UserAgentResponsePayload; type Output = OperatorResponsePayload;
fn convert(self) -> UserAgentResponsePayload { fn convert(self) -> OperatorResponsePayload {
wrap_unseal_response(UnsealResponsePayload::Start( wrap_unseal_response(UnsealResponsePayload::Start(
proto_unseal::UnsealStartResponse { proto_unseal::UnsealStartResponse {
server_pubkey: self.server_pubkey.as_bytes().to_vec(), server_pubkey: self.server_pubkey.as_bytes().to_vec(),
@@ -66,10 +66,10 @@ impl Convert for vault_gate::HandshakeResponse {
} }
impl TryConvert for vault_gate::Outbound { impl TryConvert for vault_gate::Outbound {
type Output = UserAgentResponsePayload; type Output = OperatorResponsePayload;
type Error = Status; type Error = Status;
fn try_convert(self) -> Result<UserAgentResponsePayload, Status> { fn try_convert(self) -> Result<OperatorResponsePayload, Status> {
match self { match self {
Self::HandleVaultState(result) => result Self::HandleVaultState(result) => result
.map_err(|err| { .map_err(|err| {

View File

@@ -54,7 +54,7 @@ impl From<diesel::result::Error> for Error {
pub enum ApproveError { pub enum ApproveError {
#[error("Internal error")] #[error("Internal error")]
Internal, Internal,
#[error("Client connection denied by user agents")] #[error("Client connection denied by operators")]
Denied, Denied,
#[error("Upstream error: {0}")] #[error("Upstream error: {0}")]
Upstream(flow_coordinator::ApprovalError), Upstream(flow_coordinator::ApprovalError),

View File

@@ -1,2 +1,2 @@
pub mod client; pub mod client;
pub mod user_agent; pub mod operator;

View File

@@ -1,4 +1,4 @@
use super::{Credentials, UserAgentConnection}; use super::{Credentials, OperatorConnection};
use arbiter_crypto::authn::{self, AuthChallenge}; use arbiter_crypto::authn::{self, AuthChallenge};
use arbiter_proto::transport::Bi; use arbiter_proto::transport::Bi;
@@ -69,7 +69,7 @@ fn parse_auth_event(payload: Inbound) -> AuthEvents {
} }
pub async fn authenticate<T>( pub async fn authenticate<T>(
props: &mut UserAgentConnection, props: &mut OperatorConnection,
transport: &mut T, transport: &mut T,
) -> Result<Credentials, Error> ) -> Result<Credentials, Error>
where where

View File

@@ -1,13 +1,13 @@
use super::{ use super::{
super::{Credentials, UserAgentConnection}, super::{Credentials, OperatorConnection},
Error, Error,
}; };
use crate::{ use crate::{
actors::bootstrap::ConsumeToken, actors::bootstrap::ConsumeToken,
db::{DatabasePool, schema::useragent_client}, db::{DatabasePool, schema::operator_client},
peers::user_agent::auth::Outbound, peers::operator::auth::Outbound,
}; };
use arbiter_crypto::authn::{self, AuthChallenge, USERAGENT_CONTEXT}; use arbiter_crypto::authn::{self, AuthChallenge, OPERATOR_CONTEXT};
use arbiter_proto::transport::Bi; use arbiter_proto::transport::Bi;
use diesel::{ExpressionMethods as _, OptionalExtension as _, QueryDsl}; use diesel::{ExpressionMethods as _, OptionalExtension as _, QueryDsl};
@@ -44,9 +44,9 @@ async fn get_client_id(db: &DatabasePool, pubkey: &authn::PublicKey) -> Result<O
Error::internal("Database unavailable") Error::internal("Database unavailable")
})?; })?;
useragent_client::table operator_client::table
.filter(useragent_client::public_key.eq(pubkey.to_bytes())) .filter(operator_client::public_key.eq(pubkey.to_bytes()))
.select(useragent_client::id) .select(operator_client::id)
.first::<i32>(&mut conn) .first::<i32>(&mut conn)
.await .await
.optional() .optional()
@@ -63,9 +63,9 @@ async fn register_key(db: &DatabasePool, pubkey: &authn::PublicKey) -> Result<i3
Error::internal("Database unavailable") Error::internal("Database unavailable")
})?; })?;
let id: i32 = diesel::insert_into(useragent_client::table) let id: i32 = diesel::insert_into(operator_client::table)
.values((useragent_client::public_key.eq(pubkey_bytes),)) .values((operator_client::public_key.eq(pubkey_bytes),))
.returning(useragent_client::id) .returning(operator_client::id)
.get_result(&mut conn) .get_result(&mut conn)
.await .await
.map_err(|e| { .map_err(|e| {
@@ -77,12 +77,12 @@ async fn register_key(db: &DatabasePool, pubkey: &authn::PublicKey) -> Result<i3
} }
pub(super) struct AuthContext<'a, T: ?Sized> { pub(super) struct AuthContext<'a, T: ?Sized> {
pub(super) conn: &'a mut UserAgentConnection, pub(super) conn: &'a mut OperatorConnection,
pub(super) transport: &'a mut T, pub(super) transport: &'a mut T,
} }
impl<'a, T: ?Sized> AuthContext<'a, T> { impl<'a, T: ?Sized> AuthContext<'a, T> {
pub(super) const fn new(conn: &'a mut UserAgentConnection, transport: &'a mut T) -> Self { pub(super) const fn new(conn: &'a mut OperatorConnection, transport: &'a mut T) -> Self {
Self { conn, transport } Self { conn, transport }
} }
} }
@@ -143,7 +143,7 @@ where
Error::InvalidChallengeSolution Error::InvalidChallengeSolution
})?; })?;
let valid = pubkey.verify(challenge, USERAGENT_CONTEXT, &signature); let valid = pubkey.verify(challenge, OPERATOR_CONTEXT, &signature);
if !valid { if !valid {
self.transport self.transport

View File

@@ -17,7 +17,7 @@ use tokio::sync::oneshot;
use tracing::{error, warn}; use tracing::{error, warn};
pub use auth::authenticate; pub use auth::authenticate;
pub use session::UserAgentSession; pub use session::OperatorSession;
pub mod auth; pub mod auth;
pub mod session; pub mod session;
@@ -30,10 +30,10 @@ pub struct Credentials {
} }
impl Integrable for Credentials { impl Integrable for Credentials {
const KIND: &'static str = "useragent_credentials"; const KIND: &'static str = "operator_credentials";
} }
// Messages, sent by user agent to connection client without having a request // Messages, sent by operator to connection client without having a request
#[derive(Debug)] #[derive(Debug)]
pub enum OutOfBand { pub enum OutOfBand {
ClientConnectionRequest { profile: ClientProfile }, ClientConnectionRequest { profile: ClientProfile },
@@ -41,12 +41,12 @@ pub enum OutOfBand {
} }
#[derive(Clone)] #[derive(Clone)]
pub struct UserAgentConnection { pub struct OperatorConnection {
pub(crate) db: DatabasePool, pub(crate) db: DatabasePool,
pub(crate) actors: GlobalActors, pub(crate) actors: GlobalActors,
} }
impl UserAgentConnection { impl OperatorConnection {
pub const fn new(db: DatabasePool, actors: GlobalActors) -> Self { pub const fn new(db: DatabasePool, actors: GlobalActors) -> Self {
Self { db, actors } Self { db, actors }
} }
@@ -106,7 +106,7 @@ async fn should_run_gate(vault: &ActorRef<Vault>) -> Result<bool, Error> {
} }
async fn run_vault_gate<T>( async fn run_vault_gate<T>(
props: &UserAgentConnection, props: &OperatorConnection,
transport: &mut T, transport: &mut T,
auth_creds: Credentials, auth_creds: Credentials,
) -> Result<(), Error> ) -> Result<(), Error>
@@ -160,10 +160,10 @@ where
} }
pub async fn start<T>( pub async fn start<T>(
props: &mut UserAgentConnection, props: &mut OperatorConnection,
mut transport: T, mut transport: T,
oob_sender: Box<dyn Sender<OutOfBand>>, oob_sender: Box<dyn Sender<OutOfBand>>,
) -> Result<ActorRef<UserAgentSession>, Error> ) -> Result<ActorRef<OperatorSession>, Error>
where where
T: Bi<auth::Inbound, Result<auth::Outbound, auth::Error>> + Send, T: Bi<auth::Inbound, Result<auth::Outbound, auth::Error>> + Send,
T: Bi<vault_gate::Inbound, Result<vault_gate::Outbound, vault_gate::Error>> + Send, T: Bi<vault_gate::Inbound, Result<vault_gate::Outbound, vault_gate::Error>> + Send,
@@ -178,7 +178,7 @@ where
// checking the integrity // checking the integrity
verify_integrity(&props.db, &props.actors.vault, &creds).await?; verify_integrity(&props.db, &props.actors.vault, &creds).await?;
Ok(UserAgentSession::spawn(UserAgentSession::new( Ok(OperatorSession::spawn(OperatorSession::new(
props.clone(), props.clone(),
oob_sender, oob_sender,
))) )))

View File

@@ -1,8 +1,8 @@
use super::{Error, UserAgentSession}; use super::{Error, OperatorSession};
use crate::{ use crate::{
actors::evm::{ actors::evm::{
ClientSignTransaction, Generate, ListWallets, SignTransactionError as EvmSignError, ClientSignTransaction, Generate, ListWallets, SignTransactionError as EvmSignError,
UseragentCreateGrant, UseragentListGrants, OperatorCreateGrant, OperatorListGrants,
}, },
actors::flow_coordinator::client_connect_approval::ClientApprovalAnswer, actors::flow_coordinator::client_connect_approval::ClientApprovalAnswer,
actors::vault::VaultState, actors::vault::VaultState,
@@ -36,7 +36,7 @@ pub enum GrantMutationError {
} }
#[messages] #[messages]
impl UserAgentSession { impl OperatorSession {
#[message] #[message]
pub(crate) async fn handle_query_vault_state(&mut self) -> Result<VaultState, Error> { pub(crate) async fn handle_query_vault_state(&mut self) -> Result<VaultState, Error> {
use crate::actors::vault::GetState; use crate::actors::vault::GetState;
@@ -44,7 +44,7 @@ impl UserAgentSession {
let vault_state = match self.props.actors.vault.ask(GetState {}).await { let vault_state = match self.props.actors.vault.ask(GetState {}).await {
Ok(state) => state, Ok(state) => state,
Err(err) => { Err(err) => {
error!(?err, actor = "useragent", "vault.query.failed"); error!(?err, actor = "operator", "vault.query.failed");
return Err(Error::internal("Vault is in broken state")); return Err(Error::internal("Vault is in broken state"));
} }
}; };
@@ -54,7 +54,7 @@ impl UserAgentSession {
} }
#[messages] #[messages]
impl UserAgentSession { impl OperatorSession {
#[message] #[message]
pub(crate) async fn handle_evm_wallet_create(&mut self) -> Result<(i32, Address), Error> { pub(crate) async fn handle_evm_wallet_create(&mut self) -> Result<(i32, Address), Error> {
match self.props.actors.evm.ask(Generate {}).await { match self.props.actors.evm.ask(Generate {}).await {
@@ -82,10 +82,10 @@ impl UserAgentSession {
} }
#[messages] #[messages]
impl UserAgentSession { impl OperatorSession {
#[message] #[message]
pub(crate) async fn handle_grant_list(&mut self) -> Result<Vec<Grant<SpecificGrant>>, Error> { pub(crate) async fn handle_grant_list(&mut self) -> Result<Vec<Grant<SpecificGrant>>, Error> {
match self.props.actors.evm.ask(UseragentListGrants {}).await { match self.props.actors.evm.ask(OperatorListGrants {}).await {
Ok(grants) => Ok(grants), Ok(grants) => Ok(grants),
Err(err) => { Err(err) => {
error!(?err, "EVM grant list failed"); error!(?err, "EVM grant list failed");
@@ -104,7 +104,7 @@ impl UserAgentSession {
.props .props
.actors .actors
.evm .evm
.ask(UseragentCreateGrant { basic, grant }) .ask(OperatorCreateGrant { basic, grant })
.await .await
{ {
Ok(grant_id) => Ok(grant_id), Ok(grant_id) => Ok(grant_id),
@@ -121,7 +121,7 @@ impl UserAgentSession {
// .props // .props
// .actors // .actors
// .evm // .evm
// .ask(UseragentDeleteGrant { grant_id }) // .ask(OperatorDeleteGrant { grant_id })
// .await // .await
// { // {
// Ok(()) => Ok(()), // Ok(()) => Ok(()),
@@ -157,7 +157,7 @@ impl UserAgentSession {
Err(SignTransactionError::Vet(vet_error)) Err(SignTransactionError::Vet(vet_error))
} }
Err(err) => { Err(err) => {
error!(?err, "EVM sign transaction failed in user-agent session"); error!(?err, "EVM sign transaction failed in operator session");
Err(SignTransactionError::Internal) Err(SignTransactionError::Internal)
} }
} }
@@ -226,7 +226,7 @@ impl UserAgentSession {
} }
#[messages] #[messages]
impl UserAgentSession { impl OperatorSession {
#[message(ctx)] #[message(ctx)]
pub(crate) async fn handle_new_client_approve( pub(crate) async fn handle_new_client_approve(
&mut self, &mut self,

View File

@@ -1,8 +1,8 @@
use super::{OutOfBand, UserAgentConnection}; use super::{OutOfBand, OperatorConnection};
use crate::{ use crate::{
actors::{ actors::{
flow_coordinator::client_connect_approval::ClientApprovalController, flow_coordinator::client_connect_approval::ClientApprovalController,
useragent_registry::ConnectUseragent, operator_registry::ConnectOperator,
}, },
peers::client::ClientProfile, peers::client::ClientProfile,
}; };
@@ -49,8 +49,8 @@ pub struct PendingClientApproval {
controller: ActorRef<ClientApprovalController>, controller: ActorRef<ClientApprovalController>,
} }
pub struct UserAgentSession { pub struct OperatorSession {
props: UserAgentConnection, props: OperatorConnection,
sender: Box<dyn Sender<OutOfBand>>, sender: Box<dyn Sender<OutOfBand>>,
pending_client_approvals: HashMap<Vec<u8>, PendingClientApproval>, pending_client_approvals: HashMap<Vec<u8>, PendingClientApproval>,
@@ -58,8 +58,8 @@ pub struct UserAgentSession {
pub mod handlers; pub mod handlers;
impl UserAgentSession { impl OperatorSession {
pub(crate) fn new(props: UserAgentConnection, sender: Box<dyn Sender<OutOfBand>>) -> Self { pub(crate) fn new(props: OperatorConnection, sender: Box<dyn Sender<OutOfBand>>) -> Self {
Self { Self {
props, props,
sender, sender,
@@ -69,7 +69,7 @@ impl UserAgentSession {
} }
#[messages] #[messages]
impl UserAgentSession { impl OperatorSession {
#[message] #[message]
pub async fn begin_new_client_approval( pub async fn begin_new_client_approval(
&mut self, &mut self,
@@ -85,7 +85,7 @@ impl UserAgentSession {
{ {
error!( error!(
?e, ?e,
actor = "user_agent", actor = "operator",
event = "failed to announce new client connection" event = "failed to announce new client connection"
); );
return; return;
@@ -101,7 +101,7 @@ impl UserAgentSession {
} }
} }
impl Actor for UserAgentSession { impl Actor for OperatorSession {
type Args = Self; type Args = Self;
type Error = Error; type Error = Error;
@@ -109,17 +109,17 @@ impl Actor for UserAgentSession {
async fn on_start(args: Self::Args, this: ActorRef<Self>) -> Result<Self, Self::Error> { async fn on_start(args: Self::Args, this: ActorRef<Self>) -> Result<Self, Self::Error> {
args.props args.props
.actors .actors
.useragent_registry .operator_registry
.ask(ConnectUseragent { .ask(ConnectOperator {
actor: this.clone(), actor: this.clone(),
}) })
.await .await
.map_err(|err| { .map_err(|err| {
error!( error!(
?err, ?err,
"Failed to register user agent connection with user agent registry" "Failed to register operator connection with operator registry"
); );
Error::internal("Failed to register user agent connection with user agent registry") Error::internal("Failed to register operator connection with operator registry")
})?; })?;
Ok(args) Ok(args)
} }
@@ -149,7 +149,7 @@ impl Actor for UserAgentSession {
{ {
error!( error!(
?e, ?e,
actor = "user_agent", actor = "operator",
event = "failed to announce client connection cancellation" event = "failed to announce client connection cancellation"
); );
} }

View File

@@ -79,22 +79,22 @@ fn sign_client_challenge(key: &SigningKey<MlDsa87>, challenge: &AuthChallenge) -
.into() .into()
} }
async fn insert_bootstrap_sentinel_useragent(db: &db::DatabasePool) { async fn insert_bootstrap_sentinel_operator(db: &db::DatabasePool) {
let mut conn = db.get().await.unwrap(); let mut conn = db.get().await.unwrap();
let sentinel_key = verifying_key(&MlDsa87::key_gen(&mut rand::rng())) let sentinel_key = verifying_key(&MlDsa87::key_gen(&mut rand::rng()))
.encode() .encode()
.0 .0
.to_vec(); .to_vec();
insert_into(schema::useragent_client::table) insert_into(schema::operator_client::table)
.values((schema::useragent_client::public_key.eq(sentinel_key),)) .values((schema::operator_client::public_key.eq(sentinel_key),))
.execute(&mut conn) .execute(&mut conn)
.await .await
.unwrap(); .unwrap();
} }
async fn spawn_test_actors(db: &db::DatabasePool) -> GlobalActors { async fn spawn_test_actors(db: &db::DatabasePool) -> GlobalActors {
insert_bootstrap_sentinel_useragent(db).await; insert_bootstrap_sentinel_operator(db).await;
let actors = GlobalActors::spawn(db.clone()).await.unwrap(); let actors = GlobalActors::spawn(db.clone()).await.unwrap();
actors actors

View File

@@ -0,0 +1,6 @@
mod common;
#[path = "operator/auth.rs"]
mod auth;
#[path = "operator/unseal.rs"]
mod unseal;

View File

@@ -1,6 +1,6 @@
use super::common::ChannelTransport; use super::common::ChannelTransport;
use arbiter_crypto::{ use arbiter_crypto::{
authn::{self, AuthChallenge, USERAGENT_CONTEXT}, authn::{self, AuthChallenge, OPERATOR_CONTEXT},
safecell::{SafeCell, SafeCellHandle as _}, safecell::{SafeCell, SafeCellHandle as _},
}; };
use arbiter_proto::transport::{Error as TransportError, Receiver, Sender}; use arbiter_proto::transport::{Error as TransportError, Receiver, Sender};
@@ -8,7 +8,7 @@ use arbiter_server::{
actors::{GlobalActors, bootstrap::GetToken, vault::Bootstrap}, actors::{GlobalActors, bootstrap::GetToken, vault::Bootstrap},
crypto::integrity, crypto::integrity,
db::{self, schema}, db::{self, schema},
peers::user_agent::{self, Credentials, UserAgentConnection, auth, vault_gate}, peers::operator::{self, Credentials, OperatorConnection, auth, vault_gate},
}; };
use async_trait::async_trait; use async_trait::async_trait;
@@ -21,13 +21,13 @@ fn verifying_key(key: &SigningKey<MlDsa87>) -> VerifyingKey<MlDsa87> {
<SigningKey<MlDsa87> as Keypair>::verifying_key(key) <SigningKey<MlDsa87> as Keypair>::verifying_key(key)
} }
fn sign_useragent_challenge( fn sign_operator_challenge(
key: &SigningKey<MlDsa87>, key: &SigningKey<MlDsa87>,
challenge: &AuthChallenge, challenge: &AuthChallenge,
) -> authn::Signature { ) -> authn::Signature {
let challenge = challenge.format(); let challenge = challenge.format();
key.signing_key() key.signing_key()
.sign_deterministic(&challenge, USERAGENT_CONTEXT) .sign_deterministic(&challenge, OPERATOR_CONTEXT)
.unwrap() .unwrap()
.into() .into()
} }
@@ -41,8 +41,8 @@ fn tamper_challenge(challenge: &AuthChallenge) -> AuthChallenge {
struct NullOobSender; struct NullOobSender;
#[async_trait] #[async_trait]
impl Sender<user_agent::OutOfBand> for NullOobSender { impl Sender<operator::OutOfBand> for NullOobSender {
async fn send(&mut self, _item: user_agent::OutOfBand) -> Result<(), TransportError> { async fn send(&mut self, _item: operator::OutOfBand) -> Result<(), TransportError> {
Ok(()) Ok(())
} }
} }
@@ -166,7 +166,7 @@ pub async fn bootstrap_token_auth() {
let (mut server_transport, mut test_transport) = ChannelTransport::new(); let (mut server_transport, mut test_transport) = ChannelTransport::new();
let db_for_task = db.clone(); let db_for_task = db.clone();
let task = tokio::spawn(async move { let task = tokio::spawn(async move {
let mut props = UserAgentConnection::new(db_for_task, actors); let mut props = OperatorConnection::new(db_for_task, actors);
auth::authenticate(&mut props, &mut server_transport).await auth::authenticate(&mut props, &mut server_transport).await
}); });
@@ -188,7 +188,7 @@ pub async fn bootstrap_token_auth() {
other => panic!("Expected AuthChallenge, got {other:?}"), other => panic!("Expected AuthChallenge, got {other:?}"),
}; };
let signature = sign_useragent_challenge(&new_key, &challenge); let signature = sign_operator_challenge(&new_key, &challenge);
test_transport test_transport
.send(auth::Inbound::AuthChallengeSolution { .send(auth::Inbound::AuthChallengeSolution {
@@ -206,8 +206,8 @@ pub async fn bootstrap_token_auth() {
task.await.unwrap().unwrap(); task.await.unwrap().unwrap();
let mut conn = db.get().await.unwrap(); let mut conn = db.get().await.unwrap();
let stored_pubkey: Vec<u8> = schema::useragent_client::table let stored_pubkey: Vec<u8> = schema::operator_client::table
.select(schema::useragent_client::public_key) .select(schema::operator_client::public_key)
.first::<Vec<u8>>(&mut conn) .first::<Vec<u8>>(&mut conn)
.await .await
.unwrap(); .unwrap();
@@ -223,7 +223,7 @@ pub async fn bootstrap_invalid_token_auth() {
let (mut server_transport, mut test_transport) = ChannelTransport::new(); let (mut server_transport, mut test_transport) = ChannelTransport::new();
let db_for_task = db.clone(); let db_for_task = db.clone();
let task = tokio::spawn(async move { let task = tokio::spawn(async move {
let mut props = UserAgentConnection::new(db_for_task, actors); let mut props = OperatorConnection::new(db_for_task, actors);
auth::authenticate(&mut props, &mut server_transport).await auth::authenticate(&mut props, &mut server_transport).await
}); });
@@ -245,7 +245,7 @@ pub async fn bootstrap_invalid_token_auth() {
other => panic!("Expected AuthChallenge, got {other:?}"), other => panic!("Expected AuthChallenge, got {other:?}"),
}; };
let signature = sign_useragent_challenge(&new_key, &challenge); let signature = sign_operator_challenge(&new_key, &challenge);
test_transport test_transport
.send(auth::Inbound::AuthChallengeSolution { .send(auth::Inbound::AuthChallengeSolution {
signature: signature.to_bytes(), signature: signature.to_bytes(),
@@ -259,7 +259,7 @@ pub async fn bootstrap_invalid_token_auth() {
)); ));
let mut conn = db.get().await.unwrap(); let mut conn = db.get().await.unwrap();
let count: i64 = schema::useragent_client::table let count: i64 = schema::operator_client::table
.count() .count()
.get_result::<i64>(&mut conn) .get_result::<i64>(&mut conn)
.await .await
@@ -285,9 +285,9 @@ pub async fn challenge_auth() {
{ {
let mut conn = db.get().await.unwrap(); let mut conn = db.get().await.unwrap();
let id: i32 = insert_into(schema::useragent_client::table) let id: i32 = insert_into(schema::operator_client::table)
.values((schema::useragent_client::public_key.eq(pubkey_bytes.clone()),)) .values((schema::operator_client::public_key.eq(pubkey_bytes.clone()),))
.returning(schema::useragent_client::id) .returning(schema::operator_client::id)
.get_result(&mut conn) .get_result(&mut conn)
.await .await
.unwrap(); .unwrap();
@@ -307,7 +307,7 @@ pub async fn challenge_auth() {
let (mut server_transport, mut test_transport) = ChannelTransport::new(); let (mut server_transport, mut test_transport) = ChannelTransport::new();
let db_for_task = db.clone(); let db_for_task = db.clone();
let task = tokio::spawn(async move { let task = tokio::spawn(async move {
let mut props = UserAgentConnection::new(db_for_task, actors); let mut props = OperatorConnection::new(db_for_task, actors);
auth::authenticate(&mut props, &mut server_transport).await auth::authenticate(&mut props, &mut server_transport).await
}); });
@@ -331,7 +331,7 @@ pub async fn challenge_auth() {
Err(err) => panic!("Expected Ok response, got Err({err:?})"), Err(err) => panic!("Expected Ok response, got Err({err:?})"),
}; };
let signature = sign_useragent_challenge(&new_key, &challenge); let signature = sign_operator_challenge(&new_key, &challenge);
test_transport test_transport
.send(auth::Inbound::AuthChallengeSolution { .send(auth::Inbound::AuthChallengeSolution {
@@ -371,8 +371,8 @@ pub async fn challenge_auth_rejects_integrity_tag_mismatch_when_unsealed() {
{ {
let mut conn = db.get().await.unwrap(); let mut conn = db.get().await.unwrap();
insert_into(schema::useragent_client::table) insert_into(schema::operator_client::table)
.values((schema::useragent_client::public_key.eq(pubkey_bytes.clone()),)) .values((schema::operator_client::public_key.eq(pubkey_bytes.clone()),))
.execute(&mut conn) .execute(&mut conn)
.await .await
.unwrap(); .unwrap();
@@ -381,8 +381,8 @@ pub async fn challenge_auth_rejects_integrity_tag_mismatch_when_unsealed() {
let (server_transport, mut test_transport) = start_transport_pair(); let (server_transport, mut test_transport) = start_transport_pair();
let db_for_task = db.clone(); let db_for_task = db.clone();
let task = tokio::spawn(async move { let task = tokio::spawn(async move {
let mut props = UserAgentConnection::new(db_for_task, actors); let mut props = OperatorConnection::new(db_for_task, actors);
user_agent::start(&mut props, server_transport, Box::new(NullOobSender)).await operator::start(&mut props, server_transport, Box::new(NullOobSender)).await
}); });
test_transport test_transport
@@ -405,7 +405,7 @@ pub async fn challenge_auth_rejects_integrity_tag_mismatch_when_unsealed() {
Err(err) => panic!("Expected Ok response, got Err({err:?})"), Err(err) => panic!("Expected Ok response, got Err({err:?})"),
}; };
let signature = sign_useragent_challenge(&new_key, &challenge); let signature = sign_operator_challenge(&new_key, &challenge);
test_transport test_transport
.send(auth::Inbound::AuthChallengeSolution { .send(auth::Inbound::AuthChallengeSolution {
@@ -422,7 +422,7 @@ pub async fn challenge_auth_rejects_integrity_tag_mismatch_when_unsealed() {
assert!(matches!( assert!(matches!(
task.await.unwrap(), task.await.unwrap(),
Err(user_agent::Error::Internal(_)) Err(operator::Error::Internal(_))
)); ));
} }
@@ -444,9 +444,9 @@ pub async fn challenge_auth_rejects_invalid_signature() {
{ {
let mut conn = db.get().await.unwrap(); let mut conn = db.get().await.unwrap();
let id: i32 = insert_into(schema::useragent_client::table) let id: i32 = insert_into(schema::operator_client::table)
.values((schema::useragent_client::public_key.eq(pubkey_bytes.clone()),)) .values((schema::operator_client::public_key.eq(pubkey_bytes.clone()),))
.returning(schema::useragent_client::id) .returning(schema::operator_client::id)
.get_result(&mut conn) .get_result(&mut conn)
.await .await
.unwrap(); .unwrap();
@@ -466,7 +466,7 @@ pub async fn challenge_auth_rejects_invalid_signature() {
let (mut server_transport, mut test_transport) = ChannelTransport::new(); let (mut server_transport, mut test_transport) = ChannelTransport::new();
let db_for_task = db.clone(); let db_for_task = db.clone();
let task = tokio::spawn(async move { let task = tokio::spawn(async move {
let mut props = UserAgentConnection::new(db_for_task, actors); let mut props = OperatorConnection::new(db_for_task, actors);
auth::authenticate(&mut props, &mut server_transport).await auth::authenticate(&mut props, &mut server_transport).await
}); });
@@ -490,7 +490,7 @@ pub async fn challenge_auth_rejects_invalid_signature() {
Err(err) => panic!("Expected Ok response, got Err({err:?})"), Err(err) => panic!("Expected Ok response, got Err({err:?})"),
}; };
let signature = sign_useragent_challenge(&new_key, &tamper_challenge(&challenge)); let signature = sign_operator_challenge(&new_key, &tamper_challenge(&challenge));
test_transport test_transport
.send(auth::Inbound::AuthChallengeSolution { .send(auth::Inbound::AuthChallengeSolution {

View File

@@ -8,7 +8,7 @@ use arbiter_server::{
vault::{Bootstrap, Seal}, vault::{Bootstrap, Seal},
}, },
db, db,
peers::user_agent::{ peers::operator::{
Credentials, Credentials,
vault_gate::{ vault_gate::{
Error as VaultGateError, HandleHandshake, HandleUnsealEncryptedKey, VaultGate, Error as VaultGateError, HandleHandshake, HandleUnsealEncryptedKey, VaultGate,

View File

@@ -1,6 +0,0 @@
mod common;
#[path = "user_agent/auth.rs"]
mod auth;
#[path = "user_agent/unseal.rs"]
mod unseal;