[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:
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:
- Sends a
needs_authorization event to the requesting connection's
SSE channel (via state-bus.js)
- Blocks the action in a Promise
- Resolves when the user clicks allow/deny in the UI
- Times out with
AuthorizationDeclinedError after N seconds
(default 5 min)
- 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
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
[Stage 1 / P0] governance: event log + per-request authorize + anomaly channel
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:
destructive routes (
DELETE /api/sessions/:id, slashdelete/clear/reset) are executed as side effects of an LLMreply, with no user-facing confirmation beyond a hidden modal.
get appended to
cs.chatas! [error] msg, mixed with normalflow. The user has to scan the whole conversation to find them.
settings.jsonandsessions.jsonare full-file overwrites, nodiff, no transaction, no checksum. Concurrent
POST /api/settingswould race. There is no event log anywhere.
The deepseek-harness (dsh) project demonstrates a clean pattern
for all three: append-only
SessionEventlog + per-request"ask human" authorization +
/api/alertsSSE channel. dsh'spackage boundaries map cleanly to plugin-scope work in
mcode-webui.
What
Three changes, sharing a new
server/lib/events.jsmodule. Theymust 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 logNDJSON line per write to
~/.mcode-webui/events.ndjson. Inspiredby dsh's
SessionEventrow 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):
policy)
after_hashincludesprev_after_hashfortamper-evidence
Effort: ~300 lines for
events.js+ tests, plus ~50 lines per writesite.
2.
server/lib/authorize.js— per-request "ask human"Helper that:
needs_authorizationevent to the requesting connection'sSSE channel (via
state-bus.js)AuthorizationDeclinedErrorafter N seconds(default 5 min)
events.ndjsonline for both the request and theresolution
Wrap destructive routes with this:
DELETE /api/sessions/:id, slashdelete/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 channelParallel to
state-bus.jsand/api/events. Replaceconsole.warnfor user-facing errors withpushAlert({ level, msg, src }).Frontend:
cs.chat's! [error]becomes a secondary indicator thathighlights when an alert is live
Effort: ~300 lines across server + frontend.
Acceptance criteria
events.ndjsonexists after a settings change, containsbefore_hash/after_hash/actor/kind/cidforward + reverse read pass (test in
test/lib-events.test.js)POST /api/settingsfrom two connections doesNOT race (test in
test/lib-events-concurrency.test.js)DELETE /api/sessions/:idshows a top-bar modal asking forconfirmation; declining in the modal leaves the session
intact; the action emits 2 events (request + resolve)
/api/alerts,the top-bar bell increments, the toast history shows it
npm testpasses (target 450/0/2)changes from ✗ to ✓ in the review doc
Out of scope (covered in other issues)
?token=drop on first access — see Stage 2 issueevent:audit,event:anomaly,permission:request) — see host-scope issue