feat: engine/model in audit log + docs update

- tool_audit: ContextVars (engine, model) set at orchestrator run start; fields added to every entry
- orchestrator_engine: tool_audit.set_context("gemini", model_name) at run() start
- openai_orchestrator: tool_audit.set_context("openai", model label) at run() start
- audit table: Model column between Status and Args
- HELP.md: push notifications section, audit log in Files section, tool count 30→40, new API endpoints
- TODO__Agents.md: web_push and audit log marked complete with full detail

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-05-05 20:42:32 -04:00
parent 02accefe8f
commit 7d221863dc
7 changed files with 55 additions and 6 deletions

View File

@@ -26,6 +26,7 @@ from openai import AsyncOpenAI
from config import settings from config import settings
from orchestrator_engine import OrchestrateCheckpoint, OrchestratorResult from orchestrator_engine import OrchestrateCheckpoint, OrchestratorResult
from tools import OPENAI_TOOL_SCHEMAS, call_tool, get_openai_tools_for_role, get_tools_for_role, CONFIRM_REQUIRED from tools import OPENAI_TOOL_SCHEMAS, call_tool, get_openai_tools_for_role, get_tools_for_role, CONFIRM_REQUIRED
import tool_audit
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -73,6 +74,7 @@ async def run(
effective_confirm = (CONFIRM_REQUIRED - set(_confirm_allow)) | set(_confirm_deny) effective_confirm = (CONFIRM_REQUIRED - set(_confirm_allow)) | set(_confirm_deny)
client, model_name, active_tools = _build_client(model_cfg, user_role, tool_list) client, model_name, active_tools = _build_client(model_cfg, user_role, tool_list)
tool_audit.set_context("openai", model_cfg.get("label") or model_name)
sys_content = (system_prompt or "") + _TOOL_INSTRUCTION sys_content = (system_prompt or "") + _TOOL_INSTRUCTION
messages: list[dict] = [{"role": "system", "content": sys_content}] messages: list[dict] = [{"role": "system", "content": sys_content}]

View File

@@ -27,6 +27,7 @@ from config import settings
from llm_client import complete from llm_client import complete
from tools import TOOL_DECLARATIONS, call_tool, get_tools_for_role, CONFIRM_REQUIRED from tools import TOOL_DECLARATIONS, call_tool, get_tools_for_role, CONFIRM_REQUIRED
import usage_tracker import usage_tracker
import tool_audit
from persona import _user from persona import _user
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -140,6 +141,7 @@ async def run(
) )
client = genai.Client(api_key=api_key) client = genai.Client(api_key=api_key)
tool_audit.set_context("gemini", model_name or settings.orchestrator_model)
_confirm_allow = frozenset(confirm_allow or ()) _confirm_allow = frozenset(confirm_allow or ())
_confirm_deny = frozenset(confirm_deny or ()) _confirm_deny = frozenset(confirm_deny or ())

View File

