[iris]
Concepts

Fork

Instantly branch a sandbox using copy-on-write

What is a Fork?

sandbox.fork() creates a new independent sandbox from the current sandbox's filesystem state using copy-on-write. The original sandbox keeps running. The two sandboxes share unmodified disk blocks — a block is only copied when one side writes to it.

const branch = await sandbox.fork()

await branch.exec.run('python3 risky_operation.py')

// Discard when done — original is untouched
await branch.delete()
branch = sandbox.fork()

branch.exec.run(["python3", "risky_operation.py"])

# Discard when done — original is untouched
branch.delete()

Fork copies the filesystem only. In-memory state (heap, stack, open file descriptors, network connections) is not captured. Processes in the forked sandbox start fresh from their on-disk state — they do not resume mid-execution from the parent.

Parallel Experiments

Fork multiple times from the same decision point to explore approaches simultaneously:

await sandbox.exec.run('python3 setup.py')

const [expA, expB, expC] = await Promise.all([
  sandbox.fork(),
  sandbox.fork(),
  sandbox.fork(),
])

const results = await Promise.all([
  expA.exec.run('python3 train.py --lr=0.001'),
  expB.exec.run('python3 train.py --lr=0.01'),
  expC.exec.run('python3 train.py --lr=0.1'),
])

await Promise.all([expA.delete(), expB.delete(), expC.delete()])
import asyncio
from iris import AsyncIrisClient

async def main():
    client = AsyncIrisClient()
    sandbox = await client.sandboxes.create()
    await sandbox.exec.run(["python3", "setup.py"])

    exp_a, exp_b, exp_c = await asyncio.gather(
        sandbox.fork(),
        sandbox.fork(),
        sandbox.fork(),
    )

    results = await asyncio.gather(
        exp_a.exec.run(["python3", "train.py", "--lr=0.001"]),
        exp_b.exec.run(["python3", "train.py", "--lr=0.01"]),
        exp_c.exec.run(["python3", "train.py", "--lr=0.1"]),
    )

    await asyncio.gather(exp_a.delete(), exp_b.delete(), exp_c.delete())

AI Agent Rollback

Fork before a risky operation — if it fails, the original sandbox is unaffected:

const branch = await sandbox.fork()
const result = await branch.exec.run(aiGeneratedCode)

if (result.ok) {
  await branch.delete()
  await sandbox.exec.run(aiGeneratedCode) // replay on original
} else {
  // Failure — discard the branch, original is clean
  await branch.delete()
}
branch = sandbox.fork()
result = branch.exec.run(cmd)

if result.ok:
    branch.delete()
    sandbox.exec.run(cmd)  # replay on original
else:
    # Failure — discard the branch, original is clean
    branch.delete()

Test Isolation

Keep one seeded base sandbox alive. Each test forks from it:

const base = await Sandbox.create()
await base.exec.run('npm install && npm run db:seed')

for (const test of tests) {
  const env = await base.fork()
  const result = await env.exec.run(`npm test -- ${test}`)
  expect(result.exit_code).toBe(0)
  await env.delete()
}

await base.delete()
from iris import IrisClient

client = IrisClient()
base = client.sandboxes.create()
base.exec.run(["bash", "-c", "npm install && npm run db:seed"])

for test in tests:
    env = base.fork()
    result = env.exec.run(["bash", "-c", f"npm test -- {test}"])
    assert result.exit_code == 0
    env.delete()

base.delete()

Each fork gets an identical filesystem. Tests cannot affect each other regardless of what they write to disk.

Debugging

Keep the sandbox alive at the point of interest and fork from it repeatedly:

// Sandbox is at the buggy state
for (let i = 0; i < 5; i++) {
  const investigation = await sandbox.fork()
  const logs = await investigation.exec.run('strace ./program 2>&1 | head -100')
  console.log(logs.stdout)
  await investigation.delete()
}
# Sandbox is at the buggy state
for _ in range(5):
    investigation = sandbox.fork()
    logs = investigation.exec.run(["bash", "-c", "strace ./program 2>&1 | head -100"])
    print(logs.stdout)
    investigation.delete()

On this page