Files
box/bin/main-chat-watchdog.py
T
operator-main 550e30901b fix: placement verification and sidechat policy enforcement
- dm.py: placement-aware verify (verify_placement), checked_uuid logging, placement_mismatch events
- main-chat-watchdog.py: P0 alerts on placement_mismatch
- box-ctl.py: notify routes to sidechat, box policy command
- jobs: ops-audit and pipe-demo use sidechats
- response-harvester.py: chain deduplication

Session: sidechat/ops-restore
2026-10-04 18:29:13 +00:00

195 lines
7.9 KiB
Python
Executable File

#!/usr/bin/env python3
"""main-chat-watchdog.py — enforce the sidechat-first DM policy.
Tails dm-log.jsonl (read-only) and flags any DM actually SENT to main chat
without an explicit allow_main_chat marker, plus any placement_mismatch /
placement_failed events (message landed in the wrong chat). Distinguishes:
VIOLATION : type=sent, target=main, no tags.allow_main_chat -> policy broken (P0)
AUTHORIZED: type=sent, target=main, tags.allow_main_chat=true -> explicit opt-in
BLOCKED : type=main_chat_blocked -> policy working
MISMATCH : type=placement_mismatch -> placed in wrong chat (P0)
PFAILED : type=placement_failed -> placement hard-failed (P0)
State: byte-offset watermark in main-chat-watchdog.state (handles log
rotation via inode check). First run starts at EOF (no historical backfill —
old entries predate the allow_main_chat audit marker).
Violations are appended to logs/main-chat-violations.jsonl and printed to
stdout (journal). Exit 0 = clean, 1 = violations/P0s found, 2 = error.
Read-only: never modifies dm-log.jsonl.
"""
import json
import os
import sys
from datetime import datetime, timezone
BASE = "/home/super/Projects/NetVM"
DM_LOG = os.path.join(BASE, "dm-log.jsonl")
STATE_FILE = os.path.join(BASE, "main-chat-watchdog.state")
VIOLATIONS_LOG = os.path.join(BASE, "logs", "main-chat-violations.jsonl")
def utcnow():
return datetime.now(timezone.utc).isoformat()
def load_state():
try:
with open(STATE_FILE) as f:
return json.load(f)
except (FileNotFoundError, json.JSONDecodeError, ValueError):
return {}
def save_state(state):
tmp = STATE_FILE + ".tmp"
with open(tmp, "w") as f:
json.dump(state, f)
os.replace(tmp, STATE_FILE)
def main():
try:
st = os.stat(DM_LOG)
except FileNotFoundError:
print(f"watchdog ERROR: {DM_LOG} not found", file=sys.stderr)
return 2
state = load_state()
# First run (or rotation): start at EOF, don't backfill history that
# predates the allow_main_chat audit marker.
if state.get("inode") != st.st_ino:
offset = st.st_size
if state:
print(f"watchdog: log rotated or first run (inode {st.st_ino}), "
f"starting at EOF offset {offset}")
else:
offset = min(state.get("offset", 0), st.st_size)
violations = [] # P0: gate bypass (sent to main, no opt-in)
mismatches = [] # P0: placement_mismatch / placement_failed
blocked = 0
authorized = 0
scanned = 0
malformed = 0
try:
with open(DM_LOG, "r", encoding="utf-8", errors="replace") as f:
f.seek(offset)
for line in f:
line = line.strip()
if not line:
continue
scanned += 1
try:
ev = json.loads(line)
except json.JSONDecodeError:
malformed += 1
continue
etype = ev.get("type")
if etype == "main_chat_blocked":
# Policy working: the dm.py gate refused a main send.
blocked += 1
elif etype == "placement_mismatch":
# P0: verify-time URL check found the browser parked in a
# different chat than the intended target. The send
# completed but landed in the wrong place. Schema has two
# variants: sidechat ("expected_uuid") and main-drift
# ("expected": "main").
mismatches.append({
"ts": utcnow(),
"kind": "placement_mismatch",
"severity": "P0",
"dm_id": ev.get("id"),
"agent": ev.get("agent"),
"to": ev.get("to"),
"target": ev.get("target"),
"expected_uuid": ev.get("expected_uuid") or ev.get("expected"),
"actual_uuid": ev.get("actual_uuid"),
"attempt": ev.get("attempt"),
"event_ts": ev.get("ts"),
})
elif etype == "placement_failed":
# P0: the authoritative post-send placement check failed
# hard (sender exited 1, send NOT marked verified).
d = ev.get("detail") or {}
mismatches.append({
"ts": utcnow(),
"kind": "placement_failed",
"severity": "P0",
"dm_id": ev.get("id"),
"agent": ev.get("agent"),
"to": ev.get("to"),
"target": ev.get("target"),
"reason": ev.get("reason") or d.get("reason"),
"expected_uuid": ev.get("expected_uuid") or d.get("expected_uuid"),
"actual_url": ev.get("actual_url") or d.get("actual_url"),
"loop_attempt": ev.get("loop_attempt"),
"event_ts": ev.get("ts"),
})
elif etype == "sent" and ev.get("target") == "main":
tags = ev.get("tags") or {}
if tags.get("allow_main_chat"):
authorized += 1
else:
violations.append({
"ts": utcnow(),
"kind": "main_chat_send",
"severity": "P0",
"dm_id": ev.get("id"),
"agent": ev.get("agent"),
"to": ev.get("to"),
"target": "main",
"msg_preview": (ev.get("msg") or "")[:120],
"event_ts": ev.get("ts"),
})
new_offset = f.tell()
except OSError as e:
print(f"watchdog ERROR reading log: {e}", file=sys.stderr)
return 2
save_state({"offset": new_offset, "inode": st.st_ino})
findings = violations + mismatches
summary = (f"watchdog: scanned={scanned} blocked={blocked} "
f"authorized_main={authorized} "
f"violations={len(violations)} "
f"placement_mismatches={len(mismatches)} "
f"malformed={malformed}")
print(summary)
if findings:
try:
os.makedirs(os.path.dirname(VIOLATIONS_LOG), exist_ok=True)
with open(VIOLATIONS_LOG, "a", encoding="utf-8") as vf:
for v in findings:
vf.write(json.dumps(v) + "\n")
except OSError as e:
print(f"watchdog ERROR writing violations log: {e}", file=sys.stderr)
return 2
for v in violations:
print(f"VIOLATION main-chat send without opt-in: "
f"id={v['dm_id']} agent={v['agent']} to={v['to']} "
f"at={v['event_ts']} preview={v['msg_preview']!r}")
for m in mismatches:
if m["kind"] == "placement_mismatch":
print(f"P0 PLACEMENT_MISMATCH message landed in wrong chat: "
f"id={m['dm_id']} agent={m['agent']} to={m['to']} "
f"target={m['target']} expected={m['expected_uuid']} "
f"actual={m['actual_uuid']} at={m['event_ts']}")
else:
print(f"P0 PLACEMENT_FAILED placement check failed: "
f"id={m['dm_id']} agent={m['agent']} to={m['to']} "
f"target={m['target']} reason={m['reason']} "
f"expected={m['expected_uuid']} "
f"actual_url={m['actual_url']} at={m['event_ts']}")
return 1
return 0
if __name__ == "__main__":
sys.exit(main())