@@ -6,7 +6,7 @@
and are appended automatically by help.html when present. and are appended automatically by help.html when present.
--> -->
*Last updated: 2026-04-30* *Last updated: 2026-05-05*
--- ---
@@ -16,7 +16,7 @@
|---|---| |---|---|
| **Sessions** | Open the sessions panel — list, resume, or start sessions | | **Sessions** | Open the sessions panel — list, resume, or start sessions |
| **N** (sliders icon) | Open the Context & Memory panel (N = current context tier) | | **N** (sliders icon) | Open the Context & Memory panel (N = current context tier) |
| **☰** | Settings menu — Files, Account, Sign Out | | **☰** | Settings menu — Files, push notification toggle, Account, Sign Out |
| **?** | Open this help panel | | **?** | Open this help panel |
The **Context & Memory** panel (sliders icon with tier number) contains all configuration options: The **Context & Memory** panel (sliders icon with tier number) contains all configuration options:
@@ -59,7 +59,7 @@ The orchestrator runs a multi-step tool loop:
The ⚡ toggle is **independent of the Role selector** — you can use any role (chat, coder, research, etc.) with or without tools. The orchestrator model is configured in **Account → Model Registry → Role Assignments → Orchestrator**. By default this is Gemini API. The ⚡ toggle is **independent of the Role selector** — you can use any role (chat, coder, research, etc.) with or without tools. The orchestrator model is configured in **Account → Model Registry → Role Assignments → Orchestrator**. By default this is Gemini API.
The full tool reference is in the **Tools** tab. 30 tools across web, files, shell, system, tasks, cron, reminders, scratchpad, notifications, and Aether Journals. The full tool reference is in the **Tools** tab. 40 tools across web, files, shell, system, tasks, cron, reminders, scratchpad, notifications, and Aether Journals.
Tools mode is best for tasks requiring research, multi-step reasoning, or side effects (e.g. "search for X", "add a task", "what's on my list?", "append this to my journal"). Regular chat is faster for conversational turns. Tools mode is best for tasks requiring research, multi-step reasoning, or side effects (e.g. "search for X", "add a task", "what's on my list?", "append this to my journal"). Regular chat is faster for conversational turns.
@@ -222,6 +222,19 @@ The **Files** button opens an editor for your persona's identity and memory file
Toggle **preview** / **edit** to switch between rendered markdown and raw text. **Ctrl+S** saves, **Esc** closes. Toggle **preview** / **edit** to switch between rendered markdown and raw text. **Ctrl+S** saves, **Esc** closes.
The **Audit Log** group at the bottom of the sidebar (collapsed by default) lists tool call logs by date (`YYYY-MM-DD.jsonl`). Click any date to view a read-only table of every orchestrator tool call: time, tool name, status, model, args, and result snippet. Status is colour-coded: green = ok, red = error, amber = denied.
---
## Push Notifications
Cortex can send browser push notifications — even when the tab is closed.
- Open **☰ → Enable notifications** and accept the browser permission prompt.
- Once enabled, the button shows **Notifications on** (in accent colour).
- Click again to disable. Subscriptions are stored per-device.
- The orchestrator's `web_push` tool lets Inara send you a push proactively (e.g. when a long task completes).
--- ---
## Context & Memory ( ⚙ panel ) ## Context & Memory ( ⚙ panel )
@@ -305,6 +318,13 @@ For direct access or scripting:
| `GET` | `/orchestrate/{job_id}` | Poll job status and result | | `GET` | `/orchestrate/{job_id}` | Poll job status and result |
| `GET` | `/settings/models` | Model registry UI | | `GET` | `/settings/models` | Model registry UI |
| `POST` | `/api/models/role` | Set a role assignment (JSON body) | | `POST` | `/api/models/role` | Set a role assignment (JSON body) |
| `GET` | `/api/push/vapid-key` | VAPID public key (for push subscription) |
| `POST` | `/api/push/subscribe` | Register a push subscription |
| `DELETE` | `/api/push/subscribe` | Remove a push subscription |
| `GET` | `/api/audit/files` | List available audit log dates (own data) |
| `GET` | `/api/audit/day?date=` | Tool call entries for a specific date (own data) |
| `GET` | `/api/audit/recent` | Recent tool calls across days (admin) |
| `GET` | `/api/audit/stats` | Tool call counts by tool/status/user (admin) |
| `GET` | `/health` | Health check — returns `{"status": "ok"}` | | `GET` | `/health` | Health check — returns `{"status": "ok"}` |
Chat request body (`POST /chat`): Chat request body (`POST /chat`):

View File

