mirror of
https://github.com/andrewyng/openworker.git
synced 2026-09-04 07:20:10 +00:00
Imported from andrewyng/aisuite@1b4bbf303e (contents of its platform/ directory, hoisted to the repo root). Development history prior to this commit lives in that repository. Co-authored-by: Devika <devikaverma11@gmail.com>
175 lines
5.5 KiB
Python
175 lines
5.5 KiB
Python
"""Durable local audit log for connector/tool actions."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import sqlite3
|
|
import threading
|
|
from pathlib import Path
|
|
from typing import Any, Optional
|
|
|
|
from .connectors import connector_for_tool
|
|
|
|
_SECRET_KEYS = (
|
|
"token",
|
|
"secret",
|
|
"password",
|
|
"api_key",
|
|
"access_token",
|
|
"bot_token",
|
|
"app_token",
|
|
"raw",
|
|
)
|
|
_BODY_KEYS = ("body", "content", "html")
|
|
|
|
|
|
class AuditStore:
|
|
def __init__(self, db_path: str | Path) -> None:
|
|
self.db_path = Path(db_path).expanduser()
|
|
self._lock = threading.RLock()
|
|
self._conn = sqlite3.connect(self.db_path, check_same_thread=False)
|
|
self._conn.row_factory = sqlite3.Row
|
|
self._conn.execute("""
|
|
CREATE TABLE IF NOT EXISTS audit_events (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
timestamp TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
session_id TEXT,
|
|
agent TEXT,
|
|
workspace TEXT,
|
|
connector TEXT,
|
|
tool TEXT,
|
|
stage TEXT,
|
|
status TEXT,
|
|
approval TEXT,
|
|
args TEXT,
|
|
result_preview TEXT,
|
|
reason TEXT,
|
|
resource TEXT
|
|
)
|
|
""")
|
|
self._conn.commit()
|
|
|
|
def append(self, event: dict[str, Any]) -> None:
|
|
tool = str(event.get("tool") or event.get("tool_name") or "")
|
|
connector = str(event.get("connector") or connector_for_tool(tool) or "")
|
|
args = _sanitize_args(tool, event.get("arguments") or {})
|
|
resource = _resource(
|
|
tool, event.get("arguments") or {}, event.get("result") or {}
|
|
)
|
|
with self._lock:
|
|
self._conn.execute(
|
|
"""
|
|
INSERT INTO audit_events
|
|
(session_id, agent, workspace, connector, tool, stage, status, approval, args, result_preview, reason, resource)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
""",
|
|
(
|
|
event.get("session_id") or "",
|
|
event.get("agent") or "",
|
|
event.get("workspace") or "",
|
|
connector,
|
|
tool,
|
|
event.get("stage") or "",
|
|
event.get("status") or "",
|
|
event.get("approval") or "",
|
|
json.dumps(args, default=str),
|
|
_truncate(str(event.get("result_preview") or "")),
|
|
_truncate(str(event.get("reason") or "")),
|
|
_truncate(str(resource or "")),
|
|
),
|
|
)
|
|
self._conn.commit()
|
|
|
|
def list(
|
|
self,
|
|
*,
|
|
limit: int = 100,
|
|
session_id: Optional[str] = None,
|
|
connector: Optional[str] = None,
|
|
tool: Optional[str] = None,
|
|
) -> list[dict[str, Any]]:
|
|
where = []
|
|
params: list[Any] = []
|
|
if session_id:
|
|
where.append("session_id = ?")
|
|
params.append(session_id)
|
|
if connector:
|
|
where.append("connector = ?")
|
|
params.append(connector)
|
|
if tool:
|
|
where.append("tool = ?")
|
|
params.append(tool)
|
|
sql = "SELECT * FROM audit_events"
|
|
if where:
|
|
sql += " WHERE " + " AND ".join(where)
|
|
sql += " ORDER BY id DESC LIMIT ?"
|
|
params.append(max(1, min(int(limit or 100), 500)))
|
|
with self._lock:
|
|
rows = self._conn.execute(sql, params).fetchall()
|
|
out = []
|
|
for row in rows:
|
|
item = dict(row)
|
|
try:
|
|
item["args"] = json.loads(item.get("args") or "{}")
|
|
except json.JSONDecodeError:
|
|
item["args"] = {}
|
|
out.append(item)
|
|
return out
|
|
|
|
def close(self) -> None:
|
|
self._conn.close()
|
|
|
|
|
|
def _sanitize_args(tool: str, args: dict[str, Any]) -> dict[str, Any]:
|
|
if not isinstance(args, dict):
|
|
return {}
|
|
out: dict[str, Any] = {}
|
|
for key, value in args.items():
|
|
lk = str(key).lower()
|
|
if any(s in lk for s in _SECRET_KEYS):
|
|
out[key] = "[redacted]"
|
|
elif tool == "browser_type" and lk == "text":
|
|
out[key] = "[redacted input]"
|
|
elif any(b == lk or lk.endswith("_" + b) for b in _BODY_KEYS):
|
|
out[key] = "[redacted body]"
|
|
else:
|
|
out[key] = _summarize(value)
|
|
return out
|
|
|
|
|
|
def _summarize(value: Any) -> Any:
|
|
if isinstance(value, str):
|
|
return _truncate(value)
|
|
if isinstance(value, (int, float, bool)) or value is None:
|
|
return value
|
|
if isinstance(value, list):
|
|
return [_summarize(v) for v in value[:10]]
|
|
if isinstance(value, dict):
|
|
return {str(k): _summarize(v) for k, v in list(value.items())[:20]}
|
|
return _truncate(str(value))
|
|
|
|
|
|
def _resource(tool: str, args: dict[str, Any], result: Any) -> str:
|
|
for key in (
|
|
"url",
|
|
"owner",
|
|
"repo",
|
|
"issue_key",
|
|
"page_id",
|
|
"ticket_id",
|
|
"calendar_id",
|
|
"message_id",
|
|
):
|
|
if isinstance(args, dict) and args.get(key):
|
|
return str(args[key])
|
|
if isinstance(args, dict) and args.get("subdomain"):
|
|
return f"{args['subdomain']}.zendesk.com"
|
|
if isinstance(result, dict) and result.get("url"):
|
|
return str(result["url"])
|
|
return ""
|
|
|
|
|
|
def _truncate(text: str, limit: int = 500) -> str:
|
|
text = text.replace("\n", "\\n")
|
|
return text if len(text) <= limit else text[: limit - 3] + "..."
|