14, Audit Logging

Audit Logging

An append-only JSONL trail of every desk decision, runs, approvals, order events, halts, and injection attempts, written through a dependency-free helper.

The desk keeps an append-only record of what it did: every desk run, every approval, every order event, every trading halt, and every prompt-injection attempt the Macro/News Analyst flags. This realizes the project's Transparency & Audit principle and satisfies `CLAUDE.md` step 6, *log it where the user asks*.

Where the log lives

text
logs/
├── desk-runs.example.jsonl   # committed demo arc (illustrative only, not a track record)
├── desk-runs.jsonl           # real, gitignored, holds live account decisions
├── .gitignore                # ignores *.jsonl EXCEPT the example
└── .gitkeep                  # keeps the dir in git even when only real logs exist

Real logs are gitignored, exactly like `ui/public/desk-state.json`. Only the sanitized *.example.jsonl is committed, so a fresh clone has something to read. Everything is illustrative unless fed real data.

Format: one JSON object per line

  • One record = one line. No pretty-printing, no array wrapping the file.
  • Append-only. New records are appended; existing lines are never edited, reordered, or deleted. An audit trail you can rewrite is not an audit trail.
  • UTF-8, newline-terminated. Blank lines are ignored by the tooling.

Every line carries three required keys, ts (ISO-8601 with timezone offset), event, and session, plus a few descriptive fields per event. A one-line summary is recommended on every record; the dashboard uses it verbatim.

event is one of six values: desk_run, approval, order_placed, order_filled, halt, injection.

Per-event fields

desk_run
One entry per pass. tickers (screened symbols), proposed (the proposed trade or null, shaped { symbol, side, qty, strategy, riskDecision }), vetoes (symbols the Risk Manager vetoed), injectionAlerts (count flagged), summary.
approval
The human decision on a preview card. symbol, decision (approved / rejected), by (e.g. user), summary.
order_placed
An order was submitted, only ever after an approval. symbol, side, qty, type (limit / market), optional limitPrice, status (e.g. submitted), summary.
order_filled
A fill confirmation. symbol, qty (filled), price, status (e.g. filled), summary.
halt
The desk stopped trading (e.g. the daily-loss halt). reason (e.g. daily-loss-halt), optional dayPnlPct and limitPct, symbol usually null (account-level), summary.
injection
A prompt-injection attempt the Macro/News Analyst flagged, recorded as a quote and never acted on. source, quote (verbatim), handledBy (usually macro-news-analyst), action (quoted and ignored), symbol usually null, summary.

Example arc

logs/desk-runs.example.jsonl
{"ts":"2026-07-11T10:05:00-04:00","session":"demo","event":"desk_run","tickers":["MSFT","NVDA"],"summary":"Screened 2; proposed BUY 3 MSFT (mean-reversion); NVDA vetoed.","proposed":{"symbol":"MSFT","side":"buy","qty":3,"strategy":"mean-reversion","riskDecision":"APPROVE"},"vetoes":["NVDA"],"injectionAlerts":1}
{"ts":"2026-07-11T10:07:30-04:00","session":"demo","event":"approval","symbol":"MSFT","decision":"approved","by":"user"}
{"ts":"2026-07-11T10:07:31-04:00","session":"demo","event":"order_placed","symbol":"MSFT","side":"buy","qty":3,"type":"limit","limitPrice":415.5,"status":"submitted"}
{"ts":"2026-07-11T10:07:45-04:00","session":"demo","event":"order_filled","symbol":"MSFT","qty":3,"price":415.5,"status":"filled"}

The helper: tools/desk-log.mjs

tools/desk-log.mjs is a pure Node ESM helper (no dependencies) that validates and appends records. appendRecord throws, writing nothing, if ts, event, or session is missing or if event is outside the enum, so a malformed record can never enter the trail.

text
Library:
  appendRecord(file, record)  → validate required keys + event enum, then append exactly one line
  readTail(file, n)           → last n parsed records (oldest → newest)
  toDeskState(records)        → map records to the dashboard decisionLog tail
  validateFile(file)          → { ok, count, errors: [{ line, message }] }

CLI:
  node tools/desk-log.mjs validate  <file.jsonl>       parse + assert every line
  node tools/desk-log.mjs tail      <file.jsonl> [n]   pretty-print last n
  node tools/desk-log.mjs deskstate <file.jsonl> [n]   print decisionLog[] JSON

Mapping to the dashboard

The log surfaces on the dashboard as a decisionLog array, a compact tail flattened to { ts, event, summary, symbol, tone }. tone drives the badge/accent color and is derived deterministically:

Eventtone
order_filledpos
approvalpos if approved, neg if rejected, else flat
order_placedflat
desk_runwarn if injectionAlerts > 0, else flat
haltneg
injectionwarn

Generate the array and paste it into desk-state.json after a run:

bash
node tools/desk-log.mjs deskstate logs/desk-runs.jsonl 20

Related: Prompt-Injection Defense is the source of injection events; Strategies & Risk defines the daily-loss halt a halt event records.