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
This commit is contained in:
+225
-9
@@ -36,9 +36,23 @@ CTL_LOG = NETVM_ROOT / "box-ctl.jsonl"
|
||||
JOB_LOG = NETVM_ROOT / "job-log.jsonl"
|
||||
DISPATCHER = BIN / "job-dispatch.py"
|
||||
DM_PY = BIN / "dm.py"
|
||||
POLICY_FILE = NETVM_ROOT / "CHAT_POLICY.md"
|
||||
DM_LOG = NETVM_ROOT / "dm-log.jsonl"
|
||||
WATCHDOG_STATE = NETVM_ROOT / "main-chat-watchdog.state"
|
||||
|
||||
NAME_RE = re.compile(r"^[a-z0-9-]{1,64}$")
|
||||
VALID_AGENTS = {"muse", "pip", "646", "opm"}
|
||||
|
||||
# Default sidechat per agent for `box notify`. Mirrors super-cli.py
|
||||
# DEFAULT_AGENT_SIDECHATS (kept in sync manually; super-cli is the
|
||||
# canonical copy). Sidechat-first policy: notify never targets main
|
||||
# chat unless --allow-main-chat is passed explicitly.
|
||||
NOTIFY_SIDECHATS = {
|
||||
"646": "646 tasks",
|
||||
"opm": "heartbeat",
|
||||
"pip": "646-pip-coord",
|
||||
"muse": "muse tasks",
|
||||
}
|
||||
VALID_ON_FAILURE = {"retry", "alert", "ignore"}
|
||||
KNOWN_PLACEHOLDERS = {"job_id", "job_name", "datetime", "date", "last_run"}
|
||||
PROTOCOL_LITERALS = ("[REQ", "[CONFIRM", "[JOB", "[RESULT")
|
||||
@@ -602,19 +616,38 @@ def act_job_trigger(name):
|
||||
out(True, name=name, triggered=True)
|
||||
|
||||
|
||||
def act_notify(agent, message):
|
||||
def _valid_sidechat_name(name):
|
||||
# Sidechat names may contain spaces ("646 tasks"); reject only
|
||||
# control characters and enforce a sane length. dm.py resolves
|
||||
# the name (alias -> job-sidechats.json -> autoprovision).
|
||||
return bool(name) and len(name) <= 64 and not any(
|
||||
ord(c) < 32 or ord(c) == 127 for c in name)
|
||||
|
||||
|
||||
def act_notify(agent, message, sidechat=None, allow_main_chat=False):
|
||||
if agent not in VALID_AGENTS:
|
||||
fail("BAD_NAME", f"agent must be one of {sorted(VALID_AGENTS)}")
|
||||
if not message or len(message) > 1000:
|
||||
fail("INVALID_JOB", "message must be 1–1000 chars")
|
||||
# dm.py send --agent opm --to <agent> --target main "<message>"
|
||||
fail("INVALID_JOB", "message must be 1\u20131000 chars")
|
||||
# Sidechat-first policy: default to the agent's sidechat, never main.
|
||||
if allow_main_chat:
|
||||
target = "main"
|
||||
extra = ["--allow-main-chat"]
|
||||
else:
|
||||
target = sidechat if sidechat else NOTIFY_SIDECHATS.get(agent)
|
||||
if not target:
|
||||
fail("BAD_NAME", f"no default sidechat for agent {agent}; pass --sidechat <name>")
|
||||
if sidechat and not _valid_sidechat_name(sidechat):
|
||||
fail("BAD_NAME", "sidechat name must be 1-64 chars, no control characters")
|
||||
extra = []
|
||||
r = run([sys.executable, str(DM_PY), "send",
|
||||
"--agent", "opm", "--to", agent, "--target", "main", message],
|
||||
"--agent", "opm", "--to", agent, "--target", target,
|
||||
*extra, message],
|
||||
timeout=120)
|
||||
if r.returncode != 0:
|
||||
fail("DISPATCH_FAILED", f"dm.py send failed: {(r.stderr or r.stdout).strip()[-500:]}")
|
||||
audit("notify", agent)
|
||||
out(True, agent=agent, sent=True)
|
||||
out(True, agent=agent, target=target, sent=True)
|
||||
|
||||
|
||||
def act_fleet_status():
|
||||
@@ -646,6 +679,156 @@ def act_dm_log(limit=50):
|
||||
fail("DM_LOG_ERROR", "failed to read dm log", {"stderr": r.stderr})
|
||||
|
||||
|
||||
|
||||
def _policy_meta():
|
||||
"""Parse Version:/Date: from CHAT_POLICY.md."""
|
||||
version, date = "unknown", "unknown"
|
||||
try:
|
||||
for line in POLICY_FILE.read_text(encoding="utf-8", errors="replace").splitlines():
|
||||
s = line.strip()
|
||||
if s.startswith("**Version:**"):
|
||||
version = s.split("**Version:**", 1)[1].strip().rstrip("\\")
|
||||
elif s.startswith("**Date:**"):
|
||||
date = s.split("**Date:**", 1)[1].strip().rstrip("\\")
|
||||
except OSError:
|
||||
pass
|
||||
return version, date
|
||||
|
||||
|
||||
def _watchdog_state():
|
||||
"""Read the main-chat watchdog watermark (None if missing)."""
|
||||
try:
|
||||
return json.loads(WATCHDOG_STATE.read_text(encoding="utf-8"))
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _policy_scan():
|
||||
"""Classify dm-log.jsonl events for the sidechat-first policy.
|
||||
|
||||
Mirrors main-chat-watchdog.py classification:
|
||||
VIOLATION : type=sent, target=main, no tags.allow_main_chat
|
||||
AUTHORIZED: type=sent, target=main, tags.allow_main_chat set
|
||||
BLOCKED : type=main_chat_blocked (gate worked)
|
||||
Events before the allow_main_chat audit marker was adopted cannot be
|
||||
classified, so the compliance window starts at marker adoption.
|
||||
"""
|
||||
per_agent = {}
|
||||
legacy_untagged = 0
|
||||
malformed = 0
|
||||
adoption_ts = None
|
||||
total_scanned = 0
|
||||
|
||||
def bucket(agent):
|
||||
return per_agent.setdefault(agent, {
|
||||
"blocked": 0,
|
||||
"authorized_main": 0,
|
||||
"violations": 0,
|
||||
"total_sends": 0,
|
||||
})
|
||||
|
||||
try:
|
||||
with open(DM_LOG, "r", encoding="utf-8", errors="replace") as f:
|
||||
lines = f.readlines()
|
||||
except OSError:
|
||||
return None, {"error": f"cannot read {DM_LOG}"}
|
||||
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
try:
|
||||
ev = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
tags = ev.get("tags")
|
||||
if isinstance(tags, dict) and "allow_main_chat" in tags:
|
||||
ts = ev.get("ts") or ""
|
||||
if adoption_ts is None or ts < adoption_ts:
|
||||
adoption_ts = ts
|
||||
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
total_scanned += 1
|
||||
try:
|
||||
ev = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
malformed += 1
|
||||
continue
|
||||
ts = ev.get("ts") or ""
|
||||
if adoption_ts is not None and ts < adoption_ts:
|
||||
if ev.get("type") == "sent" and ev.get("target") == "main":
|
||||
legacy_untagged += 1
|
||||
continue
|
||||
etype = ev.get("type")
|
||||
agent = ev.get("agent") or "unknown"
|
||||
if etype == "main_chat_blocked":
|
||||
bucket(agent)["blocked"] += 1
|
||||
elif etype == "sent":
|
||||
bucket(agent)["total_sends"] += 1
|
||||
if ev.get("target") == "main":
|
||||
tags = ev.get("tags") or {}
|
||||
if tags.get("allow_main_chat"):
|
||||
bucket(agent)["authorized_main"] += 1
|
||||
else:
|
||||
bucket(agent)["violations"] += 1
|
||||
|
||||
return per_agent, {
|
||||
"window_start": adoption_ts,
|
||||
"events_scanned": total_scanned,
|
||||
"malformed": malformed,
|
||||
"legacy_untagged_main_sends": legacy_untagged,
|
||||
}
|
||||
|
||||
|
||||
def act_policy():
|
||||
audit("policy")
|
||||
version, date = _policy_meta()
|
||||
per_agent, meta = _policy_scan()
|
||||
if per_agent is None:
|
||||
fail("POLICY_ERROR", "failed to read dm log", meta)
|
||||
totals = {"blocked": 0, "authorized_main": 0, "violations": 0, "total_sends": 0}
|
||||
for counts in per_agent.values():
|
||||
for k in totals:
|
||||
totals[k] += counts[k]
|
||||
out(True,
|
||||
version=version,
|
||||
date=date,
|
||||
rule=("No DM naturally lands in main chat. Main requires explicit "
|
||||
"opt-in (--allow-main-chat / allow_main_chat)."),
|
||||
compliance_window=meta,
|
||||
watchdog_state=_watchdog_state(),
|
||||
agents=per_agent,
|
||||
totals=totals,
|
||||
status="clean" if totals["violations"] == 0 else "violations found")
|
||||
|
||||
|
||||
def act_policy_check(agent):
|
||||
if agent not in VALID_AGENTS:
|
||||
fail("BAD_NAME", f"agent must be one of {sorted(VALID_AGENTS)}")
|
||||
audit("policy-check", agent)
|
||||
per_agent, meta = _policy_scan()
|
||||
if per_agent is None:
|
||||
fail("POLICY_ERROR", "failed to read dm log", meta)
|
||||
counts = per_agent.get(agent, {
|
||||
"blocked": 0, "authorized_main": 0, "violations": 0, "total_sends": 0})
|
||||
out(True, agent=agent,
|
||||
compliance_window=meta, **counts,
|
||||
status="clean" if counts["violations"] == 0 else "violations found")
|
||||
|
||||
|
||||
def act_policy_show():
|
||||
audit("policy-show")
|
||||
try:
|
||||
text = POLICY_FILE.read_text(encoding="utf-8", errors="replace")
|
||||
except OSError as e:
|
||||
fail("POLICY_ERROR", f"cannot read {POLICY_FILE}: {e}")
|
||||
version, date = _policy_meta()
|
||||
out(True, version=version, date=date, path=str(POLICY_FILE), policy=text)
|
||||
|
||||
|
||||
def act_vars_list():
|
||||
try:
|
||||
from variables import Variables
|
||||
@@ -904,7 +1087,14 @@ loop actions:
|
||||
loop-remediate [--dry-run]
|
||||
|
||||
notify:
|
||||
notify <agent> <message>"""
|
||||
notify <agent> <message> [--sidechat <name>] [--allow-main-chat]
|
||||
Send a DM to an agent's default sidechat (never main chat unless
|
||||
--allow-main-chat is passed explicitly).
|
||||
|
||||
policy:
|
||||
policy chat policy version, rule, and compliance summary
|
||||
policy check <agent> per-agent compliance detail
|
||||
policy show print the full CHAT_POLICY.md"""
|
||||
|
||||
|
||||
def main(argv):
|
||||
@@ -1055,9 +1245,35 @@ def main(argv):
|
||||
dry = "--dry-run" in rest
|
||||
act_loop_remediate(dry_run=dry)
|
||||
elif action == "notify":
|
||||
if len(rest) != 2:
|
||||
fail("BAD_NAME", "usage: notify <agent> <message>")
|
||||
act_notify(rest[0], rest[1])
|
||||
# notify <agent> <message> [--sidechat <name>] [--allow-main-chat]
|
||||
args = list(rest)
|
||||
allow_main = False
|
||||
sidechat = None
|
||||
if "--allow-main-chat" in args:
|
||||
allow_main = True
|
||||
args.remove("--allow-main-chat")
|
||||
if "--sidechat" in args:
|
||||
i = args.index("--sidechat")
|
||||
if i + 1 >= len(args):
|
||||
fail("BAD_NAME", "usage: notify <agent> <message> [--sidechat <name>] [--allow-main-chat]")
|
||||
sidechat = args[i + 1]
|
||||
del args[i:i + 2]
|
||||
if len(args) != 2:
|
||||
fail("BAD_NAME", "usage: notify <agent> <message> [--sidechat <name>] [--allow-main-chat]")
|
||||
act_notify(args[0], args[1], sidechat=sidechat, allow_main_chat=allow_main)
|
||||
elif action == "policy":
|
||||
if not rest:
|
||||
act_policy()
|
||||
elif rest[0] == "check":
|
||||
if len(rest) != 2:
|
||||
fail("BAD_NAME", "usage: policy check <agent>")
|
||||
act_policy_check(rest[1])
|
||||
elif rest[0] == "show":
|
||||
if len(rest) != 1:
|
||||
fail("BAD_NAME", "usage: policy show")
|
||||
act_policy_show()
|
||||
else:
|
||||
fail("BAD_NAME", "usage: policy [check <agent>|show]")
|
||||
elif action == "fleet-status":
|
||||
act_fleet_status()
|
||||
elif action == "dm-log":
|
||||
|
||||
Reference in New Issue
Block a user