Files
aios/active-runtime/app/aios_v2/app.py
T

808 lines
31 KiB
Python

"""AIOS V2 production-candidate application entrypoint."""
from __future__ import annotations
import dataclasses
import json
import os
import stat
import urllib.parse
from enum import Enum
from http import HTTPStatus
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from typing import Any, Mapping
from aios_hermes import HermesAdapter, HermesJobState, HermesResult, SQLiteHermesStore
from aios_openclaw import (
Ed25519CommandSigner,
HTTPRuntimeTransport,
OpenClawAdapter,
)
from aios_openclaw.callback import AuditCallbackSink, EventPublisher
from .console import CONSOLE_HTML
from .control_plane import AuditEvent, RequestContext, Unauthorized
from .dispatch_store import SQLiteDispatchRepository
from .execution_tracking import ExecutionEvidence, ExecutionTracker
from .gateway import GatewayApplication, error_response
from .gitops_status import collect_gitops_status
from .orchestrator import ExecutionOrchestrator
from .persistence import DurableControlPlaneService, SQLiteSnapshotRepository
from .provenance_operations import build_provenance_writer_from_env
def _json_value(value: Any) -> Any:
if dataclasses.is_dataclass(value):
return {
item.name: _json_value(getattr(value, item.name))
for item in dataclasses.fields(value)
}
if isinstance(value, Enum):
return value.value
if isinstance(value, Mapping):
return {str(key): _json_value(item) for key, item in value.items()}
if isinstance(value, (list, tuple, set, frozenset)):
return [_json_value(item) for item in value]
return value
def _enabled(value: str | None) -> bool:
return str(value or "").strip().lower() in {"1", "true", "yes", "on"}
def _header(headers: Mapping[str, str], name: str) -> str | None:
target = name.lower()
for key, value in headers.items():
if str(key).lower() == target:
normalized = str(value or "").strip()
return normalized or None
return None
def _provenance_context(
headers: Mapping[str, str],
payload: Mapping[str, Any],
) -> tuple[str | None, str | None]:
header_provenance = _header(headers, "X-AIOS-Provenance-ID")
header_run = _header(headers, "X-AIOS-Run-ID")
body_provenance = str(payload.get("provenance_id") or "").strip() or None
body_run = str(payload.get("run_id") or "").strip() or None
if header_provenance and body_provenance and header_provenance != body_provenance:
raise ValueError("provenance_id header/body mismatch")
if header_run and body_run and header_run != body_run:
raise ValueError("run_id header/body mismatch")
return header_provenance or body_provenance, header_run or body_run
def _read_protected_file(path: Path, *, private: bool) -> bytes:
flags = os.O_RDONLY
if hasattr(os, "O_NOFOLLOW"):
flags |= os.O_NOFOLLOW
descriptor = os.open(str(path), flags)
try:
metadata = os.fstat(descriptor)
if not stat.S_ISREG(metadata.st_mode):
raise RuntimeError("key reference is not a regular file")
denied = 0o077 if private else 0o022
if metadata.st_mode & denied:
raise RuntimeError(
"private key file permissions must be 0600 or stricter"
if private
else "public key file must not be group/world writable"
)
chunks: list[bytes] = []
while True:
chunk = os.read(descriptor, 65536)
if not chunk:
break
chunks.append(chunk)
value = b"".join(chunks)
if not value:
raise RuntimeError("key file is empty")
return value
finally:
os.close(descriptor)
class FileCommandSignerResolver:
"""Resolve tenant/key-id scoped OpenClaw Ed25519 signing material."""
def __init__(self, path: str | Path, *, configured_key_id: str) -> None:
self.path = Path(path).resolve()
self.configured_key_id = str(configured_key_id or "").strip()
if not self.configured_key_id:
raise ValueError("configured_key_id is required")
def __call__(self, tenant_id: str, key_id: str) -> Ed25519CommandSigner:
if not str(tenant_id or "").strip():
raise RuntimeError("tenant_id is required for command signing")
if key_id != self.configured_key_id:
raise RuntimeError("OpenClaw signing key_id mismatch")
return Ed25519CommandSigner.from_bytes(
_read_protected_file(self.path, private=True),
key_id=key_id,
)
class ControlPlaneEventPublisher(EventPublisher):
"""Append Runtime callback events to the tenant audit stream.
Callback audit remains informational. Authoritative task reconciliation is
performed only by the orchestrator after dispatch/result validation.
"""
def __init__(self, service: DurableControlPlaneService) -> None:
self.service = service
def publish(self, event: Mapping[str, Any]) -> None:
tenant_id = str(event.get("tenant_id") or "").strip()
trace_id = str(event.get("trace_id") or "").strip()
command_id = str(event.get("command_id") or "").strip()
event_type = str(event.get("event_type") or "OPENCLAW_EVENT").strip()
if not tenant_id or not trace_id or not command_id:
raise ValueError("runtime audit event is missing tenant identity")
state_value = str(event.get("state") or "unknown").lower()
outcome = "allowed" if state_value == "completed" else state_value
runtime = str(event.get("runtime") or "openclaw").strip().lower()
principal_id = str(
event.get("principal_id") or f"{runtime}-runtime"
).strip()
audit = AuditEvent(
event_id=f"runtime_{command_id}_{len(self.service.store.audit_events) + 1}",
tenant_id=tenant_id,
principal_id=principal_id,
trace_id=trace_id,
action=event_type,
resource_type=f"{runtime}_execution",
resource_id=command_id,
outcome=outcome,
policy_version="runtime-v2",
details=dict(event.get("details") or {}),
)
with self.service.store.lock:
self.service.store.audit_events.append(audit)
self.service.repository.save(self.service._snapshot())
class CandidateApplication:
def __init__(
self,
gateway: GatewayApplication,
orchestrator: ExecutionOrchestrator,
tracker: ExecutionTracker,
hermes: HermesAdapter,
openclaw: OpenClawAdapter | None,
*,
hermes_lifecycle_api_enabled: bool = False,
) -> None:
self.gateway = gateway
self.orchestrator = orchestrator
self.tracker = tracker
self.hermes = hermes
self.openclaw = openclaw
self.hermes_lifecycle_api_enabled = bool(hermes_lifecycle_api_enabled)
self.runtime_events = ControlPlaneEventPublisher(orchestrator.control_plane)
def context(self, headers: Mapping[str, str]) -> RequestContext:
return self.gateway.context_from_headers(headers)
@staticmethod
def _require_runtime_role(context: RequestContext) -> None:
if not ({"runtime", "platform_admin"} & set(context.roles)):
raise Unauthorized("runtime reconciliation role is required")
@staticmethod
def _require_control_plane_cancel(context: RequestContext) -> None:
required = {"runtime", "platform_admin"}
if not required.issubset(set(context.roles)):
raise Unauthorized(
"Hermes cancellation requires trusted AIOS control-plane authority"
)
@staticmethod
def _required_identity(payload: Mapping[str, Any], name: str) -> str:
value = str(payload.get(name) or "").strip()
if not value:
raise ValueError(f"{name} is required")
return value
def _record_hermes_lifecycle(
self,
context: RequestContext,
dispatch: Any,
job: Any,
*,
action: str,
worker_id: str | None,
provenance_id: str,
run_id: str,
) -> None:
executor = worker_id or "aios-control-plane"
evidence = ExecutionEvidence(
execution_id=job.job_id,
tenant_id=job.tenant_id,
task_id=job.task_id,
agent_id=dispatch.agent_id,
executor=executor,
branch="runtime/hermes",
status=job.state.value.lower(),
progress_completed=0,
progress_total=3,
current_step=action,
provenance_id=provenance_id,
run_id=run_id,
)
current_evidence = self.tracker.claim(
evidence,
principal_id=context.principal_id,
provenance_id=provenance_id,
run_id=run_id,
)
if job.state in {
HermesJobState.COMPLETED,
HermesJobState.FAILED,
HermesJobState.CANCELLED,
}:
if current_evidence.completed_at is None:
self.tracker.complete(
tenant_id=job.tenant_id,
execution_id=job.job_id,
status=job.state.value.lower(),
test_status="runtime_terminal",
test_summary={"lifecycle_action": action},
result_summary={
"state": job.state.value,
"worker_id": worker_id,
"dispatch_id": dispatch.dispatch_id,
},
principal_id=context.principal_id,
provenance_id=provenance_id,
run_id=run_id,
)
else:
self.tracker.heartbeat(
tenant_id=job.tenant_id,
execution_id=job.job_id,
status=job.state.value.lower(),
current_step=action,
progress_completed=(1 if job.state == HermesJobState.RUNNING else 0),
progress_total=3,
principal_id=context.principal_id,
provenance_id=provenance_id,
run_id=run_id,
)
self.runtime_events.publish(
{
"runtime": "hermes",
"tenant_id": job.tenant_id,
"principal_id": context.principal_id,
"trace_id": job.trace_id,
"command_id": job.job_id,
"event_type": f"HERMES_{action.upper()}",
"state": job.state.value,
"details": {
"dispatch_id": dispatch.dispatch_id,
"task_id": job.task_id,
"worker_id": worker_id,
"cell_id": job.cell_id,
"profile_id": job.profile_id,
"revision": job.revision,
"provenance_id": provenance_id,
"run_id": run_id,
},
}
)
def handle_json(
self,
method: str,
raw_path: str,
headers: Mapping[str, str],
body: Any,
) -> tuple[int, Mapping[str, Any]]:
parsed = urllib.parse.urlparse(raw_path)
path = parsed.path.rstrip("/") or "/"
method = method.upper()
if method == "GET" and path == "/api/v2/gitops":
context = self.context(headers)
return HTTPStatus.OK, {
"tenant_id": context.tenant_id,
"gitops": collect_gitops_status().as_dict(),
}
if method == "GET" and path == "/api/v2/runtime":
context = self.context(headers)
runtimes: list[dict[str, Any]] = []
if self.openclaw is None:
runtimes.append(
{
"name": "openclaw",
"status": "disabled",
"details": {"reason": "runtime_not_configured"},
}
)
else:
try:
health = self.openclaw.health()
runtimes.append(
{
"name": "openclaw",
"status": health.status,
"details": _json_value(health),
}
)
except Exception as exc:
runtimes.append(
{
"name": "openclaw",
"status": "unavailable",
"details": {"message": str(exc)},
}
)
hermes_health = self.hermes.health()
runtimes.append(
{
"name": "hermes",
"status": hermes_health.status,
"details": _json_value(hermes_health),
}
)
return HTTPStatus.OK, {
"tenant_id": context.tenant_id,
"runtimes": runtimes,
}
if method == "GET" and path == "/api/v2/executions":
context = self.context(headers)
return HTTPStatus.OK, {
"executions": _json_value(
self.tracker.list(tenant_id=context.tenant_id)
)
}
dispatch_match = __import__("re").fullmatch(
r"/api/v2/tasks/([^/]+)/dispatch", path
)
if method == "POST" and dispatch_match:
context = self.context(headers)
task_id = urllib.parse.unquote(dispatch_match.group(1))
payload = body if isinstance(body, Mapping) else {}
timeout = payload.get("timeout_seconds")
provenance_id, run_id = _provenance_context(headers, payload)
record = self.orchestrator.dispatch(
context,
task_id=task_id,
timeout_seconds=float(timeout) if timeout is not None else None,
provenance_id=provenance_id,
run_id=run_id,
)
return HTTPStatus.ACCEPTED, {"dispatch": _json_value(record)}
lifecycle_match = __import__("re").fullmatch(
r"/api/v2/internal/runtime/hermes/lifecycle/"
r"(claim|start|heartbeat|complete|fail|cancel|status)",
path,
)
if method == "POST" and lifecycle_match:
if not self.hermes_lifecycle_api_enabled:
return HTTPStatus.NOT_FOUND, {
"error": "not_found",
"message": "route not found",
}
context = self.context(headers)
self._require_runtime_role(context)
if not isinstance(body, Mapping):
raise ValueError("JSON body must be an object")
payload = body
action = lifecycle_match.group(1)
identity = {
name: self._required_identity(payload, name)
for name in (
"dispatch_id",
"job_id",
"tenant_id",
"task_id",
"trace_id",
"principal_id",
"provenance_id",
"run_id",
"cell_id",
"profile_id",
)
}
header_provenance, header_run = _provenance_context(headers, payload)
if header_provenance != identity["provenance_id"]:
raise ValueError("provenance_id context mismatch")
if header_run != identity["run_id"]:
raise ValueError("run_id context mismatch")
requeued_job_ids = self.hermes.requeue_expired()
dispatch, job = self.orchestrator.hermes_lifecycle_context(
context,
dispatch_id=identity["dispatch_id"],
job_id=identity["job_id"],
tenant_id=identity["tenant_id"],
task_id=identity["task_id"],
trace_id=identity["trace_id"],
principal_id=identity["principal_id"],
provenance_id=identity["provenance_id"],
run_id=identity["run_id"],
)
if job.cell_id != identity["cell_id"]:
raise ValueError("cell_id mismatch")
if job.profile_id != identity["profile_id"]:
raise ValueError("profile_id mismatch")
if action == "status":
if job.job_id in requeued_job_ids:
self._record_hermes_lifecycle(
context,
dispatch,
job,
action="lease_expired_requeue",
worker_id=None,
provenance_id=identity["provenance_id"],
run_id=identity["run_id"],
)
task = self.orchestrator.control_plane.get_task(
context, dispatch.task_id
)
response: dict[str, Any] = {
"job": _json_value(job),
"dispatch": _json_value(dispatch),
"task": _json_value(task),
}
if job.state in {
HermesJobState.COMPLETED,
HermesJobState.FAILED,
HermesJobState.CANCELLED,
}:
response["result"] = _json_value(
self.hermes.result(job.job_id, tenant_id=job.tenant_id)
)
return HTTPStatus.OK, response
expected_revision = payload.get("expected_revision")
expected = (
int(expected_revision) if expected_revision is not None else None
)
if action == "cancel":
self._require_control_plane_cancel(context)
if payload.get("worker_id") is not None:
raise Unauthorized("Hermes workers may observe but not initiate cancel")
if payload.get("cancellation_source") != "aios-control-plane":
raise ValueError(
"cancellation_source must be aios-control-plane"
)
result = self.hermes.cancel(
job.job_id,
tenant_id=job.tenant_id,
expected_revision=expected,
)
else:
worker_id = self._required_identity(payload, "worker_id")
if action == "claim":
claimed = self.hermes.claim(
tenant_id=job.tenant_id,
cell_id=job.cell_id,
profile_id=job.profile_id,
worker_id=worker_id,
lease_seconds=(
int(payload["lease_seconds"])
if payload.get("lease_seconds") is not None
else None
),
job_id=job.job_id,
)
if claimed is None:
raise ValueError("Hermes job is not claimable")
bound = self.orchestrator.bind_hermes_worker(
context,
claimed,
dispatch_id=dispatch.dispatch_id,
provenance_id=identity["provenance_id"],
run_id=identity["run_id"],
worker_id=worker_id,
)
self._record_hermes_lifecycle(
context,
bound,
claimed,
action="claim",
worker_id=worker_id,
provenance_id=identity["provenance_id"],
run_id=identity["run_id"],
)
return HTTPStatus.OK, {
"job": _json_value(claimed),
"dispatch": _json_value(bound),
}
if action == "start":
started = self.hermes.start(
job.job_id,
tenant_id=job.tenant_id,
worker_id=worker_id,
expected_revision=expected,
)
self._record_hermes_lifecycle(
context,
dispatch,
started,
action="start",
worker_id=worker_id,
provenance_id=identity["provenance_id"],
run_id=identity["run_id"],
)
return HTTPStatus.OK, {"job": _json_value(started)}
if action == "heartbeat":
heartbeat = self.hermes.heartbeat(
job.job_id,
tenant_id=job.tenant_id,
worker_id=worker_id,
lease_seconds=(
int(payload["lease_seconds"])
if payload.get("lease_seconds") is not None
else None
),
)
self._record_hermes_lifecycle(
context,
dispatch,
heartbeat,
action="heartbeat",
worker_id=worker_id,
provenance_id=identity["provenance_id"],
run_id=identity["run_id"],
)
return HTTPStatus.OK, {"job": _json_value(heartbeat)}
if action == "complete":
output = payload.get("output")
if not isinstance(output, Mapping):
raise ValueError("output must be an object")
result = self.hermes.complete(
job.job_id,
tenant_id=job.tenant_id,
worker_id=worker_id,
output=output,
expected_revision=expected,
)
elif action == "fail":
error = payload.get("error")
if not isinstance(error, Mapping):
raise ValueError("error must be an object")
result = self.hermes.fail(
job.job_id,
tenant_id=job.tenant_id,
worker_id=worker_id,
error=error,
expected_revision=expected,
)
else:
raise ValueError("unsupported Hermes lifecycle action")
task = self.orchestrator.reconcile_hermes(
context,
result,
dispatch_id=dispatch.dispatch_id,
provenance_id=identity["provenance_id"],
run_id=identity["run_id"],
)
updated_dispatch = self.orchestrator.dispatch_repository.get(
dispatch.dispatch_id, tenant_id=dispatch.tenant_id
)
terminal_job = self.hermes.get(job.job_id, tenant_id=job.tenant_id)
self._record_hermes_lifecycle(
context,
updated_dispatch,
terminal_job,
action=action,
worker_id=result.worker_id,
provenance_id=identity["provenance_id"],
run_id=identity["run_id"],
)
return HTTPStatus.OK, {
"result": _json_value(result),
"task": _json_value(task),
"dispatch": _json_value(updated_dispatch),
}
if method == "POST" and path == "/api/v2/internal/runtime/hermes/reconcile":
context = self.context(headers)
self._require_runtime_role(context)
if not isinstance(body, Mapping):
raise ValueError("JSON body must be an object")
provenance_id, run_id = _provenance_context(headers, body)
if not provenance_id or not run_id:
raise ValueError("provenance_id and run_id are required")
dispatch_id = str(body.get("dispatch_id") or "").strip()
if not dispatch_id:
raise ValueError("dispatch_id is required")
result = HermesResult(
job_id=str(body["job_id"]),
tenant_id=str(body["tenant_id"]),
task_id=str(body["task_id"]),
trace_id=str(body["trace_id"]),
state=HermesJobState(str(body["state"])),
worker_id=(
str(body["worker_id"])
if body.get("worker_id") is not None
else None
),
output=body.get("output"),
error=body.get("error"),
started_at=body.get("started_at"),
finished_at=body.get("finished_at"),
)
task = self.orchestrator.reconcile_hermes(
context,
result,
dispatch_id=dispatch_id,
provenance_id=provenance_id,
run_id=run_id,
)
return HTTPStatus.OK, {"task": _json_value(task)}
if method == "POST" and path == "/api/v2/hermes/results/reconcile":
return HTTPStatus.GONE, {
"error": "RuntimeReconcileEndpointMoved",
"message": "use /api/v2/internal/runtime/hermes/reconcile",
}
return self.gateway.handle(method, raw_path, headers, body)
class CandidateRequestHandler(BaseHTTPRequestHandler):
server_version = "AIOSV2Candidate/2.0"
max_body_bytes = 1_048_576
@property
def application(self) -> CandidateApplication:
return self.server.application # type: ignore[attr-defined]
def _read_body(self) -> Any:
length = int(self.headers.get("Content-Length", "0") or "0")
if length < 0 or length > self.max_body_bytes:
raise ValueError("request body is too large")
if length == 0:
return {}
content_type = self.headers.get("Content-Type", "").split(";", 1)[0].strip()
if content_type != "application/json":
raise ValueError("Content-Type must be application/json")
return json.loads(self.rfile.read(length).decode("utf-8"))
def _security_headers(self) -> None:
self.send_header("Cache-Control", "no-store")
self.send_header("X-Content-Type-Options", "nosniff")
self.send_header("X-Frame-Options", "DENY")
self.send_header("Referrer-Policy", "no-referrer")
self.send_header(
"Content-Security-Policy",
"default-src 'self'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; connect-src 'self'",
)
def _send_json(self, status_code: int, payload: Mapping[str, Any]) -> None:
encoded = json.dumps(
_json_value(payload), ensure_ascii=False, separators=(",", ":")
).encode("utf-8")
self.send_response(int(status_code))
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(encoded)))
self._security_headers()
self.end_headers()
self.wfile.write(encoded)
def _send_console(self) -> None:
encoded = CONSOLE_HTML.encode("utf-8")
self.send_response(HTTPStatus.OK)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Content-Length", str(len(encoded)))
self._security_headers()
self.end_headers()
self.wfile.write(encoded)
def _dispatch(self, method: str) -> None:
path = urllib.parse.urlparse(self.path).path.rstrip("/") or "/"
if method == "GET" and path in {"/", "/console"}:
self._send_console()
return
try:
body = self._read_body() if method == "POST" else {}
status_code, payload = self.application.handle_json(
method,
self.path,
{key: value for key, value in self.headers.items()},
body,
)
except Exception as exc:
status_code, payload = error_response(exc)
self._send_json(status_code, payload)
def do_GET(self) -> None: # noqa: N802
self._dispatch("GET")
def do_POST(self) -> None: # noqa: N802
self._dispatch("POST")
class CandidateHTTPServer(ThreadingHTTPServer):
daemon_threads = True
allow_reuse_address = True
def __init__(self, address: tuple[str, int], application: CandidateApplication) -> None:
self.application = application
super().__init__(address, CandidateRequestHandler)
def build_application() -> CandidateApplication:
state_dir = Path(os.getenv("AIOS_V2_STATE_DIR", "/var/lib/aios-v2")).resolve()
state_dir.mkdir(parents=True, exist_ok=True)
repository = SQLiteSnapshotRepository(state_dir / "control-plane.sqlite3")
dispatch_repository = SQLiteDispatchRepository(state_dir / "dispatches.sqlite3")
service = DurableControlPlaneService(repository)
require_provenance = _enabled(os.getenv("AIOS_REQUIRE_RUN_PROVENANCE", "0"))
provenance_writer = build_provenance_writer_from_env(
state_dir=state_dir, required=require_provenance
)
tracker = ExecutionTracker(
state_dir / "executions",
provenance_writer=provenance_writer,
require_provenance=require_provenance,
)
hermes = HermesAdapter(store=SQLiteHermesStore(state_dir / "hermes.sqlite3"))
openclaw_url = os.getenv("AIOS_OPENCLAW_URL", "").strip()
signing_file = os.getenv("AIOS_OPENCLAW_SIGNING_MATERIAL_FILE", "").strip()
signing_key_id = os.getenv(
"AIOS_OPENCLAW_SIGNING_KEY_ID", "openclaw-v2r1-candidate-ed25519"
).strip()
openclaw = None
resolver = None
if openclaw_url and signing_file:
resolver = FileCommandSignerResolver(
signing_file,
configured_key_id=signing_key_id,
)
transport = HTTPRuntimeTransport(
openclaw_url,
credential_ref=os.getenv("AIOS_OPENCLAW_CREDENTIAL_REF") or None,
)
openclaw = OpenClawAdapter(
transport,
resolver,
callback_sink=AuditCallbackSink(ControlPlaneEventPublisher(service)),
)
gateway = GatewayApplication(service)
orchestrator = ExecutionOrchestrator(
service,
openclaw=openclaw,
hermes=hermes,
openclaw_signing_material_resolver=resolver,
openclaw_key_id=signing_key_id,
provenance_writer=provenance_writer,
require_provenance=require_provenance,
dispatch_repository=dispatch_repository,
)
return CandidateApplication(
gateway,
orchestrator,
tracker,
hermes,
openclaw,
hermes_lifecycle_api_enabled=_enabled(
os.getenv("AIOS_HERMES_LIFECYCLE_API_ENABLED", "0")
),
)
def main() -> None:
host = os.getenv("AIOS_V2_HOST", "127.0.0.1")
port = int(os.getenv("AIOS_V2_PORT", "18920"))
server = CandidateHTTPServer((host, port), build_application())
print(f"AIOS V2 candidate listening on {host}:{port}")
server.serve_forever()
if __name__ == "__main__":
main()