# 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](/docs/pricing#published-rate-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 ```ts check import { 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 upgrade const 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")); ``` ```python check import json from withruntime import Sandbox repo = "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](/docs/frameworks#any-other-framework) 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: ```ts check import { 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](/docs/pricing#snapshots-images-and-volumes)). ## 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: ``` CPU: 200 × 900 s / 3,600 × $0.025 = $1.25 Memory: 200 × 1,200 s / 3,600 × 4 GiB × $0.0075 = $2.00 Total: $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](/docs/pricing)). An account starts with 50 free hours and no card. ## Sources - [npm outdated](https://docs.npmjs.com/cli/v10/commands/npm-outdated) and [npm audit](https://docs.npmjs.com/cli/v10/commands/npm-audit), read 25 September 2026. - [pip list](https://pip.pypa.io/en/stable/cli/pip_list/): `--outdated` and `--format json`, read 25 September 2026. Related: [test generation](/use-cases/test-generation), [parallel agent exploration](/use-cases/parallel-agent-exploration), [what a sandbox snapshot is](/glossary/sandbox-snapshot), [coding agent sandbox](/use-cases/coding-agent-sandbox), [static analysis agent](/use-cases/static-analysis-agent). Facts on this page were checked on 25 September 2026.