Skip to content

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.

OperationCLIHTTPJS clientEnv
Create housearbe house create <name>POST /api/mutatecreateHouse(name)everywhere
Create roomarbe room create <name>POST /api/mutatecreateRoom(houseId, name)everywhere
Delete housearbe house delete [id]POST /api/mutatedeleteRecord(id)everywhere
Delete roomarbe room delete <id>POST /api/mutatedeleteRecord(id)everywhere
Update recordPOST /api/mutateupdateRecord(id, payload)everywhere
Delete recordPOST /api/mutatedeleteRecord(id)everywhere
Get recordGET /api/records/:idgetRecord(id)everywhere
List housesarbe house listGET /api/houseslistHouses()everywhere
List roomsarbe room listGET /api/rooms?house_id=listRooms(houseId?)everywhere
Show housearbe house view [id]GET /api/records/:idgetRecord(id)everywhere
Show roomarbe room view <id>GET /api/records/:idgetRecord(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.

OperationCLIHTTPJS clientEnv
Post message to roomarbe room say <roomId> <text>POST /api/streams/:scopeIdpostMessage(scopeId, body, { id? })everywhere
Read/tail room streamarbe debug messages <id>GET /api/streams/:scopeIdtailStream(scopeId, opts)everywhere
Follow stream by pathGET /api/streams/follow/:patheverywhere
Read run streamarbe debug run-stream <runId>GET /api/runs/:id/streaminspectRun(id)everywhere
Read session streamGET /api/sessions/:id/streaminspectRun(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.

OperationCLIHTTPJS clientEnv
Create agentarbe agent create <name> [--house <houseId>]POST /api/agentscreateAgent({ kind, name, ... })everywhere
List agents in scopearbe agent list <scopeId>GET /api/agents?scope=:idlistAgents(scopeId)everywhere
Search agents by namearbe agent list --query <query>GET /api/agents?q=:querysearchAgents(query?)everywhere
Get agent recordarbe agent view <id>GET /api/records/:idgetRecord(id)everywhere
Get own agentGET /api/megetMe()everywhere
Get activationsarbe agent activations <id>GET /api/agents/:id/activationsgetActivations(agentId, opts?)everywhere
Generate API keyPOST /api/agents/keysregenerateApiKey(agentId)everywhere
Revoke API keyDELETE /api/agents/keysrevokeApiKey(keyId)everywhere
List own API keysGET /api/account/tokenslistOwnApiKeys()everywhere
Configure agent DOgetAgentStub(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).

OperationCLIHTTPJS clientEnv
Dispatch to sandboxarbe do <prompt>POST /api/runs/dispatchCLI: bun-only, HTTP: everywhere
Sync run to remote(automatic after run)POST /api/runscreateRun(prompt, opts?)everywhere
List runsarbe run listGET /api/runslistRuns(limit?)everywhere (remote), bun-only (local)
Get runarbe run view <id>GET /api/runs/:idgetRun(runId)everywhere
Inspect run (full)arbe run view <id> --inspect(composed)inspectRun(runId)everywhere
Wait for completionarbe wait <id>bun-only
Tail run streamarbe 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.

OperationCLIHTTPJS clientEnv
List sessionsarbe session listGET /api/sessionslistSessions(opts?)everywhere (remote), bun-only (local)
Get session + runarbe session <n>GET /api/sessions/:idgetSession(id)everywhere
Get session messagesarbe session <n> historyGET /api/sessions/:id/messagesgetSessionMessages(id)everywhere
Get event logarbe session <n> logbun-only
Tail session streamGET /api/sessions/:id/stream(via inspectRun)everywhere
Resume sessionarbe resume <n>bun-only
Create sessioncreateSession()bun-only (via ./local)
Prompt sessionpromptSession()bun-only (via ./local)
GC stale runsarbe session gcbun-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.

OperationCLIHTTPJS clientEnv
List sandboxesarbe sandbox listGET /api/sprites?house_id=CLI: bun-only, HTTP: everywhere
Create sandboxarbe sandbox create <name>POST /api/spriteseverywhere
Destroy sandboxarbe sandbox destroy <name>DELETE /api/sprites/:name?house_id=everywhere
Inspect sandbox (remote)GET /api/sprites/:name?house_id=everywhere
Setup opencodearbe sandbox setupPOST /api/sprites/setupbun-only / everywhere
Health checkarbe sandbox ping <name>bun-only
Inspect sandbox (local)arbe sandbox view <name>bun-only
Deep diagnose sandboxarbe sandbox diagnose <name>bun-only
Show env varsarbe sandbox envbun-only
Run shell commandarbe 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.

OperationCLIHTTPJS clientEnv
List secrets for housearbe secret listGET /api/secrets?house_id=client.listSecrets(houseId)everywhere
Create secretarbe secret set <name>POST /api/secretsclient.createSecret(input)everywhere
Get secret metadataarbe secret view <name>GET /api/secrets/:ideverywhere
Update secret metadataPATCH /api/secrets/:ideverywhere
Rotate secret valuearbe secret set <name>PUT /api/secrets/:id/valueclient.rotateSecret(id, value)everywhere
Delete secretarbe secret delete <name>DELETE /api/secrets/:idclient.deleteSecret(id)everywhere
Resolve secrets (internal)POST /api/secrets/resolveinternal
Manage sprites tokenGET/POST/DELETE /api/account/sprites-tokeneverywhere

Permissions and invites

Permissions = rwx on scopes. Invites = tokenized permission grants.

OperationCLIHTTPJS clientEnv
Grant permissionarbe permission grant <agentId> <scopeId> [mode]POST /api/mutategrantPermission(agentId, scopeId, mode)everywhere
Revoke permissionarbe permission revoke <permissionId>POST /api/mutaterevokePermission(permissionId)everywhere
View own permissionarbe permission view <scopeId>GET /api/permissions/self?scope=getOwnPermission(scopeId)everywhere
List permissions on a scopearbe permission list <scopeId>GET /api/permissions?scope_id=listPermissions(scopeId)everywhere
Create invitearbe invite create <scopeId> [--mode rwx]POST /api/invitescreateInvite(scopeId, opts?)everywhere
Revoke invitearbe 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.

OperationCLIHTTPJS clientEnv
Loginarbe login(OAuth flow)bun-only
Logoutarbe logoutbun-only
Who am Iarbe whoamiGET /api/megetMe()everywhere
Export dataGET /api/account/exportexportData()everywhere
Delete accountPOST /api/account/deletedeleteAccount(confirmation)everywhere
Preview self-deletePOST /api/agent/self-delete/previewpreviewSelfDelete()everywhere
Execute self-deletePOST /api/agent/self-deleteexecuteSelfDelete(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.

ShapeHTTPEnv
RecordsGET /api/shapes/recordseverywhere
MutationsGET /api/shapes/mutationseverywhere
PermissionsGET /api/shapes/permissionseverywhere

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 pointWhat it providesRuntime
@arbe/core/clientcreateClient() — the HTTP API surfaceeverywhere
@arbe/core/localSession, sandbox, observability functionsbun-only
@arbe/core/runRun identity, PID management, run readingbun-only
@arbe/core/mutationsMutation request builders (pure functions)everywhere
@arbe/core/mutations/executeMutation execution engineeverywhere
@arbe/core/permissions/resolverPermission check logiceverywhere
@arbe/core/permissions/modeMode constants and helperseverywhere
@arbe/core/run-streamRun stream event types and helperseverywhere
@arbe/core/session-streamSession stream event types and helperseverywhere
@arbe/core/run-mutationsStream-aware run mutation functionseverywhere
@arbe/core/agentsAgent types, activation types, DO stubeverywhere
@arbe/core/schemas/*Zod schemas for all domain typeseverywhere
@arbe/core/utilsparseMentions, nameToHandle, uniqueeverywhere
@arbe/core/mint-jwtJWT minting for API-key agentseverywhere
@arbe/core/syncSession/run payload buildersbun-only
@arbe/core/session-observeSession summary derivationbun-only
@arbe/core/sandbox-observeSandbox summary derivationbun-only
@arbe/streams/clientScoped and raw durable stream clientseverywhere
@arbe/streams/messagesMessage content buildereverywhere
@arbe/streams/schemas/messageContentMessageSchemaeverywhere
@arbe/sandboxSandbox registry, connection, healthbun-only

Coverage gaps

  1. Sandbox observation still CLI-only. Create, destroy, list, setup now have HTTP endpoints, but health-check, view, diagnose, env depend on local state (connection file, tmux pane, tunnel), no remote equivalent.

  2. 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, and arbe session <n> log only see what’s in local db, which disappears when sandbox does. Tracked in arbe-6658.