actenon-kernel 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (269) hide show
  1. actenon/__init__.py +11 -0
  2. actenon/adapters/__init__.py +20 -0
  3. actenon/adapters/_authorization.py +30 -0
  4. actenon/adapters/base.py +104 -0
  5. actenon/adapters/edge.py +129 -0
  6. actenon/adapters/fastapi.py +161 -0
  7. actenon/adapters/langchain.py +111 -0
  8. actenon/adapters/mcp.py +118 -0
  9. actenon/anchors/__init__.py +29 -0
  10. actenon/anchors/local_log.py +224 -0
  11. actenon/anchors/models.py +153 -0
  12. actenon/api/__init__.py +12 -0
  13. actenon/api/intake.py +39 -0
  14. actenon/api/invoice_payment.py +143 -0
  15. actenon/api/refund.py +57 -0
  16. actenon/cli.py +2023 -0
  17. actenon/conformance/__init__.py +5 -0
  18. actenon/conformance/helpers.py +136 -0
  19. actenon/conformance/manifest.py +185 -0
  20. actenon/conformance/test_artifact_shape_conformance.py +53 -0
  21. actenon/conformance/test_canonicalization_strict_conformance.py +288 -0
  22. actenon/conformance/test_countersignature_conformance.py +95 -0
  23. actenon/conformance/test_execution_state_conformance.py +78 -0
  24. actenon/conformance/test_outcome_attestation_conformance.py +109 -0
  25. actenon/conformance/test_replay_conformance.py +38 -0
  26. actenon/conformance/test_transparency_log_conformance.py +130 -0
  27. actenon/conformance/test_trust_artifacts_conformance.py +107 -0
  28. actenon/conformance/test_verifier_sdk_conformance.py +191 -0
  29. actenon/conformance/vectors/canonicalization_strict_v1/cases.json +142 -0
  30. actenon/conformance/vectors/canonicalization_strict_v1/profile.json +24 -0
  31. actenon/conformance/vectors/verifier_sdk_v1/README.md +18 -0
  32. actenon/conformance/vectors/verifier_sdk_v1/action_intent.json +41 -0
  33. actenon/conformance/vectors/verifier_sdk_v1/cases.json +285 -0
  34. actenon/conformance/vectors/verifier_sdk_v1/pccb.json +73 -0
  35. actenon/conformance/version.py +6 -0
  36. actenon/core/__init__.py +48 -0
  37. actenon/core/errors.py +111 -0
  38. actenon/core/json.py +68 -0
  39. actenon/core/kernel.py +195 -0
  40. actenon/core/redaction.py +21 -0
  41. actenon/coverage_matrix.py +903 -0
  42. actenon/credentials/__init__.py +9 -0
  43. actenon/credentials/broker.py +91 -0
  44. actenon/demo/__init__.py +2 -0
  45. actenon/demo/local_proof.py +652 -0
  46. actenon/demo/portable_local_proof.py +135 -0
  47. actenon/escrow/__init__.py +16 -0
  48. actenon/escrow/base.py +31 -0
  49. actenon/escrow/memory.py +65 -0
  50. actenon/escrow/service.py +25 -0
  51. actenon/escrow/sqlite.py +261 -0
  52. actenon/evidence/__init__.py +24 -0
  53. actenon/evidence/query.py +535 -0
  54. actenon/evidence/stores.py +117 -0
  55. actenon/execution/__init__.py +32 -0
  56. actenon/execution/mode_aware.py +404 -0
  57. actenon/execution/protected_executor.py +337 -0
  58. actenon/execution_graph.py +129 -0
  59. actenon/gate.py +620 -0
  60. actenon/idempotency.py +162 -0
  61. actenon/local_runtime.py +3412 -0
  62. actenon/local_runtime_server.py +960 -0
  63. actenon/models/__init__.py +125 -0
  64. actenon/models/contracts.py +866 -0
  65. actenon/models/evidence.py +15 -0
  66. actenon/models/execution.py +89 -0
  67. actenon/models/intent_record.py +334 -0
  68. actenon/models/invoice_payment.py +243 -0
  69. actenon/models/policy_bundle.py +147 -0
  70. actenon/models/runtime.py +73 -0
  71. actenon/models/serialization.py +46 -0
  72. actenon/outcomes.py +234 -0
  73. actenon/policy/__init__.py +65 -0
  74. actenon/policy/engine.py +252 -0
  75. actenon/policy/evidence.py +92 -0
  76. actenon/policy/invoice_payment.py +442 -0
  77. actenon/policy/refund.py +177 -0
  78. actenon/preflight/__init__.py +57 -0
  79. actenon/preflight/engine.py +208 -0
  80. actenon/preflight/evidence.py +135 -0
  81. actenon/preflight/models.py +129 -0
  82. actenon/preflight/policy_packs.py +1050 -0
  83. actenon/proof/__init__.py +131 -0
  84. actenon/proof/audit.py +87 -0
  85. actenon/proof/canonical.py +102 -0
  86. actenon/proof/local.py +19 -0
  87. actenon/proof/refusal_messages.py +40 -0
  88. actenon/proof/service.py +612 -0
  89. actenon/proof/signers/__init__.py +120 -0
  90. actenon/proof/signers/base.py +61 -0
  91. actenon/proof/signers/external_managed.py +252 -0
  92. actenon/proof/signers/hsm.py +63 -0
  93. actenon/proof/signers/kms.py +68 -0
  94. actenon/proof/signers/local.py +124 -0
  95. actenon/proof/signers/proof_seal.py +220 -0
  96. actenon/proof/signers/well_known.py +814 -0
  97. actenon/proof/signing.py +10 -0
  98. actenon/receipts/__init__.py +47 -0
  99. actenon/receipts/attestation.py +499 -0
  100. actenon/receipts/factory.py +211 -0
  101. actenon/receipts/invoice_payment.py +102 -0
  102. actenon/receipts/refund.py +60 -0
  103. actenon/receipts/store.py +110 -0
  104. actenon/receipts/writers.py +152 -0
  105. actenon/reconciliation/__init__.py +19 -0
  106. actenon/reconciliation/base.py +114 -0
  107. actenon/replay/__init__.py +20 -0
  108. actenon/replay/base.py +64 -0
  109. actenon/replay/dbapi.py +442 -0
  110. actenon/replay/postgres.py +55 -0
  111. actenon/replay/service.py +84 -0
  112. actenon/replay/sqlite.py +33 -0
  113. actenon/scanner.py +3264 -0
  114. actenon/ui/__init__.py +2 -0
  115. actenon/ui/trace_viewer/__init__.py +2 -0
  116. actenon/ui/trace_viewer/app.py +110 -0
  117. actenon/ui/trace_viewer/static/app.js +253 -0
  118. actenon/ui/trace_viewer/static/index.html +136 -0
  119. actenon/ui/trace_viewer/static/styles.css +288 -0
  120. actenon/ui/trace_viewer/trace_loader.py +875 -0
  121. actenon/verifier/__init__.py +56 -0
  122. actenon/verifier/countersignature.py +404 -0
  123. actenon/verifier/endpoint.py +271 -0
  124. actenon/verifier/middleware.py +96 -0
  125. actenon/verifier/sdk.py +104 -0
  126. actenon/verifier/transparency.py +778 -0
  127. actenon/verifier/trust_artifacts.py +570 -0
  128. actenon_kernel-0.1.0.dist-info/METADATA +644 -0
  129. actenon_kernel-0.1.0.dist-info/RECORD +269 -0
  130. actenon_kernel-0.1.0.dist-info/WHEEL +5 -0
  131. actenon_kernel-0.1.0.dist-info/entry_points.txt +4 -0
  132. actenon_kernel-0.1.0.dist-info/licenses/LICENSE +201 -0
  133. actenon_kernel-0.1.0.dist-info/top_level.txt +4 -0
  134. conformance/CHANGELOG.md +21 -0
  135. conformance/MATRIX.md +27 -0
  136. conformance/README.md +103 -0
  137. conformance/VERSION +1 -0
  138. conformance/__init__.py +1 -0
  139. conformance/suite.json +108 -0
  140. conformance/vector-lock.json +63 -0
  141. conformance/vectors/cloud_invoice_payment_v1/action_intent.json +74 -0
  142. conformance/vectors/cloud_invoice_payment_v1/issuer_keys.json +57 -0
  143. conformance/vectors/cloud_invoice_payment_v1/mutations/action_hash_changed_pccb.json +78 -0
  144. conformance/vectors/cloud_invoice_payment_v1/mutations/amount_changed_action_intent.json +74 -0
  145. conformance/vectors/cloud_invoice_payment_v1/mutations/audience_changed_pccb.json +78 -0
  146. conformance/vectors/cloud_invoice_payment_v1/mutations/expired_pccb.json +78 -0
  147. conformance/vectors/cloud_invoice_payment_v1/mutations/outcome_attestation_hard_revoked_issuer_keys.json +58 -0
  148. conformance/vectors/cloud_invoice_payment_v1/mutations/outcome_attestation_wrong_purpose_issuer_keys.json +57 -0
  149. conformance/vectors/cloud_invoice_payment_v1/mutations/receipt_attestation_artifact_digest_changed.json +55 -0
  150. conformance/vectors/cloud_invoice_payment_v1/mutations/receipt_attestation_issued_at_changed.json +55 -0
  151. conformance/vectors/cloud_invoice_payment_v1/mutations/receipt_attestation_issuer_changed.json +55 -0
  152. conformance/vectors/cloud_invoice_payment_v1/mutations/receipt_attestation_outcome_artifact_changed.json +55 -0
  153. conformance/vectors/cloud_invoice_payment_v1/mutations/receipt_attestation_proof_binding_changed.json +55 -0
  154. conformance/vectors/cloud_invoice_payment_v1/mutations/receipt_attestation_signature_changed.json +55 -0
  155. conformance/vectors/cloud_invoice_payment_v1/mutations/refusal_attestation_issued_at_changed.json +56 -0
  156. conformance/vectors/cloud_invoice_payment_v1/mutations/refusal_attestation_issuer_changed.json +56 -0
  157. conformance/vectors/cloud_invoice_payment_v1/mutations/refusal_attestation_signature_changed.json +56 -0
  158. conformance/vectors/cloud_invoice_payment_v1/mutations/signature_changed_pccb.json +78 -0
  159. conformance/vectors/cloud_invoice_payment_v1/mutations/wrong_kid_pccb.json +78 -0
  160. conformance/vectors/cloud_invoice_payment_v1/mutations/wrong_purpose_issuer_keys.json +57 -0
  161. conformance/vectors/cloud_invoice_payment_v1/pccb.json +78 -0
  162. conformance/vectors/cloud_invoice_payment_v1/receipt.json +19 -0
  163. conformance/vectors/cloud_invoice_payment_v1/receipt_attestation.json +55 -0
  164. conformance/vectors/cloud_invoice_payment_v1/refusal.json +20 -0
  165. conformance/vectors/cloud_invoice_payment_v1/refusal_attestation.json +56 -0
  166. conformance/vectors/receipt_countersignature_v1/countersignature.json +28 -0
  167. conformance/vectors/receipt_countersignature_v1/mutations/countersignature_altered_digest.json +28 -0
  168. conformance/vectors/receipt_countersignature_v1/mutations/countersignature_unknown_kid.json +28 -0
  169. conformance/vectors/receipt_countersignature_v1/mutations/trusted_keys_wrong_key.json +50 -0
  170. conformance/vectors/receipt_countersignature_v1/receipt.json +19 -0
  171. conformance/vectors/receipt_countersignature_v1/trusted_keys.json +50 -0
  172. conformance/vectors/transparency_log_v1/checkpoint_new.json +23 -0
  173. conformance/vectors/transparency_log_v1/checkpoint_old.json +23 -0
  174. conformance/vectors/transparency_log_v1/consistency_proof.json +13 -0
  175. conformance/vectors/transparency_log_v1/countersignature.json +27 -0
  176. conformance/vectors/transparency_log_v1/inclusion_proof.json +19 -0
  177. conformance/vectors/transparency_log_v1/leaf_digest.json +5 -0
  178. conformance/vectors/transparency_log_v1/mutations/checkpoint_new_forked.json +23 -0
  179. conformance/vectors/transparency_log_v1/mutations/checkpoint_unknown_kid.json +23 -0
  180. conformance/vectors/transparency_log_v1/mutations/countersignature_orphan.json +27 -0
  181. conformance/vectors/transparency_log_v1/trusted_keys.json +60 -0
  182. conformance/vectors/trust_artifacts_v1/README.md +13 -0
  183. conformance/vectors/trust_artifacts_v1/action_intent.json +74 -0
  184. conformance/vectors/trust_artifacts_v1/approval.json +25 -0
  185. conformance/vectors/trust_artifacts_v1/approval_trusted_keys.json +28 -0
  186. conformance/vectors/trust_artifacts_v1/issuer.json +4 -0
  187. conformance/vectors/trust_artifacts_v1/issuer_status_expired.json +24 -0
  188. conformance/vectors/trust_artifacts_v1/issuer_status_good.json +24 -0
  189. conformance/vectors/trust_artifacts_v1/issuer_status_revoked.json +24 -0
  190. conformance/vectors/trust_artifacts_v1/issuer_status_stale.json +24 -0
  191. conformance/vectors/trust_artifacts_v1/issuer_status_trusted_keys.json +28 -0
  192. conformance/vectors/trust_artifacts_v1/mutations/approval_action_changed.json +25 -0
  193. conformance/vectors/trust_artifacts_v1/mutations/approval_signature_changed.json +25 -0
  194. conformance/vectors/trust_artifacts_v1/mutations/issuer_status_signature_changed.json +24 -0
  195. examples/__init__.py +2 -0
  196. examples/adversarial_rsa_policy_stress_test.py +339 -0
  197. examples/claude_managed_agents_protected_tool/tool.py +456 -0
  198. examples/credential_broker_infra_delete/__init__.py +1 -0
  199. examples/credential_broker_infra_delete/demo.py +171 -0
  200. examples/crewai_protected_tool/tool.py +160 -0
  201. examples/edge_templates/__init__.py +2 -0
  202. examples/edge_templates/_support.py +66 -0
  203. examples/edge_templates/ci_cd.py +14 -0
  204. examples/edge_templates/cloud.py +14 -0
  205. examples/edge_templates/communications.py +14 -0
  206. examples/edge_templates/database.py +14 -0
  207. examples/edge_templates/http_api.py +14 -0
  208. examples/edge_templates/iam.py +14 -0
  209. examples/edge_templates/ot.py +14 -0
  210. examples/edge_templates/storage.py +14 -0
  211. examples/fastapi_protected_route/app.py +114 -0
  212. examples/fastapi_resource_boundary_refund/__init__.py +0 -0
  213. examples/fastapi_resource_boundary_refund/app.py +151 -0
  214. examples/fastapi_resource_boundary_refund/test_resource_boundary_refund.py +92 -0
  215. examples/fastapi_verifier_only_refund/__init__.py +0 -0
  216. examples/fastapi_verifier_only_refund/app.py +122 -0
  217. examples/fastapi_verifier_only_refund/issuer.py +47 -0
  218. examples/fastapi_verifier_only_refund/test_verifier_only_refund.py +58 -0
  219. examples/fastmcp_financial_transfer/__init__.py +1 -0
  220. examples/fastmcp_financial_transfer/mcp_server.py +88 -0
  221. examples/fastmcp_financial_transfer/test_fastmcp_financial_transfer.py +120 -0
  222. examples/financial_agent_protected_transfer/financial_agent.py +182 -0
  223. examples/financial_agent_protected_transfer/test_financial_agent.py +132 -0
  224. examples/hello_protected_endpoint/server.py +113 -0
  225. examples/hello_world_protected_resource_python/__init__.py +1 -0
  226. examples/hello_world_protected_resource_python/protected_resource.py +49 -0
  227. examples/integration_support.py +123 -0
  228. examples/interactive_execution_demo.py +139 -0
  229. examples/invoice_payment_guard_local/__init__.py +1 -0
  230. examples/invoice_payment_guard_local/protected_endpoint.py +376 -0
  231. examples/langchain_protected_tool/tool.py +123 -0
  232. examples/llamaindex_protected_tool/tool.py +142 -0
  233. examples/mcp_protected_tool/__init__.py +2 -0
  234. examples/mcp_protected_tool/demo.py +269 -0
  235. examples/mcp_server_protected_tool/__init__.py +2 -0
  236. examples/mcp_server_protected_tool/demo.py +66 -0
  237. examples/mcp_server_protected_tool/proof_gate.py +463 -0
  238. examples/mcp_server_protected_tool/server.py +208 -0
  239. examples/openai_agents_sdk_protected_tool/app.py +104 -0
  240. examples/protected_clinical_ehr_agent/protected_clinical_ehr_agent.py +274 -0
  241. examples/protected_clinical_ehr_agent/test_protected_clinical_ehr_agent.py +25 -0
  242. examples/protected_iam_control_plane/protected_iam_control_plane.py +273 -0
  243. examples/protected_iam_control_plane/test_protected_iam_control_plane.py +21 -0
  244. examples/protected_langchain_finance_agent/protected_langchain_finance_agent.py +421 -0
  245. examples/protected_langchain_finance_agent/test_protected_langchain_finance_agent.py +27 -0
  246. examples/protected_multi_agent_swarm/protected_multi_agent_swarm.py +233 -0
  247. examples/protected_multi_agent_swarm/test_protected_multi_agent_swarm.py +20 -0
  248. examples/protected_policy_preflight_refund/__init__.py +0 -0
  249. examples/protected_policy_preflight_refund/policy_preflight_refund.py +87 -0
  250. examples/protected_policy_preflight_refund/test_policy_preflight_refund.py +76 -0
  251. examples/quickstart_min.py +16 -0
  252. examples/refund_guard_local/__init__.py +2 -0
  253. examples/refund_guard_local/call_endpoint.py +113 -0
  254. examples/refund_guard_local/protected_endpoint.py +87 -0
  255. examples/refund_guard_local/server.py +560 -0
  256. examples/semantic_kernel_protected_tool/tool.py +141 -0
  257. schemas/__init__.py +1 -0
  258. schemas/action_intent.v1.json +256 -0
  259. schemas/approval_artifact.v1.json +62 -0
  260. schemas/issuer_status.v1.json +53 -0
  261. schemas/pccb.v1.json +336 -0
  262. schemas/receipt.v1.json +343 -0
  263. schemas/receipt_attestation.v2alpha1.json +237 -0
  264. schemas/receipt_countersignature.v1.json +150 -0
  265. schemas/refusal.v1.json +348 -0
  266. schemas/refusal_attestation.v2alpha1.json +237 -0
  267. schemas/transparency_checkpoint.v1.json +51 -0
  268. schemas/transparency_consistency_proof.v1.json +28 -0
  269. schemas/transparency_inclusion_proof.v1.json +42 -0
