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.
@@ -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."""