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.
- actenon/__init__.py +11 -0
- actenon/adapters/__init__.py +20 -0
- actenon/adapters/_authorization.py +30 -0
- actenon/adapters/base.py +104 -0
- actenon/adapters/edge.py +129 -0
- actenon/adapters/fastapi.py +161 -0
- actenon/adapters/langchain.py +111 -0
- actenon/adapters/mcp.py +118 -0
- actenon/anchors/__init__.py +29 -0
- actenon/anchors/local_log.py +224 -0
- actenon/anchors/models.py +153 -0
- actenon/api/__init__.py +12 -0
- actenon/api/intake.py +39 -0
- actenon/api/invoice_payment.py +143 -0
- actenon/api/refund.py +57 -0
- actenon/cli.py +2023 -0
- actenon/conformance/__init__.py +5 -0
- actenon/conformance/helpers.py +136 -0
- actenon/conformance/manifest.py +185 -0
- actenon/conformance/test_artifact_shape_conformance.py +53 -0
- actenon/conformance/test_canonicalization_strict_conformance.py +288 -0
- actenon/conformance/test_countersignature_conformance.py +95 -0
- actenon/conformance/test_execution_state_conformance.py +78 -0
- actenon/conformance/test_outcome_attestation_conformance.py +109 -0
- actenon/conformance/test_replay_conformance.py +38 -0
- actenon/conformance/test_transparency_log_conformance.py +130 -0
- actenon/conformance/test_trust_artifacts_conformance.py +107 -0
- actenon/conformance/test_verifier_sdk_conformance.py +191 -0
- actenon/conformance/vectors/canonicalization_strict_v1/cases.json +142 -0
- actenon/conformance/vectors/canonicalization_strict_v1/profile.json +24 -0
- actenon/conformance/vectors/verifier_sdk_v1/README.md +18 -0
- actenon/conformance/vectors/verifier_sdk_v1/action_intent.json +41 -0
- actenon/conformance/vectors/verifier_sdk_v1/cases.json +285 -0
- actenon/conformance/vectors/verifier_sdk_v1/pccb.json +73 -0
- actenon/conformance/version.py +6 -0
- actenon/core/__init__.py +48 -0
- actenon/core/errors.py +111 -0
- actenon/core/json.py +68 -0
- actenon/core/kernel.py +195 -0
- actenon/core/redaction.py +21 -0
- actenon/coverage_matrix.py +903 -0
- actenon/credentials/__init__.py +9 -0
- actenon/credentials/broker.py +91 -0
- actenon/demo/__init__.py +2 -0
- actenon/demo/local_proof.py +652 -0
- actenon/demo/portable_local_proof.py +135 -0
- actenon/escrow/__init__.py +16 -0
- actenon/escrow/base.py +31 -0
- actenon/escrow/memory.py +65 -0
- actenon/escrow/service.py +25 -0
- actenon/escrow/sqlite.py +261 -0
- actenon/evidence/__init__.py +24 -0
- actenon/evidence/query.py +535 -0
- actenon/evidence/stores.py +117 -0
- actenon/execution/__init__.py +32 -0
- actenon/execution/mode_aware.py +404 -0
- actenon/execution/protected_executor.py +337 -0
- actenon/execution_graph.py +129 -0
- actenon/gate.py +620 -0
- actenon/idempotency.py +162 -0
- actenon/local_runtime.py +3412 -0
- actenon/local_runtime_server.py +960 -0
- actenon/models/__init__.py +125 -0
- actenon/models/contracts.py +866 -0
- actenon/models/evidence.py +15 -0
- actenon/models/execution.py +89 -0
- actenon/models/intent_record.py +334 -0
- actenon/models/invoice_payment.py +243 -0
- actenon/models/policy_bundle.py +147 -0
- actenon/models/runtime.py +73 -0
- actenon/models/serialization.py +46 -0
- actenon/outcomes.py +234 -0
- actenon/policy/__init__.py +65 -0
- actenon/policy/engine.py +252 -0
- actenon/policy/evidence.py +92 -0
- actenon/policy/invoice_payment.py +442 -0
- actenon/policy/refund.py +177 -0
- actenon/preflight/__init__.py +57 -0
- actenon/preflight/engine.py +208 -0
- actenon/preflight/evidence.py +135 -0
- actenon/preflight/models.py +129 -0
- actenon/preflight/policy_packs.py +1050 -0
- actenon/proof/__init__.py +131 -0
- actenon/proof/audit.py +87 -0
- actenon/proof/canonical.py +102 -0
- actenon/proof/local.py +19 -0
- actenon/proof/refusal_messages.py +40 -0
- actenon/proof/service.py +612 -0
- actenon/proof/signers/__init__.py +120 -0
- actenon/proof/signers/base.py +61 -0
- actenon/proof/signers/external_managed.py +252 -0
- actenon/proof/signers/hsm.py +63 -0
- actenon/proof/signers/kms.py +68 -0
- actenon/proof/signers/local.py +124 -0
- actenon/proof/signers/proof_seal.py +220 -0
- actenon/proof/signers/well_known.py +814 -0
- actenon/proof/signing.py +10 -0
- actenon/receipts/__init__.py +47 -0
- actenon/receipts/attestation.py +499 -0
- actenon/receipts/factory.py +211 -0
- actenon/receipts/invoice_payment.py +102 -0
- actenon/receipts/refund.py +60 -0
- actenon/receipts/store.py +110 -0
- actenon/receipts/writers.py +152 -0
- actenon/reconciliation/__init__.py +19 -0
- actenon/reconciliation/base.py +114 -0
- actenon/replay/__init__.py +20 -0
- actenon/replay/base.py +64 -0
- actenon/replay/dbapi.py +442 -0
- actenon/replay/postgres.py +55 -0
- actenon/replay/service.py +84 -0
- actenon/replay/sqlite.py +33 -0
- actenon/scanner.py +3264 -0
- actenon/ui/__init__.py +2 -0
- actenon/ui/trace_viewer/__init__.py +2 -0
- actenon/ui/trace_viewer/app.py +110 -0
- actenon/ui/trace_viewer/static/app.js +253 -0
- actenon/ui/trace_viewer/static/index.html +136 -0
- actenon/ui/trace_viewer/static/styles.css +288 -0
- actenon/ui/trace_viewer/trace_loader.py +875 -0
- actenon/verifier/__init__.py +56 -0
- actenon/verifier/countersignature.py +404 -0
- actenon/verifier/endpoint.py +271 -0
- actenon/verifier/middleware.py +96 -0
- actenon/verifier/sdk.py +104 -0
- actenon/verifier/transparency.py +778 -0
- actenon/verifier/trust_artifacts.py +570 -0
- actenon_kernel-0.1.0.dist-info/METADATA +644 -0
- actenon_kernel-0.1.0.dist-info/RECORD +269 -0
- actenon_kernel-0.1.0.dist-info/WHEEL +5 -0
- actenon_kernel-0.1.0.dist-info/entry_points.txt +4 -0
- actenon_kernel-0.1.0.dist-info/licenses/LICENSE +201 -0
- actenon_kernel-0.1.0.dist-info/top_level.txt +4 -0
- conformance/CHANGELOG.md +21 -0
- conformance/MATRIX.md +27 -0
- conformance/README.md +103 -0
- conformance/VERSION +1 -0
- conformance/__init__.py +1 -0
- conformance/suite.json +108 -0
- conformance/vector-lock.json +63 -0
- conformance/vectors/cloud_invoice_payment_v1/action_intent.json +74 -0
- conformance/vectors/cloud_invoice_payment_v1/issuer_keys.json +57 -0
- conformance/vectors/cloud_invoice_payment_v1/mutations/action_hash_changed_pccb.json +78 -0
- conformance/vectors/cloud_invoice_payment_v1/mutations/amount_changed_action_intent.json +74 -0
- conformance/vectors/cloud_invoice_payment_v1/mutations/audience_changed_pccb.json +78 -0
- conformance/vectors/cloud_invoice_payment_v1/mutations/expired_pccb.json +78 -0
- conformance/vectors/cloud_invoice_payment_v1/mutations/outcome_attestation_hard_revoked_issuer_keys.json +58 -0
- conformance/vectors/cloud_invoice_payment_v1/mutations/outcome_attestation_wrong_purpose_issuer_keys.json +57 -0
- conformance/vectors/cloud_invoice_payment_v1/mutations/receipt_attestation_artifact_digest_changed.json +55 -0
- conformance/vectors/cloud_invoice_payment_v1/mutations/receipt_attestation_issued_at_changed.json +55 -0
- conformance/vectors/cloud_invoice_payment_v1/mutations/receipt_attestation_issuer_changed.json +55 -0
- conformance/vectors/cloud_invoice_payment_v1/mutations/receipt_attestation_outcome_artifact_changed.json +55 -0
- conformance/vectors/cloud_invoice_payment_v1/mutations/receipt_attestation_proof_binding_changed.json +55 -0
- conformance/vectors/cloud_invoice_payment_v1/mutations/receipt_attestation_signature_changed.json +55 -0
- conformance/vectors/cloud_invoice_payment_v1/mutations/refusal_attestation_issued_at_changed.json +56 -0
- conformance/vectors/cloud_invoice_payment_v1/mutations/refusal_attestation_issuer_changed.json +56 -0
- conformance/vectors/cloud_invoice_payment_v1/mutations/refusal_attestation_signature_changed.json +56 -0
- conformance/vectors/cloud_invoice_payment_v1/mutations/signature_changed_pccb.json +78 -0
- conformance/vectors/cloud_invoice_payment_v1/mutations/wrong_kid_pccb.json +78 -0
- conformance/vectors/cloud_invoice_payment_v1/mutations/wrong_purpose_issuer_keys.json +57 -0
- conformance/vectors/cloud_invoice_payment_v1/pccb.json +78 -0
- conformance/vectors/cloud_invoice_payment_v1/receipt.json +19 -0
- conformance/vectors/cloud_invoice_payment_v1/receipt_attestation.json +55 -0
- conformance/vectors/cloud_invoice_payment_v1/refusal.json +20 -0
- conformance/vectors/cloud_invoice_payment_v1/refusal_attestation.json +56 -0
- conformance/vectors/receipt_countersignature_v1/countersignature.json +28 -0
- conformance/vectors/receipt_countersignature_v1/mutations/countersignature_altered_digest.json +28 -0
- conformance/vectors/receipt_countersignature_v1/mutations/countersignature_unknown_kid.json +28 -0
- conformance/vectors/receipt_countersignature_v1/mutations/trusted_keys_wrong_key.json +50 -0
- conformance/vectors/receipt_countersignature_v1/receipt.json +19 -0
- conformance/vectors/receipt_countersignature_v1/trusted_keys.json +50 -0
- conformance/vectors/transparency_log_v1/checkpoint_new.json +23 -0
- conformance/vectors/transparency_log_v1/checkpoint_old.json +23 -0
- conformance/vectors/transparency_log_v1/consistency_proof.json +13 -0
- conformance/vectors/transparency_log_v1/countersignature.json +27 -0
- conformance/vectors/transparency_log_v1/inclusion_proof.json +19 -0
- conformance/vectors/transparency_log_v1/leaf_digest.json +5 -0
- conformance/vectors/transparency_log_v1/mutations/checkpoint_new_forked.json +23 -0
- conformance/vectors/transparency_log_v1/mutations/checkpoint_unknown_kid.json +23 -0
- conformance/vectors/transparency_log_v1/mutations/countersignature_orphan.json +27 -0
- conformance/vectors/transparency_log_v1/trusted_keys.json +60 -0
- conformance/vectors/trust_artifacts_v1/README.md +13 -0
- conformance/vectors/trust_artifacts_v1/action_intent.json +74 -0
- conformance/vectors/trust_artifacts_v1/approval.json +25 -0
- conformance/vectors/trust_artifacts_v1/approval_trusted_keys.json +28 -0
- conformance/vectors/trust_artifacts_v1/issuer.json +4 -0
- conformance/vectors/trust_artifacts_v1/issuer_status_expired.json +24 -0
- conformance/vectors/trust_artifacts_v1/issuer_status_good.json +24 -0
- conformance/vectors/trust_artifacts_v1/issuer_status_revoked.json +24 -0
- conformance/vectors/trust_artifacts_v1/issuer_status_stale.json +24 -0
- conformance/vectors/trust_artifacts_v1/issuer_status_trusted_keys.json +28 -0
- conformance/vectors/trust_artifacts_v1/mutations/approval_action_changed.json +25 -0
- conformance/vectors/trust_artifacts_v1/mutations/approval_signature_changed.json +25 -0
- conformance/vectors/trust_artifacts_v1/mutations/issuer_status_signature_changed.json +24 -0
- examples/__init__.py +2 -0
- examples/adversarial_rsa_policy_stress_test.py +339 -0
- examples/claude_managed_agents_protected_tool/tool.py +456 -0
- examples/credential_broker_infra_delete/__init__.py +1 -0
- examples/credential_broker_infra_delete/demo.py +171 -0
- examples/crewai_protected_tool/tool.py +160 -0
- examples/edge_templates/__init__.py +2 -0
- examples/edge_templates/_support.py +66 -0
- examples/edge_templates/ci_cd.py +14 -0
- examples/edge_templates/cloud.py +14 -0
- examples/edge_templates/communications.py +14 -0
- examples/edge_templates/database.py +14 -0
- examples/edge_templates/http_api.py +14 -0
- examples/edge_templates/iam.py +14 -0
- examples/edge_templates/ot.py +14 -0
- examples/edge_templates/storage.py +14 -0
- examples/fastapi_protected_route/app.py +114 -0
- examples/fastapi_resource_boundary_refund/__init__.py +0 -0
- examples/fastapi_resource_boundary_refund/app.py +151 -0
- examples/fastapi_resource_boundary_refund/test_resource_boundary_refund.py +92 -0
- examples/fastapi_verifier_only_refund/__init__.py +0 -0
- examples/fastapi_verifier_only_refund/app.py +122 -0
- examples/fastapi_verifier_only_refund/issuer.py +47 -0
- examples/fastapi_verifier_only_refund/test_verifier_only_refund.py +58 -0
- examples/fastmcp_financial_transfer/__init__.py +1 -0
- examples/fastmcp_financial_transfer/mcp_server.py +88 -0
- examples/fastmcp_financial_transfer/test_fastmcp_financial_transfer.py +120 -0
- examples/financial_agent_protected_transfer/financial_agent.py +182 -0
- examples/financial_agent_protected_transfer/test_financial_agent.py +132 -0
- examples/hello_protected_endpoint/server.py +113 -0
- examples/hello_world_protected_resource_python/__init__.py +1 -0
- examples/hello_world_protected_resource_python/protected_resource.py +49 -0
- examples/integration_support.py +123 -0
- examples/interactive_execution_demo.py +139 -0
- examples/invoice_payment_guard_local/__init__.py +1 -0
- examples/invoice_payment_guard_local/protected_endpoint.py +376 -0
- examples/langchain_protected_tool/tool.py +123 -0
- examples/llamaindex_protected_tool/tool.py +142 -0
- examples/mcp_protected_tool/__init__.py +2 -0
- examples/mcp_protected_tool/demo.py +269 -0
- examples/mcp_server_protected_tool/__init__.py +2 -0
- examples/mcp_server_protected_tool/demo.py +66 -0
- examples/mcp_server_protected_tool/proof_gate.py +463 -0
- examples/mcp_server_protected_tool/server.py +208 -0
- examples/openai_agents_sdk_protected_tool/app.py +104 -0
- examples/protected_clinical_ehr_agent/protected_clinical_ehr_agent.py +274 -0
- examples/protected_clinical_ehr_agent/test_protected_clinical_ehr_agent.py +25 -0
- examples/protected_iam_control_plane/protected_iam_control_plane.py +273 -0
- examples/protected_iam_control_plane/test_protected_iam_control_plane.py +21 -0
- examples/protected_langchain_finance_agent/protected_langchain_finance_agent.py +421 -0
- examples/protected_langchain_finance_agent/test_protected_langchain_finance_agent.py +27 -0
- examples/protected_multi_agent_swarm/protected_multi_agent_swarm.py +233 -0
- examples/protected_multi_agent_swarm/test_protected_multi_agent_swarm.py +20 -0
- examples/protected_policy_preflight_refund/__init__.py +0 -0
- examples/protected_policy_preflight_refund/policy_preflight_refund.py +87 -0
- examples/protected_policy_preflight_refund/test_policy_preflight_refund.py +76 -0
- examples/quickstart_min.py +16 -0
- examples/refund_guard_local/__init__.py +2 -0
- examples/refund_guard_local/call_endpoint.py +113 -0
- examples/refund_guard_local/protected_endpoint.py +87 -0
- examples/refund_guard_local/server.py +560 -0
- examples/semantic_kernel_protected_tool/tool.py +141 -0
- schemas/__init__.py +1 -0
- schemas/action_intent.v1.json +256 -0
- schemas/approval_artifact.v1.json +62 -0
- schemas/issuer_status.v1.json +53 -0
- schemas/pccb.v1.json +336 -0
- schemas/receipt.v1.json +343 -0
- schemas/receipt_attestation.v2alpha1.json +237 -0
- schemas/receipt_countersignature.v1.json +150 -0
- schemas/refusal.v1.json +348 -0
- schemas/refusal_attestation.v2alpha1.json +237 -0
- schemas/transparency_checkpoint.v1.json +51 -0
- schemas/transparency_consistency_proof.v1.json +28 -0
- schemas/transparency_inclusion_proof.v1.json +42 -0
actenon/__init__.py
ADDED
|
@@ -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
|
+
)
|
actenon/adapters/base.py
ADDED
|
@@ -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."""
|
actenon/adapters/edge.py
ADDED
|
@@ -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
|
+
]
|
actenon/adapters/mcp.py
ADDED
|
@@ -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
|
+
]
|