PAD-12: per-terminal transport (direct Tailscale SSH or Warpgate, pre-session fallback, pinned host keys, agent key from Vault), one-call win10 launch and one-call collect, remote call timings, agentd ping

This commit is contained in:
jj.wang
2026-10-22 02:59:37 +00:00
parent 83bc0f243c
commit 5192e04088
23 changed files with 1850 additions and 132 deletions
+93 -17
View File
@@ -2,8 +2,10 @@
The config only ever holds Vault *key names* (`token_key`). The token itself is read from the environment
variable of the same name, which `agent-access.py exec --secret-env KEY=KEY` injects into the process."""
import base64
import json
import os
import struct
from pathlib import Path
import yaml
@@ -11,6 +13,7 @@ import yaml
from . import ROOT
SCHEMA_PATH = ROOT / "config" / "schema.json"
DEFAULT_SSH_KEY = "AGENTD_SSH_KEY" # env var (== Vault key name) carrying the direct-ssh private key
DEFAULT_PATHS = (ROOT / "config" / "agentd.local.yaml", ROOT / "config" / "agentd.yaml")
@@ -83,6 +86,48 @@ def schema_errors(data, schema):
for e in validator.iter_errors(data)]
def host_key_error(value):
"""None if `value` is a usable pinned host key ('ssh-ed25519 <base64>' whose blob names the same type)."""
parts = str(value).split()
if len(parts) < 2 or parts[0] != "ssh-ed25519":
return "must be 'ssh-ed25519 <base64>' (the terminal's /etc/ssh/ssh_host_ed25519_key.pub, verified out of band)"
try:
blob = base64.b64decode(parts[1], validate=True)
n = struct.unpack(">I", blob[:4])[0]
ok = blob[4:4 + n] == b"ssh-ed25519" and len(blob) == 4 + n + 4 + 32
except (ValueError, struct.error):
ok = False
return None if ok else "the base64 part is not an ssh-ed25519 public key (copy/paste damage?)"
def ssh_dir_of(data):
d = (data.get("agentd") or {}).get("ssh_dir")
if d:
p = Path(os.path.expanduser(d))
return p if p.is_absolute() else ROOT / p
s = Path(os.path.expanduser((data.get("agentd") or {}).get("state_dir", "state")))
return (s if s.is_absolute() else ROOT / s) / "ssh"
def remote_errors(data, tname, t, kind):
"""Rules every remote transport (warpgate-ssh, direct-ssh, and the fallback channel) shares."""
errs = []
if t.get("os") == "windows":
if t.get("launch") != "schtasks":
errs.append("terminals.%s: %s on windows needs launch=schtasks" % (tname, kind))
if kind == "warpgate-ssh" and t.get("target", tname) != "win10":
errs.append("terminals.%s: the Windows helper scripts only reach target 'win10' for now" % tname)
else:
if t.get("launch") != "nohup":
errs.append("terminals.%s: %s on %s needs launch=nohup" % (tname, kind, 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 kind == "warpgate-ssh" and not data.get("vault", {}).get("config"):
errs.append("terminals.%s: remote POSIX terminals need vault.config (relay identity)" % tname)
return errs
def semantic_errors(data):
"""Cross-references the schema cannot express."""
errs = []
@@ -91,21 +136,36 @@ 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" 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":
kind = t.get("transport")
if kind == "warpgate-ssh":
errs += remote_errors(data, tname, t, kind)
elif kind == "direct-ssh":
errs += remote_errors(data, tname, t, kind)
s = t.get("ssh")
if not s:
errs.append("terminals.%s: direct-ssh needs ssh: {host, user, port, host_key}" % tname)
else:
if not s.get("host") or str(s["host"]).startswith("-") or not s.get("user"):
errs.append("terminals.%s.ssh: host and user must be non-empty (host not starting with '-')" % tname)
if s.get("port", 22) > 65535:
errs.append("terminals.%s.ssh.port: %r > 65535" % (tname, s["port"]))
why = host_key_error(s.get("host_key", ""))
if why:
errs.append("terminals.%s.ssh.host_key: %s" % (tname, why))
if t.get("fallback") == "warpgate-ssh":
if "target" not in t:
errs.append("terminals.%s: fallback warpgate-ssh needs the Warpgate 'target'" % tname)
elif t.get("os") == "windows" and t["target"] != "win10":
errs.append("terminals.%s: the Windows helper scripts (fallback) only reach target 'win10' for now" % tname)
if t.get("os") != "windows" and not data.get("vault", {}).get("config"):
errs.append("terminals.%s: fallback warpgate-ssh needs vault.config (relay identity)" % tname)
elif t.get("fallback"):
errs.append("terminals.%s: 'fallback' only applies to transport direct-ssh" % tname)
if kind == "local" and t.get("os") != "linux":
errs.append("terminals.%s: local transport supports only os=linux" % tname)
ctl = ssh_dir_of(data) # "/<pin8>-<40-char %C>", plus ".<16 chars>" while ssh sets the socket up
if any(t.get("transport") == "direct-ssh" for t in terms.values()) and len(str(ctl)) + 1 + 9 + 40 + 17 > 104:
errs.append("agentd.ssh_dir: %s is too long for the ControlMaster socket path (UNIX limit 104)" % ctl)
for ename, e in execs.items():
adapter = e.get("adapter", ename)
if adapter not in ("codex", "claude-code", "shell"):
@@ -156,6 +216,19 @@ class Config:
p = Path(os.path.expanduser(self.agentd.get("state_dir", "state")))
return p if p.is_absolute() else ROOT / p
@property
def ssh_dir(self):
"""direct-ssh ControlMaster sockets + generated known_hosts (default <state_dir>/ssh)."""
return ssh_dir_of(self.data)
@property
def ssh_key_env(self):
"""Env var (== Vault key name) holding the direct-ssh private key."""
return self.agentd.get("ssh_key") or DEFAULT_SSH_KEY
def uses_direct_ssh(self):
return any(t["transport"] == "direct-ssh" for t in self.terminals.values())
def helpers_dir(self, terminal):
d = self.terminals[terminal].get("helpers_dir") or self.agentd.get("helpers_dir") or "tools/win10"
p = Path(os.path.expanduser(d))
@@ -185,13 +258,16 @@ class Config:
return keys
def scrubbed_env(self):
"""os.environ without the secrets above, for every child process agentd starts."""
drop = self.secret_keys()
"""os.environ without the secrets above, for every child process agentd starts. The direct-ssh key variable
is always dropped, even with no direct-ssh terminal (a rollback may leave the cron script injecting it)."""
drop = self.secret_keys() | {self.ssh_key_env}
return {k: v for k, v in os.environ.items() if k not in drop}
def secret_env_args(self):
"""`--secret-env` arguments for agent-access.py exec, one per distinct token key."""
"""`--secret-env` arguments for agent-access.py exec: one per distinct token key, plus the direct-ssh key."""
keys = sorted({t["token_key"] for t in self.terminals.values()})
if self.uses_direct_ssh():
keys.append(self.ssh_key_env)
return ["--secret-env %s=%s" % (k, k) for k in keys]
def project(self, name):