# Isolation and access boundaries

> What an integration can do, what it cannot, and what ensures it: the OS user, directories, addresses, credentials.

This page is written for those who decide whether to let someone else’s code into their network. It
describes mechanisms, not intentions: what exactly limits an integration and where the
boundary lies beyond which a limit becomes your responsibility.

## A process per connection, a user per connection

Each **connection** (not the integration, but precisely the link to a specific system) runs
as its own process under **its own OS user**. The connector assigns the identifiers and
remembers them so that nothing gets mixed up after a restart.

A user per connection rather than per integration, because two connections of one
integration hold **different** credentials: production and a sandbox must not reach
each other.

If the connector cannot switch users (it is not running as root — root specifically is
checked — or the container was started with `--user`), integrations **do not start at all**. This
is deliberate: working “as it happens, without isolation” is a different security promise,
and we do not make it. The git bridge works fully in this mode.

## What is visible on disk

The state directory is laid out so that OS permissions repeat the trust boundary:

| Directory | Permissions | Who and what |
|---|---|---|
| `<state_dir>/` | `0755` | traversable |
| `<state_dir>/identity` | `0700` | the agent’s key — the core only |
| `<state_dir>/secrets` | `0700` | tokens of git providers and connections — the core only |
| `<state_dir>/plugins` | `0755` | integration code and the interpreter — readable |
| `<state_dir>/runtime` | `0755` | the runtime — readable |
| `<state_dir>/instances` | `0711` | traversable but **not listable**: a connection cannot even learn its neighbours’ names |
| `<state_dir>/instances/<GUID>` | `0700`, owned by the connection’s user | the working directory: the process’s `HOME` and `TMPDIR` |

Secrets sit in a directory the connection’s user has no access to. The core reads them.

You can check this on your machine with one command — this is what a working
connector with one connection looks like:

```console
$ ls -ld /var/lib/connector /var/lib/connector/*
drwxr-xr-x root   /var/lib/connector
drwxr-xr-x root   /var/lib/connector/catalog
drwx------ root   /var/lib/connector/identity     ← the agent's key
drwx--x--x root   /var/lib/connector/instances    ← traversable, not listable
drwxr-xr-x root   /var/lib/connector/plugins
drwxr-xr-x root   /var/lib/connector/runtime
drwx------ root   /var/lib/connector/secrets      ← tokens
-rw------- root   /var/lib/connector/state.json

$ ls -ld /var/lib/connector/instances/*
drwx------ 61000  /var/lib/connector/instances/FHTZDP8A706GTS67   ← its own, owned by its user
-rw------- root   /var/lib/connector/instances/uids.json
```

Connection users are assigned by the connector from a separate range and
remembered so that nothing gets mixed up after a restart.

## Credentials: the plugin does not see them

An integration gets no tokens — neither in its settings nor in environment variables. It
asks the connector to make a request, and the connector:

1. checks the address, method and path against the declared rule;
2. **strips** the authorisation header if the integration tried to set its own;
3. inserts the credentials itself — as a token, an “account + secret” pair or
   the tracker’s own header, as declared in the manifest; and where the service’s
   tokens live for an hour (Google), it **issues them itself** — from the service account key
   via a signed JWT or from your own account’s refresh token — and refreshes them
   before they expire. Even then the plugin sees only the answer it got;
4. records the call in the connection’s log: which credentials were used (by name,
   not by value), the method, the address, the response code and the duration.

The process’s environment variables contain only the connection’s identifier, its slug and
**the settings you entered in the form**. Neither the agent’s key nor tokens are there.

## Where it may go

The permission is derived from **your own field** in the connection form: “to the address from
`base_url`”, not “to github.com”. So the catalogue shows the rule, and the specific
address appears once you have entered it. The rule limits the host, methods and path
prefixes.

In addition, the connector **refuses** to go to link-local addresses — that is where cloud metadata and
credential services live. Private ranges, on the contrary, are allowed
deliberately: the point of the connector is to reach systems inside your network, and
banning private networks would ban the product itself.

The connector limits the rate of tool calls from runs on its side: no more than 120
calls per minute per connection.

**The exception is the web rule** of the [“Web”](/en/docs/connector/web) integration: there the model
names the address, not you, so the rule works the other way round. Instead of a host — your allow and
block lists of sites and content types, and private ranges are **always forbidden**:
the name is resolved before every request, a single non-public address means refusal, and
the connection goes to exactly the checked address. Credentials are never attached to
such a request.

<Callout type="warn">
The honesty boundary: isolation is at the level of **the OS user and credentials**, not of
a network namespace. An integration’s process is an ordinary process on your machine and
can technically open a socket itself; it has none of your secrets, so such
a request authenticates nowhere. If you need a ban at the network level, put
it in place with your own means (per-user firewall rules, a separate namespace,
a dedicated machine for the connector) — this is exactly the case where we will not claim
the question is closed.
</Callout>

## What reaches us

The connector sends a **status report** — the same as what you see in the app:

- the agent’s version, online/offline, the list of repositories it serves;
- for each connection: the slug and name, the integration and its version, the status, the last
  error, the development project, the **list of tools** and the **allowed addresses**;
- the facts of tool calls: which tool, how long it took, whether there was an error.

The report **never contains secrets** — this is a property of its format, not of the carefulness of
the calling code.

Tracker events are sent separately, but only those addressed to the bot directly and only in
the selected projects: [what exactly](/en/docs/connector/trackers#what-reaches-us-from-the-tracker).

At the model’s request, the content it read with the connection’s tools for a
particular answer also leaves: pages, files, tickets, a pull request diff, web pages found
— exactly what the tool returned and nothing more; every call is recorded in
the connection’s log.

Everything that comes to us is then processed by the model according to the organisation’s plan: on OpenRouter plans — a third-party provider; on dedicated capacity and in a custom setup — a dedicated machine, without third-party providers. See [Plans](/en/docs/tariffs).

## Deleting a connection

Deletion removes the connection from the config and stops its process. The secret files and
the working directory stay on disk — the operator deletes them, deliberately; the connector does not
clean up directories beyond what it created itself at that moment. If the machine is being
decommissioned, remove the whole `state_dir`: it holds the agent’s key and all tokens.
