Skip to Content
Platform
BuildThe sandbox

The sandbox

Each task runs in its own isolated Linux sandbox with a /workspace, a fixed toolchain, and only the network you bind.

A sandbox is the machine one task runs in. It is torn down after 10 minutes idle by default; the next message starts a fresh one with the full history.

You need: a recipe, and introspection login to bind hosts and variables.

What it can reach

Only bound hosts, over HTTPS. Anything else fails with 403 Destination host not on session allowlist. Allowed by default: github.com and api.github.com (granted repositories), registry.npmjs.org, pypi.org, files.pythonhosted.org, slack.com, files.slack.com, mcp.deepwiki.com, mcp.linear.app, and the managed model providers.

To allow another host, create an api endpoint with no header:

introspection bindings endpoints create --kind api --name sec \ --endpoint-url https://www.sec.gov -r support-agent

An endpoint names one exact host. Hosts reached only through URLs that carry their own credential (pre-signed links, signed WebSocket URLs) go in the endpoint’s metadata.extra_hosts, which accepts *.example.com. No raw TCP: put an HTTPS API in front of a database.

What’s installed

Node 24 (npm, pnpm, tsx), Python 3.14 with uv, git, gh, curl, jq, openssl, ripgrep, fd, unzip, and Poppler (pdftotext, pdftoppm). No other language runtimes, browser, ffmpeg, or apt. Declare dependencies in package.json.

Built-in tools

read (images go to the model as images), bash, edit, write, grep, find, ls. An agent gets only what its tools: list names.

Block a tool call

A tool_call hook sees every call before it runs. Return { block: true, reason } to refuse; the agent sees the reason.

This one caps refunds at an amount set per environment; larger ones become drafts:

extensions/refund-limit.ts
import type { ExtensionFactory } from "@earendil-works/pi-coding-agent"; const extension: ExtensionFactory = (pi) => { pi.on("tool_call", async (event) => { if (event.toolName !== "issue_refund") return; const limit = Number(process.env.REFUND_LIMIT); const amount = Number((event.input as { amount?: unknown }).amount); if (!Number.isFinite(limit) || !(amount <= limit)) { return { block: true, reason: `Refunds over ${process.env.REFUND_LIMIT ?? "the limit"} need approval. Call save_draft instead.`, }; } }); }; export default extension;

Set the limit as a binding variable; the sandbox exposes it in process.env:

export REFUND_LIMIT=500 introspection bindings variables create --name REFUND_LIMIT --from-env REFUND_LIMIT \ -r support-agent -e production

Declare the file under pi.extensions (Add a tool). A missing variable blocks every refund, so a forgotten binding fails closed.

  • Throwing also blocks. Mutate event.input to rewrite arguments.
  • MCP tools in tools mode are named mcp_<server>_<tool>_<hash>: match the prefix. In cli mode they are bash commands.
  • For no network shell at all, leave bash out and use mcp.mode: tools.

Secrets

A credential reaches an API only as a header the egress adds for that host (Connect services and secrets). It never enters the sandbox. Variables are readable in the sandbox: never store a secret in one.

Verify

With issue_refund and save_draft tools in the recipe:

REFUND_LIMIT=500 introspection local -p "Refund order 1842 for 900 dollars."

Expect a blocked issue_refund call, then a save_draft call.

Limits

Not supportedDo this instead
Asking for approval inside a hookThe sandbox is headless. Draft into your system (Approve outside the chat) or ask the user.
Private networks, IPs, localhost, custom portsExpose a public HTTPS endpoint, or run Introspection in your cloud (Security).
Mid-turn checkpointsA sandbox lost mid-turn fails the task until the next message restarts it. Keep results in the /workspace folders that survive (Files and memory).

Or ask your agent: “Block refunds over the REFUND_LIMIT variable and make the agent draft them instead.”

Last updated on