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:
@@ -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}]
|
||||||
|
|||||||
@@ -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 ())
|
||||||
|
|||||||
@@ -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`):
|
||||||
|
|||||||
@@ -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, '"')}">${_fmtArgs(e.args)}</td>
|
<td class="at-args" title="${(_fmtArgs(e.args) || '').replace(/"/g, '"')}">${_fmtArgs(e.args)}</td>
|
||||||
<td class="at-result" title="${(e.result_snippet || '').replace(/</g, '<').replace(/"/g, '"')}">${
|
<td class="at-result" title="${(e.result_snippet || '').replace(/</g, '<').replace(/"/g, '"')}">${
|
||||||
(e.result_snippet || '').replace(/</g, '<').slice(0, 80)
|
(e.result_snippet || '').replace(/</g, '<').slice(0, 80)
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user