# Networking & egress Every sandbox runs with one of three egress policies, chosen with the `network` argument at create time. The default is `off` - no network at all. - **`off`** (default) - no network device. The sandbox cannot reach anything. Maximum isolation. - **`restricted`** - egress only through a forward proxy that allows a fixed set of hosts over HTTPS.`HTTP_PROXY` and `HTTPS_PROXY` are already set inside the sandbox, so proxy-aware tools (pip, npm, curl, standard HTTP libraries) work as-is. Direct DNS and raw sockets do not resolve. - **`open`** - unrestricted egress. Use only when you trust the code or genuinely need arbitrary network access. Every mode supports pause and resume - a networked sandbox's IP is held across the pause and restored on resume, so long-lived sessions can park with `restricted` or `open` networking too. ## The default restricted allowlist On `restricted`, a sandbox can reach a curated set of hosts that cover the things agent code usually needs - package registries, source hosting and the major model APIs: - Python packages - `pypi.org`, `pythonhosted.org` - npm - `registry.npmjs.org` - Debian apt - `deb.debian.org`, `security.debian.org` - Source + releases - `github.com`, `githubusercontent.com` - Model APIs - OpenAI, Anthropic, Google, Mistral, Cohere, Groq, and Hugging Face Each entry also matches its subdomains, and only HTTPS (port 443) is allowed. The live list is returned as `default_egress_domains` from `GET /v1/sandboxes/limits` (and `Sandbox.limits()` in the SDKs). ## A custom egress allowlist When your code needs a host the default set does not cover - an internal API, a private package mirror, a vendor endpoint - declare your own allowlist instead of opening the sandbox up to the whole internet. It is a tier strictly between `restricted` and `open`: still HTTPS-only, still proxy-mediated, still no raw sockets or DNS - just scoped to the domains you name. > **A custom list replaces the default - it does not add to it** > > When you pass `allow_domains`, those domains become the entire allowlist for that sandbox. Include any registries you still need (e.g. `pypi.org` if you `pip install`). Start from `default_egress_domains` and edit from there. Custom allowlists are available on every plan. Pass it per sandbox at create time: **Python:** ```python from orkestr import Sandbox # Restricted egress, but reach your own hosts instead of the default set. sbx = Sandbox.create( template="python-3.12", network="restricted", allow_domains=["pypi.org", "pythonhosted.org", "api.internal.acme.com"], ) # See the default set to start from: print(Sandbox.limits().default_egress_domains) ``` Entries are bare hostnames - no scheme, port, path or wildcard.`api.internal.acme.com` is matched along with its subdomains. The list only takes effect on `restricted`; it is ignored for `off` and `open`. ## Baking a default into a template You can also bake an allowlist into a [custom template](https://orkestr.eu/docs/sandboxes/templates). Every restricted sandbox booted from that template inherits the list, so a team can standardize the reachable hosts once. A create call's own `allow_domains` still wins over the baked default when both are present. ```bash curl -X POST https://api.orkestr.eu/v1/templates \ -H "Authorization: Bearer $ORKESTR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "acme-agent", "base_template": "python-3.12", "recipe": ["pip install httpx"], "network": "restricted", "allow_domains": ["pypi.org", "pythonhosted.org", "api.internal.acme.com"] }' # Every restricted sandbox booted from this template inherits the list, # unless the create call passes its own allow_domains (which wins). ``` > **Resolution order** > > For a restricted sandbox the effective allowlist is, in order: the create call's `allow_domains` if given, else the template's baked list if it has one, else the platform default set. ## Public preview URLs Egress is about what a sandbox can reach *out* to. Preview URLs are the other direction: exposing an HTTP port a process inside the sandbox is serving at a public URL. Open it in a browser or embed it in your app and you get a live preview of a dev server running inside the sandbox. Call `sbx.get_host(port)` (or `GET /v1/sandboxes/{id}/host?port=N`) and you get back a hostname of the form `-.sbx.orkestr.run`, where `` is the sandbox's public id with the `sbx_` prefix dropped and lowercased. A sandbox `sbx_01HXYZ...` serving on port `3000` is reachable at `https://3000-01hxyz....sbx.orkestr.run`. ```python from orkestr import Sandbox # A networked sandbox is required - "off" has no interface to expose. sbx = Sandbox.create(template="node-22", network="open") # Start a server inside the sandbox (here, in the background): sbx.exec("nohup python3 -m http.server 3000 >/tmp/srv.log 2>&1 &") host = sbx.get_host(3000) # "3000-01hxyz....sbx.orkestr.run" url = f"https://{host}" # open it in a browser or embed it in your app ``` > **The URL is public by default** > > With no auth, a preview URL's only capability is the unguessable sandbox id baked into the hostname - treat it like a secret link: anyone who has it reaches the port while the sandbox is running. The host is stable for the sandbox's lifetime but serves traffic **only while the sandbox is running** - a paused or terminated sandbox returns nothing. To require a token instead, see **Lock a preview down** below. Two requirements: the sandbox must be created with `network="restricted"` or `network="open"` (an `off` sandbox has no network interface and no port to expose), and exposing a public port needs a card on file - the free tier cannot. WebSockets ride through, so a dev server's hot-reload (HMR) works through the preview URL. For building these links in a UI, the sandbox GET and list payloads also carry `preview_host_base` - the port-less `.sbx.orkestr.run` suffix, or `null` when the sandbox cannot expose a port. ## Lock a preview down Pass `preview_auth=True` at create (`previewAuth: true` in JS) and the sandbox's preview URLs stop being public: every request must carry the minted `preview_auth_token` or the edge returns `401`. Use it when the served content should not be reachable by anyone who simply learns the URL. ```python from orkestr import Sandbox # Opt in at create: preview URLs now require an access token. sbx = Sandbox.create(template="node-22", network="open", preview_auth=True) sbx.exec("nohup python3 -m http.server 3000 >/tmp/srv.log 2>&1 &") sbx.preview_auth_token # the minted secret, e.g. "wUc7dj8_..." # 1) Browser / iframe: preview_url() appends the token as a query param. # First hit -> 302 that sets an ork_preview_token cookie, so the page # AND its relative sub-resources (css/js/images) stay authenticated. embed_url = sbx.preview_url(3000) # https://3000-.sbx.orkestr.run?ork_preview_token=wUc7dj8_... # 2) Programmatic (curl / fetch / a backend proxy): send the header instead. # curl -H "X-Orkestr-Preview-Token: " https://3000-.sbx.orkestr.run # Without a valid token the edge returns 401. A public sandbox (the default, # no preview_auth) has preview_auth_token = None and needs nothing. ``` There are three ways to present the token, and they cover both audiences: - **Header** - `X-Orkestr-Preview-Token: `. The clean path for programmatic callers (curl, fetch, a backend proxying the preview). - **Query param** - `?ork_preview_token=`, what `preview_url(port)` builds. On the first hit the edge answers with a redirect that sets an `ork_preview_token` cookie, so the page and every relative sub-resource it loads (CSS, JS, images) stay authenticated - this is what makes a full app embed cleanly in an `