Fleet

Sandbox on Windows

Check Windows node capabilities and the PSEC preview. Routed work remains at sandbox tier 0.

For: cluster operators.

Windows nodes currently route work as tier 0. The PSEC probe and preview help evaluate support; they do not mean tier-1 jobs run there.

A Windows node runs sandbox tier 0: work started from inside runs there with the node's own permissions, and work that needs tier 1, every job an outside event starts among it, goes to Linux or macOS nodes of the tenant; a tenant with only Windows nodes takes no webhook, event or chat message until it has one.

Windows tier 1 runs in Windows 11's process security environment (PSEC, CreateProcessSecurityEnvironment in processmodel.dll), the mechanism Microsoft's MXC uses for its process container. The node binary can run a command in it, as a preview, and a node started with --sandbox psec tests it on itself and reports tier 1 when every check holds.

No work that needs tier 1 is sent to a Windows node yet. A node probes for PSEC when it starts and reports what it found, so you can see which of your Windows machines have it. Started with --sandbox psec, it also runs a self-test and reports tier 1 when it passes, but the edge still routes it as tier 0: harness sessions and plugin sandboxes do not run in PSEC yet.

What sandbox check says

oz0-node sandbox check (on Windows, orchestrator-zero-node.exe sandbox check) asks Windows what it has, as MXC does: whether the API set api-win-appmodel-processmodel~securityenvironment is implemented, whether processmodel.dll has its exports, which PSEC contract it supports and which optional features it reports. It never decides by build number. Nothing it calls needs an administrator or changes the machine. install.ps1 prints the first two lines when it installs a node.

& 'C:\Program Files\Orchestrator Zero\orchestrator-zero-node.exe' sandbox check
# Sandbox: tier 0: Windows build 26200.9457 has PSEC contract 1.0 (deny paths, ...), but this node does not run work in it: --sandbox psec runs the node's self-test of it at start
# Windows 26200.9457: PSEC contract 1.0 (deny paths, flags 0x2, which MXC v1.0.0 does not name)
# Fix: Windows' process security environment (PSEC contract 1.0) is here, and this node does not run work in it: started with --sandbox psec, it reports tier 1 when its self-test passes, ...
The machineWhat the node reportsThe fix the Nodes page shows
Windows 11 whose probes find PSEC (seen on build 26200.9457 with contract 1.0, and on 26300.9711 with contract 1.1)Tier 0, its build, the PSEC contract and its featuresPSEC is here, and the node does not run work in it: start it with --sandbox psec; meanwhile run tier-1 work on Linux or macOS nodes
The same, started with --sandbox psec, whose self-test passedTier 1 with PSEC, its build and contract, no fixNone: the Nodes page shows tier 1 (psec 1.0, routed as tier 0)
The same, started with --sandbox psec, whose self-test failedTier 0, the property that failed (one of eight, below)The self-test failed at that property: sandbox check --psec on the node says which check and how
Windows 11 whose probes find no PSECTier 0, its build, no PSECThe build has no PSEC, which Windows tier 1 runs in; the fix names a build seen with it, when one has been seen on that release
Windows Server (MXC does not support it)Tier 0, its buildWindows Server has no tier-1 sandbox

The node sends only these facts and the kind of fix. Management words the fix itself and keeps a build number, a contract or a failed property only when it matches a strict pattern or is one of the eight, as for every node (Nodes). The finding is not one oz0 doctor warns about.

Self-test

The self-test behind --sandbox psec

node run --sandbox psec turns PSEC on for a Windows 11 node. At start the node runs a self-test, once, in containers of its own under a temporary folder it removes afterwards, with its own binary as the bridge and as the process each check runs. It checks every property the Windows tests check, in this order, and stops at the first that fails:

PropertyWhat the check does
launchA process starts in the container and writes in its working directory; a process it starts ends when the first one does.
filesA write outside the working directory, and a read or listing of a stand-in for the node's data directory, are refused; so are a listing of the node's own data directory (--data-dir) and a read of its key. A data directory whose access list lets containers read it (for example a grant to ALL APPLICATION PACKAGES) fails here. Before the node has a data directory, only the stand-in is checked, and the check says so.
egressA direct connection to a listener on the host's loopback, and to a forwarded port, is refused; through the egress proxy an allowed host and a forwarded port answer, the proxy wants the run's token (407 without it), and a host the allowlist does not name gets 403. On a build that closes a container's own loopback the check goes through the egress relay and says so.
ingressA process in the container listens on loopback, and the host cannot reach it. On the egress relay's route, where nothing in PSEC itself keeps other machines from a listener in the container, Windows Firewall must also be on and block inbound connections by default for every profile (domain, private and public); the check fails when one is off or allows them.
job_limitsThe job kills its processes when it closes and holds memory, processes and CPU: 20 processes under a limit of 8 start at most 6, and 512 MiB under a limit of 256 MiB is refused.
fail_closedA process that cannot be put in its job never runs.
protected_paths.git/hooks and .git/config are read-only and .git cannot be renamed aside, while the rest of .git stays writable.
environmentThe process gets the caller's variables, what Windows needs and the proxy's with the run's token, and none of the node's own OZ0_ variables.

