KestrelDocs

Sandbox

The sandbox lifecycle: create one for a task, run commands in it, pause it to keep its state, and kill it when the work is done. Runs do all of this for you; the SDK lets you do it yourself.

Every run gets its own sandbox: a small Linux VM with a copy of the repo and nothing else. It starts in under a second, has no outbound network unless you allow it, and is wiped when the run ends. You can also make sandboxes yourself, to try a command, reproduce a bug, or prepare a fixture before a run.

Fig. 1Sandbox
Sandbox A rack of sealed cells, one task in each: the repo is copied into one cell, an agent works inside it, its request to the network stops at the wall, and only the diff it wrote leaves through the out port before the cell is wiped clean. const float T0 = 3.52; float rr(float a, float lo, float hi) { return step(lo - 0.5, a) * step(a, hi + 0.5); } float eq(float a, float b) { return step(abs(a - b), 0.5); } float X(float c) { return 4.0 + 7.0 * c; } float Y(float r) { return 3.5 + 9.4 * r; } float eout(float u) { u = clamp(u, 0.0, 1.0); return 1.0 - pow(1.0 - u, 3.0); } vec3 treeRow(float i) { if (i < 0.5) return vec3(0.0, 4.0, 1.0); if (i < 1.5) return vec3(1.0, 6.0, 0.0); if (i < 2.5) return vec3(1.0, 8.0, 0.0); if (i < 3.5) return vec3(1.0, 3.0, 1.0); if (i < 4.5) return vec3(2.0, 5.0, 0.0); if (i < 5.5) return vec3(2.0, 7.0, 0.0); if (i < 6.5) return vec3(1.0, 5.0, 0.0); if (i < 7.5) return vec3(0.0, 4.0, 1.0); if (i < 8.5) return vec3(1.0, 7.0, 0.0); if (i < 9.5) return vec3(1.0, 4.0, 0.0); if (i < 10.5) return vec3(0.0, 6.0, 0.0); if (i < 11.5) return vec3(0.0, 3.0, 0.0); return vec3(0.0); } vec2 diffRow(float i) { if (i < 0.5) return vec2(1.0, 7.0); if (i < 1.5) return vec2(1.0, 5.0); if (i < 2.5) return vec2(0.0, 4.0); if (i < 3.5) return vec2(1.0, 6.0); if (i < 4.5) return vec2(0.0, 5.0); if (i < 5.5) return vec2(1.0, 3.0); return vec2(0.0); } float entry(float C, float s, vec3 e) { return eq(C, s) * (e.z > 0.5 ? 1.05 : 0.72) + rr(C, s + 2.0, s + 1.0 + e.y) * (e.z > 0.5 ? 0.62 : 0.42); } float dline(float C, float g, vec2 d, float upto) { float txt = rr(C, g + 2.0, min(g + 1.0 + d.y, g + 1.0 + upto)); return eq(C, g) * (d.x > 0.5 ? 1.0 : 0.45) + txt * (d.x > 0.5 ? 0.72 : 0.4); } float walls(float C, float R, float c0, float c1, float r0, float r1, float pr, float oin, float oout) { float port = rr(R, pr - 1.0, pr + 1.0); float wall = (eq(C, c0) + eq(C, c1)) * rr(R, r0, r1) + (eq(R, r0) + eq(R, r1)) * rr(C, c0 + 1.0, c1 - 1.0); wall *= 1.0 - port * (eq(C, c0) * oin + eq(C, c1) * oout); float inner = (eq(C, c0 + 1.0) + eq(C, c1 - 1.0)) * rr(R, r0 + 1.0, r1 - 1.0); inner *= 1.0 - port * (eq(C, c0 + 1.0) * oin + eq(C, c1 - 1.0) * oout); float liner = (eq(R, r0 + 1.0) + eq(R, r1 - 1.0)) * rr(C, c0 + 2.0, c1 - 2.0); float collar = (rr(C, c0 - 2.0, c0 - 1.0) + rr(C, c1 + 1.0, c1 + 2.0)) * (eq(R, pr - 2.0) + eq(R, pr + 2.0)); float bolt = (eq(C, c0 - 1.0) + eq(C, c1 + 1.0)) * rr(R, r0, r1) * eq(mod(R - r0, 4.0), 2.0) * step(2.5, abs(R - pr)); bolt += (eq(R, r0 - 1.0) + eq(R, r1 + 1.0)) * rr(C, c0, c1) * eq(mod(C - c0, 5.0), 2.0); float room = rr(C, c0 + 2.0, c1 - 2.0) * rr(R, r0 + 2.0, r1 - 2.0); return 1.05 * wall + 0.62 * inner + 0.4 * liner + 0.62 * collar + 0.5 * bolt + room * (0.13 + 0.12 * step(0.9, hash(vec2(C, R) + 7.1))); } float agent(vec2 p, vec2 c, float R0, float on, float work, float ang, float ph, float fi) { float d = length(p - c); float churn = noise(p * 0.1 + vec2(3.0 * fi + 1.3 * cos(ph), 2.0 * fi + 1.3 * sin(ph))); float v = blob(p, c, R0 * 0.6, on * (1.3 + 0.2 * work + 0.4 * (churn - 0.5))); v += on * 0.45 * exp(-pow(d - R0, 2.0) / 30.0); v += on * 0.12 * step(d, R0 - 4.0); v += work * blob(p, c + R0 * vec2(cos(ang), sin(ang)), 6.0, 1.2); return v; } float sibling(vec2 p, float C, float R, float r0, float fi, float ang, float lines, float ph) { float v = walls(C, R, 30.0, 70.0, r0, r0 + 8.0, r0 + 4.0, 0.0, 0.0); float i = R - (r0 + 2.0); if (i > -0.5 && i < 4.5) v += entry(C, 34.0 + treeRow(i).x, treeRow(i)) + eq(C, 33.0) * 0.26; float k = R - (r0 + 3.0); if (k > -0.5 && k < lines - 0.5) v += dline(C, 58.0, diffRow(k), 9.0); v += agent(p, vec2(X(52.0), Y(r0 + 4.0)), 15.0, 1.0, 1.0, ang, ph, fi); return 0.5 * v; } float field(vec2 p, float t) { if (p.x < 18.0 || p.x > 702.0 || p.y < 18.0 || p.y > 442.0) return 0.0; float C = floor((p.x - 4.0) / 7.0 + 0.5); float R = floor((p.y - 3.5) / 9.4 + 0.5); float ph = TAU * t / T; float tau = mod(t + T0, T); float v = 0.0; float oin = sm(0.0, 0.08, tau) * (1.0 - sm(0.98, 1.06, tau)); float oout = sm(3.10, 3.18, tau) * (1.0 - sm(3.42, 3.50, tau)); float wx = tau < 3.55 ? -10.0 : mix(214.0, 500.0, ease((tau - 3.55) / 0.45)); float keep = 1.0 - step(p.x, wx); v += walls(C, R, 30.0, 70.0, 16.0, 33.0, 25.0, oin, oout); v += sibling(p, C, R, 4.0, 1.0, ph + 0.8, 2.0, ph); v += sibling(p, C, R, 37.0, 2.0, ph + 3.4, 0.0, ph); float hit = sm(2.19, 2.22, tau) * (1.0 - sm(2.24, 2.55, tau)); v += 0.45 * hit * eq(C, 70.0) * rr(R, 17.0, 21.0) * exp(-pow(R - 19.0, 2.0) / 3.0); v += 0.3 * (eq(R, 24.0) + eq(R, 26.0)) * (rr(C, 21.0, 27.0) + rr(C, 73.0, 75.0)); { float i = R - 19.0; if (i > -0.5 && i < 11.5) { vec3 e = treeRow(i); float dep = 0.10 + 0.1 * floor(i / 2.0); float lit = sm(dep, dep + 0.04, tau) * (1.0 - sm(dep + 0.12, dep + 0.28, tau)); v += entry(C, 9.0 + e.x, e) * (1.0 + 0.6 * lit) + eq(C, 8.0) * 0.26; } } float crossed = 0.0; for (int j = 0; j < 6; j++) { float dep = 0.10 + 0.1 * float(j); float u = (tau - dep) / 0.36; crossed += step(dep + 0.2, tau); if (u > 0.0 && u < 1.0) { vec2 A = vec2(146.0, 238.5), B = vec2(228.0, 238.5), D = vec2(300.0, Y(19.5 + 2.0 * float(j))); float l1 = length(B - A), l2 = length(D - B); float s = u * (l1 + l2); vec2 c = s < l1 ? mix(A, B, s / l1) : mix(B, D, (s - l1) / l2); v += blob(p, c, 6.5, 1.15 * sm(0.0, 0.08, u) * (1.0 - sm(0.88, 1.0, u))); } } { float i = R - 19.0; if (i > -0.5 && i < 11.5) { vec3 e = treeRow(i); float j = floor(i / 2.0); float arr = 0.46 + 0.1 * j; float rd = 1.18 + 0.1333 * j; float lit = sm(rd, rd + 0.03, tau) * (1.0 - sm(rd + 0.12, rd + 0.2, tau)); v += keep * sm(arr - 0.03, arr + 0.03, tau) * (entry(C, 34.0 + e.x, e) * (1.0 + 0.6 * lit) + eq(C, 33.0) * 0.26); } } vec2 core = vec2(X(52.0), Y(25.0)); float on = sm(0.98, 1.18, tau); float work = sm(1.18, 1.26, tau) * (1.0 - sm(2.98, 3.08, tau)); v += keep * agent(p, core, 24.0, on, work, TAU * 3.0 * (tau - 1.18) / 1.9, ph, 0.0); if (tau > 1.18 && tau < 1.98) { float j = floor((tau - 1.18) / 0.1333); v += seg(p, core, vec2(X(45.0), Y(19.5 + 2.0 * j)), 2.8, 0.42); } if (tau > 2.46 && tau < 3.06) { float i = floor((tau - 2.46) / 0.1); v += seg(p, core, vec2(X(59.0), Y(21.0 + i)), 2.8, 0.42); } { vec2 A = vec2(388.0, 226.0), W = vec2(476.0, 182.1); vec2 back = normalize(A - W); float k = 1.15 * sm(1.98, 2.03, tau) * (1.0 - sm(2.28, 2.42, tau)); if (k > 0.0) { vec2 c = tau < 2.2 ? mix(A, W, ease((tau - 1.98) / 0.22)) : W + back * 20.0 * eout((tau - 2.2) / 0.22); float sq = exp(-pow((tau - 2.2) / 0.05, 2.0)); vec2 d = (p - c) / vec2(9.0 * (1.0 - 0.45 * sq), 9.0 * (1.0 + 0.6 * sq)); v += k * exp(-dot(d, d)); } float sy = 10.0 + 60.0 * max(tau - 2.2, 0.0); v += 0.8 * hit * step(p.x, 488.0) * exp(-pow(p.x - 482.0, 2.0) / 18.0 - pow(p.y - 182.1, 2.0) / (sy * sy)); } { float g = sm(3.05, 3.2, tau); float e = ease(g); for (int k = 0; k < 6; k++) { float i = float(k); float rowR = floor(mix(21.0 + i, 25.0, e) + 0.5); float wr = clamp((tau - 2.46 - 0.1 * i) / 0.08, 0.0, 1.0); if (wr > 0.0 && eq(R, rowR) > 0.5) v += keep * (1.0 - sm(0.6, 1.0, g)) * dline(C - floor(3.0 * e + 0.5), 58.0, diffRow(i), 8.0 * wr); } float u = (tau - 3.2) / 0.22; v += blob(p, vec2(462.0, 238.5), 9.0, 1.3 * sm(3.12, 3.2, tau) * step(tau, 3.2)); if (u > 0.0 && u < 1.0) { vec2 A = vec2(462.0, 238.5), B = vec2(518.0, 238.5), D = vec2(572.0, 252.0); float l1 = length(B - A), l2 = length(D - B); float s = ease(u) * (l1 + l2); vec2 c = s < l1 ? mix(A, B, s / l1) : mix(B, D, (s - l1) / l2); v += blob(p, c, 9.0, 1.3 * (1.0 - sm(0.85, 1.0, u))); } } { float i = R - 24.0; if (i > -0.5 && i < 5.5) { float a0 = 3.38 + 0.015 * i; float hand = ease((tau - 4.02) / 0.28); float vis = sm(a0, a0 + 0.04, tau) * (1.0 - sm(4.06, 4.3, tau)); v += vis * dline(C - floor(3.0 * hand + 0.5), 79.0, diffRow(i), 9.0); } } if (tau > 3.55 && tau < 4.0) { float wv = sm(3.55, 3.6, tau) * (1.0 - sm(3.94, 4.0, tau)); v += wv * rr(R, 18.0, 31.0) * (1.05 * exp(-pow(p.x - wx, 2.0) / 28.0) + 0.22 * step(p.x, wx) * step(wx - 40.0, p.x) * step(0.6, hash(vec2(C, R) + 2.3))); } { float clr = tau < 0.12 ? 1.0 - sm(0.0, 0.12, tau) : 0.0; float hIn = tau < 0.12 ? 6.0 * clr : crossed; float nNet = tau < 0.12 ? clr : step(2.22, tau); float hOut = tau < 0.12 ? clr : step(3.29, tau); float base = 43.0; float slots = (rr(C, 79.0, 80.0) + rr(C, 85.0, 86.0) + rr(C, 91.0, 92.0)) * rr(R, 37.0, base) * 0.13; float inBar = rr(C, 79.0, 80.0) * rr(R, base - hIn + 1.0, base) * step(0.5, hIn) * (eq(R, base - floor(hIn + 0.5) + 1.0) > 0.5 ? 1.05 : 0.62); float outBar = rr(C, 91.0, 92.0) * eq(R, base) * hOut * 1.05; float netMark = rr(C, 85.0, 86.0) * eq(R, base) * nNet * 0.5; v += max(slots, max(inBar, max(outBar, netMark))); } float near = 0.0; v += blob(p, vec2(566.0, 102.0), 6.0, 1.0 + 0.12 * sin(ph + 0.0)); near = max(near, exp(-dot(p - vec2(566.0, 102.0), p - vec2(566.0, 102.0)) / 420.0)); v += blob(p, vec2(600.0, 88.0), 6.0, 1.0 + 0.12 * sin(ph + 1.3)); near = max(near, exp(-dot(p - vec2(600.0, 88.0), p - vec2(600.0, 88.0)) / 420.0)); v += blob(p, vec2(636.0, 100.0), 6.0, 1.0 + 0.12 * sin(ph + 2.6)); near = max(near, exp(-dot(p - vec2(636.0, 100.0), p - vec2(636.0, 100.0)) / 420.0)); v += blob(p, vec2(584.0, 128.0), 6.0, 1.0 + 0.12 * sin(ph + 3.9)); near = max(near, exp(-dot(p - vec2(584.0, 128.0), p - vec2(584.0, 128.0)) / 420.0)); v += blob(p, vec2(620.0, 128.0), 6.0, 1.0 + 0.12 * sin(ph + 5.2)); near = max(near, exp(-dot(p - vec2(620.0, 128.0), p - vec2(620.0, 128.0)) / 420.0)); v += blob(p, vec2(648.0, 118.0), 6.0, 1.0 + 0.12 * sin(ph + 6.5)); near = max(near, exp(-dot(p - vec2(648.0, 118.0), p - vec2(648.0, 118.0)) / 420.0)); float ln = 0.0; ln = max(ln, seg(p, vec2(566.0, 102.0), vec2(600.0, 88.0), 3.4, 0.3)); ln = max(ln, seg(p, vec2(600.0, 88.0), vec2(636.0, 100.0), 3.4, 0.3)); ln = max(ln, seg(p, vec2(566.0, 102.0), vec2(584.0, 128.0), 3.4, 0.3)); ln = max(ln, seg(p, vec2(584.0, 128.0), vec2(620.0, 128.0), 3.4, 0.3)); ln = max(ln, seg(p, vec2(600.0, 88.0), vec2(620.0, 128.0), 3.4, 0.3)); ln = max(ln, seg(p, vec2(636.0, 100.0), vec2(648.0, 118.0), 3.4, 0.3)); ln = max(ln, seg(p, vec2(620.0, 128.0), vec2(648.0, 118.0), 3.4, 0.3)); v += ln; float hs = hash(vec2(C, R) + 3.7); v += near * step(0.78, hs) * (0.24 + 0.1 * sin(ph + hs * 40.0)); float ne = length((p - vec2(607.0, 110.0)) / vec2(56.0, 36.0)); v += (1.0 - sm(0.82, 1.0, ne)) * step(0.5, hs) * 0.19 + 0.3 * exp(-pow(ne - 1.0, 2.0) / 0.006) * step(0.35, hs); { vec2 A = vec2(510.0, 176.0), B = vec2(578.0, 132.0); vec2 ab = B - A; float h = clamp(dot(p - A, ab) / dot(ab, ab), 0.0, 1.0); vec2 d = (p - A - ab * h) / vec2(3.6, 4.8); v += 0.32 * (1.0 - sm(0.8, 1.05, length(d))) * step(0.5, fract(h * length(ab) / 16.0)); } return v; }
One sealed cell per task. The repo is copied in, the agent works inside, its call to the network stops at the wall, and only the diff leaves before the cell is wiped.
Note

