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 <t>) 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.
This commit is contained in:
jj.wang
2026-10-21 08:08:01 +00:00
parent f2f7e4308a
commit dc8271a06a
9 changed files with 490 additions and 45 deletions
+205
View File
@@ -0,0 +1,205 @@
"""Remote POSIX transport (warpgate-ssh, os linux/darwin): the generated bash is executed by a local bash in
place of the relay, so quoting, chunked uploads, detached launch and process-group kill are exercised for real."""
import base64
import os
import shutil
import subprocess
import tempfile
import time
import unittest
from pathlib import Path
from unittest import mock
from agentd import ROOT, config
from agentd.config import Config
from agentd.executors import Run, make_executor
from agentd.transports import make_transport
from agentd.transports.warpgate_ssh import PosixWarpgateTransport, relay_argv
from tests.fakes import config_data
FAKE_CLAUDE = """#!/usr/bin/env bash
prompt="$(cat)"
echo "args: $*" >&2
printf '{"type":"result","subtype":"success","is_error":false,"result":"[review] got %s","session_id":"sess-7"}' "${#prompt}"
"""
def remote_data(root):
d = config_data()
d["vault"] = {"client": "/opt/vault/agent-access.py", "config": "https://vault.example/identity.json"}
d["terminals"]["mac"] = {"transport": "warpgate-ssh", "target": "mac-target", "os": "linux", "launch": "nohup",
"repo": str(root / "my repo's"), "run_root": str(root / "run root"), "plane_user": "oregon",
"token_key": "TOK_MAC", "concurrency": 1, "executors": ["claude-code", "shell"],
"bins": {"claude": str(root / "bin dir" / "claude")}}
return d
class LocalBash(PosixWarpgateTransport):
"""The relay replaced by `bash -c` on this machine; records every script (one entry = one relay call)."""
def __init__(self, cfg, terminal):
super().__init__(cfg, terminal)
self.calls = []
def sh(self, script, timeout=150, marker=None):
self.calls.append(script)
out = subprocess.run(["bash", "-c", script], capture_output=True, text=True, timeout=timeout,
env=self.cfg.scrubbed_env())
if marker and marker not in out.stdout:
raise AssertionError("marker %s missing: %s %s" % (marker, out.stdout, out.stderr))
return out.stdout
def wait(ex, h, seconds=10):
end = time.time() + seconds
while time.time() < end:
if ex.poll(h) == "finished":
return True
time.sleep(0.1)
return False
class PosixRemoteTest(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.root = Path(self.tmp.name)
(self.root / "my repo's").mkdir()
(self.root / "bin dir").mkdir()
fake = self.root / "bin dir" / "claude"
fake.write_text(FAKE_CLAUDE)
fake.chmod(0o755)
self.cfg = Config(remote_data(self.root))
self.t = LocalBash(self.cfg, "mac")
def tearDown(self):
self.tmp.cleanup()
def test_factory_picks_posix_for_linux_and_config_is_valid(self):
self.assertEqual(config.validate(remote_data(self.root)), [])
self.assertIsInstance(make_transport(self.cfg, "mac"), PosixWarpgateTransport)
self.assertEqual(make_transport(self.cfg, "win10").os, "windows")
def test_live_config_runs_oregon_remotely(self):
cfg = config.load(ROOT / "config" / "agentd.yaml")
t = make_transport(cfg, "oregon")
self.assertIsInstance(t, PosixWarpgateTransport)
self.assertEqual((t.target, t.os), ("oregon", "linux"))
self.assertEqual(cfg.executors["codex"]["tier_defaults"], {"gpt-6.1-sol": "fast"})
def test_relay_argv_and_scrubbed_env(self):
argv = relay_argv(self.cfg, "mac-target", ["bash", "-c", "echo hi"], 30)
self.assertEqual(argv[1:], ["-I", "/opt/vault/agent-access.py", "--config", "https://vault.example/identity.json",
"run", "--target", "mac-target", "--timeout", "30", "--", "bash", "-c", "echo hi"])
seen = {}
def fake_run(args, **kw):
seen.update(args=args, env=kw["env"])
return mock.Mock(returncode=0, stdout=b"MKDIR=ok\n", stderr=b"")
with mock.patch.dict(os.environ, {"TOK_MAC": "s", "TOK_W10": "s"}), \
mock.patch("agentd.transports.warpgate_ssh.subprocess.run", fake_run):
PosixWarpgateTransport(self.cfg, "mac").mkdir("/x")
self.assertNotIn("TOK_MAC", seen["env"])
self.assertNotIn("TOK_W10", seen["env"])
self.assertEqual(seen["args"][-3:-1], ["bash", "-c"])
def test_claude_round_is_one_call_and_resume_uses_session_id(self):
ex = make_executor(self.cfg, "mac", "claude-code", self.t)
prompt = "it's \"quoted\" $HOME `x` \\ 中文\n"
h = ex.start(Run("GW-4-1", self.cfg.terminals["mac"]["repo"], prompt, model="sonnet"))
self.assertEqual(len(self.t.calls), 1) # mkdir + prompt + run.sh + launch in one relay call
self.assertIn("setsid nohup bash 'run.sh' </dev/null >/dev/null 2>&1 &", self.t.calls[0])
self.assertTrue(wait(ex, h))
rd = Path(h["run_dir"])
self.assertEqual((rd / "prompt.md").read_text(encoding="utf-8"), prompt)
self.assertEqual((rd / "pid.txt").read_text().strip(), (rd / "launch.pid").read_text().strip())
res = ex.collect(h)
self.assertEqual((res.last_message, res.exit_code, res.thread_id),
("[review] got %d" % len(prompt.rstrip("\n")), 0, "sess-7"))
cmd = ex.command(Run("GW-4-2", "r", "p", model="sonnet", thread_id="sess-7"), h["run_dir"])
self.assertEqual(cmd, "'%s' -p --resume sess-7" % (self.root / "bin dir" / "claude"))
def test_runner_quotes_paths_and_cd_failure_writes_exit(self):
_, script = self.t.runner("/a b/O'Neil/run", "/no such/repo's", "cmd", "out.json")
self.assertIn("cd '/no such/repo'\\''s' ||", script)
self.assertIn("< '/a b/O'\\''Neil/run'/prompt.md > '/a b/O'\\''Neil/run'/out.json", script)
ex = make_executor(self.cfg, "mac", "shell", self.t)
h = ex.start(Run("GW-4-3", str(self.root / "missing"), "", script="true"))
self.assertTrue(wait(ex, h))
self.assertEqual(ex.exit_code(h), 97)
def test_chunked_upload_and_reads(self):
self.t.CHUNK = 64
path = str(self.root / "run root" / "x y" / "f'.txt")
text = "line1 'q' \"d\" $(nope)\n" * 20 + "last ünïcode"
self.t.mkdir(os.path.dirname(path))
n = len(self.t.calls)
self.t.write_text(path, text)
self.assertGreater(len(self.t.calls) - n, 2)
self.assertEqual(Path(path).read_text(encoding="utf-8"), text)
self.assertFalse(os.path.exists(path + ".b64"))
self.assertEqual(self.t.read_text(path), text)
self.assertIsNone(self.t.read_text(path + ".missing"))
self.assertEqual(self.t.grep_first(path, "$(nope)"), "line1 'q' \"d\" $(nope)")
self.assertIsNone(self.t.grep_first(path, "absent"))
self.assertEqual(self.t.tail(path, 2), "line1 'q' \"d\" $(nope)\nlast ünïcode")
self.assertEqual(self.t.tail(path + ".missing", 2), "")
def test_big_prompt_goes_up_before_the_launch_call(self):
self.t.CHUNK = 100
ex = make_executor(self.cfg, "mac", "shell", self.t)
h = ex.start(Run("GW-4-4", self.cfg.terminals["mac"]["repo"], "z" * 500, script="wc -c"))
self.assertGreater(len(self.t.calls), 2)
self.assertTrue(wait(ex, h))
self.assertEqual(ex.collect(h).last_message, "500")
@unittest.skipUnless(shutil.which("setsid") and shutil.which("pgrep"), "needs setsid/pgrep")
def test_kill_ends_the_whole_process_group(self):
ex = make_executor(self.cfg, "mac", "shell", self.t)
marker = "agentd-test-%d" % os.getpid()
h = ex.start(Run("GW-4-5", self.cfg.terminals["mac"]["repo"], "",
script="bash -c 'sleep 300; : %s' & sleep 300; : %s" % (marker, marker)))
end = time.time() + 5
while time.time() < end and not subprocess.run(["pgrep", "-f", marker], capture_output=True).stdout:
time.sleep(0.1)
self.assertTrue(subprocess.run(["pgrep", "-f", marker], capture_output=True).stdout)
ex.kill(h)
self.assertIn("KILLED=ok left=0", subprocess.run(["bash", "-c", self.t.kill_script(h["run_dir"])],
capture_output=True, text=True).stdout)
self.assertFalse(subprocess.run(["pgrep", "-f", marker], capture_output=True).stdout)
self.assertEqual(ex.poll(h), "running") # killed runners write no exit.txt; core treats it as cancelled
def test_kill_after_finish_does_not_signal_a_reused_pid(self):
ex = make_executor(self.cfg, "mac", "shell", self.t)
h = ex.start(Run("GW-4-6", self.cfg.terminals["mac"]["repo"], "", script="true"))
self.assertTrue(wait(ex, h))
(Path(h["run_dir"]) / "pid.txt").write_text("1\n")
self.assertIn("KILLED=finished", subprocess.run(["bash", "-c", self.t.kill_script(h["run_dir"])],
capture_output=True, text=True).stdout)
def test_darwin_detaches_without_setsid_binary(self):
d = remote_data(self.root)
d["terminals"]["mac"]["os"] = "darwin"
t = PosixWarpgateTransport(Config(d), "mac")
s = t.launch_script("/r", "run.sh")
self.assertIn("nohup perl -MPOSIX -e 'POSIX::setsid() or die", s)
self.assertNotIn("setsid nohup", s)
self.assertIn("echo $! > launch.pid", s)
def test_semantic_checks(self):
d = remote_data(self.root)
d["terminals"]["mac"]["launch"] = "schtasks"
d["terminals"]["mac"]["run_root"] = "~/runs"
del d["vault"]["config"]
errs = config.validate(d)
for frag in ("needs launch=nohup", "mac.run_root: must be an absolute path", "need vault.config"):
self.assertTrue(any(frag in e for e in errs), (frag, errs))
d = remote_data(self.root)
d["terminals"]["win10"]["target"] = "win11"
self.assertTrue(any("only reach target 'win10'" in e for e in config.validate(d)))
def test_upload_payload_is_plain_base64(self):
s = self.t.put_script("/r/p.md", base64.b64encode("x'y".encode()).decode())
self.assertIn("printf %s 'eCd5' | base64 -d > '/r/p.md.part' && mv -f '/r/p.md.part' '/r/p.md'", s)
if __name__ == "__main__":
unittest.main()
+3 -3
View File
@@ -12,11 +12,11 @@ from agentd.config import Config
from agentd.executors import Run, make_executor
from agentd.store import Store
from agentd.transports.local import LocalTransport
from agentd.transports.warpgate_ssh import WarpgateSshTransport
from agentd.transports.warpgate_ssh import PosixWarpgateTransport, WarpgateSshTransport, WindowsWarpgateTransport
from tests.fakes import config_data
class RecordingWin(WarpgateSshTransport):
class RecordingWin(WindowsWarpgateTransport):
"""Windows transport with the relay replaced by an in-memory recorder."""
def __init__(self, cfg, terminal):
super().__init__(cfg, terminal)
@@ -95,7 +95,7 @@ class WindowsScriptTest(unittest.TestCase):
def fake_run(args, **kw):
seen.update(kw["env"])
return mock.Mock(returncode=0, stdout="X=ok", stderr="")
return mock.Mock(returncode=0, stdout=b"X=ok", stderr=b"")
with mock.patch.dict(os.environ, {"PLANE_WINDOW10_API_TOKEN": "s"}), \
mock.patch("agentd.transports.warpgate_ssh.subprocess.run", fake_run):
WarpgateSshTransport(self.cfg, "win10").ps("'X=ok'")