Every connection the self-test makes is to a listener it opened on the machine's loopback; the host the allowlist does not name is a name under .invalid, which the proxy refuses before any lookup. Nothing needs an administrator. On Windows 11 25H2 (build 26200.9457) it takes about 6 seconds, most of it the ingress check's wait; on build 26300.9711, through the egress relay, about 25 seconds.

When every check holds, the node reports tier 1 with PSEC. The edge still routes it as tier 0: it polls no sandbox queue, so no harness session or job that needs tier 1 is sent to it, and its own harness sessions and plugin servers run as on tier 0.

The PSEC preview covers commands started with sandbox run. Harness sessions and plugin servers do not use it, and --share-net (a plugin server on the host network) is refused.

When a check fails, the node reports tier 0 with that property, logs each check, and starts as usual.

To see the checks on a machine, run the same self-test by hand:

& .\orchestrator-zero-node.exe sandbox check --psec
# Sandbox: tier 1 (psec 1.0)
# Windows 26200.9457: PSEC contract 1.0 (deny paths, flags 0x2, which MXC v1.0.0 does not name)
# Self-test launch: ok (1.185s)
# Self-test files: ok (284ms); the node's data directory C:\Users\you\AppData\Local\orchestrator-zero-node: listing refused, reading identity\node.key refused
# ...
# PSEC contract 1.0 on Windows 26200.9457 passed the node's self-test (8 checks); ... routed as tier 0 until they run in PSEC.

--sandbox psec is for Windows 11 only; on Linux and macOS the node refuses it, and on Windows Server or a build without PSEC the node reports tier 0 with the probes' reason, as without it. The self-test has passed on one Windows 11 machine, on builds 26200.9457 and 26300.9711, unelevated and in the foreground; it has not run under a Windows service (session 0), nor on GitHub's runners, which are Windows Server and have no PSEC.

The egress relay

A process in a PSEC container reaches the network only through the node's egress proxy. Where the Windows build allows it, the node's bridge listens on a loopback port inside the container and forwards to the proxy. Some builds refuse a container's connection to its own loopback (build 26300.9711 does), and the only settings that would open it open every listener on the machine's loopback to the container too. The node never uses them. On such a build it reaches the proxy through a relay instead:

  • How the node knows: its self-test, at start, starts a container that listens on its own loopback and dials itself. Where Windows refuses that, the checks take the relay; elsewhere, the bridge. It asks the build, never its number. The node hands the answer to its runtime as OZ0_SANDBOX_PSEC_ROUTE, which every sandbox run the runtime starts inherits, so the build is asked once per node start. A sandbox run started by hand without the variable asks once itself, and fails with that reason when the question gets no answer, rather than guess. OZ0_SANDBOX_PSEC_ROUTE=bridge or relay names one by hand.
  • What the relay is: the node binary's sandbox relay, started for each run outside the container, as an AppContainer profile of the node's own, oz0.egress-relay, with no capabilities, in a job that ends with the run. It listens on one port of 127.0.0.1:47330-47361 and forwards each connection to the run's egress proxy, which still wants the run's token and allows only the run's allowlist. At most 32 runs at once on a machine.
  • What the container may reach: its spec names the relay's endpoint as its proxy and the relay's profile as its one network peer. On build 26300.9711 the container reaches its relay and nothing else: its own loopback, listeners on the machine's 127.0.0.1 and ::1, another run's relay, the LAN, private addresses and the internet are all refused. Other processes on the machine do not reach the relay either.
  • What keeps other machines out: Windows takes the relay's spec only as MXC writes it, with the privateNetworkClientServer capability and no ingress table; with an ingress table beside the proxy, the container does not start. So on this route PSEC itself does not refuse another machine's connection to a program listening on a network address in the container: Windows Firewall does, by blocking inbound connections that no rule allows.
    The self-test's ingress check holds the node to that (on, and blocking by default, for every profile), and the node reports tier 0 when it does not hold.

What it cannot see is an inbound allow rule for a program a run starts, such as one Windows Firewall's prompt made when someone clicked Allow, or one a policy adds: whether such a rule opens that program's listener in a container to the network is not measured yet.

On a machine whose PSEC runs take the relay, keep inbound allow rules for the programs your plugins run (Python, Node.js) off, or scoped to the addresses that need them. Listing them: Get-NetFirewallRule -Direction Inbound -Action Allow -Enabled True | Get-NetFirewallApplicationFilter | Where-Object Program -ne Any.

  • What it changes on the machine: the node makes the profile for the user it runs as (no administrator), and grants it read and execute on the node binary and access to each run's sockets folder; a binary under C:\Program Files is readable by every AppContainer already. To remove the profile, as an uninstall does, run orchestrator-zero-node.exe sandbox relay --remove-profile; a later run makes it again.

