feat: tool call audit log
Every orchestrator tool invocation is recorded to home/{user}/tool_audit/YYYY-MM-DD.jsonl.
Each entry captures: timestamp, user, tool, args (truncated), status (ok/error/denied),
result length, and a 300-char result snippet.
- tool_audit.py: JSONL writer with per-file asyncio locks; read_recent / read_recent_all_users helpers
- tools/__init__.py: hook in call_tool() — fire-and-forget record on every dispatch
- routers/audit.py: GET /api/audit/recent and /api/audit/stats (admin-only)
- tools/files.py: add home_root() to file_read allowed roots so agents can read audit JSONL
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -207,12 +207,28 @@ async def call_tool(name: str, args: dict, callables: dict | None = None) -> str
|
||||
|
||||
Pass `callables` (from get_tools_for_role) to enforce role restrictions.
|
||||
Falls back to the full _CALLABLES dict if omitted.
|
||||
|
||||
Every call is recorded to the tool audit log (tool_audit.py).
|
||||
"""
|
||||
import asyncio
|
||||
import tool_audit
|
||||
from persona import get_user
|
||||
|
||||
user = get_user() or "unknown"
|
||||
dispatch = callables if callables is not None else _CALLABLES
|
||||
fn = dispatch.get(name)
|
||||
|
||||
if fn is None:
|
||||
asyncio.create_task(tool_audit.record(user, name, args, "denied"))
|
||||
return f"Tool not available or access denied: {name}"
|
||||
return await fn(**args)
|
||||
|
||||
try:
|
||||
result = await fn(**args)
|
||||
asyncio.create_task(tool_audit.record(user, name, args, "ok", result))
|
||||
return result
|
||||
except Exception as e:
|
||||
asyncio.create_task(tool_audit.record(user, name, args, "error", str(e)))
|
||||
raise
|
||||
|
||||
|
||||
# ── OpenAI JSON Schema conversion ────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user