nullrun 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nullrun-0.4.0/.dockerignore +6 -0
- nullrun-0.4.0/.gitignore +69 -0
- nullrun-0.4.0/.pre-commit-config.yaml +21 -0
- nullrun-0.4.0/CHANGELOG.md +501 -0
- nullrun-0.4.0/Dockerfile +40 -0
- nullrun-0.4.0/Dockerfile.dev +17 -0
- nullrun-0.4.0/LICENSE +201 -0
- nullrun-0.4.0/Makefile +64 -0
- nullrun-0.4.0/PKG-INFO +194 -0
- nullrun-0.4.0/README.md +116 -0
- nullrun-0.4.0/examples/async_usage.py +36 -0
- nullrun-0.4.0/examples/basic.py +27 -0
- nullrun-0.4.0/examples/basic_observe.py +53 -0
- nullrun-0.4.0/examples/cost_dashboard.py +95 -0
- nullrun-0.4.0/pyproject.toml +203 -0
- nullrun-0.4.0/src/nullrun/__init__.py +282 -0
- nullrun-0.4.0/src/nullrun/__version__.py +4 -0
- nullrun-0.4.0/src/nullrun/actions.py +455 -0
- nullrun-0.4.0/src/nullrun/breaker/__init__.py +27 -0
- nullrun-0.4.0/src/nullrun/breaker/circuit_breaker.py +402 -0
- nullrun-0.4.0/src/nullrun/breaker/exceptions.py +319 -0
- nullrun-0.4.0/src/nullrun/context.py +208 -0
- nullrun-0.4.0/src/nullrun/decorators.py +649 -0
- nullrun-0.4.0/src/nullrun/instrumentation/__init__.py +23 -0
- nullrun-0.4.0/src/nullrun/instrumentation/_safe_patch.py +99 -0
- nullrun-0.4.0/src/nullrun/instrumentation/auto.py +1095 -0
- nullrun-0.4.0/src/nullrun/instrumentation/auto_requests.py +257 -0
- nullrun-0.4.0/src/nullrun/instrumentation/autogen.py +163 -0
- nullrun-0.4.0/src/nullrun/instrumentation/crewai.py +140 -0
- nullrun-0.4.0/src/nullrun/instrumentation/langgraph.py +412 -0
- nullrun-0.4.0/src/nullrun/instrumentation/llama_index.py +110 -0
- nullrun-0.4.0/src/nullrun/observability.py +160 -0
- nullrun-0.4.0/src/nullrun/py.typed +0 -0
- nullrun-0.4.0/src/nullrun/runtime.py +1806 -0
- nullrun-0.4.0/src/nullrun/toolbox/__init__.py +20 -0
- nullrun-0.4.0/src/nullrun/toolbox/langgraph.py +94 -0
- nullrun-0.4.0/src/nullrun/tracing.py +155 -0
- nullrun-0.4.0/src/nullrun/transport.py +1509 -0
- nullrun-0.4.0/src/nullrun/transport_websocket.py +627 -0
nullrun-0.4.0/.gitignore
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
|
|
7
|
+
# Distribution / packaging
|
|
8
|
+
.Python
|
|
9
|
+
build/
|
|
10
|
+
develop-eggs/
|
|
11
|
+
dist/
|
|
12
|
+
downloads/
|
|
13
|
+
eggs/
|
|
14
|
+
.eggs/
|
|
15
|
+
lib/
|
|
16
|
+
lib64/
|
|
17
|
+
parts/
|
|
18
|
+
sdist/
|
|
19
|
+
var/
|
|
20
|
+
wheels/
|
|
21
|
+
share/python-wheels/
|
|
22
|
+
*.egg-info/
|
|
23
|
+
.installed.cfg
|
|
24
|
+
*.egg
|
|
25
|
+
MANIFEST
|
|
26
|
+
|
|
27
|
+
# Virtual environments
|
|
28
|
+
.venv/
|
|
29
|
+
venv/
|
|
30
|
+
env/
|
|
31
|
+
ENV/
|
|
32
|
+
env.bak/
|
|
33
|
+
venv.bak/
|
|
34
|
+
.python-version
|
|
35
|
+
|
|
36
|
+
# Test / coverage / type / lint caches
|
|
37
|
+
.pytest_cache/
|
|
38
|
+
.coverage
|
|
39
|
+
.coverage.*
|
|
40
|
+
htmlcov/
|
|
41
|
+
coverage.xml
|
|
42
|
+
.tox/
|
|
43
|
+
.nox/
|
|
44
|
+
.mypy_cache/
|
|
45
|
+
.ruff_cache/
|
|
46
|
+
.hypothesis/
|
|
47
|
+
|
|
48
|
+
# IDE / editor
|
|
49
|
+
.idea/
|
|
50
|
+
.vscode/
|
|
51
|
+
*.swp
|
|
52
|
+
*.swo
|
|
53
|
+
*~
|
|
54
|
+
.DS_Store
|
|
55
|
+
|
|
56
|
+
# Secrets / local config
|
|
57
|
+
.env
|
|
58
|
+
.env.local
|
|
59
|
+
.env.*.local
|
|
60
|
+
*.pem
|
|
61
|
+
*.key
|
|
62
|
+
|
|
63
|
+
# Claude Code / claude-flow project-local state
|
|
64
|
+
.claude/
|
|
65
|
+
.claude-flow/
|
|
66
|
+
CLAUDE.md
|
|
67
|
+
|
|
68
|
+
# Project-local working notes (kept on disk, not in VCS)
|
|
69
|
+
analyze.md
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
repos:
|
|
2
|
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
3
|
+
rev: v4.5.0
|
|
4
|
+
hooks:
|
|
5
|
+
- id: trailing-whitespace
|
|
6
|
+
- id: end-of-file-fixer
|
|
7
|
+
- id: check-yaml
|
|
8
|
+
- id: check-toml
|
|
9
|
+
|
|
10
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
11
|
+
rev: v0.2.0
|
|
12
|
+
hooks:
|
|
13
|
+
- id: ruff
|
|
14
|
+
args: [--fix]
|
|
15
|
+
- id: ruff-format
|
|
16
|
+
|
|
17
|
+
- repo: https://github.com/pre-commit/mypy
|
|
18
|
+
rev: v1.8.0
|
|
19
|
+
hooks:
|
|
20
|
+
- id: mypy
|
|
21
|
+
additional_dependencies: [types-all]
|
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `nullrun-sdk` will be documented here.
|
|
4
|
+
|
|
5
|
+
Format: [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
|
|
6
|
+
Versioning: [Semantic Versioning](https://semver.org/spec/v2.0.0.html)
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## [0.3.1] — 2026-06-17
|
|
11
|
+
|
|
12
|
+
Production-readiness hardening. No public-API changes; the curated 6-symbol
|
|
13
|
+
surface is unchanged. Aligns the SDK with the contracts in
|
|
14
|
+
`NULLRUN/docs/adr/008-sdk-preflight-fail-policy.md` and
|
|
15
|
+
`NULLRUN/docs/kill-contract.md`.
|
|
16
|
+
|
|
17
|
+
### Fixed (P0 — must-fix)
|
|
18
|
+
|
|
19
|
+
- **gRPC transport code path removed.** `create_grpc_transport` was
|
|
20
|
+
referenced but never defined, so setting `NULLRUN_USE_GRPC=1` raised
|
|
21
|
+
`NameError` at init. The gRPC server at the platform is intentionally
|
|
22
|
+
frozen until the activation checklist (TLS, auth, proto extensions,
|
|
23
|
+
cost pipeline parity, tests) is complete. The SDK now logs an
|
|
24
|
+
INFO line on `NULLRUN_USE_GRPC=1` and silently falls back to
|
|
25
|
+
HTTP. The `grpcio` hard dependency has been dropped from
|
|
26
|
+
`pyproject.toml`. If/when gRPC is unblocked, the SDK will add it back
|
|
27
|
+
as a separate optional extra.
|
|
28
|
+
- **`InsecureTransportError` URL check hardened.** Replaced the
|
|
29
|
+
`startswith("http://127.0.0.1")` chain with a `urllib.parse.urlparse`
|
|
30
|
+
+ `ipaddress.ip_address` check. The previous check let
|
|
31
|
+
`http://127.0.0.1.attacker.com` and `http://localhost.evil.com`
|
|
32
|
+
through (homograph attacks) and rejected `http://[::1]:8080`
|
|
33
|
+
(IPv6 loopback). The new check allows the full `127.0.0.0/8`
|
|
34
|
+
IPv4 loopback range, `::1`, and `localhost` (case-insensitive).
|
|
35
|
+
- **`signal.signal` global hijack removed.** `Transport.__init__` no
|
|
36
|
+
longer installs a process-wide `SIGTERM` / `SIGINT` handler
|
|
37
|
+
that called `sys.exit(0)` from inside the signal context.
|
|
38
|
+
The fix contract was already pinned in `tests/test_signal_safety.py`
|
|
39
|
+
and is now applied to the source.
|
|
40
|
+
- **`atexit.register` replaced with `weakref.finalize`.** The
|
|
41
|
+
per-Transport `atexit` chain was growing without bound in
|
|
42
|
+
long-running deployments; weakref finalizers only fire if the
|
|
43
|
+
transport is still alive at process exit.
|
|
44
|
+
- **`Transport` is now a context manager.** `with Transport(...) as t:`
|
|
45
|
+
starts the flush thread on enter and stops it on exit. Replaces
|
|
46
|
+
the manual `start() / stop()` pair that was easy to forget.
|
|
47
|
+
- **HMAC body byte-equality in the legacy batch path.** The
|
|
48
|
+
pre-fix code signed `body = json.dumps({"events": batch})` and
|
|
49
|
+
then sent the same payload via httpx's `json=...` parameter,
|
|
50
|
+
which re-serialises with compact separators. The signed bytes
|
|
51
|
+
and the wire bytes were not identical. Now the path uses
|
|
52
|
+
`content=body` so the signed bytes are the wire bytes.
|
|
53
|
+
- **All 4 examples fixed.** `basic.py` was calling `init()` with no
|
|
54
|
+
args (raises in 0.3.0). `basic_observe.py` was passing
|
|
55
|
+
`organization_id=` (not in the signature) and calling
|
|
56
|
+
`nullrun.coverage_report()` (did not exist). `cost_dashboard.py`
|
|
57
|
+
was using `Authorization: Bearer` and the non-existent
|
|
58
|
+
`/api/v1/orgs/{org_id}/usage` endpoint. All four now use the
|
|
59
|
+
current SDK surface and the canonical `/api/v1/orgs/{org_id}/status`
|
|
60
|
+
endpoint.
|
|
61
|
+
|
|
62
|
+
### Fixed (P1)
|
|
63
|
+
|
|
64
|
+
- **AsyncTransport dead code deleted.** 626 lines of unused
|
|
65
|
+
async transport that had no call sites. Tests already removed.
|
|
66
|
+
- **TrackResult dead class deleted.** `track()` returns `dict`,
|
|
67
|
+
not `TrackResult`. The class was unreferenced.
|
|
68
|
+
- **Singleton-state lock added.** `init()` now wraps the three
|
|
69
|
+
singleton-slot writes (`NullRunRuntime._instance`,
|
|
70
|
+
`_rt_mod._runtime`, `_dec_mod._runtime`) in a module-level
|
|
71
|
+
`threading.Lock` so concurrent `init()` calls cannot leave
|
|
72
|
+
the slots pointing at two different runtimes.
|
|
73
|
+
- **Legacy API key warning.** Pre-Phase-139 API keys (no
|
|
74
|
+
`workflow_id` from `/auth/verify`) now emit a one-time
|
|
75
|
+
WARNING explaining that remote kill/pause will not be
|
|
76
|
+
honoured. Without the warning, the dashboard KILL button
|
|
77
|
+
silently no-ops for users on legacy keys.
|
|
78
|
+
- **Distributed circuit-breaker race fix.** The pre-fix code
|
|
79
|
+
defined `_publish_half_open_state` but never called it. The
|
|
80
|
+
`state` property now calls it on the `OPEN → HALF_OPEN`
|
|
81
|
+
transition so other workers see the new state in Redis
|
|
82
|
+
instead of falling back to PERMISSIVE.
|
|
83
|
+
|
|
84
|
+
### Removed (dead code)
|
|
85
|
+
|
|
86
|
+
- `AsyncTransport` (626 lines)
|
|
87
|
+
- `TrackResult` (12 lines)
|
|
88
|
+
- `BoundedDict` cost / loop / retry counters
|
|
89
|
+
- `_check_local_limits` (the local budget check that read
|
|
90
|
+
`cost_cents` which the SDK never sets — was dead for the
|
|
91
|
+
public API)
|
|
92
|
+
- `StructuredLogger`, `get_logger`, `TenantFilter`,
|
|
93
|
+
`configure_logging_with_tenant_context`, `timed` from
|
|
94
|
+
`observability.py` (zero call sites)
|
|
95
|
+
- `tenant_context`, `set_tenant_context`, `get_org_id` from
|
|
96
|
+
`context.py` (zero call sites; `get_org_id` was already
|
|
97
|
+
documented as gone in 0.3.0 CHANGELOG)
|
|
98
|
+
- `instrumentation/openai.py` (the v0.x patcher that no
|
|
99
|
+
longer applied to `openai>=1.0`)
|
|
100
|
+
|
|
101
|
+
### Added
|
|
102
|
+
|
|
103
|
+
- `NullRunRuntime.coverage_report()` — public method that
|
|
104
|
+
returns `{"seen": ..., "tracked": ...,
|
|
105
|
+
"streaming_skipped": ...}`. The auto-instrumentation layer
|
|
106
|
+
already populates the counters; this method just exposes
|
|
107
|
+
them. Called by `examples/basic_observe.py`.
|
|
108
|
+
- `Transport.__enter__` / `__exit__` (see above)
|
|
109
|
+
- `tests/test_init_contract.py` — pins the 0.3.0 init
|
|
110
|
+
contract (api_key required, singleton state, no
|
|
111
|
+
organization_id kwarg)
|
|
112
|
+
- `tests/test_insecure_transport.py` — homograph / IPv6 /
|
|
113
|
+
case-insensitive coverage for the new URL check
|
|
114
|
+
- `tests/test_grpc_removed.py` — pins the post-deletion
|
|
115
|
+
gRPC contract
|
|
116
|
+
- `tests/test_legacy_key_warning.py` — pins the legacy
|
|
117
|
+
API key warning
|
|
118
|
+
- `tests/test_cb_halfopen_publish.py` — pins the
|
|
119
|
+
HALF_OPEN Redis publish
|
|
120
|
+
- `tests/test_kill_deprecation.py` — pins the
|
|
121
|
+
`WorkflowKilledInterrupt` deprecation-bypass contract
|
|
122
|
+
|
|
123
|
+
### Documentation
|
|
124
|
+
|
|
125
|
+
- `WorkflowKilledInterrupt` docstring now includes a
|
|
126
|
+
"Catching in production" section with the recommended
|
|
127
|
+
Sentry / OpenTelemetry pattern (`except BaseException`,
|
|
128
|
+
not `except Exception`).
|
|
129
|
+
- `NULLRUN/docs/sdk/README.md` rewritten to match the
|
|
130
|
+
actual 6-symbol SDK surface and current `track_*`
|
|
131
|
+
signatures. The previous 7-symbol reference was a
|
|
132
|
+
description of an older design that did not match the
|
|
133
|
+
shipped SDK.
|
|
134
|
+
|
|
135
|
+
## [Unreleased]
|
|
136
|
+
|
|
137
|
+
### Added (production-readiness hardening)
|
|
138
|
+
|
|
139
|
+
- **HMAC always-on when `secret_key` is present.** The SDK now signs every
|
|
140
|
+
outgoing POST/GET (auth/verify, /track/batch, /gate, /evaluate, /status)
|
|
141
|
+
via the new `Transport._signed_post` / `_signed_request` helpers. The
|
|
142
|
+
outgoing WebSocket ACK is also signed (mirroring incoming-message
|
|
143
|
+
verification). Header set is built once via `_build_signed_headers`
|
|
144
|
+
(Content-Type, X-API-Version, X-API-Key, X-Signature,
|
|
145
|
+
X-Signature-Timestamp, W3C trace context). Previously only
|
|
146
|
+
/track/batch and /gate were signed; auth/verify, /status GET, and
|
|
147
|
+
WS ACKs were not. Compliant with the canonical
|
|
148
|
+
`HMAC-SHA256(secret_key, "<ts>:<api_key>:<sha256_hex(body)>")` formula
|
|
149
|
+
from `backend/src/auth/hmac.rs:6-9`.
|
|
150
|
+
|
|
151
|
+
- **WebSocket protocol compliance (Phase 2 of the plan).** The SDK now
|
|
152
|
+
honours `resync_required` (closes the connection, clears local state,
|
|
153
|
+
reconnects — no merge per ADR-007), enforces per-workflow `version`
|
|
154
|
+
monotonic dedup (drops events with `version <= last` to survive
|
|
155
|
+
at-least-once delivery), and signs outgoing ACKs. The URL uses
|
|
156
|
+
`X-API-Key` header (never the query string — per SEC-7, the server
|
|
157
|
+
rejects `?api_key=…`).
|
|
158
|
+
|
|
159
|
+
- **`track_event` fingerprint + coverage counters (Phase 3).** `track_event`
|
|
160
|
+
now emits a stable `_fingerprint` so the dedup LRU at the `track()`
|
|
161
|
+
sink collapses repeat emissions of the same event (the user's manual
|
|
162
|
+
`track_event` plus the httpx transport hook firing on the same LLM
|
|
163
|
+
call). The fingerprint is stripped before the wire send. The
|
|
164
|
+
`_coverage_seen` / `_coverage_tracked` / `_coverage_streaming_skipped`
|
|
165
|
+
counters are now initialised in `__init__` so the
|
|
166
|
+
`_safe_bump_coverage` helper in `nullrun.instrumentation.auto`
|
|
167
|
+
actually increments the dashboard's coverage tab.
|
|
168
|
+
|
|
169
|
+
- **`SENSITIVE_ARG_KEYS` expanded from 7 to 29 tokens.** Now masks
|
|
170
|
+
`password`, `passwd`, `pwd`, `token`, `secret`, `api_key`, `apikey`,
|
|
171
|
+
`key`, `auth`, `authorization`, `bearer`, `session`, `session_id`,
|
|
172
|
+
`cookie`, `access_token`, `refresh_token`, `id_token`, `private_key`,
|
|
173
|
+
`secret_key`, `email`, `phone`, `ssn`, `credit_card`,
|
|
174
|
+
`credit_card_number`, `cvv`, `cvc`, `pin`, `otp`, `mfa`. Matching
|
|
175
|
+
is case-insensitive.
|
|
176
|
+
|
|
177
|
+
- **Recursive `_safe_error_str` (Phase 3).** The previous one-level
|
|
178
|
+
regex was replaced with a balanced-brace walker that handles
|
|
179
|
+
arbitrary nesting depth and dict values that contain `{` / `}` in
|
|
180
|
+
string content. Bare `details=foo` (no opening brace) is preserved
|
|
181
|
+
so we don't lose free-form text.
|
|
182
|
+
|
|
183
|
+
- **`RateLimitError` exception class (Phase 4).** A new
|
|
184
|
+
`RateLimitError(NullRunTransportError)` carries the parsed
|
|
185
|
+
`Retry-After` (seconds) and `upgrade_url` from the 429 envelope
|
|
186
|
+
per `contracts/errors.ts`. The transport layer's
|
|
187
|
+
`_parse_error_envelope` helper maps 4xx / 5xx / 429 to typed
|
|
188
|
+
exceptions (`NullRunAuthenticationError` /
|
|
189
|
+
`NullRunTransportError(GATEWAY_ERROR)` / `RateLimitError`) so
|
|
190
|
+
callers can branch on the type instead of string-matching
|
|
191
|
+
`str(exc)`.
|
|
192
|
+
|
|
193
|
+
- **`Transport.post_signed_with_401_retry` helper (Phase 4).** The
|
|
194
|
+
runtime can opt into transparent one-shot re-authentication on
|
|
195
|
+
HTTP 401 by passing a `reauth_callback` (typically
|
|
196
|
+
`lambda: self._authenticate()`). The first 401 re-calls
|
|
197
|
+
`auth/verify` to pick up the freshly-rotated `secret_key` and
|
|
198
|
+
retries the original request. A second 401 propagates as
|
|
199
|
+
`NullRunAuthenticationError`.
|
|
200
|
+
|
|
201
|
+
- **`PolicyCache.clear()` (Phase 2).** New method on the transport's
|
|
202
|
+
policy cache so the `PolicyInvalidated` WebSocket callback can
|
|
203
|
+
flush every cached decision atomically. The
|
|
204
|
+
`Transport.clear_policy_cache` public method now delegates to it
|
|
205
|
+
instead of poking the internal `_cache` dict.
|
|
206
|
+
|
|
207
|
+
- **`_fingerprint_for_event_dict` helper (Phase 3).** New in
|
|
208
|
+
`nullrun.instrumentation.auto` for the generic event-dict
|
|
209
|
+
fingerprint used by `track_event` (the existing
|
|
210
|
+
`_fingerprint_for` is for HTTP responses keyed on host+body+status).
|
|
211
|
+
|
|
212
|
+
### Removed (Phase 5)
|
|
213
|
+
|
|
214
|
+
- **Empty placeholder modules deleted.** `src/nullrun/flow/`,
|
|
215
|
+
`src/nullrun/gate/`, `src/nullrun/common/` were placeholders for
|
|
216
|
+
promised-but-unimplemented products. Removed.
|
|
217
|
+
- **Orphan `protos/` directory deleted.** `grpc_transport.py` was
|
|
218
|
+
removed in 0.4.0; the proto schema is no longer needed in the SDK.
|
|
219
|
+
- **`instrumentation/openai.py` (v0.x patcher) deleted.** It patched
|
|
220
|
+
`openai.ChatCompletion.create` which `openai>=1.0` does not
|
|
221
|
+
expose. All OpenAI v1.0+ traffic is now tracked via the httpx
|
|
222
|
+
transport hook in `nullrun.instrumentation.auto`.
|
|
223
|
+
- **`DecisionHistoryRecorder.replay_locally` / `replay_event` /
|
|
224
|
+
`replay_from_file` deleted.** They called `runtime.track` (which
|
|
225
|
+
hits the backend) despite the docstring claiming "local-only".
|
|
226
|
+
The honest-scope local recorder surface (`start_recording`,
|
|
227
|
+
`stop_recording`, `record_event`, `estimate_cost`,
|
|
228
|
+
`RecordingSession.to_dict` / `from_dict`) is preserved.
|
|
229
|
+
- **`observability.TenantFilter` no longer writes the deprecated
|
|
230
|
+
`org_id` field** — only the canonical `organization_id` and
|
|
231
|
+
`api_key_id` remain. The legacy `get_org_id()` helper is gone
|
|
232
|
+
alongside the workspace_id → organization_id migration.
|
|
233
|
+
|
|
234
|
+
### Fixed
|
|
235
|
+
|
|
236
|
+
- **`examples/cost_dashboard.py`** switched from
|
|
237
|
+
`Authorization: Bearer` (which the SDK never uses on the user's
|
|
238
|
+
behalf) to `X-API-Key`, and from the non-existent `/usage`
|
|
239
|
+
endpoint to the canonical `/quota` per `contracts/openapi.yaml`.
|
|
240
|
+
|
|
241
|
+
### Notes
|
|
242
|
+
|
|
243
|
+
- Public surface unchanged. `init`, `protect`, `track_llm`,
|
|
244
|
+
`track_tool`, `track_event` retain the same call signatures
|
|
245
|
+
documented in the existing examples. The platform's
|
|
246
|
+
`docs/sdk/README.md` describes an alternative 7-symbol surface
|
|
247
|
+
(with `wrap` alias and a different `init(organization_id, ...)`
|
|
248
|
+
signature) — that doc is out of sync with the SDK; an update
|
|
249
|
+
to the platform docs is tracked separately. Per the production
|
|
250
|
+
plan's user decisions, the SDK's surface is the source of truth.
|
|
251
|
+
|
|
252
|
+
## [Unreleased]
|
|
253
|
+
|
|
254
|
+
### Added
|
|
255
|
+
|
|
256
|
+
- **Async Policy Cache**: `AsyncTransport` now uses `PolicyCache` for CACHED fallback mode. Previously the async transport always fell back to PERMISSIVE when gateway was unreachable. Now it caches successful execute decisions and uses them when gateway is unavailable.
|
|
257
|
+
- **Custom Sensitive Tools API**: Added `add_sensitive_tool()`, `remove_sensitive_tool()`, `register_sensitive_tools()`, and `get_sensitive_tools()` methods to `NullRunRuntime`. Users can now register custom tools as sensitive requiring strict mode enforcement.
|
|
258
|
+
- **`NullRunBlockedException.tool_name` attribute** (FIX-5): The `tool_name`
|
|
259
|
+
kwarg is now a first-class attribute on `NullRunBlockedException`
|
|
260
|
+
(and its subclasses `LoopDetectedException`, etc.) instead of being
|
|
261
|
+
absorbed into `**details`. Cookbook examples that read `exc.tool_name`
|
|
262
|
+
no longer raise `AttributeError`. Backwards-compatible: `tool_name`
|
|
263
|
+
defaults to `None` and does not appear in `exc.details` when unset.
|
|
264
|
+
The stringified exception now includes `tool={name}` when set.
|
|
265
|
+
|
|
266
|
+
### Fixed
|
|
267
|
+
|
|
268
|
+
- **SDK silent runtime fallback removed** (FIX-4): `_get_or_create_runtime`
|
|
269
|
+
in `nullrun.decorators` no longer wraps `NullRunRuntime.get_instance()`
|
|
270
|
+
in a `try/except Exception` that rebuilds a no-arg `NullRunRuntime()`.
|
|
271
|
+
In 0.3.0 (T3-S2) the no-arg constructor requires `api_key` and raises
|
|
272
|
+
`NullRunAuthenticationError` — so the fallback swallowed the auth
|
|
273
|
+
error from `get_instance()` only to crash with the same error from
|
|
274
|
+
the fallback path itself. After this fix, the auth error propagates
|
|
275
|
+
cleanly to the first `@protect` invocation, mirroring the fail-loud
|
|
276
|
+
contract of `nullrun.init()`. Aligns with the T3-S2 invariant that
|
|
277
|
+
the SDK has no local mode: a missing API key is a hard error, not a
|
|
278
|
+
silent allow-all.
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## [0.4.0] — 2026-06-17
|
|
283
|
+
|
|
284
|
+
Production-readiness release. Resolves all BLOCKER + HIGH + MEDIUM + LOW
|
|
285
|
+
audit findings from the 0.3.x audit. The curated 6-symbol public surface
|
|
286
|
+
(`init`, `protect`, `track_llm`, `track_tool`, `track_event`,
|
|
287
|
+
`__version__`) is unchanged. Full PR-by-PR description follows; this
|
|
288
|
+
entry is the summary. Phase-7 (framework patches) and Phase-8
|
|
289
|
+
(release-prep polish) ship as follow-up releases under the same 0.4.x
|
|
290
|
+
line.
|
|
291
|
+
|
|
292
|
+
### Removed (dead code)
|
|
293
|
+
|
|
294
|
+
- `BoundedDict` class (`runtime.py`) — dead since 0.3.1.
|
|
295
|
+
- `wrap_tool`, `wrap`, `check_before_tool`, `enforce_check_before_llm`,
|
|
296
|
+
`check_before_llm` (and the `CheckDecision` dataclass), `evaluate`
|
|
297
|
+
(`runtime.py`) — zero in-tree callers; `wrap` had a latent
|
|
298
|
+
`NameError` that's gone with the deletion.
|
|
299
|
+
- `clear_pause` (`actions.py`) — zero callers.
|
|
300
|
+
- `WorkflowContext` class (`context.py`) — duplicate of the
|
|
301
|
+
`workflow()` contextmanager.
|
|
302
|
+
- `WebSocketManager` (`transport_websocket.py`) — never instantiated;
|
|
303
|
+
the runtime uses `WebSocketConnection` directly.
|
|
304
|
+
- `PoolConfig` + `AdaptivePool` (`transport.py`) — never instantiated;
|
|
305
|
+
`httpx.Limits` is the real pool.
|
|
306
|
+
- `Transport._atexit_flush` (`transport.py`) — orphan method from the
|
|
307
|
+
pre-weakref.finalize migration.
|
|
308
|
+
- `EventRecorder` (`decision_history.py`) — never used.
|
|
309
|
+
|
|
310
|
+
### Fixed (BLOCKER)
|
|
311
|
+
|
|
312
|
+
- **First-`track()` `AttributeError` (Phase 2).** `runtime.track()` no
|
|
313
|
+
longer reads `self._workflow_costs` (a BoundedDict removed in 0.3.1
|
|
314
|
+
whose two callers survived). Returns `local_cost_cents = 0` from
|
|
315
|
+
the new `_local_cost_cents_estimate` attribute.
|
|
316
|
+
- **`auto_requests` module was unimportable.** The missing
|
|
317
|
+
`_safe_bump_coverage` helper that `auto_requests.py` imports is
|
|
318
|
+
now defined in `auto.py`. The whole module imports cleanly and the
|
|
319
|
+
coverage dashboard counter is reachable.
|
|
320
|
+
- **`auto_instrument()` now calls `patch_requests`.** The `requests`
|
|
321
|
+
library path is no longer dead; ~30-50% of real codebases that use
|
|
322
|
+
`requests` directly are now tracked.
|
|
323
|
+
|
|
324
|
+
### Fixed (HIGH reliability — Phase 5)
|
|
325
|
+
|
|
326
|
+
- `_remote_states` now protected by `threading.RLock`. New helpers
|
|
327
|
+
`_remote_state_for` / `_set_remote_state` are the only public mutation
|
|
328
|
+
path. `test_remote_states_race.py` is now meaningful.
|
|
329
|
+
- `PolicyCache` no longer writes `policy_version` into the `ttl_seconds`
|
|
330
|
+
field (silent cache-lifetime corruption). Added dedicated
|
|
331
|
+
`policy_version` field on `CachedDecision`.
|
|
332
|
+
- `get_instance()` re-auth path is now inside the singleton lock; no
|
|
333
|
+
more TOCTOU window where a concurrent caller can observe a
|
|
334
|
+
half-shutdown runtime.
|
|
335
|
+
- `_fetch_remote_state` uses `self._transport._client` (shared pool
|
|
336
|
+
+ circuit breaker) instead of a raw `httpx.get`.
|
|
337
|
+
- `workflow()` emits a real UUID4 instead of `wf-{hex32}`.
|
|
338
|
+
- `@sensitive` propagates `NullRunAuthenticationError` instead of
|
|
339
|
+
silently swallowing it.
|
|
340
|
+
- Custom-host LLM endpoints now honour the dashboard KILL switch
|
|
341
|
+
(the kill check is no longer gated on the extractor table).
|
|
342
|
+
- `Transport.execute` accepts an `on_transport_error` callback
|
|
343
|
+
(per ADR-008) so sensitive-tool pre-checks can fail-CLOSED on
|
|
344
|
+
classified transport errors.
|
|
345
|
+
|
|
346
|
+
### Changed (MEDIUM hygiene — Phase 6)
|
|
347
|
+
|
|
348
|
+
- `NULLRUN_FALLBACK_MODE` env var (or `fallback_mode` constructor arg)
|
|
349
|
+
selects PERMISSIVE / STRICT / CACHED.
|
|
350
|
+
- `_rebuild` strips `Transfer-Encoding` alongside `Content-Encoding`.
|
|
351
|
+
- `shutdown()` caps join waits at 0.5s (was 2.0s) — safe from
|
|
352
|
+
signal handlers.
|
|
353
|
+
- WS URL constructed via `urllib.parse` (rejects unknown schemes).
|
|
354
|
+
- `DEDUP_LRU_MAX` raised 512 -> 4096.
|
|
355
|
+
|
|
356
|
+
### Added (Phase 7 — framework patches)
|
|
357
|
+
|
|
358
|
+
- `nullrun.instrumentation.llama_index` — `patch_llama_index`
|
|
359
|
+
subscribes to `LLMChatEndEvent` and `FunctionCallEvent` on the
|
|
360
|
+
llama-index core Dispatcher. Optional extra `pip install
|
|
361
|
+
nullrun[llama-index]`.
|
|
362
|
+
- `nullrun.instrumentation.crewai` — `patch_crewai` wraps
|
|
363
|
+
`Crew.kickoff` and `Crew.kickoff_async` to install
|
|
364
|
+
`step_callback` / `task_callback`. Post-run reads
|
|
365
|
+
`crew.usage_metrics` and emits one `llm_call` event per model.
|
|
366
|
+
Optional extra `pip install nullrun[crewai]`.
|
|
367
|
+
- `nullrun.instrumentation.autogen` — `patch_autogen` wraps
|
|
368
|
+
`BaseChatAgent.on_messages` for span tracking and
|
|
369
|
+
`OpenAIChatCompletionClient.create` for streaming-safe usage
|
|
370
|
+
capture. Optional extra `pip install nullrun[autogen]`.
|
|
371
|
+
|
|
372
|
+
### Added (Phase 8 — release polish)
|
|
373
|
+
|
|
374
|
+
- `NullRunRuntime.get_org_status(org_id)` — public helper for
|
|
375
|
+
reading `/api/v1/orgs/{org_id}/status`. Routes through the shared
|
|
376
|
+
transport client. Used by `examples/cost_dashboard.py`.
|
|
377
|
+
- `NULLRUN_BATCH_SIZE` and `NULLRUN_FLUSH_INTERVAL_MS` env vars
|
|
378
|
+
override `FlushConfig` without subclassing.
|
|
379
|
+
- README "mTLS / client certificate authentication" section
|
|
380
|
+
documenting `NULLRUN_TLS_CLIENT_CERT`, `NULLRUN_TLS_CLIENT_KEY`,
|
|
381
|
+
`NULLRUN_TLS_CA_CERT`.
|
|
382
|
+
- Circuit-breaker `OPEN -> HALF_OPEN` jitter sleep capped at 5s
|
|
383
|
+
(was 30s).
|
|
384
|
+
- `RecordingSession` no longer persists the dedup `_fingerprint`
|
|
385
|
+
field — it leaks to disk via `save()` otherwise.
|
|
386
|
+
|
|
387
|
+
### Notes
|
|
388
|
+
|
|
389
|
+
- The platform's `docs/sdk/README.md` describes a 7-symbol surface that
|
|
390
|
+
does not match the shipped SDK. The SDK's curated surface is the
|
|
391
|
+
source of truth; platform docs re-alignment is tracked separately.
|
|
392
|
+
|
|
393
|
+
---
|
|
394
|
+
|
|
395
|
+
## [0.3.0] — 2026-06-15
|
|
396
|
+
|
|
397
|
+
### Breaking
|
|
398
|
+
|
|
399
|
+
- **No-api-key init now raises** (T3-S2): `nullrun.init()` and
|
|
400
|
+
`NullRunRuntime(...)` without an `api_key` (and with `NULLRUN_API_KEY`
|
|
401
|
+
unset) now raise `NullRunAuthenticationError` instead of falling back
|
|
402
|
+
to a `NullRunNoop` stub. The previous silent fallback silently
|
|
403
|
+
bypassed every backend gate (budget, policy, control plane) — a real
|
|
404
|
+
safety hole in production. **Action required:** ensure
|
|
405
|
+
`api_key="nr_live_..."` is passed to `init()` (or `NULLRUN_API_KEY`
|
|
406
|
+
is set) in every entry point. The `0.2.0` deprecation warning has
|
|
407
|
+
been removed; the new behavior is hard.
|
|
408
|
+
- **`local_mode` field removed**: The auto-derived `local_mode` flag
|
|
409
|
+
on `NullRunRuntime` is gone. The `is_local_mode` property and the
|
|
410
|
+
`NullRunNoop` / `NullRunNoopBreaker` / `_NullContext` classes are
|
|
411
|
+
deleted (`nullrun.noop` module removed). All call sites that read
|
|
412
|
+
`runtime.local_mode` will see `AttributeError` — there is no
|
|
413
|
+
migration path because the field no longer has meaning. Code paths
|
|
414
|
+
that previously branched on `local_mode` now always go through the
|
|
415
|
+
cloud runtime (auth + policy fetch + control plane).
|
|
416
|
+
|
|
417
|
+
### Removed
|
|
418
|
+
|
|
419
|
+
- **Legacy Breaker exports** (T9): The 7 legacy re-exports
|
|
420
|
+
(`nullrun.BreakerError`, `nullrun.CostLimitExceeded`,
|
|
421
|
+
`nullrun.ApprovalRequired`, `nullrun.BreakerTimeout`,
|
|
422
|
+
`nullrun.Policy`, `nullrun.FallbackMode`, `nullrun.PoolConfig`)
|
|
423
|
+
are no longer reachable as `from nullrun import X`. The canonical
|
|
424
|
+
exception names (`NullRunBlockedException`, `WorkflowPausedException`,
|
|
425
|
+
`WorkflowKilledException`, `NullRunAuthenticationError`, …) and the
|
|
426
|
+
canonical policy/transport modules
|
|
427
|
+
(`from nullrun.runtime import Policy`,
|
|
428
|
+
`from nullrun.transport import FallbackMode, PoolConfig`) remain
|
|
429
|
+
available. Audited for 0 external callers.
|
|
430
|
+
|
|
431
|
+
### Migration
|
|
432
|
+
|
|
433
|
+
- **0.2.x → 0.3.0**:
|
|
434
|
+
- `nullrun.init()` calls without `api_key` will raise. Pass
|
|
435
|
+
`api_key="nr_live_..."` explicitly or set `NULLRUN_API_KEY`.
|
|
436
|
+
- `NullRunRuntime(...)` constructions without `api_key` will raise
|
|
437
|
+
(same fix).
|
|
438
|
+
- Tests using `NullRunNoop` / `local_mode=True` mocking must switch
|
|
439
|
+
to `NullRunRuntime(api_key="test-key", _test_mode=True)` —
|
|
440
|
+
`_test_mode` skips the network calls without silently bypassing
|
|
441
|
+
policy.
|
|
442
|
+
- `from nullrun import BreakerError` (and the 6 other legacy names)
|
|
443
|
+
must use the canonical paths above.
|
|
444
|
+
|
|
445
|
+
### Added
|
|
446
|
+
|
|
447
|
+
- **Async Policy Cache**: `AsyncTransport` now uses `PolicyCache` for CACHED fallback mode. Previously the async transport always fell back to PERMISSIVE when gateway was unreachable. Now it caches successful execute decisions and uses them when gateway is unavailable.
|
|
448
|
+
- **Custom Sensitive Tools API**: Added `add_sensitive_tool()`, `remove_sensitive_tool()`, `register_sensitive_tools()`, and `get_sensitive_tools()` methods to `NullRunRuntime`. Users can now register custom tools as sensitive requiring strict mode enforcement.
|
|
449
|
+
|
|
450
|
+
### Deprecated
|
|
451
|
+
|
|
452
|
+
- **No-api-key init / local mode** (T3-S1): Calling `nullrun.init()` or constructing `NullRunRuntime(...)` without an `api_key` (and with `NULLRUN_API_KEY` unset) now emits a `DeprecationWarning`. The runtime still falls back to local mode and silently bypasses every backend gate (budget, policy, control plane). The fallback will be **removed in 0.3.0** — passing `api_key='nr_live_...'` explicitly or setting `NULLRUN_API_KEY` is the only supported path going forward. Pin the warning to a hard error with `python -W error::DeprecationWarning` to catch callers in CI.
|
|
453
|
+
|
|
454
|
+
---
|
|
455
|
+
|
|
456
|
+
## [0.1.1] — 2026-05-20
|
|
457
|
+
|
|
458
|
+
### Fixed
|
|
459
|
+
|
|
460
|
+
- **CR-2**: Fixed buffer overflow when circuit breaker is OPEN. Previously, re-queued events were prepended to buffer, causing newest events to be dropped first. Now appends to buffer end and checks max_buffer_size before re-queue.
|
|
461
|
+
- **CR-5**: Async circuit breaker now uses `asyncio.Lock` instead of `threading.Lock` for proper async context handling.
|
|
462
|
+
- **CR-1+CR-4**: `runtime.py` now creates Transport before `_authenticate()` and `_fetch_policy()`, reusing the HTTP client for connection pooling and consistent timeout/retry policies.
|
|
463
|
+
- **AsyncAwait**: Fixed `_call_async()` not awaiting `_on_success_async()` and `_on_failure_async()` coroutines, causing "coroutine was never awaited" warnings in async transport.
|
|
464
|
+
|
|
465
|
+
### Changed
|
|
466
|
+
|
|
467
|
+
- Transport buffer now enforces max_buffer_size **before** re-queuing events on circuit breaker OPEN
|
|
468
|
+
|
|
469
|
+
---
|
|
470
|
+
|
|
471
|
+
## [0.1.0] — 2026-05-18
|
|
472
|
+
|
|
473
|
+
### Added
|
|
474
|
+
|
|
475
|
+
- Circuit breaker core (`src/nullrun/breaker/`) with STRICT / PERMISSIVE / CACHED fallback modes
|
|
476
|
+
- HTTP transport with batch event sending (`transport.py`)
|
|
477
|
+
- Async transport for asyncio applications
|
|
478
|
+
- Retry logic with jitter and policy-aware backoff
|
|
479
|
+
- `@protect` decorator for wrapping functions (`decorators.py`)
|
|
480
|
+
- Workflow context support (`context.py`)
|
|
481
|
+
- Main runtime entrypoint (`runtime.py`)
|
|
482
|
+
- `X-API-Version` header on all outgoing requests
|
|
483
|
+
|
|
484
|
+
### Notes
|
|
485
|
+
|
|
486
|
+
- Requires Python ≥ 3.10
|
|
487
|
+
- Compatible with NullRun API version `2024-01-15`
|
|
488
|
+
|
|
489
|
+
---
|
|
490
|
+
|
|
491
|
+
## How to upgrade
|
|
492
|
+
|
|
493
|
+
### 0.x → next
|
|
494
|
+
|
|
495
|
+
_No breaking changes yet. Watch this file._
|
|
496
|
+
|
|
497
|
+
---
|
|
498
|
+
|
|
499
|
+
[Unreleased]: https://github.com/maltsev-dev/nullrun-sdk/compare/v0.1.1...HEAD
|
|
500
|
+
[0.1.1]: https://github.com/maltsev-dev/nullrun-sdk/releases/tag/v0.1.1
|
|
501
|
+
[0.1.0]: https://github.com/maltsev-dev/nullrun-sdk/releases/tag/v0.1.0
|
nullrun-0.4.0/Dockerfile
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Build stage for Python SDK
|
|
2
|
+
FROM python:3.11-slim as builder
|
|
3
|
+
|
|
4
|
+
WORKDIR /app
|
|
5
|
+
|
|
6
|
+
# Install build dependencies
|
|
7
|
+
RUN apt-get update && apt-get install -y \
|
|
8
|
+
build-essential \
|
|
9
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
10
|
+
|
|
11
|
+
# Copy source first (needed for build with src layout)
|
|
12
|
+
COPY pyproject.toml ./
|
|
13
|
+
COPY src ./src
|
|
14
|
+
RUN pip install build && python -m build
|
|
15
|
+
|
|
16
|
+
# Runtime stage
|
|
17
|
+
FROM python:3.11-slim
|
|
18
|
+
|
|
19
|
+
WORKDIR /app
|
|
20
|
+
|
|
21
|
+
# Install runtime dependencies
|
|
22
|
+
RUN apt-get update && apt-get install -y \
|
|
23
|
+
curl \
|
|
24
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
25
|
+
|
|
26
|
+
# Copy builder output
|
|
27
|
+
COPY --from=builder /app/dist /app/dist
|
|
28
|
+
RUN pip install /app/dist/*.whl --force-reinstall
|
|
29
|
+
|
|
30
|
+
# Non-root user
|
|
31
|
+
RUN useradd -m -u 1000 nullrun
|
|
32
|
+
USER nullrun
|
|
33
|
+
|
|
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.
|
|
38
|
+
RUN pip install "nullrun[langgraph]"
|
|
39
|
+
|
|
40
|
+
ENTRYPOINT ["python", "-m", "nullrun.breaker"]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Development Dockerfile for Python SDK
|
|
2
|
+
FROM python:3.11-slim
|
|
3
|
+
|
|
4
|
+
WORKDIR /app
|
|
5
|
+
|
|
6
|
+
# Copy source first (needed for editable install with src layout)
|
|
7
|
+
COPY pyproject.toml README.md ./
|
|
8
|
+
COPY src ./src
|
|
9
|
+
|
|
10
|
+
# Install dependencies
|
|
11
|
+
RUN pip install -e ".[dev,langgraph]"
|
|
12
|
+
|
|
13
|
+
# Copy tests
|
|
14
|
+
COPY tests ./tests
|
|
15
|
+
|
|
16
|
+
# Stay alive for debugging - user can exec in to run tests manually
|
|
17
|
+
CMD ["tail", "-f", "/dev/null"]
|