problem-frame-gate 0.3.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.
- problem_frame_gate/__init__.py +104 -0
- problem_frame_gate/_version.py +1 -0
- problem_frame_gate/certificates.py +116 -0
- problem_frame_gate/cli.py +183 -0
- problem_frame_gate/digest.py +69 -0
- problem_frame_gate/errors.py +17 -0
- problem_frame_gate/fold.py +425 -0
- problem_frame_gate/formation.py +155 -0
- problem_frame_gate/gate.py +365 -0
- problem_frame_gate/join.py +150 -0
- problem_frame_gate/model.py +441 -0
- problem_frame_gate/patch.py +191 -0
- problem_frame_gate/py.typed +1 -0
- problem_frame_gate/records.py +210 -0
- problem_frame_gate/result.py +148 -0
- problem_frame_gate/risk.py +203 -0
- problem_frame_gate/security.py +88 -0
- problem_frame_gate/verifier.py +594 -0
- problem_frame_gate-0.3.0.dist-info/METADATA +153 -0
- problem_frame_gate-0.3.0.dist-info/RECORD +24 -0
- problem_frame_gate-0.3.0.dist-info/WHEEL +4 -0
- problem_frame_gate-0.3.0.dist-info/entry_points.txt +2 -0
- problem_frame_gate-0.3.0.dist-info/licenses/LICENSE +175 -0
- problem_frame_gate-0.3.0.dist-info/licenses/NOTICE +4 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Practical finite audit calculus for AI problem-frame activation."""
|
|
2
|
+
|
|
3
|
+
from ._version import __version__
|
|
4
|
+
from .certificates import CertificateFamily, all_certificates_live, check_certificate_live
|
|
5
|
+
from .digest import canonical_json_bytes, digest_json, digest_many
|
|
6
|
+
from .fold import FoldKernel, FoldState, default_components
|
|
7
|
+
from .formation import FormationProof, check_formation, check_well_audited
|
|
8
|
+
from .gate import ExecutorGate, GateBundle, GateRecord, GateRequest
|
|
9
|
+
from .join import JoinChecker, JoinProposal, union_join
|
|
10
|
+
from .model import (
|
|
11
|
+
AuditTranscript,
|
|
12
|
+
DependencyRef,
|
|
13
|
+
Envelope,
|
|
14
|
+
EnvelopeClass,
|
|
15
|
+
Frame,
|
|
16
|
+
Horizon,
|
|
17
|
+
OrderEdge,
|
|
18
|
+
Status,
|
|
19
|
+
StrictManifest,
|
|
20
|
+
VersionInterval,
|
|
21
|
+
)
|
|
22
|
+
from .patch import AffectedClauseSet, PatchChecker, PatchProposal, ReadFootprint, TouchMatrix, WriteClass
|
|
23
|
+
from .records import (
|
|
24
|
+
ReachabilityTranscript,
|
|
25
|
+
ReplayCertificate,
|
|
26
|
+
SourceCut,
|
|
27
|
+
SwapCover,
|
|
28
|
+
TransitionRecord,
|
|
29
|
+
check_reachability,
|
|
30
|
+
check_replay_certificate,
|
|
31
|
+
check_source_cut,
|
|
32
|
+
)
|
|
33
|
+
from .result import CheckResult, Issue
|
|
34
|
+
from .risk import (
|
|
35
|
+
RiskClaimRecord,
|
|
36
|
+
RiskLedgerSummary,
|
|
37
|
+
check_risk_claims,
|
|
38
|
+
check_risk_ledger,
|
|
39
|
+
check_risk_spend_live,
|
|
40
|
+
summarize_risk_ledger,
|
|
41
|
+
)
|
|
42
|
+
from .security import SensitiveDataIssue, scan_for_sensitive_data
|
|
43
|
+
from .verifier import EnvelopeVerifier, canonical_order, digest_log, legal_log
|
|
44
|
+
|
|
45
|
+
__all__ = [
|
|
46
|
+
"AffectedClauseSet",
|
|
47
|
+
"AuditTranscript",
|
|
48
|
+
"CertificateFamily",
|
|
49
|
+
"CheckResult",
|
|
50
|
+
"DependencyRef",
|
|
51
|
+
"Envelope",
|
|
52
|
+
"EnvelopeClass",
|
|
53
|
+
"EnvelopeVerifier",
|
|
54
|
+
"ExecutorGate",
|
|
55
|
+
"FoldKernel",
|
|
56
|
+
"FoldState",
|
|
57
|
+
"FormationProof",
|
|
58
|
+
"Frame",
|
|
59
|
+
"GateBundle",
|
|
60
|
+
"GateRecord",
|
|
61
|
+
"GateRequest",
|
|
62
|
+
"Horizon",
|
|
63
|
+
"Issue",
|
|
64
|
+
"JoinChecker",
|
|
65
|
+
"JoinProposal",
|
|
66
|
+
"OrderEdge",
|
|
67
|
+
"PatchChecker",
|
|
68
|
+
"PatchProposal",
|
|
69
|
+
"ReachabilityTranscript",
|
|
70
|
+
"ReadFootprint",
|
|
71
|
+
"ReplayCertificate",
|
|
72
|
+
"RiskClaimRecord",
|
|
73
|
+
"RiskLedgerSummary",
|
|
74
|
+
"SensitiveDataIssue",
|
|
75
|
+
"SourceCut",
|
|
76
|
+
"Status",
|
|
77
|
+
"StrictManifest",
|
|
78
|
+
"SwapCover",
|
|
79
|
+
"TouchMatrix",
|
|
80
|
+
"TransitionRecord",
|
|
81
|
+
"VersionInterval",
|
|
82
|
+
"WriteClass",
|
|
83
|
+
"__version__",
|
|
84
|
+
"all_certificates_live",
|
|
85
|
+
"canonical_json_bytes",
|
|
86
|
+
"canonical_order",
|
|
87
|
+
"check_certificate_live",
|
|
88
|
+
"check_formation",
|
|
89
|
+
"check_reachability",
|
|
90
|
+
"check_replay_certificate",
|
|
91
|
+
"check_risk_claims",
|
|
92
|
+
"check_risk_ledger",
|
|
93
|
+
"check_risk_spend_live",
|
|
94
|
+
"check_source_cut",
|
|
95
|
+
"check_well_audited",
|
|
96
|
+
"default_components",
|
|
97
|
+
"digest_json",
|
|
98
|
+
"digest_log",
|
|
99
|
+
"digest_many",
|
|
100
|
+
"legal_log",
|
|
101
|
+
"scan_for_sensitive_data",
|
|
102
|
+
"summarize_risk_ledger",
|
|
103
|
+
"union_join",
|
|
104
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.0"
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Time-indexed certificate checks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from .fold import FoldState
|
|
9
|
+
from .model import Horizon
|
|
10
|
+
from .result import CheckBuilder, CheckResult
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True, slots=True)
|
|
14
|
+
class CertificateFamily:
|
|
15
|
+
"""Finite certificate-family policy."""
|
|
16
|
+
|
|
17
|
+
name: str
|
|
18
|
+
issuers: tuple[str, ...]
|
|
19
|
+
assumption: str = ""
|
|
20
|
+
|
|
21
|
+
def to_json(self) -> dict[str, Any]:
|
|
22
|
+
return {"name": self.name, "issuers": list(self.issuers), "assumption": self.assumption}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def check_certificate_live(
|
|
26
|
+
state: FoldState, cert_id: str, at_time: int, *, horizon: Horizon | None = None
|
|
27
|
+
) -> CheckResult:
|
|
28
|
+
"""Check that a certificate was issued and not revoked or expired at a time."""
|
|
29
|
+
|
|
30
|
+
builder = CheckBuilder(footprint={"IssuerAuthentication", "RevocationOracle", "ClockWatermark"})
|
|
31
|
+
certs: dict[str, dict[str, Any]] = state.component("certificates")
|
|
32
|
+
cert = certs.get(cert_id)
|
|
33
|
+
if cert is None or not cert.get("issued"):
|
|
34
|
+
builder.error("certificate-missing", "certificate is not issued", location=cert_id)
|
|
35
|
+
return builder.result()
|
|
36
|
+
|
|
37
|
+
issued_at = int(cert.get("issued_at", 0))
|
|
38
|
+
if issued_at > at_time:
|
|
39
|
+
builder.error(
|
|
40
|
+
"certificate-issued-after-use",
|
|
41
|
+
"certificate issue time is after the checked time",
|
|
42
|
+
location=cert_id,
|
|
43
|
+
details={"issued_at": issued_at, "at_time": at_time},
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
revoked_at = cert.get("revoked_at")
|
|
47
|
+
if revoked_at is not None and int(revoked_at) <= at_time:
|
|
48
|
+
builder.error(
|
|
49
|
+
"certificate-revoked",
|
|
50
|
+
"certificate is revoked at the checked time",
|
|
51
|
+
location=cert_id,
|
|
52
|
+
details={"revoked_at": revoked_at, "at_time": at_time},
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
expires_at = cert.get("expires_at")
|
|
56
|
+
if expires_at is not None and int(expires_at) <= at_time:
|
|
57
|
+
builder.error(
|
|
58
|
+
"certificate-expired",
|
|
59
|
+
"certificate is expired at the checked time",
|
|
60
|
+
location=cert_id,
|
|
61
|
+
details={"expires_at": expires_at, "at_time": at_time},
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
family = str(cert.get("family", ""))
|
|
65
|
+
issuer = str(cert.get("issuer", ""))
|
|
66
|
+
if horizon is not None and horizon.strict:
|
|
67
|
+
allowed_issuers = horizon.certificate_families.get(family)
|
|
68
|
+
if allowed_issuers is None:
|
|
69
|
+
builder.error("certificate-family", "certificate family is not declared by the manifest", location=cert_id)
|
|
70
|
+
elif issuer not in allowed_issuers:
|
|
71
|
+
builder.error(
|
|
72
|
+
"certificate-issuer",
|
|
73
|
+
"certificate issuer is not authorized for its family",
|
|
74
|
+
location=cert_id,
|
|
75
|
+
details={"family": family, "issuer": issuer, "allowed": list(allowed_issuers)},
|
|
76
|
+
)
|
|
77
|
+
if cert.get("family_check") not in {True, "ok", "OK"}:
|
|
78
|
+
builder.error(
|
|
79
|
+
"certificate-family-check",
|
|
80
|
+
"strict certificate must carry an accepted finite family check",
|
|
81
|
+
location=cert_id,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
ordered = set(state.ordered_eids)
|
|
85
|
+
evidence = state.component("evidence")
|
|
86
|
+
for dep in cert.get("dependencies", ()):
|
|
87
|
+
dep_id = str(dep)
|
|
88
|
+
if dep_id not in ordered and dep_id not in evidence and dep_id not in certs:
|
|
89
|
+
builder.error(
|
|
90
|
+
"certificate-dependency",
|
|
91
|
+
"certificate dependency is absent from the source prefix",
|
|
92
|
+
location=cert_id,
|
|
93
|
+
details={"dependency": dep_id},
|
|
94
|
+
)
|
|
95
|
+
for source_id in cert.get("source_ids", ()):
|
|
96
|
+
source = evidence.get(str(source_id))
|
|
97
|
+
if source is None:
|
|
98
|
+
builder.error("certificate-source", "certificate source object is absent", location=str(source_id))
|
|
99
|
+
elif int(source.get("committed_at", 0)) > at_time:
|
|
100
|
+
builder.error(
|
|
101
|
+
"certificate-source-time",
|
|
102
|
+
"certificate source object is committed after the checked time",
|
|
103
|
+
location=str(source_id),
|
|
104
|
+
)
|
|
105
|
+
if cert.get("assumption"):
|
|
106
|
+
builder.add_assumption(str(cert["assumption"]))
|
|
107
|
+
return builder.result()
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def all_certificates_live(
|
|
111
|
+
state: FoldState, cert_ids: tuple[str, ...], at_time: int, *, horizon: Horizon | None = None
|
|
112
|
+
) -> CheckResult:
|
|
113
|
+
result = CheckResult.success(footprint={"IssuerAuthentication", "RevocationOracle", "ClockWatermark"})
|
|
114
|
+
for cert_id in cert_ids:
|
|
115
|
+
result = result.merge(check_certificate_live(state, cert_id, at_time, horizon=horizon))
|
|
116
|
+
return result
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""Command line interface."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from .digest import digest_json
|
|
12
|
+
from .fold import FoldKernel
|
|
13
|
+
from .gate import ExecutorGate, GateRequest
|
|
14
|
+
from .model import Envelope, Horizon
|
|
15
|
+
from .security import scan_for_sensitive_data
|
|
16
|
+
from .verifier import EnvelopeVerifier
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def main(argv: list[str] | None = None) -> int:
|
|
20
|
+
parser = argparse.ArgumentParser(prog="pfg", description="Finite audit-log checker for AI action gates")
|
|
21
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
22
|
+
|
|
23
|
+
init_cmd = sub.add_parser("init-manifest", help="print a strict starter manifest")
|
|
24
|
+
init_cmd.add_argument("--agent-writer", default="agent")
|
|
25
|
+
init_cmd.add_argument("--executor-writer", default="executor-gate")
|
|
26
|
+
|
|
27
|
+
digest_cmd = sub.add_parser("digest", help="print the canonical SHA-256 digest of a JSON file")
|
|
28
|
+
digest_cmd.add_argument("path")
|
|
29
|
+
|
|
30
|
+
scan_cmd = sub.add_parser("scan", help="scan a JSON file for secrets and machine-local paths")
|
|
31
|
+
scan_cmd.add_argument("path")
|
|
32
|
+
scan_cmd.add_argument("--allow-local-paths", action="store_true")
|
|
33
|
+
|
|
34
|
+
verify_cmd = sub.add_parser("verify-log", help="verify a JSON envelope log")
|
|
35
|
+
verify_cmd.add_argument("log")
|
|
36
|
+
verify_cmd.add_argument("--horizon", required=True)
|
|
37
|
+
|
|
38
|
+
fold_cmd = sub.add_parser("fold", help="fold a JSON envelope log with default components")
|
|
39
|
+
fold_cmd.add_argument("log")
|
|
40
|
+
fold_cmd.add_argument("--horizon", required=True)
|
|
41
|
+
|
|
42
|
+
gate_cmd = sub.add_parser("check-gate", help="check a gate request against a JSON envelope log")
|
|
43
|
+
gate_cmd.add_argument("request")
|
|
44
|
+
gate_cmd.add_argument("log")
|
|
45
|
+
gate_cmd.add_argument("--horizon", required=True)
|
|
46
|
+
gate_cmd.add_argument("--bundle", action="store_true", help="print the accepted gate bundle")
|
|
47
|
+
|
|
48
|
+
schema_cmd = sub.add_parser("validate-schema", help="validate a known JSON artifact shape")
|
|
49
|
+
schema_cmd.add_argument("kind", choices=["horizon", "log", "gate-request"])
|
|
50
|
+
schema_cmd.add_argument("path")
|
|
51
|
+
|
|
52
|
+
explain_cmd = sub.add_parser("explain", help="explain a checker issue code")
|
|
53
|
+
explain_cmd.add_argument("code")
|
|
54
|
+
|
|
55
|
+
args = parser.parse_args(argv)
|
|
56
|
+
if args.command == "init-manifest":
|
|
57
|
+
horizon = Horizon.strict_default(agent_writers=(args.agent_writer,), executor_writer=args.executor_writer)
|
|
58
|
+
print(json.dumps(horizon.to_json(), indent=2, sort_keys=True))
|
|
59
|
+
return 0
|
|
60
|
+
if args.command == "digest":
|
|
61
|
+
print(digest_json(_read_json(args.path)))
|
|
62
|
+
return 0
|
|
63
|
+
if args.command == "scan":
|
|
64
|
+
issues = scan_for_sensitive_data(_read_json(args.path), allow_local_paths=args.allow_local_paths)
|
|
65
|
+
print(json.dumps([issue.to_json() for issue in issues], indent=2, sort_keys=True))
|
|
66
|
+
return 1 if issues else 0
|
|
67
|
+
if args.command == "verify-log":
|
|
68
|
+
horizon = Horizon.from_mapping(_read_json(args.horizon))
|
|
69
|
+
envelopes = _read_log(args.log)
|
|
70
|
+
result = EnvelopeVerifier().verify(horizon, envelopes)
|
|
71
|
+
print(json.dumps(result.to_json(), indent=2, sort_keys=True))
|
|
72
|
+
return 0 if result.ok else 1
|
|
73
|
+
if args.command == "fold":
|
|
74
|
+
horizon = Horizon.from_mapping(_read_json(args.horizon))
|
|
75
|
+
envelopes = _read_log(args.log)
|
|
76
|
+
state = FoldKernel().fold(horizon, envelopes)
|
|
77
|
+
print(json.dumps(state.to_json(), indent=2, sort_keys=True, default=str))
|
|
78
|
+
return 0
|
|
79
|
+
if args.command == "check-gate":
|
|
80
|
+
horizon = Horizon.from_mapping(_read_json(args.horizon))
|
|
81
|
+
envelopes = _read_log(args.log)
|
|
82
|
+
request = _read_gate_request(args.request)
|
|
83
|
+
gate = ExecutorGate()
|
|
84
|
+
result = gate.check(horizon, envelopes, request)
|
|
85
|
+
if args.bundle and result.ok:
|
|
86
|
+
bundle = gate.create_bundle(horizon, envelopes, request)
|
|
87
|
+
print(json.dumps(bundle.to_json(), indent=2, sort_keys=True, default=str))
|
|
88
|
+
else:
|
|
89
|
+
print(json.dumps(result.to_json(), indent=2, sort_keys=True))
|
|
90
|
+
return 0 if result.ok else 1
|
|
91
|
+
if args.command == "validate-schema":
|
|
92
|
+
errors = _validate_schema(args.kind, _read_json(args.path))
|
|
93
|
+
print(json.dumps({"ok": not errors, "errors": errors}, indent=2, sort_keys=True))
|
|
94
|
+
return 0 if not errors else 1
|
|
95
|
+
if args.command == "explain":
|
|
96
|
+
print(_explain(args.code))
|
|
97
|
+
return 0
|
|
98
|
+
return 2
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _read_json(path: str | Path) -> Any:
|
|
102
|
+
with Path(path).open("r", encoding="utf-8") as handle:
|
|
103
|
+
return json.load(handle)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _read_log(path: str | Path) -> tuple[Envelope, ...]:
|
|
107
|
+
data = _read_json(path)
|
|
108
|
+
if not isinstance(data, list):
|
|
109
|
+
raise ValueError("log JSON must be an array of envelopes")
|
|
110
|
+
return tuple(Envelope.from_mapping(item) for item in data)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _read_gate_request(path: str | Path) -> GateRequest:
|
|
114
|
+
data = _read_json(path)
|
|
115
|
+
if not isinstance(data, dict):
|
|
116
|
+
raise ValueError("gate request JSON must be an object")
|
|
117
|
+
return GateRequest(**data)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _validate_schema(kind: str, data: Any) -> list[str]:
|
|
121
|
+
errors: list[str] = []
|
|
122
|
+
if kind == "horizon":
|
|
123
|
+
if not isinstance(data, dict):
|
|
124
|
+
return ["horizon must be a JSON object"]
|
|
125
|
+
required_horizon: tuple[str, ...] = (
|
|
126
|
+
"capacities",
|
|
127
|
+
"writer_authority",
|
|
128
|
+
"version_intervals",
|
|
129
|
+
"protected_constructors",
|
|
130
|
+
"certificate_families",
|
|
131
|
+
"risk_modes",
|
|
132
|
+
)
|
|
133
|
+
errors.extend(f"missing {field}" for field in required_horizon if field not in data)
|
|
134
|
+
elif kind == "log":
|
|
135
|
+
if not isinstance(data, list):
|
|
136
|
+
return ["log must be a JSON array"]
|
|
137
|
+
for index, item in enumerate(data):
|
|
138
|
+
if not isinstance(item, dict):
|
|
139
|
+
errors.append(f"log[{index}] must be an object")
|
|
140
|
+
continue
|
|
141
|
+
if "payload" not in item or not isinstance(item["payload"], dict) or "kind" not in item["payload"]:
|
|
142
|
+
errors.append(f"log[{index}] must contain payload.kind")
|
|
143
|
+
elif kind == "gate-request":
|
|
144
|
+
if not isinstance(data, dict):
|
|
145
|
+
return ["gate request must be a JSON object"]
|
|
146
|
+
required_gate: tuple[str, ...] = (
|
|
147
|
+
"gate_id",
|
|
148
|
+
"bundle_id",
|
|
149
|
+
"frame_id",
|
|
150
|
+
"action",
|
|
151
|
+
"outbox_id",
|
|
152
|
+
"capability_id",
|
|
153
|
+
"lease_id",
|
|
154
|
+
"risk_id",
|
|
155
|
+
"hypothesis_id",
|
|
156
|
+
"risk_mode",
|
|
157
|
+
"risk_cert_id",
|
|
158
|
+
"source_time",
|
|
159
|
+
"commit_time",
|
|
160
|
+
)
|
|
161
|
+
errors.extend(f"missing {field}" for field in required_gate if field not in data)
|
|
162
|
+
return errors
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _explain(code: str) -> str:
|
|
166
|
+
explanations = {
|
|
167
|
+
"incomplete-manifest": (
|
|
168
|
+
"Strict manifests must declare capacities, writer authority, versions, protected constructors, "
|
|
169
|
+
"certificate families, and risk modes."
|
|
170
|
+
),
|
|
171
|
+
"protected-writer-authority": "A protected action constructor was written by a non-reserved writer.",
|
|
172
|
+
"gate-bundle-missing-group": "Gate rows must be committed as one atomic group.",
|
|
173
|
+
"gate-bundle-coherence": "The five gate rows do not bind the same GateCheck tuple.",
|
|
174
|
+
"certificate-family-check": "A strict certificate must carry an accepted finite family-specific check.",
|
|
175
|
+
"risk-alpha-bound": "Installed finite risk claims exceed the declared risk budget.",
|
|
176
|
+
"patch-affected-completeness": "A touched invariant was not listed for recheck.",
|
|
177
|
+
"join-ancestor-missing": "Join proposals must cite a common ancestor.",
|
|
178
|
+
}
|
|
179
|
+
return explanations.get(code, "No detailed explanation is registered for this issue code.")
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
if __name__ == "__main__": # pragma: no cover
|
|
183
|
+
sys.exit(main())
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Deterministic JSON encoding and digest helpers.
|
|
2
|
+
|
|
3
|
+
The public wire format is deliberately plain JSON. Other implementations only
|
|
4
|
+
need sorted-object canonicalization, UTF-8, and SHA-256 to interoperate.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import dataclasses
|
|
10
|
+
import hashlib
|
|
11
|
+
import json
|
|
12
|
+
import math
|
|
13
|
+
from collections.abc import Iterable, Mapping
|
|
14
|
+
from enum import Enum
|
|
15
|
+
from fractions import Fraction
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def normalize_json(value: Any) -> Any:
|
|
20
|
+
"""Convert common Python objects into canonical JSON-compatible values."""
|
|
21
|
+
|
|
22
|
+
if hasattr(value, "to_json") and callable(value.to_json):
|
|
23
|
+
return normalize_json(value.to_json())
|
|
24
|
+
if dataclasses.is_dataclass(value) and not isinstance(value, type):
|
|
25
|
+
return normalize_json(dataclasses.asdict(value))
|
|
26
|
+
if isinstance(value, Enum):
|
|
27
|
+
return value.value
|
|
28
|
+
if isinstance(value, Fraction):
|
|
29
|
+
return f"{value.numerator}/{value.denominator}"
|
|
30
|
+
if isinstance(value, Mapping):
|
|
31
|
+
return {str(k): normalize_json(v) for k, v in sorted(value.items(), key=lambda item: str(item[0]))}
|
|
32
|
+
if isinstance(value, tuple | list):
|
|
33
|
+
return [normalize_json(v) for v in value]
|
|
34
|
+
if isinstance(value, set | frozenset):
|
|
35
|
+
return sorted((normalize_json(v) for v in value), key=lambda item: json.dumps(item, sort_keys=True))
|
|
36
|
+
if isinstance(value, bytes):
|
|
37
|
+
raise TypeError("bytes are not allowed in canonical JSON; pass an encoded string")
|
|
38
|
+
if isinstance(value, float) and not math.isfinite(value):
|
|
39
|
+
raise ValueError("non-finite floats are not allowed in canonical JSON")
|
|
40
|
+
if value is None or isinstance(value, str | int | float | bool):
|
|
41
|
+
return value
|
|
42
|
+
raise TypeError(f"unsupported canonical JSON value: {type(value).__name__}")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def canonical_json_bytes(value: Any) -> bytes:
|
|
46
|
+
"""Return RFC-8259-compatible canonical bytes for a JSON value."""
|
|
47
|
+
|
|
48
|
+
normalized = normalize_json(value)
|
|
49
|
+
return json.dumps(
|
|
50
|
+
normalized,
|
|
51
|
+
sort_keys=True,
|
|
52
|
+
separators=(",", ":"),
|
|
53
|
+
ensure_ascii=False,
|
|
54
|
+
allow_nan=False,
|
|
55
|
+
).encode("utf-8")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def digest_json(value: Any, *, algorithm: str = "sha256") -> str:
|
|
59
|
+
"""Digest a JSON-like value and prefix the algorithm name."""
|
|
60
|
+
|
|
61
|
+
if algorithm != "sha256":
|
|
62
|
+
raise ValueError("only sha256 is currently supported")
|
|
63
|
+
return f"sha256:{hashlib.sha256(canonical_json_bytes(value)).hexdigest()}"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def digest_many(values: Iterable[Any], *, algorithm: str = "sha256") -> str:
|
|
67
|
+
"""Digest a finite sequence as one canonical JSON array."""
|
|
68
|
+
|
|
69
|
+
return digest_json(list(values), algorithm=algorithm)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Package exceptions."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ProblemFrameGateError(Exception):
|
|
5
|
+
"""Base class for package-specific errors."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SecurityError(ProblemFrameGateError):
|
|
9
|
+
"""Raised when a payload contains data that should not enter an audit log."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class LogVerificationError(ProblemFrameGateError):
|
|
13
|
+
"""Raised when an envelope log is not legal for a horizon."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class FoldError(ProblemFrameGateError):
|
|
17
|
+
"""Raised when a deterministic component replay rejects an envelope."""
|