Files
plane-agentd/agentd/transports/local.py
T
jj.wang 00d42267e5 Harden transports and state files
- Executors and relay helpers get an environment without any configured
  Plane token / webhook secret (design 8: executors hold no Plane credentials).
- Windows runner: Set-Location -LiteralPath, and a failed cd writes exit=97
  instead of running the agent in the wrong directory.
- schtasks launch: escape apostrophes and pass the script path as \"...\" so
  paths with spaces survive PowerShell 5.1 native quoting.
- State and heartbeat files are written temp + fsync + rename.
- Tests for the above plus codex run/resume command shapes and unique run dirs.
2026-10-21 07:10:41 +00:00

81 lines
2.8 KiB
Python

"""Local Linux transport: plain files plus a detached `bash run.sh` in its own session (setsid)."""
import os
import signal
import subprocess
from pathlib import Path
RUNNER = """#!/usr/bin/env bash
cd {cwd} || {{ echo "exit=97 at $(date -Is) (cd failed)" > {d}/exit.txt; exit 97; }}
echo $$ > {d}/pid.txt
date -Is > {d}/started.txt
(
{command}
) < {d}/prompt.md > {d}/{stdout} 2> {d}/stderr.txt
echo "exit=$? at $(date -Is)" > {d}/exit.txt
"""
class LocalTransport:
os = "linux"
def __init__(self, cfg, terminal):
self.cfg, self.terminal = cfg, terminal
def default_run_root(self):
return str(self.cfg.state_dir / "runs")
def join(self, *parts):
return os.path.join(*parts)
def quote(self, s):
return "'" + str(s).replace("'", "'\\''") + "'"
def mkdir(self, path, clean=False):
p = Path(path)
p.mkdir(parents=True, exist_ok=True)
if clean:
for f in p.iterdir():
if f.is_file():
f.unlink()
def write_text(self, path, text):
Path(path).write_text(text, encoding="utf-8")
def read_text(self, path):
p = Path(path)
return p.read_text(encoding="utf-8", errors="replace") if p.exists() else None
def grep_first(self, path, needle):
text = self.read_text(path) or ""
return next((line for line in text.splitlines() if needle in line), None)
def tail(self, path, n):
text = self.read_text(path) or ""
return "\n".join(text.splitlines()[-n:])
def runner(self, run_dir, cwd, command, stdout_name):
return "run.sh", RUNNER.format(cwd=self.quote(cwd), d=self.quote(run_dir), command=command, stdout=stdout_name)
def launch(self, task, script_path, run_dir):
# `setsid -f` double-forks: the runner leads its own session (pgid == pid, see kill) and is reparented
# to init, so a long-running `tick --loop` never accumulates zombies.
# The executor must not hold Plane credentials (design §8): drop every configured secret from its env.
subprocess.run(["setsid", "-f", "bash", script_path], cwd=run_dir, check=True, close_fds=True,
env=self.cfg.scrubbed_env(),
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
def kill(self, task, run_dir):
pid = (self.read_text(os.path.join(run_dir, "pid.txt")) or "").strip()
if pid.isdigit():
try:
os.killpg(int(pid), signal.SIGTERM) # runner is a session leader: pgid == pid
except ProcessLookupError:
pass
def forget(self, task):
pass
def process_count(self, name):
out = subprocess.run(["pgrep", "-c", "-x", name], capture_output=True, text=True)
return int(out.stdout.strip() or 0)