# 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`](./config.md) |
| `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:

```text
Sandbox: <program>(<pid>) deny(1) network-outbound remote:*:<port>
```

**The browser tab** refuses with its own message, which carries no record yet:

```text
<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

```json
{ "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:

```text
[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:

```text
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:

```text
[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:

```text
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:

```text
[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.
