sclite-core 0.2.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.
Files changed (64) hide show
  1. sclite/__init__.py +83 -0
  2. sclite/artifacts.py +539 -0
  3. sclite/cli.py +224 -0
  4. sclite/examples/__init__.py +0 -0
  5. sclite/examples/contract-lifecycle-v0.2/README.md +14 -0
  6. sclite/examples/contract-lifecycle-v0.2/artifact_chain_manifest.json +114 -0
  7. sclite/examples/contract-lifecycle-v0.2/evidence_contract.json +54 -0
  8. sclite/examples/contract-lifecycle-v0.2/execution_contract.json +67 -0
  9. sclite/examples/contract-lifecycle-v0.2/execution_receipt.json +62 -0
  10. sclite/examples/contract-lifecycle-v0.2/execution_ticket.json +72 -0
  11. sclite/examples/contract-lifecycle-v0.2/intent_contract.json +38 -0
  12. sclite/examples/contract-lifecycle-v0.2/policy_decision.json +46 -0
  13. sclite/examples/prepared-execution-spec/README.md +3 -0
  14. sclite/examples/prepared-execution-spec/prepared_execution_spec.json +69 -0
  15. sclite/examples/public-snapshot-manifest/README.md +3 -0
  16. sclite/examples/public-snapshot-manifest/public_snapshot_manifest.json +85 -0
  17. sclite/examples/public-validation-surface-index/README.md +3 -0
  18. sclite/examples/public-validation-surface-index/public_validation_surface_index.json +65 -0
  19. sclite/examples/redaction-policy/README.md +3 -0
  20. sclite/examples/redaction-policy/redaction_policy.json +66 -0
  21. sclite/examples/redaction-receipt/README.md +3 -0
  22. sclite/examples/redaction-receipt/redaction_receipt.json +35 -0
  23. sclite/examples/scope-fidelity-report/README.md +11 -0
  24. sclite/examples/scope-fidelity-report/scope_fidelity_report.json +39 -0
  25. sclite/examples/security-contract-proof/README.md +25 -0
  26. sclite/examples/security-contract-proof/approved_execution_spec.json +107 -0
  27. sclite/examples/security-contract-proof/evidence_bundle.json +65 -0
  28. sclite/examples/security-contract-proof/evidence_summary.md +22 -0
  29. sclite/examples/security-contract-proof/execution_receipt.json +22 -0
  30. sclite/examples/security-contract-proof/policy_decision.json +29 -0
  31. sclite/examples/security-contract-proof/prepared_execution_spec.redacted.json +86 -0
  32. sclite/hosts.py +56 -0
  33. sclite/integrity/__init__.py +19 -0
  34. sclite/integrity/chain.py +263 -0
  35. sclite/redaction.py +216 -0
  36. sclite/schemas/__init__.py +0 -0
  37. sclite/schemas/approved_execution_spec.v0.1.schema.json +290 -0
  38. sclite/schemas/artifact_chain_manifest.v0.2.schema.json +35 -0
  39. sclite/schemas/evidence_bundle.v0.1.schema.json +137 -0
  40. sclite/schemas/evidence_contract.v0.2.schema.json +67 -0
  41. sclite/schemas/execution_contract.v0.2.schema.json +56 -0
  42. sclite/schemas/execution_receipt.v0.1.schema.json +112 -0
  43. sclite/schemas/execution_receipt.v0.2.schema.json +20 -0
  44. sclite/schemas/execution_ticket.v0.2.schema.json +89 -0
  45. sclite/schemas/intent_contract.v0.2.schema.json +21 -0
  46. sclite/schemas/policy_decision.v0.1.schema.json +90 -0
  47. sclite/schemas/policy_decision.v0.2.schema.json +21 -0
  48. sclite/schemas/prepared_execution_spec.v0.1.schema.json +180 -0
  49. sclite/schemas/public_snapshot_manifest.v0.1.schema.json +146 -0
  50. sclite/schemas/public_validation_surface_index.v0.1.schema.json +125 -0
  51. sclite/schemas/redacted_prepared_execution_spec.v0.1.schema.json +245 -0
  52. sclite/schemas/redaction_policy.v0.1.schema.json +103 -0
  53. sclite/schemas/redaction_receipt.v0.1.schema.json +145 -0
  54. sclite/schemas/scope_fidelity_report.v0.1.schema.json +111 -0
  55. sclite/schemas/security_contract_validation_receipt.v0.1.schema.json +156 -0
  56. sclite/scope_fidelity.py +168 -0
  57. sclite/surfaces.py +148 -0
  58. sclite/validation.py +223 -0
  59. sclite_core-0.2.1.dist-info/METADATA +266 -0
  60. sclite_core-0.2.1.dist-info/RECORD +64 -0
  61. sclite_core-0.2.1.dist-info/WHEEL +5 -0
  62. sclite_core-0.2.1.dist-info/entry_points.txt +3 -0
  63. sclite_core-0.2.1.dist-info/licenses/LICENSE +21 -0
  64. sclite_core-0.2.1.dist-info/top_level.txt +1 -0
