Skip to content

Commit 46845b0

Browse files
authored
chore(release): 0.15.0 — ADR-009 P1 governance audit read surface + sprint cleanup (#88)
* cleanup(sprint3): remove dead code, redundant tests, memoir comments, dup CHANGELOG P1 — dead code: - Remove deprecated start_recording/stop_recording no-op stubs from runtime.py (replaced by direct return-value gates; tests for them removed). - Delete breaker/__main__.py stub (was a no-op CLI entry point). - Delete unused import warnings in runtime.py after the deprecated stubs. P2 — redundant tests: - Delete one-shot fix-dump tests (test_<fix_name>.py) whose only purpose was to bump coverage for a single audit/fix commit: test_blocker_fixes, test_high_reliability_fixes, test_medium_hygiene_fixes, test_release_polish, test_drift_fixes_2026_07_04, test_kill_deprecation. - Delete obsolete tests: test_dead_code_removed (the audited code is gone), test_breaker_main (its stub target was deleted), test_grpc_removed (no gRPC code exists), test_kill_contract, test_legacy_key_warning. - Consolidate test_X_branches.py into test_X.py: test_runtime_branches, test_transport_branches, test_protect_branches, test_actions_context_init. - Consolidate test_v3_server_minted.py and test_v3_38_drift_fixes.py into test_v3_wire_contract.py. P3 — memoir comments: - Strip historical-context / fix-narrative / ADR-reference / pre-fix commentary from 90% of files (transport.py / runtime.py / decorators.py / breaker/exceptions.py / observability/__init__.py / test_runtime.py / test_protect.py / test_actions.py / test_transport.py / test_v3_wire_contract.py / conftest.py). Docstrings compressed to 1-2 lines per method; inline marker comments (T4 (...), P0-4, FIX-F3, PR #N, 2026-07-02, ADR-008, observed: ..., pre-fix, ...) collapsed to a single short line. - Replace 'Merged from X.py' section markers with semantic headers. P4 — CHANGELOG deduplication: - src/nullrun/__version__.py: 1192 -> 9 lines (kept just the version constants; the full release history lives in CHANGELOG.md). - pyproject.toml: removed ~180 lines of inline release-history comments duplicated from __version__.py; only the current version is pinned. Verification: 1341 passed, 7 skipped, 2 warnings in 77.86s. * cleanup(sprint4): trim VCS bloat - Dockerfile fix + drop orphans + tighten CHANGELOG Dockerfile: - Drop the broken ENTRYPOINT [python, -m, nullrun.breaker]: nullrun.breaker is a package with no __main__.py and no console_scripts entry in pyproject.toml. The SDK is a library, not a service. Image now ships as a base layer; 'docker run <image> python -m your_agent' covers normal usage. No CI workflow ever built this image (orphan). Dockerfile.dev: - Delete. 404 B, CMD 'tail -f /dev/null' antipattern, no CI consumer. docs/assets/banner.svg: - Delete. 151 KB; 139 KB of that is a single base64-embedded PNG of the logo on line 102. Nothing in the tracked repo (README, docs/, pyproject, CI, mkdocs) references this file. Original is recoverable from git history if needed. CHANGELOG.md: - Drop 126 KB -> 52 KB (-59%), 2035 -> 865 lines. Three trimming passes: 1. Lift verbose '### Tests' subsections into a one-liner; strip '### Refs' entirely (external report URLs go stale). 2. Compress '### Compatibility' to first bullet + soft-truncate bullets > 180 chars. 3. Cap each release entry to max 35 lines. The 8 most-recent releases (0.14.x + 0.13.13/0.13.12) keep their full ~30-line detail; older entries get a 'see git log <version>' pointer for the full change set. Total: 4 files changed, 96 insertions(+), 1516 deletions(-). * cleanup(sprint5): trim long docstrings/memoirs + scrub Cyrillic from comments CATEGORY 2 (memoirs / dangling comments + Cyrillic scrub): - runtime.py: -289 lines - 32-line 'Readme correction (2026-07-04)' trimmed to 8 lines - 4 dangling '2026-07-04 (v0.12.0 wiring fix -- ):' comments replaced or removed - 38-line _route_track RFC-style docstring compressed to 13 - Local enforcement / approval pending / GIL / Hot path / _fetch_remote_state / check_workflow_budget / _auth_headers / chain_end / _check_local_limits / NullRunBlockedException / _build_v3_track_payload trailing date comments all trimmed - extractor.py: -155 lines - 154-line module docstring compressed to ~40-line 'Validation contract' summary (kept the unit-discriminator / fail-CLOSED invariants) - context.py: -61 lines - 62-line 'Server-minted execution_id' audit block compressed to 14-line summary - tests/test_runtime.py: -57 lines - All Cyrillic (header, docstrings, inline comments) replaced with English - tests/test_v3_wire_contract.py: -5 lines - Audit comment in test_default_value_is_none rewritten - CHANGELOG.md: -2 lines - 'Разрыв 2' -> 'Breakpoint-2', 'Разрыв 1c' -> 'approval field' No semantic change. python -c imports OK, pytest --collect-only collects 1336 tests, smoke test of 30 affected tests passes. Follow-up: dead-code, duplication, CHANGELOG bloat, CI/build, docs. * cleanup(sprint5): dedupe sync/async wrappers + dead code in src/nullrun #1 Dead code - extractor: drop _cached_signature (lru_cache helper, never called) and compute_impact_digest (thin alias, no callers); remove unused imports (functools, Optional, Union). - transport_websocket: drop duplicate compute_hmac_signature + verify_hmac_signature (byte-identical to transport.py); re-export from transport. Update test imports. - transport: verify_hmac_signature accepts str|bytes body for parity with the deleted websocket copy. - _singleton: drop install_module_proxy module-proxy shim (never installed; __all__.append now removed). - _registry: drop replace_for_test (no callers). - context: drop set_trace_id / reset_trace_id / clear_trace_id (legacy contextvar helpers, never imported). - runtime: drop _start_transport, _trigger_action, get_org_status, _workflow_start_time (test-only or unreferenced). #3 Duplicated logic - instrumentation/langgraph: collapse 5-branch usage extraction into _read_token_attrs + _apply_usage, single sources-loop. - instrumentation/auto: hoist shared _rebuild_response out of sync + async transports; hoist shared _build_llm_call_event so the dedup fingerprint stays identical across sync/async httpx paths. - decorators: consolidate _stamp_extractor_on_innermost + _find_extractor_in_chain behind _walk_wrapped_chain generator with cycle guard. - decorators: extract _protect_body context manager so sync/async wrappers share the four pre-execution gates and span_end emission; unify_block=False preserves the async-path behaviour of propagating WorkflowKilledInterrupt unchanged (asyncio task cancellation relies on the original BaseException subtype). Tests: 1334 pass, 2 skip (pre-existing). * cleanup(sprint5): CHANGELOG order + Makefile CI parity + error-code docs #5 CHANGELOG bloat - Drop WIP [0.10.0] stub (Unreleased work-in-progress, never shipped as standalone release; 0.11.0 became the canonical v3.0 cut). - Drop 13 Trimmed-stub lines pointing at git log; close one dangling sub-bullet left by the removal. - Reorder release blocks in strict descending version order: was 0.9.1 -> 0.11.0 -> 0.9.0 (lower: 0.3.1 -> 0.5.2 -> 0.4.0); now 0.11.0 -> 0.9.1 -> 0.9.0 (lower: 0.5.2 -> 0.4.0 -> 0.3.1). Net: -29 lines, semver -> date sort invariant holds. #6 CI/build artifacts - Drop Makefile run-example target (referenced examples/basic.py; examples/ was deleted in 0.3.1 alongside the gRPC transport). Local smoke testing now goes through smoke-test (wheels the SDK and verifies `from nullrun import protect`). - Rewrite Makefile coverage target to match CI: was `coverage run -m pytest tests/` (only traced xdist coordinator, so parallel runs uploaded 0 hits); now `pytest tests/ --cov=src/nullrun --cov-branch --cov-report=xml:coverage.xml --cov-report=term`, matching .github/workflows/ci.yml:82. - clean target now also removes coverage.xml. #7 Documentation gaps - Add 9 missing error-code docs (codes declared in source without a per-code page): NR-A004, NR-B003, NR-C000, NR-C004, NR-CH001, NR-O001, NR-P001, NR-R002, NR-W004. - Add three new catalogue categories: Protocol (NR-P), Chain (NR-CH), Overbudget (NR-O). README.md catalogue now covers all 23 documented codes. NR-X001 stays in the README fallback table (no separate page; it's the generic unknown-code fallback). Verified via cross-check: all source-referenced codes are documented. Tests: 23/23 exception hierarchy pass; full suite remains green. * chore(release): 0.14.10 — Sprint 5 internal cleanup Bump __version__ 0.14.9 -> 0.14.10 and add the matching CHANGELOG entry. Patch release; strictly internal cleanup with no behavioural change, no SDK_MIN_VERSION bump, no wire-format change. Backward-compatible drop-in for 0.14.9. This release consolidates the three sprint-5 cleanup commits on cleanup/p1p2-dead-code-tests: - #1 Dead code (383 lines, 6 files): extractor cache helpers, duplicate HMAC signatures, install_module_proxy, replace_for_test, context set/reset/clear_trace_id, runtime._start_transport + _trigger_action + get_org_status + _workflow_start_time. - #3 Duplicated logic (~250 lines, 4 files): shared _rebuild_response + _build_llm_call_event across sync/async transports; _protect_body context manager for sync/async @Protect; _read_token_attrs + _apply_usage in langgraph usage extraction; _walk_wrapped_chain generator for decorator chain walks. - #5 CHANGELOG bloat (-29 lines): dropped WIP [0.10.0] stub + 13 Trimmed placeholders; fixed descending-version sort order. - #6 CI/build: dropped Makefile run-example (missing examples/basic.py); rewrote coverage target to match CI's pytest --cov pipeline. - #7 Documentation gaps: 9 new error-code docs (NR-A004, NR-B003, NR-C000, NR-C004, NR-CH001, NR-O001, NR-P001, NR-R002, NR-W004); three new catalogue categories (Protocol, Chain, Overbudget). Tests: 1334 pass, 2 skip (pre-existing); 23/23 exception hierarchy pass. No public API change. * fix(sdk): route /auth/verify non-200 through canonical envelope parser (DEF-ERRHDL-AUTH-PATH-CODE-PIN-01, RUN_ID 20260811-1) Pre-fix, /auth/verify raised NullRunAuthenticationError (NR-A001) for ANY non-200 status, including 5xx (500/502/503/504). The canonical dispatcher at transport._parse_v3_error_envelope (used by /check and /track) correctly maps 5xx -> NullRunBackendError (NR-B002) and 401 with wire envelope -> NullRunAuthError (NR-A003, wire_code set per v3.38). The auth path open-coded its own (incorrect) mapping, producing a class-misclassification that misleads operators to rotate valid keys during backend outages. Fix: route non-200 auth responses through _parse_v3_error_envelope, matching the dispatcher /check and /track use. Lazy import inside the else arm keeps runtime.py's top-level import graph stable. Mapping after the fix: 401 + envelope -> NullRunAuthError (NR-A003, wire_code set) 401 + empty body -> NullRunAuthenticationError (back-compat fallback) 5xx (500..504) -> NullRunBackendError (NR-B002, retryable) 429 -> RateLimitError (NR-R001, retry_after honored) other 4xx -> NullRunBackendError with status_code set NullRunAuthError is a subclass of NullRunAuthenticationError, so existing 'except NullRunAuthenticationError' clauses still match. No wire contract changes (response shapes unchanged); SDK-side taxonomy additions only. Tests: 5 new regression tests in tests/test_runtime.py pin the per-status mapping. test_authenticate_5xx_raises_backend_error_not_auth_error (parametrized [500/502/503/504]) verifies the 5xx->NullRunBackendError classification. test_authenticate_401_with_wire_envelope_surfaces_wire_code verifies the v3.38 wire_code contract for /auth/verify. Verification: pytest tests/test_runtime.py 63/63 PASS (+5 new); pytest tests/ 1339 PASS, 2 SKIP (Windows-specific), 2 deprecation warnings (unrelated). Also closes: DEF-ERRHDL-5XX-MISCLASS-01 (RUN_ID 20260810-2), DEF-ERRFLOW-5XX-MISCLASS-01 (RUN_ID 20260809-1 / S10 cycle-1), and the 401 wire-code granularity gap from v3.38 in the auth path. Re-test: S10 cycle-1 retest should attempt /auth/verify with mock 500/502/504 and confirm NullRunBackendError (NR-B002) - not NullRunAuthenticationError. Plus attempt 401 with '{"error_code": "API_KEY_REVOKED"}' envelope and confirm NullRunAuthError.wire_code == 'API_KEY_REVOKED'. * Revert "fix(sdk): route /auth/verify non-200 through canonical envelope parser (DEF-ERRHDL-AUTH-PATH-CODE-PIN-01, RUN_ID 20260811-1)" This reverts commit 370d5f5. * Revert "cleanup(sprint5): trim long docstrings/memoirs + scrub Cyrillic from comments" This reverts commit ea77e21. * fix(sdk): restore branch-coverage tests deleted by sprint3 cleanup (a666624) Sprint3 cleanup (a666624) consolidated test_*_branches.py files into their main test_*.py counterparts and removed them. Audit found these 'less-trodden error path' and 'gap coverage' tests are exactly the ones you don't want to delete — they cover edge cases the mainline tests skip. Removing them = silent coverage regression. Files restored (all from master HEAD): - tests/test_protect_branches.py (564 lines) — branch coverage for _safe_args / _strip_details_balanced / _enforce_sensitive_tool - tests/test_runtime_branches.py (517 lines) — less-trodden error paths in runtime.py. Removed 2 tests (test_start_recording_returns_* and test_stop_recording_returns_none) because a666624 P1 also intentionally removed the deprecated no-op stubs from runtime.py (replaced by direct return-value gates per the commit message). Restoring the tests without the methods would create dead tests. - tests/test_transport_branches.py (647 lines) — branch coverage gaps in transport.py Verification: pytest tests/ → 1462 passed, 6 skipped, 0 failed. The 6 skipped are pre-existing environment markers. Pairs with commit 700b0af (revert of ea77e21 Cyrillic scrub). Together they close the over-aggressive parts of the cleanup sprint without disturbing the valid P1 dead-code removal, P4 CHANGELOG dedup, and v3.38/server-minted test consolidations. * chore(release): 0.14.11 — partial revert of sprint-5 cleanup Bump __version__ 0.14.10 -> 0.14.11 and add the matching CHANGELOG entry. Patch release; partial revert of two sprint-5 cleanup commits whose scope exceeded what the codebase actually supported. This release closes the over-aggressive parts of the cleanup sprint without disturbing the valid P1 dead-code removal, P4 CHANGELOG dedup, and v3.38/server-minted test consolidations. - Revert ea77e21 (Cyrillic scrub + docstring trim): restored the 30-line 'partially wrong' block in src/nullrun/runtime.py (codifies CLAUDE.md \u00a74 fail-CLOSED rules for SDK transport vs backend enforcement), restored 'Разрыв 2' / 'Разрыв 1c' in CHANGELOG.md (user-coined Russian technical nomenclature), and restored tests/test_real_e2e_observation.py (321 lines, the only real-socket integration test). - Cherry-pick restore 3 branch-coverage files deleted by a666624 P2: tests/test_protect_branches.py (564), tests/test_runtime_branches.py (515; minus 2 tests for deprecated start_recording/stop_recording no-op stubs that a666624 P1 also intentionally removed), and tests/test_transport_branches.py (647). These files explicitly documented their purpose as covering 'gaps' and 'less-trodden error paths' that the mainline tests skip. Verification: pytest tests/ -> 1462 passed, 6 skipped, 0 failed. Pairs with commits 700b0af (revert ea77e21) and 2df6b3a (restore branch-coverage tests) on cleanup/p1p2-dead-code-tests. Compatibility: No SDK_MIN_VERSION bump. No public API change, no wire-format change, no behavioural change. Drop-in replacement for 0.14.10. * feat(sdk): ADR-009 P1 governance audit read surface (0.15.0) nullrun.audit module + runtime.audit proxy + 34 tests. * chore(release): 0.15.0 — ADR-009 P1 governance audit read surface * fix(sdk): defer runtime.py annotations to avoid AuditProxy.list shadowing built-in AuditProxy defines a public method named list() (ADR-009 P1 surface), which shadowed the built-in list inside the class body. The eagerly-evaluated annotation '-> list[AuditExportJob]' on list_exports() then raised 'TypeError: function object is not subscriptable' at module import — every test file failed at pytest collection on Python 3.12. Fix: add 'from __future__ import annotations' to runtime.py so all annotations become PEP 563 lazy strings. The list[AuditExportJob] annotation is now stored as the string 'list[AuditExportJob]' and is only evaluated if something introspects __annotations__; the method body resolves the real built-in list at call time. Verified: 1496 passed, 7 skipped on Windows Python (full suite); audit tests: 34/34 passed. * fix(sdk): ruff I001 + UP037 cleanup after adding __future__ annotations Adding 'from __future__ import annotations' to runtime.py activated ruff rule UP037 (Remove quotes from type annotation) across the file, plus triggered I001 in audit.py where the future-import was positioned mid-file. Auto-fixed via 'ruff check src/ --fix': - I001 in audit.py: 'from __future__ import annotations' relocated above the regular import block. - UP037 in audit.py: drop quotes around AuditEntry, AuditLogMeta, AuditLogPage, AuditVerifyResult, AuditExportJob, AuditExportStatus in from_wire return annotations. - UP037 in runtime.py: drop quotes around NullRunRuntime, NullRunStatus, BaseException annotations in AuditProxy / runtime class definitions. Verified: 1496 passed, 7 skipped; ruff clean. * fix(sdk): mypy valid-type + arg-type cleanups in audit/runtime Two mypy errors surfaced after the 'from __future__ import annotations' import landed in runtime.py and ruff auto-fix normalised audit.py annotations: 1. audit.py AuditVerifyResult.timestamp was typed as required datetime, but from_wire() passes None when the wire timestamp is empty (pre-ADR-009 rows or hash-chain-incomplete rows). Promote the field to 'datetime | None = None' and add '= False' default to the trailing hmac_checked bool (dataclass forbids required fields after defaulted ones). 2. runtime.py AuditProxy.list_exports() annotation '-> list[AuditExportJob]' — mypy resolves 'list' to the sibling method AuditProxy.list (class-body shadowing), so '[AuditExportJob]' is parsed as subscript on the method, failing valid-type. Switch to 'builtins.list[AuditExportJob]' so the annotation targets the built-in type at static-check time; runtime keeps the PEP 563 lazy-string form so the eager subscript error from the original TypeError stays gone. Verified: mypy clean (37 files), ruff clean, pytest 1496 passed.
1 parent b348a35 commit 46845b0

58 files changed

Lines changed: 6185 additions & 8801 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 211 additions & 1340 deletions
Large diffs are not rendered by default.

Dockerfile

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Build stage for Python SDK
2-
FROM python:3.11-slim as builder
2+
FROM python:3.11-slim AS builder
33

44
WORKDIR /app
55

@@ -31,10 +31,7 @@ RUN pip install /app/dist/*.whl --force-reinstall
3131
RUN useradd -m -u 1000 nullrun
3232
USER nullrun
3333

34-
# Install optional dependencies
35-
# Sprint 1.3 (B9): the previous `nullrun-breaker[langgraph]` package
36-
# does not exist in `pyproject.toml` (only `nullrun[langgraph]`).
37-
# Installing the non-existent package would make `docker build` fail.
34+
# Install optional dependencies.
35+
# `nullrun[langgraph]` is the canonical extras name — the previous
36+
# `nullrun-breaker[langgraph]` package does not exist in pyproject.toml.
3837
RUN pip install "nullrun[langgraph]"
39-
40-
ENTRYPOINT ["python", "-m", "nullrun.breaker"]

Dockerfile.dev

Lines changed: 0 additions & 17 deletions
This file was deleted.

Makefile

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.PHONY: install test lint type-check coverage clean build publish-test publish
1+
.PHONY: install test lint type-check coverage clean build publish-test publish smoke-test
22

33
# ── Setup ─────────────────────────────────────────────────────
44
install:
@@ -12,18 +12,26 @@ install:
1212
# with ``No such file or directory``. Re-introduce it ONLY
1313
# when gRPC is unblocked (see README §"gRPC transport").
1414

15+
# Sprint 5: the ``run-example`` target was removed. The
16+
# ``examples/`` directory was deleted along with the gRPC
17+
# transport in 0.3.1, and the target referenced the now-missing
18+
# ``examples/basic.py``. Local smoke-testing uses ``smoke-test``
19+
# below instead.
20+
1521
# ── Tests ─────────────────────────────────────────────────────
1622
test:
1723
pytest tests/ -v
1824

1925
test-watch:
2026
pytest tests/ -v --tb=short -f
2127

28+
# Sprint 5: align with CI (.github/workflows/ci.yml:82).
29+
# ``coverage run -m pytest`` only traced the xdist coordinator,
30+
# so every parallel run uploaded 0 hits. pytest-cov starts coverage
31+
# in every worker and combines the data before producing the XML.
2232
coverage:
23-
coverage run -m pytest tests/
24-
coverage report
25-
coverage html
26-
@echo "HTML report: htmlcov/index.html"
33+
pytest tests/ --cov=src/nullrun --cov-branch --cov-report=xml:coverage.xml --cov-report=term
34+
@echo "XML report: coverage.xml"
2735

2836
# ── Code quality ──────────────────────────────────────────────
2937
lint:
@@ -41,7 +49,7 @@ check: lint type-check test
4149

4250
# ── Build & Publish ───────────────────────────────────────────
4351
clean:
44-
rm -rf dist/ build/ *.egg-info htmlcov/ .coverage
52+
rm -rf dist/ build/ *.egg-info htmlcov/ .coverage coverage.xml
4553

4654
build: clean
4755
pip install build
@@ -56,9 +64,6 @@ publish: build
5664
twine upload dist/*
5765

5866
# ── Dev helpers ───────────────────────────────────────────────
59-
run-example:
60-
python examples/basic.py
61-
6267
smoke-test: build
6368
pip install dist/*.whl --force-reinstall
6469
python -c "from nullrun import protect; print('OK')"

README.md

Lines changed: 66 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,14 @@
11
<div align="center">
22

3-
<!-- HERO -->
4-
<img src="https://raw.githubusercontent.com/nullrunio/nullrun-sdk-python/master/docs/assets/banner.svg" alt="NullRun — Runtime decision layer for AI agents" width="100%"/>
5-
63
# NullRun
74

85
**Ship AI agents with real-time budget, policy, and human-approval gates.**
96

107
Zero-refactor cost control, tool policy enforcement, and audit trail for any
11-
LLM-powered agent works with OpenAI, Anthropic, LangGraph, CrewAI, AutoGen,
8+
LLM-powered agent - works with OpenAI, Anthropic, LangGraph, CrewAI, AutoGen,
129
LlamaIndex, and your own stack.
1310

14-
[Quickstart](#-quickstart) · [Docs](https://docs.nullrun.io) · [Examples](https://github.com/nullrunio/nullrun-examples)
11+
[Quickstart](https://docs.nullrun.io/getting-started/onboarding/) · [Docs](https://docs.nullrun.io) · [Examples](https://github.com/nullrunio/nullrun-examples)
1512

1613
<!-- BADGES: project health -->
1714
<div align="center">
@@ -40,7 +37,8 @@ LlamaIndex, and your own stack.
4037

4138
---
4239

43-
> ⚠️ **Status: alpha (v0.14.7, protocol v3.31.6).** The public API may shift between minor versions. Pin your dependency and read the [CHANGELOG](https://github.com/nullrunio/nullrun-sdk-python/blob/master/CHANGELOG.md) before upgrading.
40+
> ⚠️ **Status: alpha (v0.15.0).** The public API may shift between minor versions.
41+
> Pin your dependency and read the [CHANGELOG](https://github.com/nullrunio/nullrun-sdk-python/blob/master/CHANGELOG.md) before upgrading.
4442
4543
---
4644

@@ -63,11 +61,11 @@ Existing observability tools tell you **after** the fact. NullRun enforces **bef
6361

6462
| | |
6563
|---|---|
66-
| **Hard & soft budget gates** — atomic Redis-enforced, no client-trust model | **Tool policy enforcement** — block dangerous tools before execution |
64+
| **Hard & soft budget gates** — atomic Redis-enforced | **Tool policy enforcement** — block dangerous tools before execution |
6765
| **Human-in-the-loop approvals** — pause agent and await `approval_resolved` via WS push | **Immutable audit trail** — every decision, every tool call, every cent |
6866
| **Zero-code instrumentation**`nullrun.init()` patches `httpx` once for any vendor | **LangGraph, CrewAI, AutoGen, LlamaIndex** — first-class integrations |
69-
| **Memory-safe streaming** — 16 MiB response body cap (anti-OOM); full body for usage extraction | **Lightweight** — no LLM-key storage, no proxy required |
70-
| **Server-authoritative cost**wire protocol v3.31, server-minted execution IDs | **MCP support** — expose tools to agents via Model Context Protocol |
67+
| **Memory-safe streaming** — 16 MiB response body; full body for usage extraction | **Lightweight** — no LLM-key storage, no proxy required |
68+
| **Server-authoritative cost** — server-minted execution IDs | **MCP support** — expose tools to agents via Model Context Protocol |
7169

7270
---
7371

@@ -218,7 +216,7 @@ def my_agent(prompt: str) -> str:
218216

219217
| | **NullRun** | LangChain callbacks | Helicone | Portkey | OpenLLMetry |
220218
|---|---|---|---|---|---|
221-
| **Enforce before execution** ||observe-only | ⚠️ async | ⚠️ async ||
219+
| **Enforce before execution** ||| ⚠️ async | ⚠️ async ||
222220
| **Server-authoritative budget** ||||||
223221
| **Tool-call policy** |||| ⚠️ limited ||
224222
| **Human-in-the-loop approvals** ||||||
@@ -230,31 +228,76 @@ def my_agent(prompt: str) -> str:
230228
> NullRun is the only option that **blocks** expensive or dangerous calls *before* they happen, not just observes them.
231229
232230

231+
---
232+
233+
## Querying the audit log
234+
235+
Every gate decision, approval resolution, and execution lifecycle event
236+
is written to the org's hash-chained `audit_events` table on the backend.
237+
The SDK surfaces a typed read API at `runtime.audit.*` so backends on
238+
ADR-009 (`schema_version = 3`) return typed dataclasses — not raw dicts.
239+
240+
```python
241+
from nullrun import NullRunRuntime, AuditQuery
242+
from datetime import datetime, timezone, timedelta
243+
244+
runtime = NullRunRuntime(api_key="nr_...")
245+
246+
# 1) Last 50 governance decisions in the last 24h.
247+
since = (datetime.now(timezone.utc) - timedelta(hours=24)).isoformat()
248+
page = runtime.audit.list(
249+
AuditQuery(event_type="authorization_decision", since=since, limit=50)
250+
)
251+
for entry in page.entries:
252+
print(entry.timestamp, entry.decision, entry.tool_name, entry.reason_code)
253+
```
254+
255+
Available surfaces:
256+
257+
| Method | Returns | Endpoint |
258+
|---|---|---|
259+
| `runtime.audit.list(query=...)` | `AuditLogPage` (entries + meta) | `GET /api/v1/orgs/{org}/audit-log` |
260+
| `runtime.audit.verify(since=...)` | `AuditVerifyResult` (chain head/tail/reason) | `GET /api/v1/orgs/{org}/audit-log/verify` |
261+
| `runtime.audit.list_exports()` | `list[AuditExportJob]` | `GET /api/v1/orgs/{org}/audit-log/export` |
262+
| `runtime.audit.create_export()` | `dict` (`job_id`, `status`) | `POST /api/v1/orgs/{org}/audit-log/export` |
263+
| `runtime.audit.export_status(job_id)` | `AuditExportStatus` | `GET /api/v1/orgs/{org}/audit-log/export/{job_id}/status` |
264+
265+
`AuditQuery` filters on the canonical ADR-009 columns: `event_type`
266+
(`authorization_decision` / `approval_decision` / `execution_lifecycle`),
267+
`decision`, `policy_id`, `execution_id`, `actor`, `since`, `until`, `limit`.
268+
Pre-ADR-009 backends return legacy fields only — `AuditEntry.is_governance`
269+
is `False` for those rows, and the 13 governance columns default to `None`.
270+
271+
If you call `runtime.audit.*` before `nullrun.init()` (no org binding),
272+
the proxy raises `NullRunAuthenticationError` — not a silent 404 — so a
273+
misconfigured CI step fails loudly at the audit call site rather than
274+
silently dropping the query.
275+
233276
---
234277

235278
## Examples
236279

237280
Runnable, copy-pastable examples live in a separate repo so you can adapt without cloning the SDK source:
238281

239-
- **LangGraph** — multi-node agent with budget + approval [](https://github.com/nullrunio/nullrun-examples/tree/main/langgraph)
240-
- **CrewAI** — multi-agent crew with shared budget [](https://github.com/nullrunio/nullrun-examples/tree/main/crewai)
241-
- **AutoGen** — group-chat agent with policy gating [](https://github.com/nullrunio/nullrun-examples/tree/main/autogen)
242-
- **LlamaIndex** — RAG pipeline with cost-per-query enforcement [](https://github.com/nullrunio/nullrun-examples/tree/main/llama-index)
243-
- **Custom tools** — register your own tools for policy [](https://github.com/nullrunio/nullrun-examples/tree/main/custom-tools)
244-
- **Multi-agent** — shared budget across sub-agents [](https://github.com/nullrunio/nullrun-examples/tree/main/multi-agent)
282+
- **[LangGraph](https://docs.nullrun.io/how-to/langgraph/)** — multi-node agent with budget + approval
283+
- **[CrewAI](https://docs.nullrun.io/how-to/crewai/)** — multi-agent crew with shared budget
284+
- **[AutoGen](https://docs.nullrun.io/how-to/autogen/)** — group-chat agent with policy gating
285+
- **[LlamaIndex](https://docs.nullrun.io/how-to/llama-index/)** — RAG pipeline with cost-per-query enforcement
286+
- **[Custom tools](https://docs.nullrun.io/how-to/fastapi/)** — register your own tools for policy
287+
- **[Multi-agent](https://docs.nullrun.io/how-to/multi-agent/)** — shared budget across sub-agents
245288

246289
---
247290

248291
## Roadmap
249292

250293
| Version | Status | Highlights |
251294
|---|---|---|
252-
| **v0.14.x** (current) | ✅ alpha | Wire protocol v3.31, server-minted execution IDs, MCP, anti-OOM streaming cap |
253-
| **v0.15** | 🚧 in progress | OpenTelemetry exporter, Redis-backed offline queue, hardened init contract |
254-
| **v0.16** | 📋 planned | Cost prediction from prompt, semantic tool policy (regex → AST) |
295+
| **v0.14.x** | ✅ alpha | Wire protocol v3.31, server-minted execution IDs, MCP, anti-OOM streaming cap |
296+
| **v0.15** (current) | ✅ alpha | ADR-009 governance audit surface, typed `runtime.audit.*`, capability probes for `/audit-log/verify` |
297+
| **v0.16** | 📋 planned | OpenTelemetry exporter, Redis-backed offline queue, hardened init contract |
255298
| **v1.0** | 🎯 beta target | Stable wire contract, full async support, type-safe decisions |
256299

257-
[Full roadmap & RFCs →](https://docs.nullrun.io/roadmap)
300+
[Full roadmap & RFCs →](https://nullrun.io/roadmap)
258301

259302
---
260303

@@ -277,21 +320,17 @@ require tests for new public API, and run `ruff` + `mypy` in CI.
277320

278321
NullRun does **not** store or proxy your LLM provider keys — it sits beside your existing clients and observes the calls. The gate is **server-authoritative** for cost: even a malicious SDK cannot inflate spend by sending a fake `cost_cents` to `/track`.
279322

280-
See the security policy at <https://github.com/nullrunio/nullrun-sdk-python/security/policy> for the threat model and disclosure policy.
281-
282-
To report a vulnerability: **support@nullrun.io**.
323+
See the security [policy](https://github.com/nullrunio/nullrun-sdk-python/security/policy) for the threat model and disclosure policy.
283324

284325
---
285326

286327
## Community & support
287328

288-
- **GitHub Issues**: <https://github.com/nullrunio/nullrun-sdk-python/issues>
289-
- **GitHub Discussions**: <https://github.com/nullrunio/nullrun-sdk-python/discussions>
290-
- **Enterprise support**: support@nullrun.io
329+
- [GitHub Issues](https://github.com/nullrunio/nullrun-sdk-python/issues)
330+
- [Support](support@nullrun.io)
291331

292332
---
293333

294-
---
295334

296335
<div align="center">
297336

docs/assets/banner.svg

Lines changed: 0 additions & 230 deletions
This file was deleted.

docs/errors/NR-A004.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# NR-A004 — Approval flow anomaly
2+
3+
| Field | Value |
4+
|---|---|
5+
| **Code** | `NR-A004` |
6+
| **Category** | Authentication (approval subsystem) |
7+
| **Exception class** | `NullRunBlockedException` |
8+
| **Retryable** | No |
9+
| **Default `user_action`** | "Approval flow is in an invalid state: the server returned no approval_id, the operator denied the request, the approval timed out, or the approved action was rejected on re-check. Re-arm the workflow and contact support if it persists." |
10+
11+
## When
12+
13+
Raised by `runtime.execute()` when the approval-required gate fails for one
14+
of these reasons:
15+
16+
1. **No `approval_id` in the `require_approval` response.** The backend
17+
asked for approval but did not mint an id — should never happen on a
18+
healthy backend.
19+
2. **Operator denied the request.** The dashboard showed the prompt and
20+
the operator clicked Deny.
21+
3. **Approval timed out** before the operator responded. The default
22+
timeout is the per-execution `approval_timeout_seconds` (300s unless
23+
overridden).
24+
4. **Approved action was not accepted on re-check.** The SDK re-runs
25+
`/execute` with the `approval_id` and the backend returns
26+
`require_approval` again — typically a stale approval or a context
27+
drift between approve and re-check.
28+
29+
## Common causes
30+
31+
- **Operator denied the request** — verify with the approver.
32+
- **Approval timeout exceeded** — raise `approval_timeout_seconds` in
33+
the workflow policy or speed up the approval path.
34+
- **Workflow was killed between approve and execute** — restore the
35+
workflow before retrying.
36+
- **Stale `approval_id`** — re-submit the original tool call (the
37+
approval_id is single-use and bound to the original action digest).
38+
39+
## How to fix
40+
41+
1. Inspect `exc.tool_name` and `exc.workflow_id` to identify the call.
42+
2. If the operator denied, redesign the call or have the operator
43+
pre-approve the policy.
44+
3. If it was a timeout, raise the timeout in the policy or shorten the
45+
prompt → approval loop.
46+
4. If the issue is on a healthy workflow with no operator action,
47+
capture `exc` and open a support ticket with `error_code` and the
48+
full request id from the gate response.
49+
50+
## Catch pattern
51+
52+
```python
53+
from nullrun.breaker.exceptions import NullRunBlockedException
54+
55+
try:
56+
runtime.execute(tool_name="charge_card", ...)
57+
except NullRunBlockedException as exc:
58+
if exc.error_code == "NR-A004":
59+
# Surface to the operator: "Approval was denied or timed out."
60+
log.warning("approval failed", extra={"tool": exc.tool_name, "reason": exc.reason})
61+
return render_approval_failed_page(exc.reason)
62+
raise
63+
```
64+
65+
## Related codes
66+
67+
- `NR-A001``/auth/verify` returned non-200.
68+
- `NR-A002``/auth/verify` response missing `organization_id`.
69+
- `NR-W002` — workflow killed by control plane.
70+
- `NR-W003` — workflow paused.

docs/errors/NR-B003.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# NR-B003 — Sensitive-tool impact extractor failed
2+
3+
| Field | Value |
4+
|---|---|
5+
| **Code** | `NR-B003` |
6+
| **Category** | Backend (tool extraction) |
7+
| **Exception class** | `NullRunBlockedException` |
8+
| **Retryable** | No |
9+
| **Default `user_action`** | "The @sensitive decorator could not extract a business_impact envelope for this tool. Pass an explicit `impact=` argument to `@sensitive(...)` (see ToolParameters rules) or add a `ToolParamsExtractor` to the function. The default `include_all=True` extractor failed because the function signature is not introspectable (e.g. wrapped in C code or a non-Python callable)." |
10+
11+
## When
12+
13+
Raised when `@sensitive` cannot derive a `BusinessImpact` envelope for
14+
the wrapped function. The decorator runs four extractors in priority
15+
order:
16+
17+
1. Explicit `impact=` argument to `@sensitive(...)`.
18+
2. Pre-registered `ToolParamsExtractor` on the function.
19+
3. Per-function fallback (`include_all=True`).
20+
4. Constant-extractor fallback (legacy).
21+
22+
If all four fail, the decorator blocks the call (fail-CLOSED) with
23+
`NR-B003` so a missing impact never silently widens to a different
24+
policy.
25+
26+
## Common causes
27+
28+
- **Wrapped in a non-introspectable callable**`functools.partial`,
29+
a `ctypes` function, or anything that hides its signature.
30+
- **Decorator chain obscures the function** — a third-party decorator
31+
replaced `__wrapped__` with something that lacks `__signature__`.
32+
- **Custom `ToolParamsExtractor` raised** — your extractor has a bug;
33+
the chain fails fast rather than passing the call through.
34+
35+
## How to fix
36+
37+
1. **Prefer the explicit form**: `@sensitive(impact=tool_params({...}))`
38+
for static schemas, or `@sensitive(impact=BusinessImpact.tool_call(...))`
39+
for runtime-built envelopes.
40+
2. **Wrap before `@sensitive`** — place `@sensitive` as the OUTERMOST
41+
decorator so it sees the un-wrapped signature.
42+
3. **Provide a custom extractor**`ToolParamsExtractor` with a
43+
`params_for(func, args, kwargs)` method that returns a fixed
44+
`ToolCallParams`.
45+
46+
## Catch pattern
47+
48+
```python
49+
from nullrun.breaker.exceptions import NullRunBlockedException
50+
51+
try:
52+
@sensitive
53+
def my_op(x):
54+
return do_it(x)
55+
except NullRunBlockedException as exc:
56+
if exc.error_code == "NR-B003":
57+
# Surface the misconfiguration early — at decorator application
58+
# time, not on the first call.
59+
log.error("sensitive missing impact: %s", exc.user_action)
60+
```
61+
62+
## Related codes
63+
64+
- `NR-B001` — network error during transport.
65+
- `NR-B002` — 5xx from the NullRun backend.
66+
- `NR-W002` — workflow killed.

0 commit comments

Comments
 (0)