actenon/cli.py ADDED
@@ -0,0 +1,2023 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import sys
6
+ import unittest
7
+ from datetime import datetime, timezone
8
+ from io import StringIO
9
+ from pathlib import Path
10
+ from typing import Any, Sequence
11
+
12
+ from actenon.api.intake import ActionIntentIntakeService
13
+ from actenon.core import ContractValidationError, RefusalException
14
+ from actenon.core.json import loads_no_duplicate_keys
15
+ from actenon.coverage_matrix import DEFAULT_EVIDENCE_PATH, render_coverage_matrix_text, run_consequential_action_matrix
16
+ from actenon.evidence import (
17
+ EvidenceQuery,
18
+ EvidenceQueryService,
19
+ EvidenceResult,
20
+ EvidenceVerdict,
21
+ JsonArtifactActionIntentStore,
22
+ JsonArtifactPCCBStore,
23
+ )
24
+ from actenon.execution_graph import (
25
+ HttpExecutionGraphClient,
26
+ build_execution_anchor_hash,
27
+ create_execution_anchor_from_receipt,
28
+ create_execution_anchor_from_refusal,
29
+ )
30
+ from actenon.local_runtime import (
31
+ DEFAULT_LOCAL_RUNTIME_DIR,
32
+ bootstrap_local_runtime,
33
+ doctor_local_runtime,
34
+ export_local_runtime_bundle,
35
+ generate_local_hmac_key_material,
36
+ simulate_local_runtime,
37
+ verify_local_runtime_bundle,
38
+ )
39
+ from actenon.local_runtime_server import start_local_runtime_services
40
+ from actenon.models import ActionIntent, ActionSpec, PCCB, Receipt, Refusal, TargetRef, TenantRef
41
+ from actenon.models.contracts import AudienceRef, PartyRef, format_timestamp, parse_timestamp
42
+ from actenon.preflight import PreflightDecision, PreflightEngine
43
+ from actenon.proof import (
44
+ ALLOWED_DISCOVERY_KEY_STATUSES,
45
+ ALLOWED_DISCOVERY_KEY_USES,
46
+ LOCAL_PROOF_KEY_ID,
47
+ SignatureVerifier,
48
+ build_key_discovery_document,
49
+ build_local_proof_signer,
50
+ )
51
+ from actenon.receipts import (
52
+ JsonArtifactReceiptStore,
53
+ JsonArtifactRefusalStore,
54
+ OutcomeAttestationService,
55
+ OutcomeAttestationVerificationError,
56
+ ReceiptAttestationV2Alpha1,
57
+ RefusalAttestationV2Alpha1,
58
+ RefusalFactory,
59
+ )
60
+ from actenon.scanner import (
61
+ ScanReport,
62
+ ScannerOptions,
63
+ render_badge_markdown,
64
+ render_markdown_report,
65
+ scan_artifact_pair,
66
+ scan_local,
67
+ scan_replay_harness,
68
+ scan_repository,
69
+ )
70
+ from actenon.verifier import VerifierSDK
71
+
72
+
73
+ def _load_json(path: str) -> dict[str, Any]:
74
+ target = Path(path)
75
+ payload = loads_no_duplicate_keys(target.read_text(encoding="utf-8"))
76
+ if not isinstance(payload, dict):
77
+ raise ValueError("JSON artifact must be a JSON object")
78
+ return payload
79
+
80
+
81
+ def _load_intent(path: str) -> ActionIntent:
82
+ return ActionIntentIntakeService().parse(_load_json(path))
83
+
84
+
85
+ def _load_pccb(path: str) -> PCCB:
86
+ return PCCB.from_dict(_load_json(path))
87
+
88
+
89
+ def _load_receipt(path: str) -> Receipt:
90
+ return Receipt.from_dict(_load_json(path))
91
+
92
+
93
+ def _load_refusal(path: str) -> Refusal:
94
+ return Refusal.from_dict(_load_json(path))
95
+
96
+
97
+ def _load_preflight_decision(path: str) -> PreflightDecision:
98
+ return PreflightDecision.from_dict(_load_json(path))
99
+
100
+
101
+ def _load_receipt_attestation(path: str) -> ReceiptAttestationV2Alpha1:
102
+ return ReceiptAttestationV2Alpha1.from_dict(_load_json(path))
103
+
104
+
105
+ def _load_refusal_attestation(path: str) -> RefusalAttestationV2Alpha1:
106
+ return RefusalAttestationV2Alpha1.from_dict(_load_json(path))
107
+
108
+
109
+ def _write_json(path: str, payload: dict[str, Any]) -> Path:
110
+ output_path = Path(path).resolve()
111
+ output_path.parent.mkdir(parents=True, exist_ok=True)
112
+ output_path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
113
+ return output_path
114
+
115
+
116
+ def _resolve_verification_time(raw: str, pccb: PCCB) -> datetime:
117
+ if raw == "now":
118
+ return datetime.now(timezone.utc)
119
+ if raw == "pccb-issued-at":
120
+ return pccb.issued_at
121
+ if raw == "pccb-not-before":
122
+ return pccb.not_before
123
+ return parse_timestamp(raw, "verification_time")
124
+
125
+
126
+ def _resolve_signature_verifier(pccb: PCCB, verifier_name: str) -> SignatureVerifier:
127
+ if verifier_name == "local":
128
+ signature_verifier = build_local_proof_signer()
129
+ if pccb.signature.key_id != signature_verifier.key_id:
130
+ raise ValueError(
131
+ f"local verifier selected but proof key_id is {pccb.signature.key_id!r}, expected {signature_verifier.key_id!r}"
132
+ )
133
+ return signature_verifier
134
+ if verifier_name == "auto":
135
+ if pccb.signature.key_id == LOCAL_PROOF_KEY_ID:
136
+ return build_local_proof_signer()
137
+ raise ValueError(
138
+ "no local verifier is available for this proof key_id; use an environment-specific verifier outside the OSS CLI"
139
+ )
140
+ raise ValueError(f"unsupported verifier mode: {verifier_name}")
141
+
142
+
143
+ def _resolve_attestation_signer(signer_name: str):
144
+ if signer_name == "local":
145
+ return build_local_proof_signer()
146
+ raise ValueError(f"unsupported outcome-attestation signer mode: {signer_name}")
147
+
148
+
149
+ def _resolve_attestation_verifier(key_id: str, verifier_name: str) -> SignatureVerifier:
150
+ if verifier_name == "local":
151
+ signature_verifier = build_local_proof_signer()
152
+ if key_id != signature_verifier.key_id:
153
+ raise ValueError(
154
+ f"local verifier selected but attestation key_id is {key_id!r}, expected {signature_verifier.key_id!r}"
155
+ )
156
+ return signature_verifier
157
+ if verifier_name == "auto":
158
+ if key_id == LOCAL_PROOF_KEY_ID:
159
+ return build_local_proof_signer()
160
+ raise ValueError(
161
+ "no local verifier is available for this attestation key_id; use an environment-specific verifier outside the OSS CLI"
162
+ )
163
+ raise ValueError(f"unsupported outcome-attestation verifier mode: {verifier_name}")
164
+
165
+
166
+ def _resolve_audience(raw: str | None, *, audience_type: str) -> AudienceRef:
167
+ if raw is None:
168
+ raise ContractValidationError("audience is required for verifier context; use --audience '<type>:<id>' or a bare id with --audience-type.")
169
+ if ":" in raw:
170
+ parsed_type, parsed_id = raw.split(":", 1)
171
+ else:
172
+ parsed_type, parsed_id = audience_type, raw
173
+ if not parsed_type or not parsed_id:
174
+ raise ContractValidationError("audience must be '<type>:<id>' or a non-empty id with --audience-type.")
175
+ return AudienceRef(type=parsed_type, id=parsed_id)
176
+
177
+
178
+ def _resolve_party(raw: str, *, default_type: str = "service") -> PartyRef:
179
+ if ":" in raw:
180
+ parsed_type, parsed_id = raw.split(":", 1)
181
+ else:
182
+ parsed_type, parsed_id = default_type, raw
183
+ if not parsed_type or not parsed_id:
184
+ raise ContractValidationError("party reference must be '<type>:<id>' or a non-empty id.")
185
+ return PartyRef(type=parsed_type, id=parsed_id)
186
+
187
+
188
+ def _resolve_optional_timestamp(raw: str | None, field_name: str) -> datetime | None:
189
+ if raw is None or raw == "now":
190
+ return None
191
+ return parse_timestamp(raw, field_name)
192
+
193
+
194
+ def _build_cli_refusal(
195
+ *,
196
+ request_id: str,
197
+ occurred_at: datetime,
198
+ exc: RefusalException,
199
+ intent: ActionIntent | None,
200
+ context,
201
+ pccb: PCCB | None,
202
+ ) -> Refusal:
203
+ return RefusalFactory(refusal_id_factory=lambda: f"rfsl_{request_id}").create_from_exception(
204
+ exc,
205
+ occurred_at=occurred_at,
206
+ intent=intent,
207
+ context=context,
208
+ pccb_id=pccb.pccb_id if pccb is not None else None,
209
+ )
210
+
211
+
212
+ def _emit_json(payload: dict[str, Any]) -> None:
213
+ print(json.dumps(payload, indent=2, sort_keys=True), flush=True)
214
+
215
+
216
+ def _print_runtime_up_result(*, args: argparse.Namespace, payload: dict[str, Any]) -> None:
217
+ if args.json:
218
+ _emit_json({"ok": True, "mode": "bootstrap-only" if args.bootstrap_only else "serve", "runtime": payload})
219
+ return
220
+ if args.bootstrap_only:
221
+ print("Local runtime bootstrapped.")
222
+ print(f"Runtime root: {payload['runtime_root']}")
223
+ print(
224
+ f"Trust mode: {payload['trust_mode']['type']} "
225
+ f"({payload['trust_mode']['algorithm']}, key_id={payload['trust_mode']['key_id']})"
226
+ )
227
+ print(f"Local proof lab: {payload['paths']['local_proof']}")
228
+ print(f"Portable verifier lab: {payload['paths']['portable_local_proof']}")
229
+ print("Next commands:")
230
+ print(f"- actenon up --runtime-dir {payload['runtime_root']}")
231
+ print(f"- actenon doctor --runtime-dir {payload['runtime_root']}")
232
+ print(f"- actenon simulate --runtime-dir {payload['runtime_root']} --scenario all")
233
+ print(f"- actenon bundle export --runtime-dir {payload['runtime_root']}")
234
+ sys.stdout.flush()
235
+ return
236
+
237
+ print("Actenon local runtime is serving.")
238
+ print(f"Runtime root: {payload['runtime_root']}")
239
+ print(f"Issuer URL: {payload['issuer_url']}")
240
+ print(f"POST intents: {payload['intents_url']}")
241
+ print(f"Issuer id: {payload['issuer']['type']}:{payload['issuer']['id']}")
242
+ print("Supported capabilities: " + ", ".join(payload["supported_capabilities"]))
243
+ print(f"Health: {payload['health_url']}")
244
+ print(f"Key discovery: {payload['key_discovery_url']}")
245
+ print(f"Key discovery alias: {payload['key_discovery_alias_url']}")
246
+ print(f"Key publication file: {payload['key_discovery_document_path']}")
247
+ if payload["trace_viewer_url"] is not None:
248
+ print(f"Trace viewer: {payload['trace_viewer_url']}")
249
+ else:
250
+ print(f"Trace viewer: {payload['trace_viewer_status']}")
251
+ print(f"Artifact directory: {payload['artifact_dir']}")
252
+ print(f"Replay store: {payload['replay_store_path']}")
253
+ print(f"Escrow store: {payload['escrow_store_path']}")
254
+ print(f"Key discovery status: {'available' if payload['key_discovery_available'] else 'unavailable'}")
255
+ print(f"Key discovery note: {payload['key_discovery_summary']}")
256
+ print(f"Next step: {payload['next_step_example']}")
257
+ print("Press Ctrl-C to stop.")
258
+ sys.stdout.flush()
259
+
260
+
261
+ def _print_doctor_report(*, args: argparse.Namespace, report) -> None:
262
+ payload = report.to_dict()
263
+ if args.json:
264
+ _emit_json(payload)
265
+ return
266
+ print("Local runtime doctor.")
267
+ print(f"Runtime root: {payload['runtime_root']}")
268
+ print(f"Mode: {payload['mode']}")
269
+ print(f"Overall status: {payload['overall_status']}")
270
+ print(
271
+ "Summary: "
272
+ f"{payload['summary']['ok']} ok, "
273
+ f"{payload['summary']['fail']} fail, "
274
+ f"{payload['summary']['total']} total"
275
+ )
276
+ print("Checks:")
277
+ for check in payload["checks"]:
278
+ print(f"- [{check['status'].upper()}] {check['name']}")
279
+ print(f" {check['summary']}")
280
+ if check["details"]:
281
+ print(f" Details: {json.dumps(check['details'], sort_keys=True)}")
282
+ if check.get("remediation"):
283
+ print(f" Remediation: {check['remediation']}")
284
+ if payload["action_items"]:
285
+ print("Action items:")
286
+ for item in payload["action_items"]:
287
+ print(f"- {item['name']}: {item['remediation']}")
288
+
289
+
290
+ def _print_simulation_report(*, args: argparse.Namespace, report) -> None:
291
+ payload = report.to_dict()
292
+ if args.json:
293
+ _emit_json(payload)
294
+ return
295
+ print("Local incident simulation.")
296
+ print(f"Runtime root: {payload['runtime_root']}")
297
+ print(f"Mode: {payload['mode']}")
298
+ print(f"Scenario: {payload['scenario']}")
299
+ print(f"Succeeded: {'yes' if payload['succeeded'] else 'no'}")
300
+ if payload.get("framing_note"):
301
+ print(f"Framing: {payload['framing_note']}")
302
+ if payload["takeaways"]:
303
+ print("Takeaways:")
304
+ for item in payload["takeaways"]:
305
+ print(f"- {item}")
306
+ print("Results:")
307
+ for result in payload["results"]:
308
+ label = result.get("title") or result["name"]
309
+ print(f"- {label}: {result['status'].upper()}")
310
+ print(f" {result['summary']}")
311
+ if result.get("lesson"):
312
+ print(f" Lesson: {result['lesson']}")
313
+ print(f" Artifacts: {result['artifact_dir']}")
314
+ if result.get("summary_path") is not None:
315
+ print(f" Incident summary: {result['summary_path']}")
316
+ if result["refusal_code"] is not None:
317
+ print(f" Refusal code: {result['refusal_code']}")
318
+ if result["receipt_id"] is not None:
319
+ print(f" Receipt: {result['receipt_id']}")
320
+ if result.get("perspectives"):
321
+ print(" Perspectives:")
322
+ for perspective in result["perspectives"]:
323
+ print(
324
+ f" - {perspective['key']} [{perspective['basis']}]: "
325
+ f"{perspective['status'].upper()}"
326
+ )
327
+ print(f" {perspective['summary']}")
328
+ if result.get("details", {}).get("trace_viewer_follow_up"):
329
+ trace = result["details"]["trace_viewer_follow_up"]
330
+ print(f" Trace viewer: {trace['summary']}")
331
+ if trace.get("url"):
332
+ print(f" Trace viewer URL: {trace['url']}")
333
+
334
+
335
+ def _print_bundle_export_result(*, args: argparse.Namespace, payload: dict[str, Any]) -> None:
336
+ if args.json:
337
+ _emit_json(payload)
338
+ return
339
+ print("Portable execution evidence bundle exported.")
340
+ print(f"Output: {payload['output']}")
341
+ print(f"Kind: {payload['kind']}")
342
+ print(f"Class: {payload['bundle_manifest']['artifact_class']}")
343
+ print(f"Format: {payload['bundle_manifest']['format']}")
344
+ print(
345
+ "Summary: "
346
+ f"{payload['bundle_manifest']['summary']['proof_chain_count']} proof chain(s), "
347
+ f"{payload['bundle_manifest']['summary']['decision_record_count']} decision record(s), "
348
+ f"{payload['bundle_manifest']['summary']['file_count']} hashed file(s)"
349
+ )
350
+ print("Integrity:")
351
+ print("- portable execution evidence with manifest-linked file hashes")
352
+ print("- canonical artifact digests for Action Intent, PCCB, and Receipt or Refusal")
353
+ print("- not an attestation of origin in v1")
354
+ print("Next step:")
355
+ print(f"- actenon bundle verify {payload['output']}")
356
+
357
+
358
+ def _print_bundle_verify_result(*, args: argparse.Namespace, payload: dict[str, Any]) -> None:
359
+ if args.json:
360
+ _emit_json(payload)
361
+ return
362
+ if payload["ok"]:
363
+ print("Portable execution evidence bundle verified.")
364
+ else:
365
+ print("Portable execution evidence bundle verification failed.")
366
+ print(f"Bundle: {payload['bundle']}")
367
+ print(f"Kind: {payload['kind']}")
368
+ print(f"Format: {payload['format']}")
369
+ print(f"Class: {payload['artifact_class']}")
370
+ print(
371
+ "Summary: "
372
+ f"{payload['summary']['verified_file_count']}/{payload['summary']['declared_file_count']} hashed file(s) verified, "
373
+ f"{payload['summary']['verified_proof_chain_count']}/{payload['summary']['declared_proof_chain_count']} proof chain(s) verified, "
374
+ f"{payload['summary']['verified_decision_record_count']}/{payload['summary']['declared_decision_record_count']} decision record(s) verified"
375
+ )
376
+ print("Trust limits:")
377
+ print("- portable execution evidence and internal tamper checks are present")
378
+ print("- v1 bundles are not cryptographic attestations of origin")
379
+ if payload["errors"]:
380
+ print("Errors:")
381
+ for error in payload["errors"]:
382
+ print(f"- {error}")
383
+
384
+
385
+ def _print_keys_generate_result(*, args: argparse.Namespace, output_path: str, payload: dict[str, Any]) -> None:
386
+ if args.json:
387
+ _emit_json({"ok": True, "output": output_path, "key": payload})
388
+ return
389
+ print("Local key material written.")
390
+ print(f"Output: {output_path}")
391
+ print(f"Mode: {payload['mode']}")
392
+ print(f"Algorithm: {payload['algorithm']}")
393
+ print(f"Key id: {payload['key_id']}")
394
+ print("This key is for single-node local issuer/verifier use and is not publishable through key discovery.")
395
+
396
+
397
+ def _print_verify_proof_success(*, args: argparse.Namespace, verified, verification_time: datetime) -> None:
398
+ if args.json:
399
+ _emit_json(
400
+ {
401
+ "ok": True,
402
+ "intent_id": verified.intent.intent_id,
403
+ "pccb_id": verified.pccb.pccb_id,
404
+ "request_id": verified.context.request_id,
405
+ "audience": verified.context.audience.to_dict(),
406
+ "verified_at": format_timestamp(verification_time),
407
+ }
408
+ )
409
+ return
410
+ print("Proof verified.")
411
+ print(f"Intent: {verified.intent.intent_id}")
412
+ print(f"PCCB: {verified.pccb.pccb_id}")
413
+ print(f"Audience: {verified.context.audience.type}:{verified.context.audience.id}")
414
+ print(f"Verified at: {format_timestamp(verification_time)}")
415
+
416
+
417
+ def _print_verify_proof_failure(*, args: argparse.Namespace, refusal: Refusal) -> None:
418
+ if args.json:
419
+ _emit_json({"ok": False, "refusal": refusal.to_dict()})
420
+ return
421
+ print("Proof verification failed.")
422
+ print(f"Category: {refusal.category}")
423
+ print(f"Code: {refusal.reason_code}")
424
+ print(f"Message: {refusal.message}")
425
+ if refusal.intent_id is not None:
426
+ print(f"Intent: {refusal.intent_id}")
427
+ if refusal.correlation is not None and refusal.correlation.pccb_id is not None:
428
+ print(f"PCCB: {refusal.correlation.pccb_id}")
429
+ if refusal.audience is not None:
430
+ print(f"Audience: {refusal.audience.type}:{refusal.audience.id}")
431
+ print(f"Refused at: {format_timestamp(refusal.refused_at)}")
432
+ if refusal.details:
433
+ print("Details:")
434
+ print(json.dumps(refusal.details, indent=2, sort_keys=True))
435
+
436
+
437
+ def _print_attestation_written(
438
+ *,
439
+ args: argparse.Namespace,
440
+ artifact_kind: str,
441
+ artifact_id: str,
442
+ output_path: Path,
443
+ attestation: ReceiptAttestationV2Alpha1 | RefusalAttestationV2Alpha1,
444
+ ) -> None:
445
+ if args.json:
446
+ _emit_json(
447
+ {
448
+ "ok": True,
449
+ "artifact_kind": artifact_kind,
450
+ "artifact_id": artifact_id,
451
+ "attestation_id": attestation.attestation_id,
452
+ "output": str(output_path),
453
+ "signature": attestation.signature.to_dict(),
454
+ }
455
+ )
456
+ return
457
+ print(f"{artifact_kind.title()} attested.")
458
+ print(f"{artifact_kind.title()}: {artifact_id}")
459
+ print(f"Attestation: {attestation.attestation_id}")
460
+ print(f"Output: {output_path}")
461
+ print(f"Signature key: {attestation.signature.key_id}")
462
+
463
+
464
+ def _print_attestation_verification_success(
465
+ *,
466
+ args: argparse.Namespace,
467
+ artifact_kind: str,
468
+ artifact_id: str,
469
+ attestation: ReceiptAttestationV2Alpha1 | RefusalAttestationV2Alpha1,
470
+ ) -> None:
471
+ if args.json:
472
+ _emit_json(
473
+ {
474
+ "ok": True,
475
+ "artifact_kind": artifact_kind,
476
+ "artifact_id": artifact_id,
477
+ "attestation_id": attestation.attestation_id,
478
+ "signature": attestation.signature.to_dict(),
479
+ }
480
+ )
481
+ return
482
+ print(f"{artifact_kind.title()} attestation verified.")
483
+ print(f"{artifact_kind.title()}: {artifact_id}")
484
+ print(f"Attestation: {attestation.attestation_id}")
485
+ print(f"Signature key: {attestation.signature.key_id}")
486
+
487
+
488
+ def _print_attestation_verification_failure(
489
+ *,
490
+ args: argparse.Namespace,
491
+ artifact_kind: str,
492
+ attestation_id: str,
493
+ error: Exception,
494
+ ) -> None:
495
+ if args.json:
496
+ _emit_json(
497
+ {
498
+ "ok": False,
499
+ "artifact_kind": artifact_kind,
500
+ "attestation_id": attestation_id,
501
+ "error": str(error),
502
+ }
503
+ )
504
+ return
505
+ print(f"{artifact_kind.title()} attestation verification failed.")
506
+ print(f"Attestation: {attestation_id}")
507
+ print(f"Error: {error}")
508
+
509
+
510
+ def _print_scan_report(*, args: argparse.Namespace, report: ScanReport) -> None:
511
+ report_mode = getattr(args, "report_mode", "executive")
512
+ if getattr(args, "report_json", None):
513
+ Path(args.report_json).parent.mkdir(parents=True, exist_ok=True)
514
+ Path(args.report_json).write_text(json.dumps(report.to_dict(), indent=2, sort_keys=True) + "\n", encoding="utf-8")
515
+ if getattr(args, "report_markdown", None):
516
+ Path(args.report_markdown).parent.mkdir(parents=True, exist_ok=True)
517
+ Path(args.report_markdown).write_text(render_markdown_report(report, mode=report_mode), encoding="utf-8")
518
+ if getattr(args, "badge_output", None):
519
+ Path(args.badge_output).parent.mkdir(parents=True, exist_ok=True)
520
+ Path(args.badge_output).write_text(render_badge_markdown(report) + "\n", encoding="utf-8")
521
+
522
+ if args.json:
523
+ _emit_json(report.to_dict())
524
+ return
525
+ if getattr(args, "markdown", False):
526
+ print(render_markdown_report(report, mode=report_mode), end="")
527
+ return
528
+
529
+ print("Actenon Agentic Action Scan.")
530
+ print(f"Mode: {report.mode}")
531
+ print(f"Target: {report.target}")
532
+ print(f"Overall status: {report.overall_status}")
533
+ payload = report.to_dict()
534
+ print(f"Runtime-source candidate paths: {payload['runtime_source_candidate_paths']}")
535
+ print(f"Additional test/example/context findings: {payload['additional_test_example_context_findings']} (downgraded by context)")
536
+ print(f"Consequence class: {payload['consequence_class_label']}")
537
+ print(f"Gating status: {payload['gating_status']}")
538
+ print(f"Runtime reachability: {payload['runtime_reachability']}")
539
+ print(f"Vulnerability claim: {'yes' if payload['vulnerability_claim'] else 'no'}")
540
+ print(f"Manual review required: {'yes' if payload['manual_review_required'] else 'no'}")
541
+ print(f"Confidence: {payload['confidence']}")
542
+ print(f"Categories detected: {', '.join(payload['categories_detected']) or 'none'}")
543
+ print("Actenon Scanner maps agent authority. It does not accuse your repo of being vulnerable.")
544
+ print("This is not a vulnerability severity rating. It is a consequence-class map of candidate action surfaces found by static analysis.")
545
+ print("Runtime reachability, exploitability, production exposure and business impact are not proven by this scan.")
546
+ print(f"Scanner version: {report.scanner_version}")
547
+ print(f"Registry version: {report.registry_version}")
548
+ print(f"Summary: {report.summary}")
549
+ print("Checks:")
550
+ for check in report.checks:
551
+ print(f"- {check.label}: {check.status.upper()}")
552
+ print(f" {check.summary}")
553
+ if check.reason_code is not None:
554
+ print(f" Reason code: {check.reason_code}")
555
+ if report.findings:
556
+ print("Findings:")
557
+ for finding in report.findings:
558
+ location = ""
559
+ if finding.path is not None:
560
+ location = f" ({finding.path}"
561
+ if finding.line is not None:
562
+ location += f":{finding.line}"
563
+ location += ")"
564
+ finding_payload = finding.to_dict()
565
+ print(f"- [{finding_payload['consequence_class_label']}] {finding.title}{location}")
566
+ print(f" {finding.summary}")
567
+ print(
568
+ " "
569
+ f"Category: {finding.category}; "
570
+ f"surface: {finding.surface_id or finding.category}; "
571
+ f"path-type: {finding.path_type}; "
572
+ f"primitive: {finding.primitive or 'unknown'}; "
573
+ f"agent-control: {finding.agent_control_context}; "
574
+ f"confidence: {finding.confidence}; "
575
+ f"gating: {finding_payload['gating_status']}; "
576
+ f"runtime reachability: {finding_payload['runtime_reachability']}; "
577
+ "vulnerability claim: no"
578
+ )
579
+ if finding.control_gaps:
580
+ print(f" Control gaps: {', '.join(finding.control_gaps)}")
581
+ if report.remediation_steps:
582
+ print("Remediation:")
583
+ for step in report.remediation_steps:
584
+ print(f"- {step}")
585
+ if getattr(args, "badge", False):
586
+ print("Badge:")
587
+ print(render_badge_markdown(report))
588
+
589
+
590
+ def _print_preflight_decision(*, args: argparse.Namespace, decision: PreflightDecision) -> None:
591
+ if args.json:
592
+ _emit_json(decision.to_dict())
593
+ return
594
+ print("Actenon Preflight decision.")
595
+ print(f"Decision: {decision.decision_id}")
596
+ print(f"Outcome: {decision.outcome}")
597
+ print(f"Reason code: {decision.reason_code}")
598
+ print(f"Risk: {decision.risk_level}")
599
+ print(f"Summary: {decision.summary}")
600
+ if decision.required_evidence:
601
+ print("Required evidence:")
602
+ for item in decision.required_evidence:
603
+ print(f"- {item}")
604
+ if decision.required_approvals:
605
+ print("Required approvals:")
606
+ for item in decision.required_approvals:
607
+ print(f"- {item}")
608
+ if decision.unmet_requirements:
609
+ print("Unmet requirements:")
610
+ for requirement in decision.unmet_requirements:
611
+ print(f"- {requirement.reason_code}: {requirement.summary}")
612
+ for evidence_key in requirement.evidence_keys:
613
+ example = json.dumps(evidence_key.example, sort_keys=True)
614
+ print(
615
+ " "
616
+ f"evidence.{evidence_key.key} ({evidence_key.value_type}), "
617
+ f"example: {example}"
618
+ )
619
+ if decision.matched_rules:
620
+ print("Matched rules:")
621
+ for item in decision.matched_rules:
622
+ print(f"- {item}")
623
+
624
+
625
+ MCP_HERO_TOOL_CAPABILITIES: dict[str, str] = {
626
+ "filesystem.delete": "infrastructure.delete",
627
+ "database.migrate": "migration.apply",
628
+ "iam.grant": "iam.permission.grant",
629
+ "data.export": "data.export",
630
+ "payment.release": "payment.release",
631
+ }
632
+
633
+
634
+ def _mcp_wrapper_function_name(tool_name: str) -> str:
635
+ return tool_name.replace(".", "_").replace("-", "_").replace(":", "_").replace("/", "_")
636
+
637
+
638
+ def _mcp_wrap_payload(*, args: argparse.Namespace) -> dict[str, Any]:
639
+ capability = args.capability or MCP_HERO_TOOL_CAPABILITIES[args.tool]
640
+ function_name = _mcp_wrapper_function_name(args.tool)
641
+ wrapper = "\n".join(
642
+ [
643
+ "from mcp.server.fastmcp import Context",
644
+ "from actenon.adapters.mcp import protected_mcp_tool",
645
+ "",
646
+ "",
647
+ f"@mcp.tool(name={args.tool!r})",
648
+ "@protected_mcp_tool(",
649
+ " gate,",
650
+ " action_builder=build_action_intent,",
651
+ f" audience={args.audience!r},",
652
+ ")",
653
+ f"def {function_name}(target: str, ctx: Context):",
654
+ ' """Domain fields only; proof arrives in MCP request metadata."""',
655
+ " return simulated_or_real_domain_operation(target)",
656
+ ]
657
+ )
658
+ return {
659
+ "ok": True,
660
+ "tool_name": args.tool,
661
+ "capability": capability,
662
+ "audience": args.audience,
663
+ "flow": [
664
+ "agent",
665
+ "MCP tool call",
666
+ "Actenon proof gate",
667
+ "tool executes/refuses",
668
+ "VAR emitted",
669
+ ],
670
+ "required_inputs": ["domain fields only"],
671
+ "runtime_injected": [
672
+ "proof via request metadata key 'actenon'",
673
+ "optional Preflight evidence via the same request metadata",
674
+ "FastMCP Context, hidden from the model-facing schema",
675
+ ],
676
+ "proof_gate_steps": [
677
+ "build the exact Action Intent from validated domain arguments",
678
+ "read PCCB from out-of-band MCP request metadata",
679
+ "verify exact proof binding for the MCP tool audience",
680
+ "run Preflight or endpoint policy",
681
+ "acquire a Credential Broker reference only after allow",
682
+ "execute or refuse",
683
+ "emit a canonical Receipt, and a Refusal linked by a refused Receipt when blocked",
684
+ ],
685
+ "python_wrapper": wrapper,
686
+ "local_demo": f"python3 -m examples.mcp_server_protected_tool.demo --tool {args.tool} --scenario allow",
687
+ "hosted_dependency": False,
688
+ "cloud_dependency": False,
689
+ }
690
+
691
+
692
+ def _print_mcp_wrap_result(*, args: argparse.Namespace, payload: dict[str, Any]) -> None:
693
+ if args.json:
694
+ _emit_json(payload)
695
+ return
696
+ print("Actenon MCP wrapper.")
697
+ print(f"Tool: {payload['tool_name']}")
698
+ print(f"Capability: {payload['capability']}")
699
+ print(f"Audience: {payload['audience']}")
700
+ print("Flow: " + " -> ".join(payload["flow"]))
701
+ print("Required tool inputs:")
702
+ for item in payload["required_inputs"]:
703
+ print(f"- {item}")
704
+ print("Runtime-injected inputs:")
705
+ for item in payload["runtime_injected"]:
706
+ print(f"- {item}")
707
+ print("Proof gate steps:")
708
+ for item in payload["proof_gate_steps"]:
709
+ print(f"- {item}")
710
+ print("Python wrapper:")
711
+ print(payload["python_wrapper"])
712
+ print("Local demo:")
713
+ print(payload["local_demo"])
714
+
715
+
716
+ def _load_preflight_evidence(args: argparse.Namespace) -> dict[str, Any]:
717
+ raw_json = getattr(args, "evidence_json", None)
718
+ raw_file = getattr(args, "evidence_file", None)
719
+ if raw_json is None and raw_file is None:
720
+ return {}
721
+ payload = loads_no_duplicate_keys(raw_json) if raw_json is not None else _load_json(raw_file)
722
+ if not isinstance(payload, dict):
723
+ raise ValueError("preflight evidence context must be a JSON object")
724
+ return payload
725
+
726
+
727
+ def _build_preflight_simulation_intent() -> ActionIntent:
728
+ issued_at = parse_timestamp("2026-01-01T12:00:00Z", "issued_at")
729
+ return ActionIntent(
730
+ intent_id="intent_preflight_infra_delete_demo",
731
+ issued_at=issued_at,
732
+ expires_at=parse_timestamp("2026-01-01T12:10:00Z", "expires_at"),
733
+ tenant=TenantRef(tenant_id="tenant_demo"),
734
+ requester=PartyRef(type="agent", id="infra-agent"),
735
+ action=ActionSpec(
736
+ name="database.delete",
737
+ capability="database.delete",
738
+ parameters={"environment": "production", "resource_id": "prod-db-primary"},
739
+ ),
740
+ target=TargetRef(resource_type="database", resource_id="prod-db-primary", selectors={"environment": "production"}),
741
+ justification="Simulate a production database delete preflight check.",
742
+ )
743
+
744
+
745
+ def _resolve_outcomes_root(artifact_root: Path) -> Path | None:
746
+ if (artifact_root / "receipts").is_dir() or (artifact_root / "refusals").is_dir():
747
+ return artifact_root
748
+ if (artifact_root / "outcomes").is_dir():
749
+ return artifact_root / "outcomes"
750
+ return None
751
+
752
+
753
+ def _build_evidence_query_service(artifacts_dir: str) -> EvidenceQueryService:
754
+ artifact_root = Path(artifacts_dir).resolve()
755
+ intent_store = JsonArtifactActionIntentStore(artifact_root)
756
+ pccb_store = JsonArtifactPCCBStore(artifact_root)
757
+ outcomes_root = _resolve_outcomes_root(artifact_root)
758
+ receipt_store = JsonArtifactReceiptStore(outcomes_root) if outcomes_root is not None else None
759
+ refusal_store = JsonArtifactRefusalStore(outcomes_root) if outcomes_root is not None else None
760
+ return EvidenceQueryService(
761
+ intent_store=intent_store,
762
+ pccb_store=pccb_store,
763
+ receipt_store=receipt_store,
764
+ refusal_store=refusal_store,
765
+ )
766
+
767
+
768
+ def _build_evidence_query_from_args(args: argparse.Namespace) -> EvidenceQuery:
769
+ return EvidenceQuery(
770
+ receipt_id=args.receipt_id,
771
+ pccb_id=args.pccb_id,
772
+ intent_id=args.intent_id,
773
+ action_hash=args.action_hash,
774
+ )
775
+
776
+
777
+ def _evidence_query_selector(args: argparse.Namespace) -> tuple[str, str]:
778
+ for key in ("receipt_id", "pccb_id", "intent_id", "action_hash"):
779
+ value = getattr(args, key)
780
+ if value is not None:
781
+ return key, value
782
+ raise AssertionError("evidence query selector is required")
783
+
784
+
785
+ def _hash_verification_status(result: EvidenceResult) -> str:
786
+ if result.verdict == EvidenceVerdict.HASH_MISMATCH:
787
+ return "failed"
788
+ if result.verdict in (EvidenceVerdict.VERIFIED_EXECUTION, EvidenceVerdict.VERIFIED_REFUSAL):
789
+ return "passed"
790
+ return "not_confirmed"
791
+
792
+
793
+ def _evidence_result_payload(*, args: argparse.Namespace, result: EvidenceResult) -> dict[str, Any]:
794
+ query_kind, query_value = _evidence_query_selector(args)
795
+ return {
796
+ "ok": result.verdict in (EvidenceVerdict.VERIFIED_EXECUTION, EvidenceVerdict.VERIFIED_REFUSAL),
797
+ "verdict": result.verdict.value,
798
+ "summary": result.summary,
799
+ "query": {"kind": query_kind, "value": query_value},
800
+ "artifacts_dir": str(Path(args.artifacts_dir).resolve()),
801
+ "receipt_id": result.receipt_id,
802
+ "refusal_id": result.refusal_id,
803
+ "pccb_id": result.pccb_id,
804
+ "intent_id": result.intent_id,
805
+ "action_hash": result.action_hash,
806
+ "chain_length": result.chain_depth,
807
+ "hash_verification": _hash_verification_status(result),
808
+ "details": result.details,
809
+ }
810
+
811
+
812
+ def _print_evidence_query_result(*, args: argparse.Namespace, result: EvidenceResult) -> None:
813
+ payload = _evidence_result_payload(args=args, result=result)
814
+ if args.json:
815
+ _emit_json(payload)
816
+ return
817
+
818
+ print("Execution evidence query.")
819
+ print(f"Source: {payload['artifacts_dir']}")
820
+ print(f"Query: {payload['query']['kind']}={payload['query']['value']}")
821
+ print(f"Verdict: {payload['verdict']}")
822
+ print(f"Summary: {payload['summary']}")
823
+ print(f"Chain length: {payload['chain_length']}")
824
+ print(f"Hash verification: {payload['hash_verification']}")
825
+ if payload["receipt_id"] is not None:
826
+ print(f"Receipt: {payload['receipt_id']}")
827
+ if payload["refusal_id"] is not None:
828
+ print(f"Refusal: {payload['refusal_id']}")
829
+ if payload["pccb_id"] is not None:
830
+ print(f"PCCB: {payload['pccb_id']}")
831
+ if payload["intent_id"] is not None:
832
+ print(f"Intent: {payload['intent_id']}")
833
+ if payload["action_hash"] is not None:
834
+ print(f"Action hash: {payload['action_hash']}")
835
+ if payload["details"]:
836
+ print("Details:")
837
+ print(json.dumps(payload["details"], indent=2, sort_keys=True))
838
+
839
+
840
+ def _load_public_jwk(*, path: str | None, raw_json: str | None) -> dict[str, Any]:
841
+ if path is not None:
842
+ return _load_json(path)
843
+ if raw_json is None:
844
+ raise ValueError("either --public-jwk-file or --public-jwk-json is required")
845
+ parsed = loads_no_duplicate_keys(raw_json)
846
+ if not isinstance(parsed, dict):
847
+ raise ValueError("public JWK input must be a JSON object")
848
+ return parsed
849
+
850
+
851
+ def _build_key_discovery_document_from_args(args: argparse.Namespace) -> dict[str, Any]:
852
+ public_key_jwk = _load_public_jwk(path=args.public_jwk_file, raw_json=args.public_jwk_json)
853
+ issuer = PartyRef(
854
+ type=args.issuer_type,
855
+ id=args.issuer_id,
856
+ display_name=args.issuer_display_name,
857
+ )
858
+ published_at = parse_timestamp(args.published_at, "published_at") if args.published_at else datetime.now(timezone.utc)
859
+ not_before = parse_timestamp(args.not_before, "not_before") if args.not_before else None
860
+ expires_at = parse_timestamp(args.expires_at, "expires_at") if args.expires_at else None
861
+ revoked_at = parse_timestamp(args.revoked_at, "revoked_at") if args.revoked_at else None
862
+ return build_key_discovery_document(
863
+ issuer=issuer,
864
+ origin=args.issuer_origin,
865
+ key_id=args.key_id,
866
+ algorithm=args.algorithm,
867
+ public_key_jwk=public_key_jwk,
868
+ published_at=published_at,
869
+ status=args.status,
870
+ use=args.use or "proof_issuance",
871
+ cache_max_age_seconds=args.cache_max_age_seconds,
872
+ not_before=not_before,
873
+ expires_at=expires_at,
874
+ revoked_at=revoked_at,
875
+ replaced_by=args.replaced_by,
876
+ revocation_reason=args.revocation_reason,
877
+ )
878
+
879
+
880
+ def _parse_cli_metadata(items: Sequence[str] | None) -> dict[str, str]:
881
+ if not items:
882
+ return {}
883
+ metadata: dict[str, str] = {}
884
+ for item in items:
885
+ if "=" not in item:
886
+ raise ValueError(f"metadata entry {item!r} must use key=value form")
887
+ key, value = item.split("=", 1)
888
+ key = key.strip()
889
+ if not key:
890
+ raise ValueError("metadata key must be non-empty")
891
+ metadata[key] = value
892
+ return metadata
893
+
894
+
895
+ def _resolve_anchor_pccb(*, args: argparse.Namespace, correlation_pccb_id: str | None, artifact_path: str) -> PCCB:
896
+ if args.pccb:
897
+ pccb = _load_pccb(args.pccb)
898
+ if correlation_pccb_id is not None and pccb.pccb_id != correlation_pccb_id:
899
+ raise ValueError(f"--pccb artifact has id {pccb.pccb_id!r}, expected {correlation_pccb_id!r}")
900
+ return pccb
901
+
902
+ sibling_pccb = Path(artifact_path).resolve().parent / "pccb.json"
903
+ if sibling_pccb.exists():
904
+ pccb = _load_pccb(str(sibling_pccb))
905
+ if correlation_pccb_id is None or pccb.pccb_id == correlation_pccb_id:
906
+ return pccb
907
+
908
+ if args.artifacts_dir and correlation_pccb_id is not None:
909
+ pccb = JsonArtifactPCCBStore(Path(args.artifacts_dir).resolve()).get_pccb(correlation_pccb_id)
910
+ if pccb is not None:
911
+ return pccb
912
+
913
+ if correlation_pccb_id is None:
914
+ raise ValueError("artifact does not include correlation.pccb_id; supply --pccb explicitly")
915
+ raise ValueError(
916
+ f"could not resolve PCCB {correlation_pccb_id!r}; pass --pccb explicitly or point --artifacts-dir at a local artifact root"
917
+ )
918
+
919
+
920
+ def _graph_anchor_payload(*, args: argparse.Namespace, anchor, anchor_hash: str, source_kind: str, source_path: str) -> dict[str, Any]:
921
+ publish_requested = bool(args.publish_url and not args.dry_run)
922
+ return {
923
+ "ok": True,
924
+ "source": {"kind": source_kind, "path": str(Path(source_path).resolve())},
925
+ "anchor_hash": anchor_hash,
926
+ "anchor": anchor.to_dict(),
927
+ "publication": {
928
+ "requested": publish_requested,
929
+ "mode": "http" if publish_requested else "none",
930
+ "endpoint_url": args.publish_url if publish_requested else None,
931
+ },
932
+ }
933
+
934
+
935
+ def _print_graph_anchor_result(*, args: argparse.Namespace, payload: dict[str, Any]) -> None:
936
+ if args.json:
937
+ _emit_json(payload)
938
+ return
939
+ print("Execution anchor created.")
940
+ print(f"Source: {payload['source']['kind']} {payload['source']['path']}")
941
+ print(f"Outcome: {payload['anchor']['outcome']}")
942
+ print(f"Anchor hash: {payload['anchor_hash']}")
943
+ if payload["publication"]["requested"]:
944
+ print(f"Publication: requested (fire-and-forget) via {payload['publication']['endpoint_url']}")
945
+ else:
946
+ print("Publication: not requested")
947
+ print("Anchor:")
948
+ print(json.dumps(payload["anchor"], indent=2, sort_keys=True))
949
+
950
+
951
+ def _require(condition: bool, message: str) -> None:
952
+ if not condition:
953
+ raise ValueError(message)
954
+
955
+
956
+ def _compare_intent_fields(*, artifact_name: str, intent_id: str, tenant, subject, action, target, other_name: str, other_intent_id: str, other_tenant, other_subject, other_action, other_target) -> None:
957
+ _require(intent_id == other_intent_id, f"{artifact_name} intent_id does not match {other_name}")
958
+ _require(tenant == other_tenant, f"{artifact_name} tenant does not match {other_name}")
959
+ _require(subject == other_subject, f"{artifact_name} subject does not match {other_name}")
960
+ _require(action == other_action, f"{artifact_name} action does not match {other_name}")
961
+ _require(target == other_target, f"{artifact_name} target does not match {other_name}")
962
+
963
+
964
+ def _verify_receipt_links(receipt: Receipt, *, intent: ActionIntent | None, pccb: PCCB | None, refusal: Refusal | None) -> None:
965
+ if intent is not None:
966
+ _compare_intent_fields(
967
+ artifact_name="receipt",
968
+ intent_id=receipt.intent_id,
969
+ tenant=receipt.tenant,
970
+ subject=receipt.subject,
971
+ action=receipt.action,
972
+ target=receipt.target,
973
+ other_name="action intent",
974
+ other_intent_id=intent.intent_id,
975
+ other_tenant=intent.tenant,
976
+ other_subject=intent.requester,
977
+ other_action=intent.action,
978
+ other_target=intent.target,
979
+ )
980
+ if pccb is not None:
981
+ if pccb.intent_id is not None:
982
+ _require(receipt.intent_id == pccb.intent_id, "receipt intent_id does not match PCCB intent_id")
983
+ _require(receipt.correlation is not None and receipt.correlation.pccb_id == pccb.pccb_id, "receipt correlation.pccb_id does not match PCCB")
984
+ _compare_intent_fields(
985
+ artifact_name="receipt",
986
+ intent_id=receipt.intent_id,
987
+ tenant=receipt.tenant,
988
+ subject=receipt.subject,
989
+ action=receipt.action,
990
+ target=receipt.target,
991
+ other_name="PCCB",
992
+ other_intent_id=pccb.intent_id or receipt.intent_id,
993
+ other_tenant=pccb.tenant,
994
+ other_subject=pccb.subject,
995
+ other_action=pccb.action,
996
+ other_target=pccb.target,
997
+ )
998
+ if refusal is not None:
999
+ if receipt.correlation is not None and receipt.correlation.refusal_id is not None:
1000
+ _require(receipt.correlation.refusal_id == refusal.refusal_id, "receipt correlation.refusal_id does not match refusal")
1001
+ else:
1002
+ _require(
1003
+ receipt.correlation is not None
1004
+ and refusal.correlation is not None
1005
+ and receipt.correlation.request_id == refusal.correlation.request_id,
1006
+ "receipt request_id does not match refusal request_id",
1007
+ )
1008
+ _require(receipt.intent_id == (refusal.intent_id or receipt.intent_id), "receipt intent_id does not match refusal")
1009
+ if receipt.reason_codes:
1010
+ _require(refusal.reason_code in receipt.reason_codes, "receipt reason_codes do not include reason_code")
1011
+
1012
+
1013
+ def _verify_refusal_links(refusal: Refusal, *, intent: ActionIntent | None, pccb: PCCB | None, receipt: Receipt | None) -> None:
1014
+ if intent is not None:
1015
+ _require(refusal.intent_id == intent.intent_id, "refusal intent_id does not match action intent")
1016
+ if refusal.tenant is not None:
1017
+ _require(refusal.tenant == intent.tenant, "refusal tenant does not match action intent")
1018
+ if refusal.subject is not None:
1019
+ _require(refusal.subject == intent.requester, "refusal subject does not match action intent")
1020
+ if refusal.action is not None:
1021
+ _require(refusal.action == intent.action, "refusal action does not match action intent")
1022
+ if refusal.target is not None:
1023
+ _require(refusal.target == intent.target, "refusal target does not match action intent")
1024
+ if pccb is not None:
1025
+ _require(refusal.correlation is not None and refusal.correlation.pccb_id == pccb.pccb_id, "refusal correlation.pccb_id does not match PCCB")
1026
+ if refusal.intent_id is not None and pccb.intent_id is not None:
1027
+ _require(refusal.intent_id == pccb.intent_id, "refusal intent_id does not match PCCB intent_id")
1028
+ if receipt is not None:
1029
+ if receipt.correlation is not None and receipt.correlation.refusal_id is not None:
1030
+ _require(receipt.correlation.refusal_id == refusal.refusal_id, "receipt correlation.refusal_id does not match refusal")
1031
+ else:
1032
+ _require(
1033
+ receipt.correlation is not None
1034
+ and refusal.correlation is not None
1035
+ and receipt.correlation.request_id == refusal.correlation.request_id,
1036
+ "receipt request_id does not match refusal request_id",
1037
+ )
1038
+ _require(receipt.intent_id == (refusal.intent_id or receipt.intent_id), "receipt intent_id does not match refusal")
1039
+ if receipt.reason_codes:
1040
+ _require(refusal.reason_code in receipt.reason_codes, "receipt reason_codes do not include reason_code")
1041
+
1042
+
1043
+ def _cmd_verify_proof(args: argparse.Namespace) -> int:
1044
+ intent: ActionIntent | None = None
1045
+ pccb: PCCB | None = None
1046
+ context = None
1047
+ verification_time = datetime.now(timezone.utc)
1048
+
1049
+ try:
1050
+ intent = _load_intent(args.intent)
1051
+ pccb = _load_pccb(args.pccb)
1052
+ signature_verifier = _resolve_signature_verifier(pccb, args.signer)
1053
+ sdk = VerifierSDK(signature_verifier)
1054
+ verification_time = _resolve_verification_time(args.verification_time, pccb)
1055
+ context = sdk.build_context(
1056
+ request_id=args.request_id,
1057
+ audience=_resolve_audience(args.audience, audience_type=args.audience_type),
1058
+ now=verification_time,
1059
+ scope_capabilities=pccb.scope.capabilities,
1060
+ parameter_constraints=pccb.scope.parameter_constraints,
1061
+ resource_selectors=pccb.scope.resource_selectors,
1062
+ )
1063
+ verified = sdk.verify(intent=intent, pccb=pccb, context=context)
1064
+ except RefusalException as exc:
1065
+ refusal = _build_cli_refusal(
1066
+ request_id=args.request_id,
1067
+ occurred_at=verification_time,
1068
+ exc=exc,
1069
+ intent=intent,
1070
+ context=context,
1071
+ pccb=pccb,
1072
+ )
1073
+ _print_verify_proof_failure(args=args, refusal=refusal)
1074
+ return 1
1075
+ except (FileNotFoundError, json.JSONDecodeError, ValueError) as exc:
1076
+ refusal = _build_cli_refusal(
1077
+ request_id=args.request_id,
1078
+ occurred_at=verification_time,
1079
+ exc=ContractValidationError(str(exc)),
1080
+ intent=intent,
1081
+ context=context,
1082
+ pccb=pccb,
1083
+ )
1084
+ _print_verify_proof_failure(args=args, refusal=refusal)
1085
+ return 1
1086
+
1087
+ _print_verify_proof_success(args=args, verified=verified, verification_time=verification_time)
1088
+ return 0
1089
+
1090
+
1091
+ def _cmd_verify_receipt(args: argparse.Namespace) -> int:
1092
+ receipt = _load_receipt(args.receipt)
1093
+ intent = _load_intent(args.intent) if args.intent else None
1094
+ pccb = _load_pccb(args.pccb) if args.pccb else None
1095
+ refusal = _load_refusal(args.refusal) if args.refusal else None
1096
+ _verify_receipt_links(receipt, intent=intent, pccb=pccb, refusal=refusal)
1097
+ print("Receipt verified.")
1098
+ print(f"Receipt: {receipt.receipt_id}")
1099
+ print(f"Outcome: {receipt.outcome}")
1100
+ print(f"Intent: {receipt.intent_id}")
1101
+ if receipt.phase is not None:
1102
+ print(f"Phase: {receipt.phase}")
1103
+ return 0
1104
+
1105
+
1106
+ def _cmd_verify_refusal(args: argparse.Namespace) -> int:
1107
+ refusal = _load_refusal(args.refusal)
1108
+ intent = _load_intent(args.intent) if args.intent else None
1109
+ pccb = _load_pccb(args.pccb) if args.pccb else None
1110
+ receipt = _load_receipt(args.receipt) if args.receipt else None
1111
+ _verify_refusal_links(refusal, intent=intent, pccb=pccb, receipt=receipt)
1112
+ print("Refusal verified.")
1113
+ print(f"Refusal: {refusal.refusal_id}")
1114
+ print(f"Category: {refusal.category}")
1115
+ print(f"Code: {refusal.reason_code}")
1116
+ return 0
1117
+
1118
+
1119
+ def _cmd_attest_receipt(args: argparse.Namespace) -> int:
1120
+ receipt = _load_receipt(args.receipt)
1121
+ service = OutcomeAttestationService(
1122
+ signer=_resolve_attestation_signer(args.signer),
1123
+ issuer=_resolve_party(args.issuer),
1124
+ )
1125
+ attestation = service.attest_receipt(
1126
+ receipt,
1127
+ issued_at=_resolve_optional_timestamp(args.issued_at, "issued_at"),
1128
+ )
1129
+ output_path = _write_json(args.output, attestation.to_dict())
1130
+ _print_attestation_written(
1131
+ args=args,
1132
+ artifact_kind="receipt",
1133
+ artifact_id=receipt.receipt_id,
1134
+ output_path=output_path,
1135
+ attestation=attestation,
1136
+ )
1137
+ return 0
1138
+
1139
+
1140
+ def _cmd_attest_refusal(args: argparse.Namespace) -> int:
1141
+ refusal = _load_refusal(args.refusal)
1142
+ service = OutcomeAttestationService(
1143
+ signer=_resolve_attestation_signer(args.signer),
1144
+ issuer=_resolve_party(args.issuer),
1145
+ )
1146
+ attestation = service.attest_refusal(
1147
+ refusal,
1148
+ issued_at=_resolve_optional_timestamp(args.issued_at, "issued_at"),
1149
+ )
1150
+ output_path = _write_json(args.output, attestation.to_dict())
1151
+ _print_attestation_written(
1152
+ args=args,
1153
+ artifact_kind="refusal",
1154
+ artifact_id=refusal.refusal_id,
1155
+ output_path=output_path,
1156
+ attestation=attestation,
1157
+ )
1158
+ return 0
1159
+
1160
+
1161
+ def _cmd_verify_receipt_attestation(args: argparse.Namespace) -> int:
1162
+ attestation = _load_receipt_attestation(args.attestation)
1163
+ verifier = _resolve_attestation_verifier(attestation.signature.key_id, args.signer)
1164
+ service = OutcomeAttestationService(signer=verifier, issuer=attestation.issuer)
1165
+ try:
1166
+ receipt = service.verify_receipt_attestation(attestation, verifier=verifier)
1167
+ except OutcomeAttestationVerificationError as exc:
1168
+ _print_attestation_verification_failure(
1169
+ args=args,
1170
+ artifact_kind="receipt",
1171
+ attestation_id=attestation.attestation_id,
1172
+ error=exc,
1173
+ )
1174
+ return 1
1175
+ receipt_id = receipt.receipt_id if hasattr(receipt, "receipt_id") else str(receipt.get("receipt_id", "unknown-receipt"))
1176
+ _print_attestation_verification_success(
1177
+ args=args,
1178
+ artifact_kind="receipt",
1179
+ artifact_id=receipt_id,
1180
+ attestation=attestation,
1181
+ )
1182
+ return 0
1183
+
1184
+
1185
+ def _cmd_verify_refusal_attestation(args: argparse.Namespace) -> int:
1186
+ attestation = _load_refusal_attestation(args.attestation)
1187
+ verifier = _resolve_attestation_verifier(attestation.signature.key_id, args.signer)
1188
+ service = OutcomeAttestationService(signer=verifier, issuer=attestation.issuer)
1189
+ try:
1190
+ refusal = service.verify_refusal_attestation(attestation, verifier=verifier)
1191
+ except OutcomeAttestationVerificationError as exc:
1192
+ _print_attestation_verification_failure(
1193
+ args=args,
1194
+ artifact_kind="refusal",
1195
+ attestation_id=attestation.attestation_id,
1196
+ error=exc,
1197
+ )
1198
+ return 1
1199
+ refusal_id = refusal.refusal_id if hasattr(refusal, "refusal_id") else str(refusal.get("refusal_id", "unknown-refusal"))
1200
+ _print_attestation_verification_success(
1201
+ args=args,
1202
+ artifact_kind="refusal",
1203
+ artifact_id=refusal_id,
1204
+ attestation=attestation,
1205
+ )
1206
+ return 0
1207
+
1208
+
1209
+ def _cmd_conformance_run(args: argparse.Namespace) -> int:
1210
+ from actenon.conformance import CONFORMANCE_VERSION, VERIFIED_MARK
1211
+
1212
+ package_root = Path(__file__).resolve().parent
1213
+ conformance_dir = package_root / "conformance"
1214
+ if not conformance_dir.exists():
1215
+ raise ValueError(f"conformance directory not found at {conformance_dir}")
1216
+ suite = unittest.defaultTestLoader.discover(
1217
+ str(conformance_dir),
1218
+ pattern="test_*.py",
1219
+ top_level_dir=str(package_root.parent),
1220
+ )
1221
+ print(f"Conformance version: {CONFORMANCE_VERSION}")
1222
+ stream = sys.stdout if args.verbose else StringIO()
1223
+ result = unittest.TextTestRunner(stream=stream, verbosity=2 if args.verbose else 1).run(suite)
1224
+ skipped = len(result.skipped)
1225
+ complete = result.wasSuccessful() and skipped == 0
1226
+ if not args.verbose:
1227
+ print(f"Conformance tests {'passed' if result.wasSuccessful() else 'failed'}.")
1228
+ print(f"Ran {result.testsRun} test(s).")
1229
+ print(f"Skipped: {skipped}.")
1230
+ if complete:
1231
+ print(f"Mark eligibility: {VERIFIED_MARK}")
1232
+ elif skipped:
1233
+ print(
1234
+ "Mark eligibility: INCOMPLETE "
1235
+ "(install required extras and run with no skipped checks)."
1236
+ )
1237
+ if args.require_complete and skipped:
1238
+ return 1
1239
+ return 0 if result.wasSuccessful() else 1
1240
+
1241
+
1242
+ def _cmd_coverage_run(args: argparse.Namespace) -> int:
1243
+ result = run_consequential_action_matrix(evidence_path=args.output)
1244
+ if args.json:
1245
+ _emit_json(result.to_dict())
1246
+ else:
1247
+ print(render_coverage_matrix_text(result))
1248
+ return 0 if result.result == "PASS" else 1
1249
+
1250
+
1251
+ def _scan_target_from_args(args: argparse.Namespace) -> str:
1252
+ scan_command = getattr(args, "scan_command", None)
1253
+ if scan_command is not None:
1254
+ return scan_command
1255
+ if args.target is not None:
1256
+ return args.target
1257
+ if args.intent or args.pccb or args.audience:
1258
+ return "artifact-pair"
1259
+ return "replay-harness"
1260
+
1261
+
1262
+ def _cmd_scan(args: argparse.Namespace) -> int:
1263
+ if getattr(args, "json", False) and getattr(args, "markdown", False):
1264
+ raise ValueError("use either --json or --markdown, not both")
1265
+ target = _scan_target_from_args(args)
1266
+
1267
+ if target in {"repo", "mcp", "endpoint"}:
1268
+ extensions: list[str] | None = None
1269
+ if getattr(args, "extensions", None):
1270
+ extensions = []
1271
+ for raw in args.extensions:
1272
+ extensions.extend(item.strip() for item in raw.split(",") if item.strip())
1273
+ progress_callback = (
1274
+ (lambda message: print(message, file=sys.stderr))
1275
+ if getattr(args, "progress", False)
1276
+ else None
1277
+ )
1278
+ options = ScannerOptions(
1279
+ exclude=tuple(getattr(args, "exclude", ()) or ()),
1280
+ include=tuple(getattr(args, "include", ()) or ()),
1281
+ extensions=tuple(extensions) if extensions else None,
1282
+ max_files=getattr(args, "max_files", None),
1283
+ max_file_size=getattr(args, "max_file_size", 1_000_000),
1284
+ timeout_seconds=getattr(args, "timeout_seconds", None),
1285
+ partial_report_on_timeout=getattr(args, "partial_report_on_timeout", False),
1286
+ progress_callback=progress_callback,
1287
+ )
1288
+ report = scan_repository(args.path, mode=target, options=options)
1289
+ elif target == "local":
1290
+ report = scan_local()
1291
+ elif target == "artifact-pair":
1292
+ missing = [flag for flag, value in (("--intent", args.intent), ("--pccb", args.pccb), ("--audience", args.audience)) if not value]
1293
+ if missing:
1294
+ raise ValueError("artifact-pair scan requires " + ", ".join(missing))
1295
+
1296
+ intent = _load_intent(args.intent)
1297
+ pccb = _load_pccb(args.pccb)
1298
+ signature_verifier = _resolve_signature_verifier(pccb, args.signer)
1299
+ sdk = VerifierSDK(signature_verifier)
1300
+ verification_time = _resolve_verification_time(args.verification_time, pccb)
1301
+ audience = _resolve_audience(args.audience, audience_type=args.audience_type)
1302
+ report = scan_artifact_pair(
1303
+ intent=intent,
1304
+ pccb=pccb,
1305
+ sdk=sdk,
1306
+ audience=audience,
1307
+ verification_time=verification_time,
1308
+ request_id=args.request_id,
1309
+ )
1310
+ else:
1311
+ ignored_flags = [flag for flag, value in (("--intent", args.intent), ("--pccb", args.pccb), ("--audience", args.audience)) if value]
1312
+ if ignored_flags:
1313
+ raise ValueError("replay-harness scan does not accept " + ", ".join(ignored_flags) + "; omit them or use --target artifact-pair")
1314
+ report = scan_replay_harness()
1315
+
1316
+ _print_scan_report(args=args, report=report)
1317
+ return 1 if report.overall_status == "EXECUTION_GAP_PRESENT" else 0
1318
+
1319
+
1320
+ def _cmd_preflight_check(args: argparse.Namespace) -> int:
1321
+ intent = _load_intent(args.intent)
1322
+ decision = PreflightEngine().check(intent, evidence_context=_load_preflight_evidence(args))
1323
+ _print_preflight_decision(args=args, decision=decision)
1324
+ return 0
1325
+
1326
+
1327
+ def _cmd_preflight_explain(args: argparse.Namespace) -> int:
1328
+ decision = _load_preflight_decision(args.decision)
1329
+ _print_preflight_decision(args=args, decision=decision)
1330
+ return 0
1331
+
1332
+
1333
+ def _cmd_preflight_simulate(args: argparse.Namespace) -> int:
1334
+ if args.wedge != "infra_delete":
1335
+ raise ValueError(f"unsupported preflight simulation wedge: {args.wedge!r}")
1336
+ decision = PreflightEngine().check(
1337
+ _build_preflight_simulation_intent(),
1338
+ evidence_context={
1339
+ "environment": "production",
1340
+ "change_ticket": "CHG-PREFLIGHT-DEMO",
1341
+ "backup_verified": True,
1342
+ },
1343
+ )
1344
+ _print_preflight_decision(args=args, decision=decision)
1345
+ return 0
1346
+
1347
+
1348
+ def _cmd_mcp_wrap(args: argparse.Namespace) -> int:
1349
+ _print_mcp_wrap_result(args=args, payload=_mcp_wrap_payload(args=args))
1350
+ return 0
1351
+
1352
+
1353
+ def _cmd_evidence_query(args: argparse.Namespace) -> int:
1354
+ service = _build_evidence_query_service(args.artifacts_dir)
1355
+ result = service.query(_build_evidence_query_from_args(args))
1356
+ _print_evidence_query_result(args=args, result=result)
1357
+ return 0 if result.verdict in (EvidenceVerdict.VERIFIED_EXECUTION, EvidenceVerdict.VERIFIED_REFUSAL) else 1
1358
+
1359
+
1360
+ def _cmd_keys_publish(args: argparse.Namespace) -> int:
1361
+ document = _build_key_discovery_document_from_args(args)
1362
+ output_path = Path(args.output).resolve()
1363
+ output_path.parent.mkdir(parents=True, exist_ok=True)
1364
+ output_path.write_text(json.dumps(document, indent=2, sort_keys=True) + "\n", encoding="utf-8")
1365
+ print("Key-discovery document written.")
1366
+ print(f"Output: {output_path}")
1367
+ print(f"Origin: {document['origin']}")
1368
+ print(f"Issuer: {document['issuer']['type']}:{document['issuer']['id']}")
1369
+ print(f"Key: {args.key_id}")
1370
+ print(f"Algorithm: {args.algorithm}")
1371
+ return 0
1372
+
1373
+
1374
+ def _cmd_graph_anchor(args: argparse.Namespace) -> int:
1375
+ published_at = parse_timestamp(args.published_at, "published_at") if args.published_at else datetime.now(timezone.utc)
1376
+ metadata = _parse_cli_metadata(args.metadata)
1377
+
1378
+ if args.receipt is not None:
1379
+ receipt = _load_receipt(args.receipt)
1380
+ correlation_pccb_id = receipt.correlation.pccb_id if receipt.correlation is not None else None
1381
+ pccb = _resolve_anchor_pccb(args=args, correlation_pccb_id=correlation_pccb_id, artifact_path=args.receipt)
1382
+ anchor = create_execution_anchor_from_receipt(
1383
+ receipt,
1384
+ pccb,
1385
+ published_at=published_at,
1386
+ metadata=metadata,
1387
+ )
1388
+ payload = _graph_anchor_payload(
1389
+ args=args,
1390
+ anchor=anchor,
1391
+ anchor_hash=build_execution_anchor_hash(anchor),
1392
+ source_kind="receipt",
1393
+ source_path=args.receipt,
1394
+ )
1395
+ else:
1396
+ refusal = _load_refusal(args.refusal)
1397
+ correlation_pccb_id = refusal.correlation.pccb_id if refusal.correlation is not None else None
1398
+ pccb = _resolve_anchor_pccb(args=args, correlation_pccb_id=correlation_pccb_id, artifact_path=args.refusal)
1399
+ anchor = create_execution_anchor_from_refusal(
1400
+ refusal,
1401
+ pccb,
1402
+ published_at=published_at,
1403
+ metadata=metadata,
1404
+ )
1405
+ payload = _graph_anchor_payload(
1406
+ args=args,
1407
+ anchor=anchor,
1408
+ anchor_hash=build_execution_anchor_hash(anchor),
1409
+ source_kind="refusal",
1410
+ source_path=args.refusal,
1411
+ )
1412
+
1413
+ if args.publish_url and not args.dry_run:
1414
+ HttpExecutionGraphClient(endpoint_url=args.publish_url).publish(anchor)
1415
+
1416
+ _print_graph_anchor_result(args=args, payload=payload)
1417
+ return 0
1418
+
1419
+
1420
+ def _cmd_up(args: argparse.Namespace) -> int:
1421
+ if args.bootstrap_only:
1422
+ manifest = bootstrap_local_runtime(args.runtime_dir)
1423
+ _print_runtime_up_result(args=args, payload=manifest)
1424
+ return 0
1425
+
1426
+ session = start_local_runtime_services(
1427
+ runtime_dir=args.runtime_dir,
1428
+ host=args.host,
1429
+ port=args.port,
1430
+ enable_trace_viewer=not args.no_trace_viewer,
1431
+ trace_viewer_port=args.trace_viewer_port,
1432
+ )
1433
+ try:
1434
+ _print_runtime_up_result(args=args, payload=session.startup_info.to_dict())
1435
+ session.runtime_server.thread.join()
1436
+ except KeyboardInterrupt:
1437
+ pass
1438
+ finally:
1439
+ session.close()
1440
+ return 0
1441
+
1442
+
1443
+ def _cmd_doctor(args: argparse.Namespace) -> int:
1444
+ report = doctor_local_runtime(args.runtime_dir, deep=args.deep)
1445
+ _print_doctor_report(args=args, report=report)
1446
+ return 0 if report.overall_status == "ready" else 1
1447
+
1448
+
1449
+ def _cmd_simulate(args: argparse.Namespace) -> int:
1450
+ if args.incident is not None and args.scenario is not None:
1451
+ raise ValueError("use either --incident or --scenario, not both")
1452
+ report = simulate_local_runtime(args.runtime_dir, scenario=args.scenario or "all", incident=args.incident)
1453
+ _print_simulation_report(args=args, report=report)
1454
+ return 0 if report.succeeded else 1
1455
+
1456
+
1457
+ def _cmd_bundle_export(args: argparse.Namespace) -> int:
1458
+ payload = export_local_runtime_bundle(
1459
+ args.runtime_dir,
1460
+ output_path=args.output,
1461
+ force=args.force,
1462
+ )
1463
+ _print_bundle_export_result(args=args, payload=payload)
1464
+ return 0
1465
+
1466
+
1467
+ def _cmd_bundle_verify(args: argparse.Namespace) -> int:
1468
+ payload = verify_local_runtime_bundle(args.bundle)
1469
+ _print_bundle_verify_result(args=args, payload=payload)
1470
+ return 0 if payload["ok"] else 1
1471
+
1472
+
1473
+ def _cmd_keys_generate(args: argparse.Namespace) -> int:
1474
+ payload = generate_local_hmac_key_material(
1475
+ output_path=args.output,
1476
+ key_id=args.key_id,
1477
+ secret_bytes=args.secret_bytes,
1478
+ )
1479
+ _print_keys_generate_result(args=args, output_path=str(Path(args.output).resolve()), payload=payload)
1480
+ return 0
1481
+
1482
+
1483
+ def build_parser() -> argparse.ArgumentParser:
1484
+ parser = argparse.ArgumentParser(
1485
+ prog="actenon",
1486
+ description="Local CLI for bootstrapping the single-node trust runtime, verifying proof, generating local key material, creating execution anchors, publishing key-discovery documents, querying execution evidence, scanning execution gaps, running coverage matrices, validating artifacts, and running conformance.",
1487
+ )
1488
+ subparsers = parser.add_subparsers(dest="command", required=True)
1489
+
1490
+ up = subparsers.add_parser(
1491
+ "up",
1492
+ help="Start the local single-node trust runtime server and local trace viewer.",
1493
+ )
1494
+ up.add_argument(
1495
+ "--runtime-dir",
1496
+ default=str(DEFAULT_LOCAL_RUNTIME_DIR),
1497
+ help="Local runtime root. Defaults to artifacts/local_runtime.",
1498
+ )
1499
+ up.add_argument("--host", default="127.0.0.1", help="Host interface to bind for the local runtime server.")
1500
+ up.add_argument("--port", default=8787, type=int, help="Port to bind for the local runtime server.")
1501
+ up.add_argument(
1502
+ "--trace-viewer-port",
1503
+ default=8421,
1504
+ type=int,
1505
+ help="Port to bind for the local read-only trace viewer. Defaults to 8421.",
1506
+ )
1507
+ up.add_argument(
1508
+ "--no-trace-viewer",
1509
+ action="store_true",
1510
+ help="Disable the local read-only trace viewer.",
1511
+ )
1512
+ up.add_argument(
1513
+ "--bootstrap-only",
1514
+ action="store_true",
1515
+ help="Prepare the runtime files and labs without starting the local HTTP services.",
1516
+ )
1517
+ up.add_argument("--json", action="store_true", help="Emit structured JSON output.")
1518
+ up.set_defaults(func=_cmd_up)
1519
+
1520
+ doctor = subparsers.add_parser(
1521
+ "doctor",
1522
+ help="Check whether the local single-node trust runtime is healthy and complete.",
1523
+ )
1524
+ doctor.add_argument(
1525
+ "--runtime-dir",
1526
+ default=str(DEFAULT_LOCAL_RUNTIME_DIR),
1527
+ help="Local runtime root to inspect. Defaults to artifacts/local_runtime.",
1528
+ )
1529
+ doctor.add_argument(
1530
+ "--deep",
1531
+ action="store_true",
1532
+ help="Run slower lab and scanner checks in addition to the fast local runtime diagnostic.",
1533
+ )
1534
+ doctor.add_argument("--json", action="store_true", help="Emit structured JSON output.")
1535
+ doctor.set_defaults(func=_cmd_doctor)
1536
+
1537
+ simulate = subparsers.add_parser(
1538
+ "simulate",
1539
+ help="Run local incident simulations that contrast unprotected execution, proof checks, and protected-endpoint runtime behavior.",
1540
+ )
1541
+ simulate.add_argument(
1542
+ "--runtime-dir",
1543
+ default=str(DEFAULT_LOCAL_RUNTIME_DIR),
1544
+ help="Local runtime root where simulation artifacts should be written. Defaults to artifacts/local_runtime.",
1545
+ )
1546
+ simulate.add_argument(
1547
+ "--scenario",
1548
+ default=None,
1549
+ choices=(
1550
+ "all",
1551
+ "valid-proof",
1552
+ "audience-mismatch",
1553
+ "action-hash-mismatch",
1554
+ "expired-proof",
1555
+ "replay-refused",
1556
+ "mcp-tool-proof-laundering",
1557
+ "iam-escalation",
1558
+ "data-export",
1559
+ ),
1560
+ help="Lower-level technical scenario to run. Defaults to 'all' when --incident is not used.",
1561
+ )
1562
+ simulate.add_argument(
1563
+ "--incident",
1564
+ choices=("all", "prod-delete", "replit", "openai-eggs", "amazon-kiro"),
1565
+ help="Educational incident or pattern simulation. Use prod-delete for the generic hero pattern.",
1566
+ )
1567
+ simulate.add_argument("--json", action="store_true", help="Emit structured JSON output.")
1568
+ simulate.set_defaults(func=_cmd_simulate)
1569
+
1570
+ verify_proof = subparsers.add_parser(
1571
+ "verify-proof",
1572
+ help="Verify an Action Intent and PCCB pair against explicit verifier context.",
1573
+ )
1574
+ verify_proof.add_argument("--intent", required=True, help="Path to the Action Intent JSON payload.")
1575
+ verify_proof.add_argument("--pccb", required=True, help="Path to the PCCB JSON payload.")
1576
+ verify_proof.add_argument(
1577
+ "--audience",
1578
+ required=True,
1579
+ help="Protected endpoint audience to enforce. Use '<type>:<id>' or a bare id with --audience-type.",
1580
+ )
1581
+ verify_proof.add_argument(
1582
+ "--audience-type",
1583
+ default="service",
1584
+ help="Audience type to use when --audience is provided as a bare id. Defaults to 'service'.",
1585
+ )
1586
+ verify_proof.add_argument(
1587
+ "--verification-time",
1588
+ default="now",
1589
+ help="Verification time to use: 'now', 'pccb-issued-at', 'pccb-not-before', or an RFC3339 timestamp.",
1590
+ )
1591
+ verify_proof.add_argument("--request-id", default="cli_verify_proof", help="Request identifier used for local verification context.")
1592
+ verify_proof.add_argument("--json", action="store_true", help="Emit structured JSON output.")
1593
+ verify_proof.add_argument(
1594
+ "--signer",
1595
+ default="auto",
1596
+ choices=("auto", "local"),
1597
+ help="Signature verifier to use. 'auto' currently supports the open-source local trust root only.",
1598
+ )
1599
+ verify_proof.set_defaults(func=_cmd_verify_proof)
1600
+
1601
+ verify_receipt = subparsers.add_parser("verify-receipt", help="Validate a receipt artifact and optional linked artifacts.")
1602
+ verify_receipt.add_argument("--receipt", required=True, help="Path to the receipt JSON payload.")
1603
+ verify_receipt.add_argument("--intent", help="Optional Action Intent JSON to cross-check against the receipt.")
1604
+ verify_receipt.add_argument("--pccb", help="Optional PCCB JSON to cross-check against the receipt.")
1605
+ verify_receipt.add_argument("--refusal", help="Optional refusal JSON to cross-check against the receipt.")
1606
+ verify_receipt.set_defaults(func=_cmd_verify_receipt)
1607
+
1608
+ verify_refusal = subparsers.add_parser("verify-refusal", help="Validate a refusal artifact and optional linked artifacts.")
1609
+ verify_refusal.add_argument("--refusal", required=True, help="Path to the refusal JSON payload.")
1610
+ verify_refusal.add_argument("--intent", help="Optional Action Intent JSON to cross-check against the refusal.")
1611
+ verify_refusal.add_argument("--pccb", help="Optional PCCB JSON to cross-check against the refusal.")
1612
+ verify_refusal.add_argument("--receipt", help="Optional receipt JSON to cross-check against the refusal.")
1613
+ verify_refusal.set_defaults(func=_cmd_verify_refusal)
1614
+
1615
+ attest_receipt = subparsers.add_parser("attest-receipt", help="Create an opt-in signed attestation envelope for a receipt.")
1616
+ attest_receipt.add_argument("--receipt", required=True, help="Path to the v1 receipt JSON payload.")
1617
+ attest_receipt.add_argument("--output", required=True, help="Path to write the receipt attestation JSON payload.")
1618
+ attest_receipt.add_argument(
1619
+ "--issuer",
1620
+ default="service:actenon-local-outcome-attestor",
1621
+ help="Attestation issuer as '<type>:<id>' or a bare service id.",
1622
+ )
1623
+ attest_receipt.add_argument("--issued-at", help="Optional RFC3339 attestation timestamp. Defaults to current UTC time.")
1624
+ attest_receipt.add_argument(
1625
+ "--signer",
1626
+ default="local",
1627
+ choices=("local",),
1628
+ help="Signer to use. The OSS CLI currently supports local proof signing only.",
1629
+ )
1630
+ attest_receipt.add_argument("--json", action="store_true", help="Emit structured JSON output.")
1631
+ attest_receipt.set_defaults(func=_cmd_attest_receipt)
1632
+
1633
+ attest_refusal = subparsers.add_parser("attest-refusal", help="Create an opt-in signed attestation envelope for a refusal.")
1634
+ attest_refusal.add_argument("--refusal", required=True, help="Path to the v1 refusal JSON payload.")
1635
+ attest_refusal.add_argument("--output", required=True, help="Path to write the refusal attestation JSON payload.")
1636
+ attest_refusal.add_argument(
1637
+ "--issuer",
1638
+ default="service:actenon-local-outcome-attestor",
1639
+ help="Attestation issuer as '<type>:<id>' or a bare service id.",
1640
+ )
1641
+ attest_refusal.add_argument("--issued-at", help="Optional RFC3339 attestation timestamp. Defaults to current UTC time.")
1642
+ attest_refusal.add_argument(
1643
+ "--signer",
1644
+ default="local",
1645
+ choices=("local",),
1646
+ help="Signer to use. The OSS CLI currently supports local proof signing only.",
1647
+ )
1648
+ attest_refusal.add_argument("--json", action="store_true", help="Emit structured JSON output.")
1649
+ attest_refusal.set_defaults(func=_cmd_attest_refusal)
1650
+
1651
+ verify_receipt_attestation = subparsers.add_parser(
1652
+ "verify-receipt-attestation",
1653
+ help="Verify a signed receipt attestation envelope.",
1654
+ )
1655
+ verify_receipt_attestation.add_argument("--attestation", required=True, help="Path to the receipt attestation JSON payload.")
1656
+ verify_receipt_attestation.add_argument(
1657
+ "--signer",
1658
+ default="auto",
1659
+ choices=("auto", "local"),
1660
+ help="Signature verifier to use. 'auto' currently supports the OSS local trust root only.",
1661
+ )
1662
+ verify_receipt_attestation.add_argument("--json", action="store_true", help="Emit structured JSON output.")
1663
+ verify_receipt_attestation.set_defaults(func=_cmd_verify_receipt_attestation)
1664
+
1665
+ verify_refusal_attestation = subparsers.add_parser(
1666
+ "verify-refusal-attestation",
1667
+ help="Verify a signed refusal attestation envelope.",
1668
+ )
1669
+ verify_refusal_attestation.add_argument("--attestation", required=True, help="Path to the refusal attestation JSON payload.")
1670
+ verify_refusal_attestation.add_argument(
1671
+ "--signer",
1672
+ default="auto",
1673
+ choices=("auto", "local"),
1674
+ help="Signature verifier to use. 'auto' currently supports the OSS local trust root only.",
1675
+ )
1676
+ verify_refusal_attestation.add_argument("--json", action="store_true", help="Emit structured JSON output.")
1677
+ verify_refusal_attestation.set_defaults(func=_cmd_verify_refusal_attestation)
1678
+
1679
+ evidence = subparsers.add_parser(
1680
+ "evidence",
1681
+ help="Query local execution evidence artifacts and receipt chains.",
1682
+ )
1683
+ evidence_subparsers = evidence.add_subparsers(dest="evidence_command", required=True)
1684
+ evidence_query = evidence_subparsers.add_parser(
1685
+ "query",
1686
+ help="Query local execution evidence by receipt id, PCCB id, intent id, or action hash.",
1687
+ )
1688
+ evidence_selector = evidence_query.add_mutually_exclusive_group(required=True)
1689
+ evidence_selector.add_argument("--receipt-id", help="Receipt id to resolve from the local artifact source.")
1690
+ evidence_selector.add_argument("--pccb-id", help="PCCB id to resolve from the local artifact source.")
1691
+ evidence_selector.add_argument("--intent-id", help="Action Intent id to resolve from the local artifact source.")
1692
+ evidence_selector.add_argument("--action-hash", help="Canonical action hash to resolve from the local artifact source.")
1693
+ evidence_query.add_argument(
1694
+ "--artifacts-dir",
1695
+ required=True,
1696
+ help="Local artifact root to scan. Point this at a portable-local-proof root, a local-proof demo root, or an outcomes root.",
1697
+ )
1698
+ evidence_query.add_argument("--json", action="store_true", help="Emit structured JSON output.")
1699
+ evidence_query.set_defaults(func=_cmd_evidence_query)
1700
+
1701
+ keys = subparsers.add_parser(
1702
+ "keys",
1703
+ help="Generate local key material or publishable key-discovery documents.",
1704
+ )
1705
+ keys_subparsers = keys.add_subparsers(dest="keys_command", required=True)
1706
+ keys_generate = keys_subparsers.add_parser(
1707
+ "generate",
1708
+ help="Generate local single-node HS256 key material for issuer and verifier experiments.",
1709
+ )
1710
+ keys_generate.add_argument("--key-id", required=True, help="Exact key identifier to embed in the local key material.")
1711
+ keys_generate.add_argument(
1712
+ "--secret-bytes",
1713
+ type=int,
1714
+ default=32,
1715
+ help="Secret size in bytes. Defaults to 32.",
1716
+ )
1717
+ keys_generate.add_argument("--output", required=True, help="Path to write the generated local key file.")
1718
+ keys_generate.add_argument("--json", action="store_true", help="Emit structured JSON output.")
1719
+ keys_generate.set_defaults(func=_cmd_keys_generate)
1720
+
1721
+ keys_publish = keys_subparsers.add_parser(
1722
+ "publish",
1723
+ help="Generate a conformant key-discovery JSON document for one verification key.",
1724
+ )
1725
+ keys_publish.add_argument("--issuer-origin", required=True, help="HTTPS origin that will serve the discovery document.")
1726
+ keys_publish.add_argument("--issuer-id", required=True, help="Portable issuer identifier for the discovery document.")
1727
+ keys_publish.add_argument(
1728
+ "--issuer-type",
1729
+ default="service",
1730
+ help="Portable issuer type. Defaults to 'service'.",
1731
+ )
1732
+ keys_publish.add_argument("--issuer-display-name", help="Optional human-readable issuer display name.")
1733
+ keys_publish.add_argument("--key-id", required=True, help="Exact key identifier verifiers will match against signature.key_id.")
1734
+ keys_publish.add_argument("--algorithm", required=True, help="Signature algorithm for this verification key, for example 'EdDSA' or 'RS256'.")
1735
+ public_jwk_group = keys_publish.add_mutually_exclusive_group(required=True)
1736
+ public_jwk_group.add_argument("--public-jwk-file", help="Path to a public JWK JSON object for the verification key.")
1737
+ public_jwk_group.add_argument("--public-jwk-json", help="Inline public JWK JSON object for the verification key.")
1738
+ keys_publish.add_argument(
1739
+ "--status",
1740
+ default="active",
1741
+ choices=ALLOWED_DISCOVERY_KEY_STATUSES,
1742
+ help="Published key status. Defaults to 'active'.",
1743
+ )
1744
+ keys_publish.add_argument(
1745
+ "--use",
1746
+ action="append",
1747
+ choices=ALLOWED_DISCOVERY_KEY_USES,
1748
+ default=None,
1749
+ help="Verification purpose for the key. May be repeated. Defaults to 'proof_issuance'.",
1750
+ )
1751
+ keys_publish.add_argument("--published-at", help="RFC3339 publication timestamp. Defaults to the current UTC time.")
1752
+ keys_publish.add_argument("--not-before", help="Optional RFC3339 timestamp before which the key should not be used.")
1753
+ keys_publish.add_argument("--expires-at", help="Optional RFC3339 timestamp after which the key is expired.")
1754
+ keys_publish.add_argument("--revoked-at", help="Optional RFC3339 timestamp recording when the key was revoked.")
1755
+ keys_publish.add_argument("--replaced-by", help="Optional replacement key_id for operator guidance.")
1756
+ keys_publish.add_argument("--revocation-reason", help="Optional revocation reason string.")
1757
+ keys_publish.add_argument(
1758
+ "--cache-max-age-seconds",
1759
+ type=int,
1760
+ default=300,
1761
+ help="Advisory cache lifetime to include in the document. Defaults to 300.",
1762
+ )
1763
+ keys_publish.add_argument("--output", required=True, help="Path to write the generated key-discovery JSON document.")
1764
+ keys_publish.set_defaults(func=_cmd_keys_publish)
1765
+
1766
+ graph = subparsers.add_parser(
1767
+ "graph",
1768
+ help="Create local execution anchors and optionally request publication.",
1769
+ )
1770
+ graph_subparsers = graph.add_subparsers(dest="graph_command", required=True)
1771
+ graph_anchor = graph_subparsers.add_parser(
1772
+ "anchor",
1773
+ help="Create an execution anchor from a local receipt or refusal artifact.",
1774
+ )
1775
+ graph_selector = graph_anchor.add_mutually_exclusive_group(required=True)
1776
+ graph_selector.add_argument("--receipt", help="Path to an executed receipt JSON artifact.")
1777
+ graph_selector.add_argument("--refusal", help="Path to a refusal JSON artifact.")
1778
+ graph_anchor.add_argument(
1779
+ "--pccb",
1780
+ help="Optional PCCB JSON artifact. If omitted, the CLI first looks for a sibling pccb.json and then tries --artifacts-dir.",
1781
+ )
1782
+ graph_anchor.add_argument(
1783
+ "--artifacts-dir",
1784
+ help="Optional local artifact root used to resolve the governing PCCB by correlation.pccb_id when --pccb is omitted.",
1785
+ )
1786
+ graph_anchor.add_argument(
1787
+ "--publish-url",
1788
+ help="Optional HTTP endpoint for fire-and-forget execution-anchor publication.",
1789
+ )
1790
+ graph_anchor.add_argument(
1791
+ "--dry-run",
1792
+ action="store_true",
1793
+ help="Create and print the anchor without requesting publication.",
1794
+ )
1795
+ graph_anchor.add_argument(
1796
+ "--published-at",
1797
+ help="Optional RFC3339 timestamp to embed in the anchor. Defaults to the current UTC time.",
1798
+ )
1799
+ graph_anchor.add_argument(
1800
+ "--metadata",
1801
+ action="append",
1802
+ help="Optional public metadata entry in key=value form. Repeat to add multiple entries.",
1803
+ )
1804
+ graph_anchor.add_argument("--json", action="store_true", help="Emit structured JSON output.")
1805
+ graph_anchor.set_defaults(func=_cmd_graph_anchor)
1806
+
1807
+ bundle = subparsers.add_parser(
1808
+ "bundle",
1809
+ help="Export the local single-node runtime as a portable local bundle.",
1810
+ )
1811
+ bundle_subparsers = bundle.add_subparsers(dest="bundle_command", required=True)
1812
+ bundle_export = bundle_subparsers.add_parser(
1813
+ "export",
1814
+ help="Export the local runtime as a portable execution evidence bundle.",
1815
+ )
1816
+ bundle_export.add_argument(
1817
+ "--runtime-dir",
1818
+ default=str(DEFAULT_LOCAL_RUNTIME_DIR),
1819
+ help="Local runtime root to export. Defaults to artifacts/local_runtime.",
1820
+ )
1821
+ bundle_export.add_argument(
1822
+ "--output",
1823
+ help="Bundle output path. Defaults to <runtime-dir>/bundles/actenon-local-runtime.actenon. Use a .actenon or .zip path for an archive or any other path for a directory export.",
1824
+ )
1825
+ bundle_export.add_argument("--force", action="store_true", help="Replace an existing output path.")
1826
+ bundle_export.add_argument("--json", action="store_true", help="Emit structured JSON output.")
1827
+ bundle_export.set_defaults(func=_cmd_bundle_export)
1828
+ bundle_verify = bundle_subparsers.add_parser(
1829
+ "verify",
1830
+ help="Verify a portable execution evidence bundle and its declared proof chains.",
1831
+ )
1832
+ bundle_verify.add_argument(
1833
+ "bundle",
1834
+ help="Path to a .actenon archive, .zip archive, or directory bundle export.",
1835
+ )
1836
+ bundle_verify.add_argument("--json", action="store_true", help="Emit structured JSON output.")
1837
+ bundle_verify.set_defaults(func=_cmd_bundle_verify)
1838
+
1839
+ preflight = subparsers.add_parser(
1840
+ "preflight",
1841
+ help="Ask Actenon before a consequential action executes.",
1842
+ )
1843
+ preflight_subparsers = preflight.add_subparsers(dest="preflight_command", required=True)
1844
+ preflight_check = preflight_subparsers.add_parser(
1845
+ "check",
1846
+ help="Evaluate an Action Intent with the local preflight policy pack.",
1847
+ )
1848
+ preflight_check.add_argument("--intent", required=True, help="Path to the Action Intent JSON payload.")
1849
+ evidence_group = preflight_check.add_mutually_exclusive_group()
1850
+ evidence_group.add_argument("--evidence-json", help="Inline preflight evidence/context JSON object.")
1851
+ evidence_group.add_argument("--evidence-file", help="Path to a preflight evidence/context JSON object.")
1852
+ preflight_check.add_argument("--json", action="store_true", help="Emit structured JSON output.")
1853
+ preflight_check.set_defaults(func=_cmd_preflight_check)
1854
+
1855
+ preflight_explain = preflight_subparsers.add_parser(
1856
+ "explain",
1857
+ help="Explain a saved preflight decision JSON artifact.",
1858
+ )
1859
+ preflight_explain.add_argument("--decision", required=True, help="Path to the PreflightDecision JSON payload.")
1860
+ preflight_explain.add_argument("--json", action="store_true", help="Emit structured JSON output.")
1861
+ preflight_explain.set_defaults(func=_cmd_preflight_explain)
1862
+
1863
+ preflight_simulate = preflight_subparsers.add_parser(
1864
+ "simulate",
1865
+ help="Run a deterministic local preflight simulation.",
1866
+ )
1867
+ preflight_simulate.add_argument(
1868
+ "--wedge",
1869
+ required=True,
1870
+ choices=("infra_delete",),
1871
+ help="Preflight wedge to simulate. Currently supports infra_delete.",
1872
+ )
1873
+ preflight_simulate.add_argument("--json", action="store_true", help="Emit structured JSON output.")
1874
+ preflight_simulate.set_defaults(func=_cmd_preflight_simulate)
1875
+
1876
+ mcp = subparsers.add_parser(
1877
+ "mcp",
1878
+ help="Inspect local MCP proof-gate wrapper patterns.",
1879
+ )
1880
+ mcp_subparsers = mcp.add_subparsers(dest="mcp_command", required=True)
1881
+ mcp_wrap = mcp_subparsers.add_parser(
1882
+ "wrap",
1883
+ help="Print a local proof-gate wrapper pattern for a consequential MCP tool.",
1884
+ )
1885
+ mcp_wrap.add_argument(
1886
+ "--tool",
1887
+ default="filesystem.delete",
1888
+ choices=tuple(MCP_HERO_TOOL_CAPABILITIES),
1889
+ help="Consequential MCP tool to wrap. Defaults to filesystem.delete.",
1890
+ )
1891
+ mcp_wrap.add_argument(
1892
+ "--capability",
1893
+ help="Optional capability override. Defaults to the hero-path capability for --tool.",
1894
+ )
1895
+ mcp_wrap.add_argument(
1896
+ "--audience",
1897
+ default="service:actenon-mcp-consequential-tools",
1898
+ help="Protected MCP tool audience to document for the wrapper.",
1899
+ )
1900
+ mcp_wrap.add_argument("--json", action="store_true", help="Emit structured JSON output.")
1901
+ mcp_wrap.set_defaults(func=_cmd_mcp_wrap)
1902
+
1903
+ scan = subparsers.add_parser(
1904
+ "scan",
1905
+ help="Run the local execution-gap scanner against a repo, MCP tools, endpoints, artifacts, or the built-in local harness.",
1906
+ )
1907
+ scan.add_argument(
1908
+ "scan_command",
1909
+ nargs="?",
1910
+ choices=("repo", "mcp", "endpoint", "local"),
1911
+ help="Scanner mode. Use repo, mcp, endpoint, or local. Omit for legacy --target behavior.",
1912
+ )
1913
+ scan.add_argument(
1914
+ "--path",
1915
+ default=".",
1916
+ help="Path to scan for repo, MCP, or endpoint modes. Defaults to the current directory.",
1917
+ )
1918
+ scan.add_argument(
1919
+ "--target",
1920
+ choices=("artifact-pair", "replay-harness"),
1921
+ help="Scanner target. Defaults to 'replay-harness' unless --intent/--pccb inputs are supplied.",
1922
+ )
1923
+ scan.add_argument("--intent", help="Path to the Action Intent JSON payload for artifact-pair scans.")
1924
+ scan.add_argument("--pccb", help="Path to the PCCB JSON payload for artifact-pair scans.")
1925
+ scan.add_argument(
1926
+ "--audience",
1927
+ help="Protected endpoint audience for artifact-pair scans. Use '<type>:<id>' or a bare id with --audience-type.",
1928
+ )
1929
+ scan.add_argument(
1930
+ "--audience-type",
1931
+ default="service",
1932
+ help="Audience type to use when --audience is provided as a bare id. Defaults to 'service'.",
1933
+ )
1934
+ scan.add_argument(
1935
+ "--verification-time",
1936
+ default="pccb-issued-at",
1937
+ help="Artifact-pair scan time: 'now', 'pccb-issued-at', 'pccb-not-before', or an RFC3339 timestamp.",
1938
+ )
1939
+ scan.add_argument("--request-id", default="cli_scan", help="Request identifier used for artifact-pair scan context.")
1940
+ scan.add_argument("--json", action="store_true", help="Emit structured JSON output.")
1941
+ scan.add_argument("--markdown", action="store_true", help="Emit a Markdown scan report to stdout.")
1942
+ scan.add_argument(
1943
+ "--report-mode",
1944
+ choices=("executive", "developer"),
1945
+ default="executive",
1946
+ help="Markdown/text report mode. Executive leads with a plain-English action-risk summary; developer includes full finding details.",
1947
+ )
1948
+ scan.add_argument("--report-json", help="Write the JSON scan report to this path.")
1949
+ scan.add_argument("--report-markdown", help="Write the Markdown scan report to this path.")
1950
+ scan.add_argument("--badge", action="store_true", help="Print local badge Markdown in text mode.")
1951
+ scan.add_argument("--badge-output", help="Write local badge Markdown to this path.")
1952
+ scan.add_argument("--exclude", action="append", help="Exclude a path, glob, or substring from repo scanning. Repeatable.")
1953
+ scan.add_argument("--include", action="append", help="Only include matching paths/globs during repo scanning. Repeatable.")
1954
+ scan.add_argument(
1955
+ "--extensions",
1956
+ action="append",
1957
+ help="Comma-separated source extensions to scan, for example py,ts,tsx,js,go,rs,java. Repeatable.",
1958
+ )
1959
+ scan.add_argument("--max-files", type=int, help="Stop discovery after this many scan-eligible files and emit a partial report.")
1960
+ scan.add_argument(
1961
+ "--max-file-size",
1962
+ type=int,
1963
+ default=1_000_000,
1964
+ help="Skip files larger than this many bytes. Defaults to 1000000.",
1965
+ )
1966
+ scan.add_argument("--timeout-seconds", type=float, help="Stop scanning after this many seconds.")
1967
+ scan.add_argument("--progress", action="store_true", help="Emit scan progress to stderr.")
1968
+ scan.add_argument(
1969
+ "--partial-report-on-timeout",
1970
+ action="store_true",
1971
+ help="Emit a partial advisory report instead of failing when --timeout-seconds is reached.",
1972
+ )
1973
+ scan.add_argument(
1974
+ "--signer",
1975
+ default="auto",
1976
+ choices=("auto", "local"),
1977
+ help="Signature verifier to use for artifact-pair scans. 'auto' currently supports the OSS local trust root only.",
1978
+ )
1979
+ scan.set_defaults(func=_cmd_scan)
1980
+
1981
+ conformance = subparsers.add_parser("conformance", help="Run local open-source conformance checks.")
1982
+ conformance_subparsers = conformance.add_subparsers(dest="conformance_command", required=True)
1983
+ conformance_run = conformance_subparsers.add_parser("run", help="Run the repository conformance suite.")
1984
+ conformance_run.add_argument("--verbose", action="store_true", help="Show full unittest output.")
1985
+ conformance_run.add_argument(
1986
+ "--require-complete",
1987
+ action="store_true",
1988
+ help="Fail when any conformance check is skipped; required for the Actenon Verified mark.",
1989
+ )
1990
+ conformance_run.set_defaults(func=_cmd_conformance_run)
1991
+
1992
+ coverage = subparsers.add_parser(
1993
+ "coverage",
1994
+ help="Run local deterministic coverage matrices for proof-bound consequential actions.",
1995
+ )
1996
+ coverage_subparsers = coverage.add_subparsers(dest="coverage_command", required=True)
1997
+ coverage_run = coverage_subparsers.add_parser(
1998
+ "run",
1999
+ help="Run the Consequential Action Coverage Matrix.",
2000
+ )
2001
+ coverage_run.add_argument(
2002
+ "--output",
2003
+ default=str(DEFAULT_EVIDENCE_PATH),
2004
+ help=f"Evidence JSON path. Defaults to {DEFAULT_EVIDENCE_PATH}.",
2005
+ )
2006
+ coverage_run.add_argument("--json", action="store_true", help="Emit structured JSON output.")
2007
+ coverage_run.set_defaults(func=_cmd_coverage_run)
2008
+
2009
+ return parser
2010
+
2011
+
2012
+ def main(argv: Sequence[str] | None = None) -> int:
2013
+ parser = build_parser()
2014
+ args = parser.parse_args(list(argv) if argv is not None else None)
2015
+ try:
2016
+ return args.func(args)
2017
+ except Exception as exc:
2018
+ print(f"ERROR: {exc}", file=sys.stderr)
2019
+ return 1
2020
+
2021
+
2022
+ if __name__ == "__main__":
2023
+ raise SystemExit(main())