@@ -1547,6 +1547,7 @@
<th class="at-time">Time</th> <th class="at-time">Time</th>
<th class="at-tool">Tool</th> <th class="at-tool">Tool</th>
<th class="at-status">Status</th> <th class="at-status">Status</th>
<th class="at-model">Model</th>
<th class="at-args">Args</th> <th class="at-args">Args</th>
<th class="at-result">Result</th> <th class="at-result">Result</th>
</tr></thead>`; </tr></thead>`;
@@ -1554,11 +1555,13 @@
const tbody = document.createElement('tbody'); const tbody = document.createElement('tbody');
for (const e of entries) { for (const e of entries) {
const time = (e.ts || '').slice(11, 19); // HH:MM:SS const time = (e.ts || '').slice(11, 19); // HH:MM:SS
const model = e.model || e.engine || '';
const tr = document.createElement('tr'); const tr = document.createElement('tr');
tr.innerHTML = ` tr.innerHTML = `
<td class="at-time">${time}</td> <td class="at-time">${time}</td>
<td class="at-tool" title="${e.tool || ''}">${e.tool || '?'}</td> <td class="at-tool" title="${e.tool || ''}">${e.tool || '?'}</td>
<td class="${_auditStatusClass(e.status)}">${e.status || '?'}</td> <td class="${_auditStatusClass(e.status)}">${e.status || '?'}</td>
<td class="at-model" title="${model}">${model}</td>
<td class="at-args" title="${(_fmtArgs(e.args) || '').replace(/"/g, '&quot;')}">${_fmtArgs(e.args)}</td> <td class="at-args" title="${(_fmtArgs(e.args) || '').replace(/"/g, '&quot;')}">${_fmtArgs(e.args)}</td>
<td class="at-result" title="${(e.result_snippet || '').replace(/</g, '&lt;').replace(/"/g, '&quot;')}">${ <td class="at-result" title="${(e.result_snippet || '').replace(/</g, '&lt;').replace(/"/g, '&quot;')}">${
(e.result_snippet || '').replace(/</g, '&lt;').slice(0, 80) (e.result_snippet || '').replace(/</g, '&lt;').slice(0, 80)

View File

@@ -1261,7 +1261,8 @@
.at-time { width: 7em; color: var(--muted); white-space: nowrap; } .at-time { width: 7em; color: var(--muted); white-space: nowrap; }
.at-tool { width: 11em; color: var(--accent); font-weight: 500; } .at-tool { width: 11em; color: var(--accent); font-weight: 500; }
.at-status { width: 4.5em; font-weight: 600; } .at-status { width: 4.5em; font-weight: 600; }
.at-args { width: 30%; color: var(--muted); } .at-model { width: 10em; color: var(--muted); }
.at-args { width: 25%; color: var(--muted); }
.at-result { color: var(--muted); } .at-result { color: var(--muted); }
.at-status.ok { color: #4ade80; } .at-status.ok { color: #4ade80; }
.at-status.error { color: #f87171; } .at-status.error { color: #f87171; }

View File

@@ -16,6 +16,7 @@ Each line is a JSON object:
import asyncio import asyncio
import json import json
import logging import logging
from contextvars import ContextVar
from datetime import datetime, date from datetime import datetime, date
from pathlib import Path from pathlib import Path
@@ -29,6 +30,16 @@ _SNIPPET_MAX = 300 # chars of result to keep as snippet
# Per-file write locks — prevents interleaved lines under concurrent tool calls # Per-file write locks — prevents interleaved lines under concurrent tool calls
_locks: dict[str, asyncio.Lock] = {} _locks: dict[str, asyncio.Lock] = {}
# ContextVars set by orchestrators before their tool loop runs
_audit_engine: ContextVar[str] = ContextVar("_audit_engine", default="")
_audit_model: ContextVar[str] = ContextVar("_audit_model", default="")
def set_context(engine: str, model: str) -> None:
"""Call at the start of each orchestrator run to tag subsequent tool calls."""
_audit_engine.set(engine)
_audit_model.set(model)
def _truncate_args(args: dict) -> dict: def _truncate_args(args: dict) -> dict:
out = {} out = {}
@@ -63,6 +74,8 @@ async def record(
entry = { entry = {
"ts": datetime.now().isoformat(timespec="seconds"), "ts": datetime.now().isoformat(timespec="seconds"),
"user": user, "user": user,
"engine": _audit_engine.get(),
"model": _audit_model.get(),
"tool": tool, "tool": tool,
"args": _truncate_args(args), "args": _truncate_args(args),
"status": status, "status": status,

View File

@@ -39,8 +39,7 @@ New tools for `cortex/tools/` — higher-value additions that fill obvious gaps.
- [x] **`file_write`** — overwrite/append to home_root paths, admin-only, confirm-required — 2026-04-29 - [x] **`file_write`** — overwrite/append to home_root paths, admin-only, confirm-required — 2026-04-29
- [x] **`nc_talk_send`** — outbound NC Talk message via notification.py, admin-only — 2026-04-29 - [x] **`nc_talk_send`** — outbound NC Talk message via notification.py, admin-only — 2026-04-29
- [x] **`email_send`** — SMTP via email_utils, per-user regex allowlist in `home/{user}/email_allowlist.json`, managed via Settings UI textarea + Files panel raw editor — 2026-04-29 - [x] **`email_send`** — SMTP via email_utils, per-user regex allowlist in `home/{user}/email_allowlist.json`, managed via Settings UI textarea + Files panel raw editor — 2026-04-29
- [ ] **`web_push`** — send a browser push notification (requires push subscription stored - [x] **`web_push`** — VAPID push via pywebpush; subscriptions in `home/{user}/push_subscriptions.json`; "Enable notifications" toggle in ☰ menu; sw.js push+notificationclick handlers — 2026-05-05
per-user; pairs well with the PWA service worker already in place)
### [Channel] Proactive notifications ### [Channel] Proactive notifications
Inara reaches out on her own initiative via NC Talk or Google Chat when a reminder Inara reaches out on her own initiative via NC Talk or Google Chat when a reminder
@@ -121,6 +120,15 @@ so Scott can see who's spending what.
- [ ] Expose via `/api/usage` endpoint; add a summary row to the Settings page - [ ] Expose via `/api/usage` endpoint; add a summary row to the Settings page
- [ ] Optional: soft spending limit with a warning toast when exceeded - [ ] Optional: soft spending limit with a warning toast when exceeded
### [Security] Tool call audit log — 2026-05-05
Every orchestrator tool invocation logged to `home/{user}/tool_audit/YYYY-MM-DD.jsonl`.
- [x] `tool_audit.py` — JSONL writer with asyncio locks; ContextVars for engine/model set by each orchestrator at run start
- [x] Hook in `call_tool()` — fire-and-forget `asyncio.create_task`; captures status ok/error/denied, 300-char result snippet, args (truncated at 500 chars)
- [x] `GET /api/audit/files` — lists available dates for current user (self-service)
- [x] `GET /api/audit/day?date=` — returns entries for one date (self-service)
- [x] `GET /api/audit/recent` + `/stats` — cross-user aggregation (admin only)
- [x] "Audit Log" group in Files panel sidebar (collapsed by default) — read-only table with time/tool/status/model/args/result columns; colour-coded status
### [Intelligence] Dev agent pipeline ### [Intelligence] Dev agent pipeline
See `ARCH__Intelligence_Layer.md`. Full design not yet started. See `ARCH__Intelligence_Layer.md`. Full design not yet started.
- [ ] Specialist agent: frontend (SvelteKit) code changes - [ ] Specialist agent: frontend (SvelteKit) code changes