diff --git a/skills/manage-release/SKILL.md b/skills/manage-release/SKILL.md index 2acc8e0..7363ef0 100644 --- a/skills/manage-release/SKILL.md +++ b/skills/manage-release/SKILL.md @@ -56,8 +56,10 @@ description: >- `git worktree list --porcelain`。 3. 获取远端最新引用后,记录 base 分支的准确 commit。不要把本地过期分支当作发布基线。 4. 识别版本来源、最近稳定 tag、tag 格式、分支命名、合并方式和发布说明来源。 -5. 分别检查 Git remote 的读取与推送能力,以及 PR/MR、合并或 Forge Release 阶段所需的 - GitHub、GitLab、Gitea 或 Forgejo CLI。只有请求涉及平台能力时才要求对应 CLI。 +5. 分别用 `git remote get-url --all origin` 和 + `git remote get-url --push --all origin` 检查读取与推送目标;远端写操作要求两者各只有 + 一个值且规范化后指向同一仓库。再检查 PR/MR、合并或 Forge Release 阶段所需的 GitHub、 + GitLab、Gitea 或 Forgejo CLI。只有请求涉及平台能力时才要求对应 CLI。 记录匹配该 host 的活动账号;认证检查只读取状态。 6. 查询已有分支、worktree、PR/MR、tag 和 Forge Release,从真实状态判断流程已走到哪一步。 @@ -119,8 +121,10 @@ CHANGELOG 或发布说明,保证它们进入同一个 PR/MR。运行项目声 ### 5. 推送并创建 PR/MR -执行远端写操作前再次确认 remote、base、head、版本、活动账号和授权范围。Git 分支与 -tag 通过已确认的 remote 读写;创建或操作 PR/MR 时使用与托管平台匹配的官方 CLI。 +执行远端写操作前再次确认 remote、base、head、版本、活动账号和授权范围,并重新读取 +唯一 fetch/push URL。Git 分支与 tag 通过已确认的 remote 读写;创建或操作 PR/MR 时使用 +与托管平台匹配的官方 CLI。直接执行当前 shell 中的 `git` 和平台 CLI,让工具使用其正常 +登录;不要读取、解析或复制 `~/.gitconfig`、tea/gh/glab 配置,也不要从中提取 token。 不要让 CLI 隐式创建 fork、改变 base 或选择另一个 remote。 PR/MR 内容至少说明目标版本、变更摘要、验证命令和结果、发布后续动作。用户只要求开 diff --git a/skills/manage-release/references/forge-adapters.md b/skills/manage-release/references/forge-adapters.md index 3cf48db..41142f6 100644 --- a/skills/manage-release/references/forge-adapters.md +++ b/skills/manage-release/references/forge-adapters.md @@ -21,6 +21,10 @@ CLI。只发布 tag 时,不要因为缺少 Forge CLI 而停止;仍要验证 个账号或 profile 时,必须唯一确定本次使用的身份。CLI 版本之间的 flags 可能不同, 每次执行写操作前读取对应子命令的 `--help`,不要凭记忆拼接参数。 +直接从目标 worktree 的当前 shell 运行 `git` 与平台 CLI,让它们使用各自正常的登录机制。 +Gitea/Forgejo 使用 `tea whoami` 和 `tea` 对应子命令;不要由 Agent 读取 tea 配置文件、 +调用 credential helper 导出 token,或把 token 转存到命令参数、环境文件和发布说明。 + 当请求确实涉及 PR/MR、合并或 Forge Release,且 remote host 无法识别、CLI 缺失、认证 失败或 CLI 指向另一个实例时,停止对应平台阶段并报告: diff --git a/skills/orc/README.md b/skills/orc/README.md index b4a1d49..4128e0f 100644 --- a/skills/orc/README.md +++ b/skills/orc/README.md @@ -22,6 +22,11 @@ ORC 是显式调用的薄路由器:只把开发、版本发布、DEB 和 Docke resolver 以 `-I -S` 隔离模式运行,不加载项目模块、用户 site-packages 或第三方解析器。 - ORC v2 worker 使用 `workspace-write` sandbox,以便发送 Orca lifecycle 消息; 只读任务会在阶段 prompt 中禁止文件修改。 +- release worker 是受信任的发布角色,直接调用当前 shell 中已认证的 `git` 与 Forge CLI + (Gitea/Forgejo 使用 `tea`);ORC 不读取认证配置,也不注入 credential helper。 +- release 阶段要求 `origin` 只有一个且一致的 fetch/push URL。Codex 只为该阶段开启网络, + 并用 network proxy 把出站目标限制到 remote/Forge API host;GitHub 额外允许其固定 API + 与 release upload host,其它本地阶段默认继续禁网。 ## Worker 档位 diff --git a/skills/orc/SKILL.md b/skills/orc/SKILL.md index fa5c3d4..0e261e7 100644 --- a/skills/orc/SKILL.md +++ b/skills/orc/SKILL.md @@ -98,10 +98,13 @@ JSON-compatible YAML,并由隔离的 Python 标准库解析。它固定包含 `--global-level`,阶段 档位用 `--stage-level`;项目根与目标 worktree 必须使用规范绝对路径。Codex 默认复用 `codex-login`,Cursor 默认复用 `cursor-login`;只有明确使用环境凭据时才分别选择 - `openai`、`azure-openai` 或 `cursor-api-key`。远端认证默认 `none`;只有目标 provider - 与 transport 已确认时,才选择一个精确的 `github-token`、`gitlab-token`、 - `gitea-token`、`forgejo-token`、`ssh-agent` 或 `deb-token`。不得把多个 provider 凭据 - 一起交给 worker。resolver 不存在静默 fallback: + `openai`、`azure-openai` 或 `cursor-api-key`。远端认证默认 `none`。release worker 作为 + 受信任发布角色,直接调用当前 shell 中已认证的 `git` 与 Forge CLI;Gitea/Forgejo 由 + `$manage-release` 在目标 worktree 中运行 `tea`,resolver 不读取 tea 配置、不注入 + credential helper,也不把 tea token 转成环境变量。只有明确改用环境 token 或 SSH + agent 时,才选择一个精确的 `github-token`、`gitlab-token`、`gitea-token`、 + `forgejo-token`、`ssh-agent` 或 `deb-token`,不得同时交给 worker 多组环境凭据。resolver + 不存在静默 fallback: ```bash /usr/bin/python3 -I -S /scripts/resolve_profile.py resolve \ @@ -145,6 +148,13 @@ JSON-compatible YAML,并由隔离的 Python 标准库解析。它固定包含 Docker 或部署;这些动作由 ORC 的独立阶段负责。 - ORC v2 的 `permissionMode` 固定为 `workspace-write`,因为受监督 worker 需要写入 Orca 运行时目录才能发送 lifecycle 消息。只读任务仍由 prompt 限制不得改文件。 +- release 阶段必须绑定 `origin` 唯一且规范化后完全一致的 fetch/push URL,并把 remote + identity 写入 fingerprint;多个 URL、独立 `pushurl` 或不支持的 remote 直接停止。 +- Codex release worker 固定使用 `on-request` 与 `auto_review`,开启 + `sandbox_workspace_write.network_access=true`,并用 network proxy 只允许绑定的 remote + 与确定性的 Forge API host。GitHub 固定追加 `api.github.com` 和 `uploads.github.com`; + Gitea、Forgejo 与 GitLab 默认只使用 remote host。其它本地阶段默认禁网;不得改用 + `danger-full-access`。Cursor 继续使用自身的 `--auto-review --sandbox enabled`。 - 安全启动适配器只支持 `codex` 和 `cursor-agent`。Codex 使用结构化 sandbox 与 approval 参数;Cursor 使用 `--auto-review --sandbox enabled --workspace `。不接受 full-access、bypass、`--force`、`--yolo` 或关闭 sandbox。 @@ -154,9 +164,11 @@ JSON-compatible YAML,并由隔离的 Python 标准库解析。它固定包含 Codex 则显式传递 `model_reasoning_effort`。 - resolver 只读取 skill 内有大小上限的普通共享配置文件,拒绝 symlink/special file; 启动计划绑定配置快照、root-owned 隔离 Python、可信绝对 Agent/Orca executable、Git - worktree identity、精确认证选择和固定 argv。实际启动会重新校验 fingerprint,并只注入 - 所选模型认证与单一目标认证的环境变量;不得改写 resolver 返回的 worker argv,也不得 - 把终端创建 argv 的绝对 Orca 路径换成项目 `PATH` 解析。 + worktree identity、release remote identity、精确环境认证选择和固定 argv。实际启动会 + 重新校验 fingerprint;release 阶段允许受信任的 `$manage-release` 直接使用当前 shell + 已有的 Git/Forge 登录,resolver 不检查或复制认证文件。显式 token/SSH 认证仍只注入所选 + 环境变量。不得改写 resolver 返回的 worker argv,也不得把终端创建 argv 的绝对 Orca + 路径换成项目 `PATH` 解析。 - 不把模型档位当作权限。`high` 不自动获得更多文件、凭据、网络或远端写权限。 - 不执行 `orca orchestration reset`,除非用户明确要求放弃全部相关运行时状态。 diff --git a/skills/orc/references/orca-adapter.md b/skills/orc/references/orca-adapter.md index febd272..603ba2a 100644 --- a/skills/orc/references/orca-adapter.md +++ b/skills/orc/references/orca-adapter.md @@ -30,9 +30,16 @@ CLI/Orca executable、 - `terminalCreateArgv` 的首项是 resolver 选定的绝对 Orca executable,不替换成裸 `orca` 或重新通过项目 `PATH` 查找。 - 不从 YAML 自行拼接命令,不追加 argv、环境变量、hook 或权限参数。 -- `remote-auth` 必须与本阶段唯一目标 provider/transport 一致;默认 `none`,不为方便 - 同时暴露多个 token 或 SSH agent。需要代理、额外凭据或 Docker registry env secret 时 - fail closed,由用户确认新的最小权限适配,不继承 ambient proxy/credential 变量。 +- `remote-auth` 只表示显式注入的单一环境 token 或 SSH agent;默认 `none`,不为方便同时 + 暴露多组环境凭据。release worker 直接使用当前 shell 已有的 Git/Forge CLI 登录, + resolver 不读取 tea/gh/glab 配置,也不注入 credential helper。 +- release 计划要求 `origin` 恰好一个 fetch URL 和一个 push URL,二者规范化后完全一致; + remote identity 会进入 fingerprint,并在实际 launch 时重新解析。独立 `pushurl`、多个 + push URL、local/file remote 或带内嵌 HTTPS 凭据的 URL 都 fail closed。 +- Codex release 计划固定使用 `on-request` + `auto_review`,开启 workspace-write 网络并启用 + network proxy,只允许绑定的 remote 与确定性 Forge API host。GitHub 固定追加 + `api.github.com` 和 `uploads.github.com`;其它本地阶段保持默认禁网。不得替换成 + `danger-full-access`。Cursor 继续使用自身的 auto-review sandbox。 - resolver 失败时停止;不改用默认模型、邻近档位或已有未知终端。 - 不从任务文本、可执行文件存在性或共享配置猜宿主;Codex 传 `--host-cli codex`,Cursor 传 `--host-cli cursor-agent`。宿主身份不明确时停止。 diff --git a/skills/orc/scripts/resolve_profile.py b/skills/orc/scripts/resolve_profile.py index 02632ad..1fa0ebb 100755 --- a/skills/orc/scripts/resolve_profile.py +++ b/skills/orc/scripts/resolve_profile.py @@ -5,6 +5,7 @@ from __future__ import annotations import argparse import hashlib +import ipaddress import json import os import pwd @@ -15,6 +16,7 @@ import subprocess import sys from pathlib import Path from typing import Any +from urllib.parse import urlsplit LEVELS = ("low", "mid", "high") STAGES = ("code", "release", "deb", "docker") @@ -35,6 +37,14 @@ PROFILE_KEYS = { "approvalPolicy", } MODEL_RE = re.compile(r"[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}\Z") +REMOTE_HOST_RE = re.compile( + r"(?=.{1,253}\Z)(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)*" + r"[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\Z" +) +SCP_REMOTE_RE = re.compile( + r"(?:(?P[A-Za-z0-9._-]+)@)?" + r"(?P[A-Za-z0-9.-]+):(?P[^\s]+)\Z" +) MAX_CONFIG_SIZE = 64 * 1024 MAX_CONTROL_OUTPUT = 64 * 1024 CONTROL_TIMEOUT_SECONDS = 15 @@ -302,10 +312,13 @@ def resolve_profile( else: level, source = config["defaultLevel"], "config.defaultLevel" - profile = config["profiles"][selected_cli].get(level) - if profile is None: + configured_profile = config["profiles"][selected_cli].get(level) + if configured_profile is None: raise ConfigError(f"requested profile does not exist: {selected_cli}/{level}") + profile = dict(configured_profile) if selected_cli == "codex": + if stage == "release": + profile["approvalPolicy"] = "on-request" worker_args = [ "--model", profile["model"], @@ -317,6 +330,8 @@ def resolve_profile( profile["approvalPolicy"], "--strict-config", ] + if stage == "release": + worker_args.extend(["-c", 'approvals_reviewer="auto_review"']) else: worker_args = [ "--model", @@ -333,7 +348,7 @@ def resolve_profile( "selectionSource": source, "modelAuth": selected_auth, "remoteAuth": remote_auth, - "profile": dict(profile), + "profile": profile, "workerArgs": worker_args, } @@ -549,6 +564,117 @@ def _run_control(argv: list[str], label: str) -> str: return output +def _validated_remote_host(value: str, label: str) -> str: + host = value.lower() + if not REMOTE_HOST_RE.fullmatch(host): + raise ConfigError(f"{label} has an unsupported host") + if host == "localhost" or host.endswith(".localhost"): + raise ConfigError(f"{label} must not target a local host") + try: + address = ipaddress.ip_address(host) + except ValueError: + pass + else: + if not address.is_global: + raise ConfigError(f"{label} must not target a private or local address") + return host + + +def _remote_url_facts(value: str, label: str) -> dict[str, Any]: + if ( + not value + or value != value.strip() + or any(character.isspace() or character == "\x00" for character in value) + ): + raise ConfigError(f"{label} is not a safe remote URL") + + if "://" not in value: + match = SCP_REMOTE_RE.fullmatch(value) + if match is None: + raise ConfigError(f"{label} is not a supported HTTPS or SSH URL") + scheme = "ssh" + username = match.group("username") or "git" + host = _validated_remote_host(match.group("host"), label) + port = 22 + path = "/" + match.group("path").lstrip("/") + else: + parsed = urlsplit(value) + if ( + parsed.scheme not in {"https", "ssh"} + or not parsed.hostname + or parsed.password is not None + or parsed.query + or parsed.fragment + or not parsed.path + or parsed.path == "/" + ): + raise ConfigError(f"{label} is not a supported HTTPS or SSH URL") + scheme = parsed.scheme + username = parsed.username + if scheme == "https" and username is not None: + raise ConfigError(f"{label} must not embed HTTPS credentials") + host = _validated_remote_host(parsed.hostname, label) + try: + port = parsed.port or (443 if scheme == "https" else 22) + except ValueError as exc: + raise ConfigError(f"{label} is not a safe remote URL") from exc + path = parsed.path + + if not path.startswith("/") or _path_has_parent_reference(path): + raise ConfigError(f"{label} has an unsafe repository path") + user_prefix = f"{username}@" if username is not None else "" + canonical = f"{scheme}://{user_prefix}{host}:{port}{path}" + return { + "url": value, + "canonical": canonical, + "scheme": scheme, + "host": host, + "port": port, + "path": path, + } + + +def _origin_urls(git: Path, worktree: Path, *, push: bool) -> list[str]: + argv = [str(git), "-C", str(worktree), "remote", "get-url"] + if push: + argv.append("--push") + argv.extend(["--all", "origin"]) + output = _run_control( + argv, + f"Git origin {'push' if push else 'fetch'} URL check", + ) + return output.splitlines() + + +def resolve_release_remote(worktree: Path) -> dict[str, Any]: + git = resolve_trusted_executable("git") + fetch_urls = _origin_urls(git, worktree, push=False) + push_urls = _origin_urls(git, worktree, push=True) + if len(fetch_urls) != 1: + raise ConfigError("release origin must have exactly one fetch URL") + if len(push_urls) != 1: + raise ConfigError("release origin must have exactly one push URL") + + fetch = _remote_url_facts(fetch_urls[0], "Git origin fetch URL") + push = _remote_url_facts(push_urls[0], "Git origin push URL") + if fetch["canonical"] != push["canonical"]: + raise ConfigError("release origin fetch and push URLs must match") + network_hosts = [push["host"]] + if push["host"] == "github.com": + network_hosts.extend(["api.github.com", "uploads.github.com"]) + return { + "name": "origin", + "fetchUrl": fetch["url"], + "pushUrl": push["url"], + "canonical": push["canonical"], + "scheme": push["scheme"], + "host": push["host"], + "port": push["port"], + "path": push["path"], + "networkHosts": network_hosts, + } + + def _git_path(value: str, cwd: Path, label: str) -> Path: candidate = Path(value) if not candidate.is_absolute(): @@ -721,6 +847,35 @@ def build_launch_plan( model_auth=profile["modelAuth"], remote_auth=remote_auth, ) + release_remote = None + if stage == "release": + release_remote = resolve_release_remote(Path(worktree_facts["worktree"])) + if profile["cli"] == "codex": + network_domains = ", ".join( + f'"{host}" = "allow"' for host in release_remote["networkHosts"] + ) + profile = { + **profile, + "workerArgs": [ + *profile["workerArgs"], + "-c", + "sandbox_workspace_write.network_access=true", + "-c", + "features.network_proxy.enabled=true", + "-c", + "features.network_proxy.allow_local_binding=false", + "-c", + "features.network_proxy.allow_upstream_proxy=false", + "-c", + "features.network_proxy.dangerously_allow_all_unix_sockets=false", + "-c", + "features.network_proxy.dangerously_allow_non_loopback_proxy=false", + "-c", + "features.network_proxy.unix_sockets={}", + "-c", + f"features.network_proxy.domains={{ {network_domains} }}", + ], + } agent_cli = resolve_trusted_executable(profile["cli"]) executable = _executable_facts(agent_cli) orca = resolve_trusted_executable("orca") @@ -749,6 +904,8 @@ def build_launch_plan( "sha256": script_sha256, }, } + if release_remote is not None: + launch_facts["releaseRemote"] = release_remote fingerprint = _fingerprint(launch_facts) worker_argv = [str(agent_cli), *profile["workerArgs"]] if profile["cli"] == "cursor-agent": diff --git a/tests/test_orc_skill.py b/tests/test_orc_skill.py index 8fa4037..8a34c52 100644 --- a/tests/test_orc_skill.py +++ b/tests/test_orc_skill.py @@ -41,6 +41,7 @@ class OrcSkillTests(unittest.TestCase): self.assertIn("不得在项目中创建 `docs/orc/config.yaml`", skill) self.assertIn("薄路由器", skill) self.assertIn("不做领域判断", skill) + self.assertIn("当前 shell", skill) self.assertNotIn("用户未指定档位时采用以下判断", skill) self.assertNotIn("若该档位不足以安全完成", skill) for child in ("$ack", "$manage-release", "$deb-publisher", "$publish-docker-image"): @@ -146,6 +147,13 @@ class OrcSkillTests(unittest.TestCase): self.assertEqual(plan["workerArgs"][:2], ["--model", "gpt-5.6-terra"]) self.assertIn("--strict-config", plan["workerArgs"]) + approval_index = plan["workerArgs"].index("--ask-for-approval") + self.assertEqual(plan["workerArgs"][approval_index + 1], "on-request") + self.assertIn('approvals_reviewer="auto_review"', plan["workerArgs"]) + self.assertNotIn( + "sandbox_workspace_write.network_access=true", + plan["workerArgs"], + ) self.assertNotIn("danger-full-access", " ".join(plan["workerArgs"])) self.assertNotIn("env", plan["profile"]) self.assertNotIn("command", plan["profile"]) @@ -258,6 +266,122 @@ class OrcSkillTests(unittest.TestCase): remote_auth="none", ) + def test_release_remote_requires_one_identical_fetch_and_push_url(self) -> None: + git = shutil.which("git") + self.assertIsNotNone(git) + assert git is not None + git_path = Path(git).resolve() + + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary).resolve() + subprocess.run( + [str(git_path), "init", "-q", str(root)], + check=True, + capture_output=True, + ) + remote = "https://git.yumee.top/laily/musicpilot.git" + subprocess.run( + [str(git_path), "-C", str(root), "remote", "add", "origin", remote], + check=True, + capture_output=True, + ) + with mock.patch.object( + orc_profiles, + "resolve_trusted_executable", + return_value=git_path, + ): + facts = orc_profiles.resolve_release_remote(root) + + self.assertEqual(facts["fetchUrl"], remote) + self.assertEqual(facts["pushUrl"], remote) + self.assertEqual(facts["host"], "git.yumee.top") + self.assertEqual(facts["networkHosts"], ["git.yumee.top"]) + + subprocess.run( + [ + str(git_path), + "-C", + str(root), + "config", + "remote.origin.pushurl", + "https://git.example.invalid/other/repo.git", + ], + check=True, + capture_output=True, + ) + with mock.patch.object( + orc_profiles, + "resolve_trusted_executable", + return_value=git_path, + ): + with self.assertRaisesRegex( + orc_profiles.ConfigError, + "fetch and push URLs must match", + ): + orc_profiles.resolve_release_remote(root) + + subprocess.run( + [ + str(git_path), + "-C", + str(root), + "config", + "--add", + "remote.origin.pushurl", + "https://git.example.invalid/second/repo.git", + ], + check=True, + capture_output=True, + ) + with mock.patch.object( + orc_profiles, + "resolve_trusted_executable", + return_value=git_path, + ): + with self.assertRaisesRegex( + orc_profiles.ConfigError, + "exactly one push URL", + ): + orc_profiles.resolve_release_remote(root) + + with self.assertRaisesRegex(orc_profiles.ConfigError, "safe remote URL"): + orc_profiles._remote_url_facts( + "https://git.example.invalid:notaport/repo.git", + "test remote", + ) + scp = orc_profiles._remote_url_facts( + "git@git.yumee.top:laily/musicpilot.git", + "scp remote", + ) + ssh = orc_profiles._remote_url_facts( + "ssh://git@git.yumee.top/laily/musicpilot.git", + "ssh remote", + ) + self.assertEqual(scp["canonical"], ssh["canonical"]) + with self.assertRaisesRegex(orc_profiles.ConfigError, "local host"): + orc_profiles._remote_url_facts( + "https://localhost/laily/musicpilot.git", + "local remote", + ) + + with mock.patch.object( + orc_profiles, + "_origin_urls", + side_effect=[ + ["https://github.com/example/project.git"], + ["https://github.com/example/project.git"], + ], + ), mock.patch.object( + orc_profiles, + "resolve_trusted_executable", + return_value=git_path, + ): + github = orc_profiles.resolve_release_remote(Path("/tmp/project")) + self.assertEqual( + github["networkHosts"], + ["github.com", "api.github.com", "uploads.github.com"], + ) + def test_ssh_auth_requires_a_trusted_user_socket(self) -> None: with tempfile.TemporaryDirectory() as temporary: root = Path(temporary) @@ -410,6 +534,19 @@ class OrcSkillTests(unittest.TestCase): check=True, capture_output=True, ) + subprocess.run( + [ + str(git_path), + "-C", + str(root), + "remote", + "add", + "origin", + "https://git.yumee.top/laily/project.git", + ], + check=True, + capture_output=True, + ) fake_codex = root / "trusted-codex" / "codex" fake_codex.parent.mkdir() fake_codex.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8") @@ -474,6 +611,14 @@ class OrcSkillTests(unittest.TestCase): stage_level="low", ) + release_plan = orc_profiles.build_launch_plan( + project_root=root, + worktree=root, + stage="release", + host_cli="codex", + stage_level="low", + ) + self.assertEqual(plan["argv"][0], str(fake_codex)) self.assertEqual(plan["config"]["path"], str(CONFIG)) self.assertEqual(cursor_plan["argv"][0], str(fake_cursor)) @@ -481,7 +626,20 @@ class OrcSkillTests(unittest.TestCase): self.assertEqual(cursor_plan["argv"][1:3], ["--model", "auto"]) self.assertIn("--auto-review", cursor_plan["argv"]) self.assertEqual(cursor_plan["modelAuth"], "cursor-login") + self.assertEqual(cursor_plan["releaseRemote"]["host"], "git.yumee.top") self.assertIn("--host-cli", cursor_plan["launcherArgv"]) + release_args = release_plan["workerArgs"] + self.assertIn("sandbox_workspace_write.network_access=true", release_args) + self.assertIn("features.network_proxy.enabled=true", release_args) + self.assertIn("features.network_proxy.allow_upstream_proxy=false", release_args) + self.assertIn("features.network_proxy.unix_sockets={}", release_args) + self.assertIn( + 'features.network_proxy.domains={ "git.yumee.top" = "allow" }', + release_args, + ) + release_approval = release_args.index("--ask-for-approval") + self.assertEqual(release_args[release_approval + 1], "on-request") + self.assertIn('approvals_reviewer="auto_review"', release_args) self.assertEqual(plan["terminalCreateArgv"][0], str(fake_orca)) self.assertEqual(plan["worktree"], str(root)) self.assertEqual(plan["selectionSource"], "request.stage")