123 lines
4.8 KiB
Python
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}}
|