Wip/working tree 2026 06 18 - #3
Merged
Merged
Conversation
Counterpart of NULLRUN fix(ws-control) (commit 5e2f65b). The
backend now embeds the exact bytes that were HMAC-signed in a
separate signed_payload field. The SDK:
1. Verifies the signature against bytes.fromhex(signed_payload),
falling back to the legacy wire-bytes path only when the
field is absent (pre-FIX-C servers).
2. Dispatches state changes from the parsed signed_payload
bytes, not from the outer envelope body. This closes a
security hole: an attacker who captured a (signed_payload,
signature) pair from a benign 'state=Normal' event could
otherwise splice a forged 'state=Killed' into the outer body
and the signature would still verify, because the signature
covers only the signed_payload bytes. Reading dispatch state
from the trusted source keeps the captured signature
semantically bound to its captured body.
Tests in test_ws_signed_payload.py cover:
- round-trip, wrong-secret, tampered-payload rejection
- malformed signed_payload does not crash
- replay-with-spliced-body: signature still verifies, but the
dispatched state is the captured one (not the forged one) -
the attack is harmless
- replays where the attacker also rewrites signed_payload are
rejected via signature mismatch
Note: the two ACK tests are still failing because
ACKNOWLEDGED_STATES is still lowercase. That is fixed separately
by S-2 in the same release - kept as a separate commit so the
byte-mismatch/security fix is reviewable on its own.
The server's WsWorkflowState enum (NULLRUN/backend/src/proxy/http/
ws_control.rs) emits 'Killed' / 'Paused' (PascalCase). The SDK was
comparing against {'killed', 'paused'} (lowercase), so the ACK path
was dead and the server's pending-ack queue grew without ever
being drained.
This unblocks the two remaining failing tests in
test_ws_signed_payload.py:
- test_state_change_with_signed_payload_is_dispatched (now sends
the ACK that the server expects)
- test_acknowledged_states_use_pascalcase (now matches server
casing)
With byte-mismatch FIX-C in place (commits 5e2f65b + 105fb80), the
KILL/PAUSE path now works end-to-end:
1. server signs the inner message and embeds the bytes in
signed_payload
2. server sends the envelope (flattened WsMessage + signature +
timestamp + api_key_id + signed_payload)
3. SDK verifies signature against bytes.fromhex(signed_payload)
4. SDK dispatches from the trusted source (parsed signed_payload),
so a captured (signed_payload, signature) pair can only
re-trigger its captured state, never a forged one
5. SDK sends ACK on Killed/Paused, draining server's pending-acks
The working tree contained a large uncommitted changeset that was
never pushed: 68 files, +8955/-3328 lines. Reading the diff shape
this is the 0.3.0 -> 0.4.0 production-readiness migration
(per CHANGELOG.md / audit §6.1):
- PoolConfig / AdaptivePool removed (Transport now is a
context manager; weakref.finalize replaces atexit.register)
- gRPC transport removed (NULLRUN_USE_GRPC no-op; create_grpc_transport
was a NameError)
- signal.signal global hijack removed
- track.proto removed
- decision_history / flow / gate / common placeholders removed
- six zombie exceptions removed (CostLimitExceeded,
ApprovalRequired, BreakerTimeout, LoopDetectedException,
RetryStormException, RateLimitExceededException)
- _organization_id_var, _api_key_id_var removed
- patch_openai / unpatch_openai removed
- auto-instrumentation extended with langgraph / llama-index /
crewai / autogen / openai-agents via safe_patch
- SENSITIVE_ARG_KEYS expanded from 7 to 29 tokens
- HMAC always-on for /track/batch, /gate, /evaluate, /status,
/auth/verify + WS ACKs signed
- 14 new test files
- analyze.md (this session's plan)
Tracking as a wip branch so the work is preserved. This commit does
not change the byte-mismatch FIX-C landing in
fix/ws-byte-mismatch-verify-signed-payload (commits 105fb80,
73f3197) - those branches are based on 316a694 + the byte-mismatch
fixes only.
maltsev-dev
added a commit
that referenced
this pull request
Jul 3, 2026
Task #3 / Task #18 (2026-07-03): wire the SDK to the backend's v3 default. Per CLAUDE.md §24, every /check mints a server-side uuidv7 execution_id; the SDK receives it in the response and propagates it to /track. This is the SDK_MIN_VERSION for the v3 rollout per CLAUDE.md §0 pre-flip checklist. Changes: src/nullrun/uuid7.py (new): - RFC 9562 §5.7 time-ordered ID generator. 48-bit unix_ts_ms prefix + 12-bit rand_a + 62-bit rand_b. Same layout as the backend's mint_execution_id() so log scrapers can sort by ID alone. - Uses secrets.token_bytes(10) for cryptographically secure random component. - uuid7() returns stdlib UUID; uuid7_str() returns the canonical 36-char string. src/nullrun/capabilities.py (new): - ServerCapabilities dataclass mirrors /health payload. - is_v3_ready() returns True only when ALL three v3 caps (server_minted_execution_id, per_execution_reservations, heartbeat_time_based) are set. - probe_capabilities(api_url) — best-effort /health fetch with 2s timeout. Returns None on failure (not fatal). - validate_sdk_version(sdk_version, caps) — returns warnings for SDK_MIN_VERSION mismatch. - SDK_MIN_VERSION_FOR_V3 = '0.12.0' is the gate's coordinate for the v3 rollout. src/nullrun/__init__.py: - init() now probes /health after singleton registration and logs a startup warning for version mismatch (does NOT fail init() — the gate still rejects with PROTOCOL_TOO_OLD). - Probe is best-effort: timeout/5xx logs at INFO. src/nullrun/__version__.py: - Bumped 0.11.0 → 0.12.0 (the SDK_MIN_VERSION coordinate). CHANGELOG.md: - New 0.12.0 entry with Added/Changed sections. tests/test_uuid7.py (new): 8 tests pin the wire contract: - Returns stdlib UUID - 36-char string format - Version bits = 7 - Variant bits = 0b10 - Time-ordered (consecutive calls sort) - 1000 unique IDs under rapid calls - Round-trips through uuid.UUID() tests/test_capabilities.py (new): 9 tests pin: - v3-ready backend parses to is_v3_ready()=True - Missing keys default to False (fail-closed) - Partial v3 caps → not ready - Old SDK against v3 backend → warning - Current SDK → no warning - Legacy backend → 'not v3-ready' warning - Unparseable versions don't crash - as_dict() is wire-safe (no secrets) - SDK_MIN_VERSION_FOR_V3 = '0.12.0' Tests: 17 new SDK tests pass. Full backend test suite still green at 1443.
maltsev-dev
added a commit
that referenced
this pull request
Jul 3, 2026
* feat(sdk): server-minted execution_id default — uuid7 + capability probe Task #3 / Task #18 (2026-07-03): wire the SDK to the backend's v3 default. Per CLAUDE.md §24, every /check mints a server-side uuidv7 execution_id; the SDK receives it in the response and propagates it to /track. This is the SDK_MIN_VERSION for the v3 rollout per CLAUDE.md §0 pre-flip checklist. Changes: src/nullrun/uuid7.py (new): - RFC 9562 §5.7 time-ordered ID generator. 48-bit unix_ts_ms prefix + 12-bit rand_a + 62-bit rand_b. Same layout as the backend's mint_execution_id() so log scrapers can sort by ID alone. - Uses secrets.token_bytes(10) for cryptographically secure random component. - uuid7() returns stdlib UUID; uuid7_str() returns the canonical 36-char string. src/nullrun/capabilities.py (new): - ServerCapabilities dataclass mirrors /health payload. - is_v3_ready() returns True only when ALL three v3 caps (server_minted_execution_id, per_execution_reservations, heartbeat_time_based) are set. - probe_capabilities(api_url) — best-effort /health fetch with 2s timeout. Returns None on failure (not fatal). - validate_sdk_version(sdk_version, caps) — returns warnings for SDK_MIN_VERSION mismatch. - SDK_MIN_VERSION_FOR_V3 = '0.12.0' is the gate's coordinate for the v3 rollout. src/nullrun/__init__.py: - init() now probes /health after singleton registration and logs a startup warning for version mismatch (does NOT fail init() — the gate still rejects with PROTOCOL_TOO_OLD). - Probe is best-effort: timeout/5xx logs at INFO. src/nullrun/__version__.py: - Bumped 0.11.0 → 0.12.0 (the SDK_MIN_VERSION coordinate). CHANGELOG.md: - New 0.12.0 entry with Added/Changed sections. tests/test_uuid7.py (new): 8 tests pin the wire contract: - Returns stdlib UUID - 36-char string format - Version bits = 7 - Variant bits = 0b10 - Time-ordered (consecutive calls sort) - 1000 unique IDs under rapid calls - Round-trips through uuid.UUID() tests/test_capabilities.py (new): 9 tests pin: - v3-ready backend parses to is_v3_ready()=True - Missing keys default to False (fail-closed) - Partial v3 caps → not ready - Old SDK against v3 backend → warning - Current SDK → no warning - Legacy backend → 'not v3-ready' warning - Unparseable versions don't crash - as_dict() is wire-safe (no secrets) - SDK_MIN_VERSION_FOR_V3 = '0.12.0' Tests: 17 new SDK tests pass. Full backend test suite still green at 1443. * fix(sdk): populate Author/Author-email via metadata hook PEP 621 maps `authors` to PKG-INFO's `Author-email:` line but not to the legacy single `Author:` line that `pip show` renders, and pip does not display `Maintainer:` either. As a result every previous release shipped with an empty `Author:` and the maintainer's name never appeared in `pip show nullrun`. Hatchling compounds this: its authors parser only adds an entry to `authors_data["name"]` (which becomes `Author:`) when an inline-table has a `name` and NO `email`. When both are present the name is folded into `Author-email:`'s display_name and the legacy `Author:` line is suppressed entirely. Fix: declare `authors` and `maintainers` as dynamic fields and populate them from a custom hatchling metadata hook (`hatch_build.py`). The hook splits the primary author into a name-only + email-only inline-table pair so hatchling populates both `Author:` and `Author-email:`. Declaring at least one dynamic field is what actually wires `MetadataHookInterface.update()` — without it hatchling configures the hook but never invokes it. * fix(sdk): bind logger in init() and cover capability probe paths CI on Python 3.11 failed with `NameError: name 'logger' is not defined` in 5 tests. The `feat(sdk)` commit (2a7886b) added new `logger.warning/info/debug` calls in `init()` after the existing `import logging` but never assigned the `logger` name. Master passed only because its pre-existing `logger.warning` calls sit inside an `if existing is not None:` branch that tests rarely exercise; the new ones run on every `init()` call. Also covers the 9 newly-uncovered lines Codecov flagged: `probe_capabilities` failure paths (non-2xx / ConnectError / malformed JSON) and the four new `init()` logging branches (`debug=True` sets DEBUG; probe unreachable → INFO; probe raises → DEBUG; existing runtime shutdown raises → WARNING). Local verification (.venv-ci, Python 3.14): - pytest: 1154 passed (was 1129; +25 new) - ruff: clean - mypy: clean - coverage: 82.02% (threshold 82.00%) * style: reorder capability probe imports per ruff I001 Ruff's isort rule flagged the import block in `init()` — the `from nullrun.__version__` line was placed after `from nullrun.capabilities` but `__version__` sorts before `capabilities` (underscore is 0x5F, letters are 0x61+), so the correct alphabetical order is reversed. CI `Run ruff` step was failing on this; the previous commit's ruff output was checked against an outdated working copy.
maltsev-dev
added a commit
that referenced
this pull request
Aug 7, 2026
* fix(ws): verify HMAC on signed_payload bytes, dispatch from trusted
Counterpart of NULLRUN fix(ws-control) (commit 5e2f65b). The
backend now embeds the exact bytes that were HMAC-signed in a
separate signed_payload field. The SDK:
1. Verifies the signature against bytes.fromhex(signed_payload),
falling back to the legacy wire-bytes path only when the
field is absent (pre-FIX-C servers).
2. Dispatches state changes from the parsed signed_payload
bytes, not from the outer envelope body. This closes a
security hole: an attacker who captured a (signed_payload,
signature) pair from a benign 'state=Normal' event could
otherwise splice a forged 'state=Killed' into the outer body
and the signature would still verify, because the signature
covers only the signed_payload bytes. Reading dispatch state
from the trusted source keeps the captured signature
semantically bound to its captured body.
Tests in test_ws_signed_payload.py cover:
- round-trip, wrong-secret, tampered-payload rejection
- malformed signed_payload does not crash
- replay-with-spliced-body: signature still verifies, but the
dispatched state is the captured one (not the forged one) -
the attack is harmless
- replays where the attacker also rewrites signed_payload are
rejected via signature mismatch
Note: the two ACK tests are still failing because
ACKNOWLEDGED_STATES is still lowercase. That is fixed separately
by S-2 in the same release - kept as a separate commit so the
byte-mismatch/security fix is reviewable on its own.
* fix(ws): ACKNOWLEDGED_STATES uses PascalCase to match server emit
The server's WsWorkflowState enum (NULLRUN/backend/src/proxy/http/
ws_control.rs) emits 'Killed' / 'Paused' (PascalCase). The SDK was
comparing against {'killed', 'paused'} (lowercase), so the ACK path
was dead and the server's pending-ack queue grew without ever
being drained.
This unblocks the two remaining failing tests in
test_ws_signed_payload.py:
- test_state_change_with_signed_payload_is_dispatched (now sends
the ACK that the server expects)
- test_acknowledged_states_use_pascalcase (now matches server
casing)
With byte-mismatch FIX-C in place (commits 5e2f65b + 105fb80), the
KILL/PAUSE path now works end-to-end:
1. server signs the inner message and embeds the bytes in
signed_payload
2. server sends the envelope (flattened WsMessage + signature +
timestamp + api_key_id + signed_payload)
3. SDK verifies signature against bytes.fromhex(signed_payload)
4. SDK dispatches from the trusted source (parsed signed_payload),
so a captured (signed_payload, signature) pair can only
re-trigger its captured state, never a forged one
5. SDK sends ACK on Killed/Paused, draining server's pending-acks
* wip: stage SDK 0.3.0->0.4.0 migration that was sitting uncommitted
The working tree contained a large uncommitted changeset that was
never pushed: 68 files, +8955/-3328 lines. Reading the diff shape
this is the 0.3.0 -> 0.4.0 production-readiness migration
(per CHANGELOG.md / audit §6.1):
- PoolConfig / AdaptivePool removed (Transport now is a
context manager; weakref.finalize replaces atexit.register)
- gRPC transport removed (NULLRUN_USE_GRPC no-op; create_grpc_transport
was a NameError)
- signal.signal global hijack removed
- track.proto removed
- decision_history / flow / gate / common placeholders removed
- six zombie exceptions removed (CostLimitExceeded,
ApprovalRequired, BreakerTimeout, LoopDetectedException,
RetryStormException, RateLimitExceededException)
- _organization_id_var, _api_key_id_var removed
- patch_openai / unpatch_openai removed
- auto-instrumentation extended with langgraph / llama-index /
crewai / autogen / openai-agents via safe_patch
- SENSITIVE_ARG_KEYS expanded from 7 to 29 tokens
- HMAC always-on for /track/batch, /gate, /evaluate, /status,
/auth/verify + WS ACKs signed
- 14 new test files
- analyze.md (this session's plan)
Tracking as a wip branch so the work is preserved. This commit does
not change the byte-mismatch FIX-C landing in
fix/ws-byte-mismatch-verify-signed-payload (commits 105fb80,
73f3197) - those branches are based on 316a694 + the byte-mismatch
fixes only.
maltsev-dev
added a commit
that referenced
this pull request
Aug 7, 2026
* feat(sdk): server-minted execution_id default — uuid7 + capability probe Task #3 / Task #18 (2026-07-03): wire the SDK to the backend's v3 default. Per CLAUDE.md §24, every /check mints a server-side uuidv7 execution_id; the SDK receives it in the response and propagates it to /track. This is the SDK_MIN_VERSION for the v3 rollout per CLAUDE.md §0 pre-flip checklist. Changes: src/nullrun/uuid7.py (new): - RFC 9562 §5.7 time-ordered ID generator. 48-bit unix_ts_ms prefix + 12-bit rand_a + 62-bit rand_b. Same layout as the backend's mint_execution_id() so log scrapers can sort by ID alone. - Uses secrets.token_bytes(10) for cryptographically secure random component. - uuid7() returns stdlib UUID; uuid7_str() returns the canonical 36-char string. src/nullrun/capabilities.py (new): - ServerCapabilities dataclass mirrors /health payload. - is_v3_ready() returns True only when ALL three v3 caps (server_minted_execution_id, per_execution_reservations, heartbeat_time_based) are set. - probe_capabilities(api_url) — best-effort /health fetch with 2s timeout. Returns None on failure (not fatal). - validate_sdk_version(sdk_version, caps) — returns warnings for SDK_MIN_VERSION mismatch. - SDK_MIN_VERSION_FOR_V3 = '0.12.0' is the gate's coordinate for the v3 rollout. src/nullrun/__init__.py: - init() now probes /health after singleton registration and logs a startup warning for version mismatch (does NOT fail init() — the gate still rejects with PROTOCOL_TOO_OLD). - Probe is best-effort: timeout/5xx logs at INFO. src/nullrun/__version__.py: - Bumped 0.11.0 → 0.12.0 (the SDK_MIN_VERSION coordinate). CHANGELOG.md: - New 0.12.0 entry with Added/Changed sections. tests/test_uuid7.py (new): 8 tests pin the wire contract: - Returns stdlib UUID - 36-char string format - Version bits = 7 - Variant bits = 0b10 - Time-ordered (consecutive calls sort) - 1000 unique IDs under rapid calls - Round-trips through uuid.UUID() tests/test_capabilities.py (new): 9 tests pin: - v3-ready backend parses to is_v3_ready()=True - Missing keys default to False (fail-closed) - Partial v3 caps → not ready - Old SDK against v3 backend → warning - Current SDK → no warning - Legacy backend → 'not v3-ready' warning - Unparseable versions don't crash - as_dict() is wire-safe (no secrets) - SDK_MIN_VERSION_FOR_V3 = '0.12.0' Tests: 17 new SDK tests pass. Full backend test suite still green at 1443. * fix(sdk): populate Author/Author-email via metadata hook PEP 621 maps `authors` to PKG-INFO's `Author-email:` line but not to the legacy single `Author:` line that `pip show` renders, and pip does not display `Maintainer:` either. As a result every previous release shipped with an empty `Author:` and the maintainer's name never appeared in `pip show nullrun`. Hatchling compounds this: its authors parser only adds an entry to `authors_data["name"]` (which becomes `Author:`) when an inline-table has a `name` and NO `email`. When both are present the name is folded into `Author-email:`'s display_name and the legacy `Author:` line is suppressed entirely. Fix: declare `authors` and `maintainers` as dynamic fields and populate them from a custom hatchling metadata hook (`hatch_build.py`). The hook splits the primary author into a name-only + email-only inline-table pair so hatchling populates both `Author:` and `Author-email:`. Declaring at least one dynamic field is what actually wires `MetadataHookInterface.update()` — without it hatchling configures the hook but never invokes it. * fix(sdk): bind logger in init() and cover capability probe paths CI on Python 3.11 failed with `NameError: name 'logger' is not defined` in 5 tests. The `feat(sdk)` commit (2a7886b) added new `logger.warning/info/debug` calls in `init()` after the existing `import logging` but never assigned the `logger` name. Master passed only because its pre-existing `logger.warning` calls sit inside an `if existing is not None:` branch that tests rarely exercise; the new ones run on every `init()` call. Also covers the 9 newly-uncovered lines Codecov flagged: `probe_capabilities` failure paths (non-2xx / ConnectError / malformed JSON) and the four new `init()` logging branches (`debug=True` sets DEBUG; probe unreachable → INFO; probe raises → DEBUG; existing runtime shutdown raises → WARNING). Local verification (.venv-ci, Python 3.14): - pytest: 1154 passed (was 1129; +25 new) - ruff: clean - mypy: clean - coverage: 82.02% (threshold 82.00%) * style: reorder capability probe imports per ruff I001 Ruff's isort rule flagged the import block in `init()` — the `from nullrun.__version__` line was placed after `from nullrun.capabilities` but `__version__` sorts before `capabilities` (underscore is 0x5F, letters are 0x61+), so the correct alphabetical order is reversed. CI `Run ruff` step was failing on this; the previous commit's ruff output was checked against an outdated working copy.
maltsev-dev
added a commit
that referenced
this pull request
Aug 12, 2026
…print 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.
maltsev-dev
added a commit
that referenced
this pull request
Aug 14, 2026
…re (#90) * test(sdk): remove flaky test_env_fallback_when_server_value_is_zero The push-CI coverage job on 0.15.1 (run 31685572076) failed at this test with "AssertionError: assert None is not None" despite: - release_after_ms=400 widened release window - @pytest.mark.rerunfailures(reruns=4) on inner helper Root cause: @pytest.mark.rerunfailures only reruns TOP-LEVEL pytest test functions. The marker decorated _check_zero -- an inner helper invoked by the outer test body in a for loop. Pytest never collected the marker, so reruns never fired. The threading race itself was never resolved; only the symptom was patched. Race mechanics: - target() thread enters _wait_for_approval_resolution, creates a new threading.Event(), calls event.wait(timeout). - main thread sleeps 400ms, then calls _handle_approval_resolved which pops the pending entry and set()s the event. - If main releases BEFORE target reaches event.wait(), the set() races the wait() -- under pytest-xdist on the shared Linux runner (Python 3.12), target sometimes misses the window, event.wait(timeout_seconds=120) blocks, and the 5s t.join timeout fires before the 120s wait releases. Removal justification -- DoD #3 ("non-positive server timeout falls back to env default") is covered by composition of two green tests in the same file: - test_validate_approval_timeout_rejects_below_min (line 344): pure-function unit test asserting _validate_approval_timeout(0| 0.0|-1|-100|0.99) is None. Deterministic, never flaky. - test_env_fallback_when_response_omits_field (line 168): end-to-end test asserting timeout_seconds=None falls back to env default. Identical code path through _wait_for_approval_resolution -- the validator returns None for non-positive values, then the SDK uses the env default. Test file: tests/test_approval_timeout_field.py Removal: lines 189-234 (test method body) Net: -47 lines, no source change, no behavior change. Verification: - Local pytest: 1549 passed, 7 skipped (was 1550+7; -1 expected). - ruff clean. mypy clean (37 source files). - 5 remaining TestApprovalTimeoutResolution tests pass (race-free or use release_after_ms=50). - 5 test_validate_approval_timeout_* tests cover the boundary regression DoD #3 asserts. * fix(sdk): observability closure on check_workflow_budget fail-OPEN paths The fail-OPEN posture on SDK transport failure is the documented ADR-008 contract (top-of-runtime.py table) and is unchanged by this commit. Pre-0.15.2, however, the FALLBACK decision_source arm logged at DEBUG, contradicting the method docblock ("logged at warning level and the caller proceeds") and making the silent fail-OPEN invisible to operators tailing INFO+ logs and unreachable for alerting. Promote logger.debug to logger.warning on the synthetic FALLBACK path (runtime.py:2017) and add metrics.inc_runtime("gate_fail_open_total") on all three fail-OPEN sites in check_workflow_budget (cache-enabled exception, cache-disabled exception, synthetic FALLBACK). Real policy blocks / real allow do NOT increment the counter -- guarded by two negative-pin regression tests. New RuntimeMetrics.gate_fail_open_total field (observability/__init__.py) exposed via metrics.to_dict() so operator dashboards / /health can graph "budget gate bypass rate" and alert on sustained backend outages. 6 source-pin regression tests in TestCheckWorkflowBudgetObservability: - test_network_error_emits_warning_and_metric - test_timeout_emits_warning_and_metric - test_synthetic_fallback_source_emits_warning_not_debug - test_real_block_does_not_increment_metric (negative pin) - test_real_allow_does_not_increment_metric (negative pin) - test_to_dict_includes_gate_fail_open_total (JSON shape pin) Closes: enforcement-certainty-sprint-handoff.md (Bug #4, HIGHEST severity) Test count: 1556 passed (+6), 7 skipped. ruff clean. * @ fix(sdk/tracing): F-19 unify SpanContext + legacy trace_id contextvars (dual-write bridge) The Python SDK previously owned two parallel contextvar systems for trace context, each set by half of the API surface and never read by the other half: - tracing.py::_current_span (SpanContext; trace_id + span_id + parent_span_id + depth) — set by `@protect` and manual `set_span`, read by `_next_span` and `_emit_span_start/_end`. - context.py::_trace_id_var / _span_id_var — set by `with workflow(...)` and `with span(...)`, read by `runtime._enrich_event` (cost-event trace_id / span_id / parent_trace_id). Result (`UI-UX-AUDIT-REPORT.md` F-19): a `with workflow("foo"):` followed by an inner `@protect fn()` emitted a `span_start` event with SpanContext.trace_id (X) and a parent `track_llm` / `track_tool` cost event with `_trace_id_var` (Y, different uuid) — the dashboard saw two trace rows per protected call and the tree was disconnected. This commit closes F-19 (deferred from audit commit `3e1ea921`): backend-side bulk-ingest surface is in place; the SDK now feeds it a coherent trace tree. Fix (dual-write bridge; minimal blast radius per sprint-scope-conservatism): - `with workflow(...)` — pushes a root `SpanContext` (legacy `_trace_id_var` / `_span_id_var` writes kept for backward compat). New token-based `reset_span(...)` paired with the legacy resets in `finally`. - `with span(...)` — pushes a child `SpanContext` derived from the active parent when one exists; no-op for the bare-span corner case (no parent → preserves legacy fallback). - `@protect` `_protect_body` — after `set_span(span)`, mirrors `span.trace_id` / `span.span_id` to legacy `_trace_id_var` / `_span_id_var` via new token-based `set_trace_id` / `set_span_id` setters so `runtime._enrich_event` reads the SAME trace id for both span_start and cost events. `finally` resets all four tokens in lockstep. Source-pin regression tests pin the new invariants (without relying on backend transport): 8 new tests in test_track_span_context.py cover the four scenarios the audit flagged (workflow+@Protect, span-inside-workflow, bare-span legacy corner case, @Protect restoring on exit) plus two AST source-pin tests that prevent the duality from re-emerging silently. Verification: - 19/19 tests in test_track_span_context.py pass (11 pre-existing + 8 new F-19 source-pin regressions). - 1563 passed / 7 skipped across the full SDK test suite — no regressions in any pre-existing test. - `ruff check` and `ruff format --check` clean on the three changed files. Wire / contract preservation: - No public API change: `nullrun.workflow`, `nullrun.span`, `get_trace_id`, `get_span_id`, `get_current_span`, `set_span` / `reset_span`, etc. all keep their existing signatures and semantics. - The legacy contextvars remain readable (used by `runtime._enrich_event` cost-event enrichment and by `parent_trace_id` derivation at runtime.py:2967). - `@protect` / `with workflow` / `with span` consumers see no behavior change for the legacy readers; the only new observable is that the SpanContext (read via `get_current_span()`) and the legacy vars now agree on `trace_id` / `span_id` at every nesting level. @ * fix(instrumentation/langgraph): F-28 threading.RLock protects _active_runs UI-UX-AUDIT 2026-08-14 finding F-28: NullRunCallback._active_runs is read/written without synchronisation on multi-threaded LangChain runners (and on free-threaded CPython PEP 703 builds). Two callbacks on different threads can interleave on_chain_start / on_chain_end in ways that orphan the span_end lookup (parent_span_id on the wire doesn't match anything in the dict). Fix: wrap every read/write of _active_runs in with self._lock: (threading.RLock) RLock (not Lock) is required because _begin_run -> _register_active_run nests two acquisitions on the same thread — reentrant acquisition is the entire point. Five access sites wrapped: 1. _register_active_run (insert + cap-check eviction) 2. on_llm_start parent_ctx lookup 3. on_llm_end llm_ctx lookup 4. _begin_run parent_ctx lookup 5. _end_run pop Trade-off: the lock briefly spans runtime.track_event. Per callback that's one outbound HTTP round-trip holding the lock; acceptable because the Lock protects ONE NullRunCallback's dict (not all of them) and concurrent chains on the SAME callback are rare. Documented inline at __init__ so a future maintainer doesn't 'optimise' it away. Regression: tests/test_langgraph_callback_race.py (5 tests): - test_active_runs_lock_is_rlock : reentrant acquire from this thread - test_active_runs_protected_under_concurrent_register : 200 iter, 2 threads, cap=64 - test_active_runs_protected_under_register_end_race : register + pop race - test_active_runs_lock_does_not_deadlock_on_nested_register : nested acquire - test_register_then_end_round_trip : canonical happy-path sanity Verification: pytest tests/test_langgraph_callback_race.py tests/test_lru_active_runs.py tests/test_langgraph_callback.py -q: 54 passed ruff check src/nullrun/instrumentation/langgraph.py tests/test_langgraph_callback_race.py: All checks passed pytest tests/ -q (excluding integration): 1568 passed, 7 skipped * fix(instrumentation/auto): F-29 async _emit falls back to request-body model UI-UX-AUDIT 2026-08-14 finding F-29: NullRunAsyncTransport._emit stopped at usage.get('model') only — when the upstream Anthropic or OpenAI streaming response omitted a top-level model field, the emitted llm_call event had model=None, which the wire-format builder dropped, which the backend then unwrap_or('default')'d to DEFAULT_RATE. Net effect: silent zero-billing for async streaming clients. Fix: mirror the sync path's fallback chain at auto.py:882-885: model_for_event = ( usage.get('model') or _extract_model_from_request_body(request) ) _extract_model_from_request_body is a module-level pure-sync helper that reads request.content + json.loads — safe to call from the async event loop (no I/O, no blocking). The response body is tried first; the request body is the fallback when the response omits the field. The pre-fix comment at lines 967-971 explicitly noted 'async path doesn't have the request-body model fallback yet' — that comment is now stale and replaced with F-29 context. Regression: tests/test_model_fallback_async.py (3 tests): - test_async_transport_falls_back_to_request_body_model : main F-29 case - test_async_transport_prefers_response_body_model : response wins when both - test_async_transport_emits_none_when_neither_source_has_model : corner case Verification: pytest tests/test_model_fallback.py tests/test_model_fallback_async.py tests/test_streaming_oom_cap.py: 17 passed ruff check src/nullrun/instrumentation/auto.py tests/test_model_fallback_async.py: All checks passed pytest tests/ -q (excluding integration): 1571 passed, 7 skipped * chore(release): 0.15.2 — observability + UI-UX-AUDIT 2026-08-14 closure Patch release bundling the 5 commits accumulated since 0.15.1: - 1c96654 — check_workflow_budget fail-OPEN observability closure (sprint handoff Bug #4): synthetic FALLBACK path now logs at WARNING (not DEBUG), and a new gate_fail_open_total counter fires on all three fail-OPEN sites. - 9b87d20 — F-19 SpanContext ↔ legacy trace_id/span_id contextvars now form a single coherent trace tree. Pre-0.15.2 inner @Protect fn() inside a with workflow("foo") block emitted two disconnected trace rows on the dashboard. - 127b003 — F-28 NullRunCallback._active_runs now protected by threading.RLock; five access sites wrapped, reentrant for _begin_run → _register_active_run nesting. - 0f86c8c — F-29 NullRunAsyncTransport._emit now falls back to the request body model field when the upstream Anthropic / OpenAI streaming response omits it. Closes silent-zero-billing bug for async streaming clients. - 35728b9 — removed flaky test_env_fallback_when_server_value_is_zero; the contract is covered by composition of test_validate_approval_timeout_rejects_below_min + test_env_fallback_when_response_omits_field (both deterministic). Verification: - pytest: 1571 passed, 7 skipped in 103.85s - ruff: clean - mypy: clean (37 source files) Compatibility: No SDK_MIN_VERSION bump. No public API change. No wire-format change. Drop-in replacement for 0.15.1.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Why
How
Test plan
cd backend && cargo test,cd frontend && npm test)cd frontend && npm run lint)cd frontend && npm run type-check)Risk
Checklist
CONTRIBUTING.md(if present)