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>
73 lines
2.0 KiB
Python
73 lines
2.0 KiB
Python
import logging
|
|
from contextlib import asynccontextmanager
|
|
from fastapi import FastAPI
|
|
from fastapi.staticfiles import StaticFiles
|
|
import uvicorn
|
|
|
|
logging.basicConfig(level=logging.INFO, format="%(levelname)s:%(name)s: %(message)s")
|
|
|
|
from config import settings
|
|
from auth_middleware import SessionAuthMiddleware
|
|
from routers import chat, google_chat, nextcloud_talk, files, distill, auth, orchestrator
|
|
from routers import ui, onboarding, settings, help, auth_google, local_llm, push, audit
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
import scheduler
|
|
scheduler.start()
|
|
yield
|
|
scheduler.stop()
|
|
from llm_client import cleanup
|
|
await cleanup()
|
|
|
|
|
|
app = FastAPI(title="Cortex Dispatcher", lifespan=lifespan)
|
|
|
|
app.add_middleware(SessionAuthMiddleware)
|
|
|
|
# API routers
|
|
app.include_router(chat.router)
|
|
app.include_router(google_chat.router)
|
|
app.include_router(nextcloud_talk.router)
|
|
app.include_router(files.router)
|
|
app.include_router(distill.router)
|
|
app.include_router(auth.router)
|
|
app.include_router(orchestrator.router)
|
|
app.include_router(push.router)
|
|
app.include_router(audit.router)
|
|
|
|
# Static files — must be mounted BEFORE ui.router so /static/* is matched first.
|
|
# ui.router has a wildcard /{username}/{persona} that would otherwise catch /static/style.css etc.
|
|
app.mount("/static", StaticFiles(directory="static"), name="static")
|
|
|
|
# Google OAuth — must be before ui.router (wildcard /{user}/{persona} would swallow it)
|
|
app.include_router(auth_google.router)
|
|
|
|
# Onboarding (invite tokens + persona creation — before ui.router)
|
|
app.include_router(onboarding.router)
|
|
|
|
# Account settings
|
|
app.include_router(settings.router)
|
|
app.include_router(local_llm.router)
|
|
|
|
# Help page
|
|
app.include_router(help.router)
|
|
|
|
# UI router (login + /{user}/{persona} — must be last to avoid swallowing API paths)
|
|
app.include_router(ui.router)
|
|
|
|
|
|
@app.get("/health")
|
|
async def health() -> dict:
|
|
return {"status": "ok"}
|
|
|
|
|
|
if __name__ == "__main__":
|
|
uvicorn.run(
|
|
"main:app",
|
|
host=settings.host,
|
|
port=settings.port,
|
|
reload=True,
|
|
)
|