# How to give an AI research agent a sandbox to fetch pages and run code Give the agent one sandbox per research question. Put 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. 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 results from it, checks a claim, and writes up what it found with citations. 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 one is a risk on your own infrastructure. A fetch from your API server can reach internal addresses. Code the agent writes after it reads a hostile page can do what that page told it. A sandbox runs all three in a Firecracker microVM with its own kernel. The host refuses private and internal addresses there, and the machine can be discarded at the end. ## The core loop Create the sandbox, start the fetch server in it, and give 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. It 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 that Runtime can start in a sandbox. Each entry shows its license, 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. It is 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. The host's proxy adds the real value to requests bound for that provider's own hosts. So a page that tricks 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 A sandbox has no time limit, so a long question is never cut off. Two settings make the waiting cheap: - **`idlePauseSeconds`** pauses the sandbox after that many seconds with nothing happening in it. The default is 300 unless you set another value. The pause keeps files, memory and processes. 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 immediately, including to open connections. Root in the guest cannot remove 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 | The host refuses private and internal addresses, 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 | The host's proxy injects secrets; they are never present in the guest | | Pauses for human review | Pause keeps memory and processes while you have credit | | Many questions at the same time | 100 sandboxes at the same time on a paid account | | 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. 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)). You pay for model tokens to your model provider, not to Runtime. A new account gets 100 free hours of a 1 GB machine in its first 30 days, about 75 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 26 October 2026.