Skip to main content
An app is one file: app.tsx, a single default-exported React component. The row that owns it is an AppDocument with format vendo/app@1, and every user owns their own copy — import, fork, sharing, and publishing mint a fresh app_ id, so artifacts carry no data, grants, or authority. Zero config: layer 1 needs nothing. createVendo() mounts the apps block unless you pass apps: false, and a screen renders in the host-embedded renderer with no sandbox account, no key, and no flag. Layer 2 is gated on one thing, a configured sandbox adapter.

The screen

A screen is plain TSX with a small, closed surface:
Four rules, and each one is enforced rather than advised:
  • Two imports, no more: react (its hooks) and @vendo/screen. There is no bundler and no node_modules inside a screen.
  • Data through useQuery("tool_name", input?). The tool name and the input are written-out literals, because a screen’s queries are read out of the file and executed before the component renders — so no prop, state, or other query’s result can reach one. Only read tools, one result per tool.
  • Actions through tools.tool_name(args), from a handler. A tool call in the render body would fire on every render, so it is refused.
  • No DOM. document, fetch, timers, process and <div> do not exist here. Layout is <Stack>/<Row>/<Grid>; text is <Text>.
What @vendo/screen exports is exactly the component Kit plus the components your host registered — so a name the screen renders is a name your app really has. Props are checked against each component’s own schema. A screen stores no tree. Its tree is what rendering it produces, so the screen is re-run on every open: the queries resolve against the world as it is that instant, and the payload carries today’s numbers. There is no snapshot to go stale.

The checks

Every save faces the same five stages, whoever wrote it — the generation loop, Claude Code, or a person with an editor:
  1. Compile — it has to be valid TSX.
  2. Scan — the two rules a compiler cannot state: the import surface, and the query/tool discipline above.
  3. Type-check — the real TypeScript compiler, against declarations derived from the Kit’s schemas and your tools’ own input and output schemas. A misspelled response field or a wrong payload key is a type error.
  4. Run once — the query plan is executed for real and the screen is booted on the answers a tool actually gave.
  5. Tree — the rendered tree is validated, and every component in it must be one the host registered.
Each stage is the next one’s precondition, and the first one that finds something is the last one that runs. A refusal is a repair instruction naming the line and what to write instead. Nothing paints and no row lands: the last good screen keeps serving. One AI reviewer pass runs on top, over the TSX and the rows its queries really returned.

The receipt your agent gets

Whoever called vendo_make gets four fields of words, never pixels:
That is the whole contract, not a summary of a richer one. An earlier version handed the agent the whole document, and a model handed a tree eventually talks about the tree: narrating a screen it has not seen, to someone looking at a different one. So the receipt is deliberately unusable for narration. Say say, close to verbatim, and stop. "failed" means the bindings claimed data your host does not return, or the props did not type-check. "partial" is the one a host branching on status most needs: a half-built app reads as half-built instead of as plain success. In process the call returns fast. The first streamed view part carries the app’s permanent id, so your loop gets a vendo/app-ref@1 (envelope contract) while the build streams over the wire.

Where the screen lands

By default a screen lands in the person’s own list of views. A slot puts it in your product instead, and a slot is your markup:
Add @vendoai/ui as a direct dependency. Empty, the slot renders your children untouched with no wrapper, so you can inline one anywhere. Filled, it shows the build skeleton, then the live view, then a failure with a retry. Never a blank hole. One view per slot per person, and a second view evicts the first. When a person presses a guarded button inside the view — one that parks on the approval guard — VendoSlot auto-mounts the approval modal centered over the page. Nothing to wire. If you render a bespoke slot with AppFrame or TreeView, use useApprovalModal to mount it yourself. Your agent aims at a slot by id:
Or you place it yourself, from the appId in the vendo/app-ref@1 envelope your loop already received:
Building a document or block editor? That is this shape. One <VendoSlot> per block, with the block’s own id as the slot id. A generated view then lives in a block like any other: it moves when the block moves, it is scoped to the person with that document open, and your editor keeps owning layout.
Slot ids are yours, and nothing enumerates them for an agent. Tell the agent the id, or let the person say it. That is why the prompt block forbids inventing one.

Moving a screen after the fact

“Put that on my dashboard”, said about a view they already have, is vendo_apps_pin. It answers with what it displaced, so your agent can say what moved:
vendo_apps_unpin clears the slot, and the view itself is untouched. Both are writes, so under a cautious policy the first one parks in your approvals queue, where the person sees the tool, the view, the slot, and the exact arguments before anything moves. Routes: HTTP routes.
Both pin tools reach an agent over the MCP door only. The in-process tool pack carries no vendo_apps_* tool. On that path you move a view from your own code: write the app id into your own record and re-render the slot.

The three layers

  1. Screen app: no server. app.tsx, rendered by the host-embedded renderer, interactive through React state and guarded host tools. Most apps stay here. Server-shaped needs (schedules, away runs) usually ride an automation on the same app, not a machine.
  2. Screen app + machine: the same screen plus a persistent per-app sandbox where execution lives — custom server code, third-party egress with secrets, heavy logic, working data. The machine never draws UI. Gated by one thing: a configured sandbox adapter (createVendo({ sandbox })). There is no flag beside it.
  3. Machine everything: the machine also serves a real web app and the host embeds its URL as the app surface. Same sandbox gate; see Layer 3 reachability and venues for what it additionally needs and its current caveats.
