KestrelDocs

Sandbox

Sandboxes have no outbound network by default. Allow the hosts a build needs by name; every other name never resolves, so nothing else can leave.

An agent with open network access can install anything, send your code anywhere, and pass a test by calling a live service. Kestrel turns outbound traffic off. You open it one host at a time.

Fig. 1DNS lookup
DNS lookup A laptop asks for a name; its resolver asks the root of the name tree, then the COM zone, then the domain's own server, each pointing it to the next, and the server's address record lights and its address comes back to the laptop. // a faint line from a to b, at least a column wide and a cell tall, so it never falls between samples float lk(vec2 p, vec2 a, vec2 b, float k) { 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); return k * (1.0 - sm(0.8, 1.05, length(d))); } // the same, dotted: the resolver's paths float dl(vec2 p, vec2 a, vec2 b, float k) { return lk(p, a, b, k) * step(0.45, fract(dot(p - a, normalize(b - a)) / 12.0)); } // a server block of w by h cells at column c0, cell k0: records on every row, a short type field then a ragged value. // lit flashes it while it answers; ph flickers its rows. float rows(float col, float cell, float c0, float k0, float w, float h, float s, float k, float lit, float ph) { float lc = col - c0; float lr = cell - k0; if (lc < 0.0 || lc > w - 1.0 || lr < 0.0 || lr > h - 1.0) return 0.0; float hs = hash(vec2(s, lr)); float v = 0.18; if (lc < 1.5) v = 0.62 + 0.08 * sin(ph + hs * 30.0); else if (lc > 2.5 && lc < 3.0 + floor((w - 3.0) * (0.45 + 0.55 * hs))) v = 0.45; return (v + 0.55 * lit * step(0.3, v)) * k; } float field(vec2 p, float t) { if (p.x < 24.0 || p.x > 696.0 || p.y < 24.0 || p.y > 436.0) return 0.0; float ph = TAU * t / T; float col = floor((p.x - 4.0) / 7.0 + 0.5); float cell = floor(p.y / 9.4); float v = 0.0; float reset = 1.0 - sm(0.3, 0.6, t); float ans = exp(-pow(t - 1.18, 2.0) / 0.01); // the root answering float ansC = exp(-pow(t - 2.10, 2.0) / 0.01); float ansE = exp(-pow(t - 3.02, 2.0) / 0.01); // the tree: the root's branches and COM's branches, faint; a branch lights once it is the referral given float toC = max(reset, sm(1.22, 1.32, t)); float toE = max(reset, sm(2.14, 2.24, t)); v = max(v, lk(p, vec2(410.0, 113.700), vec2(354.0, 182.0), 0.3 + 0.32 * toC)); v = max(v, lk(p, vec2(422.0, 111.700), vec2(473.0, 191.400), 0.28)); v = max(v, lk(p, vec2(454.0, 90.700), vec2(599.0, 182.0), 0.28)); v = max(v, lk(p, vec2(354.0, 239.0), vec2(361.0, 296.600), 0.3 + 0.32 * toE)); v = max(v, lk(p, vec2(368.0, 239.0), vec2(494.0, 332.400), 0.28)); v = max(v, lk(p, vec2(378.0, 239.0), vec2(613.0, 323.0), 0.28)); // the root: thirteen server points round a core for (int i = 0; i < 13; i++) { float a = TAU * float(i) / 13.0 - 1.5708; v += blob(p, vec2(410.0, 78.700) + vec2(40.0 * cos(a), 29.0 * sin(a)), 6.5, 0.95 + 0.12 * sin(ph + float(i) * 2.1) + 0.3 * ans); } v += blob(p, vec2(410.0, 78.700), 13.0, 0.62 + 0.55 * ans); { vec2 d = (p - vec2(410.0, 78.700)) / vec2(30.0, 21.0); float hs = hash(floor(p / vec2(7.0, 9.4)) + 3.0); if (length(d) < 1.0) v = max(v, step(0.6, hs) * (0.24 + 0.06 * sin(ph + hs * 40.0))); } // the top-level zones and the domains' servers v = max(v, rows(col, cell, 46.0, 20.0, 9.0, 5.0, 1.0, 1.0, ansC, ph)); v = max(v, rows(col, cell, 64.0, 21.0, 7.0, 4.0, 2.0, 0.7, 0.0, ph)); v = max(v, rows(col, cell, 82.0, 20.0, 7.0, 4.0, 3.0, 0.7, 0.0, ph)); v = max(v, rows(col, cell, 67.0, 36.0, 7.0, 4.0, 4.0, 0.7, 0.0, ph)); v = max(v, rows(col, cell, 84.0, 35.0, 7.0, 4.0, 5.0, 0.7, 0.0, ph)); v = max(v, rows(col, cell, 48.0, 35.0, 11.0, 7.0, 6.0, 1.0, ansE, ph)); // EXAMPLE.COM's A record: its row lights once it is found if (abs(cell - 38.0) < 0.5 && col >= 48.0 && col <= 58.0) v = max(v, 1.1 * max(reset, sm(2.980, 3.060, t))); // the resolver: a dense core in a ring, its dotted paths out to the three it asks v += blob(p, vec2(158.0, 341.900), 15.0, 1.1); v += 0.55 * exp(-pow(length((p - vec2(158.0, 341.900)) * vec2(1.0, 1.15)) - 27.0, 2.0) / 22.0); v = max(v, dl(p, vec2(182.0, 317.900), vec2(380.0, 108.700), 0.34)); v = max(v, dl(p, vec2(190.0, 329.900), vec2(318.0, 211.500), 0.34)); v = max(v, dl(p, vec2(194.0, 345.900), vec2(298.500, 361.900), 0.34)); v = max(v, dl(p, vec2(140.0, 313.900), vec2(67.0, 212.800), 0.34)); // the laptop: a screen with a rim and lines on it, a base under it; a line lights once the address arrives { float lc = col - 5.0; float lr = cell - 16.0; if (lr >= 0.0 && lr <= 4.0 && lc >= 0.0 && lc <= 8.0) { float rim = (lc < 0.5 || lc > 7.5 || lr < 0.5 || lr > 3.5) ? 1.0 : 0.0; float on = max(reset, sm(3.880, 3.980, t)); float a = 0.16; if (abs(lr - 1.0) < 0.5 && lc > 0.5 && lc < 6.5) a = 0.3 + 0.8 * on; // the page it asked for, once it has the address if (abs(lr - 2.0) < 0.5 && lc > 0.5 && lc < 4.5) a = 0.3 + 0.45 * on; v = max(v, rim > 0.5 ? 0.62 : a); } if (abs(lr - 5.0) < 0.5 && lc >= -1.0 && lc <= 9.0) v = max(v, abs(lc - 4.0) < 0.5 ? 0.25 : 0.7); } // the resolver's questions and the answers { float u = clamp((t - 0.300) / 0.400, 0.0, 1.0); if (u > 0.0 && u < 1.0) { vec2 c = mix(vec2(67.0, 212.800), vec2(158.0, 341.900), ease(u)); vec2 dir = normalize(vec2(158.0, 341.900) - vec2(67.0, 212.800)); float k = sm(0.0, 0.08, u) * (1.0 - sm(0.92, 1.0, u)); v += blob(p, c, 9.0, 1.2 * k) + blob(p, c - dir * 14.0, 6.5, 0.5 * k) + blob(p, c - dir * 25.0, 4.5, 0.25 * k); } } { float u = clamp((t - 0.740) / 0.400, 0.0, 1.0); if (u > 0.0 && u < 1.0) { vec2 c = mix(vec2(158.0, 341.900), vec2(380.0, 108.700), ease(u)); vec2 dir = normalize(vec2(380.0, 108.700) - vec2(158.0, 341.900)); float k = sm(0.0, 0.08, u) * (1.0 - sm(0.92, 1.0, u)); v += blob(p, c, 9.0, 1.2 * k) + blob(p, c - dir * 14.0, 6.5, 0.5 * k) + blob(p, c - dir * 25.0, 4.5, 0.25 * k); } } { float u = clamp((t - 1.220) / 0.400, 0.0, 1.0); if (u > 0.0 && u < 1.0) { vec2 c = mix(vec2(380.0, 108.700), vec2(158.0, 341.900), ease(u)); vec2 dir = normalize(vec2(158.0, 341.900) - vec2(380.0, 108.700)); float k = sm(0.0, 0.08, u) * (1.0 - sm(0.92, 1.0, u)); v += blob(p, c, 9.0, 1.2 * k) + blob(p, c - dir * 14.0, 6.5, 0.5 * k) + blob(p, c - dir * 25.0, 4.5, 0.25 * k); } } { float u = clamp((t - 1.660) / 0.400, 0.0, 1.0); if (u > 0.0 && u < 1.0) { vec2 c = mix(vec2(158.0, 341.900), vec2(318.0, 211.500), ease(u)); vec2 dir = normalize(vec2(318.0, 211.500) - vec2(158.0, 341.900)); float k = sm(0.0, 0.08, u) * (1.0 - sm(0.92, 1.0, u)); v += blob(p, c, 9.0, 1.2 * k) + blob(p, c - dir * 14.0, 6.5, 0.5 * k) + blob(p, c - dir * 25.0, 4.5, 0.25 * k); } } { float u = clamp((t - 2.140) / 0.400, 0.0, 1.0); if (u > 0.0 && u < 1.0) { vec2 c = mix(vec2(318.0, 211.500), vec2(158.0, 341.900), ease(u)); vec2 dir = normalize(vec2(158.0, 341.900) - vec2(318.0, 211.500)); float k = sm(0.0, 0.08, u) * (1.0 - sm(0.92, 1.0, u)); v += blob(p, c, 9.0, 1.2 * k) + blob(p, c - dir * 14.0, 6.5, 0.5 * k) + blob(p, c - dir * 25.0, 4.5, 0.25 * k); } } { float u = clamp((t - 2.580) / 0.400, 0.0, 1.0); if (u > 0.0 && u < 1.0) { vec2 c = mix(vec2(158.0, 341.900), vec2(298.500, 361.900), ease(u)); vec2 dir = normalize(vec2(298.500, 361.900) - vec2(158.0, 341.900)); float k = sm(0.0, 0.08, u) * (1.0 - sm(0.92, 1.0, u)); v += blob(p, c, 9.0, 1.2 * k) + blob(p, c - dir * 14.0, 6.5, 0.5 * k) + blob(p, c - dir * 25.0, 4.5, 0.25 * k); } } { float u = clamp((t - 3.060) / 0.400, 0.0, 1.0); if (u > 0.0 && u < 1.0) { vec2 c = mix(vec2(298.500, 361.900), vec2(158.0, 341.900), ease(u)); vec2 dir = normalize(vec2(158.0, 341.900) - vec2(298.500, 361.900)); float k = sm(0.0, 0.08, u) * (1.0 - sm(0.92, 1.0, u)); v += blob(p, c, 9.0, 1.2 * k) + blob(p, c - dir * 14.0, 6.5, 0.5 * k) + blob(p, c - dir * 25.0, 4.5, 0.25 * k); } } { float u = clamp((t - 3.500) / 0.400, 0.0, 1.0); if (u > 0.0 && u < 1.0) { vec2 c = mix(vec2(158.0, 341.900), vec2(67.0, 212.800), ease(u)); vec2 dir = normalize(vec2(67.0, 212.800) - vec2(158.0, 341.900)); float k = sm(0.0, 0.08, u) * (1.0 - sm(0.92, 1.0, u)); v += blob(p, c, 9.0, 1.2 * k) + blob(p, c - dir * 14.0, 6.5, 0.5 * k) + blob(p, c - dir * 25.0, 4.5, 0.25 * k); } } // the resolver flashes as each answer comes back v += blob(p, vec2(158.0, 341.900), 18.0, 0.45 * (exp(-pow(t - 0.7, 2.0) / 0.004) + exp(-pow(t - 1.62, 2.0) / 0.004) + exp(-pow(t - 2.54, 2.0) / 0.004) + exp(-pow(t - 3.46, 2.0) / 0.004))); return v; }
Kestrel answers the sandbox's name lookups itself. A host on the allow list resolves and the request goes out. Any other name gets no address, so the request never starts.

Allow a host

List hosts in kestrel.yaml at the root of the repo. Wildcards match one level of subdomain.

kestrel.yaml
sandbox:
  network:
    allow:
      - registry.npmjs.org
      - pypi.org
      - "*.githubusercontent.com"

The allow list applies to every run on the repo, and to sandboxes you make with the SDK for that repo.

Presets

Most builds only need a package registry. Use a preset instead of listing hosts.

PresetAllows
npmregistry.npmjs.org, registry.yarnpkg.com
pythonpypi.org, files.pythonhosted.org
goproxy.golang.org, sum.golang.org
githubgithub.com, *.githubusercontent.com
kestrel.yaml
sandbox:
  network:
    presets: [npm, github]

What a blocked request looks like

A name that isn't on the list doesn't resolve, so the request fails at once with a DNS error rather than hanging until a timeout. The trace marks it, so you can see which host a build was reaching for.

Terminal
$ curl https://example.com
curl: (6) Could not resolve host: example.com
kestrel: example.com is not on the allow list for acme/web-app
Warning

Allowing a host lets the agent send data to it, not only fetch from it. Keep the list to registries and mirrors you trust.

Was this page helpful?

Open the dashboard