Building

Sandbox model

What your plugin can and cannot reach, and the memory, time and network limits it runs under.

Users install plugins without reading their code. That only works if the code can't do more than it says. Here is exactly what plugin code runs inside, what it can reach, and the limits it runs under.

One fresh isolate per call

Every invocation — one tool call, one middleware hook, one lifecycle event, one channel message — runs in a new V8 isolate that is created for that call and thrown away after it. Nothing survives between calls: no module-level variables, no caches, no timers. State that must persist goes in your own storage (via host.fetch), in the agent's workspace (host.workspace), in a live connection's state, or in the settings the user set.

Your code sits behind a gRPC boundary in a separate service. It never runs inside the platform's own process.

What the isolate doesn't have

A bare JavaScript engine (ES2022): no fetch, no XMLHttpRequest, no require or import() at run time, no process, no filesystem, no Buffer, no atob/btoa, no TextEncoder/TextDecoder. The only way out is host:

Need Use
HTTP host.fetch — only to hosts you declared (egress)
Hashes, HMACs, AES-CBC decrypt, Ed25519, JWT (RS256) checks, base64, hex host.crypto (synchronous)
Settings and secrets host.settings.get / getSecret
The chat, todos, timers, other agents host.session.*, host.agents.*
Files host.workspace.*
A model host.session.llm, host.llm, host.decide
Logs host.log

For binary data, host.fetch can send and receive base64 (bodyEncoding / responseEncoding).

Secrets and tokens stay outside

  • host.settings.getSecret fetches a secret into your code only when you call it, and only a secret your plugin declared and the user bound.
  • The credential behind host.session, host.agents and host.models — a short-lived token scoped to one account (and one session when there is one) — is held by the runtime outside the isolate. Your code calls a function; the runtime makes the request.
  • Upload URLs used by host.media also stay outside; your code only sees opaque tokens.

Limits

Per invocation

Invocation Wall clock Memory
Tool, lifecycle event, web search / fetch, chat complete 30 s 128 MB
Middleware hook 5 s 128 MB
Compaction summarize 120 s 128 MB
Decision noul / choose / score 10 s 128 MB
speak / voices / transcribe 15 s / 10 s / 20 s 128 MB
Channel receive 10 s (15 s per attachment fetch, 25 s for the whole request) 128 MB
Any invocation holding host:media:transcode raised to at least 60 s raised for media
SSH hooks using host.ssh.command raised to cover the SSH connection 128 MB

The wall clock is a real timer: it also counts time your code spends waiting on host.fetch or other host calls. When it runs out, the isolate is destroyed mid-flight.

Per call to host

Surface Limit
host.fetch 60 requests per minute per installation (rolling), 10 s timeout, 5 MB response
host.session.sendError 5 per invocation
other host.session.*, host.agents.*, host.models.* operations no per-invocation count (bounded by your wall clock)
host.crypto.* 200 operations and 8 MB of input per invocation; 1 MB of data per call
host.session.llm.complete 5 per invocation
host.llm.complete 5 per invocation (counted separately)
host.decide.* no per-invocation cap (bounded by your wall clock)
host.media.* 10 per invocation
host.ssh.command 10 per invocation, 60 s default / 90 s max each
host.connection.send 20 frames per call, 20 calls per invocation

Per plan (per account)

Free Solo Entrepreneur
Plugin wakes per hour (each channel receive, each live-connection open / receive / timer) 60 600 3,000
Live connections held at once 1 5 20

What happens when your code fails

Faults are one of: timeout, memory limit, bad request (your handler returned the wrong shape), or crash (it threw; the message is cut to 500 characters).

The platform is built so a broken plugin degrades one feature, never the agent:

Capability On failure
Middleware Skipped — the value passes through unchanged and the chain continues ("fail-open"). A handler that returns the wrong shape is skipped the same way.
Lifecycle event Retried with exponential backoff (30 s base, up to 8 attempts). The install or chat that triggered it is never blocked.
Tool The error becomes the tool's result; the model sees it and the turn continues.
Web search / fetch provider The model sees a "provider failed" tool error. It is not silently swapped for the built-in engine.
Compaction The built-in summarizer runs instead and a notice is posted in the chat.
Model provider Treated as the vendor being unreachable; use tools.fail(…) for a precise error.
Channel send Retried for a few minutes; throw an error containing [rejected] to stop retrying.
Channel receive A timeout is retried; any other error is final and logged.

Network boundary — precisely

Outbound HTTP from host.fetch is deny-by-default: only hosts you declared in manifest.egress, plus any the installer added through a hosts setting, are reachable — and the installer sees that list before installing. The check is by host name (and port) against that list.

Live connections are dialled by a separate service that additionally refuses private, loopback, link-local and cloud-metadata addresses and re-checks the address it actually connects to.

Being a good citizen

  • Keep middleware fast. It runs in the middle of an agent's turn with a 5 s budget; a slow hook delays every reply on the account and is skipped anyway when it runs out.
  • Make lifecycle handlers idempotent — they can run more than once.
  • Log sparingly with host.log (debug, info, warn, error). Logs show on the installed plugin's page and are kept for 24 hours.