@mcp-audit-gateway/core 0.1.0 → 0.4.0
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.
- package/.github/workflows/ci.yml +37 -0
- package/.well-known/agent-governance.json +47 -0
- package/.well-known/security-insights-snippet.yml +9 -0
- package/CHANGELOG.md +32 -0
- package/README.md +31 -3
- package/dist/attestation/audit-log.d.ts +35 -3
- package/dist/attestation/audit-log.d.ts.map +1 -1
- package/dist/attestation/audit-log.js +303 -8
- package/dist/attestation/audit-log.js.map +1 -1
- package/dist/attestation/checkpoint.test.d.ts +2 -0
- package/dist/attestation/checkpoint.test.d.ts.map +1 -0
- package/dist/attestation/checkpoint.test.js +870 -0
- package/dist/attestation/checkpoint.test.js.map +1 -0
- package/dist/attestation/signer.d.ts +24 -9
- package/dist/attestation/signer.d.ts.map +1 -1
- package/dist/attestation/signer.js +151 -10
- package/dist/attestation/signer.js.map +1 -1
- package/dist/attestation/signer.test.js +83 -0
- package/dist/attestation/signer.test.js.map +1 -1
- package/dist/attestation/verify.d.ts +23 -2
- package/dist/attestation/verify.d.ts.map +1 -1
- package/dist/attestation/verify.js +219 -2
- package/dist/attestation/verify.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/integration.test.js +1 -0
- package/dist/integration.test.js.map +1 -1
- package/dist/policy/engine.d.ts +12 -1
- package/dist/policy/engine.d.ts.map +1 -1
- package/dist/policy/engine.js +26 -4
- package/dist/policy/engine.js.map +1 -1
- package/dist/policy/engine.test.js +42 -1
- package/dist/policy/engine.test.js.map +1 -1
- package/dist/proxy/gateway.d.ts +4 -0
- package/dist/proxy/gateway.d.ts.map +1 -1
- package/dist/proxy/gateway.js +9 -2
- package/dist/proxy/gateway.js.map +1 -1
- package/dist/proxy/gateway.test.js +19 -0
- package/dist/proxy/gateway.test.js.map +1 -1
- package/dist/proxy/mcp-server-adapter.d.ts +1 -0
- package/dist/proxy/mcp-server-adapter.d.ts.map +1 -1
- package/dist/proxy/mcp-server-adapter.js +28 -1
- package/dist/proxy/mcp-server-adapter.js.map +1 -1
- package/dist/proxy/mcp-server-adapter.test.js +1 -0
- package/dist/proxy/mcp-server-adapter.test.js.map +1 -1
- package/dist/types.d.ts +81 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +13 -0
- package/dist/types.js.map +1 -1
- package/dist/wrap/proxy.test.js +2 -2
- package/dist/wrap/proxy.test.js.map +1 -1
- package/docs/BACKLOG.md +33 -0
- package/docs/SECURITY-DESIGN.md +126 -0
- package/docs/v0.4.0-patch-audit.md +115 -0
- package/package.json +1 -1
- package/src/attestation/audit-log.ts +357 -13
- package/src/attestation/checkpoint.test.ts +956 -0
- package/src/attestation/signer.test.ts +98 -0
- package/src/attestation/signer.ts +159 -19
- package/src/attestation/verify.ts +270 -4
- package/src/index.ts +1 -1
- package/src/integration.test.ts +1 -0
- package/src/policy/engine.test.ts +47 -1
- package/src/policy/engine.ts +38 -5
- package/src/proxy/gateway.test.ts +18 -0
- package/src/proxy/gateway.ts +10 -1
- package/src/proxy/mcp-server-adapter.test.ts +1 -0
- package/src/proxy/mcp-server-adapter.ts +26 -0
- package/src/types.ts +56 -0
- package/src/wrap/proxy.test.ts +2 -2
- package/test/vectors/README.md +44 -0
- package/test/vectors/aps-action-ref-v1-vectors.json +351 -0
- package/test/vectors/aps-action-ref-v1.mjs +145 -0
- package/test/vectors/canonicalization.json +764 -0
- package/test/vectors/checkpoint.json +450 -0
- package/test/vectors/generate.mjs +354 -0
- package/test/vectors/verify-checkpoint.mjs +344 -0
- package/test/vectors/verify-checkpoint.py +358 -0
- package/test/vectors/verify.mjs +346 -0
- package/test/vectors/verify.py +354 -0
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Checkpoint conformance vector verifier (Python).
|
|
4
|
+
Verifies that checkpoint canonicalization, chain hashing, and truncation
|
|
5
|
+
detection produce byte-identical results across implementations.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import hashlib
|
|
9
|
+
import json
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
vectors_path = Path(__file__).parent / "checkpoint.json"
|
|
14
|
+
vectors = json.loads(vectors_path.read_text())
|
|
15
|
+
|
|
16
|
+
passed = 0
|
|
17
|
+
failed = 0
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def sha256(data: str) -> str:
|
|
21
|
+
return hashlib.sha256(data.encode("utf-8")).hexdigest()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def assert_well_formed_string(value: str, context: str) -> None:
|
|
25
|
+
for i, ch in enumerate(value):
|
|
26
|
+
code = ord(ch)
|
|
27
|
+
if 0xD800 <= code <= 0xDFFF:
|
|
28
|
+
raise ValueError(f"{context}: unpaired surrogate at index {i}")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def canonicalize_checkpoint(record: dict) -> str:
|
|
32
|
+
assert_well_formed_string(record["id"], "canonicalizeCheckpoint.id")
|
|
33
|
+
assert_well_formed_string(record["timestamp"], "canonicalizeCheckpoint.timestamp")
|
|
34
|
+
assert_well_formed_string(record["previousHash"], "canonicalizeCheckpoint.previousHash")
|
|
35
|
+
ordered = [
|
|
36
|
+
["id", record["id"]],
|
|
37
|
+
["type", "checkpoint"],
|
|
38
|
+
["timestamp", record["timestamp"]],
|
|
39
|
+
["sequence", record["sequence"]],
|
|
40
|
+
["recordCount", record["recordCount"]],
|
|
41
|
+
["previousHash", record["previousHash"]],
|
|
42
|
+
]
|
|
43
|
+
if record.get("parties") is not None:
|
|
44
|
+
ordered.append(["parties", record["parties"]])
|
|
45
|
+
return json.dumps(ordered, separators=(",", ":"), ensure_ascii=False)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def hash_record(record: dict) -> str:
|
|
49
|
+
serialized = json.dumps(record, separators=(",", ":"), ensure_ascii=False)
|
|
50
|
+
return hashlib.sha256(serialized.encode("utf-8")).hexdigest()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def check(condition: bool, name: str, detail: str = ""):
|
|
54
|
+
global passed, failed
|
|
55
|
+
if condition:
|
|
56
|
+
passed += 1
|
|
57
|
+
print(f" PASS: {name}")
|
|
58
|
+
else:
|
|
59
|
+
failed += 1
|
|
60
|
+
print(f" FAIL: {name}")
|
|
61
|
+
if detail:
|
|
62
|
+
print(f" {detail}")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# --- Checkpoint Canonicalization ---
|
|
66
|
+
print("\n=== Checkpoint Canonicalization ===")
|
|
67
|
+
for vec in vectors["checkpoint_canonicalization"]:
|
|
68
|
+
canonical = canonicalize_checkpoint(vec["record"])
|
|
69
|
+
check(
|
|
70
|
+
canonical == vec["canonical"],
|
|
71
|
+
f"{vec['name']} canonical form",
|
|
72
|
+
f"expected: {vec['canonical']}\n got: {canonical}",
|
|
73
|
+
)
|
|
74
|
+
h = sha256(canonical)
|
|
75
|
+
check(
|
|
76
|
+
h == vec["sha256_canonical"],
|
|
77
|
+
f"{vec['name']} SHA-256",
|
|
78
|
+
f"expected: {vec['sha256_canonical']}\n got: {h}",
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# --- Checkpoint Chain ---
|
|
82
|
+
print("\n=== Checkpoint Chain ===")
|
|
83
|
+
chain_records = vectors["checkpoint_chain"]["records"]
|
|
84
|
+
for i, entry in enumerate(chain_records):
|
|
85
|
+
computed = hash_record(entry["record"])
|
|
86
|
+
check(
|
|
87
|
+
computed == entry["record_hash"],
|
|
88
|
+
f"chain record {i} hash",
|
|
89
|
+
f"expected: {entry['record_hash']}\n got: {computed}",
|
|
90
|
+
)
|
|
91
|
+
if i > 0:
|
|
92
|
+
check(
|
|
93
|
+
entry["record"]["previousHash"] == chain_records[i - 1]["record_hash"],
|
|
94
|
+
f"chain record {i} previousHash links to record {i - 1}",
|
|
95
|
+
f"expected: {chain_records[i - 1]['record_hash']}\n got: {entry['record']['previousHash']}",
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# --- Truncation Detection ---
|
|
99
|
+
print("\n=== Truncation Detection ===")
|
|
100
|
+
trunc_vec = vectors["truncation_detection"]
|
|
101
|
+
ext_ckpt = trunc_vec["external_checkpoint"]
|
|
102
|
+
|
|
103
|
+
# Full chain should contain the checkpoint
|
|
104
|
+
full_chain = [e["record"] for e in chain_records]
|
|
105
|
+
found_in_full = any(
|
|
106
|
+
r.get("type") == "checkpoint"
|
|
107
|
+
and r.get("previousHash") == ext_ckpt["previousHash"]
|
|
108
|
+
and r.get("sequence") == ext_ckpt["sequence"]
|
|
109
|
+
and r.get("recordCount") == ext_ckpt["recordCount"]
|
|
110
|
+
for r in full_chain
|
|
111
|
+
)
|
|
112
|
+
check(found_in_full, "full chain contains externalized checkpoint")
|
|
113
|
+
|
|
114
|
+
# Truncated chain should NOT contain the checkpoint
|
|
115
|
+
truncated_chain = trunc_vec["truncated_chain"]["records_delivered"]
|
|
116
|
+
found_in_truncated = any(
|
|
117
|
+
r.get("type") == "checkpoint"
|
|
118
|
+
and r.get("previousHash") == ext_ckpt["previousHash"]
|
|
119
|
+
and r.get("sequence") == ext_ckpt["sequence"]
|
|
120
|
+
and r.get("recordCount") == ext_ckpt["recordCount"]
|
|
121
|
+
for r in truncated_chain
|
|
122
|
+
)
|
|
123
|
+
has_descendant = any(
|
|
124
|
+
r.get("type") == "checkpoint" and r.get("sequence", 0) > ext_ckpt["sequence"]
|
|
125
|
+
for r in truncated_chain
|
|
126
|
+
)
|
|
127
|
+
check(
|
|
128
|
+
not found_in_truncated and not has_descendant,
|
|
129
|
+
"truncated chain missing checkpoint (truncation detected)",
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# --- canonicalizeValue ---
|
|
133
|
+
print("\n=== canonicalizeValue ===")
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def canonicalize_value(value):
|
|
137
|
+
if value is None:
|
|
138
|
+
return None
|
|
139
|
+
if isinstance(value, bool):
|
|
140
|
+
return value
|
|
141
|
+
if isinstance(value, int):
|
|
142
|
+
if abs(value) > 2**53 - 1:
|
|
143
|
+
raise ValueError(f"unsafe number {value}")
|
|
144
|
+
return value
|
|
145
|
+
if isinstance(value, float):
|
|
146
|
+
raise ValueError(f"unsafe number {value}")
|
|
147
|
+
if isinstance(value, str):
|
|
148
|
+
for i, ch in enumerate(value):
|
|
149
|
+
code = ord(ch)
|
|
150
|
+
if 0xD800 <= code <= 0xDFFF:
|
|
151
|
+
raise ValueError(f"unpaired surrogate at index {i}")
|
|
152
|
+
return value
|
|
153
|
+
if isinstance(value, list):
|
|
154
|
+
return ["L", [canonicalize_value(v) for v in value]]
|
|
155
|
+
if isinstance(value, dict):
|
|
156
|
+
# Sort by UTF-16 code-unit order to match JavaScript's String.prototype.sort().
|
|
157
|
+
# Python sorts by code points; these diverge for astral-plane characters.
|
|
158
|
+
keys = sorted(value.keys(), key=lambda k: k.encode("utf-16-be"))
|
|
159
|
+
return ["M", [[k, canonicalize_value(value[k])] for k in keys]]
|
|
160
|
+
raise ValueError(f"unsupported type {type(value)}")
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def compute_extensions_digest(extensions: dict) -> tuple:
|
|
164
|
+
canonicalized = canonicalize_value(extensions)
|
|
165
|
+
canonical = json.dumps(canonicalized, separators=(",", ":"), ensure_ascii=False)
|
|
166
|
+
return canonical, sha256(canonical)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
cv_vectors = vectors["canonicalize_value"]["vectors"]
|
|
170
|
+
for vec in cv_vectors:
|
|
171
|
+
if "expected_error" in vec:
|
|
172
|
+
threw = False
|
|
173
|
+
if "construct" in vec:
|
|
174
|
+
# Programmatic: lone surrogate can't reliably live in JSON
|
|
175
|
+
try:
|
|
176
|
+
canonicalize_value(chr(0xD800))
|
|
177
|
+
except (ValueError, UnicodeEncodeError):
|
|
178
|
+
threw = True
|
|
179
|
+
else:
|
|
180
|
+
try:
|
|
181
|
+
canonicalize_value(vec["input"])
|
|
182
|
+
except ValueError:
|
|
183
|
+
threw = True
|
|
184
|
+
check(threw, f"{vec['name']} throws on invalid input")
|
|
185
|
+
continue
|
|
186
|
+
if "input_a" in vec and "input_b" in vec and "canonical_form" in vec:
|
|
187
|
+
ra_c, ra_d = compute_extensions_digest(vec["input_a"])
|
|
188
|
+
rb_c, rb_d = compute_extensions_digest(vec["input_b"])
|
|
189
|
+
check(ra_c == vec["canonical_form"], f"{vec['name']} canonical form",
|
|
190
|
+
f"expected: {vec['canonical_form']}\n got: {ra_c}")
|
|
191
|
+
check(ra_d == vec["digest"], f"{vec['name']} digest",
|
|
192
|
+
f"expected: {vec['digest']}\n got: {ra_d}")
|
|
193
|
+
check(ra_d == rb_d, f"{vec['name']} both inputs produce same digest")
|
|
194
|
+
elif "input_a" in vec and "canonical_a" in vec:
|
|
195
|
+
ra_c, ra_d = compute_extensions_digest(vec["input_a"])
|
|
196
|
+
rb_c, rb_d = compute_extensions_digest(vec["input_b"])
|
|
197
|
+
check(ra_c == vec["canonical_a"], f"{vec['name']} canonical_a")
|
|
198
|
+
check(ra_d == vec["digest_a"], f"{vec['name']} digest_a")
|
|
199
|
+
check(rb_c == vec["canonical_b"], f"{vec['name']} canonical_b")
|
|
200
|
+
check(rb_d == vec["digest_b"], f"{vec['name']} digest_b")
|
|
201
|
+
check(ra_d != rb_d, f"{vec['name']} digests differ")
|
|
202
|
+
elif "input" in vec:
|
|
203
|
+
r_c, r_d = compute_extensions_digest(vec["input"])
|
|
204
|
+
check(r_c == vec["canonical_form"], f"{vec['name']} canonical form",
|
|
205
|
+
f"expected: {vec['canonical_form']}\n got: {r_c}")
|
|
206
|
+
check(r_d == vec["digest"], f"{vec['name']} digest",
|
|
207
|
+
f"expected: {vec['digest']}\n got: {r_d}")
|
|
208
|
+
|
|
209
|
+
# --- Extensions Digest ---
|
|
210
|
+
print("\n=== Extensions Digest ===")
|
|
211
|
+
ext_vectors = vectors["extensions_digest"]["vectors"]
|
|
212
|
+
|
|
213
|
+
for vec in ext_vectors:
|
|
214
|
+
canonical, digest = compute_extensions_digest(vec["extensions"])
|
|
215
|
+
check(
|
|
216
|
+
canonical == vec["canonical_form"],
|
|
217
|
+
f"{vec['name']} canonical form",
|
|
218
|
+
f"expected: {vec['canonical_form']}\n got: {canonical}",
|
|
219
|
+
)
|
|
220
|
+
check(
|
|
221
|
+
digest == vec["digest"],
|
|
222
|
+
f"{vec['name']} digest",
|
|
223
|
+
f"expected: {vec['digest']}\n got: {digest}",
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def canonicalize_record(record: dict) -> str:
|
|
228
|
+
assert_well_formed_string(record["id"], "canonicalizeRecord.id")
|
|
229
|
+
assert_well_formed_string(record["timestamp"], "canonicalizeRecord.timestamp")
|
|
230
|
+
assert_well_formed_string(record["method"], "canonicalizeRecord.method")
|
|
231
|
+
if record.get("toolName") is not None:
|
|
232
|
+
assert_well_formed_string(record["toolName"], "canonicalizeRecord.toolName")
|
|
233
|
+
if record.get("namespace") is not None:
|
|
234
|
+
assert_well_formed_string(record["namespace"], "canonicalizeRecord.namespace")
|
|
235
|
+
if record.get("upstream") is not None:
|
|
236
|
+
assert_well_formed_string(record["upstream"], "canonicalizeRecord.upstream")
|
|
237
|
+
if record.get("principal") is not None:
|
|
238
|
+
assert_well_formed_string(record["principal"], "canonicalizeRecord.principal")
|
|
239
|
+
if record.get("previousHash") is not None:
|
|
240
|
+
assert_well_formed_string(record["previousHash"], "canonicalizeRecord.previousHash")
|
|
241
|
+
if record.get("decisionContextDigest") is not None:
|
|
242
|
+
assert_well_formed_string(record["decisionContextDigest"], "canonicalizeRecord.decisionContextDigest")
|
|
243
|
+
if record.get("extensionsDigest") is not None:
|
|
244
|
+
assert_well_formed_string(record["extensionsDigest"], "canonicalizeRecord.extensionsDigest")
|
|
245
|
+
ordered = [
|
|
246
|
+
["id", record["id"]],
|
|
247
|
+
["timestamp", record["timestamp"]],
|
|
248
|
+
["method", record["method"]],
|
|
249
|
+
["toolName", record.get("toolName")],
|
|
250
|
+
["namespace", record.get("namespace")],
|
|
251
|
+
["upstream", record.get("upstream")],
|
|
252
|
+
["principal", record.get("principal")],
|
|
253
|
+
["durationMs", record["durationMs"]],
|
|
254
|
+
["success", record["success"]],
|
|
255
|
+
["errorCode", record.get("errorCode")],
|
|
256
|
+
["previousHash", record.get("previousHash")],
|
|
257
|
+
]
|
|
258
|
+
insert_at = 11
|
|
259
|
+
if record.get("decisionContextDigest") is not None:
|
|
260
|
+
ordered.insert(10, ["decisionContextDigest", record["decisionContextDigest"]])
|
|
261
|
+
insert_at = 12
|
|
262
|
+
if record.get("extensionsDigest") is not None:
|
|
263
|
+
ordered.insert(insert_at, ["extensionsDigest", record["extensionsDigest"]])
|
|
264
|
+
insert_at += 1
|
|
265
|
+
if record.get("aiInvocation") is not None:
|
|
266
|
+
ordered.insert(insert_at, ["aiInvocation", canonicalize_value(record["aiInvocation"])])
|
|
267
|
+
insert_at += 1
|
|
268
|
+
if record.get("parties") is not None:
|
|
269
|
+
ordered.insert(insert_at, ["parties", record["parties"]])
|
|
270
|
+
return json.dumps(ordered, separators=(",", ":"), ensure_ascii=False)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
rec_canon = vectors["extensions_digest"]["record_canonicalization"]
|
|
274
|
+
|
|
275
|
+
with_ext = rec_canon["with_extensions_digest"]
|
|
276
|
+
with_ext_canonical = canonicalize_record(with_ext["record"])
|
|
277
|
+
check(
|
|
278
|
+
with_ext_canonical == with_ext["canonical"],
|
|
279
|
+
"record with extensionsDigest canonical form",
|
|
280
|
+
f"expected: {with_ext['canonical']}\n got: {with_ext_canonical}",
|
|
281
|
+
)
|
|
282
|
+
check(
|
|
283
|
+
sha256(with_ext_canonical) == with_ext["sha256_canonical"],
|
|
284
|
+
"record with extensionsDigest SHA-256",
|
|
285
|
+
f"expected: {with_ext['sha256_canonical']}\n got: {sha256(with_ext_canonical)}",
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
without_ext = rec_canon["without_extensions_digest"]
|
|
289
|
+
without_ext_canonical = canonicalize_record(without_ext["record"])
|
|
290
|
+
check(
|
|
291
|
+
without_ext_canonical == without_ext["canonical"],
|
|
292
|
+
"record without extensionsDigest canonical form (backward compat)",
|
|
293
|
+
f"expected: {without_ext['canonical']}\n got: {without_ext_canonical}",
|
|
294
|
+
)
|
|
295
|
+
check(
|
|
296
|
+
sha256(without_ext_canonical) == without_ext["sha256_canonical"],
|
|
297
|
+
"record without extensionsDigest SHA-256",
|
|
298
|
+
f"expected: {without_ext['sha256_canonical']}\n got: {sha256(without_ext_canonical)}",
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
# --- Rotation Boundary ---
|
|
302
|
+
print("\n=== Rotation Boundary ===")
|
|
303
|
+
rotation = vectors["rotation_boundary"]
|
|
304
|
+
|
|
305
|
+
for i, entry in enumerate(rotation["file_1_records"]):
|
|
306
|
+
h = hash_record(entry["record"])
|
|
307
|
+
check(
|
|
308
|
+
h == entry["record_hash"],
|
|
309
|
+
f"rotation file1 record {i} hash",
|
|
310
|
+
f"expected: {entry['record_hash']}\n got: {h}",
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
file1_last_hash = rotation["file_1_records"][-1]["record_hash"]
|
|
314
|
+
file2_first = rotation["file_2_records"][0]
|
|
315
|
+
check(
|
|
316
|
+
file2_first["record"]["previousHash"] == file1_last_hash,
|
|
317
|
+
"rotation: file2 first record chains to file1 last hash",
|
|
318
|
+
f"expected: {file1_last_hash}\n got: {file2_first['record']['previousHash']}",
|
|
319
|
+
)
|
|
320
|
+
file2_hash = hash_record(file2_first["record"])
|
|
321
|
+
check(
|
|
322
|
+
file2_hash == file2_first["record_hash"],
|
|
323
|
+
"rotation file2 record 0 hash",
|
|
324
|
+
f"expected: {file2_first['record_hash']}\n got: {file2_hash}",
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
# --- Sequence Regression ---
|
|
328
|
+
print("\n=== Sequence Regression ===")
|
|
329
|
+
seq_reg = vectors["sequence_regression"]
|
|
330
|
+
checkpoints = [e for e in seq_reg["chain"] if e["record"].get("type") == "checkpoint"]
|
|
331
|
+
regression_detected = False
|
|
332
|
+
for i in range(1, len(checkpoints)):
|
|
333
|
+
if checkpoints[i]["record"]["sequence"] <= checkpoints[i - 1]["record"]["sequence"]:
|
|
334
|
+
regression_detected = True
|
|
335
|
+
check(regression_detected, "sequence regression detected in chain")
|
|
336
|
+
check(
|
|
337
|
+
seq_reg["detection_result"]["failureCode"] == "sequence_regression",
|
|
338
|
+
"failure code is sequence_regression",
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
# --- Chain Break ---
|
|
342
|
+
print("\n=== Chain Break ===")
|
|
343
|
+
chain_break = vectors["chain_break"]
|
|
344
|
+
for i, entry in enumerate(chain_break["records"]):
|
|
345
|
+
h = hash_record(entry["record"])
|
|
346
|
+
check(
|
|
347
|
+
h == entry["record_hash"],
|
|
348
|
+
f"chain_break record {i} hash",
|
|
349
|
+
f"expected: {entry['record_hash']}\n got: {h}",
|
|
350
|
+
)
|
|
351
|
+
check(
|
|
352
|
+
chain_break["records"][1]["record"]["previousHash"] == chain_break["records"][0]["record_hash"],
|
|
353
|
+
"record after chain_break chains from break record hash",
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
# --- Summary ---
|
|
357
|
+
print(f"\n=== Results: {passed} passed, {failed} failed ({passed + failed} total) ===")
|
|
358
|
+
sys.exit(1 if failed > 0 else 0)
|