actseal 0.1.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.
- actseal/__init__.py +83 -0
- actseal/__main__.py +8 -0
- actseal/adapters/__init__.py +8 -0
- actseal/adapters/base.py +35 -0
- actseal/adapters/fixture.py +166 -0
- actseal/adapters/laya.py +738 -0
- actseal/assessment.py +281 -0
- actseal/cli.py +492 -0
- actseal/contract.py +204 -0
- actseal/demo_data/__init__.py +23 -0
- actseal/demo_data/bad.toml +22 -0
- actseal/demo_data/bad_calibration.jsonl +12 -0
- actseal/demo_data/bad_responses.jsonl +128 -0
- actseal/demo_data/bad_verification.jsonl +128 -0
- actseal/demo_data/fixed.toml +22 -0
- actseal/demo_data/fixed_calibration.jsonl +12 -0
- actseal/demo_data/fixed_responses.jsonl +128 -0
- actseal/demo_data/fixed_verification.jsonl +128 -0
- actseal/errors.py +25 -0
- actseal/evidence.py +533 -0
- actseal/faults.py +136 -0
- actseal/locking.py +283 -0
- actseal/normalization.py +274 -0
- actseal/policy.py +83 -0
- actseal/py.typed +0 -0
- actseal/records.py +747 -0
- actseal/replay.py +263 -0
- actseal/runner.py +375 -0
- actseal/serialization.py +578 -0
- actseal/stats.py +170 -0
- actseal-0.1.0.dist-info/METADATA +136 -0
- actseal-0.1.0.dist-info/RECORD +36 -0
- actseal-0.1.0.dist-info/WHEEL +4 -0
- actseal-0.1.0.dist-info/entry_points.txt +2 -0
- actseal-0.1.0.dist-info/licenses/LICENSE +201 -0
- actseal-0.1.0.dist-info/licenses/NOTICE +12 -0
actseal/__init__.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Actseal: verify frozen categorical decision policies with sealed, replayable evidence.
|
|
2
|
+
|
|
3
|
+
The runtime core depends only on the Python standard library. Importing this
|
|
4
|
+
package never imports an optional model stack.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from actseal.errors import ActsealError, IntegrityError, ProviderSetupError, SchemaError
|
|
10
|
+
from actseal.records import (
|
|
11
|
+
Action,
|
|
12
|
+
CapturedOutcome,
|
|
13
|
+
Case,
|
|
14
|
+
CaseRef,
|
|
15
|
+
ChoiceAnswer,
|
|
16
|
+
ChoiceQuestion,
|
|
17
|
+
Contract,
|
|
18
|
+
DecisionRecord,
|
|
19
|
+
DecisionRequest,
|
|
20
|
+
EvidenceBundle,
|
|
21
|
+
EvidenceScope,
|
|
22
|
+
FaultResult,
|
|
23
|
+
FaultSpec,
|
|
24
|
+
GateLimits,
|
|
25
|
+
Interval,
|
|
26
|
+
LockedPolicy,
|
|
27
|
+
ModelIdentity,
|
|
28
|
+
Option,
|
|
29
|
+
Outcome,
|
|
30
|
+
PlanLock,
|
|
31
|
+
PolicyDecision,
|
|
32
|
+
ProviderFailure,
|
|
33
|
+
Status,
|
|
34
|
+
Verdict,
|
|
35
|
+
)
|
|
36
|
+
from actseal.serialization import (
|
|
37
|
+
canonical_json,
|
|
38
|
+
from_data,
|
|
39
|
+
implementation_fingerprint,
|
|
40
|
+
sha256_bytes,
|
|
41
|
+
strict_json_loads,
|
|
42
|
+
to_data,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
__version__ = "0.1.0"
|
|
46
|
+
|
|
47
|
+
__all__ = [
|
|
48
|
+
"Action",
|
|
49
|
+
"ActsealError",
|
|
50
|
+
"CapturedOutcome",
|
|
51
|
+
"Case",
|
|
52
|
+
"CaseRef",
|
|
53
|
+
"ChoiceAnswer",
|
|
54
|
+
"ChoiceQuestion",
|
|
55
|
+
"Contract",
|
|
56
|
+
"DecisionRecord",
|
|
57
|
+
"DecisionRequest",
|
|
58
|
+
"EvidenceBundle",
|
|
59
|
+
"EvidenceScope",
|
|
60
|
+
"FaultResult",
|
|
61
|
+
"FaultSpec",
|
|
62
|
+
"GateLimits",
|
|
63
|
+
"IntegrityError",
|
|
64
|
+
"Interval",
|
|
65
|
+
"LockedPolicy",
|
|
66
|
+
"ModelIdentity",
|
|
67
|
+
"Option",
|
|
68
|
+
"Outcome",
|
|
69
|
+
"PlanLock",
|
|
70
|
+
"PolicyDecision",
|
|
71
|
+
"ProviderFailure",
|
|
72
|
+
"ProviderSetupError",
|
|
73
|
+
"SchemaError",
|
|
74
|
+
"Status",
|
|
75
|
+
"Verdict",
|
|
76
|
+
"__version__",
|
|
77
|
+
"canonical_json",
|
|
78
|
+
"from_data",
|
|
79
|
+
"implementation_fingerprint",
|
|
80
|
+
"sha256_bytes",
|
|
81
|
+
"strict_json_loads",
|
|
82
|
+
"to_data",
|
|
83
|
+
]
|
actseal/__main__.py
ADDED
actseal/adapters/base.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""The decision-model protocol (plan/CONTRACTS.md section 4).
|
|
2
|
+
|
|
3
|
+
A provider captures raw evidence; it never normalizes, retries, falls back or
|
|
4
|
+
authorizes an action. ``decide`` returns a :class:`CapturedOutcome` whose
|
|
5
|
+
``request_sha256`` is the canonical hash of the request, whose ``identity`` is
|
|
6
|
+
the provider's actual identity, and whose body/failure are exclusive. Setup
|
|
7
|
+
problems raise :class:`actseal.errors.ProviderSetupError`; captured failures
|
|
8
|
+
use the frozen failure codes.
|
|
9
|
+
|
|
10
|
+
This module imports nothing beyond the standard library and ``records``.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from typing import Protocol
|
|
16
|
+
|
|
17
|
+
from actseal.records import CapturedOutcome, DecisionRequest, ModelIdentity
|
|
18
|
+
|
|
19
|
+
__all__ = ["DecisionModel"]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class DecisionModel(Protocol):
|
|
23
|
+
"""Sequential capture of locked decision requests against one resident model."""
|
|
24
|
+
|
|
25
|
+
def identity(self) -> ModelIdentity:
|
|
26
|
+
"""The actual identity of the loaded model, fixed for the object's lifetime."""
|
|
27
|
+
...
|
|
28
|
+
|
|
29
|
+
def decide(self, request: DecisionRequest, *, timeout_s: float) -> CapturedOutcome:
|
|
30
|
+
"""Capture one request within a finite positive deadline; never raise for a failure."""
|
|
31
|
+
...
|
|
32
|
+
|
|
33
|
+
def close(self) -> None:
|
|
34
|
+
"""Release resources; idempotent and leaves no worker alive."""
|
|
35
|
+
...
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""Recorded-response fixture adapter.
|
|
2
|
+
|
|
3
|
+
The fixture file is JSONL with rows ``{case_id, body_json, failure_code,
|
|
4
|
+
warnings}``; ``body_json`` is a string (the recorded inner answer object, see
|
|
5
|
+
docs/providers.md), exactly one of ``body_json``/``failure_code`` is present,
|
|
6
|
+
and ``warnings`` is a list of nonempty strings. Rows are at most 1 MiB each and
|
|
7
|
+
the whole file at most 128 MiB (ADR 0011), enforced with a bounded read of at
|
|
8
|
+
most limit+1 bytes before decoding, splitting or hashing.
|
|
9
|
+
The file supplies no identity: the identity is derived from the raw file bytes
|
|
10
|
+
so that any change to the recording changes the revision.
|
|
11
|
+
|
|
12
|
+
A requested case that is not in the file is a :class:`ProviderSetupError`,
|
|
13
|
+
never a skip. Captures are deterministic and carry ``fallback_used=False``.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import math
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Final
|
|
21
|
+
|
|
22
|
+
from actseal.errors import ProviderSetupError, SchemaError
|
|
23
|
+
from actseal.normalization import NORMALIZER_VERSION, request_sha256
|
|
24
|
+
from actseal.records import (
|
|
25
|
+
FAILURE_CODES,
|
|
26
|
+
CapturedOutcome,
|
|
27
|
+
DecisionRequest,
|
|
28
|
+
ModelIdentity,
|
|
29
|
+
)
|
|
30
|
+
from actseal.serialization import sha256_bytes, strict_json_loads
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"ADAPTER_VERSION",
|
|
34
|
+
"MAX_FIXTURE_BYTES",
|
|
35
|
+
"MAX_ROW_BYTES",
|
|
36
|
+
"MODEL_NAME",
|
|
37
|
+
"FixtureModel",
|
|
38
|
+
"validate_timeout",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
ADAPTER_VERSION: Final = "1"
|
|
42
|
+
MODEL_NAME: Final = "recorded-choice-v1"
|
|
43
|
+
MAX_ROW_BYTES: Final = 1024 * 1024
|
|
44
|
+
MAX_FIXTURE_BYTES: Final = 128 * 1024 * 1024
|
|
45
|
+
_ROW_KEYS: Final[frozenset[str]] = frozenset({"case_id", "body_json", "failure_code", "warnings"})
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def validate_timeout(timeout_s: object) -> float:
|
|
49
|
+
"""Return ``timeout_s`` as a finite positive float or raise :class:`SchemaError`."""
|
|
50
|
+
if type(timeout_s) is bool or not isinstance(timeout_s, int | float):
|
|
51
|
+
raise SchemaError("timeout_s: must be a number")
|
|
52
|
+
try:
|
|
53
|
+
value = float(timeout_s)
|
|
54
|
+
except OverflowError:
|
|
55
|
+
raise SchemaError("timeout_s: must be finite") from None
|
|
56
|
+
if not math.isfinite(value) or value <= 0.0:
|
|
57
|
+
raise SchemaError("timeout_s: must be a finite positive number")
|
|
58
|
+
return value
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class _Row:
|
|
62
|
+
__slots__ = ("body_json", "failure_code", "warnings")
|
|
63
|
+
|
|
64
|
+
def __init__(
|
|
65
|
+
self, body_json: str | None, failure_code: str | None, warnings: tuple[str, ...]
|
|
66
|
+
) -> None:
|
|
67
|
+
self.body_json = body_json
|
|
68
|
+
self.failure_code = failure_code
|
|
69
|
+
self.warnings = warnings
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _parse_row(index: int, text: str) -> tuple[str, _Row]:
|
|
73
|
+
path = f"responses[{index}]"
|
|
74
|
+
if len(text.encode("utf-8")) > MAX_ROW_BYTES:
|
|
75
|
+
raise SchemaError(f"{path}: row exceeds {MAX_ROW_BYTES} bytes")
|
|
76
|
+
try:
|
|
77
|
+
value = strict_json_loads(text)
|
|
78
|
+
except SchemaError as exc:
|
|
79
|
+
raise SchemaError(f"{path}: {exc}") from None
|
|
80
|
+
if type(value) is not dict:
|
|
81
|
+
raise SchemaError(f"{path}: expected object")
|
|
82
|
+
if set(value) != _ROW_KEYS:
|
|
83
|
+
raise SchemaError(f"{path}: expected exactly case_id, body_json, failure_code, warnings")
|
|
84
|
+
case_id = value["case_id"]
|
|
85
|
+
if type(case_id) is not str or not case_id:
|
|
86
|
+
raise SchemaError(f"{path}.case_id: must be a nonempty string")
|
|
87
|
+
body = value["body_json"]
|
|
88
|
+
failure = value["failure_code"]
|
|
89
|
+
if body is not None and type(body) is not str:
|
|
90
|
+
raise SchemaError(f"{path}.body_json: must be a string or null")
|
|
91
|
+
if failure is not None and (type(failure) is not str or failure not in FAILURE_CODES):
|
|
92
|
+
raise SchemaError(f"{path}.failure_code: unsupported value")
|
|
93
|
+
if (body is None) == (failure is None):
|
|
94
|
+
raise SchemaError(f"{path}: exactly one of body_json/failure_code must be present")
|
|
95
|
+
warnings = value["warnings"]
|
|
96
|
+
if type(warnings) is not list or any(type(w) is not str or not w for w in warnings):
|
|
97
|
+
raise SchemaError(f"{path}.warnings: must be a list of nonempty strings")
|
|
98
|
+
return case_id, _Row(body, failure, tuple(warnings))
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _parse_rows(data: bytes) -> dict[str, _Row]:
|
|
102
|
+
try:
|
|
103
|
+
text = data.decode("utf-8")
|
|
104
|
+
except UnicodeDecodeError:
|
|
105
|
+
raise SchemaError("responses: must be valid UTF-8") from None
|
|
106
|
+
lines = text.split("\n")
|
|
107
|
+
if lines and lines[-1] == "":
|
|
108
|
+
lines.pop()
|
|
109
|
+
if not lines:
|
|
110
|
+
raise SchemaError("responses: must contain at least one row")
|
|
111
|
+
rows: dict[str, _Row] = {}
|
|
112
|
+
for index, line in enumerate(lines):
|
|
113
|
+
case_id, row = _parse_row(index, line)
|
|
114
|
+
if case_id in rows:
|
|
115
|
+
raise SchemaError(f"responses[{index}].case_id: duplicate case id")
|
|
116
|
+
rows[case_id] = row
|
|
117
|
+
return rows
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class FixtureModel:
|
|
121
|
+
"""Replay recorded provider responses keyed by case id."""
|
|
122
|
+
|
|
123
|
+
def __init__(self, responses: Path) -> None:
|
|
124
|
+
if not isinstance(responses, Path):
|
|
125
|
+
raise SchemaError("responses: must be a Path")
|
|
126
|
+
try:
|
|
127
|
+
with responses.open("rb") as handle:
|
|
128
|
+
data = handle.read(MAX_FIXTURE_BYTES + 1)
|
|
129
|
+
except OSError:
|
|
130
|
+
raise ProviderSetupError("responses: fixture file could not be read") from None
|
|
131
|
+
if len(data) > MAX_FIXTURE_BYTES:
|
|
132
|
+
raise SchemaError(f"responses: file exceeds {MAX_FIXTURE_BYTES} bytes")
|
|
133
|
+
self._rows = _parse_rows(data)
|
|
134
|
+
file_hash = sha256_bytes(data)
|
|
135
|
+
self._identity = ModelIdentity(
|
|
136
|
+
"fixture",
|
|
137
|
+
MODEL_NAME,
|
|
138
|
+
file_hash,
|
|
139
|
+
(("responses", file_hash),),
|
|
140
|
+
ADAPTER_VERSION,
|
|
141
|
+
NORMALIZER_VERSION,
|
|
142
|
+
(),
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
def identity(self) -> ModelIdentity:
|
|
146
|
+
return self._identity
|
|
147
|
+
|
|
148
|
+
def decide(self, request: DecisionRequest, *, timeout_s: float) -> CapturedOutcome:
|
|
149
|
+
validate_timeout(timeout_s)
|
|
150
|
+
if not isinstance(request, DecisionRequest):
|
|
151
|
+
raise SchemaError("request: must be DecisionRequest")
|
|
152
|
+
row = self._rows.get(request.case_id)
|
|
153
|
+
if row is None:
|
|
154
|
+
raise ProviderSetupError("responses: requested case id is not recorded")
|
|
155
|
+
return CapturedOutcome(
|
|
156
|
+
request_sha256(request),
|
|
157
|
+
self._identity,
|
|
158
|
+
row.body_json,
|
|
159
|
+
row.failure_code,
|
|
160
|
+
row.warnings,
|
|
161
|
+
fallback_used=False,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
def close(self) -> None:
|
|
165
|
+
"""Nothing to release; recorded responses stay available until the object is dropped."""
|
|
166
|
+
return
|