Commit 78dacfc
authored
chore(release): 0.17.1 — op_id mint-fresh + error-code map closure (#103)
* fix(gate): mint fresh operation_id per /check call (DEF-OPID-REUSE-HASH-MISMATCH)
Pre-fix, NullRunRuntime._check_workflow_budget_impl at runtime.py:1947-1976
read _operation_id_var; if None (first wire call in scope), minted once
and stashed. Subsequent /check calls within the same scope REUSED the
first call's op_id.
The backend's IDEM-01 dedup (compute_gate_semantic_hash,
backend/src/redis/idempotency_store.rs:125-140) keys on op_id but verifies
an 11-field semantic hash (operation_id, tools, tool, mode, check_type,
model, estimated_tokens, input, business_impact, workflow_id,
organization_id). A second /check with a different tools / model / input
on the SAME op_id therefore 409 IDEMPOTENCY_KEY_MISMATCH, surfaced as
NR-B004 in the SDK.
Canonical repro: nullrun_openai_approval_demo.py fires a tools=None
/gate (LangGraph NullRunCallback.on_llm_start) BEFORE the
@sensitive(refund_customer) /gate (tools=['refund_customer']).
Both /check calls share the same op_id; the second's semantic hash
diverges from the first's, server rejects with 409, SDK reports
NR-B004 "You've reached the usage limit for this conversation".
Fix: mint-fresh-per-call. /check always reads-and-discards the contextvar
(value unused) then unconditionally mints a new UUID v4 and stashes it.
/execute at runtime.py:3048-3051 reads the freshly-stashed value within
the same logical action (synchronous /check -> /execute chain), so the
P0-27 within-action binding (one op_id across /check + /execute) is
preserved. The read-then-overwrite pattern also keeps the P0-27
source-pin test "test_check_workflow_budget_reads_contextvar" green.
Verified:
- 8/8 P0-27 source-pin tests pass (test_audit_p0_27_operation_id_hoist.py)
- 1807 pytest pass / 4 skipped / 0 fail (full SDK suite)
- Live probe (probe_full.py): 4 distinct operation_ids across 3 /check + 1 /execute
- /execute fallback mint pattern unchanged (read contextvar first, mint only if None)
- _GATE_CACHE invariant unaffected (cache key doesn't include op_id)
- Backend IDEM-01 logic unchanged (compute_gate_semantic_hash unaffected)
* fix(sdk): add INVALID_JSON/INVALID_FIELD to _V3_ERROR_CODE_MAP (DEF-SDKT-004 fix-wave-2)
DEF-SDKT-004 fix-wave-2 (2026-09-13): backend split
`From<JsonRejection> for ApiError` onto three distinct wire codes:
- JsonDataError -> 422 + INVALID_FIELD (validation_error slug)
- JsonSyntaxError -> 400 + INVALID_JSON (invalid_json slug)
- MissingJsonContentType -> 415 + INVALID_INPUT (bad_request slug)
The two NEW codes (INVALID_FIELD, INVALID_JSON) are emitted on /gate,
/execute, and /track. Pre-fix the SDK's `_V3_ERROR_CODE_MAP` had no
entries for them, so they fell through to the generic
`NullRunBackendError` (transport.py:2961 fallback). Cookbook recipes
that branch on `error_code` lost diagnostic class for parse-level
vs schema-level rejections.
Map both to `NullRunBackendError` -- siblings to EXECUTION_ID_MALFORMED,
EXECUTION_ID_REQUIRED, INVALID_EXECUTION_ID, and IDEMPOTENCY_REDIS_UNAVAILABLE
which already follow the same pattern for wire-shape parsing failures.
This mirrors the backend's intent: wire-level parsing failures are
infrastructure-side issues and the SDK round-trips them through the
generic catch-all.
The new wire codes are exercised in:
- /gate POST body rejection (gate.rs)
- /execute POST body rejection (execute.rs)
- /track POST body rejection (handlers.rs)
- TC-SDKG-006 (truncated JSON) expects 400 + INVALID_JSON
NR-007a (new) at `backend/tests/nr007_sdk_error_code_parity.rs`
pins the required SDK mappings and will fail CI on future drift
(e.g., if someone reverts INVALID_JSON from this map).
Tests: pytest passing (existing suite continues to green).
* chore(release): 0.17.1 — op_id mint-fresh + error-code map closure
Patch release closing two SDK-side gaps on the 0.17.0 baseline: (1) `/check` mints a fresh `operation_id` per call instead of reusing the first call's op_id within the same scope — closing a silent collision with the backend's `IDEM-01` 11-field semantic-hash dedup that surfaced as a misleading `NR-B004` "usage limit" error whenever a second `/check` diverged in `tools` / `model` / `input`, and (2) `_V3_ERROR_CODE_MAP` now covers the two new wire codes from backend `fix-wave-2` (`INVALID_JSON` 400 + `INVALID_FIELD` 422) — closing a diagnostic-class gap where cookbook recipes lost the ability to branch on `error_code` for parse-level vs schema-level rejections. Both fixes are wire-format-compatible and SDK_MIN_VERSION-unchanged. Cookbook code that already handles `NullRunBackendError` sees no behaviour change.
**Fixed**
- **DEF-OPID-REUSE-HASH-MISMATCH** — `/check` mints a fresh `operation_id` per call instead of reusing the first call's op_id within the same scope (`src/nullrun/runtime.py`, `a05726e`, +38/-7). Closes the silent collision with backend `IDEM-01` (`compute_gate_semantic_hash` in `backend/src/redis/idempotency_store.rs:125-140` keys on op_id but verifies an 11-field semantic hash — `operation_id`, `tools`, `tool`, `mode`, `check_type`, `model`, `estimated_tokens`, `input`, `business_impact`, `workflow_id`, `organization_id` — so a second `/check` with a different `tools` / `model` / `input` on the same op_id 409s with `IDEMPOTENCY_KEY_MISMATCH` and surfaces to the SDK as the misleading `NR-B004` "You've reached the usage limit for this conversation"). `/execute` continues to read the freshly-stashed op_id within the same logical action so the **P0-27 within-action binding** (one op_id across `/check` + `/execute`) is preserved — pinned by the 8 P0-27 source-pin tests at `tests/test_audit_p0_27_operation_id_hoist.py`.
- **DEF-SDKT-004 fix-wave-2** — `_V3_ERROR_CODE_MAP` now contains entries for `INVALID_JSON` (400, `invalid_json` slug, `JsonSyntaxError`) and `INVALID_FIELD` (422, `validation_error` slug, `JsonDataError`) (`src/nullrun/transport.py`, `f5aca80`, +24/-0). Backend `fix-wave-2` (2026-09-13) split `From<JsonRejection> for ApiError` onto three distinct wire codes (also adding `INVALID_INPUT` 415 for `MissingJsonContentType`, which already fell through to the legacy `BAD_REQUEST` arm). Pre-fix the SDK's map missed both new codes so they fell through to the generic `NullRunBackendError` fallback at `transport.py:2961`; cookbook recipes that branch on `error_code` lost diagnostic class. Map both to `NullRunBackendError` — the same exception class used for the legacy wire-shape parsing failures (`EXECUTION_ID_MALFORMED`, `EXECUTION_ID_REQUIRED`, `INVALID_EXECUTION_ID`, `IDEMPOTENCY_REDIS_UNAVAILABLE`), matching the backend's intent that wire-level parsing failures are infrastructure-side issues. **NR-007a** (new) at `backend/tests/nr007_sdk_error_code_parity.rs` pins the required SDK mappings and fails CI on future drift.
**Verification**
| Check | Result |
|---|---|
| `ruff check src tests` | All checks passed |
| `mypy src/nullrun` | Success: no issues found in 37 source files |
| `pytest -q` | **1807 passed, 4 skipped** in 108.84s (vs baseline 1807 at 0.17.0 — no new tests; both fixes fold into existing coverage) |
| Scratch diff | clean (no `dist_local/`, no `*.defect*`) |
| `nullrun.__version__` | `0.17.1` |
| Wire-format compatibility | unchanged from 0.17.0 |
**Commits included**
- `a05726e` — `fix(gate): mint fresh operation_id per /check call (DEF-OPID-REUSE-HASH-MISMATCH)` (+38/-7 in `src/nullrun/runtime.py`)
- `f5aca80` — `fix(sdk): add INVALID_JSON/INVALID_FIELD to _V3_ERROR_CODE_MAP (DEF-SDKT-004 fix-wave-2)` (+24/-0 in `src/nullrun/transport.py`)
- version bump commit (folded into this release commit) — `chore: bump 0.17.1` (+27/-3 across `pyproject.toml`, `src/nullrun/__version__.py`, `CHANGELOG.md`, `uv.lock`)1 parent 30756f2 commit 78dacfc
6 files changed
Lines changed: 89 additions & 10 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
1 | 25 | | |
2 | 26 | | |
3 | 27 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
9 | | - | |
| 9 | + | |
10 | 10 | | |
11 | 11 | | |
12 | 12 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
8 | | - | |
| 8 | + | |
9 | 9 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1947 | 1947 | | |
1948 | 1948 | | |
1949 | 1949 | | |
| 1950 | + | |
| 1951 | + | |
| 1952 | + | |
| 1953 | + | |
| 1954 | + | |
| 1955 | + | |
| 1956 | + | |
| 1957 | + | |
| 1958 | + | |
| 1959 | + | |
| 1960 | + | |
| 1961 | + | |
| 1962 | + | |
| 1963 | + | |
| 1964 | + | |
| 1965 | + | |
| 1966 | + | |
| 1967 | + | |
| 1968 | + | |
| 1969 | + | |
| 1970 | + | |
| 1971 | + | |
| 1972 | + | |
| 1973 | + | |
| 1974 | + | |
| 1975 | + | |
| 1976 | + | |
| 1977 | + | |
| 1978 | + | |
| 1979 | + | |
| 1980 | + | |
| 1981 | + | |
| 1982 | + | |
| 1983 | + | |
| 1984 | + | |
| 1985 | + | |
1950 | 1986 | | |
1951 | | - | |
1952 | | - | |
1953 | | - | |
1954 | | - | |
1955 | | - | |
1956 | | - | |
1957 | | - | |
| 1987 | + | |
| 1988 | + | |
1958 | 1989 | | |
1959 | 1990 | | |
1960 | 1991 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
3169 | 3169 | | |
3170 | 3170 | | |
3171 | 3171 | | |
| 3172 | + | |
| 3173 | + | |
| 3174 | + | |
| 3175 | + | |
| 3176 | + | |
| 3177 | + | |
| 3178 | + | |
| 3179 | + | |
| 3180 | + | |
| 3181 | + | |
| 3182 | + | |
| 3183 | + | |
| 3184 | + | |
| 3185 | + | |
| 3186 | + | |
| 3187 | + | |
| 3188 | + | |
| 3189 | + | |
| 3190 | + | |
| 3191 | + | |
| 3192 | + | |
| 3193 | + | |
| 3194 | + | |
| 3195 | + | |
3172 | 3196 | | |
3173 | 3197 | | |
3174 | 3198 | | |
| |||
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
0 commit comments