Skip to content

chore(release): 0.14.9 - v3.38 wire-drift close - #87

Merged
maltsev-dev merged 7 commits into
masterfrom
release/0.14.9
Aug 7, 2026
Merged

chore(release): 0.14.9 - v3.38 wire-drift close#87
maltsev-dev merged 7 commits into
masterfrom
release/0.14.9

Conversation

@maltsev-dev

Copy link
Copy Markdown
Member

Summary

Bump SDK to 0.14.9. Closes the v3.38 wire-contract drift between SDK and backend: the SDK was on a stale wire surface that no longer matched the backend's contract, which would cause runtime 503 / 400 mismatches on the first call against a v3.38+ backend.

Wire-drift close (v3.38)

  • capabilities probe route — switched from /health (legacy liveness) to /api/v1/capabilities (canonical). parse_capabilities now reads v3-gating flags from the nested capabilities.* sub-object, not top level. Without this, is_v3_ready() was always False and every capability flag was a no-op at runtime.
  • API key lifecycle wire codes — SDK now distinguishes the four DB-level states (API_KEY_EXPIRED, API_KEY_DISABLED, API_KEY_REVOKED, API_KEY_INVALID) per backend v3.38. Previously all four collapsed into a single generic 401.
  • soft_pass decision branch — gate response decision is now parsed as a discriminated string (allow / soft_pass / block / require_approval), not bool-coerced. This restores the soft-mode contract where chain-active requests return soft_pass instead of being silently dropped.

Other fixes (cherry-picked from local master)

  • Execution Graph v0 — forward parent_execution_id on /gate for sub-agent lineage. New capability flag execution_graph (False by default; pre-Graph backends ignore unknown fields).
  • per-event idempotency_key — generate a new key per /check to prevent IDEMPOTENCY_KEY_MISMATCH in chain context.
  • DEF-ARFLOW-TOOLNAME-01 — surface backend error_code on execute block instead of generic denied message.
  • README / pyproject alignment — update positioning + link refresh.

Diff

75 files changed, 2288 insertions(+), 1090 deletions(-)

Commits (7)

  1. 1e3f8ac chore(release): 0.14.9 - v3.38 wire-drift close
  2. a69b57e fix(sdk): v3.38 wire-drift close
  3. fc0fed2 feat(sdk): Execution Graph v0 - parent_execution_id forward
  4. df23134 fix(sdk): per-event idempotency_key
  5. 6a3dca7 update texts
  6. 29fe246 docs(sdk): align README + pyproject
  7. 3109863 fix(sdk): DEF-ARFLOW-TOOLNAME-01 surface error_code on execute block

Compatibility

  • Wire contract additive: execution_graph capability flag is new (default False, pre-Graph backends unaffected).
  • Wire contract semantic: decision field was always string-shaped on backend; SDK's bool coercion was a latent bug. No backend changes required for SDK upgrade.
  • Auth error codes are additive — old SDKs that ignore error_code continue to work.

Tests

1457 passed, 7 skipped

New tests:

  • tests/test_v3_38_drift_fixes.py (12 tests covering capabilities route, parse layout, soft_pass branch, API key lifecycle codes)
  • tests/test_execution_graph.py (parent_execution_id forwarding + capability flag gating)
  • tests/test_idempotency_per_event.py (per-event key uniqueness in chain context)

…e block

E2E 2026-08-05: backend now stamps a structured
`details.error_code` on every block response via
`classify_approval_create_error` (Разрыв 1c follow-up, exposes
APPROVAL_DB_UNAVAILABLE / APPROVAL_PERSISTENCE_FAILED /
APPROVAL_VALIDATION_FAILED / APPROVAL_CONFLICT / APPROVAL_NOT_FOUND /
APPROVAL_CREATE_FAILED — see backend commit 19e381e3 for the
generator).

Without this fix the SDK fell back to keyword-on-explanation matching
which classified 'Approval infrastructure unavailable: validation
error during approval row creation' as the generic NR-X001 — the very
bug the journal test surfaced. The whole point of the backend
classify_approval_create_error change was to give operators a stable
discriminator, and the SDK was the second half of that contract.

