Runtime

How to let an AI agent upgrade dependencies and prove nothing broke

Install the project in a sandbox, fork one copy per outdated package, test each upgrade alone, and fix only the failures.

On Runtime ten upgrades are tested at once from one installed machine, because a fork makes 1 to 10 running copies of a sandbox, files, memory and processes included, and the fork's own snapshot is not billed. Measured CPU costs $0.025 per vCPU-hour, and Runtime was 42% to 88% cheaper than eleven other sandbox providers for a job that mostly waits, at rates checked 23 September 2026 (the comparison).

Why upgrades go wrong one at a time

Bots that open a pull request per outdated package leave the hard part to a person: which bumps break the build, and what the fix is. Doing every bump in one branch is worse; when the tests fail, nobody knows which package did it.

The reliable order is:

  1. Get a green baseline: install and pass the tests before changing anything.
  2. Try each upgrade alone, from that same baseline.
  3. Merge the ones that pass as they are.
  4. Give the agent only the failures, one package each, with the error.
  5. Combine every passing upgrade and test once more.

Step 2 is where the time goes, and where forks help. Every attempt starts from the installed baseline without reinstalling, and the attempts cannot see each other's node_modules.

Find what is outdated

npm outdated --json lists each package with current, wanted (the newest version the range in package.json allows) and latest (the registry's latest tag). For Python, pip list --outdated --format json does the same (npm's and pip's documentation, read 25 September 2026).

The core loop

TypeScriptimport { Sandbox } from "withruntime";const repo = "https://github.com/your-org/your-app.git";await using base = await Sandbox.create({  diskMiB: 8192,  timeoutSeconds: 3600,  labels: { job: "upgrades" },  network: { internet: true, allow: ["github.com", "*.github.com", "registry.npmjs.org"] },});await base.exec(["git", "clone", "--depth", "1", repo, "app"], { check: true, timeoutMs: 300_000 });await base.exec("npm ci && npm test", { cwd: "/workspace/app", check: true, timeoutMs: 900_000 });const report = await base.exec("npm outdated --json", { cwd: "/workspace/app" });const outdated = Object.keys(JSON.parse(report.stdout || "{}")).slice(0, 10);if (outdated.length === 0) process.exit(0); // nothing to upgradeconst copies = await base.fork({ count: outdated.length });const results = await Promise.all(  outdated.map(async (name, i) => {    const copy = copies[i]!;    const bump = await copy.exec(["npm", "install", `${name}@latest`], {      cwd: "/workspace/app",      timeoutMs: 600_000,    });    const tests = await copy.exec("npm run build --if-present && npm test", {      cwd: "/workspace/app",      timeoutMs: 900_000,    });    return {      name,      sandboxId: copy.id,      passed: bump.exitCode === 0 && tests.exitCode === 0,      log: tests.stdout.slice(-3000),    };  }),);// Stop the copies that passed; keep the failures for the agent to work in.await Promise.all(  results.filter((r) => r.passed).map((r) => copies[outdated.indexOf(r.name)]!.stop()),);console.log(results.map((r) => `${r.passed ? "ok  " : "FAIL"} ${r.name}`).join("\n"));
Pythonimport jsonfrom withruntime import Sandboxrepo = "https://github.com/your-org/your-lib.git"with Sandbox.create(    disk_mib=8192,    timeout_seconds=3600,    labels={"job": "upgrades"},    network={"internet": True, "allow": ["github.com", "*.github.com", "pypi.org", "*.pythonhosted.org"]},) as base:    base.exec(["git", "clone", "--depth", "1", repo, "lib"], check=True, timeout_ms=300_000)    base.exec("pip install -e '.[test]' && python3 -m pytest -q", cwd="/workspace/lib",              check=True, timeout_ms=900_000)    listed = base.exec("pip list --outdated --format json", cwd="/workspace/lib")    outdated = [p["name"] for p in json.loads(listed.stdout or "[]")][:10]    copies = base.fork(count=len(outdated)) if outdated else []    results = []    for name, copy in zip(outdated, copies):        bump = copy.exec(["pip", "install", "--upgrade", name], cwd="/workspace/lib", timeout_ms=600_000)        tests = copy.exec("python3 -m pytest -q", cwd="/workspace/lib", timeout_ms=900_000)        passed = bump.exit_code == 0 and tests.exit_code == 0        results.append({"name": name, "sandbox": copy.id, "passed": passed, "log": tests.stdout[-3000:]})        if passed:            copy.stop()    for r in results:        print("ok  " if r["passed"] else "FAIL", r["name"])

A failing copy is already the right place for the agent to work: the upgrade is installed, the failure is reproducible, and nothing else has changed. Bind sandbox tools to that copy, give the model the test log and the package's changelog, and ask for the smallest change that makes the tests pass. When it is done, git diff in that copy is the fix. Stop each remaining copy when you have its diff.

More than ten packages

A fork makes at most ten copies per call. For a larger list, fork again from the same base, or take a snapshot of the baseline once and start as many sandboxes from it as the account allows:

TypeScriptimport { Runtime, Sandbox } from "withruntime";const runtime = new Runtime({ waitForCapacityMs: 600_000 });const base = await Sandbox.connect("your-baseline-sandbox-id");const baseline = await base.snapshot({ name: "app-baseline", retentionDays: 1 });await using copy = await runtime.sandboxes.create({ snapshot: baseline.id });// ... the same bump-and-test as above, in as many copies as you need ...await runtime.snapshots.delete(baseline.id);

A kept snapshot is billed at $0.08 per decimal GB per 30-day month on the bytes it alone stores; deleting it when the run ends keeps that to hours (pricing).

Security updates first

npm audit sends a description of your dependency tree to the registry and reports known vulnerabilities, and npm audit fix applies compatible updates by running an install. Run it in the baseline sandbox and put the packages it names at the top of the list, so the security bumps are tested first.

What an upgrade agent needs

Need How Runtime covers it
One upgrade per clean environment fork copies the installed baseline; up to 10 per call
Installs from the registry only An allow list with the registry and your Git host
Post-install scripts from packages They run inside a microVM with its own kernel, not on your runner
Builds that need services sudo enable-docker and Compose in the sandbox
Failures kept for the agent A failing copy keeps running until you stop it or its lease ends
Many repositories each week 100 sandboxes at once on a paid account to start

What it costs

Take 50 repositories upgraded weekly, 200 runs a month. Each run adds up to 20 minutes of 2 vCPU, 4 GiB sandbox time across the base and its copies, with 900 CPU-seconds of installing, building and testing:

TextCPU:    200 × 900 s / 3,600 × $0.025             = $1.25Memory: 200 × 1,200 s / 3,600 × 4 GiB × $0.0075  = $2.00Total:                                             $3.25

Under 2 cents per repository per run, at Runtime's rates of $0.025 per vCPU-hour of measured CPU and $0.0075 per GiB-hour of memory (pricing). An account starts with 50 free hours and no card.

Sources

Related: test generation, parallel agent exploration, what a sandbox snapshot is, coding agent sandbox, static analysis agent.

Facts on this page were checked on 25 September 2026.