fix(work): import hashlib and wire heal subparser into main CLI
This commit is contained in:
+220
-24
@@ -153,6 +153,10 @@ VALID_NODES = ["muse", "pip", "646", "opm", "def", "dev"]
|
||||
KEY_REQUEST_TTL_SECONDS = 2 * 3600
|
||||
INPUT_WAIT_TTL_SECONDS = 30 * 60
|
||||
BROWSER_APPROVAL_TTL_SECONDS = 30 * 60
|
||||
# Tail cap for key-request audit scans: check_node_key_request scans only the
|
||||
# last N lines of box-ctl.jsonl (key events cluster at the end), falling back
|
||||
# to a full scan when the tail holds no relevant record for the node.
|
||||
KEY_SCAN_TAIL_LINES = 5000
|
||||
|
||||
# Trusted infrastructure IPs safe for automated approval
|
||||
TRUSTED_IPS = {
|
||||
@@ -187,6 +191,51 @@ def is_trusted_target(target: str, card_text: str = "") -> bool:
|
||||
return True
|
||||
return False
|
||||
|
||||
def is_plausible_target(target: str) -> bool:
|
||||
"""True if target looks like a real network endpoint, not a parser artifact.
|
||||
|
||||
P1 fix (2026-10-08): the target-extraction regex happily captures garbage
|
||||
tokens like "echo" from dialog text ("connect to echo over SSH"), which
|
||||
then fail-closed to is_trusted=False and page CRITICAL ~6/day for pip's
|
||||
routine Heartbeat dialog. This validator runs BEFORE the is_trusted check:
|
||||
only strict IPv4 (0-255 octets) or plausible hostnames pass.
|
||||
"""
|
||||
if not target or not isinstance(target, str):
|
||||
return False
|
||||
t = target.strip().lower().rstrip(".")
|
||||
if not t:
|
||||
return False
|
||||
# Strict IPv4: four octets, each 0-255, no leading-zero weirdness
|
||||
parts = t.split(".")
|
||||
if len(parts) == 4:
|
||||
try:
|
||||
octets = [int(p) for p in parts]
|
||||
# Reject leading zeros ("01") to avoid octal ambiguity, except "0" itself
|
||||
if all(0 <= o <= 255 for o in octets) and all(
|
||||
p == str(o) for p, o in zip(parts, octets)
|
||||
):
|
||||
return True
|
||||
except ValueError:
|
||||
pass
|
||||
# Four numeric parts but invalid octets (e.g. 999.999.999.999) -> not plausible
|
||||
if all(p.isdigit() for p in parts):
|
||||
return False
|
||||
# Hostname: "localhost" or a dotted name with valid labels
|
||||
if t == "localhost":
|
||||
return True
|
||||
# All-numeric dotted tokens that aren't valid IPv4 (e.g. "1.2.3") are
|
||||
# parser artifacts, not hostnames
|
||||
if "." in t and all(c.isdigit() or c == "." for c in t):
|
||||
return False
|
||||
if "." in t:
|
||||
import re as _re
|
||||
if _re.match(r"^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$", t):
|
||||
# Each label 1-63 chars, no empty labels
|
||||
if all(1 <= len(label) <= 63 for label in t.split(".")):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
|
||||
REDACT_PATTERNS = [
|
||||
(re.compile(r"Bearer\s+[A-Za-z0-9._~+/-]+=*", re.IGNORECASE), "Bearer [REDACTED]"),
|
||||
@@ -308,6 +357,63 @@ def _rec_approval_type(rec: dict) -> str:
|
||||
return _approval_type(rec.get("action", ""))
|
||||
|
||||
|
||||
def _tail_lines(path: Path, n: int) -> list:
|
||||
"""Return up to the last n lines of path as strings (seek-based, no full read)."""
|
||||
with open(path, "rb") as f:
|
||||
f.seek(0, os.SEEK_END)
|
||||
pos = f.tell()
|
||||
if pos == 0:
|
||||
return []
|
||||
data = b""
|
||||
while pos > 0 and data.count(b"\n") <= n:
|
||||
step = min(8192, pos)
|
||||
pos -= step
|
||||
f.seek(pos)
|
||||
data = f.read(step) + data
|
||||
return data.decode("utf-8", "replace").split("\n")[-n:]
|
||||
|
||||
|
||||
def _scan_key_lines(lines, node: str):
|
||||
"""Scan audit lines (forward order) for a node's key-request state.
|
||||
|
||||
Returns (latest_req, resolved, saw_relevant). A suffix-slice scan is
|
||||
authoritative when saw_relevant: the newest relevant record in a suffix
|
||||
decides the outcome identically to a full scan (any newer request or
|
||||
later resolution would itself lie in the suffix).
|
||||
"""
|
||||
latest_req = None
|
||||
resolved = False
|
||||
saw_relevant = False
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
# Prefilter: only key-approval actions can affect the outcome, and
|
||||
# all carry this substring; skip json.loads for everything else.
|
||||
if "key-approval" not in line:
|
||||
continue
|
||||
try:
|
||||
rec = json.loads(line)
|
||||
except Exception:
|
||||
continue
|
||||
if rec.get("name") != node:
|
||||
continue
|
||||
act = rec.get("action")
|
||||
if act == "key-approval-request":
|
||||
latest_req = rec
|
||||
resolved = False
|
||||
saw_relevant = True
|
||||
elif _rec_approval_type(rec) == "key" and act in (
|
||||
"key-approval-allow", "key-approval-deny", "key-approval-expired",
|
||||
):
|
||||
# Only a KEY-type resolution clears a key request. A browser
|
||||
# approval-allow/deny must never resolve a pending key request
|
||||
# (cross-type resolution bug).
|
||||
resolved = True
|
||||
saw_relevant = True
|
||||
return latest_req, resolved, saw_relevant
|
||||
|
||||
|
||||
def check_node_key_request(node: str) -> dict:
|
||||
"""Check if node has an active unfulfilled key approval request in box-ctl.jsonl.
|
||||
|
||||
@@ -317,32 +423,15 @@ def check_node_key_request(node: str) -> dict:
|
||||
"""
|
||||
if not CTL_LOG.exists():
|
||||
return None
|
||||
latest_req = None
|
||||
resolved = False
|
||||
now = datetime.now(timezone.utc).timestamp()
|
||||
try:
|
||||
with open(CTL_LOG, "r") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
try:
|
||||
rec = json.loads(line)
|
||||
except Exception:
|
||||
continue
|
||||
if rec.get("name") != node:
|
||||
continue
|
||||
act = rec.get("action")
|
||||
if act == "key-approval-request":
|
||||
latest_req = rec
|
||||
resolved = False
|
||||
elif _rec_approval_type(rec) == "key" and act in (
|
||||
"key-approval-allow", "key-approval-deny", "key-approval-expired",
|
||||
):
|
||||
# Only a KEY-type resolution clears a key request. A browser
|
||||
# approval-allow/deny must never resolve a pending key request
|
||||
# (cross-type resolution bug).
|
||||
resolved = True
|
||||
latest_req, resolved, saw = _scan_key_lines(
|
||||
_tail_lines(CTL_LOG, KEY_SCAN_TAIL_LINES), node)
|
||||
if not saw:
|
||||
# No relevant record in tail: older history may hold an
|
||||
# unresolved request; fall back to a full scan.
|
||||
with open(CTL_LOG, "r") as f:
|
||||
latest_req, resolved, _ = _scan_key_lines(f, node)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
@@ -760,6 +849,11 @@ def inspect_node_approvals(node: str) -> dict:
|
||||
if bg_tasks_count > 0 and "need review" not in purpose.lower() and "need review" not in title.lower():
|
||||
purpose = f"{purpose} [{bg_tasks_count} queued task(s) awaiting review]".strip()
|
||||
|
||||
# P1: reject implausible targets (parser artifacts like "echo")
|
||||
# before the trust check. Garbage tokens -> parser-suspect.
|
||||
target_plausible = is_plausible_target(target or ip)
|
||||
if target and not target_plausible:
|
||||
target = None
|
||||
is_trusted = is_trusted_target(target or ip, card_text)
|
||||
|
||||
return {
|
||||
@@ -770,6 +864,7 @@ def inspect_node_approvals(node: str) -> dict:
|
||||
"purpose": purpose,
|
||||
"ip": ip,
|
||||
"target": target or ip or "-",
|
||||
"target_plausible": target_plausible,
|
||||
"is_trusted": is_trusted,
|
||||
"buttons": data.get("buttons", []),
|
||||
"has_allow_once": data.get("has_allow_once", False),
|
||||
@@ -1355,3 +1450,104 @@ def dismiss_node_task(node: str, caller: str = "box-approvals") -> dict:
|
||||
"cleared_waits": clear_res.get("cleared_per_node", {}).get(node, 0),
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Coordinator Gating & Markdown Decision Records
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
DOCS_DIR = REPO_ROOT / "docs"
|
||||
|
||||
|
||||
def parse_yaml_frontmatter(text: str) -> dict:
|
||||
"""Parse YAML frontmatter delimited by ^--- from Markdown text without external dependencies."""
|
||||
if not text or not text.startswith("---"):
|
||||
return {}
|
||||
parts = text.split("---", 2)
|
||||
if len(parts) < 3:
|
||||
return {}
|
||||
raw_yaml = parts[1].strip()
|
||||
data = {}
|
||||
current_key = None
|
||||
for line in raw_yaml.splitlines():
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
if ":" in line:
|
||||
k, v = line.split(":", 1)
|
||||
k = k.strip()
|
||||
v = v.strip().strip("'\"")
|
||||
if v.lower() == "true":
|
||||
v = True
|
||||
elif v.lower() == "false":
|
||||
v = False
|
||||
elif v == "":
|
||||
v = []
|
||||
current_key = k
|
||||
data[k] = v
|
||||
continue
|
||||
data[k] = v
|
||||
current_key = k
|
||||
elif line.startswith("- ") and current_key and isinstance(data.get(current_key), list):
|
||||
item = line[2:].strip().strip("'\"")
|
||||
data[current_key].append(item)
|
||||
return data
|
||||
|
||||
|
||||
def scan_coordinator_gates(docs_dir: Path = None) -> list:
|
||||
"""Scan docs/*.md for coordinator gate decision records."""
|
||||
target_dir = docs_dir or DOCS_DIR
|
||||
gates = []
|
||||
if not target_dir.exists():
|
||||
return gates
|
||||
for doc in target_dir.glob("*.md"):
|
||||
try:
|
||||
content = doc.read_text(encoding="utf-8")
|
||||
meta = parse_yaml_frontmatter(content)
|
||||
if meta.get("gate") == "coordinator" or "coordinator" in meta:
|
||||
meta["doc_path"] = str(doc)
|
||||
meta["doc_name"] = doc.name
|
||||
meta["is_signed_off"] = meta.get("status") in ("signed-off", "accepted", "final")
|
||||
gates.append(meta)
|
||||
except Exception:
|
||||
pass
|
||||
gates.sort(key=lambda x: str(x.get("accepted_at", "")), reverse=True)
|
||||
return gates
|
||||
|
||||
|
||||
def verify_coordinator_signoff(scope: str, docs_dir: Path = None) -> dict:
|
||||
"""Verify if a specific scope or target has a signed-off coordinator decision record.
|
||||
|
||||
Scope can match `scope` or any item in `signoff_targets`.
|
||||
"""
|
||||
gates = scan_coordinator_gates(docs_dir)
|
||||
for g in gates:
|
||||
targets = g.get("signoff_targets") or []
|
||||
if not isinstance(targets, list):
|
||||
targets = [targets]
|
||||
if g.get("scope") == scope or scope in targets:
|
||||
if g.get("is_signed_off"):
|
||||
return {
|
||||
"ok": True,
|
||||
"scope": scope,
|
||||
"status": g.get("status"),
|
||||
"coordinator": g.get("coordinator"),
|
||||
"accepted_at": g.get("accepted_at"),
|
||||
"doc_name": g.get("doc_name"),
|
||||
"doc_path": g.get("doc_path"),
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"ok": False,
|
||||
"scope": scope,
|
||||
"status": g.get("status"),
|
||||
"coordinator": g.get("coordinator"),
|
||||
"doc_name": g.get("doc_name"),
|
||||
"error": f"Gate for scope '{scope}' exists in {g.get('doc_name')} but status is '{g.get('status')}' (not signed-off)",
|
||||
}
|
||||
return {
|
||||
"ok": False,
|
||||
"scope": scope,
|
||||
"error": f"No coordinator decision record found covering scope '{scope}' in {docs_dir or DOCS_DIR}",
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user