Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
61d5a6c
docs: describe the threaded-host case without naming an app
evansibok Aug 30, 2026
765c46c
docs: record what v0.6.0 removed
evansibok Aug 30, 2026
73135d8
docs: record the authority primitives v0.6.0 adds
evansibok Aug 30, 2026
6e94676
docs: record the audit and identity work
evansibok Aug 30, 2026
8ac502d
docs: correct the v0.6.0 entry
evansibok Aug 30, 2026
055956d
docs: name what Nanny is
evansibok Aug 30, 2026
0cd13a8
docs: retire the old tagline
evansibok Aug 30, 2026
13a2eb8
chore: describe the crates for what they are
evansibok Aug 30, 2026
7b4e790
docs(sdk-python): describe the SDK for what it is
evansibok Aug 30, 2026
24a2c73
test: use neutral names in fixtures and samples
evansibok Aug 30, 2026
de48274
docs: correct claims the reframe left behind
evansibok Aug 30, 2026
8ac0f5d
docs: drop em dashes
evansibok Aug 30, 2026
eea6afa
docs: drop em dashes from the root docs
evansibok Aug 30, 2026
132feb5
chore: drop em dashes from the published manifests
evansibok Aug 30, 2026
54817a9
docs(sdk-python): drop em dashes from docstrings and comments
evansibok Aug 30, 2026
2f79b9f
refactor: drop em dashes and tracking labels from comments
evansibok Aug 30, 2026
1610713
docs: drop em dashes from the changelog and scripts
evansibok Aug 30, 2026
5496856
chore: drop em dashes from workflows and config
evansibok Aug 30, 2026
7417248
test: wait on the address the server bound, and on a deadline that su…
evansibok Aug 30, 2026
5687948
docs(sdk-rust): name the crate and the version a dependant needs
evansibok Aug 30, 2026
3ae76f2
docs: put Removed last, as the other entries do
evansibok Aug 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
version: 2
updates:
# The real, shipped product. Security-update PRs for these are the ones
# dependabot-auto-merge.yml is willing to auto-merge see that file for
# dependabot-auto-merge.yml is willing to auto-merge: see that file for
# why this is currently gated off pending branch protection on main.
- package-ecosystem: cargo
directory: /
Expand All @@ -17,7 +17,7 @@ updates:
open-pull-requests-limit: 10
labels: ["dependencies", "python", "sdk"]

# Demo apps, not installed by any real user. Grouped and capped lower
# Demo apps, not installed by any real user. Grouped and capped lower,
# these matter for security-audit.yml's non-blocking report, not for
# release gating, so they don't need the same update cadence as shipped
# code and shouldn't compete with it for review attention.
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci-python.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: CI Python SDK
name: CI, Python SDK

on:
push:
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/ci-rust.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: CI Rust
name: CI, Rust

on:
push:
Expand Down Expand Up @@ -33,14 +33,14 @@ jobs:
shell: pwsh
run: |
$bin = "target\debug\nanny.exe"
# Static CRT check VCRUNTIME140.dll must not appear in the PE import table.
# Static CRT check: VCRUNTIME140.dll must not appear in the PE import table.
$bytes = [System.IO.File]::ReadAllBytes($bin)
$ascii = [System.Text.Encoding]::ASCII.GetString($bytes)
if ($ascii -match "VCRUNTIME140\.dll") {
Write-Error "FAIL: nanny.exe imports VCRUNTIME140.dll +crt-static not applied"
Write-Error "FAIL: nanny.exe imports VCRUNTIME140.dll, +crt-static not applied"
exit 1
}
Write-Host "PASS: static CRT confirmed no VCRUNTIME140.dll dependency"
Write-Host "PASS: static CRT confirmed, no VCRUNTIME140.dll dependency"

clippy:
name: Clippy
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/dependabot-auto-merge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ name: Dependabot auto-merge
#
# Does not rely on branch protection to enforce "wait for CI": this repo's
# main had none configured when this was written, and GitHub's auto-merge
# only waits for checks marked *required* in branch protection with none
# only waits for checks marked *required* in branch protection: with none
# required, auto-merge can complete before CI even finishes. So this
# explicitly blocks on `gh pr checks --watch` itself before ever merging,
# correct with or without branch protection. Turning on branch protection
Expand Down Expand Up @@ -47,15 +47,15 @@ jobs:
PR_URL: ${{ github.event.pull_request.html_url }}
run: |
set -euo pipefail
echo "Security patch-level bump (${{ steps.metadata.outputs.ghsas }}) waiting for all checks on $PR_URL"
echo "Security patch-level bump (${{ steps.metadata.outputs.ghsas }}), waiting for all checks on $PR_URL"
# Blocks until every check on the PR finishes; exits non-zero if
# any failed. Nothing below this line runs on a red build.
gh pr checks "$PR_URL" --watch
echo "All checks green enabling auto-merge (squash)"
echo "All checks green, enabling auto-merge (squash)"
gh pr merge "$PR_URL" --auto --squash