Changes:
* runtime.py:2628-2692 — read result['details']['error_code'] before
  falling back to keyword matching. Wire details payload is preserved
  on exc.details['details'] so callers can introspect
  decision_source for routing/alerting. Back-compat shim:
  mapped_class still populated for callers that branched on the
  legacy keyword path.
* transport.py — add APPROVAL_DB_UNAVAILABLE / APPROVAL_PERSISTENCE_FAILED /
  APPROVAL_VALIDATION_FAILED / APPROVAL_CONFLICT / APPROVAL_NOT_FOUND /
  APPROVAL_CREATE_FAILED to _V3_ERROR_CODE_MAP, all mapping to
  NullRunBlockedException (they're hard-rejects per CLAUDE.md §5 / §8
  — body did NOT run, fail-CLOSED posture preserved). Lazy import
  updated to pull NullRunBlockedException.
* test_runtime.py — new test_execute_blocked_surfaces_wire_error_code
  regression test pinning the structured-error path. Asserts
  exc.error_code == 'APPROVAL_VALIDATION_FAILED' and that
  decision_source + mapped_class survive into the exception details.

No push per repo convention.
README:
- Headline: "Enforcement gateway for AI agents." -> "Runtime decision
  layer for tool-using AI agents." (matches positioning.md §1 canonical
  statement; the old framing undersells Phase 1 typed predicates and
  Разрыв 3 MCP-aware enforcement).
- Description: rewritten around the /gate decision contract (allow /
  block / require_approval) + Phase 1 typed BusinessImpact predicates
  bound via SHA-256 action_digest + Разрыв 3 / v3.31 MCP-aware gate
  enforcement for per-org MCP servers.

pyproject.toml:
- description: updated to match the new headline and surface the two
  newest differentiators ("BusinessImpact", "MCP-aware") while keeping
  the PyPI-friendly vendor list and staying under the 200-char preview
  threshold (199 chars). The comment above the field documents why.

No code change. No SDK_MIN_VERSION bump. No on-wire change.
@
…CH in chain context

DEF-SDKWRAP-CHAIN-SOFT-EXECUTION-ID-REUSE-01 (Critical, billing integrity,
Session 6 TC-SDKWRAP-05/07/16). The captured /check operation_id is reused
across every llm_call event within the same chain-context _GATE_CACHE
window (5s TTL). Backend's v3 /track idempotency layer hashes request
body against stored body for the same key — every event after the FIRST
one in the cache window has a DIFFERENT body (tokens, model, latency) →
409 IDEMPOTENCY_KEY_MISMATCH and the event is silently dropped.

Fix: in _enrich_event, derive a per-event idempotency_key by combining
the captured /check operation_id with a per-event discriminator (span_id,
minted once per @Protect invocation). Format: '<op_id>:<span_short>'
where span_short = span_id[:16]. Preserves retry-replay contract (same
event retried → same idempotency_key → backend returns 200 +
idempotent_replay:true) while ensuring distinct events get distinct
server-side cache slots (no more silent drop on body hash mismatch).

Per CLAUDE.md §22 (Trust model): 'losing actual token counts means
downstream billing sees tokens=0 instead of the real cost'.
…ity flag

Matches backend commit df88eca8 (not pushed yet). Backward-compatible
additive wire change for sub-agent lineage on /api/v1/gate. SDKs
spawning a sub-agent can name the parent execution id; backend
validates ownership against the parent execution Redis binding
(mirrors the /cancel ownership check) and rejects cross-org /
cross-key / not-found with 403 PARENT_EXECUTION_*.

Wire changes:
- Transport.check(check_request, ..., parent_execution_id=None) --
  forwards the optional field additively (omitted when None or
  absent). Resolution order: check_request[parent_execution_id]
  wins, then the kwarg, then omit. Mirrors the conditional forward
  pattern at transport.py:1607-1626 (chain_id / tool_arguments /
  idempotency_key).
- ServerCapabilities.execution_graph: bool -- probes the new flag
  on /api/v1/capabilities. Nested-then-flat parser mirrors the
  pre-1.0.0 backward-compat shape. NOT included in is_v3_ready() --
  informational, not a hard gate.

