Skip to content

[Stage 1 / P0] governance: event log + per-request authorize + anomaly channel #24

Description

@modacker

[Stage 1 / P0] governance: event log + per-request authorize + anomaly channel

Author: @modacker
Source review: docs/REVIEW-sihankor-baselines-2026-08-28.md Part 4
Tier 1 items #1 (first-run token modal) deferred to Stage 2; this
issue bundles the three work items that share server/lib/events.js
as their foundation.
Source borrow: docs/BORROW-dsh-deepseek-harness-2026-08-28.md
borrows #1 (event log) and #2 (authorize).
Related PR: modacker/MiniMax-Code-Plugins#23 (round 7
Token Plan integration already shipped; this is the next
governance layer).

Why

The mcode-webui plugin is a thin shell over the mcode agent runtime.
Today it has no audit trail, no authorization envelope for
destructive actions, and no anomaly channel
. Three SiHankor
engineering baselines fail:

  • Baseline 1 (LLM is symbol-material generator, not governor):
    destructive routes (DELETE /api/sessions/:id, slash
    delete/clear/reset) are executed as side effects of an LLM
    reply, with no user-facing confirmation beyond a hidden modal.
  • Baseline 3 (human attention only to anomaly signals): errors
    get appended to cs.chat as ! [error] msg, mixed with normal
    flow. The user has to scan the whole conversation to find them.
  • Baseline 4 (verifiability — traceable / checkable / tamper-proof):
    settings.json and sessions.json are full-file overwrites, no
    diff, no transaction, no checksum. Concurrent POST /api/settings
    would race. There is no event log anywhere.

The deepseek-harness (dsh) project demonstrates a clean pattern
for all three: append-only SessionEvent log + per-request
"ask human" authorization + /api/alerts SSE channel. dsh's
package boundaries map cleanly to plugin-scope work in
mcode-webui.

What

Three changes, sharing a new server/lib/events.js module. They
must land together because the authorize helper and the anomaly
channel both write event-log entries, and the event log itself
needs the same append-only semantics.

1. server/lib/events.js — append-only event log

NDJSON line per write to ~/.mcode-webui/events.ndjson. Inspired
by dsh's SessionEvent row format:

{
  "seq": <monotonic>,
  "ts": "<ISO-8601>",
  "actor": "user" | "mcode" | "system",
  "kind": "settings.write" | "sessions.create" | "sessions.delete"
        | "upload.create" | "slash.exec" | "authz.request"
        | "authz.resolve" | "alert.raise",
  "target": "<file path or session id>",
  "before_hash": "<sha256 of prev state or 'null'>",
  "after_hash":  "<sha256 of new state or 'null'>",
  "cid": "<connection id from state-bus>",
  "data": { ...event-specific payload... }
}

Properties (copied from dsh):

  • Append-only — never update, never delete (except by retention
    policy)
  • Monotonic sequence — easy to detect gaps
  • Hash-chained: after_hash includes prev_after_hash for
    tamper-evidence
  • 200 ms write-behind window for high-frequency events (chat ticks)
  • Reverse + forward read passes for replay

Effort: ~300 lines for events.js + tests, plus ~50 lines per write
site.

2. server/lib/authorize.js — per-request "ask human"

// in routes/sessions.js
const r = await authorize({ action: 'delete-session', sessionId: id, ctx });
if (!r.ok) return;  // user declined or timed out
// ... existing delete logic

Helper that:

  1. Sends a needs_authorization event to the requesting connection's
    SSE channel (via state-bus.js)
  2. Blocks the action in a Promise
  3. Resolves when the user clicks allow/deny in the UI
  4. Times out with AuthorizationDeclinedError after N seconds
    (default 5 min)
  5. Writes an events.ndjson line for both the request and the
    resolution

Wrap destructive routes with this:
DELETE /api/sessions/:id, slash delete / clear / reset,
token rotation (if you want explicit confirmation), workspace reset.

This is the cleanest way to enforce SiHankor prohibition 1 ("LLM
must not directly modify knowledge/intent") without rewriting
the host.

Effort: ~250 lines for the helper + 4–5 wrapping sites.

3. server/lib/alerts.js + /api/alerts — anomaly channel

Parallel to state-bus.js and /api/events. Replace
console.warn for user-facing errors with
pushAlert({ level, msg, src }).

Frontend:

  • Top-right bell icon with unread count, click → toast history
  • cs.chat's ! [error] becomes a secondary indicator that
    highlights when an alert is live

Effort: ~300 lines across server + frontend.

Acceptance criteria

  • events.ndjson exists after a settings change, contains
    before_hash / after_hash / actor / kind / cid
  • Tampering with a row (delete or modify) is detectable by
    forward + reverse read pass (test in test/lib-events.test.js)
  • Concurrent POST /api/settings from two connections does
    NOT race (test in test/lib-events-concurrency.test.js)
  • DELETE /api/sessions/:id shows a top-bar modal asking for
    confirmation; declining in the modal leaves the session
    intact; the action emits 2 events (request + resolve)
  • A server-side error in any route pushes to /api/alerts,
    the top-bar bell increments, the toast history shows it
  • npm test passes (target 450/0/2)
  • SiHankor Baseline 1 + 3 + 4 + prohibition 1 + 4 + 5 status
    changes from ✗ to ✓ in the review doc

Out of scope (covered in other issues)

  • First-run token modal — see Stage 2 issue
  • ?token= drop on first access — see Stage 2 issue
  • Slash command autocomplete — see Stage 2 issue
  • mcode-side hooks (event:audit, event:anomaly,
    permission:request) — see host-scope issue

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions