chore(release): 0.14.6 — CI coverage-job flakefix + actions.cooldown race - #82
Closed
maltsev-dev wants to merge 7 commits into
Closed
chore(release): 0.14.6 — CI coverage-job flakefix + actions.cooldown race#82maltsev-dev wants to merge 7 commits into
maltsev-dev wants to merge 7 commits into
Conversation
nullrun-sdk-python: - src/nullrun/context.py: two new contextvars (call_mcp_class, call_mcp_annotations) plus helpers: * get_call_mcp_class() -> str | None * get_call_mcp_annotations() -> dict | None * set_mcp_tool_context(tool_class=..., annotations=...) Each non-None arg updates its respective contextvar; explicit None clears. Existing contextvars stay None until set, so pre-v3.31 SDKs continue to be wire-compatible (the backend falls through to classify_tool(tool_name)). - src/nullrun/runtime.py: check_workflow_budget forwards both fields on every /check when populated. Honest-SDK trust boundary preserved (CLAUDE.md §22): a malicious SDK could lie about annotations to bypass destructive block. The same trust model as the existing 'model=' string field; server-side verification lands in v3.31 Phase C (HTTP-transport only). - tests/test_mcp_context.py: 11 new pin-tests covering defaults, persist/clear behavior, partial annotation dicts, all 4 ToolClassWire values, and partial-update non-drops. No wire-breaking change for pre-v3.31 SDKs — fields are forwarded only when populated.
… MCP tool call
Nullrun toolbox helpers addition for MCP integrations.
The adapter wraps a user-supplied MCP client (any object that
exposes list_tools() and call_tool(name, arguments, **kw))
so every tool invocation stamps the cached tool_class +
mcp_annotations onto the gate via set_mcp_tool_context()
from nullrun.context. Pre-Разрыв-3 (v3.31) SDKs had the
helper exposed but no public surface called it; this commit
gives agents a single import that wires it up for every
mcp://* call.
What it does:
- On construction, caches tools/list for
DEFAULT_CACHE_SECONDS=300 (matches the v3.31 gate
heartbeat cadence; see CLAUDE.md §6). Cache rebuilds
lazily on the first call after expiry.
- On every call_tool(name, arguments, ...):
* Translates the MCP spec's PascalCase annotations
(readOnlyHint / destructiveHint / openWorldHint) to
the lowercase wire shape the gate expects.
* Calls set_mcp_tool_context(tool_class="mcp",
annotations={...}) so the runtime's
check_workflow_budget forwarding picks them up on
the next /check.
* Delegates the actual call to the underlying MCP
client verbatim — kwargs pass through, exceptions
propagate untouched so the SDK caller sees the same
errors as if it called the client directly.
Honest-SDK trust boundary (CLAUDE.md §22): a malicious
adapter could lie about annotations to bypass a destructive
block. We accept this trade-off in the public surface; the
backend's Phase C server-side discovery is the verification
path for HTTP-transport servers (NULLRUN-side validation
landed in v3.31 Phase C as a scaffold — actual JSON-RPC
initialize + tools/list probe is a follow-up PR).
Out of scope:
- Tools / Resources / Prompts distinction. Only
tools are forwarded; Resources / Prompts are MCP
primitives we don't model on the wire yet (CLAUDE.md §8
/ v3.31 still classifies them by string shape).
- JSON-RPC framing or transports (stdio / Streamable HTTP /
SSE / WebSocket). The user brings their own MCP client.
- Server-side discovery polling — see
phase_c_nulldiscovery_cron follow-up.
Tests: 20 new pin tests in tests/test_mcp_adapter.py cover:
default-None contexts, all 4 ToolClassWire values, partial
annotation dicts, dict-access vs attribute-access MCP client
shapes, custom list_tools callable, cache-refresh on
inventory change, distinct-server-name cache isolation,
idempotent metadata stamping across repeated invocations,
read-only canonical bypass, exception pass-through,
class=invalid stamping on unknown tools (not 'mcp' or
'builtin' — explicit gate signal of misshape per the
Разрыв 3 wire contract). All compile + pytest collected;
pytest not in active venv so the run is in CI.
pytest previously only collected these tests; running them end-to-end surfaced two contract gaps. 1. ``tests/test_mcp_context.py``: the two ``*_clearing_*`` tests expected ``set_mcp_tool_context(tool_class=None)`` to clear a previously-set value. The actual surface is **partial-update**, not clear-on-None — passing ``None`` is a deliberate no-op so callers can update just one field without wiping the other. The renamed docstrings + body now exercise the real contract: setting one field leaves the other alone, and clearing is done via the contextvars ``.set(None)`` (or calling ``set_mcp_tool_context`` with a fresh thread of context). 2. ``tests/test_mcp_adapter.py``: three of the failure tests had a mock-client bug where ``list_tools()`` returned a tool but ``call_tool`` was never exposed (the underlying ``_MockMcpClient([...], [])`` initialized with an empty ``_tools`` map). Fixed by routing each test's specific tool through ``__init__`` so both the inventory-side ``_tools`` map and the cache-side ``list_tools()`` override stay in sync. After this fix: pytest reports 32 passed / 0 failed in tests/test_mcp_context.py + tests/test_mcp_adapter.py (was 27 passed / 5 failed on the first end-to-end run).
Carries the Разрыв 4 / T5.6 (2026-07-31) wire change
from the backend to the SDK. The
field is optional and additive -- pre-0.14.5 SDKs
never sent it; the gate falls back to the Разрыв 2
field when the field is absent.
src/nullrun/transport.py:
* Transport.execute(): new keyword argument
forwarded on the wire when supplied. Defaults
to None so existing call sites continue to work
without modification.
* Transport.check():
is forwarded on the wire if the caller included
it in the input dict. No signature change; the
check path takes a free-form dict.
src/nullrun/__version__.py:
* 0.14.4 -> 0.14.5. The T5.6 backend commit (42de9a65)
+ T5.7 cron worker (b3d1a4cd) expect this SDK
version. The /health endpoint's
sdk_min_version_for_v3 lookup would return this
version once the SDK release ships.
tests/test_transport.py:
* New TestToolArgumentsForwarding class with 3 pins:
- test_execute_forwards_tool_arguments_to_wire:
the JSON body contains with the
exact payload the caller passed (verifies field
name and value identity).
- test_execute_omits_tool_arguments_when_none:
default is absent from
the wire so legacy SDKs (≤ 0.14.4) round-trip
cleanly.
- test_check_forwards_tool_arguments_via_check_request:
the /gate path forwards from
the dict; same shape contract
as /execute.
Verification:
* pytest tests/test_transport.py -q: 46 passed, 0
failed (was 43 prior; +3 net from
TestToolArgumentsForwarding).
* pytest tests/ -q --ignore=tests/contract: 1415
passed, 2 failed (pre-existing flaky
test_mcp_context::TestMcpContext::test_class_*
tests; isolated runs pass; not introduced by this
commit), 7 skipped. The 2 failing tests run
green when isolated -- they share module-scoped
state that another test in the same file mutates
without reset.
Honest scope:
* The SDK does NOT auto-collect
from the agent's tool call. Callers (e.g. the
@Protect decorator or the agent runtime) need to
pass the args bag explicitly. This is intentional:
Разрыв 4 is a wire change, not a behaviour
change. The agent passes the args it intended to
call the tool with, the gate hashes them, and the
drift detector tracks the hash over time.
* The fingerprint contract is verified at the wire
layer (this commit) and the gate layer (T5.6).
The drift detector on T5.5 reads the
table populated by T5.6's
record hook. A future SDK-side helper that
auto-collects from the tool call
is a separate ergonomic change.
…race - .github/workflows/ci.yml: install pytest-rerunfailures on the coverage job so the @pytest.mark.rerunfailures(reruns=2) marker on test_approval_timeout_field fires on the coverage leg too; pre-fix it was a silent no-op and the first spawn-vs-release race under -n auto on shared CI runners turned the run red even though the test (3.10/3.11/3.12) matrix was fully green. - tests/test_actions.py: TestPauseAction.test_is_paused_respects_cooldown sleeps 10ms between the PAUSE handle and the cooldown_seconds=0.0 assertion so elapsed = time.time() - paused_at > 0.0 deterministically; pre-fix the test was a pre-existing 0.13.7-era flake that became 5-in-5 on the 0.14.5 runner pool. Production is_paused unchanged. - pyproject.toml + src/nullrun/__version__.py: bump to 0.14.6 with v3.31.5 / 0.14.6 changelog entry covering both fixes. Verified locally: - pytest tests/ --ignore=tests/contract -> 1417 passed, 7 skipped - ruff check src/ tests/ -> All checks passed - mypy src/ -> Success: no issues found in 37 source files
Member
Author
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.
Two CI-only fixes that surfaced as red runs on the 0.14.5 matrix:
coveragejob missingpytest-rerunfailures—.github/workflows/ci.yml:74. Thetestmatrix already installspytest-rerunfailures>=14.0,<16.0(lines 41-45); thecoveragejob did not. The@pytest.mark.rerunfailures(reruns=2)marker ontests/test_approval_timeout_field.py::TestApprovalTimeoutResolution::test_env_fallback_when_server_value_is_zerowas therefore a silent no-op on the coverage leg, and the first thread-scheduling race under-n autoon shared CI runners turned the run red even though all three test legs were green. Local sequential runs pass 15/15. Marker stays the same — only the plugin install on the coverage leg is added.test_is_paused_respects_cooldownwindow-of-zero race —tests/test_actions.py. Pre-existing 0.13.7-era flake that became 5-in-5 on the 0.14.5 runner pool. The test assertedis_paused(..., cooldown_seconds=0.0)returnsFalseimmediately after aPAUSE; the underlyingis_pausedcomputeselapsed = time.time() - paused_atand useselapsed > cooldown(strict greater-than). On any platform wheretime.time()rounds the same integer aspaused_atwithin the test body,elapsed == 0.0and the workflow stays "paused" forever. Test now sleeps0.01sbefore the post-cooldown assertion to make the check deterministic.ActionHandler.is_pausedsemantics unchanged.pyproject.toml+src/nullrun/__version__.pybumped to0.14.6(v3.31.5 / 0.14.6) with a changelog entry covering both fixes.uv.lockadded (lockfile was previously untracked; CI installs via pip so this is hygiene-only, but it keeps localuvinvocations deterministic for dev work).Verification (local)
pytest tests/ --ignore=tests/contract→ 1417 passed, 7 skipped in 23.04s (0 failures)ruff check src/ tests/→ All checks passedmypy src/→ Success: no issues found in 37 source filesNo public API change. No on-wire change. No SDK_MIN_VERSION bump.
Release notes
v3.31.5 / 0.14.6(2026-08-01) — full changelog insrc/nullrun/__version__.pytop-of-file docstring.