agp-tpe 2.3.3__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.
Files changed (50) hide show
  1. agp_tpe-2.3.3/.gitignore +22 -0
  2. agp_tpe-2.3.3/LICENSE +9 -0
  3. agp_tpe-2.3.3/PKG-INFO +201 -0
  4. agp_tpe-2.3.3/README.md +169 -0
  5. agp_tpe-2.3.3/canonicalization/python/canonicalize.py +293 -0
  6. agp_tpe-2.3.3/decision_context/python/validate_decision_context.py +212 -0
  7. agp_tpe-2.3.3/pyproject.toml +67 -0
  8. agp_tpe-2.3.3/registry/schemas/agp.canonicalization-receipt-1.schema.json +78 -0
  9. agp_tpe-2.3.3/registry/schemas/agp.decision-context-1.schema.json +163 -0
  10. agp_tpe-2.3.3/registry/schemas/agp.decision-context-2.schema.json +169 -0
  11. agp_tpe-2.3.3/registry/schemas/agp.signature-statement-1.schema.json +61 -0
  12. agp_tpe-2.3.3/registry/schemas/agp.signature-statement-2.schema.json +61 -0
  13. agp_tpe-2.3.3/registry/schemas/agp.signed-decision-context-1.schema.json +65 -0
  14. agp_tpe-2.3.3/registry/schemas/agp.signed-decision-context-2.schema.json +65 -0
  15. agp_tpe-2.3.3/registry/schemas/agp.trust-policy-1.schema.json +101 -0
  16. agp_tpe-2.3.3/registry/schemas/agp.trust-policy-2.schema.json +614 -0
  17. agp_tpe-2.3.3/registry/schemas/reserved.schema.json +6 -0
  18. agp_tpe-2.3.3/signed_decision_context/python/validate_signed_decision_context.py +350 -0
  19. agp_tpe-2.3.3/signed_decision_context/python/verify_signed_decision_context.py +329 -0
  20. agp_tpe-2.3.3/trust_primitive_engine/python/engine/README.md +16 -0
  21. agp_tpe-2.3.3/trust_primitive_engine/python/engine/__init__.py +65 -0
  22. agp_tpe-2.3.3/trust_primitive_engine/python/engine/composition.py +174 -0
  23. agp_tpe-2.3.3/trust_primitive_engine/python/engine/dispatcher.py +80 -0
  24. agp_tpe-2.3.3/trust_primitive_engine/python/engine/policy_evaluation.py +408 -0
  25. agp_tpe-2.3.3/trust_primitive_engine/python/engine/policy_set.py +126 -0
  26. agp_tpe-2.3.3/trust_primitive_engine/python/engine/policy_tree.py +179 -0
  27. agp_tpe-2.3.3/trust_primitive_engine/python/engine/primitive.py +27 -0
  28. agp_tpe-2.3.3/trust_primitive_engine/python/engine/registry.py +49 -0
  29. agp_tpe-2.3.3/trust_primitive_engine/python/engine/result.py +139 -0
  30. agp_tpe-2.3.3/trust_primitive_engine/python/engine/state.py +139 -0
  31. agp_tpe-2.3.3/trust_primitive_engine/python/evaluate_trust_policy_v2.py +1066 -0
  32. agp_tpe-2.3.3/trust_primitive_engine/python/primitives/__init__.py +42 -0
  33. agp_tpe-2.3.3/trust_primitive_engine/python/primitives/all_of_signers.py +138 -0
  34. agp_tpe-2.3.3/trust_primitive_engine/python/primitives/any_of_signers.py +132 -0
  35. agp_tpe-2.3.3/trust_primitive_engine/python/primitives/at_least_n_signers.py +148 -0
  36. agp_tpe-2.3.3/trust_primitive_engine/python/primitives/at_most_n_signers.py +148 -0
  37. agp_tpe-2.3.3/trust_primitive_engine/python/primitives/exactly_n_signers.py +148 -0
  38. agp_tpe-2.3.3/trust_primitive_engine/python/primitives/exactly_one_of_signers.py +132 -0
  39. agp_tpe-2.3.3/trust_primitive_engine/python/primitives/global_signature_threshold.py +108 -0
  40. agp_tpe-2.3.3/trust_primitive_engine/python/primitives/global_weight_threshold.py +106 -0
  41. agp_tpe-2.3.3/trust_primitive_engine/python/primitives/mutual_exclusion.py +132 -0
  42. agp_tpe-2.3.3/trust_primitive_engine/python/primitives/prohibited_signer.py +96 -0
  43. agp_tpe-2.3.3/trust_primitive_engine/python/primitives/required_signer.py +99 -0
  44. agp_tpe-2.3.3/trust_primitive_engine/python/primitives/role_threshold.py +136 -0
  45. agp_tpe-2.3.3/trust_primitive_engine/python/primitives/role_weight_threshold.py +140 -0
  46. agp_tpe-2.3.3/trust_primitive_engine/python/primitives/separation_of_duties.py +155 -0
  47. agp_tpe-2.3.3/trust_primitive_engine/python/primitives/signer_threshold.py +143 -0
  48. agp_tpe-2.3.3/trust_primitive_engine/python/primitives/time_window.py +122 -0
  49. agp_tpe-2.3.3/trust_primitive_engine/python/trust_primitive_engine/__init__.py +13 -0
  50. agp_tpe-2.3.3/trust_primitive_engine/python/trust_primitive_engine/api.py +193 -0
