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.
This commit is contained in:
2026-10-19 09:28:50 +08:00
parent 9075ff6a5a
commit adaa8e4edd
9 changed files with 1179 additions and 11 deletions
+705 -7
View File
@@ -8,15 +8,24 @@ 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
@@ -38,6 +47,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):
@@ -223,7 +235,17 @@ def orca_json(args: list[str], environment: str | None = None) -> dict[str, Any]
if environment and environment != "local":
command.extend(["--environment", environment])
command.append("--json")
proc = subprocess.run(command, capture_output=True, text=True, check=False)
# 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)
@@ -282,12 +304,19 @@ def worktree_for(binding_value: dict[str, Any]) -> dict[str, Any]:
if location["environment"] == "local" and expected_host == "local" and str(item.get("hostId")) != "local":
continue
candidates.append(item)
if len(candidates) != 1:
if not candidates:
raise ConfigError(
f"找不到精确 worktree: environment={environment}, host={location_host(location)}, "
f"project={location['project']}, path={location['path']};先创建或登记该 worktree"
)
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]
@@ -358,6 +387,673 @@ def make_command(value: dict[str, Any]) -> str:
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)
@@ -474,6 +1170,8 @@ def parser() -> argparse.ArgumentParser:
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)