#!/usr/bin/env python3 """Validate and execute the shared project Agent runtime contract. The command surface is deliberately small. It never evaluates project text as shell, and it only creates builder sessions from a fixed make invocation. """ from __future__ import annotations import argparse import copy import hashlib import json import os import re import subprocess import sys import time import uuid from pathlib import Path from typing import Any try: import yaml except ImportError as exc: # pragma: no cover - dependency is checked at runtime raise SystemExit("需要 PyYAML: python3 -m pip install pyyaml") from exc CLI_NAMES = {"codex", "cursor-agent", "grok", "omp", "pi"} ADAPTERS = {"builder", "tester", "ack"} EFFORTS = {None, "low", "medium", "high", "xhigh"} PERMISSIONS = {"read-only", "workspace-write"} TIERS = {"standard", "strong"} LIFECYCLES = {"persistent", "ephemeral"} ID_RE = re.compile(r"^[a-z][a-z0-9-]{1,63}$") BINDING_RE = re.compile(r"^[a-z][a-z0-9-]*(?:\.[a-z][a-z0-9-]*)+$") TARGET_RE = re.compile(r"^[A-Za-z0-9_.-]{1,96}$") MODEL_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:/+@-]{0,127}$") MAKEFILE_RE = re.compile(r"^[A-Za-z0-9_.-]+(?:/[A-Za-z0-9_.-]+)*$") FORBIDDEN_KEYS = {"command", "argv", "shell", "env", "environmentVariables", "token", "secret", "password"} class ConfigError(ValueError): pass class UniqueLoader(yaml.SafeLoader): pass def _construct_mapping(loader: UniqueLoader, node: yaml.MappingNode, deep: bool = False) -> dict[str, Any]: mapping: dict[str, Any] = {} for key_node, value_node in node.value: key = loader.construct_object(key_node, deep=deep) if not isinstance(key, str): raise ConfigError("YAML mapping key 必须是字符串") if key in mapping: raise ConfigError(f"YAML 存在重复键: {key}") mapping[key] = loader.construct_object(value_node, deep=deep) return mapping UniqueLoader.add_constructor(yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, _construct_mapping) def load_yaml(path: Path) -> dict[str, Any]: try: value = yaml.load(path.read_text(encoding="utf-8"), Loader=UniqueLoader) except (OSError, yaml.YAMLError) as exc: raise ConfigError(f"无法读取 {path}: {exc}") from exc if value is None: return {} if not isinstance(value, dict): raise ConfigError(f"{path} 顶层必须是 mapping") return value def deep_merge(base: dict[str, Any], override: dict[str, Any], where: str = "root") -> dict[str, Any]: result = copy.deepcopy(base) for key, value in override.items(): if key not in result: raise ConfigError(f"{where}: local 配置不能增加未声明字段 {key!r}") if isinstance(result[key], dict) and isinstance(value, dict): result[key] = deep_merge(result[key], value, f"{where}.{key}") else: result[key] = copy.deepcopy(value) return result def text(value: Any, where: str) -> str: if not isinstance(value, str) or not value.strip() or any(ord(c) < 32 for c in value): raise ConfigError(f"{where}: 必须是非空文本") return value def safe_id(value: Any, where: str) -> str: value = text(value, where) if not ID_RE.fullmatch(value): raise ConfigError(f"{where}: ID 只能使用小写字母、数字和连字符") return value def safe_binding_id(value: Any, where: str) -> str: value = text(value, where) if not BINDING_RE.fullmatch(value): raise ConfigError(f"{where}: binding 必须是 namespace.name 形式") return value def safe_path(value: Any, where: str) -> str: value = text(value, where) path = Path(value) if not path.is_absolute() or value == os.path.sep or ".." in path.parts or os.path.normpath(value) != value: raise ConfigError(f"{where}: 必须是规范绝对路径") return value def validate(config: dict[str, Any]) -> None: if config.get("schemaVersion") != 1: raise ConfigError("schemaVersion 必须为 1") for key in ("locations", "profiles", "bindings"): if not isinstance(config.get(key), dict) or not config[key]: raise ConfigError(f"{key}: 必须是非空 mapping") locations = config["locations"] for location_id, location in locations.items(): safe_id(location_id, f"locations.{location_id}") if not isinstance(location, dict): raise ConfigError(f"locations.{location_id}: 必须是 mapping") for required in ("environment", "project", "path"): if required not in location: raise ConfigError(f"locations.{location_id}: 缺少 {required}") environment = text(location["environment"], f"locations.{location_id}.environment") if environment != "local" and not ID_RE.fullmatch(environment): raise ConfigError(f"locations.{location_id}.environment: 必须是 local 或安全环境名") text(location["project"], f"locations.{location_id}.project") safe_path(location["path"], f"locations.{location_id}.path") host = location.get("host") if host is not None: host = text(host, f"locations.{location_id}.host") if host != "local" and not re.fullmatch(r"runtime:[0-9a-f-]{16,}", host): raise ConfigError(f"locations.{location_id}.host: 必须是 local 或 runtime:") profiles = config["profiles"] for profile_id, profile in profiles.items(): safe_id(profile_id, f"profiles.{profile_id}") if not isinstance(profile, dict): raise ConfigError(f"profiles.{profile_id}: 必须是 mapping") for required in ("cli", "model", "reasoningEffort", "permissionMode"): if required not in profile: raise ConfigError(f"profiles.{profile_id}: 缺少 {required}") if profile["cli"] not in CLI_NAMES: raise ConfigError(f"profiles.{profile_id}.cli: 不支持 {profile['cli']!r}") if profile["model"] is not None and MODEL_RE.fullmatch(str(profile["model"])) is None: raise ConfigError(f"profiles.{profile_id}.model: 不是安全模型 ID") if profile["reasoningEffort"] not in EFFORTS: raise ConfigError(f"profiles.{profile_id}.reasoningEffort: 不支持该值") if profile["permissionMode"] not in PERMISSIONS: raise ConfigError(f"profiles.{profile_id}.permissionMode: 不支持该值") if profile.get("tier", "standard") not in TIERS: raise ConfigError(f"profiles.{profile_id}.tier: 必须是 standard/strong") for binding_id, binding in config["bindings"].items(): safe_binding_id(binding_id, f"bindings.{binding_id}") if not isinstance(binding, dict): raise ConfigError(f"bindings.{binding_id}: 必须是 mapping") for required in ("adapter", "location", "profile"): if required not in binding: raise ConfigError(f"bindings.{binding_id}: 缺少 {required}") if binding["adapter"] not in ADAPTERS: raise ConfigError(f"bindings.{binding_id}.adapter: 不支持 {binding['adapter']!r}") location_id = safe_id(binding["location"], f"bindings.{binding_id}.location") profile_id = safe_id(binding["profile"], f"bindings.{binding_id}.profile") if location_id not in locations or profile_id not in profiles: raise ConfigError(f"bindings.{binding_id}: location/profile 引用不存在") if binding["adapter"] == "builder": makefile = text(binding.get("makefile", "makefile.builder"), f"bindings.{binding_id}.makefile") if Path(makefile).is_absolute() or ".." in Path(makefile).parts or MAKEFILE_RE.fullmatch(makefile) is None: raise ConfigError(f"bindings.{binding_id}.makefile: 必须是项目内相对路径") target = text(binding.get("target"), f"bindings.{binding_id}.target") if TARGET_RE.fullmatch(target) is None: raise ConfigError(f"bindings.{binding_id}.target: 不是安全 make target") session = binding.get("session", {}) if not isinstance(session, dict) or session.get("lifecycle", "ephemeral") not in LIFECYCLES: raise ConfigError(f"bindings.{binding_id}.session.lifecycle: 必须是 persistent/ephemeral") scan_forbidden(config) def scan_forbidden(value: Any, where: str = "root") -> None: if isinstance(value, dict): for key, child in value.items(): if key in FORBIDDEN_KEYS: raise ConfigError(f"{where}: 禁止配置字段 {key!r}") scan_forbidden(child, f"{where}.{key}") elif isinstance(value, list): for index, child in enumerate(value): scan_forbidden(child, f"{where}[{index}]") def load_config(root: Path) -> dict[str, Any]: path = root / ".pouch" / "agent.yaml" if not path.is_file(): raise ConfigError(f"缺少 {path},请先创建项目通用 Agent 配置") config = load_yaml(path) local_path = root / ".pouch" / "agent.local.yaml" if local_path.is_file(): config = deep_merge(config, load_yaml(local_path)) validate(config) return config def binding(config: dict[str, Any], binding_id: str) -> dict[str, Any]: if binding_id not in config["bindings"]: raise ConfigError(f"不存在 binding: {binding_id}") value = copy.deepcopy(config["bindings"][binding_id]) value["id"] = binding_id value["locationId"] = value["location"] value["location"] = copy.deepcopy(config["locations"][value["locationId"]]) value["profileId"] = value["profile"] value["profile"] = copy.deepcopy(config["profiles"][value["profileId"]]) return value def orca_json(args: list[str], environment: str | None = None) -> dict[str, Any]: command = ["orca", *args] if environment and environment != "local": command.extend(["--environment", environment]) command.append("--json") proc = subprocess.run(command, capture_output=True, text=True, check=False) raw = (proc.stdout or "").strip() try: data = json.loads(raw) except json.JSONDecodeError as exc: detail = (proc.stderr or raw or str(exc)).strip() raise ConfigError(f"Orca 没有返回 JSON: {' '.join(command)}\n{detail}") from exc if not data.get("ok"): error = data.get("error") or {} raise ConfigError(f"{' '.join(command)}: {error.get('message') or error.get('code') or 'unknown error'}") result = data.get("result") return result if isinstance(result, dict) else {} def environment_id(name: str) -> str: if name == "local": return "local" result = orca_json(["environment", "list"]) for item in result.get("environments", []): if isinstance(item, dict) and item.get("name") == name: return str(item.get("id")) raise ConfigError(f"找不到已保存 Orca environment: {name}") def location_host(location: dict[str, Any]) -> str: host = location.get("host") if host: return str(host) if location["environment"] == "local": return "local" return f"runtime:{environment_id(str(location['environment']))}" def worktree_for(binding_value: dict[str, Any]) -> dict[str, Any]: location = binding_value["location"] environment = str(location["environment"]) project_ids = {str(location["project"])} projects = orca_json(["project", "list"], environment).get("projects", []) for project in projects: if not isinstance(project, dict): continue if str(project.get("id")) == str(location["project"]) or str(project.get("displayName")) == str(location["project"]): project_ids.update(str(repo_id) for repo_id in project.get("sourceRepoIds", []) if repo_id) result = orca_json(["worktree", "list"], environment) candidates = [] for item in result.get("worktrees", []): if not isinstance(item, dict): continue item_project_ids = {str(item.get("projectId")), str(item.get("repoId"))} if not project_ids.intersection(item_project_ids): continue if str(item.get("path")) != str(location["path"]): continue expected_host = location_host(location) # A routed remote runtime stamps its own worktrees with hostId=local; # the environment name/ID is the remote-machine discriminator. if location["environment"] == "local" and expected_host == "local" and str(item.get("hostId")) != "local": continue candidates.append(item) if len(candidates) != 1: if not candidates: raise ConfigError( f"找不到精确 worktree: environment={environment}, host={location_host(location)}, " f"project={location['project']}, path={location['path']};先创建或登记该 worktree" ) raise ConfigError(f"binding 匹配到多个 worktree: {len(candidates)}") return candidates[0] def project_root_commit(root: Path) -> str | None: proc = subprocess.run(["git", "-C", str(root), "rev-parse", "HEAD"], capture_output=True, text=True, check=False) return proc.stdout.strip() if proc.returncode == 0 else None def run_dir(root: Path) -> Path: path = root / ".pouch" / "agent" / "runs" path.mkdir(parents=True, exist_ok=True) return path def run_path(root: Path, run_id: str) -> Path: if not re.fullmatch(r"run_[0-9a-f]{16,64}", run_id): raise ConfigError("run id 格式无效") return run_dir(root) / f"{run_id}.json" def cmd_validate(args: argparse.Namespace) -> int: config = load_config(Path(args.root).resolve()) print(json.dumps({"ok": True, "schemaVersion": config["schemaVersion"], "bindings": sorted(config["bindings"])}, ensure_ascii=False, indent=2)) return 0 def cmd_show(args: argparse.Namespace) -> int: config = load_config(Path(args.root).resolve()) value = binding(config, args.binding) print(json.dumps(value, ensure_ascii=False, indent=2, sort_keys=True)) return 0 def cmd_worktree_resolve(args: argparse.Namespace) -> int: root = Path(args.root).resolve() value = binding(load_config(root), args.binding) worktree = worktree_for(value) print(json.dumps({"binding": args.binding, "worktree": worktree}, ensure_ascii=False, indent=2)) return 0 def cmd_worktree_create(args: argparse.Namespace) -> int: root = Path(args.root).resolve() value = binding(load_config(root), args.binding) location = value["location"] try: existing = worktree_for(value) except ConfigError as exc: if "找不到精确 worktree" not in str(exc): raise existing = None if existing: print(json.dumps({"created": False, "binding": args.binding, "worktree": existing}, ensure_ascii=False, indent=2)) return 0 host = location_host(location) command = ["worktree", "create", "--project", str(location["project"]), "--host", host, "--name", args.name, "--no-parent"] if args.base: command.extend(["--base-branch", args.base]) result = orca_json(command, str(location["environment"])) print(json.dumps({"created": True, "binding": args.binding, "result": result, "next": "将返回 worktree 的 path 写回 location.path 后再运行 resolve"}, ensure_ascii=False, indent=2)) return 0 def make_command(value: dict[str, Any]) -> str: # Replace the shell so terminal wait observes the make exit code instead # of waiting forever on the interactive shell after make returns. return f"exec make -f {value.get('makefile', 'makefile.builder')} {value['target']}" def cmd_run_start(args: argparse.Namespace) -> int: root = Path(args.root).resolve() value = binding(load_config(root), args.binding) if value["adapter"] != "builder": raise ConfigError(f"run start 目前只为 builder 提供受控 make session;{value['adapter']} 请走对应 skill adapter") if value.get("session", {}).get("lifecycle", "ephemeral") != "persistent": raise ConfigError(f"binding {args.binding} 不是 persistent session") worktree = worktree_for(value) environment = str(value["location"]["environment"]) result = orca_json(["terminal", "create", "--worktree", f"id:{worktree['id']}", "--command", make_command(value), "--title", args.binding], environment) terminal = result.get("terminal") or result handle = terminal.get("handle") or result.get("handle") if not handle: raise ConfigError("Orca terminal create 没有返回 handle") run_id = "run_" + uuid.uuid4().hex record = { "schemaVersion": 1, "id": run_id, "binding": args.binding, "adapter": value["adapter"], "profileId": value["profileId"], "profileHash": hashlib.sha256(json.dumps(value["profile"], sort_keys=True).encode()).hexdigest(), "environment": environment, "project": value["location"]["project"], "path": value["location"]["path"], "worktreeId": worktree["id"], "terminalHandle": handle, "command": make_command(value), "sourceCommit": project_root_commit(root), "createdAt": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), "status": "started", } run_path(root, run_id).write_text(json.dumps(record, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") print(json.dumps(record, ensure_ascii=False, indent=2)) if args.wait_ms: wait = orca_json(["terminal", "wait", "--terminal", handle, "--for", "exit", "--timeout-ms", str(args.wait_ms)], environment) record["wait"] = wait record["status"] = "completed" if wait.get("exited") or wait.get("status") == "exited" else "waited" run_path(root, run_id).write_text(json.dumps(record, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") print(json.dumps(wait, ensure_ascii=False, indent=2)) return 0 def load_run(root: Path, run_id: str) -> dict[str, Any]: path = run_path(root, run_id) if not path.is_file(): raise ConfigError(f"找不到 run 记录: {run_id}") value = json.loads(path.read_text(encoding="utf-8")) if not isinstance(value, dict): raise ConfigError(f"run 记录无效: {run_id}") return value def cmd_run_status(args: argparse.Namespace) -> int: root = Path(args.root).resolve() record = load_run(root, args.run) result = orca_json(["terminal", "show", "--terminal", record["terminalHandle"]], record.get("environment")) print(json.dumps({"run": record, "terminal": result}, ensure_ascii=False, indent=2)) return 0 def cmd_run_attach(args: argparse.Namespace) -> int: root = Path(args.root).resolve() record = load_run(root, args.run) result = orca_json(["terminal", "read", "--terminal", record["terminalHandle"]], record.get("environment")) print(json.dumps(result, ensure_ascii=False, indent=2)) return 0 def cmd_run_resume(args: argparse.Namespace) -> int: """Explicitly reattach to a recorded terminal; never search for another one.""" root = Path(args.root).resolve() record = load_run(root, args.run) result = orca_json(["terminal", "show", "--terminal", record["terminalHandle"]], record.get("environment")) print(json.dumps({"resumed": True, "run": record, "terminal": result}, ensure_ascii=False, indent=2)) return 0 def cmd_run_stop(args: argparse.Namespace) -> int: root = Path(args.root).resolve() record = load_run(root, args.run) result = orca_json(["terminal", "close", "--terminal", record["terminalHandle"]], record.get("environment")) record["status"] = "stopped" record["stoppedAt"] = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()) run_path(root, args.run).write_text(json.dumps(record, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") print(json.dumps({"run": record, "close": result}, ensure_ascii=False, indent=2)) return 0 def parser() -> argparse.ArgumentParser: root_parent = argparse.ArgumentParser(add_help=False) root_parent.add_argument("--root", default=".") parser = argparse.ArgumentParser(description="共享 Agent 配置、worktree 和 session 运行时") sub = parser.add_subparsers(dest="command", required=True) validate_parser = sub.add_parser("validate", parents=[root_parent]) validate_parser.set_defaults(func=cmd_validate) show = sub.add_parser("show", parents=[root_parent]) show.add_argument("--binding", required=True) show.set_defaults(func=cmd_show) wt = sub.add_parser("worktree") wt_sub = wt.add_subparsers(dest="worktree_command", required=True) resolve = wt_sub.add_parser("resolve", parents=[root_parent]) resolve.add_argument("--binding", required=True) resolve.set_defaults(func=cmd_worktree_resolve) create = wt_sub.add_parser("create", parents=[root_parent]) create.add_argument("--binding", required=True) create.add_argument("--name", required=True) create.add_argument("--base", default="") create.set_defaults(func=cmd_worktree_create) run = sub.add_parser("run") run_sub = run.add_subparsers(dest="run_command", required=True) start = run_sub.add_parser("start", parents=[root_parent]) start.add_argument("--binding", required=True) start.add_argument("--wait-ms", type=int, default=0) start.set_defaults(func=cmd_run_start) for name, func in (("status", cmd_run_status), ("attach", cmd_run_attach), ("resume", cmd_run_resume), ("stop", cmd_run_stop)): item = run_sub.add_parser(name, parents=[root_parent]) item.add_argument("--run", required=True) item.set_defaults(func=func) return parser if __name__ == "__main__": try: parsed = parser().parse_args() raise SystemExit(parsed.func(parsed)) except ConfigError as exc: print(f"environment-fail: {exc}", file=sys.stderr) raise SystemExit(2)