sclite/__init__.py ADDED
@@ -0,0 +1,83 @@
1
+ from __future__ import annotations
2
+
3
+ __version__ = '0.2.1'
4
+
5
+ from .artifacts import (
6
+ APPROVED_EXECUTION_SPEC_FILE,
7
+ APPROVED_EXECUTION_SPEC_VERSION,
8
+ ARTIFACT_CANONICALIZATION_VERSION,
9
+ ARTIFACT_HASH_ALGORITHM,
10
+ DEMO_PROOF_MODE,
11
+ EVIDENCE_BUNDLE_ARTIFACT_TYPE,
12
+ EVIDENCE_BUNDLE_FILE,
13
+ EVIDENCE_BUNDLE_SCHEMA_VERSION,
14
+ EVIDENCE_SUMMARY_FILE,
15
+ EXECUTION_RECEIPT_ARTIFACT_TYPE,
16
+ EXECUTION_RECEIPT_FILE,
17
+ POLICY_DECISION_FILE,
18
+ POLICY_DECISION_SCHEMA_VERSION,
19
+ PROOF_TRACE_FILES,
20
+ PUBLIC_DEMO_NON_CLAIMS,
21
+ PUBLIC_DEMO_TARGET_HOST,
22
+ REDACTED_PREPARED_EXECUTION_SPEC_FILE,
23
+ JsonSchemaValidationError,
24
+ ProofTraceInvariantError,
25
+ assert_public_proof_trace_artifacts,
26
+ build_demo_success_criteria,
27
+ build_evidence_bundle_artifact,
28
+ build_evidence_summary_markdown,
29
+ build_artifact_hash,
30
+ build_execution_receipt_artifact,
31
+ build_proof_trace_artifacts,
32
+ canonical_artifact_bytes,
33
+ canonicalize_artifact,
34
+ examples_dir,
35
+ load_json_schema,
36
+ proof_trace_manifest,
37
+ repo_root,
38
+ schema_dir,
39
+ artifact_sha256,
40
+ validate_artifact,
41
+ validate_json_schema_value,
42
+ validate_public_proof_trace_artifacts,
43
+ validate_schema_ref,
44
+ validate_trace,
45
+ )
46
+ from .integrity import (
47
+ CHAIN_CANONICALIZATION_VERSION,
48
+ CHAIN_HASH_ALGORITHM,
49
+ ChainVerificationError,
50
+ artifact_descriptor,
51
+ build_artifact_chain_manifest,
52
+ verify_artifact_chain_manifest,
53
+ )
54
+ from .redaction import (
55
+ REDACTION_POLICY_ARTIFACT_TYPE,
56
+ REDACTION_POLICY_SCHEMA_VERSION,
57
+ REDACTION_RECEIPT_ARTIFACT_TYPE,
58
+ REDACTION_RECEIPT_SCHEMA_VERSION,
59
+ build_default_redaction_policy,
60
+ build_redaction_receipt,
61
+ redact_prepared_spec,
62
+ sanitize_public_artifact,
63
+ )
64
+ from .scope_fidelity import (
65
+ SCOPE_FIDELITY_ARTIFACT_TYPE,
66
+ SCOPE_FIDELITY_SCHEMA_REF,
67
+ SCOPE_FIDELITY_SCHEMA_VERSION,
68
+ build_scope_fidelity_report,
69
+ build_scope_fidelity_report_from_approved_spec,
70
+ summarize_scope_fidelity,
71
+ validate_scope_fidelity_report,
72
+ )
73
+ from .surfaces import (
74
+ PUBLIC_SNAPSHOT_MANIFEST_ARTIFACT_TYPE,
75
+ PUBLIC_SNAPSHOT_MANIFEST_SCHEMA_VERSION,
76
+ PUBLIC_VALIDATION_SURFACE_INDEX_ARTIFACT_TYPE,
77
+ PUBLIC_VALIDATION_SURFACE_INDEX_SCHEMA_VERSION,
78
+ build_public_snapshot_manifest,
79
+ build_public_validation_surface_index,
80
+ )
81
+ from .validation import build_validation_receipt, validate_fixture_dir
82
+
83
+ __all__ = [name for name in globals() if not name.startswith('_')]
sclite/artifacts.py ADDED
@@ -0,0 +1,539 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import json
5
+ from pathlib import Path
6
+ from typing import Any, Dict, List, Mapping
7
+
8
+ from .redaction import sanitize_public_artifact
9
+
10
+
11
+ POLICY_DECISION_SCHEMA_VERSION = '2026-04-27.policy-decision.v0.1'
12
+ PREPARED_EXECUTION_SPEC_VERSION = '2026-03-18.prepared.v1'
13
+ PREPARED_EXECUTION_SPEC_ARTIFACT_TYPE = 'prepared_execution_spec'
14
+ REDACTED_PREPARED_EXECUTION_SPEC_ARTIFACT_TYPE = 'redacted_prepared_execution_spec'
15
+ APPROVED_EXECUTION_SPEC_VERSION = '2026-03-18.approved.v1'
16
+ EXECUTION_RECEIPT_ARTIFACT_TYPE = 'execution_receipt'
17
+ EVIDENCE_BUNDLE_SCHEMA_VERSION = '2026-04-28.evidence-bundle.v0.1'
18
+ EVIDENCE_BUNDLE_ARTIFACT_TYPE = 'evidence_bundle'
19
+ DEMO_PROOF_MODE = 'dry_run_contract_proof'
20
+ PUBLIC_DEMO_TARGET_HOST = 'example.com'
21
+ ARTIFACT_CANONICALIZATION_VERSION = 'sclite-json-v0.1'
22
+ ARTIFACT_HASH_ALGORITHM = 'sha256'
23
+
24
+ POLICY_DECISION_FILE = 'policy_decision.json'
25
+ REDACTED_PREPARED_EXECUTION_SPEC_FILE = 'prepared_execution_spec.redacted.json'
26
+ APPROVED_EXECUTION_SPEC_FILE = 'approved_execution_spec.json'
27
+ EXECUTION_RECEIPT_FILE = 'execution_receipt.json'
28
+ EVIDENCE_BUNDLE_FILE = 'evidence_bundle.json'
29
+ EVIDENCE_SUMMARY_FILE = 'evidence_summary.md'
30
+
31
+ PUBLIC_DEMO_NON_CLAIMS = [
32
+ 'does_not_claim_live_vulnerability_evidence',
33
+ 'does_not_execute_against_live_private_targets',
34
+ 'does_not_include_raw_stdout_stderr_or_private_paths',
35
+ ]
36
+
37
+ PROOF_TRACE_FILES = [
38
+ POLICY_DECISION_FILE,
39
+ REDACTED_PREPARED_EXECUTION_SPEC_FILE,
40
+ APPROVED_EXECUTION_SPEC_FILE,
41
+ EXECUTION_RECEIPT_FILE,
42
+ EVIDENCE_BUNDLE_FILE,
43
+ EVIDENCE_SUMMARY_FILE,
44
+ ]
45
+
46
+ SCHEMA_FILES = {
47
+ 'policy_decision.v0.1': 'policy_decision.v0.1.schema.json',
48
+ 'prepared_execution_spec.v0.1': 'prepared_execution_spec.v0.1.schema.json',
49
+ 'redacted_prepared_execution_spec.v0.1': 'redacted_prepared_execution_spec.v0.1.schema.json',
50
+ 'approved_execution_spec.v0.1': 'approved_execution_spec.v0.1.schema.json',
51
+ 'execution_receipt.v0.1': 'execution_receipt.v0.1.schema.json',
52
+ 'evidence_bundle.v0.1': 'evidence_bundle.v0.1.schema.json',
53
+ 'scope_fidelity_report.v0.1': 'scope_fidelity_report.v0.1.schema.json',
54
+ 'security_contract_validation_receipt.v0.1': 'security_contract_validation_receipt.v0.1.schema.json',
55
+ 'redaction_policy.v0.1': 'redaction_policy.v0.1.schema.json',
56
+ 'redaction_receipt.v0.1': 'redaction_receipt.v0.1.schema.json',
57
+ 'public_validation_surface_index.v0.1': 'public_validation_surface_index.v0.1.schema.json',
58
+ 'public_snapshot_manifest.v0.1': 'public_snapshot_manifest.v0.1.schema.json',
59
+ 'intent_contract.v0.2': 'intent_contract.v0.2.schema.json',
60
+ 'policy_decision.v0.2': 'policy_decision.v0.2.schema.json',
61
+ 'execution_contract.v0.2': 'execution_contract.v0.2.schema.json',
62
+ 'execution_ticket.v0.2': 'execution_ticket.v0.2.schema.json',
63
+ 'execution_receipt.v0.2': 'execution_receipt.v0.2.schema.json',
64
+ 'evidence_contract.v0.2': 'evidence_contract.v0.2.schema.json',
65
+ 'artifact_chain_manifest.v0.2': 'artifact_chain_manifest.v0.2.schema.json',
66
+ }
67
+
68
+
69
+ class ProofTraceInvariantError(ValueError):
70
+ pass
71
+
72
+
73
+ class JsonSchemaValidationError(AssertionError):
74
+ pass
75
+
76
+
77
+ def repo_root() -> Path:
78
+ return Path(__file__).resolve().parents[1]
79
+
80
+
81
+ def package_root() -> Path:
82
+ return Path(__file__).resolve().parent
83
+
84
+
85
+ def schema_dir() -> Path:
86
+ return package_root() / 'schemas'
87
+
88
+
89
+ def examples_dir() -> Path:
90
+ return package_root() / 'examples'
91
+
92
+
93
+ def _json_type_name(value: Any) -> str:
94
+ if isinstance(value, bool):
95
+ return 'boolean'
96
+ if isinstance(value, dict):
97
+ return 'object'
98
+ if isinstance(value, list):
99
+ return 'array'
100
+ if isinstance(value, int) and not isinstance(value, bool):
101
+ return 'integer'
102
+ if isinstance(value, float):
103
+ return 'number'
104
+ if isinstance(value, str):
105
+ return 'string'
106
+ if value is None:
107
+ return 'null'
108
+ return type(value).__name__
109
+
110
+
111
+ def _assert_json_schema_type(value: Any, expected: Any, path: str) -> None:
112
+ expected_types = expected if isinstance(expected, list) else [expected]
113
+ actual = _json_type_name(value)
114
+ if actual == 'integer' and 'number' in expected_types:
115
+ return
116
+ if actual not in expected_types:
117
+ raise JsonSchemaValidationError(f'{path}: expected {expected_types}, got {actual}')
118
+
119
+
120
+ def validate_json_schema_value(schema: Mapping[str, Any], value: Any, path: str = '$') -> None:
121
+ """Validate the JSON Schema subset used by SCL v0.1 artifacts."""
122
+ if 'const' in schema and value != schema['const']:
123
+ raise JsonSchemaValidationError(f'{path}: expected const {schema["const"]!r}, got {value!r}')
124
+ if 'enum' in schema and value not in schema['enum']:
125
+ raise JsonSchemaValidationError(f'{path}: expected one of {schema["enum"]!r}, got {value!r}')
126
+ if 'type' in schema:
127
+ _assert_json_schema_type(value, schema['type'], path)
128
+ if isinstance(value, str) and 'minLength' in schema and len(value) < int(schema['minLength']):
129
+ raise JsonSchemaValidationError(f'{path}: expected minLength {schema["minLength"]}')
130
+ if isinstance(value, (int, float)) and not isinstance(value, bool) and 'minimum' in schema and value < float(schema['minimum']):
131
+ raise JsonSchemaValidationError(f'{path}: expected minimum {schema["minimum"]}')
132
+ if schema.get('type') == 'object':
133
+ if not isinstance(value, dict):
134
+ raise JsonSchemaValidationError(f'{path}: expected object')
135
+ for key in schema.get('required', []):
136
+ if key not in value:
137
+ raise JsonSchemaValidationError(f'{path}: missing required field {key!r}')
138
+ properties = schema.get('properties') if isinstance(schema.get('properties'), dict) else {}
139
+ for key, subschema in properties.items():
140
+ if key in value and isinstance(subschema, dict):
141
+ validate_json_schema_value(subschema, value[key], f'{path}.{key}')
142
+ if schema.get('additionalProperties') is False:
143
+ extra = sorted(set(value) - set(properties))
144
+ if extra:
145
+ raise JsonSchemaValidationError(f'{path}: unexpected fields {extra!r}')
146
+ if schema.get('type') == 'array':
147
+ if not isinstance(value, list):
148
+ raise JsonSchemaValidationError(f'{path}: expected array')
149
+ item_schema = schema.get('items')
150
+ if isinstance(item_schema, dict):
151
+ for idx, item in enumerate(value):
152
+ validate_json_schema_value(item_schema, item, f'{path}[{idx}]')
153
+
154
+
155
+ def _resolve_schema_ref(schema_ref: str, *, root: Path | None = None) -> Path:
156
+ raw_ref = str(schema_ref or '')
157
+ candidates: List[Path] = []
158
+ if root is not None:
159
+ candidates.append(root / raw_ref)
160
+ candidates.append(repo_root() / raw_ref)
161
+ basename = Path(raw_ref).name
162
+ if raw_ref in SCHEMA_FILES:
163
+ basename = SCHEMA_FILES[raw_ref]
164
+ candidates.append(schema_dir() / basename)
165
+ for candidate in candidates:
166
+ if candidate.exists():
167
+ return candidate
168
+ return candidates[-1]
169
+
170
+
171
+ def load_json_schema(schema_ref: str, *, root: Path | None = None) -> Dict[str, Any]:
172
+ schema_path = _resolve_schema_ref(schema_ref, root=root)
173
+ value = json.loads(schema_path.read_text(encoding='utf-8'))
174
+ if not isinstance(value, dict):
175
+ raise JsonSchemaValidationError(f'{schema_ref}: schema root is not an object')
176
+ return value
177
+
178
+
179
+ def validate_schema_ref(schema_ref: str, value: Any, *, root: Path | None = None, path: str = '$') -> None:
180
+ validate_json_schema_value(load_json_schema(schema_ref, root=root), value, path=path)
181
+
182
+
183
+ def validate_artifact(value: Any, schema_name: str, *, root: Path | None = None) -> None:
184
+ """Validate one artifact against a named SCL schema."""
185
+ validate_schema_ref(schema_name, value, root=root)
186
+
187
+
188
+ def canonicalize_artifact(value: Any) -> str:
189
+ """Return deterministic compact JSON for a JSON-compatible artifact.
190
+
191
+ The v0.1 canonicalization is intentionally small: UTF-8 JSON with sorted
192
+ object keys, compact separators, preserved Unicode, and no NaN/Infinity.
193
+ It is a content-addressing helper, not a signature or tamper-proof proof.
194
+ """
195
+ return json.dumps(value, sort_keys=True, separators=(',', ':'), ensure_ascii=False, allow_nan=False)
196
+
197
+
198
+ def canonical_artifact_bytes(value: Any) -> bytes:
199
+ """Return UTF-8 bytes for the v0.1 canonical JSON representation."""
200
+ return canonicalize_artifact(value).encode('utf-8')
201
+
202
+
203
+ def artifact_sha256(value: Any) -> str:
204
+ """Return SHA-256 hex digest over v0.1 canonical artifact bytes."""
205
+ return hashlib.sha256(canonical_artifact_bytes(value)).hexdigest()
206
+
207
+
208
+ def build_artifact_hash(value: Any) -> Dict[str, Any]:
209
+ """Return a public-safe hash descriptor for one JSON-compatible artifact."""
210
+ canonical = canonical_artifact_bytes(value)
211
+ return {
212
+ 'canonicalization': ARTIFACT_CANONICALIZATION_VERSION,
213
+ 'algorithm': ARTIFACT_HASH_ALGORITHM,
214
+ 'digest': hashlib.sha256(canonical).hexdigest(),
215
+ 'canonical_bytes': len(canonical),
216
+ }
217
+
218
+
219
+ def build_execution_receipt_artifact(pipeline_data: Dict[str, Any]) -> Dict[str, Any]:
220
+ engine = dict(pipeline_data.get('engine') or {}) if isinstance(pipeline_data.get('engine'), dict) else {}
221
+ return sanitize_public_artifact({
222
+ 'artifact_type': EXECUTION_RECEIPT_ARTIFACT_TYPE,
223
+ 'runtime_mode': str((pipeline_data.get('settings') or {}).get('runtime_mode') or ''),
224
+ 'status': str(engine.get('status') or ''),
225
+ 'returncode': int(engine.get('returncode', 0) or 0),
226
+ 'reason': str(engine.get('reason') or ''),
227
+ 'execution_source': str(engine.get('execution_source') or ''),
228
+ 'dry_run': str(engine.get('status') or '') == 'dry-run',
229
+ 'compiled_action': dict(engine.get('compiled_action') or {}) if isinstance(engine.get('compiled_action'), dict) else {},
230
+ 'command_input_summary': dict(engine.get('command_input_summary') or {}) if isinstance(engine.get('command_input_summary'), dict) else {},
231
+ 'planned_command_count': len(engine.get('planned_commands') or []) if isinstance(engine.get('planned_commands'), list) else 0,
232
+ 'executed_command_count': len(engine.get('executed_commands') or []) if isinstance(engine.get('executed_commands'), list) else 0,
233
+ 'stdout_present': bool(engine.get('stdout')),
234
+ 'stderr_present': bool(engine.get('stderr')),
235
+ })
236
+
237
+
238
+ def build_demo_success_criteria(pipeline_data: Dict[str, Any]) -> Dict[str, Any]:
239
+ """Return public-safe proof criteria for a dry-run SCL proof bundle."""
240
+ existing = pipeline_data.get('success_criteria')
241
+ if isinstance(existing, dict) and isinstance(existing.get('evidence'), list) and existing.get('evidence'):
242
+ return sanitize_public_artifact(existing)
243
+
244
+ runtime_mode = str((pipeline_data.get('settings') or {}).get('runtime_mode') or '')
245
+ engine = dict(pipeline_data.get('engine') or {}) if isinstance(pipeline_data.get('engine'), dict) else {}
246
+ policy_gate = dict(pipeline_data.get('policy_gate') or {}) if isinstance(pipeline_data.get('policy_gate'), dict) else {}
247
+ approved = pipeline_data.get('approved_execution_spec') if isinstance(pipeline_data.get('approved_execution_spec'), dict) else {}
248
+ prepared = pipeline_data.get('prepared_execution_spec') if isinstance(pipeline_data.get('prepared_execution_spec'), dict) else {}
249
+
250
+ criteria = [
251
+ {
252
+ 'id': 'demo_runtime_mode',
253
+ 'claim': 'Demo bundle was generated in demo mode.',
254
+ 'source': 'run_pipeline.demo.json',
255
+ 'status': 'met' if runtime_mode == 'demo' else 'not_met',
256
+ 'observed': runtime_mode,
257
+ },
258
+ {
259
+ 'id': 'policy_decision_recorded',
260
+ 'claim': 'Policy gate decision was captured as a contract artifact.',
261
+ 'source': POLICY_DECISION_FILE,
262
+ 'status': 'met' if policy_gate else 'not_met',
263
+ 'observed': str(policy_gate.get('reason') or ''),
264
+ },
265
+ {
266
+ 'id': 'prepared_spec_redacted',
267
+ 'claim': 'Prepared execution spec can be redacted for public/auditor review.',
268
+ 'source': REDACTED_PREPARED_EXECUTION_SPEC_FILE,
269
+ 'status': 'met' if prepared else 'not_met',
270
+ },
271
+ {
272
+ 'id': 'approved_spec_recorded',
273
+ 'claim': 'Approved execution spec was produced before executor handoff.',
274
+ 'source': APPROVED_EXECUTION_SPEC_FILE,
275
+ 'status': 'met' if approved else 'not_met',
276
+ 'observed': str((approved or {}).get('spec_version') or ''),
277
+ },
278
+ {
279
+ 'id': 'dry_run_receipt_recorded',
280
+ 'claim': 'Execution receipt records dry-run/mock execution instead of live offensive execution.',
281
+ 'source': EXECUTION_RECEIPT_FILE,
282
+ 'status': 'met' if str(engine.get('status') or '') == 'dry-run' else 'not_met',
283
+ 'observed': str(engine.get('status') or ''),
284
+ },
285
+ {
286
+ 'id': 'public_safe_target',
287
+ 'claim': 'Public demo target remains example.com/local-safe.',
288
+ 'source': APPROVED_EXECUTION_SPEC_FILE,
289
+ 'status': 'met' if str((approved or {}).get('target_host') or '') == PUBLIC_DEMO_TARGET_HOST else 'not_met',
290
+ 'observed': str((approved or {}).get('target_host') or ''),
291
+ },
292
+ ]
293
+ met = all(item.get('status') == 'met' for item in criteria)
294
+ return sanitize_public_artifact({
295
+ 'status': DEMO_PROOF_MODE,
296
+ 'met': met,
297
+ 'gap': 'live_target_evidence_not_collected_by_design',
298
+ 'evidence': criteria,
299
+ 'non_claims': list(PUBLIC_DEMO_NON_CLAIMS),
300
+ })
301
+
302
+
303
+ def build_evidence_bundle_artifact(pipeline_data: Dict[str, Any]) -> Dict[str, Any]:
304
+ success = build_demo_success_criteria(pipeline_data)
305
+ evidence = success.get('evidence') if isinstance(success.get('evidence'), list) else []
306
+ settings = dict(pipeline_data.get('settings') or {}) if isinstance(pipeline_data.get('settings'), dict) else {}
307
+ approved = dict(pipeline_data.get('approved_execution_spec') or {}) if isinstance(pipeline_data.get('approved_execution_spec'), dict) else {}
308
+ engine = dict(pipeline_data.get('engine') or {}) if isinstance(pipeline_data.get('engine'), dict) else {}
309
+ runtime_mode = str(settings.get('runtime_mode') or '')
310
+ target_host = str(approved.get('target_host') or '')
311
+ dry_run = str(engine.get('status') or '') == 'dry-run'
312
+ return sanitize_public_artifact({
313
+ 'schema_version': EVIDENCE_BUNDLE_SCHEMA_VERSION,
314
+ 'artifact_type': EVIDENCE_BUNDLE_ARTIFACT_TYPE,
315
+ 'proof_mode': DEMO_PROOF_MODE,
316
+ 'status': str(success.get('status') or ''),
317
+ 'met': bool(success.get('met', False)),
318
+ 'gap': str(success.get('gap') or ''),
319
+ 'evidence_items': len(evidence),
320
+ 'criteria': evidence,
321
+ 'non_claims': list(success.get('non_claims') or []) if isinstance(success.get('non_claims'), list) else [],
322
+ 'source_artifacts': {
323
+ 'policy_decision': POLICY_DECISION_FILE,
324
+ 'prepared_execution_spec': REDACTED_PREPARED_EXECUTION_SPEC_FILE,
325
+ 'approved_execution_spec': APPROVED_EXECUTION_SPEC_FILE,
326
+ 'execution_receipt': EXECUTION_RECEIPT_FILE,
327
+ 'evidence_summary': EVIDENCE_SUMMARY_FILE,
328
+ },
329
+ 'public_safety': {
330
+ 'runtime_mode': runtime_mode,
331
+ 'target_host': target_host,
332
+ 'dry_run': dry_run,
333
+ 'raw_live_evidence_included': False,
334
+ 'raw_stdout_stderr_included': False,
335
+ },
336
+ })
337
+
338
+
339
+ def build_evidence_summary_markdown(pipeline_data: Dict[str, Any]) -> str:
340
+ bundle = build_evidence_bundle_artifact(pipeline_data)
341
+ evidence = bundle.get('criteria') if isinstance(bundle.get('criteria'), list) else []
342
+ lines = [
343
+ '# Ravenclaw Demo Evidence Summary',
344
+ '',
345
+ f"- final_status: `{pipeline_data.get('final_status', '')}`",
346
+ f"- reason_code: `{pipeline_data.get('reason_code', '')}`",
347
+ f"- success_status: `{bundle.get('status', 'not_provided')}`",
348
+ f"- success_met: `{bool(bundle.get('met', False))}`",
349
+ f"- evidence_items: `{len(evidence)}`",
350
+ '',
351
+ '## Evidence criteria',
352
+ '',
353
+ ]
354
+ gap = str(bundle.get('gap') or '').strip()
355
+ if gap:
356
+ lines.insert(6, f"- evidence_gap: `{gap}`")
357
+ for item in evidence:
358
+ if not isinstance(item, dict):
359
+ continue
360
+ observed = str(item.get('observed') or '').strip()
361
+ suffix = f" Observed: `{observed}`." if observed else ''
362
+ lines.append(f"- `{item.get('status', '')}` — {item.get('id', '')}: {item.get('claim', '')} Source: `{item.get('source', '')}`.{suffix}")
363
+ non_claims = bundle.get('non_claims') if isinstance(bundle.get('non_claims'), list) else []
364
+ if non_claims:
365
+ lines.extend(['', '## Non-claims', ''])
366
+ for item in non_claims:
367
+ lines.append(f"- `{item}`")
368
+ lines.extend([
369
+ '',
370
+ 'This public demo bundle is dry-run/local and intentionally does not include raw live-target evidence.',
371
+ ])
372
+ return '\n'.join(lines) + '\n'
373
+
374
+
375
+ def build_proof_trace_artifacts(
376
+ pipeline_data: Dict[str, Any],
377
+ *,
378
+ policy_decision_artifact: Dict[str, Any] | None = None,
379
+ redacted_prepared_execution_spec: Dict[str, Any] | None = None,
380
+ ) -> Dict[str, Any]:
381
+ """Build public-safe proof trace artifacts from already-prepared inputs.
382
+
383
+ Ravenclaw-specific adapters supply the policy decision and richer prepared
384
+ spec redaction. SCL then owns the artifact ordering and public-safety checks.
385
+ """
386
+ approved = dict(pipeline_data.get('approved_execution_spec') or {}) if isinstance(pipeline_data.get('approved_execution_spec'), dict) else {}
387
+ return {
388
+ POLICY_DECISION_FILE: sanitize_public_artifact(policy_decision_artifact or {}),
389
+ REDACTED_PREPARED_EXECUTION_SPEC_FILE: sanitize_public_artifact(redacted_prepared_execution_spec or {}),
390
+ APPROVED_EXECUTION_SPEC_FILE: sanitize_public_artifact(approved),
391
+ EXECUTION_RECEIPT_FILE: build_execution_receipt_artifact(pipeline_data),
392
+ EVIDENCE_BUNDLE_FILE: build_evidence_bundle_artifact(pipeline_data),
393
+ EVIDENCE_SUMMARY_FILE: build_evidence_summary_markdown(pipeline_data),
394
+ }
395
+
396
+
397
+ def proof_trace_manifest() -> Dict[str, Dict[str, str]]:
398
+ return {
399
+ POLICY_DECISION_FILE: {
400
+ 'kind': 'json',
401
+ 'schema': 'schemas/policy_decision.v0.1.schema.json',
402
+ 'schema_version': POLICY_DECISION_SCHEMA_VERSION,
403
+ },
404
+ REDACTED_PREPARED_EXECUTION_SPEC_FILE: {
405
+ 'kind': 'json',
406
+ 'schema': 'schemas/redacted_prepared_execution_spec.v0.1.schema.json',
407
+ 'schema_version': PREPARED_EXECUTION_SPEC_VERSION,
408
+ },
409
+ APPROVED_EXECUTION_SPEC_FILE: {
410
+ 'kind': 'json',
411
+ 'schema': 'schemas/approved_execution_spec.v0.1.schema.json',
412
+ 'schema_version': APPROVED_EXECUTION_SPEC_VERSION,
413
+ },
414
+ EXECUTION_RECEIPT_FILE: {
415
+ 'kind': 'json',
416
+ 'schema': 'schemas/execution_receipt.v0.1.schema.json',
417
+ 'artifact_type': EXECUTION_RECEIPT_ARTIFACT_TYPE,
418
+ },
419
+ EVIDENCE_BUNDLE_FILE: {
420
+ 'kind': 'json',
421
+ 'schema': 'schemas/evidence_bundle.v0.1.schema.json',
422
+ 'schema_version': EVIDENCE_BUNDLE_SCHEMA_VERSION,
423
+ },
424
+ EVIDENCE_SUMMARY_FILE: {
425
+ 'kind': 'markdown',
426
+ 'schema': '',
427
+ 'schema_version': '',
428
+ },
429
+ }
430
+
431
+
432
+ def _expect_dict(artifacts: Dict[str, Any], filename: str, errors: List[str]) -> Dict[str, Any]:
433
+ value = artifacts.get(filename)
434
+ if not isinstance(value, dict):
435
+ errors.append(f'{filename}:not_object')
436
+ return {}
437
+ return value
438
+
439
+
440
+ def validate_public_proof_trace_artifacts(artifacts: Dict[str, Any]) -> List[str]:
441
+ errors: List[str] = []
442
+ for filename in PROOF_TRACE_FILES:
443
+ if filename not in artifacts:
444
+ errors.append(f'{filename}:missing')
445
+
446
+ policy = _expect_dict(artifacts, POLICY_DECISION_FILE, errors)
447
+ if policy:
448
+ if policy.get('schema_version') != POLICY_DECISION_SCHEMA_VERSION:
449
+ errors.append(f'{POLICY_DECISION_FILE}:schema_version')
450
+ if policy.get('decision') not in {'allow_prepare', 'owner_approval_required', 'deny'}:
451
+ errors.append(f'{POLICY_DECISION_FILE}:decision')
452
+ if policy.get('redaction_required') is not True:
453
+ errors.append(f'{POLICY_DECISION_FILE}:redaction_required')
454
+
455
+ prepared = _expect_dict(artifacts, REDACTED_PREPARED_EXECUTION_SPEC_FILE, errors)
456
+ if prepared:
457
+ if prepared.get('artifact_type') != REDACTED_PREPARED_EXECUTION_SPEC_ARTIFACT_TYPE:
458
+ errors.append(f'{REDACTED_PREPARED_EXECUTION_SPEC_FILE}:artifact_type')
459
+ if prepared.get('spec_version') != PREPARED_EXECUTION_SPEC_VERSION:
460
+ errors.append(f'{REDACTED_PREPARED_EXECUTION_SPEC_FILE}:spec_version')
461
+ redaction = prepared.get('redaction') if isinstance(prepared.get('redaction'), dict) else {}
462
+ safety = prepared.get('public_safety') if isinstance(prepared.get('public_safety'), dict) else {}
463
+ if redaction.get('raw_stdout_stderr_included') is not False:
464
+ errors.append(f'{REDACTED_PREPARED_EXECUTION_SPEC_FILE}:raw_stdout_stderr_redaction')
465
+ if redaction.get('credentials_included') is not False:
466
+ errors.append(f'{REDACTED_PREPARED_EXECUTION_SPEC_FILE}:credentials_redaction')
467
+ if redaction.get('private_paths_included') is not False:
468
+ errors.append(f'{REDACTED_PREPARED_EXECUTION_SPEC_FILE}:private_paths_redaction')
469
+ if safety.get('live_target_execution') is not False:
470
+ errors.append(f'{REDACTED_PREPARED_EXECUTION_SPEC_FILE}:live_target_execution')
471
+ if safety.get('raw_live_evidence_included') is not False:
472
+ errors.append(f'{REDACTED_PREPARED_EXECUTION_SPEC_FILE}:raw_live_evidence')
473
+ if safety.get('raw_stdout_stderr_included') is not False:
474
+ errors.append(f'{REDACTED_PREPARED_EXECUTION_SPEC_FILE}:raw_stdout_stderr')
475
+ if any(secret in str(prepared) for secret in ('private-researcher-handle', 'session=abc', str(repo_root()))):
476
+ errors.append(f'{REDACTED_PREPARED_EXECUTION_SPEC_FILE}:public_sanitization')
477
+
478
+ approved = _expect_dict(artifacts, APPROVED_EXECUTION_SPEC_FILE, errors)
479
+ if approved:
480
+ if approved.get('spec_version') != APPROVED_EXECUTION_SPEC_VERSION:
481
+ errors.append(f'{APPROVED_EXECUTION_SPEC_FILE}:spec_version')
482
+ if str(approved.get('target_host') or '') != PUBLIC_DEMO_TARGET_HOST:
483
+ errors.append(f'{APPROVED_EXECUTION_SPEC_FILE}:public_target')
484
+
485
+ receipt = _expect_dict(artifacts, EXECUTION_RECEIPT_FILE, errors)
486
+ if receipt:
487
+ if receipt.get('artifact_type') != EXECUTION_RECEIPT_ARTIFACT_TYPE:
488
+ errors.append(f'{EXECUTION_RECEIPT_FILE}:artifact_type')
489
+ if receipt.get('dry_run') is not True:
490
+ errors.append(f'{EXECUTION_RECEIPT_FILE}:dry_run')
491
+ if 'stdout' in receipt or 'stderr' in receipt:
492
+ errors.append(f'{EXECUTION_RECEIPT_FILE}:raw_output_present')
493
+
494
+ bundle = _expect_dict(artifacts, EVIDENCE_BUNDLE_FILE, errors)
495
+ if bundle:
496
+ if bundle.get('schema_version') != EVIDENCE_BUNDLE_SCHEMA_VERSION:
497
+ errors.append(f'{EVIDENCE_BUNDLE_FILE}:schema_version')
498
+ if bundle.get('artifact_type') != EVIDENCE_BUNDLE_ARTIFACT_TYPE:
499
+ errors.append(f'{EVIDENCE_BUNDLE_FILE}:artifact_type')
500
+ if bundle.get('proof_mode') != DEMO_PROOF_MODE:
501
+ errors.append(f'{EVIDENCE_BUNDLE_FILE}:proof_mode')
502
+ criteria = bundle.get('criteria') if isinstance(bundle.get('criteria'), list) else []
503
+ evidence_items = bundle.get('evidence_items')
504
+ if not isinstance(evidence_items, int) or evidence_items != len(criteria):
505
+ errors.append(f'{EVIDENCE_BUNDLE_FILE}:evidence_items_mismatch')
506
+ safety = bundle.get('public_safety') if isinstance(bundle.get('public_safety'), dict) else {}
507
+ if safety.get('runtime_mode') != 'demo':
508
+ errors.append(f'{EVIDENCE_BUNDLE_FILE}:runtime_mode')
509
+ if safety.get('target_host') != PUBLIC_DEMO_TARGET_HOST:
510
+ errors.append(f'{EVIDENCE_BUNDLE_FILE}:target_host')
511
+ if safety.get('dry_run') is not True:
512
+ errors.append(f'{EVIDENCE_BUNDLE_FILE}:dry_run')
513
+ if safety.get('raw_live_evidence_included') is not False:
514
+ errors.append(f'{EVIDENCE_BUNDLE_FILE}:raw_live_evidence')
515
+ if safety.get('raw_stdout_stderr_included') is not False:
516
+ errors.append(f'{EVIDENCE_BUNDLE_FILE}:raw_stdout_stderr')
517
+ non_claims = set(str(item) for item in (bundle.get('non_claims') or []))
518
+ for item in PUBLIC_DEMO_NON_CLAIMS:
519
+ if item not in non_claims:
520
+ errors.append(f'{EVIDENCE_BUNDLE_FILE}:missing_non_claim:{item}')
521
+
522
+ summary = artifacts.get(EVIDENCE_SUMMARY_FILE)
523
+ if not isinstance(summary, str):
524
+ errors.append(f'{EVIDENCE_SUMMARY_FILE}:not_markdown')
525
+ elif 'does_not_claim_live_vulnerability_evidence' not in summary:
526
+ errors.append(f'{EVIDENCE_SUMMARY_FILE}:missing_non_claims')
527
+ return errors
528
+
529
+
530
+ def assert_public_proof_trace_artifacts(artifacts: Dict[str, Any]) -> None:
531
+ errors = validate_public_proof_trace_artifacts(artifacts)
532
+ if errors:
533
+ raise ProofTraceInvariantError(';'.join(errors))
534
+
535
+
536
+ def validate_trace(path: str | Path) -> List[str]:
537
+ from .validation import validate_fixture_dir
538
+
539
+ return validate_fixture_dir(Path(path))