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.
Files changed (39) hide show
  1. nullrun-0.4.0/.dockerignore +6 -0
  2. nullrun-0.4.0/.gitignore +69 -0
  3. nullrun-0.4.0/.pre-commit-config.yaml +21 -0
  4. nullrun-0.4.0/CHANGELOG.md +501 -0
  5. nullrun-0.4.0/Dockerfile +40 -0
  6. nullrun-0.4.0/Dockerfile.dev +17 -0
  7. nullrun-0.4.0/LICENSE +201 -0
  8. nullrun-0.4.0/Makefile +64 -0
  9. nullrun-0.4.0/PKG-INFO +194 -0
  10. nullrun-0.4.0/README.md +116 -0
  11. nullrun-0.4.0/examples/async_usage.py +36 -0
  12. nullrun-0.4.0/examples/basic.py +27 -0
  13. nullrun-0.4.0/examples/basic_observe.py +53 -0
  14. nullrun-0.4.0/examples/cost_dashboard.py +95 -0
  15. nullrun-0.4.0/pyproject.toml +203 -0
  16. nullrun-0.4.0/src/nullrun/__init__.py +282 -0
  17. nullrun-0.4.0/src/nullrun/__version__.py +4 -0
  18. nullrun-0.4.0/src/nullrun/actions.py +455 -0
  19. nullrun-0.4.0/src/nullrun/breaker/__init__.py +27 -0
  20. nullrun-0.4.0/src/nullrun/breaker/circuit_breaker.py +402 -0
  21. nullrun-0.4.0/src/nullrun/breaker/exceptions.py +319 -0
  22. nullrun-0.4.0/src/nullrun/context.py +208 -0
  23. nullrun-0.4.0/src/nullrun/decorators.py +649 -0
  24. nullrun-0.4.0/src/nullrun/instrumentation/__init__.py +23 -0
  25. nullrun-0.4.0/src/nullrun/instrumentation/_safe_patch.py +99 -0
  26. nullrun-0.4.0/src/nullrun/instrumentation/auto.py +1095 -0
  27. nullrun-0.4.0/src/nullrun/instrumentation/auto_requests.py +257 -0
  28. nullrun-0.4.0/src/nullrun/instrumentation/autogen.py +163 -0
  29. nullrun-0.4.0/src/nullrun/instrumentation/crewai.py +140 -0
  30. nullrun-0.4.0/src/nullrun/instrumentation/langgraph.py +412 -0
  31. nullrun-0.4.0/src/nullrun/instrumentation/llama_index.py +110 -0
  32. nullrun-0.4.0/src/nullrun/observability.py +160 -0
  33. nullrun-0.4.0/src/nullrun/py.typed +0 -0
  34. nullrun-0.4.0/src/nullrun/runtime.py +1806 -0
  35. nullrun-0.4.0/src/nullrun/toolbox/__init__.py +20 -0
  36. nullrun-0.4.0/src/nullrun/toolbox/langgraph.py +94 -0
  37. nullrun-0.4.0/src/nullrun/tracing.py +155 -0
  38. nullrun-0.4.0/src/nullrun/transport.py +1509 -0
  39. nullrun-0.4.0/src/nullrun/transport_websocket.py +627 -0
@@ -0,0 +1,6 @@
1
+ .git
2
+ *.log
3
+ *.lock
4
+ .env
5
+ **/node_modules
6
+ dist
@@ -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
@@ -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"]