---
title: "Prepare for sandbox changes"
description: "Check plugin egress, build restrictions and the session sandbox floor before upgrading affected nodes."
canonical_url: "https://docs.oz0.ai/fleet/upgrades/sandbox-changes"
---
# Prepare for sandbox changes

> Check plugin egress, build restrictions and the session sandbox floor before upgrading affected nodes.

For: cluster operators.

Check plugin egress, build restrictions and the session sandbox floor before upgrading affected nodes.

## Installed plugins are held to the egress allowlist

From this release a node with sandbox tier 1 runs every installed plugin's servers with `--plugin-sandbox strict`: in a sandbox of their own behind the egress proxy, as sessions run. Before, they ran on the host's network (`auto`) and only the plugin's own code held them to its lists. On macOS they ran outside any sandbox, and now run under Seatbelt. Review these effects before upgrading nodes.

### Add destinations to the correct egress list

A plugin that reached a host outside its allowlist stops reaching it. The manifest's `egress:` and the tenant's list are what it reaches. A refused connection says which list to change: the job shows the denial with its fix, and `oz0 egress learn <job>` proposes the entry. A host on your own network goes on the tenant's list (`oz0 tenant egress`), since a plugin's own entries reach public addresses only. The verified plugins that reach an address you chose (GitHub Enterprise, Jira and Confluence Data Center, GovSlack, a SearXNG server, a ServiceNow instance) need it on that list.

### Exempt tools that need the host

A tool that must run outside the sandbox stops. ai.oz0/host-health and ai.oz0/pr-reviewer are the verified plugins with such a tool (`sandbox: host` and `sandbox: own` in a manifest).

The tool does not start, the rest of its plugin stays ready, and the node gets the finding `plugin tool held back`, with its fix.

Run the node with `--plugin-sandbox-exempt <plugin>` to allow it there, or `oz0-node service install` with the same flag for a service; a node's operator is the only one who can.

`<plugin>@<commit>` allows one version alone, with the whole commit or digest as the finding gives it: `oz0 plugin list` shows it shortened, which the flag refuses.

A function of such a tool that no node has listed yet waits for `oz0 plugin approve-tools`, as any new function does.

A job that needs the tool fails at its start on a node that holds it back, with that fix, and so does an install's quality gate that ran its evals there: exempt the plugin on every node that may take its jobs, or send them to one that does with a label.

A hook on a tool that needs the host stops its jobs at the start. A hook is a function of a tool, so a plugin that hooks through a tool the node holds back cannot answer. Rather than count as its `on_timeout` on every event, a job that would run it fails at its start and names the hook. No verified plugin has such a hook.

### Configure clients to use the proxy

A client that ignores the proxy variables fails with a name error. A sandbox has no network but the proxy, reached through `HTTP_PROXY` and `HTTPS_PROXY`, and it resolves names for its clients. Python, Go, curl and most HTTP libraries read them, and Node.js does too: the sandbox sets `NODE_USE_ENV_PROXY=1`, which Node.js 22.21 and 24 read, as the node's own Node.js 24 is.

Node.js's `fetch` answers a refused tunnel with an error of its own, so the job's denial is what says which list to change.

A raw socket, or a client that dials by itself, gets a name lookup error instead of the proxy's refusal, and no denial is on record, so `oz0 egress learn` has nothing to propose. Point such a client at the proxy, or give that node `--plugin-sandbox auto`.

### Check network diagnostic tools

net-diag's `dns_lookup` and `trace_route` answer that they cannot run, as they do in a job's sandbox, since they need the node's own network. Its other functions work.

### Check environment variable names

Some names and addresses in a manifest's `env` are refused. The dynamic loader's variables, `PATH` and `OZ0_*` in a tool's `env` or as a secret's name fail `oz0 plugin lint`, and a node leaves them out of a plugin installed before, saying so in its log. A loopback URL under `OPENAI_BASE_URL` or `ANTHROPIC_BASE_URL` fails lint too: a tool that asks a model says `uses_model`.

A tool from an OCI image with a value that spans lines, such as a key in PEM form, may not use a name its container runtime reads on the host, such as `DOCKER_HOST` or `HOME`; it does not start until that name changes.

### Build plugins without running their code

A plugin whose build runs code stops installing. A strict node builds a plugin with none of its code run: its project on `uv_build`, as `oz0 plugin new` writes it, or with no build at all, and its dependencies as wheels.

A new version on setuptools or hatchling, or with a dependency that comes only as source, fails its install and says so, as does a Node.js plugin whose `package-lock.json` takes a dependency from a git repository, a URL or a file outside its folder: npm prepares a git dependency by running its scripts.

Its author can move to `uv_build` or a version with a wheel; to let it build as it is, run the node with `--plugin-sandbox-exempt <plugin>`, which runs that build as the node's user.

A plugin already installed keeps its environment. Every verified plugin builds this way.

The node's own settings for uv and npm are what apply: a `uv.toml`, `.npmrc` or `.python-version` in a plugin's folder is left out, so an index a plugin named there goes in the node's settings instead.

### Check host tools in harness sessions

A `sandbox: host` tool is left out of a harness session's own servers on a node with tier 1, whatever the mode: a session runs in a sandbox there, where the tool would read that sandbox.

### Apply tenant egress changes

