# Event Log Source: https://docs.nanny.run/v0.5/concepts/event-log Every Nanny execution emits a structured NDJSON event log. Here's what's in it. ## Format The event log is **NDJSON** — one JSON object per line, emitted in chronological order. Every object has an `"event"` field identifying its type and a `"ts"` field with a Unix timestamp in milliseconds. ## Output destination By default, events are written to **stdout**, interleaved with your agent's own output. To separate them, configure file output: ```toml theme={null} [observability] log = "file" ``` This writes to `.nanny/logs/log.ndjson`, a location Nanny always owns and auto-creates, never something you point at a path yourself. Only set `file` if you want a different name — a bare name, no extension, Nanny always appends `.ndjson` itself: ```toml theme={null} [observability] log = "file" file = "events" # writes .nanny/logs/events.ndjson ``` `.nanny/logs/` is meant to stay out of git (Nanny adds it to `.gitignore` automatically the first time it's created): it's a local audit trail, not source. If you later log this machine in (`nanny auth login`), Cloud sync can back-fill from whatever accumulated in that folder before sync was ever turned on, not just events going forward. Or pipe stdout to a file at the shell level: ```bash theme={null} nanny run > nanny.log ``` ## Guaranteed events Every execution emits exactly these two events, in this order: ### ExecutionStarted Always the first event. Emitted immediately before the child process is spawned. ```json theme={null} { "event": "ExecutionStarted", "ts": 1711234567000, "limits_set": "[limits]", "command": "python agent.py", "limits": { "steps": 100, "tokens": 1000, "timeout": 30000 } } ``` ### ExecutionStopped Always the last event. Emitted on every exit path — clean exit, timeout, error, or signal. ```json theme={null} { "event": "ExecutionStopped", "ts": 1711234572000, "reason": "AgentCompleted", "steps": 7, "tokens_spent": 70, "elapsed_ms": 4823 } ``` If the process was killed, `reason` will be one of the stop reasons listed in [Limits & Enforcement](/v0.5/concepts/limits). ## SDK events When the Rust SDK macros or Python SDK decorators are active, additional events are emitted for each tool call. These appear between `ExecutionStarted` and `ExecutionStopped`: | Event | When emitted | | ------------------- | ---------------------------------------------------------------------------- | | `StepCompleted` | After each allowed tool call, immediately following its `ToolAllowed` event | | `ToolAllowed` | When Nanny permits a tool call | | `ToolDenied` | When a tool call is blocked because the tool is not in the allowlist | | `RuleDenied` | When a tool call is blocked by a custom rule or a per-tool `max_calls` limit | | `ToolFailed` | When a permitted tool fails at runtime (network error, bad args, etc.) | | `AgentScopeEntered` | When a `#[nanny::agent("name")]` function is entered | | `AgentScopeExited` | When a `#[nanny::agent("name")]` function returns | `ToolDenied` and `RuleDenied` are distinct denial events — `ToolDenied` means the tool was not permitted at all; `RuleDenied` means the tool was permitted but a rule blocked this specific call. `ToolFailed` is different from both — the tool was allowed and called, but encountered a runtime error. No tokens are charged on failure. ## Using the log The event log is designed to be piped into standard tools: ```bash theme={null} # Count total steps (every allowed tool call is one step) cat nanny.log | grep StepCompleted | wc -l # Find all denied tool calls (allowlist blocks and rule denials) cat nanny.log | jq 'select(.event == "ToolDenied" or .event == "RuleDenied")' # Check why a run stopped cat nanny.log | jq 'select(.event == "ExecutionStopped") | .reason' ``` # How It Works Source: https://docs.nanny.run/v0.5/concepts/how-it-works What Nanny actually does when you run a command under it. ## The enforcement model When you run `nanny run`, Nanny becomes the parent process of your agent. It reads `[start].cmd` from `nanny.toml`, spawns it as a child, and owns the process lifecycle — it decides when the process lives and when it dies. ```mermaid theme={null} flowchart TD CMD(["$ nanny run"]) CMD --> NANNY subgraph NANNY["Nanny — parent process"] direction LR subgraph CHILD["Child process"] AGENT["python agent.py"] end subgraph ENFORCE[" "] direction TB STEPS["steps"] TOKENS["tokens"] TIMER["timeout"] end AGENT -- "tool call" --> ENFORCE ENFORCE -- "✓ allowed" --> AGENT end ENFORCE -- "✗ limit reached → killed" --> DEAD(["process exits"]) DEAD --> LOG["ExecutionStopped\nreason · steps · tokens_spent\n→ stdout"] ``` The moment any limit is crossed, Nanny kills the child process immediately — the process cannot catch, delay, or prevent the stop. An `ExecutionStopped` event is emitted with the reason, and Nanny exits with a non-zero status code. ## Multi-agent governance When multiple agents run in the same process — as in CrewAI, LangGraph, AutoGen, or any framework that orchestrates agents within a single Python or Rust runtime — the enforcement model above applies to all of them simultaneously. A single `nanny run` governs the entire fleet. ```mermaid theme={null} flowchart TD CMD(["$ nanny run"]) CMD --> NANNY subgraph NANNY["Nanny — parent process"] direction TB subgraph CHILD["Child process — crew.kickoff()"] direction LR A1["@agent('ingestion')"] A2["@agent('analysis')"] A3["@agent('visualization')"] A4["@agent('reporter')"] end subgraph ENFORCE[" "] direction TB STEPS["steps"] TOKENS["tokens"] ALLOW["allowlist"] RULES["rules"] end A1 & A2 & A3 & A4 -- "tool call" --> ENFORCE ENFORCE -- "✓ allowed" --> A1 & A2 & A3 & A4 end ENFORCE -- "✗ limit reached → killed" --> DEAD(["process exits"]) DEAD --> LOG["ExecutionStopped\nreason · agent · tool · tokens_spent\n→ stdout"] ``` Each agent activates its own named limit set via `@agent("role")`. Tool calls from any agent flow through Nanny’s enforcement layer. Each agent's budget is tracked independently — hitting the analysis budget does not kill the reporter. For cross-process and cross-machine enforcement, use the [governance server](/v0.5/guides/governance-server). *** ## What Nanny enforces All three limits are enforced on every run: | Limit | Requirement | Behaviour | | --------- | ---------------------------- | ---------------------------------------------------------- | | `timeout` | None — works for any process | Killed when wall-clock time exceeds the configured value | | `steps` | Rust SDK or Python SDK | Killed when step count reaches the configured limit | | `tokens` | Rust SDK or Python SDK | Killed when accumulated tokens reach the configured budget | Timeout enforcement works for any process in any language — no SDK required. Step and token enforcement require the agent to report tool calls, either via the [Rust SDK](/v0.5/guides/rust-sdk) macros or the Python SDK decorators. ## Passthrough mode When running outside `nanny run`, every macro becomes a no-op: ```rust theme={null} #[tool(tokens = 10)] fn search(query: &str) -> String { // runs normally, no enforcement } ``` This means you can ship instrumented code and run it in development, CI, and production without Nanny — until you explicitly wrap it with `nanny run`. The behaviour is identical either way. # Limits & Enforcement Source: https://docs.nanny.run/v0.5/concepts/limits How steps, tokens, and timeout work — and how to configure them. ## The three limit types Every Nanny execution is governed by three independent limits. Any one of them can stop a run. ### Timeout The wall-clock time limit in milliseconds. The moment the child process has been running for `timeout` ms, Nanny kills it — regardless of what it's doing. ```toml theme={null} [limits] timeout = 30000 # 30 seconds ``` Timeout enforcement requires no instrumentation — it works for any process in any language. ### Steps The maximum number of agent steps allowed. Requires `#[nanny::tool]` (Rust) or `@tool` (Python) to report tool calls. ```toml theme={null} [limits] steps = 100 ``` ### Tokens The maximum number of tokens the agent may spend. Each tool declares its token cost per call; Nanny tracks the running total and stops the moment the budget is exhausted. ```toml theme={null} [limits] tokens = 1000 ``` ## Named limit sets In a multi-agent system, each agent has a different risk profile. The analysis agent makes expensive API calls — it deserves a tight token ceiling. The reporter just writes a file — it barely needs a budget at all. Named limit sets let each role get exactly the ceiling it deserves, configured once in `nanny.toml`. ```toml theme={null} [limits] # Global ceiling — applies to any run not using a named set steps = 200 tokens = 500 timeout = 120000 [limits.ingestion] steps = 20 tokens = 50 timeout = 30000 [limits.analysis] steps = 60 tokens = 200 # tighter — this agent makes expensive calls timeout = 60000 [limits.visualization] steps = 20 tokens = 100 timeout = 30000 [limits.reporter] steps = 20 tokens = 50 # loose — this agent just writes a file timeout = 30000 ``` Named sets **inherit** from `[limits]` and override only the fields you declare. In the example above, `[limits.ingestion]` inherits from the global `[limits]` and overrides all three fields. A set that only declares `timeout` would inherit `steps` and `tokens` from the base. Each agent activates its own set via the `@agent("role")` decorator: ```python theme={null} @agent("analysis") def run_analysis(path: str): ... # governed by [limits.analysis] @agent("reporter") def run_reporter(output: str): ... # governed by [limits.reporter] ``` Or activate a named set from the CLI for the entire run: ```bash theme={null} nanny run --limits=analysis ``` ### Named sets share one counter — they are not separate budgets This is easy to misread, so it's worth being explicit: entering `[limits.analysis]` does **not** give the analysis role its own fresh 200-token pool. There is exactly one running total for tokens spent (and steps taken) in a given run, for its entire lifetime. A named set only changes which ceiling that *same* total is compared against while the set is active. Concretely: if `[limits.ingestion]` already spent 40 of its own 50-token ceiling before handing off to `[limits.analysis]` (tokens = 200), the analysis phase starts with only 160 tokens of real headroom left, not a fresh 200. If the run had already spent close to 200 tokens by the time `[limits.reporter]` (tokens = 50) becomes active, the very next governed call in that phase can fail immediately, even though the reporter itself hasn't done anything yet. Size named-set ceilings with this in mind: later scopes in a pipeline need ceilings large enough to absorb everything earlier scopes may have already spent, not just their own expected usage in isolation. If your process runs multiple, logically independent phases and you want each one to start from a genuinely clean budget, unrelated to what came before, that's a different thing: a new **run**, not a new named set. See `fresh_run()` in the [Python](/v0.5/guides/python-sdk#fresh-run-starting-a-fresh-run-mid-process) or [Rust](/v0.5/guides/rust-sdk#nanny-fresh-run-starting-a-fresh-run-mid-process) SDK guide for how to do that. ## What happens when a limit is hit 1. Nanny kills the child process immediately — no grace period, no way for the agent to catch or delay the stop. 2. An `ExecutionStopped` event is emitted with the reason. 3. A human-readable message is printed to stderr: `nanny: stopped — TimeoutExpired`. 4. Nanny exits with code `1`. The stop reasons are: | Reason | Trigger | | ------------------- | -------------------------------------------------------------------------------------------------- | | `AgentCompleted` | Process exited cleanly on its own | | `TimeoutExpired` | Wall-clock timeout exceeded | | `MaxStepsReached` | Step limit hit | | `BudgetExhausted` | Token budget exhausted | | `ToolDenied` | Tool not in allowlist | | `RuleDenied` | Custom rule returned denial | | `ManualStop` | Stopped programmatically | | `ProcessCrashed` | Child process exited with non-zero code unexpectedly | | `BridgeUnavailable` | Enforcement was active but became unreachable — Nanny fails closed rather than continue ungoverned | # Governance server Source: https://docs.nanny.run/v0.5/guides/governance-server Run Nanny enforcement across multiple processes and machines using the standalone governance server. ## When to use the governance server For most projects, `nanny run` is all you need. It enforces locally, inside the same process group as your agent, governs it, and exits when the agent exits. No server. No cert setup. No network. Use the governance server when agents run in **separate processes or on separate machines** and you need a shared enforcement boundary across all of them. Common scenarios: * **Microservices:** three containers, each running an agent, all sharing a single token budget * **CI workers:** a coordinator spins up worker processes on different machines; you want each worker's tool calls counted against the same budget * **Development clusters:** a Kubernetes pod runs the server; a local dev agent connects to it while you iterate If your agents all run inside a single `nanny run` — even a complex CrewAI or LangGraph pipeline with many agents — you do not need the governance server. *** ## The three deployment modes | Mode | Command | Transport | When to use | | ------------------------------- | ---------------------------------------- | ---------------------------------------- | -------------------------------------------- | | Local inline | `nanny run` | Unix socket / TCP loopback (OS-enforced) | Single process, single machine — the default | | Local governance server | `nanny run --serve` | Plain HTTP on loopback — no certs needed | Multiple processes on one machine | | Cross-machine governance server | `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. *** ## 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//`. 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= ``` `--join` reads the address and tokens from `~/.nanny/servers//`, then injects `NANNY_BRIDGE_ADDR` and `NANNY_SESSION_TOKEN` into the agent process (and, if `[proxy]` is configured, an authenticated proxy URL: see [HTTP proxy mode](/v0.5/guides/http-proxy-mode)). 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 five 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: Configure your agents The session token is generated by the server at startup and saved to `~/.nanny/servers//server.token` on the server machine. Copy it: ```bash theme={null} cat ~/.nanny/servers//server.token ``` On each agent machine, create a `.env` file with the token value you just copied and the cert paths from Step 3: ```ini theme={null} # .env (never commit this file — add it to .gitignore) NANNY_BRIDGE_ADDR=server.example.com:62669 NANNY_SESSION_TOKEN= NANNY_BRIDGE_CERT=/path/to/client.crt NANNY_BRIDGE_KEY=/path/to/client.key NANNY_BRIDGE_CA=/path/to/ca.crt ``` For Docker or Kubernetes, pass these as deployment secrets (`environment:` in Docker Compose, a k8s `Secret`, or CI/CD secret injection) rather than a file. When an agent is launched with `nanny run` and these variables are 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. *** ## Shared budget All agents connected to the same governance server share one enforcement state. Token budget, steps, and timeout are tracked across all of them simultaneously. This means: * If Agent A spends 400 tokens and Agent B spends 600, and the budget is 1000, the next call from either agent stops the execution — regardless of which agent makes it. * A loop in Agent B counts toward the global step limit alongside Agent A's steps. The governance server is a **shared enforcement layer** for a team of agents working on one task. Each agent is subject to the same limits as every other agent. **Named limits still work the same way.** When an agent activates `@agent("researcher")`, it enters the `[limits.researcher]` scope from `nanny.toml`. That scope's ceiling applies for the duration of that function. The shared token budget still tracks totals across all agents — a named scope does not isolate budget from other agents, it only sets the ceiling for that scope. *** ## 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 when a limit is hit. 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 breaches a limit, 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 fresh_run, NannyStop def handle_request(payload: dict) -> dict: fresh_run() # this request gets a clean budget, independent of every other request try: 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 [`fresh_run`](/v0.5/guides/python-sdk#fresh_run-starting-a-fresh-run-mid-process) for giving each request its own clean budget, and [What happens on stop](/v0.5/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= # Stop the server (sends SIGTERM, 10-second graceful drain) nanny stop nanny stop --app= # 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/` (and everything else nanny keeps) 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. *** ## 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 `~/.nanny/certs/` 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. *** ## HTTP proxy mode The governance server can also act as an HTTP CONNECT proxy on the same port. Any outbound HTTP or HTTPS request your agent makes through the proxy is subject to Nanny's allowlist — even calls from code that has no `@nanny_tool` decorator. See [HTTP proxy mode](/v0.5/guides/http-proxy-mode) for setup and configuration. *** ## Port 62669 The governance server listens on port `62669` by default. Both the governance API and the HTTP CONNECT proxy share this port. 62669 spells NANNY on a phone keypad (N=6, A=2, N=6, N=6, Y=9). Genuinely memorable for ops and firewall rules. # HTTP proxy mode Source: https://docs.nanny.run/v0.5/guides/http-proxy-mode Intercept and allowlist all outbound HTTP from your agent — even from code with no Nanny decorator. ## What it does `@nanny_tool` and `#[nanny::tool]` govern tool calls that go through your Rust or Python code. But some tools make HTTP requests without any decorator — LLM client libraries, database drivers, third-party SDKs, HTTP-based MCP tools. Those calls bypass `@nanny_tool` entirely. HTTP proxy mode fills that gap. When enabled, the governance server acts as an HTTP CONNECT proxy. Your agent sets standard proxy environment variables. All outbound HTTP and HTTPS traffic from the agent — regardless of which library or function makes the call — routes through the server. The server checks each request against an allowlist before forwarding it. A request to a host not on the allowlist gets a `403 Forbidden` and the governance server emits a `ToolDenied` event in the NDJSON log. This covers the outbound HTTP surface without any code changes to your agent. *** ## Enable proxy mode `nanny init` generates a `nanny.toml` with a `[proxy]` section already present but commented out. To activate proxy mode, uncomment `allowed_hosts` and add the hosts your agent needs to reach: ```toml theme={null} [proxy] allowed_hosts = ["api.openai.com", "api.groq.com", "*.anthropic.com"] ``` Proxy mode is active only when `allowed_hosts` is non-empty. Leaving it commented out — or setting an empty list — disables proxy mode entirely. The governance server validates this list at startup. If `--proxy` is passed but `allowed_hosts` is empty, the server refuses to start with a clear error. *** ## Configure your agent Don't set `HTTP_PROXY`/`HTTPS_PROXY` by hand. The CONNECT tunnel authenticates with its own credential (`proxy_token`, separate from the ordinary `session_token`), so a bare proxy URL with no credential gets a `407` from the governance server. `nanny run` sets this up for you automatically whenever it starts or joins a governance server that has `[proxy]` configured: ```bash theme={null} nanny run --serve # local: proxy vars injected into the agent's own process nanny run --join= # remote: proxy vars injected the same way ``` Under the hood, the injected URL embeds the proxy token as userinfo (`http://:@host:port`), so the child process's own HTTP client sends it as a standard `Proxy-Authorization: Basic ...` header on the CONNECT handshake, with no code changes required. Most HTTP client libraries (Python's `httpx`, `requests`, `aiohttp`; Node's `fetch`; curl) respect `HTTP_PROXY`/`HTTPS_PROXY` automatically once it's set. For HTTPS traffic, the proxy uses HTTP CONNECT tunneling: the client sends a `CONNECT` request to the proxy, the proxy opens a TCP tunnel to the target, and the TLS handshake happens inside the tunnel between the client and the target server. The proxy sees the hostname and port but not the decrypted content of HTTPS requests. *** ## Host allowlist rules ### Exact hostnames ```toml theme={null} allowed_hosts = ["api.openai.com", "api.anthropic.com"] ``` Matches only the exact hostname. `api.openai.com` does not match `beta.openai.com`. ### Wildcard subdomains ```toml theme={null} allowed_hosts = ["*.openai.com", "*.anthropic.com"] ``` `*.openai.com` matches `api.openai.com`, `beta.openai.com`, `platform.openai.com` — any single subdomain level. It does not match `openai.com` itself (no wildcard prefix) and does not match `api.us.openai.com` (wildcards are single-level only). ### Combining both ```toml theme={null} allowed_hosts = [ "api.openai.com", # exact — this specific API endpoint "*.anthropic.com", # wildcard — any Anthropic subdomain "api.groq.com", # exact — Groq API ] ``` *** ## What is always blocked Some address ranges are blocked regardless of your `allowed_hosts` list: | Range | Blocked because | | -------------------------------------------------------------------- | ------------------------------------------ | | Loopback (`127.x.x.x`, `::1`) | localhost services | | Link-local (`169.254.x.x`) | cloud metadata endpoints (AWS, GCP, Azure) | | RFC-1918 private ranges (`10.x.x.x`, `172.16–31.x.x`, `192.168.x.x`) | internal network services | *** ## The event log Every proxied request produces an event in the NDJSON log: **Allowed request:** ```json theme={null} {"event":"ToolAllowed","ts":1711234567120,"tool":"http_proxy","target":"api.openai.com:443"} ``` **Denied request (not in allowlist):** ```json theme={null} {"event":"ToolDenied","ts":1711234567320,"tool":"http_proxy","target":"malicious.example.com:443"} {"event":"ExecutionStopped","ts":1711234567321,"reason":"ToolDenied","steps":3,"tokens_spent":30,"elapsed_ms":1250} ``` A proxy denial is a hard stop — the same outcome as any other `ToolDenied` event. The agent process exits immediately. *** ## Token accounting and HTTPS content Two things to be aware of when combining proxy mode with the rest of Nanny: * **Token accounting requires `@tool`.** Proxy requests are logged as events but not charged against your token budget. For token-tracked HTTP calls, decorate the function with `@tool` or use `nanny::http_get`. * **HTTPS content is not inspected.** The proxy allows or denies by hostname. It cannot read request or response bodies inside HTTPS tunnels. *** ## Example: LLM client with proxy Most LLM client libraries pick up `HTTP_PROXY` and `HTTPS_PROXY` automatically. Here's a complete example with the OpenAI Python client: ```python theme={null} import os from openai import OpenAI # The client picks up HTTP_PROXY / HTTPS_PROXY from the environment. # No code changes needed — just set the env vars before starting your agent. client = OpenAI() response = client.chat.completions.create( model="gpt-4o-mini", messages=[{"role": "user", "content": "Hello"}], ) ``` `nanny.toml`: ```toml theme={null} [proxy] allowed_hosts = ["api.openai.com"] ``` If your agent tries to call any other API host, it gets a `403` and the execution stops. # Connect to Nanny Cloud Source: https://docs.nanny.run/v0.5/guides/managed-mode Log in once, and Nanny forwards your runs to Nanny Cloud (dashboards, cost, and a durable audit trail) while enforcement stays fully local. ## What syncing does By default, Nanny runs **local-only**: it enforces your limits and rules on your machine and writes an event log to stdout. Nothing leaves the box. Once a machine is **logged in**, `nanny run` additionally forwards a copy of your run's event log to [Nanny Cloud](https://nanny.run/cloud). In return you get, per organization: * A dashboard of every run: steps, tokens, stop reasons, models * Spend and usage trends across all your agents * Alerts when a run starts to spiral * A durable, exportable audit trail (compliance tiers) Enforcement never depends on the network. If the cloud is slow or unreachable, your agent runs exactly as it would locally. Forwarding is best-effort and never blocks or fails a run. Syncing is **additive**. It changes nothing about how limits and rules are enforced, only whether a copy of the event log is sent to your dashboard. ## Turn it on One step, no config to edit, no secret in `nanny.toml`: ```bash theme={null} nanny auth login ``` This opens your browser to approve. Approve, and the machine is connected: `nanny run` self-mints an app-scoped credential the first time it runs in a given app directory, stored in a gitignored `.nanny/credentials.local.json` alongside your permanent `.nanny/app.json` identity. Now run as usual: ```bash theme={null} nanny run python agent.py ``` Your run appears in the dashboard within a few seconds of finishing. No code changes, no per-call instrumentation. Syncing needs only one thing: being logged in on the machine (`nanny auth login`). A machine that never logged in never syncs; there's no separate project-level switch to flip. ## CI and headless machines A browser flow needs a person. For CI or a headless machine, log in with an API key instead, supplied through `NANNY_API_KEY` (a CI secret) or stdin, never a command argument: ```bash theme={null} NANNY_API_KEY="nny_..." nanny auth login --token --env prod # or: echo "$NANNY_API_KEY" | nanny auth login --token --env staging ``` This logs in without opening a browser. `--env` is required so it targets the right cloud. ## Skip a single run Forward nothing for one run, without logging out: ```bash theme={null} nanny run --no-sync python agent.py ``` ## Local vs synced | | Not logged in (default) | Logged in | | ----------------------- | ----------------------- | -------------------------------------- | | Enforcement | Local, deterministic | Local, deterministic (unchanged) | | Event log | stdout / file | stdout / file **+** forwarded to cloud | | Network required to run | No | No (forwarding is best-effort) | | Dashboard & trends | No | Yes | ## Turning it off * **One run:** `nanny run --no-sync`. * **Your machine, for good:** `nanny auth logout`. To revoke the key everywhere, use the dashboard. # Python Source: https://docs.nanny.run/v0.5/guides/python-sdk Per-function governance for Python agents using Nanny decorators. The Python SDK brings the same enforcement model as the [Rust SDK](/v0.5/guides/rust-sdk) to Python, `@tool`, `@rule`, and `@agent` decorators that enforce limits per function call. ```bash theme={null} pip install nanny-sdk ``` *** ## Passthrough mode When running outside `nanny run`, every decorator is a no-op. The function executes normally with no enforcement overhead: ```bash theme={null} # Governed — enforcement active (reads [start].cmd from nanny.toml) nanny run # Not governed — decorators silent, agent runs normally python agent.py uv run agent.py ``` *** ## `@tool` — declare a governed tool Mark a function as a tool that Nanny should track and charge against the budget: ```python theme={null} from nanny_sdk import tool @tool(tokens=10) def fetch_page(url: str) -> str: import httpx return httpx.get(url).text ``` When the agent calls `fetch_page`: 1. Nanny checks: is `fetch_page` in the `[tools] allowed` list? 2. Nanny checks: has `fetch_page` exceeded `[tools.fetch_page] max_calls`? 3. Nanny charges 10 tokens against the budget. 4. If any check fails, a `NannyStop` exception is raised — the function body never runs. Works identically for async functions: ```python theme={null} @tool(tokens=10) async def fetch_page(url: str) -> str: import httpx async with httpx.AsyncClient() as client: return (await client.get(url)).text ``` ### Tokens The `tokens` argument is required. Set it to `0` for tools you want tracked but not charged: ```python theme={null} @tool(tokens=0) def log_step(msg: str) -> None: ... ``` ### Matching the tool allowlist The tool name used for allowlist checks is the **function name** as declared in Python: ```toml theme={null} # nanny.toml [tools] allowed = ["fetch_page", "read_file"] [tools.fetch_page] max_calls = 20 tokens_per_call = 10 # nanny.toml value overrides the decorator default ``` metrics_crew — ToolDenied fires when the analysis agent calls write_report, a tool outside its allowlist *** ## `instrument` — automatic LLM token tracking Call `nanny_sdk.instrument(client)` once at agent startup to automatically report LLM token usage to Nanny's budget. Every completion response is intercepted and its token counts are debited from the same ledger that `@tool` charges against. ```python theme={null} import openai import nanny_sdk client = openai.OpenAI() nanny_sdk.instrument(client) # one line — done # From here on, every response's token usage is reported to Nanny. response = client.chat.completions.create( model="gpt-4o-mini", messages=[{"role": "user", "content": "Hello"}], ) ``` Supported clients (detected by duck-typing — no provider package is imported): * **OpenAI, Groq, Together AI, Azure OpenAI, LiteLLM** — any client with a `chat.completions.create` method * **Anthropic** — `client.messages.create` * **Mistral** — `client.chat.complete` * **Google Gemini** (`google-genai` SDK) — `client.models.generate_content` * **Cohere v2** — `cohere.ClientV2` `instrument` returns the same client unchanged — it patches the method in-place. In passthrough mode (outside `nanny run`), it is a no-op: no wrapping, no overhead. For providers that report prompt-caching usage (OpenAI, Anthropic, DeepSeek, Gemini), `instrument` also captures it and reports it as `cache_read`/`cache_write` — a finer split of `input`, never additional tokens beyond it, and never used for enforcement (Nanny still debits `input + output` exactly as before). Every provider names and shapes this data differently, so `instrument` normalizes each one's own vocabulary into these two generic fields; absent, not zero, for a provider/response that doesn't report cache usage at all. Exists purely so a downstream cost calculator can price cache-hit tokens at their real, much cheaper rate — see [`LlmUsageRecorded`](/v0.5/reference/events#llmusagerecorded) for the full field reference. *** ## `@rule` — declare an enforcement rule A rule is a function that returns a verdict on whether execution should continue. Return `True` to allow, `False` to deny: ```python theme={null} from nanny_sdk import rule @rule("no_spiral") def check_spiral(ctx) -> bool: h = ctx.tool_call_history # Deny if the last three tool calls were all the same return not (len(h) >= 3 and len(set(h[-3:])) == 1) ``` Rules are evaluated client-side on every tool call, before Nanny’s enforcement layer is contacted. When a rule returns `False`, Nanny raises: ```python theme={null} RuleDenied("no_spiral") ``` The denied tool never runs and no tokens are charged. ### PolicyContext fields The `ctx` parameter gives you a snapshot of the current execution state: | Field | Type | Description | | ------------------- | ---------------- | ------------------------------------------ | | `step_count` | `int` | Steps completed so far | | `elapsed_ms` | `int` | Wall-clock time elapsed | | `tokens_spent` | `int` | Total tokens spent | | `tool_call_counts` | `dict[str, int]` | Per-tool call counts | | `tool_call_history` | `list[str]` | Ordered log of tool names called | | `requested_tool` | `str \| None` | The tool being evaluated right now | | `last_tool_args` | `dict[str, str]` | Arguments of the tool call being evaluated | Rules are evaluated **before** the tool runs — `requested_tool` is set to the tool name being checked. Use `last_tool_args` for content-based enforcement: ```python theme={null} @rule("no_sensitive_files") def block_sensitive(ctx) -> bool: path = ctx.last_tool_args.get("path", "") return ".env" not in path and "secret" not in path ``` All fields are live. Before each rule evaluation, the SDK fetches current counters from Nanny’s enforcement layer — `step_count`, `tokens_spent`, `tool_call_counts`, and `tool_call_history` reflect the actual execution state at the moment your rule runs. metrics_crew — RuleDenied fires on the loop-detection rule, stopping the analysis agent from repeating the same computation *** ## `@agent` — activate named limits for a scope In a multi-agent system, each agent has a different role and a different risk profile. The analysis agent makes expensive API calls and deserves a tight token ceiling. The reporter just writes a file and barely needs a budget at all. `@agent` activates the right named limit set when each role runs, then reverts automatically when it's done. ```python theme={null} from nanny_sdk import agent @agent("researcher") def run_research(topic: str) -> list[str]: # Runs under [limits.researcher] from nanny.toml pages = [fetch_page(f"https://en.wikipedia.org/wiki/{topic}")] return pages ``` ```toml theme={null} # nanny.toml [limits.researcher] steps = 200 tokens = 5000 timeout = 120000 ``` The named set inherits from `[limits]` and overrides only the declared fields. Works identically for async functions. Limits revert on exit whether the function returns normally or raises. metrics_crew running under nanny run — ingestion, analysis, visualization, and reporter agent scopes entering and exiting with structured NDJSON events *** ## `fresh_run` — starting a fresh run mid-process `@agent` changes which ceiling the run's *one* running total is checked against; it does not give a role its own budget (see [Named sets share one counter](/v0.5/concepts/limits#named-sets-share-one-counter-they-are-not-separate-budgets)). If your process genuinely runs multiple, independent phases back to back, say, a research phase that hands off to a drafting phase, or a long-lived server that should give each incoming request its own clean slate, and you want each one to start from zero rather than inherit whatever an earlier phase already spent, that's a new **run**, Nanny's real unit of governance: one cumulative counter, one stop state, "a stop is final." ```python theme={null} import nanny_sdk nanny_sdk.fresh_run() # everything governed after this point is a fresh run ``` Only meaningful when governed through a network server (`nanny run --serve` / `--join`): the server keys independent state per run, so a stop in the run you just left has zero effect on the one you're starting. Under local `nanny run` (no `--serve`), one process is already always exactly one run, so this is a safe no-op there, code that might run under either mode doesn't need to branch on which one it's in. ```python theme={null} from nanny_sdk import agent, fresh_run def handle_request(payload: dict) -> dict: fresh_run() # this request's tokens/steps start from zero, unrelated to # whatever the previous request already spent return process(payload) @agent("researcher") def research(topic: str) -> list[str]: ... ``` Before this existed, the only way to do this was setting the `NANNY_RUN_ID` environment variable directly, an internal detail the client happens to read fresh on every call, never documented as something to rely on. `fresh_run()` is that same mechanism, given a real, discoverable name. *** ## What happens on stop When Nanny stops execution, it raises a `NannyStop` exception. All stop reasons are distinct subclasses: ```python theme={null} from nanny_sdk import ( NannyStop, MaxStepsReached, BudgetExhausted, TimeoutExpired, ToolDenied, RuleDenied, AgentCompleted, AgentNotFound, BridgeUnavailable, ) ``` Catch them by category or individually: ```python theme={null} from nanny_sdk import NannyStop, BudgetExhausted, ToolDenied try: run_research("Alan Turing") except BudgetExhausted: print("Hit the token ceiling") except ToolDenied as e: print(f"Blocked tool: {e.tool_name}") except NannyStop as e: print(f"Stopped: {type(e).__name__}") ``` `BridgeUnavailable` is raised when Nanny’s enforcement layer is active but unreachable during rule evaluation or a tool call. Nanny fails closed — the agent does not continue ungoverned. Like all `NannyStop` subclasses it extends `BaseException`, so it propagates through broad `except Exception` handlers in agent frameworks without being swallowed. Under plain `nanny run` (single process, no `--serve`), you do not need to handle stop reasons in most agent code. They propagate up the call stack and terminate the process, since `nanny run` is watching that process and exits it for you. Catching them there is useful mainly in test code and at the CLI entry point. **This is not true once you're running under `nanny run --serve` / `--join`.** When your process joins a governance server, the wrapping `nanny run --join` command does not watch your process for stops at all: no polling loop, no kill on budget exhaustion, nothing. That's deliberate. [The stop guarantee](/v0.5/guides/governance-server#long-lived-processes-and-nannystop) is that a stop ends one *run*, not your whole process, so a long-lived server (a web app, a Discord bot, anything handling more than one request per process) has to keep running after another request's run gets stopped. Nothing does that for you. If a request handler lets `NannyStop` propagate uncaught, your framework's own crash behavior decides what happens next, which for many servers means the whole process going down over one governed request. Catch it at the boundary of whatever "one governed unit of work" means for your app, typically each request handler, and turn it into a failure response for that one request, not an unhandled exception: ```python theme={null} from nanny_sdk import fresh_run, NannyStop def handle_request(payload: dict) -> dict: fresh_run() # this request's budget starts clean, independent of others try: return process(payload) except NannyStop as e: return {"error": f"stopped: {type(e).__name__}"} ``` See [Long-lived processes and NannyStop](/v0.5/guides/governance-server#long-lived-processes-and-nannystop) for the full explanation of why this only applies under `--serve`/`--join`. metrics_crew — BudgetExhausted stops the analysis agent when it hits its token ceiling *** ## Complete example ```python theme={null} from nanny_sdk import tool, rule, agent @tool(tokens=10) def fetch_page(url: str) -> str: import httpx return httpx.get(url).text @tool(tokens=5) def read_file(path: str) -> str: with open(path) as f: return f.read() @rule("no_spiral") def check_spiral(ctx) -> bool: h = ctx.tool_call_history return not (len(h) >= 3 and len(set(h[-3:])) == 1) @agent("researcher") def research(topic: str) -> list[str]: results = [] page = fetch_page(f"https://en.wikipedia.org/wiki/{topic}") results.append(page) return results if __name__ == "__main__": pages = research("Alan Turing") print(f"Collected {len(pages)} pages") ``` Run it under Nanny: ```bash theme={null} nanny run ``` Run it without Nanny (decorators silent, agent runs normally): ```bash theme={null} python agent.py ``` *** ## Multi-agent pattern The canonical use case: a pipeline where each agent has a specific role, a specific budget, and access to only the tools it needs. This is the `metrics_crew` pattern — four specialized agents, each governed independently. ```python theme={null} from nanny_sdk import tool, rule, agent from collections import deque # Each tool declares its token cost. The decorator fires on every call # regardless of which agent invoked it. @tool(tokens=10) def compute_stats(metric: str, path: str) -> dict: ... @tool(tokens=10) def detect_anomalies(metric: str, path: str) -> list: ... @tool(tokens=5) def write_report(content: str, output_path: str) -> str: ... # A rule that prevents the analysis agent from looping on the same computation. _recent: deque[str] = deque(maxlen=5) @rule("no_analysis_loop") def check_loop(ctx) -> bool: tool = ctx.requested_tool or "" _recent.append(tool) return not (len(_recent) == 5 and all(t == "compute_stats" for t in _recent)) # Each agent activates its own limit scope. @agent("analysis") def run_analysis(path: str): # Governed by [limits.analysis]: steps=60, tokens=200, timeout=60000 # Tool allowlist: ["compute_stats", "detect_anomalies"] only # write_report() here would raise ToolDenied immediately stats = compute_stats("cpu_usage", path) anomalies = detect_anomalies("cpu_usage", path) return anomalies @agent("reporter") def run_reporter(findings: list, output_dir: str): # Governed by [limits.reporter]: steps=20, tokens=50, timeout=30000 # Tool allowlist: ["write_report"] only # compute_stats() here would raise ToolDenied immediately return write_report(str(findings), f"{output_dir}/report.md") ``` ```toml theme={null} # nanny.toml [limits] steps = 200 tokens = 500 timeout = 120000 [limits.analysis] steps = 60 tokens = 200 timeout = 60000 [limits.reporter] steps = 20 tokens = 50 timeout = 30000 [tools] allowed = ["compute_stats", "detect_anomalies", "write_report"] ``` The key properties this gives you: * **A ceiling tailored to each role:** the analysis agent gets a tighter check than the reporter while it's running, expressed once in `nanny.toml`. Worth being precise about what this is *not*: tokens spent is one running total for the whole run, not a separate pool per role, so if analysis exhausts its own ceiling, the run stops there, the reporter never gets to run at all. See [Named sets share one counter](/v0.5/concepts/limits#named-sets-share-one-counter-they-are-not-separate-budgets) for the full explanation, and `fresh_run()` below if you actually want each phase to have its own independent budget. * **Least-privilege tool access:** each agent only receives the tools it needs; calling outside its role raises `ToolDenied` immediately * **Loop detection:** the `@rule` fires client-side before Nanny’s enforcement layer is contacted — the denied tool never runs and no tokens are charged * **Full audit trail:** every tool call, every limit activation, every stop reason logged to NDJSON metrics_crew — MaxStepsReached stops the analysis agent after it exhausts its step allowance For cross-process and cross-machine enforcement, use the [governance server](/v0.5/guides/governance-server). See [`examples/python/metrics_crew`](https://github.com/nanny-run/nanny/tree/main/examples/python/metrics_crew) for the complete working implementation of this pattern with four agents, Plotly chart generation, and a full incident report output. *** ## Framework integration ### LangChain Stack `@lc_tool` (outer) and `@nanny_tool` (inner). LangChain registers the function for dispatch; Nanny intercepts every call regardless of which model or API style invoked it: ```python theme={null} from langchain_core.tools import tool as lc_tool from nanny_sdk import tool as nanny_tool @lc_tool # outer — LangChain registers for tool dispatch @nanny_tool(tokens=5) # inner — Nanny intercepts before file is opened def read_file(path: str) -> str: """Read a source file from disk.""" with open(path) as f: return f.read() ``` Execution order: your code calls `tool.run(args)` → LangChain validates args → Nanny wrapper intercepts → enforcement check → if allowed, file is read. ### CrewAI Same stacking pattern. CrewAI's `@tool` decorator and Nanny's `@tool` decorator both wrap the function — Nanny's wrapper fires on every `tool.run()` call inside the crew: ```python theme={null} from crewai.tools import tool as crew_tool from nanny_sdk import tool as nanny_tool @crew_tool # outer — CrewAI registers for agent dispatch @nanny_tool(tokens=15) # inner — Nanny intercepts before function runs def generate_chart(metric: str, output_dir: str) -> str: """Generate an interactive Plotly chart for a metric.""" # ... chart generation ... return output_path ``` See [`examples/python/dev_assist`](https://github.com/nanny-run/nanny/tree/main/examples/python/dev_assist) for a complete LangGraph integration and [`examples/python/metrics_crew`](https://github.com/nanny-run/nanny/tree/main/examples/python/metrics_crew) for the canonical multi-agent governance example with four specialized agents, per-role limits, and per-role tool allowlists. # Rust Source: https://docs.nanny.run/v0.5/guides/rust-sdk Per-function governance for Rust agents using Nanny macros. The Rust SDK brings Nanny's enforcement boundary into your code. Instead of relying only on process-level limits, you mark individual functions as tools and rules — Nanny governs each call before it executes. *** ## Installation The SDK ships inside the same crate as the CLI binary. Add it to your project: ```bash theme={null} cargo add nannyd ``` Then import what you need: ```rust theme={null} use nanny::{tool, rule, agent, PolicyContext}; ``` *** ## Passthrough mode If your agent runs without `nanny run`, every macro is a no-op. The function executes normally with no enforcement overhead: ```bash theme={null} # Governed — enforcement active (reads [start].cmd from nanny.toml) nanny run # Not governed — macros silent, agent runs normally cargo run ``` *** ## `#[tool]` — declare a governed tool Mark a function as a tool that Nanny should track and charge against the budget: ```rust theme={null} use nanny::tool; #[tool(tokens = 10)] fn fetch_page(url: &str) -> String { // HTTP call, file read, LLM call, or any side-effecting operation reqwest::blocking::get(url).unwrap().text().unwrap() } ``` When the agent calls `fetch_page`: 1. Nanny checks: is `fetch_page` in the `[tools] allowed` list? 2. Nanny checks: has `fetch_page` exceeded `[tools.fetch_page] max_calls`? 3. Nanny charges 10 tokens against the budget. 4. If any check fails, execution stops immediately — the function body never runs. ### Tokens The `tokens` argument is required. It is the number of tokens charged per call. Set it to `0` for tools you want tracked but not charged: ```rust theme={null} #[tool(tokens = 0)] fn log_step(msg: &str) { ... } ``` ### Matching the tool allowlist The tool name used for allowlist checks is the **function name** as declared in Rust: ```toml theme={null} # nanny.toml [tools] allowed = ["fetch_page", "read_file"] [tools.fetch_page] max_calls = 20 tokens_per_call = 10 # nanny.toml value overrides the macro default ``` *** ## `nanny::http_get` — built-in HTTP tool `nanny::http_get` is a built-in governed HTTP GET function. It requires no `#[tool]` annotation — Nanny applies allowlist, call-count, and token enforcement automatically. ```rust theme={null} use nanny::http_get; let html = http_get("https://example.com")?; ``` Governance applied automatically: * Checked against the `[tools] allowed` list (tool name: `"http_get"`) * Subject to `[tools.http_get] max_calls` limit * Costs 10 tokens per successful call (configurable via `[tools.http_get] tokens_per_call`) ```toml theme={null} # nanny.toml [tools] allowed = ["http_get"] [tools.http_get] max_calls = 15 tokens_per_call = 10 ``` In passthrough mode (no `nanny run`), `nanny::http_get` makes the request directly with no enforcement overhead. *** ## `nanny::report_usage` — report LLM token usage `#[tool(tokens = N)]` charges a *declared* token cost per call. To charge the *actual* tokens an LLM used, report them after the call with `nanny::report_usage`. Hand Nanny the token counts already present on the response — Nanny debits `input + output` from the budget. ```rust theme={null} use nanny::{report_usage, Usage}; let resp = client.chat().create(request).await?; report_usage(Usage { input: resp.usage.prompt_tokens, output: resp.usage.completion_tokens, ..Default::default() }); ``` Only `input` and `output` are required. You can optionally attach `model` and `provider` labels — identifiers only, never prompt or response content — and, if your provider's response reports it, `cache_read`/`cache_write` (a finer split of `input`, never additional tokens beyond it): ```rust theme={null} report_usage(Usage { input: resp.usage.prompt_tokens, output: resp.usage.completion_tokens, model: Some("gpt-4o".into()), provider: Some("openai".into()), ..Default::default() }); ``` Each call emits an `LlmUsageRecorded` event to the log. `report_usage` is fire-and-forget: it never blocks your agent and never panics. In passthrough mode (no `nanny run`) it is a no-op. Rust reports usage explicitly, one call per LLM response. In Python, `nanny_sdk.instrument(client)` does the same thing automatically by wrapping the client — Rust cannot patch a client at runtime, so the reporting is an explicit call instead. *** ## `#[rule]` — declare an enforcement rule A rule is a function that returns a verdict on whether execution should continue. Return `true` to allow, `false` to deny: ```rust theme={null} use nanny::{rule, PolicyContext}; #[rule("no_spiral")] fn check_spiral(ctx: &PolicyContext) -> bool { let h = &ctx.tool_call_history; // Deny if the last three tool calls were all the same URL !(h.len() >= 3 && h[h.len()-3..].iter().all(|u| u == &h[h.len()-1])) } ``` Rules are evaluated on every tool call. When a rule returns `false`, Nanny stops execution with: ``` StopReason::RuleDenied { rule_name: "no_spiral" } ``` ### PolicyContext fields The `ctx` parameter gives you a snapshot of the current execution state: | Field | Type | Description | | ------------------- | ------------------------- | ---------------------------------------------------------------------------------------------- | | `step_count` | `u32` | Number of steps completed | | `elapsed_ms` | `u64` | Wall-clock time elapsed | | `tokens_spent` | `u64` | Total tokens spent so far | | `next_tool_tokens` | `u64` | Declared token cost of the tool currently being evaluated. `0` when no tool call is in flight. | | `tool_call_counts` | `HashMap` | Per-tool call counts | | `tool_call_history` | `Vec` | Ordered log of tool names called | | `requested_tool` | `Option` | The tool being evaluated right now | | `last_tool_args` | `HashMap` | Arguments of the tool call currently being evaluated | Rules are evaluated **before** the tool runs — `requested_tool` is set to the tool name being checked. *** ## `#[agent]` — activate named limits for a scope Mark a function to run under a named limit set from `nanny.toml`. When the function is entered, Nanny switches to those limits; when it exits, limits revert: ```rust theme={null} use nanny::agent; #[agent("researcher")] fn run_research(topic: &str) { // This runs under [limits.researcher] from nanny.toml let page = fetch_page(&format!("https://en.wikipedia.org/wiki/{topic}")); // ... } ``` ```toml theme={null} # nanny.toml [limits.researcher] steps = 200 tokens = 5000 timeout = 120000 ``` The named set inherits from `[limits]` and overrides only the declared fields. Nesting `#[agent]` functions is supported — limits revert to the caller's set on exit. webdingo running under nanny run — planner, researcher, and synthesizer agent scopes entering and exiting with structured NDJSON events *** ## `nanny::fresh_run` — starting a fresh run mid-process `#[agent(...)]` changes which ceiling the run's *one* running total is checked against; it does not give a scope its own budget (see [Named sets share one counter](/v0.5/concepts/limits#named-sets-share-one-counter-they-are-not-separate-budgets)). If your process runs multiple, independent phases back to back and you want each one to start from zero rather than inherit whatever an earlier phase already spent, that's a new **run**, Nanny's real unit of governance: one cumulative counter, one stop state, "a stop is final." ```rust theme={null} use nanny::fresh_run; fn handle_request(payload: Payload) -> Response { fresh_run(); // this request's tokens/steps start from zero process(payload) } ``` Only meaningful when governed through a network server (`nanny run --serve` / `--join`): the server keys independent state per run, so a stop in the run you just left has zero effect on the one you're starting. Under local `nanny run` (no `--serve`), one process is already always exactly one run, so this is a safe no-op there. Mirrors `nanny_sdk.fresh_run()` on the Python side. *** ## Complete example ```rust theme={null} use nanny::{tool, rule, agent, PolicyContext}; use std::collections::HashMap; #[tool(tokens = 10)] fn fetch_page(url: &str) -> String { reqwest::blocking::get(url).unwrap().text().unwrap() } #[rule("no_spiral")] fn check_spiral(ctx: &PolicyContext) -> bool { let h = &ctx.tool_call_history; !(h.len() >= 3 && h[h.len()-3..].iter().all(|u| u == &h[h.len()-1])) } #[agent("researcher")] fn research(topic: &str) -> Vec { let mut pages = Vec::new(); let mut url = format!("https://en.wikipedia.org/wiki/{topic}"); loop { let content = fetch_page(&url); pages.push(content.clone()); // extract next URL from content ... break; } pages } fn main() { let results = research("Alan Turing"); println!("Collected {} pages", results.len()); } ``` Run it under Nanny: ```bash theme={null} nanny run ``` Run it without Nanny (macros silent, agent runs normally): ```bash theme={null} cargo run ``` *** ## What happens on stop When Nanny stops execution inside an instrumented function, the macro propagates the stop signal by panicking with a structured message. The panic is caught by the Nanny runtime — your agent process exits cleanly with a non-zero code and an `ExecutionStopped` event in the log. You do not need to handle stop reasons in your agent code. Nanny handles the exit path. # Introduction Source: https://docs.nanny.run/v0.5/index What Nanny is, what it is not, and when to use it. You deploy a multi-agent system on Friday. Monday morning your CFO sends a Slack: "Why did we spend \$4,000 over the weekend?" One agent got stuck in a loop. Nobody stopped it. No audit trail. Nothing. This is happening right now at hundreds of companies. **Nanny is the enforcement primitive that prevents it.** Nanny is an open-source enforcement primitive for autonomous agents and multi-agent systems. You tell it how far each agent is allowed to go — in steps, tokens, wall-clock time, and which tools it can touch — and the moment any limit is crossed, Nanny kills the process immediately and emits a structured log saying exactly what happened and why. No grace period. No soft warnings. No recovery logic. No negotiation. That boundary is deterministic, auditable, and structurally impossible for any agent to bypass. *** ## What nanny guarantees When you run an agent under nanny, these three things are true: * It **will not** take more steps than you allow. * It **will not** spend more than your token budget. * It **will not** run longer than your timeout. If any limit is breached, Nanny kills the process immediately — the agent cannot catch, delay, or prevent the stop. An `ExecutionStopped` event is emitted with the exact reason, and Nanny exits with a non-zero status code. *** ## What nanny is not Nanny sets limits and enforces them. It adds no intelligence — it doesn't interpret your agent's behavior, suggest better limits, or make any decisions. If you set a limit, it holds. *** ## Who it is for Nanny is for developers and teams running agents in production — or preparing to. It is a good fit if you: * Are building **multi-agent systems** where different agents have different roles, tool access, and budget ceilings — and you need enforcement that fires per-role, not just globally * Are running **autonomous agents** that call external tools, browse the web, or write to APIs * Need hard guarantees that an agent **cannot exceed a token budget or run indefinitely** * Want a **structured audit trail** of every tool call and stop reason for every execution * Are building with **CrewAI, LangChain, or any Python or Rust agent framework** * Want enforcement that is **not tied to any agent framework** — use CrewAI, LangGraph, or any Python or Rust framework without lock-in *** ## The multi-agent scenario A fintech team builds a system where a manager agent spawns 12 specialists: one checks regulations, one pulls market data, one drafts reports. They deploy on Friday. One agent gets stuck looping on a market data API call over the weekend. The team has no per-role kill switch and no audit trail of which agent made which call. With Nanny, each specialist has its own named limit set in `nanny.toml`: ```toml theme={null} [limits.analysis] steps = 60 tokens = 200 # tight — this agent makes expensive calls timeout = 60000 [limits.reporter] steps = 20 tokens = 50 # loose — this agent just writes a file timeout = 30000 ``` The analysis agent activates `[limits.analysis]` when it runs. The reporter activates `[limits.reporter]`. Each has its own tool allowlist — the analysis agent cannot call `write_report`, the reporter cannot call `compute_stats`. The moment any agent exceeds its ceiling or reaches for the wrong tool, Nanny stops it. The event log shows exactly which agent, which tool, which limit, and when. **Scope today:** This works for any multi-agent framework that runs agents within a single process — CrewAI, LangGraph, AutoGen, plain Python. See [`examples/python/metrics_crew`](https://github.com/nanny-run/nanny/tree/main/examples/python/metrics_crew) for the complete working example. Cross-process and cross-machine enforcement is supported via the governance server. *** ## The nanny ecosystem Nanny is designed to meet you where you are and grow with you. **Nanny CLI** — The enforcement entry point. Governs any agent process in any language as its parent process supervisor. Install it once as a system tool and use `nanny run` from any project that has a `nanny.toml` with a `[start]` command configured. ```bash theme={null} nanny run # reads [start].cmd from nanny.toml nanny run --limits=researcher # activates a named limit set ``` **Rust SDK**, For Rust agents, go deeper. Annotate individual functions with `#[nanny::tool]`, `#[nanny::rule]`, and `#[nanny::agent]` to get per-function token accounting, allowlist enforcement, and custom rules. See the [Rust SDK guide](/v0.5/guides/rust-sdk). **Python SDK**, The same model as the Rust SDK, as Python decorators. `@tool`, `@rule`, `@agent`. Each agent in your fleet gets its own budget ceiling, tool allowlist, and custom rules. Works with LangChain, CrewAI, or any Python agent framework. See [Python SDK](/v0.5/guides/python-sdk). **Nanny Cloud** *(coming soon)* — Durable audit logs, team dashboards, org-level budget aggregation, and managed enforcement across all your agents. The OSS runtime stays unchanged — Cloud is the observability and coordination layer above it. *** ## Open source The Nanny runtime is fully open source under the **Apache 2.0 licence**. Source code, issues, and contributions live at [github.com/nanny-run/nanny](https://github.com/nanny-run/nanny). Cloud is the managed layer above the OSS primitive — not a replacement for it. *** ## Next steps Install nanny and run your first governed agent in under five minutes. Install, upgrade, and uninstall on macOS, Linux, and Windows. Understand the enforcement model and passthrough mode. Learn how timeout, steps, and token limits work. Full schema for the configuration file. Per-function governance with `#[nanny::tool]`, `#[nanny::rule]`, `#[nanny::agent]`. Per-function governance with `@tool`, `@rule`, `@agent` decorators. Works with LangChain, CrewAI, and any Python agent framework. Cross-process and cross-machine enforcement with shared budget and mTLS. # nanny init Source: https://docs.nanny.run/v0.5/init Scaffold a nanny.toml configuration file in the current directory. Writes a `nanny.toml` with safe defaults into the current working directory. ```bash theme={null} nanny init ``` *** ## What it does Creates a `nanny.toml` with conservative defaults that work for most agents out of the box. Set `[start].cmd` to your agent's entry point and adjust the limit values to match your requirements. The generated file includes inline comments for every field and links to the full `nanny.toml` reference. It also writes `.nanny/app.json`, this app's permanent identity (an `app_id` plus a human-facing `name`, which you'll be prompted for). This is written **once, ever**: if it already exists, `nanny init` leaves it untouched, even if you choose to replace `nanny.toml`. There's no way to regenerate an existing identity; a genuinely different app means running `nanny init` in a genuinely different directory. `app_id` is what `--serve`, `--join=`, `--app=`, and Cloud sync all use to address this app. It isn't a secret, and `.nanny/app.json` is meant to be committed to git so the identity travels with the code to every environment that runs it (laptop, VPS, CI) unmodified. *** ## Flags `nanny init` takes no flags. It always writes to the current working directory. *** ## Errors | Condition | Behaviour | | ---------------------------------------- | ------------------------------------------------------------------------------------------------ | | `nanny.toml` already exists | Prompts for confirmation. Overwrites on `y` or `yes`. Exits without changes on any other input. | | Multiple `nanny*.toml` files exist | Exits with an error listing the conflicting files. A project must have exactly one `nanny.toml`. | | No write permission in current directory | Exits with an error. | To reset a config to defaults, run `nanny init` and confirm when prompted. This never affects `.nanny/app.json`: the app identity is independent of `nanny.toml` and is never reset. *** ## Next step Once your config is in place, run your agent: ```bash theme={null} nanny run ``` See [nanny run](/v0.5/run) for the full command reference. # Installation Source: https://docs.nanny.run/v0.5/install Install, upgrade, and uninstall the Nanny CLI on macOS, Linux, and Windows. ## Install ```bash theme={null} brew tap nanny-run/nanny brew install nannyd ``` ```bash theme={null} curl -fsSL https://install.nanny.run | sh ``` Installs to `/usr/local/bin` if writable, otherwise `~/.local/bin`. Have Rust installed? `cargo install nannyd` also works. ```powershell theme={null} irm https://install.nanny.run/windows | iex ``` Downloads `nanny-windows-x86_64.zip` from the latest GitHub Release, extracts to `%LOCALAPPDATA%\nanny\`, and adds that directory to your user PATH. Restart your terminal after installing. Verify: ```bash theme={null} nanny --version ``` *** ## Upgrade ```bash theme={null} brew upgrade nannyd ``` Re-run the install script. It overwrites the existing binary. ```bash theme={null} curl -fsSL https://install.nanny.run | sh ``` Re-run the install script. It overwrites the existing binary. ```powershell theme={null} irm https://install.nanny.run/windows | iex ``` *** ## Uninstall ```bash theme={null} nanny uninstall ``` If installed via Homebrew, this redirects you to `brew uninstall nannyd` to keep Homebrew metadata consistent. ```bash theme={null} nanny uninstall ``` ```powershell theme={null} nanny uninstall ``` Spawns a background process that removes the binary and cleans up your PATH automatically. Restart your terminal after uninstalling. *** ## Troubleshooting ### `nanny: command not found` after install Open a new terminal window. If the issue persists, confirm the install directory is in your PATH: ```bash theme={null} echo $PATH ``` Open a new terminal window. PATH changes do not apply to already-open sessions. To verify the entry was added: ```powershell theme={null} [Environment]::GetEnvironmentVariable("PATH", "User") -split ";" | Where-Object { $_ -match "nanny" } ``` **Windows Defender blocks the download** The binary is unsigned. If Defender quarantines it, add an exclusion for `%LOCALAPPDATA%\nanny\` or download the `.zip` directly from the [GitHub Releases](https://github.com/nanny-run/nanny/releases) page and extract manually. # Quickstart Source: https://docs.nanny.run/v0.5/quickstart Install Nanny, configure limits, and run your first enforced process in under five minutes. ## Install The Nanny CLI is a system tool — install it once and use `nanny run` from any project. ```bash theme={null} brew tap nanny-run/nanny brew install nannyd ``` ```bash theme={null} curl -fsSL https://install.nanny.run | sh ``` Have Rust installed? `cargo install nannyd` also works. ```powershell theme={null} irm https://install.nanny.run/windows | iex ``` Installs the binary to `%LOCALAPPDATA%\nanny\` and adds it to your PATH. Restart your terminal after installing. Verify the installation: ```bash theme={null} nanny --version ``` For upgrade paths, uninstall, and troubleshooting on each platform, see [Installing Nanny](/v0.5/install). The CLI is the enforcement engine. The SDK instruments your functions. You need both: `nanny run` owns the process lifecycle and enforces limits; `@tool`, `@rule`, and `@agent` report tool calls and activate named limit sets from inside your agent code. ## Initialise a config Run this in the root of your project: ```bash theme={null} nanny init ``` This writes a `nanny.toml` with safe defaults: ```toml theme={null} [start] # How to launch your agent. nanny run reads this command. cmd = "python agent.py" [limits] steps = 100 tokens = 1000 timeout = 30000 ``` Set `[start].cmd` to your agent's entry point and edit the limit values to match your requirements. `nanny init` also writes `.nanny/app.json`, a permanent, one-time identity for this app (an `app_id` plus a name you'll be prompted for). It's meant to be committed alongside `nanny.toml`. ## Run your agent ```bash theme={null} nanny run ``` Nanny reads `[start].cmd` from `nanny.toml`, spawns the process, and kills it the moment any limit is crossed. ## Use named limits Define limit sets for different workloads in the same `nanny.toml`: ```toml theme={null} [limits] steps = 50 tokens = 500 timeout = 15000 [limits.researcher] steps = 200 tokens = 5000 timeout = 120000 ``` Then activate a named set at runtime: ```bash theme={null} nanny run --limits=researcher ``` Named sets inherit from `[limits]` and override only the fields you declare. ## Read the event log Every run emits structured NDJSON to stdout: ```json theme={null} {"event":"ExecutionStarted","ts":1711234567000,"limits":{"steps":100,"tokens":1000,"timeout":30000},"limits_set":"[limits]","command":"python agent.py"} {"event":"ExecutionStopped","ts":1711234572000,"reason":"AgentCompleted","steps":7,"tokens_spent":70,"elapsed_ms":4823} ``` Pipe it to a file or your log aggregator: ```bash theme={null} nanny run >> nanny.log ``` Or configure file output directly in `nanny.toml`: ```toml theme={null} [observability] log = "file" ``` This writes to `.nanny/logs/log.ndjson`, auto-created, gitignored. Set `file = "..."` only if you want a different filename, see [Event Log](/v0.5/concepts/event-log) for details. # nanny auth Source: https://docs.nanny.run/v0.5/reference/cli-auth Log in to Nanny Cloud, or log out. Enforcement stays fully local either way. `nanny auth` logs you in to [Nanny Cloud](https://nanny.run/cloud) so `nanny run` can sync its event log to your dashboard. It's optional — enforcement is always local and never depends on it. Sync is automatic once a machine is logged in, no config field to set. See [Connect to Nanny Cloud](/v0.5/guides/managed-mode) for the full flow. ```bash theme={null} nanny auth ``` *** ## Commands ### login Log in by approving in your browser. ```bash theme={null} nanny auth login [--env ] ``` Approve in the browser and you're connected. `nanny run` starts syncing automatically, no config change needed. #### Flags | Flag | Type | Default | Description | | --------- | ---------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------- | | `--env` | `dev` \| `staging` \| `prod` | `prod` | Which Nanny Cloud to log in to. `dev`/`staging` are for Nanny's own testing. Accepts `--env staging` or `--env=staging`. | | `--token` | flag | — | Log in without a browser, for CI and headless machines. Reads an API key from `NANNY_API_KEY` or stdin. Requires `--env`. | #### CI and headless machines A browser flow needs a person. For CI, log in with an API key instead — supplied through `NANNY_API_KEY` or stdin, never a command argument: ```bash theme={null} NANNY_API_KEY="nny_..." nanny auth login --token --env prod # or pipe it: echo "$NANNY_API_KEY" | nanny auth login --token --env staging ``` `--env` is required so it targets the right cloud. *** ### logout Log out and stop syncing. ```bash theme={null} nanny auth logout ``` Enforcement is unaffected. To revoke the key everywhere, use the dashboard. *** ## Skip sync for one run To forward nothing for a single run without logging out, pass `--no-sync` to `nanny run`: ```bash theme={null} nanny run --no-sync ``` # nanny certs Source: https://docs.nanny.run/v0.5/reference/cli-certs Generate, import, rotate, inspect, and remove TLS certificates for the Nanny governance server. TLS certificates are required when the governance server binds to a non-loopback address (anything outside `127.x.x.x`). For local multi-process development on loopback, no certs are needed. ```bash theme={null} nanny certs ``` *** ## Commands ### generate Generate a complete certificate bundle for the governance server. ```bash theme={null} nanny certs generate [--out-dir ] [--days ] [--force] ``` Always generates all five files atomically — PKI requires a CA to sign the server and client certs, so partial generation is not supported. #### Files generated ``` ~/.nanny/certs/ ca.crt — CA certificate (the trust anchor) ca.key — CA private key (keep this on the server machine; needed for nanny certs rotate) server.crt — server TLS certificate server.key — server TLS private key client.crt — client certificate (copy to each agent machine) client.key — client private key (copy to each agent machine) ``` `nanny run --serve` reads `server.crt`, `server.key`, and `ca.crt` automatically. Agents on other machines need `client.crt`, `client.key`, and `ca.crt`. #### Flags | Flag | Type | Default | Description | | ----------- | ------- | ----------------- | ---------------------------------------------------------------------------------------------------------------------- | | `--out-dir` | path | `~/.nanny/certs/` | Directory to write cert files. The default is outside any project directory so files are never accidentally committed. | | `--days` | integer | `365` | Certificate validity period in days. | | `--force` | flag | — | Overwrite existing cert files. Without this flag, `generate` refuses if any cert files already exist. | #### Example output ``` nanny certs: generated certificate bundle in '/Users/you/.nanny/certs' ca.crt — CA certificate ca.key — CA private key (keep secure, used for rotate) server.crt — server certificate server.key — server private key client.crt — client certificate (distribute to agents) client.key — client private key (distribute to agents) valid until: 2027-05-01T09:00:00Z Start the server: nanny run --serve Cross-machine agents: copy client.crt + client.key to each agent machine and set NANNY_BRIDGE_CERT, NANNY_BRIDGE_KEY, NANNY_BRIDGE_CA in the env. ``` #### If cert files already exist ``` cert bundle already exists at '/Users/you/.nanny/certs' To keep your CA and regenerate only the server + client certs: nanny certs rotate — regenerate server + client certs, keep CA To inspect what you have: nanny certs show — inspect current expiry To regenerate everything (new CA, new server cert, new client cert): nanny certs generate --force ``` *** ### import Import externally-issued certificates. Use this when your organization has its own PKI (HashiCorp Vault, AWS ACM, cert-manager, an internal CA). ```bash theme={null} nanny certs import [ca=] [cert=] [key=] ``` Takes `key=value` pairs. Values are either a PEM string or a `@file` reference (path prefixed with `@`). Three keys are accepted: `ca`, `cert`, `key`. **Partial imports are supported.** Omit any key to leave the existing file unchanged. This is useful for rotating only the server cert and key while keeping the existing CA: ```bash theme={null} nanny certs import cert=@new-server.crt key=@new-server.key ``` After any import, Nanny validates that the certificate is signed by the CA (imported or existing). A mismatched cert/CA pair fails loudly with a clear error before any file is written. #### Examples **From files:** ```bash theme={null} nanny certs import \ ca=@/vault/secrets/ca.pem \ cert=@/vault/secrets/tls.crt \ key=@/vault/secrets/tls.key ``` **From environment variables (Vault Agent, CI/CD injection):** ```bash theme={null} nanny certs import \ ca="$VAULT_CA" \ cert="$VAULT_CERT" \ key="$VAULT_KEY" ``` **Directly from Vault CLI (no temp files):** ```bash theme={null} nanny certs import \ ca="$(vault read -field=issuing_ca pki/cert/ca)" \ cert="$(vault read -field=certificate pki/issue/nanny-server)" \ key="$(vault read -field=private_key pki/issue/nanny-server)" ``` **Updating CA, cert, and key together (CA was replaced by external PKI):** ```bash theme={null} nanny certs import \ ca=@new-ca.crt \ cert=@new-server.crt \ key=@new-server.key ``` After a successful import, if the governance server is running, it hot-reloads the new certs automatically. No restart needed. *** ### rotate Regenerate the server and client certificates, preserving the existing CA. ```bash theme={null} nanny certs rotate ``` `rotate` signs new server and client certs using the CA that `nanny certs generate` created. The CA itself is not changed — existing agents that trust this CA continue to work without re-importing `ca.crt`. **When to use `rotate`:** * Cert expiry is approaching (check with `nanny certs show`) * You want to invalidate the existing client cert (rotation generates a new `client.crt` + `client.key`) **When `rotate` does not apply:** * Your certs were issued by an external PKI (Vault, AWS ACM, etc.) — those systems hold the CA private key, not Nanny. Use `nanny certs import` instead. `rotate` requires `ca.key` to be present in `~/.nanny/certs/`. This file only exists when `nanny certs generate` created the CA. If `ca.key` is missing, `rotate` exits with an error and suggests `nanny certs import`. #### Example output ``` nanny certs: rotated — server + client certs regenerated, CA preserved valid until: 2027-05-01T09:00:00Z CA unchanged — existing agents retain their trust anchor Redistribute client.crt + client.key to agents on other machines nanny certs: server is running — certs will hot-reload automatically ``` *** ### show Show expiry dates, file inventory, and SAN list for the current cert bundle. ```bash theme={null} nanny certs show ``` Does not print private key material or file paths. Status only. #### Example output ``` nanny certs: '/Users/you/.nanny/certs' expires : 2027-05-01T09:00:00Z san : localhost, 127.0.0.1 present ca.crt present ca.key present server.crt present server.key present client.crt present client.key ``` If no certs exist: ``` nanny certs: no certificates found — run `nanny certs generate` ``` *** ### remove Delete all cert files from `~/.nanny/certs/`. ```bash theme={null} nanny certs remove ``` Prompts for confirmation before deleting. After removal, `nanny run --serve` with a non-loopback address will refuse to start until new certs are generated or imported. **If no certs exist:** ``` nanny certs: nothing to remove — '/Users/you/.nanny/certs' does not exist ``` *** ## Certificate hot-reload The governance server watches `~/.nanny/certs/` for file changes. When cert files are updated — by `nanny certs import`, `nanny certs rotate`, or an external PKI agent writing to the directory — the server reloads the new certs without restarting. New connections use the new cert immediately; existing connections complete on the old cert. This is designed for short-lived PKI certs (for example, Vault PKI secrets engine issuing 8-hour certs renewed automatically by Vault Agent). The server stays up; certs rotate underneath it. *** ## Keeping `ca.key` secure The CA private key (`ca.key`) is the trust anchor for your entire certificate bundle. Anyone with access to `ca.key` can generate new certificates that your server will accept. * Keep `ca.key` on the server machine only. Never copy it to agent machines. * The client machines need only `ca.crt` (to verify the server) and `client.crt` + `client.key` (to present to the server). * Back up `ca.key` securely. If you lose it, run `nanny certs generate --force` to start fresh — but you will need to redistribute `ca.crt` to all agent machines. # nanny health Source: https://docs.nanny.run/v0.5/reference/cli-health Check the health of all active Nanny components in one command. Shows the status of every Nanny component that is currently active. Exits `0` if all active components are healthy, `1` if any are unhealthy. ```bash theme={null} nanny health ``` *** ## What it checks `nanny health` checks three things: | Component | Active when | What "healthy" means | | ----------------- | --------------------------------------------------- | ----------------------------------------------- | | Local enforcement | `NANNY_BRIDGE_SOCKET` or `NANNY_BRIDGE_PORT` is set | A TCP connection succeeds | | Network server | `NANNY_BRIDGE_ADDR` is set | A TCP connection to the server address succeeds | | Certificates | `~/.nanny/certs/` exists | Cert files are present and not expired | A component that was never started is **not checked** and does not cause a non-zero exit. `nanny health` only reports on what is active. This means `nanny health` run from a regular terminal (outside `nanny run`) will typically show `local enforcement: not running` — that's expected. These env vars are injected by `nanny run` into the child process, not into the terminal that launched it. *** ## Example output **Server running, certs valid:** ``` local enforcement: not running network server : running (0.0.0.0:62669) [plain HTTP] certs : valid (expires 2027-05-01T09:00:00Z) ``` **All components active and healthy (within a governed process):** ``` local enforcement: running network server : not running certs : not found (run `nanny certs generate`) ``` **Server unreachable:** ``` local enforcement: not running network server : unreachable (server.example.com:62669) — connection refused certs : valid (expires 2027-05-01T09:00:00Z) ``` Exit code is `1` when any active component is unreachable. *** ## Certificate expiry warning When certs exist and are valid but expire within 30 days, `nanny health` prints a warning to stderr: ``` nanny health : warning — certs expire in 14 day(s). Run `nanny certs rotate` to renew. ``` The exit code is still `0` — a near-expiry cert is healthy, just worth knowing about. *** ## Use in scripts and health checks `nanny health` is designed for scripts, Docker health checks, and Kubernetes liveness probes: **Docker:** ```dockerfile theme={null} HEALTHCHECK --interval=30s --timeout=5s \ CMD nanny health || exit 1 ``` **Shell script:** ```bash theme={null} if ! nanny health; then echo "Nanny is not healthy — aborting" exit 1 fi ``` **Kubernetes:** ```yaml theme={null} livenessProbe: exec: command: ["nanny", "health"] initialDelaySeconds: 5 periodSeconds: 30 ``` *** ## See also * [`nanny status`](/v0.5/run#governance-server), focused status view for the governance server * [`nanny certs show`](/v0.5/reference/cli-certs), cert expiry and file inventory # Event Schema Source: https://docs.nanny.run/v0.5/reference/events Every event type emitted by Nanny, with full field definitions. ## Format All events are JSON objects emitted one per line (NDJSON). Every event has: | Field | Type | Description | | ------- | ------- | --------------------------------- | | `event` | string | Event type identifier (see below) | | `ts` | integer | Unix timestamp in milliseconds | *** ## ExecutionStarted Emitted immediately before the child process is spawned. Always the first event in any log. ```json theme={null} { "event": "ExecutionStarted", "ts": 1711234567000, "limits_set": "[limits]", "command": "python agent.py", "limits": { "steps": 100, "tokens": 1000, "timeout": 30000 } } ``` | Field | Type | Description | | ---------------- | ------- | -------------------------------------------------------- | | `limits_set` | string | The active limit set name. `"[limits]"` = base defaults. | | `command` | string | The full command string as passed to `nanny run`. | | `limits.steps` | integer | Active step limit. | | `limits.tokens` | integer | Active token limit. | | `limits.timeout` | integer | Active timeout in milliseconds. | *** ## ExecutionStopped Emitted on every exit path — clean completion, limit breach, spawn failure, or internal error. Always the last event in any log. ```json theme={null} { "event": "ExecutionStopped", "ts": 1711234572000, "reason": "AgentCompleted", "steps": 42, "tokens_spent": 380, "elapsed_ms": 4823 } ``` | Field | Type | Description | | -------------- | ------- | ----------------------------------------- | | `reason` | string | Why execution stopped. See reasons below. | | `steps` | integer | Total steps completed. | | `tokens_spent` | integer | Total tokens spent. | | `elapsed_ms` | integer | Total wall-clock time in milliseconds. | ### Stop reasons | Reason | Description | | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `AgentCompleted` | Process exited cleanly on its own. | | `TimeoutExpired` | Wall-clock timeout was reached. Process was killed. | | `MaxStepsReached` | Step limit was reached. Process was killed. | | `BudgetExhausted` | Token budget was exhausted. Process was killed. | | `ToolDenied` | A tool call was blocked because the tool is not in the allowlist. | | `RuleDenied` | A tool call was blocked by a custom rule or a per-tool `max_calls` limit. | | `ManualStop` | Execution was stopped programmatically. | | `ProcessCrashed` | The child process exited with a non-zero code unexpectedly. | | `BridgeUnavailable` | Enforcement was active but became unreachable during rule evaluation or a tool call. Nanny fails closed — silently continuing with ungoverned execution is never allowed. | *** ## AgentScopeEntered Emitted when a function annotated with `#[nanny::agent("name")]` is entered. Records the limits active for that scope. ```json theme={null} { "event": "AgentScopeEntered", "ts": 1711234567200, "name": "researcher", "limits": { "steps": 200, "tokens": 5000, "timeout": 120000 } } ``` | Field | Type | Description | | ---------------- | ------- | ------------------------------------------------------------------------ | | `name` | string | The name of the agent scope (matches `[limits.]` in `nanny.toml`). | | `limits.steps` | integer | Step limit active for this scope. | | `limits.tokens` | integer | Token limit active for this scope. | | `limits.timeout` | integer | Timeout active for this scope in milliseconds. | *** ## AgentScopeExited Emitted when a function annotated with `#[nanny::agent("name")]` returns. Records usage during that scope. ```json theme={null} { "event": "AgentScopeExited", "ts": 1711234571800, "name": "researcher", "steps_used": 12, "tokens_used": 240 } ``` | Field | Type | Description | | ------------- | ------- | ---------------------------------- | | `name` | string | The name of the agent scope. | | `steps_used` | integer | Steps consumed during this scope. | | `tokens_used` | integer | Tokens consumed during this scope. | *** ## StepCompleted Emitted after each agent step when using the Rust SDK or Python SDK. ```json theme={null} { "event": "StepCompleted", "ts": 1711234568000, "step": 1 } ``` | Field | Type | Description | | ------ | ------- | --------------------------------- | | `step` | integer | The step number, starting from 1. | *** ## ToolAllowed Emitted when Nanny permits a tool call to proceed. ```json theme={null} { "event": "ToolAllowed", "ts": 1711234568101, "tool": "http_get" } ``` | Field | Type | Description | | ------ | ------ | -------------------------------------- | | `tool` | string | The name of the tool that was allowed. | *** ## ToolDenied Emitted when Nanny blocks a tool call because the tool is not in the `[tools] allowed` list in `nanny.toml`. ```json theme={null} { "event": "ToolDenied", "ts": 1711234568101, "tool": "write_file" } ``` | Field | Type | Description | | ------ | ------ | -------------------------------------- | | `tool` | string | The name of the tool that was blocked. | *** ## RuleDenied Emitted when a custom rule or a per-tool `max_calls` limit blocks a tool call. The tool was on the allowlist but a rule returned a denial before the call executed. ```json theme={null} { "event": "RuleDenied", "ts": 1711234568101, "tool": "http_get", "rule_name": "no_loop" } ``` | Field | Type | Description | | ----------- | ------ | -------------------------------------------------------------------------------------------------------------------- | | `tool` | string | The name of the tool that triggered the rule. | | `rule_name` | string | The name of the rule that fired (e.g. `"no_loop"`) or the per-tool limit that was hit (e.g. `"http_get.max_calls"`). | *** ## ToolFailed Emitted when a permitted tool fails at runtime. Distinct from `ToolDenied` — the tool was allowed but encountered an error (network failure, bad arguments, timeout). No tokens are charged on failure. ```json theme={null} { "event": "ToolFailed", "ts": 1711234568200, "tool": "http_get", "error": "connection refused" } ``` | Field | Type | Description | | ------- | ------ | --------------------------------- | | `tool` | string | The name of the tool that failed. | | `error` | string | A description of the error. | *** ## LlmUsageRecorded Emitted when LLM token usage is reported to Nanny — via the Python SDK's `instrument()` or the Rust SDK's `report_usage()`. Records the measured input and output tokens debited from the budget, plus optional model, provider, and cache-usage labels. ```json theme={null} { "event": "LlmUsageRecorded", "ts": 1711234568150, "input": 1200, "output": 340, "model": "gpt-4o", "provider": "openai", "cache_read": 900 } ``` | Field | Type | Description | | ------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `input` | integer | Prompt / input tokens consumed by the LLM call. | | `output` | integer | Completion / output tokens produced by the LLM call. | | `model` | string | Optional. Model identifier reported by the SDK (e.g. `"gpt-4o"`). Omitted when not provided. | | `provider` | string | Optional. Provider identifier reported by the SDK (e.g. `"openai"`). Omitted when not provided. | | `cache_read` | integer | Optional. A finer split of `input` — never additional tokens beyond it — for providers that report prompt-cache reads (OpenAI, Anthropic, DeepSeek, Gemini). Omitted, not zero, for providers/responses that don't report cache usage. | | `cache_write` | integer | Optional. Same as `cache_read`, but for tokens written to cache (a real, separate cost for providers like Anthropic; most providers have no write-cost concept and never set this). | `cache_read`/`cache_write` are reporting-only, same as `model`/`provider`: enforcement always debits `input + output`, unaffected by whether either is present. They exist so a downstream cost calculator can price cache-hit tokens at their real, much cheaper rate instead of treating all input as one undifferentiated price. Every provider uses its own field name and shape for cache usage — there's no shared convention the way there is for input/output tokens — so the SDK normalizes each provider's own vocabulary into these two generic fields; see `nanny_sdk.instrument`'s module docs for the per-provider mapping. # nanny.toml reference Source: https://docs.nanny.run/v0.5/reference/nanny-toml Complete schema for the nanny.toml configuration file. ## Full schema ```toml theme={null} # ── Start ───────────────────────────────────────────────────────────────────── [start] cmd = "python agent.py" # required, the command nanny run executes # ── Limits ──────────────────────────────────────────────────────────────────── [limits] steps = 100 tokens = 1000 timeout = 30000 # Named limit sets — inherit from [limits], override only declared fields. [limits.researcher] steps = 200 tokens = 5000 timeout = 120000 # ── Tools ───────────────────────────────────────────────────────────────────── [tools] allowed = ["http_get", "read_file"] # empty list denies every tool call # Per-tool configuration — tool name must match the function name in code. [tools.http_get] max_calls = 10 # max number of calls in one execution tokens_per_call = 20 # overrides the decorator/macro default if set # ── Observability ───────────────────────────────────────────────────────────── [observability] log = "stdout" # "stdout" | "file" — "file" writes to .nanny/logs/log.ndjson # file = "events" # optional, bare name only, no extension — see below # ── Proxy ───────────────────────────────────────────────────────────────────── [proxy] allowed_hosts = ["api.openai.com", "*.anthropic.com"] # required when proxy mode is used # Cloud sync isn't a config field. It turns on for any machine that's run # `nanny auth login`. There is no endpoint or key in this file. ``` *** ## \[start] | Field | Type | Default | Description | | ----- | ------ | ------- | -------------------------------------------------------------------------------------------------------------------------------- | | `cmd` | string | — | **Required.** The shell command `nanny run` executes. Examples: `"python agent.py"`, `"cargo run --release"`, `"node agent.js"`. | *** ## \[limits] The global execution ceiling. Any one limit stopping the agent stops the entire run. | Field | Type | Default | Description | | --------- | ------------ | ------- | ----------------------------------------------------------------------------------- | | `steps` | integer | — | Maximum tool calls allowed. Requires SDK instrumentation. | | `tokens` | integer | — | Maximum tokens allowed. Requires SDK instrumentation. | | `timeout` | integer (ms) | — | Maximum wall-clock time in milliseconds. Enforced for any process, no SDK required. | **Named limit sets** — `[limits.]` inherits all fields from `[limits]` and overrides only the fields it declares: ```toml theme={null} [limits] steps = 50 tokens = 500 timeout = 15000 [limits.researcher] steps = 200 # overrides 50 tokens = 5000 # overrides 500 # timeout inherits 15000 [limits.reporter] tokens = 100 # overrides 500 only; steps and timeout inherit ``` Activate a named set from the CLI: ```bash theme={null} nanny run --limits=researcher ``` Or from inside agent code using `@agent("researcher")` (Python) or `#[nanny::agent("researcher")]` (Rust). *** ## \[tools] | Field | Type | Default | Description | | --------- | ------------ | ------- | -------------------------------------------------------------------------------------------------------------------------------- | | `allowed` | string array | — | Explicit allowlist of tool names. **An empty array denies every tool call.** If `[tools]` is not present, all tools are allowed. | ### Per-tool configuration — `[tools.]` ```toml theme={null} [tools.http_get] max_calls = 10 tokens_per_call = 20 ``` | Field | Type | Default | Description | | ----------------- | ------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | | `max_calls` | integer | unlimited | Maximum number of calls to this tool in one execution. Exceeding this limit fires `RuleDenied` with `rule_name = ".max_calls"`. | | `tokens_per_call` | integer | decorator/macro value | Tokens charged per call. Overrides the value declared in `@tool(tokens=N)` or `#[tool(tokens = N)]`. | The tool name in `[tools.]` must exactly match the function name used in the `@tool` decorator or `#[nanny::tool]` macro. *** ## \[observability] | Field | Type | Default | Description | | ------ | ---------------------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `log` | `"stdout"` \| `"file"` | `"stdout"` | Destination for the NDJSON event stream. | | `file` | string | `"log"` | Optional. A bare name, no extension and no path separators — Nanny always appends `.ndjson`. The directory is always `.nanny/logs/`, owned by Nanny, auto-created and gitignored — not configurable. | *** ## \[proxy] Optional. Enables HTTP CONNECT proxy mode on the governance server. Proxy mode is active only when `allowed_hosts` is present and non-empty. An empty list — or omitting `[proxy]` entirely — disables proxy mode. | Field | Type | Default | Description | | --------------- | ------------ | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `allowed_hosts` | string array | `[]` | Hostnames the proxy may forward to. Supports exact names (`api.openai.com`) and `*.suffix` wildcard patterns. Loopback, link-local, and RFC-1918 ranges are always blocked regardless of this list. | The CONNECT tunnel authenticates with its own `proxy_token`, separate from the ordinary session token. `nanny run --serve`/`--join` inject it automatically, so nothing in this section needs a credential. See [HTTP proxy mode](/v0.5/guides/http-proxy-mode) for full details. *** ## Cloud sync Cloud sync has no config block. It turns on for any machine that's logged in via `nanny auth login`, no separate field to flip. No key, endpoint, or org lives in `nanny.toml`: login handles the credential, so your committed config never holds a secret. See [Connect to Nanny Cloud](/v0.5/guides/managed-mode) for the full flow, including the `--token` path for CI and headless machines. # nanny run Source: https://docs.nanny.run/v0.5/run Run a command under Nanny enforcement. Spawns a child process under full Nanny enforcement. Reads `nanny.toml` from the current directory and kills the process the moment any limit is crossed. ```bash theme={null} nanny run [OPTIONS] [-- ARGS...] ``` *** ## Examples ```bash theme={null} # Run with base [limits] (reads [start].cmd from nanny.toml) nanny run # Run with a named limit set nanny run --limits=researcher ``` *** ## Options | Flag | Type | Default | Description | | ----------------- | ------ | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--limits=` | string | — | Activate a named limit set from `nanny.toml`. Inherits from `[limits]`. | | `--join=` | string | — | Join an existing governance server by app id (from that server's `.nanny/app.json`), instead of enforcing locally. Explicit and app-id-only: never a name, never auto-detected. Not compatible with `--serve`. | | `--serve` | flag | — | Run as the governance server that other processes and machines join, instead of launching a command. See [Governance server](#governance-server) below. | | `--no-sync` | flag | — | Do not forward events to Nanny Cloud for this run, even if the machine is logged in. Enforcement is unaffected. See [Connect to Nanny Cloud](/v0.5/guides/managed-mode). | | `--config=` | path | `./nanny.toml` | Path to config file. | *** ## Exit codes | Code | Meaning | | ---- | ---------------------------------------------------------------------------------- | | `0` | Process exited cleanly (`AgentCompleted`) | | `1` | Nanny stopped the process, a spawn failure occurred, or an internal error occurred | *** ## Stderr When Nanny stops a process it prints the reason to stderr: ``` nanny: stopped — TimeoutExpired nanny: stopped — BudgetExhausted nanny: stopped — MaxStepsReached nanny: stopped — ToolDenied nanny: stopped — RuleDenied ``` This message is separate from the structured event log, which goes to stdout (or a configured file). *** ## Event log Every run emits [NDJSON events](/v0.5/concepts/event-log) to stdout. `ExecutionStarted` is always first; `ExecutionStopped` is always last: ```json theme={null} {"event":"ExecutionStarted","ts":1711234567000,"limits":{"steps":100,"tokens":1000,"timeout":30000},"limits_set":"[limits]","command":"python agent.py"} {"event":"ToolAllowed","ts":1711234567120,"tool":"http_get"} {"event":"StepCompleted","ts":1711234567800,"step":1} {"event":"ExecutionStopped","ts":1711234572000,"reason":"BudgetExhausted","steps":12,"tokens_spent":1000,"elapsed_ms":5000} ``` Pipe to a file to keep your agent's own output separate: ```bash theme={null} nanny run > nanny.log ``` *** ## Governance server `nanny run --serve` starts a long-lived **governance server** instead of launching a command. Other processes and machines connect to it over TCP with `nanny run --join=`, and every tool call from every connected agent counts against one shared budget and step limit. For a single-process agent you don't need this: plain `nanny run` is enough. Starting a server requires `nanny init` to have already run in that directory: the server's state is keyed by the app's permanent `app_id`, not a global path, so two unrelated apps' servers on one machine never collide. ```bash theme={null} nanny run --serve [--addr ] [--cert ] [--key ] [--ca ] ``` It reads `nanny.toml` from the current directory and blocks until stopped (`nanny stop` or `CTRL-C`). ### Serve flags | Flag | Type | Default | Description | | -------- | -------------- | --------------------------- | -------------------------------------------------------------------------------------------------------------- | | `--addr` | socket address | `127.0.0.1:62669` | Listen address. The governance API and HTTP proxy share this port. Loopback = plain HTTP; non-loopback = mTLS. | | `--cert` | path | `~/.nanny/certs/server.crt` | Server TLS certificate PEM. Required only for non-loopback addresses. | | `--key` | path | `~/.nanny/certs/server.key` | Server TLS private key PEM. Required only for non-loopback addresses. | | `--ca` | path | `~/.nanny/certs/ca.crt` | CA certificate PEM used to validate agent client certs. Required only for non-loopback addresses. | The bind address sets the security posture: **loopback** (`127.0.0.1`) is plain HTTP for same-machine agents; a **non-loopback** address (`0.0.0.0`) makes mTLS mandatory, and the server refuses to start without certs. For cross-machine setup, certificates, and connecting agents, see the [Governance server guide](/v0.5/guides/governance-server). ### Manage a running server ```bash theme={null} nanny status # current directory's own app, by .nanny/app.json nanny status --app= # or target a specific app explicitly nanny stop # SIGTERM, then a 10-second graceful drain nanny stop --app= ``` Both commands default to the current directory's own `app_id` when `--app` is omitted. `nanny status` reads `~/.nanny/servers//server.addr` and probes the server (exit `0` if reachable, `1` otherwise). `nanny stop` reads the PID from `~/.nanny/servers//server.pid` and sends `SIGTERM` (on Windows, `taskkill /F`). ### Relocating the state directory Set `NANNY_HOME` to put `.nanny/servers/` (and everything else normally under `~/.nanny/`) somewhere other than the home directory: ```bash theme={null} NANNY_HOME=/opt/nanny nanny run --serve NANNY_HOME=/opt/nanny nanny run --join= ``` Both sides of a `--serve`/`--join` pair need the same `NANNY_HOME` to find each other's state. Falls back to the OS home directory when unset. *** For per-function governance (marking individual tools and rules in code), see the [Rust SDK guide](/v0.5/guides/rust-sdk) or [Python SDK guide](/v0.5/guides/python-sdk). # nanny uninstall Source: https://docs.nanny.run/v0.5/uninstall Remove the nanny binary from its current install location. Removes the `nanny` binary from its current install location. ```bash theme={null} nanny uninstall ``` Works on all platforms. On Windows, the running executable cannot delete itself — `nanny uninstall` spawns a hidden background process that waits for nanny to exit, then removes the binary and cleans up your PATH automatically. Restart your terminal after uninstalling. *** ## Errors | Condition | Behaviour | | ---------------------------------------- | -------------------------------------------------------------------------- | | Homebrew installation detected | Exits with an error and prints the correct `brew uninstall nannyd` command | | Insufficient permissions (macOS / Linux) | Exits with an error and prints a `sudo rm` fallback | | Binary path cannot be determined | Exits with an error |