Surface map
What arbe do, how from each surface.
Three surfaces expose same core: CLI (arbe commands), HTTP API (www routes), JS client (createClient() from @arbe/core/client). Some ops also direct imports from core subpaths for server-side/local use.
Env tags = where surface work:
- everywhere — pure fetch, runs browser, worker, node, bun
- bun-only — need bun:sqlite, node:fs, or subprocesses
- cf-worker — runs inside Cloudflare Worker or Durable Object
- internal — service-to-service, not caller-facing
Records and mutations
All structural writes through single mutation endpoint. JS client wraps each mutation type into typed method.
| Operation | CLI | HTTP | JS client | Env |
|---|---|---|---|---|
| Create house | arbe house create <name> | POST /api/mutate | createHouse(name) | everywhere |
| Create room | arbe room create <name> | POST /api/mutate | createRoom(houseId, name) | everywhere |
| Delete house | arbe house delete [id] | POST /api/mutate | deleteRecord(id) | everywhere |
| Delete room | arbe room delete <id> | POST /api/mutate | deleteRecord(id) | everywhere |
| Update record | — | POST /api/mutate | updateRecord(id, payload) | everywhere |
| Delete record | — | POST /api/mutate | deleteRecord(id) | everywhere |
| Get record | — | GET /api/records/:id | getRecord(id) | everywhere |
| List houses | arbe house list | GET /api/houses | listHouses() | everywhere |
| List rooms | arbe room list | GET /api/rooms?house_id= | listRooms(houseId?) | everywhere |
| Show house | arbe house view [id] | GET /api/records/:id | getRecord(id) | everywhere |
| Show room | arbe room view <id> | GET /api/records/:id | getRecord(id) | everywhere |
Mutation endpoint accepts { action, record_type?, record_id?, parent_id?, payload }. Builders in @arbe/core/mutations construct these. Execution engine in @arbe/core/mutations/execute handle permission checks, cascade deletes, stream hooks — used by www route, testable independently.
Agent creation = one structural write that does not go through /api/mutate — see Agents below for dedicated endpoint.
Streams and messages
Durable streams = append-only content layer. Each room has stream; runs and sessions also have own streams for lifecycle events.
| Operation | CLI | HTTP | JS client | Env |
|---|---|---|---|---|
| Post message to room | arbe room say <roomId> <text> | POST /api/streams/:scopeId | postMessage(scopeId, body, { id? }) | everywhere |
| Read/tail room stream | arbe debug messages <id> | GET /api/streams/:scopeId | tailStream(scopeId, opts) | everywhere |
| Follow stream by path | — | GET /api/streams/follow/:path | — | everywhere |
| Read run stream | arbe debug run-stream <runId> | GET /api/runs/:id/stream | inspectRun(id) | everywhere |
| Read session stream | — | GET /api/sessions/:id/stream | inspectRun(runId) | everywhere |
Scoped stream client (@arbe/streams/client) handle offset tracking, long-poll retry, abort-aware tailing. Core client delegates postMessage and tailStream to it.
arbe room say generate UUID client-side, pass as id so server adopts as chat.message.id; command prints id on success so callers correlate with stream events (pair with arbe debug why <id> to inspect dispatch/activation outcome).
Gaps: no standalone tailStream method on client for run/session streams — get them as part of inspectRun(), which fetch everything at once. Following stream by raw path has no JS client wrapper.
Agents
Agents = records with identity, permissions, and (for bots) Durable Object that handles activation.
| Operation | CLI | HTTP | JS client | Env |
|---|---|---|---|---|
| Create agent | arbe agent create <name> [--house <houseId>] | POST /api/agents | createAgent({ kind, name, ... }) | everywhere |
| List agents in scope | arbe agent list <scopeId> | GET /api/agents?scope=:id | listAgents(scopeId) | everywhere |
| Search agents by name | arbe agent list --query <query> | GET /api/agents?q=:query | searchAgents(query?) | everywhere |
| Get agent record | arbe agent view <id> | GET /api/records/:id | getRecord(id) | everywhere |
| Get own agent | — | GET /api/me | getMe() | everywhere |
| Get activations | arbe agent activations <id> | GET /api/agents/:id/activations | getActivations(agentId, opts?) | everywhere |
| Generate API key | — | POST /api/agents/keys | regenerateApiKey(agentId) | everywhere |
| Revoke API key | — | DELETE /api/agents/keys | revokeApiKey(keyId) | everywhere |
| List own API keys | — | GET /api/account/tokens | listOwnApiKeys() | everywhere |
| Configure agent DO | — | — | getAgentStub(ns, id).configure(opts) | cf-worker |
Agent Durable Object (ArbeAgent in @arbe/worker) owns activation logic: cooldown, relevance gating, LLM calls. Configured via RPC from www API route when bot created or updated. See docs/system/agents.md for activation pipeline.
POST /api/agents = single creation path for humans and bots — differ only in credential origin (OAuth session for kind: 'human', minted API key for kind: 'bot'). Endpoint returns { agent } for humans and { agent, apiKey } for bots; plaintext key shown once. Fresh bot has no permissions — attach to scope with grantPermission or via invite.
Gaps: Agent DO configuration only reachable from inside Cloudflare Worker (via getAgentStub), which correct, but configuration input shape (ConfigureOpts) defined in @arbe/core/agents, could be documented more visibly.
Runs
Run = one top-level execution attempt. Runs originate from CLI (arbe do, arbe loop, arbe chat) or web (POST /api/runs/dispatch).
| Operation | CLI | HTTP | JS client | Env |
|---|---|---|---|---|
| Dispatch to sandbox | arbe do <prompt> | POST /api/runs/dispatch | — | CLI: bun-only, HTTP: everywhere |
| Sync run to remote | (automatic after run) | POST /api/runs | createRun(prompt, opts?) | everywhere |
| List runs | arbe run list | GET /api/runs | listRuns(limit?) | everywhere (remote), bun-only (local) |
| Get run | arbe run view <id> | GET /api/runs/:id | getRun(runId) | everywhere |
| Inspect run (full) | arbe run view <id> --inspect | (composed) | inspectRun(runId) | everywhere |
| Wait for completion | arbe wait <id> | — | — | bun-only |
| Tail run stream | arbe debug run-stream <id> | GET /api/runs/:id/stream | (via inspectRun) | everywhere |
Run lifecycle emit events to durable run stream: status.changed, session.started, session.finished, result. Defined in @arbe/core/run-stream.
Gaps: arbe do dispatches via direct sprite connection (bun-only), web uses POST /api/runs/dispatch which goes through ArbeDispatcher DO. Two different dispatch paths with different error handling and status tracking. JS client has createRun() for syncing completed run but no dispatchRun() for triggering one — web dispatch endpoint not wrapped. arbe wait has no HTTP or JS equivalent; run stream serves that purpose but requires caller implement wait logic.
Sessions
Session = conversational thread inside run. Sessions created in opencode (underlying LLM harness) and observed/synced by arbe.
| Operation | CLI | HTTP | JS client | Env |
|---|---|---|---|---|
| List sessions | arbe session list | GET /api/sessions | listSessions(opts?) | everywhere (remote), bun-only (local) |
| Get session + run | arbe session <n> | GET /api/sessions/:id | getSession(id) | everywhere |
| Get session messages | arbe session <n> history | GET /api/sessions/:id/messages | getSessionMessages(id) | everywhere |
| Get event log | arbe session <n> log | — | — | bun-only |
| Tail session stream | — | GET /api/sessions/:id/stream | (via inspectRun) | everywhere |
| Resume session | arbe resume <n> | — | — | bun-only |
| Create session | — | — | createSession() | bun-only (via ./local) |
| Prompt session | — | — | promptSession() | bun-only (via ./local) |
| GC stale runs | arbe session gc | — | — | bun-only |
Session observation (derive status, artifacts, summaries from run data) lives in @arbe/core/session-observe. Session stream events (message.text, tool.started, etc.) defined in @arbe/core/session-stream.
Gaps: Event log (arbe session <n> log) reads from local opencode SQLite, no remote equivalent — if session ran in sandbox, can’t get detailed event log after sandbox gone. Session creation and prompting bun-only (./local entry point), correct for opencode SDK dep, but means no way to create session from browser or worker except through dispatch path.
Sandbox / sprites
Sandbox = remote execution environment (Fly.io sprite) with opencode server. CLI manages full lifecycle; web can provision and list them.
| Operation | CLI | HTTP | JS client | Env |
|---|---|---|---|---|
| List sandboxes | arbe sandbox list | GET /api/sprites?house_id= | — | CLI: bun-only, HTTP: everywhere |
| Create sandbox | arbe sandbox create <name> | POST /api/sprites | — | everywhere |
| Destroy sandbox | arbe sandbox destroy <name> | DELETE /api/sprites/:name?house_id= | — | everywhere |
| Inspect sandbox (remote) | — | GET /api/sprites/:name?house_id= | — | everywhere |
| Setup opencode | arbe sandbox setup | POST /api/sprites/setup | — | bun-only / everywhere |
| Health check | arbe sandbox ping <name> | — | — | bun-only |
| Inspect sandbox (local) | arbe sandbox view <name> | — | — | bun-only |
| Deep diagnose sandbox | arbe sandbox diagnose <name> | — | — | bun-only |
| Show env vars | arbe sandbox env | — | — | bun-only |
| Run shell command | arbe x <cmd> | — | — | bun-only |
Sandbox types and connection logic live in @arbe/sandbox. Sprites API client (SpritesClient) bun-safe but needs sprites token. HTTP routes resolve per-house SPRITES_TOKEN secret each request, so callers only need session or API-key auth plus house_id. Worker-safe create/destroy/get/update helpers live in @arbe/sandbox/http (fetch-only) alongside existing list/exec helpers.
Gaps: No JS client method yet wraps sandbox HTTP routes. arbe sandbox ping, view, and diagnose stay CLI-only because depend on local connection file, tmux pane, tunnelled opencode health checks — remote-friendly version would need read connection details from agent record and call sprite over public URL. arbe x (run command in sandbox) inherently interactive/bun-only but could have fire-and-forget HTTP equivalent.
Secrets
Secrets = encrypted values stored in Supabase Vault, scoped to houses, resolved at dispatch via env bindings. See secrets for full model.
| Operation | CLI | HTTP | JS client | Env |
|---|---|---|---|---|
| List secrets for house | arbe secret list | GET /api/secrets?house_id= | client.listSecrets(houseId) | everywhere |
| Create secret | arbe secret set <name> | POST /api/secrets | client.createSecret(input) | everywhere |
| Get secret metadata | arbe secret view <name> | GET /api/secrets/:id | — | everywhere |
| Update secret metadata | — | PATCH /api/secrets/:id | — | everywhere |
| Rotate secret value | arbe secret set <name> | PUT /api/secrets/:id/value | client.rotateSecret(id, value) | everywhere |
| Delete secret | arbe secret delete <name> | DELETE /api/secrets/:id | client.deleteSecret(id) | everywhere |
| Resolve secrets (internal) | — | POST /api/secrets/resolve | — | internal |
| Manage sprites token | — | GET/POST/DELETE /api/account/sprites-token | — | everywhere |
Permissions and invites
Permissions = rwx on scopes. Invites = tokenized permission grants.
| Operation | CLI | HTTP | JS client | Env |
|---|---|---|---|---|
| Grant permission | arbe permission grant <agentId> <scopeId> [mode] | POST /api/mutate | grantPermission(agentId, scopeId, mode) | everywhere |
| Revoke permission | arbe permission revoke <permissionId> | POST /api/mutate | revokePermission(permissionId) | everywhere |
| View own permission | arbe permission view <scopeId> | GET /api/permissions/self?scope= | getOwnPermission(scopeId) | everywhere |
| List permissions on a scope | arbe permission list <scopeId> | GET /api/permissions?scope_id= | listPermissions(scopeId) | everywhere |
| Create invite | arbe invite create <scopeId> [--mode rwx] | POST /api/invites | createInvite(scopeId, opts?) | everywhere |
| Revoke invite | arbe invite revoke <inviteId> | DELETE /api/invites?id= | revokeInvite(inviteId) | everywhere |
Permission resolution logic lives in @arbe/core/permissions/resolver. Mode constants, modeToString, and parseMode (accepts rwx strings or 0-7 bitmasks) live in @arbe/core/permissions/mode.
Account
Account ops for authenticated human.
| Operation | CLI | HTTP | JS client | Env |
|---|---|---|---|---|
| Login | arbe login | (OAuth flow) | — | bun-only |
| Logout | arbe logout | — | — | bun-only |
| Who am I | arbe whoami | GET /api/me | getMe() | everywhere |
| Export data | — | GET /api/account/export | exportData() | everywhere |
| Delete account | — | POST /api/account/delete | deleteAccount(confirmation) | everywhere |
| Preview self-delete | — | POST /api/agent/self-delete/preview | previewSelfDelete() | everywhere |
| Execute self-delete | — | POST /api/agent/self-delete | executeSelfDelete(input) | everywhere |
Electric sync shapes
Real-time sync for browser UI via Electric SQL. Read-only GET endpoints, proxy to Electric with permission-scoped WHERE clauses.
| Shape | HTTP | Env |
|---|---|---|
| Records | GET /api/shapes/records | everywhere |
| Mutations | GET /api/shapes/mutations | everywhere |
| Permissions | GET /api/shapes/permissions | everywhere |
No CLI or JS client equivalents — consumed directly by SvelteKit frontend’s Electric integration.
Import entry points
For JS/TS consumers, @arbe/core exposes different entry points depending on runtime:
| Entry point | What it provides | Runtime |
|---|---|---|
@arbe/core/client | createClient() — the HTTP API surface | everywhere |
@arbe/core/local | Session, sandbox, observability functions | bun-only |
@arbe/core/run | Run identity, PID management, run reading | bun-only |
@arbe/core/mutations | Mutation request builders (pure functions) | everywhere |
@arbe/core/mutations/execute | Mutation execution engine | everywhere |
@arbe/core/permissions/resolver | Permission check logic | everywhere |
@arbe/core/permissions/mode | Mode constants and helpers | everywhere |
@arbe/core/run-stream | Run stream event types and helpers | everywhere |
@arbe/core/session-stream | Session stream event types and helpers | everywhere |
@arbe/core/run-mutations | Stream-aware run mutation functions | everywhere |
@arbe/core/agents | Agent types, activation types, DO stub | everywhere |
@arbe/core/schemas/* | Zod schemas for all domain types | everywhere |
@arbe/core/utils | parseMentions, nameToHandle, unique | everywhere |
@arbe/core/mint-jwt | JWT minting for API-key agents | everywhere |
@arbe/core/sync | Session/run payload builders | bun-only |
@arbe/core/session-observe | Session summary derivation | bun-only |
@arbe/core/sandbox-observe | Sandbox summary derivation | bun-only |
@arbe/streams/client | Scoped and raw durable stream clients | everywhere |
@arbe/streams/messages | Message content builder | everywhere |
@arbe/streams/schemas/message | ContentMessageSchema | everywhere |
@arbe/sandbox | Sandbox registry, connection, health | bun-only |
Coverage gaps
-
Sandbox observation still CLI-only. Create, destroy, list, setup now have HTTP endpoints, but health-check,
view,diagnose,envdepend on local state (connection file, tmux pane, tunnel), no remote equivalent. -
CLI observation reads local SQLite, not remote streams. Derive layer (
session-derive.ts,sandbox-derive.ts) pure and portable, but CLI wrappers (session-observe.ts,sandbox-observe.ts) fetch from local SQLite. For authenticated remote runs, canonical data lives in Postgres and durable streams — derive functions just need remote data source wired in. Until then,arbe run,arbe session, andarbe session <n> logonly see what’s in local db, which disappears when sandbox does. Tracked in arbe-6658.