openbox-sdk-python 1.1.0__tar.gz → 1.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/CHANGELOG.md +18 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/PKG-INFO +1 -1
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/openbox_core/__init__.py +1 -1
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/openbox_core/contracts/results.py +39 -38
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/pyproject.toml +1 -1
- openbox_sdk_python-1.2.0/tests/contracts/test_patch.py +207 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/uv.lock +1 -1
- openbox_sdk_python-1.1.0/tests/contracts/test_retry_plan.py +0 -207
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/.github/instructions/openbox-sdk-python.instructions.md +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/.github/workflows/ci.yml +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/.github/workflows/publish.yml +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/.gitignore +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/.python-version +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/README.md +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/openbox_core/adapters/__init__.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/openbox_core/adapters/base.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/openbox_core/approvals.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/openbox_core/client.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/openbox_core/config.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/openbox_core/conformance/__init__.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/openbox_core/conformance/fake_core.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/openbox_core/conformance/hook_preflight.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/openbox_core/conformance/instrumentation.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/openbox_core/context.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/openbox_core/contracts/__init__.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/openbox_core/contracts/context.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/openbox_core/contracts/events.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/openbox_core/contracts/otel_spans.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/openbox_core/errors.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/openbox_core/gate.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/openbox_core/hooks/__init__.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/openbox_core/hooks/events.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/openbox_core/hooks/preflight.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/openbox_core/hooks/wrappers.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/openbox_core/identity.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/openbox_core/instrumentation/__init__.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/openbox_core/instrumentation/db.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/openbox_core/instrumentation/file.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/openbox_core/instrumentation/function.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/openbox_core/instrumentation/http.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/openbox_core/instrumentation/llm.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/openbox_core/instrumentation/manager.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/openbox_core/instrumentation/shared.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/openbox_core/otel/__init__.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/openbox_core/otel/propagation.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/openbox_core/otel/provider.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/openbox_core/otel/setup.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/openbox_core/otel/span_processor.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/openbox_core/otel/trace_context.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/openbox_core/py.typed +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/openbox_core/runtime.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/openbox_core/sdk_version.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/openbox_core/serialization.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/openbox_core/validation/__init__.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/openbox_core/validation/diagnostics.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/openbox_core/validation/event_rules.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/openbox_core/validation/registry.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/openbox_core/validation/span_normalization.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/openbox_core/wire/__init__.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/openbox_core/wire/core_span.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/openbox_core/wire/evaluate_payload.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/tests/client/test_approval_poll.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/tests/client/test_fail_modes.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/tests/config/test_resolution_order.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/tests/conformance/test_required_cases.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/tests/context/test_bind_reset.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/tests/context/test_trace_key.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/tests/contracts/test_approval_parsing.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/tests/contracts/test_event_classify.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/tests/contracts/test_result_parsing.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/tests/gate/test_diagnostics.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/tests/gate/test_strict_failures.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/tests/instrumentation/conftest.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/tests/instrumentation/instrumented_env.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/tests/instrumentation/test_db_block.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/tests/instrumentation/test_db_redis_mongo.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/tests/instrumentation/test_file_function_block.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/tests/instrumentation/test_hook_failclosed_and_sync_approval.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/tests/instrumentation/test_hook_runtime.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/tests/instrumentation/test_http_preflight_block.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/tests/instrumentation/test_http_urllib.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/tests/instrumentation/test_manager_and_otel_lifecycle.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/tests/runtime/test_runtime_delegation.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/tests/signing/generate_golden_fixture_from_temporal_signer.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/tests/signing/golden_temporal_signed_request.json +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/tests/signing/test_golden_signing.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/tests/test_import_safety.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/tests/test_sdk_version.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/tests/wire/go_spandata_compat/go.mod +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/tests/wire/go_spandata_compat/main.go +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/tests/wire/span_fixtures.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/tests/wire/test_backend_compat.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/tests/wire/test_core_span_projection.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/tests/wire/test_flat_hook_contract.py +0 -0
- {openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/tests/wire/test_hex_ids.py +0 -0
|
@@ -5,6 +5,24 @@ All notable changes to this project are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.2.0] - 2026-07-23
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- **BREAKING:** Renamed the public BLOCK remediation contract from `retry_plan` to `patch`.
|
|
12
|
+
`RetryPlan` is now `Patch`, `RetryDirective` is now `PatchDirective`, and
|
|
13
|
+
`handle_retryable_block()` is now `handle_patch()`. `EvaluationResult.retry_plan` and
|
|
14
|
+
`ApprovalResult.retry_plan` are now `.patch`. The parser now reads only the canonical outer
|
|
15
|
+
wire key `patch`; the old `retry_plan` key is no longer recognized.
|
|
16
|
+
- The verdict gate is unchanged: a directive is still surfaced only for an exact `BLOCK` verdict
|
|
17
|
+
carrying a valid patch — never for HALT/`should_stop()`, and never for an expired approval
|
|
18
|
+
result.
|
|
19
|
+
|
|
20
|
+
### Notes
|
|
21
|
+
- This is a breaking release: the old public names (`RetryPlan`, `RetryDirective`,
|
|
22
|
+
`handle_retryable_block`, `.retry_plan`) are removed, not aliased. Consumers still pinned to
|
|
23
|
+
`1.1.0` remain fail-safe — they ignore the unknown `patch` field but still enforce the `BLOCK`
|
|
24
|
+
verdict.
|
|
25
|
+
|
|
8
26
|
## [1.1.0] - 2026-07-21
|
|
9
27
|
|
|
10
28
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openbox-sdk-python
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: OpenBox base SDK - governance contracts, strict gate, identity/signing, evaluate client, context runtime, OTel span wire serialization, and generic instrumentation shared by every OpenBox framework SDK
|
|
5
5
|
Author-email: OpenBox Team <tino@openbox.ai>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -37,7 +37,7 @@ from .errors import (
|
|
|
37
37
|
# governance; eagerly it can deadlock package init as a circular import, and
|
|
38
38
|
# lazily it can recurse unboundedly when a per-request header builder resolves
|
|
39
39
|
# the version. Keep in sync with pyproject.toml on release.
|
|
40
|
-
__version__ = "1.
|
|
40
|
+
__version__ = "1.2.0"
|
|
41
41
|
|
|
42
42
|
__all__ = [
|
|
43
43
|
"__version__",
|
|
@@ -20,9 +20,9 @@ __all__ = [
|
|
|
20
20
|
"GuardrailsResult",
|
|
21
21
|
"EvaluationResult",
|
|
22
22
|
"ApprovalResult",
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
23
|
+
"Patch",
|
|
24
|
+
"PatchDirective",
|
|
25
|
+
"handle_patch",
|
|
26
26
|
]
|
|
27
27
|
|
|
28
28
|
|
|
@@ -106,8 +106,8 @@ class GuardrailsResult:
|
|
|
106
106
|
return [r.get("reason", "") for r in self.reasons if r.get("reason")]
|
|
107
107
|
|
|
108
108
|
|
|
109
|
-
# Sentinel distinguishing an absent ``
|
|
110
|
-
# ``dict.get("
|
|
109
|
+
# Sentinel distinguishing an absent ``patch`` field from a present JSON ``null``.
|
|
110
|
+
# ``dict.get("patch")`` would collapse both to None; membership via this sentinel does not.
|
|
111
111
|
_MISSING = object()
|
|
112
112
|
|
|
113
113
|
# JS Number.MAX_SAFE_INTEGER. Integral numbers must fit within +/- this bound so ``new_input`` can
|
|
@@ -120,7 +120,7 @@ def _numbers_are_safe(value: Any) -> bool:
|
|
|
120
120
|
"""Every number (recursively) must be finite and, if integral, within the JS-safe range.
|
|
121
121
|
|
|
122
122
|
``bool`` is a subclass of ``int`` but is not a number here; a nested bool is a plain leaf
|
|
123
|
-
(top-level ``new_input`` booleans are rejected separately by :func:`
|
|
123
|
+
(top-level ``new_input`` booleans are rejected separately by :func:`_parse_patch`).
|
|
124
124
|
"""
|
|
125
125
|
if isinstance(value, bool):
|
|
126
126
|
return True
|
|
@@ -140,22 +140,22 @@ def _numbers_are_safe(value: Any) -> bool:
|
|
|
140
140
|
|
|
141
141
|
|
|
142
142
|
@dataclass
|
|
143
|
-
class
|
|
144
|
-
"""Optional remediation hint carried on a BLOCK verdict (policy-produced or admin-
|
|
143
|
+
class Patch:
|
|
144
|
+
"""Optional remediation hint carried on a BLOCK verdict (policy-produced or admin-patch).
|
|
145
145
|
|
|
146
146
|
``new_input`` may be null | str | number | list | dict (boolean rejected). A present
|
|
147
|
-
``new_input`` of ``None`` is a valid
|
|
148
|
-
from an absent ``
|
|
147
|
+
``new_input`` of ``None`` is a valid patch (reuses the original input) and is distinct
|
|
148
|
+
from an absent ``patch`` field.
|
|
149
149
|
"""
|
|
150
150
|
|
|
151
151
|
new_input: Any = None
|
|
152
152
|
|
|
153
153
|
|
|
154
154
|
@dataclass
|
|
155
|
-
class
|
|
156
|
-
"""Caller-facing view of a
|
|
155
|
+
class PatchDirective:
|
|
156
|
+
"""Caller-facing view of a BLOCK with patch, surfaced only via :func:`handle_patch`.
|
|
157
157
|
|
|
158
|
-
It is a remediation hint, not proof that a
|
|
158
|
+
It is a remediation hint, not proof that a patch was evaluated or executed.
|
|
159
159
|
"""
|
|
160
160
|
|
|
161
161
|
new_input: Any = None
|
|
@@ -163,28 +163,28 @@ class RetryDirective:
|
|
|
163
163
|
reason: str | None = None
|
|
164
164
|
|
|
165
165
|
|
|
166
|
-
def
|
|
167
|
-
"""Parse an optional ``
|
|
166
|
+
def _parse_patch(container: dict[str, Any]) -> Patch | None:
|
|
167
|
+
"""Parse an optional ``patch`` from a result dict against the frozen wire contract.
|
|
168
168
|
|
|
169
|
-
Returns a
|
|
169
|
+
Returns a patch only when ``patch`` is present, is a dict whose ONLY key is ``new_input``,
|
|
170
170
|
and ``new_input`` is null/str/number/list/dict (boolean rejected) with every number finite and,
|
|
171
171
|
if integral, within the JS-safe integer range. An absent field, a JSON ``null`` field, or any
|
|
172
172
|
contract violation yields ``None`` (treated as absent — never an error). A present
|
|
173
|
-
``new_input`` of ``None`` is preserved as a valid
|
|
173
|
+
``new_input`` of ``None`` is preserved as a valid patch.
|
|
174
174
|
"""
|
|
175
|
-
|
|
176
|
-
if
|
|
175
|
+
patch = container.get("patch", _MISSING)
|
|
176
|
+
if patch is _MISSING or not isinstance(patch, dict):
|
|
177
177
|
return None
|
|
178
178
|
# Exactly the single key new_input (also guarantees new_input is present, not merely null).
|
|
179
|
-
if set(
|
|
179
|
+
if set(patch.keys()) != {"new_input"}:
|
|
180
180
|
return None
|
|
181
|
-
new_input =
|
|
181
|
+
new_input = patch["new_input"]
|
|
182
182
|
# bool is a subclass of int — reject a boolean new_input BEFORE the number check.
|
|
183
183
|
if isinstance(new_input, bool):
|
|
184
184
|
return None
|
|
185
185
|
if not _numbers_are_safe(new_input):
|
|
186
186
|
return None
|
|
187
|
-
return
|
|
187
|
+
return Patch(new_input=new_input)
|
|
188
188
|
|
|
189
189
|
|
|
190
190
|
@dataclass
|
|
@@ -215,7 +215,7 @@ class EvaluationResult:
|
|
|
215
215
|
fallback_used: bool = False # True when fail-open produced this result
|
|
216
216
|
diagnostics: list[Any] = field(default_factory=list)
|
|
217
217
|
raw: dict[str, Any] = field(default_factory=dict)
|
|
218
|
-
|
|
218
|
+
patch: Patch | None = None # Optional BLOCK remediation hint; None = absent
|
|
219
219
|
|
|
220
220
|
@property
|
|
221
221
|
def guardrails_result(self) -> GuardrailsResult | None:
|
|
@@ -266,7 +266,7 @@ class EvaluationResult:
|
|
|
266
266
|
fallback_used=bool(data.get("fallback_used", False)),
|
|
267
267
|
diagnostics=data.get("diagnostics") or [],
|
|
268
268
|
raw=dict(data),
|
|
269
|
-
|
|
269
|
+
patch=_parse_patch(data),
|
|
270
270
|
)
|
|
271
271
|
|
|
272
272
|
@classmethod
|
|
@@ -299,7 +299,7 @@ class ApprovalResult:
|
|
|
299
299
|
approval_expiration_time: str | None = None
|
|
300
300
|
expired: bool = False
|
|
301
301
|
raw: dict[str, Any] = field(default_factory=dict)
|
|
302
|
-
|
|
302
|
+
patch: Patch | None = None # Present only on an admin-patch BLOCK
|
|
303
303
|
|
|
304
304
|
# Known decision vocabulary for approvals (current values + accepted aliases).
|
|
305
305
|
# Anything OUTSIDE this set parses to None (pending) — the evaluate-path
|
|
@@ -345,7 +345,7 @@ class ApprovalResult:
|
|
|
345
345
|
approval_expiration_time=data.get("approval_expiration_time"),
|
|
346
346
|
expired=bool(data.get("expired", False)),
|
|
347
347
|
raw=dict(data),
|
|
348
|
-
|
|
348
|
+
patch=_parse_patch(data),
|
|
349
349
|
)
|
|
350
350
|
|
|
351
351
|
@property
|
|
@@ -376,21 +376,22 @@ class ApprovalResult:
|
|
|
376
376
|
return self.verdict in (Verdict.REQUIRE_APPROVAL, Verdict.CONSTRAIN)
|
|
377
377
|
|
|
378
378
|
|
|
379
|
-
def
|
|
379
|
+
def handle_patch(
|
|
380
380
|
result: EvaluationResult | ApprovalResult,
|
|
381
|
-
) ->
|
|
382
|
-
"""Opt-in, pure inspector: surface a
|
|
381
|
+
) -> PatchDirective | None:
|
|
382
|
+
"""Opt-in, pure inspector: surface a patch directive from a BLOCK with patch.
|
|
383
383
|
|
|
384
|
-
Returns a :class:`
|
|
385
|
-
``
|
|
384
|
+
Returns a :class:`PatchDirective` ONLY for ``verdict == Verdict.BLOCK`` with a present, valid
|
|
385
|
+
``patch``. Returns ``None`` for plain BLOCK, every non-BLOCK verdict (including HALT),
|
|
386
386
|
a pending/``None`` verdict, and an expired :class:`ApprovalResult`.
|
|
387
387
|
|
|
388
|
-
This does NOT change enforcement: importing or exposing it never auto-
|
|
389
|
-
invoke it explicitly, and it makes no claim that a replacement was evaluated or
|
|
388
|
+
This does NOT change enforcement: importing or exposing it never auto-applies the patch. A
|
|
389
|
+
caller must invoke it explicitly, and it makes no claim that a replacement was evaluated or
|
|
390
|
+
executed.
|
|
390
391
|
|
|
391
392
|
The gate is deliberately ``verdict == Verdict.BLOCK`` — NOT ``is_blocking()`` /
|
|
392
393
|
``verdict.should_stop()``, which also match HALT (and, for approvals, expired results) and would
|
|
393
|
-
violate the wire-contract invariant that HALT always strips the
|
|
394
|
+
violate the wire-contract invariant that HALT always strips the patch.
|
|
394
395
|
"""
|
|
395
396
|
# A stale/expired poll result must not surface a directive, even if verdict == BLOCK.
|
|
396
397
|
if isinstance(result, ApprovalResult) and result.expired:
|
|
@@ -400,8 +401,8 @@ def handle_retryable_block(
|
|
|
400
401
|
if getattr(result, "verdict", None) != Verdict.BLOCK:
|
|
401
402
|
return None
|
|
402
403
|
|
|
403
|
-
|
|
404
|
-
if
|
|
404
|
+
patch = result.patch
|
|
405
|
+
if patch is None:
|
|
405
406
|
return None
|
|
406
407
|
|
|
407
408
|
# governance_event_id is typed on EvaluationResult; ApprovalResult carries it only in raw.
|
|
@@ -410,8 +411,8 @@ def handle_retryable_block(
|
|
|
410
411
|
else:
|
|
411
412
|
governance_event_id = result.raw.get("governance_event_id") or result.raw.get("id")
|
|
412
413
|
|
|
413
|
-
return
|
|
414
|
-
new_input=
|
|
414
|
+
return PatchDirective(
|
|
415
|
+
new_input=patch.new_input,
|
|
415
416
|
governance_event_id=governance_event_id,
|
|
416
417
|
reason=result.reason,
|
|
417
418
|
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "openbox-sdk-python"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.2.0"
|
|
4
4
|
description = "OpenBox base SDK - governance contracts, strict gate, identity/signing, evaluate client, context runtime, OTel span wire serialization, and generic instrumentation shared by every OpenBox framework SDK"
|
|
5
5
|
authors = [
|
|
6
6
|
{ name = "OpenBox Team", email = "tino@openbox.ai" },
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""patch parsing + handle_patch tests (frozen wire contract).
|
|
2
|
+
|
|
3
|
+
Covers: present-null vs absent, falsy-value preservation, boolean rejection, the finite/
|
|
4
|
+
safe-integer numeric rule (incl. nested), action="block" approvals, HALT/expired -> None,
|
|
5
|
+
the EvaluationResult path not raising AttributeError, and default BLOCK enforcement unchanged.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from openbox_core.contracts.results import (
|
|
11
|
+
ApprovalResult,
|
|
12
|
+
EvaluationResult,
|
|
13
|
+
Patch,
|
|
14
|
+
PatchDirective,
|
|
15
|
+
Verdict,
|
|
16
|
+
handle_patch,
|
|
17
|
+
)
|
|
18
|
+
from openbox_core.errors import GovernanceBlockedError
|
|
19
|
+
from openbox_core.gate import raise_for_verdict
|
|
20
|
+
|
|
21
|
+
SAFE_MAX = 2**53 - 1
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _block(**extra):
|
|
25
|
+
return {"verdict": "block", **extra}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class TestPatchParsing:
|
|
29
|
+
def test_valid_patch_parses_on_evaluation(self):
|
|
30
|
+
r = EvaluationResult.from_dict(_block(patch={"new_input": "x"}))
|
|
31
|
+
assert isinstance(r.patch, Patch)
|
|
32
|
+
assert r.patch.new_input == "x"
|
|
33
|
+
|
|
34
|
+
def test_valid_patch_parses_on_approval_block_action(self):
|
|
35
|
+
r = ApprovalResult.from_dict(
|
|
36
|
+
{"action": "block", "patch": {"new_input": [1, 2]}}
|
|
37
|
+
)
|
|
38
|
+
assert r.verdict is Verdict.BLOCK
|
|
39
|
+
assert isinstance(r.patch, Patch)
|
|
40
|
+
assert r.patch.new_input == [1, 2]
|
|
41
|
+
|
|
42
|
+
def test_present_null_is_distinct_from_absent(self):
|
|
43
|
+
present = EvaluationResult.from_dict(_block(patch={"new_input": None}))
|
|
44
|
+
assert present.patch is not None
|
|
45
|
+
assert present.patch.new_input is None
|
|
46
|
+
|
|
47
|
+
absent = EvaluationResult.from_dict(_block())
|
|
48
|
+
assert absent.patch is None
|
|
49
|
+
|
|
50
|
+
def test_json_null_field_is_treated_as_absent(self):
|
|
51
|
+
assert EvaluationResult.from_dict(_block(patch=None)).patch is None
|
|
52
|
+
|
|
53
|
+
def test_falsy_values_preserved(self):
|
|
54
|
+
for val in (None, "", 0, [], {}):
|
|
55
|
+
r = EvaluationResult.from_dict(_block(patch={"new_input": val}))
|
|
56
|
+
assert r.patch is not None, val
|
|
57
|
+
assert r.patch.new_input == val
|
|
58
|
+
|
|
59
|
+
def test_boolean_new_input_rejected(self):
|
|
60
|
+
for val in (True, False):
|
|
61
|
+
r = EvaluationResult.from_dict(_block(patch={"new_input": val}))
|
|
62
|
+
assert r.patch is None
|
|
63
|
+
|
|
64
|
+
def test_extra_key_rejected(self):
|
|
65
|
+
r = EvaluationResult.from_dict(
|
|
66
|
+
_block(patch={"new_input": None, "x": 1})
|
|
67
|
+
)
|
|
68
|
+
assert r.patch is None
|
|
69
|
+
|
|
70
|
+
def test_missing_new_input_rejected(self):
|
|
71
|
+
r = EvaluationResult.from_dict(_block(patch={"other": 1}))
|
|
72
|
+
assert r.patch is None
|
|
73
|
+
|
|
74
|
+
def test_non_dict_patch_rejected(self):
|
|
75
|
+
for bad in ("str", 5, [1], True):
|
|
76
|
+
r = EvaluationResult.from_dict(_block(patch=bad))
|
|
77
|
+
assert r.patch is None
|
|
78
|
+
|
|
79
|
+
def test_numeric_safe_boundary(self):
|
|
80
|
+
assert (
|
|
81
|
+
EvaluationResult.from_dict(
|
|
82
|
+
_block(patch={"new_input": SAFE_MAX})
|
|
83
|
+
).patch
|
|
84
|
+
is not None
|
|
85
|
+
)
|
|
86
|
+
assert (
|
|
87
|
+
EvaluationResult.from_dict(
|
|
88
|
+
_block(patch={"new_input": SAFE_MAX + 1})
|
|
89
|
+
).patch
|
|
90
|
+
is None
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
def test_non_finite_and_nested_unsafe_rejected(self):
|
|
94
|
+
assert (
|
|
95
|
+
EvaluationResult.from_dict(
|
|
96
|
+
_block(patch={"new_input": float("inf")})
|
|
97
|
+
).patch
|
|
98
|
+
is None
|
|
99
|
+
)
|
|
100
|
+
assert (
|
|
101
|
+
EvaluationResult.from_dict(
|
|
102
|
+
_block(patch={"new_input": float("nan")})
|
|
103
|
+
).patch
|
|
104
|
+
is None
|
|
105
|
+
)
|
|
106
|
+
assert (
|
|
107
|
+
EvaluationResult.from_dict(
|
|
108
|
+
_block(patch={"new_input": {"a": [SAFE_MAX + 1]}})
|
|
109
|
+
).patch
|
|
110
|
+
is None
|
|
111
|
+
)
|
|
112
|
+
assert (
|
|
113
|
+
EvaluationResult.from_dict(
|
|
114
|
+
_block(patch={"new_input": {"a": [SAFE_MAX]}})
|
|
115
|
+
).patch
|
|
116
|
+
is not None
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
def test_raw_preserved(self):
|
|
120
|
+
data = _block(patch={"new_input": "x"}, extra="keep")
|
|
121
|
+
r = EvaluationResult.from_dict(data)
|
|
122
|
+
assert r.raw["extra"] == "keep"
|
|
123
|
+
assert r.raw["patch"] == {"new_input": "x"}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class TestHandlePatch:
|
|
127
|
+
def test_directive_from_evaluation_block(self):
|
|
128
|
+
r = EvaluationResult.from_dict(
|
|
129
|
+
_block(
|
|
130
|
+
patch={"new_input": "x"},
|
|
131
|
+
governance_event_id="ev-1",
|
|
132
|
+
reason="blocked",
|
|
133
|
+
)
|
|
134
|
+
)
|
|
135
|
+
d = handle_patch(r)
|
|
136
|
+
assert isinstance(d, PatchDirective)
|
|
137
|
+
assert d.new_input == "x"
|
|
138
|
+
assert d.governance_event_id == "ev-1"
|
|
139
|
+
assert d.reason == "blocked"
|
|
140
|
+
|
|
141
|
+
def test_directive_from_approval_reads_event_id_from_raw(self):
|
|
142
|
+
r = ApprovalResult.from_dict(
|
|
143
|
+
{
|
|
144
|
+
"action": "block",
|
|
145
|
+
"patch": {"new_input": None},
|
|
146
|
+
"governance_event_id": "ev-2",
|
|
147
|
+
"reason": "patch",
|
|
148
|
+
}
|
|
149
|
+
)
|
|
150
|
+
d = handle_patch(r)
|
|
151
|
+
assert d is not None
|
|
152
|
+
assert d.new_input is None
|
|
153
|
+
assert d.governance_event_id == "ev-2"
|
|
154
|
+
|
|
155
|
+
def test_approval_event_id_falls_back_to_id(self):
|
|
156
|
+
r = ApprovalResult.from_dict(
|
|
157
|
+
{"action": "block", "patch": {"new_input": 1}, "id": "ev-3"}
|
|
158
|
+
)
|
|
159
|
+
d = handle_patch(r)
|
|
160
|
+
assert d is not None
|
|
161
|
+
assert d.governance_event_id == "ev-3"
|
|
162
|
+
|
|
163
|
+
def test_plain_block_returns_none(self):
|
|
164
|
+
assert handle_patch(EvaluationResult.from_dict(_block())) is None
|
|
165
|
+
|
|
166
|
+
def test_non_block_verdicts_return_none(self):
|
|
167
|
+
for verdict in ("allow", "constrain", "require_approval", "halt"):
|
|
168
|
+
r = EvaluationResult.from_dict(
|
|
169
|
+
{"verdict": verdict, "patch": {"new_input": "x"}}
|
|
170
|
+
)
|
|
171
|
+
assert handle_patch(r) is None, verdict
|
|
172
|
+
|
|
173
|
+
def test_halt_approval_returns_none_even_with_patch(self):
|
|
174
|
+
r = ApprovalResult.from_dict(
|
|
175
|
+
{"action": "halt", "patch": {"new_input": "x"}}
|
|
176
|
+
)
|
|
177
|
+
assert handle_patch(r) is None
|
|
178
|
+
|
|
179
|
+
def test_expired_approval_returns_none_even_if_block(self):
|
|
180
|
+
r = ApprovalResult.from_dict(
|
|
181
|
+
{"action": "block", "patch": {"new_input": "x"}, "expired": True}
|
|
182
|
+
)
|
|
183
|
+
assert r.verdict is Verdict.BLOCK
|
|
184
|
+
assert handle_patch(r) is None
|
|
185
|
+
|
|
186
|
+
def test_pending_approval_returns_none(self):
|
|
187
|
+
r = ApprovalResult.from_dict({"patch": {"new_input": "x"}})
|
|
188
|
+
assert r.verdict is None
|
|
189
|
+
assert handle_patch(r) is None
|
|
190
|
+
|
|
191
|
+
def test_evaluation_result_never_raises_attributeerror(self):
|
|
192
|
+
# EvaluationResult has no is_blocking()/expired; the gate must not touch them.
|
|
193
|
+
r = EvaluationResult.from_dict(_block(patch={"new_input": "x"}))
|
|
194
|
+
assert handle_patch(r) is not None
|
|
195
|
+
|
|
196
|
+
def test_block_without_patch_returns_none(self):
|
|
197
|
+
assert handle_patch(EvaluationResult.from_dict(_block())) is None
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class TestEnforcementUnchanged:
|
|
201
|
+
def test_block_still_raises_even_with_patch(self):
|
|
202
|
+
# Adding patch support must NOT suppress the default block; the helper is opt-in only.
|
|
203
|
+
r = EvaluationResult.from_dict(
|
|
204
|
+
_block(patch={"new_input": "x"}, reason="nope")
|
|
205
|
+
)
|
|
206
|
+
with pytest.raises(GovernanceBlockedError):
|
|
207
|
+
raise_for_verdict(r)
|
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
"""retry_plan parsing + handle_retryable_block tests (frozen wire contract).
|
|
2
|
-
|
|
3
|
-
Covers: present-null vs absent, falsy-value preservation, boolean rejection, the finite/
|
|
4
|
-
safe-integer numeric rule (incl. nested), action="block" approvals, HALT/expired -> None,
|
|
5
|
-
the EvaluationResult path not raising AttributeError, and default BLOCK enforcement unchanged.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
import pytest
|
|
9
|
-
|
|
10
|
-
from openbox_core.contracts.results import (
|
|
11
|
-
ApprovalResult,
|
|
12
|
-
EvaluationResult,
|
|
13
|
-
RetryDirective,
|
|
14
|
-
RetryPlan,
|
|
15
|
-
Verdict,
|
|
16
|
-
handle_retryable_block,
|
|
17
|
-
)
|
|
18
|
-
from openbox_core.errors import GovernanceBlockedError
|
|
19
|
-
from openbox_core.gate import raise_for_verdict
|
|
20
|
-
|
|
21
|
-
SAFE_MAX = 2**53 - 1
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def _block(**extra):
|
|
25
|
-
return {"verdict": "block", **extra}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
class TestRetryPlanParsing:
|
|
29
|
-
def test_valid_plan_parses_on_evaluation(self):
|
|
30
|
-
r = EvaluationResult.from_dict(_block(retry_plan={"new_input": "x"}))
|
|
31
|
-
assert isinstance(r.retry_plan, RetryPlan)
|
|
32
|
-
assert r.retry_plan.new_input == "x"
|
|
33
|
-
|
|
34
|
-
def test_valid_plan_parses_on_approval_block_action(self):
|
|
35
|
-
r = ApprovalResult.from_dict(
|
|
36
|
-
{"action": "block", "retry_plan": {"new_input": [1, 2]}}
|
|
37
|
-
)
|
|
38
|
-
assert r.verdict is Verdict.BLOCK
|
|
39
|
-
assert isinstance(r.retry_plan, RetryPlan)
|
|
40
|
-
assert r.retry_plan.new_input == [1, 2]
|
|
41
|
-
|
|
42
|
-
def test_present_null_is_distinct_from_absent(self):
|
|
43
|
-
present = EvaluationResult.from_dict(_block(retry_plan={"new_input": None}))
|
|
44
|
-
assert present.retry_plan is not None
|
|
45
|
-
assert present.retry_plan.new_input is None
|
|
46
|
-
|
|
47
|
-
absent = EvaluationResult.from_dict(_block())
|
|
48
|
-
assert absent.retry_plan is None
|
|
49
|
-
|
|
50
|
-
def test_json_null_field_is_treated_as_absent(self):
|
|
51
|
-
assert EvaluationResult.from_dict(_block(retry_plan=None)).retry_plan is None
|
|
52
|
-
|
|
53
|
-
def test_falsy_values_preserved(self):
|
|
54
|
-
for val in (None, "", 0, [], {}):
|
|
55
|
-
r = EvaluationResult.from_dict(_block(retry_plan={"new_input": val}))
|
|
56
|
-
assert r.retry_plan is not None, val
|
|
57
|
-
assert r.retry_plan.new_input == val
|
|
58
|
-
|
|
59
|
-
def test_boolean_new_input_rejected(self):
|
|
60
|
-
for val in (True, False):
|
|
61
|
-
r = EvaluationResult.from_dict(_block(retry_plan={"new_input": val}))
|
|
62
|
-
assert r.retry_plan is None
|
|
63
|
-
|
|
64
|
-
def test_extra_key_rejected(self):
|
|
65
|
-
r = EvaluationResult.from_dict(
|
|
66
|
-
_block(retry_plan={"new_input": None, "x": 1})
|
|
67
|
-
)
|
|
68
|
-
assert r.retry_plan is None
|
|
69
|
-
|
|
70
|
-
def test_missing_new_input_rejected(self):
|
|
71
|
-
r = EvaluationResult.from_dict(_block(retry_plan={"other": 1}))
|
|
72
|
-
assert r.retry_plan is None
|
|
73
|
-
|
|
74
|
-
def test_non_dict_plan_rejected(self):
|
|
75
|
-
for bad in ("str", 5, [1], True):
|
|
76
|
-
r = EvaluationResult.from_dict(_block(retry_plan=bad))
|
|
77
|
-
assert r.retry_plan is None
|
|
78
|
-
|
|
79
|
-
def test_numeric_safe_boundary(self):
|
|
80
|
-
assert (
|
|
81
|
-
EvaluationResult.from_dict(
|
|
82
|
-
_block(retry_plan={"new_input": SAFE_MAX})
|
|
83
|
-
).retry_plan
|
|
84
|
-
is not None
|
|
85
|
-
)
|
|
86
|
-
assert (
|
|
87
|
-
EvaluationResult.from_dict(
|
|
88
|
-
_block(retry_plan={"new_input": SAFE_MAX + 1})
|
|
89
|
-
).retry_plan
|
|
90
|
-
is None
|
|
91
|
-
)
|
|
92
|
-
|
|
93
|
-
def test_non_finite_and_nested_unsafe_rejected(self):
|
|
94
|
-
assert (
|
|
95
|
-
EvaluationResult.from_dict(
|
|
96
|
-
_block(retry_plan={"new_input": float("inf")})
|
|
97
|
-
).retry_plan
|
|
98
|
-
is None
|
|
99
|
-
)
|
|
100
|
-
assert (
|
|
101
|
-
EvaluationResult.from_dict(
|
|
102
|
-
_block(retry_plan={"new_input": float("nan")})
|
|
103
|
-
).retry_plan
|
|
104
|
-
is None
|
|
105
|
-
)
|
|
106
|
-
assert (
|
|
107
|
-
EvaluationResult.from_dict(
|
|
108
|
-
_block(retry_plan={"new_input": {"a": [SAFE_MAX + 1]}})
|
|
109
|
-
).retry_plan
|
|
110
|
-
is None
|
|
111
|
-
)
|
|
112
|
-
assert (
|
|
113
|
-
EvaluationResult.from_dict(
|
|
114
|
-
_block(retry_plan={"new_input": {"a": [SAFE_MAX]}})
|
|
115
|
-
).retry_plan
|
|
116
|
-
is not None
|
|
117
|
-
)
|
|
118
|
-
|
|
119
|
-
def test_raw_preserved(self):
|
|
120
|
-
data = _block(retry_plan={"new_input": "x"}, extra="keep")
|
|
121
|
-
r = EvaluationResult.from_dict(data)
|
|
122
|
-
assert r.raw["extra"] == "keep"
|
|
123
|
-
assert r.raw["retry_plan"] == {"new_input": "x"}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
class TestHandleRetryableBlock:
|
|
127
|
-
def test_directive_from_evaluation_block(self):
|
|
128
|
-
r = EvaluationResult.from_dict(
|
|
129
|
-
_block(
|
|
130
|
-
retry_plan={"new_input": "x"},
|
|
131
|
-
governance_event_id="ev-1",
|
|
132
|
-
reason="blocked",
|
|
133
|
-
)
|
|
134
|
-
)
|
|
135
|
-
d = handle_retryable_block(r)
|
|
136
|
-
assert isinstance(d, RetryDirective)
|
|
137
|
-
assert d.new_input == "x"
|
|
138
|
-
assert d.governance_event_id == "ev-1"
|
|
139
|
-
assert d.reason == "blocked"
|
|
140
|
-
|
|
141
|
-
def test_directive_from_approval_reads_event_id_from_raw(self):
|
|
142
|
-
r = ApprovalResult.from_dict(
|
|
143
|
-
{
|
|
144
|
-
"action": "block",
|
|
145
|
-
"retry_plan": {"new_input": None},
|
|
146
|
-
"governance_event_id": "ev-2",
|
|
147
|
-
"reason": "retry",
|
|
148
|
-
}
|
|
149
|
-
)
|
|
150
|
-
d = handle_retryable_block(r)
|
|
151
|
-
assert d is not None
|
|
152
|
-
assert d.new_input is None
|
|
153
|
-
assert d.governance_event_id == "ev-2"
|
|
154
|
-
|
|
155
|
-
def test_approval_event_id_falls_back_to_id(self):
|
|
156
|
-
r = ApprovalResult.from_dict(
|
|
157
|
-
{"action": "block", "retry_plan": {"new_input": 1}, "id": "ev-3"}
|
|
158
|
-
)
|
|
159
|
-
d = handle_retryable_block(r)
|
|
160
|
-
assert d is not None
|
|
161
|
-
assert d.governance_event_id == "ev-3"
|
|
162
|
-
|
|
163
|
-
def test_plain_block_returns_none(self):
|
|
164
|
-
assert handle_retryable_block(EvaluationResult.from_dict(_block())) is None
|
|
165
|
-
|
|
166
|
-
def test_non_block_verdicts_return_none(self):
|
|
167
|
-
for verdict in ("allow", "constrain", "require_approval", "halt"):
|
|
168
|
-
r = EvaluationResult.from_dict(
|
|
169
|
-
{"verdict": verdict, "retry_plan": {"new_input": "x"}}
|
|
170
|
-
)
|
|
171
|
-
assert handle_retryable_block(r) is None, verdict
|
|
172
|
-
|
|
173
|
-
def test_halt_approval_returns_none_even_with_plan(self):
|
|
174
|
-
r = ApprovalResult.from_dict(
|
|
175
|
-
{"action": "halt", "retry_plan": {"new_input": "x"}}
|
|
176
|
-
)
|
|
177
|
-
assert handle_retryable_block(r) is None
|
|
178
|
-
|
|
179
|
-
def test_expired_approval_returns_none_even_if_block(self):
|
|
180
|
-
r = ApprovalResult.from_dict(
|
|
181
|
-
{"action": "block", "retry_plan": {"new_input": "x"}, "expired": True}
|
|
182
|
-
)
|
|
183
|
-
assert r.verdict is Verdict.BLOCK
|
|
184
|
-
assert handle_retryable_block(r) is None
|
|
185
|
-
|
|
186
|
-
def test_pending_approval_returns_none(self):
|
|
187
|
-
r = ApprovalResult.from_dict({"retry_plan": {"new_input": "x"}})
|
|
188
|
-
assert r.verdict is None
|
|
189
|
-
assert handle_retryable_block(r) is None
|
|
190
|
-
|
|
191
|
-
def test_evaluation_result_never_raises_attributeerror(self):
|
|
192
|
-
# EvaluationResult has no is_blocking()/expired; the gate must not touch them.
|
|
193
|
-
r = EvaluationResult.from_dict(_block(retry_plan={"new_input": "x"}))
|
|
194
|
-
assert handle_retryable_block(r) is not None
|
|
195
|
-
|
|
196
|
-
def test_block_without_plan_returns_none(self):
|
|
197
|
-
assert handle_retryable_block(EvaluationResult.from_dict(_block())) is None
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
class TestEnforcementUnchanged:
|
|
201
|
-
def test_block_still_raises_even_with_retry_plan(self):
|
|
202
|
-
# Adding retry_plan support must NOT suppress the default block; the helper is opt-in only.
|
|
203
|
-
r = EvaluationResult.from_dict(
|
|
204
|
-
_block(retry_plan={"new_input": "x"}, reason="nope")
|
|
205
|
-
)
|
|
206
|
-
with pytest.raises(GovernanceBlockedError):
|
|
207
|
-
raise_for_verdict(r)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/openbox_core/conformance/hook_preflight.py
RENAMED
|
File without changes
|
{openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/openbox_core/conformance/instrumentation.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/openbox_core/instrumentation/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/openbox_core/instrumentation/function.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/openbox_core/instrumentation/manager.py
RENAMED
|
File without changes
|
{openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/openbox_core/instrumentation/shared.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/openbox_core/validation/diagnostics.py
RENAMED
|
File without changes
|
{openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/openbox_core/validation/event_rules.py
RENAMED
|
File without changes
|
|
File without changes
|
{openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/openbox_core/validation/span_normalization.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/tests/conformance/test_required_cases.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/tests/contracts/test_approval_parsing.py
RENAMED
|
File without changes
|
{openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/tests/contracts/test_event_classify.py
RENAMED
|
File without changes
|
{openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/tests/contracts/test_result_parsing.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/tests/instrumentation/instrumented_env.py
RENAMED
|
File without changes
|
{openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/tests/instrumentation/test_db_block.py
RENAMED
|
File without changes
|
{openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/tests/instrumentation/test_db_redis_mongo.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/tests/instrumentation/test_hook_runtime.py
RENAMED
|
File without changes
|
|
File without changes
|
{openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/tests/instrumentation/test_http_urllib.py
RENAMED
|
File without changes
|
|
File without changes
|
{openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/tests/runtime/test_runtime_delegation.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openbox_sdk_python-1.1.0 → openbox_sdk_python-1.2.0}/tests/wire/test_core_span_projection.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|