approvals: stale-wait cleanup, key-decision notify, TTLs, key/browser isolation

- bin/approvals.py: responded-wait filtering + auto-mark, key-decision sidechat-first notify (notified flag, --message, --allow-main-chat), TTL defaults (input 30m / browser 30m / key 2h), cross-type guard (browser actions cannot resolve key requests), sweep_expired_key_requests; restores check_node_key_request fallback in inspect_node_approvals

- bin/box-ctl.py + bin/super-cli.py (approvals hunks only): --message/--allow-main-chat passthrough on allow/deny, clear/clear-all actions, def sidechat routing; restores sys.exit(1) on dismiss failure

- bin/fleet-alert-check.sh: TTL-aware state_machine (EXPIRED action), auto-deny expired browser approvals (fail closed), auto-dismiss expired input waits, targeted per-agent DM for input waits

- bin/job-dispatch.py + bin/gravity.py: KEY_APPROVAL excluded from auto-approval

Reviewed by 5 independent reviewers (all APPROVE/APPROVE WITH NOTES); integration gate GO (17/17 tests). TUI hunks in super-cli.py intentionally excluded.
This commit is contained in:
operator
2026-10-06 00:49:46 +00:00
parent adfcd2e602
commit 740648e973
6 changed files with 515 additions and 48 deletions
+33 -4
View File
@@ -519,13 +519,17 @@ def cmd_approvals(args):
sys.exit(1)
always = getattr(args, "always", False)
force = getattr(args, "force", False)
res = approvals.allow_node_approval(node, always=always, force=force)
message = getattr(args, "message", None)
allow_main_chat = getattr(args, "allow_main_chat", False)
res = approvals.allow_node_approval(node, always=always, force=force, message=message,
allow_main_chat=allow_main_chat)
if getattr(args, "json", False):
print(json.dumps(res, indent=2))
return
if res.get("ok"):
if res.get("type") == "key_approval":
print(c_green(f"✔ Approved operator key request for node '{node}'. Granted and logged to audit trail."))
notified = "agent notified" if res.get("notified") else "⚠ agent NOT notified (follow up manually)"
print(c_green(f"✔ Approved operator key request for node '{node}'. Logged to audit trail. {notified}."))
else:
decision_str = "Always allow this site" if always else "Allow once"
print(c_green(f"✔ Approved request on node '{node}' ({decision_str}). Dialog dismissed: {res.get('dismissed')}."))
@@ -537,13 +541,16 @@ def cmd_approvals(args):
if not node:
print(c_red("Error: Must specify node for deny. e.g. 'box approvals deny 646'"), file=sys.stderr)
sys.exit(1)
res = approvals.deny_node_approval(node)
message = getattr(args, "message", None)
allow_main_chat = getattr(args, "allow_main_chat", False)
res = approvals.deny_node_approval(node, message=message, allow_main_chat=allow_main_chat)
if getattr(args, "json", False):
print(json.dumps(res, indent=2))
return
if res.get("ok"):
if res.get("type") == "key_approval":
print(c_green(f"✔ Denied operator key request for node '{node}'. Denied and logged to audit trail."))
notified = "agent notified" if res.get("notified") else "⚠ agent NOT notified (follow up manually)"
print(c_green(f"✔ Denied operator key request for node '{node}'. Logged to audit trail. {notified}."))
else:
print(c_green(f"✔ Denied request on node '{node}'. Dialog dismissed: {res.get('dismissed')}."))
else:
@@ -622,6 +629,17 @@ def cmd_approvals(args):
else:
print(c_red(f"✖ Failed to dismiss task popup on node '{node}': {res.get('error')}"))
sys.exit(1)
elif action in ("clear", "clear-all", "clear_all"):
target_node = node
if action in ("clear-all", "clear_all"):
target_node = None
res = approvals.clear_node_waits(target_node)
if getattr(args, "json", False):
print(json.dumps(res, indent=2))
return
cleared = res.get("total_cleared", 0)
target_desc = f"node '{target_node}'" if target_node else "all fleet nodes"
print(c_green(f"✔ Cleared {cleared} input wait(s) on {target_desc}."))
elif action in ("request-key", "request_key"):
if not node:
@@ -4211,14 +4229,20 @@ def build_parser():
p_app_allow.add_argument("node", choices=VALID_NODES, help="Target node to approve")
p_app_allow.add_argument("--always", action="store_true", help="Click 'Always allow this site' instead of 'Allow once'")
p_app_allow.add_argument("--force", action="store_true", help="Force approval even if target is untrusted")
p_app_allow.add_argument("--message", default=None, help="Optional message/credential to deliver to the waiting agent")
p_app_allow.add_argument("--allow-main-chat", action="store_true", help="Allow reply into Main Chat if agent is waiting there")
p_app_approve = app_sub.add_parser("approve", parents=[common], help="Alias for 'allow'")
p_app_approve.add_argument("node", choices=VALID_NODES, help="Target node to approve")
p_app_approve.add_argument("--always", action="store_true", help="Click 'Always allow this site' instead of 'Allow once'")
p_app_approve.add_argument("--force", action="store_true", help="Force approval even if target is untrusted")
p_app_approve.add_argument("--message", default=None, help="Optional message/credential to deliver to the waiting agent")
p_app_approve.add_argument("--allow-main-chat", action="store_true", help="Allow reply into Main Chat if agent is waiting there")
p_app_deny = app_sub.add_parser("deny", parents=[common], help="Deny pending browser request")
p_app_deny.add_argument("node", choices=VALID_NODES, help="Target node to deny")
p_app_deny.add_argument("--message", default=None, help="Optional message to deliver to the waiting agent")
p_app_deny.add_argument("--allow-main-chat", action="store_true", help="Allow reply into Main Chat if agent is waiting there")
p_app_auto = app_sub.add_parser("auto", parents=[common], help="Auto-approve all trusted requests across fleet")
p_app_auto.add_argument("--node", choices=VALID_NODES, default=None, help="Target node (or all nodes)")
@@ -4236,6 +4260,11 @@ def build_parser():
p_app_dismiss = app_sub.add_parser("dismiss", parents=[common], help="Dismiss any open task dialog/popup on a node")
p_app_dismiss.add_argument("node", choices=VALID_NODES, help="Target node to dismiss dialog on")
p_app_clear = app_sub.add_parser("clear", parents=[common], help="Clear and dismiss pending input waits on a node or all nodes")
p_app_clear.add_argument("node", nargs="?", choices=VALID_NODES, default=None, help="Target node (or omit for all nodes)")
p_app_clear_all = app_sub.add_parser("clear-all", parents=[common], help="Clear and dismiss all pending input waits across fleet")
p_app_req_key = app_sub.add_parser("request-key", parents=[common], help="Request operator passkey/key approval for an agent")
p_app_req_key.add_argument("node", choices=VALID_NODES, help="Target node requesting key")
p_app_req_key.add_argument("--reason", default="Passkey authentication required", help="Reason for key request")