> ## Documentation Index
> Fetch the complete documentation index at: https://docs.nanny.run/llms.txt
> Use this file to discover all available pages before exploring further.

# Governance server

> Run Nanny enforcement across multiple processes and machines using the standalone governance server.

## When to use the governance server

Every governed app runs under one: `nanny run --serve` starts a governor and
launches your `[start].cmd` underneath it. On loopback that needs no
certificates and no network setup, so a single process on one machine is
already the simple case, and this page is only about what changes when there
is more than one.

What changes is that agents in **separate processes or on separate machines**
share one enforcement boundary. Common scenarios:

* **Microservices:** three containers, each running an agent, all governed by one rule set
* **CI workers:** a coordinator spins up workers on different machines; you want every worker held to the same rules
* **Development clusters:** a Kubernetes pod runs the server; a local dev agent connects to it while you iterate

If your agents all run inside one process, even a complex CrewAI or LangGraph
pipeline with many agents, the governor you already have covers them and
nothing here applies: one `nanny run --serve` governs the whole fleet.

***

## The two deployment modes

One command, and the address decides the rest.

| Mode          | Command                                  | Transport                               | When to use                            |
| ------------- | ---------------------------------------- | --------------------------------------- | -------------------------------------- |
| Same machine  | `nanny run --serve`                      | Plain HTTP on loopback, no certs needed | One process, or several on one machine |
| Cross-machine | `nanny run --serve --addr 0.0.0.0:62669` | mTLS, mandatory, certs required         | Docker, Kubernetes, remote agents      |

The bind address determines the security posture. The default (`127.0.0.1:62669`) is loopback, plain HTTP, no cert setup required. Binding to a non-loopback address enables network access and makes mTLS mandatory; the server refuses to start without cert files.

Going from the first row to the second is a flag and a certificate bundle. Nothing about your app, your `nanny.toml`, or the rules it is held to changes.

***

## Setup: local (same machine, no certs required)

This covers the common case where all your agents run on the same machine but in separate processes.

**1. Start the server:**

```bash theme={null}
nanny run --serve
```

This requires the directory to have run `nanny init` first: the server's state is keyed by that app's permanent `app_id` (from `.nanny/app.json`), so two unrelated apps' servers on the same machine never collide. The server starts on `127.0.0.1:62669` (loopback) by default, generates a session token and a separate proxy token, and writes the address and both tokens to `~/.nanny/servers/<app_id>/`. The token is printed at startup for reference. No certs needed.

**2. Join it from your agents, explicitly, by app id:**

```bash theme={null}
nanny run --join=<appId>
```

`--join` reads the address and tokens from `~/.nanny/servers/<appId>/`, then injects `NANNY_BRIDGE_ADDR` and `NANNY_SESSION_TOKEN` into the agent process . There's no auto-detection: if the target app id isn't reachable, `--join` fails loudly rather than silently falling back to local enforcement.

A confirmation message appears before your agent starts:

```
nanny: network server detected at 127.0.0.1:62669
nanny: governance enforced remotely, limits and rules apply
```

***

## Setup: cross-machine (mTLS required)

When your agents run on different machines, the server must verify that only your agents can connect, not anyone else on the network.

### Step 1: Generate certificates

```bash theme={null}
nanny certs generate
```

This creates six files in `~/.nanny/certs/`:

```
ca.crt      CA certificate (the trust anchor, share with all agents)
ca.key      CA private key (keep this on the server machine only)
server.crt  server certificate (used by nanny run --serve)
server.key  server private key (keep this on the server machine only)
client.crt  client certificate (copy to each agent machine)
client.key  client private key (copy to each agent machine)
```

Default validity: 365 days. To see expiry dates at any time:

```bash theme={null}
nanny certs show
```

### Step 2: Start the server

```bash theme={null}
nanny run --serve --addr 0.0.0.0:62669
```

The server automatically reads `server.crt`, `server.key`, and `ca.crt` from `~/.nanny/certs/`. Pass explicit paths if your certs are elsewhere:

```bash theme={null}
nanny run --serve --addr 0.0.0.0:62669 \
  --cert /path/to/server.crt \
  --key  /path/to/server.key \
  --ca   /path/to/ca.crt
```

**Important:** The server will refuse to start if any cert file is missing. Run `nanny certs generate` first or provide paths to existing cert files.

### Step 3: Distribute client certificates

Copy `client.crt`, `client.key`, and `ca.crt` to each agent machine. These are the only files agents need. The `ca.key` and `server.key` never leave the server machine.

### Step 4: Choose a session token

The session token is what admits a process to the governor, on top of mTLS.

**Choose one and set it on both sides.** Left unset, the server mints one and
writes it to `~/.nanny/servers/<appId>/server.token`, which only helps when the
agent can read the server's filesystem. Across machines there is no such read,
so a fleet sets its own and holds still across redeploys:

```bash theme={null}
openssl rand -hex 32
```

Minimum 32 characters, refused below that: this token is a policy decision, not
a label.

### Step 5: Configure your agents

Certificates may be given as a file path or as inline PEM, and a path is the
form to use in a deployment. The session token is always the token itself: both
ends read the same variable, so a second form to interpret is only a way for
them to disagree.

```ini theme={null}
# .env (never commit this file: add it to .gitignore)
NANNY_BRIDGE_ADDR=server.example.com:62669
NANNY_SESSION_TOKEN=a3f9...
NANNY_BRIDGE_CERT=/run/secrets/nanny/client.crt
NANNY_BRIDGE_KEY=/run/secrets/nanny/client.key
NANNY_BRIDGE_CA=/run/secrets/nanny/ca.crt
```

For Docker or Kubernetes, mount the secrets as files and point these at them: a
Docker secret, a Kubernetes `Secret` mounted as a volume, or whatever your PKI
writes. Two reasons to prefer that over pasting the values in directly. A value
in the environment is readable through `/proc/<pid>/environ`, inherited by every
child process, and visible to anything that can inspect the container, while a
mounted file can be `0600` and is inherited by nothing. And **only files
rotate**: an environment variable cannot change in a running process, so
certificates supplied that way are replaced on the next restart rather than in
place.

When an agent is launched with these variables set, it connects to the
governance server with mTLS. The server verifies the client certificate and
refuses connections from anything without a valid cert signed by the same CA.

### Rotating the token

The governor accepts a **set** of tokens, newline-separated in the variable, so
a rotation never needs everything to restart at the same instant:

1. Set the variable to the old and new token on separate lines, and restart the
   governor. Both are now accepted.
2. Move the joiners onto the new token, at whatever pace you like.
3. Drop the old line and restart the governor. The old token is dead.

Certificates do not need this, because the CA keeps the old and new leaves
valid at the same time. A shared secret has no such authority, so the overlap
is held by the governor instead.

***

## A worked example: two containers

The shape most deployments actually have. One service runs the governor, a
second joins it, and neither runs a shell script to reshape a secret.

**Generate the bundle once, on your machine, never in a container.** `--san`
must cover the name the joiner dials, because the client verifies the name it
dialled against that list and a mismatch fails the handshake before any request
is made.

```bash theme={null}
nanny certs generate --san governor --days 3650
openssl rand -hex 32
```

Mount `server.crt`, `server.key` and `ca.crt` into the governor, and
`client.crt`, `client.key` and `ca.crt` into the joiner. The token is an
environment variable on both, set to the same value.
`ca.key` never leaves your machine: it signs certificates, so anyone holding it
can mint a client the governor will trust.

**The governor:**

```ini theme={null}
NANNY_SESSION_TOKEN=a3f9...
```

```dockerfile theme={null}
CMD ["nanny", "run", "--serve", "--addr", "0.0.0.0:62669", \
     "--cert", "/run/secrets/nanny/server.crt", \
     "--key",  "/run/secrets/nanny/server.key", \
     "--ca",   "/run/secrets/nanny/ca.crt"]
```

**The joiner**, which needs no `nanny run` wrapper at all. A Python process
reads its transport from the environment and connects itself, so an existing
entrypoint is left alone:

```ini theme={null}
NANNY_BRIDGE_ADDR=governor:62669
NANNY_SESSION_TOKEN=a3f9...
NANNY_BRIDGE_CERT=/run/secrets/nanny/client.crt
NANNY_BRIDGE_KEY=/run/secrets/nanny/client.key
NANNY_BRIDGE_CA=/run/secrets/nanny/ca.crt
```

No `NANNY_API_KEY` on the joiner: joined processes report through the governor,
and a second key would split the audit trail and count twice.

**Health checks.** `/health` is served without the session token and reports
whether the run is up and nothing else, so an orchestrator can probe it:

```
GET http://governor:62669/health  →  {"state":"running"}
```

`/status` carries call counts, history and the stop reason, and stays behind the
token.

**Start order does not matter.** A joiner started before its governor retries
its first connection for 30 seconds rather than failing the work it was handed.
Once it has connected, a later failure stops the run immediately: waiting for a
first connection is patience, waiting mid-run is running ungoverned.

***

## One shared rule set

All agents connected to the same governance server are evaluated against the same allowlist and the same rules. Each connection is its own run, with its own history, so a stop ends that run and never the server or its peers.

Every run writes to the same log, and each event carries the `run_id` that tells them apart.

This means:

* A tool absent from `[tools] allowed` is refused for every agent, not just the one that declared it.
* A per-tool `max_calls` cap is that agent's own: each run counts its own calls, so one agent exhausting a cap does not refuse another's.

The governance server is a **shared authority layer** for a team of agents working on one task. Every agent may do exactly what the config permits, and no more.

**Named scopes still work the same way.** When an agent activates `@agent("researcher")`, every event until that function returns is attributed to that phase. A scope does not change what the agent may do; it records which phase a verdict belongs to, so an audit can tell them apart.

***

## Long-lived processes and NannyStop

If the thing you're wrapping with `--join` is a long-lived server of its own (a web app, a Discord bot, anything that keeps running and handles more than one request per process), read this before you deploy it.

