Upgrade a cluster

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:

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). 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).

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.

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).
  • 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).
Copyright © 2026