Skip to main content

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. 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:
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:
--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:

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

This creates six files in ~/.nanny/certs/:
Default validity: 365 days. To see expiry dates at any time:

Step 2: Start the server

The server automatically reads server.crt, server.key, and ca.crt from ~/.nanny/certs/. Pass explicit paths if your certs are elsewhere:
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:
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.
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.
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:
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:
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:
/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.
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 for giving each request its own run, and What happens on stop for the full list of NannyStop subclasses to catch.

Server management

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

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:
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.