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
|
@@ -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."""
|
actenon/api/__init__.py
ADDED
|
@@ -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
|