A change of the tenant's list reaches running servers at once, with no restart.

### Roll out to one node first

To see it before it matters, upgrade one node first, watch `oz0 node show <node>` and the jobs of the plugins it runs, then go on. To keep the old behaviour on a node while a plugin's author catches up:

```bash [Terminal]
oz0-node service install --plugin-sandbox auto   # or: oz0-node run --plugin-sandbox auto
```

A node with no tier 1 changes nothing: its plugins run as its user, and it keeps the tier-0 finding it shows.

## Private networks only through the tenant's egress list

From this release, inside sandbox tier 1 a plugin's, an agent's or a harness's egress entry reaches public addresses
only; private networks (10/8, 172.16/12, 192.168/16, 100.64/10 and fc00::/7) are reached only through entries in the
tenant's own list, which the tenant's admins write ([Security](https://docs.oz0.ai/concepts/security)). A plugin or agent whose egress
lists a private address now fails lint.

A name is checked when the call connects, by the address it resolves to, so
lint cannot catch an intranet host such as `wiki.corp.example`, nor a public service name that your network resolves
to a private address: a cloud's private endpoint (Azure Private Endpoints, AWS PrivateLink and S3 VPC endpoints) or
split-horizon DNS. Calls to such names from a plugin's or an agent's entry are refused after the upgrade, with the
denial kind `private`.

Before you upgrade, put those names on the tenant's list.

A refusal names the address's class,
not the address: the denial report and the node's log have the address, the tool does not.

## Shell and write sessions run on sandbox tier 1, whoever started the job

Before this release only a job an outside event started had to run its shell and write harness sessions on a node
with sandbox tier 1. Now every such session does: a harness agent with `Bash`, `Write`, `Edit`, an ACP `execute`, or a
harness the platform cannot check runs only on a tier-1 node, in a job a person or an app started too.

A fleet whose
nodes are all tier 0 (Windows nodes, macOS nodes before their sandbox, Kubernetes pods without the tier-1
configuration, Linux nodes without bubblewrap or with `--sandbox off`) sees those jobs fail after the upgrade, after 10
minutes, with `no node with sandbox tier 1 took the session of ...` and the fixes.

Before you upgrade the servers, run the new version's `oz0 doctor` against the cluster: its sandbox floor line
reads only what the old servers already answer (the catalog's agents and the nodes' sandbox tiers), and warns when a
tenant has such agents and no tier-1 node. Then, for each tenant it names, either give a node sandbox tier 1
(`sudo oz0-node sandbox setup` on Linux, then restart the node), or let one node run them without a sandbox, with a
reason, once the servers run the new version: `oz0 node sandbox-optout <node> --reason ...` ([The sandbox
opt-out](https://docs.oz0.ai/fleet/nodes#the-sandbox-opt-out)).

The upgrade opts no node out by itself. A migration that opted every tier-0 node out would keep the old behaviour, but
it would lower the floor for every such node without anyone's reason, and leave each a finding nobody chose; the floor
rises at the upgrade, and an admin who needs a node without a sandbox says why. A dev server (`oz0 dev`, `oz0 server start --dev`) is the exception: it lets every node run them and [says so](https://docs.oz0.ai/fleet/dev-mode#no-sandbox-floor).

A node takes these sessions only once its node binary and its runtime are from this release, a tier-1 node and an
opted-out one alike: an older node would run them without recording where a session ran without a sandbox, so the
servers give it none of them, and it shows a `floor queues` finding until it is upgraded (`oz0 node upgrade --binary <node>`). Upgrade the nodes that run shell and write agents with the servers, or those jobs wait for one that is.

## What a plugin's MCP servers say is held to more

A malicious or careless MCP server, or the plugin that brings it, now meets these checks; a plugin that met them
before is not affected:

- Lint refuses a connection URL that is not a web address. A browser step's, an exchange's, a check's and a guided
step's create link start with `https://` or `http://`, or with a param that holds one
([Connections](https://docs.oz0.ai/build/parts/connections#the-fields)).
- Text another ecosystem wrote is written out. An imported plugin's description (from a Claude Code plugin, an
Agent Plugin or an MCP Registry entry) has its invisible and control characters written as escapes such as
`\u{001B}`, a carriage return included. `oz0 plugin mcp`'s listing of the registry and `oz0 plugin preview` write
them out too, line breaks and tabs as well, so a description can neither write over its own text nor add a row.
- The CLI writes out control characters in what tools and agents wrote. `oz0 run`, `oz0 tool call`, `job get`,
`job step`, `job trace`, `chat` and the live view print every control character but a newline and a tab as an
escape, in results and in the errors a tool's server sends back alike, so neither can clear the screen, set the
clipboard or retitle the window. A script that needs the exact bytes reads `--json`.
- A node rechecks the tools it is sent. It refuses a bundle whose tool IDs are not tool IDs, are the built-ins'
(`oz`) or repeat another tool's, holds a version whose approved listings or endpoint pins carry invisible
characters or name something other than a web origin, and installs no tool of a kind it does not know.
- A result over 31 MiB is a failed call ([Large results](https://docs.oz0.ai/build/parts/tools/results-and-files#large-results)).


## Sitemap

See the full [sitemap](https://docs.oz0.ai/sitemap.md) for all pages.
