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/__init__.py ADDED
@@ -0,0 +1,11 @@
1
+ """Public package front door for the Actenon kernel."""
2
+
3
+ from .gate import ActenonGate, GateOutcome
4
+ from .models import ActionIntent, PCCB
5
+
6
+ __all__ = [
7
+ "ActenonGate",
8
+ "ActionIntent",
9
+ "GateOutcome",
10
+ "PCCB",
11
+ ]
@@ -0,0 +1,20 @@
1
+ """Provider adapter interfaces for protected consequential actions."""
2
+
3
+ from .base import (
4
+ ProviderAdapter,
5
+ ProviderAdapterContext,
6
+ ProviderAdapterRequest,
7
+ ProviderAdapterResult,
8
+ ReconciliationCapableProviderAdapter,
9
+ )
10
+ from .edge import EdgeConfigurationError, ProtectedEdge
11
+
12
+ __all__ = [
13
+ "EdgeConfigurationError",
14
+ "ProtectedEdge",
15
+ "ProviderAdapter",
16
+ "ProviderAdapterContext",
17
+ "ProviderAdapterRequest",
18
+ "ProviderAdapterResult",
19
+ "ReconciliationCapableProviderAdapter",
20
+ ]
@@ -0,0 +1,30 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any, Mapping
5
+
6
+ from actenon.models import PCCB
7
+ from actenon.preflight import PreflightEvidence
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class AdapterAuthorization:
12
+ """Proof material supplied by framework runtime context, never model input."""
13
+
14
+ proof: Mapping[str, Any] | PCCB | None
15
+ evidence: Mapping[str, Any] | PreflightEvidence | None = None
16
+
17
+
18
+ def authorization_from_mapping(raw: Any) -> AdapterAuthorization:
19
+ if not isinstance(raw, Mapping):
20
+ return AdapterAuthorization(proof=None)
21
+ evidence = raw.get("evidence")
22
+ if evidence is not None and not isinstance(
23
+ evidence,
24
+ (Mapping, PreflightEvidence),
25
+ ):
26
+ evidence = None
27
+ return AdapterAuthorization(
28
+ proof=raw.get("proof"),
29
+ evidence=evidence,
30
+ )
@@ -0,0 +1,104 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from datetime import datetime
5
+ from typing import TYPE_CHECKING, Any, Protocol
6
+
7
+ from actenon.models.contracts import ActionSpec, PCCB, TargetRef
8
+ from actenon.models.runtime import ProtectedExecutionRequest
9
+
10
+ if TYPE_CHECKING: # pragma: no cover
11
+ from actenon.reconciliation.base import ProviderReconciliationSnapshot
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class ProviderAdapterContext:
16
+ """Execution context passed from the kernel to a provider adapter.
17
+
18
+ The context contains only execution-relevant identifiers and timing metadata.
19
+ It intentionally avoids embedding host-specific control-plane state so that
20
+ adapter implementations remain portable across integration environments.
21
+ """
22
+
23
+ request_id: str
24
+ intent_id: str
25
+ pccb_id: str
26
+ tenant_id: str
27
+ subject_id: str
28
+ audience: str
29
+ executed_at: datetime
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class ProviderAdapterRequest:
34
+ """Portable provider-adapter request produced after proof verification.
35
+
36
+ Adapters receive a normalized request derived from the protected execution
37
+ path. The request carries only the action, target, and execution context the
38
+ adapter needs to perform a side effect or translate the call into a
39
+ provider-specific operation.
40
+ """
41
+
42
+ action: ActionSpec
43
+ target: TargetRef
44
+ context: ProviderAdapterContext
45
+ pccb: PCCB
46
+ metadata: dict[str, Any] = field(default_factory=dict)
47
+
48
+ @classmethod
49
+ def from_execution_request(cls, request: ProtectedExecutionRequest, *, metadata: dict[str, Any] | None = None) -> "ProviderAdapterRequest":
50
+ """Build a portable adapter request from a verified execution request."""
51
+
52
+ return cls(
53
+ action=request.intent.action,
54
+ target=request.intent.target,
55
+ context=ProviderAdapterContext(
56
+ request_id=request.context.request_id,
57
+ intent_id=request.intent.intent_id,
58
+ pccb_id=request.pccb.pccb_id,
59
+ tenant_id=request.intent.tenant.tenant_id,
60
+ subject_id=request.intent.requester.id,
61
+ audience=f"{request.context.audience.type}:{request.context.audience.id}",
62
+ executed_at=request.context.now,
63
+ ),
64
+ pccb=request.pccb,
65
+ metadata=dict(metadata or {}),
66
+ )
67
+
68
+
69
+ @dataclass(frozen=True)
70
+ class ProviderAdapterResult:
71
+ """Normalized side-effect result returned by a provider adapter.
72
+
73
+ The adapter result is intentionally generic. It lets the kernel or a future
74
+ reconciliation layer capture stable references without committing this
75
+ repository to a particular provider or control-plane product model.
76
+ """
77
+
78
+ provider_reference: str | None
79
+ provider_state: str
80
+ side_effect_reference: str | None = None
81
+ details: dict[str, Any] = field(default_factory=dict)
82
+
83
+
84
+ class ProviderAdapter(Protocol):
85
+ """Protocol for execution adapters that call consequential downstream systems.
86
+
87
+ Concrete provider integrations belong outside the open kernel. This
88
+ interface gives the paid or external ecosystem a stable execution boundary
89
+ without embedding those integrations here.
90
+ """
91
+
92
+ adapter_id: str
93
+ provider_name: str
94
+ supported_capabilities: tuple[str, ...]
95
+
96
+ def execute(self, request: ProviderAdapterRequest) -> ProviderAdapterResult:
97
+ """Execute a verified protected action against a downstream provider."""
98
+
99
+
100
+ class ReconciliationCapableProviderAdapter(ProviderAdapter, Protocol):
101
+ """Optional provider-adapter extension for reconciliation-aware integrations."""
102
+
103
+ def fetch_reconciliation_snapshot(self, provider_reference: str) -> "ProviderReconciliationSnapshot":
104
+ """Return the latest provider view for a previously executed side effect."""
@@ -0,0 +1,129 @@
1
+ """Hardened resource-side adapter for exact-request proof enforcement."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import copy
6
+ import inspect
7
+ from typing import Any, Callable, Mapping, Union
8
+
9
+ from actenon.credentials import BrokeredCredential
10
+ from actenon.gate import ActenonGate, GateOutcome
11
+ from actenon.models import ActionIntent, AudienceRef, PCCB
12
+ from actenon.preflight import PreflightEvidence
13
+ from actenon.proof import canonicalize_json
14
+
15
+
16
+ RawRequestedAction = Mapping[str, Any]
17
+ IntentBuilder = Callable[
18
+ [RawRequestedAction],
19
+ Union[ActionIntent, dict[str, Any]],
20
+ ]
21
+ BrokeredBackend = Callable[[ActionIntent, BrokeredCredential], Any]
22
+
23
+
24
+ class EdgeConfigurationError(ValueError):
25
+ """Raised when an edge cannot bind every requested field into its intent."""
26
+
27
+
28
+ class ProtectedEdge:
29
+ """Verify an agent request before a brokered resource-side side effect.
30
+
31
+ The raw request is snapshotted and must appear exactly in
32
+ ``ActionIntent.action.parameters``. The backend receives only the verified
33
+ request intent and a brokered credential; it never receives the PCCB or an
34
+ action reconstructed from proof contents.
35
+ """
36
+
37
+ def __init__(
38
+ self,
39
+ gate: ActenonGate,
40
+ *,
41
+ intent_builder: IntentBuilder,
42
+ backend: BrokeredBackend,
43
+ audience: AudienceRef | str | None = None,
44
+ ) -> None:
45
+ if not isinstance(gate, ActenonGate):
46
+ raise TypeError("gate must be an ActenonGate")
47
+ if not callable(intent_builder):
48
+ raise TypeError("intent_builder must be callable")
49
+ if not callable(backend):
50
+ raise TypeError("backend must be callable")
51
+ if inspect.iscoroutinefunction(intent_builder):
52
+ raise TypeError("ProtectedEdge currently requires a synchronous intent_builder")
53
+ if inspect.iscoroutinefunction(backend):
54
+ raise TypeError("ProtectedEdge currently requires a synchronous backend")
55
+ self._gate = gate
56
+ self._intent_builder = intent_builder
57
+ self._backend = backend
58
+ self._audience = audience
59
+
60
+ def intent_for(self, requested_action: RawRequestedAction) -> ActionIntent:
61
+ """Build a detached Action Intent that binds the complete raw request."""
62
+
63
+ if isinstance(requested_action, ActionIntent) or not isinstance(
64
+ requested_action, Mapping
65
+ ):
66
+ raise TypeError("requested_action must be a raw mapping, not an ActionIntent")
67
+ request_snapshot = copy.deepcopy(dict(requested_action))
68
+ try:
69
+ request_canonical = canonicalize_json(request_snapshot)
70
+ except (TypeError, ValueError, RecursionError) as exc:
71
+ raise EdgeConfigurationError(
72
+ "requested_action must be valid canonical JSON data"
73
+ ) from exc
74
+
75
+ built = self._intent_builder(copy.deepcopy(request_snapshot))
76
+ try:
77
+ intent = (
78
+ ActionIntent.from_dict(built.to_dict())
79
+ if isinstance(built, ActionIntent)
80
+ else ActionIntent.from_dict(built)
81
+ )
82
+ except (TypeError, ValueError) as exc:
83
+ raise EdgeConfigurationError(
84
+ "intent_builder must return a valid ActionIntent or Action Intent mapping"
85
+ ) from exc
86
+
87
+ try:
88
+ bound_parameters = canonicalize_json(intent.action.parameters)
89
+ except (TypeError, ValueError, RecursionError) as exc:
90
+ raise EdgeConfigurationError(
91
+ "intent_builder produced action parameters that are not canonical JSON"
92
+ ) from exc
93
+ if bound_parameters != request_canonical:
94
+ raise EdgeConfigurationError(
95
+ "intent_builder must bind the complete raw requested action exactly "
96
+ "as ActionIntent.action.parameters"
97
+ )
98
+ return intent
99
+
100
+ def execute(
101
+ self,
102
+ requested_action: RawRequestedAction,
103
+ proof: Mapping[str, Any] | PCCB | None,
104
+ *,
105
+ evidence: Mapping[str, Any] | PreflightEvidence | None = None,
106
+ ) -> GateOutcome:
107
+ """Execute only the raw request proven by the supplied PCCB."""
108
+
109
+ intent = self.intent_for(requested_action)
110
+ return self._gate.protect(
111
+ intent,
112
+ proof,
113
+ lambda protected_request, credential: self._backend(
114
+ protected_request.intent,
115
+ credential,
116
+ ),
117
+ audience=self._audience,
118
+ evidence=evidence,
119
+ _missing_proof_reason_code="NO_PROOF",
120
+ )
121
+
122
+
123
+ __all__ = [
124
+ "BrokeredBackend",
125
+ "EdgeConfigurationError",
126
+ "IntentBuilder",
127
+ "ProtectedEdge",
128
+ "RawRequestedAction",
129
+ ]
@@ -0,0 +1,161 @@
1
+ """FastAPI dependency for proof-gated endpoint execution."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ import inspect
7
+ import json
8
+ from typing import Any, Callable, Mapping, Optional, Union
9
+
10
+ from actenon.core import ContractValidationError
11
+ from actenon.gate import ActenonGate, GateOutcome
12
+ from actenon.models import ActionIntent, PCCB
13
+ from actenon.preflight import PreflightEvidence
14
+
15
+ try:
16
+ from fastapi import HTTPException, Request
17
+ from pydantic import ValidationError
18
+ except ImportError as exc: # pragma: no cover - exercised without the optional extra
19
+ raise ImportError(
20
+ "Actenon's FastAPI adapter requires the 'fastapi' extra: "
21
+ "python -m pip install 'actenon-kernel[fastapi]'"
22
+ ) from exc
23
+
24
+
25
+ ACTENON_PROOF_HEADER = "X-Actenon-Proof"
26
+ ACTENON_EVIDENCE_HEADER = "X-Actenon-Evidence"
27
+
28
+ ActionBuilder = Callable[[Mapping[str, Any]], Union[ActionIntent, dict[str, Any]]]
29
+ EvidenceBuilder = Callable[
30
+ [Mapping[str, Any]],
31
+ Optional[Union[Mapping[str, Any], PreflightEvidence]],
32
+ ]
33
+ SideEffect = Callable[[Mapping[str, Any]], Any]
34
+
35
+
36
+ def encode_json_header(value: Mapping[str, Any] | PCCB) -> str:
37
+ payload = value.to_dict() if isinstance(value, PCCB) else dict(value)
38
+ encoded = base64.urlsafe_b64encode(
39
+ json.dumps(payload, separators=(",", ":"), sort_keys=True).encode("utf-8")
40
+ )
41
+ return encoded.decode("ascii").rstrip("=")
42
+
43
+
44
+ def _decode_json_header(value: str, *, field_name: str) -> dict[str, Any]:
45
+ try:
46
+ padded = value + ("=" * (-len(value) % 4))
47
+ decoded = base64.urlsafe_b64decode(padded.encode("ascii"))
48
+ payload = json.loads(decoded.decode("utf-8"))
49
+ except (ValueError, UnicodeError, json.JSONDecodeError) as exc:
50
+ raise ValueError(f"{field_name} must be base64url-encoded JSON") from exc
51
+ if not isinstance(payload, dict):
52
+ raise ValueError(f"{field_name} must decode to a JSON object")
53
+ return payload
54
+
55
+
56
+ def fastapi_dependency(
57
+ gate: ActenonGate,
58
+ *,
59
+ action_builder: ActionBuilder,
60
+ side_effect: SideEffect,
61
+ body_model: type[Any] | None = None,
62
+ audience: Optional[str] = None,
63
+ evidence_builder: Optional[EvidenceBuilder] = None,
64
+ proof_header: str = ACTENON_PROOF_HEADER,
65
+ evidence_header: str = ACTENON_EVIDENCE_HEADER,
66
+ ) -> Callable[[Request], Any]:
67
+ """Build a dependency that executes the protected operation before the route.
68
+
69
+ The HTTP body contains only domain fields. The proof and optional local
70
+ evidence travel in base64url-encoded headers outside that domain schema.
71
+ """
72
+
73
+ if inspect.iscoroutinefunction(side_effect):
74
+ raise TypeError(
75
+ "fastapi_dependency currently requires a synchronous side_effect"
76
+ )
77
+
78
+ async def dependency(request: Request) -> GateOutcome:
79
+ try:
80
+ body = await request.json()
81
+ except (ValueError, UnicodeError, json.JSONDecodeError) as exc:
82
+ raise HTTPException(
83
+ status_code=400,
84
+ detail={"error": "INVALID_JSON_BODY", "message": str(exc)},
85
+ ) from exc
86
+ if not isinstance(body, dict):
87
+ raise HTTPException(
88
+ status_code=400,
89
+ detail={
90
+ "error": "INVALID_JSON_BODY",
91
+ "message": "The protected endpoint body must be a JSON object.",
92
+ },
93
+ )
94
+ if body_model is not None:
95
+ try:
96
+ if hasattr(body_model, "model_validate"):
97
+ validated = body_model.model_validate(body)
98
+ body = validated.model_dump()
99
+ else: # pragma: no cover - Pydantic v1 compatibility
100
+ validated = body_model.parse_obj(body)
101
+ body = validated.dict()
102
+ except ValidationError as exc:
103
+ raise HTTPException(status_code=422, detail=exc.errors()) from exc
104
+
105
+ proof: dict[str, Any] | None = None
106
+ raw_proof = request.headers.get(proof_header)
107
+ if raw_proof:
108
+ try:
109
+ proof = _decode_json_header(raw_proof, field_name=proof_header)
110
+ except ValueError:
111
+ # Let the gate emit the canonical schema Refusal and refused Receipt.
112
+ proof = {}
113
+
114
+ if evidence_builder is not None:
115
+ evidence = evidence_builder(body)
116
+ else:
117
+ evidence = None
118
+ raw_evidence = request.headers.get(evidence_header)
119
+ if raw_evidence:
120
+ try:
121
+ evidence = _decode_json_header(
122
+ raw_evidence,
123
+ field_name=evidence_header,
124
+ )
125
+ except ValueError as exc:
126
+ raise HTTPException(
127
+ status_code=400,
128
+ detail={
129
+ "error": "INVALID_ACTENON_EVIDENCE",
130
+ "message": str(exc),
131
+ },
132
+ ) from exc
133
+
134
+ try:
135
+ action = action_builder(body)
136
+ except (ContractValidationError, KeyError, TypeError, ValueError) as exc:
137
+ raise HTTPException(
138
+ status_code=400,
139
+ detail={"error": "INVALID_ACTION", "message": str(exc)},
140
+ ) from exc
141
+
142
+ outcome = gate.protect(
143
+ action,
144
+ proof,
145
+ lambda: side_effect(body),
146
+ audience=audience,
147
+ evidence=evidence,
148
+ )
149
+ if not outcome.ok:
150
+ raise HTTPException(status_code=403, detail=outcome.to_dict())
151
+ return outcome
152
+
153
+ return dependency
154
+
155
+
156
+ __all__ = [
157
+ "ACTENON_EVIDENCE_HEADER",
158
+ "ACTENON_PROOF_HEADER",
159
+ "encode_json_header",
160
+ "fastapi_dependency",
161
+ ]
@@ -0,0 +1,111 @@
1
+ """LangChain StructuredTool adapter with out-of-band proof injection."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import inspect
6
+ from typing import Any, Callable, Mapping, Optional, Union
7
+
8
+ from actenon.gate import ActenonGate
9
+ from actenon.models import ActionIntent, PCCB
10
+ from actenon.preflight import PreflightEvidence
11
+
12
+ from ._authorization import authorization_from_mapping
13
+
14
+ try:
15
+ from langchain_core.runnables import RunnableConfig
16
+ from langchain_core.tools import StructuredTool, create_schema_from_function
17
+ except ImportError as exc: # pragma: no cover - exercised without the optional extra
18
+ raise ImportError(
19
+ "Actenon's LangChain adapter requires the 'langchain' extra: "
20
+ "python -m pip install 'actenon-kernel[langchain]'"
21
+ ) from exc
22
+
23
+
24
+ ACTENON_CONFIG_KEY = "actenon"
25
+ ActionBuilder = Callable[[Mapping[str, Any]], Union[ActionIntent, dict[str, Any]]]
26
+ EvidenceBuilder = Callable[
27
+ [Mapping[str, Any]],
28
+ Optional[Union[Mapping[str, Any], PreflightEvidence]],
29
+ ]
30
+
31
+
32
+ def actenon_runnable_config(
33
+ proof: Optional[Union[Mapping[str, Any], PCCB]],
34
+ *,
35
+ evidence: Optional[Union[Mapping[str, Any], PreflightEvidence]] = None,
36
+ ) -> RunnableConfig:
37
+ """Build runtime-only LangChain config; it is excluded from tool schemas."""
38
+
39
+ return {
40
+ "configurable": {
41
+ ACTENON_CONFIG_KEY: {
42
+ "proof": proof,
43
+ "evidence": evidence,
44
+ }
45
+ }
46
+ }
47
+
48
+
49
+ def protected_structured_tool(
50
+ gate: ActenonGate,
51
+ domain_function: Callable[..., Any],
52
+ *,
53
+ action_builder: ActionBuilder,
54
+ audience: Optional[str] = None,
55
+ evidence_builder: Optional[EvidenceBuilder] = None,
56
+ name: Optional[str] = None,
57
+ description: Optional[str] = None,
58
+ return_direct: bool = False,
59
+ ) -> StructuredTool:
60
+ """Wrap a plain sync function without exposing proof fields to the model."""
61
+
62
+ if not callable(domain_function):
63
+ raise TypeError("domain_function must be callable")
64
+ if inspect.iscoroutinefunction(domain_function):
65
+ raise TypeError(
66
+ "protected_structured_tool currently requires a synchronous domain function"
67
+ )
68
+ tool_name = name or domain_function.__name__
69
+ tool_description = description or domain_function.__doc__
70
+ if not tool_description:
71
+ raise ValueError("LangChain tools require a description or function docstring")
72
+ args_schema = create_schema_from_function(
73
+ f"{tool_name.title().replace('_', '')}Input",
74
+ domain_function,
75
+ )
76
+
77
+ def protected_call(config: RunnableConfig, **domain_args: Any) -> dict[str, Any]:
78
+ authorization = authorization_from_mapping(
79
+ config.get("configurable", {}).get(ACTENON_CONFIG_KEY)
80
+ )
81
+ evidence = (
82
+ evidence_builder(domain_args)
83
+ if evidence_builder is not None
84
+ else authorization.evidence
85
+ )
86
+ action = action_builder(domain_args)
87
+ outcome = gate.protect(
88
+ action,
89
+ authorization.proof,
90
+ lambda: domain_function(**domain_args),
91
+ audience=audience,
92
+ evidence=evidence,
93
+ )
94
+ return outcome.to_dict()
95
+
96
+ protected_call.__name__ = tool_name
97
+ protected_call.__doc__ = tool_description
98
+ return StructuredTool.from_function(
99
+ func=protected_call,
100
+ name=tool_name,
101
+ description=tool_description,
102
+ args_schema=args_schema,
103
+ return_direct=return_direct,
104
+ )
105
+
106
+
107
+ __all__ = [
108
+ "ACTENON_CONFIG_KEY",
109
+ "actenon_runnable_config",
110
+ "protected_structured_tool",
111
+ ]
@@ -0,0 +1,118 @@
1
+ """FastMCP decorator for proof-gated tools with request-metadata proof."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import inspect
6
+ from functools import wraps
7
+ from typing import Any, Callable, Mapping
8
+
9
+ from actenon.gate import ActenonGate
10
+ from actenon.models import ActionIntent, PCCB
11
+ from actenon.preflight import PreflightEvidence
12
+
13
+ from ._authorization import authorization_from_mapping
14
+
15
+ try:
16
+ from mcp.server.fastmcp import Context
17
+ except ImportError as exc: # pragma: no cover - exercised without the optional extra
18
+ raise ImportError(
19
+ "Actenon's MCP adapter requires the 'mcp' extra: "
20
+ "python -m pip install 'actenon-kernel[mcp]'"
21
+ ) from exc
22
+
23
+
24
+ ACTENON_MCP_META_KEY = "actenon"
25
+ ActionBuilder = Callable[[Mapping[str, Any]], ActionIntent | dict[str, Any]]
26
+ EvidenceBuilder = Callable[
27
+ [Mapping[str, Any]],
28
+ Mapping[str, Any] | PreflightEvidence | None,
29
+ ]
30
+
31
+
32
+ def mcp_authorization_meta(
33
+ proof: Mapping[str, Any] | PCCB | None,
34
+ *,
35
+ evidence: Mapping[str, Any] | PreflightEvidence | None = None,
36
+ ) -> dict[str, Any]:
37
+ if isinstance(proof, PCCB):
38
+ proof = proof.to_dict()
39
+ if isinstance(evidence, PreflightEvidence):
40
+ evidence = evidence.to_dict()
41
+ return {
42
+ ACTENON_MCP_META_KEY: {
43
+ "proof": proof,
44
+ "evidence": evidence,
45
+ }
46
+ }
47
+
48
+
49
+ def _context_meta(context: Context[Any, Any, Any]) -> Mapping[str, Any]:
50
+ request_context = context.request_context
51
+ meta = getattr(request_context, "meta", None)
52
+ return meta if isinstance(meta, Mapping) else {}
53
+
54
+
55
+ def protected_mcp_tool(
56
+ gate: ActenonGate,
57
+ *,
58
+ action_builder: ActionBuilder,
59
+ audience: str | None = None,
60
+ evidence_builder: EvidenceBuilder | None = None,
61
+ context_parameter: str = "ctx",
62
+ ) -> Callable[[Callable[..., Any]], Callable[..., dict[str, Any]]]:
63
+ """Decorate an arbitrary sync FastMCP tool using injected request Context."""
64
+
65
+ def decorator(domain_function: Callable[..., Any]) -> Callable[..., dict[str, Any]]:
66
+ if inspect.iscoroutinefunction(domain_function):
67
+ raise TypeError(
68
+ "protected_mcp_tool currently requires a synchronous tool function"
69
+ )
70
+ signature = inspect.signature(domain_function)
71
+ context = signature.parameters.get(context_parameter)
72
+ if context is None:
73
+ raise TypeError(
74
+ f"protected MCP tool must declare an injected '{context_parameter}: Context' parameter"
75
+ )
76
+
77
+ @wraps(domain_function)
78
+ def protected_call(*args: Any, **kwargs: Any) -> dict[str, Any]:
79
+ bound = signature.bind(*args, **kwargs)
80
+ bound.apply_defaults()
81
+ mcp_context = bound.arguments.get(context_parameter)
82
+ if not isinstance(mcp_context, Context):
83
+ raise TypeError(
84
+ f"'{context_parameter}' must be an mcp.server.fastmcp.Context"
85
+ )
86
+ domain_args = {
87
+ key: value
88
+ for key, value in bound.arguments.items()
89
+ if key != context_parameter
90
+ }
91
+ authorization = authorization_from_mapping(
92
+ _context_meta(mcp_context).get(ACTENON_MCP_META_KEY)
93
+ )
94
+ evidence = (
95
+ evidence_builder(domain_args)
96
+ if evidence_builder is not None
97
+ else authorization.evidence
98
+ )
99
+ action = action_builder(domain_args)
100
+ outcome = gate.protect(
101
+ action,
102
+ authorization.proof,
103
+ lambda: domain_function(*args, **kwargs),
104
+ audience=audience,
105
+ evidence=evidence,
106
+ )
107
+ return outcome.to_dict()
108
+
109
+ return protected_call
110
+
111
+ return decorator
112
+
113
+
114
+ __all__ = [
115
+ "ACTENON_MCP_META_KEY",
116
+ "mcp_authorization_meta",
117
+ "protected_mcp_tool",
118
+ ]