feat(fleet): expand VALID_AGENTS to include dev and def

This commit is contained in:
operator
2026-10-05 01:38:26 +00:00
parent 61cf9d027b
commit aba4bce682
2 changed files with 183 additions and 5 deletions
+175 -2
View File
@@ -41,7 +41,9 @@ 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"}
VALID_AGENTS = {"muse", "pip", "646", "opm", "dev", "def"}
THREAD_RE = re.compile(r"^[A-Za-z0-9-]{1,64}$")
THREAD_OPS = ("list", "pin", "unpin", "archive", "unarchive")
# Default sidechat per agent for `box notify`. Mirrors super-cli.py
# DEFAULT_AGENT_SIDECHATS (kept in sync manually; super-cli is the
@@ -1216,6 +1218,14 @@ loop actions:
loop-resolve <dm_id> [note]
loop-remediate [--dry-run]
thread actions (muse-cli gateway; hyphenated aliases for `thread <sub>`):
thread-list --agent <agent> [--limit N]
thread-pin --agent <agent> --thread <id>
thread-unpin --agent <agent> --thread <id>
thread-archive --agent <agent> --thread <id> --confirm
thread-unarchive --agent <agent> --thread <id>
thread-rename --agent <agent> --thread <id> --title <title>
notify:
notify <agent> <message> [--sidechat <name>] [--allow-main-chat]
Send a DM to an agent's default sidechat (never main chat unless
@@ -1224,7 +1234,74 @@ notify:
policy:
policy chat policy version, rule, and compliance summary
policy check <agent> per-agent compliance detail
policy show print the full CHAT_POLICY.md"""
policy show print the full CHAT_POLICY.md
thread actions (muse-cli gateway):
thread list <agent> [--limit N]
thread pin <agent> <thread-id>
thread unpin <agent> <thread-id>
thread archive <agent> <thread-id> --confirm
thread unarchive <agent> <thread-id>"""
def act_thread(op, agent, thread=None, title=None, limit=None, confirm=False):
"""Thread bookkeeping via bin/muse-threads.py (hybrid gateway).
ops: list | pin | unpin | archive | unarchive | rename
archive requires confirm=True (the --confirm flag; BatchMode SSH
cannot prompt, so the flag is the explicit confirmation).
"""
if op not in ("list", "pin", "unpin", "archive", "unarchive", "rename"):
fail("BAD_NAME", "usage: thread list|pin|unpin|archive|unarchive|rename <agent> [args]")
if agent not in VALID_AGENTS:
fail("BAD_NAME", f"agent must be one of {sorted(VALID_AGENTS)}")
if op == "list":
audit("thread-list-" + agent)
else:
if not thread or not THREAD_RE.match(thread):
fail("BAD_NAME", "thread id must match ^[A-Za-z0-9-]{1,64}$",
{"field": "thread", "value": thread})
if op == "archive" and not confirm:
fail("CONFIRM_REQUIRED",
"archive is a visible side effect -- pass --confirm to proceed",
{"would_archive": {"agent": agent, "thread": thread}})
if op == "rename" and not title:
fail("BAD_NAME", "rename requires a title")
audit("thread-" + op, f"{agent}/{thread}")
helper = os.path.join(os.path.dirname(os.path.abspath(__file__)), "muse-threads.py")
cmd = [sys.executable, helper, op, "--agent", agent]
if thread:
cmd += ["--thread", thread]
if title:
cmd += ["--title", title]
try:
p = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
except subprocess.TimeoutExpired:
fail("GATEWAY_UNAVAILABLE", f"muse-threads.py timed out (op={op})")
except OSError as e:
fail("GATEWAY_UNAVAILABLE", f"cannot run muse-threads.py: {e}")
# Helper prints one JSON object; relay it, applying --limit for list
if p.returncode == 0:
raw = p.stdout.strip()
if limit is not None and op == "list" and raw:
try:
data = json.loads(raw)
if isinstance(data.get("threads"), list):
data["threads"] = data["threads"][:limit]
print(json.dumps(data))
return
except ValueError:
pass
print(raw if raw else '{"ok": true}')
return
# Failure: try to extract the helper's JSON error, else wrap
try:
err = json.loads(p.stdout.strip() or "{}")
fail(err.get("code", "GATEWAY_ERROR"), err.get("error", "thread op failed"),
err.get("detail"))
except (ValueError, AttributeError):
fail("GATEWAY_ERROR", f"thread {op} failed",
(p.stderr.strip() or p.stdout.strip())[:300])
def main(argv):
@@ -1374,6 +1451,30 @@ def main(argv):
elif action == "loop-remediate":
dry = "--dry-run" in rest
act_loop_remediate(dry_run=dry)
elif action in ("thread-list", "thread-pin", "thread-unpin",
"thread-archive", "thread-unarchive", "thread-rename"):
op = action[len("thread-"):]
agent = None
thread = None
title = None
idx = 0
while idx < len(rest):
if rest[idx] == "--agent" and idx + 1 < len(rest):
agent = rest[idx + 1]
idx += 2
elif rest[idx] == "--thread" and idx + 1 < len(rest):
thread = rest[idx + 1]
idx += 2
elif rest[idx] == "--title" and idx + 1 < len(rest):
title = rest[idx + 1]
idx += 2
else:
idx += 1
if not agent:
fail("BAD_NAME", f"usage: {action} --agent <agent> [--thread <id>] [--title <title>]")
if op in ("pin", "unpin", "archive", "unarchive", "rename") and not thread:
fail("BAD_NAME", f"usage: {action} --agent <agent> --thread <id> [--title <title>]")
act_thread(op, agent, thread, title)
elif action == "notify":
# notify <agent> <message> [--sidechat <name>] [--allow-main-chat]
args = list(rest)
@@ -1437,6 +1538,78 @@ def main(argv):
if args:
fail("BAD_ARGS", "usage: main-loop %s [--agent <name>]" % sub)
act_main_loop(sub, agent)
elif action == "thread":
if not rest or rest[0] not in THREAD_OPS:
fail("BAD_NAME", "usage: thread list|pin|unpin|archive|unarchive <agent> [args]")
sub = rest[0]
args = rest[1:]
if not args:
fail("BAD_NAME", f"usage: thread {sub} <agent> [...]")
agent = args[0]
args = args[1:]
thread_id = None
limit = None
confirm = "--confirm" in args
args = [a for a in args if a != "--confirm"]
if confirm and sub != "archive":
fail("BAD_ARGS", "--confirm only applies to thread archive")
idx = 0
while idx < len(args):
if args[idx] == "--limit" and idx + 1 < len(args):
try:
limit = int(args[idx + 1])
except ValueError:
fail("BAD_LIMIT", "usage: thread list <agent> [--limit N]")
idx += 2
elif thread_id is None and not args[idx].startswith("--"):
thread_id = args[idx]
idx += 1
else:
fail("BAD_ARGS", f"usage: thread {sub} <agent> [...]")
if limit is not None and sub != "list":
fail("BAD_ARGS", "--limit only applies to thread list")
if sub in ("pin", "unpin", "archive", "unarchive") and not thread_id:
fail("BAD_NAME", f"usage: thread {sub} <agent> <thread-id> [--confirm]")
if sub == "list" and thread_id:
fail("BAD_ARGS", "usage: thread list <agent> [--limit N]")
act_thread(sub, agent, thread=thread_id, limit=limit, confirm=confirm)
elif action in ("thread-list", "thread-pin", "thread-unpin",
"thread-archive", "thread-unarchive", "thread-rename"):
op = action[len("thread-"):]
agent = None
thread_id = None
title = None
limit = None
confirm = "--confirm" in rest
args = [a for a in rest if a != "--confirm"]
if confirm and op != "archive":
fail("BAD_ARGS", "--confirm only applies to thread-archive")
idx = 0
while idx < len(args):
if args[idx] == "--agent" and idx + 1 < len(args):
agent = args[idx + 1]
idx += 2
elif args[idx] == "--thread" and idx + 1 < len(args):
thread_id = args[idx + 1]
idx += 2
elif args[idx] == "--title" and idx + 1 < len(args):
title = args[idx + 1]
idx += 2
elif args[idx] == "--limit" and idx + 1 < len(args):
try:
limit = int(args[idx + 1])
except ValueError:
fail("BAD_LIMIT", "usage: thread-list --agent <agent> [--limit N]")
idx += 2
else:
fail("BAD_ARGS", f"usage: {action} --agent <agent> [--thread <id>] [--title <t>] [--confirm] [--limit N]")
if not agent:
fail("BAD_NAME", f"usage: {action} --agent <agent> [...]")
if limit is not None and op != "list":
fail("BAD_ARGS", "--limit only applies to thread-list")
if op in ("pin", "unpin", "archive", "unarchive", "rename") and not thread_id:
fail("BAD_NAME", f"usage: {action} --agent <agent> --thread <id> [...]")
act_thread(op, agent, thread=thread_id, title=title, limit=limit, confirm=confirm)
elif action == "dm-log":
limit = 50
if rest: