feat(sdk/python): @rule, @agent, exception mapping + bridge protocol fixes - #23
Merged
Conversation
Adds sdks/python/ with the full package structure for nanny-sdk: - pyproject.toml: hatchling build, Apache-2.0, requires-python >=3.10, httpx as the only runtime dep; dev group: pytest, pytest-httpserver, pytest-asyncio, ruff, mypy - nanny_sdk/__init__.py: public API surface (__all__ defined) - nanny_sdk/exceptions.py: all StopReason variants as typed Python exceptions matching Rust names exactly (NannyStop base, ToolDenied, RuleDenied carry detail fields) - nanny_sdk/_context.py: PolicyContext @DataClass mirroring Rust struct field-for-field, with from_dict() constructor - nanny_sdk/_client.py: lazy env-var bridge client (reads NANNY_BRIDGE_PORT at call time so monkeypatch works in tests); call_tool() raises NannyStop subclass on deny, returns None on allow - nanny_sdk/_decorators.py: @tool fully wired — contacts bridge before each call, stringifies args by parameter name, raises on deny, never calls function body; @rule and @agent are Day 3/4 stubs - nanny_sdk/py.typed: PEP 561 marker - tests/conftest.py: mock_bridge fixture (pytest-httpserver fake bridge) - tests/test_client.py: 4 connectivity smoke tests - tests/test_tool.py: 13 @tool tests — routing, payload, all deny reasons, function body not called on deny, passthrough, async Also: - .gitignore: add Python build/cache patterns (.venv, __pycache__, .mypy_cache, .ruff_cache, .pytest_cache, *.egg-info, dist); note uv.lock is intentionally committed - examples/rust/qabud/.env.example: add API_KEY sentinel with instructions to cp .env.example .env before recording demos 17 tests passing. ruff clean. mypy strict clean.
…e protocol fixes - @rule: client-side evaluation via _RULES ordered dict; RuleDenied raised before bridge is ever contacted; _reset_rules autouse fixture added to conftest - @agent: /agent/enter + /agent/exit with try/finally; AgentNotFound on 404; sync and async both supported - test_rule.py, test_agent.py, test_exceptions.py: 51 tests total, all passing Bridge protocol corrections (found via Rust bridge source audit): - Auth header: X-Nanny-Token → X-Nanny-Session-Token - /health response: {"status":"ok"} → {"state":"running"} - /tool/call deny fields: "detail" → "tool_name" / "rule_name" (separate fields) - /agent/enter: 404 → AgentNotFound, not {"status":"denied"} - /agent/exit: request body {} not {"name": name} Transport fix (Unix socket + TCP): - SDK previously only read NANNY_BRIDGE_PORT — silently broken on macOS/Linux where CLI injects NANNY_BRIDGE_SOCKET (Unix domain socket) instead - _make_client() now routes to HTTPTransport(uds=...) on Unix, TCP on Windows - is_passthrough() checks both NANNY_BRIDGE_SOCKET and NANNY_BRIDGE_PORT
…line Four-agent CrewAI pipeline governed by Nanny. Demonstrates multi-agent role-based limits, tool allowlist enforcement per role, and three stop reasons (BudgetExhausted, MaxStepsReached, RuleDenied) all verified under `nanny run`. Covers the four agentic patterns from the v0.1.4 plan: tool-calling, multi-agent/role-based limits, Plan-and-Execute (CrewAI sequential process), and partial event-driven (task context chaining). CrewAI 1.14 compatibility: patches BaseTool.run and Tool.run at import time to re-raise NannyStop as _NannySignal(BaseException), bypassing CrewAI's executor `except Exception` handlers that would otherwise swallow stop signals silently. Also fixes nanny_sdk._client.agent_exit to silently ignore ConnectError when the bridge closes the connection after issuing a stop — prevents the cleanup call from masking the real stop reason in both dev_assist and metrics_crew.
Consistent with metrics_crew. Excludes __pycache__, .venv, dist/, and other Python/uv build artefacts from version control.
Consistent naming convention: ci-<language>.yml makes it immediately
clear which pipeline tests which SDK. Scales cleanly when Node is added.
ci-rust.yml: renamed from ci.yml, name field updated to "CI — Rust"
ci-python.yml: new — pytest + ruff + mypy on macOS + Linux,
Python 3.10 and 3.12, triggers on sdks/python/** changes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…11+3.13 3.10 EOL is October 2026 — 6 months after launch. LangChain and CrewAI both target 3.11+. 3.11 is 10-60% faster than 3.10. - requires-python: >=3.10 → >=3.11 - ruff target-version: py310 → py311 - mypy python_version: 3.10 → 3.11 - ci-python.yml matrix: ["3.10", "3.12"] → ["3.11", "3.13"] Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds PyPI publish as a fifth job in the existing release pipeline, following the same pattern as homebrew-tap-publish and publish-crates. - Triggers on v* tag push after release succeeds; skips rc tags - workflow_dispatch re-run support: same version input as other publish jobs — safe to re-run all publish jobs simultaneously (PyPI rejects duplicate versions gracefully) - Uses PyPI Trusted Publishing (OIDC) — no API token in secrets - Verifies pyproject.toml version matches the tag before building - uv build produces sdist + wheel; pure-Python, single wheel covers all platforms Pre-requisite before first publish: configure Trusted Publishing on pypi.org — project: nanny-sdk, repo: nanny-run/nanny, workflow: release.yml, environment: pypi Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Switch to minor-level versioning: v0.1.x covers all v0.1 patches - Rename v0.1.4/ → v0.1/, drop v0.1.3/ (superseded) - Remove old unversioned root-level doc pages - Update all internal links from /v0.1.4/ to /v0.1/ - Add redirects for /quickstart, /v0.1.3/:slug*, /v0.1.4/:slug* - Collapse CLI and Guides tabs into Getting Started as groups - Reorder groups: Concepts → SDKs → CLI → Reference - Fix broken links in v0.1.3 that were missing version prefix - Remove patch-specific version mentions from prose - Document versioning policy (max 4 versions) in AGENTS.md
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
Python SDK (
sdks/python/nanny-sdk)@tool(cost=N): sync + async wrappers,/tool/callPOST, deny → typed exception, passthrough mode@rule("name"): client-side evaluation via_RULESordered dict;RuleDeniedraised before bridge is contacted;_reset_rulesautouse fixture keeps tests isolated@agent("name"):/agent/enter+/agent/exitwithtry/finally;AgentNotFoundon 404; sync and async both supportedagent_exitfix: silently ignoresConnectErrorwhen bridge closes connection after issuing a stop — prevents cleanup call from masking the real stop reasonStopReasonvariants map to typed exceptions (BudgetExhausted,MaxStepsReached,RuleDenied,Timeout)NANNY_BRIDGE_SOCKET/NANNY_BRIDGE_PORTare unset — decorators are no-opstest_tool.py,test_rule.py,test_agent.py,test_exceptions.py,test_passthrough.py>=3.11— 3.10 EOL is October 2026; rufftarget-versionand mypypython_versionupdated to matchBridge protocol corrections
Found via Rust bridge source audit — the original implementation had drifted from the actual bridge:
X-Nanny-Token→X-Nanny-Session-Token/healthresponse:{"status":"ok"}→{"state":"running"}/tool/calldeny fields: generic"detail"→"tool_name"/"rule_name"as separate fields/agent/enter: 404 →AgentNotFound, not{"status":"denied"}/agent/exit: request body{}not{"name": name}Transport fix
The SDK previously only read
NANNY_BRIDGE_PORT— silently broken on macOS/Linux where the CLI injectsNANNY_BRIDGE_SOCKET(Unix domain socket) instead.NANNY_BRIDGE_PORTis Windows-only._make_client()routes toHTTPTransport(uds=...)on Unix, plain TCP on Windowsis_passthrough()checks both env varsdev_assist— LangChain example app (examples/python/dev_assist)file_reader,ripgrep,write_file— each decorated with@nanny_tool@rule("no_read_loop"): fires if last 5 tool calls are allfile_reader@nanny_agent("debugger"): activates[limits.debugger]scope for the runuv run dev debug --trace <file> [--mode react|plan]nanny.tomldemo limits:demo-budget,demo-steps,demo-rule— all three stop reasons verified undernanny runmetrics_crew— CrewAI example app (examples/python/metrics_crew)ToolDeniedvalidate_schema,load_metrics,compute_stats,detect_anomalies,correlate_signals,generate_chart,write_report— each decorated with@nanny_tool@rule("no_analysis_loop"): fires if last 5 tool calls are allcompute_stats@nanny_agent("analysis"): activates[limits.analysis]scope for the runuv run metrics-crew analyze --data <csv>nanny.tomldemo limits:demo-budget,demo-steps,demo-rule— all three stop reasons verified undernanny runBaseTool.runandTool.runat import time to re-raiseNannyStopas_NannySignal(BaseException), bypassing CrewAI's executorexcept Exceptionhandlers that would otherwise swallow stop signalsCI
ci.ymlrenamed toci-rust.yml(name: CI — Rust)ci-python.ymladded (name: CI — Python SDK) — pytest + ruff + mypy, macOS + Linux, Python 3.11 + 3.13, triggers onsdks/python/**changesTest plan
uv run pytest -q— 66 tests passinguv run mypy nanny_sdk— zero errorsuv run ruff check .— zero warningscd examples/python/dev_assist && nanny run --limits=demo-budget—BudgetExhaustedpanel, NDJSONExecutionStoppedon stdoutcd examples/python/metrics_crew && nanny run --limits=demo-budget—BudgetExhaustedpanel, NDJSONExecutionStoppedon stdoutcd examples/python/metrics_crew && nanny run --limits=demo-steps—MaxStepsReachedpanelcd examples/python/metrics_crew && nanny run --limits=demo-rule—RuleDeniedpanel