# 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
<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** content-blindness at rest, 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 and never
  sends — the server stores only its commitment, and reads are content-blind (the SDK opens the sealed ledger locally).
  See [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](./delegated-authority.md).
- **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](./room-link-auth.md).
- **Idle-cheap persistence.** A quiet Space is ciphertext at rest, 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](./delegated-authority.md) · [Private lanes](./private-lanes.md)), 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](./on-prem.md).

## 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),
  [Private lanes](./private-lanes.md), and [The attested tier](./the-attested-tier.md).

Read next: [Quickstart](./quickstart.md) for the raw `/space` endpoint underneath, and [Platform](./platform.md) for
the runtime this rides on.
