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
@@ -0,0 +1,29 @@
1
+ """Local external anchor primitives for receipt/refusal durability."""
2
+
3
+ from .local_log import LocalAppendOnlyAnchorLog
4
+ from .models import (
5
+ EXTERNAL_ANCHOR_CONTRACT,
6
+ LOCAL_APPEND_ONLY_ANCHOR_TYPE,
7
+ AnchorVerificationResult,
8
+ ExternalAnchor,
9
+ ExternalAnchorError,
10
+ ExternalAnchorFormatError,
11
+ ExternalAnchorVerificationError,
12
+ ExternalAnchorVerifier,
13
+ artifact_digests_match,
14
+ normalize_artifact_digest,
15
+ )
16
+
17
+ __all__ = [
18
+ "EXTERNAL_ANCHOR_CONTRACT",
19
+ "LOCAL_APPEND_ONLY_ANCHOR_TYPE",
20
+ "AnchorVerificationResult",
21
+ "ExternalAnchor",
22
+ "ExternalAnchorError",
23
+ "ExternalAnchorFormatError",
24
+ "ExternalAnchorVerificationError",
25
+ "ExternalAnchorVerifier",
26
+ "LocalAppendOnlyAnchorLog",
27
+ "artifact_digests_match",
28
+ "normalize_artifact_digest",
29
+ ]
@@ -0,0 +1,224 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from dataclasses import replace
5
+ from datetime import datetime
6
+ from pathlib import Path
7
+ from typing import Any, Mapping
8
+ from uuid import uuid4
9
+
10
+ from actenon.core.json import loads_no_duplicate_keys
11
+ from actenon.models.contracts import expect_mapping, format_timestamp, parse_timestamp, utc_now
12
+ from actenon.proof.canonical import sha256_hex
13
+
14
+ from .models import (
15
+ EXTERNAL_ANCHOR_CONTRACT,
16
+ LOCAL_APPEND_ONLY_ANCHOR_TYPE,
17
+ AnchorVerificationResult,
18
+ ExternalAnchor,
19
+ ExternalAnchorFormatError,
20
+ ExternalAnchorVerificationError,
21
+ artifact_digests_match,
22
+ normalize_artifact_digest,
23
+ )
24
+
25
+
26
+ LOCAL_ANCHOR_LOG_ENTRY_CONTRACT = {"name": "local_anchor_log_entry", "version": "v1"}
27
+ ENTRY_HASH_ALGORITHM = "sha-256"
28
+ from actenon.proof.canonical import CANONICALIZATION_PROFILE as ENTRY_HASH_CANONICALIZATION
29
+
30
+
31
+ def _hash_spec(value: Mapping[str, Any]) -> dict[str, str]:
32
+ return {
33
+ "algorithm": ENTRY_HASH_ALGORITHM,
34
+ "canonicalization": ENTRY_HASH_CANONICALIZATION,
35
+ "value": sha256_hex(dict(value)),
36
+ }
37
+
38
+
39
+ def _entry_hash_payload(entry: Mapping[str, Any]) -> dict[str, Any]:
40
+ payload = dict(entry)
41
+ payload.pop("entry_hash", None)
42
+ return payload
43
+
44
+
45
+ def _compute_entry_hash(entry: Mapping[str, Any]) -> dict[str, str]:
46
+ return _hash_spec(_entry_hash_payload(entry))
47
+
48
+
49
+ def _entry_anchor(entry: Mapping[str, Any]) -> ExternalAnchor:
50
+ return ExternalAnchor(
51
+ anchor_id=str(entry["anchor_id"]),
52
+ anchor_type=str(entry["anchor_type"]),
53
+ artifact_digest=normalize_artifact_digest(entry["artifact_digest"]),
54
+ anchored_at=parse_timestamp(entry["anchored_at"], "anchored_at"),
55
+ log_uri=str(entry["log_uri"]),
56
+ sequence=int(entry["sequence"]),
57
+ entry_hash=dict(expect_mapping(entry["entry_hash"], "entry_hash")),
58
+ previous_entry_hash=dict(expect_mapping(entry["previous_entry_hash"], "previous_entry_hash"))
59
+ if entry.get("previous_entry_hash") is not None
60
+ else None,
61
+ artifact_type=entry.get("artifact_type") if isinstance(entry.get("artifact_type"), str) else None,
62
+ artifact_id=entry.get("artifact_id") if isinstance(entry.get("artifact_id"), str) else None,
63
+ metadata=dict(expect_mapping(entry.get("metadata"), "metadata")) if entry.get("metadata") else {},
64
+ )
65
+
66
+
67
+ class LocalAppendOnlyAnchorLog:
68
+ """JSONL hash-chain for local receipt/refusal durability anchors.
69
+
70
+ The log is intentionally local-only. It provides an append-and-verify
71
+ primitive for development, pilots, and single-node custody workflows. It is
72
+ not a hosted trust network and does not prevent an operator with filesystem
73
+ write access from deleting or replacing the whole file.
74
+ """
75
+
76
+ def __init__(self, path: str | Path) -> None:
77
+ self.path = Path(path)
78
+
79
+ @property
80
+ def log_uri(self) -> str:
81
+ return self.path.expanduser().resolve().as_uri()
82
+
83
+ def anchor_artifact_digest(
84
+ self,
85
+ artifact_digest: Mapping[str, Any],
86
+ *,
87
+ artifact_type: str | None = None,
88
+ artifact_id: str | None = None,
89
+ anchored_at: datetime | None = None,
90
+ metadata: Mapping[str, Any] | None = None,
91
+ ) -> ExternalAnchor:
92
+ digest = normalize_artifact_digest(artifact_digest)
93
+ existing_entries = self._read_entries(validate_chain=True)
94
+ previous_hash = existing_entries[-1]["entry_hash"] if existing_entries else None
95
+ sequence = len(existing_entries) + 1
96
+ entry: dict[str, Any] = {
97
+ "contract": dict(LOCAL_ANCHOR_LOG_ENTRY_CONTRACT),
98
+ "anchor_contract": dict(EXTERNAL_ANCHOR_CONTRACT),
99
+ "anchor_id": f"anc_local_{uuid4().hex}",
100
+ "anchor_type": LOCAL_APPEND_ONLY_ANCHOR_TYPE,
101
+ "artifact_digest": digest,
102
+ "anchored_at": format_timestamp(anchored_at or utc_now()),
103
+ "log_uri": self.log_uri,
104
+ "sequence": sequence,
105
+ "previous_entry_hash": previous_hash,
106
+ }
107
+ if artifact_type is not None:
108
+ entry["artifact_type"] = artifact_type
109
+ if artifact_id is not None:
110
+ entry["artifact_id"] = artifact_id
111
+ if metadata:
112
+ entry["metadata"] = dict(metadata)
113
+ entry["entry_hash"] = _compute_entry_hash(entry)
114
+ self._append_entry(entry)
115
+ return _entry_anchor(entry)
116
+
117
+ def anchor_attestation(
118
+ self,
119
+ attestation: Any,
120
+ *,
121
+ anchored_at: datetime | None = None,
122
+ metadata: Mapping[str, Any] | None = None,
123
+ ) -> ExternalAnchor:
124
+ artifact_digest = getattr(attestation, "artifact_digest")
125
+ artifact_type = getattr(attestation, "artifact_type", None)
126
+ artifact_id = getattr(attestation, "artifact_id", None)
127
+ return self.anchor_artifact_digest(
128
+ artifact_digest,
129
+ artifact_type=artifact_type,
130
+ artifact_id=artifact_id,
131
+ anchored_at=anchored_at,
132
+ metadata=metadata,
133
+ )
134
+
135
+ def append_anchor_to_attestation(
136
+ self,
137
+ attestation: Any,
138
+ *,
139
+ anchored_at: datetime | None = None,
140
+ metadata: Mapping[str, Any] | None = None,
141
+ ) -> Any:
142
+ anchor = self.anchor_attestation(
143
+ attestation,
144
+ anchored_at=anchored_at,
145
+ metadata=metadata,
146
+ )
147
+ return replace(
148
+ attestation,
149
+ external_anchors=[*getattr(attestation, "external_anchors"), anchor.to_dict()],
150
+ )
151
+
152
+ def verify_external_anchor(
153
+ self,
154
+ anchor: ExternalAnchor | Mapping[str, Any],
155
+ *,
156
+ artifact_digest: Mapping[str, Any],
157
+ ) -> AnchorVerificationResult:
158
+ parsed_anchor = anchor if isinstance(anchor, ExternalAnchor) else ExternalAnchor.from_dict(anchor)
159
+ if parsed_anchor.anchor_type != LOCAL_APPEND_ONLY_ANCHOR_TYPE:
160
+ raise ExternalAnchorVerificationError(
161
+ f"unsupported external anchor type {parsed_anchor.anchor_type!r}"
162
+ )
163
+ if parsed_anchor.log_uri != self.log_uri:
164
+ raise ExternalAnchorVerificationError("external anchor points at a different local log")
165
+ if not artifact_digests_match(parsed_anchor.artifact_digest, artifact_digest):
166
+ raise ExternalAnchorVerificationError(
167
+ "external anchor artifact_digest does not match the signed artifact_digest"
168
+ )
169
+
170
+ entries = self._read_entries(validate_chain=True)
171
+ if parsed_anchor.sequence > len(entries):
172
+ raise ExternalAnchorVerificationError("external anchor sequence is not present in the local log")
173
+ entry = entries[parsed_anchor.sequence - 1]
174
+ entry_anchor = _entry_anchor(entry)
175
+ if entry_anchor != parsed_anchor:
176
+ raise ExternalAnchorVerificationError("external anchor does not match the local log entry")
177
+ return AnchorVerificationResult(anchor=parsed_anchor, anchored_at=parsed_anchor.anchored_at)
178
+
179
+ def _append_entry(self, entry: Mapping[str, Any]) -> None:
180
+ self.path.parent.mkdir(parents=True, exist_ok=True)
181
+ with self.path.open("a", encoding="utf-8") as handle:
182
+ handle.write(json.dumps(dict(entry), ensure_ascii=False, separators=(",", ":"), sort_keys=True))
183
+ handle.write("\n")
184
+
185
+ def _read_entries(self, *, validate_chain: bool) -> list[dict[str, Any]]:
186
+ if not self.path.exists():
187
+ return []
188
+ entries: list[dict[str, Any]] = []
189
+ with self.path.open("r", encoding="utf-8") as handle:
190
+ for line_number, line in enumerate(handle, start=1):
191
+ stripped = line.strip()
192
+ if not stripped:
193
+ continue
194
+ try:
195
+ entry = loads_no_duplicate_keys(stripped)
196
+ except ValueError as exc:
197
+ raise ExternalAnchorFormatError(
198
+ f"local anchor log line {line_number} is not valid JSON"
199
+ ) from exc
200
+ if not isinstance(entry, dict):
201
+ raise ExternalAnchorFormatError(
202
+ f"local anchor log line {line_number} must be a JSON object"
203
+ )
204
+ entries.append(entry)
205
+ if validate_chain:
206
+ self._validate_chain(entries)
207
+ return entries
208
+
209
+ def _validate_chain(self, entries: list[dict[str, Any]]) -> None:
210
+ previous_hash: dict[str, str] | None = None
211
+ for index, entry in enumerate(entries, start=1):
212
+ contract = entry.get("contract")
213
+ if contract != LOCAL_ANCHOR_LOG_ENTRY_CONTRACT:
214
+ raise ExternalAnchorFormatError("local anchor log entry contract is unsupported")
215
+ if entry.get("anchor_type") != LOCAL_APPEND_ONLY_ANCHOR_TYPE:
216
+ raise ExternalAnchorFormatError("local anchor log entry anchor_type is unsupported")
217
+ if entry.get("sequence") != index:
218
+ raise ExternalAnchorFormatError("local anchor log sequence is not contiguous")
219
+ if entry.get("previous_entry_hash") != previous_hash:
220
+ raise ExternalAnchorVerificationError("local anchor log hash chain is broken")
221
+ expected_hash = _compute_entry_hash(entry)
222
+ if entry.get("entry_hash") != expected_hash:
223
+ raise ExternalAnchorVerificationError("local anchor log entry hash does not match")
224
+ previous_hash = expected_hash
@@ -0,0 +1,153 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from datetime import datetime
5
+ from typing import Any, Mapping, Protocol
6
+
7
+ from actenon.models.contracts import expect_mapping, expect_string, format_timestamp, parse_timestamp
8
+
9
+
10
+ EXTERNAL_ANCHOR_CONTRACT = {"name": "external_anchor", "version": "v1"}
11
+ LOCAL_APPEND_ONLY_ANCHOR_TYPE = "local_append_only_log"
12
+
13
+
14
+ class ExternalAnchorError(ValueError):
15
+ """Base class for external anchor parsing and verification errors."""
16
+
17
+
18
+ class ExternalAnchorFormatError(ExternalAnchorError):
19
+ """Raised when an external anchor has an unsupported wire shape."""
20
+
21
+
22
+ class ExternalAnchorVerificationError(ExternalAnchorError):
23
+ """Raised when an external anchor does not verify against its local source."""
24
+
25
+
26
+ def normalize_artifact_digest(raw: Mapping[str, Any] | Any) -> dict[str, str]:
27
+ data = expect_mapping(raw, "artifact_digest")
28
+ normalized = {
29
+ "algorithm": expect_string(data.get("algorithm"), "artifact_digest.algorithm"),
30
+ "value": expect_string(data.get("value"), "artifact_digest.value"),
31
+ }
32
+ canonicalization = data.get("canonicalization")
33
+ if canonicalization is not None:
34
+ normalized["canonicalization"] = expect_string(
35
+ canonicalization,
36
+ "artifact_digest.canonicalization",
37
+ )
38
+ return normalized
39
+
40
+
41
+ def artifact_digests_match(left: Mapping[str, Any], right: Mapping[str, Any]) -> bool:
42
+ left_digest = normalize_artifact_digest(left)
43
+ right_digest = normalize_artifact_digest(right)
44
+ if left_digest["algorithm"] != right_digest["algorithm"]:
45
+ return False
46
+ if left_digest["value"] != right_digest["value"]:
47
+ return False
48
+ left_canonicalization = left_digest.get("canonicalization")
49
+ right_canonicalization = right_digest.get("canonicalization")
50
+ if left_canonicalization is not None and right_canonicalization is not None:
51
+ return left_canonicalization == right_canonicalization
52
+ return True
53
+
54
+
55
+ def _parse_hash_spec(raw: Any, field_name: str) -> dict[str, str]:
56
+ data = expect_mapping(raw, field_name)
57
+ parsed = {
58
+ "algorithm": expect_string(data.get("algorithm"), f"{field_name}.algorithm"),
59
+ "value": expect_string(data.get("value"), f"{field_name}.value"),
60
+ }
61
+ canonicalization = data.get("canonicalization")
62
+ if canonicalization is not None:
63
+ parsed["canonicalization"] = expect_string(
64
+ canonicalization,
65
+ f"{field_name}.canonicalization",
66
+ )
67
+ return parsed
68
+
69
+
70
+ def _parse_positive_sequence(raw: Any) -> int:
71
+ if not isinstance(raw, int) or raw <= 0:
72
+ raise ExternalAnchorFormatError("external anchor sequence must be a positive integer")
73
+ return raw
74
+
75
+
76
+ @dataclass(frozen=True)
77
+ class ExternalAnchor:
78
+ anchor_id: str
79
+ anchor_type: str
80
+ artifact_digest: dict[str, str]
81
+ anchored_at: datetime
82
+ log_uri: str
83
+ sequence: int
84
+ entry_hash: dict[str, str]
85
+ previous_entry_hash: dict[str, str] | None = None
86
+ artifact_type: str | None = None
87
+ artifact_id: str | None = None
88
+ metadata: dict[str, Any] = field(default_factory=dict)
89
+
90
+ @classmethod
91
+ def from_dict(cls, raw: Mapping[str, Any] | Any) -> "ExternalAnchor":
92
+ data = expect_mapping(raw, "external_anchor")
93
+ contract = expect_mapping(data.get("contract"), "external_anchor.contract")
94
+ if (
95
+ contract.get("name") != EXTERNAL_ANCHOR_CONTRACT["name"]
96
+ or contract.get("version") != EXTERNAL_ANCHOR_CONTRACT["version"]
97
+ ):
98
+ raise ExternalAnchorFormatError("external anchor contract must declare external_anchor v1")
99
+ previous_entry_hash = data.get("previous_entry_hash")
100
+ metadata = data.get("metadata", {})
101
+ return cls(
102
+ anchor_id=expect_string(data.get("anchor_id"), "external_anchor.anchor_id"),
103
+ anchor_type=expect_string(data.get("anchor_type"), "external_anchor.anchor_type"),
104
+ artifact_digest=normalize_artifact_digest(data.get("artifact_digest")),
105
+ anchored_at=parse_timestamp(data.get("anchored_at"), "external_anchor.anchored_at"),
106
+ log_uri=expect_string(data.get("log_uri"), "external_anchor.log_uri"),
107
+ sequence=_parse_positive_sequence(data.get("sequence")),
108
+ entry_hash=_parse_hash_spec(data.get("entry_hash"), "external_anchor.entry_hash"),
109
+ previous_entry_hash=_parse_hash_spec(previous_entry_hash, "external_anchor.previous_entry_hash")
110
+ if previous_entry_hash is not None
111
+ else None,
112
+ artifact_type=data.get("artifact_type") if isinstance(data.get("artifact_type"), str) else None,
113
+ artifact_id=data.get("artifact_id") if isinstance(data.get("artifact_id"), str) else None,
114
+ metadata=dict(expect_mapping(metadata, "external_anchor.metadata")) if metadata else {},
115
+ )
116
+
117
+ def to_dict(self) -> dict[str, Any]:
118
+ payload: dict[str, Any] = {
119
+ "contract": dict(EXTERNAL_ANCHOR_CONTRACT),
120
+ "anchor_id": self.anchor_id,
121
+ "anchor_type": self.anchor_type,
122
+ "artifact_digest": dict(self.artifact_digest),
123
+ "anchored_at": format_timestamp(self.anchored_at),
124
+ "log_uri": self.log_uri,
125
+ "sequence": self.sequence,
126
+ "entry_hash": dict(self.entry_hash),
127
+ }
128
+ if self.previous_entry_hash is not None:
129
+ payload["previous_entry_hash"] = dict(self.previous_entry_hash)
130
+ if self.artifact_type is not None:
131
+ payload["artifact_type"] = self.artifact_type
132
+ if self.artifact_id is not None:
133
+ payload["artifact_id"] = self.artifact_id
134
+ if self.metadata:
135
+ payload["metadata"] = dict(self.metadata)
136
+ return payload
137
+
138
+
139
+ @dataclass(frozen=True)
140
+ class AnchorVerificationResult:
141
+ anchor: ExternalAnchor
142
+ anchored_at: datetime
143
+ status: str = "verified"
144
+
145
+
146
+ class ExternalAnchorVerifier(Protocol):
147
+ def verify_external_anchor(
148
+ self,
149
+ anchor: ExternalAnchor | Mapping[str, Any],
150
+ *,
151
+ artifact_digest: Mapping[str, Any],
152
+ ) -> AnchorVerificationResult:
153
+ """Verify an external anchor against the expected signed artifact digest."""
@@ -0,0 +1,12 @@
1
+ """Intake services for external action-intent payloads."""
2
+
3
+ from .invoice_payment import build_invoice_payment_action_intent_payload, compute_invoice_payment_batch_hash
4
+ from .intake import ActionIntentIntakeService
5
+ from .refund import build_refund_action_intent_payload
6
+
7
+ __all__ = [
8
+ "ActionIntentIntakeService",
9
+ "build_invoice_payment_action_intent_payload",
10
+ "build_refund_action_intent_payload",
11
+ "compute_invoice_payment_batch_hash",
12
+ ]
actenon/api/intake.py ADDED
@@ -0,0 +1,39 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Mapping
4
+
5
+ from actenon.core.errors import ContractValidationError
6
+ from actenon.models.contracts import ActionIntent, Violation
7
+
8
+
9
+ class ActionIntentIntakeService:
10
+ """Parses and validates external Action Intent payloads."""
11
+
12
+ def parse(self, payload: Mapping[str, Any]) -> ActionIntent:
13
+ try:
14
+ intent = ActionIntent.from_dict(payload)
15
+ except ValueError as exc:
16
+ violation = Violation(code="INVALID_CONTRACT", message=str(exc))
17
+ raise ContractValidationError("Action Intent contract validation failed", violations=(violation,)) from exc
18
+
19
+ violations: list[Violation] = []
20
+ if intent.expires_at <= intent.issued_at:
21
+ violations.append(
22
+ Violation(
23
+ code="INVALID_WINDOW",
24
+ field_path="expires_at",
25
+ message="expires_at must be later than issued_at",
26
+ )
27
+ )
28
+ if not intent.action.parameters:
29
+ violations.append(
30
+ Violation(
31
+ code="MISSING_ACTION_PARAMETERS",
32
+ field_path="action.parameters",
33
+ message="action.parameters must contain at least one value",
34
+ )
35
+ )
36
+ if violations:
37
+ raise ContractValidationError("Action Intent semantic validation failed", violations=tuple(violations))
38
+ return intent
39
+
@@ -0,0 +1,143 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import date, datetime, timedelta
4
+ from typing import Any, Iterable
5
+
6
+ from actenon.proof.canonical import sha256_hex
7
+
8
+
9
+ def normalize_invoice_ids(invoice_ids: Iterable[str]) -> list[str]:
10
+ normalized = sorted({str(item) for item in invoice_ids if str(item)})
11
+ if not normalized:
12
+ raise ValueError("invoice_ids must contain at least one non-empty value")
13
+ return normalized
14
+
15
+
16
+ def normalize_payment_date(value: date | str) -> str:
17
+ if isinstance(value, date):
18
+ return value.isoformat()
19
+ parsed = date.fromisoformat(value)
20
+ return parsed.isoformat()
21
+
22
+
23
+ def compute_invoice_payment_batch_hash(
24
+ *,
25
+ payer_entity_id: str,
26
+ supplier_id: str,
27
+ bank_account_reference: str,
28
+ invoice_ids: Iterable[str],
29
+ amount_minor: int,
30
+ currency: str,
31
+ payment_date: date | str,
32
+ payment_batch_id: str,
33
+ ) -> str:
34
+ hash_input = {
35
+ "payer_entity_id": payer_entity_id,
36
+ "supplier_id": supplier_id,
37
+ "bank_account_reference": bank_account_reference,
38
+ "invoice_ids": normalize_invoice_ids(invoice_ids),
39
+ "amount_minor": amount_minor,
40
+ "currency": currency,
41
+ "payment_date": normalize_payment_date(payment_date),
42
+ "payment_batch_id": payment_batch_id,
43
+ }
44
+ return f"batch_{sha256_hex(hash_input)}"
45
+
46
+
47
+ def build_invoice_payment_action_intent_payload(
48
+ *,
49
+ intent_id: str,
50
+ tenant_id: str,
51
+ requester_id: str,
52
+ payer_entity_id: str,
53
+ supplier_id: str,
54
+ bank_account_reference: str,
55
+ invoice_ids: Iterable[str],
56
+ amount_minor: int,
57
+ currency: str,
58
+ payment_date: date | str,
59
+ payment_batch_id: str,
60
+ issued_at: datetime,
61
+ proposer_id: str | None = None,
62
+ ttl_seconds: int = 300,
63
+ justification: str | None = None,
64
+ evidence_refs: list[dict[str, Any]] | None = None,
65
+ context: dict[str, Any] | None = None,
66
+ metadata: dict[str, Any] | None = None,
67
+ batch_hash: str | None = None,
68
+ ) -> dict[str, Any]:
69
+ normalized_invoice_ids = normalize_invoice_ids(invoice_ids)
70
+ normalized_payment_date = normalize_payment_date(payment_date)
71
+ resolved_batch_hash = batch_hash or compute_invoice_payment_batch_hash(
72
+ payer_entity_id=payer_entity_id,
73
+ supplier_id=supplier_id,
74
+ bank_account_reference=bank_account_reference,
75
+ invoice_ids=normalized_invoice_ids,
76
+ amount_minor=amount_minor,
77
+ currency=currency,
78
+ payment_date=normalized_payment_date,
79
+ payment_batch_id=payment_batch_id,
80
+ )
81
+ resolved_proposer_id = proposer_id or requester_id
82
+ idempotency_key_input = {
83
+ "payment_batch_id": payment_batch_id,
84
+ "invoice_ids": normalized_invoice_ids,
85
+ "amount_minor": amount_minor,
86
+ "currency": currency,
87
+ "payer_entity_id": payer_entity_id,
88
+ "supplier_id": supplier_id,
89
+ }
90
+ idempotency_key = f"ipy_{sha256_hex(idempotency_key_input)}"
91
+ expires_at = issued_at + timedelta(seconds=ttl_seconds)
92
+
93
+ payload: dict[str, Any] = {
94
+ "contract": {"name": "action_intent", "version": "v1"},
95
+ "intent_id": intent_id,
96
+ "idempotency_key": idempotency_key,
97
+ "issued_at": issued_at.isoformat().replace("+00:00", "Z"),
98
+ "expires_at": expires_at.isoformat().replace("+00:00", "Z"),
99
+ "tenant": {"tenant_id": tenant_id},
100
+ "requester": {"type": "service", "id": requester_id},
101
+ "action": {
102
+ "name": "invoice_payment.execute",
103
+ "capability": "invoice_payment.execute",
104
+ "parameters": {
105
+ "payer_entity_id": payer_entity_id,
106
+ "supplier_id": supplier_id,
107
+ "bank_account_reference": bank_account_reference,
108
+ "invoice_ids": normalized_invoice_ids,
109
+ "amount_minor": amount_minor,
110
+ "currency": currency,
111
+ "payment_date": normalized_payment_date,
112
+ "payment_batch_id": payment_batch_id,
113
+ "batch_hash": resolved_batch_hash,
114
+ "proposer_id": resolved_proposer_id,
115
+ },
116
+ "constraints": {
117
+ "exact_payer_entity_id": payer_entity_id,
118
+ "exact_supplier_id": supplier_id,
119
+ "exact_bank_account_reference": bank_account_reference,
120
+ "exact_invoice_ids": normalized_invoice_ids,
121
+ "exact_amount_minor": amount_minor,
122
+ "exact_currency": currency,
123
+ "exact_payment_date": normalized_payment_date,
124
+ "exact_payment_batch_id": payment_batch_id,
125
+ "exact_batch_hash": resolved_batch_hash,
126
+ },
127
+ "scope": {
128
+ "target_resource_type": "payment_batch",
129
+ "single_use": True,
130
+ },
131
+ },
132
+ "target": {
133
+ "resource_type": "payment_batch",
134
+ "resource_id": payment_batch_id,
135
+ },
136
+ "context": context or {},
137
+ "metadata": metadata or {},
138
+ }
139
+ if justification is not None:
140
+ payload["justification"] = justification
141
+ if evidence_refs:
142
+ payload["evidence_refs"] = evidence_refs
143
+ return payload
actenon/api/refund.py ADDED
@@ -0,0 +1,57 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime, timedelta
4
+ from typing import Any
5
+
6
+
7
+ def build_refund_action_intent_payload(
8
+ *,
9
+ intent_id: str,
10
+ tenant_id: str,
11
+ requester_id: str,
12
+ payment_id: str,
13
+ amount_minor: int,
14
+ currency: str,
15
+ issued_at: datetime,
16
+ ttl_seconds: int = 300,
17
+ justification: str | None = None,
18
+ metadata: dict[str, Any] | None = None,
19
+ context: dict[str, Any] | None = None,
20
+ evidence_refs: list[dict[str, Any]] | None = None,
21
+ ) -> dict[str, Any]:
22
+ expires_at = issued_at + timedelta(seconds=ttl_seconds)
23
+ payload: dict[str, Any] = {
24
+ "contract": {"name": "action_intent", "version": "v1"},
25
+ "intent_id": intent_id,
26
+ "issued_at": issued_at.isoformat().replace("+00:00", "Z"),
27
+ "expires_at": expires_at.isoformat().replace("+00:00", "Z"),
28
+ "tenant": {"tenant_id": tenant_id},
29
+ "requester": {"type": "service", "id": requester_id},
30
+ "action": {
31
+ "name": "refund.create",
32
+ "capability": "refund.execute",
33
+ "parameters": {
34
+ "amount_minor": amount_minor,
35
+ "currency": currency,
36
+ },
37
+ "constraints": {
38
+ "exact_amount_minor": amount_minor,
39
+ "exact_currency": currency,
40
+ },
41
+ "scope": {
42
+ "target_resource_type": "payment",
43
+ "single_use": True,
44
+ },
45
+ },
46
+ "target": {
47
+ "resource_type": "payment",
48
+ "resource_id": payment_id,
49
+ },
50
+ "metadata": metadata or {},
51
+ "context": context or {},
52
+ }
53
+ if justification is not None:
54
+ payload["justification"] = justification
55
+ if evidence_refs:
56
+ payload["evidence_refs"] = evidence_refs
57
+ return payload