The SDK — a Witbitz app in an afternoon
Witbitz is a runtime: a shared room with humans, an AI agent, sealed persistent state, identity and authority. The SDK is the small, stable surface over it. The trust machinery — the room key that never reaches the server, content-blind reads, the op-dispatch, the poll loop — disappears, so a static page can stand up a private, agent-backed, link-shared room in a few lines.
Status: public beta. Usable today from any static page; the surface below is small on purpose and meant to stay stable. It wraps the exact reference client the production Spaces app runs on — nothing is forked or reimplemented.
The whole thing
<script type="module">
import { Witbitz } from 'https://witbitz-spaces.pages.dev/witbitz-sdk.js'
const wb = new Witbitz()
// A private, agent-backed room. The link IS the auth — share it to let someone in.
const space = await wb.createSpace({
agent: { name: 'Helper', instructions: 'You help a small group plan and decide together.' }
})
space.on('message', (m) => console.log(m.from, m.text)) // yours, others', and the agent's — all arrive here
await space.send('Plan us a weekend in Kyoto') // the agent answers on its own turn → via on('message')
console.log('Share this:', space.shareLink)
</script>That is a complete, multiplayer, private AI application. No backend of your own, no key handling, no database.
The surface
Three calls. That is the whole API.
new Witbitz({ endpoint?, viewer?, app? }) |
A client bound to the Space endpoint. Make one, reuse it. Defaults to the public production endpoint. |
await wb.createSpace({ agent, tools?, immediateTools?, model? }) |
Mint a Space and return a live Space. agent = { name, instructions, tools?, model? }. |
await wb.openSpace(link) |
Join a Space from a share link (or the current page URL, if it is one). The link's fragment carries the key; it never touches the server. |
A Space:
space.shareLink |
The link to hand someone — the link is the admission capability. |
space.room |
The server-visible room id. |
space.on('message', cb) |
cb({ from, text, self, id, ts, widgets? }) for every entry — yours, other members', and the agent's (self: true). On an opened Space, the existing history replays here first. |
await space.send(text, { from? }) |
Post a message. The agent decides whether to answer and does so on its own turn; the reply arrives via on('message'), not as a return value. |
await space.sealedProof() |
What the server actually stores for this room — the opaque, sealed ledger, plus its size, hash, and who can decrypt it (the operator never can). Lets your app prove the operator can't read the store, not just claim it. null before the first message. |
await space.privateLane({ agent, actions? }) |
Attach a private assistant lane for the current member — see Private lanes and human-approved actions. → a Lane. |
space.on('error', cb) |
Transient poll errors (non-fatal; the loop keeps going). |
space.close() |
Stop streaming and release the handle. |
What you get, and what's handled for you
Standing up a Space gives your app, for free:
- Privacy that's verifiable, not promised. State is sealed to a room key the SDK derives on the client; the server
stores only its commitment and never retains the key. The SDK reads via
pollsealed, somkstays on the device for reads — it opens the sealed ledger locally. An agent turn is the one exception: it handsmkto the server-side render for the transient decrypt-once that runs the turn, then the render drops it — that single instant is what the attested tier closes. See Async Spaces, Trust model, and Verify it yourself. - An agent that participates, not one that rules. The agent answers turns and can use tools, but high-stakes actions become proposals a human approves — it never holds the credential. See Delegated authority.
- Admission in the link. The share link is a self-contained capability; there is no account server to sign up against. For allow-listed rooms, add an email gate (see below). See The Space link is the auth.
- Idle-cheap persistence. A quiet Space is sealed bytes on disk, not running compute; the agent is a function invoked on a turn, not a daemon. See Async Spaces.
You never touch: the room key, envelope sealing, the /space op-dispatch, the poll/etag loop, or endpoint resolution.
Tools and human authority
Give the agent platform tools by name; the ones in immediateTools it may call directly, the rest become proposals a
member approves before they run.
const space = await wb.createSpace({
agent: { name: 'Travel', instructions: 'Plan trips; put options on the shared map and list.' },
tools: ['search_places', 'show_places', 'add_place', 'search_flights', 'show_flights', 'set_itinerary'],
immediateTools: ['search_places', 'show_places', 'set_itinerary'] // read/show freely; anything effectful is a proposal
})The full catalogue (places, flights, itinerary, chart, photo, read_file, read_url, write_pdf) is in
Tools and widgets.
Private lanes and human-approved actions
Attach a private assistant lane to a shared Space with space.privateLane({ agent, actions }). The lane is the
member's own room: its agent sees the shared room's messages and thinks/drafts with the member privately, but can post
into the shared room only through a crossing the member approves. Give it actions, and it can only ever propose
them — nothing runs until a human approves, and the action then executes on the member's device from your own handler.
The agent never holds the credential.
const shared = await wb.createSpace({ agent: { name: 'Team' } })
const lane = await shared.privateLane({
agent: { name: 'Ops assistant', instructions: 'Help me run the store. Propose refunds and emails; I approve.' },
actions: [
{ name: 'issue_refund', description: 'Refund a customer',
input: { type: 'object', properties: { customer: { type: 'string' }, amount: { type: 'number' } }, required: ['customer', 'amount'] },
run: (a) => myBackend.refund(a.customer, a.amount) }, // ← runs ONLY after the member approves
],
})
lane.on('message', renderPrivateThread)
lane.on('proposal', (p) => showApproveDeny(p)) // { id, kind:'message'|'action', action, args, text, approve(), deny() }
lane.on('resolved', ({ approved, action }) => { /* … */ })
await lane.send('Refund Jane $40 for the late order, and email her an apology')A Lane is a Space (its private thread streams on message), plus:
await lane.send(text) |
Talk to your assistant privately. Anything it wants the shared room to see, or any effectful action, it must propose. |
lane.on('proposal', cb) |
cb({ id, kind, action, args, text, approve(), deny() }). kind:'message' is a drafted crossing to the shared room; kind:'action' is one of your actions. Nothing happens until approve() (posts the crossing / runs your run handler on the device) or deny(). |
lane.on('resolved', cb) |
cb({ id, approved, action, result? }) after a decision. |
Effectful actions execute on the member's device from your run() handler — the runtime records the approval but never runs them itself, so the credential and the side-effect stay on your side. This is the delegated-authority model (Delegated authority), one method call away. It's the piece that's genuinely hard to build safely by hand: a propose→approve→execute state machine, an audit trail, and a hard guarantee the agent can't act unilaterally.
Configuration
endpoint— the Space API. Defaults to the public production endpoint (https://api.witbitz.chat/v1/space), a self-authenticating endpoint: a Space proves itself by its key-commitment, not a tenant account.viewer— the origin ashareLinkopens in. Defaults to the reference Spaces app, so a link Just Works. Point it at your own origin to open Spaces in a viewer you build (the SDK also exposesspace.keyso you can construct links yourself).app— an optional tag recorded with the Space.- On-prem / your own model, keys, and IdP: point
endpointat your deployment. See Deployment options and On-prem.
Try it
A complete example app (create-or-join a shared assistant room, ~40 lines) is live at
witbitz-spaces.pages.dev/sdk-example.html — open it, then open the
share link in a second tab to see the same sealed room from another "member." Its source is sdk-example.js on the
same origin.
A second example shows what the SDK uniquely makes easy — The Confidential Room:
talk to an AI advisor about something you'd never paste into an ordinary chatbot, while a live panel fetches
space.sealedProof() and shows the server holds only an opaque envelope, sealed under a key it never receives.
Building this safely without Witbitz means end-to-end encryption, a key model where the server can't hold the key, and a
way for a user to audit it — here it's the runtime, and the proof is one method call. A normal AI product asks you to
trust it won't read your data; this one lets a user check.
And The Ops Desk shows human-in-the-loop authority: a
private assistant that can draft real actions — refunds, emails — but can never run one on its own. Each is a
proposal you approve, and only then does it execute, on your device, from your handler. The agent never holds the
credential. It's a privateLane({ actions }) plus on('proposal') + approve().
Notes and limits
- The SDK is a single ES module; cross-origin
importfrom the URL above pulls its dependencies from that origin automatically, so your page's CSP must allow it (script-srcandconnect-srcfor yourendpoint). - The reference
vieweris the fastest way to see a Space; a production app typically builds its own UI withon('message')+send, exactly like the example. - Email-gated admission, private per-member lanes, and running a Space inside the attested enclave are supported by the runtime; the thin surface above covers the open-room path first. See Identity and admission and The attested tier.
Read next: Vault Spaces to build with the reference Spaces, and Platform for the runtime this rides on.
Machine-readable source: sdk.md · Generated 2026-08-29T18:46:30Z · build f3ff88cc · every doc in one fetch: llms-full.txt (HTML) · ← api-reference · embed →