Files
.pouch/skills/agent-runtime/scripts/agent.py
T
laily adaa8e4edd feat: add test-preflight runner and deployer prepare adapters
agent-runtime executes fixed tester node operations over JSON stdin.
deployer prepare brings up the local test Compose service the same way.
test-preflight now passes the tester binding; tester select skips
unauthorized cases before creating a runner.
2026-10-19 09:28:50 +08:00

1189 lines
64 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Validate and execute the shared project Agent runtime contract.
The command surface is deliberately small. It never evaluates project text as
shell, and it only creates builder sessions from a fixed make invocation.
"""
from __future__ import annotations
import argparse
import base64
import copy
import gzip
import hashlib
import json
import os
import platform as host_platform
import re
import shlex
import shutil
import socket
import subprocess
import sys
import time
import uuid
import urllib.error
import urllib.parse
import urllib.request
from pathlib import Path
from typing import Any
try:
import yaml
except ImportError as exc: # pragma: no cover - dependency is checked at runtime
raise SystemExit("需要 PyYAML: python3 -m pip install pyyaml") from exc
CLI_NAMES = {"codex", "cursor-agent", "grok", "omp", "pi"}
ADAPTERS = {"builder", "tester", "ack"}
EFFORTS = {None, "low", "medium", "high", "xhigh"}
PERMISSIONS = {"read-only", "workspace-write"}
TIERS = {"standard", "strong"}
LIFECYCLES = {"persistent", "ephemeral"}
ID_RE = re.compile(r"^[a-z][a-z0-9-]{1,63}$")
BINDING_RE = re.compile(r"^[a-z][a-z0-9-]*(?:\.[a-z][a-z0-9-]*)+$")
TARGET_RE = re.compile(r"^[A-Za-z0-9_.-]{1,96}$")
MODEL_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:/+@-]{0,127}$")
MAKEFILE_RE = re.compile(r"^[A-Za-z0-9_.-]+(?:/[A-Za-z0-9_.-]+)*$")
FORBIDDEN_KEYS = {"command", "argv", "shell", "env", "environmentVariables", "token", "secret", "password"}
RUNNER_OPERATIONS = {"platform_probe", "network_probe", "install", "start", "maestro", "maestro_batch", "artifact_download"}
RUNNER_PLATFORMS = {"android", "ios"}
LOCAL_NODE_NAMES = {"local", "localhost", "127.0.0.1", "::1"}
class ConfigError(ValueError):
pass
class UniqueLoader(yaml.SafeLoader):
pass
def _construct_mapping(loader: UniqueLoader, node: yaml.MappingNode, deep: bool = False) -> dict[str, Any]:
mapping: dict[str, Any] = {}
for key_node, value_node in node.value:
key = loader.construct_object(key_node, deep=deep)
if not isinstance(key, str):
raise ConfigError("YAML mapping key 必须是字符串")
if key in mapping:
raise ConfigError(f"YAML 存在重复键: {key}")
mapping[key] = loader.construct_object(value_node, deep=deep)
return mapping
UniqueLoader.add_constructor(yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, _construct_mapping)
def load_yaml(path: Path) -> dict[str, Any]:
try:
value = yaml.load(path.read_text(encoding="utf-8"), Loader=UniqueLoader)
except (OSError, yaml.YAMLError) as exc:
raise ConfigError(f"无法读取 {path}: {exc}") from exc
if value is None:
return {}
if not isinstance(value, dict):
raise ConfigError(f"{path} 顶层必须是 mapping")
return value
def deep_merge(base: dict[str, Any], override: dict[str, Any], where: str = "root") -> dict[str, Any]:
result = copy.deepcopy(base)
for key, value in override.items():
if key not in result:
raise ConfigError(f"{where}: local 配置不能增加未声明字段 {key!r}")
if isinstance(result[key], dict) and isinstance(value, dict):
result[key] = deep_merge(result[key], value, f"{where}.{key}")
else:
result[key] = copy.deepcopy(value)
return result
def text(value: Any, where: str) -> str:
if not isinstance(value, str) or not value.strip() or any(ord(c) < 32 for c in value):
raise ConfigError(f"{where}: 必须是非空文本")
return value
def safe_id(value: Any, where: str) -> str:
value = text(value, where)
if not ID_RE.fullmatch(value):
raise ConfigError(f"{where}: ID 只能使用小写字母、数字和连字符")
return value
def safe_binding_id(value: Any, where: str) -> str:
value = text(value, where)
if not BINDING_RE.fullmatch(value):
raise ConfigError(f"{where}: binding 必须是 namespace.name 形式")
return value
def safe_path(value: Any, where: str) -> str:
value = text(value, where)
path = Path(value)
if not path.is_absolute() or value == os.path.sep or ".." in path.parts or os.path.normpath(value) != value:
raise ConfigError(f"{where}: 必须是规范绝对路径")
return value
def validate(config: dict[str, Any]) -> None:
if config.get("schemaVersion") != 1:
raise ConfigError("schemaVersion 必须为 1")
for key in ("locations", "profiles", "bindings"):
if not isinstance(config.get(key), dict) or not config[key]:
raise ConfigError(f"{key}: 必须是非空 mapping")
locations = config["locations"]
for location_id, location in locations.items():
safe_id(location_id, f"locations.{location_id}")
if not isinstance(location, dict):
raise ConfigError(f"locations.{location_id}: 必须是 mapping")
for required in ("environment", "project", "path"):
if required not in location:
raise ConfigError(f"locations.{location_id}: 缺少 {required}")
environment = text(location["environment"], f"locations.{location_id}.environment")
if environment != "local" and not ID_RE.fullmatch(environment):
raise ConfigError(f"locations.{location_id}.environment: 必须是 local 或安全环境名")
text(location["project"], f"locations.{location_id}.project")
safe_path(location["path"], f"locations.{location_id}.path")
host = location.get("host")
if host is not None:
host = text(host, f"locations.{location_id}.host")
if host != "local" and not re.fullmatch(r"runtime:[0-9a-f-]{16,}", host):
raise ConfigError(f"locations.{location_id}.host: 必须是 local 或 runtime:<environment-id>")
profiles = config["profiles"]
for profile_id, profile in profiles.items():
safe_id(profile_id, f"profiles.{profile_id}")
if not isinstance(profile, dict):
raise ConfigError(f"profiles.{profile_id}: 必须是 mapping")
for required in ("cli", "model", "reasoningEffort", "permissionMode"):
if required not in profile:
raise ConfigError(f"profiles.{profile_id}: 缺少 {required}")
if profile["cli"] not in CLI_NAMES:
raise ConfigError(f"profiles.{profile_id}.cli: 不支持 {profile['cli']!r}")
if profile["model"] is not None and MODEL_RE.fullmatch(str(profile["model"])) is None:
raise ConfigError(f"profiles.{profile_id}.model: 不是安全模型 ID")
if profile["reasoningEffort"] not in EFFORTS:
raise ConfigError(f"profiles.{profile_id}.reasoningEffort: 不支持该值")
if profile["permissionMode"] not in PERMISSIONS:
raise ConfigError(f"profiles.{profile_id}.permissionMode: 不支持该值")
if profile.get("tier", "standard") not in TIERS:
raise ConfigError(f"profiles.{profile_id}.tier: 必须是 standard/strong")
for binding_id, binding in config["bindings"].items():
safe_binding_id(binding_id, f"bindings.{binding_id}")
if not isinstance(binding, dict):
raise ConfigError(f"bindings.{binding_id}: 必须是 mapping")
for required in ("adapter", "location", "profile"):
if required not in binding:
raise ConfigError(f"bindings.{binding_id}: 缺少 {required}")
if binding["adapter"] not in ADAPTERS:
raise ConfigError(f"bindings.{binding_id}.adapter: 不支持 {binding['adapter']!r}")
location_id = safe_id(binding["location"], f"bindings.{binding_id}.location")
profile_id = safe_id(binding["profile"], f"bindings.{binding_id}.profile")
if location_id not in locations or profile_id not in profiles:
raise ConfigError(f"bindings.{binding_id}: location/profile 引用不存在")
if binding["adapter"] == "builder":
makefile = text(binding.get("makefile", "makefile.builder"), f"bindings.{binding_id}.makefile")
if Path(makefile).is_absolute() or ".." in Path(makefile).parts or MAKEFILE_RE.fullmatch(makefile) is None:
raise ConfigError(f"bindings.{binding_id}.makefile: 必须是项目内相对路径")
target = text(binding.get("target"), f"bindings.{binding_id}.target")
if TARGET_RE.fullmatch(target) is None:
raise ConfigError(f"bindings.{binding_id}.target: 不是安全 make target")
session = binding.get("session", {})
if not isinstance(session, dict) or session.get("lifecycle", "ephemeral") not in LIFECYCLES:
raise ConfigError(f"bindings.{binding_id}.session.lifecycle: 必须是 persistent/ephemeral")
scan_forbidden(config)
def scan_forbidden(value: Any, where: str = "root") -> None:
if isinstance(value, dict):
for key, child in value.items():
if key in FORBIDDEN_KEYS:
raise ConfigError(f"{where}: 禁止配置字段 {key!r}")
scan_forbidden(child, f"{where}.{key}")
elif isinstance(value, list):
for index, child in enumerate(value):
scan_forbidden(child, f"{where}[{index}]")
def load_config(root: Path) -> dict[str, Any]:
path = root / ".pouch" / "agent.yaml"
if not path.is_file():
raise ConfigError(f"缺少 {path},请先创建项目通用 Agent 配置")
config = load_yaml(path)
local_path = root / ".pouch" / "agent.local.yaml"
if local_path.is_file():
config = deep_merge(config, load_yaml(local_path))
validate(config)
return config
def binding(config: dict[str, Any], binding_id: str) -> dict[str, Any]:
if binding_id not in config["bindings"]:
raise ConfigError(f"不存在 binding: {binding_id}")
value = copy.deepcopy(config["bindings"][binding_id])
value["id"] = binding_id
value["locationId"] = value["location"]
value["location"] = copy.deepcopy(config["locations"][value["locationId"]])
value["profileId"] = value["profile"]
value["profile"] = copy.deepcopy(config["profiles"][value["profileId"]])
return value
def orca_json(args: list[str], environment: str | None = None) -> dict[str, Any]:
command = ["orca", *args]
if environment and environment != "local":
command.extend(["--environment", environment])
command.append("--json")
# A remote terminal read is a poll, not a blocking operation. Some Orca
# providers keep the CLI open when a PTY has no new output, so bound the
# read and let the caller poll again. Other commands may legitimately
# take longer (for example terminal creation), hence the larger default.
timeout = 10 if args[:2] == ["terminal", "read"] else 120
try:
proc = subprocess.run(command, capture_output=True, text=True, check=False, timeout=timeout)
except subprocess.TimeoutExpired as exc:
if args[:2] == ["terminal", "read"]:
return {}
raise ConfigError(f"Orca command timed out after {timeout}s: {' '.join(command)}") from exc
raw = (proc.stdout or "").strip()
try:
data = json.loads(raw)
except json.JSONDecodeError as exc:
detail = (proc.stderr or raw or str(exc)).strip()
raise ConfigError(f"Orca 没有返回 JSON: {' '.join(command)}\n{detail}") from exc
if not data.get("ok"):
error = data.get("error") or {}
raise ConfigError(f"{' '.join(command)}: {error.get('message') or error.get('code') or 'unknown error'}")
result = data.get("result")
return result if isinstance(result, dict) else {}
def environment_id(name: str) -> str:
if name == "local":
return "local"
result = orca_json(["environment", "list"])
for item in result.get("environments", []):
if isinstance(item, dict) and item.get("name") == name:
return str(item.get("id"))
raise ConfigError(f"找不到已保存 Orca environment: {name}")
def location_host(location: dict[str, Any]) -> str:
host = location.get("host")
if host:
return str(host)
if location["environment"] == "local":
return "local"
return f"runtime:{environment_id(str(location['environment']))}"
def worktree_for(binding_value: dict[str, Any]) -> dict[str, Any]:
location = binding_value["location"]
environment = str(location["environment"])
project_ids = {str(location["project"])}
projects = orca_json(["project", "list"], environment).get("projects", [])
for project in projects:
if not isinstance(project, dict):
continue
if str(project.get("id")) == str(location["project"]) or str(project.get("displayName")) == str(location["project"]):
project_ids.update(str(repo_id) for repo_id in project.get("sourceRepoIds", []) if repo_id)
result = orca_json(["worktree", "list"], environment)
candidates = []
for item in result.get("worktrees", []):
if not isinstance(item, dict):
continue
item_project_ids = {str(item.get("projectId")), str(item.get("repoId"))}
if not project_ids.intersection(item_project_ids):
continue
if str(item.get("path")) != str(location["path"]):
continue
expected_host = location_host(location)
# A routed remote runtime stamps its own worktrees with hostId=local;
# the environment name/ID is the remote-machine discriminator.
if location["environment"] == "local" and expected_host == "local" and str(item.get("hostId")) != "local":
continue
candidates.append(item)
if not candidates:
raise ConfigError(
f"找不到精确 worktree: environment={environment}, host={location_host(location)}, "
f"project={location['project']}, path={location['path']};先创建或登记该 worktree"
)
if len(candidates) > 1:
# Orca can retain duplicate records for the same physical worktree
# after an environment is paired again. They are safe to collapse only
# when every execution-relevant field is identical; divergent records
# must still fail closed instead of guessing a node or checkout.
identity = {(str(item.get("path")), str(item.get("projectId")), str(item.get("head")), str(item.get("hostId"))) for item in candidates}
if len(identity) == 1:
return candidates[0]
raise ConfigError(f"binding 匹配到多个 worktree: {len(candidates)}")
return candidates[0]
def project_root_commit(root: Path) -> str | None:
proc = subprocess.run(["git", "-C", str(root), "rev-parse", "HEAD"], capture_output=True, text=True, check=False)
return proc.stdout.strip() if proc.returncode == 0 else None
def run_dir(root: Path) -> Path:
path = root / ".pouch" / "agent" / "runs"
path.mkdir(parents=True, exist_ok=True)
return path
def run_path(root: Path, run_id: str) -> Path:
if not re.fullmatch(r"run_[0-9a-f]{16,64}", run_id):
raise ConfigError("run id 格式无效")
return run_dir(root) / f"{run_id}.json"
def cmd_validate(args: argparse.Namespace) -> int:
config = load_config(Path(args.root).resolve())
print(json.dumps({"ok": True, "schemaVersion": config["schemaVersion"], "bindings": sorted(config["bindings"])}, ensure_ascii=False, indent=2))
return 0
def cmd_show(args: argparse.Namespace) -> int:
config = load_config(Path(args.root).resolve())
value = binding(config, args.binding)
print(json.dumps(value, ensure_ascii=False, indent=2, sort_keys=True))
return 0
def cmd_worktree_resolve(args: argparse.Namespace) -> int:
root = Path(args.root).resolve()
value = binding(load_config(root), args.binding)
worktree = worktree_for(value)
print(json.dumps({"binding": args.binding, "worktree": worktree}, ensure_ascii=False, indent=2))
return 0
def cmd_worktree_create(args: argparse.Namespace) -> int:
root = Path(args.root).resolve()
value = binding(load_config(root), args.binding)
location = value["location"]
try:
existing = worktree_for(value)
except ConfigError as exc:
if "找不到精确 worktree" not in str(exc):
raise
existing = None
if existing:
print(json.dumps({"created": False, "binding": args.binding, "worktree": existing}, ensure_ascii=False, indent=2))
return 0
host = location_host(location)
command = ["worktree", "create", "--project", str(location["project"]), "--host", host, "--name", args.name, "--no-parent"]
if args.base:
command.extend(["--base-branch", args.base])
result = orca_json(command, str(location["environment"]))
print(json.dumps({"created": True, "binding": args.binding, "result": result, "next": "将返回 worktree 的 path 写回 location.path 后再运行 resolve"}, ensure_ascii=False, indent=2))
return 0
def make_command(value: dict[str, Any]) -> str:
# Replace the shell so terminal wait observes the make exit code instead
# of waiting forever on the interactive shell after make returns.
return f"exec make -f {value.get('makefile', 'makefile.builder')} {value['target']}"
def runner_response(status: str, observed: Any = "", *, checks: dict[str, Any] | None = None, remediation: str = "", **extra: Any) -> dict[str, Any]:
"""Build the JSON response shared with test-preflight's runner contract."""
response: dict[str, Any] = {"status": status, "observed": observed}
if checks:
response["checks"] = checks
if remediation:
response["remediation"] = remediation
response.update(extra)
return response
def run_fixed_command(command: list[str], *, cwd: str | None = None) -> tuple[int, str]:
"""Run an adapter-owned argv; never invoke a shell or print command output."""
try:
process = subprocess.run(
command,
cwd=cwd,
stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
check=False,
)
except OSError as exc:
return 127, str(exc)
return process.returncode, (process.stdout or "").strip()
def _runner_check(status: str, observed: Any, expected: str, remediation: str = "") -> dict[str, Any]:
value: dict[str, Any] = {"status": status, "observed": observed, "expected": expected}
if remediation:
value["remediation"] = remediation
return value
def _local_platform_probe(platform_name: str) -> dict[str, Any]:
if platform_name == "android":
adb_code, adb_output = run_fixed_command(["adb", "version"])
if adb_code != 0:
failure = "adb unavailable on the local tester node"
return runner_response("environment-fail", failure, checks={
"adb": _runner_check("environment-fail", failure, "adb responds", "Install or expose adb on the tester node"),
"device": _runner_check("environment-fail", "not checked", "authorized Android device", "Make adb available first"),
"boot": _runner_check("environment-fail", "not checked", "Android device boot complete", "Make adb available first"),
})
devices_code, devices_output = run_fixed_command(["adb", "devices"])
devices: list[str] = []
if devices_code == 0:
for line in devices_output.splitlines():
if "\tdevice" in line:
devices.append(line.split("\t", 1)[0].strip())
if devices_code != 0 or not devices:
observed = "adb responded but no authorized device is available"
return runner_response("environment-fail", observed, checks={
"adb": _runner_check("ready" if devices_code == 0 else "environment-fail", "adb responded" if devices_code == 0 else "adb devices failed", "adb responds"),
"device": _runner_check("environment-fail", observed, "authorized Android device", "Connect or start an authorized tester device"),
"boot": _runner_check("environment-fail", "not checked", "Android device boot complete", "Provide a booted authorized device"),
})
serial = devices[0]
boot_code, boot_output = run_fixed_command(["adb", "-s", serial, "shell", "getprop", "sys.boot_completed"])
booted = boot_code == 0 and boot_output.strip() == "1"
return runner_response("ready" if booted else "environment-fail", "adb/device probe completed", checks={
"adb": _runner_check("ready", "adb responded", "adb responds"),
"device": _runner_check("ready", "authorized", "authorized Android device"),
"boot": _runner_check("ready" if booted else "environment-fail", "boot complete" if booted else "device is not booted", "Android device boot complete", "Boot the Android tester device" if not booted else ""),
})
if platform_name != "ios":
return runner_response("blocked", "unsupported platform", remediation="Use platform android or ios")
if host_platform.system() != "Darwin":
observed = "iOS runner requires a macOS tester node; xcrun was not invoked"
return runner_response("environment-fail", observed, checks={
"macos": _runner_check("environment-fail", observed, "Darwin tester node", "Route iOS tests to a Mac tester binding"),
"xcrun": _runner_check("environment-fail", "not invoked", "xcrun available", "Route iOS tests to a Mac tester binding"),
"simctl": _runner_check("environment-fail", "not invoked", "simctl available", "Route iOS tests to a Mac tester binding"),
"boot": _runner_check("environment-fail", "not checked", "booted iOS simulator", "Route iOS tests to a Mac tester binding"),
})
xcrun_code, _ = run_fixed_command(["xcrun", "--version"])
simctl_code, simctl_output = run_fixed_command(["xcrun", "simctl", "list", "devices", "--json"])
booted = False
if simctl_code == 0:
try:
data = json.loads(simctl_output)
booted = any(
isinstance(device, dict) and device.get("state") == "Booted"
for devices in (data.get("devices") or {}).values()
if isinstance(devices, list)
for device in devices
)
except (TypeError, json.JSONDecodeError):
booted = False
status = "ready" if xcrun_code == 0 and simctl_code == 0 and booted else "environment-fail"
return runner_response(status, "macOS/simctl probe completed", checks={
"macos": _runner_check("ready", "Darwin", "Darwin tester node"),
"xcrun": _runner_check("ready" if xcrun_code == 0 else "environment-fail", "available" if xcrun_code == 0 else "unavailable", "xcrun available"),
"simctl": _runner_check("ready" if simctl_code == 0 else "environment-fail", "available" if simctl_code == 0 else "unavailable", "simctl available"),
"boot": _runner_check("ready" if booted else "environment-fail", "booted simulator" if booted else "no booted simulator", "booted iOS simulator", "Boot an iOS simulator" if not booted else ""),
})
def _network_probe(request: dict[str, Any]) -> dict[str, Any]:
raw_url = request.get("url")
if not isinstance(raw_url, str) or not raw_url.strip():
return runner_response("blocked", "network_probe requires url", remediation="Provide an http(s) URL")
parsed = urllib.parse.urlsplit(raw_url)
if parsed.scheme not in {"http", "https"} or not parsed.hostname or parsed.username or parsed.password:
return runner_response("blocked", "URL must be http(s) without embedded credentials", remediation="Provide a credential-free probe URL")
try:
port = parsed.port or (443 if parsed.scheme == "https" else 80)
except ValueError:
return runner_response("blocked", "URL contains an invalid port", remediation="Provide a valid http(s) probe URL")
try:
addresses = socket.getaddrinfo(parsed.hostname, port, type=socket.SOCK_STREAM)
except (OSError, ValueError) as exc:
return runner_response("environment-fail", "DNS probe failed", checks={
"dns": _runner_check("environment-fail", str(exc), "resolvable hostname", "Check tester-node DNS/VPN")
})
checks: dict[str, Any] = {"dns": _runner_check("ready", sorted({item[4][0] for item in addresses}), "resolvable hostname")}
try:
import socket as _socket
with _socket.create_connection((parsed.hostname, port), timeout=5):
pass
checks["tcp"] = _runner_check("ready", f"{parsed.hostname}:{port}", "TCP connection")
except OSError as exc:
checks["tcp"] = _runner_check("environment-fail", str(exc), f"TCP {parsed.hostname}:{port}", "Check the tester-node route and firewall")
return runner_response("environment-fail", "TCP probe failed", checks=checks)
try:
with urllib.request.urlopen(urllib.request.Request(raw_url, method="GET"), timeout=5) as response:
code = response.status
except urllib.error.HTTPError as exc:
code = exc.code
except (OSError, urllib.error.URLError) as exc:
checks["http"] = _runner_check("environment-fail", str(exc), "HTTP response", "Check service listener, proxy and TLS")
return runner_response("environment-fail", "HTTP probe failed", checks=checks)
expected = request.get("probe", {}).get("expected_status") if isinstance(request.get("probe"), dict) else None
matches = not isinstance(expected, list) or code in expected
checks["http"] = _runner_check("ready" if 200 <= code < 500 and matches else "environment-fail", code, expected or "HTTP 2xx-4xx", "Check service health or expected_status" if not matches or code >= 500 else "")
return runner_response("ready" if checks["http"]["status"] == "ready" else "environment-fail", "DNS/TCP/HTTP probe completed", checks=checks)
def _request_artifact(request: dict[str, Any]) -> tuple[Path | None, str | None]:
value = request.get("artifact")
if not isinstance(value, str) or not value.strip():
return None, "install requires an artifact path"
path = Path(value)
if not path.is_absolute() or ".." in path.parts or path == Path("/"):
return None, "artifact must be a safe absolute path"
return path, None
def _request_flow(request: dict[str, Any]) -> tuple[str | None, str | None]:
flow = request.get("flow")
if not isinstance(flow, str) or not flow.strip():
return None, "maestro requires a non-empty flow"
if len(flow.encode("utf-8")) > 120_000:
return None, "maestro flow is too large"
env = request.get("env", {})
if env is not None:
if not isinstance(env, dict) or any(
not isinstance(key, str) or not re.fullmatch(r"[A-Z][A-Z0-9_]{0,63}", key)
or not isinstance(value, str) or len(value) > 4096
for key, value in env.items()
):
return None, "maestro env must contain safe uppercase names and text values"
return flow, None
def _maestro_env(request: dict[str, Any]) -> dict[str, str]:
values = request.get("env") if isinstance(request.get("env"), dict) else {}
return {str(key): str(value) for key, value in values.items()}
def _local_maestro(platform_name: str, request: dict[str, Any]) -> dict[str, Any]:
flow, error = _request_flow(request)
if error:
return runner_response("blocked", error, remediation="Generate a bounded Maestro flow from the catalog case")
if platform_name != "android":
return runner_response("blocked", "local Maestro runner only supports Android", remediation="Route iOS Maestro to tester.ios")
maestro = Path("/home/ace/.maestro/bin/maestro")
if not maestro.is_file():
maestro_path = shutil.which("maestro")
maestro = Path(maestro_path) if maestro_path else maestro
if not maestro.is_file():
return runner_response("environment-fail", "maestro is not installed on the local tester node", remediation="Install Maestro and rerun test-preflight")
device_code, device_output = run_fixed_command(["adb", "devices"])
serial = next((line.split("\t", 1)[0].strip() for line in device_output.splitlines() if "\tdevice" in line), "") if device_code == 0 else ""
if not serial:
return runner_response("environment-fail", "no authorized Android device for Maestro")
flow_path = Path("/tmp") / f"pouch-maestro-{uuid.uuid4().hex}.yaml"
flow_path.write_text(flow, encoding="utf-8")
try:
command = [str(maestro), "test", "--device", serial, "--no-color", str(flow_path)]
for key, value in _maestro_env(request).items():
command.extend(["-e", f"{key}={value}"])
env = os.environ.copy()
env["MAESTRO_CLI_NO_ANALYTICS"] = "true"
process = subprocess.run(command, capture_output=True, text=True, check=False, timeout=float(request.get("timeout") or 180), env=env)
output = ((process.stdout or "") + ("\n" + process.stderr if process.stderr else "")).strip()[-6000:]
return runner_response("ready", "maestro completed", checks={"maestro": _runner_check("ready", output, "Maestro flow executed")}, test_result="pass" if process.returncode == 0 else "fail", exit=process.returncode, output=output)
except subprocess.TimeoutExpired as exc:
return runner_response("ready", "maestro timed out", checks={"maestro": _runner_check("ready", "timeout", "Maestro flow executed")}, test_result="environment-fail", exit=124, output=str(exc))
finally:
flow_path.unlink(missing_ok=True)
def _local_mutation(platform_name: str, operation: str, request: dict[str, Any]) -> dict[str, Any]:
if platform_name == "ios":
return runner_response("blocked", "local iOS mutation requires an explicit Mac transport", remediation="Use a non-local tester binding with a supported node transport")
if platform_name != "android":
return runner_response("blocked", "unsupported platform", remediation="Use platform android or ios")
path, error = _request_artifact(request) if operation == "install" else (None, None)
identity = request.get("identity")
if operation == "install" and error:
return runner_response("blocked", error, remediation="Provide the caller-supplied APK path")
if not isinstance(identity, str) or not re.fullmatch(r"[A-Za-z0-9_.-]{1,192}", identity):
return runner_response("blocked", "identity must be a safe Android package id", remediation="Provide the declared artifact package")
if operation == "install" and not path.is_file():
return runner_response("blocked", "artifact does not exist", remediation="Provide an existing caller-supplied APK")
probe = _local_platform_probe("android")
if probe.get("status") != "ready":
return probe
device_code, device_output = run_fixed_command(["adb", "devices"])
serial = next((line.split("\t", 1)[0].strip() for line in device_output.splitlines() if "\tdevice" in line), "") if device_code == 0 else ""
if not serial:
return runner_response("environment-fail", "no authorized Android device")
command = ["adb", "-s", serial, "install", "-r", str(path)] if operation == "install" else ["adb", "-s", serial, "shell", "monkey", "-p", identity, "1"]
code, _ = run_fixed_command(command)
if code != 0:
return runner_response("environment-fail", f"{operation} command failed", remediation=f"Inspect the tester-node {operation} evidence")
return runner_response("ready", f"{operation} completed", checks={operation: _runner_check("ready", "completed", "operation succeeded")})
REMOTE_RUNNER_SOURCE = r'''
import base64, gzip, json, os, re, shutil, socket, subprocess, sys, urllib.error, urllib.parse, urllib.request, uuid, zipfile
from pathlib import Path
request_bytes = base64.b64decode(sys.stdin.read())
try:
request = json.loads(request_bytes.decode("utf-8"))
except UnicodeDecodeError:
request = json.loads(gzip.decompress(request_bytes).decode("utf-8"))
marker = request.pop("_marker")
def check(status, observed, expected, remediation=""):
item = {"status": status, "observed": observed, "expected": expected}
if remediation:
item["remediation"] = remediation
return item
def run(argv, env=None, timeout=180):
try:
proc = subprocess.run(argv, env=env, timeout=timeout, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, check=False)
except subprocess.TimeoutExpired as exc:
return 124, (str(exc) or "command timed out").strip()
except OSError as exc:
return 127, str(exc).strip()
return proc.returncode, (proc.stdout or "").strip()
def response(status, observed, checks=None, remediation=""):
item = {"status": status, "observed": observed}
if checks:
item["checks"] = checks
if remediation:
item["remediation"] = remediation
return item
def ios_platform():
uname_code, uname_output = run(["uname", "-s"])
darwin = uname_code == 0 and uname_output.splitlines()[-1:] == ["Darwin"]
checks = {"macos": check("ready" if darwin else "environment-fail", uname_output or "unavailable", "Darwin tester node", "Route iOS tests to the Mac tester node" if not darwin else "")}
if not darwin:
checks.update({
"xcrun": check("environment-fail", "not invoked", "xcrun available", "Use a macOS tester node"),
"simctl": check("environment-fail", "not invoked", "simctl available", "Use a macOS tester node"),
"boot": check("environment-fail", "not checked", "booted iOS simulator", "Use a macOS tester node"),
})
return response("environment-fail", "remote node is not macOS", checks)
xcrun_code, xcrun_output = run(["xcrun", "--version"])
simctl_code, simctl_output = run(["xcrun", "simctl", "list", "devices", "--json"])
booted = False
if simctl_code == 0:
try:
data = json.loads(simctl_output)
booted = any(
isinstance(device, dict) and device.get("state") == "Booted"
for devices in (data.get("devices") or {}).values()
if isinstance(devices, list)
for device in devices
)
except (TypeError, json.JSONDecodeError):
booted = False
checks.update({
"xcrun": check("ready" if xcrun_code == 0 else "environment-fail", xcrun_output or "unavailable", "xcrun available", "Install/select Xcode command-line tools" if xcrun_code else ""),
"simctl": check("ready" if simctl_code == 0 else "environment-fail", "available" if simctl_code == 0 else simctl_output, "simctl available", "Check Xcode simulator runtime" if simctl_code else ""),
"boot": check("ready" if booted else "environment-fail", "booted simulator" if booted else "no booted simulator", "booted iOS simulator", "Boot an iOS simulator" if not booted else ""),
})
status = "ready" if all(item["status"] == "ready" for item in checks.values()) else "environment-fail"
return response(status, "macOS/xcrun/simctl probe completed", checks)
def network_probe():
raw_url = request.get("url")
parsed = urllib.parse.urlsplit(raw_url or "")
if parsed.scheme not in {"http", "https"} or not parsed.hostname or parsed.username or parsed.password:
return response("blocked", "URL must be http(s) without embedded credentials", remediation="Provide a credential-free probe URL")
port = parsed.port or (443 if parsed.scheme == "https" else 80)
try:
addresses = socket.getaddrinfo(parsed.hostname, port, type=socket.SOCK_STREAM)
except (OSError, ValueError) as exc:
return response("environment-fail", "DNS probe failed", {"dns": check("environment-fail", str(exc), "resolvable hostname", "Check tester-node DNS/VPN")})
checks = {"dns": check("ready", sorted({item[4][0] for item in addresses}), "resolvable hostname")}
try:
with socket.create_connection((parsed.hostname, port), timeout=5):
pass
checks["tcp"] = check("ready", f"{parsed.hostname}:{port}", "TCP connection")
except OSError as exc:
checks["tcp"] = check("environment-fail", str(exc), f"TCP {parsed.hostname}:{port}", "Check tester-node route and firewall")
return response("environment-fail", "TCP probe failed", checks)
try:
with urllib.request.urlopen(urllib.request.Request(raw_url, method="GET"), timeout=5) as result:
code = result.status
except urllib.error.HTTPError as exc:
code = exc.code
except (OSError, urllib.error.URLError) as exc:
checks["http"] = check("environment-fail", str(exc), "HTTP response", "Check service listener, proxy and TLS")
return response("environment-fail", "HTTP probe failed", checks)
expected = request.get("probe", {}).get("expected_status") if isinstance(request.get("probe"), dict) else None
matches = not isinstance(expected, list) or code in expected
checks["http"] = check("ready" if 200 <= code < 500 and matches else "environment-fail", code, expected or "HTTP 2xx-4xx", "Check service health or expected_status" if not matches or code >= 500 else "")
return response("ready" if checks["http"]["status"] == "ready" else "environment-fail", "DNS/TCP/HTTP probe completed", checks)
operation = request.get("operation")
if operation == "platform_probe" and request.get("platform") == "ios":
result = ios_platform()
elif operation == "network_probe":
result = network_probe()
elif operation == "maestro" and request.get("platform") == "ios":
flow = request.get("flow")
values = request.get("env") if isinstance(request.get("env"), dict) else {}
if not isinstance(flow, str) or not flow.strip() or len(flow.encode("utf-8")) > 120000:
result = response("blocked", "maestro requires a bounded non-empty flow")
elif any(not isinstance(key, str) or not key.isidentifier() or not key.isupper() or not isinstance(value, str) or len(value) > 4096 for key, value in values.items()):
result = response("blocked", "maestro env must contain uppercase text values")
else:
flow_path = Path("/tmp") / ("pouch-maestro-" + uuid.uuid4().hex + ".yaml")
try:
flow_path.write_text(flow, encoding="utf-8")
env = os.environ.copy()
env["PATH"] = "/Users/laily/bin:/Users/laily/.maestro/bin:/opt/homebrew/bin:" + env.get("PATH", "")
env["JAVA_HOME"] = "/opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home"
env["MAESTRO_CLI_NO_ANALYTICS"] = "true"
device = "booted"
sim_code, sim_output = run(["xcrun", "simctl", "list", "devices", "--json"], env=env, timeout=30)
if sim_code == 0:
try:
sim_data = json.loads(sim_output)
booted = [
item.get("udid") for devices in (sim_data.get("devices") or {}).values()
if isinstance(devices, list)
for item in devices
if isinstance(item, dict) and item.get("state") == "Booted" and item.get("udid")
]
if booted:
device = str(booted[0])
# Maestro's iOS driver uses IDB for simulator I/O. The
# companion is deliberately connected through the user
# local path; Homebrew cannot create /usr/local/bin here.
run(["idb", "connect", device, "--json"], env=env, timeout=20)
except (TypeError, json.JSONDecodeError):
pass
command = ["/Users/laily/.maestro/bin/maestro", "test", "--device", device, "--no-color", str(flow_path)]
for key, value in values.items():
command.extend(["-e", key + "=" + value])
code, output = run(command, env=env, timeout=float(request.get("timeout") or 180))
result = response("ready" if code not in {124, 127} else "environment-fail", "maestro completed" if code == 0 else ("maestro command unavailable" if code == 127 else "maestro timed out"), {"maestro": check("ready" if code == 0 else ("environment-fail" if code in {124, 127} else "ready"), output[-6000:], "Maestro flow executed")})
result.update({"test_result": "pass" if code == 0 else ("environment-fail" if code in {124, 127} else "fail"), "exit": code, "output": output[-6000:]})
finally:
try:
flow_path.unlink()
except OSError:
pass
elif operation == "maestro_batch" and request.get("platform") == "ios":
cases = request.get("cases")
if not isinstance(cases, list) or not cases or len(cases) > 64:
result = response("blocked", "maestro_batch requires 1-64 cases")
else:
values = request.get("env", {})
if not isinstance(values, dict) or any(not isinstance(key, str) or not key.isidentifier() or not key.isupper() or not isinstance(value, str) or len(value) > 4096 for key, value in values.items()):
result = response("blocked", "maestro env must contain uppercase text values")
else:
env = os.environ.copy()
env["PATH"] = "/Users/laily/bin:/Users/laily/.maestro/bin:/opt/homebrew/bin:" + env.get("PATH", "")
env["JAVA_HOME"] = "/opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home"
env["MAESTRO_CLI_NO_ANALYTICS"] = "true"
device = "booted"
sim_code, sim_output = run(["xcrun", "simctl", "list", "devices", "--json"], env=env, timeout=30)
if sim_code == 0:
try:
sim_data = json.loads(sim_output)
booted = [
item.get("udid") for devices in (sim_data.get("devices") or {}).values()
if isinstance(devices, list)
for item in devices
if isinstance(item, dict) and item.get("state") == "Booted" and item.get("udid")
]
if booted:
device = str(booted[0])
run(["idb", "connect", device, "--json"], env=env, timeout=20)
except (TypeError, json.JSONDecodeError):
pass
results = []
for item in cases:
case_id = item.get("id") if isinstance(item, dict) else None
flow = item.get("flow") if isinstance(item, dict) else None
case_env = item.get("env", values) if isinstance(item, dict) else values
if not isinstance(case_id, str) or not case_id or not isinstance(flow, str) or not flow.strip() or len(flow.encode("utf-8")) > 120000:
results.append({"id": str(case_id or ""), "test_result": "environment-fail", "exit": 2, "output": "invalid maestro batch case"})
continue
if not isinstance(case_env, dict) or any(not isinstance(key, str) or not key.isidentifier() or not key.isupper() or not isinstance(value, str) or len(value) > 4096 for key, value in case_env.items()):
results.append({"id": case_id, "test_result": "environment-fail", "exit": 2, "output": "invalid maestro batch env"})
continue
flow_path = Path("/tmp") / ("pouch-maestro-batch-" + uuid.uuid4().hex + ".yaml")
try:
flow_path.write_text(flow, encoding="utf-8")
command = ["/Users/laily/.maestro/bin/maestro", "test", "--device", device, "--no-color", str(flow_path)]
for key, value in case_env.items():
command.extend(["-e", key + "=" + value])
code, output = run(command, env=env, timeout=float(request.get("case_timeout") or 180))
test_result = "pass" if code == 0 else ("environment-fail" if code in {124, 127} else "fail")
results.append({"id": case_id, "test_result": test_result, "exit": code, "output": output[-6000:]})
except Exception as exc:
results.append({"id": case_id, "test_result": "environment-fail", "exit": 124, "output": str(exc)})
finally:
try:
flow_path.unlink()
except OSError:
pass
has_env_failure = any(item.get("test_result") == "environment-fail" for item in results)
result = response("environment-fail" if has_env_failure else "ready", "maestro batch completed", {"maestro": check("environment-fail" if has_env_failure else "ready", f"{len(results)} cases", "Maestro batch executed")})
result.update({"results": results})
elif operation == "artifact_download":
url = request.get("url")
destination = request.get("destination")
if not isinstance(url, str) or not url.startswith("https://git.duguying.net/"):
result = response("blocked", "artifact_download only accepts the configured Gitea host")
elif not isinstance(destination, str) or not destination.startswith("/Users/laily/orca/workspaces/app/"):
result = response("blocked", "artifact destination must stay under the Mac tester artifact root")
else:
dest = Path(destination)
archive = dest.with_suffix(".artifact.zip")
try:
config_paths = [
Path.home() / ".config" / "tea" / "config.yml",
Path("/Users/duguying/.config/tea/config.yml"),
]
config_path = next((item for item in config_paths if item.is_file()), None)
if config_path is None:
raise RuntimeError("Mac mini tea login for git.duguying.net is missing")
config_text = config_path.read_text(encoding="utf-8")
token_match = re.search(r"name:\s*git\.duguying\.net[\s\S]*?token:\s*([^\s]+)", config_text)
token = token_match.group(1) if token_match else ""
if not token:
raise RuntimeError(f"Mac mini tea login for git.duguying.net is missing in {config_path}")
request_obj = urllib.request.Request(url, headers={"Authorization": f"token {token}", "User-Agent": "pouch-tester/1"})
dest.parent.mkdir(parents=True, exist_ok=True)
with urllib.request.urlopen(request_obj, timeout=120) as source, archive.open("wb") as target:
shutil.copyfileobj(source, target)
with zipfile.ZipFile(archive) as artifact_archive:
nested = next((name for name in artifact_archive.namelist() if name.endswith("ios-simulator.zip")), None)
if not nested:
raise RuntimeError("Gitea artifact does not contain ios-simulator.zip")
nested_bytes = artifact_archive.read(nested)
nested_archive = dest.with_suffix(".ios-simulator.zip")
nested_archive.write_bytes(nested_bytes)
with zipfile.ZipFile(nested_archive) as app_archive:
app_archive.extractall(dest.parent)
app = dest.parent / "Writes.app"
if not app.is_dir() or not (app / "Info.plist").is_file():
raise RuntimeError("downloaded iOS artifact does not contain Writes.app/Info.plist")
result = response("ready", "iOS simulator artifact downloaded", {"artifact": check("ready", str(app), "remote artifact present")})
except Exception as exc:
result = response("environment-fail", "iOS simulator artifact download failed", remediation=str(exc))
elif operation in {"install", "start"} and request.get("platform") == "ios":
identity = request.get("identity")
artifact = request.get("artifact")
if operation == "install":
if not isinstance(artifact, str) or not (Path(artifact).is_file() or Path(artifact).is_dir()):
result = response("environment-fail", "remote iOS artifact is not present", remediation="Provide artifact.remote_path on the Mac tester node")
else:
sim_code, sim_output = run(["xcrun", "simctl", "list", "devices", "--json"])
device = "booted"
if sim_code == 0:
try:
sim_data = json.loads(sim_output)
booted = [
item.get("udid") for devices in (sim_data.get("devices") or {}).values()
if isinstance(devices, list)
for item in devices
if isinstance(item, dict) and item.get("state") == "Booted" and item.get("udid")
]
if booted:
device = str(booted[0])
except (TypeError, json.JSONDecodeError):
pass
code, output = run(["xcrun", "simctl", "install", device, artifact])
result = response("ready" if code == 0 else "environment-fail", "iOS app installed" if code == 0 else output, {"install": check("ready" if code == 0 else "environment-fail", output or "installed", "simctl install succeeds", "Check IPA and simulator" if code else "")})
elif not isinstance(identity, str) or not identity:
result = response("blocked", "start requires bundle id")
else:
code, output = run(["xcrun", "simctl", "launch", "booted", identity])
result = response("ready" if code == 0 else "environment-fail", "iOS app started" if code == 0 else output, {"start": check("ready" if code == 0 else "environment-fail", output or "started", "simctl launch succeeds", "Check bundle id and installed app" if code else "")})
else:
result = response("blocked", "unsupported remote runner operation", remediation="Use iOS platform_probe, network_probe, install, or start")
print(marker + base64.b64encode(json.dumps(result, separators=(",", ":")).encode("utf-8")).decode("ascii"))
'''
def _terminal_output(result: dict[str, Any]) -> str:
terminal = result.get("terminal") if isinstance(result, dict) else None
if not isinstance(terminal, dict):
terminal = result
tail = terminal.get("tail") if isinstance(terminal, dict) else None
if isinstance(tail, list):
return "\n".join(str(item) for item in tail)
if isinstance(tail, str):
return tail
output = terminal.get("output") if isinstance(terminal, dict) else None
return output if isinstance(output, str) else ""
def remote_runner_transport(binding_value: dict[str, Any], request: dict[str, Any], worktree: dict[str, Any]) -> dict[str, Any]:
environment = str(binding_value["location"]["environment"])
marker = "POUCH_RUNNER_RESULT_" + uuid.uuid4().hex
source = REMOTE_RUNNER_SOURCE
source_b64 = base64.b64encode(gzip.compress(source.encode("utf-8"), mtime=0)).decode("ascii")
payload = dict(request)
payload["_marker"] = marker
payload_b64 = base64.b64encode(gzip.compress(json.dumps(payload, ensure_ascii=False, separators=(",", ":")).encode("utf-8"), mtime=0)).decode("ascii")
handle = ""
try:
def send_text(text: str) -> None:
# Keep each PTY write below the provider's line-editor limit, then
# submit it explicitly. Long base64 lines are accepted as input
# but may remain unsubmitted in fish.
orca_json(["terminal", "send", "--terminal", handle, "--text", text], environment)
orca_json(["terminal", "send", "--terminal", handle, "--text", "\r"], environment)
# The remote PTY acknowledges accepted bytes before fish has
# drained its input buffer. Leave room between chunks so no
# base64 data is dropped on slower Mac sessions.
time.sleep(0.2)
created = orca_json(["terminal", "create", "--worktree", f"id:{worktree['id']}", "--title", "tester-node-runner"], environment)
terminal = created.get("terminal") if isinstance(created, dict) else None
handle = str(terminal.get("handle")) if isinstance(terminal, dict) and terminal.get("handle") else ""
if not handle:
return runner_response("environment-fail", "remote runner terminal was not created", remediation="Check the Orca macmini terminal service")
remote_base = f"/tmp/pouch-remote-runner-{uuid.uuid4().hex}"
source_file = remote_base + ".b64"
payload_file = remote_base + ".payload.b64"
send_text(f": > {source_file}")
for offset in range(0, len(source_b64), 400):
send_text(f"printf '%s' '{source_b64[offset:offset + 400]}' >> {source_file}")
send_text(
"python3 -c 'import base64,gzip;open(\"%s.py\",\"wb\").write(gzip.decompress(base64.b64decode(open(\"%s\",\"rb\").read())))'"
% (remote_base, source_file)
)
send_text(f": > {payload_file}")
for offset in range(0, len(payload_b64), 400):
send_text(f"printf '%s' '{payload_b64[offset:offset + 400]}' >> {payload_file}")
# The embedded runner accepts base64(gzip(JSON)) on stdin directly.
send_text(f"python3 {remote_base}.py < {payload_file}")
runner_wait_ms = min(
900000,
max(15000, int(float(request.get("timeout") or request.get("case_timeout") or 180) * 1000) + 15000),
)
output = ""
deadline = time.monotonic() + runner_wait_ms / 1000
while time.monotonic() < deadline:
output = _terminal_output(orca_json(["terminal", "read", "--terminal", handle, "--limit", "20000"], environment))
for line in reversed(output.splitlines()):
if line.startswith(marker):
try:
result = json.loads(base64.b64decode(line[len(marker):]).decode("utf-8"))
except (ValueError, json.JSONDecodeError):
break
if isinstance(result, dict):
result.setdefault("checks", {})
result["checks"].setdefault("binding", _runner_check("ready", binding_value["id"], "tester binding resolved"))
result["checks"].setdefault("worktree", _runner_check("ready", worktree.get("path", "resolved"), "exact Orca worktree"))
return result
time.sleep(1)
return runner_response("environment-fail", "remote runner did not return a protocol result", checks={
"binding": _runner_check("ready", binding_value["id"], "tester binding resolved"),
"worktree": _runner_check("ready", worktree.get("path", "resolved"), "exact Orca worktree"),
"transport": _runner_check("environment-fail", output[-1000:], "node runner protocol result", "Inspect the macmini terminal output"),
})
except (ConfigError, OSError) as exc:
return runner_response("environment-fail", "remote runner transport failed", remediation=str(exc))
finally:
if handle:
try:
orca_json(["terminal", "close", "--terminal", handle], environment)
except ConfigError:
pass
def execute_runner_request(root: Path, request: Any) -> dict[str, Any]:
if not isinstance(request, dict):
return runner_response("blocked", "runner request must be a JSON object", remediation="Send one JSON object on stdin")
binding_id = request.get("binding")
operation = request.get("operation")
platform_name = request.get("platform")
if not isinstance(binding_id, str) or not BINDING_RE.fullmatch(binding_id):
return runner_response("blocked", "binding must be a safe namespace.name", remediation="Use the resolved tester.<name> binding")
if not isinstance(operation, str) or operation not in RUNNER_OPERATIONS:
return runner_response("blocked", "unknown runner operation", remediation="Use platform_probe, network_probe, install, or start")
if not isinstance(platform_name, str) or platform_name not in RUNNER_PLATFORMS:
return runner_response("blocked", "platform must be android or ios", remediation="Use the matrix platform")
try:
value = binding(load_config(root), binding_id)
except (ConfigError, OSError) as exc:
return runner_response("blocked", "tester binding could not be resolved", remediation=str(exc))
if value.get("adapter") != "tester":
return runner_response("blocked", "binding is not a tester binding", remediation="Use a binding with adapter: tester")
location = value["location"]
environment = str(location.get("environment"))
requested_node = request.get("node")
if requested_node is not None and (not isinstance(requested_node, str) or not requested_node.strip()):
return runner_response("blocked", "node must be non-empty text when provided")
if environment == "local":
if requested_node and requested_node not in LOCAL_NODE_NAMES:
return runner_response("blocked", "local binding does not match requested tester node", remediation="Use node: local or a local alias")
if operation == "platform_probe":
return _local_platform_probe(platform_name)
if operation == "network_probe":
return _network_probe(request)
if operation == "maestro":
return _local_maestro(platform_name, request)
return _local_mutation(platform_name, operation, request)
try:
worktree = worktree_for(value)
except ConfigError as exc:
return runner_response("blocked", "remote tester worktree could not be resolved", checks={
"binding": _runner_check("ready", binding_id, "tester binding resolved"),
"worktree": _runner_check("blocked", "unresolved", "exact Orca worktree", str(exc)),
}, remediation="Register one exact environment/host/project/path worktree before using the remote tester")
return remote_runner_transport(value, request, worktree)
def cmd_run_exec(args: argparse.Namespace) -> int:
root = Path(args.root).resolve()
try:
request = json.load(sys.stdin)
except (json.JSONDecodeError, OSError):
response = runner_response("blocked", "runner stdin is not one valid JSON object", remediation="Send exactly one JSON object on stdin")
else:
try:
response = execute_runner_request(root, request)
except Exception:
print("agent-runtime runner: unexpected adapter failure", file=sys.stderr)
response = runner_response("environment-fail", "runner adapter failed before producing evidence", remediation="Inspect the tester binding and adapter environment")
print(json.dumps(response, ensure_ascii=False, separators=(",", ":")))
return 0
def cmd_run_start(args: argparse.Namespace) -> int:
root = Path(args.root).resolve()
value = binding(load_config(root), args.binding)
if value["adapter"] != "builder":
raise ConfigError(f"run start 目前只为 builder 提供受控 make session;{value['adapter']} 请走对应 skill adapter")
if value.get("session", {}).get("lifecycle", "ephemeral") != "persistent":
raise ConfigError(f"binding {args.binding} 不是 persistent session")
worktree = worktree_for(value)
environment = str(value["location"]["environment"])
result = orca_json(["terminal", "create", "--worktree", f"id:{worktree['id']}", "--command", make_command(value), "--title", args.binding], environment)
terminal = result.get("terminal") or result
handle = terminal.get("handle") or result.get("handle")
if not handle:
raise ConfigError("Orca terminal create 没有返回 handle")
run_id = "run_" + uuid.uuid4().hex
record = {
"schemaVersion": 1,
"id": run_id,
"binding": args.binding,
"adapter": value["adapter"],
"profileId": value["profileId"],
"profileHash": hashlib.sha256(json.dumps(value["profile"], sort_keys=True).encode()).hexdigest(),
"environment": environment,
"project": value["location"]["project"],
"path": value["location"]["path"],
"worktreeId": worktree["id"],
"terminalHandle": handle,
"command": make_command(value),
"sourceCommit": project_root_commit(root),
"createdAt": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
"status": "started",
}
run_path(root, run_id).write_text(json.dumps(record, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
print(json.dumps(record, ensure_ascii=False, indent=2))
if args.wait_ms:
wait = orca_json(["terminal", "wait", "--terminal", handle, "--for", "exit", "--timeout-ms", str(args.wait_ms)], environment)
record["wait"] = wait
record["status"] = "completed" if wait.get("exited") or wait.get("status") == "exited" else "waited"
run_path(root, run_id).write_text(json.dumps(record, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
print(json.dumps(wait, ensure_ascii=False, indent=2))
return 0
def load_run(root: Path, run_id: str) -> dict[str, Any]:
path = run_path(root, run_id)
if not path.is_file():
raise ConfigError(f"找不到 run 记录: {run_id}")
value = json.loads(path.read_text(encoding="utf-8"))
if not isinstance(value, dict):
raise ConfigError(f"run 记录无效: {run_id}")
return value
def cmd_run_status(args: argparse.Namespace) -> int:
root = Path(args.root).resolve()
record = load_run(root, args.run)
result = orca_json(["terminal", "show", "--terminal", record["terminalHandle"]], record.get("environment"))
print(json.dumps({"run": record, "terminal": result}, ensure_ascii=False, indent=2))
return 0
def cmd_run_attach(args: argparse.Namespace) -> int:
root = Path(args.root).resolve()
record = load_run(root, args.run)
result = orca_json(["terminal", "read", "--terminal", record["terminalHandle"]], record.get("environment"))
print(json.dumps(result, ensure_ascii=False, indent=2))
return 0
def cmd_run_resume(args: argparse.Namespace) -> int:
"""Explicitly reattach to a recorded terminal; never search for another one."""
root = Path(args.root).resolve()
record = load_run(root, args.run)
result = orca_json(["terminal", "show", "--terminal", record["terminalHandle"]], record.get("environment"))
print(json.dumps({"resumed": True, "run": record, "terminal": result}, ensure_ascii=False, indent=2))
return 0
def cmd_run_stop(args: argparse.Namespace) -> int:
root = Path(args.root).resolve()
record = load_run(root, args.run)
result = orca_json(["terminal", "close", "--terminal", record["terminalHandle"]], record.get("environment"))
record["status"] = "stopped"
record["stoppedAt"] = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
run_path(root, args.run).write_text(json.dumps(record, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
print(json.dumps({"run": record, "close": result}, ensure_ascii=False, indent=2))
return 0
def parser() -> argparse.ArgumentParser:
root_parent = argparse.ArgumentParser(add_help=False)
root_parent.add_argument("--root", default=".")
parser = argparse.ArgumentParser(description="共享 Agent 配置、worktree 和 session 运行时")
sub = parser.add_subparsers(dest="command", required=True)
validate_parser = sub.add_parser("validate", parents=[root_parent])
validate_parser.set_defaults(func=cmd_validate)
show = sub.add_parser("show", parents=[root_parent])
show.add_argument("--binding", required=True)
show.set_defaults(func=cmd_show)
wt = sub.add_parser("worktree")
wt_sub = wt.add_subparsers(dest="worktree_command", required=True)
resolve = wt_sub.add_parser("resolve", parents=[root_parent])
resolve.add_argument("--binding", required=True)
resolve.set_defaults(func=cmd_worktree_resolve)
create = wt_sub.add_parser("create", parents=[root_parent])
create.add_argument("--binding", required=True)
create.add_argument("--name", required=True)
create.add_argument("--base", default="")
create.set_defaults(func=cmd_worktree_create)
run = sub.add_parser("run")
run_sub = run.add_subparsers(dest="run_command", required=True)
start = run_sub.add_parser("start", parents=[root_parent])
start.add_argument("--binding", required=True)
start.add_argument("--wait-ms", type=int, default=0)
start.set_defaults(func=cmd_run_start)
execute = run_sub.add_parser("exec", parents=[root_parent])
execute.set_defaults(func=cmd_run_exec)
for name, func in (("status", cmd_run_status), ("attach", cmd_run_attach), ("resume", cmd_run_resume), ("stop", cmd_run_stop)):
item = run_sub.add_parser(name, parents=[root_parent])
item.add_argument("--run", required=True)
item.set_defaults(func=func)
return parser
if __name__ == "__main__":
try:
parsed = parser().parse_args()
raise SystemExit(parsed.func(parsed))
except ConfigError as exc:
print(f"environment-fail: {exc}", file=sys.stderr)
raise SystemExit(2)