**`nanny run --join` never kills your process on a stop.** This is deliberate, and it's different from plain `nanny run`. Under plain `nanny run` (no `--serve`), the CLI actively watches your process and kills it on a denial. That's the right behavior for a one-shot batch job. Under `--join`, there is no such watcher: the CLI injects the bridge address and token, starts your process, and waits for it to exit on its own. Enforcement happens entirely inside your process, at the specific call site that is denied, as a `NannyStop` exception raised right there, not a kill signal from outside. This is what makes the governance server's core guarantee possible: **a stop ends one run, not the server.** The whole point of hosting a shared governor is that it keeps running and keeps serving other agents and requests after any one of them gets stopped.

The consequence: if your server has its own request loop, you are responsible for containing that exception per request. Nanny does not do it for you, because from outside your process there is nothing to watch. The stop already happened inside a function call your own code made.

```python theme={null}
from nanny_sdk import run_scope, NannyStop

def handle_request(payload: dict) -> dict:
    try:
        # Its own run, independent of every other request.
        with run_scope():
            return process(payload)
    except NannyStop as e:
        # Contained here: this request fails, the server keeps running.
        return {"error": f"stopped: {type(e).__name__}"}
```

If you skip the `try`/`except` and let `NannyStop` propagate out of a request handler, what happens next depends entirely on your web framework's own exception handling. For many frameworks, an unhandled exception in a request handler brings down the whole process, which defeats the reason you reached for a governance server in the first place. Treat it the same way you'd treat any other exception a request handler can raise: catch it at the boundary. See [`run_scope`](/v0.6/guides/python-sdk#run_scope-an-independent-run-inside-one-process) for giving each request its own run, and [What happens on stop](/v0.6/guides/python-sdk#what-happens-on-stop) for the full list of `NannyStop` subclasses to catch.

***

## Server management

```bash theme={null}
# Show address, PID, and reachability for the current directory's app
nanny status

# Or target a specific app explicitly
nanny status --app=<appId>

# Stop the server (sends SIGTERM, 10-second graceful drain)
nanny stop
nanny stop --app=<appId>

# Check all active Nanny components at once
nanny health
```

Both commands default to the current directory's own `app_id` (from `.nanny/app.json`) when `--app` is omitted. `nanny stop` sends `SIGTERM` to the server process. In-flight requests complete within a 10-second grace window before the server exits. This means a running agent can finish its current tool call before the server shuts down.

Set `NANNY_HOME` to move `~/.nanny/servers/` to a directory of your choice instead of the home directory. Every `--serve`/`--join`/`status`/`stop` invocation for a given governor needs the same `NANNY_HOME` to find its state. It moves the server state directory only: certificates are resolved from the home directory or from the paths you pass to `--cert`, `--key` and `--ca`, which is what a deployment mounting its bundle somewhere specific should use.

***

## Certificate operations

```bash theme={null}
# Regenerate server + client certs (preserves the CA)
nanny certs rotate

# Show expiry dates and SAN list
nanny certs show

# Import externally-issued certs (BYOC: HashiCorp Vault, AWS ACM, etc.)
nanny certs import ca=@/path/to/ca.crt cert=@/path/to/server.crt key=@/path/to/server.key

# Remove all certs from ~/.nanny/certs/ (asks for confirmation)
nanny certs remove
```

### Certificate hot-reload

The server watches the directory holding its server certificate, wherever `--cert` points, for file changes. When any cert file is replaced, by `nanny certs rotate`, `nanny certs import`, or a PKI automation system writing to the directory, the server reloads the cert into memory without restarting. New connections use the new cert immediately; existing connections finish on the old cert.

This matters for short-lived PKI certs (for example, 8-hour Vault-issued certs renewed by Vault Agent), you do not need to restart the governance server when certs are rotated.

### Externally-issued certificates (BYOC)

If your organization uses HashiCorp Vault, AWS ACM, cert-manager, or any other PKI system, you can import those certs directly:

```bash theme={null}
# From files
nanny certs import \
  ca=@/vault/secrets/ca.pem \
  cert=@/vault/secrets/tls.crt \
  key=@/vault/secrets/tls.key

# From environment variables (Vault Agent injection, CI/CD secrets)
nanny certs import \
  ca="$VAULT_CA" \
  cert="$VAULT_CERT" \
  key="$VAULT_KEY"

# Partial import: update only the cert and key, keep the existing CA
nanny certs import cert=@new-server.crt key=@new-server.key
```

Partial imports are supported: omit any key to leave the existing file unchanged. After any import, Nanny validates that the imported cert is signed by the imported (or existing) CA and fails loudly on a mismatch.

`nanny certs rotate` works only when `nanny certs generate` created the CA, because it needs the CA private key (`ca.key`) on disk to sign new certs. For externally-issued certs, the CA private key never leaves your PKI system. Use `nanny certs import` instead for rotation.

***

## Port 62669

The governance server listens on port `62669` by default.

62669 spells NANNY on a phone keypad (N=6, A=2, N=6, N=6, Y=9). Genuinely memorable for ops and firewall rules.
