Add controlled Athena platform operator
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Single MCP facade for end-to-end Athena platform operation."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import socket
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
|
||||
VERSION = "1.0.0"
|
||||
SOCKET_PATH = os.environ.get("ATHENA_OPERATOR_SOCKET", "/operator/operator.sock")
|
||||
|
||||
if hasattr(sys.stdin, "reconfigure"):
|
||||
sys.stdin.reconfigure(encoding="utf-8", errors="replace")
|
||||
if hasattr(sys.stdout, "reconfigure"):
|
||||
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||
|
||||
|
||||
TOOLS = [
|
||||
{
|
||||
"name": "athena_operator_inspect",
|
||||
"description": (
|
||||
"USE FIRST for Athena administration. Inspect live platform state without changing it. "
|
||||
"Subjects: overview, git_status, containers, models, jobs. This is the authoritative "
|
||||
"starting point before MCP, Docker, model, profile, benchmark or recovery work."
|
||||
),
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {"subject": {"type": "string", "enum": ["overview", "git_status", "containers", "models", "jobs"], "default": "overview"}},
|
||||
"additionalProperties": False,
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "athena_operator_read_source",
|
||||
"description": (
|
||||
"Read a versioned Athena repository file with SHA-256 and bounded line range. Use this "
|
||||
"instead of guessing current Compose, MCP, router, model, OpenWebUI or recovery code."
|
||||
),
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {"type": "string", "pattern": "^[A-Za-z0-9_.+/-]{1,240}$"},
|
||||
"start_line": {"type": "integer", "minimum": 1, "maximum": 1000000, "default": 1},
|
||||
"line_count": {"type": "integer", "minimum": 1, "maximum": 1000, "default": 300},
|
||||
},
|
||||
"required": ["path"], "additionalProperties": False,
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "athena_operator_search_source",
|
||||
"description": "Search the complete versioned Athena repository for exact text before designing or modifying a component.",
|
||||
"inputSchema": {"type": "object", "properties": {"query": {"type": "string", "minLength": 1, "maxLength": 200}}, "required": ["query"], "additionalProperties": False},
|
||||
},
|
||||
{
|
||||
"name": "athena_operator_prepare",
|
||||
"description": (
|
||||
"PREPARE a state-changing Athena operation. This never changes state. It returns a "
|
||||
"content-bound ticket, exact preview and confirmation phrase. Supported operations: "
|
||||
"file_update (write repository and deployed stack files), run_checks, compose_deploy, "
|
||||
"container_action, openwebui_sync, git_publish, model_download, benchmark, recovery. Show the complete "
|
||||
"preview to the user and stop. Never execute in the same autonomous tool sequence. "
|
||||
"file_update payload: {files:[{path,content,expected_sha256?}]}; run_checks: "
|
||||
"{checks:[operator-tests,openwebui-filter-tests,platform-verify,compose-main,compose-mcp]}; "
|
||||
"compose_deploy: {compose_file,services,build}; container_action: {action,containers}; "
|
||||
"openwebui_sync: {}; "
|
||||
"git_publish: {message}; model_download: {url,destination,sha256?}; benchmark: "
|
||||
"{script,arguments}; recovery: {label}."
|
||||
),
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"operation": {"type": "string", "enum": ["file_update", "run_checks", "compose_deploy", "container_action", "openwebui_sync", "git_publish", "model_download", "benchmark", "recovery"]},
|
||||
"payload": {"type": "object"},
|
||||
},
|
||||
"required": ["operation", "payload"], "additionalProperties": False,
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "athena_operator_execute",
|
||||
"description": (
|
||||
"EXECUTE exactly one previously prepared Athena operation. Call only after the user "
|
||||
"approved the exact preview in a later message and supplied the exact confirmation. "
|
||||
"Tickets expire, are content-bound and single-use. Long downloads, benchmarks and "
|
||||
"recovery work return a job id. This tool cannot alter SSH, networking, WireGuard, "
|
||||
"firewall, boot, kernel, drivers, partitions, mounts, shutdown or reboot."
|
||||
),
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ticket": {"type": "string", "pattern": "^[0-9a-f]{32}$"},
|
||||
"confirmation": {"type": "string", "pattern": "^EXECUTE [0-9a-f]{32}$"},
|
||||
},
|
||||
"required": ["ticket", "confirmation"], "additionalProperties": False,
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "athena_operator_job",
|
||||
"description": "Poll one asynchronous model download, benchmark or recovery job. Do not start duplicate jobs while it is running.",
|
||||
"inputSchema": {"type": "object", "properties": {"job_id": {"type": "string", "pattern": "^[0-9a-f]{32}$"}}, "required": ["job_id"], "additionalProperties": False},
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def request(action: str, arguments: dict[str, Any]) -> dict[str, Any]:
|
||||
payload = json.dumps({"action": action, "arguments": arguments}, ensure_ascii=False, separators=(",", ":")).encode() + b"\n"
|
||||
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as client:
|
||||
client.settimeout(20)
|
||||
client.connect(SOCKET_PATH)
|
||||
client.sendall(payload)
|
||||
chunks = bytearray()
|
||||
while not chunks.endswith(b"\n"):
|
||||
part = client.recv(65536)
|
||||
if not part:
|
||||
break
|
||||
chunks.extend(part)
|
||||
if len(chunks) > 2_000_000:
|
||||
raise RuntimeError("operator response exceeds limit")
|
||||
response = json.loads(chunks)
|
||||
if not response.get("ok"):
|
||||
raise RuntimeError(response.get("error", "operator request failed"))
|
||||
return response["result"]
|
||||
|
||||
|
||||
def call(name: str, arguments: dict[str, Any]) -> dict[str, Any]:
|
||||
mapping = {
|
||||
"athena_operator_inspect": "inspect",
|
||||
"athena_operator_read_source": "read_source",
|
||||
"athena_operator_search_source": "search_source",
|
||||
"athena_operator_prepare": "prepare",
|
||||
"athena_operator_execute": "execute",
|
||||
"athena_operator_job": "job",
|
||||
}
|
||||
if name not in mapping:
|
||||
raise ValueError("unknown tool")
|
||||
return request(mapping[name], arguments)
|
||||
|
||||
|
||||
def emit(request_id: Any, result: Any = None, error: dict[str, Any] | None = None) -> None:
|
||||
message = {"jsonrpc": "2.0", "id": request_id}
|
||||
message["error" if error else "result"] = error or result
|
||||
sys.stdout.write(json.dumps(message, ensure_ascii=False, separators=(",", ":")) + "\n")
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
def handle(message: dict[str, Any]) -> None:
|
||||
method, request_id = message.get("method"), message.get("id")
|
||||
if method == "initialize":
|
||||
emit(request_id, {"protocolVersion": message.get("params", {}).get("protocolVersion", "2024-11-05"), "capabilities": {"tools": {"listChanged": False}}, "serverInfo": {"name": "mike-ai-athena-operator", "version": VERSION}})
|
||||
elif method == "tools/list":
|
||||
emit(request_id, {"tools": TOOLS})
|
||||
elif method == "tools/call":
|
||||
params = message.get("params", {})
|
||||
try:
|
||||
value = call(str(params.get("name", "")), params.get("arguments") or {})
|
||||
emit(request_id, {"content": [{"type": "text", "text": json.dumps(value, ensure_ascii=False, separators=(",", ":"))}], "structuredContent": value, "isError": False})
|
||||
except Exception as exc:
|
||||
emit(request_id, {"content": [{"type": "text", "text": f"ERROR: {exc}"}], "isError": True})
|
||||
elif request_id is not None:
|
||||
emit(request_id, error={"code": -32601, "message": "method not found"})
|
||||
|
||||
|
||||
def main() -> None:
|
||||
for line in sys.stdin:
|
||||
try:
|
||||
if line.strip(): handle(json.loads(line))
|
||||
except Exception as exc:
|
||||
sys.stderr.write(f"MCP input error: {exc}\n"); sys.stderr.flush()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user