Files
box/bin/swarm_worker/executor.py
T
operator 59c9965791 feat(swarm-worker): dispatch prose swarm slots to ephemeral Muse subagents
- daemon: route non-shell slot tasks to a fresh subagent session on dev/def/muse
  via muse_hybrid; add bin/ to sys.path so muse_hybrid/prompt_envelope import
  under the tmux supervisor
- prompt_envelope: add wrap_subagent_task() - plain task assignment without
  [TOOL tmux/swarm/cron] meta tags (subagents refused those as relayed test traffic)
- box-ctl/reporter: swarm-attach accepts optional session-id, stored as
  slot.subagent_session_id
- executor: _looks_like_shell requires an existing executable (prose like
  'verify ...' no longer misread as shell)
- poller: pick up pending swarms as well as running
- response-harvester: monitor running slot subagent sessions from swarms.json,
  allow '/' in RESULT/VERB job ids, archive ephemeral threads on any verdict
  (OK or FAIL), reap slot sessions for terminal swarms
2026-10-05 20:18:46 +00:00

203 lines
6.0 KiB
Python

"""Swarm worker task executor (Bridge Builder 2/5).
Executes a swarm slot's task text in a sandbox and captures the result.
Sandbox rules (hard):
- No network calls except to localhost.
- No writes outside /tmp.
- No sudo / privilege escalation.
- No credential access (no reading ~/.ssh, ~/.aws, key stores, etc).
- Strict timeout; kill the process group on exceed.
- Refuse tasks that look destructive without executing anything.
"""
import os
import re
import shlex
import signal
import subprocess
import time
OUTPUT_TRUNCATE = 2000
# Patterns that indicate a potentially destructive command. Matched against
# the raw task text (case-insensitive) before any execution is attempted.
_REFUSE_PATTERNS = [
r"\brm\s+-rf?\b", # rm -r / rm -rf
r"\brm\s+.*\s+/\s*$", # rm ... / (trailing root)
r"\bmkfs\b",
r"\bdd\b.*\bof=/dev/",
r"\bdd\b.*\bof=\s*/dev/",
r":\(\)\s*\{", # fork bomb
r"\bshutdown\b",
r"\breboot\b",
r"\bpoweroff\b",
r"\bhalt\b",
r"\binit\s+[06]\b",
r"\bsudo\b",
r"\bsu\b",
r"\bchmod\s+-R\s+777\s+/\b",
r"\bchown\s+-R\b.*\s+/\s*$",
r"\bmv\s+.*\s+/\s*$",
r"\bwipefs\b",
r"\bshred\b.*\s/dev/",
r">\s*/dev/sd",
r"\bcurl\b.*\|\s*(ba)?sh", # curl|sh pipe-to-shell
r"\bwget\b.*\|\s*(ba)?sh",
r"\bnc\b.*-e\s", # netcat reverse shell
r"\bpython\w*\s+-c\b.*socket",
]
_REFUSE_RE = re.compile("|".join("(?:%s)" % p for p in _REFUSE_PATTERNS),
re.IGNORECASE)
# Paths that must never be read (credential / identity material).
_FORBIDDEN_READ_PREFIXES = (
os.path.expanduser("~/.ssh"),
os.path.expanduser("~/.aws"),
os.path.expanduser("~/.gnupg"),
"/etc/shadow",
"/etc/netvm",
"/etc/ssl/private",
)
# A task is treated as a shell command when it is short, single-purpose,
# and does not look like prose/instructions. Heuristic: one or two lines,
# starts with a plausible command token, no sentence-like structure.
_PROSE_RE = re.compile(r"[.?!]\s+[A-Z]|\n\n|please\s|you\s+are\s|your\s+task",
re.IGNORECASE)
def _looks_like_shell(task_text):
"""Heuristic: is this plausibly a shell command?"""
t = task_text.strip()
if not t:
return False
if len(t.splitlines()) > 3:
return False
if _PROSE_RE.search(t):
return False
# Must start with a plausible executable command or path
first = t.split()[0] if t.split() else ""
if not re.match(r"^[a-zA-Z0-9_.\-/]+$", first):
return False
# If it's a relative/absolute path, verify it exists and is executable
if "/" in first:
return os.path.isfile(first) and os.access(first, os.X_OK)
# If it's a bare command name, it must exist in standard system bin paths
for p in ("/bin", "/usr/bin", "/usr/local/bin"):
candidate = os.path.join(p, first)
if os.path.isfile(candidate) and os.access(candidate, os.X_OK):
return True
return False
def _refused(task_text):
return bool(_REFUSE_RE.search(task_text))
def _sandbox_env():
"""Minimal environment: strip anything credential-shaped."""
env = {
"PATH": "/usr/bin:/bin",
"HOME": "/tmp",
"TMPDIR": "/tmp",
"LANG": "C.UTF-8",
}
return env
def execute_task(task_text, timeout=600):
"""Execute a swarm slot's task text in a sandbox.
Args:
task_text: the task string from the swarm slot.
timeout: max seconds for execution (default 600). Strictly enforced.
Returns:
dict(success=bool, output=str, duration_s=float, error=str|None)
"""
started = time.monotonic()
text = (task_text or "").strip()
def done(success, output, error=None):
dur = round(time.monotonic() - started, 3)
out = (output or "")[:OUTPUT_TRUNCATE]
return {
"success": success,
"output": out,
"duration_s": dur,
"error": error,
}
if not text:
return done(False, "", error="empty task")
if _refused(text):
return done(False, "", error="refused: potentially destructive")
if not _looks_like_shell(text):
return done(
True,
"received but not executable: task is prose/instructions, "
"not a shell command; recorded %d chars" % len(text),
error=None,
)
# Sandbox: run in /tmp, fresh process group so timeout kills children too.
try:
proc = subprocess.Popen(
text,
shell=True,
executable="/bin/bash",
cwd="/tmp",
env=_sandbox_env(),
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
start_new_session=True, # new process group for reliable kill
)
except Exception as e: # e.g. /bin/bash missing
return done(False, "", error="spawn failed: %s" % e)
try:
stdout, _ = proc.communicate(timeout=timeout)
rc = proc.returncode
except subprocess.TimeoutExpired:
# Kill the whole process group.
try:
os.killpg(os.getpgid(proc.pid), signal.SIGKILL)
except ProcessLookupError:
pass
try:
stdout, _ = proc.communicate(timeout=5)
except Exception:
stdout = ""
return done(
False,
stdout or "",
error="timeout: exceeded %ss, process group killed" % timeout,
)
output = stdout or ""
if rc == 0:
return done(True, output)
return done(False, output, error="exit code %d" % rc)
if __name__ == "__main__":
import json
import sys
cases = [
("echo hello", 10),
("rm -rf /", 10),
]
# Allow ad-hoc: python executor.py "<task>" [timeout]
if len(sys.argv) > 1:
cases = [(sys.argv[1], int(sys.argv[2]) if len(sys.argv) > 2 else 600)]
for task, to in cases:
print("TASK:", task)
print(json.dumps(execute_task(task, timeout=to), indent=1))
print("-" * 40)