Files
aios/active-runtime/app/aios_openclaw/callback.py
T

98 lines
3.0 KiB
Python

"""Audit-oriented lifecycle callbacks for OpenClaw execution."""
from __future__ import annotations
import json
import threading
from dataclasses import dataclass, field
from typing import Any, Mapping, Protocol
from .contract import OpenClawResult, utc_now
class EventPublisher(Protocol):
def publish(self, event: Mapping[str, Any]) -> None: ...
@dataclass(frozen=True)
class ExecutionEvent:
event_type: str
tenant_id: str
task_id: str
trace_id: str
command_id: str
state: str
created_at: str = field(default_factory=utc_now)
details: Mapping[str, Any] = field(default_factory=dict)
def as_dict(self) -> dict[str, Any]:
return {
"event_type": self.event_type,
"tenant_id": self.tenant_id,
"task_id": self.task_id,
"trace_id": self.trace_id,
"command_id": self.command_id,
"state": self.state,
"created_at": self.created_at,
"details": json.loads(
json.dumps(dict(self.details), ensure_ascii=False, default=str)
),
}
class InMemoryEventPublisher:
"""Thread-safe publisher used by tests and first integration slice."""
def __init__(self) -> None:
self._lock = threading.RLock()
self.events: list[dict[str, Any]] = []
def publish(self, event: Mapping[str, Any]) -> None:
with self._lock:
self.events.append(
json.loads(json.dumps(dict(event), ensure_ascii=False, default=str))
)
class AuditCallbackSink:
"""Convert runtime callbacks into tenant-scoped audit events."""
def __init__(self, publisher: EventPublisher) -> None:
self.publisher = publisher
@staticmethod
def _event(
event_type: str,
result: OpenClawResult,
) -> ExecutionEvent:
details: dict[str, Any] = {
"runtime_id": result.runtime_id,
"attempts": result.attempts,
}
if result.error:
details["error"] = dict(result.error)
if result.output:
details["output_present"] = True
return ExecutionEvent(
event_type=event_type,
tenant_id=result.tenant_id,
task_id=result.task_id,
trace_id=result.trace_id,
command_id=result.command_id,
state=result.state.value,
details=details,
)
def on_started(self, result: OpenClawResult) -> None:
self.publisher.publish(self._event("OPENCLAW_STARTED", result).as_dict())
def on_completed(self, result: OpenClawResult) -> None:
self.publisher.publish(self._event("OPENCLAW_COMPLETED", result).as_dict())
def on_failed(self, result: OpenClawResult) -> None:
event_type = {
"CANCELLED": "OPENCLAW_CANCELLED",
"TIMED_OUT": "OPENCLAW_TIMED_OUT",
"REJECTED": "OPENCLAW_REJECTED",
}.get(result.state.value, "OPENCLAW_FAILED")
self.publisher.publish(self._event(event_type, result).as_dict())