Import AITURK IDE 1.0.0-beta.1 from Hermes 63279301; preserve MIT license
This commit is contained in:
@@ -0,0 +1,219 @@
|
||||
"""Host-owned contract for plugin-provided human approval transports.
|
||||
|
||||
Transports only present an immutable, redacted request and return a correlated
|
||||
human decision. They do not participate in command detection or authorization
|
||||
policy. The host validates scope, request binding, and timeout fail-closed.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import hashlib
|
||||
import inspect
|
||||
import json
|
||||
import logging
|
||||
import queue
|
||||
import threading
|
||||
import time
|
||||
import uuid
|
||||
from dataclasses import dataclass
|
||||
from typing import Awaitable, Callable, Literal
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_MAX_ACTIVE_TRANSPORT_WORKERS = 8
|
||||
_transport_worker_slots = threading.BoundedSemaphore(_MAX_ACTIVE_TRANSPORT_WORKERS)
|
||||
|
||||
ApprovalChoice = Literal["once", "session", "always", "deny"]
|
||||
ApprovalPresentFn = Callable[
|
||||
["ApprovalRequest"], "ApprovalDecision | Awaitable[ApprovalDecision]"
|
||||
]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ApprovalDecision:
|
||||
"""A transport response bound to one exact host-created request."""
|
||||
|
||||
request_id: str
|
||||
request_digest: str
|
||||
choice: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ApprovalRequest:
|
||||
"""Immutable, display-only approval request passed to a transport plugin."""
|
||||
|
||||
schema_version: int
|
||||
request_id: str
|
||||
digest: str
|
||||
command: str
|
||||
description: str
|
||||
pattern_key: str
|
||||
pattern_keys: tuple[str, ...]
|
||||
surface: str
|
||||
timeout_seconds: float
|
||||
allowed_choices: tuple[ApprovalChoice, ...]
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
cls,
|
||||
*,
|
||||
command: str,
|
||||
description: str,
|
||||
pattern_key: str,
|
||||
pattern_keys: tuple[str, ...],
|
||||
session_key: str,
|
||||
surface: str,
|
||||
allow_session: bool,
|
||||
allow_permanent: bool,
|
||||
timeout_seconds: float = 300,
|
||||
) -> "ApprovalRequest":
|
||||
request_id = uuid.uuid4().hex
|
||||
choices: list[ApprovalChoice] = ["once"]
|
||||
if allow_session:
|
||||
choices.append("session")
|
||||
if allow_permanent:
|
||||
choices.append("always")
|
||||
choices.append("deny")
|
||||
canonical = {
|
||||
"schema_version": 1,
|
||||
"request_id": request_id,
|
||||
"command": command,
|
||||
"description": description,
|
||||
"pattern_key": pattern_key,
|
||||
"pattern_keys": list(pattern_keys),
|
||||
"session_key": session_key,
|
||||
"surface": surface,
|
||||
"timeout_seconds": timeout_seconds,
|
||||
"allowed_choices": choices,
|
||||
}
|
||||
digest = hashlib.sha256(
|
||||
json.dumps(canonical, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
||||
).hexdigest()
|
||||
return cls(
|
||||
schema_version=1,
|
||||
request_id=request_id,
|
||||
digest=digest,
|
||||
command=command,
|
||||
description=description,
|
||||
pattern_key=pattern_key,
|
||||
pattern_keys=pattern_keys,
|
||||
surface=surface,
|
||||
timeout_seconds=timeout_seconds,
|
||||
allowed_choices=tuple(choices),
|
||||
)
|
||||
|
||||
def respond(self, choice: ApprovalChoice | str) -> ApprovalDecision:
|
||||
"""Build the correlated response a transport should return."""
|
||||
return ApprovalDecision(
|
||||
request_id=self.request_id,
|
||||
request_digest=self.digest,
|
||||
choice=choice,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ApprovalTransportResult:
|
||||
"""Normalized host result. Any failure is represented as a denial."""
|
||||
|
||||
choice: ApprovalChoice
|
||||
failure: str | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RegisteredApprovalTransport:
|
||||
"""Plugin-owned registration retained by one profile's PluginManager."""
|
||||
|
||||
name: str
|
||||
present: ApprovalPresentFn
|
||||
plugin_id: str
|
||||
profile_home: str
|
||||
|
||||
|
||||
def invoke_approval_transport(
|
||||
present: ApprovalPresentFn,
|
||||
request: ApprovalRequest,
|
||||
*,
|
||||
timeout_seconds: float,
|
||||
poll_interval: float = 1.0,
|
||||
on_poll: Callable[[], None] | None = None,
|
||||
is_interrupted: Callable[[], bool] | None = None,
|
||||
) -> ApprovalTransportResult:
|
||||
"""Run a sync or async transport on a bounded daemon worker.
|
||||
|
||||
Async callbacks are awaited with ``asyncio.run`` on that worker, never on a
|
||||
gateway or TUI event loop. A callback must return before the host timeout;
|
||||
late results are discarded and cannot authorize another request.
|
||||
"""
|
||||
|
||||
if not _transport_worker_slots.acquire(blocking=False):
|
||||
logger.warning("Approval transport worker capacity exhausted")
|
||||
return ApprovalTransportResult("deny", "busy")
|
||||
|
||||
results: queue.Queue[tuple[str, object, float]] = queue.Queue(maxsize=1)
|
||||
deadline = time.monotonic() + max(float(timeout_seconds), 0.0)
|
||||
|
||||
async def _await_value(value):
|
||||
return await value
|
||||
|
||||
def _run() -> None:
|
||||
try:
|
||||
value = present(request)
|
||||
if inspect.isawaitable(value):
|
||||
value = asyncio.run(_await_value(value))
|
||||
results.put_nowait(("result", value, time.monotonic()))
|
||||
except BaseException as exc: # fail closed even for unusual callback exits
|
||||
try:
|
||||
results.put_nowait(("error", exc, time.monotonic()))
|
||||
except queue.Full:
|
||||
pass
|
||||
finally:
|
||||
_transport_worker_slots.release()
|
||||
|
||||
worker = threading.Thread(
|
||||
target=_run,
|
||||
name=f"approval-transport-{request.request_id[:8]}",
|
||||
daemon=True,
|
||||
)
|
||||
try:
|
||||
worker.start()
|
||||
except BaseException:
|
||||
_transport_worker_slots.release()
|
||||
logger.warning("Could not start approval transport worker")
|
||||
return ApprovalTransportResult("deny", "error")
|
||||
while True:
|
||||
if is_interrupted is not None and is_interrupted():
|
||||
logger.info("Approval transport wait interrupted for %s", request.request_id)
|
||||
return ApprovalTransportResult("deny", "interrupted")
|
||||
remaining = deadline - time.monotonic()
|
||||
if remaining <= 0:
|
||||
logger.warning("Approval transport timed out for request %s", request.request_id)
|
||||
return ApprovalTransportResult("deny", "timeout")
|
||||
try:
|
||||
kind, value, completed_at = results.get(
|
||||
timeout=min(max(float(poll_interval), 0.001), remaining)
|
||||
)
|
||||
break
|
||||
except queue.Empty:
|
||||
if on_poll is not None:
|
||||
try:
|
||||
on_poll()
|
||||
except Exception:
|
||||
logger.debug("Approval transport poll callback failed", exc_info=True)
|
||||
|
||||
if completed_at > deadline:
|
||||
logger.warning("Approval transport timed out for request %s", request.request_id)
|
||||
return ApprovalTransportResult("deny", "timeout")
|
||||
if kind == "error":
|
||||
logger.warning("Approval transport failed for request %s", request.request_id)
|
||||
return ApprovalTransportResult("deny", "error")
|
||||
if not isinstance(value, ApprovalDecision):
|
||||
logger.warning("Approval transport returned an invalid decision type")
|
||||
return ApprovalTransportResult("deny", "invalid")
|
||||
if value.request_id != request.request_id or value.request_digest != request.digest:
|
||||
logger.warning("Approval transport returned a stale or mismatched decision")
|
||||
return ApprovalTransportResult("deny", "stale")
|
||||
if value.choice not in request.allowed_choices:
|
||||
logger.warning("Approval transport returned a disallowed choice")
|
||||
return ApprovalTransportResult("deny", "invalid")
|
||||
return ApprovalTransportResult(value.choice)
|
||||
Reference in New Issue
Block a user