SecurityTrust boundaries, prompt-injection defenses, grant enforcement — and an honest list of the gaps.

The agents system runs untrusted model output against real credentials and a real network identity. This page describes what defends that, and what does not.

Trust boundaries

Six of them (agents/docs/security.md:6-13): desktop ↔ agents server, agents server ↔ model providers, agents server ↔ hypermedia/content servers, agents server ↔ SQLite, desktop renderer ↔ daemon signing API, and agent runtime ↔ sandbox microVM.

Authorization

Every action is a signed envelope (Protocol); the signature is checked against account_authorizations for the OWNER or AGENT role. Account isolation is enforced in the queries themselves — the pattern WHERE account_id = ? AND id = ? appears 58 times in api-service.ts.

Two reads are scoped by account_id alone: thread: and run: addresses (readThreadAddress, readRunAddress). One agent can therefore read another agent's transcripts and run journals on the same account. This is called out in the source docs as a possibly-unintended isolation gap rather than a deliberate design.

Prompt injection

The runtime frames untrusted text for the model — <user_action>, <user_action_result>, <plan_state>, <space> — and framing is only safe if content cannot close its own frame. escapeActionFraming (agents/src/api-service.ts:9478-9480) escapes < in every payload placed inside a frame:

function escapeActionFraming(text: string): string { return text.replace(/</g, '\\u003c') }

Applied at three sites: plan step ids and labels (which are model-authored text handed back to the model), and both user-action tags. A step label crafted to be the closing tag is neutralized, not censored.

The promotion intersection. Touch-expand promotes tools by scanning durable events — which means a model can durably record any tool name it hallucinates. The filter is the whole defense (:4425-4437):

> SECURITY: promotion must never exceed the enabled callable set — a hallucinated call {tool: 'bash'} durably stores that name, and an unfiltered allowlist would hand it to Pi, activating Pi's own host bash/edit builtins outside the sandbox.

Relatedly, the Pi session is constructed with noTools: 'builtin', disabling Pi's own built-in tool suite entirely. The only executable surface is Seed-owned tools.

Grants

The grant model is small on purpose: a callable set plus a publish grant. Two enforcement properties matter more than the list:

    A delegated child's tools can only narrow authority, never widen it.

    An authored lambda requires the same execute grant its runtime needs, so writing a tool is not a way around disabled code execution.

Memory writes are ungated by design; publishing to the network is not.

The sandbox

Code execution runs in hardware-isolated microVMs, .security('restricted') and .ephemeral(true), with network egress restricted to public destinations (NetworkPolicy.fromProfiles(['public'])) — private, link-local, and metadata addresses are blocked. Timeouts clamp at 300s with a 30s grace on the sandbox lifetime; stdout and stderr are each bounded at 64 KiB.

The script engine is a separate, tighter box: no network, no clock, no randomness, a memory ceiling and a fuel limit.

Secrets

AES-256-GCM, fresh nonce per write, and no plaintext in any API response (Providers). The logging rules are explicit about what must never be written: plaintext secrets, decrypted keys, provider config, signed request bodies, full prompts or responses, full session content, and large tool outputs.

Known gaps

Stated plainly, because a security page that only lists defenses is marketing:

    No nonce cache. A captured signed request is replayable for the full 30-second skew window.

    No KMS or OS keychain. The AES key that protects the secrets lives in the same SQLite database as the ciphertext.

    No SSRF protection for web-tool fetches or custom provider base URLs, beyond enforcing http(s).

    No per-account code-execution concurrency limit, and no cost or token budget for runs — only wall-time, depth, fan-out, and concurrency caps.

    Effects are at-least-once. A ctx.call interrupted between execution and journaling re-executes on resume; a write could double-apply.

    The /agents inspector is unauthenticated and exposes account ids, session titles, and event payloads. Binding it to localhost or putting auth in front of it is an open hardening item.

    thread:/run: reads cross agent boundaries within an account, as above.

    Memory has no size quota. Earlier documentation claimed 1 MiB/100 MiB/1 GiB caps; those were false and the source doc now says so. Only path length (512 bytes) and depth (16) are bounded.

Trigger consent, and why it is the next thing

A trigger is standing authority to act without a person present. The designed document model makes activation a user-only step — an agent may write a trigger, but it lands as a draft and only a user action makes it live. The reasoning is the sharpest security argument in the codebase: an agent that could grant itself standing authority — including on the strength of a web page it just read — is an agent with an unbounded prompt-injection surface. That consent step is designed and not yet built.

Related

Do you like what you are reading? Subscribe to receive updates.

Unsubscribe anytime