Conversation
… real hard stop Two gaps found while auditing [proxy] allowed_hosts for the OpenClaw demo: - G8: nothing injected HTTPS_PROXY/HTTP_PROXY into the governed child, so the allowlist silently did nothing unless a human remembered to set it by hand — a fail-open gap the manifesto forbids. cmd_run_via_network_server now injects these (plus lowercase and NO_PROXY) whenever the SERVER (not the joining client's own nanny.toml, which may live elsewhere) has [proxy] configured, via a new ~/.nanny/server.proxy discovery file. - G9: a proxy denial only failed that one CONNECT; the run itself was never marked stopped, contradicting docs and every other denial path. route_proxy now calls mark_stopped, and also gates on an already-stopped run the same way route_tool_call/route_rule_evaluate already do, so a denied run can't keep tunneling to an allowed host afterward. Python SDK needed no code change for G9 — existing 410 handling already maps an unknown/non-limit reason (including "ToolDenied") to ExecutionStopped; a test was added to prove it, not new logic.
Comments and a docstring referenced internal plan-tracker shorthand (tracker IDs used to sequence work on a planning doc). Those don't belong in source — they rot as the tracker moves on and mean nothing to anyone reading the code without that context. Reworded to explain the reasoning directly.
… Telegram channel setup Covers the install method chosen (npm over the curl|bash shell installer), the LaunchAgent runtime shape and config file location, the channel pivot from mail to Telegram (mail is a skill wrapping himalaya/gog with no simple sandbox override; WhatsApp links a real account via QR, Telegram uses an isolated bot identity), and the security audit findings from a fresh install, including the tool-hardening attempt that broke basic message sending and was reverted.
Opt-in, dollars-denominated, mirrors [limits]'s existing named-subtable pattern ([budget.<name>] alongside [limits.<name>]). Purely inert here — parsed and validated, never resolved. Resolution into the real tokens = N value happens once in the CLI, before the engine ever sees the config; nanny-core and nanny-bridge never see [budget] at all. Absent by default, so every existing nanny.toml keeps working unchanged.
…ema" This reverts commit 9648879.
Local runtime side of the app-identity/governor-isolation redesign (cloud/IMPLEMENTATION_PLAN.md, "App identity, governor isolation & Cloud-scoped auth"): - `nanny init` now writes a permanent `.nanny/app.toml` identity (appId + a human-facing name) alongside `nanny.toml` — one-time, local, never regenerated. - `--serve` state (`server.addr`/`.token`/`.pid`/`.proxy`) is keyed by app id under `~/.nanny/servers/<app_id>/`, replacing the old global, unkeyed files that let two unrelated apps' governors collide. - `nanny run --join=<appId>` replaces blind auto-detection of "whatever governor is running" with an explicit, id-only join; fails loudly if the target isn't reachable instead of silently falling back to a local bridge. - `nanny status --app=<id>` / `nanny stop --app=<id>` extend the former zero-argument commands to target one app's governor. - `mode` is removed from `nanny.toml` entirely. Sync is decided purely by whether an app-scoped Cloud credential exists locally; no credential, no sync, no config knob. `--no-sync` still overrides. - Per-app, gitignored credential storage (`.nanny/credentials.local.toml`), self-minted on `nanny run` per machine (not at `nanny init`, which is one-time and can't reach every machine an app is later deployed to). CONNECT proxy: real bugs found and fixed, not just the isolation work above: - Routing a CONNECT request through axum's `Router::call()` silently broke hyper's server-side upgrade handoff (confirmed with a minimal reproduction outside this codebase). `GovernorService` now intercepts CONNECT before the router ever sees it; regular routes are unaffected. - CONNECT now authenticates via `Proxy-Authorization: Basic <b64(proxy_token:)>`, a new, separate credential from `session_token` — the only credential mechanism a generic proxy-aware HTTP client can actually deliver on a CONNECT handshake with zero app-side code changes. If it ever leaks (e.g. via a developer's own verbose HTTP client logging, which prints proxy URLs), the blast radius is "open a tunnel to an already-allowlisted host," not full run control. - Rate-limit and auth checks moved out of axum `.layer()`s (which structurally can't see CONNECT) into `GovernorService::call`, the one dispatch point every request — CONNECT or not — passes through. There is no longer a second place a future universal check could be added and silently not cover CONNECT. - Token comparisons (`session_token`, `proxy_token`) now use a constant-time compare instead of `==`, closing a timing side-channel. - The injected `HTTPS_PROXY` URL always includes an explicit empty password (`http://token:@host`) — some HTTP clients (Python's `requests`/urllib3) silently drop the username too when the password is merely absent rather than empty. Full workspace test suite green throughout; new tests cover --join, --serve state keying, CONNECT auth (both credentials), and the proxy-URL env var injection.
… global path nanny_server_is_running() only ever checked the old global ~/.nanny/server.addr, so `certs import`/`rotate` could never detect a running governor under the per-app ~/.nanny/servers/<app_id>/ layout unless NANNY_BRIDGE_ADDR happened to already be set. Now scans all per-app server dirs for a reachable server.addr. Also drops a stale "there is no --force" mention in identity.rs's doc comments — --force was never implemented or shipped.
Covers the breaking local-runtime redesign already on this branch: per-app identity, keyed governor state, explicit --join, per-app Cloud credentials, and the CONNECT-proxy auth rework.
Rewrites managed-mode.mdx, governance-server.mdx, http-proxy-mode.mdx, and init.mdx around the new model: sync decided by login presence (no mode/[runtime] field), explicit --join=<appId> (no auto-detect), and per-app state under ~/.nanny/servers/<appId>/. Single-block fixes to README.md, sdks/python/README.md, quickstart.mdx, nanny-toml.mdx, and cli-auth.mdx for the same reasons. Also cleans up stray em-dashes in commit-adjacent comments in certs.rs and identity.rs.
run.mdx: add --join to the options table, reword --no-sync off "managed mode", update status/stop examples and paths to the per-app --app form. auth.rs: fix a stale doc comment describing sync as gated on mode = "managed". examples/: strip the dead [runtime]/mode block from the 4 example nanny.toml files, and the two stale `nanny server start` lines from metrics_crew's proxy comment. No other changes to the example apps.
Follows this project's own established convention (0.3->0.4 got the same folder rename in b7cdd0d, documented in docs/AGENTS.md's own versioning policy): a minor version with a meaningfully different mental model gets a fresh docs folder, not an in-place patch update. Adds a /v0.4/:slug* -> /v0.5/:slug* redirect alongside the existing v0.1-v0.3 ones, and updates docs.json's version switcher and all internal cross-links accordingly. Also removes stray em-dashes from prose introduced earlier in this branch's doc sweep.
…ying Adds regression coverage for gaps found while auditing this branch's test suite: - secure_compare: equal, unequal, different-length, empty inputs - a session_token must not authenticate CONNECT, and a proxy_token must not authenticate an ordinary request (the whole point of splitting them into two credentials) - nanny_server_state_dir: different app ids never collide, the same id is always stable, and --app wins outright over cwd resolution - AppCredentials: save/load round trip, 0600 file permissions, the gitignore entry is added exactly once, and self-minting short- circuits to the existing credential without a network call Also removes stray em-dashes from comments introduced earlier in this branch (some of the earlier cleanup had merged unrelated parentheticals across line breaks; fixed those to read correctly instead of just removing the character).
Five tests covering the exact features this branch adds (--join, loopback --serve without certs, and proxy env-var injection) were Windows-excluded because they sandboxed state into a temp dir by overriding HOME, and dirs::home_dir() ignores HOME on Windows. Adds NANNY_HOME, a real override checked before falling back to the OS home directory, since a plain env var read behaves identically on every platform where an OS profile-directory lookup does not. Used it in nanny_server_state_dir (the one function both --serve and --join resolve state through), then switched the five tests to it and dropped their #[cfg(not(windows))] gates. T10 and T11 also assumed a POSIX `sh` on PATH for their [start].cmd, which this project's own existing Windows tests already avoid elsewhere in this file. Gave both a cmd.exe equivalent, and accounted for the one real behavioral difference this surfaces: an unset variable prints as empty in sh but as the literal %VAR% text in cmd.exe, so T11's assertion branches on platform for that reason only.
Added while un-skipping the Windows --join/--serve tests; it's a real, always-available override (not test-only), so it belongs in docs, not just a code comment.
…on an unreachable bridge These four raised a raw httpx.ConnectError (full traceback and all) straight through @agent/@tool when the governor wasn't reachable, instead of the typed BridgeUnavailable every other bridge failure mode already gets. @rule's own GET /status check already had this exact guard; this makes every bridge call consistent with it via one shared _bridge_call() context manager instead of relying on each call site to remember. Also bumps sdks/python/pyproject.toml (and its lockfile) from 0.4.2 to 0.5.0 — it was still unbumped on this branch, and release.yml's publish-pypi job hard-fails if the SDK version doesn't exactly match the release tag.
GoTM consumes nanny-sdk via a local path dependency, not a version pin, so this number has no effect on local testing either way — it only matters to release.yml's publish-pypi version-match check, which only runs on an actual tag push. Bumping it now just locks in "0.5.0 is final" before that's decided. Bump this as the last step before actually tagging and publishing, not before.
Cargo.toml and CHANGELOG.md already call this release 0.5.0; leaving the SDK at 0.4.2 just left the branch internally inconsistent for no benefit, since the version string has no effect on anything until an actual tag push (GoTM consumes it via a local path dependency, not a version pin). Trivially revertable later if the release scope changes.
nanny.toml's [observability] table has always promised "here's where your event log goes." Local `nanny run` kept that promise via EventWriter; `nanny run --serve` silently didn't, it never read config.observability at all, so log = "file" quietly did nothing the moment an app moved from local runs to a long-lived governor. Any app relying on that file (piping it to Datadog, tailing it for a live token count, whatever) would work locally and silently stop working under --serve, with no error. NetworkServer::start_blocking_synced gains an optional local_log_path: Option<PathBuf>. The existing drain thread (previously spawned only when a cloud event_sink was attached) now also spawns when a local log path is given, and both destinations get their own copy of each drained batch: draining is destructive, so this is the one place that reads a run's events, cloud sync and the local file never steal from each other. start_blocking's own signature is unchanged (passes None through), so none of its existing test call sites needed updating. commands/server.rs resolves local_log_path from the server's own nanny.toml, mirroring EventWriter::from_config's exact decision logic (same missing-log_file error) so both paths fail the same way. log = "stdout" stays a no-op for --serve on purpose: a long-lived server continuously mixing NDJSON into its own stdout status output would be noisy and wrong, unlike a short-lived local run. Verified against a real joined client (gotm-nanny's --join flow): gotm.ndjson, empty at server start, filled with real ToolAllowed/ AgentScopeEntered/LlmUsageRecorded events the moment a real governed execute cycle ran against it, flushed per event, same guarantee local mode already had.
…equired log_file (a developer-specified path) is replaced by file (an optional bare name, no extension, no path separators). Nanny always owns the directory now: .nanny/logs/, auto-created, auto-gitignored. Unset, the filename defaults to log.ndjson; Nanny always appends .ndjson itself. Both nanny run and nanny run --serve resolve through the same ObservabilityConfig::resolve_log_path, so the two paths can no longer drift the way --serve's own [observability] gap just did (e968fee).
…/step endpoint handle_tool_call already incremented step_count on every allowed call (both the registry-tool and user-defined @tool paths), but never emitted the matching StepCompleted event, only a separate /step endpoint did, and nothing in any SDK, Python or Rust, ever called it. Confirmed via a repo-wide grep and a test that had already noticed the symptom in passing ("tool call also ticks"). handle_tool_call now emits StepCompleted alongside ToolAllowed, the pairing the event-log docs already claimed. /step, handle_step, and route_step are removed entirely: keeping an unused endpoint that writes to the same counter as the real path was a live double-count risk, not just dead code. Rewrote the 8 tests that exercised /step to exercise real tool calls instead. Full workspace suite passes (117 bridge tests + rest).
…un mid-process
Both SDKs let you set NANNY_RUN_ID directly, an internal detail the
client happens to read fresh on every call, never documented as
something to rely on. A real integration need (a process running
several independent phases back to back, each wanting its own clean
token/step budget instead of inheriting whatever an earlier phase
already spent) had no discoverable way to express it.
nanny::fresh_run() (Rust) / nanny_sdk.fresh_run() (Python) mint a new
run id and set it, mirroring each other exactly. Only meaningful under
--serve/--join (the server keys independent state per run id); a safe
no-op under local nanny run, where one process is already one run.
docs/v0.5/concepts/limits.mdx also gets a direct explanation of why
this exists: named limit sets share one cumulative counter for the
whole run, they are not independent per-scope budgets, a real
misconception the guide's own wording previously encouraged ("hitting
the analysis budget doesn't kill the reporter" was flatly wrong under
the real, shared-counter behavior). Both SDK guides get a matching
fresh_run reference section.
…leanup handle_connect emitted ToolAllowed on an allowed proxy tunnel but never incremented step_count or emitted StepCompleted, the only one of the three real ToolAllowed call sites in the codebase that didn't. Invisible unless an agent's only governed actions are proxied LLM calls with no @tool calls, exactly the case that showed a real run doing real, budget-consuming work with its step count stuck at zero. Also reworks "bridge" language across docs/CLI health output toward "enforcement", per an audience audit distinguishing internal implementation terms from what users should see, and documents the shared-counter (not per-scope-budget) mechanics of named limit scopes more precisely in ARCHITECTURE.md and the Python SDK README.
…le except nanny.toml LlmUsageRecorded gains optional cache_read/cache_write fields, a generic finer split of input tokens for providers that report prompt-caching usage (OpenAI, Anthropic, DeepSeek, Gemini). Reporting only: enforcement still debits input + output exactly as before. nanny_sdk.instrument() normalizes each provider's own incompatible field shape into these two fields, with a real fix along the way: Anthropic's own input_tokens is exclusive of cache usage (unlike DeepSeek/OpenAI), so the SDK now adds cache_read/cache_write into input for Anthropic specifically, keeping Nanny's own input field one universal, provider-independent total regardless of source. Also converts every Nanny-owned data file to JSON except nanny.toml itself, the only file meant to be hand-edited and commented: .nanny/app.toml, ~/.nanny/credentials.toml, and .nanny/credentials.local.toml are now .json. credentials.toml was a real, already-published 0.4.2 format; the other two were new and unpublished in this branch. No automatic migration.
Chat Completions rejects tool calls combined with real reasoning_effort for every current OpenAI reasoning model, so GoTM's move to the Responses API for real reasoning+tools left every OpenAI call unmeasured: instrument() only ever patched client.chat.completions.create. Adds a second patch for client.responses.create, plus usage extraction for its distinct ResponseUsage shape (input_tokens/output_tokens, input_tokens_details for cache), disambiguated from Anthropic's coincidentally same-named fields so a Responses-API call can never fall into Anthropic's additive cache-total formula and silently over-debit the budget.
The Python SDK guide claimed stop reasons propagate up and terminate the process via nanny run in all cases. That's only true under plain nanny run (no --serve): --join has no watcher on the child process at all, so an uncaught NannyStop in a long-lived server's request handler is left to the host framework's own crash behavior, not something Nanny handles for you. Adds a matching section to the governance-server guide explaining why, with a runnable fresh_run()/try-except pattern for containing a stop per request.
Three real changes from this branch weren't yet reflected: the CONNECT proxy auto-injection fix (HTTPS_PROXY was silently a no-op unless set by hand), the proxy-denial-now-a-real-stop fix, and OpenAI Responses API instrumentation. All three landed earlier on this branch but predated or were missed by the original changelog entry. Also bumps the 0.5.0 date to today, the actual publish date.
…lippy failure Internal runtime::report_usage took 8 positional args, one over clippy's too_many_arguments threshold, which CI runs with -D warnings (a hard error, not the warning the same lint prints locally without that flag). The two split harness fields were reconstructed from a single Option<Harness> at the public boundary just to split it again three lines later. Passing the struct straight through removes both the split and the arg, no behavior change. Verified with cargo clippy --workspace --all-targets -- -D warnings (CI's exact invocation) and cargo test --workspace, both clean.
…auto-merge Verified before writing this: of the 80 currently-open Dependabot alerts, only 1 is in sdks/python/uv.lock (the actual shipped SDK); the 3 criticals and most of the 36 highs are in examples/python/metrics_crew/uv.lock, a demo app nobody installs. 4 highs are real, in the root Cargo.lock (quinn-proto, openssl) — unrelated to Python entirely. - dependency-review.yml: blocks a PR from introducing a new dependency with a known critical/high vulnerability (diff-only, PR-triggered). - security-audit.yml: scheduled (daily) full-state check via the Dependabot Alerts API, since a diff check alone can never catch a new CVE published against an already-merged, untouched dependency. Hard fails on shipped code (root Cargo.lock, sdks/python/uv.lock); reports examples/** as non-blocking, since nothing there ships to a user. - dependabot.yml: weekly version-update PRs, shipped code and examples configured and labeled separately. - dependabot-auto-merge.yml: auto-merges only patch-level, security-driven Dependabot PRs, and only after explicitly waiting on and checking every CI run itself (gh pr checks --watch), not by relying on GitHub's native auto-merge timing. That's deliberate: main currently has no branch protection, so auto-merge's default wait-for-required-checks behavior would not actually wait for anything. Recommend enabling branch protection on main (require ci-rust, ci-python, dependency-review, security-audit) regardless — this workflow's own wait covers Dependabot PRs specifically, not every other PR.
…ty alerts quinn-proto (memory exhaustion, RUSTSEC) is confirmed unreachable from any workspace member on any target (cargo tree -i --target all: nothing to print) — a stale, unused lockfile entry with zero effect on the compiled binary either way. Updated anyway to clear the alert; the cascade this pulled (rand 0.9->0.10, dropping zerocopy/ppv-lite86) is confined to that same dead subgraph. openssl (UB in X509Ref::ocsp_responders, real and reachable via reqwest -> native-tls -> hyper-tls, used by nannyd directly) bumped 0.10.78 -> 0.10.81, a same-minor patch release, no API change. Both scoped with cargo update -p, not a full workspace update — a first attempt at a full `cargo update` touched over a dozen unrelated packages (rand, wasm-bindgen, webpki-roots, zerocopy) and was reverted before committing anything, in favor of these two narrow, verified fixes. Verified: cargo build --workspace, cargo test --workspace (118+22+38+5+61+ 9+13 passed, 0 failed), cargo clippy --workspace --all-targets -- -D warnings (CI's exact invocation), all clean.
Emulates a pattern from a sibling project's npm-audit gate, adapted for this repo's ecosystems since the underlying tool (audit-ci) is npm-only: kept the Dependabot Alerts API as the data source (consistent, GitHub- computed severity across ecosystems — raw RUSTSEC advisories often carry no CVSS score at all, so cargo-audit/pip-audit output alone can't reliably answer "is this critical/high") and added the missing piece, a reviewed allowlist with mandatory expiry, keyed by the alert's own stable number rather than package name so a distinct new advisory against an already-listed package is never silently covered by an old entry. - .security-allowlist.jsonc: starts empty — both real shipped-code findings (quinn-proto, openssl, previous commit) are fixed outright, not allowlisted. - scripts/check_security_allowlist.py: rejects any entry missing a real expiry, with a placeholder/too-short "notes", or expired-but-still-active. Tested directly against both a clean and a deliberately-bad allowlist. - scripts/security_audit_gate.py: filters open critical/high alerts by scope (shipped vs examples/), cross-references shipped-scope findings against the allowlist, exits 1 on anything uncovered. Verified against the real, current alert data for this repo. - security-audit.yml: wires both scripts in; behavior (shipped hard-fails, examples/ reports only) is unchanged, the allowlist is additive.
…-fix lag security-audit.yml already re-runs on every push to main, not just the daily cron, so a normal fix was never really waiting a full day. What remained was GitHub's own alert-index re-scan lag after a merge, which can briefly leave a just-fixed alert reporting as still open. No API exists to force that re-scan faster, so this retries the gate itself (4 attempts, 15s apart) before actually failing — shrinks the visible window from "potentially until the next scheduled run" to "under a minute," without masking a real, persistent finding (still fails loudly after all retries are exhausted).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
nanny initwrites a permanent.nanny/app.json(app_id+name), once, ever, per app. Meant to be committed. Never regenerated.nanny run --join=<appId>replaces blind auto-detection for joining a governance server.--app=<id>extendsnanny status/nanny stopto target one app explicitly.~/.nanny/servers/<appId>/...) replaces the old global, unkeyed~/.nanny/server.*files, fixing a real collision between unrelated apps'--serveinstances on one machine.[runtime]/moderemoved fromnanny.tomlentirely. Sync now turns on purely from local credential presence (nanny auth login+ a self-minted, app-scoped key). No config field, no way for it to disagree with reality.nanny runon any logged-in machine, independent of the app's one-timenanny init.proxy_tokenfor the CONNECT tunnel (never the session token), a real axum/hyper upgrade-handling bug fix, a single dispatch checkpoint (GovernorService) so CONNECT can't bypass a router-only check, constant-time token comparison.HTTPS_PROXY/HTTP_PROXYare now actually auto-injected when the joined server has[proxy]configured (previously silent no-op unless set by hand), and a denied proxy request now actually stops the run, matching every other denial path.fresh_run()in both SDKs: start a new governed run mid-process with its own independent counter. Replaces the previously-undocumented pattern of settingNANNY_RUN_IDdirectly.instrument()now covers OpenAI's Responses API, not just Chat Completions, so reasoning-model calls that require Responses (tool calls + realreasoning_effort) are measured instead of silently unmeasured.cache_read/cache_writeonLlmUsageRecorded, normalized across OpenAI, Anthropic, DeepSeek, and Gemini's incompatible cache-accounting shapes.nanny.tomlitself, the only file meant to be hand-edited and commented.StepCompletedactually fires now (was silently dead sincePOST /step, the only caller, had none), and proxied CONNECT calls now count as steps too, closing the last of threeToolAllowedsites that didn't.nanny/doc sweep for all of the above.Full detail in CHANGELOG.md.
Test plan
cargo build --workspacecargo test --workspacecargo clippy --workspace --all-targets--join, real DeepSeek + Tavily calls through the CONNECT proxy (GoTM deployment)--joinsurvives aNannyStopwithout killing the governor, governor keeps serving after