core-runtime-engine 11.5.1__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.
- core_runtime/__init__.py +10 -0
- core_runtime/__version__.py +17 -0
- core_runtime/cli/__init__.py +7 -0
- core_runtime/cli/__main__.py +22 -0
- core_runtime/cli/bump_version.py +176 -0
- core_runtime/cli/contract_preflight.py +69 -0
- core_runtime/cli/create_domain.py +66 -0
- core_runtime/cli/doctor.py +44 -0
- core_runtime/cli/inventory.py +85 -0
- core_runtime/cli/lint.py +8 -0
- core_runtime/cli/main.py +579 -0
- core_runtime/cli/release_check.py +65 -0
- core_runtime/cli/repair_artifact_paths.py +48 -0
- core_runtime/cli/sync_template.py +67 -0
- core_runtime/cli/validate.py +73 -0
- core_runtime/core/__init__.py +34 -0
- core_runtime/core/audit_event.py +760 -0
- core_runtime/core/audit_trail_index.py +100 -0
- core_runtime/core/canonicalization.py +55 -0
- core_runtime/core/contract_evaluator.py +945 -0
- core_runtime/core/contract_executability.py +307 -0
- core_runtime/core/contract_loader.py +57 -0
- core_runtime/core/contract_probes.py +630 -0
- core_runtime/core/contract_program.py +131 -0
- core_runtime/core/contract_program_registry.py +104 -0
- core_runtime/core/contract_program_v2.py +126 -0
- core_runtime/core/dsk_v3.py +142 -0
- core_runtime/core/explainability.py +2115 -0
- core_runtime/core/numeric_normalization.py +120 -0
- core_runtime/core/rule_anchor.py +1388 -0
- core_runtime/core/schema_fingerprint.py +69 -0
- core_runtime/core/sensor_evidence.py +548 -0
- core_runtime/data/contracts/CoreAnchor.sol +109 -0
- core_runtime/data/contracts/CoreRuleAnchor.abi.json +111 -0
- core_runtime/data/contracts/CoreRuleAnchor.bin +1 -0
- core_runtime/data/contracts/CoreRuleAnchor.build.json +20 -0
- core_runtime/data/contracts/CoreRuleAnchor.runtime.bin +1 -0
- core_runtime/data/contracts/CoreRuleAnchor.sol +74 -0
- core_runtime/data/package_data_manifest.v1.json +173 -0
- core_runtime/data/schemas/core/causal_trace.v1.json +141 -0
- core_runtime/data/schemas/core/context_gate.v1.json +38 -0
- core_runtime/data/schemas/core/context_threshold.v1.json +46 -0
- core_runtime/data/schemas/core/contract_program.v1.json +186 -0
- core_runtime/data/schemas/core/contract_program.v2.json +187 -0
- core_runtime/data/schemas/core/control_decision.v1.json +114 -0
- core_runtime/data/schemas/core/dsk.v3.json +105 -0
- core_runtime/data/schemas/core/effect_result.v1.json +39 -0
- core_runtime/data/schemas/core/entropy_signal.v1.json +108 -0
- core_runtime/data/schemas/core/execution_receipt.v1.json +93 -0
- core_runtime/data/schemas/core/frozen_release_manifest.v1.json +87 -0
- core_runtime/data/schemas/core/frozen_release_manifest.v2.json +72 -0
- core_runtime/data/schemas/core/frozen_release_manifest.v3.json +38 -0
- core_runtime/data/schemas/core/frozen_release_manifest.v4.json +72 -0
- core_runtime/data/schemas/core/frozen_release_manifest.v5.json +38 -0
- core_runtime/data/schemas/core/frozen_release_manifest.v6.json +116 -0
- core_runtime/data/schemas/core/frozen_release_manifest.v7.json +37 -0
- core_runtime/data/schemas/core/frozen_release_manifest.v8.json +114 -0
- core_runtime/data/schemas/core/frozen_rule_set.v1.json +282 -0
- core_runtime/data/schemas/core/memory_artifact.v1.json +120 -0
- core_runtime/data/schemas/core/memory_generation_result.v1.json +37 -0
- core_runtime/data/schemas/core/operational_learning_event.v1.json +63 -0
- core_runtime/data/schemas/core/pattern_candidate.v1.json +114 -0
- core_runtime/data/schemas/core/physical_safety_assurance_case.v1.json +676 -0
- core_runtime/data/schemas/core/policy_lifecycle.v1.json +99 -0
- core_runtime/data/schemas/core/retention_manifest.v1.json +53 -0
- core_runtime/data/schemas/core/reversibility_policy.v1.json +107 -0
- core_runtime/data/schemas/core/rule_anchor_batch.v1.json +93 -0
- core_runtime/data/schemas/core/rule_anchor_chain_evidence.v1.json +56 -0
- core_runtime/data/schemas/core/rule_approval.v1.json +49 -0
- core_runtime/data/schemas/core/rule_approval_request.v1.json +42 -0
- core_runtime/data/schemas/core/state_transition.v1.json +115 -0
- core_runtime/data/schemas/core/task_closeout.v1.json +47 -0
- core_runtime/data/schemas/core/template_promotion_candidate.v1.json +83 -0
- core_runtime/data/schemas/core/unsigned_rule_anchor_deployment.v1.json +106 -0
- core_runtime/data/schemas/core/unsigned_rule_anchor_transaction.v1.json +116 -0
- core_runtime/tooling/__init__.py +48 -0
- core_runtime/tooling/bump_version.py +900 -0
- core_runtime/tooling/contract_preflight.py +293 -0
- core_runtime/tooling/create_domain.py +254 -0
- core_runtime/tooling/diagnostics.py +129 -0
- core_runtime/tooling/doctor.py +482 -0
- core_runtime/tooling/file_inventory.py +182 -0
- core_runtime/tooling/json_checks.py +100 -0
- core_runtime/tooling/release_check.py +1017 -0
- core_runtime/tooling/repair_artifact_paths.py +458 -0
- core_runtime/tooling/report_writer.py +176 -0
- core_runtime/tooling/repository_inventory.py +399 -0
- core_runtime/tooling/safety_checks.py +172 -0
- core_runtime/tooling/sync_template.py +303 -0
- core_runtime/tooling/validation.py +507 -0
- core_runtime/tooling/version_inventory.py +256 -0
- core_runtime_engine-11.5.1.dist-info/METADATA +35 -0
- core_runtime_engine-11.5.1.dist-info/RECORD +96 -0
- core_runtime_engine-11.5.1.dist-info/WHEEL +5 -0
- core_runtime_engine-11.5.1.dist-info/entry_points.txt +2 -0
- core_runtime_engine-11.5.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,945 @@
|
|
|
1
|
+
"""Executable semantics for public CORE contracts.
|
|
2
|
+
|
|
3
|
+
JSON Schema answers whether an artifact has a compatible shape. This module
|
|
4
|
+
answers the separate question that matters operationally: whether the values
|
|
5
|
+
form a coherent, evidence-bound decision under deterministic rules.
|
|
6
|
+
|
|
7
|
+
The evaluator never grants execution, deployment, legal, or moral authority.
|
|
8
|
+
It also never turns finite observations into universal truth. A formal proof
|
|
9
|
+
may demonstrate a proposition only inside its declared model and assumptions.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import copy
|
|
15
|
+
import re
|
|
16
|
+
from collections.abc import Callable, Mapping
|
|
17
|
+
from datetime import datetime
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
from jsonschema import FormatChecker
|
|
22
|
+
from jsonschema.validators import validator_for
|
|
23
|
+
|
|
24
|
+
from core_runtime.core.canonicalization import canonical_json_hash
|
|
25
|
+
from core_runtime.core.contract_loader import available_contracts, load_contract_schema
|
|
26
|
+
from core_runtime.core.contract_program_registry import validate_registry_program
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
FINGERPRINT_RE = re.compile(r"^sha256:[a-f0-9]{64}$")
|
|
30
|
+
WINDOWS_ABSOLUTE_RE = re.compile(r"^[A-Za-z]:[\\/]")
|
|
31
|
+
ABSOLUTE_CLAIM_RE = re.compile(
|
|
32
|
+
r"\b(?:unhackable|zero[ -]risk|impossible to hack|guaranteed safe|"
|
|
33
|
+
r"all circumstances|cero riesgo|imposible de hackear|seguridad absoluta|"
|
|
34
|
+
r"verdad absoluta)\b",
|
|
35
|
+
re.IGNORECASE,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
SAFETY_REQUIRED_SCENARIOS = frozenset(
|
|
39
|
+
{
|
|
40
|
+
"llm_prompt_injection",
|
|
41
|
+
"network_compromise",
|
|
42
|
+
"general_compute_compromise",
|
|
43
|
+
"sensor_fault",
|
|
44
|
+
"communication_loss",
|
|
45
|
+
"power_loss",
|
|
46
|
+
"update_tamper",
|
|
47
|
+
"replay_attack",
|
|
48
|
+
"emergency_stop",
|
|
49
|
+
"out_of_distribution_input",
|
|
50
|
+
}
|
|
51
|
+
)
|
|
52
|
+
SAFETY_REQUIRED_LEDGER_EVENTS = frozenset(
|
|
53
|
+
{
|
|
54
|
+
"safety_policy_changed",
|
|
55
|
+
"hazardous_command_rejected",
|
|
56
|
+
"safety_interlock_activated",
|
|
57
|
+
"unexpected_physical_outcome",
|
|
58
|
+
"assurance_invalidated",
|
|
59
|
+
}
|
|
60
|
+
)
|
|
61
|
+
ASSURANCE_RANK = {
|
|
62
|
+
"rejected": 0,
|
|
63
|
+
"simulation_only": 1,
|
|
64
|
+
"evidence_ready": 2,
|
|
65
|
+
"independent_evidence_ready": 3,
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
Error = dict[str, Any]
|
|
70
|
+
SemanticResult = tuple[list[Error], list[Error], dict[str, Any]]
|
|
71
|
+
SemanticValidator = Callable[[dict[str, Any]], SemanticResult]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def error(code: str, message: str, field: str = "$", **extra: Any) -> Error:
|
|
75
|
+
"""Build a stable error or warning entry."""
|
|
76
|
+
|
|
77
|
+
item: Error = {"code": code, "message": message, "field": field}
|
|
78
|
+
item.update(extra)
|
|
79
|
+
return item
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def artifact_fingerprint(payload: Mapping[str, Any]) -> str:
|
|
83
|
+
"""Fingerprint canonical artifact content, excluding its own fingerprint."""
|
|
84
|
+
|
|
85
|
+
body = {key: value for key, value in payload.items() if key != "fingerprint"}
|
|
86
|
+
return f"sha256:{canonical_json_hash(body)}"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def input_fingerprint(payload: Any) -> str:
|
|
90
|
+
"""Fingerprint the exact parsed input used by the evaluator."""
|
|
91
|
+
|
|
92
|
+
return f"sha256:{canonical_json_hash(payload)}"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _timezone_datetime(value: Any) -> datetime | None:
|
|
96
|
+
if not isinstance(value, str):
|
|
97
|
+
return None
|
|
98
|
+
normalized = value[:-1] + "+00:00" if value.endswith("Z") else value
|
|
99
|
+
try:
|
|
100
|
+
parsed = datetime.fromisoformat(normalized)
|
|
101
|
+
except ValueError:
|
|
102
|
+
return None
|
|
103
|
+
if parsed.tzinfo is None or parsed.utcoffset() is None:
|
|
104
|
+
return None
|
|
105
|
+
return parsed
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _schema_version_map() -> dict[str, str]:
|
|
109
|
+
result: dict[str, str] = {}
|
|
110
|
+
for contract_name in available_contracts():
|
|
111
|
+
schema = load_contract_schema(contract_name)
|
|
112
|
+
version = schema.get("properties", {}).get("schema_version", {}).get("const")
|
|
113
|
+
if isinstance(version, str):
|
|
114
|
+
result[version] = contract_name
|
|
115
|
+
return result
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _schema_errors(payload: Any, schema: dict[str, Any]) -> list[Error]:
|
|
119
|
+
validator_cls = validator_for(schema)
|
|
120
|
+
validator_cls.check_schema(schema)
|
|
121
|
+
validator = validator_cls(schema, format_checker=FormatChecker())
|
|
122
|
+
errors: list[Error] = []
|
|
123
|
+
for item in sorted(validator.iter_errors(payload), key=lambda entry: list(entry.absolute_path)):
|
|
124
|
+
field = ".".join(str(part) for part in item.absolute_path) or "$"
|
|
125
|
+
errors.append(error("schema_validation_error", item.message, field))
|
|
126
|
+
return errors
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def validate_contract_structure(payload: Any) -> list[Error]:
|
|
130
|
+
"""Validate only the published JSON Schema, without semantic decisions."""
|
|
131
|
+
|
|
132
|
+
if not isinstance(payload, dict):
|
|
133
|
+
return [error("invalid_artifact", "Contract artifact must be an object.")]
|
|
134
|
+
version = payload.get("schema_version")
|
|
135
|
+
contract_name = _schema_version_map().get(version)
|
|
136
|
+
if contract_name is None:
|
|
137
|
+
return [error("unknown_schema_version", f"Unknown schema_version: {version!r}.", "schema_version")]
|
|
138
|
+
return _schema_errors(payload, load_contract_schema(contract_name))
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _resolve_local_ref(schema: dict[str, Any], root: dict[str, Any]) -> dict[str, Any]:
|
|
142
|
+
ref = schema.get("$ref")
|
|
143
|
+
if not isinstance(ref, str) or not ref.startswith("#/"):
|
|
144
|
+
return schema
|
|
145
|
+
current: Any = root
|
|
146
|
+
for part in ref[2:].split("/"):
|
|
147
|
+
token = part.replace("~1", "/").replace("~0", "~")
|
|
148
|
+
if not isinstance(current, dict) or token not in current:
|
|
149
|
+
return schema
|
|
150
|
+
current = current[token]
|
|
151
|
+
return current if isinstance(current, dict) else schema
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _strict_shape_errors(
|
|
155
|
+
value: Any,
|
|
156
|
+
schema: dict[str, Any],
|
|
157
|
+
root: dict[str, Any],
|
|
158
|
+
field: str = "$",
|
|
159
|
+
) -> list[Error]:
|
|
160
|
+
"""Close legacy extension points when the strict executable profile is used."""
|
|
161
|
+
|
|
162
|
+
schema = _resolve_local_ref(schema, root)
|
|
163
|
+
errors: list[Error] = []
|
|
164
|
+
if isinstance(value, dict):
|
|
165
|
+
properties = schema.get("properties")
|
|
166
|
+
if isinstance(properties, dict) and properties:
|
|
167
|
+
unknown = sorted(set(value) - set(properties))
|
|
168
|
+
for key in unknown:
|
|
169
|
+
errors.append(
|
|
170
|
+
error(
|
|
171
|
+
"undeclared_field",
|
|
172
|
+
"Strict contract evaluation rejects undeclared fields.",
|
|
173
|
+
f"{field}.{key}",
|
|
174
|
+
)
|
|
175
|
+
)
|
|
176
|
+
for key, child in value.items():
|
|
177
|
+
child_schema = properties.get(key)
|
|
178
|
+
if isinstance(child_schema, dict):
|
|
179
|
+
errors.extend(_strict_shape_errors(child, child_schema, root, f"{field}.{key}"))
|
|
180
|
+
elif isinstance(value, list):
|
|
181
|
+
item_schema = schema.get("items")
|
|
182
|
+
if isinstance(item_schema, dict):
|
|
183
|
+
for index, child in enumerate(value):
|
|
184
|
+
errors.extend(_strict_shape_errors(child, item_schema, root, f"{field}[{index}]"))
|
|
185
|
+
return errors
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _unsafe_ref(value: str) -> bool:
|
|
189
|
+
if not value or value.startswith("/") or WINDOWS_ABSOLUTE_RE.match(value):
|
|
190
|
+
return True
|
|
191
|
+
normalized = value.replace("\\", "/")
|
|
192
|
+
return ".." in normalized.split("/") or "\x00" in value
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _reference_errors(value: Any, field: str = "$", key: str = "") -> list[Error]:
|
|
196
|
+
errors: list[Error] = []
|
|
197
|
+
if isinstance(value, dict):
|
|
198
|
+
for child_key, child in value.items():
|
|
199
|
+
errors.extend(_reference_errors(child, f"{field}.{child_key}", child_key))
|
|
200
|
+
elif isinstance(value, list):
|
|
201
|
+
if key.endswith("_refs") and all(isinstance(item, str) for item in value):
|
|
202
|
+
if len(value) != len(set(value)):
|
|
203
|
+
errors.append(error("duplicate_reference", "Reference arrays must be unique.", field))
|
|
204
|
+
singular_key = key[:-1] if key.endswith("_refs") else key
|
|
205
|
+
for index, child in enumerate(value):
|
|
206
|
+
errors.extend(_reference_errors(child, f"{field}[{index}]", singular_key))
|
|
207
|
+
elif isinstance(value, str) and (key.endswith("_ref") or key in {"root_ref", "before_ref", "after_ref"}):
|
|
208
|
+
if _unsafe_ref(value):
|
|
209
|
+
errors.append(error("unsafe_reference", "References must be bounded and relative.", field))
|
|
210
|
+
return errors
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _fingerprint_errors(payload: dict[str, Any]) -> list[Error]:
|
|
214
|
+
if "fingerprint" not in payload:
|
|
215
|
+
return []
|
|
216
|
+
declared = payload.get("fingerprint")
|
|
217
|
+
if not isinstance(declared, str) or not FINGERPRINT_RE.fullmatch(declared):
|
|
218
|
+
return [error("invalid_fingerprint", "fingerprint must be sha256:<64 lowercase hex>.", "fingerprint")]
|
|
219
|
+
computed = artifact_fingerprint(payload)
|
|
220
|
+
if declared != computed:
|
|
221
|
+
return [
|
|
222
|
+
error(
|
|
223
|
+
"fingerprint_mismatch",
|
|
224
|
+
"fingerprint does not match canonical artifact content.",
|
|
225
|
+
"fingerprint",
|
|
226
|
+
declared=declared,
|
|
227
|
+
computed=computed,
|
|
228
|
+
)
|
|
229
|
+
]
|
|
230
|
+
return []
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _timestamp_error(payload: dict[str, Any], key: str) -> list[Error]:
|
|
234
|
+
if key in payload and _timezone_datetime(payload.get(key)) is None:
|
|
235
|
+
return [error("invalid_timestamp", "Timestamp must include an explicit timezone.", key)]
|
|
236
|
+
return []
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _empty_result() -> SemanticResult:
|
|
240
|
+
return [], [], {}
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _validate_causal_trace(payload: dict[str, Any]) -> SemanticResult:
|
|
244
|
+
errors: list[Error] = []
|
|
245
|
+
nodes = payload.get("nodes", [])
|
|
246
|
+
refs: list[str] = []
|
|
247
|
+
ids: list[str] = []
|
|
248
|
+
if isinstance(nodes, list):
|
|
249
|
+
refs = [item.get("ref") for item in nodes if isinstance(item, dict) and isinstance(item.get("ref"), str)]
|
|
250
|
+
ids = [item.get("node_id") for item in nodes if isinstance(item, dict) and isinstance(item.get("node_id"), str)]
|
|
251
|
+
if len(ids) != len(set(ids)):
|
|
252
|
+
errors.append(error("duplicate_node_id", "node_id values must be unique.", "nodes"))
|
|
253
|
+
if len(refs) != len(set(refs)):
|
|
254
|
+
errors.append(error("duplicate_node_ref", "Node ref values must be unique.", "nodes"))
|
|
255
|
+
if payload.get("root_ref") not in set(refs):
|
|
256
|
+
errors.append(error("missing_root_node", "root_ref must resolve to a node ref.", "root_ref"))
|
|
257
|
+
|
|
258
|
+
graph: dict[str, list[str]] = {ref: [] for ref in refs}
|
|
259
|
+
for index, edge in enumerate(payload.get("edges", [])):
|
|
260
|
+
if not isinstance(edge, dict):
|
|
261
|
+
continue
|
|
262
|
+
source = edge.get("from_ref")
|
|
263
|
+
target = edge.get("to_ref")
|
|
264
|
+
if source not in graph or target not in graph:
|
|
265
|
+
errors.append(error("dangling_edge_ref", "Every edge endpoint must resolve to a node ref.", f"edges[{index}]"))
|
|
266
|
+
continue
|
|
267
|
+
if source == target:
|
|
268
|
+
errors.append(error("causal_self_loop", "A causal edge cannot reference itself.", f"edges[{index}]"))
|
|
269
|
+
graph[source].append(target)
|
|
270
|
+
|
|
271
|
+
visiting: set[str] = set()
|
|
272
|
+
visited: set[str] = set()
|
|
273
|
+
|
|
274
|
+
def visit(node: str) -> bool:
|
|
275
|
+
if node in visiting:
|
|
276
|
+
return True
|
|
277
|
+
if node in visited:
|
|
278
|
+
return False
|
|
279
|
+
visiting.add(node)
|
|
280
|
+
if any(visit(child) for child in graph.get(node, [])):
|
|
281
|
+
return True
|
|
282
|
+
visiting.remove(node)
|
|
283
|
+
visited.add(node)
|
|
284
|
+
return False
|
|
285
|
+
|
|
286
|
+
if any(visit(node) for node in sorted(graph)):
|
|
287
|
+
errors.append(error("causal_cycle", "Causal traces must be acyclic.", "edges"))
|
|
288
|
+
errors.extend(_timestamp_error(payload, "created_at"))
|
|
289
|
+
return errors, [], {"node_count": len(refs), "edge_count": len(payload.get("edges", []))}
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _validate_entropy_signal(payload: dict[str, Any]) -> SemanticResult:
|
|
293
|
+
errors: list[Error] = []
|
|
294
|
+
measurement = payload.get("measurement")
|
|
295
|
+
if not isinstance(measurement, dict) or not measurement:
|
|
296
|
+
errors.append(error("measurement_required", "Entropy signals require a non-empty measurement.", "measurement"))
|
|
297
|
+
response = str(payload.get("suggested_response", "")).strip().lower()
|
|
298
|
+
if payload.get("severity") == "critical" and response in {"ignore", "allow", "continue"}:
|
|
299
|
+
errors.append(error("critical_signal_cannot_be_ignored", "A critical signal cannot recommend continuation.", "suggested_response"))
|
|
300
|
+
errors.extend(_timestamp_error(payload, "timestamp"))
|
|
301
|
+
return errors, [], {}
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _validate_control_decision(payload: dict[str, Any]) -> SemanticResult:
|
|
305
|
+
errors: list[Error] = []
|
|
306
|
+
decision = payload.get("decision")
|
|
307
|
+
reversibility = payload.get("reversibility_class")
|
|
308
|
+
if decision == "allow" and reversibility in {"irreversible", "unknown"}:
|
|
309
|
+
errors.append(error("unsafe_allow_decision", "Unknown or irreversible actions cannot be directly allowed.", "decision"))
|
|
310
|
+
if decision in {"allow", "require_confirmation"} and not payload.get("evidence_refs"):
|
|
311
|
+
errors.append(error("required_evidence_missing", "A non-blocked decision must bind observed evidence.", "evidence_refs"))
|
|
312
|
+
errors.extend(_timestamp_error(payload, "created_at"))
|
|
313
|
+
return errors, [], {"execution_authorized": False}
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _validate_execution_receipt(payload: dict[str, Any]) -> SemanticResult:
|
|
317
|
+
errors: list[Error] = []
|
|
318
|
+
status = payload.get("status")
|
|
319
|
+
transitions = payload.get("state_transition_refs", [])
|
|
320
|
+
if status == "succeeded" and not transitions:
|
|
321
|
+
errors.append(error("successful_receipt_requires_transition", "A successful effect requires at least one state transition.", "state_transition_refs"))
|
|
322
|
+
if status in {"skipped", "simulated"} and transitions:
|
|
323
|
+
errors.append(error("simulated_transition_forbidden", "Skipped or simulated execution cannot claim state changes.", "state_transition_refs"))
|
|
324
|
+
errors.extend(_timestamp_error(payload, "created_at"))
|
|
325
|
+
return errors, [], {"execution_authorized": False}
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _validate_memory_artifact(payload: dict[str, Any]) -> SemanticResult:
|
|
329
|
+
errors: list[Error] = []
|
|
330
|
+
retention = payload.get("retention", {})
|
|
331
|
+
protected = any(payload.get(key) for key in ("stable_facts", "decisions", "invariants", "open_risks"))
|
|
332
|
+
if isinstance(retention, dict) and retention.get("retention_class") == "forget" and protected:
|
|
333
|
+
errors.append(error("unsafe_memory_forgetting", "Memory containing facts, decisions, invariants, or risks cannot be marked forget.", "retention.retention_class"))
|
|
334
|
+
errors.extend(_timestamp_error(payload, "created_at"))
|
|
335
|
+
return errors, [], {"authority": "reference_only"}
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _validate_task_closeout(payload: dict[str, Any]) -> SemanticResult:
|
|
339
|
+
errors: list[Error] = []
|
|
340
|
+
if payload.get("status") not in {"passed", "failed", "blocked", "partial"}:
|
|
341
|
+
errors.append(error("invalid_closeout_status", "Closeout status must be passed, failed, blocked, or partial.", "status"))
|
|
342
|
+
evidence_fields = ("report_ref", "events_ref", "effect_results", "memory_generation_result")
|
|
343
|
+
if payload.get("status") == "passed" and not any(payload.get(key) for key in evidence_fields):
|
|
344
|
+
errors.append(error("closeout_evidence_required", "A passed closeout requires an evidence-bearing result or report reference.", "status"))
|
|
345
|
+
return errors, [], {}
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _validate_effect_result(payload: dict[str, Any]) -> SemanticResult:
|
|
349
|
+
errors: list[Error] = []
|
|
350
|
+
status = payload.get("status")
|
|
351
|
+
dry_run = payload.get("dry_run")
|
|
352
|
+
if (status == "dry_run") != (dry_run is True):
|
|
353
|
+
errors.append(error("effect_status_dry_run_mismatch", "status=dry_run and dry_run=true must agree.", "dry_run"))
|
|
354
|
+
if status in {"sent", "applied"}:
|
|
355
|
+
if dry_run is not False:
|
|
356
|
+
errors.append(error("applied_effect_cannot_be_dry_run", "Applied effects must declare dry_run=false.", "dry_run"))
|
|
357
|
+
if not payload.get("provider") or not payload.get("target_ref"):
|
|
358
|
+
errors.append(error("effect_destination_required", "Applied effects require provider and target_ref.", "target_ref"))
|
|
359
|
+
if status == "failed" and not payload.get("error"):
|
|
360
|
+
errors.append(error("failed_effect_requires_error", "Failed effects require an error description.", "error"))
|
|
361
|
+
return errors, [], {"execution_authorized": False}
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _validate_memory_generation_result(payload: dict[str, Any]) -> SemanticResult:
|
|
365
|
+
errors: list[Error] = []
|
|
366
|
+
status = payload.get("status")
|
|
367
|
+
if status not in {"passed", "failed", "skipped"}:
|
|
368
|
+
errors.append(error("invalid_memory_result_status", "Memory result status must be passed, failed, or skipped.", "status"))
|
|
369
|
+
if status == "passed" and (not payload.get("memory_id") or not payload.get("memory_ref")):
|
|
370
|
+
errors.append(error("memory_reference_required", "A passed memory result requires memory_id and memory_ref.", "memory_ref"))
|
|
371
|
+
if payload.get("reused") is True and (not payload.get("memory_id") or not payload.get("memory_ref")):
|
|
372
|
+
errors.append(error("reused_memory_reference_required", "Reused memory must resolve to an immutable memory reference.", "memory_ref"))
|
|
373
|
+
if status == "failed" and payload.get("reused") is True:
|
|
374
|
+
errors.append(error("failed_memory_cannot_be_reused", "A failed result cannot claim reuse.", "reused"))
|
|
375
|
+
return errors, [], {}
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def _validate_operational_learning_event(payload: dict[str, Any]) -> SemanticResult:
|
|
379
|
+
errors: list[Error] = []
|
|
380
|
+
if payload.get("status") not in {"recorded", "candidate", "accepted", "rejected", "quarantined"}:
|
|
381
|
+
errors.append(error("invalid_learning_status", "Learning status is not recognized.", "status"))
|
|
382
|
+
event_payload = payload.get("payload")
|
|
383
|
+
if not isinstance(event_payload, dict) or not event_payload:
|
|
384
|
+
errors.append(error("learning_payload_required", "Operational learning requires a non-empty payload.", "payload"))
|
|
385
|
+
|
|
386
|
+
def scan(value: Any, field: str) -> None:
|
|
387
|
+
if isinstance(value, dict):
|
|
388
|
+
for key, child in value.items():
|
|
389
|
+
child_field = f"{field}.{key}"
|
|
390
|
+
if key in {"auto_execute", "self_modify"} and child is True:
|
|
391
|
+
errors.append(error("learning_event_authority_escalation", "Learning events cannot grant execution or self-modification.", child_field))
|
|
392
|
+
if key in {"authority", "activation_default"} and str(child).lower() in {"binding", "execution_authority", "enabled", "automatic"}:
|
|
393
|
+
errors.append(error("learning_event_authority_escalation", "Learning events remain candidate-only.", child_field))
|
|
394
|
+
scan(child, child_field)
|
|
395
|
+
elif isinstance(value, list):
|
|
396
|
+
for index, child in enumerate(value):
|
|
397
|
+
scan(child, f"{field}[{index}]")
|
|
398
|
+
|
|
399
|
+
scan(event_payload, "payload")
|
|
400
|
+
errors.extend(_timestamp_error(payload, "timestamp"))
|
|
401
|
+
return errors, [], {"authority": "candidate_only"}
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def _validate_policy_lifecycle(payload: dict[str, Any]) -> SemanticResult:
|
|
405
|
+
errors: list[Error] = []
|
|
406
|
+
start = _timezone_datetime(payload.get("effective_from"))
|
|
407
|
+
end = _timezone_datetime(payload.get("effective_to")) if payload.get("effective_to") is not None else None
|
|
408
|
+
if start is None:
|
|
409
|
+
errors.append(error("invalid_effective_from", "effective_from must include a timezone.", "effective_from"))
|
|
410
|
+
if payload.get("effective_to") is not None and end is None:
|
|
411
|
+
errors.append(error("invalid_effective_to", "effective_to must include a timezone.", "effective_to"))
|
|
412
|
+
if start is not None and end is not None and end <= start:
|
|
413
|
+
errors.append(error("invalid_effective_interval", "effective_to must be later than effective_from.", "effective_to"))
|
|
414
|
+
if payload.get("status") in {"superseded", "retired"} and end is None:
|
|
415
|
+
errors.append(error("closed_policy_requires_end", "Superseded or retired policies require effective_to.", "effective_to"))
|
|
416
|
+
if payload.get("supersedes") == payload.get("policy_id"):
|
|
417
|
+
errors.append(error("policy_cannot_supersede_itself", "A policy cannot supersede itself.", "supersedes"))
|
|
418
|
+
return errors, [], {}
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def _validate_context_threshold(payload: dict[str, Any]) -> SemanticResult:
|
|
422
|
+
errors: list[Error] = []
|
|
423
|
+
for key in ("current_usage_percent", "previous_usage_percent", "ema_usage_percent", "compression_threshold_percent"):
|
|
424
|
+
value = payload.get(key)
|
|
425
|
+
if value is not None and (not isinstance(value, (int, float)) or isinstance(value, bool) or value < 0 or value > 100):
|
|
426
|
+
errors.append(error("invalid_percentage", "Percentages must be between 0 and 100.", key))
|
|
427
|
+
current = payload.get("current_usage_percent")
|
|
428
|
+
threshold = payload.get("compression_threshold_percent")
|
|
429
|
+
if isinstance(current, (int, float)) and isinstance(threshold, (int, float)):
|
|
430
|
+
expected = current >= threshold
|
|
431
|
+
if payload.get("should_compress_now") is not expected:
|
|
432
|
+
errors.append(error("threshold_decision_mismatch", "should_compress_now must be derived from current usage and threshold.", "should_compress_now"))
|
|
433
|
+
if payload.get("status") not in {"passed", "failed", "skipped"}:
|
|
434
|
+
errors.append(error("invalid_threshold_status", "Threshold status must be passed, failed, or skipped.", "status"))
|
|
435
|
+
return errors, [], {}
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def _validate_context_gate(payload: dict[str, Any]) -> SemanticResult:
|
|
439
|
+
errors: list[Error] = []
|
|
440
|
+
status = payload.get("status")
|
|
441
|
+
mode = payload.get("mode")
|
|
442
|
+
if status not in {"passed", "applied", "skipped", "blocked", "failed"}:
|
|
443
|
+
errors.append(error("invalid_context_gate_status", "Context gate status is not recognized.", "status"))
|
|
444
|
+
if mode == "dry-run" and status == "applied":
|
|
445
|
+
errors.append(error("dry_run_cannot_apply", "A dry-run gate cannot report an applied mutation.", "status"))
|
|
446
|
+
if mode == "apply" and status == "applied" and not payload.get("memory_generation_result"):
|
|
447
|
+
errors.append(error("apply_result_required", "An applied gate requires the resulting memory artifact reference.", "memory_generation_result"))
|
|
448
|
+
return errors, [], {"execution_authorized": False}
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def _validate_retention_manifest(payload: dict[str, Any]) -> SemanticResult:
|
|
452
|
+
errors: list[Error] = []
|
|
453
|
+
entries = payload.get("entries", [])
|
|
454
|
+
if not entries:
|
|
455
|
+
errors.append(error("retention_entries_required", "A retention manifest cannot be empty.", "entries"))
|
|
456
|
+
refs = [item.get("artifact_ref") for item in entries if isinstance(item, dict)]
|
|
457
|
+
if len(refs) != len(set(refs)):
|
|
458
|
+
errors.append(error("duplicate_artifact_ref", "Each artifact may have only one retention decision.", "entries"))
|
|
459
|
+
for index, item in enumerate(entries):
|
|
460
|
+
if not isinstance(item, dict):
|
|
461
|
+
continue
|
|
462
|
+
retention_class = item.get("retention_class")
|
|
463
|
+
if retention_class in {"compress", "forget", "quarantine"} and not item.get("checksum"):
|
|
464
|
+
errors.append(error("retention_checksum_required", "Mutating retention actions require the source checksum.", f"entries[{index}].checksum"))
|
|
465
|
+
if retention_class in {"forget", "quarantine"} and not item.get("restore_ref"):
|
|
466
|
+
errors.append(error("retention_restore_required", "Destructive or isolating retention actions require a restore reference.", f"entries[{index}].restore_ref"))
|
|
467
|
+
return errors, [], {}
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def _validate_reversibility_policy(payload: dict[str, Any]) -> SemanticResult:
|
|
471
|
+
errors: list[Error] = []
|
|
472
|
+
reversibility = payload.get("reversibility_class")
|
|
473
|
+
if reversibility in {"irreversible", "unknown"} and payload.get("human_approval_required") is not True:
|
|
474
|
+
errors.append(error("responsible_approval_required", "Unknown or irreversible actions require responsible-person approval.", "human_approval_required"))
|
|
475
|
+
if reversibility == "compensable" and payload.get("compensation_required") is not True:
|
|
476
|
+
errors.append(error("compensation_plan_required", "Compensable actions require compensation.", "compensation_required"))
|
|
477
|
+
errors.extend(_timestamp_error(payload, "created_at"))
|
|
478
|
+
return errors, [], {"execution_authorized": False}
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def _validate_state_transition(payload: dict[str, Any]) -> SemanticResult:
|
|
482
|
+
errors: list[Error] = []
|
|
483
|
+
if payload.get("before_ref") == payload.get("after_ref"):
|
|
484
|
+
errors.append(error("state_transition_noop", "before_ref and after_ref must differ.", "after_ref"))
|
|
485
|
+
actor = str(payload.get("actor_kind", "")).lower()
|
|
486
|
+
responsible_actors = {"human", "human_operator", "responsible_operator", "authorized_signer", "human_directed_software"}
|
|
487
|
+
if payload.get("reversibility_class") in {"irreversible", "unknown"} and actor not in responsible_actors:
|
|
488
|
+
errors.append(error("irreversible_actor_not_responsible", "Irreversible transitions require an explicitly accountable actor kind.", "actor_kind"))
|
|
489
|
+
errors.extend(_timestamp_error(payload, "timestamp"))
|
|
490
|
+
return errors, [], {"execution_authorized": False}
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def _validate_contract_program(payload: dict[str, Any]) -> SemanticResult:
|
|
494
|
+
"""Validate the closed, effect-free instruction language before replay."""
|
|
495
|
+
|
|
496
|
+
errors: list[Error] = []
|
|
497
|
+
policy = payload.get("effect_policy", {})
|
|
498
|
+
if not isinstance(policy, dict) or any(
|
|
499
|
+
policy.get(key) is not False
|
|
500
|
+
for key in ("external_effects", "network_access", "filesystem_access", "state_apply")
|
|
501
|
+
):
|
|
502
|
+
errors.append(error("effect_policy_forbidden", "Contract programs are validation-only and cannot grant effects, network, filesystem, or state application.", "effect_policy"))
|
|
503
|
+
|
|
504
|
+
instructions = payload.get("instructions", [])
|
|
505
|
+
limits = payload.get("limits", {})
|
|
506
|
+
capabilities = set(payload.get("capabilities", []))
|
|
507
|
+
if isinstance(instructions, list) and isinstance(limits, dict):
|
|
508
|
+
if len(instructions) > limits.get("max_steps", 0):
|
|
509
|
+
errors.append(error("program_step_limit_exceeded", "Instruction count exceeds max_steps.", "limits.max_steps"))
|
|
510
|
+
emits = sum(item.get("opcode") == "emit" for item in instructions if isinstance(item, dict))
|
|
511
|
+
if emits > limits.get("max_emits", 0):
|
|
512
|
+
errors.append(error("program_emit_limit_exceeded", "Emit count exceeds max_emits.", "limits.max_emits"))
|
|
513
|
+
halt_positions = [index for index, item in enumerate(instructions) if isinstance(item, dict) and item.get("opcode") == "halt"]
|
|
514
|
+
if halt_positions != [len(instructions) - 1]:
|
|
515
|
+
errors.append(error("program_halt_must_be_terminal", "A contract program must have exactly one terminal halt instruction.", "instructions"))
|
|
516
|
+
required_capabilities = {
|
|
517
|
+
"load": "read_input",
|
|
518
|
+
"assert": "assert",
|
|
519
|
+
"derive": "derive",
|
|
520
|
+
"transition": "stage_transition",
|
|
521
|
+
"emit": "emit_result",
|
|
522
|
+
}
|
|
523
|
+
produced: set[str] = set()
|
|
524
|
+
for index, instruction in enumerate(instructions):
|
|
525
|
+
if not isinstance(instruction, dict):
|
|
526
|
+
continue
|
|
527
|
+
opcode = instruction.get("opcode")
|
|
528
|
+
capability = required_capabilities.get(opcode)
|
|
529
|
+
if capability and capability not in capabilities:
|
|
530
|
+
errors.append(error("program_capability_missing", "Instruction requires a declared capability.", f"instructions[{index}].opcode", capability=capability))
|
|
531
|
+
output = instruction.get("output")
|
|
532
|
+
if isinstance(output, str):
|
|
533
|
+
if output in produced:
|
|
534
|
+
errors.append(error("program_output_redefined", "Instruction outputs must be unique.", f"instructions[{index}].output"))
|
|
535
|
+
produced.add(output)
|
|
536
|
+
if opcode == "transition" and instruction.get("before_ref") == instruction.get("after_ref"):
|
|
537
|
+
errors.append(error("program_transition_noop", "Staged before_ref and after_ref must differ.", f"instructions[{index}].after_ref"))
|
|
538
|
+
if opcode == "derive" and instruction.get("operation") == "copy" and len(instruction.get("input_keys", [])) != 1:
|
|
539
|
+
errors.append(error("program_copy_arity", "copy derivation requires exactly one input key.", f"instructions[{index}].input_keys"))
|
|
540
|
+
if opcode == "derive" and instruction.get("operation") == "registry":
|
|
541
|
+
registry_errors = validate_registry_program({"instructions": [instruction]})
|
|
542
|
+
errors.extend(
|
|
543
|
+
error("registry_instruction_invalid", message, f"instructions[{index}]")
|
|
544
|
+
for message in registry_errors
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
return errors, [], {
|
|
548
|
+
"execution_authorized": False,
|
|
549
|
+
"state_application_authorized": False,
|
|
550
|
+
"instruction_count": len(instructions) if isinstance(instructions, list) else 0,
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def _validate_template_promotion(payload: dict[str, Any]) -> SemanticResult:
|
|
555
|
+
errors: list[Error] = []
|
|
556
|
+
for key in ("required_inputs", "expected_evidence", "stop_conditions"):
|
|
557
|
+
if not payload.get(key):
|
|
558
|
+
errors.append(error("promotion_contract_incomplete", f"{key} cannot be empty.", key))
|
|
559
|
+
if payload.get("risk_tier") in {"medium", "high"} and payload.get("human_approval_required") is not True:
|
|
560
|
+
errors.append(error("promotion_approval_required", "Medium and high-risk templates require responsible approval.", "human_approval_required"))
|
|
561
|
+
if payload.get("risk_tier") == "high" and not payload.get("source_refs"):
|
|
562
|
+
errors.append(error("promotion_source_evidence_required", "High-risk promotion requires source_refs.", "source_refs"))
|
|
563
|
+
return errors, [], {"activation_authorized": False}
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
def _validate_pattern_candidate(payload: dict[str, Any]) -> SemanticResult:
|
|
567
|
+
errors: list[Error] = []
|
|
568
|
+
signature = payload.get("normalized_signature")
|
|
569
|
+
if not isinstance(signature, dict) or not signature:
|
|
570
|
+
errors.append(error("normalized_signature_required", "Pattern candidates require a non-empty normalized signature.", "normalized_signature"))
|
|
571
|
+
classification = payload.get("classification")
|
|
572
|
+
if classification == "candidate_for_template":
|
|
573
|
+
if payload.get("occurrences", 0) < 2 or payload.get("confidence", 0) < 0.8:
|
|
574
|
+
errors.append(error("insufficient_pattern_support", "Template candidates require at least two occurrences and confidence >= 0.8.", "confidence"))
|
|
575
|
+
if not payload.get("evidence_refs") or not payload.get("example_refs"):
|
|
576
|
+
errors.append(error("pattern_evidence_required", "Template candidates require evidence_refs and example_refs.", "evidence_refs"))
|
|
577
|
+
action = str(payload.get("suggested_action", "")).lower()
|
|
578
|
+
if classification == "too_ambiguous" and any(token in action for token in ("promote", "activate", "auto", "template")):
|
|
579
|
+
errors.append(error("ambiguous_pattern_cannot_promote", "Ambiguous patterns may request clarification only.", "suggested_action"))
|
|
580
|
+
errors.extend(_timestamp_error(payload, "observed_at"))
|
|
581
|
+
return errors, [], {"authority": "candidate_only"}
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
def _duplicate_ids(items: Any, key: str, field: str) -> tuple[set[str], list[Error]]:
|
|
585
|
+
values = [item.get(key) for item in items if isinstance(item, dict) and isinstance(item.get(key), str)] if isinstance(items, list) else []
|
|
586
|
+
errors = []
|
|
587
|
+
if len(values) != len(set(values)):
|
|
588
|
+
errors.append(error("duplicate_identifier", f"{key} values must be unique.", field))
|
|
589
|
+
return set(values), errors
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
def _validate_physical_safety_case(payload: dict[str, Any]) -> SemanticResult:
|
|
593
|
+
errors: list[Error] = []
|
|
594
|
+
warnings: list[Error] = []
|
|
595
|
+
claim = payload.get("claim", {})
|
|
596
|
+
envelope = payload.get("observed_envelope", {})
|
|
597
|
+
hazards = payload.get("hazards", [])
|
|
598
|
+
barriers = payload.get("barriers", [])
|
|
599
|
+
tests = payload.get("verification_tests", [])
|
|
600
|
+
evidence = payload.get("evidence", [])
|
|
601
|
+
|
|
602
|
+
evidence_ids, id_errors = _duplicate_ids(evidence, "evidence_id", "evidence")
|
|
603
|
+
errors.extend(id_errors)
|
|
604
|
+
barrier_ids, id_errors = _duplicate_ids(barriers, "barrier_id", "barriers")
|
|
605
|
+
errors.extend(id_errors)
|
|
606
|
+
hazard_ids, id_errors = _duplicate_ids(hazards, "hazard_id", "hazards")
|
|
607
|
+
errors.extend(id_errors)
|
|
608
|
+
test_ids, id_errors = _duplicate_ids(tests, "test_id", "verification_tests")
|
|
609
|
+
errors.extend(id_errors)
|
|
610
|
+
|
|
611
|
+
evidence_by_id = {item.get("evidence_id"): item for item in evidence if isinstance(item, dict)}
|
|
612
|
+
barrier_by_id = {item.get("barrier_id"): item for item in barriers if isinstance(item, dict)}
|
|
613
|
+
test_by_id = {item.get("test_id"): item for item in tests if isinstance(item, dict)}
|
|
614
|
+
|
|
615
|
+
scope = str(claim.get("scope", "")) if isinstance(claim, dict) else ""
|
|
616
|
+
if ABSOLUTE_CLAIM_RE.search(scope):
|
|
617
|
+
errors.append(error("absolute_safety_claim_forbidden", "Safety scope cannot claim universal truth, zero risk, or unhackability.", "claim.scope"))
|
|
618
|
+
if isinstance(claim, dict) and claim.get("claim_status") == "demonstrated_within_model":
|
|
619
|
+
if not claim.get("proof_refs") or "formal_model" not in claim.get("reference_classes", []):
|
|
620
|
+
errors.append(error("formal_demonstration_evidence_required", "Demonstrated-within-model claims require formal_model and proof_refs.", "claim.proof_refs"))
|
|
621
|
+
|
|
622
|
+
extremes = envelope.get("known_extremes", []) if isinstance(envelope, dict) else []
|
|
623
|
+
extreme_ids, id_errors = _duplicate_ids(extremes, "extreme_id", "observed_envelope.known_extremes")
|
|
624
|
+
errors.extend(id_errors)
|
|
625
|
+
directions = {item.get("direction") for item in extremes if isinstance(item, dict)}
|
|
626
|
+
if len(directions) < 2:
|
|
627
|
+
errors.append(error("extreme_diversity_required", "The observed envelope must preserve at least two distinct extreme directions.", "observed_envelope.known_extremes"))
|
|
628
|
+
if isinstance(envelope, dict) and isinstance(envelope.get("sample_count"), int) and envelope.get("sample_count", 0) < len(extreme_ids):
|
|
629
|
+
errors.append(error("sample_count_below_extremes", "sample_count cannot be smaller than the number of extreme observations.", "observed_envelope.sample_count"))
|
|
630
|
+
|
|
631
|
+
evaluated_at = _timezone_datetime(payload.get("evaluated_at"))
|
|
632
|
+
if evaluated_at is None:
|
|
633
|
+
errors.append(error("invalid_evaluated_at", "evaluated_at must include a timezone.", "evaluated_at"))
|
|
634
|
+
evidence_fingerprints: list[str] = []
|
|
635
|
+
for index, item in enumerate(evidence):
|
|
636
|
+
if not isinstance(item, dict):
|
|
637
|
+
continue
|
|
638
|
+
fp = item.get("fingerprint")
|
|
639
|
+
if isinstance(fp, str):
|
|
640
|
+
evidence_fingerprints.append(fp)
|
|
641
|
+
if fp == "sha256:" + ("0" * 64):
|
|
642
|
+
errors.append(error("zero_evidence_fingerprint", "Evidence fingerprints cannot be all zero.", f"evidence[{index}].fingerprint"))
|
|
643
|
+
captured = _timezone_datetime(item.get("captured_at"))
|
|
644
|
+
if captured is None:
|
|
645
|
+
errors.append(error("invalid_evidence_timestamp", "Evidence timestamps require a timezone.", f"evidence[{index}].captured_at"))
|
|
646
|
+
elif evaluated_at is not None and captured > evaluated_at:
|
|
647
|
+
errors.append(error("future_evidence_forbidden", "Evidence cannot be captured after evaluated_at.", f"evidence[{index}].captured_at"))
|
|
648
|
+
if len(evidence_fingerprints) != len(set(evidence_fingerprints)):
|
|
649
|
+
errors.append(error("duplicate_evidence_fingerprint", "Independent evidence items cannot reuse the same fingerprint.", "evidence"))
|
|
650
|
+
|
|
651
|
+
def require_evidence_ref(ref: Any, field: str) -> None:
|
|
652
|
+
if ref not in evidence_ids:
|
|
653
|
+
errors.append(error("unresolved_evidence_ref", "Evidence reference does not resolve inside this frozen case.", field, ref=ref))
|
|
654
|
+
|
|
655
|
+
for index, item in enumerate(extremes):
|
|
656
|
+
if isinstance(item, dict):
|
|
657
|
+
require_evidence_ref(item.get("evidence_ref"), f"observed_envelope.known_extremes[{index}].evidence_ref")
|
|
658
|
+
if _timezone_datetime(item.get("observed_at")) is None:
|
|
659
|
+
errors.append(error("invalid_extreme_timestamp", "Extreme observations require a timezone.", f"observed_envelope.known_extremes[{index}].observed_at"))
|
|
660
|
+
|
|
661
|
+
if isinstance(claim, dict):
|
|
662
|
+
for index, ref in enumerate(claim.get("proof_refs", [])):
|
|
663
|
+
require_evidence_ref(ref, f"claim.proof_refs[{index}]")
|
|
664
|
+
|
|
665
|
+
for index, barrier in enumerate(barriers):
|
|
666
|
+
if not isinstance(barrier, dict):
|
|
667
|
+
continue
|
|
668
|
+
barrier_id = barrier.get("barrier_id")
|
|
669
|
+
for relation_index, ref in enumerate(barrier.get("independent_from", [])):
|
|
670
|
+
if ref == barrier_id:
|
|
671
|
+
errors.append(error("barrier_self_independence", "A barrier cannot be independent from itself.", f"barriers[{index}].independent_from[{relation_index}]"))
|
|
672
|
+
elif ref not in barrier_ids:
|
|
673
|
+
errors.append(error("unresolved_barrier_ref", "independent_from must resolve to another barrier.", f"barriers[{index}].independent_from[{relation_index}]"))
|
|
674
|
+
elif barrier_id not in barrier_by_id.get(ref, {}).get("independent_from", []):
|
|
675
|
+
errors.append(error("asymmetric_barrier_independence", "Barrier independence must be declared by both barriers.", f"barriers[{index}].independent_from[{relation_index}]"))
|
|
676
|
+
for evidence_index, ref in enumerate(barrier.get("evidence_refs", [])):
|
|
677
|
+
require_evidence_ref(ref, f"barriers[{index}].evidence_refs[{evidence_index}]")
|
|
678
|
+
|
|
679
|
+
methods_by_hazard_scenario: dict[str, dict[str, set[str]]] = {
|
|
680
|
+
hazard_id: {scenario: set() for scenario in SAFETY_REQUIRED_SCENARIOS}
|
|
681
|
+
for hazard_id in hazard_ids
|
|
682
|
+
}
|
|
683
|
+
independent_evidence_by_test: dict[str, bool] = {}
|
|
684
|
+
for index, test in enumerate(tests):
|
|
685
|
+
if not isinstance(test, dict):
|
|
686
|
+
continue
|
|
687
|
+
test_id = test.get("test_id")
|
|
688
|
+
evidence_refs = test.get("evidence_refs", [])
|
|
689
|
+
for evidence_index, ref in enumerate(evidence_refs):
|
|
690
|
+
require_evidence_ref(ref, f"verification_tests[{index}].evidence_refs[{evidence_index}]")
|
|
691
|
+
independent_evidence_by_test[str(test_id)] = any(
|
|
692
|
+
isinstance(evidence_by_id.get(ref), dict)
|
|
693
|
+
and evidence_by_id[ref].get("source_kind") == "independent_assessor"
|
|
694
|
+
and evidence_by_id[ref].get("reference_class") == "independent_assessment"
|
|
695
|
+
for ref in evidence_refs
|
|
696
|
+
)
|
|
697
|
+
if test.get("result") != "passed":
|
|
698
|
+
errors.append(error("safety_test_not_passed", "Failed or inconclusive safety tests reject the assurance case.", f"verification_tests[{index}].result"))
|
|
699
|
+
if test.get("hazardous_actuation_observed") is not False:
|
|
700
|
+
errors.append(error("hazardous_actuation_observed", "Any observed hazardous actuation rejects the assurance case.", f"verification_tests[{index}].hazardous_actuation_observed"))
|
|
701
|
+
if test.get("expected_safe_state") != test.get("observed_safe_state"):
|
|
702
|
+
errors.append(error("safe_state_mismatch", "Observed and expected safe states must match exactly.", f"verification_tests[{index}].observed_safe_state"))
|
|
703
|
+
for hazard_ref in test.get("hazard_refs", []):
|
|
704
|
+
if hazard_ref not in hazard_ids:
|
|
705
|
+
errors.append(error("unresolved_hazard_ref", "Test hazard_refs must resolve inside the case.", f"verification_tests[{index}].hazard_refs"))
|
|
706
|
+
continue
|
|
707
|
+
scenario = test.get("scenario")
|
|
708
|
+
method = test.get("method")
|
|
709
|
+
if scenario in SAFETY_REQUIRED_SCENARIOS and isinstance(method, str):
|
|
710
|
+
methods_by_hazard_scenario[hazard_ref][scenario].add(method)
|
|
711
|
+
|
|
712
|
+
for index, hazard in enumerate(hazards):
|
|
713
|
+
if not isinstance(hazard, dict):
|
|
714
|
+
continue
|
|
715
|
+
hazard_id = hazard.get("hazard_id")
|
|
716
|
+
resolved_barriers: list[dict[str, Any]] = []
|
|
717
|
+
for barrier_index, ref in enumerate(hazard.get("barrier_refs", [])):
|
|
718
|
+
barrier = barrier_by_id.get(ref)
|
|
719
|
+
if not isinstance(barrier, dict):
|
|
720
|
+
errors.append(error("unresolved_barrier_ref", "Hazard barrier_refs must resolve inside the case.", f"hazards[{index}].barrier_refs[{barrier_index}]"))
|
|
721
|
+
else:
|
|
722
|
+
resolved_barriers.append(barrier)
|
|
723
|
+
for test_index, ref in enumerate(hazard.get("test_refs", [])):
|
|
724
|
+
test = test_by_id.get(ref)
|
|
725
|
+
if not isinstance(test, dict):
|
|
726
|
+
errors.append(error("unresolved_test_ref", "Hazard test_refs must resolve inside the case.", f"hazards[{index}].test_refs[{test_index}]"))
|
|
727
|
+
elif hazard_id not in test.get("hazard_refs", []):
|
|
728
|
+
errors.append(error("test_hazard_link_mismatch", "Hazard and test links must be bidirectional.", f"hazards[{index}].test_refs[{test_index}]"))
|
|
729
|
+
|
|
730
|
+
if hazard.get("severity") == "catastrophic":
|
|
731
|
+
non_bypassable = [barrier for barrier in resolved_barriers if barrier.get("bypassable_by_general_compute") is False]
|
|
732
|
+
domains = {barrier.get("enforcement_domain") for barrier in non_bypassable}
|
|
733
|
+
kinds = {barrier.get("kind") for barrier in non_bypassable}
|
|
734
|
+
if len(non_bypassable) < 2 or len(domains) < 2:
|
|
735
|
+
errors.append(error("catastrophic_hazard_needs_independent_barriers", "Catastrophic hazards require at least two non-bypassable barriers in distinct enforcement domains.", f"hazards[{index}].barrier_refs"))
|
|
736
|
+
if "isolated_safety_controller" not in kinds:
|
|
737
|
+
errors.append(error("isolated_safety_controller_required", "Catastrophic hazards require an isolated safety controller.", f"hazards[{index}].barrier_refs"))
|
|
738
|
+
if not kinds.intersection({"physical_energy_isolation", "mechanical_limit"}):
|
|
739
|
+
errors.append(error("physical_isolation_required", "Catastrophic hazards require hardware energy isolation or a mechanical limit.", f"hazards[{index}].barrier_refs"))
|
|
740
|
+
|
|
741
|
+
coverage = methods_by_hazard_scenario.get(str(hazard_id), {})
|
|
742
|
+
missing = sorted(scenario for scenario in SAFETY_REQUIRED_SCENARIOS if not coverage.get(scenario))
|
|
743
|
+
if missing:
|
|
744
|
+
errors.append(error("required_scenario_missing", "Every hazard must cover the mandatory adversarial and fault scenarios.", f"hazards[{index}].test_refs", missing=missing))
|
|
745
|
+
|
|
746
|
+
epistemic = payload.get("epistemic_dignity", {})
|
|
747
|
+
if isinstance(epistemic, dict):
|
|
748
|
+
for key in (
|
|
749
|
+
"plain_language_disclosure_ref",
|
|
750
|
+
"evidence_limitations_ref",
|
|
751
|
+
"contestability_ref",
|
|
752
|
+
"local_stop_ref",
|
|
753
|
+
):
|
|
754
|
+
require_evidence_ref(epistemic.get(key), f"epistemic_dignity.{key}")
|
|
755
|
+
lifecycle = payload.get("lifecycle", {})
|
|
756
|
+
if isinstance(lifecycle, dict):
|
|
757
|
+
for key in (
|
|
758
|
+
"secure_boot_evidence_ref",
|
|
759
|
+
"signed_update_evidence_ref",
|
|
760
|
+
"unique_credentials_evidence_ref",
|
|
761
|
+
"sbom_evidence_ref",
|
|
762
|
+
"vulnerability_process_evidence_ref",
|
|
763
|
+
):
|
|
764
|
+
require_evidence_ref(lifecycle.get(key), f"lifecycle.{key}")
|
|
765
|
+
traceability = payload.get("traceability", {})
|
|
766
|
+
if isinstance(traceability, dict):
|
|
767
|
+
require_evidence_ref(traceability.get("immutable_event_ledger_ref"), "traceability.immutable_event_ledger_ref")
|
|
768
|
+
recorded = set(traceability.get("recorded_event_types", []))
|
|
769
|
+
missing_events = sorted(SAFETY_REQUIRED_LEDGER_EVENTS - recorded)
|
|
770
|
+
if missing_events:
|
|
771
|
+
errors.append(error("critical_ledger_event_missing", "The immutable ledger contract omits required safety events.", "traceability.recorded_event_types", missing=missing_events))
|
|
772
|
+
|
|
773
|
+
achieved = "simulation_only"
|
|
774
|
+
if hazard_ids and all(
|
|
775
|
+
all(methods_by_hazard_scenario[hazard_id][scenario] - {"simulation"} for scenario in SAFETY_REQUIRED_SCENARIOS)
|
|
776
|
+
for hazard_id in hazard_ids
|
|
777
|
+
):
|
|
778
|
+
achieved = "evidence_ready"
|
|
779
|
+
if hazard_ids and all(
|
|
780
|
+
"independent_assessment" in methods_by_hazard_scenario[hazard_id][scenario]
|
|
781
|
+
for hazard_id in hazard_ids
|
|
782
|
+
for scenario in SAFETY_REQUIRED_SCENARIOS
|
|
783
|
+
):
|
|
784
|
+
independent_tests = [test for test in tests if isinstance(test, dict) and test.get("method") == "independent_assessment"]
|
|
785
|
+
if independent_tests and all(independent_evidence_by_test.get(str(test.get("test_id")), False) for test in independent_tests):
|
|
786
|
+
achieved = "independent_evidence_ready"
|
|
787
|
+
|
|
788
|
+
requested = payload.get("requested_assurance_level")
|
|
789
|
+
if isinstance(requested, str) and ASSURANCE_RANK.get(achieved, 0) < ASSURANCE_RANK.get(requested, 0):
|
|
790
|
+
errors.append(error("requested_assurance_level_not_met", "Observed evidence does not meet the requested assurance level.", "requested_assurance_level", requested=requested, achieved=achieved))
|
|
791
|
+
if achieved == "simulation_only":
|
|
792
|
+
warnings.append(error("simulation_is_not_deployment_evidence", "Simulation can discover hazards but cannot authorize physical deployment.", "requested_assurance_level"))
|
|
793
|
+
|
|
794
|
+
details = {
|
|
795
|
+
"requested_assurance_level": requested,
|
|
796
|
+
"achieved_assurance_level": "rejected" if errors else achieved,
|
|
797
|
+
"hazard_count": len(hazard_ids),
|
|
798
|
+
"barrier_count": len(barrier_ids),
|
|
799
|
+
"test_count": len(test_ids),
|
|
800
|
+
"evidence_count": len(evidence_ids),
|
|
801
|
+
"execution_authorized": False,
|
|
802
|
+
"deployment_authorized": False,
|
|
803
|
+
}
|
|
804
|
+
return errors, warnings, details
|
|
805
|
+
|
|
806
|
+
|
|
807
|
+
SEMANTIC_VALIDATORS: dict[str, SemanticValidator] = {
|
|
808
|
+
"core.causal_trace.v1": _validate_causal_trace,
|
|
809
|
+
"core.contract_program.v1": _validate_contract_program,
|
|
810
|
+
"core.context_gate.v1": _validate_context_gate,
|
|
811
|
+
"core.context_threshold.v1": _validate_context_threshold,
|
|
812
|
+
"core.control_decision.v1": _validate_control_decision,
|
|
813
|
+
"core.effect_result.v1": _validate_effect_result,
|
|
814
|
+
"core.entropy_signal.v1": _validate_entropy_signal,
|
|
815
|
+
"core.execution_receipt.v1": _validate_execution_receipt,
|
|
816
|
+
"core.memory_artifact.v1": _validate_memory_artifact,
|
|
817
|
+
"core.memory_generation_result.v1": _validate_memory_generation_result,
|
|
818
|
+
"core.operational_learning_event.v1": _validate_operational_learning_event,
|
|
819
|
+
"core.pattern_candidate.v1": _validate_pattern_candidate,
|
|
820
|
+
"core.physical_safety_assurance_case.v1": _validate_physical_safety_case,
|
|
821
|
+
"core.policy_lifecycle.v1": _validate_policy_lifecycle,
|
|
822
|
+
"core.retention_manifest.v1": _validate_retention_manifest,
|
|
823
|
+
"core.reversibility_policy.v1": _validate_reversibility_policy,
|
|
824
|
+
"core.state_transition.v1": _validate_state_transition,
|
|
825
|
+
"core.task_closeout.v1": _validate_task_closeout,
|
|
826
|
+
"core.template_promotion_candidate.v1": _validate_template_promotion,
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
SEMANTIC_RULE_IDS: dict[str, tuple[str, ...]] = {
|
|
830
|
+
"core.causal_trace.v1": ("resolved_graph", "acyclic_graph", "timezone", "fingerprint"),
|
|
831
|
+
"core.contract_program.v1": ("closed_instruction_language", "bounded_steps", "terminal_halt", "declared_capabilities", "no_effect_authority"),
|
|
832
|
+
"core.context_gate.v1": ("mode_status_consistency", "result_binding"),
|
|
833
|
+
"core.context_threshold.v1": ("bounded_percentages", "derived_threshold_decision"),
|
|
834
|
+
"core.control_decision.v1": ("reversibility_gate", "evidence_binding", "no_execution_authority"),
|
|
835
|
+
"core.effect_result.v1": ("dry_run_consistency", "destination_binding", "failure_evidence"),
|
|
836
|
+
"core.entropy_signal.v1": ("measurement_required", "critical_fail_closed", "timezone"),
|
|
837
|
+
"core.execution_receipt.v1": ("status_transition_consistency", "timezone", "fingerprint"),
|
|
838
|
+
"core.memory_artifact.v1": ("reference_only", "protected_retention", "timezone"),
|
|
839
|
+
"core.memory_generation_result.v1": ("result_reference_binding", "reuse_consistency"),
|
|
840
|
+
"core.operational_learning_event.v1": ("candidate_only", "no_self_authority", "timezone"),
|
|
841
|
+
"core.pattern_candidate.v1": ("support_threshold", "ambiguity_gate", "candidate_only"),
|
|
842
|
+
"core.physical_safety_assurance_case.v1": (
|
|
843
|
+
"bounded_claim",
|
|
844
|
+
"extreme_preservation",
|
|
845
|
+
"fail_closed_out_of_distribution",
|
|
846
|
+
"independent_physical_barriers",
|
|
847
|
+
"mandatory_adversarial_coverage",
|
|
848
|
+
"epistemic_dignity",
|
|
849
|
+
"immutable_critical_trace",
|
|
850
|
+
"no_deployment_authority",
|
|
851
|
+
),
|
|
852
|
+
"core.policy_lifecycle.v1": ("temporal_order", "closed_policy_end", "no_self_supersession"),
|
|
853
|
+
"core.retention_manifest.v1": ("unique_decision", "mutation_checksum", "restore_path"),
|
|
854
|
+
"core.reversibility_policy.v1": ("responsible_approval", "compensation_gate", "no_execution_authority"),
|
|
855
|
+
"core.state_transition.v1": ("actual_transition", "responsible_irreversible_actor", "timezone"),
|
|
856
|
+
"core.task_closeout.v1": ("bounded_status", "passed_requires_evidence"),
|
|
857
|
+
"core.template_promotion_candidate.v1": ("complete_candidate", "risk_approval", "no_auto_activation"),
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
|
|
861
|
+
def executable_contract_versions() -> tuple[str, ...]:
|
|
862
|
+
"""Return schema versions with registered semantic evaluators."""
|
|
863
|
+
|
|
864
|
+
return tuple(sorted(SEMANTIC_VALIDATORS))
|
|
865
|
+
|
|
866
|
+
|
|
867
|
+
def evaluate_contract_payload(payload: Any, *, strict: bool = True) -> dict[str, Any]:
|
|
868
|
+
"""Evaluate structure, invariants, evidence links, and authority limits."""
|
|
869
|
+
|
|
870
|
+
errors: list[Error] = []
|
|
871
|
+
warnings: list[Error] = []
|
|
872
|
+
details: dict[str, Any] = {}
|
|
873
|
+
version = payload.get("schema_version") if isinstance(payload, dict) else None
|
|
874
|
+
contract_name = _schema_version_map().get(version)
|
|
875
|
+
if not isinstance(payload, dict):
|
|
876
|
+
errors.append(error("invalid_artifact", "Contract artifact must be an object."))
|
|
877
|
+
elif contract_name is None:
|
|
878
|
+
errors.append(error("unknown_schema_version", f"Unknown schema_version: {version!r}.", "schema_version"))
|
|
879
|
+
else:
|
|
880
|
+
schema = load_contract_schema(contract_name)
|
|
881
|
+
structural_errors = _schema_errors(payload, schema)
|
|
882
|
+
if strict:
|
|
883
|
+
structural_errors.extend(_strict_shape_errors(payload, schema, schema))
|
|
884
|
+
errors.extend(structural_errors)
|
|
885
|
+
errors.extend(_reference_errors(payload))
|
|
886
|
+
errors.extend(_fingerprint_errors(payload))
|
|
887
|
+
semantic = SEMANTIC_VALIDATORS.get(str(version))
|
|
888
|
+
if semantic is None:
|
|
889
|
+
errors.append(error("semantic_evaluator_missing", "Contract has no executable semantic evaluator.", "schema_version"))
|
|
890
|
+
elif not structural_errors:
|
|
891
|
+
semantic_errors, semantic_warnings, details = semantic(payload)
|
|
892
|
+
errors.extend(semantic_errors)
|
|
893
|
+
warnings.extend(semantic_warnings)
|
|
894
|
+
|
|
895
|
+
status = "passed" if not errors else "failed"
|
|
896
|
+
knowledge_status = "bounded_artifact"
|
|
897
|
+
if isinstance(payload, dict) and version == "core.physical_safety_assurance_case.v1":
|
|
898
|
+
claim = payload.get("claim")
|
|
899
|
+
if isinstance(claim, dict):
|
|
900
|
+
knowledge_status = str(claim.get("claim_status", "bounded_artifact"))
|
|
901
|
+
report: dict[str, Any] = {
|
|
902
|
+
"schema": "core.contract_evaluation.v1",
|
|
903
|
+
"contract_schema": version,
|
|
904
|
+
"status": status,
|
|
905
|
+
"decision": "accepted" if status == "passed" else "rejected",
|
|
906
|
+
"authority": "validation_only",
|
|
907
|
+
"knowledge_status": knowledge_status,
|
|
908
|
+
"truth_claimed": False,
|
|
909
|
+
"execution_authorized": False,
|
|
910
|
+
"deployment_authorized": False,
|
|
911
|
+
"input_fingerprint": input_fingerprint(payload),
|
|
912
|
+
"evaluated_rules": list(SEMANTIC_RULE_IDS.get(str(version), ())),
|
|
913
|
+
"details": details,
|
|
914
|
+
"errors": errors,
|
|
915
|
+
"warnings": warnings,
|
|
916
|
+
}
|
|
917
|
+
report["report_fingerprint"] = f"sha256:{canonical_json_hash(report)}"
|
|
918
|
+
return report
|
|
919
|
+
|
|
920
|
+
|
|
921
|
+
def evaluate_contract_file(path: Path, *, strict: bool = True) -> dict[str, Any]:
|
|
922
|
+
"""Read and evaluate one JSON contract artifact."""
|
|
923
|
+
|
|
924
|
+
import json
|
|
925
|
+
|
|
926
|
+
try:
|
|
927
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
928
|
+
except FileNotFoundError:
|
|
929
|
+
return evaluate_contract_payload({"schema_version": "unknown", "read_error": "file_not_found"}, strict=strict)
|
|
930
|
+
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
931
|
+
result = evaluate_contract_payload({"schema_version": "unknown", "read_error": exc.__class__.__name__}, strict=strict)
|
|
932
|
+
result["errors"] = [error("invalid_json", "Contract file is not valid readable JSON.", "path")]
|
|
933
|
+
result["status"] = "failed"
|
|
934
|
+
result["decision"] = "rejected"
|
|
935
|
+
result["report_fingerprint"] = f"sha256:{canonical_json_hash({key: value for key, value in result.items() if key != 'report_fingerprint'})}"
|
|
936
|
+
return result
|
|
937
|
+
return evaluate_contract_payload(payload, strict=strict)
|
|
938
|
+
|
|
939
|
+
|
|
940
|
+
def bind_artifact_fingerprint(payload: Mapping[str, Any]) -> dict[str, Any]:
|
|
941
|
+
"""Return a deep-copied artifact with its canonical fingerprint bound."""
|
|
942
|
+
|
|
943
|
+
result = copy.deepcopy(dict(payload))
|
|
944
|
+
result["fingerprint"] = artifact_fingerprint(result)
|
|
945
|
+
return result
|