Exception taxonomy:
- NullRunChainError(message, *, parent_execution_id=None, ...) --
  new optional kwarg mirroring the existing chain_id kwarg at
  breaker/exceptions.py:425. Three new error codes mapped to
  NullRunChainError (diagnostic clarity wins over a new class per
  CLAUDE.md §13 philosophy):
  - PARENT_EXECUTION_NOT_FOUND
  - PARENT_EXECUTION_ORG_MISMATCH
  - PARENT_EXECUTION_KEY_MISMATCH

Tests:
- tests/test_transport.py::TestParentExecutionIdForwarding -- 3
  new tests: forward when present, omit when absent, omit when
  explicit None. Mirrors the existing
  test_check_forwards_tool_arguments_via_check_request pattern.

CHANGELOG: 0.14.8 entry with full backward-compat notes.

Compatibility:
- Wire change strictly additive: pre-Execution-Graph SDKs never
  set parent_execution_id; field is omitted entirely.
- No SDK_MIN_VERSION bump.
- Existing 4-arg NullRunChainError call sites unchanged.
- Pre-Graph backends return execution_graph: false (or omit);
  SDK treats as do-not-send. No 400 on first call.
…s, soft_pass decision

Three contract-level fixes verified against backend source (not
docs/comments):

1. **capabilities probe route** — `CAPABILITIES_PATH` was `/health`
   (legacy v1/v2 liveness payload that carries no v3-gating
   fields), so `is_v3_ready()` was always False and every
   capability flag was a runtime no-op. Switched to
   `/api/v1/capabilities` per backend/src/proxy/http/protocol.rs:189.

2. **v3.38 API_KEY_* error codes** — backend (memory
   `auth-error-distinct-states`) emits six distinct 401 codes
   (API_KEY_REVOKED/EXPIRED/DISABLED/INVALID/MISSING/MALFORMED).
   Pre-fix only API_KEY_REVOKED was mapped in
   `_V3_ERROR_CODE_MAP`; the other five fell through to the
   generic HTTP-status fallback (NullRunAuthenticationError),
   losing the diagnostic class. Added all five codes + a
   `wire_code` attribute on NullRunAuthError that exposes the
   granular backend code without clobbering the SDK-side NR-A003
   taxonomy. Mirrors NullRunChainError.backend_code pattern.

3. **decision == "soft_pass" handling** — runtime.py:1741 had no
   branch for soft_pass; the call fell through to the implicit
   `allow` default so body executed (correct) but operator saw
   no log line and no overdraft counter incremented (silent
   budget drift). Added a branch that logs at WARNING with
   overdraft telemetry and increments `soft_overdraft_used`
   counter for dashboard graphing. Removed a duplicate `if
   decision == "throttle"` block discovered during the fix.

Also fixed a latent TypeError in the auth-error envelope
dispatch: `_parse_v3_error_envelope` was forwarding every key in
the envelope `details` dict as a kwarg to NullRunAuthError →
NullRunError, which only accepts {error_code, user_action,
retryable, docs_url, cause}. Unknown keys (e.g. `expires_at`
emitted for v3.38 API_KEY_EXPIRED) threw TypeError. Now filters
to known kwargs and lands extras on `self.details`.

Updated `__init__.py`, `tests/conftest.py`,
`tests/test_capabilities.py`, `tests/test_init_contract.py`
to reference the canonical `/api/v1/capabilities` route and
the corresponding mock shape. Updated transport.py header
comment that referenced /health as protocol-version source.

Added tests/test_v3_38_drift_fixes.py with 14 regression tests
(constant pin + mock-API negative pin + 6-code parametrized
catalog pin + wire_code surface pin + unknown-details pin +
3 source-level soft_pass branch pins).

Full SDK suite: 1457 passed, 7 skipped, 0 failed.
@codecov

codecov Bot commented Aug 7, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 46.55172% with 31 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/nullrun/runtime.py 25.71% 20 Missing and 6 partials ⚠️
src/nullrun/transport.py 64.28% 3 Missing and 2 partials ⚠️

📢 Thoughts on this report? Let us know!

@maltsev-dev
maltsev-dev merged commit b348a35 into master Aug 7, 2026
3 of 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