workbooks docs

Nexus runtime — HTTP/RCP surface

The Nexus exposes its capabilities over HTTP/WebSocket. There are two separate listeners — two planes — never merged: the authenticated control plane (Workbooks.Web) and the anonymous, GET-only content plane (Workbooks.PublicWeb). RCP (the Runtime-Connect Protocol) is the token-authed verb subset of the control plane the thin wbx CLI drives against a running engine.

Every fact below is anchored at :SRC: a real file:line. Routes are listed verbatim from the routers; nothing is paraphrased.

The two planes

Each plane is a distinct Bandit listener bound to a distinct port. The public plane never shares the authed router or its pipeline — that isolation is the whole point of the split (public_web.ex:1-20).

planerouterdefault portenv to enableauthmethods
controlWorkbooks.Web4000WB_WEB=1Workbooks.Auth (see ladder)all
contentWorkbooks.PublicWeb4001WB_PUBLIC=1NONE (anonymous)GET only
content (TLS)Workbooks.PublicWeb4443WB_PUBLIC_TLS=1NONE (anonymous), per-host SNIGET only

Ports are overridable: PORT / PUBLIC_PORT / PUBLIC_TLS_PORT (application.ex:241-243). The desktop daemon (WB_DESKTOP=1) implies the control plane bound on a fixed desktop port with a single-acceptor IPv4 listener (application.ex:142-156). All non-desktop planes listen dual-stack IPv4+IPv6 (application.ex:195-196).

The content plane is anonymous by construction: it has no Workbooks.Auth plug, only GET clauses (any other method falls to 404), no Dock / commands / build / agent / secret routes, and the page it serves carries no call-home script — published content is static bytes only (public_web.ex:8-20, public_web.ex:181-188).

Auth ladder (control plane)

Workbooks.Auth runs after CORS and before :match=/:dispatch= (web.ex:15-18). First match wins; it assigns :identity (%{user_id, tenant_id, session_id}) and :tenant (auth.ex:102-106).

