# How to give an AI research agent a sandbox to fetch pages and run code Give the agent one sandbox per research question, with fetch and search tools inside it, a shell for analysis and files for its notes. **On Runtime a research agent's machine costs $0.03125 an hour while the model reads and writes**, for 2 vCPUs and 4 GiB, and a new one ran its first Python command 351 ms after the request at the median on 24 September 2026 ([speed](/docs/speed)). Web tools start inside the sandbox with one call, so whatever a fetched page tries to make the agent do happens on a disposable machine, not on your server. ## What a research agent does that a chatbot does not A research agent answers a question by working: it searches, opens sources, downloads a dataset, computes something from it, checks a claim, and writes up what it found with citations. That means three things run on its behalf: - **Fetching arbitrary URLs**, chosen by a model, from pages written by anyone. - **Running code** to parse, count and chart what it downloaded. - **Keeping state** across dozens of steps: downloaded files, notes, a draft. Each is a risk on your own infrastructure. A fetch from your API server can reach internal addresses. Code the agent writes after reading a hostile page can do what that page told it. A sandbox holds all three in a Firecracker microVM with its own kernel, where private and internal addresses are refused by the host and the machine can be thrown away at the end. ## The core loop Create the sandbox, start the fetch server in it, and hand the agent two kinds of tool: the MCP server's URL for reading the web, and shell and file tools for everything else. ```ts check import { Sandbox } from "withruntime"; import { sandboxTools } from "withruntime/tools"; await using sbx = await Sandbox.create({ timeoutSeconds: 3600, idlePauseSeconds: 600, labels: { agent: "research", question: "q-311" }, }); // An MCP server for the web, running inside the sandbox. await sbx.mcp.start([{ id: "fetch" }]); const gateway = await sbx.mcp.ready(); const fetchServer = gateway.servers[0]; // give its url and gateway.headers to your MCP client // Shell and files for analysis and notes. const tools = sandboxTools(sbx); // runtime_exec, runtime_read_file, runtime_write_file, runtime_list_files console.log( fetchServer?.url, tools.map((t) => t.name), ); // ... the model's tool-calling loop runs here ... const report = await sbx.files.readText("/workspace/report.md"); console.log(report); ``` ```python check from withruntime import Sandbox from withruntime.tools import sandbox_tools with Sandbox.create( timeout_seconds=3600, idle_pause_seconds=600, labels={"agent": "research", "question": "q-311"}, ) as sbx: sbx.mcp.start([{"id": "fetch"}]) gateway = sbx.mcp.ready() fetch_server = gateway["servers"][0] # its url and gateway["headers"] go to your MCP client run, read, write, ls = sandbox_tools(sbx) print(fetch_server["url"]) # ... the model's tool-calling loop runs here ... print(sbx.files.read_text("/workspace/report.md")) ``` Tell the model in its system prompt to save every source it uses under `/workspace/sources/` and to write its answer to `/workspace/report.md`. Your code then reads the report and can check that every cited file exists before it shows anything to a user. ## Which web tools can run inside `runtime.mcp.catalog()` lists the servers Runtime can start in a sandbox, each with its licence, pinned version and the hosts it calls. For research the relevant ones are fetch, Brave Search, Exa, Firecrawl, a Playwright browser, Context7 and memory ([MCP servers in a sandbox](/docs/javascript#mcp-servers-in-a-sandbox)). | Server (`id`) | What the agent gets | | -------------- | ------------------------------------------------- | | `fetch` | A web page by URL, returned as Markdown | | `brave-search` | Web, news, image and local search via Brave's API | | `exa` | Exa's search and page contents | | `firecrawl` | Scraping, crawling and structured extraction | | `playwright` | A headless Chromium the agent drives | | `memory` | A knowledge graph kept in a file in the sandbox | A search provider's API key is stored once as a Runtime secret and named when the server starts, as in `{ id: "brave-search", secrets: { BRAVE_API_KEY: "BRAVE_API_KEY" } }`. The server in the sandbox sees only a placeholder, and the host's proxy adds the real value to requests bound for that provider's own hosts, so a page that talks the model into printing its environment gets nothing it can use ([secrets sandboxes never see](/docs/security#secrets-sandboxes-never-see)). ## Research that takes hours or days Long questions do not fit one lease. Two settings make that cheap: - **`idlePauseSeconds`** pauses the sandbox after that many seconds with no request, keeping files, memory and processes. The next tool call wakes it, usually in about half a second, and a paused sandbox pays no compute. - **A name.** `Sandbox.getOrCreate("q-311")` returns the same sandbox from any process tomorrow, woken if it was paused, so a queue worker can pick the question up where another left it. ```ts check import { Sandbox } from "withruntime"; const sbx = await Sandbox.getOrCreate("research-q-311", { idlePauseSeconds: 900 }); const notes = await sbx.exec("ls sources | wc -l", { cwd: "/workspace" }); console.log(sbx.info.reused, notes.stdout.trim(), "sources so far"); ``` A paused sandbox is billed as storage at $0.08 per decimal GB per 30-day month ([pricing](/docs/pricing#paused-storage)). ## Narrowing what it can reach By default a paid sandbox reaches any public host. For a research agent that should read only certain sites, such as a company's own docs, a standards body or a statistics office, pass an allow list at create, or change it mid-task with `sbx.network.set({ internet: true, allow: [...] })`. A rule applies at once, to open connections too, and root in the guest cannot lift it ([the sandbox environment](/docs/sandbox-environment#the-network)). ## What the agent's sandbox covers | Need | How Runtime covers it | | ------------------------------ | --------------------------------------------------------------------- | | Fetching URLs a model picked | Private and internal addresses refused on the host, for every sandbox | | Analysis of what it downloaded | Python 3.12 with pandas, NumPy and matplotlib; Node.js 24; `sudo apt` | | Web tools without your servers | Catalog MCP servers started inside the sandbox, behind a bearer token | | Search keys that cannot leak | Secrets injected by the host's proxy, never present in the guest | | Pauses for human review | Pause keeps memory and processes, 1 to 365 days | | Many questions at once | 100 sandboxes at once on a paid account to start | | Knowing what it did | Lifecycle events and per-minute CPU and memory readings | ## What it costs Take 500 research tasks a month. Each keeps a 2 vCPU, 4 GiB sandbox running for 20 minutes, and its fetches, parsing and charts use 90 CPU-seconds in all: ``` CPU: 500 × 90 s / 3,600 × $0.025 = $0.31 Memory: 500 × 1,200 s / 3,600 × 4 GiB × $0.0075 = $5.00 Total: $5.31 ``` About a cent each, at Runtime's rates of $0.025 per vCPU-hour of measured CPU and $0.0075 per GiB-hour ([pricing](/docs/pricing)). Model tokens are paid to your model provider, not to Runtime. A new account gets 50 free sandbox hours, about 150 such tasks, with no card. ## Start ```bash no-run claude mcp add --scope user runtime -- npx -y withruntime mcp ``` That gives Claude Code, or any MCP client, Runtime's own tools; approve the link it shows once in your browser. For your own agent, install the [JavaScript](/docs/javascript) or [Python](/docs/python) SDK and start from the loop above. Related: [browser automation agent](/use-cases/browser-automation-agent), [what MCP is](/glossary/model-context-protocol), [egress control](/glossary/egress-control), [pause and resume a sandbox](/how-to/pause-and-resume-a-sandbox), [AI data pipelines](/use-cases/ai-data-pipelines). Facts on this page were checked on 25 September 2026.