Network egress rules
Every sandbox has an egress policy: the outbound internet it may reach. Set it when you create the sandbox, change it while the sandbox runs, and read what the sandbox tried to reach. A sandbox-side proxy enforces the policy, backed by a host firewall that routes all sandbox traffic through it.
| Mode | The sandbox can reach |
|---|---|
open | Any host. The default for a personal sandbox. |
strict | The hosts in allowDomains, plus the model endpoint and providers the platform provisioned into the sandbox. Nothing else: no package registry, no source host, unless you list it. |
blocked | No outbound internet. |
A team sandbox created without a policy inherits the team's default policy.
Lock a build to the hosts it needs
This sandbox can install npm packages and read GitHub's API, and nothing else.
import { Sandbox } from "@tangle-network/sandbox";
const client = new Sandbox({
apiKey: process.env.TANGLE_API_KEY!,
baseUrl: "https://sandbox.tangle.tools",
});
const box = await client.create({
name: "locked-build",
egressPolicy: {
mode: "strict",
allowDomains: ["registry.npmjs.org", "api.github.com"],
},
maxLifetimeSeconds: 3600,
});
try {
await box.waitFor("running");
await box.exec("git clone --depth 1 https://github.com/vercel/ms /workspace/ms");
const build = await box.exec("cd /workspace/ms && npm ci && npm test");
console.log(build.exitCode, build.stdout.slice(-400));
} finally {
await box.delete();
}The git clone fails here: github.com is not api.github.com. The next
recipe shows how to find every host a build needs instead of guessing.
Discover the allowlist from denials
A refused request gets a bodyless 403, so code inside the sandbox cannot tell
a policy refusal from a broken network. box.egress.denials() tells you which
hosts were refused and how often. Start strict with nothing allowed, run the
build once, and read the refusals:
const box = await client.create({
egressPolicy: { mode: "strict", allowDomains: [] },
});
await box.waitFor("running");
await box.exec(
"git clone --depth 1 https://github.com/vercel/ms /workspace/ms && cd /workspace/ms && npm ci",
);
for (const denial of await box.egress.denials({ limit: 50 })) {
console.log(`${denial.host} refused ${denial.count}x by ${denial.rejectedBy}`);
}
// github.com refused 1x by …
// registry.npmjs.org refused 3x by …Add the hosts you trust and apply them to the running sandbox:
await box.egress.update({
mode: "strict",
allowDomains: ["github.com", "codeload.github.com", "registry.npmjs.org"],
});update() restarts the sandbox's proxy to apply the policy, which interrupts
outbound connections for a few seconds. The restart also starts a new activity
window, so read denials() and activity() before you change the policy.
Install, then cut the network
Untrusted code should not run with network access. Install dependencies under
a strict policy, then switch to blocked before the code runs:
await box.exec("cd /workspace/app && npm ci");
await box.egress.update({ mode: "blocked" });
const result = await box.exec("cd /workspace/app && npm test");Prove what a sandbox reached
denials() is best effort: an empty list can mean nothing was refused or that
the proxy log could not be read. When you need evidence, for an audit or a
benchmark control arm, use activity(). It lists allowed and refused hosts and
says whether the record is complete.
const activity = await box.egress.activity({ limit: 500 });
if (!activity.complete) {
throw new Error(`incomplete record: ${activity.incompleteReasons.join(", ")}`);
}
for (const destination of activity.destinations) {
console.log(destination.action, destination.host, destination.count);
}An empty destinations list with complete: true is evidence the sandbox
reached nothing. With complete: false it proves nothing.
Domain patterns
| Pattern | Matches |
|---|---|
api.github.com | That host only. |
*.githubusercontent.com | githubusercontent.com and every subdomain at any depth. |
**.githubusercontent.com | Subdomains only, not githubusercontent.com itself. |
includeImplicitDomains: true merges the platform's default developer list
into a strict policy, including github.com, gist.github.com, and
gitlab.com. That makes any public repository readable and a gist an
allowed destination, so use it only for trusted development sandboxes.
Cloud agents behind a strict policy
A coding agent's model calls go to the endpoint provisioned into the sandbox, which strict mode allows. When the agent uses your own subscription or a coding plan endpoint, list that provider's host so the policy states it explicitly:
egressPolicy: {
mode: "strict",
allowDomains: ["api.anthropic.com", "registry.npmjs.org", "github.com"],
}See Start a cloud agent for the agent side.
From HTTP or the CLI
The CLI has no egress flag yet. Use the HTTP API, which merges the body into the current policy:
curl -X PATCH "https://sandbox.tangle.tools/v1/sandboxes/$SANDBOX_ID/egress" \
-H "Authorization: Bearer $TANGLE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"mode":"strict","allowDomains":["registry.npmjs.org"]}'| Route | Purpose |
|---|---|
PATCH /v1/sandboxes/{id}/egress | Change the policy on a running sandbox. |
GET /v1/sandboxes/{id}/egress/denials | Refused hosts, newest first. |
GET /v1/sandboxes/{id}/egress/activity | Allowed and refused hosts, with completeness. |
GET /v1/sandboxes/{id}/egress/usage | This month's egress bytes and charge. |
PUT /v1/teams/{id}/egress | Team default policy (team owners and admins). |
Egress pricing is published at
/v1/pricing/egress, and
box.egress.usage() returns this sandbox's bytes and charge for the month.