Files
aios/services/aios_orchestrator_gateway/remote_tool.py
T

123 lines
4.8 KiB
Python

from __future__ import annotations
import hmac
import json
import os
from pathlib import Path
import secrets
import stat
import time
from typing import Any, Callable
from .callback_transport import CallbackTransport
from .errors import AuthenticationError, ValidationError
from .models import Decision
from .models import ID_RE, OID_RE
from .securefs import ensure_secure_directory
AUDIT_FIELDS = (
"callback_request_id", "event_id", "contract_commit", "received_at",
"schema_valid", "auth_valid", "forward_status", "state_version_after",
)
class CallbackAudit:
def __init__(self, path: Path):
self.path = path
def write(self, record: dict[str, Any]) -> None:
if set(record) != set(AUDIT_FIELDS):
raise ValueError("invalid_callback_audit_fields")
ensure_secure_directory(self.path.parent)
flags = os.O_WRONLY | os.O_APPEND | os.O_CREAT | getattr(os, "O_NOFOLLOW", 0)
fd = os.open(self.path, flags, 0o600)
try:
info = os.fstat(fd)
if not stat.S_ISREG(info.st_mode) or stat.S_IMODE(info.st_mode) & 0o077:
raise ValueError("unsafe_callback_audit_file")
data = (json.dumps(record, separators=(",", ":"), sort_keys=True) + "\n").encode()
os.write(fd, data)
os.fsync(fd)
finally:
os.close(fd)
class MinimalDecisionTool:
"""Transport-neutral single-tool boundary; no generic GitOps operations."""
def __init__(
self,
transport: CallbackTransport,
*,
contract_commit: str,
access_token: str,
forward: Callable[[dict[str, Any]], dict[str, Any]],
audit: CallbackAudit,
):
self.transport = transport
self.contract_commit = contract_commit
self.access_token = access_token
self.forward = forward
self.audit = audit
def tools(self) -> list[dict[str, Any]]:
return [self.transport.mcp_tool()] if self.transport.mode == "secure_mcp_tunnel" else []
def authenticate(self, authorization: str) -> None:
if not hmac.compare_digest(authorization, f"Bearer {self.access_token}"):
raise AuthenticationError("invalid_bearer")
def submit(self, payload: dict[str, Any], authorization: str) -> dict[str, Any]:
request_id = secrets.token_hex(16)
received_at = int(time.time())
schema_valid = False
auth_valid = False
raw_event_id = payload.get("event_id") if isinstance(payload, dict) else None
raw_contract = payload.get("contract_commit") if isinstance(payload, dict) else None
event_id = raw_event_id if isinstance(raw_event_id, str) and ID_RE.fullmatch(raw_event_id) else None
contract = raw_contract if isinstance(raw_contract, str) and OID_RE.fullmatch(raw_contract) else None
forward_status = "REJECTED"
state_version = None
try:
self.authenticate(authorization)
auth_valid = True
decision = Decision.from_dict(payload)
schema_valid = True
if decision.contract_commit != self.contract_commit:
raise ValidationError("contract_version_mismatch")
result = self.forward(decision.to_dict())
if result.get("accepted") is not True or not isinstance(result.get("state_version"), int):
raise ValidationError("invalid_forward_response")
forward_status = "202"
state_version = result["state_version"]
return result
finally:
self.audit.write({
"callback_request_id": request_id,
"event_id": event_id,
"contract_commit": contract,
"received_at": received_at,
"schema_valid": schema_valid,
"auth_valid": auth_valid,
"forward_status": forward_status,
"state_version_after": state_version,
})
def handle_mcp(self, request: dict[str, Any], authorization: str) -> dict[str, Any]:
self.authenticate(authorization)
method = request.get("method")
request_id = request.get("id")
if method == "tools/list":
return {"jsonrpc": "2.0", "id": request_id, "result": {"tools": self.tools()}}
if method != "tools/call":
raise ValidationError("unsupported_mcp_method")
params = request.get("params")
if not isinstance(params, dict) or params.get("name") != "submit_orchestration_decision":
raise ValidationError("unsupported_mcp_tool")
arguments = params.get("arguments")
if not isinstance(arguments, dict):
raise ValidationError("invalid_mcp_arguments")
result = self.submit(arguments, authorization)
return {"jsonrpc": "2.0", "id": request_id, "result": {"structuredContent": result}}