- name: Leave everything else for manual review
if: steps.metadata.outputs.update-type != 'version-update:semver-patch' || steps.metadata.outputs.ghsas == ''
run: |
echo "update-type='${{ steps.metadata.outputs.update-type }}', ghsas='${{ steps.metadata.outputs.ghsas }}'"
echo "Not a patch-level security fix leaving for manual review, not auto-merging."
echo "Not a patch-level security fix, leaving for manual review, not auto-merging."
4 changes: 2 additions & 2 deletions .github/workflows/dependency-review.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: Dependency review

# Blocks a PR from introducing a NEW dependency with a known critical/high
# vulnerability. This is a diff check only it compares what a PR changes
# vulnerability. This is a diff check only: it compares what a PR changes
# against what's already on main, so it can't see or flag a vulnerability in
# a dependency nobody touched. That's what security-audit.yml (scheduled,
# full-state) is for; the two are complementary, not redundant.
Expand All @@ -26,4 +26,4 @@ jobs:
# that doesn't touch examples/**'s lockfiles never sees their
# existing, pre-known alerts. It does apply the same threshold if a
# PR bumps something inside examples/** to a version with its own
# new high/critical issue no manifest is exempted outright.
# new high/critical issue: no manifest is exempted outright.
8 changes: 4 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ jobs:
# Extract the block between ## [VERSION] and the next ## heading
awk "/^## \[${VERSION}\]/{found=1; next} found && /^## /{exit} found{print}" CHANGELOG.md > /tmp/release-notes.md
if [ ! -s /tmp/release-notes.md ]; then
echo "WARNING: No CHANGELOG entry found for ${VERSION} release notes will be empty." >&2
echo "WARNING: No CHANGELOG entry found for ${VERSION}, release notes will be empty." >&2
fi