A sandbox runs for up to 24 hours on Team and 1 hour on Hobby. For longer work, pause it: pausing resets the clock, and its disk and memory are kept until you resume or kill it.

Create a sandbox

A sandbox stays up until its timeout runs out. The timeout is in milliseconds and counts from the last command you ran, so a busy sandbox is never cut off mid-task.

import { Kestrel } from "@kestrel/sdk";

const kestrel = new Kestrel();
const sandbox = await kestrel.sandboxes.create({
  repo: "acme/web-app",
  timeoutMs: 600_000, // 10 minutes after the last command
});
console.log(sandbox.id, sandbox.state); // "sbx_41c9", "running"

Run a command

const result = await sandbox.commands.run("npm test -- checkout");
console.log(result.exitCode, result.stdout);

The states

StateWhat it meansBilled
startingThe VM boots and the repo is copied in. Usually under a second.No
runningCommands run. The timeout counts down between them.Sandbox minutes
pausedDisk and memory are saved and the VM is stopped.Storage only
killedThe VM and its disk are gone. This can't be undone.No

Change the timeout while it runs

await sandbox.setTimeout(30 * 60_000); // 30 more minutes from now

Kill a sandbox

Killing is immediate. Anything not downloaded or snapshotted is lost.

await sandbox.kill();

Was this page helpful?

Open the dashboard