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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant