Refusals
When a world refuses a request your app makes, the refusal names the side that refused and links the one change that
fixes it. This page lists every refusal, keyed by the rule the refusal carries. Each refusal's message ends with a
link to its entry here.
A request passes only when the app declares it and the world grants it, and a world answers a vendor's API with its twin. So a refusal names one missing side:
| missing | meaning | where the fix goes |
|---|---|---|
declaration | the app (an image, a browser program) does not declare it | the app's declaration: its Dockerfile or compose file, or its program's hosts |
grant | the world does not grant it | the world's .volter/world.json, usually runtime.network |
transport | the way the client connects cannot carry what the world grants | the client: send it as an HTTP request, or through the world's proxy |
twin | the host is a twin's, and no twin in this world serves this path | the world's twins |
A world's refusals are not a security boundary. The Node redirect and the world's proxy refuse cooperatively: a program that ignores them is not refused. A native image run's Seatbelt profile reproduces what Docker lets a container do, so an app that depends on your machine (its files, its other services) fails here the way it would in Docker. Run only trusted code natively; untrusted code runs in the browser tab.
Where a refusal shows
A Node process run in the world (volter world run, a service) gets an Error. Its message is the refusal's text,
then the missing side and the fix link in parentheses. error.worldRefusal holds the record. An error from a socket
also has code: 'ECONNREFUSED'.
Any other client behind the world's proxy (curl, Python, Go, a browser) gets 502 Bad Gateway:
- the header
X-Volter-World-Refusal: <missing>; - the refusal's text, with the missing side and the fix link;
- the record as JSON on the next line. A browser gets a page with the record in
<script type="application/json" id="volter-world-refusal">.
A host that does not answer is a 502 without the header; no rule refused it.
A native image run (macOS Seatbelt) refuses a connection with EPERM. When the run carries its image's declaration
(VOLTER_WORLD_DECLARATION, set by the images CLI from the image's runtime binding), a Node process there gets the
record for rule native-direct. For other programs, Seatbelt writes a kernel log line, and the address in it is masked:
Sandbox: <program>(<pid>) deny(1) network-outbound remote:*:<port>The browser tab refuses with its own message, which carries no record yet:
<program> may not reach <host>: the tab's network is closed unless granted, by the World the tab is attached to or the page that embeds it. Grant <program> egress to <host> there to allow it.Its fix is the same runtime.network grant as not-granted below.
The record
{ "kind": "volter-world-refusal", "version": 1, "capability": "network.egress", "target": "https://example.com/",
"missing": "grant", "rule": "not-granted", "reason": "the World network policy does not allow GET https://example.com",
"grantField": "runtime.network.egress", "see": "https://world.volter.ai/docs/reference/refusals#not-granted" }| field | meaning |
|---|---|
capability | what was asked for: network.egress (a destination outside the world), network.services (one of the world's own services), network.ownLoopback (a listener the run holds), network.host (anything else on the machine's network), files.read (a file the app reads), files.persist (a path the app keeps as its data) |
target | the URL, host:port or path refused |
missing | declaration, grant, transport or twin |
rule | this page's entry |
reason | the refusal in words |
grantField | where missing is grant and one field grants it: that field of .volter/world.json |
declaredBy | where the app's declaration is derived from a file: the capability, the file and its line |
see | the link to the entry |
Refusals seen in a world
These lines were recorded from one disposable world. It ran one Stripe twin, and its runtime.network granted only
https://registry.npmjs.org (phase: build, that origin also in passthrough, publicReads: false). The world ran
through volter-world run, on twin-world main at 7aadc14 with this page's change applied.
A Node fetch to a host the world does not list:
[twin-inject] blocked untwinned external fetch to example.com (the World does not grant it: runtime.network.egress; fix: https://world.volter.ai/docs/reference/refusals#not-granted)The same request from curl, through the world's proxy:
x-volter-world-refusal: grant
example.com is outside this World: no twin serves it, and the World does not reach the internet for it. (the World network policy does not allow GET https://example.com: the World does not grant it: runtime.network.egress; fix: https://world.volter.ai/docs/reference/refusals#not-granted)
{"kind":"volter-world-refusal","version":1,"capability":"network.egress","target":"https://example.com/","missing":"grant","rule":"not-granted","reason":"the World network policy does not allow GET https://example.com","grantField":"runtime.network.egress","see":"https://world.volter.ai/docs/reference/refusals#not-granted"}The same fetch in a sandbox (volter world up --sandbox), whose record names no grant field:
[twin-inject] blocked untwinned external fetch to example.com (the World does not grant it; fix: https://world.volter.ai/docs/reference/refusals#sealed)A path of Stripe's host that the Stripe twin does not serve, from curl:
x-volter-world-refusal: twin
checkout.stripe.com is outside this World: no twin serves it, and the World does not reach the internet for it. (no twin in this World claims /unclaimed on checkout.stripe.com (stripe): no twin in this World serves it; fix: https://world.volter.ai/docs/reference/refusals#unclaimed-path)A run carrying an image's declaration that lists only https://api.stripe.com for egress:
[twin-inject] the guest does not declare egress to https://example.com (the guest does not declare it; fix: https://world.volter.ai/docs/reference/refusals#undeclared)Its record carries "declaredBy": {"capability": "network.egress", "file": "Dockerfile", "line": 1}.
Every refusal
not-granted
network.egress, missing grant. The world's runtime.network does not list the destination. Reads need
egress; any method other than GET, HEAD or OPTIONS needs the destination in writes.
Fix: add the origin, the exact path or the subtree (ending in /) to runtime.network.egress, or to
runtime.network.writes for a write. A world whose parent lists less is held to the parent's list.
sealed
network.egress, missing grant. The world is a sandbox (volter world up --sandbox). It reaches only its twins, so
no grant lists anything for it.
Fix: give the destination a twin, or run the world without --sandbox and grant it in runtime.network.
policy-unreadable
network.egress, missing grant. The process's copy of the world's network policy (VOLTER_WORLD_NETWORK_POLICY)
cannot be read, or belongs to another world. Every destination outside the world is refused.
Fix: run the process through the world (volter world run) rather than copying its environment by hand.
invalid
network.egress, missing grant. The request's address or path cannot be read unambiguously: a backslash, an encoded
/ or \, or a malformed escape. It is refused wherever the world lists any grant.
Fix: send the request to a plain URL.
unclaimed-path
network.egress, missing twin. The host belongs to a twin in this world (several vendors can share one host), but
no twin here serves this path. The request is neither answered by the wrong twin nor sent to the real vendor.
Fix: add the twin that serves that API to the world, or set its *_TWIN_URL. If no twin serves it yet, the API needs
one.
undeclared
network.egress, missing declaration. The app's declaration does not list the destination. A Dockerfile declares
open egress, so this comes from a declaration that lists hosts: a browser program's hosts, or an image binding's
capabilities.network.egress.
Fix: add the host to that list; declaredBy names the file and line.
raw-socket-twinned
network.egress, missing transport. The client opened its own socket to a host a twin serves, so the twin never
saw the request.
Fix: point the SDK at the twin's endpoint variable the message names, which the world sets when it runs the app.
raw-socket-app
network.egress, missing transport. The client opened its own socket to a host the world routes to the application
(volter-world app-url --host).
Fix: reach that host with an HTTP request, which the world routes.
inspected-transport
network.egress, missing transport. The destination's origin has a grant, so the world reads every request to it
(its path and method are checked one request at a time), and this client connects in a way that cannot be read request
by request: a raw socket, HTTP/2, a protocol upgrade, or an undici dispatcher of its own.
Fix: send it as an HTTP/1.1 request, or, in a phase: build world, list the exact origin in
runtime.network.passthrough (its grant must be the whole origin).
guest-upgrade, guest-http2
network.egress, missing transport. A local guest's HTTP leaves through the world's guest listener, which carries
plain HTTP requests only.
Fix: use HTTP/1.1 without an upgrade from the guest.
guest-inspected
network.egress, missing transport. A World Machine guest asked for a destination the world reads request by request.
The world cannot read it without losing the address it checked for the guest.
Fix: grant the whole origin, or reach it from the world's host.
native-direct
network.egress, missing transport. A native image run reaches other hosts only through the world's proxy, so the
world sees each request, and this client dialled the host directly.
Fix: send the client's traffic through the world's proxy: HTTPS_PROXY (for Node's own fetch, NODE_USE_ENV_PROXY=1).
service-undeclared
network.services, missing declaration. The destination is one of the world's own services, and the app's
declaration does not name it.
Fix: name the service in the image's compose depends_on (capabilities.network.services).
own-loopback-ungranted
network.ownLoopback, missing grant. The run reached a listener it holds, and the world sets
runtime.network.ownLoopback: false.
Fix: remove ownLoopback: false from runtime.network, here and in any parent world.
guest-private
network.host, missing grant. A guest asked for an address on the machine's own network that is neither one of
the world's endpoints, nor a service it declares, nor its own declared listener. No world grants a guest the
machine's network.
Fix: if it is the run's own listener (worker processes on ports no Dockerfile names), the image declares own loopback, which every container image does. If it is a service the app needs, make it a service of the world, and declare it.
host-undeclared
network.host, missing declaration. A process that is not confined from the machine's network, run with a declaration
that does not include it, asked for an address on it.
Fix: declare it (compose network_mode: host), and see guest-private for what a world grants a guest.
persist-undeclared
files.persist, missing declaration. The app keeps a path as its data that its declaration does not name.
Fix: declare the path as a VOLUME. A declared path is writable for the run and does not outlive it.
read-undeclared
files.read, missing declaration. An image's process read a file that, in Docker, the container could not see: a
file in your home folder, for example. A container sees its own filesystem, the world's provided files and its declared
mounts. Natively it would have worked, and in Docker it would not, so the native run fails the same way.
Fix: if the app needs the file, mount it (a compose bind mount inside the world's folder) or copy it into the image.
mount-outside-world
files.read, missing grant. The image declares a bind mount whose host path is outside the world's folder. No world
grants a host path it does not own.
Fix: move the mounted data into the world's folder, and mount it from there.
read-invalid
files.read, missing declaration. The path read is not absolute, so it names no file the decision can place.
Fix: read the file by its absolute path.