1676 lines
58 KiB
Python
Executable File
1676 lines
58 KiB
Python
Executable File
#!/usr/bin/env python3
|
||
"""Launch an ACK worker from a validated, structured profile.
|
||
|
||
Public commands:
|
||
profile-hash Print the canonical hash for one configured profile.
|
||
plan Validate and print the exact launch plan without creating a terminal.
|
||
launch Create an Orca terminal through a fixed bootstrap command.
|
||
|
||
The private ``_bootstrap`` command accepts only an opaque hexadecimal launch ID.
|
||
It re-reads the authoritative task board, revalidates the profile and worktree,
|
||
then starts the Agent CLI with an argv array and ``shell=False``.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import fcntl
|
||
import hashlib
|
||
import hmac
|
||
import json
|
||
import os
|
||
import pwd
|
||
import re
|
||
import secrets
|
||
import select
|
||
import shlex
|
||
import stat
|
||
import subprocess
|
||
import sys
|
||
import time
|
||
from contextlib import contextmanager
|
||
from datetime import datetime, timedelta, timezone
|
||
from pathlib import Path
|
||
from typing import Iterator
|
||
|
||
SCRIPT_DIR = Path(__file__).resolve().parent
|
||
if str(SCRIPT_DIR) not in sys.path:
|
||
sys.path.insert(0, str(SCRIPT_DIR))
|
||
|
||
from validate_tasks import load_document, validate_builtin # noqa: E402
|
||
from worker_profiles import ( # noqa: E402
|
||
GROK_EXECUTABLE_NAME_RE,
|
||
LAUNCH_PROTOCOL_VERSION,
|
||
canonical_sha256,
|
||
executable_basename_matches_cli,
|
||
profile_hash,
|
||
render_worker_argv,
|
||
validate_routing_document,
|
||
)
|
||
|
||
PROTOCOL_VERSION = LAUNCH_PROTOCOL_VERSION
|
||
RECEIPT_VERSION = 1
|
||
ENVIRONMENT_POLICY = "per-cli-allowlist-v1"
|
||
TASKS_RELATIVE_PATH = Path("docs/ack/tasks.yaml")
|
||
MAX_CONTROL_OUTPUT = 1024 * 1024
|
||
MAX_RECORD_SIZE = 256 * 1024
|
||
LAUNCH_TTL_SECONDS = 120
|
||
BOOTSTRAP_READY_TIMEOUT_SECONDS = 10
|
||
CONTROL_TIMEOUT_SECONDS = 30
|
||
LAUNCH_ID_RE = re.compile(r"^[0-9a-f]{64}$")
|
||
PROFILE_ID_RE = re.compile(r"^[a-z][a-z0-9-]{1,63}$")
|
||
TASK_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$")
|
||
SHA256_RE = re.compile(r"^sha256:[0-9a-f]{64}$")
|
||
|
||
CONTROL_ENVIRONMENT_NAMES = {
|
||
"COLORTERM",
|
||
"LANG",
|
||
"LC_ALL",
|
||
"NO_COLOR",
|
||
"TERM",
|
||
"TZ",
|
||
}
|
||
WORKER_ENVIRONMENT_NAMES = CONTROL_ENVIRONMENT_NAMES | {
|
||
"HTTPS_PROXY",
|
||
"HTTP_PROXY",
|
||
"SSL_CERT_DIR",
|
||
"SSL_CERT_FILE",
|
||
"https_proxy",
|
||
"http_proxy",
|
||
"no_proxy",
|
||
"NO_PROXY",
|
||
}
|
||
WORKER_CREDENTIAL_NAMES = {
|
||
"codex": frozenset({"AZURE_OPENAI_API_KEY", "OPENAI_API_KEY"}),
|
||
"cursor-agent": frozenset({"CURSOR_API_KEY"}),
|
||
"grok": frozenset({"XAI_API_KEY"}),
|
||
"omp": frozenset({"OPENCODE_API_KEY"}),
|
||
}
|
||
CLI_TITLE_LABELS = {
|
||
"codex": "CODEX",
|
||
"cursor-agent": "CURSOR",
|
||
"grok": "GROK",
|
||
"omp": "OMP",
|
||
}
|
||
INHERITED_ENVIRONMENT_PREFIXES = (
|
||
"LC_",
|
||
)
|
||
|
||
|
||
class LaunchError(RuntimeError):
|
||
"""A deterministic launch validation or runtime error."""
|
||
|
||
|
||
class IndeterminateLaunch(LaunchError):
|
||
"""Orca may have created a terminal, but no safe identity was obtained."""
|
||
|
||
|
||
def utc_now() -> datetime:
|
||
return datetime.now(timezone.utc)
|
||
|
||
|
||
def format_timestamp(value: datetime) -> str:
|
||
return value.astimezone(timezone.utc).isoformat().replace("+00:00", "Z")
|
||
|
||
|
||
def parse_timestamp(value: object, label: str) -> datetime:
|
||
if not isinstance(value, str) or not value:
|
||
raise LaunchError(f"{label} 必须是 RFC3339 时间字符串")
|
||
normalized = value[:-1] + "+00:00" if value.endswith("Z") else value
|
||
try:
|
||
parsed = datetime.fromisoformat(normalized)
|
||
except ValueError as exc:
|
||
raise LaunchError(f"{label} 不是合法 RFC3339 时间") from exc
|
||
if parsed.tzinfo is None:
|
||
raise LaunchError(f"{label} 必须包含时区")
|
||
return parsed.astimezone(timezone.utc)
|
||
|
||
|
||
def safe_identity_text(value: object, label: str, *, max_length: int = 512) -> str:
|
||
if (
|
||
not isinstance(value, str)
|
||
or not value.strip()
|
||
or value != value.strip()
|
||
or len(value) > max_length
|
||
or any(character in value for character in ("\0", "\n", "\r"))
|
||
):
|
||
raise LaunchError(f"{label} 必须是安全的单行非空字符串")
|
||
return value
|
||
|
||
|
||
def account_identity() -> tuple[Path, str]:
|
||
account = pwd.getpwuid(os.getuid())
|
||
home = Path(account.pw_dir).resolve(strict=True)
|
||
if not home.is_dir():
|
||
raise LaunchError("当前用户 home 不是可访问目录")
|
||
return home, account.pw_name
|
||
|
||
|
||
def trusted_path_entries() -> list[Path]:
|
||
home, _ = account_identity()
|
||
candidates = [
|
||
home / ".local" / "bin",
|
||
home / ".local" / "share" / "mise" / "shims",
|
||
home / ".local" / "share" / "mise" / "installs" / "github-can1357-oh-my-pi" / "latest",
|
||
home / ".cargo" / "bin",
|
||
Path("/home/linuxbrew/.linuxbrew/bin"),
|
||
Path("/usr/local/go/bin"),
|
||
Path("/usr/local/bin"),
|
||
Path("/usr/bin"),
|
||
Path("/bin"),
|
||
]
|
||
result: list[Path] = []
|
||
for candidate in candidates:
|
||
try:
|
||
resolved = candidate.resolve(strict=True)
|
||
except OSError:
|
||
continue
|
||
if resolved.is_dir() and resolved not in result:
|
||
result.append(resolved)
|
||
return result
|
||
|
||
|
||
def _sanitized_environment(allowed_names: frozenset[str] | set[str]) -> dict[str, str]:
|
||
"""Build an environment from exact names plus locale categories."""
|
||
|
||
home, username = account_identity()
|
||
result = {
|
||
"HOME": str(home),
|
||
"LOGNAME": username,
|
||
"PATH": os.pathsep.join(str(path) for path in trusted_path_entries()),
|
||
"USER": username,
|
||
}
|
||
for name, value in os.environ.items():
|
||
if name in allowed_names or any(
|
||
name.startswith(prefix) for prefix in INHERITED_ENVIRONMENT_PREFIXES
|
||
):
|
||
if "\0" not in value:
|
||
result[name] = value
|
||
return result
|
||
|
||
|
||
def control_environment() -> dict[str, str]:
|
||
"""Return a credential-free environment for Git, Orca, and CLI probes."""
|
||
|
||
return _sanitized_environment(CONTROL_ENVIRONMENT_NAMES)
|
||
|
||
|
||
def worker_environment(cli: str) -> dict[str, str]:
|
||
"""Return only the supported CLI's own credentials and common runtime data."""
|
||
|
||
credential_names = WORKER_CREDENTIAL_NAMES.get(cli)
|
||
if credential_names is None:
|
||
raise LaunchError(f"不支持的 worker CLI 环境: {cli}")
|
||
return _sanitized_environment(WORKER_ENVIRONMENT_NAMES | credential_names)
|
||
|
||
|
||
def reject_duplicate_or_separator_args(argv: list[str]) -> None:
|
||
seen: set[str] = set()
|
||
for token in argv:
|
||
if token == "--":
|
||
raise LaunchError("不接受 -- 分隔符或额外位置参数")
|
||
if not token.startswith("--"):
|
||
continue
|
||
name = token.split("=", 1)[0]
|
||
if name in seen:
|
||
raise LaunchError(f"命令行参数不能重复: {name}")
|
||
seen.add(name)
|
||
|
||
|
||
def _path_has_parent_reference(value: str) -> bool:
|
||
return ".." in Path(value).parts
|
||
|
||
|
||
def _assert_no_symlink_components(path: Path, label: str) -> None:
|
||
if not path.is_absolute():
|
||
raise LaunchError(f"{label}必须是绝对路径: {path}")
|
||
current = Path(path.anchor)
|
||
for part in path.parts[1:]:
|
||
current /= part
|
||
try:
|
||
metadata = os.lstat(current)
|
||
except OSError as exc:
|
||
raise LaunchError(f"{label}不存在或不可访问: {current}") from exc
|
||
if stat.S_ISLNK(metadata.st_mode):
|
||
raise LaunchError(f"{label}不能包含 symlink: {current}")
|
||
|
||
|
||
def canonical_directory(value: str, label: str) -> Path:
|
||
if not value or _path_has_parent_reference(value):
|
||
raise LaunchError(f"{label}必须是无 '..' 的绝对目录")
|
||
raw = Path(value)
|
||
_assert_no_symlink_components(raw, label)
|
||
try:
|
||
resolved = raw.resolve(strict=True)
|
||
except OSError as exc:
|
||
raise LaunchError(f"{label}不存在: {raw}") from exc
|
||
if resolved != raw:
|
||
raise LaunchError(f"{label}必须使用规范绝对路径: {resolved}")
|
||
if not resolved.is_dir():
|
||
raise LaunchError(f"{label}不是目录: {resolved}")
|
||
return resolved
|
||
|
||
|
||
def authoritative_tasks_path(project_root: Path) -> Path:
|
||
tasks_path = project_root / TASKS_RELATIVE_PATH
|
||
_assert_no_symlink_components(tasks_path, "任务板路径")
|
||
metadata = tasks_path.stat()
|
||
if not stat.S_ISREG(metadata.st_mode):
|
||
raise LaunchError(f"任务板必须是普通文件: {tasks_path}")
|
||
if metadata.st_size > MAX_RECORD_SIZE:
|
||
raise LaunchError(f"任务板超过 {MAX_RECORD_SIZE} 字节上限")
|
||
return tasks_path
|
||
|
||
|
||
def load_authoritative_board(project_root_value: str) -> tuple[Path, dict]:
|
||
project_root = canonical_directory(project_root_value, "项目根目录")
|
||
tasks_path = authoritative_tasks_path(project_root)
|
||
board = load_document(tasks_path)
|
||
errors = validate_builtin(board)
|
||
errors.extend(validate_routing_document(board))
|
||
if errors:
|
||
formatted = "\n".join(f" - {error}" for error in dict.fromkeys(errors))
|
||
raise LaunchError(f"任务板未通过 worker 路由校验:\n{formatted}")
|
||
return project_root, board
|
||
|
||
|
||
def _is_under_real_grok_home(resolved: Path) -> bool:
|
||
"""Return True when ``resolved`` is under a non-symlink ``$HOME/.grok``."""
|
||
|
||
home, _ = account_identity()
|
||
grok_root = home / ".grok"
|
||
try:
|
||
root_metadata = os.lstat(grok_root)
|
||
if stat.S_ISLNK(root_metadata.st_mode) or not stat.S_ISDIR(root_metadata.st_mode):
|
||
return False
|
||
grok_root.resolve(strict=True)
|
||
resolved.relative_to(grok_root.resolve(strict=True))
|
||
except (OSError, ValueError):
|
||
return False
|
||
return True
|
||
|
||
|
||
def _is_trusted_grok_executable(resolved: Path, metadata: os.stat_result) -> bool:
|
||
"""Accept Grok's vendor artifact names under ``~/.grok``, or a 0755 ``grok``."""
|
||
|
||
if GROK_EXECUTABLE_NAME_RE.fullmatch(resolved.name) is None:
|
||
return False
|
||
if not executable_basename_matches_cli(str(resolved), "grok"):
|
||
return False
|
||
mode = stat.S_IMODE(metadata.st_mode)
|
||
if mode & 0o002:
|
||
return False
|
||
if resolved.name == "grok" and not (mode & 0o020):
|
||
return True
|
||
if not _is_under_real_grok_home(resolved):
|
||
return False
|
||
return metadata.st_uid == os.getuid() and metadata.st_gid == os.getgid()
|
||
|
||
|
||
def resolve_executable(name: str) -> Path:
|
||
supported = {"codex", "cursor-agent", "grok", "omp", "git", "orca"}
|
||
if name not in supported:
|
||
raise LaunchError(f"不支持的可执行文件: {name}")
|
||
search_paths = trusted_path_entries()
|
||
if name == "git":
|
||
search_paths = [
|
||
path for path in search_paths if str(path) in {"/usr/local/bin", "/usr/bin", "/bin"}
|
||
]
|
||
for directory in search_paths:
|
||
candidate = directory / name
|
||
try:
|
||
candidate_metadata = os.lstat(candidate)
|
||
resolved = candidate.resolve(strict=True)
|
||
metadata = resolved.stat()
|
||
except OSError:
|
||
continue
|
||
if not (
|
||
stat.S_ISREG(candidate_metadata.st_mode)
|
||
or stat.S_ISLNK(candidate_metadata.st_mode)
|
||
):
|
||
continue
|
||
if not stat.S_ISREG(metadata.st_mode) or not os.access(resolved, os.X_OK):
|
||
continue
|
||
if metadata.st_uid not in {0, os.getuid()}:
|
||
continue
|
||
if name == "grok":
|
||
if _is_trusted_grok_executable(resolved, metadata):
|
||
return resolved
|
||
continue
|
||
if stat.S_IMODE(metadata.st_mode) & 0o022:
|
||
continue
|
||
if resolved.name != name:
|
||
continue
|
||
return resolved
|
||
raise LaunchError(
|
||
f"找不到可信 {name};只搜索固定用户工具目录和系统目录,不读取 PATH"
|
||
)
|
||
|
||
|
||
def run_process(
|
||
argv: list[str],
|
||
*,
|
||
timeout: int = CONTROL_TIMEOUT_SECONDS,
|
||
indeterminate_on_timeout: bool = False,
|
||
) -> subprocess.CompletedProcess[str]:
|
||
try:
|
||
return subprocess.run(
|
||
argv,
|
||
shell=False,
|
||
check=False,
|
||
capture_output=True,
|
||
text=True,
|
||
timeout=timeout,
|
||
env=control_environment(),
|
||
)
|
||
except subprocess.TimeoutExpired as exc:
|
||
error_type = IndeterminateLaunch if indeterminate_on_timeout else LaunchError
|
||
raise error_type(f"命令超时: {argv[0]}") from exc
|
||
|
||
|
||
def run_text(argv: list[str], label: str) -> str:
|
||
completed = run_process(argv)
|
||
if completed.returncode != 0:
|
||
detail = (completed.stderr or completed.stdout).strip()[:2000]
|
||
raise LaunchError(f"{label}失败(exit {completed.returncode}): {detail}")
|
||
output = completed.stdout.strip()
|
||
if not output:
|
||
raise LaunchError(f"{label}没有输出")
|
||
if len(output.encode("utf-8")) > MAX_CONTROL_OUTPUT:
|
||
raise LaunchError(f"{label}输出超过安全上限")
|
||
return output
|
||
|
||
|
||
def parse_json_output(output: str, label: str) -> dict:
|
||
if len(output.encode("utf-8")) > MAX_CONTROL_OUTPUT:
|
||
raise LaunchError(f"{label} JSON 超过安全上限")
|
||
|
||
def reject_duplicate(pairs: list[tuple[str, object]]) -> dict:
|
||
result: dict = {}
|
||
for key, value in pairs:
|
||
if key in result:
|
||
raise LaunchError(f"{label} JSON 存在重复键: {key}")
|
||
result[key] = value
|
||
return result
|
||
|
||
try:
|
||
value = json.loads(output, object_pairs_hook=reject_duplicate)
|
||
except (json.JSONDecodeError, UnicodeDecodeError) as exc:
|
||
raise LaunchError(f"{label} 返回畸形 JSON") from exc
|
||
if not isinstance(value, dict):
|
||
raise LaunchError(f"{label} JSON 顶层必须是对象")
|
||
if value.get("ok") is False:
|
||
raise LaunchError(f"{label} 返回 ok=false")
|
||
return value
|
||
|
||
|
||
def run_json(argv: list[str], label: str) -> dict:
|
||
response = parse_json_output(run_text(argv, label), label)
|
||
if response.get("ok") is not True:
|
||
raise LaunchError(f"{label} 未明确返回 ok=true")
|
||
return response
|
||
|
||
|
||
def run_orca_create(argv: list[str]) -> tuple[dict, str]:
|
||
"""Run Orca's mutating create once and obtain a safe terminal identity.
|
||
|
||
Once the mutating call is attempted, any timeout, transport/decode error,
|
||
interruption, non-zero exit, malformed response, or response without a
|
||
handle is indeterminate: Orca may already have created a terminal, so
|
||
callers must not retry automatically.
|
||
"""
|
||
|
||
label = "orca terminal create"
|
||
try:
|
||
completed = run_process(argv, indeterminate_on_timeout=True)
|
||
if completed.returncode != 0:
|
||
detail = (completed.stderr or completed.stdout).strip()[:2000]
|
||
raise IndeterminateLaunch(
|
||
f"{label} 返回 exit {completed.returncode},可能已创建终端: {detail}"
|
||
)
|
||
output = completed.stdout.strip()
|
||
if not output:
|
||
raise IndeterminateLaunch(f"{label} 无响应,可能已创建终端")
|
||
response = parse_json_output(output, label)
|
||
if response.get("ok") is not True:
|
||
raise IndeterminateLaunch(
|
||
f"{label} 未明确返回 ok=true,可能已创建终端"
|
||
)
|
||
handle = terminal_handle_from_create(response)
|
||
if not handle:
|
||
raise IndeterminateLaunch(f"{label} 响应缺少 handle,可能已创建终端")
|
||
return response, handle
|
||
except IndeterminateLaunch:
|
||
raise
|
||
except BaseException as exc:
|
||
detail = str(exc).strip() or type(exc).__name__
|
||
raise IndeterminateLaunch(
|
||
f"{label} 调用或响应处理异常,可能已创建终端: "
|
||
f"{type(exc).__name__}: {detail[:2000]}"
|
||
) from exc
|
||
|
||
|
||
def terminal_metadata(response: dict, expected_handle: str) -> dict | None:
|
||
"""Read only Orca's documented result.terminal/result handle shapes."""
|
||
|
||
result = response.get("result")
|
||
if not isinstance(result, dict):
|
||
return None
|
||
terminal = result.get("terminal")
|
||
candidates = [
|
||
candidate
|
||
for candidate in (terminal, result)
|
||
if isinstance(candidate, dict) and candidate.get("handle") == expected_handle
|
||
]
|
||
if len(candidates) != 1:
|
||
return None
|
||
return candidates[0]
|
||
|
||
|
||
def terminal_handle_from_create(response: dict) -> str | None:
|
||
result = response.get("result")
|
||
if not isinstance(result, dict):
|
||
return None
|
||
terminal = result.get("terminal")
|
||
values = [
|
||
candidate.get("handle")
|
||
for candidate in (terminal, result)
|
||
if isinstance(candidate, dict)
|
||
and isinstance(candidate.get("handle"), str)
|
||
and candidate.get("handle")
|
||
]
|
||
unique = set(values)
|
||
if len(values) != 1 or len(unique) != 1:
|
||
return None
|
||
try:
|
||
return safe_identity_text(values[0], "Orca terminal handle")
|
||
except LaunchError:
|
||
return None
|
||
|
||
|
||
def git_output(git: Path, directory: Path, *args: str) -> str:
|
||
return run_text(
|
||
[str(git), "-C", str(directory), *args],
|
||
f"git {' '.join(args)}",
|
||
).splitlines()[0]
|
||
|
||
|
||
def git_common_directory(git: Path, worktree: Path) -> Path:
|
||
raw = git_output(git, worktree, "rev-parse", "--git-common-dir")
|
||
candidate = Path(raw)
|
||
if not candidate.is_absolute():
|
||
candidate = worktree / candidate
|
||
try:
|
||
return candidate.resolve(strict=True)
|
||
except OSError as exc:
|
||
raise LaunchError(f"无法解析 Git common-dir: {candidate}") from exc
|
||
|
||
|
||
def registered_git_worktrees(git: Path, project_root: Path) -> set[Path]:
|
||
output = run_text(
|
||
[
|
||
str(git),
|
||
"-C",
|
||
str(project_root),
|
||
"worktree",
|
||
"list",
|
||
"--porcelain",
|
||
"-z",
|
||
],
|
||
"git worktree list",
|
||
)
|
||
paths: set[Path] = set()
|
||
for record in output.split("\0\0"):
|
||
if not record:
|
||
continue
|
||
fields = record.split("\0")
|
||
if not fields or not fields[0].startswith("worktree "):
|
||
raise LaunchError("git worktree list 返回畸形 porcelain 记录")
|
||
raw_path = fields[0][len("worktree ") :]
|
||
path = Path(raw_path)
|
||
if (
|
||
not path.is_absolute()
|
||
or _path_has_parent_reference(raw_path)
|
||
or os.path.normpath(raw_path) != raw_path
|
||
):
|
||
raise LaunchError("git worktree list 返回非规范绝对路径")
|
||
if path in paths:
|
||
raise LaunchError(f"git worktree list 返回重复路径: {path}")
|
||
paths.add(path)
|
||
if not paths:
|
||
raise LaunchError("git worktree list 未返回任何注册 worktree")
|
||
return paths
|
||
|
||
|
||
def assert_git_control_entry(worktree: Path) -> None:
|
||
control_path = worktree / ".git"
|
||
try:
|
||
metadata = os.lstat(control_path)
|
||
except OSError as exc:
|
||
raise LaunchError(f"worktree 缺少 .git 控制入口: {worktree}") from exc
|
||
if stat.S_ISLNK(metadata.st_mode):
|
||
raise LaunchError(f"worktree .git 不能是 symlink: {control_path}")
|
||
if not (stat.S_ISDIR(metadata.st_mode) or stat.S_ISREG(metadata.st_mode)):
|
||
raise LaunchError(f"worktree .git 必须是目录或普通 gitfile: {control_path}")
|
||
|
||
|
||
def capture_worktree_identity(
|
||
project_root: Path,
|
||
worktree_value: str,
|
||
) -> dict:
|
||
# allowedWorktrees 白名单已废弃(v0.19 起)。worktree 合法性由以下硬约束保证:
|
||
# 1) 必须是 Git worktree 根目录(不是任意子目录);
|
||
# 2) 必须出现在 `git worktree list` 注册表中;
|
||
# 3) 必须与 --project-root 属于同一 Git common-dir(同仓库)。
|
||
# worker 默认就在 --project-root(权威状态目录)工作,天然满足这些约束。
|
||
worktree = canonical_directory(worktree_value, "worker worktree")
|
||
|
||
git = resolve_executable("git")
|
||
assert_git_control_entry(project_root)
|
||
assert_git_control_entry(worktree)
|
||
top_level = canonical_directory(
|
||
git_output(git, worktree, "rev-parse", "--show-toplevel"),
|
||
"Git worktree top-level",
|
||
)
|
||
if top_level != worktree:
|
||
raise LaunchError("worker worktree 必须是 Git worktree 根目录,不能是子目录")
|
||
project_top = canonical_directory(
|
||
git_output(git, project_root, "rev-parse", "--show-toplevel"),
|
||
"项目 Git top-level",
|
||
)
|
||
if project_top != project_root:
|
||
raise LaunchError("--project-root 必须是 Git worktree 根目录")
|
||
registered = registered_git_worktrees(git, project_root)
|
||
if project_root not in registered:
|
||
raise LaunchError("项目根目录不在 git worktree 注册表中")
|
||
if worktree not in registered:
|
||
raise LaunchError("worker worktree 未出现在 git worktree list 中")
|
||
|
||
common = git_common_directory(git, worktree)
|
||
project_common = git_common_directory(git, project_root)
|
||
common_stat = common.stat()
|
||
project_common_stat = project_common.stat()
|
||
if (
|
||
common_stat.st_dev,
|
||
common_stat.st_ino,
|
||
) != (
|
||
project_common_stat.st_dev,
|
||
project_common_stat.st_ino,
|
||
):
|
||
raise LaunchError("worker worktree 不属于项目的 Git common-dir")
|
||
|
||
worktree_stat = worktree.stat()
|
||
return {
|
||
"path": str(worktree),
|
||
"device": worktree_stat.st_dev,
|
||
"inode": worktree_stat.st_ino,
|
||
"gitCommonDir": str(common),
|
||
"gitCommonDevice": common_stat.st_dev,
|
||
"gitCommonInode": common_stat.st_ino,
|
||
}
|
||
|
||
|
||
def assert_identity_current(identity: dict, label: str) -> None:
|
||
path = canonical_directory(str(identity.get("path", "")), label)
|
||
metadata = path.stat()
|
||
if (
|
||
metadata.st_dev,
|
||
metadata.st_ino,
|
||
) != (
|
||
identity.get("device"),
|
||
identity.get("inode"),
|
||
):
|
||
raise LaunchError(f"{label} inode 已变化: {path}")
|
||
common = canonical_directory(
|
||
str(identity.get("gitCommonDir", "")),
|
||
f"{label} Git common-dir",
|
||
)
|
||
common_metadata = common.stat()
|
||
if (
|
||
common_metadata.st_dev,
|
||
common_metadata.st_ino,
|
||
) != (
|
||
identity.get("gitCommonDevice"),
|
||
identity.get("gitCommonInode"),
|
||
):
|
||
raise LaunchError(f"{label} Git common-dir identity 已变化")
|
||
|
||
|
||
def find_task(board: dict, task_id: str) -> dict:
|
||
tasks = board.get("tasks")
|
||
if not isinstance(tasks, list):
|
||
raise LaunchError("tasks 必须是列表")
|
||
matches = [
|
||
task
|
||
for task in tasks
|
||
if isinstance(task, dict) and task.get("id") == task_id
|
||
]
|
||
if len(matches) != 1:
|
||
raise LaunchError(f"任务必须唯一存在: {task_id}")
|
||
return matches[0]
|
||
|
||
|
||
def build_plan(
|
||
*,
|
||
project_root_value: str,
|
||
task_id: str,
|
||
attempt_id: str,
|
||
role: str,
|
||
profile_id: str,
|
||
worktree_value: str,
|
||
slot: int,
|
||
) -> dict:
|
||
if not TASK_ID_RE.fullmatch(task_id):
|
||
raise LaunchError("task-id 只允许字母、数字、点、下划线和连字符")
|
||
if attempt_id not in {f"{task_id}-A1", f"{task_id}-A2", f"{task_id}-A3"}:
|
||
raise LaunchError("attempt-id 必须精确为 <task-id>-A1..A3")
|
||
if role not in {"developer", "test"}:
|
||
raise LaunchError("role 必须是 developer 或 test")
|
||
if not PROFILE_ID_RE.fullmatch(profile_id):
|
||
raise LaunchError("profile-id 格式非法")
|
||
if not isinstance(slot, int) or isinstance(slot, bool) or not 1 <= slot <= 99:
|
||
raise LaunchError("slot 必须是 1..99 的整数")
|
||
|
||
project_root, board = load_authoritative_board(project_root_value)
|
||
board_hash = canonical_sha256(board)
|
||
find_task(board, task_id)
|
||
project = board["project"]
|
||
orchestration = project.get("orchestration")
|
||
if not isinstance(orchestration, dict):
|
||
raise LaunchError("旧任务板缺少 project.orchestration,只能使用手动模式")
|
||
if orchestration.get("mode") != "orca":
|
||
raise LaunchError("project.orchestration.mode 不是 orca,拒绝自动创建 worker")
|
||
profiles = orchestration.get("profiles")
|
||
profile = profiles.get(profile_id) if isinstance(profiles, dict) else None
|
||
if not isinstance(profile, dict):
|
||
raise LaunchError(f"找不到结构化 profile: {profile_id}")
|
||
if profile.get("role") != role:
|
||
raise LaunchError(
|
||
f"profile {profile_id} 的角色是 {profile.get('role')!r},不是 {role}"
|
||
)
|
||
|
||
worktree = capture_worktree_identity(
|
||
project_root,
|
||
worktree_value,
|
||
)
|
||
executable = resolve_executable(str(profile["cli"]))
|
||
executable_stat = executable.stat()
|
||
cli_version = run_text([str(executable), "--version"], "读取 Agent CLI 版本")
|
||
if len(cli_version) > 256 or any(ord(char) < 32 for char in cli_version):
|
||
raise LaunchError("Agent CLI 版本输出包含控制字符或过长")
|
||
|
||
argv = render_worker_argv(profile, str(executable), worktree["path"])
|
||
requested = {
|
||
"cli": profile["cli"],
|
||
"tier": profile["tier"],
|
||
"model": profile["model"],
|
||
"reasoningEffort": profile["reasoningEffort"],
|
||
"permissionMode": profile["permissionMode"],
|
||
"executable": str(executable),
|
||
"executableDevice": executable_stat.st_dev,
|
||
"executableInode": executable_stat.st_ino,
|
||
"cliVersion": cli_version,
|
||
"argv": argv,
|
||
"argvHash": canonical_sha256(argv),
|
||
"environmentPolicy": ENVIRONMENT_POLICY,
|
||
}
|
||
current_profile_hash = profile_hash(
|
||
profile,
|
||
profile_version=orchestration["profileVersion"],
|
||
)
|
||
created_for = {
|
||
"taskId": task_id,
|
||
"attemptId": attempt_id,
|
||
"role": role,
|
||
}
|
||
launch_fingerprint = canonical_sha256(
|
||
{
|
||
"protocolVersion": PROTOCOL_VERSION,
|
||
"backend": "orca",
|
||
"projectRoot": str(project_root),
|
||
"boardHash": board_hash,
|
||
"profileId": profile_id,
|
||
"profileHash": current_profile_hash,
|
||
"createdFor": created_for,
|
||
"worktree": worktree,
|
||
"requested": requested,
|
||
"slot": slot,
|
||
}
|
||
)
|
||
try:
|
||
cli_label = CLI_TITLE_LABELS[str(profile["cli"])]
|
||
except KeyError as exc:
|
||
raise LaunchError(f"不支持的 worker CLI: {profile['cli']}") from exc
|
||
role_label = "DEV" if role == "developer" else "TEST"
|
||
digest_short = launch_fingerprint.split(":", 1)[-1][:10]
|
||
title = (
|
||
f"ACK-{role_label}-{cli_label}-{str(profile['tier']).upper()}-"
|
||
f"{digest_short}-{slot}"
|
||
)
|
||
return {
|
||
"protocolVersion": PROTOCOL_VERSION,
|
||
"backend": "orca",
|
||
"projectRoot": str(project_root),
|
||
"boardHash": board_hash,
|
||
"taskId": task_id,
|
||
"attemptId": attempt_id,
|
||
"role": role,
|
||
"profileId": profile_id,
|
||
"profileHash": current_profile_hash,
|
||
"launchFingerprint": launch_fingerprint,
|
||
"worktree": worktree,
|
||
"requested": requested,
|
||
"slot": slot,
|
||
"title": title,
|
||
}
|
||
|
||
|
||
def record_directory() -> Path:
|
||
root = Path("/tmp") / f"ack-worker-launch-{os.getuid()}"
|
||
try:
|
||
os.mkdir(root, 0o700)
|
||
except FileExistsError:
|
||
pass
|
||
metadata = os.lstat(root)
|
||
if not stat.S_ISDIR(metadata.st_mode) or stat.S_ISLNK(metadata.st_mode):
|
||
raise LaunchError(f"启动记录目录不是普通目录: {root}")
|
||
if metadata.st_uid != os.getuid() or stat.S_IMODE(metadata.st_mode) != 0o700:
|
||
raise LaunchError(f"启动记录目录必须由当前用户拥有且权限为 0700: {root}")
|
||
return root
|
||
|
||
|
||
def record_path(launch_id: str) -> Path:
|
||
if not LAUNCH_ID_RE.fullmatch(launch_id):
|
||
raise LaunchError("launch-id 必须是 64 位小写十六进制")
|
||
return record_directory() / f"{launch_id}.json"
|
||
|
||
|
||
def _read_record_file(path: Path) -> dict:
|
||
file_descriptor = os.open(path, os.O_RDONLY | os.O_NOFOLLOW)
|
||
try:
|
||
metadata = os.fstat(file_descriptor)
|
||
_assert_private_file(metadata, "启动记录")
|
||
if metadata.st_size > MAX_RECORD_SIZE:
|
||
raise LaunchError("启动记录超过安全上限")
|
||
chunks: list[bytes] = []
|
||
remaining = MAX_RECORD_SIZE + 1
|
||
while remaining:
|
||
chunk = os.read(file_descriptor, min(65536, remaining))
|
||
if not chunk:
|
||
break
|
||
chunks.append(chunk)
|
||
remaining -= len(chunk)
|
||
content_bytes = b"".join(chunks)
|
||
if len(content_bytes) > MAX_RECORD_SIZE:
|
||
raise LaunchError("启动记录超过安全上限")
|
||
try:
|
||
content = content_bytes.decode("utf-8")
|
||
except UnicodeDecodeError as exc:
|
||
raise LaunchError("启动记录不是 UTF-8") from exc
|
||
finally:
|
||
os.close(file_descriptor)
|
||
value = parse_json_output(content, "启动记录")
|
||
if value.get("launchId") != path.stem:
|
||
raise LaunchError("启动记录 launchId 与文件名不一致")
|
||
return value
|
||
|
||
|
||
def _assert_private_file(metadata: os.stat_result, label: str) -> None:
|
||
if not stat.S_ISREG(metadata.st_mode):
|
||
raise LaunchError(f"{label}必须是普通文件")
|
||
if metadata.st_uid != os.getuid():
|
||
raise LaunchError(f"{label}必须由当前用户拥有")
|
||
if stat.S_IMODE(metadata.st_mode) != 0o600:
|
||
raise LaunchError(f"{label}权限必须精确为 0600")
|
||
if metadata.st_nlink != 1:
|
||
raise LaunchError(f"{label}不能有额外 hard link")
|
||
|
||
|
||
def _write_record_file(path: Path, value: dict, *, exclusive: bool) -> None:
|
||
payload = (
|
||
json.dumps(
|
||
value,
|
||
sort_keys=True,
|
||
separators=(",", ":"),
|
||
ensure_ascii=False,
|
||
allow_nan=False,
|
||
)
|
||
+ "\n"
|
||
).encode("utf-8")
|
||
if len(payload) > MAX_RECORD_SIZE:
|
||
raise LaunchError("启动记录超过安全上限")
|
||
if exclusive:
|
||
flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW
|
||
file_descriptor = os.open(path, flags, 0o600)
|
||
with os.fdopen(file_descriptor, "wb") as output:
|
||
output.write(payload)
|
||
output.flush()
|
||
os.fsync(output.fileno())
|
||
else:
|
||
temporary = path.with_name(f".{path.name}.{secrets.token_hex(8)}.tmp")
|
||
flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW
|
||
file_descriptor = os.open(temporary, flags, 0o600)
|
||
try:
|
||
with os.fdopen(file_descriptor, "wb") as output:
|
||
output.write(payload)
|
||
output.flush()
|
||
os.fsync(output.fileno())
|
||
os.replace(temporary, path)
|
||
finally:
|
||
try:
|
||
os.unlink(temporary)
|
||
except FileNotFoundError:
|
||
pass
|
||
directory_fd = os.open(path.parent, os.O_RDONLY | os.O_DIRECTORY)
|
||
try:
|
||
os.fsync(directory_fd)
|
||
finally:
|
||
os.close(directory_fd)
|
||
|
||
|
||
def create_record(value: dict) -> Path:
|
||
path = record_path(str(value.get("launchId", "")))
|
||
_write_record_file(path, value, exclusive=True)
|
||
return path
|
||
|
||
|
||
@contextmanager
|
||
def locked_record(launch_id: str) -> Iterator[dict]:
|
||
path = record_path(launch_id)
|
||
lock_path = path.with_suffix(".lock")
|
||
lock_fd = os.open(
|
||
lock_path,
|
||
os.O_RDWR | os.O_CREAT | os.O_NOFOLLOW,
|
||
0o600,
|
||
)
|
||
try:
|
||
_assert_private_file(os.fstat(lock_fd), "启动记录锁")
|
||
fcntl.flock(lock_fd, fcntl.LOCK_EX)
|
||
value = _read_record_file(path)
|
||
yield value
|
||
_write_record_file(path, value, exclusive=False)
|
||
finally:
|
||
fcntl.flock(lock_fd, fcntl.LOCK_UN)
|
||
os.close(lock_fd)
|
||
|
||
|
||
def read_record(launch_id: str) -> dict:
|
||
with locked_record(launch_id) as value:
|
||
return json.loads(json.dumps(value))
|
||
|
||
|
||
def update_record(
|
||
launch_id: str,
|
||
*,
|
||
preserve_cleanup_state: bool = False,
|
||
**changes: object,
|
||
) -> dict:
|
||
with locked_record(launch_id) as value:
|
||
if preserve_cleanup_state and isinstance(value.get("cleanup"), dict):
|
||
return json.loads(json.dumps(value))
|
||
value.update(changes)
|
||
return json.loads(json.dumps(value))
|
||
|
||
|
||
def try_update_record(launch_id: str, **changes: object) -> str | None:
|
||
"""Attempt an exception-path record update without changing control flow."""
|
||
|
||
try:
|
||
update_record(launch_id, **changes)
|
||
except BaseException as exc:
|
||
detail = str(exc).strip() or type(exc).__name__
|
||
return f"{type(exc).__name__}: {detail[:2000]}"
|
||
return None
|
||
|
||
|
||
def build_bootstrap_command(launch_id: str) -> str:
|
||
if not LAUNCH_ID_RE.fullmatch(launch_id):
|
||
raise LaunchError("launch-id 格式非法")
|
||
python = Path(sys.executable).resolve(strict=True)
|
||
script = Path(__file__).resolve(strict=True)
|
||
argv = [
|
||
str(python),
|
||
"-I",
|
||
str(script),
|
||
"_bootstrap",
|
||
"--launch-id",
|
||
launch_id,
|
||
]
|
||
return "exec " + shlex.join(argv)
|
||
|
||
|
||
def bootstrap_authorization_hash(
|
||
launch_id: str,
|
||
launch_fingerprint: str,
|
||
nonce: str,
|
||
) -> str:
|
||
if not LAUNCH_ID_RE.fullmatch(launch_id):
|
||
raise LaunchError("launch-id 格式非法")
|
||
if not SHA256_RE.fullmatch(launch_fingerprint):
|
||
raise LaunchError("launch fingerprint 格式非法")
|
||
if not LAUNCH_ID_RE.fullmatch(nonce):
|
||
raise LaunchError("bootstrap nonce 格式非法")
|
||
return canonical_sha256(
|
||
{
|
||
"protocolVersion": PROTOCOL_VERSION,
|
||
"launchId": launch_id,
|
||
"launchFingerprint": launch_fingerprint,
|
||
"nonce": nonce,
|
||
}
|
||
)
|
||
|
||
|
||
def bootstrap_ready_proof(
|
||
launch_id: str,
|
||
launch_fingerprint: str,
|
||
nonce: str,
|
||
) -> str:
|
||
bootstrap_authorization_hash(launch_id, launch_fingerprint, nonce)
|
||
message = (
|
||
f"ack-bootstrap-ready-v{PROTOCOL_VERSION}:"
|
||
f"{launch_id}:{launch_fingerprint}"
|
||
).encode("utf-8")
|
||
digest = hmac.new(bytes.fromhex(nonce), message, hashlib.sha256).hexdigest()
|
||
return f"sha256:{digest}"
|
||
|
||
|
||
def runtime_id_from_response(response: dict) -> str:
|
||
metadata = response.get("_meta")
|
||
runtime_id = metadata.get("runtimeId") if isinstance(metadata, dict) else None
|
||
return safe_identity_text(runtime_id, "Orca runtimeId")
|
||
|
||
|
||
def run_orca_close(
|
||
orca: Path,
|
||
handle: str,
|
||
expected_runtime_id: str,
|
||
) -> dict[str, str]:
|
||
"""Durably close the created tab and require a bound JSON confirmation."""
|
||
|
||
label = "orca terminal close --tab"
|
||
try:
|
||
completed = run_process(
|
||
[
|
||
str(orca),
|
||
"terminal",
|
||
"close",
|
||
"--terminal",
|
||
handle,
|
||
"--tab",
|
||
"--json",
|
||
],
|
||
timeout=10,
|
||
indeterminate_on_timeout=True,
|
||
)
|
||
except (LaunchError, OSError) as exc:
|
||
raise IndeterminateLaunch(
|
||
f"{label} 未获得确认,reconcile required: {exc}"
|
||
) from exc
|
||
|
||
if completed.returncode != 0:
|
||
detail = (completed.stderr or completed.stdout).strip()[:2000]
|
||
raise IndeterminateLaunch(
|
||
f"{label} 返回 exit {completed.returncode},reconcile required: {detail}"
|
||
)
|
||
output = completed.stdout.strip()
|
||
if not output:
|
||
raise IndeterminateLaunch(f"{label} 无响应,reconcile required")
|
||
try:
|
||
response = parse_json_output(output, label)
|
||
if response.get("ok") is not True:
|
||
raise LaunchError(f"{label} 未明确返回 ok=true")
|
||
runtime_id = runtime_id_from_response(response)
|
||
if runtime_id != expected_runtime_id:
|
||
raise LaunchError(
|
||
f"{label} runtimeId 不匹配: {runtime_id} != {expected_runtime_id}"
|
||
)
|
||
result = response.get("result")
|
||
close = result.get("close") if isinstance(result, dict) else None
|
||
if not isinstance(close, dict):
|
||
raise LaunchError(f"{label} 缺少 result.close")
|
||
closed_handle = safe_identity_text(close.get("handle"), "close handle")
|
||
tab_id = safe_identity_text(close.get("tabId"), "close tabId")
|
||
if closed_handle != handle:
|
||
raise LaunchError(
|
||
f"{label} handle 不匹配: {closed_handle} != {handle}"
|
||
)
|
||
if close.get("closeMode") != "tab":
|
||
raise LaunchError(f"{label} 未确认整 tab 持久关闭")
|
||
except (LaunchError, OSError, ValueError, KeyError) as exc:
|
||
raise IndeterminateLaunch(
|
||
f"{label} 回执无法确认,reconcile required: {exc}"
|
||
) from exc
|
||
|
||
return {
|
||
"runtimeId": runtime_id,
|
||
"handle": closed_handle,
|
||
"tabId": tab_id,
|
||
"closeMode": "tab",
|
||
}
|
||
|
||
|
||
def wait_for_bootstrap(launch_id: str, expected_proof: str) -> dict:
|
||
deadline = time.monotonic() + BOOTSTRAP_READY_TIMEOUT_SECONDS
|
||
while time.monotonic() < deadline:
|
||
value = read_record(launch_id)
|
||
state = value.get("state")
|
||
if state in {"bootstrap-ready", "ready"}:
|
||
if not hmac.compare_digest(
|
||
str(value.get("bootstrapProof", "")),
|
||
expected_proof,
|
||
):
|
||
raise LaunchError("worker bootstrap ready proof 不匹配")
|
||
return value
|
||
if state in {"failed", "closed", "indeterminate"}:
|
||
raise LaunchError(
|
||
f"worker bootstrap 未就绪: state={state}, error={value.get('error')}"
|
||
)
|
||
time.sleep(0.1)
|
||
raise LaunchError("worker bootstrap ready handshake 超时")
|
||
|
||
|
||
def validate_terminal_binding(plan: dict, metadata: dict) -> dict[str, str]:
|
||
handle = safe_identity_text(metadata.get("handle"), "terminal show handle")
|
||
incarnation_id = safe_identity_text(
|
||
metadata.get("incarnationId"),
|
||
"terminal show incarnationId",
|
||
)
|
||
connected = metadata.get("connected")
|
||
writable = metadata.get("writable")
|
||
if connected is not True or writable is not True:
|
||
raise LaunchError("新 worker terminal 必须 connected=true 且 writable=true")
|
||
observed_worktree = metadata.get("worktreePath")
|
||
if not isinstance(observed_worktree, str) or not observed_worktree:
|
||
raise LaunchError("terminal show 缺少可核对的 worktreePath")
|
||
observed_path = canonical_directory(
|
||
observed_worktree,
|
||
"Orca observed worktreePath",
|
||
)
|
||
if str(observed_path) != plan["worktree"]["path"]:
|
||
raise LaunchError(
|
||
"Orca observed worktreePath 与请求不一致: "
|
||
f"{observed_path} != {plan['worktree']['path']}"
|
||
)
|
||
return {
|
||
"handle": handle,
|
||
"incarnationId": incarnation_id,
|
||
"observedWorktreePath": str(observed_path),
|
||
}
|
||
|
||
|
||
def build_receipt(
|
||
launch_id: str,
|
||
plan: dict,
|
||
runtime_id: str,
|
||
metadata: dict,
|
||
created_at: str,
|
||
) -> dict:
|
||
observed = validate_terminal_binding(plan, metadata)
|
||
|
||
receipt = {
|
||
"receiptVersion": RECEIPT_VERSION,
|
||
"id": f"WR-{launch_id}",
|
||
"launchId": launch_id,
|
||
"profileId": plan["profileId"],
|
||
"profileHash": plan["profileHash"],
|
||
"launchFingerprint": plan["launchFingerprint"],
|
||
"projectRoot": plan["projectRoot"],
|
||
"boardHash": plan["boardHash"],
|
||
"slot": plan["slot"],
|
||
"createdFor": {
|
||
"taskId": plan["taskId"],
|
||
"attemptId": plan["attemptId"],
|
||
"role": plan["role"],
|
||
},
|
||
"worktree": plan["worktree"],
|
||
"requested": plan["requested"],
|
||
"binding": {
|
||
"orchestrator": "orca",
|
||
"runtimeId": runtime_id,
|
||
"handle": observed["handle"],
|
||
"incarnationId": observed["incarnationId"],
|
||
"observedWorktreePath": observed["observedWorktreePath"],
|
||
"connected": True,
|
||
"writable": True,
|
||
"boundAt": format_timestamp(utc_now()),
|
||
},
|
||
"createdAt": created_at,
|
||
}
|
||
receipt["receiptHash"] = canonical_sha256(receipt)
|
||
return receipt
|
||
|
||
|
||
def launch_with_orca(plan: dict) -> dict:
|
||
launch_id = secrets.token_hex(32)
|
||
authorization_nonce = secrets.token_hex(32)
|
||
created_at_dt = utc_now()
|
||
created_at = format_timestamp(created_at_dt)
|
||
record = {
|
||
"recordVersion": 1,
|
||
"launchId": launch_id,
|
||
"state": "prepared",
|
||
"createdAt": created_at,
|
||
"expiresAt": format_timestamp(
|
||
created_at_dt + timedelta(seconds=LAUNCH_TTL_SECONDS)
|
||
),
|
||
"plan": plan,
|
||
"runtime": None,
|
||
"receipt": None,
|
||
"authorizationHash": bootstrap_authorization_hash(
|
||
launch_id,
|
||
plan["launchFingerprint"],
|
||
authorization_nonce,
|
||
),
|
||
"bootstrapProof": None,
|
||
"error": None,
|
||
}
|
||
record_path_value = create_record(record)
|
||
|
||
orca: Path | None = None
|
||
handle: str | None = None
|
||
runtime_id: str | None = None
|
||
try:
|
||
orca = resolve_executable("orca")
|
||
status_response = run_json([str(orca), "status", "--json"], "orca status")
|
||
runtime_id = runtime_id_from_response(status_response)
|
||
bootstrap_command = build_bootstrap_command(launch_id)
|
||
create_argv = [
|
||
str(orca),
|
||
"terminal",
|
||
"create",
|
||
"--worktree",
|
||
f"path:{plan['worktree']['path']}",
|
||
"--command",
|
||
bootstrap_command,
|
||
"--title",
|
||
plan["title"],
|
||
"--json",
|
||
]
|
||
assert_identity_current(plan["worktree"], "worker worktree")
|
||
create_response, handle = run_orca_create(create_argv)
|
||
create_runtime_id = runtime_id_from_response(create_response)
|
||
if create_runtime_id != runtime_id:
|
||
raise LaunchError("Orca runtimeId 在 terminal create 期间变化")
|
||
initial_show = run_json(
|
||
[
|
||
str(orca),
|
||
"terminal",
|
||
"show",
|
||
"--terminal",
|
||
handle,
|
||
"--json",
|
||
],
|
||
"orca terminal show (pre-authorization)",
|
||
)
|
||
if runtime_id_from_response(initial_show) != runtime_id:
|
||
raise LaunchError("Orca runtimeId 在 terminal create 期间变化")
|
||
initial_metadata = terminal_metadata(initial_show, handle)
|
||
if initial_metadata is None:
|
||
raise LaunchError("terminal show 无法绑定刚创建的 handle")
|
||
initial_binding = validate_terminal_binding(plan, initial_metadata)
|
||
update_record(
|
||
launch_id,
|
||
runtime={
|
||
"runtimeId": runtime_id,
|
||
"handle": handle,
|
||
"incarnationId": initial_binding["incarnationId"],
|
||
},
|
||
)
|
||
send_response = run_json(
|
||
[
|
||
str(orca),
|
||
"terminal",
|
||
"send",
|
||
"--terminal",
|
||
handle,
|
||
"--text",
|
||
authorization_nonce,
|
||
"--enter",
|
||
"--json",
|
||
],
|
||
"orca terminal send bootstrap authorization",
|
||
)
|
||
if runtime_id_from_response(send_response) != runtime_id:
|
||
raise LaunchError("Orca runtimeId 在 bootstrap 授权期间变化")
|
||
expected_proof = bootstrap_ready_proof(
|
||
launch_id,
|
||
plan["launchFingerprint"],
|
||
authorization_nonce,
|
||
)
|
||
wait_for_bootstrap(launch_id, expected_proof)
|
||
show_response = run_json(
|
||
[
|
||
str(orca),
|
||
"terminal",
|
||
"show",
|
||
"--terminal",
|
||
handle,
|
||
"--json",
|
||
],
|
||
"orca terminal show",
|
||
)
|
||
if runtime_id_from_response(show_response) != runtime_id:
|
||
raise LaunchError("Orca runtimeId 在 terminal show 期间变化")
|
||
metadata = terminal_metadata(show_response, handle)
|
||
if metadata is None:
|
||
raise LaunchError("terminal show 无法绑定刚创建的 handle")
|
||
final_binding = validate_terminal_binding(plan, metadata)
|
||
if final_binding["incarnationId"] != initial_binding["incarnationId"]:
|
||
raise LaunchError("Orca terminal incarnation 在 bootstrap 期间变化")
|
||
assert_identity_current(plan["worktree"], "worker worktree")
|
||
receipt = build_receipt(
|
||
launch_id,
|
||
plan,
|
||
runtime_id,
|
||
metadata,
|
||
created_at,
|
||
)
|
||
update_record(
|
||
launch_id,
|
||
state="ready",
|
||
runtime={
|
||
"runtimeId": runtime_id,
|
||
"handle": handle,
|
||
"incarnationId": receipt["binding"]["incarnationId"],
|
||
},
|
||
receipt=receipt,
|
||
error=None,
|
||
)
|
||
return receipt
|
||
except BaseException as exc:
|
||
original_error = str(exc) or type(exc).__name__
|
||
if isinstance(exc, IndeterminateLaunch) and not handle:
|
||
detail = (
|
||
f"launchId={launch_id}; record={record_path_value}; "
|
||
f"{original_error}; reconcile required"
|
||
)
|
||
record_error = try_update_record(
|
||
launch_id,
|
||
state="indeterminate",
|
||
error=detail,
|
||
)
|
||
if record_error is not None:
|
||
detail += f"; record update failed: {record_error}"
|
||
raise IndeterminateLaunch(detail) from exc
|
||
|
||
if not handle:
|
||
record_error = try_update_record(
|
||
launch_id,
|
||
state="failed",
|
||
error=original_error,
|
||
)
|
||
if record_error is not None:
|
||
detail = (
|
||
f"launchId={launch_id}; record={record_path_value}; "
|
||
f"pre-handle failure: {original_error}; "
|
||
f"record update failed: {record_error}"
|
||
)
|
||
raise LaunchError(detail) from exc
|
||
raise
|
||
|
||
pending_detail = (
|
||
f"launchId={launch_id}; record={record_path_value}; "
|
||
f"post-handle failure: {original_error}; cleanup pending; "
|
||
"reconcile required"
|
||
)
|
||
pending_record_error = try_update_record(
|
||
launch_id,
|
||
state="indeterminate",
|
||
error=pending_detail,
|
||
cleanup={
|
||
"method": "terminal.closeTab",
|
||
"runtimeId": runtime_id,
|
||
"handle": handle,
|
||
"confirmed": False,
|
||
"reconcileRequired": True,
|
||
},
|
||
)
|
||
try:
|
||
if orca is None or runtime_id is None:
|
||
raise IndeterminateLaunch(
|
||
"缺少 Orca runtime identity,无法确认关闭"
|
||
)
|
||
close_confirmation = run_orca_close(orca, handle, runtime_id)
|
||
except BaseException as close_exc:
|
||
close_error = str(close_exc) or type(close_exc).__name__
|
||
detail = (
|
||
f"launchId={launch_id}; record={record_path_value}; "
|
||
f"post-handle failure: {original_error}; "
|
||
f"close confirmation failed: {close_error}; reconcile required"
|
||
)
|
||
if pending_record_error is not None:
|
||
detail += (
|
||
"; pending record update failed: "
|
||
f"{pending_record_error}"
|
||
)
|
||
reconcile_record_error = try_update_record(
|
||
launch_id,
|
||
state="indeterminate",
|
||
error=detail,
|
||
cleanup={
|
||
"method": "terminal.closeTab",
|
||
"runtimeId": runtime_id,
|
||
"handle": handle,
|
||
"confirmed": False,
|
||
"reconcileRequired": True,
|
||
"error": close_error,
|
||
},
|
||
)
|
||
if reconcile_record_error is not None:
|
||
detail += (
|
||
"; reconcile record update failed: "
|
||
f"{reconcile_record_error}"
|
||
)
|
||
raise IndeterminateLaunch(detail) from close_exc
|
||
|
||
final_record_error = try_update_record(
|
||
launch_id,
|
||
state="failed",
|
||
error=original_error,
|
||
cleanup={
|
||
"method": "terminal.closeTab",
|
||
**close_confirmation,
|
||
"confirmed": True,
|
||
"reconcileRequired": False,
|
||
},
|
||
)
|
||
if final_record_error is not None:
|
||
detail = (
|
||
f"launchId={launch_id}; record={record_path_value}; "
|
||
f"post-handle failure: {original_error}; close confirmed; "
|
||
"failed state persistence could not be confirmed: "
|
||
f"{final_record_error}; reconcile required"
|
||
)
|
||
if pending_record_error is not None:
|
||
detail += (
|
||
"; pending record update failed: "
|
||
f"{pending_record_error}"
|
||
)
|
||
raise IndeterminateLaunch(detail) from exc
|
||
raise
|
||
|
||
|
||
def bootstrap_worker(launch_id: str) -> int:
|
||
try:
|
||
with locked_record(launch_id) as record:
|
||
if record.get("state") != "prepared":
|
||
raise LaunchError(
|
||
f"启动记录不能重复消费: state={record.get('state')}"
|
||
)
|
||
expires_at = parse_timestamp(record.get("expiresAt"), "expiresAt")
|
||
if utc_now() >= expires_at:
|
||
raise LaunchError("启动记录已过期")
|
||
record["state"] = "bootstrapping"
|
||
plan_snapshot = record.get("plan")
|
||
if not isinstance(plan_snapshot, dict):
|
||
raise LaunchError("启动记录缺少 plan")
|
||
authorization_hash = record.get("authorizationHash")
|
||
if not isinstance(authorization_hash, str) or not SHA256_RE.fullmatch(
|
||
authorization_hash
|
||
):
|
||
raise LaunchError("启动记录缺少 bootstrap authorization hash")
|
||
|
||
rebuilt = build_plan(
|
||
project_root_value=str(plan_snapshot.get("projectRoot", "")),
|
||
task_id=str(plan_snapshot.get("taskId", "")),
|
||
attempt_id=str(plan_snapshot.get("attemptId", "")),
|
||
role=str(plan_snapshot.get("role", "")),
|
||
profile_id=str(plan_snapshot.get("profileId", "")),
|
||
worktree_value=str(
|
||
(plan_snapshot.get("worktree") or {}).get("path", "")
|
||
),
|
||
slot=plan_snapshot.get("slot", 0),
|
||
)
|
||
if rebuilt != plan_snapshot:
|
||
raise LaunchError("bootstrap 重读后的 launch plan 与已审阅快照不一致")
|
||
if Path.cwd().resolve(strict=True) != Path(rebuilt["worktree"]["path"]):
|
||
raise LaunchError("bootstrap cwd 与已验证 worktree 不一致")
|
||
assert_identity_current(rebuilt["worktree"], "bootstrap worktree")
|
||
|
||
executable = Path(rebuilt["requested"]["executable"])
|
||
executable_metadata = executable.stat()
|
||
if (
|
||
executable_metadata.st_dev,
|
||
executable_metadata.st_ino,
|
||
) != (
|
||
rebuilt["requested"]["executableDevice"],
|
||
rebuilt["requested"]["executableInode"],
|
||
):
|
||
raise LaunchError("Agent CLI executable identity 已变化")
|
||
|
||
awaiting_record = update_record(
|
||
launch_id,
|
||
preserve_cleanup_state=True,
|
||
state="awaiting-authorization",
|
||
error=None,
|
||
)
|
||
if (
|
||
awaiting_record.get("state") != "awaiting-authorization"
|
||
or isinstance(awaiting_record.get("cleanup"), dict)
|
||
):
|
||
raise LaunchError("bootstrap 已被父进程取消,拒绝等待授权")
|
||
readable, _, _ = select.select(
|
||
[sys.stdin],
|
||
[],
|
||
[],
|
||
BOOTSTRAP_READY_TIMEOUT_SECONDS,
|
||
)
|
||
if not readable:
|
||
raise LaunchError("bootstrap authorization 输入超时")
|
||
nonce_line = sys.stdin.readline(130)
|
||
nonce = nonce_line.rstrip("\r\n")
|
||
if not LAUNCH_ID_RE.fullmatch(nonce) or nonce_line not in {
|
||
nonce + "\n",
|
||
nonce + "\r\n",
|
||
}:
|
||
raise LaunchError("bootstrap authorization nonce 格式非法")
|
||
expected_authorization = bootstrap_authorization_hash(
|
||
launch_id,
|
||
rebuilt["launchFingerprint"],
|
||
nonce,
|
||
)
|
||
if not hmac.compare_digest(authorization_hash, expected_authorization):
|
||
raise LaunchError("bootstrap authorization hash 不匹配")
|
||
|
||
ready_proof = bootstrap_ready_proof(
|
||
launch_id,
|
||
rebuilt["launchFingerprint"],
|
||
nonce,
|
||
)
|
||
with locked_record(launch_id) as current_record:
|
||
if (
|
||
current_record.get("state") != "awaiting-authorization"
|
||
or isinstance(current_record.get("cleanup"), dict)
|
||
):
|
||
raise LaunchError("bootstrap 已被父进程取消,拒绝启动 Agent")
|
||
if not hmac.compare_digest(
|
||
str(current_record.get("authorizationHash", "")),
|
||
authorization_hash,
|
||
):
|
||
raise LaunchError("bootstrap authorization record 已漂移")
|
||
process = subprocess.Popen(
|
||
rebuilt["requested"]["argv"],
|
||
shell=False,
|
||
cwd=rebuilt["worktree"]["path"],
|
||
env=worker_environment(str(rebuilt["requested"]["cli"])),
|
||
)
|
||
current_record.update(
|
||
state="bootstrap-ready",
|
||
childPid=process.pid,
|
||
bootstrapProof=ready_proof,
|
||
error=None,
|
||
)
|
||
return_code = process.wait()
|
||
update_record(
|
||
launch_id,
|
||
preserve_cleanup_state=True,
|
||
state="closed" if return_code == 0 else "failed",
|
||
childExitCode=return_code,
|
||
error=None if return_code == 0 else f"Agent CLI exit {return_code}",
|
||
)
|
||
return return_code
|
||
except BaseException as exc:
|
||
try:
|
||
update_record(
|
||
launch_id,
|
||
preserve_cleanup_state=True,
|
||
state="failed",
|
||
error=str(exc),
|
||
)
|
||
except BaseException:
|
||
pass
|
||
sys.stderr.write(f"ACK worker bootstrap 失败: {exc}\n")
|
||
return 1
|
||
|
||
|
||
def add_launch_arguments(parser: argparse.ArgumentParser) -> None:
|
||
parser.add_argument("--project-root", required=True)
|
||
parser.add_argument("--task-id", required=True)
|
||
parser.add_argument("--attempt-id", required=True)
|
||
parser.add_argument("--role", required=True, choices=("developer", "test"))
|
||
parser.add_argument("--profile-id", required=True)
|
||
parser.add_argument("--worktree", required=True)
|
||
parser.add_argument("--slot", type=int, default=1)
|
||
|
||
|
||
def build_parser() -> argparse.ArgumentParser:
|
||
parser = argparse.ArgumentParser(
|
||
description="按 ACK 结构化 profile 规划或启动 worker",
|
||
allow_abbrev=False,
|
||
)
|
||
subparsers = parser.add_subparsers(dest="action", required=True)
|
||
|
||
profile_parser = subparsers.add_parser(
|
||
"profile-hash",
|
||
help="输出一个 profile 的规范化 SHA-256",
|
||
allow_abbrev=False,
|
||
)
|
||
profile_parser.add_argument("--project-root", required=True)
|
||
profile_parser.add_argument("--profile-id", required=True)
|
||
|
||
plan_parser = subparsers.add_parser(
|
||
"plan",
|
||
help="只校验并输出启动计划,不创建终端",
|
||
allow_abbrev=False,
|
||
)
|
||
add_launch_arguments(plan_parser)
|
||
|
||
launch_parser = subparsers.add_parser(
|
||
"launch",
|
||
help="通过 Orca 固定 bootstrap 创建 worker",
|
||
allow_abbrev=False,
|
||
)
|
||
add_launch_arguments(launch_parser)
|
||
launch_parser.add_argument(
|
||
"--expected-launch-fingerprint",
|
||
required=True,
|
||
help="必须与刚审阅的 plan.launchFingerprint 精确一致",
|
||
)
|
||
|
||
bootstrap_parser = subparsers.add_parser(
|
||
"_bootstrap",
|
||
help=argparse.SUPPRESS,
|
||
allow_abbrev=False,
|
||
)
|
||
bootstrap_parser.add_argument("--launch-id", required=True)
|
||
return parser
|
||
|
||
|
||
def profile_hash_command(project_root_value: str, profile_id: str) -> dict:
|
||
if not PROFILE_ID_RE.fullmatch(profile_id):
|
||
raise LaunchError("profile-id 格式非法")
|
||
_, board = load_authoritative_board(project_root_value)
|
||
orchestration = board["project"].get("orchestration")
|
||
profiles = orchestration.get("profiles") if isinstance(orchestration, dict) else None
|
||
profile = profiles.get(profile_id) if isinstance(profiles, dict) else None
|
||
if not isinstance(profile, dict):
|
||
raise LaunchError(f"找不到 profile: {profile_id}")
|
||
return {
|
||
"profileVersion": orchestration["profileVersion"],
|
||
"profileId": profile_id,
|
||
"profileHash": profile_hash(
|
||
profile,
|
||
profile_version=orchestration["profileVersion"],
|
||
),
|
||
"profile": profile,
|
||
}
|
||
|
||
|
||
def main(argv: list[str] | None = None) -> int:
|
||
arguments = list(sys.argv[1:] if argv is None else argv)
|
||
try:
|
||
reject_duplicate_or_separator_args(arguments)
|
||
except LaunchError as exc:
|
||
sys.stderr.write(f"{exc}\n")
|
||
return 2
|
||
|
||
parser = build_parser()
|
||
try:
|
||
args = parser.parse_args(arguments)
|
||
except SystemExit as exc:
|
||
return int(exc.code)
|
||
|
||
if args.action == "_bootstrap":
|
||
if not LAUNCH_ID_RE.fullmatch(args.launch_id):
|
||
sys.stderr.write("launch-id 必须是 64 位小写十六进制\n")
|
||
return 2
|
||
return bootstrap_worker(args.launch_id)
|
||
|
||
try:
|
||
if args.action == "profile-hash":
|
||
output = profile_hash_command(args.project_root, args.profile_id)
|
||
else:
|
||
plan = build_plan(
|
||
project_root_value=args.project_root,
|
||
task_id=args.task_id,
|
||
attempt_id=args.attempt_id,
|
||
role=args.role,
|
||
profile_id=args.profile_id,
|
||
worktree_value=args.worktree,
|
||
slot=args.slot,
|
||
)
|
||
if args.action == "plan":
|
||
output = {"mode": "plan", "plan": plan}
|
||
else:
|
||
if not SHA256_RE.fullmatch(args.expected_launch_fingerprint):
|
||
raise LaunchError(
|
||
"expected-launch-fingerprint 必须是规范 sha256 值"
|
||
)
|
||
if not hmac.compare_digest(
|
||
args.expected_launch_fingerprint,
|
||
plan["launchFingerprint"],
|
||
):
|
||
raise LaunchError(
|
||
"当前 launch plan 与已审阅 fingerprint 不一致;"
|
||
"请重新执行 plan 并审阅"
|
||
)
|
||
output = {
|
||
"mode": "launched",
|
||
"receipt": launch_with_orca(plan),
|
||
}
|
||
except IndeterminateLaunch as exc:
|
||
sys.stderr.write(
|
||
"worker 创建结果不确定;不要直接重试,请先按外部 launch record "
|
||
f"reconcile: {exc}\n"
|
||
)
|
||
return 1
|
||
except (LaunchError, OSError, ValueError, KeyError) as exc:
|
||
sys.stderr.write(f"worker 启动拒绝: {exc}\n")
|
||
return 1
|
||
|
||
sys.stdout.write(
|
||
json.dumps(
|
||
output,
|
||
sort_keys=True,
|
||
ensure_ascii=False,
|
||
separators=(",", ":"),
|
||
allow_nan=False,
|
||
)
|
||
+ "\n"
|
||
)
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(main())
|