The agent escalates when an instruction demands it, preferring the cheapest rung that can express the work: a steps automation (deterministic tool calls on a trigger, created in seconds, no machine), then an agentic automation (per-run judgment, still tool-only), and a box machine only when actual custom code is required. With machines off (the default), a request only a box can express refuses with a typed error naming the flag; apps that already carry a machine keep working. Only new graduation is gated. Users do not choose a layer. The last working surface keeps serving while the next layer builds. Screen-only creates and edits run without an approval card because they cannot reach host tools unguarded, cannot perform egress, and render in the sandboxed surface. Edits that add server behavior stay approval-gated. See Effective risk resolution. A screen renders in a sandboxed surface by default. An approved version of an app can also render in the host page; see In-client venue and approvals for how approvals are recorded, how the version hash is pinned, and how new versions drop back.

The machine

A graduated app owns one persistent sandbox. It sleeps as a snapshot, wakes in about a second when poked, and auto-sleeps after five minutes idle. Vendo owns only the boundary of the box (the environment in, HTTP on $PORT out, and a vendo.json manifest); inside it, any language and framework works. A coding agent lives in the box and does every server build and edit there, verifying its own endpoints before reporting done. The box holds no host credentials. Its single authority path is the /box callback surface on the host’s Vendo server, authenticated by a per-app bearer token: durable rows over plain HTTP, and host tools through the same guard-bound registry chat uses, with approvals and audit intact. Anything the box wants beyond that boundary, a secret value or an outbound domain, is an approve-once owner grant. See Secrets and outbound requests for the exact grant and injection rules.

Progressive generation

A build writes a plan first, and the plan’s skeleton is on screen within seconds. The screen itself paints once — when it is saved and the five stages have passed — replacing the skeleton in place under the same stream id. The stock ai-SDK client reconciles the payload rather than appending duplicates. You do not need to change your client. Existing consumers of data-vendo-view parts see the skeleton followed by the finished screen under a single stable part id.

Graduation

Layer 1 to 2 is invisible and additive. When an instruction needs server capability (a schedule, egress with secrets, heavy logic, app-owned state), the runtime provisions a machine, sends the build to the in-box agent, syncs the box’s vendo.json declarations, and parks any needed egress approval. The existing screen keeps serving throughout. Layer 2 to 3 is an honest UI rewrite in the same box, gated behind the experimental served-apps flag. The box agent builds a real web app beside the /fn endpoints, the screen keeps serving through the whole build, and the document flips to ui: "http" only after the host itself verifies the served root answers. Opening a served app wakes the machine and embeds its public URL in a sandboxed iframe; the wake latency is the loading state, and the URL is only valid for the current wake. Failure handling for both hops is specified in Graduation rollback.

Wire reference

The exact flags, environment contracts, and failure rules behind the model above.

Component limits

These three bound a stored wire document’s generated components, and the document validator enforces all three. A generated app carries at most 16 generated components, each at most 64 KB of source, and 256 KB across all of them together. An app.tsx screen has no byte budget — it is measured for compilation, types, and one real render, never for size.

Environment

The app machine environment lists the exact variables injected into the box and the /box callback routes.

Layer 3 reachability and venues

Layer 3 has no gate of its own: it is a narrowing of layer 2, so a configured sandbox adapter is the only opt-in. What it additionally needs is a door to be served THROUGH — the mounted wire (createVendo().handler, which answers /apps/:appId/serve/**) plus a VENDO_BASE_URL to make that URL absolute. A deployment missing either is told so before it plans: the layer-3 lane is shut, and “this host cannot serve its own web pages for an app” arrives as a plain line in the plan instead of a build that runs and then fails. Every served app — the owner’s own included — is opened at that authenticated proxy URL, never at the sandbox provider’s public ingress. The proxy re-checks can(viewer) against live rows on every request, so a revoke bites the next one, and it wakes the machine only after that check passes. Layer 3 works on both venues: BYO e2b (sandbox: e2bSandbox(), which reads E2B_API_KEY as its credential) and the Vendo Cloud hosted sandbox. Cloud ingress is a single-label hostname on the existing *.vendo.run certificate: https://<id-suffix>-m.vendo.run, where <id-suffix> is the machine id minus its two-character prefix. The console mints that canonical-port URL; for any other port the SDK rewrites the host string SDK-side, in your Node process, inserting the port before the suffix as <id-suffix>-<port>-m.vendo.run.

Graduation rollback

A failed server build rolls back to the pre-edit snapshot; a failed screen rewrite keeps the working screen and reports the miss. In both cases the last working surface keeps serving.

Secrets and outbound requests

Machine egress is deny-by-default at the provider network layer, and both secrets and egress are approve-once owner grants:
  • An app declares secret names. Each declared secret needs one owner approval before its value enters the box environment. Only declared and granted secrets inject, at provision and at the env re-injection before each in-box edit. An ordinary wake resumes the snapshot’s environment, so a grant or revocation decided while the machine slept fully lands at the next edit or re-provision. Grants are never carried by shares, forks, or publishes.
  • An app declares the domains it talks to in vendo.json. Each declared domain parks one approval card; a machine never provisions or wakes with an unapproved declared domain. An app that declares nothing can reach only its own boundary (the host callback origin and the inference endpoint).
The egress allowlist, unlike the environment, is re-applied at every wake: a domain grant decided or revoked while the machine slept counts at the next resume. A redaction guard scrubs known secret values from everything that crosses back out of the box. This is the SSRF and exfiltration answer, including for a bring-your-own model key: the key sits in the box, and the box can only talk to approved domains.