# 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 ```html ``` 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](#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`, so **`mk` stays on the device for reads** — it opens the sealed ledger locally. An agent **turn** is the one exception: it hands `mk` to 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](./the-attested-tier.md) closes. See [Async Spaces](./async-spaces.md), [Trust model](./trust-model.md), and [Verify it yourself](./verify.md). - **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](./trust-model.md#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](./identity-and-admission.md). - **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](./async-spaces.md). 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. ```js 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](./tools-widgets.md). ## 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. ```js 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](./trust-model.md#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 a `shareLink` opens 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 exposes `space.key` so you can construct links yourself). - **`app`** — an optional tag recorded with the Space. - **On-prem / your own model, keys, and IdP:** point `endpoint` at your deployment. See [Deployment options](./deployment-options.md) and [On-prem](./deployment-options.md#your-boundary). ## 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](https://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](https://witbitz-spaces.pages.dev/confidential-example.html)**: 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](https://witbitz-spaces.pages.dev/approvals-example.html)** 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 `import` from the URL above pulls its dependencies from that origin automatically, so your page's CSP must allow it (`script-src` and `connect-src` for your `endpoint`). - The reference `viewer` is the fastest way to see a Space; a production app typically builds its own UI with `on('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](./identity-and-admission.md) and [The attested tier](./the-attested-tier.md). Read next: [Vault Spaces](./vault-spaces.md) to build with the reference Spaces, and [Platform](./platform.md) for the runtime this rides on.