openbox-sdk-python 1.1.0__py3-none-any.whl → 1.2.0__py3-none-any.whl
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_core/__init__.py +1 -1
- openbox_core/contracts/results.py +39 -38
- {openbox_sdk_python-1.1.0.dist-info → openbox_sdk_python-1.2.0.dist-info}/METADATA +1 -1
- {openbox_sdk_python-1.1.0.dist-info → openbox_sdk_python-1.2.0.dist-info}/RECORD +5 -5
- {openbox_sdk_python-1.1.0.dist-info → openbox_sdk_python-1.2.0.dist-info}/WHEEL +0 -0
openbox_core/__init__.py
CHANGED
|
@@ -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
|
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
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
openbox_core/__init__.py,sha256=
|
|
1
|
+
openbox_core/__init__.py,sha256=PYBEmt7aGSXdg_PUsxqHbAyUXBafHC9EJRudnEZhK0E,1983
|
|
2
2
|
openbox_core/approvals.py,sha256=eobLN4rSBvaFaTbsPExu2XcGS5fSuWvo0GC1DR18w0I,4400
|
|
3
3
|
openbox_core/client.py,sha256=Mo3HSFj7VmDYLiTJ0FnJdxNdetjU-NHK8IrBU7AL4vU,12615
|
|
4
4
|
openbox_core/config.py,sha256=-YasfXrRmA2u_iywtFF0AysAp88hzHWu19je5cy4mo8,9684
|
|
@@ -20,7 +20,7 @@ openbox_core/contracts/__init__.py,sha256=7uBSUIxfUG6uZDsotQmT1jjXs3G75M2hFHtYEl
|
|
|
20
20
|
openbox_core/contracts/context.py,sha256=V-eVbB-NIrXI1NKduengG0GVx7q1wKawyhwZh-D0C9Y,2873
|
|
21
21
|
openbox_core/contracts/events.py,sha256=Loy5IW4UyPiffBVYK06IxiID6NKsNQjoTLguBb-j4Uw,13428
|
|
22
22
|
openbox_core/contracts/otel_spans.py,sha256=PnElhgdOM_qlYVF9thPxvhNxloXOb3fpgSXDQN3evWg,10753
|
|
23
|
-
openbox_core/contracts/results.py,sha256=
|
|
23
|
+
openbox_core/contracts/results.py,sha256=2ysg0wpOfA4IeKZ_wNd0RYJAX_fG3OSgreMb6hSrBX4,16075
|
|
24
24
|
openbox_core/hooks/__init__.py,sha256=jAZGEbN-sZbBD7o_tV-9ZDhBki88yHXKVQf4qhhkYTc,63
|
|
25
25
|
openbox_core/hooks/events.py,sha256=PQSzH_8vLyUopjCDu6dxCtVh7OvNbiWnLx87fCY5jnU,2129
|
|
26
26
|
openbox_core/hooks/preflight.py,sha256=uTgwuiV_WkV5RorbWz3tk0FXV85B1YIcjsDfFj-b6Fg,12520
|
|
@@ -47,6 +47,6 @@ openbox_core/validation/span_normalization.py,sha256=mB8GghMwOo3WGdKNrm6g8krP7Km
|
|
|
47
47
|
openbox_core/wire/__init__.py,sha256=bI-snmNkEvLKqyYl1fItOCwAo46ehKD9vykOFfqaZz0,105
|
|
48
48
|
openbox_core/wire/core_span.py,sha256=U_d93VSa8992DQ_dqP2dgzp4Vu7eEROishH_oQZFuvM,5059
|
|
49
49
|
openbox_core/wire/evaluate_payload.py,sha256=RQ9TaeMLQ08YSmN1FYlvVFfOy0FLp0ve6LBLDi2ZKeI,2027
|
|
50
|
-
openbox_sdk_python-1.
|
|
51
|
-
openbox_sdk_python-1.
|
|
52
|
-
openbox_sdk_python-1.
|
|
50
|
+
openbox_sdk_python-1.2.0.dist-info/METADATA,sha256=lKukdD0C2JtaTGVyKtz2PZ69VtqSBQRgifu70IxKNIQ,4385
|
|
51
|
+
openbox_sdk_python-1.2.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
52
|
+
openbox_sdk_python-1.2.0.dist-info/RECORD,,
|
|
File without changes
|