@@ -0,0 +1,22 @@
1
+ .DS_Store
2
+ __pycache__/
3
+ *.pyc
4
+
5
+ results/
6
+ generated/
7
+
8
+ *.exe
9
+ *.out
10
+ *.o
11
+
12
+ agp-log-go
13
+ agp-signed-go
14
+ agp-resolver-go
15
+
16
+ # Go build artifacts
17
+ signed_decision_context/go/agp-signed-decision-context-verify
18
+
19
+ # Python package build artifacts
20
+ dist/
21
+ build/
22
+ *.egg-info/
agp_tpe-2.3.3/LICENSE ADDED
@@ -0,0 +1,9 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ Copyright 2026 AGP contributors
6
+
7
+ Licensed under the Apache License, Version 2.0 (the "License");
8
+ you may not use this file except in compliance with the License.
9
+ You may obtain a copy of the License at the URL above.
agp_tpe-2.3.3/PKG-INFO ADDED
@@ -0,0 +1,201 @@
1
+ Metadata-Version: 2.4
2
+ Name: agp-tpe
3
+ Version: 2.3.3
4
+ Summary: AGP Trust Primitive Engine for deterministic Trust Policy 2 evaluation
5
+ Project-URL: Homepage, https://github.com/agpprotocol/agp
6
+ Project-URL: Repository, https://github.com/agpprotocol/agp
7
+ Project-URL: Documentation, https://github.com/agpprotocol/agp/blob/main/trust_primitive_engine/INTEGRATION-GUIDE.md
8
+ Project-URL: Issues, https://github.com/agpprotocol/agp/issues
9
+ Author: AGP Protocol
10
+ License: Apache License
11
+ Version 2.0, January 2004
12
+ http://www.apache.org/licenses/
13
+
14
+ Copyright 2026 AGP contributors
15
+
16
+ Licensed under the Apache License, Version 2.0 (the "License");
17
+ you may not use this file except in compliance with the License.
18
+ You may obtain a copy of the License at the URL above.
19
+ License-File: LICENSE
20
+ Keywords: agp,deterministic-evaluation,ed25519,policy-engine,trust-policy
21
+ Classifier: Development Status :: 4 - Beta
22
+ Classifier: Intended Audience :: Developers
23
+ Classifier: License :: OSI Approved :: Apache Software License
24
+ Classifier: Programming Language :: Python :: 3
25
+ Classifier: Programming Language :: Python :: 3.12
26
+ Classifier: Programming Language :: Python :: 3.13
27
+ Classifier: Topic :: Security :: Cryptography
28
+ Requires-Python: >=3.12
29
+ Requires-Dist: cryptography>=42.0
30
+ Requires-Dist: jsonschema<5,>=4.26
31
+ Description-Content-Type: text/markdown
32
+
33
+ # Agent Governance Protocol (AGP)
34
+
35
+ **AGP is an experimental governance layer for multi-agent systems.**
36
+
37
+ It defines how independent agents or authorities can propose, review, approve,
38
+ reject, veto and audit high-impact decisions without trusting a single
39
+ coordinator as the sole source of truth.
40
+
41
+ > Status: experimental. AGP is not yet a production standard.
42
+
43
+
44
+
45
+ ## 120-Second Demo
46
+
47
+ Run the public AGP demo with no external dependencies:
48
+
49
+ python3 examples/120_second_demo/demo.py
50
+
51
+ Fast mode:
52
+
53
+ python3 examples/120_second_demo/demo.py --fast
54
+
55
+ Valid governance path:
56
+
57
+ python3 examples/120_second_demo/demo.py --happy-path --fast
58
+
59
+ The demo shows a coordinator changing a deployment payload after approval.
60
+ A coordinator-trusting workflow accepts it because the approval count is still present.
61
+ AGP rejects it because the approvals are bound to the exact approved input.
62
+
63
+ [Read the demo documentation](examples/120_second_demo/README.md)
64
+
65
+ ## Whitepaper
66
+
67
+ The current public review draft is:
68
+
69
+ **AGP Whitepaper v0.9.2 — A Deterministic Governance Layer for Multi-Agent Systems**
70
+
71
+ - [Read online](docs/whitepaper/AGP_Whitepaper_v0.9.2.md)
72
+ - [Download PDF](https://github.com/agpprotocol/agp/releases/tag/whitepaper-v0.9.2)
73
+ - [Submit technical feedback](https://github.com/agpprotocol/agp/issues/1)
74
+
75
+ The whitepaper is experimental, is not a production standard, and has not yet received independent security review.
76
+
77
+ ## Why AGP?
78
+
79
+ - **MCP** connects models to tools and data.
80
+ - **A2A-style protocols** let agents communicate.
81
+ - **Workflow engines** coordinate execution.
82
+ - **AGP** governs collective decisions.
83
+
84
+ A workflow can record that a deployment was approved. AGP additionally aims to
85
+ make the authority snapshot, evidence, ballots, resolution and history
86
+ independently verifiable.
87
+
88
+ ## Core properties
89
+
90
+ - deterministic resolution;
91
+ - canonical serialization;
92
+ - independent Python and Go implementations;
93
+ - Ed25519 signed envelopes;
94
+ - replay, expiration and revocation checks;
95
+ - append-only transparency log;
96
+ - external audit receipts;
97
+ - conformance and adversarial test vectors.
98
+
99
+ ## Architecture
100
+
101
+ ```mermaid
102
+ flowchart LR
103
+ P[Proposal] --> E[Evidence]
104
+ E --> B[Signed ballots]
105
+ B --> R[Deterministic resolution]
106
+ R --> L[Transparency log]
107
+ L --> A[Independent audit]
108
+ ```
109
+
110
+ ## AGP vs. a conventional workflow
111
+
112
+ | Property | Workflow | AGP |
113
+ |---|---:|---:|
114
+ | Coordinates steps | Yes | Yes |
115
+ | Deterministic replay across implementations | Not inherent | Yes |
116
+ | Signed authority actions | Optional/custom | Built into profile |
117
+ | Evidence version validation | Optional/custom | Yes |
118
+ | Revocation-aware decisions | Optional/custom | Yes |
119
+ | Tamper-evident history | Optional/custom | Yes |
120
+ | Independent audit without trusting coordinator | Not inherent | Yes |
121
+
122
+ AGP is intentionally more complex. For low-risk internal automation, a
123
+ conventional workflow is usually the better choice.
124
+
125
+ ## Reproducible results
126
+
127
+ ```text
128
+ AGP v0.3 Conformance: 260/260 passed
129
+ AGP v0.4 Signed Conformance: 10/10 passed
130
+ AGP v0.5 Transparency: 8/8 passed
131
+ AGP vs Workflow benchmark: 8/8 attacks detected by AGP
132
+ ```
133
+
134
+ The workflow baseline in the benchmark detected 0/8 because it treated
135
+ coordinator state as authoritative. This is a limited experimental benchmark,
136
+ not a claim that all workflow engines are insecure.
137
+
138
+ ## Quick start
139
+
140
+ Requirements:
141
+
142
+ - Python 3.10+
143
+ - Go 1.22+
144
+
145
+ ```bash
146
+ git clone https://github.com/agpprotocol/agp.git
147
+ cd agp
148
+ python3 -m venv .venv
149
+ source .venv/bin/activate
150
+ pip install -r requirements-v0.4.txt
151
+ python3 run_benchmark_all.py
152
+ ```
153
+
154
+ Expected final line:
155
+
156
+ ```text
157
+ AGP BENCHMARK COMPLETE
158
+ ```
159
+
160
+ ## Repository map
161
+
162
+ ```text
163
+ spec/ Conformance profile
164
+ python/ Python resolver
165
+ go/ Go resolver
166
+ signed/ Signed envelope conformance
167
+ transparency/ Append-only audit log
168
+ benchmark/ AGP vs workflow experiment
169
+ examples/ Reference scenarios
170
+ docs/ Architecture, threat model and evaluation
171
+ ```
172
+
173
+ ## What AGP does not claim
174
+
175
+ AGP does not:
176
+
177
+ - replace authentication, authorization or IAM;
178
+ - replace orchestration engines;
179
+ - guarantee that evidence is factually true;
180
+ - eliminate compromised members;
181
+ - solve governance for every multi-agent system;
182
+ - claim production maturity.
183
+
184
+ ## Current research question
185
+
186
+ > Do high-impact multi-agent systems need a portable, independently verifiable
187
+ > governance layer distinct from communication and orchestration?
188
+
189
+ ## Documentation
190
+
191
+ - [Architecture](docs/ARCHITECTURE.md)
192
+ - [Threat model](docs/THREAT_MODEL.md)
193
+ - [Benchmark](docs/BENCHMARK.md)
194
+ - [Roadmap](ROADMAP.md)
195
+ - [Contributing](CONTRIBUTING.md)
196
+ - [Security policy](SECURITY.md)
197
+ - [Governance](GOVERNANCE.md)
198
+
199
+ ## License
200
+
201
+ Apache License 2.0. See [LICENSE](LICENSE).
@@ -0,0 +1,169 @@
1
+ # Agent Governance Protocol (AGP)
2
+
3
+ **AGP is an experimental governance layer for multi-agent systems.**
4
+
5
+ It defines how independent agents or authorities can propose, review, approve,
6
+ reject, veto and audit high-impact decisions without trusting a single
7
+ coordinator as the sole source of truth.
8
+
9
+ > Status: experimental. AGP is not yet a production standard.
10
+
11
+
12
+
13
+ ## 120-Second Demo
14
+
15
+ Run the public AGP demo with no external dependencies:
16
+
17
+ python3 examples/120_second_demo/demo.py
18
+
19
+ Fast mode:
20
+
21
+ python3 examples/120_second_demo/demo.py --fast
22
+
23
+ Valid governance path:
24
+
25
+ python3 examples/120_second_demo/demo.py --happy-path --fast
26
+
27
+ The demo shows a coordinator changing a deployment payload after approval.
28
+ A coordinator-trusting workflow accepts it because the approval count is still present.
29
+ AGP rejects it because the approvals are bound to the exact approved input.
30
+
31
+ [Read the demo documentation](examples/120_second_demo/README.md)
32
+
33
+ ## Whitepaper
34
+
35
+ The current public review draft is:
36
+
37
+ **AGP Whitepaper v0.9.2 — A Deterministic Governance Layer for Multi-Agent Systems**
38
+
39
+ - [Read online](docs/whitepaper/AGP_Whitepaper_v0.9.2.md)
40
+ - [Download PDF](https://github.com/agpprotocol/agp/releases/tag/whitepaper-v0.9.2)
41
+ - [Submit technical feedback](https://github.com/agpprotocol/agp/issues/1)
42
+
43
+ The whitepaper is experimental, is not a production standard, and has not yet received independent security review.
44
+
45
+ ## Why AGP?
46
+
47
+ - **MCP** connects models to tools and data.
48
+ - **A2A-style protocols** let agents communicate.
49
+ - **Workflow engines** coordinate execution.
50
+ - **AGP** governs collective decisions.
51
+
52
+ A workflow can record that a deployment was approved. AGP additionally aims to
53
+ make the authority snapshot, evidence, ballots, resolution and history
54
+ independently verifiable.
55
+
56
+ ## Core properties
57
+
58
+ - deterministic resolution;
59
+ - canonical serialization;
60
+ - independent Python and Go implementations;
61
+ - Ed25519 signed envelopes;
62
+ - replay, expiration and revocation checks;
63
+ - append-only transparency log;
64
+ - external audit receipts;
65
+ - conformance and adversarial test vectors.
66
+
67
+ ## Architecture
68
+
69
+ ```mermaid
70
+ flowchart LR
71
+ P[Proposal] --> E[Evidence]
72
+ E --> B[Signed ballots]
73
+ B --> R[Deterministic resolution]
74
+ R --> L[Transparency log]
75
+ L --> A[Independent audit]
76
+ ```
77
+
78
+ ## AGP vs. a conventional workflow
79
+
80
+ | Property | Workflow | AGP |
81
+ |---|---:|---:|
82
+ | Coordinates steps | Yes | Yes |
83
+ | Deterministic replay across implementations | Not inherent | Yes |
84
+ | Signed authority actions | Optional/custom | Built into profile |
85
+ | Evidence version validation | Optional/custom | Yes |
86
+ | Revocation-aware decisions | Optional/custom | Yes |
87
+ | Tamper-evident history | Optional/custom | Yes |
88
+ | Independent audit without trusting coordinator | Not inherent | Yes |
89
+
90
+ AGP is intentionally more complex. For low-risk internal automation, a
91
+ conventional workflow is usually the better choice.
92
+
93
+ ## Reproducible results
94
+
95
+ ```text
96
+ AGP v0.3 Conformance: 260/260 passed
97
+ AGP v0.4 Signed Conformance: 10/10 passed
98
+ AGP v0.5 Transparency: 8/8 passed
99
+ AGP vs Workflow benchmark: 8/8 attacks detected by AGP
100
+ ```
101
+
102
+ The workflow baseline in the benchmark detected 0/8 because it treated
103
+ coordinator state as authoritative. This is a limited experimental benchmark,
104
+ not a claim that all workflow engines are insecure.
105
+
106
+ ## Quick start
107
+
108
+ Requirements:
109
+
110
+ - Python 3.10+
111
+ - Go 1.22+
112
+
113
+ ```bash
114
+ git clone https://github.com/agpprotocol/agp.git
115
+ cd agp
116
+ python3 -m venv .venv
117
+ source .venv/bin/activate
118
+ pip install -r requirements-v0.4.txt
119
+ python3 run_benchmark_all.py
120
+ ```
121
+
122
+ Expected final line:
123
+
124
+ ```text
125
+ AGP BENCHMARK COMPLETE
126
+ ```
127
+
128
+ ## Repository map
129
+
130
+ ```text
131
+ spec/ Conformance profile
132
+ python/ Python resolver
133
+ go/ Go resolver
134
+ signed/ Signed envelope conformance
135
+ transparency/ Append-only audit log
136
+ benchmark/ AGP vs workflow experiment
137
+ examples/ Reference scenarios
138
+ docs/ Architecture, threat model and evaluation
139
+ ```
140
+
141
+ ## What AGP does not claim
142
+
143
+ AGP does not:
144
+
145
+ - replace authentication, authorization or IAM;
146
+ - replace orchestration engines;
147
+ - guarantee that evidence is factually true;
148
+ - eliminate compromised members;
149
+ - solve governance for every multi-agent system;
150
+ - claim production maturity.
151
+
152
+ ## Current research question
153
+
154
+ > Do high-impact multi-agent systems need a portable, independently verifiable
155
+ > governance layer distinct from communication and orchestration?
156
+
157
+ ## Documentation
158
+
159
+ - [Architecture](docs/ARCHITECTURE.md)
160
+ - [Threat model](docs/THREAT_MODEL.md)
161
+ - [Benchmark](docs/BENCHMARK.md)
162
+ - [Roadmap](ROADMAP.md)
163
+ - [Contributing](CONTRIBUTING.md)
164
+ - [Security policy](SECURITY.md)
165
+ - [Governance](GOVERNANCE.md)
166
+
167
+ ## License
168
+
169
+ Apache License 2.0. See [LICENSE](LICENSE).
@@ -0,0 +1,293 @@
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import hashlib
5
+ import json
6
+ import sys
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ MAX_INPUT_BYTES = 1_048_576
11
+ MAX_DEPTH = 64
12
+ MIN_SAFE_INTEGER = -(2**53 - 1)
13
+ MAX_SAFE_INTEGER = 2**53 - 1
14
+
15
+
16
+ class CanonicalizationError(ValueError):
17
+ def __init__(self, code: str, detail: str = "") -> None:
18
+ super().__init__(detail or code)
19
+ self.code = code
20
+ self.detail = detail
21
+
22
+
23
+ def _reject_constant(value: str) -> Any:
24
+ raise CanonicalizationError("INVALID_NUMBER", value)
25
+
26
+
27
+ def _parse_integer(value: str) -> int:
28
+ number = int(value, 10)
29
+ if number < MIN_SAFE_INTEGER or number > MAX_SAFE_INTEGER:
30
+ raise CanonicalizationError("INTEGER_OUT_OF_RANGE", value)
31
+ return number
32
+
33
+
34
+ def _reject_decimal(value: str) -> Any:
35
+ raise CanonicalizationError("DECIMAL_NOT_SUPPORTED", value)
36
+
37
+
38
+ def _normalize_string(value: str) -> str:
39
+ output: list[str] = []
40
+ index = 0
41
+
42
+ while index < len(value):
43
+ codepoint = ord(value[index])
44
+
45
+ if 0xD800 <= codepoint <= 0xDBFF:
46
+ if index + 1 >= len(value):
47
+ raise CanonicalizationError(
48
+ "INVALID_UNICODE",
49
+ f"unpaired high surrogate U+{codepoint:04X}",
50
+ )
51
+
52
+ low = ord(value[index + 1])
53
+ if not 0xDC00 <= low <= 0xDFFF:
54
+ raise CanonicalizationError(
55
+ "INVALID_UNICODE",
56
+ f"unpaired high surrogate U+{codepoint:04X}",
57
+ )
58
+
59
+ combined = (
60
+ 0x10000
61
+ + ((codepoint - 0xD800) << 10)
62
+ + (low - 0xDC00)
63
+ )
64
+ output.append(chr(combined))
65
+ index += 2
66
+ continue
67
+
68
+ if 0xDC00 <= codepoint <= 0xDFFF:
69
+ raise CanonicalizationError(
70
+ "INVALID_UNICODE",
71
+ f"unpaired low surrogate U+{codepoint:04X}",
72
+ )
73
+
74
+ output.append(value[index])
75
+ index += 1
76
+
77
+ return "".join(output)
78
+
79
+
80
+ def _normalize_value(value: Any) -> Any:
81
+ if isinstance(value, str):
82
+ return _normalize_string(value)
83
+
84
+ if isinstance(value, list):
85
+ return [_normalize_value(item) for item in value]
86
+
87
+ if isinstance(value, dict):
88
+ normalized: dict[str, Any] = {}
89
+ for key, item in value.items():
90
+ normalized_key = _normalize_string(key)
91
+ if normalized_key in normalized:
92
+ raise CanonicalizationError(
93
+ "DUPLICATE_KEY",
94
+ normalized_key,
95
+ )
96
+ normalized[normalized_key] = _normalize_value(item)
97
+ return normalized
98
+
99
+ return value
100
+
101
+
102
+ def _object_from_pairs(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
103
+ result: dict[str, Any] = {}
104
+ for key, value in pairs:
105
+ normalized_key = _normalize_string(key)
106
+ if normalized_key in result:
107
+ raise CanonicalizationError("DUPLICATE_KEY", normalized_key)
108
+ result[normalized_key] = value
109
+ return result
110
+
111
+
112
+ def _validate_string(value: str) -> None:
113
+ _normalize_string(value)
114
+
115
+
116
+ def _validate_value(value: Any, depth: int = 0) -> None:
117
+ if depth > MAX_DEPTH:
118
+ raise CanonicalizationError("MAX_DEPTH_EXCEEDED")
119
+
120
+ if value is None or isinstance(value, bool):
121
+ return
122
+
123
+ if isinstance(value, int):
124
+ if value < MIN_SAFE_INTEGER or value > MAX_SAFE_INTEGER:
125
+ raise CanonicalizationError("INTEGER_OUT_OF_RANGE")
126
+ return
127
+
128
+ if isinstance(value, float):
129
+ raise CanonicalizationError("DECIMAL_NOT_SUPPORTED")
130
+
131
+ if isinstance(value, str):
132
+ _validate_string(value)
133
+ return
134
+
135
+ if isinstance(value, list):
136
+ for item in value:
137
+ _validate_value(item, depth + 1)
138
+ return
139
+
140
+ if isinstance(value, dict):
141
+ for key, item in value.items():
142
+ if not isinstance(key, str):
143
+ raise CanonicalizationError("NON_STRING_KEY")
144
+ _validate_string(key)
145
+ _validate_value(item, depth + 1)
146
+ return
147
+
148
+ raise CanonicalizationError("UNSUPPORTED_TYPE", type(value).__name__)
149
+
150
+
151
+ def parse_json_bytes(raw: bytes) -> Any:
152
+ if len(raw) > MAX_INPUT_BYTES:
153
+ raise CanonicalizationError("INPUT_TOO_LARGE")
154
+
155
+ if raw.startswith(b"\xef\xbb\xbf"):
156
+ raise CanonicalizationError("UTF8_BOM_NOT_ALLOWED")
157
+
158
+ try:
159
+ text = raw.decode("utf-8", errors="strict")
160
+ except UnicodeDecodeError as error:
161
+ raise CanonicalizationError("INVALID_UTF8", str(error)) from error
162
+
163
+ try:
164
+ value = json.loads(
165
+ text,
166
+ object_pairs_hook=_object_from_pairs,
167
+ parse_int=_parse_integer,
168
+ parse_float=_reject_decimal,
169
+ parse_constant=_reject_constant,
170
+ )
171
+ except CanonicalizationError:
172
+ raise
173
+ except (json.JSONDecodeError, RecursionError) as error:
174
+ raise CanonicalizationError("INVALID_JSON", str(error)) from error
175
+
176
+ value = _normalize_value(value)
177
+ _validate_value(value)
178
+ return value
179
+
180
+
181
+ def _escape_string(value: str) -> str:
182
+ _validate_string(value)
183
+ output: list[str] = ['"']
184
+
185
+ short_escapes = {
186
+ 0x08: r"\b",
187
+ 0x09: r"\t",
188
+ 0x0A: r"\n",
189
+ 0x0C: r"\f",
190
+ 0x0D: r"\r",
191
+ }
192
+
193
+ for character in value:
194
+ codepoint = ord(character)
195
+
196
+ if character == '"':
197
+ output.append(r"\"")
198
+ elif character == "\\":
199
+ output.append(r"\\")
200
+ elif codepoint in short_escapes:
201
+ output.append(short_escapes[codepoint])
202
+ elif codepoint <= 0x1F:
203
+ output.append(f"\\u{codepoint:04x}")
204
+ else:
205
+ output.append(character)
206
+
207
+ output.append('"')
208
+ return "".join(output)
209
+
210
+
211
+ def canonical_text(value: Any) -> str:
212
+ _validate_value(value)
213
+
214
+ if value is None:
215
+ return "null"
216
+
217
+ if value is True:
218
+ return "true"
219
+
220
+ if value is False:
221
+ return "false"
222
+
223
+ if isinstance(value, int):
224
+ return str(value)
225
+
226
+ if isinstance(value, str):
227
+ return _escape_string(value)
228
+
229
+ if isinstance(value, list):
230
+ return "[" + ",".join(canonical_text(item) for item in value) + "]"
231
+
232
+ if isinstance(value, dict):
233
+ entries = (
234
+ _escape_string(key) + ":" + canonical_text(value[key])
235
+ for key in sorted(value)
236
+ )
237
+ return "{" + ",".join(entries) + "}"
238
+
239
+ raise CanonicalizationError("UNSUPPORTED_TYPE", type(value).__name__)
240
+
241
+
242
+ def canonical_bytes(value: Any) -> bytes:
243
+ return canonical_text(value).encode("utf-8")
244
+
245
+
246
+ def sha256_digest(value: Any) -> str:
247
+ return "sha256:" + hashlib.sha256(canonical_bytes(value)).hexdigest()
248
+
249
+
250
+ def make_receipt(raw: bytes) -> dict[str, Any]:
251
+ try:
252
+ value = parse_json_bytes(raw)
253
+ encoded = canonical_bytes(value)
254
+ return {
255
+ "accepted": True,
256
+ "canonical": encoded.decode("utf-8"),
257
+ "digest": "sha256:" + hashlib.sha256(encoded).hexdigest(),
258
+ "error_codes": [],
259
+ }
260
+ except CanonicalizationError as error:
261
+ return {
262
+ "accepted": False,
263
+ "canonical": None,
264
+ "digest": None,
265
+ "error_codes": [error.code],
266
+ }
267
+
268
+
269
+ def main() -> int:
270
+ if len(sys.argv) != 3:
271
+ print(
272
+ "usage: canonicalize.py INPUT.json OUTPUT.json",
273
+ file=sys.stderr,
274
+ )
275
+ return 2
276
+
277
+ source = Path(sys.argv[1])
278
+ target = Path(sys.argv[2])
279
+
280
+ try:
281
+ raw = source.read_bytes()
282
+ except OSError as error:
283
+ print(f"cannot read input: {error}", file=sys.stderr)
284
+ return 2
285
+
286
+ receipt = make_receipt(raw)
287
+ target.parent.mkdir(parents=True, exist_ok=True)
288
+ target.write_bytes(canonical_bytes(receipt) + b"\n")
289
+ return 0
290
+
291
+
292
+ if __name__ == "__main__":
293
+ raise SystemExit(main())