import argparse
import datetime
import decimal
import errno
import http.client
import json
import reprlib
import ssl
import sys
from typing import Any, Dict, NamedTuple, Optional, Protocol, Set
from ._tempus_ddb import TempusExecutor
_I64_MIN, _U64_MAX = -(2**63), 2**64 - 1
MAX_OUTPUT_DEPTH = 64 MAX_OUTPUT_NODES = (
100_000 )
MAX_OUTPUT_CHARS = 4 * 1024 * 1024 MAX_REPR_CHARS = 512
DEGRADED_MARKER = "_tempus_output_degraded"
_SUMMARY = reprlib.Repr()
_SUMMARY.maxlevel = 3
_SUMMARY.maxstring = _SUMMARY.maxother = _SUMMARY.maxlong = 100
for _name in ("maxlist", "maxtuple", "maxset", "maxfrozenset", "maxdict", "maxdeque"):
setattr(_SUMMARY, _name, 6)
class _Unrepresentable(ValueError):
class _Budget:
def __init__(self) -> None:
self.nodes = MAX_OUTPUT_NODES
self.chars = MAX_OUTPUT_CHARS
def spend(self, chars: int = 0) -> None:
self.nodes -= 1
self.chars -= chars
if self.nodes < 0 or self.chars < 0:
raise _Unrepresentable(
f"result too large (more than {MAX_OUTPUT_NODES} values or "
f"{MAX_OUTPUT_CHARS} characters)"
)
def _bounded_repr(value: Any) -> str:
try:
if isinstance(value, (bytes, bytearray)) and len(value) > MAX_REPR_CHARS:
return (
repr(bytes(value[:MAX_REPR_CHARS]))[:MAX_REPR_CHARS] + "...<truncated>"
)
text = repr(value)
except Exception: return f"<unrepresentable {type(value).__name__}>"
return (
text
if len(text) <= MAX_REPR_CHARS
else text[:MAX_REPR_CHARS] + "...<truncated>"
)
def _summary(value: Any) -> str:
try:
text = _SUMMARY.repr(value)
except Exception:
return f"<unrepresentable {type(value).__name__}>"
return text[:MAX_REPR_CHARS]
def exact_json(
value: Any,
_depth: int = 0,
_budget: Optional[_Budget] = None,
_path: Optional[set] = None,
) -> Any:
budget = _budget if _budget is not None else _Budget()
path = _path if _path is not None else set()
if _depth > MAX_OUTPUT_DEPTH:
raise _Unrepresentable(f"nesting deeper than {MAX_OUTPUT_DEPTH} levels")
if isinstance(value, bool) or value is None:
budget.spend()
return value
if isinstance(value, str):
budget.spend(len(value))
return value
if isinstance(value, int):
if _I64_MIN <= value <= _U64_MAX:
budget.spend()
return value
text = str(value)
budget.spend(len(text))
return text
if isinstance(value, float):
text = repr(value)
budget.spend(len(text))
return text
if isinstance(value, decimal.Decimal):
text = str(value)
budget.spend(len(text))
return text
if isinstance(value, (datetime.datetime, datetime.date, datetime.time)):
text = value.isoformat()
budget.spend(len(text))
return text
if isinstance(value, (dict, list, tuple)):
if id(value) in path:
raise _Unrepresentable("cyclic reference")
budget.spend()
path.add(id(value))
try:
if isinstance(value, dict):
converted: dict = {}
for key, item in value.items():
name = key if isinstance(key, str) else str(key)
budget.spend(len(name))
if name == DEGRADED_MARKER:
raise _Unrepresentable(
f"the adapter result uses the reserved key {DEGRADED_MARKER}"
)
if name in converted:
raise _Unrepresentable(
f"two keys become the same JSON key {_bounded_repr(name)}"
)
converted[name] = exact_json(item, _depth + 1, budget, path)
return converted
return [exact_json(item, _depth + 1, budget, path) for item in value]
finally:
path.discard(id(value))
text = _bounded_repr(value)
budget.spend(len(text))
return text
def sanitize_output(payload: Any) -> str:
try:
return json.dumps(exact_json(payload))
except (
Exception
) as exc: reason = str(exc) if isinstance(exc, _Unrepresentable) else type(exc).__name__
return json.dumps(
{
DEGRADED_MARKER: True,
"reason": f"adapter result not representable as JSON: {reason}",
"repr": _summary(payload),
}
)
class ExecutorRuntimeError(RuntimeError):
def _reject_duplicate_keys(pairs):
seen = {}
for key, item in pairs:
if key in seen:
raise ValueError(
f"DUPLICATE_JSON_KEY: object key {key!r} appears more than once"
)
seen[key] = item
return seen
def _parse_permit(permit_json: str):
try:
return json.loads(permit_json, object_pairs_hook=_reject_duplicate_keys)
except ValueError as exc:
if "DUPLICATE_JSON_KEY" in str(exc):
raise ExecutorRuntimeError(
"DUPLICATE_JSON_KEY: permit repeats a JSON object key"
) from exc
return None
class UnknownExecutionError(ExecutorRuntimeError):
def __init__(self, observation: str):
super().__init__(f"Execution outcome is UNKNOWN: {observation}")
self.observation = observation
class AmbiguousTransportError(ExecutorRuntimeError):
def __init__(self, reason: str):
super().__init__(reason)
self.reason = reason
_AMBIGUOUS_ERRNOS = frozenset(
number
for number in (
errno.ETIMEDOUT,
errno.ECONNRESET,
errno.EPIPE,
errno.ECONNABORTED,
getattr(errno, "ENETRESET", None),
getattr(errno, "WSAETIMEDOUT", None),
getattr(errno, "WSAECONNRESET", None),
getattr(errno, "WSAECONNABORTED", None),
)
if isinstance(number, int)
)
_AMBIGUOUS_TYPES = (
TimeoutError,
BrokenPipeError,
ConnectionAbortedError,
ConnectionResetError, ssl.SSLError,
EOFError,
http.client.IncompleteRead,
http.client.BadStatusLine,
http.client.RemoteDisconnected,
)
def _http_status(exc: BaseException) -> Optional[int]:
for name in ("status_code", "code", "status"):
value = getattr(exc, name, None)
if isinstance(value, int) and not isinstance(value, bool):
return value
return None
def is_ambiguous_transport_error(exc: BaseException) -> bool:
pending = [exc]
seen: set[int] = set()
while pending:
current = pending.pop()
if current is None or id(current) in seen:
continue
seen.add(id(current))
status = _http_status(current)
number = getattr(current, "errno", None)
if isinstance(current, ConnectionRefusedError):
pass
elif isinstance(current, _AMBIGUOUS_TYPES) or (
isinstance(number, int) and number in _AMBIGUOUS_ERRNOS
):
return True
elif status is not None and status >= 500:
return True
reason = getattr(current, "reason", None)
if isinstance(reason, BaseException):
pending.append(reason)
cause = current.__cause__
if isinstance(cause, BaseException):
pending.append(cause)
return False
class ExecutionResult(NamedTuple):
status: str payload: Dict[str, Any]
class ActionAdapter(Protocol):
@property
def supported_actions(self) -> Set[str]:
...
def execute_action(self, intent: Dict[str, Any]) -> ExecutionResult:
...
class ExecutorRuntime:
def __init__(
self,
executor_db: str,
executor_keyfile: str,
trusted_gate_id: str,
trusted_tenant_id: str,
executor_pool_size: int = 8,
gate_db: Optional[str] = None,
):
self._executor_db = executor_db
self._executor_keyfile = executor_keyfile
self._trusted_gate_id = trusted_gate_id
self._trusted_tenant_id = trusted_tenant_id
self._pool_size = executor_pool_size
self._gate_db = gate_db
self._executor = TempusExecutor(
executor_db,
executor_keyfile,
trusted_gate_id,
trusted_tenant_id,
executor_pool_size,
gate_db,
)
@property
def raw_executor(self) -> TempusExecutor:
return self._executor
def execute_permit(self, permit_json: str, adapter: ActionAdapter) -> str:
permit_data = _parse_permit(permit_json)
consumed_auth_str = self._executor.verify_and_consume_permit(permit_json)
consumed_auth = json.loads(consumed_auth_str)
auth_id = consumed_auth["authorization_id"]
action_id = consumed_auth["action_id"]
try:
if permit_data is None:
permit_data = json.loads(permit_json)
intent = permit_data.get("intent")
if not isinstance(intent, dict):
raise ExecutorRuntimeError(
"TEMPUS_INVALID_CONTRACT: verified intent must be a JSON object"
)
action_type = intent.get("action_type")
if action_type not in adapter.supported_actions:
err_code = getattr(
adapter, "unsupported_error_code", "ERR_UNSUPPORTED_ACTION"
)
error_payload = {
"error_code": err_code,
"message": f"Action '{action_type}' is not supported by {adapter.__class__.__name__}",
}
return self._executor.complete_execution(
auth_id,
action_id,
"FAILED",
json.dumps(error_payload),
)
result = adapter.execute_action(intent)
status = (
result.status if result.status in {"SUCCEEDED", "FAILED"} else "FAILED"
)
output_json = sanitize_output(result.payload)
return self._executor.complete_execution(
auth_id,
action_id,
status,
output_json,
)
except AmbiguousTransportError as exc:
observation = self._executor.mark_unknown(auth_id, exc.reason)
raise UnknownExecutionError(observation) from exc
except UnknownExecutionError:
raise
except Exception as exc:
if is_ambiguous_transport_error(exc):
observation = self._executor.mark_unknown(
auth_id,
f"TRANSPORT_AMBIGUOUS: {type(exc).__name__}",
)
raise UnknownExecutionError(observation) from exc
return self._executor.complete_execution(
auth_id,
action_id,
"FAILED",
json.dumps(
{"error_code": "EXECUTION_EXCEPTION", "message": str(exc)}
),
)
@classmethod
def run_cli(
cls,
description: str,
adapter_factory: Any,
argv: Optional[list] = None,
extra_args_fn: Optional[Any] = None,
) -> None:
parser = argparse.ArgumentParser(description=description)
parser.add_argument("--permit", required=True, help="Path to permit JSON file")
parser.add_argument(
"--executor-db", required=True, help="Path to executor SQLite DB"
)
parser.add_argument(
"--executor-keyfile", required=True, help="Path to executor keys JSON"
)
parser.add_argument(
"--gate-id", required=True, help="Trusted Tempus Gate public key"
)
parser.add_argument("--tenant-id", required=True, help="Expected tenant ID")
parser.add_argument(
"--executor-pool-size", type=int, default=8, help="Connection pool size"
)
parser.add_argument(
"--gate-db",
default=None,
help="Optional path to Tempus Gate SQLite DB for revocation and replay verification",
)
if extra_args_fn:
extra_args_fn(parser)
args = parser.parse_args(argv)
with open(args.permit, "r", encoding="utf-8") as f:
permit_content = f.read()
try:
adapter = adapter_factory(args)
except Exception as exc:
print(f"Executor configuration error: {exc}", file=sys.stderr)
sys.exit(1)
runtime = cls(
executor_db=args.executor_db,
executor_keyfile=args.executor_keyfile,
trusted_gate_id=args.gate_id,
trusted_tenant_id=args.tenant_id,
executor_pool_size=args.executor_pool_size,
gate_db=args.gate_db,
)
try:
receipt = runtime.execute_permit(permit_content, adapter)
print(receipt)
except Exception as exc:
print(f"Execution failed: {exc}", file=sys.stderr)
sys.exit(1)