No firewall rule and no administrator are needed where Windows delivers the container's connection to the relay, as build 26300.9711 does. Microsoft's MXC documents an inbound firewall rule for such a relay, and Windows Firewall holds the relay when a rule names the node's program (one its prompt made when the program listened on a network address, for example). Then the container's connection gets no answer, the self-test fails at egress with that reason, the node stays at tier 0, and sandbox check --psec prints the command for this node:

# In an elevated PowerShell, once: lets this node's relay alone receive connections, on 127.0.0.1 and its ports only.
New-NetFirewallRule -DisplayName "Orchestrator Zero egress relay" -Direction Inbound -Action Allow -Protocol TCP -LocalAddress 127.0.0.1 -LocalPort 47330-47361 -Program 'C:\Program Files\Orchestrator Zero\orchestrator-zero-node.exe' -Package <the profile's SID, which sandbox check --psec prints>
# To undo it:
Remove-NetFirewallRule -DisplayName "Orchestrator Zero egress relay"

The rule names the relay's program, its ports and its profile, so no other program, profile or port is matched. Before adding it, look for rules Windows Firewall's prompt made for the node binary (Get-NetFirewallApplicationFilter | Where-Object Program -like '*orchestrator-zero-node.exe' | Get-NetFirewallRule) and remove those instead, if nothing else needs them. The installer adds no rule.

The PSEC preview

With OZ0_SANDBOX_PSEC_PREVIEW=1 in its environment, the node binary's sandbox run runs a command in a PSEC container on Windows 11; without it, sandbox run refuses, as on any node without tier 1. Starting a preview command needs no administrator, user or service. A firewall that blocks the relay may need the scoped rule above. The runtime does not use PSEC; routed work remains at tier 0. Use it to evaluate PSEC on that machine.

$env:OZ0_SANDBOX_PSEC_PREVIEW = '1'
& .\orchestrator-zero-node.exe sandbox run --work C:\work\job1 --allow api.github.com -- C:\Windows\System32\curl.exe -sS https://api.github.com/zen

What a command in the container can do, as the Windows tests check it on Windows 11 25H2 (build 26200.9457):

  • Files: it writes its working directory, the --write folders and a TEMP of its own, which Windows makes for the container and removes after it, and nothing else. It reads the system, the --read paths and the node binary. It cannot read the node's data directory, keys or another job's folder. .git/hooks, .git/config and agents' settings are read-only, and their folders cannot be renamed aside while it runs.
  • Network: only the node's egress proxy, which wants the run's own token: a request without it gets 407, a host the allowlist does not name 403. Every direct connection is refused, to the internet, the LAN, the router and the machine's own loopback ports alike, and ICMP too.
    The node's forwarded ports, such as the LLM proxy's, are reached through the proxy, so a program must send loopback URLs through HTTP_PROXY (the sandbox sets no NO_PROXY).

A program inside the container reaches the proxy through the node's bridge, which listens on a loopback port inside the container and forwards over an AF_UNIX socket, or, on a build that closes a container's own loopback, through the egress relay.

Name lookups still go through Windows' own resolver, outside the container, so a lookup of any name gets an answer, but nothing connects to it.

  • Limits: a job object holds the kind's limits: the memory of all its processes together, a number of processes (Windows counts processes, not threads), and a hard cap on CPU time. It also blocks the clipboard, input injection, desktops and system settings. When the command's first process ends, or the run is cancelled, every process it started ends too, and the run returns only once they have, so the end-of-run look at the repository's git files sees what they left.
  • Environment: exactly what the caller passes, plus what Windows programs need (SystemRoot, a system PATH when none is given) and the proxy's variables. Never the node's own environment.
  • Programs: the preview has run cmd.exe, curl.exe and the node's Python 3.13. Node.js 24 has run through MXC separately; that does not validate the node's preview path. Git remains unvalidated.

The preview has not run as a Windows service (session 0). Harness sessions, plugin servers and routed tier-1 work remain unavailable in PSEC.

Validation limits

The PSEC self-test has passed on one Windows 11 machine, on builds 26200.9457 and 26300.9711, unelevated and in the foreground. That evidence does not cover Windows services, other machines or all firewall policies.

On the relay route, Windows Firewall supplies inbound isolation. The self-test checks that each firewall profile is on and blocks inbound traffic by default. It does not establish whether an existing allow rule for a program exposes that program's container listener to another machine. Review those rules before using the preview.

Run oz0-node sandbox check --psec on the target node to see which local property passes or fails. A successful result still leaves routed work at tier 0.

Previous section links

Previous validation section: Read the validation limits.

Windows host probes: Read the self-test and validation limits.

Copyright © 2026