What invariants are, and what they aren’t
These are architecture invariants: properties of the system’s structure. They are distinct from:- Feature flags. Clawboo used to gate subsystems (board, executors, MCP, governance) behind
CLAWBOO_*flags; that flag layer has been removed and every subsystem is always-on. Invariants are not toggleable; flags were. - Style conventions. Where files live, how a component is named; those are conventions, enforced by review, not by structure.
- Behavioral guarantees. “The team chat never floods with 40 responses” is a behavior the cascade-prevention machinery upholds; it is verifiable but it is implemented across many files, not a single structural rule.
The model
The seven invariants partition cleanly into the planes they protect: who exists and how it runs (the registry/runtime split), how the browser reaches a runtime (the proxy boundary), how events flow (the pipeline), how the dependency graph is shaped (packages never depend up), and what the canvas and the database may contain (real records, idempotent schema). Numbers in the diagram refer to the invariants below.The seven invariants
1. The registry is the source of truth for who exists; the Gateway for how OpenClaw runs
SQLite is the agent registry of record. Theagents and teams rows are canonical for who exists. The OpenClaw Gateway is one AgentSource among several, synced into SQLite by a server-side connection. The browser reads the fleet from GET /api/agents (SQLite, via the registry), never off GatewayClient.agents.* directly.
A server-side ServerAgentRegistry constructs an OpenClawAgentSource (wired to a real GatewayClient) and a peer ClawbooNativeAgentSource (SQLite-backed, no upstream), and registers both into one AgentRegistry multiplexer. GET /api/agents aggregates registry.list() and serves it from SQLite, flagging stale: true when the OpenClaw connection is down; so the fleet still renders with the Gateway offline.
name, gatewayId, sourceAgentId, identityJson, archivedAt, updatedAt, and never the SQLite-native columns (teamId, personalityConfig, execConfig, avatarSeed, participantKind, runtime, capabilities, tenantId). A re-sync therefore never clobbers a team assignment or a personality setting that Clawboo owns.
chat.*, sessions.abort/patch, config.patch). Those ride the browser→proxy connection (the RuntimeAdapter layer); for example, sending a message is still client.call('chat.send', …) from the browser, untouched by the registry.
How this generalized. The original invariant was “the Gateway is the source of truth.” When SQLite became the registry of record, it generalized to the split above: the registry owns who exists, the Gateway owns how OpenClaw runs. A second runtime (clawboo-native) plugs in as a peer
AgentSource, and a future native record would arrive through that second source, never as a “fake” agent.2. Same-origin WebSocket for the browser
The browser never talks to the Gateway directly. Its only path to the upstream is the same-origin proxy at/api/gateway/ws. The browser resolves that URL from its own window.location; there is no place to point it at a remote host:
disableDeviceAuth: true. The proxy injects the upstream auth token server-side; the browser never receives the credential, only a hasToken flag. The proxy also handles Ed25519 device signing server-side with a persistent keypair. The server’s WS upgrade router accepts exactly one path and destroys every other upgrade socket:
handleUpgrade re-checks the path and access gate before accepting, so a forged request that slips past the router is still rejected. The only non-browser Gateway connection is the server-side OpenClawAgentSource; and it authenticates with the shared proxy device identity, not a browser path.
3. All Gateway events go Bridge → Policy → Handler
Every raw Gateway event frame flows through a three-layer pipeline with no shortcuts. The convenience runner makes the contract explicit: classify, derive intents, apply:derivePolicy is a side-effect-free switch from a ClassifiedEvent to an EventIntent[]; it reads the event, decides what should happen, and returns a description of it. It dispatches nothing, mutates no store:
4. Packages never import apps
The dependency graph flows one way:apps/ depends on packages/, never the reverse. A package may be imported by an app; an app is never imported by a package. This is what keeps the @clawboo/* packages independently buildable, browser-safe where they claim to be, and free of app-specific coupling.
A repo-wide grep for any package src/ file importing from apps/ (or @clawboo/web) returns zero import statements. The only matches are comments, a package’s data-access layer noting that apps/web calls it, or a zod-schema file noting where its schemas are consumed. The arrows point the right way:
repository.ts and the events policy/ directory carry no React, no Express, and no app config; they are pure modules an app wires up, and a future surface (a CLI, a different web app) could wire up the same way.
5. Every graph edge maps to real state, no decorative edges
The Ghost Graph and Atlas render edges, and every edge corresponds to real configuration or board state. There are three edge kinds, each with a concrete source:- Dependency edges (Boo → Boo) come from parsing each agent’s
AGENTS.mdrouting rules.buildGraphElementsrunsparseAgentsMd(files.agentsMd, agentNames)and emits onedependencyedge per resolved@mentionroute, a real routing instruction the agent will act on. - Skill edges and resource edges (Boo → tool) come from the unified capability inventory. They are built from
files.capabilities, realCapabilityRecordentries from the capability registry, and the server-evaluatedavailableflag drives whether a node renders greyed.
team-root junctions in Atlas. These are not decorative; they encode the real team-membership hierarchy, and they are explicitly tagged isSynthetic: true so any consumer that scans for AGENTS.md routing (such as “Refresh Protocol”) skips them. The single exception that is purely a graph-layer attribute is Boo Zero’s “Leadership” orbital, which the code documents as “NOT a capability record; it’s a graph-layer attribute” with a reserved clawboo-leadership- id that can never collide with a real skill.
So the rule reads precisely: no edge is decorative; every edge maps to a routing rule, a capability, or a real team-membership relationship, and the one synthesized non-routing attribute is flagged and reserved.
6. Every Boo is a real agent record
A Boo on the canvas is always backed by a realAgentRecord. The graph builds one BooNode per agent in the registry; there are no placeholder or mock agents:
eq(agents.sourceId, 'openclaw'); because the live-id list it compares against came from the Gateway, only Gateway-owned rows are eligible to be deleted. Native (clawboo-native) agents are never ghosts of an OpenClaw list, so they are structurally excluded:
How this generalized. The original framing was “every Boo is an OpenClaw agent.” With the peer native source, the framing is “every Boo is a real agent record”, backed by some
AgentSource, OpenClaw or native. The cleanup scoping above is exactly the discipline that keeps a second source’s records from being mistaken for ghosts of the first.7. The schema is idempotent and additive (the evolved “forward-only” rule)
This invariant is the one that changed shape the most, so read the current form rather than the legacy one. The historical rule was “SQLite migrations are forward-only, never edit a committed migration file.” There is no migration ladder at all. The schema is created by an idempotentCREATE TABLE IF NOT EXISTS DDL block in ensureSchema(), declared as the sole source of truth:
- The unapplied drizzle ladder must not ship or run. The
@clawboo/dbpackage’sfilesarray excludesdrizzle, and there are nodb:migrate/db:generatescripts (only a read-onlydb:studio). A test pins this posture so a stray migration runner can’t be reintroduced. - The type layer and the runtime DDL must agree.
schemaSource.test.tsbuilds a real DB viacreateDb(':memory:')and asserts that every table and column in the Drizzleschema.tstype layer matches the live DDL, and vice versa. - An existing database is reconciled up to the DDL, not assumed to match it.
IF NOT EXISTSskips the wholeCREATE TABLEwhen the table is present, so a new column would never reach a database created before it.reconcileSchemaderives the declared column set from the same DDL and adds what is missing, before the DDL batch runs (an index over a new column cannot be created until the column exists). See Database schema.
.sql files applied in order, a single idempotent DDL block runs once when a database is opened, once per process for the server, which then holds one connection for its lifetime.
The in-place upgrade path is additive only. A new column on an existing table must be addable, so it may not use
PRIMARY KEY, UNIQUE, or a STORED generated column; any DEFAULT must be a literal rather than an expression; a NOT NULL column must carry one; and a REFERENCES column must not have a non-NULL one; one that is not fails the build, and would fail loudly at boot rather than silently. Changing an existing column’s type or constraints, or removing one, remains a hard reset of the local DB.Design rationale and trade-offs
The invariants exist to make whole categories of failure impossible rather than merely caught. The registry/runtime split (1) buys offline tolerance: kill the Gateway and the fleet still renders, because who exists lives in SQLite and only how it runs needs the upstream. The cost is a second persistence layer and the idempotency discipline that keeps the two from fighting. The same-origin proxy (2) buys credential safety: the browser is structurally incapable of seeing the Gateway token or signing device frames, because those happen server-side. The cost is an extra hop and a proxy that must correctly inject auth on every connect. The pure Policy layer (3) buys testability: decision logic with no side effects can be exhaustively unit-tested without a Gateway, a store, or a clock. The cost is the ceremony of three layers where a naive implementation would dispatch inline. The one-way dependency graph (4) buys independently-buildable, browser-safe packages, at the cost of pushing app-specific glue up intoapps/ rather than letting a package reach down for it.
Real-state-only edges (5) and real-record-only Boos (6) buy a canvas you can trust: what you see maps to what the system will do. The cost is the synthetic-edge tagging and the source-scoped cleanup that keep “real” honest in a multi-source world.
The idempotent schema (7) buys a zero-friction fresh install, createDb produces a usable database with no migration step, and an in-place upgrade that costs nothing to maintain because it is derived from the same DDL. The cost is that the upgrade path is additive only: a column can be added to an existing database, but not changed or removed.
Boundaries and non-goals
- Invariants are structural, not behavioral. They guarantee the shape of the system (who is canonical, who talks to whom, which way dependencies point). They do not, by themselves, guarantee that a feature behaves correctly; that is what tests and the cascade-prevention machinery are for.
- Some invariants are OpenClaw-specific and have generalized. Invariants 1, 2, 3, and 6 were originally phrased around the single OpenClaw runtime. As the multi-runtime board path became the default, they generalized: a teammate is now a
RuntimeAdapter(not only an OpenClaw agent), and a future non-OpenClaw runtime emits a normalized lifecycle-event stream server-side rather than flowing through the Gateway WS bridge. The generalized forms above are the current ones. - The schema invariant will likely evolve again. Preserving real data across an additive schema change is handled. Preserving it across a change that rewrites or removes a column is not, and is the next step whenever one is needed.
These docs describe Clawboo v0.3.1, the current release.
See also
- Gateway and events, the Bridge → Policy → Handler pipeline in depth (invariant 3)
- The agent model, Boo, Boo Zero, and the five runtime classes (invariants 1, 6)
- The board, the durable task substrate the multi-runtime direction is built on
- Capabilities, the capability inventory behind the graph’s skill/resource edges (invariant 5)
- AgentSource, registry of record, the sync discipline behind invariant 1
- Database schema, the tables the idempotent DDL declares (invariant 7)
- Glossary, canonical term definitions