2026-10-05 17:29:16 +00:00
|
|
|
"""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
|
2026-10-05 20:18:46 +00:00
|
|
|
# Must start with a plausible executable command or path
|
2026-10-05 17:29:16 +00:00
|
|
|
first = t.split()[0] if t.split() else ""
|
|
|
|
|
if not re.match(r"^[a-zA-Z0-9_.\-/]+$", first):
|
|
|
|
|
return False
|
2026-10-05 20:18:46 +00:00
|
|
|
# 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
|
2026-10-05 17:29:16 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
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)
|