- name: Create GitHub Release
Expand Down Expand Up @@ -193,7 +193,7 @@ jobs:
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
run: |
# Publish in topological order each crate before its dependants.
# Publish in topological order: each crate before its dependants.
# Idempotent: skips crates already on crates.io (safe to re-run).
# Output streams directly to the log so errors are always visible.
publish() {
Expand All @@ -210,11 +210,11 @@ jobs:
return 0
fi
if grep -q "already exists" /tmp/cp-out.txt; then
echo "Skipping $crate already published"
echo "Skipping $crate, already published"
return 0
fi
if grep -qE "no matching package|failed to select a version|not yet available|crate .* is not yet available" /tmp/cp-out.txt; then
echo "Index not ready sleeping 30s (attempt $attempt/$max)..."
echo "Index not ready, sleeping 30s (attempt $attempt/$max)..."
sleep 30
attempt=$((attempt + 1))
else
Expand Down
20 changes: 10 additions & 10 deletions .github/workflows/security-audit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: Security audit

# Complements dependency-review.yml, which only checks a PR's own diff.
# This checks the FULL current state of every dependency already merged,
# on push to main AND on a schedule the schedule is what catches a new
# on push to main AND on a schedule: the schedule is what catches a new
# CVE published against a dependency nobody has touched since it was
# pinned; the push trigger is what re-confirms a fix immediately after it
# merges, instead of waiting for tomorrow's cron. This is expected to start
Expand All @@ -19,11 +19,11 @@ name: Security audit
#
# Shipped code (root Cargo.lock, sdks/python) hard-fails on open
# critical/high alerts, UNLESS covered by an active, non-expired entry in
# .security-allowlist.jsonc see that file's header. examples/** is
# .security-allowlist.jsonc: see that file's header. examples/** is
# reported, not blocking: nothing under examples/ is installed by a real
# user, and gating a release on a demo app's dependency tree would be
# disproportionate. See cloud/IMPLEMENTATION_PLAN.md's "Future: remove
# examples/" note that decision, once acted on, removes this split
# examples/" note: that decision, once acted on, removes this split
# entirely rather than needing the allowlist extended to cover it.

on:
Expand All @@ -37,7 +37,7 @@ permissions:
contents: read

# GITHUB_TOKEN (github.token) cannot read /dependabot/alerts under any grant
# in the `permissions:` block above that endpoint isn't available to the
# in the `permissions:` block above: that endpoint isn't available to the
# Actions-generated token at all, regardless of scope requested.
# `security-events` (which used to be here) is for the Code Scanning API,
# a different endpoint; it never actually granted this. A fine-grained PAT
Expand All @@ -49,7 +49,7 @@ permissions:

jobs:
audit-shipped:
name: Shipped code fail on open critical/high alerts
name: Shipped code, fail on open critical/high alerts
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
Expand All @@ -61,7 +61,7 @@ jobs:
run: |
if [ -z "${{ secrets.NANNY_DEPENDABOT_ALERTS_TOKEN }}" ]; then
echo "::error::NANNY_DEPENDABOT_ALERTS_TOKEN secret is not set. This workflow needs a" \
"fine-grained PAT with 'Dependabot alerts: Read-only' on this repo GITHUB_TOKEN" \
"fine-grained PAT with 'Dependabot alerts: Read-only' on this repo, GITHUB_TOKEN" \
"cannot read that API under any permission grant. See this workflow's own header."
exit 1
fi
Expand All @@ -80,17 +80,17 @@ jobs:
fi
if [ "$i" -lt "$attempts" ]; then
echo ""
echo "Attempt $i/$attempts failed this can be GitHub's alert index still catching up"
echo "Attempt $i/$attempts failed, this can be GitHub's alert index still catching up"
echo "with a fix that just merged, not necessarily a real finding. Retrying in ${delay}s..."
sleep "$delay"
fi
done
echo ""
echo "Still failing after $attempts attempts treating as a real finding, not a race."
echo "Still failing after $attempts attempts, treating as a real finding, not a race."
exit 1

audit-examples:
name: Examples report only, non-blocking
name: Examples, report only, non-blocking
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
Expand All @@ -100,7 +100,7 @@ jobs:
GH_TOKEN: ${{ secrets.NANNY_DEPENDABOT_ALERTS_TOKEN }}
run: |
if [ -z "$GH_TOKEN" ]; then
echo "::warning::NANNY_DEPENDABOT_ALERTS_TOKEN secret is not set skipping (see audit-shipped job)."
echo "::warning::NANNY_DEPENDABOT_ALERTS_TOKEN secret is not set, skipping (see audit-shipped job)."
exit 0
fi
gh api "repos/${{ github.repository }}/dependabot/alerts" --paginate --slurp \
Expand Down
8 changes: 4 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Rust build output never commit this
# Rust build output: never commit this
/target

# macOS filesystem noise
Expand All @@ -13,7 +13,7 @@
*.swo
*~

# Environment / secrets never commit these
# Environment / secrets: never commit these
.env
.env.local
.env.*
Expand All @@ -28,10 +28,10 @@ __pycache__/
.pytest_cache/
*.egg-info/
dist/
# Note: uv.lock is intentionally committed reproducible Python builds.
# Note: uv.lock is intentionally committed: reproducible Python builds.

# Note: Cargo.lock is intentionally committed.
# This workspace produces a binary (nanny CLI). Committing Cargo.lock ensures
# every user gets an identical, reproducible build a direct requirement of
# every user gets an identical, reproducible build: a direct requirement of
# the determinism invariant in the Nanny manifesto.
.windsurf
6 changes: 3 additions & 3 deletions .security-allowlist.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
//
// This is a reviewed baseline, not a blanket suppression: every entry below
// must carry a real expiry, so the gate starts failing on it again if it's
// still unresolved by then a conscious decision to fix it or extend the
// still unresolved by then, a conscious decision to fix it or extend the
// expiry with a reason, not a silent, permanent exemption. A NEW open
// critical/high alert on shipped code (root Cargo.lock, sdks/python/uv.lock)
// that isn't listed here still fails the build immediately.
//
// Before adding an entry: check whether `cargo update -p <pkg>` or
// `uv lock --upgrade-package <pkg>` (sdks/python) already clears it most
// `uv lock --upgrade-package <pkg>` (sdks/python) already clears it, most
// do, scoped narrowly, without a full workspace update. If it doesn't,
// trace why (unreachable dependency? no fixed version published yet?) and
// put that in "notes", not just the package name.
Expand All @@ -19,7 +19,7 @@
//
// Keyed by the Dependabot alert's own number (stable, unique per repo,
// visible at github.com/nanny-run/nanny/security/dependabot/<N>), not by
// package name so a second, distinct advisory against an already-listed
// package name, so a second, distinct advisory against an already-listed
// package is never silently covered by an old entry meant for a different
// finding.
{
Expand Down
36 changes: 18 additions & 18 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,36 +1,36 @@
# AGENTS.md Nanny repository
# AGENTS.md: Nanny repository

## Quick start

**This is a Rust + Python monorepo with two independent build systems.**

- **Rust workspace**: `crates/` 6 crates, published to crates.io
- **Python SDK**: `sdks/python/` published as `nanny-sdk` on PyPI
- **Rust workspace**: `crates/`, 6 crates, published to crates.io
- **Python SDK**: `sdks/python/`, published as `nanny-sdk` on PyPI

They share the same repo and version number but have no toolchain overlap.

## Architecture

**Nanny is an enforcement primitive for autonomous AI agents.** It stops agents that exceed limits (steps, tokens, timeout) or violate rules.
**Nanny is the authorization and audit layer for AI agents that take real-world actions.** It refuses tool calls the operator has not authorized, and records every decision.

**Key concept**: Nanny becomes the **parent process** of your agent via `nanny run`. All enforcement happens in the parent; the child cannot bypass it.

### Core abstractions

| Term | Description |
|------|-------------|
| **tool** | Function annotated with `#[nanny::tool]` / `@tool` passes through bridge for enforcement |
| **rule** | Function annotated with `#[nanny::rule]` / `@rule` returns `false` to stop execution |
| **tool** | Function annotated with `#[nanny::tool]` / `@tool`, passes through bridge for enforcement |
| **rule** | Function annotated with `#[nanny::rule]` / `@rule`, returns `false` to stop execution |
| **agent scope** | Named limits context activated by `#[nanny::agent]` / `@agent` |
| **bridge** | Internal enforcement layer (Unix socket / TCP). **Never mention in user-facing docs.** |

### Three limits

Any one stops execution:

- `timeout` wall-clock ms (no instrumentation needed)
- `steps` tool calls (requires SDK)
- `tokens` token budget (requires SDK)
- `timeout`: wall-clock ms (no instrumentation needed)
- `steps`: tool calls (requires SDK)
- `tokens`: token budget (requires SDK)

## Developer workflow

Expand Down Expand Up @@ -98,7 +98,7 @@ uv run pytest ../../packs/nanny-recommended/tests

- **Rust**: `rustfmt`, no `unwrap()`/`expect()` outside tests, `thiserror` for errors, doc comments on public items
- **Python**: `ruff` (line-length=100, target-version=py311), `mypy --strict`, `pytest` + `pytest-httpserver` for tests
- **Versioning**: Docs folders are versioned at minor level only (`v0.4/`, `v1.0/`) never patch; a patch release updates the current folder in place
- **Versioning**: Docs folders are versioned at minor level only (`v0.4/`, `v1.0/`), never patch; a patch release updates the current folder in place

## Branching and releases

Expand All @@ -112,7 +112,7 @@ uv run pytest ../../packs/nanny-recommended/tests

| Path | Purpose |
|------|---------|
| `crates/core` | Traits and types only no implementations |
| `crates/core` | Traits and types only, no implementations |
| `crates/runtime` | Concrete impls: `ToolPermissionPolicy`, `RuleEvaluator`, built-in tools |
| `crates/bridge` | Local HTTP enforcement server. Wire protocol: `crates/bridge/PROTOCOL.md` |
| `crates/config` | Parses `nanny.toml` |
Expand All @@ -125,7 +125,7 @@ uv run pytest ../../packs/nanny-recommended/tests
## Testing

- **Rust**: `cargo test --workspace` runs in parallel; use unique temp file names
- **Python**: `uv run pytest` uses `mock_bridge` fixture no real bridge required
- **Python**: `uv run pytest` uses `mock_bridge` fixture, no real bridge required
- **Packs**: `uv run pytest ../../packs/nanny-recommended/tests` from `sdks/python`

## Documentation surfaces
Expand All @@ -142,9 +142,9 @@ uv run pytest ../../packs/nanny-recommended/tests

## Critical gotchas

1. **Direct-call pattern** your code must drive tool calls; the LLM should reason, not dispatch tools
2. **Passthrough mode** decorators and macros are no-ops outside `nanny run`; zero overhead in dev and CI
3. **Stop reasons** four, and the set is closed: `ToolDenied`, `RuleDenied`, `AgentCompleted`, `ManualStop`. Only the first two are policy violations
4. **Rules reference labels, not tool names** a rule naming `send_outreach` governs one app; a rule reading `external_effect` governs every app whose operator labelled their tools
5. **Token tracking** Python: `nanny_sdk.instrument(client)` once at startup. Rust: `nanny::report_usage(...)` after each LLM call. Measured for attribution, never enforced
6. **`--serve` is the launch mode** one governor, many runs, one shared log. Every event carries its `run_id`
1. **Direct-call pattern**, your code must drive tool calls; the LLM should reason, not dispatch tools
2. **Passthrough mode**, decorators and macros are no-ops outside `nanny run`; zero overhead in dev and CI
3. **Stop reasons**, four, and the set is closed: `ToolDenied`, `RuleDenied`, `AgentCompleted`, `ManualStop`. Only the first two are policy violations
4. **Rules reference labels, not tool names**, a rule naming `send_outreach` governs one app; a rule reading `external_effect` governs every app whose operator labelled their tools
5. **Token tracking**, Python: `nanny_sdk.instrument(client)` once at startup. Rust: `nanny::report_usage(...)` after each LLM call. Measured for attribution, never enforced
6. **`--serve` is the launch mode**, one governor, many runs, one shared log. Every event carries its `run_id`
Loading