Simplify Athena operator architecture
This commit is contained in:
@@ -29,9 +29,9 @@ from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
VERSION = "2.3.4"
|
||||
VERSION = "3.0.0"
|
||||
STACK = Path(os.environ.get("ATHENA_OPERATOR_STACK", "/opt/mike-ai/stack")).resolve()
|
||||
REPOSITORY = Path(os.environ.get("ATHENA_OPERATOR_REPOSITORY", "/data/mike-ai-operator/repository")).resolve()
|
||||
REPOSITORY = Path(os.environ.get("ATHENA_OPERATOR_REPOSITORY", str(STACK))).resolve()
|
||||
STATE = Path(os.environ.get("ATHENA_OPERATOR_STATE", "/data/mike-ai-operator/state")).resolve()
|
||||
SOCKET = Path(os.environ.get("ATHENA_OPERATOR_SOCKET", "/run/mike-ai-operator/operator.sock"))
|
||||
MODELS = Path(os.environ.get("ATHENA_OPERATOR_MODELS", "/data/models")).resolve()
|
||||
@@ -145,6 +145,7 @@ def safe_relative(value: str) -> Path:
|
||||
|
||||
|
||||
def source_file(root: Path, relative: Path) -> Path:
|
||||
root = root.resolve(strict=False)
|
||||
candidate = (root / relative).resolve(strict=False)
|
||||
candidate.relative_to(root)
|
||||
return candidate
|
||||
@@ -250,15 +251,10 @@ def audit(event: str, **fields: Any) -> None:
|
||||
|
||||
def ensure_repository() -> None:
|
||||
if not (REPOSITORY / ".git").is_dir():
|
||||
bundle = Path("/data/mike-ai-recovery-kit/source.git.bundle")
|
||||
if not bundle.is_file():
|
||||
raise RuntimeError("operator repository missing and no recovery Git bundle is available")
|
||||
REPOSITORY.parent.mkdir(parents=True, exist_ok=True)
|
||||
run(["git", "clone", str(bundle), str(REPOSITORY)], cwd=Path("/data"), check=True)
|
||||
current_file = STACK / ".mike-ai-source-commit"
|
||||
current = current_file.read_text().strip() if current_file.is_file() else ""
|
||||
if re.fullmatch(r"[0-9a-f]{40}", current):
|
||||
run(["git", "switch", "-C", "main", current], cwd=REPOSITORY, check=True)
|
||||
raise RuntimeError(
|
||||
f"canonical Git checkout is missing at {REPOSITORY}; "
|
||||
"restore /opt/mike-ai/stack with the documented recovery script"
|
||||
)
|
||||
remote = os.environ.get("ATHENA_OPERATOR_GIT_REMOTE", "").strip()
|
||||
if remote:
|
||||
run(["git", "remote", "set-url", "origin", remote], cwd=REPOSITORY, check=True)
|
||||
@@ -271,12 +267,13 @@ def inspect(subject: str, arguments: dict[str, Any]) -> dict[str, Any]:
|
||||
if subject == "overview":
|
||||
return {
|
||||
"version": VERSION,
|
||||
"source_commit": (STACK / ".mike-ai-source-commit").read_text().strip(),
|
||||
"source_commit": run(["git", "rev-parse", "HEAD"], cwd=REPOSITORY, check=True)["output"].strip(),
|
||||
"git": run(["git", "status", "--short", "--branch"], cwd=REPOSITORY),
|
||||
"containers": run(["docker", "ps", "--format", "{{.Names}}\t{{.Status}}\t{{.Image}}"]),
|
||||
"storage": run(["df", "-h", "/", "/data", str(MODELS)]),
|
||||
"gpus": run(["nvidia-smi", "--query-gpu=name,memory.total,memory.used,utilization.gpu", "--format=csv,noheader"]),
|
||||
"boundary": "Athena AI-platform operator; no arbitrary shell, shutdown, reboot, SSH/network/firewall/kernel/driver/partition operations",
|
||||
"layout": {"worktree": str(REPOSITORY), "runtime_stack": str(STACK), "single_worktree": REPOSITORY == STACK},
|
||||
"boundary": "No shutdown, reboot or Athena SSH/network/firewall/kernel/partition/mount changes",
|
||||
}
|
||||
if subject == "git_status":
|
||||
return {"status": run(["git", "status", "--short", "--branch"], cwd=REPOSITORY), "diff": run(["git", "diff", "--stat"], cwd=REPOSITORY)}
|
||||
@@ -301,12 +298,18 @@ def read_source(arguments: dict[str, Any]) -> dict[str, Any]:
|
||||
relative = safe_relative(str(arguments.get("path", "")))
|
||||
target = source_file(REPOSITORY, relative)
|
||||
if not target.is_file():
|
||||
raise FileNotFoundError("source file not found")
|
||||
return {"ok": False, "error": "source file not found", "path": str(relative), "retry": False}
|
||||
text = target.read_text(encoding="utf-8", errors="replace")
|
||||
start = max(1, int(arguments.get("start_line", 1)))
|
||||
count = min(1000, max(1, int(arguments.get("line_count", 300))))
|
||||
count = min(200, max(1, int(arguments.get("line_count", 80))))
|
||||
lines = text.splitlines()
|
||||
return {"path": str(relative), "sha256": sha(target.read_bytes()), "start_line": start, "content": "\n".join(lines[start - 1:start - 1 + count]), "total_lines": len(lines)}
|
||||
selected = lines[start - 1:start - 1 + count]
|
||||
return {
|
||||
"ok": True, "path": str(relative), "sha256": sha(target.read_bytes()),
|
||||
"start_line": start, "end_line": start + len(selected) - 1 if selected else start - 1,
|
||||
"content": "\n".join(selected), "total_lines": len(lines),
|
||||
"truncated": start - 1 + len(selected) < len(lines),
|
||||
}
|
||||
|
||||
|
||||
def search_source(arguments: dict[str, Any]) -> dict[str, Any]:
|
||||
@@ -315,12 +318,10 @@ def search_source(arguments: dict[str, Any]) -> dict[str, Any]:
|
||||
if not query or len(query) > 200 or any(x in query for x in ("\x00", "\n", "\r")):
|
||||
raise ValueError("invalid query")
|
||||
if shutil.which("rg"):
|
||||
result = run(["rg", "-n", "--hidden", "--glob", "!.git/**", "--", query, "."], cwd=REPOSITORY, timeout=20)
|
||||
return {"query": query, "matches": result["output"], "exit_code": result["exit_code"], "engine": "rg"}
|
||||
try:
|
||||
pattern = re.compile(query)
|
||||
except re.error as exc:
|
||||
raise ValueError(f"invalid search expression: {exc}") from exc
|
||||
result = run(["rg", "-n", "-F", "-m", "20", "--hidden", "--glob", "!.git/**", "--", query, "."], cwd=REPOSITORY, timeout=20)
|
||||
lines = result["output"].splitlines()[:20]
|
||||
return {"ok": True, "query": query, "matches": lines, "count": len(lines), "truncated": len(lines) == 20, "engine": "rg"}
|
||||
pattern = re.compile(re.escape(query))
|
||||
matches: list[str] = []
|
||||
for path in sorted(REPOSITORY.rglob("*")):
|
||||
if not path.is_file() or ".git" in path.parts or path.stat().st_size > MAX_FILE_BYTES:
|
||||
@@ -333,9 +334,9 @@ def search_source(arguments: dict[str, Any]) -> dict[str, Any]:
|
||||
for number, line in enumerate(lines, 1):
|
||||
if pattern.search(line):
|
||||
matches.append(f"{relative}:{number}:{line}")
|
||||
if len(matches) >= 200:
|
||||
return {"query": query, "matches": "\n".join(matches), "exit_code": 0, "engine": "python", "truncated": True}
|
||||
return {"query": query, "matches": "\n".join(matches), "exit_code": 0 if matches else 1, "engine": "python", "truncated": False}
|
||||
if len(matches) >= 20:
|
||||
return {"ok": True, "query": query, "matches": matches, "count": len(matches), "engine": "python", "truncated": True}
|
||||
return {"ok": True, "query": query, "matches": matches, "count": len(matches), "engine": "python", "truncated": False}
|
||||
|
||||
|
||||
def staged_file(item: dict[str, Any]) -> dict[str, Any]:
|
||||
@@ -564,7 +565,7 @@ def prepare(arguments: dict[str, Any]) -> dict[str, Any]:
|
||||
|
||||
|
||||
def sync_file(relative: Path, content: str, backup_root: Path) -> None:
|
||||
for root in (REPOSITORY, STACK):
|
||||
for root in dict.fromkeys((REPOSITORY, STACK)):
|
||||
target = source_file(root, relative)
|
||||
target_mode = stat.S_IMODE(target.stat().st_mode) if target.exists() else 0o644
|
||||
if target.exists():
|
||||
@@ -610,7 +611,7 @@ def apply_files(files: list[dict[str, Any]], backup: Path) -> list[str]:
|
||||
def restore_files(files: list[dict[str, Any]], backup: Path) -> None:
|
||||
for item in files:
|
||||
relative = safe_relative(item["path"])
|
||||
for root in (REPOSITORY, STACK):
|
||||
for root in dict.fromkeys((REPOSITORY, STACK)):
|
||||
target = source_file(root, relative)
|
||||
saved = backup / root.name / relative
|
||||
if saved.is_file():
|
||||
@@ -789,6 +790,28 @@ def execute(arguments: dict[str, Any]) -> dict[str, Any]:
|
||||
raise
|
||||
|
||||
|
||||
def change(arguments: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Apply one user-requested operation without a second ticket ceremony."""
|
||||
ensure_repository()
|
||||
operation = str(arguments.get("operation", ""))
|
||||
payload, preview = normalise_operation(operation, arguments.get("payload") or {})
|
||||
change_id = uuid.uuid4().hex
|
||||
audit("change_started", change=change_id, operation=operation)
|
||||
try:
|
||||
result = execute_operation(change_id, operation, payload)
|
||||
except Exception:
|
||||
audit("change_failed", change=change_id, operation=operation)
|
||||
raise
|
||||
audit("change_completed", change=change_id, operation=operation)
|
||||
return {
|
||||
"change_id": change_id,
|
||||
"operation": operation,
|
||||
"summary": compact(preview),
|
||||
"result": result,
|
||||
"instruction": "Verify the focused service behavior before declaring completion.",
|
||||
}
|
||||
|
||||
|
||||
def job(arguments: dict[str, Any]) -> dict[str, Any]:
|
||||
job_id = str(arguments.get("job_id", ""))
|
||||
if not re.fullmatch(r"[0-9a-f]{32}", job_id):
|
||||
@@ -806,6 +829,7 @@ def dispatch(request: dict[str, Any]) -> dict[str, Any]:
|
||||
if action == "read_source": return read_source(arguments)
|
||||
if action == "search_source": return search_source(arguments)
|
||||
if action == "terminal": return terminal(arguments)
|
||||
if action == "change": return change(arguments)
|
||||
if action == "prepare": return prepare(arguments)
|
||||
if action == "execute": return execute(arguments)
|
||||
if action == "job": return job(arguments)
|
||||
|
||||
@@ -15,7 +15,14 @@ chmod 0644 /etc/mike-ai/athena-operator-git.pub
|
||||
install -m 0644 "$ROOT/config/athena-operator-known-hosts" \
|
||||
/etc/mike-ai/athena-operator-known-hosts
|
||||
install -d -m 0750 -o root -g 10003 /run/mike-ai-operator
|
||||
install -d -m 0700 /data/mike-ai-operator/state /data/mike-ai-operator/repository
|
||||
install -d -m 0700 /data/mike-ai-operator/state /data/mike-ai-operator/staging
|
||||
[[ -d "$ROOT/.git" ]] || {
|
||||
echo "Der laufende Stack muss ein Git-Checkout sein: $ROOT" >&2
|
||||
exit 1
|
||||
}
|
||||
# Read-only MCP containers need to traverse the stack directory. Secrets are
|
||||
# stored separately in /etc/mike-ai and are not exposed by this permission.
|
||||
chmod 0755 "$ROOT"
|
||||
install -m 0755 "$ROOT/platform/operator/athena_operatord.py" /usr/local/libexec/mike-ai-athena-operatord
|
||||
install -m 0644 "$ROOT/platform/operator/athena-operator.service" /etc/systemd/system/mike-ai-athena-operator.service
|
||||
systemctl daemon-reload
|
||||
|
||||
Reference in New Issue
Block a user