import os
import re
from typing import Any, Dict, Optional, Protocol, Set, Tuple
from .executor_runtime import (
AmbiguousTransportError,
ExecutionResult,
ExecutorRuntime,
is_ambiguous_transport_error,
)
class PaymentExecutorError(RuntimeError):
class PaymentTransport(Protocol):
def disburse(
self,
secret_key: str,
amount: str,
asset: str,
beneficiary: str,
metadata: Dict[str, Any],
) -> Dict[str, Any]:
class MockPaymentTransport:
def disburse(
self,
secret_key: str,
amount: str,
asset: str,
beneficiary: str,
metadata: Dict[str, Any],
) -> Dict[str, Any]:
if not secret_key:
raise PaymentExecutorError("Payment provider secret key is missing")
tx_id = f"tx_mock_{os.urandom(8).hex()}"
return {
"status": "SETTLED",
"transaction_id": tx_id,
"amount": amount,
"asset": asset,
"beneficiary": beneficiary,
"metadata": metadata,
}
_DECIMAL_AMOUNT = re.compile(r"([0-9]+)(?:\.([0-9]{1,2}))?")
_EXACT_CURRENCY = re.compile(r"[A-Z]{3}")
_U64_MAX = 2**64 - 1
def _minor_units(text: str) -> int:
match = _DECIMAL_AMOUNT.fullmatch(text)
if not match:
raise PaymentExecutorError(
"money.amount must be an unsigned decimal with at most two fractional digits"
)
whole = match.group(1).lstrip("0") or "0"
if len(whole) > 20: raise PaymentExecutorError("money.amount is too large")
minor = int(whole) * 100 + int((match.group(2) or "").ljust(2, "0"))
if minor > _U64_MAX:
raise PaymentExecutorError("money.amount is too large")
return minor
def canonical_money(money: Dict[str, Any]) -> Tuple[str, str]:
names = {}
for field in ("currency", "asset"):
if field in money:
value = money[field]
if not isinstance(value, str) or not value:
raise PaymentExecutorError(f"money.{field} must be a non-empty string")
if _EXACT_CURRENCY.fullmatch(value) is None:
raise PaymentExecutorError(
f"money.{field} must be an exact 3-letter currency code"
)
names[field] = value
if not names:
raise PaymentExecutorError("Missing 'asset' (or 'currency') in money")
if len(set(names.values())) > 1:
raise PaymentExecutorError(
"money.currency and money.asset must be the same value"
)
asset = next(iter(names.values()))
text = money.get("amount")
minor = money.get("amount_minor")
if text is not None and not isinstance(text, str):
raise PaymentExecutorError("money.amount must be a decimal string")
if "amount_minor" in money and (
isinstance(minor, bool)
or not isinstance(minor, int)
or not 0 <= minor <= _U64_MAX
):
raise PaymentExecutorError("money.amount_minor must be an unsigned integer")
from_text = _minor_units(text) if text is not None else None
if from_text is not None and minor is not None and from_text != minor:
raise PaymentExecutorError(
f"money.amount ({from_text} minor units) and money.amount_minor ({minor}) disagree"
)
if text is not None:
return text, asset
if minor is None:
raise PaymentExecutorError("Missing 'amount' (or 'amount_minor') in money")
return f"{minor // 100}.{minor % 100:02d}", asset
def component_pattern_matches(pattern: str, candidate: str) -> bool:
if pattern == "*" or pattern == candidate:
return True
pattern_pieces = _component_pieces(pattern, wildcards=True)
candidate_pieces = _component_pieces(candidate, wildcards=False)
if pattern_pieces is None or candidate_pieces is None:
return False
return _pieces_match(pattern_pieces, candidate_pieces)
def _component_pieces(value: str, wildcards: bool):
pieces = []
start = 0
for index, char in enumerate(value):
if char in "./":
if start < index and not _push_piece(pieces, value[start:index], wildcards):
return None
pieces.append(("sep", char))
start = index + 1
if start < len(value) and not _push_piece(pieces, value[start:], wildcards):
return None
return pieces
def _push_piece(pieces, text: str, wildcards: bool) -> bool:
if wildcards and "*" in text:
if text != "*":
return False
pieces.append(("wild",))
else:
pieces.append(("text", text))
return True
def _pieces_match(pattern, candidate) -> bool:
if not pattern:
return not candidate
head, rest = pattern[0], pattern[1:]
kind = head[0]
if kind == "wild":
return _wildcard_matches(rest, candidate)
if kind == "sep":
return bool(candidate) and candidate[0] == head and _pieces_match(rest, candidate[1:])
if kind == "text":
return bool(candidate) and candidate[0] == head and _pieces_match(rest, candidate[1:])
return False
def _wildcard_matches(rest, candidate) -> bool:
if not rest:
return bool(candidate)
bound = rest[0][1] if rest[0][0] == "sep" else None
index = 0
texts = 0
while index < len(candidate):
kind = candidate[index][0]
if kind == "text":
texts += 1
index += 1
if _pieces_match(rest, candidate[index:]):
return True
elif kind == "sep":
if texts == 0 or bound != candidate[index][1]:
return False
index += 1
else:
return False
return False
class PaymentActionAdapter:
SUPPORTED_ACTIONS: Set[str] = {
"finance.disburse",
"finance.transfer",
"payment.charge",
}
def __init__(
self,
secret_key: Optional[str] = None,
transport: Optional[PaymentTransport] = None,
allowed_beneficiaries: Optional[Set[str]] = None,
):
self._secret_key = secret_key or os.environ.get(
"PAYMENT_SECRET_KEY", "default-isolated-payment-key"
)
self._transport = transport or MockPaymentTransport()
allowed_env = os.environ.get("PAYMENT_ALLOWED_BENEFICIARIES")
if allowed_beneficiaries is not None:
self._allowed_beneficiaries = {
b.strip() for b in allowed_beneficiaries if b.strip()
}
elif allowed_env:
self._allowed_beneficiaries = {
b.strip() for b in allowed_env.split(",") if b.strip()
}
else:
self._allowed_beneficiaries = None
@property
def supported_actions(self) -> Set[str]:
return self.SUPPORTED_ACTIONS
def execute_action(self, intent: Dict[str, Any]) -> ExecutionResult:
money = intent.get("money")
if not money or not isinstance(money, dict):
raise PaymentExecutorError(
"Financial action requires a valid 'money' envelope in intent"
)
amount, asset = canonical_money(money)
if _minor_units(amount) == 0:
raise PaymentExecutorError("a payment action requires a non-zero amount")
beneficiary = money.get("beneficiary")
if not isinstance(beneficiary, str) or not beneficiary:
raise PaymentExecutorError("money.beneficiary must be a non-empty string")
if self._allowed_beneficiaries is not None:
allowed = any(
component_pattern_matches(ab, beneficiary)
for ab in self._allowed_beneficiaries
)
if not allowed:
raise PaymentExecutorError(
f"Beneficiary '{beneficiary}' is not authorized by payment executor policy"
)
input_data = intent.get("input", {})
try:
payout_result = self._transport.disburse(
self._secret_key, amount, asset, beneficiary, input_data
)
return ExecutionResult(status="SUCCEEDED", payload=payout_result)
except AmbiguousTransportError:
raise
except Exception as exc:
if is_ambiguous_transport_error(exc):
raise AmbiguousTransportError(
f"PAYMENT_TRANSPORT_AMBIGUOUS: {type(exc).__name__}"
) from exc
return ExecutionResult(status="FAILED", payload={"error": str(exc)})
class PaymentExecutorAdapter:
SUPPORTED_ACTIONS = {"finance.disburse", "finance.transfer", "payment.charge"}
def __init__(
self,
executor_db: str,
executor_keyfile: str,
trusted_gate_id: str,
trusted_tenant_id: str,
secret_key: Optional[str] = None,
transport: Optional[PaymentTransport] = None,
executor_pool_size: int = 8,
allowed_beneficiaries: Optional[Set[str]] = None,
gate_db: Optional[str] = None,
):
self._adapter = PaymentActionAdapter(
secret_key=secret_key,
transport=transport,
allowed_beneficiaries=allowed_beneficiaries,
)
self._runtime = ExecutorRuntime(
executor_db=executor_db,
executor_keyfile=executor_keyfile,
trusted_gate_id=trusted_gate_id,
trusted_tenant_id=trusted_tenant_id,
executor_pool_size=executor_pool_size,
gate_db=gate_db,
)
def execute(self, permit_json: str) -> str:
return self._runtime.execute_permit(permit_json, self._adapter)
execute_permit = execute
def main() -> None:
def extra_args(parser):
parser.add_argument("--secret-key", help="Optional PAYMENT_SECRET_KEY")
ExecutorRuntime.run_cli(
description="Tempus Credential-Isolated Payment Executor",
adapter_factory=lambda args: PaymentActionAdapter(secret_key=args.secret_key),
extra_args_fn=extra_args,
)
if __name__ == "__main__":
main()