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

193 lines
8.0 KiB
Python

"""Authenticated API for agent orchestration observability."""
from __future__ import annotations
from collections.abc import Callable, Mapping
from typing import Any
try:
from fastapi import APIRouter, HTTPException
except Exception: # pragma: no cover
APIRouter = HTTPException = None
from .agent_observability import AgentExecutionRecord
from .agent_observability_dashboard import AgentObservabilityDashboard
from .agent_observability_store import AgentObservabilityStore
store: AgentObservabilityStore | None = None
dashboard = AgentObservabilityDashboard()
_ROLE_WHITELIST = frozenset({"user", "group_admin", "owner"})
_RECORD_FIELDS = frozenset(AgentExecutionRecord.__dataclass_fields__)
_authenticator: Callable[[], Mapping[str, Any] | None] | None = None
class ApiSecurityError(PermissionError):
def __init__(self, status_code: int, code: str):
super().__init__(code)
self.status_code, self.code = status_code, code
def _storage() -> AgentObservabilityStore:
global store
if store is None:
store = AgentObservabilityStore()
return store
def set_authenticator(fn: Callable[[], Mapping[str, Any] | None] | None) -> None:
"""Install the host authenticator; its result is fetched for every call."""
if fn is not None and not callable(fn):
raise TypeError("authenticator must be callable or None")
global _authenticator
_authenticator = fn
def _group_id(principal: Mapping[str, Any]) -> str | None:
value = principal.get("group_id")
return value.strip() if isinstance(value, str) and value.strip() else None
def authenticated_principal() -> dict[str, Any]:
"""Return a request-scoped copy of the server principal, or fail closed."""
if _authenticator is None:
raise ApiSecurityError(401, "AUTHENTICATION_REQUIRED")
try:
raw = _authenticator()
except ApiSecurityError:
raise
except Exception as exc:
raise ApiSecurityError(401, "AUTHENTICATION_FAILED") from exc
if not isinstance(raw, Mapping):
raise ApiSecurityError(401, "AUTHENTICATION_REQUIRED")
principal = dict(raw)
user_id, role = principal.get("user_id"), principal.get("role")
if not isinstance(user_id, str) or not user_id.strip() or role not in _ROLE_WHITELIST:
raise ApiSecurityError(403, "INVALID_AUTHENTICATED_PRINCIPAL")
principal["user_id"] = user_id.strip()
if principal.get("group_id") is not None and _group_id(principal) is None:
raise ApiSecurityError(403, "INVALID_AUTHENTICATED_PRINCIPAL")
if role == "group_admin" and not _group_id(principal):
raise ApiSecurityError(403, "GROUP_SCOPE_REQUIRED")
return principal
def _scope_set(principal: Mapping[str, Any], name: str) -> frozenset[str]:
raw = principal.get(name, ())
if not isinstance(raw, (list, tuple, set, frozenset)) or any(
not isinstance(item, str) or not item for item in raw
):
raise ApiSecurityError(403, "INVALID_AUTHENTICATED_SCOPE")
return frozenset(raw)
def _owner_user(principal: Mapping[str, Any], requested: str | None) -> str:
own, target = str(principal["user_id"]), requested or str(principal["user_id"])
if target != own and target not in _scope_set(principal, "allowed_user_ids"):
raise ApiSecurityError(403, "OWNER_USER_SCOPE_DENIED")
return target
def _owner_group(principal: Mapping[str, Any], requested: str | None) -> str | None:
if requested is None:
return None
if requested != _group_id(principal) and requested not in _scope_set(principal, "allowed_group_ids"):
raise ApiSecurityError(403, "OWNER_GROUP_SCOPE_DENIED")
return requested
def _summary(records: list[dict[str, Any]]) -> dict[str, Any]:
count = len(records)
hermes = sum(1 for row in records if row.get("used_hermes"))
return {
"total_executions": count,
"openclaw_independent_loops": count - hermes,
"hermes_orchestrated_loops": hermes,
"average_duration_ms": (
sum(int(row.get("duration_ms") or 0) for row in records) / count if count else 0
),
"token_usage": {
"input": sum(int(row.get("input_tokens") or 0) for row in records),
"output": sum(int(row.get("output_tokens") or 0) for row in records),
"total": sum(int(row.get("total_tokens") or 0) for row in records),
},
}
def record_agent_execution(payload: Mapping[str, Any]):
principal = authenticated_principal()
if set(payload) - _RECORD_FIELDS:
raise ApiSecurityError(403, "EXECUTION_FIELDS_FORBIDDEN")
normalized, role = dict(payload), principal["role"]
if role == "owner":
requested_user = payload.get("user_id")
if requested_user is not None and not isinstance(requested_user, str):
raise ApiSecurityError(403, "INVALID_EXECUTION_SCOPE")
target_user = _owner_user(principal, requested_user)
requested_scope = payload.get("visible_scope")
if requested_scope is not None and not isinstance(requested_scope, str):
raise ApiSecurityError(403, "INVALID_EXECUTION_SCOPE")
allowed_group = _owner_group(principal, requested_scope) if requested_scope and requested_scope != target_user else None
normalized["user_id"], normalized["visible_scope"] = target_user, allowed_group or target_user
else:
normalized["user_id"] = principal["user_id"]
normalized["visible_scope"] = _group_id(principal) or principal["user_id"]
try:
record = AgentExecutionRecord(**normalized)
except (TypeError, ValueError) as exc:
raise ApiSecurityError(403, "INVALID_EXECUTION_RECORD") from exc
return {"status": "ok", "record": _storage().append(record)}
def get_agent_observability(
user_id: str | None = None, group_id: str | None = None,
role: str = "user",
):
del role # wire compatibility only; server principal is authoritative
principal, pr_role = authenticated_principal(), None
pr_role = principal["role"]
if pr_role == "user":
if user_id not in (None, principal["user_id"]) or group_id is not None:
raise ApiSecurityError(403, "TENANT_SCOPE_DENIED")
q_user, q_group, q_role = principal["user_id"], None, "user"
elif pr_role == "group_admin":
own_group = _group_id(principal)
if group_id not in (None, own_group) or user_id not in (None, principal["user_id"]):
raise ApiSecurityError(403, "TENANT_SCOPE_DENIED")
q_user, q_group, q_role = principal["user_id"], own_group, "group_admin"
elif user_id is None and group_id is None:
q_user, q_group, q_role = principal["user_id"], None, "user"
elif group_id is not None:
q_user = _owner_user(principal, user_id) if user_id is not None else principal["user_id"]
q_group, q_role = _owner_group(principal, group_id), "group_admin"
else:
q_user, q_group, q_role = _owner_user(principal, user_id), None, "user"
records = _storage().query(q_user, q_group, q_role)
return {"status": "ok", "records": records, "summary": _summary(records)}
def _http(call: Callable[[], Any]) -> Any:
try:
return call()
except ApiSecurityError as exc:
if HTTPException is None:
raise
raise HTTPException(status_code=exc.status_code, detail=exc.code) from exc
def build_router():
if APIRouter is None:
return None
router = APIRouter(prefix="/admin/ai-evolution/agents", tags=["agent-observability"])
@router.post("/execution", operation_id="recordAgentExecution")
def record(payload: dict[str, Any]):
return _http(lambda: record_agent_execution(payload))
@router.get("/overview", operation_id="getAgentObservabilityOverview")
def overview(user_id: str | None = None, group_id: str | None = None, role: str = "user"):
return _http(lambda: get_agent_observability(user_id, group_id, role))
return router
router = build_router()
__all__ = ["ApiSecurityError", "authenticated_principal", "set_authenticator", "router", "record_agent_execution", "get_agent_observability"]