chore(release): 0.16.2 — F01 + F03 + F5 closure - #93
Merged
Conversation
…-F01)
The /gate path already threads the per-call `tools` contextvar onto
the wire body via `check_workflow_budget`. The /execute path missed
this — `Runtime.execute()` built `execute_kwargs` without reading
the contextvar, so backend's Step 3 tool_block check
(`orchestrator.rs:1847-1893`) returned
`Block { TOOL_BLOCKED, reason: "no_tools_field" }` whenever the
workflow's effective `policy.tool_patterns` was non-empty.
The /execute path is what @sensitive-decorated functions follow.
The `_enforce_sensitive_tool` decorator already passes
`tools=get_call_tools()` to `runtime.execute()`; this fix closes
the runtime/transport leg of that handoff.
Fix scope (3 src files):
- runtime.py: capture `get_call_tools()` contextvar, conditionally
add `tools` to execute_kwargs when set (preserves absence for
backward compat)
- transport.py: add `tools` kwarg to `Transport.execute` signature,
forward to wire body
- decorators.py: import `get_call_tools` and forward to
`runtime.execute(...)` via kwarg
Tests:
- 3 behavioural tests (respx-mocked /execute): tools propagated,
omitted when unset, cleared on set_call_context(tools=[])
- 2 source-pin regression tests: `tools=get_call_tools()` kwarg
literal in decorators.py + import of get_call_tools preserved
Refs: CLAUDE.md §8 (canonical tool name format + ToolBlock rules),
LATEST_PLAN.20260821-140626.journal.md (DEF-LATEST_PLAN-F01).
…cks 2026-08-21) Closes the MEDIUM finding from sdk_checks_.md §3.5 (2026-08-21). Pre-fix the chain() context manager and set_chain_id() setter accepted any string (the docstring at line 813 even said 'UUID v4 (or any unique string)'). The backend does NOT validate chain_id format — non-UUID chain_ids silently auto-register as new ACTIVE chains. The backend's chain race guard (HGET chain_key 'org_id' per CLAUDE.md §6 Q2) only fires when the chain_id already exists; for a NEW chain_id the SDK gets a fresh ACTIVE acceptance regardless of format. Fix: add _validate_chain_id helper using uuid.UUID(s).version == 4 check. Wired into chain() ctx mgr and set_chain_id() — raises ValueError on malformed input BEFORE the contextvar is mutated (no leak into outer scope). Surface to UUID v4 stricture matches the backend's documented contract (CLAUDE.md §6). Tests: 14 pytest tests in tests/test_chain_id_uuid_v4.py cover UUID v4 acceptance, nil UUID + all-ones UUID rejection, non-v4 version rejection (v1/v3/v5/v7), short/malformed/non-string rejection, context manager integration, set_chain_id integration, and reset-after-invalid-leak prevention. All 14 pass on ============================= test session starts ============================= platform win32 -- Python 3.14.2, pytest-9.0.2, pluggy-1.6.0 rootdir: C:\Users\Anatolii Maltsev\Documents\AGENTIC\nullrun-sdk-python configfile: pyproject.toml plugins: anyio-4.12.1, langsmith-0.7.14, asyncio-1.3.0, base-url-2.1.0, cov-7.0.0, playwright-0.7.2, respx-0.23.1 asyncio: mode=Mode.AUTO, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function collected 14 items tests\test_chain_id_uuid_v4.py .............. [100%] ============================= 14 passed in 0.27s ==============================. Refs: CLAUDE.md §6 (chain_id MUST be UUID v4); sdk_checks_.md §3.5 F5. Per scripts-commit-no-push: local commit only, no push.
…-F03) F01 fix (e70e55d) wired Runtime.execute to forward tools from _call_tools_var, but no internal SDK code ever populated that contextvar — only set_call_context (the public API) wrote to it, and grep -rn set_call_context has zero internal callers. Result: every @Protect / @sensitive call hit /gate (and /execute) without tools=[...], so backend Step 3 tool_block returned TOOL_BLOCKED (rule_kind: 'policy_cache_miss' / no_tools_field) BEFORE any approval-rule evaluation could fire. Fix: 1. _protect_body seeds _call_tools_var = (fn.__name__,) token-based before check_control_plane(); resets in finally. Skips when an outer set_call_context(tools=[...]) is already in effect, so explicit user intent wins. 2. Runtime.execute gains explicit kwarg so the F01 source-pin thread-through doesn't TypeError if /execute is reached. 3. TestDecoratorF03BehavioralRegression (4 new tests, all pass): - @Protect populates tools=['fn_name'] on /gate body - @Protect does not override explicit set_call_context - @Protect restores prior contextvar on exit (token reset) - @sensitive @Protect populates tools=['fn_name'] on /execute body Surfaced by LATEST_PLAN.20260822-181500-a3f1 (TC-SDK-014..017 all TOOL_BLOCKED; TC-OBS-007 pending_count=0). Memory updated; no push.
Release-prep patch on top of the three F01 / F03 / F5 commits already
cherry-picked onto release/0.16.2:
- pyproject.toml + src/nullrun/__version__.py: 0.16.1 → 0.16.2.
- CHANGELOG.md: lock [Unreleased] to [0.16.2] - 2026-08-23 and extend
the blurb to call out F03 + F5 closure alongside F01.
- src/nullrun/decorators.py: parameterize Token[tuple[str, ...]] on the
_call_tools_var.set() token so mypy --strict is clean (Token is
contextvars.Token, generic on the variable's value type).
- tests/test_chain_id_uuid_v4.py: ruff --fix I001 reorder (in-function
imports sorted alphabetically; blank-line trim). No behavior change.
- tests/test_v3_wire_contract.py:
- test_set_chain_id_persists: replace "chain-1" literal with
str(uuid.uuid4()) (chain_id now validated as UUID v4).
- test_chain_contextmanager_rejects_invalid_op: supply a valid UUID v4
+ invalid op so the op-rejection assertion is not shadowed by the
chain_id-validator.
- test_chain_nested_restores_outer_on_exit: two UUID v4 literals
for outer / inner scope.
- TestGateCacheRuntimeFlow (3 tests): chain("chain-runtime-cache" /
"chain-runtime-uuid7" / "chain-no-cache") → chain(str(uuid.uuid4()))
so the cache / no-cache / uuid7 wire assertions don't trip the new
strict validator.
Verified: pytest 1598 passed / 7 skipped; ruff clean; mypy clean on
src/nullrun. No wire-format change. The three feature commits
(f4826b2 / be9265a / 55260e5) remain the source of truth for F01 / F5 /
F03 behavior.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
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.
release/0.16.2 — F01 + F03 + F5 closure
Patch release. No wire-format breaking changes.
What's in
Four commits on top of
origin/master(release branch was cut frommasterper the release policy, not fromrelease/0.16.1):Fixes
f4826b2— F01 (DEF-LATEST_PLAN-F01).Runtime.execute()now readsget_call_tools()and conditionally addstools=list(...)to the/executewire body. Mirrors the existing/gatepath. Closes the 2026-08-21 gap where/executepayloads silently dropped the tools field.be9265a— F5 (qa/sdk_checks 2026-08-21).src/nullrun/context.py::_validate_chain_idusesuuid.UUID(s).version == 4. Rejects nil UUID, max UUID, and any non-v4 strings (including the previous"chain-1"test fixtures). Wired into bothchain()context manager andset_chain_id()— raisesValueErrorbefore the contextvar mutation, so no leak on rejection.55260e5— F03 (DEF-LATEST_PLAN-F03)._protect_bodynow seeds_call_tools_var = (fn.__name__,)via a token-based set so nested@protect(or@sensitive) calls restore the outer contextvar on reset. Populates the per-call tools contextvar that the F01 fix already forwards — the two together unblock TC-SDK-014/015/016/017 (approval-rule probes) which were gettingTOOL_BLOCKED(rule_kind: "policy_cache_miss"/no_tools_field) before the approval_rule_eval step could fire.a6ed5e8— release prep. Bumpspyproject.toml [project].versionandsrc/nullrun/__version__.pyfrom0.16.1→0.16.2. Locks[Unreleased]→[0.16.2] - 2026-08-23inCHANGELOG.mdwith an extended blurb naming all three defects. Test fixtures updated to usestr(uuid.uuid4())where the new validator would otherwise reject them.Tokentype-parameterized on_call_tools_var.set(...)to keepmypy --strictclean.Verification
The five originally-failing tests after cherry-pick of
be9265a(validator) onto this branch are all green now:tests/test_v3_wire_contract.py::TestChainContextHelpers::test_set_chain_id_persiststests/test_v3_wire_contract.py::TestChainContextHelpers::test_chain_contextmanager_rejects_invalid_optests/test_v3_wire_contract.py::TestChainContextHelpers::test_chain_nested_restores_outer_on_exittests/test_v3_wire_contract.py::TestGateCacheRuntimeFlow::test_chain_mode_collapses_calls_to_one_roundtriptests/test_v3_wire_contract.py::TestGateCacheRuntimeFlow::test_chain_mode_emits_fresh_uuid7_execution_id_per_calltests/test_v3_wire_contract.py::TestGateCacheRuntimeFlow::test_chain_mode_disabled_via_env_bypasses_cacheWhy release/0.16.2 and not 0.16.1
0.16.1shipped a wire-shape fix (action_digest) without an SDK release; this branch adds three independent SDK-side fixes that also have to ship before the next backend deploy. Bumping to0.16.2keeps the patch train monotonic and avoids forcing the backend release notes to reference a version that never went out.Roll-out
Tag once green.
hatch_build.pyreads onlypyproject.toml [project].version, so the wheel METADATA will carry0.16.2automatically. CI uploads viapypa/gh-action-pypi-publishTrusted Publishing to TestPyPI on tag, then Production on thereleaseevent (see.github/workflows/publish.yml).