rungconditiontenant source:SRC:
public allowlistpath in @public (or /gk/*)dev fallback (x-tenant else "dev")auth.ex:28, auth.ex:33-37
desktop tokenWB_DESKTOP=1 + matching per-boot tokenWorkbooks.Desktop.tenant()auth.ex:62-64
shared-secretWB_PUBLIC_BEARER set + Bearer <secret>WB_TENANT (default "local")auth.ex:67-68, auth.ex:77-85
BetterAuth JWTvalid Guardian-verified beareridentity.tenant_id from claimsauth.ex:87-95
dev x-tenantunlocked AND single-tenant, no bearerx-tenant header (else "dev")auth.ex:51-57, auth.ex:97-100

No-credential behavior (auth.ex:51-57): a locked deploy (WB_PUBLIC_BEARER set) → 401, no fallback. Multi-tenant (unlocked) → 401 (isolation can't rest on a spoofable header). Dev / single-tenant (unlocked) → x-tenant fallback.

Public allowlist — always open, no tenant data (auth.ex:28):

path:SRC:
/healthauth.ex:28
/.well-known/workbooks-runtimeauth.ex:28
/.well-known/did.jsonauth.ex:28

/gk/* is not in @public but is treated as public here because the groundskeeper bridge enforces its own credential and fails closed (auth.ex:37, web.ex:104).

CORS preflight

The :cors plug runs before Workbooks.Auth so the credential-less preflight isn't rejected (web.ex:15-16). Headers set on every response: access-control-allow-origin: *, allow-methods: GET, POST, PUT, DELETE, OPTIONS, allow-headers: authorization, content-type, x-tenant, max-age: 86400. An OPTIONS request short-circuits to 204 (web.ex:28-32). Origin is wildcarded because the local engine binds loopback / a boot-token-gated guest, not the public internet (web.ex:9-14).

Handshake & infrastructure (control plane, public)

methodpathdoesauth:SRC:
GET/healthliveness, 200 "ok"publicweb.ex:35
GET/.well-known/workbooks-runtimeRCP capabilities/handshake doc (JSON)publicweb.ex:109
GET/.well-known/did.jsonthe engine's did:web identity documentpublicweb.ex:669

The handshake doc (capabilities.ex:32-44) returns: rcp ("1"), runtime (app vsn), tenancy ("single" | "multi"), auth (rung "trusted" | "oidc-jwt" derived from tenancy; issuer; jwks_url), transports (http, ws both true), and capabilities [oql, workflow, instances, workbook, agent, library, search, publish, browse, telemetry]. A client reads this before presenting a credential to learn the required rung (capabilities.ex:2-23).

OQL, workflow & workbook serving (control plane)

methodpathbody / paramsdoes:SRC:
POST/oql/parseraw Org bodyparse Org headlines through the OQL kernelweb.ex:147
POST/api/workflow{org, input}, ?plan=1run (or plan-only) a :workflow: DAGweb.ex:157
POST/api/workflow/todo{org}run a native TODO outline async (202; poll /api/brand-book/:slug)web.ex:178
GET/instanceslist registered Instances for the tenantweb.ex:200
PUT/w/:idraw Org bodydeploy a Workbook: store its Org under :id (201)web.ex:206
GET/w/:idserve the stored Workbook as a webpage (HTML)web.ex:214
POST/w/:id/call{fn, org}the Workbook's backend: parse=/=tangle=/=validateweb.ex:222
GET/w/:id/wsWS upgradelive bridge WebSocket to the Workbookweb.ex:238
GET/api/workbookslist stored workbooksweb.ex:805
GET/api/w/:id/orgstored Org source (text/plain)web.ex:810
GET/api/w/:id/htmlserver-rendered HTML (OQL.render)web.ex:816
GET/docsthe document-viewer SPA (HTML)web.ex:801

/api/workflow encode-safes error tuples instead of 500ing, and 500s on an internal rescue (web.ex:167-172).

Agent runs (control plane)

methodpathbody / paramsdoes:SRC:
POST/api/run{system, task, model?, max_steps?, exec?}start a long-horizon agent run (202)web.ex:246
GET/api/run/:idpoll a run's status + result + eventsweb.ex:277
GET/api/run/:id/streamWSlive per-tool-step telemetry streamweb.ex:270
POST/api/ctk/commitevent, ?run=<id>deliver a human CTK approval into a runweb.ex:293
GET/api/ctk/review/:idagent polls for a pending review (204 when none)web.ex:315
GET/ctkserve the CTK human-in-the-loop shellweb.ex:331
GET/ctk/*globserve CTK toolkit files (path-contained)web.ex:335

exec on /api/run is a TRUST grant (host-brokered git/publish tools, OS workdir for fs tools) — it grants no native execution, and is honored ONLY for the trusted local case (single-tenant/desktop) or explicit WB_AGENT_EXEC=1; never for arbitrary/multi-tenant callers (web.ex:254-262).

Brandnana / brand-book pipelines (control plane)

methodpathbody / paramsdoes:SRC:
POST/api/run-brand-book{domain}run the full brand-book pipeline async (202)web.ex:342
POST/api/brandnana-ask{request}free-form brandnana agent run async (202)web.ex:369
GET/api/brand-book/:slugpoll a brand-book run's stage + published URLweb.ex:405
GET/api/telemetry/:slugper-run telemetry summary (states, tool calls, time, errors)web.ex:416
GET/api/telemetrycross-run rollup indexweb.ex:730

RCP engine verbs for the wbx CLI (control plane, token-authed)

These mirror the escript's local Library.* / Toolkits.* calls but are token-authed so the thin Rust CLI can drive a running engine. Tenant comes from the credential, not a path (web.ex:435-437). Working-tree bytes cross the wire base64'd in the :zip Workbooks.Bundle format because the engine usually runs in a container on another machine (web.ex:449-454).

methodpathwbx verbdoes:SRC:
POST/rcp/buildwbx buildcompile a workspace's components → WASMweb.ex:440
POST/rcp/library/checkoutwbx checkoutborrow a library member (zip back to caller)web.ex:455
POST/rcp/library/checkinwbx checkinpack a working tree back (zip in)web.ex:473
GET/rcp/storewbx store --listlist stored archive keysweb.ex:492
POST/rcp/storewbx storearchive a workspaceweb.ex:498
GET/rcp/fetchwbx fetchrestore stored bytes (base64)web.ex:656
GET/rcp/changesthe tenant repo's git log (newest first, 30)web.ex:641
POST/rcp/key/:key_idsealed-bundle key release (access-gated, fail-closed)web.ex:122

RCP toolkit verbs (text in/out; task execution stays server-side gated by WB_TOOLKIT_EXEC, web.ex:510-514):

methodpathdoes:SRC:
GET/rcp/toolkitlist toolkits (text)web.ex:516
GET/rcp/toolkit/showshow a toolkit / a skill (?id, ?skill)web.ex:520
GET/rcp/toolkit/searchsearch toolkits (?q)web.ex:530
POST/rcp/toolkit/verifystructural + capability checksweb.ex:534
POST/rcp/toolkit/evalrun a toolkit's eval suite server-sideweb.ex:540
POST/rcp/toolkit/buildbuild a toolkit (?id, ?which)web.ex:545
POST/rcp/toolkit/signsign a toolkit for the tenantweb.ex:555
POST/rcp/toolkit/installinstall a toolkit DIRECTORY (zip b64; id-charset + zip-slip guarded)web.ex:592
POST/rcp/toolkit/runrun a toolkit task (?id, ?task, args)web.ex:626
POST/rcp/kernel/runrun a registered kernel (bytes→bytes, b64)web.ex:563

Library, search, channels & browse (control plane)

methodpathbody / paramsdoes:SRC:
GET/api/library/:tenantthe tenant's access graph (workspaces + members)web.ex:422
POST/api/library/:tenant/query{sql}cross-workbook OQL query-throughweb.ex:428
POST/api/search/:tenant{query, mode, workbook?, k?}semantic ∪ literal searchweb.ex:720
GET/api/channelslist configured messaging channelsweb.ex:741
POST/api/channels/send{channel, peer, text, parse_mode?}send a messageweb.ex:747
POST/api/channels/approve{channel, code}allowlist a peer (DM pairing)web.ex:768
POST/api/browse{url\vert query, mode, as?}fetch/crawl/search through the configured providerweb.ex:786
GET/api/browse/search?q, ?limitkeyless SERP for the desktop browserweb.ex:89
GET/api/agents/:slug/system_promptresident agent persona by slug (default if none)web.ex:67

mode for /api/search/:tenant is one of hybrid (default) / semantic / literal (web.ex:723). /api/browse dispatches on mode: fetch (default), crawl, search; asjson (default) / org (web.ex:849-872).

Identity, federation & ledger (control plane)

methodpathbody / paramsdoes:SRC:
GET/.well-known/did.jsonthe engine's self-hosted did:web documentweb.ex:669
POST/api/radicle/:tenant/publishfederate the tenant repo over Radicle (rad: id)web.ex:675
POST/api/mirror/:tenant{url} or {forge, repo?, visibility?}mirror the tenant repo to any git hostweb.ex:691
GET/api/ledger/:slugverify a run's hash-chain + did:key signatureweb.ex:713

Internal / desktop-forwarded (control plane)

methodpathbody / paramsdoes:SRC:
POST/internal/secrets/refresh{env: {KEY: val}}desktop forwards API keys onto this runtime's env (token-authed)web.ex:46
GET/socket/websocketWS upgradethe desktop bridge's Phoenix-Channels socket (v2 wire)web.ex:79
/gk/*forwardedgroundskeeper voice-agent bridge (own credential)web.ex:104

/internal/secrets/refresh is not public — Workbooks.Auth runs first, so only the local shell holding the per-boot token can set env keys (web.ex:39-45).

Catch-all

Any unmatched route on the control plane → 404 "not found" (web.ex:821-823).

Content plane routes (Workbooks.PublicWeb)

Anonymous, GET-only. The app id is resolved from the request HOST via the Workbooks.Domains registry (registered host wins, else the leftmost DNS label) (public_web.ex:294-296). Every response carries an x-served-by: workbooks-runtime header (public_web.ex:190-191) and HTML bodies get an inline <!-- Served by the Workbooks runtime … --> marker (public_web.ex:29, public_web.ex:286-292).

methodpathdoes:SRC:
GET/healthliveness, 200 "ok"public_web.ex:31
GET/_changesthe app's git log (newest first, 30) + keeper statuspublic_web.ex:39
GET/_activitylive agent activity (singleton or crew shape)public_web.ex:64
GET/*_globserve the host's published static app (clean URLs)public_web.ex:182

Static serving (public_web.ex:215-243): pages are clean-URL (/learn/workbooklearn/workbook.html), directories default to index.html, inbound .html URLs 301 to the clean form. Path-traversal safe: .. segments rejected and the resolved path is contained within the site dir (public_web.ex:274-284). Non-GET methods never match a get clause → fall through to 404 (public_web.ex:181, public_web.ex:186-188).

The content plane has no Dock, no /w/:id/call, no commands/build/agents, no secret access — none of those routes exist here (public_web.ex:8-16).

What is north-star / not yet here

Server-side compute for public apps and the full custom-domain / TLS provisioning story are intended but not the current shipped behavior. The public plane today is static content + read-only /_changes and /_activity feeds.

See also