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
+27 -10
View File
@@ -58,6 +58,7 @@ NOTIFY_SIDECHATS = {
"pip": "pip tasks",
"muse": "muse tasks",
"dev": "onboarding-dev",
"def": "def tasks",
}
VALID_ON_FAILURE = {"retry", "alert", "ignore"}
KNOWN_PLACEHOLDERS = {"job_id", "job_name", "datetime", "date", "last_run"}
@@ -989,12 +990,13 @@ def act_approval_check(node=None):
out(True, approvals=res)
def act_approval_allow(node, always=False, force=False):
def act_approval_allow(node, always=False, force=False, message=None, allow_main_chat=False):
audit("approval-allow", node)
if node not in VALID_AGENTS:
fail("BAD_NODE", f"unknown node: {node}")
import approvals
res = approvals.allow_node_approval(node, always=always, force=force, caller="box-ctl")
res = approvals.allow_node_approval(node, always=always, force=force, caller="box-ctl",
message=message, allow_main_chat=allow_main_chat)
if res.get("ok"):
kw = {k: v for k, v in res.items() if k != "ok"}
out(True, **kw)
@@ -1002,12 +1004,13 @@ def act_approval_allow(node, always=False, force=False):
fail("APPROVAL_FAILED", res.get("error", "approval failed"), res)
def act_approval_deny(node):
def act_approval_deny(node, message=None, allow_main_chat=False):
audit("approval-deny", node)
if node not in VALID_AGENTS:
fail("BAD_NODE", f"unknown node: {node}")
import approvals
res = approvals.deny_node_approval(node, caller="box-ctl")
res = approvals.deny_node_approval(node, caller="box-ctl",
message=message, allow_main_chat=allow_main_chat)
if res.get("ok"):
kw = {k: v for k, v in res.items() if k != "ok"}
out(True, **kw)
@@ -3563,16 +3566,30 @@ def main(argv):
act_approval_check(node)
elif action in ("approval-allow", "approval-approve"):
if not rest:
fail("BAD_ARGS", "usage: approval-allow <node> [--always] [--force]")
fail("BAD_ARGS", "usage: approval-allow <node> [--always] [--force] [--message TEXT] [--allow-main-chat]")
node = rest[0]
always = "--always" in rest[1:]
force = "--force" in rest[1:]
act_approval_allow(node, always=always, force=force)
rest_args = rest[1:]
always = "--always" in rest_args
force = "--force" in rest_args
allow_main_chat = "--allow-main-chat" in rest_args
message = None
if "--message" in rest_args:
mi = rest_args.index("--message")
if mi + 1 < len(rest_args):
message = rest_args[mi + 1]
act_approval_allow(node, always=always, force=force, message=message, allow_main_chat=allow_main_chat)
elif action == "approval-deny":
if not rest:
fail("BAD_ARGS", "usage: approval-deny <node>")
fail("BAD_ARGS", "usage: approval-deny <node> [--message TEXT] [--allow-main-chat]")
node = rest[0]
act_approval_deny(node)
rest_args = rest[1:]
allow_main_chat = "--allow-main-chat" in rest_args
message = None
if "--message" in rest_args:
mi = rest_args.index("--message")
if mi + 1 < len(rest_args):
message = rest_args[mi + 1]
act_approval_deny(node, message=message, allow_main_chat=allow_main_chat)
elif action == "approval-auto":
node = rest[0] if rest else None
act_approval_auto(node)