Witbitz docs HomeTrustAll docs

Approvals — nothing effectful runs until a person says yes

The assistant can propose; only a member makes it happen. A tool not in immediateTools, a write to the store, a private lane's message into the shared room, a call on a connected service: each is parked as a proposal — a card a member approves or denies — and nothing happens until they do. The one exception is an MCP tool a room was granted to run on its own (connection calls). This page is that round trip.

Part of the SDK · every call: the reference.


Proposals, end to end

A proposal is { id, kind, action, args, text, approve(opts?), deny(opts?) }. kind is 'action' — a tool call — or 'message', a private lane's message into its shared room; action is the tool's name, and args the whole parked call, type included. data_put and data_delete are always proposals.

In a shared room, the server runs the action on approval:

javascript
const space = await wb.createSpace({ agent: { name: 'Ops', instructions: 'Track tickets.' }, data: { tickets: { title: 'string' } } })
space.on('proposal', (proposal) => showCard({
  title: proposal.action, args: proposal.args,
  onApprove: () => proposal.approve({ from: 'Ann' }),   // your card's buttons
  onDeny: () => proposal.deny({ from: 'Ann' }),
}))
space.on('resolved', (e) => hideCard(e.id))
await space.send('Open a ticket: printer on 3 is down')   // the assistant calls data_put: a card

showCard and hideCard are your page's own interface.

The approved write then arrives as a message (kind 'data') and a 'data' event. 'resolved' carries { id, approved, action, result }, where result is what the server did; result.ok === false means it did not go through (a message the room refused, for one).

In a private lane, your page runs the action on approval:

javascript
const lane = await space.privateLane({
  agent: { name: 'My assistant' },
  actions: [{ name: 'book_room', description: 'Book a meeting room', input: { room: 'string', at: 'string' },
              run: async (act) => api.book(act.room, act.at) }],   // act = { type: 'book_room', room, at }
})
lane.on('proposal', (proposal) => showCard({
  title: proposal.action, args: proposal.args,
  onApprove: () => proposal.approve({ from: 'Ann' }),   // the server records the approval, then the SDK calls run(act)
  onDeny: () => proposal.deny({ from: 'Ann' }),
}))
lane.on('resolved', (e) => console.log(e.result))
await lane.send('Book Kyoto for 3pm')

run()'s return value comes back as result, on 'resolved' and in approve()'s answer ({ error } if it threw); it is not sent to the assistant. A lane's message into the shared room (kind: 'message') is posted when approved.

Actions your page runs

A private lane's actions are your page's own functions, offered to its assistant as tools. The assistant can only propose them; when the member approves, the runtime records the approval and the SDK runs your run() handler on the member's device — the runtime never runs it, so the credential and the side effect stay with you. That is the part that is hard to build safely by hand: the propose → approve → run state machine, the record of who approved what, and the guarantee that the assistant cannot act alone.

An action can also be a read. Give it mode: 'await' and the assistant may call it during the turn for something only the device knows; the SDK runs your handler and hands its return value back to the turn (within 25 s). It needs no approval, because it changes nothing.

How to offer actions for one turn only, or change the standing set: private lanes. The limits on actions: Reference.

Deciding by id

javascript
const waiting = await space.pending()                 // checks now → the waiting cards
await space.approve(waiting[0].id, { from: 'Ann' })   // the same as the card's own approve()
await space.deny(id, { from: 'Ann' })

approve(id, { recordResult: false }) is for a connection call whose answer you hand the assistant yourself: the room records the call — who approved it, what it was, how it went — but not the answer.

Who may decide

In a room with admins, a member who is not one cannot approve or deny a card that would do something they may not do themselves: an admin-only row, or a connection call that spends the room's connected account. The room refuses with not_admin, and the card stays pending for an admin. A member's own lane is theirs to decide.

Connection calls

A call through an API-key or OAuth connection is always a proposal a member approves. An MCP tool is too, unless it is in the connection's auto set — chosen by whoever grants the connection to the room (the member in the Spaces app, or your page with grant(conn, { auto })); by default it is the tools the server itself marks read-only, a claim the server makes and Witbitz does not verify. An auto call still goes through the Sentinel: the model never sees the credential, and the connection's host and scope still apply.

The Sentinel adds the secret and makes the call; see connections and MCP servers.

Machine-readable source: sdk-approvals.md · Generated 2026-10-27T07:05:42Z · build f301388c · every doc in one fetch: llms-full.txt (HTML)