Commit bc34d61
authored
chore(release): 0.14.4 — ToolParameters Approval Rules wire contract (#80)
* some clean up
* fix(runtime): treat websocket cancellation as clean shutdown
* fix(transport): approval_resolved callback is synchronous
* SdkTrackRequest
* chore(release): 0.14.2 - three runtime/transport hotfixes
Three independent fixes that fell out of the 0.14.1 demo run,
plus a stub refresh on the two _RecordingRuntime mocks whose
shape the new @Protect emit broke.
* fix(decorators): @Protect now emits a tools/track_tool event
after the wrapped body returns. Pre-0.14.2 the protected
decorator only fired the gate check and skipped the
bookkeeping emit, so the dashboard never saw a protect
execution. The emit goes through the same sink as llm_call
events so it picks up the dedup LRU at runtime.track() for
free.
* fix(runtime): track_tool event carries tokens: 0 and a fresh
uuidv7 execution_id. The backend's SdkTrackRequest requires
both fields as non-Optional u64 / string; pre-0.14.2 the
event dict only carried type / tool_name / is_retry and the
deserializer rejected it. Span lifecycle events get the same
tokens: 0 default via _enrich_event.
* fix(transport): approval-resolved WS callback is now a plain
sync function. The WebSocket dispatch path invokes it as a
dict -> None callable; the previous async-decorated
coroutine was silently dropped, so the sync threading.Event
inside _wait_for_approval_resolution never got set on the
first approval round-trip - the demo's first approval hung
forever. Caught 2026-07-24.
* fix(runtime): treat websocket cancellation as a clean
shutdown signal. WebSocketConnection.close() cancels the
receive task to unblock the waiter during normal end of
session; on Python 3.11+ CancelledError derives from
BaseException, so the old except Exception branch re-raised
it and produced a noisy debug line on every clean exit.
The new except CancelledError branch is silent and the
finally cleanup still runs.
* test: refresh _RecordingRuntime in tests/test_protect.py
and tests/test_preflight_fail_policy.py with a track_tool
stub. The previous shape only mocked track_event, which
is why the @Protect emit silently failed under the new
decorator wiring.
* chore: ruff format on the three source files touched by
this release (decorators / runtime / transport). The
format-only reformat of the 65 unrelated files is
intentionally deferred to a separate PR.
* docs: 0.14.2 changelog entry describing the four fixes
end-to-end.
No SDK_MIN_VERSION bump. No public API change. No on-wire
breaking change. Backends on 1.0.0 keep working unchanged.
Verification:
- pytest -n auto -> 1369 passed, 7 skipped, 29 warnings.
- ruff check src/ tests/ -> All checks passed.
- mypy src/nullrun --strict -> Success: no issues found
in 36 source files.
* feat(sdk): ToolParameters phase 1 wire contract -- auto-attach ToolParamsExtractor on bare @sensitive
Phase 1 / MVP 1.1 (Tier 2 / Razryv 2 follow-up). The backend
already accepts BusinessImpact::ToolCall(ToolCallParams) on
the /execute wire (commit 1e501cd6 in the backend repo). This
commit wires the SDK-side path so users get ToolParameters
Approval Rules by default with no decorator changes:
@sensitive
@Protect
def delete_user(user_id: int, force: bool = False): ...
The above now ships BusinessImpact(kind='tool_call',
tool_name='delete_user', params={'user_id': ..., 'force': ...})
on every /execute call, matched against ToolParameters rules
on the backend. No new decorator argument required.
What ships:
- business_impact.py: ToolCallParams dataclass mirrors the
backend struct (tool_name <= 128 bytes, param_name <= 64,
JSON-roundtrippable values only). BusinessImpact.kind now
discriminates Money | ToolCall. New factory
BusinessImpact.tool_call(...) for hand-built impacts.
- extractor.py: ToolParamsExtractor class + tool_params()
factory (by analogy with MoneyImpactExtractor + money_outflow).
Three modes: explicit {rule_param: arg_name} map, include_all
(default, every kwarg), or empty (include_all=False with no
map). JSON-unsafe values (float, custom objects) and PII-masked
sentinels ("***") are filtered before the wire.
- decorators.py: _enforce_sensitive_tool dispatch now handles
both MoneyImpactExtractor and ToolParamsExtractor. NR-B003
error hint branches by extractor type so the operator sees
the right remediation advice.
- decorators.py: _do_sensitive_register auto-attaches a default
ToolParamsExtractor(include_all=True) on bare @sensitive. An
explicit @sensitive(impact=money_outflow(...)) wins -- the
auto-attach only fires when no extractor is present. The
stamp uses _stamp_extractor_on_innermost so the bare function
(the one @Protect captures as fn) carries the attribute, not
just the @Protect wrapper (the 2026-07-24 root-cause fix).
- tests/test_tool_params_extractor.py: 19 tests covering factory
shape, three extraction modes, PII sentinel filtering, JSON
round-trip, fail-CLOSED on backend rejection, action_digest
byte-identity with the backend's canonical JSON, the
auto-attach wiring, the auto-attach-vs-explicit-extractor
priority, and the dataclass validator.
Wire-contract compatibility:
- Default SDK behaviour for bare @sensitive CHANGED: was
'no business_impact on wire', now 'kind=tool_call on wire'.
Operators who relied on the Phase 0 path (approval_id-only
grant consume) must either pass
@sensitive(impact=tool_params(include_all=False)) explicitly
or accept the ToolParameters wire shape.
- Existing @sensitive(impact=money_outflow(...)) callers are
unaffected: the explicit extractor wins over the auto-attach.
- Legacy 'no impact extractor' test_sensitive_extractor.py
fixture (registers the tool manually, bypassing the
decorator) still passes because auto-attach is only wired
through _do_sensitive_register -- the @sensitive decorator
path. Users who registered sensitive tools via
rt.add_sensitive_tool(name) directly are unaffected.
Verification:
- tests/test_tool_params_extractor.py: 19 passed
- tests/test_sensitive_extractor.py: 5 passed (regression check)
- tests/test_business_impact.py: 19 passed
- tests/test_extractors.py: 35 passed
- tests/test_protect.py + test_protect_branches.py +
test_execute_approval_flow.py + test_approval_money_flow.py
+ test_gate_real_path.py + test_handle.py: 99 passed
- tests/test_runtime.py + test_runtime_branches.py +
test_init_contract.py: 70 passed, 1 skipped
Per project rhythm: local commit only, no push.
Refs:
- backend BusinessImpact::ToolCall variant:
backend/src/proxy/gate/business_impact.rs:62-307
- backend Razryv 2 / Tier 1+2 commits: 1e501cd6, 63ba9f6a
- Test companion for Phase 1 / MVP 1.0 money:
tests/test_sensitive_extractor.py
- Plan: fix-plan.md P1-2 (Phase 1 trust_level enum -- now
unblocked once this commit lands and operators actually
deploy ToolParameters rules).
* fix(sdk): auto-attach chain walk -- preserve explicit impact=tool_params() map
Ad-hoc verification after the initial commit (40d391a)
surfaced a silent regression in the auto-attach path:
@sensitive(impact=tool_params({"delete_force": "force"}))
@Protect
def delete_user(force, user_id): ...
Before this fix, the wire payload for this function used the
auto-attach DEFAULT (every kwarg, no rename) instead of the
explicit map the user wrote. Root cause: ``_do_sensitive_register``
called ``getattr(fn, "_nullrun_extractor", None)`` on the @Protect
WRAPPER, but ``@sensitive(impact=...)`` factory form stamps the
explicit extractor on the BARE function via
``_stamp_extractor_on_innermost``. The wrapper itself has no
attribute, so the check returned None and the auto-attach path
silently overwrote with the default ToolParamsExtractor(
include_all=True). The user's explicit param_extractors map was
discarded without warning.
Fix: walk the ``__wrapped__`` chain in
``_do_sensitive_register`` via a new helper
``_find_extractor_in_chain``. The walk is bounded (32 hops) to
defend against pathological ``__wrapped__`` cycles and returns
the FIRST extractor found or None. Behavior:
* ``@sensitive`` bare -> chain walk finds nothing,
auto-attach default wins.
* ``@sensitive(impact=money_outflow(...))`` -> chain walk finds
the MoneyImpactExtractor stamped
on the bare function; auto-attach
skips.
* ``@sensitive(impact=tool_params({...}))`` -> chain walk finds
the explicit map; auto-attach
skips.
Wire contract for end users:
* ``@sensitive(impact=tool_params({"force_param": "force"}))``
on a function with ``force: bool, user_id: int`` kwargs now
sends ``{force_param: <bool>}`` on the wire (the renamed
key) instead of the previous broken
``{force: <bool>, user_id: <int>}``.
* Bare ``@sensitive`` continues to ship
``{force: <bool>, user_id: <int>}`` on the wire (unchanged
from commit 40d391a).
* ``@sensitive(impact=money_outflow(...))`` is unaffected
(chain walk finds the MoneyImpactExtractor, auto-attach
skips).
Regression tests (4 new in
``tests/test_tool_params_extractor.py::TestAutoAttachChainWalk``):
* ``test_bare_sensitive_chain_walk_attaches_default``:
pin the bare-form auto-attach path.
* ``test_explicit_tool_params_chain_walk_preserves_map``:
the regression case -- explicit ``impact=tool_params({...})``
must NOT be overwritten.
* ``test_explicit_money_outflow_chain_walk_preserved``:
the original Phase 1 / MVP 1.0 money variant must NOT
be overwritten (regression on the regression).
* ``test_chain_walk_does_not_loop_on_circular_wraps``:
defensive -- a pathological ``__wrapped__`` cycle (a -> a
or a -> b -> a -> b) returns None within the bounded hop
count without hanging.
Verification:
- cargo check equivalent: ``.venv-ci/Scripts/python.exe -m pytest``
on the full touched surface (160 tests across
test_tool_params_extractor, test_sensitive_extractor,
test_business_impact, test_extractors, test_protect,
test_protect_branches, test_execute_approval_flow,
test_approval_money_flow) -- 160 passed, 0 failed.
- ad-hoc verification script at
``C:/Users/ANATOL~1/AppData/Local/Temp/hermes-verify-toolparams.py``
confirms all three decorator variants wire the correct
extractor type with the right map.
Local commit only; no push.
Refs:
- Original commit (the regression): 40d391a
- Ad-hoc verifier that surfaced the regression:
``hermes-verify-toolparams.py``
* test(sdk): ToolCall cross-language parity pin against Rust golden hex
Pins the SDK compute_action_digest(BusinessImpact.tool_call(...))
to the same hex literal the Rust backend asserts in
backend/src/proxy/gate/business_impact.rs::tests::
tool_call_digest_golden_value_stripe_charge_500.
Fixture payload: BusinessImpact.tool_call("stripe.charge",
{"region": "EU", "amount": 500}) -- mirrors the backend
helper at business_impact.rs:1473. The protocol prefix and
canonical-JSON algorithm must remain identical across both
languages; a drift on either side trips BOTH pins next time
the suite runs.
Five new tests in TestToolCallActionDigestPins:
* test_tool_call_stripe_charge_500_matches_golden_hex
-- the pin itself
* test_tool_call_two_calls_produce_identical_hex
-- determinism for the tamper-evident re-check on /execute
* test_tool_call_param_change_produces_different_hex
-- positive half: param change flips the digest
* test_tool_call_wire_dict_shape
-- kind='tool_call' discriminator (snake_case) pin so a
typo would not silently route to Money on the backend
* test_tool_call_extractor_metadata_advisory
-- extractor_* defaults are advisory provenance, present
on every wire payload
NOTE: ToolCallParams.validate() is intentionally NOT auto-
invoked by the dataclass __post_init__ -- the SDK relies on
BusinessImpact.tool_call(...) factory to enforce rejection
paths. Direct ToolCallParams(...) construction succeeds
without raising. This is a documented design choice that
matches the backend Rust struct (validation at the
construction site, not on the wire carrier).
Verification:
pytest tests/test_business_impact.py -v
-> 28 passed (23 pre-existing + 5 new pins)
Local commit only; no push.
Refs:
backend Rust pin: backend/src/proxy/gate/business_impact.rs
backend Tier 2 commit: 1e501cd6
SDK ToolParameters phase 1: 40d391a
SDK auto-attach chain walk fix: 436dc7b
Plan: docs/runbooks/action-digest-contract.md
* chore(release): 0.14.4 - ToolParameters Approval Rules wire contract
Bumps SDK 0.14.2 -> 0.14.4 and lands the Tier 2 / Разрыв 2
follow-up that wires the SDK-side ToolParameters path so
users get ToolParameters Approval Rules by default on every
bare @sensitive function with no decorator change.
Skipping 0.14.3 because the working branch was tagged
archive/cleanup-attempted-1c1e326 throughout the ToolParameters
work; this release is the first user-facing tag on the
release/0.14.4-toolparameters branch.
What ships in this release (full changelog at CHANGELOG.md):
* BusinessImpact.tool_call(...) factory + ToolCallParams
dataclass (business_impact.py)
* ToolParamsExtractor + tool_params(...) factory
(extractor.py)
* Bare @sensitive auto-attaches a default
ToolParamsExtractor(include_all=True) via
_do_sensitive_register
* @sensitive(impact=tool_params({...})) decorator form
* Auto-attach chain walk (_find_extractor_in_chain) preserves
an explicit impact=tool_params({...}) map -- the regression
that was silently dropped in 40d391a (fixed in 436dc7b)
* Cross-language ToolCall action digest parity pin (Rust +
SDK assert the same golden hex literal)
Version bumps:
* pyproject.toml: 0.14.2 -> 0.14.4 (hatchling source of truth)
* src/nullrun/__version__: 0.13.11 -> 0.14.4 (was lagging;
also bumped the docstring header to v3.30 / 0.14.4)
Behavioural change (called out in CHANGELOG > Compatibility):
* Bare @sensitive now ships kind=tool_call on the wire where
it previously shipped nothing. Money callers unaffected.
Legacy backends ignore the new envelope (additive on the
SDK side).
Verification (pre-commit, local venv):
pytest tests/ -q --ignore=tests/contract
-> 1382 passed, 7 skipped, 10 warnings in 98.36s
Local commit only; no push.
Refs:
Phase 1 wire contract: 40d391a
Chain-walk fix: 436dc7b
Cross-language parity pin: f608414
backend BusinessImpact::ToolCall variant: 1e501cd6
backend Tier 2 commits: 1e501cd6, 63ba9f6a
* fix(sdk): ruff F541 -- drop extraneous f-prefixes on placeholder-less strings
CI failed on the 0.14.4 PR (#80) at the lint step with
ruff F541 (f-string without any placeholders) -- 14 errors
in src/nullrun/decorators.py plus similar auto-fixable issues
in the other 4 files I touched.
Auto-fixed via:
ruff check src/ tests/ --fix
ruff format src/nullrun/decorators.py src/nullrun/business_impact.py src/nullrun/extractor.py src/nullrun/__version__.py tests/test_tool_params_extractor.py tests/test_business_impact.py
What changed (no behavioural change, just syntax):
* src/nullrun/decorators.py: f"..." -> "..." on the
MoneyImpactExtractor / ToolParamsExtractor / fallback
hint literals in _enforce_sensitive_tool (the @sensitive
error path). The strings had no {}-placeholders so the
f-prefix was dead syntax that ruff F541 (selected by the
default rule set in pyproject.toml) flagged.
* src/nullrun/business_impact.py: import-order normalisation
in extractor.py imports + format-only whitespace tidy.
* src/nullrun/extractor.py: same import-order + format tidy.
* tests/test_tool_params_extractor.py: import block was
un-sorted (I001 fixable). Re-ordered to ruff convention.
* tests/test_business_impact.py: format-only whitespace tidy
from the cross-language parity pin (f608414).
Verification:
ruff check src/ tests/ -> All checks passed!
ruff format --check <my 6 files> -> 6 files already formatted
pytest tests/ --ignore=tests/contract -> 1382 passed, 7 skipped
This is exactly the fix 4c143e2 (the 0.14.2 release) called
out as 'deferred to a separate PR' for its own touch surface:
'ruff format on the three source files touched by this
release. The format-only reformat of the 65 unrelated files
is intentionally deferred to a separate PR.' I am doing the
same scope discipline here -- only the files I authored or
modified for 0.14.4, not the 67 pre-existing format-debt
files (those are a separate PR).
Refs:
Failing CI: PR #80, run 30291404693 (test 3.11 + coverage)
ToolParameters phase 1: 40d391a
Cross-language parity pin: f608414
Release 0.14.4: 332acfb
Master merge: 3471bc41 parent 66ed805 commit bc34d61
8 files changed
Lines changed: 1428 additions & 53 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
7 | 7 | | |
8 | 8 | | |
9 | 9 | | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
10 | 47 | | |
11 | 48 | | |
12 | 49 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
140 | 140 | | |
141 | 141 | | |
142 | 142 | | |
143 | | - | |
| 143 | + | |
144 | 144 | | |
145 | 145 | | |
146 | 146 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | | - | |
| 3 | + | |
| 4 | + | |
4 | 5 | | |
5 | 6 | | |
6 | 7 | | |
| |||
1017 | 1018 | | |
1018 | 1019 | | |
1019 | 1020 | | |
1020 | | - | |
| 1021 | + | |
1021 | 1022 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
49 | 49 | | |
50 | 50 | | |
51 | 51 | | |
52 | | - | |
53 | | - | |
54 | | - | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
55 | 56 | | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
56 | 66 | | |
57 | 67 | | |
58 | 68 | | |
| |||
85 | 95 | | |
86 | 96 | | |
87 | 97 | | |
88 | | - | |
89 | | - | |
90 | | - | |
91 | | - | |
92 | | - | |
93 | | - | |
94 | | - | |
| 98 | + | |
| 99 | + | |
95 | 100 | | |
96 | | - | |
97 | | - | |
98 | | - | |
| 101 | + | |
99 | 102 | | |
100 | | - | |
101 | | - | |
102 | | - | |
| 103 | + | |
103 | 104 | | |
104 | 105 | | |
105 | 106 | | |
106 | 107 | | |
107 | 108 | | |
108 | 109 | | |
109 | 110 | | |
110 | | - | |
111 | | - | |
| 111 | + | |
112 | 112 | | |
113 | 113 | | |
114 | 114 | | |
| |||
129 | 129 | | |
130 | 130 | | |
131 | 131 | | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
132 | 251 | | |
133 | 252 | | |
134 | 253 | | |
| |||
146 | 265 | | |
147 | 266 | | |
148 | 267 | | |
149 | | - | |
150 | | - | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
151 | 273 | | |
152 | 274 | | |
153 | 275 | | |
154 | 276 | | |
155 | | - | |
| 277 | + | |
156 | 278 | | |
157 | 279 | | |
158 | 280 | | |
159 | 281 | | |
160 | 282 | | |
| 283 | + | |
| 284 | + | |
161 | 285 | | |
162 | 286 | | |
163 | 287 | | |
| |||
181 | 305 | | |
182 | 306 | | |
183 | 307 | | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
184 | 333 | | |
185 | 334 | | |
186 | 335 | | |
| |||
0 commit comments