From dc8271a06a5fab8b6594d98aa4df88ccd0aaaf95 Mon Sep 17 00:00:00 2001 From: "jj.wang" Date: Wed, 21 Oct 2026 08:08:01 +0000 Subject: [PATCH] warpgate-ssh: remote Linux/macOS terminals through the relay; oregon launched remotely The dispatcher moves to an LXC without agent CLIs (design 9), so every terminal is reached through Warpgate. The relay call (agent-access.py run --target ) lives in one helper; the target comes from the terminal config. os linux/darwin (PosixWarpgateTransport): each primitive is one bash -c call. Files go up as base64 (chunked above 60k), start_run stages prompt.md + run.sh and launches in a single round trip; the runner is detached with setsid nohup (pid == pgid == sid, $! kept in launch.pid); kill signals the process group and session (TERM, then KILL), skipped once exit.txt exists. macOS detaches with perl POSIX::setsid. os windows keeps the win10-ps.sh / win10-put.sh helper scripts (target win10) and schtasks, now through the shared call() helper. oregon is now warpgate-ssh (target oregon, run_root ~/.gwent-board/agentd-runs); the local transport stays for co-located setups and tests. --- agentd/config.py | 18 ++- agentd/executors/base.py | 13 +- agentd/transports/local.py | 14 +- agentd/transports/warpgate_ssh.py | 251 +++++++++++++++++++++++++++--- config/agentd.example.yaml | 13 +- config/agentd.yaml | 13 +- config/schema.json | 2 +- tests/test_posix_remote.py | 205 ++++++++++++++++++++++++ tests/test_transports.py | 6 +- 9 files changed, 490 insertions(+), 45 deletions(-) create mode 100644 tests/test_posix_remote.py diff --git a/agentd/config.py b/agentd/config.py index 652b638..e5acff0 100644 --- a/agentd/config.py +++ b/agentd/config.py @@ -91,11 +91,19 @@ def semantic_errors(data): for ex in t.get("executors", []): if ex not in execs: errs.append("terminals.%s: unknown executor '%s'" % (tname, ex)) - if t.get("transport") == "warpgate-ssh": - if t.get("os") != "windows" or t.get("launch") != "schtasks": - errs.append("terminals.%s: warpgate-ssh currently supports only os=windows, launch=schtasks" % tname) - if t.get("target") != "win10": - errs.append("terminals.%s: warpgate-ssh helper scripts only reach target 'win10' for now" % tname) + if t.get("transport") == "warpgate-ssh" and t.get("os") == "windows": + if t.get("launch") != "schtasks": + errs.append("terminals.%s: warpgate-ssh on windows needs launch=schtasks" % tname) + if t.get("target", tname) != "win10": + errs.append("terminals.%s: the Windows helper scripts only reach target 'win10' for now" % tname) + elif t.get("transport") == "warpgate-ssh": + if t.get("launch") != "nohup": + errs.append("terminals.%s: warpgate-ssh on %s needs launch=nohup" % (tname, t.get("os"))) + for key in ("repo", "run_root"): + if key in t and not str(t[key]).startswith("/"): + errs.append("terminals.%s.%s: must be an absolute path on the remote (no ~)" % (tname, key)) + if not data.get("vault", {}).get("config"): + errs.append("terminals.%s: remote POSIX terminals need vault.config (relay identity)" % tname) if t.get("transport") == "local" and t.get("os") != "linux": errs.append("terminals.%s: local transport supports only os=linux" % tname) for ename, e in execs.items(): diff --git a/agentd/executors/base.py b/agentd/executors/base.py index ca9dc9b..37f0c14 100644 --- a/agentd/executors/base.py +++ b/agentd/executors/base.py @@ -87,13 +87,16 @@ class RunnerExecutor(Executor): def start(self, run): t = self.transport run_dir = t.join(self.run_root(), run.run_id) - t.mkdir(run_dir, clean=True) - t.write_text(t.join(run_dir, "prompt.md"), run.prompt) script_name, script = t.runner(run_dir, run.repo, self.command(run, run_dir), self.stdout_name) - script_path = t.join(run_dir, script_name) - t.write_text(script_path, script) task = "agentd-" + run.run_id - t.launch(task, script_path, run_dir) + files = {"prompt.md": run.prompt, script_name: script} + if hasattr(t, "start_run"): # remote POSIX: stage + launch in one relay round trip + t.start_run(task, run_dir, files, script_name) + else: + t.mkdir(run_dir, clean=True) + for name, text in files.items(): + t.write_text(t.join(run_dir, name), text) + t.launch(task, t.join(run_dir, script_name), run_dir) return {"run_dir": run_dir, "task": task, "thread_id": run.thread_id} def poll(self, handle): diff --git a/agentd/transports/local.py b/agentd/transports/local.py index 5d49830..e5563fa 100644 --- a/agentd/transports/local.py +++ b/agentd/transports/local.py @@ -4,17 +4,23 @@ import signal import subprocess from pathlib import Path +# Shared with the remote POSIX transport (warpgate_ssh): plain sh + `date +FMT`, so it also runs on macOS. RUNNER = """#!/usr/bin/env bash -cd {cwd} || {{ echo "exit=97 at $(date -Is) (cd failed)" > {d}/exit.txt; exit 97; }} +cd {cwd} || {{ echo "exit=97 at $(date +%Y-%m-%dT%H:%M:%S%z) (cd failed)" > {d}/exit.txt; exit 97; }} echo $$ > {d}/pid.txt -date -Is > {d}/started.txt +date +%Y-%m-%dT%H:%M:%S%z > {d}/started.txt ( {command} ) < {d}/prompt.md > {d}/{stdout} 2> {d}/stderr.txt -echo "exit=$? at $(date -Is)" > {d}/exit.txt +echo "exit=$? at $(date +%Y-%m-%dT%H:%M:%S%z)" > {d}/exit.txt """ +def sh_quote(s): + """POSIX shell single-quote literal (always quoted, `'` -> `'\\''`).""" + return "'" + str(s).replace("'", "'\\''") + "'" + + class LocalTransport: os = "linux" @@ -28,7 +34,7 @@ class LocalTransport: return os.path.join(*parts) def quote(self, s): - return "'" + str(s).replace("'", "'\\''") + "'" + return sh_quote(s) def mkdir(self, path, clean=False): p = Path(path) diff --git a/agentd/transports/warpgate_ssh.py b/agentd/transports/warpgate_ssh.py index 82ae0f3..eaad1a4 100644 --- a/agentd/transports/warpgate_ssh.py +++ b/agentd/transports/warpgate_ssh.py @@ -1,16 +1,59 @@ -"""Windows terminal behind Warpgate, reached through the server-access relay helper scripts: - win10-ps.sh