Architecture
Anymo is an embedded Rust kernel wrapped in a thin desktop shell, with an append-only event log as the single source of truth. This page explains the layers and the three decisions that shape them.
The layers
| Layer | Technology | Role |
|---|---|---|
| Desktop shell | Tauri v2 + React + TypeScript + Vite | Hive graph, chat, approvals, goals, skills, Dreams, automations, plugins, settings |
| Hive Kernel | Rust, embedded library | Runs, scheduling, event log, providers, tools, policy, memory, skills, query fabric |
| Protocol | Rust types → generated TypeScript + JSON Schema | One source of truth for events, runs, agents, tools, skills |
| App-server | JSON-RPC over stdio / Unix socket / loopback WebSocket | Headless host for the same kernel |
| Workers | Optional, pooled subprocesses | Specialized capabilities (for example the local browser worker) behind a typed, policy-gated local protocol |
The kernel is a plain Rust library with no UI dependency: everything important passes cargo test without the desktop app.
Decision 1: An embedded Rust kernel
The runtime lives inside the app as a plain Rust library, hive-kernel, rather than as a separate daemon the user has to install, run, and keep alive. The kernel owns the run lifecycle, the event-sourced log, the provider abstraction, the tool registry, the policy and approval engine, the memory store, the skill forge, and the hardened workspace.
Why this shape:
- No daemon to install or manage. A central service would mean a second runtime to bundle, launch, supervise, version, and secure. An embedded library ships and starts with the app.
- One memory-safe trust boundary. The security-critical code, including path canonicalization, workspace confinement, the policy engine, secret handling, and append-only sequencing, sits in one Rust library with strong typing.
- Testable core. Because the kernel does not depend on the shell, the whole runtime is exercised by fast, headless
cargo testruns. - Provider neutrality. A single OpenAI-compatible
Providertrait covers local model servers, routers, proxies, and hosted APIs, so the choice of model is a configuration concern, not an architectural one. - Clean reuse path. Any future front end links the same library; the protocol crate keeps the types contract stable across them.
Domain types and the event schema live in a separate anymo-protocol crate that generates the TypeScript bindings the UI consumes. One source of truth, no hand-written duplicate types.
Decision 2: An event-sourced runtime
The append-only event log is the single source of truth. Current state is derived, never stored authoritatively.
- Every meaningful state change is an
Event: a uniform envelope (event_id,run_id,sequence,event_type,created_at,schema_version, actor, redaction status, optional causal links) wrapping a typed payload. - Events are append-only and gap-free per run: sequence numbers are 0, 1, 2, and so on. An append whose sequence does not match the next expected value is rejected as a conflict, so the log can never develop holes or races.
- Run state is reconstructed by replay.
reconstruct_run(events)folds a run's events into aRunProjection: the run, its agents, its tasks, its status. There is no separate mutable run object to drift. - The UI is a projection of events. The desktop app renders from the same event stream, live during a run or replayed from storage afterward.
The log is abstracted behind an EventStore trait with two backends: an in-memory store for tests and recorded mock streams, and a SQLite store for persistence. What this buys you:
- One truth, no drift. What the user saw is exactly what replays.
- Free time-travel and audit. Any consumer can rebuild any run's state from its events alone. This underpins restart-and-recover.
- Integrity by construction. Gap-free sequencing with conflict rejection makes tampering and lost-update races detectable rather than silent.
- Schema evolution. A
schema_versionon every event lets the on-disk shape evolve deliberately and append-safely.
Because events are append-only, a mistake is corrected by appending a compensating event, never by editing history.
Decision 3: Safe-by-default tools
A tool cannot do anything risky without the policy engine's agreement, and every tool call is recorded in the event log. Tools declare their required permissions and risk level up front; the policy engine evaluates every request and returns allow, require approval, or deny. This is covered in depth in Safety and permissions.
Pooled tool workers
Optional capabilities run as pooled subprocesses behind a typed, policy-gated local protocol, not as a central brain. The local browser worker is the first example: it binds to loopback only, requires a bearer token on every request, and only receives requests after the kernel has evaluated the tool call. The kernel remains the trust anchor.
Alternatives that were rejected
| Alternative | Why it was rejected |
|---|---|
| Central daemon with a thin UI client | A second runtime to bundle and supervise, an open local port as attack surface, and weaker compile-time guarantees on security-critical paths |
| All logic in TypeScript inside the webview | Couples the runtime to the UI and puts secret handling and filesystem confinement in the least-isolated layer |
| Mutable state with a side log | The two representations drift, the log becomes advisory, and audit and restore become unreliable |
| CRUD database as the source of truth | Loses history and causality; what happened, in what order, and why is not recoverable |
| Microservice split from day one | Heavy operational overhead for a single-user desktop app; pooled workers keep the door open in a bounded way |
Read the full decision records
Each decision is recorded with its full context, rationale, and consequences: