Skip to content

chore(release): 0.16.2 — F01 + F03 + F5 closure - #93

Merged
maltsev-dev merged 4 commits into
masterfrom
release/0.16.2
Aug 23, 2026
Merged

chore(release): 0.16.2 — F01 + F03 + F5 closure#93
maltsev-dev merged 4 commits into
masterfrom
release/0.16.2

Conversation

@maltsev-dev

Copy link
Copy Markdown
Member

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 from master per the release policy, not from release/0.16.1):

a6ed5e8 chore(release): 0.16.2 — bump version, update tests for UUID v4 + F03
55260e5 fix(sdk): populate _call_tools_var in decorator path (DEF-LATEST_PLAN-F03)
be9265a fix(sdk): client-side UUID v4 validation for chain_id (F5, qa/sdk_checks 2026-08-21)
f4826b2 fix(sdk): populate tools array on /execute wire body (DEF-LATEST_PLAN-F01)

Fixes

  • f4826b2 — F01 (DEF-LATEST_PLAN-F01). Runtime.execute() now reads get_call_tools() and conditionally adds tools=list(...) to the /execute wire body. Mirrors the existing /gate path. Closes the 2026-08-21 gap where /execute payloads silently dropped the tools field.
  • be9265a — F5 (qa/sdk_checks 2026-08-21). src/nullrun/context.py::_validate_chain_id uses uuid.UUID(s).version == 4. Rejects nil UUID, max UUID, and any non-v4 strings (including the previous "chain-1" test fixtures). Wired into both chain() context manager and set_chain_id() — raises ValueError before the contextvar mutation, so no leak on rejection.
  • 55260e5 — F03 (DEF-LATEST_PLAN-F03). _protect_body now 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 getting TOOL_BLOCKED (rule_kind: "policy_cache_miss" / no_tools_field) before the approval_rule_eval step could fire.
  • a6ed5e8 — release prep. Bumps pyproject.toml [project].version and src/nullrun/__version__.py from 0.16.10.16.2. Locks [Unreleased][0.16.2] - 2026-08-23 in CHANGELOG.md with an extended blurb naming all three defects. Test fixtures updated to use str(uuid.uuid4()) where the new validator would otherwise reject them. Token type-parameterized on _call_tools_var.set(...) to keep mypy --strict clean.

Verification

pytest       1598 passed, 7 skipped, 0 failed   (95.02s)
ruff check   All checks passed!
mypy         Success: no issues found in 37 source files

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_persists
  • tests/test_v3_wire_contract.py::TestChainContextHelpers::test_chain_contextmanager_rejects_invalid_op
  • tests/test_v3_wire_contract.py::TestChainContextHelpers::test_chain_nested_restores_outer_on_exit
  • tests/test_v3_wire_contract.py::TestGateCacheRuntimeFlow::test_chain_mode_collapses_calls_to_one_roundtrip
  • tests/test_v3_wire_contract.py::TestGateCacheRuntimeFlow::test_chain_mode_emits_fresh_uuid7_execution_id_per_call
  • tests/test_v3_wire_contract.py::TestGateCacheRuntimeFlow::test_chain_mode_disabled_via_env_bypasses_cache

Why release/0.16.2 and not 0.16.1

0.16.1 shipped 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 to 0.16.2 keeps 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.py reads only pyproject.toml [project].version, so the wheel METADATA will carry 0.16.2 automatically. CI uploads via pypa/gh-action-pypi-publish Trusted Publishing to TestPyPI on tag, then Production on the release event (see .github/workflows/publish.yml).

…-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

codecov Bot commented Aug 23, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@maltsev-dev
maltsev-dev merged commit 6c857b2 into master Aug 23, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

1 participant