feat: add audit log tables, indexes, and CLI/content recording hooks

This commit is contained in:
2026-06-11 20:28:17 +00:00
parent 3862958fae
commit 78d70d9b08
58 changed files with 4456 additions and 25 deletions
@@ -67,6 +67,36 @@ def _is_destructive_command(arguments: dict[str, Any]) -> bool:
return bool(DESTRUCTIVE_COMMAND.search(str(arguments.get("command", ""))))
_DEVII_MECHANIC_EVENTS = {
"update_behavior": "devii.behavior.update",
"tool_create": "devii.tool.create",
"tool_update": "devii.tool.update",
"tool_delete": "devii.tool.delete",
"create_task": "devii.task.create",
"update_task": "devii.task.update",
"delete_task": "devii.task.delete",
"reflect": "devii.lesson.reflect",
"forget_lessons": "devii.lesson.forget",
"customize_set_css": "devii.customization.css.set",
"customize_set_js": "devii.customization.js.set",
"customize_reset": "devii.customization.reset",
}
_DEVII_CONTAINER_EVENTS = {
"container_create_instance": "container.instance.create",
"container_exec": "container.instance.exec",
"container_schedule": "container.schedule.create",
}
def _safe_args(arguments: dict[str, Any]) -> dict[str, Any]:
out: dict[str, Any] = {}
for key, value in arguments.items():
text = str(value)
out[key] = text if len(text) <= 200 else text[:200] + "..."
return out
def confirmation_error(name: str, arguments: dict[str, Any]) -> ToolInputError | None:
if _is_confirmed(arguments):
return None
@@ -148,6 +178,8 @@ class Dispatcher:
self._avatar = avatar
self._browser = browser
self._is_admin = is_admin
self._owner_kind = owner_kind
self._owner_id = owner_id
self._fetch = FetchController(settings)
self._docs = DocsController(settings)
self._cost = CostController(quota_provider=quota_provider)
@@ -219,6 +251,7 @@ class Dispatcher:
logger.info("Resource cache hit for %s (%s)", name, resource_key)
return cached
result = await self._run(action, arguments)
self._audit_mechanic(action, arguments)
if action.handler == "chunks":
return result
return wrap_if_large(
@@ -231,6 +264,30 @@ class Dispatcher:
logger.exception("Dispatch %s crashed", name)
return unexpected_result(exc)
def _audit_mechanic(self, action: Action, arguments: dict[str, Any]) -> None:
from devplacepy.services.audit import record as audit
name = action.name
event_key = _DEVII_MECHANIC_EVENTS.get(name)
if event_key is None and action.handler == "container":
if name == "container_instance_action":
event_key = f"container.instance.{arguments.get('action', 'action')}"
else:
event_key = _DEVII_CONTAINER_EVENTS.get(name)
if event_key is None:
return
actor_kind = "user" if self._owner_kind == "user" else self._owner_kind
audit.record_system(
event_key,
actor_kind=actor_kind,
actor_uid=self._owner_id if self._owner_kind == "user" else None,
actor_role="admin" if self._is_admin else (actor_kind if actor_kind != "user" else "member"),
origin="devii",
via_agent=1,
summary=f"Devii {name} for {self._owner_id or self._owner_kind}",
metadata={"tool": name, "args": _safe_args(arguments)},
)
async def _run(self, action: Action, arguments: dict[str, Any]) -> str:
if action.handler == "status":
return json.dumps(
+5 -1
View File
@@ -20,7 +20,11 @@ class PlatformClient:
def __init__(
self, base_url: str, timeout_seconds: float, api_key: str = ""
) -> None:
headers = {"User-Agent": "devii/0.1", "Accept": "application/json"}
headers = {
"User-Agent": "devii/0.1",
"Accept": "application/json",
"X-Devii-Agent": "1",
}
if api_key:
headers["Authorization"] = f"Bearer {api_key}"
headers["X-API-Key"] = api_key
+20
View File
@@ -465,6 +465,26 @@ class DeviiSession:
"error": error,
}
)
from devplacepy.services.audit import record as audit
audit.record_system(
"devii.turn",
actor_kind="user" if self.owner_kind == "user" else self.owner_kind,
actor_uid=self.owner_id if self.owner_kind == "user" else None,
actor_username=self.username,
actor_role="user" if self.owner_kind == "user" else self.owner_kind,
origin="devii",
via_agent=1,
result="failure" if error else "success",
summary=f"Devii processed a turn for {self.username or self.owner_id}",
metadata={
"iterations": after["requests"] - before["requests"],
"tool_calls": self._turn_tool_calls,
"cost_usd": cost_delta,
"prompt_tokens": usage["prompt_tokens"],
"completion_tokens": usage["completion_tokens"],
},
)
except Exception: # noqa: BLE001 - persistence must never break a turn
logger.exception(
"Failed to persist turn for %s/%s", self.owner_kind, self.owner_id
@@ -12,6 +12,27 @@ from .store import TaskStore
logger = logging.getLogger("devii.tasks.scheduler")
def _audit_task_execute(row: dict[str, Any], result: str, error: str | None) -> None:
from devplacepy.services.audit import record as audit
owner_kind = row.get("owner_kind") or "user"
owner_id = row.get("owner_id") or ""
audit.record_system(
"devii.task.execute",
actor_kind="user" if owner_kind == "user" else owner_kind,
actor_uid=owner_id if owner_kind == "user" else None,
actor_role="user" if owner_kind == "user" else owner_kind,
origin="scheduler",
via_agent=1,
result="failure" if error else "success",
target_type="task",
target_uid=row.get("uid"),
summary=f"Devii task {row.get('uid')} for {owner_id or owner_kind} executed",
metadata={"run_count": row.get("run_count"), "error": error, "result": (result or "")[:140]},
links=[audit.task(row.get("uid"))],
)
PromptExecutor = Callable[[str], Awaitable[str]]
EventCallback = Callable[[str, dict[str, Any], str], None]
@@ -84,7 +105,9 @@ class Scheduler:
},
)
self._on_event("error", row, str(exc))
_audit_task_execute(row, "", str(exc))
return
self._store.update(uid, changes)
self._on_event("done" if not finished else "finished", row, result)
_audit_task_execute(row, result, None)