KestrelDocs

Sandbox

Write files into a sandbox before a run, read what the agent made, and move large files in parallel chunks.

The repo is copied into every sandbox for you. Use the files API for everything else: fixtures, data, logs, or the build output you want to keep.

Fig. 1Data transfer
Data transfer A large block of data moves in chunks along six parallel streams to a destination, which fills as the source drains, while a throughput reading climbs, holds and finishes. const float TRAVEL = 0.72; float off(float r) { return r < 0.5 ? 0.0 : r < 1.5 ? 0.11 : r < 2.5 ? 0.05 : r < 3.5 ? 0.15 : r < 4.5 ? 0.03 : 0.09; } // chunk k of lane r leaves at dep; the gaps shrink, so the rate climbs float dep(float r, float k) { return 0.3 + off(r) + (k < 0.5 ? 0.0 : 2.2 * pow(k / 5.0, 0.75)); } // slot j (left to right) of both stores resets in one sweep: the source refills, the destination clears float resetAt(float j) { return 3.8 + 0.08 * j; } float q(float s) { return 1.2 * sm(0.3, 1.5, s) * (1.0 - sm(2.8, 3.4, s)); } // the data's grain: a fixed density per cell of a chunk, carried with it when it moves float grain(float c, float r) { return 0.6 + 0.38 * hash(vec2(c * 1.31 + 3.0, r * 0.77 + 11.0)); } float field(vec2 p, float t) { float col = floor((p.x - 4.0) / 7.0 + 0.5); float cell = floor(p.y / 9.4); float v = 0.0; float rl = cell - 10.0; float r = floor(rl / 4.0); float rc = mod(rl, 4.0); if (rl >= 0.0 && r < 6.0 && rc < 3.0) { float mid = abs(rc - 1.0) < 0.5 ? 1.0 : 0.0; // the source: full until its chunk leaves (lifting just before), empty ticks after, refilled by the sweep if (col >= 5.0 && col < 29.0 && mod(col - 5.0, 4.0) < 3.0) { float j = floor((col - 5.0) / 4.0); float d = dep(r, 5.0 - j); float R = resetAt(j); float lift = sm(d - 0.14, d, t) * step(t, d); float back = step(R, t) * (1.0 - sm(0.0, 0.16, t - R)); v = t < d || t >= R ? grain(col, cell) + 0.35 * lift + 0.4 * back : 0.19; } // the destination: empty ticks until its chunk lands (with a flash), full after, cleared by the sweep if (col >= 74.0 && col < 98.0 && mod(col - 74.0, 4.0) < 3.0) { float j = floor((col - 74.0) / 4.0); float a = dep(r, 5.0 - j) + TRAVEL; float flash = step(a, t) * (1.0 - sm(0.0, 0.22, t - a)); v = t >= a && t < resetAt(j) ? grain(col - 69.0, cell) + 0.5 * flash : 0.19; } // the lane across the gap: faint ticks, a flow running right while the transfer runs if (col >= 29.0 && col < 74.0 && mid > 0.5) { float act = sm(0.2, 0.5, t) * (1.0 - sm(3.3, 3.7, t)); v = max(v, 0.13 + act * (0.1 + 0.24 * pow(0.5 + 0.5 * sin(col * 0.9 - TAU * 8.0 * t / T), 4.0))); } // chunks in flight on this lane: a dense head, the stream behind it thinning for (int ki = 0; ki < 6; ki++) { float k = float(ki); float c0 = 5.0 + 4.0 * (5.0 - k); float d = dep(r, k); if (t >= d && t < d + TRAVEL) { float u = (t - d) / TRAVEL; float xc = floor(c0 + 69.0 * mix(u, ease(u), 0.6) + 0.5); float lc = col - xc; if (lc >= 0.0 && lc < 3.0) v = max(v, grain(c0 + lc, cell) + 0.12); else if (lc < 0.0) v = max(v, (mid > 0.5 ? 0.9 * exp(lc / 7.0) : 0.55 * exp(lc / 4.5)) * step(c0, col)); } } } // the strip chart: one column per 0.1 s, drawn up to now, its newest column hot; cleared as the reset starts if (col >= 33.0 && col <= 68.0 && cell >= 37.0 && cell <= 42.0) { float s = (col - 33.0) / 35.0 * 3.6; float h = floor(q(s) / 1.2 * 6.0 + 0.5); float up = 42.0 - cell; if (s <= t && t <= 3.85 && up < h) v = t - s < 0.103 ? 0.98 : up > h - 1.5 ? 0.72 : 0.42; if (cell > 41.5) v = max(v, 0.13); } return v; }
Large files move in chunks over six streams at once. The destination fills as the source drains, so a 2 GB fixture lands in seconds.

Write a file

await sandbox.files.write("fixtures/orders.json", JSON.stringify(orders));

Paths are relative to the repo root. Folders are made as needed.

Read a file

const log = await sandbox.files.read("logs/test.log");

Upload and download large files

For anything over a few megabytes, stream it. Uploads and downloads are split into chunks that travel in parallel, and resume where they stopped if the connection drops.

import { createReadStream, createWriteStream } from "node:fs";

await sandbox.files.upload("data/events.parquet", createReadStream("./events.parquet"));
await sandbox.files.download("dist/app.tar.gz", createWriteStream("./app.tar.gz"));
LimitValue
Largest single file5 GB
Sandbox disk20 GB on Hobby, 100 GB on Team
Parallel streams per transfer6
Note

Files you write are part of the sandbox, not the repo. They never appear in a pull request unless the agent adds them to the diff itself.

Was this page helpful?

Open the dashboard