chore(release): 0.14.9 - v3.38 wire-drift close - #87
Merged
Conversation
…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.
…API_KEY_* codes, soft_pass decision)
maltsev-dev
force-pushed
the
release/0.14.9
branch
from
August 7, 2026 06:05
1e3f8ac to
0d11713
Compare
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
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.
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)
/health(legacy liveness) to/api/v1/capabilities(canonical).parse_capabilitiesnow reads v3-gating flags from the nestedcapabilities.*sub-object, not top level. Without this,is_v3_ready()was alwaysFalseand every capability flag was a no-op at runtime.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.decisionis 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 returnsoft_passinstead of being silently dropped.Other fixes (cherry-picked from local master)
parent_execution_idon/gatefor sub-agent lineage. New capability flagexecution_graph(Falseby default; pre-Graph backends ignore unknown fields)./checkto preventIDEMPOTENCY_KEY_MISMATCHin chain context.error_codeon execute block instead of generic denied message.Diff
Commits (7)
1e3f8acchore(release): 0.14.9 - v3.38 wire-drift closea69b57efix(sdk): v3.38 wire-drift closefc0fed2feat(sdk): Execution Graph v0 - parent_execution_id forwarddf23134fix(sdk): per-event idempotency_key6a3dca7update texts29fe246docs(sdk): align README + pyproject3109863fix(sdk): DEF-ARFLOW-TOOLNAME-01 surface error_code on execute blockCompatibility
execution_graphcapability flag is new (defaultFalse, pre-Graph backends unaffected).decisionfield was always string-shaped on backend; SDK's bool coercion was a latent bug. No backend changes required for SDK upgrade.error_codecontinue to work.Tests
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)