Witbitz docs PlatformTrustAll docs

Async Spaces — the agent is a function, not a server

The first Witbitz Spaces are async: a persistent room that lives over weeks, where the AI is a function invoked per turn, not an always-on process. Between turns the Space is cold ciphertext — so an idle Space costs ≈ $0. This is the model the Spaces app runs on. Live in production.

See also: the Space link is the auth (membership + identity) and the double blind (why the platform can't read a Space).


1. Agent-as-function

A live call runs the agent as a container for the call's whole life. An async Space instead invokes a turn function only when a present member acts:

A member posts → the transport invokes the turn with the member's mk → the function opens the sealed ledger, asks the model, appends the reply, re-seals, stores, and drops mk. Then the Space is cold again.

No held socket, no always-on compute → idle ≈ $0 (the reason to prefer poll/push over held connections; cost model in the repo's async-rooms-cost.md). It stays on AWS (DynamoDB ledger + S3 blobs + Bedrock/LLM in-boundary) so the ciphertext and the inference share one trust boundary.

2. The sealed ledger

A Space's conversation is one snapshot sealed under mk and stored as ciphertext (sessionStore.putLedger/getLedgeragent/envelope.mjs seal/open). It is sealed to the member key only — there is no operator recovery key. The platform holds no key; at rest it is opaque bytes. This is the persistent Space memory.

3. The turn — a decrypt-once render

agent/asyncTurn.mjs runAsyncTurn is the render, and it is the one place plaintext exists:

  1. access controlmk arrives from a present member's client; agent/spaceTransport.mjs checks commit(mk) === the Space's stored commitment (proves membership + that this turn seals to the ledger's key).
  2. attribution (for a signed Space) — verify the turn's signature against the member grant, set the verified author (room-link-auth §4).
  3. decrypt-once — open the sealed ledger, append the incoming entry, run the brain (its tools may produce widgets/artifacts), append the reply, re-seal, putLedger.
  4. drop mk — the key is never held past the turn.

Between turns nothing runs and nothing is readable.

4. Reaching members — poll, content-blind

agent/spacePoll.mjs is the read side, and it stays blind on the common path:

The sealed blob is never sent to the client — the client is thin (sends turns, receives rendered plaintext, never touches ciphertext or crypto). Deferred: reaching an away member whose app is fully closed (polling needs a running app) — real-time push/email is a later cut.

Reads are gated too, for a private Space. On an admission:'email' Space the poll (and every content op — title, state, pending, decide, import) requires an allow-listed Google sign-in before the server will decrypt-once and return anything — the room key alone no longer reveals the history. The server is the plaintext chokepoint (it decrypts in-use), so it enforces the allow-list on reads exactly as on writes (room-link-auth §4). Live + proven in prod: a token-less poll to an email-gated Space returns 403. An open Space is unchanged — the link reads.

5. The ops

One HTTP surface (agent/spaceHandler.mjsagent/spaceService.mjs), at /space:

op what it does
create register the public Space record (its key commitment + config + gk); the client mints room+mk, the server never sees mk
turn a member posts; the render replies (or proposes a high-stakes tool call — see delegated authority)
poll the content-blind read (etag → plaintext delta); on an email-gated Space it requires an allow-listed sign-in
decide approve/deny a pending proposal (delegated authority)
pending list pending proposals
import seed the Space from a public ChatGPT share link (SSRF-guarded)
title / state a sealed room title; the shared co-edited widget doc

6. What runs inside

A Space's capabilities are ordinary tools in its config (e.g. search_places → a shared map widget, search_flights → a flights widget). The turn runs the normal brain tool-loop over them. The one async-specific twist: a high-stakes tool call is deferred to a human approval instead of running in-room — that is delegated authority.

7. Code map

Concern Where
Turn render (decrypt-once) agent/asyncTurn.mjs (runAsyncTurn, renderLedger)
Access control + mk lifecycle agent/spaceTransport.mjs, agent/spaceService.mjs (handleSpaceRequest)
Content-blind poll agent/spacePoll.mjs (etagOf, pollSpaceView)
Sealed ledger agent/sessionStore.mjs, agent/envelope.mjs (seal/open/commit/newRoomKey)
HTTP handler + record store agent/spaceHandler.mjs
Thin client spaces/public/spaceClient.js, spaces/public/space.html
Delegated authority agent/delegatedAuthority.mjs (doc)

8. Status

Live in production: the /space zip Lambda (agent brain as a function) + the Spaces app at witbitz-spaces.pages.dev. The turn core, transport (mk-commit), content-blind poll, delegated authority, and per-member signed identity (solo) are all shipped. Deferred: real-time away-notification (closed app); compaction of long ledgers; the couple (2-of-2) signed path.

Machine-readable source: async-spaces.md · every doc in one fetch: llms-full.txt · ← the-owner-rule · the-double-blind