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
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: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
~/.nanny/certs/:
Step 2: Start the server
server.crt, server.key, and ca.crt from ~/.nanny/certs/. Pass explicit paths if your certs are elsewhere:
nanny certs generate first or provide paths to existing cert files.
Step 3: Distribute client certificates
Copyclient.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:
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.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:- Set the variable to the old and new token on separate lines, and restart the governor. Both are now accepted.
- Move the joiners onto the new token, at whatever pace you like.
- Drop the old line and restart the governor. The old token is dead.
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.
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:
nanny run wrapper at all. A Python process
reads its transport from the environment and connects itself, so an existing
entrypoint is left alone:
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 therun_id that tells them apart.
This means:
- A tool absent from
[tools] allowedis refused for every agent, not just the one that declared it. - A per-tool
max_callscap is that agent’s own: each run counts its own calls, so one agent exhausting a cap does not refuse another’s.
@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.
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
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: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 port62669 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.