docs
Bounds

Reach

Two questions get asked about every request, and the mistake is answering them together. What an agent may reach and whose credential goes with it have very different blast radii.

The road is open

Because a registry is never one host

deploy/config.yaml
defaults:
  grants:
    - id: web
      host: "*"
      injection:
        kind: none

An agent asked for a hello-world page needs npm install before it needs anything else, and a registry is never one host — npm is a registry and a CDN, PyPI is an index and a file server, a git clone is three names before it is a checkout. A list of them is a list that is wrong by one, and wrong by one is worse than either end: it looks like it works until the afternoon it doesn't, and what the agent does then is not raise its hand. It reads the deny as the internet being down and writes the page it was asked for as a paragraph about not being able to write it.

Every request still crosses the proxy, is still matched, and still lands in the audit log with the host and the path it went to. Delete the web grant and the plane is deny-by-default again, host by host, exactly as it was.

The keys are not

kind: none is the whole of why that line is safe to write down

Nothing of yours is attached to anything reached through the open grant. A grant on * that carried a credential would put that secret on every server the agent reaches, so it is refused where the config is read rather than discovered later:

Invalid configuration:
  - defaults.grants[0] is host "*" with a bearer credential, which would put that secret on
    every server the agent reaches. Name the host, or use injection: { kind: none }

A named host always wins the request over the open one, so the model's key goes to the model and nowhere else. The token is written onto the request after it has matched, on the way out — the agent never holds it, and an agent talked into posting its environment somewhere has nothing to post.

deploy/config.yaml
agents:
  - id: scout
    grants:
      - id: github-issues
        host: api.github.com
        pathPrefix: /repos/acme/website/issues
        methods: [GET, POST]
        injection:
          kind: bearer
          token: { ref: GITHUB_TOKEN }

ref names a variable of the control plane's own environment, never a value. An agent's own block adds to the defaults rather than replacing them, and a grant it declares with the same id wins — which is how one agent is narrowed without narrowing the rest.

Adding a host

The refusal arrives in a turn, and the answer should not be a deploy

│ config                                                         │
│                                                                │
│ An agent has no route out of its own: the sandbox sits on a    │
│ network with nowhere to go, and every request it makes is one  │
│ the proxy was told beforehand to allow. A host that is not on  │
│ this list is a connection refused.                             │
│                                                                │
│ A host opened here carries nothing. Keys are attached by name, │
│ in deploy/config.yaml, and that is the half of a grant this    │
│ screen has no box for — so what is added here widens where an  │
│ agent may go and not one thing about what it may spend.        │
│                                                                │
│ ● api.anthropic.com                       with a model         │
│ ● api.openai.com     /v1/responses  POST  for searching        │
│ ● api.github.com                          from the file        │
│ ● api.chess.com                           opened here          │
│ + a host                                                       │
│ ╭────────────────────────────────────────────────────────────╮ │
│ │ carries nothing   opened here   ⌫ closes it                │ │
│ ╰────────────────────────────────────────────────────────────╯ │

Host by host is a fine way to run this, and it stops being one the moment a host has to be added by editing a file on the server and putting the plane back up. What that costs is not the minute: it is that the refusal arrives in an agent's turn, hours after the file was last thought about.

One box and one word — api.chess.com, or the whole URL you were looking at when the refusal happened, since the host is read out of it. What a person has to hand at that moment is the address in the error and not the host in it. There is no field for a path, a method, an id or a key. The last of those is the point: this screen writes injection: { kind: none } and has nowhere to express anything else, so the console can widen where an agent goes and can never decide what it spends. That half stays in the file, which is why the rows that came from it refuse ⌫ and say which list to change them on instead.

A grant the plane derived is marked for what derived it — with a model, for searching — because a host you cannot account for is one nobody dares close. Models and web search are where those two come from.

What it does not claim

The boundary that carries weight is the one around the secrets

An agent that can run code in a sandbox and reach the internet can send what it read to somewhere you did not choose. That was already true of any grant broad enough to be useful, and it is the reason the credential is the thing held away from it rather than the address. A stolen agent gets the reach it had; it does not get your account. Trust is the rest of that.

One kind of grant carries a scope the method cannot: a git repository, where a clone and a push are both a GET and a POST under the same path. Repositories is how the branches are named, and how a push to the wrong one is turned down.