feat(orc): centralize host-aware routing

This commit is contained in:
2026-08-01 20:04:55 +08:00
parent 34bbb97406
commit f5bf35c722
9 changed files with 499 additions and 275 deletions
+22 -14
View File
@@ -1,7 +1,7 @@
# orc # orc
ORC 是显式调用的工程编排入口:把开发、版本发布、DEB 和 Docker 任务拆成阶段, ORC 是显式调用的薄路由器:只把开发、版本发布、DEB 和 Docker 意图映射成阶段,
交给对应 Skill,并为每个执行 Agent 选择 `low``mid``high` 档位 按静态配置选择 `low``mid``high` worker,再交给对应 Skill
## 什么时候使用 ## 什么时候使用
@@ -16,13 +16,14 @@ ORC 是显式调用的工程编排入口:把开发、版本发布、DEB 和 Do
- Orca 正在运行并启用了 orchestration。 - Orca 正在运行并启用了 orchestration。
- 安装本次需要的 `$ack``$manage-release``$deb-publisher` - 安装本次需要的 `$ack``$manage-release``$deb-publisher`
`$publish-docker-image` `$publish-docker-image`
- 运行 `$orc 初始化` 生成 `docs/orc/config.yaml`,确认三档对应的精确模型。 - ORC 直接使用 skill 内共享的 `config.yaml`,不需要在每个项目初始化配置。修改这份
- `/usr/bin/python3`;ORC v1 配置必须保持为内置模板使用的 JSON-compatible YAML 配置会影响所有项目;旧的项目级 `docs/orc/config.yaml` 不再参与解析
- `/usr/bin/python3`ORC v2 配置必须保持为 JSON-compatible YAML。
resolver 以 `-I -S` 隔离模式运行,不加载项目模块、用户 site-packages 或第三方解析器。 resolver 以 `-I -S` 隔离模式运行,不加载项目模块、用户 site-packages 或第三方解析器。
- ORC v1 worker 使用 `workspace-write` sandbox,以便发送 Orca lifecycle 消息; - ORC v2 worker 使用 `workspace-write` sandbox,以便发送 Orca lifecycle 消息;
只读任务会在阶段 prompt 中禁止文件修改。 只读任务会在阶段 prompt 中禁止文件修改。
## Agent 档位 ## Worker 档位
| 档位 | 典型任务 | | 档位 | 典型任务 |
|------|----------| |------|----------|
@@ -31,27 +32,34 @@ ORC 是显式调用的工程编排入口:把开发、版本发布、DEB 和 Do
| `high` | 需求理解、跨系统改动、异常恢复和高风险裁决 | | `high` | 需求理解、跨系统改动、异常恢复和高风险裁决 |
档位不是权限。三个档位仍受各自 profile 和下游 Skill 的授权边界约束。 档位不是权限。三个档位仍受各自 profile 和下游 Skill 的授权边界约束。
当前 ORC v1 的结构化启动适配器支持 Codex worker;其它 Agent CLI 需要独立适配器, 这些档位只选择下游 worker,不会切换当前 ORC Coordinator 已经使用的模型。
不会通过自由命令接入。 结构化启动适配器支持 Codex 与 Cursor worker,并固定跟随当前宿主:从 Codex 调用就使用
Codex,从 Cursor 调用就使用 Cursor。共享配置为两个 CLI 分别维护三档模型,不设置跨宿主
默认值。
## 使用示例 ## 使用示例
```text ```text
$orc high 修复登录问题,验证通过后发布新版本。 $orc high 修复登录问题,验证通过后发布新版本。
$orc code=high release=mid docker=low,完成修复、发版并推送镜像。 $orc code=high release=mid docker=low,完成修复、发版并推送镜像。
$orc low,检查并准备下一个版本,不执行远端写操作。
$orc mid 继续上次中断的 v1.4.0 发布流程。 $orc mid 继续上次中断的 v1.4.0 发布流程。
``` ```
阶段级档位优先于全局档位。用户显式指定后,ORC 不会静默改档;能力不足时会暂停并 阶段级档位优先于全局档位;未指定时只读取 `stageDefaults` / `defaultLevel`。ORC 不根据
请求确认 任务复杂度动态升降档;worker 报告能力不足时,ORC 只转发请求。
## Agent 会做什么 ## Agent 会做什么
1. 识别开发、源码发布、DEB 与 Docker 阶段及其依赖。 1. 识别开发、源码发布、DEB 与 Docker 意图,并按固定表建立阶段依赖。
2. 解析每个阶段的 Agent profile,把配置快照、可信 Codex executable 和真实 Git 2. 从当前 Agent 的系统身份锁定宿主 CLI,再解析每个阶段的 profile,把共享配置快照、
可信 Agent CLI executable 和真实 Git
worktree 绑定成 launch fingerprint,并展示计划和外部写入边界。 worktree 绑定成 launch fingerprint,并展示计划和外部写入边界。
3. 通过 Orca 分发给对应 Skill监督完成消息、异常和决策门。 3. 通过 Orca 分发给对应 Skill转发完成消息、异常和决策门。
4. 汇总每个阶段的实际状态、证据和安全恢复入口 4. 按下游 Skill 声明的证据字段汇总状态和恢复入口,不重新做领域判断
ORC 不分析实现、不建议版本、不评估发布风险、不选择合并或构建方案;这些工作全部属于
下游 Skill。
## 如何判断完成 ## 如何判断完成
+101 -67
View File
@@ -1,95 +1,128 @@
--- ---
name: orc name: orc
description: >- description: >-
显式编排开发、源码版本发布、DEB 和 Docker 产物任务,把阶段分发给对应 Skill 作为薄路由器显式识别开发、源码版本发布、DEB 和 Docker 意图,把阶段分发给对应
并用 low、mid、high 选择执行 Agent 档位。仅在用户显式调用 $orc 或 /orc,要求 Skill,并按静态规则选择 low、mid、high 执行档位。仅在用户显式调用 $orc 或 /orc,
跨阶段协调、指定 Agent 级别、监督多个 worker 或继续 ORC 编排时使用。 要求跨阶段协调、指定 Agent 级别、监督多个 worker 或继续 ORC 编排时使用。
--- ---
# ORC 工程编排入口 # ORC 工程编排入口
当前会话担任 Coordinator:拆分阶段、选择档位、派发 worker、监督依赖与决策门, 当前会话担任薄路由器(Thin Coordinator):只识别阶段、宿主 CLI、静态档位、依赖和
但不替代下游 Skill 执行其领域流程。 授权边界,然后派发 worker、转发决策门并汇总结构化状态。ORC 不做领域判断,不替代
下游 Skill 执行、分析或复核其领域流程。
开始时解析当前 `SKILL.md` 所在目录,记为 `<orc-skill-dir>`;解析真实项目根目录, 开始时解析当前 `SKILL.md` 所在目录,记为 `<orc-skill-dir>`;解析真实项目根目录,
优先使用 `git rev-parse --show-toplevel`项目配置固定为 优先使用 `git rev-parse --show-toplevel`所有项目共享唯一配置
`<project-root>/docs/orc/config.yaml` `<orc-skill-dir>/config.yaml`,不得在项目中创建 `docs/orc/config.yaml` 或其它配置副本
## 选择模式 ## 选择模式
- 用户要求初始化 ORC执行“初始化”。 - 用户要求初始化 ORC说明 ORC 已改为共享配置、不需要项目初始化,然后执行“检查”。
- 用户要求检查 ORC、档位或运行环境:执行“检查”。 - 用户要求检查 ORC、CLI、档位或运行环境:执行“检查”。
- 用户明确要求修改 ORC 的共享模型或档位默认值:执行“修改共享配置”。
- 用户要求用 ORC 完成任务:执行“编排”。 - 用户要求用 ORC 完成任务:执行“编排”。
不要静默初始化,也不要在配置缺失或无效时退回裸命令或当前会话直接执行。 配置或运行时无效时 fail closed,不退回裸命令或当前会话直接执行。
## 初始化 ## 薄路由器边界
1.`docs/orc/config.yaml` 已存在,停止创建并转入“检查”,不得覆盖。 ORC 只执行以下机械步骤:
2. 确认 `docs/orc/` 和目标文件都不是 symlink,再从
`<orc-skill-dir>/templates/config.template.yaml` 以 create-only 方式创建配置,不覆盖
或跟随既有路径。ORC v1 配置必须保持 JSON-compatible YAML,只由隔离的 Python
标准库解析;模板中的 `.` 表示当前项目根。模型 ID 必须由用户或项目的可信配置确认,
不能从任务文本猜测。
3. 运行:
```bash 1. 把用户明确表达的意图映射到 `code``release``deb``docker`
/usr/bin/python3 -I -S <orc-skill-dir>/scripts/resolve_profile.py validate \ 2. 从系统身份映射宿主 CLI,并按显式覆盖或共享配置解析档位。
<project-root>/docs/orc/config.yaml 3. 根据固定路由表建立任务依赖,生成满足契约的 worker prompt 并派发。
``` 4. 转发 `decision_gate` / `escalation`,按下游 Skill 的完成证据汇总状态。
4. 报告三个档位的 CLI、模型、reasoning、权限和阶段默认值。除非用户明确要求, 不得阅读项目实现来形成技术判断,不得决定版本号、实现方案、测试策略、发布风险、
不安装下游 Skill、不创建终端、不修改 Agent 全局配置。 合并方式或产物策略,不得代替 worker 执行命令。意图无法映射时报告范围外;缺少派发所需
的关键输入时建立 decision gate。领域问题原样交给对应下游 Skill,不由 ORC 推理补全。
## 共享配置
`<orc-skill-dir>/config.yaml` 是 ORC profile 的 SSOT,对所有项目生效。配置使用
JSON-compatible YAML,并由隔离的 Python 标准库解析。它固定包含:
- `cliPolicy: current-host`Codex 宿主只启动 Codex workerCursor 宿主只启动 Cursor worker。
- `defaultLevel``stageDefaults`:只用于下游 worker 的 `low``mid``high` 默认选择,
不表示 ORC Coordinator 自身的模型档位。
- `worktreePolicy: registered-same-repository`:允许当前仓库中已注册且身份一致的 worktree。
- `profiles.codex``profiles.cursor-agent`:两个 CLI 各自完整的三档 profile。
不要把项目路径、命令、argv、环境变量、secret、hook 或 shell 片段写入共享配置。
旧项目若残留 `docs/orc/config.yaml`,resolver 会忽略它;没有用户明确清理授权时不要删除。
## 修改共享配置
只有用户明确要求修改 ORC 的全局档位或模型时才编辑
`<orc-skill-dir>/config.yaml`。修改前说明它会影响所有项目;模型 ID 必须来自用户输入、
目标 CLI 的模型列表或其它可信配置,不能从任务文本猜测。修改后运行:
```bash
/usr/bin/python3 -I -S <orc-skill-dir>/scripts/resolve_profile.py validate
```
报告两个 CLI 的三个档位、reasoning、权限策略、阶段默认值和宿主 CLI 策略。不要创建
项目配置。
## 检查 ## 检查
1. 校验 `docs/orc/config.yaml`,确认只存在 `low`、`mid`、`high` 三档 1. 运行共享配置校验,确认 `codex``cursor-agent` 都完整配置 `low``mid``high`
2. 检查 `orca status --json`,并确认 orchestration 命令可用。 2. 检查 `orca status --json`,并确认 orchestration 命令可用。
3. 确认本次所需下游 Skill 已安装:`ack`、`manage-release`、`deb-publisher`、 3. 从当前 Agent 的系统身份确定宿主 CLI:Codex 映射为 `codex`Cursor 映射为
`cursor-agent`。身份不明确时停止,不从用户任务文本、默认值或已安装 executable 猜测。
确认宿主 CLI 可用;在 Cursor 中还要用 `cursor-agent --list-models`
核对精确模型 ID 对当前账号可见。
4. 确认本次所需下游 Skill 已安装:`ack``manage-release``deb-publisher`
`publish-docker-image`。只检查实际会用到的项。 `publish-docker-image`。只检查实际会用到的项。
4. 解析 profile 时把项目根和目标 worktree 一并交给 resolver;只有 resolver 验证目标 5. 解析 profile 时把项目根和目标 worktree 一并交给 resolver;只有 resolver 验证目标
命中 `allowedWorktrees`、属于当前 Git 仓库且身份稳定后才可创建终端。不得只做文本 当前 Git 仓库已注册的 worktree 且身份稳定后才可创建终端。不得只做文本比较或跳过
比较或跳过机器校验。 机器校验。
5. 任何 profile、Skill、运行时或 worktree 不可用时 fail closed;不得选择相邻档位 6. 任何 profile、CLI、Skill、运行时或 worktree 不可用时 fail closed;不得切换其它 CLI
复用身份不明的终端或手写替代流程。 相邻档位、复用身份不明的终端或手写替代流程。
## 编排 ## 编排
1. 读取 [routing.md](references/routing.md),把请求拆成 `code``release``deb` 1. 读取 [routing.md](references/routing.md),把请求拆成 `code``release``deb`
`docker` 阶段。没有匹配下游 Skill 的工作留在范围外并明确报告。 `docker` 阶段。没有匹配下游 Skill 的工作留在范围外并明确报告。
2. 锁定用户授权最远动作、目标版本、产物目标、源 commit/tag 与停止点。ORC 的调用 2. 原样提取用户明确给出的授权最远动作、目标版本、产物目标、源 commit/tag 与停止点
本身不扩大 push、合并、打 tag、上传或部署权限;每个下游 Skill 的授权边界继续生效。 未给出的领域参数保持未指定并交给下游 Skill。ORC 的调用本身不扩大 push、合并、
3. 解析档位:阶段级指定 > 全局指定 > `stageDefaults` > `defaultLevel`。只接受 打 tag、上传或部署权限。
`low`、`mid`、`high`;用户显式指定后不得静默升降级。若该档位不足以安全完成, 3. 从系统身份锁定当前宿主 CLI。运行在 Codex 时传 `--host-cli codex`;运行在 Cursor 时传
建立 decision gate,等待用户改档或缩小范围。 `--host-cli cursor-agent`。宿主 CLI 不是任务选项,即使用户文本点名另一个 CLI 也不得
4. 用户未指定档位时采用以下判断:清晰、机械的构建或上传可用 `low`;常规版本流程 跨宿主启动;身份不明确时 fail closed。
用 `mid`;需求理解、跨系统改动、恢复中断流程、目标含糊或高风险裁决用 `high`。 4. 解析档位:阶段级指定 > 全局指定 > `stageDefaults` > `defaultLevel`。只接受
5. 对每个阶段运行 profile resolver。全局档位用 `--global-level`,阶段档位用 `low``mid``high`;用户显式指定后不得静默升降级。ORC 不根据任务复杂度动态判断
`--stage-level`;项目根与目标 worktree 必须使用规范绝对路径。模型认证默认复用 档位;worker 报告能力不足时,只转发 escalation 或 decision gate。
`codex-login`,只有明确使用对应环境凭据时才选 `openai` 或 `azure-openai`。远端认证 5. 对每个阶段运行 profile resolver。宿主用必填的 `--host-cli`,全局档位用
默认 `none`;只有目标 provider 与 transport 已确认时,才选择一个精确的 `--global-level`,阶段
`github-token`、`gitlab-token`、`gitea-token`、`forgejo-token`、`ssh-agent` 或 档位用 `--stage-level`;项目根与目标 worktree 必须使用规范绝对路径。Codex 默认复用
`deb-token`。不得把多个 provider 凭据一起交给 worker。resolver 不存在静默 `codex-login`Cursor 默认复用 `cursor-login`;只有明确使用环境凭据时才分别选择
fallback `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
```bash ```bash
/usr/bin/python3 -I -S <orc-skill-dir>/scripts/resolve_profile.py resolve \ /usr/bin/python3 -I -S <orc-skill-dir>/scripts/resolve_profile.py resolve \
<project-root>/docs/orc/config.yaml --stage <stage> \ --stage <stage> \
--project-root <absolute-project-root> \ --project-root <absolute-project-root> \
--worktree <absolute-target-worktree> \ --worktree <absolute-target-worktree> \
--host-cli <codex|cursor-agent> \
[--global-level <low|mid|high>] [--stage-level <low|mid|high>] \ [--global-level <low|mid|high>] [--stage-level <low|mid|high>] \
[--model-auth <codex-login|openai|azure-openai>] \ [--model-auth <codex-login|openai|azure-openai|cursor-login|cursor-api-key>] \
[--remote-auth <exact-provider-or-transport>] [--remote-auth <exact-provider-or-transport>]
``` ```
6. 核对 resolver 返回的 `launchFingerprint`、绝对 executable、worktree identity 和选择 6. 核对 resolver 返回的 `launchFingerprint`、绝对 executable、worktree identity、CLI 与
来源,再读取 [orca-adapter.md](references/orca-adapter.md),把阶段组织为 Orca task DAG。 档位选择来源,再读取 [orca-adapter.md](references/orca-adapter.md),把阶段组织为 Orca
worker prompt 必须显式写出对应 `$skill`、阶段范围、输入 revision、用户授权边界、 task DAG。worker prompt 必须显式写出对应 `$skill`、阶段范围、输入 revision、用户授权
验收证据和依赖结果;下游 Skill 无需知道 ORC。 边界、验收证据和依赖结果;下游 Skill 无需知道 ORC。
7. 监督 `worker_done`、`escalation` 与 `decision_gate`。`worker_done` 只代表该 worker 7. 监督 `worker_done`、`escalation` 与 `decision_gate`。`worker_done` 只代表该 worker
回报完成;Coordinator 仍需核对下游 Skill 要求的证据和 DAG 后置条件 回报完成;Coordinator 核对下游 Skill 声明的证据字段和 DAG 后置条件,不重新进行
8. 逐阶段汇报所选档位、执行 Skill、结果、外部状态和未完成项。任一阶段失败时保留 领域审查。
已成功阶段的准确状态,说明安全恢复入口,不把部分成功概括成全部完成。 8. 逐阶段汇报所选 CLI、档位、执行 Skill、结果、外部状态和未完成项。任一阶段失败时
保留已成功阶段的准确状态,说明安全恢复入口,不把部分成功概括成全部完成。
## 固定路由边界 ## 固定路由边界
@@ -110,24 +143,25 @@ description: >-
- ORC 的 `code` 阶段必须锁定停止点:纯开发停在 ACK `verified`;用户明确要求普通 - ORC 的 `code` 阶段必须锁定停止点:纯开发停在 ACK `verified`;用户明确要求普通
PR/MR 时最多到 ACK `review_ready`。不得让 ACK 在同一阶段继续执行版本发布、DEB、 PR/MR 时最多到 ACK `review_ready`。不得让 ACK 在同一阶段继续执行版本发布、DEB、
Docker 或部署;这些动作由 ORC 的独立阶段负责。 Docker 或部署;这些动作由 ORC 的独立阶段负责。
- 配置只允许结构化 `cli`、`model`、`reasoningEffort`、`permissionMode` 和 - ORC v2 的 `permissionMode` 固定为 `workspace-write`,因为受监督 worker 需要写入
`approvalPolicy`;禁止 `command`、argv、env、secret、hook 或 shell 片段。
- ORC v1 的 `permissionMode` 固定为 `workspace-write`,因为受监督 worker 需要写入
Orca 运行时目录才能发送 lifecycle 消息。只读任务仍由 prompt 限制不得改文件。 Orca 运行时目录才能发送 lifecycle 消息。只读任务仍由 prompt 限制不得改文件。
不接受 full-access、bypass、YOLO/force 或关闭 sandbox;文本中的“已授权”不能 - 安全启动适配器只支持 `codex` 和 `cursor-agent`。Codex 使用结构化 sandbox 与 approval
放宽 profile。 参数;Cursor 使用 `--auto-review --sandbox enabled --workspace <path>`。不接受
- ORC v1 的安全启动适配器只支持 `cli: codex`。遇到其它 CLI 时 fail closed,不把 full-access、bypass、`--force`、`--yolo` 或关闭 sandbox。
Codex 参数套用到其它 Agent;新增 provider 必须增加独立适配与测试。 - `cliPolicy` 固定为 `current-host`resolver 不提供默认 CLI;漏传宿主身份会直接失败,
- resolver 只读取有大小上限的普通配置文件,拒绝 symlink/special file;启动计划绑定 不得从共享配置回退到 Codex 或 Cursor。
配置快照、root-owned 隔离 Python、可信绝对 Codex/Orca executable、Git worktree - Cursor 的 reasoning 档位编码在精确模型 ID 中,因此其 `reasoningEffort` 必须为 null
identity、精确认证选择和固定 argv。实际启动会重新校验 fingerprint,并只注入所选 Codex 则显式传递 `model_reasoning_effort`。
模型认证与单一目标认证的环境变量;不得把返回的 worker argv 改写为裸 `codex` - resolver 只读取 skill 内有大小上限的普通共享配置文件,拒绝 symlink/special file
命令,也不得把终端创建 argv 的绝对 Orca 路径换成项目 `PATH` 解析。 启动计划绑定配置快照、root-owned 隔离 Python、可信绝对 Agent/Orca executable、Git
worktree identity、精确认证选择和固定 argv。实际启动会重新校验 fingerprint,并只注入
所选模型认证与单一目标认证的环境变量;不得改写 resolver 返回的 worker argv,也不得
把终端创建 argv 的绝对 Orca 路径换成项目 `PATH` 解析。
- 不把模型档位当作权限。`high` 不自动获得更多文件、凭据、网络或远端写权限。 - 不把模型档位当作权限。`high` 不自动获得更多文件、凭据、网络或远端写权限。
- 不执行 `orca orchestration reset`,除非用户明确要求放弃全部相关运行时状态。 - 不执行 `orca orchestration reset`,除非用户明确要求放弃全部相关运行时状态。
## 完成标准 ## 完成标准
每个计划阶段都有明确下游 Skill、Agent 档位、输入 revision、授权边界和可核对结果; 每个计划阶段都有明确下游 Skill、Agent CLI、档位、输入 revision、授权边界和可核对结果;
DAG 中所有必要阶段完成,或失败阶段具有准确状态与恢复入口。ACK 和其它下游 Skill DAG 中所有必要阶段完成,或失败阶段具有准确状态与恢复入口。ACK 和其它下游 Skill
保持独立且不存在对 ORC 的反向引用。 保持独立且不存在对 ORC 的反向引用。
+2 -2
View File
@@ -1,6 +1,6 @@
interface: interface:
display_name: "ORC" display_name: "ORC"
short_description: "按阶段编排开发、版本发布与产物构建,并选择 Agent 档位" short_description: "用 Codex 或 Cursor 分档编排开发、发布与产物任务"
default_prompt: "Use $orc to coordinate this engineering task with explicit low, mid, or high worker levels." default_prompt: "Use $orc to coordinate this engineering task on the current host CLI with a low, mid, or high worker level."
policy: policy:
allow_implicit_invocation: false allow_implicit_invocation: false
+54
View File
@@ -0,0 +1,54 @@
{
"version": 2,
"cliPolicy": "current-host",
"defaultLevel": "mid",
"stageDefaults": {
"code": "high",
"release": "mid",
"deb": "low",
"docker": "low"
},
"worktreePolicy": "registered-same-repository",
"profiles": {
"codex": {
"low": {
"model": "gpt-5.6-luna",
"reasoningEffort": "low",
"permissionMode": "workspace-write",
"approvalPolicy": "never"
},
"mid": {
"model": "gpt-5.6-terra",
"reasoningEffort": "medium",
"permissionMode": "workspace-write",
"approvalPolicy": "never"
},
"high": {
"model": "gpt-5.6-sol",
"reasoningEffort": "high",
"permissionMode": "workspace-write",
"approvalPolicy": "never"
}
},
"cursor-agent": {
"low": {
"model": "auto",
"reasoningEffort": null,
"permissionMode": "workspace-write",
"approvalPolicy": "auto-review"
},
"mid": {
"model": "gpt-5.6-terra-medium",
"reasoningEffort": null,
"permissionMode": "workspace-write",
"approvalPolicy": "auto-review"
},
"high": {
"model": "gpt-5.6-sol-high",
"reasoningEffort": null,
"permissionMode": "workspace-write",
"approvalPolicy": "auto-review"
}
}
}
}
+14 -6
View File
@@ -16,9 +16,12 @@ orca orchestration inbox --limit 20 --json
## 解析安全启动计划 ## 解析安全启动计划
对每个阶段调用 `resolve_profile.py resolve`,同时传入规范绝对 `--project-root` 对每个阶段调用 `resolve_profile.py resolve`,同时传入规范绝对 `--project-root`
`--worktree`,以及本阶段精确的 `--model-auth``--remote-auth`。JSON 结果包含选择来源、 `--worktree` 和从当前 Agent 系统身份确定的必填 `--host-cli`,以及本阶段精确的
结构化 profile、认证选择、配置与 worktree identity、可信 Python/Codex/Orca executable、 `--model-auth``--remote-auth`
resolver 始终读取 skill 内共享的 `config.yaml`,不接受项目配置路径。JSON 结果包含
选择来源、结构化 profile、认证选择、配置与 worktree identity、可信 Python/Agent
CLI/Orca executable、
`launchFingerprint`、固定 worker/launcher argv,以及完整的 `terminalCreateArgv` `launchFingerprint`、固定 worker/launcher argv,以及完整的 `terminalCreateArgv`
`terminalCreateShellCommand` `terminalCreateShellCommand`
@@ -31,11 +34,16 @@ orca orchestration inbox --limit 20 --json
同时暴露多个 token 或 SSH agent。需要代理、额外凭据或 Docker registry env secret 时 同时暴露多个 token 或 SSH agent。需要代理、额外凭据或 Docker registry env secret 时
fail closed,由用户确认新的最小权限适配,不继承 ambient proxy/credential 变量。 fail closed,由用户确认新的最小权限适配,不继承 ambient proxy/credential 变量。
- resolver 失败时停止;不改用默认模型、邻近档位或已有未知终端。 - resolver 失败时停止;不改用默认模型、邻近档位或已有未知终端。
-重复手写 worktree 检查;resolver 已拒绝 root、不存在、非规范、含 symlink、未注册、 -从任务文本、可执行文件存在性或共享配置猜宿主;Codex 传 `--host-cli codex`Cursor
跨仓库或不在 allowlist 的目标,并把身份写入 fingerprint `--host-cli cursor-agent`。宿主身份不明确时停止
- ORC v1 profile 必须使用 `workspace-write``read-only` sandbox 可能阻止 Orca CLI - 不重复手写 worktree 检查;resolver 已拒绝 root、不存在、非规范、含 symlink、未注册
或跨仓库的目标,并把身份写入 fingerprint。
- ORC v2 profile 必须使用 `workspace-write``read-only` sandbox 可能阻止 Orca CLI
写入自身运行时或挂载目录,导致 worker 无法发送 `worker_done`;只读工作通过 task 写入自身运行时或挂载目录,导致 worker 无法发送 `worker_done`;只读工作通过 task
spec 限制,而不是换成无法完成 lifecycle 的 sandbox。 spec 限制,而不是换成无法完成 lifecycle 的 sandbox。
- Codex 与 Cursor 使用不同的固定 argv 适配器。Cursor 只允许 `--auto-review`、启用的
sandbox 和精确 workspace;不得把 Codex 参数套给 Cursor,也不得给 Cursor 追加
`--force``--yolo`
## 创建与派发 ## 创建与派发
+7 -3
View File
@@ -1,7 +1,7 @@
# ORC 阶段路由 # ORC 阶段路由
ORC 只负责拆分、依赖、档位和监督。领域步骤、授权检查和完成标准由下游 Skill 自己 ORC 是薄路由器,只负责意图映射、依赖、静态档位和结构化状态汇总。它不判断版本号、
决定。 实现方案、发布风险或产物策略;领域步骤、授权检查和完成标准由下游 Skill 自己决定。
## 路由表 ## 路由表
@@ -15,6 +15,10 @@ ORC 只负责拆分、依赖、档位和监督。领域步骤、授权检查和
没有匹配项时不要临时扩写某个 Skill 的职责,也不要让 ORC 自己模仿领域流程。报告缺少 没有匹配项时不要临时扩写某个 Skill 的职责,也不要让 ORC 自己模仿领域流程。报告缺少
的能力,由用户决定直接执行、安装新 Skill 或另行设计。 的能力,由用户决定直接执行、安装新 Skill 或另行设计。
路由只依据用户明确表达的目标。缺少目标版本、revision、产物目标或授权停止点时,保留
为未指定并交给下游 Skill;只有缺少创建 task 所必需的项目或阶段身份时才建立
decision gate。不得为了填满 worker prompt 而分析代码、推断版本或设计执行方案。
## 拆分规则 ## 拆分规则
1. 先从用户请求提取最终结果,再拆出真正需要的阶段;不要因为安装了某个 Skill 就 1. 先从用户请求提取最终结果,再拆出真正需要的阶段;不要因为安装了某个 Skill 就
@@ -52,7 +56,7 @@ deb ($deb-publisher) || docker ($publish-docker-image)
release ($manage-release) release ($manage-release)
``` ```
依赖不是固定模板,但 ORC v1 不拆分一个 `manage-release` 生命周期。若项目要求在打开 依赖不是固定模板,但 ORC v2 不拆分一个 `manage-release` 生命周期。若项目要求在打开
release PR 与合并之间插入 DEB/Docker gate,当前 task 粒度无法安全表达该中间里程碑; release PR 与合并之间插入 DEB/Docker gate,当前 task 粒度无法安全表达该中间里程碑;
在打开 PR 前建立 decision gate 并报告该流程暂不支持,不得用循环依赖或两个 release 在打开 PR 前建立 decision gate 并报告该流程暂不支持,不得用循环依赖或两个 release
worker 临时拼接。 worker 临时拼接。
+133 -105
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Validate ORC config and build a worktree-bound Codex launch plan.""" """Validate shared ORC config and build a worktree-bound Agent launch plan."""
from __future__ import annotations from __future__ import annotations
@@ -18,16 +18,17 @@ from typing import Any
LEVELS = ("low", "mid", "high") LEVELS = ("low", "mid", "high")
STAGES = ("code", "release", "deb", "docker") STAGES = ("code", "release", "deb", "docker")
CLIS = ("codex", "cursor-agent")
EXPECTED_EFFORT = {"low": "low", "mid": "medium", "high": "high"} EXPECTED_EFFORT = {"low": "low", "mid": "medium", "high": "high"}
TOP_LEVEL_KEYS = { TOP_LEVEL_KEYS = {
"version", "version",
"cliPolicy",
"defaultLevel", "defaultLevel",
"stageDefaults", "stageDefaults",
"allowedWorktrees", "worktreePolicy",
"profiles", "profiles",
} }
PROFILE_KEYS = { PROFILE_KEYS = {
"cli",
"model", "model",
"reasoningEffort", "reasoningEffort",
"permissionMode", "permissionMode",
@@ -37,7 +38,7 @@ MODEL_RE = re.compile(r"[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}\Z")
MAX_CONFIG_SIZE = 64 * 1024 MAX_CONFIG_SIZE = 64 * 1024
MAX_CONTROL_OUTPUT = 64 * 1024 MAX_CONTROL_OUTPUT = 64 * 1024
CONTROL_TIMEOUT_SECONDS = 15 CONTROL_TIMEOUT_SECONDS = 15
CONFIG_RELATIVE_PATH = Path("docs/orc/config.yaml") SHARED_CONFIG_PATH = Path(__file__).resolve().parents[1] / "config.yaml"
COMMON_ENVIRONMENT_NAMES = { COMMON_ENVIRONMENT_NAMES = {
"COLORTERM", "COLORTERM",
"LANG", "LANG",
@@ -50,6 +51,16 @@ MODEL_AUTH_ENVIRONMENT = {
"codex-login": frozenset(), "codex-login": frozenset(),
"openai": frozenset({"OPENAI_API_KEY"}), "openai": frozenset({"OPENAI_API_KEY"}),
"azure-openai": frozenset({"AZURE_OPENAI_API_KEY"}), "azure-openai": frozenset({"AZURE_OPENAI_API_KEY"}),
"cursor-login": frozenset(),
"cursor-api-key": frozenset({"CURSOR_API_KEY"}),
}
CLI_MODEL_AUTH = {
"codex": frozenset({"codex-login", "openai", "azure-openai"}),
"cursor-agent": frozenset({"cursor-login", "cursor-api-key"}),
}
DEFAULT_MODEL_AUTH = {
"codex": "codex-login",
"cursor-agent": "cursor-login",
} }
REMOTE_AUTH_ENVIRONMENT = { REMOTE_AUTH_ENVIRONMENT = {
"none": frozenset(), "none": frozenset(),
@@ -169,9 +180,11 @@ def validate_config(document: Any) -> dict[str, Any]:
_exact_keys(config, required=TOP_LEVEL_KEYS, path="config") _exact_keys(config, required=TOP_LEVEL_KEYS, path="config")
version = config["version"] version = config["version"]
if isinstance(version, bool) or version != 1: if isinstance(version, bool) or version != 2:
raise ConfigError("version must be integer 1") raise ConfigError("version must be integer 2")
if config["cliPolicy"] != "current-host":
raise ConfigError("cliPolicy must be current-host in ORC v2")
_enum(config["defaultLevel"], LEVELS, "defaultLevel") _enum(config["defaultLevel"], LEVELS, "defaultLevel")
stage_defaults = _mapping(config["stageDefaults"], "stageDefaults") stage_defaults = _mapping(config["stageDefaults"], "stageDefaults")
@@ -183,53 +196,51 @@ def validate_config(document: Any) -> dict[str, Any]:
for stage, level in stage_defaults.items(): for stage, level in stage_defaults.items():
_enum(level, LEVELS, f"stageDefaults.{stage}") _enum(level, LEVELS, f"stageDefaults.{stage}")
allowed = config["allowedWorktrees"] if config["worktreePolicy"] != "registered-same-repository":
if not isinstance(allowed, list) or not allowed: raise ConfigError(
raise ConfigError("allowedWorktrees must be a non-empty list") "worktreePolicy must be registered-same-repository in ORC v2"
seen_worktrees: set[str] = set() )
for index, entry in enumerate(allowed):
if (
not isinstance(entry, str)
or not entry
or entry != entry.strip()
or any(character in entry for character in ("\x00", "\n", "\r"))
):
raise ConfigError(
f"allowedWorktrees[{index}] must be a safe non-empty path"
)
if entry != "." and not Path(entry).is_absolute():
raise ConfigError(
f"allowedWorktrees[{index}] must be '.' or an absolute path"
)
normalized = entry if entry == "." else str(Path(entry).absolute())
if normalized in seen_worktrees:
raise ConfigError(f"allowedWorktrees contains duplicate path: {entry}")
seen_worktrees.add(normalized)
profiles = _mapping(config["profiles"], "profiles") profiles = _mapping(config["profiles"], "profiles")
_exact_keys(profiles, required=set(LEVELS), path="profiles") _exact_keys(profiles, required=set(CLIS), path="profiles")
for level in LEVELS: for cli in CLIS:
profile = _mapping(profiles[level], f"profiles.{level}") cli_profiles = _mapping(profiles[cli], f"profiles.{cli}")
_exact_keys(profile, required=PROFILE_KEYS, path=f"profiles.{level}") _exact_keys(cli_profiles, required=set(LEVELS), path=f"profiles.{cli}")
if profile["cli"] != "codex": for level in LEVELS:
raise ConfigError(f"profiles.{level}.cli must be codex in ORC v1") path = f"profiles.{cli}.{level}"
model = profile["model"] profile = _mapping(cli_profiles[level], path)
if not isinstance(model, str) or not MODEL_RE.fullmatch(model): _exact_keys(profile, required=PROFILE_KEYS, path=path)
raise ConfigError(f"profiles.{level}.model is not a safe exact model ID") model = profile["model"]
expected_effort = EXPECTED_EFFORT[level] if not isinstance(model, str) or not MODEL_RE.fullmatch(model):
if profile["reasoningEffort"] != expected_effort: raise ConfigError(f"{path}.model is not a safe exact model ID")
raise ConfigError( if cli == "codex":
f"profiles.{level}.reasoningEffort must be {expected_effort}" expected_effort = EXPECTED_EFFORT[level]
) if profile["reasoningEffort"] != expected_effort:
if profile["permissionMode"] != "workspace-write": raise ConfigError(
raise ConfigError( f"{path}.reasoningEffort must be {expected_effort}"
f"profiles.{level}.permissionMode must be workspace-write in ORC v1" )
) _enum(
_enum( profile["approvalPolicy"],
profile["approvalPolicy"], {"untrusted", "on-request", "never"},
{"untrusted", "on-request", "never"}, f"{path}.approvalPolicy",
f"profiles.{level}.approvalPolicy", )
) else:
if profile["reasoningEffort"] is not None:
raise ConfigError(f"{path}.reasoningEffort: Cursor requires null")
model_tokens = set(re.split(r"[^a-z0-9]+", model.lower()))
expected_token = "medium" if level == "mid" else level
if model != "auto" and expected_token not in model_tokens:
raise ConfigError(
f"{path}.model must encode the {expected_token} effort level"
)
if profile["approvalPolicy"] != "auto-review":
raise ConfigError(
f"{path}.approvalPolicy must be auto-review for Cursor"
)
if profile["permissionMode"] != "workspace-write":
raise ConfigError(
f"{path}.permissionMode must be workspace-write in ORC v2"
)
return config return config
@@ -260,7 +271,8 @@ def resolve_profile(
stage: str, stage: str,
global_level: str | None = None, global_level: str | None = None,
stage_level: str | None = None, stage_level: str | None = None,
model_auth: str = "codex-login", host_cli: str | None = None,
model_auth: str | None = None,
remote_auth: str = "none", remote_auth: str = "none",
) -> dict[str, Any]: ) -> dict[str, Any]:
_enum(stage, STAGES, "stage") _enum(stage, STAGES, "stage")
@@ -268,9 +280,19 @@ def resolve_profile(
_enum(global_level, LEVELS, "global level") _enum(global_level, LEVELS, "global level")
if stage_level is not None: if stage_level is not None:
_enum(stage_level, LEVELS, "stage level") _enum(stage_level, LEVELS, "stage level")
_enum(model_auth, set(MODEL_AUTH_ENVIRONMENT), "model auth")
_enum(remote_auth, STAGE_REMOTE_AUTH[stage], f"{stage} remote auth") _enum(remote_auth, STAGE_REMOTE_AUTH[stage], f"{stage} remote auth")
if host_cli is None:
raise ConfigError("host CLI is required; resolve it from the current Agent")
selected_cli = _enum(host_cli, CLIS, "host CLI")
cli_source = "runtime.host"
selected_auth = model_auth or DEFAULT_MODEL_AUTH[selected_cli]
_enum(selected_auth, set(MODEL_AUTH_ENVIRONMENT), "model auth")
if selected_auth not in CLI_MODEL_AUTH[selected_cli]:
raise ConfigError(
f"model auth {selected_auth} is not valid for {selected_cli}"
)
if stage_level is not None: if stage_level is not None:
level, source = stage_level, "request.stage" level, source = stage_level, "request.stage"
elif global_level is not None: elif global_level is not None:
@@ -280,25 +302,36 @@ def resolve_profile(
else: else:
level, source = config["defaultLevel"], "config.defaultLevel" level, source = config["defaultLevel"], "config.defaultLevel"
profile = config["profiles"].get(level) profile = config["profiles"][selected_cli].get(level)
if profile is None: if profile is None:
raise ConfigError(f"requested profile does not exist: {level}") raise ConfigError(f"requested profile does not exist: {selected_cli}/{level}")
worker_args = [ if selected_cli == "codex":
"--model", worker_args = [
profile["model"], "--model",
"-c", profile["model"],
f'model_reasoning_effort="{profile["reasoningEffort"]}"', "-c",
"--sandbox", f'model_reasoning_effort="{profile["reasoningEffort"]}"',
profile["permissionMode"], "--sandbox",
"--ask-for-approval", profile["permissionMode"],
profile["approvalPolicy"], "--ask-for-approval",
"--strict-config", profile["approvalPolicy"],
] "--strict-config",
]
else:
worker_args = [
"--model",
profile["model"],
"--auto-review",
"--sandbox",
"enabled",
]
return { return {
"stage": stage, "stage": stage,
"cli": selected_cli,
"cliSelectionSource": cli_source,
"level": level, "level": level,
"selectionSource": source, "selectionSource": source,
"modelAuth": model_auth, "modelAuth": selected_auth,
"remoteAuth": remote_auth, "remoteAuth": remote_auth,
"profile": dict(profile), "profile": dict(profile),
"workerArgs": worker_args, "workerArgs": worker_args,
@@ -400,7 +433,7 @@ def _trusted_executable(path: Path, expected_name: str) -> Path | None:
def resolve_trusted_executable(name: str) -> Path: def resolve_trusted_executable(name: str) -> Path:
if name not in {"codex", "git", "orca"}: if name not in {"codex", "cursor-agent", "git", "orca"}:
raise ConfigError(f"unsupported executable: {name}") raise ConfigError(f"unsupported executable: {name}")
search_paths = trusted_path_entries() search_paths = trusted_path_entries()
if name == "git": if name == "git":
@@ -572,17 +605,10 @@ def validate_worktree(
except ConfigError: except ConfigError:
continue continue
allowed: set[Path] = set() if config["worktreePolicy"] != "registered-same-repository":
for entry in config["allowedWorktrees"]: raise ConfigError("unsupported worktree policy")
allowed.add( if worktree not in registered:
project_root raise ConfigError("target worktree is not registered in the project repository")
if entry == "."
else canonical_directory(entry, "allowed worktree")
)
if not allowed <= registered:
raise ConfigError("allowedWorktrees contains an unregistered Git worktree")
if worktree not in allowed:
raise ConfigError("target worktree is not in allowedWorktrees")
target_top = canonical_directory( target_top = canonical_directory(
_run_control( _run_control(
@@ -631,7 +657,7 @@ def _file_facts(path: Path) -> dict[str, Any]:
def _executable_facts(path: Path) -> dict[str, Any]: def _executable_facts(path: Path) -> dict[str, Any]:
return { return {
**_file_facts(path), **_file_facts(path),
"version": _run_control([str(path), "--version"], "Codex version check"), "version": _run_control([str(path), "--version"], "Agent CLI version check"),
} }
@@ -663,22 +689,19 @@ def _fingerprint(value: dict[str, Any]) -> str:
def build_launch_plan( def build_launch_plan(
config_path: Path,
*, *,
project_root: str | Path, project_root: str | Path,
worktree: str | Path, worktree: str | Path,
stage: str, stage: str,
host_cli: str,
global_level: str | None = None, global_level: str | None = None,
stage_level: str | None = None, stage_level: str | None = None,
model_auth: str = "codex-login", model_auth: str | None = None,
remote_auth: str = "none", remote_auth: str = "none",
) -> dict[str, Any]: ) -> dict[str, Any]:
project = canonical_directory(project_root, "project root") project = canonical_directory(project_root, "project root")
expected_config = project / CONFIG_RELATIVE_PATH _assert_no_symlink_components(SHARED_CONFIG_PATH, "shared config path")
if config_path.absolute() != expected_config: config, config_snapshot = load_config_snapshot(SHARED_CONFIG_PATH)
raise ConfigError(f"config path must be {expected_config}")
_assert_no_symlink_components(expected_config, "config path")
config, config_snapshot = load_config_snapshot(expected_config)
worktree_facts = validate_worktree( worktree_facts = validate_worktree(
config, config,
project_root_value=project, project_root_value=project,
@@ -689,16 +712,17 @@ def build_launch_plan(
stage=stage, stage=stage,
global_level=global_level, global_level=global_level,
stage_level=stage_level, stage_level=stage_level,
host_cli=host_cli,
model_auth=model_auth, model_auth=model_auth,
remote_auth=remote_auth, remote_auth=remote_auth,
) )
selected_credential_names( selected_credential_names(
stage, stage,
model_auth=model_auth, model_auth=profile["modelAuth"],
remote_auth=remote_auth, remote_auth=remote_auth,
) )
codex = resolve_trusted_executable("codex") agent_cli = resolve_trusted_executable(profile["cli"])
executable = _executable_facts(codex) executable = _executable_facts(agent_cli)
orca = resolve_trusted_executable("orca") orca = resolve_trusted_executable("orca")
orca_executable = _file_facts(orca) orca_executable = _file_facts(orca)
python = resolve_trusted_python() python = resolve_trusted_python()
@@ -710,7 +734,7 @@ def build_launch_plan(
**profile, **profile,
**worktree_facts, **worktree_facts,
"config": { "config": {
"path": str(expected_config), "path": str(SHARED_CONFIG_PATH),
**config_snapshot, **config_snapshot,
}, },
"executable": executable, "executable": executable,
@@ -726,14 +750,15 @@ def build_launch_plan(
}, },
} }
fingerprint = _fingerprint(launch_facts) fingerprint = _fingerprint(launch_facts)
worker_argv = [str(codex), *profile["workerArgs"]] worker_argv = [str(agent_cli), *profile["workerArgs"]]
if profile["cli"] == "cursor-agent":
worker_argv.extend(["--workspace", str(worktree_facts["worktree"])])
launcher_argv = [ launcher_argv = [
str(python), str(python),
"-I", "-I",
"-S", "-S",
str(script), str(script),
"_launch", "_launch",
str(expected_config),
"--project-root", "--project-root",
str(project), str(project),
"--worktree", "--worktree",
@@ -747,13 +772,17 @@ def build_launch_plan(
launcher_argv.extend(["--global-level", global_level]) launcher_argv.extend(["--global-level", global_level])
if stage_level is not None: if stage_level is not None:
launcher_argv.extend(["--stage-level", stage_level]) launcher_argv.extend(["--stage-level", stage_level])
if model_auth != "codex-login": launcher_argv.extend(["--host-cli", host_cli])
if model_auth is not None:
launcher_argv.extend(["--model-auth", model_auth]) launcher_argv.extend(["--model-auth", model_auth])
if remote_auth != "none": if remote_auth != "none":
launcher_argv.extend(["--remote-auth", remote_auth]) launcher_argv.extend(["--remote-auth", remote_auth])
terminal_command = shlex.join(launcher_argv) terminal_command = shlex.join(launcher_argv)
worktree_selector = "path:" + str(worktree_facts["worktree"]) worktree_selector = "path:" + str(worktree_facts["worktree"])
terminal_title = f"ORC-{stage}-{profile['level']}-{fingerprint[7:15]}" cli_label = "CODEX" if profile["cli"] == "codex" else "CURSOR"
terminal_title = (
f"ORC-{stage}-{cli_label}-{profile['level']}-{fingerprint[7:15]}"
)
terminal_create_argv = [ terminal_create_argv = [
str(orca), str(orca),
"terminal", "terminal",
@@ -781,10 +810,10 @@ def build_launch_plan(
def execute_launch(args: argparse.Namespace) -> int: def execute_launch(args: argparse.Namespace) -> int:
plan = build_launch_plan( plan = build_launch_plan(
args.config,
project_root=args.project_root, project_root=args.project_root,
worktree=args.worktree, worktree=args.worktree,
stage=args.stage, stage=args.stage,
host_cli=args.host_cli,
global_level=args.global_level, global_level=args.global_level,
stage_level=args.stage_level, stage_level=args.stage_level,
model_auth=args.model_auth, model_auth=args.model_auth,
@@ -802,7 +831,7 @@ def execute_launch(args: argparse.Namespace) -> int:
raise ConfigError("Python executable changed before launch") raise ConfigError("Python executable changed before launch")
executable = Path(plan["executable"]["path"]) executable = Path(plan["executable"]["path"])
if _executable_facts(executable) != plan["executable"]: if _executable_facts(executable) != plan["executable"]:
raise ConfigError("Codex executable changed before launch") raise ConfigError("Agent CLI executable changed before launch")
try: try:
os.execve( os.execve(
executable, executable,
@@ -814,21 +843,20 @@ def execute_launch(args: argparse.Namespace) -> int:
), ),
) )
except OSError as exc: except OSError as exc:
raise ConfigError("Codex worker could not be launched") from exc raise ConfigError("Agent worker could not be launched") from exc
return 1 # pragma: no cover - os.execve does not return on success return 1 # pragma: no cover - os.execve does not return on success
def _add_resolution_arguments(parser: argparse.ArgumentParser) -> None: def _add_resolution_arguments(parser: argparse.ArgumentParser) -> None:
parser.add_argument("config", type=Path)
parser.add_argument("--project-root", required=True) parser.add_argument("--project-root", required=True)
parser.add_argument("--worktree", required=True) parser.add_argument("--worktree", required=True)
parser.add_argument("--stage", required=True, choices=STAGES) parser.add_argument("--stage", required=True, choices=STAGES)
parser.add_argument("--global-level", choices=LEVELS) parser.add_argument("--global-level", choices=LEVELS)
parser.add_argument("--stage-level", choices=LEVELS) parser.add_argument("--stage-level", choices=LEVELS)
parser.add_argument("--host-cli", required=True, choices=CLIS)
parser.add_argument( parser.add_argument(
"--model-auth", "--model-auth",
choices=tuple(MODEL_AUTH_ENVIRONMENT), choices=tuple(MODEL_AUTH_ENVIRONMENT),
default="codex-login",
) )
parser.add_argument( parser.add_argument(
"--remote-auth", "--remote-auth",
@@ -842,8 +870,6 @@ def build_parser() -> argparse.ArgumentParser:
subparsers = parser.add_subparsers(dest="command", required=True) subparsers = parser.add_subparsers(dest="command", required=True)
validate = subparsers.add_parser("validate", help="validate config only") validate = subparsers.add_parser("validate", help="validate config only")
validate.add_argument("config", type=Path)
resolve = subparsers.add_parser("resolve", help="resolve a bound launch plan") resolve = subparsers.add_parser("resolve", help="resolve a bound launch plan")
_add_resolution_arguments(resolve) _add_resolution_arguments(resolve)
@@ -857,20 +883,22 @@ def main(argv: list[str] | None = None) -> int:
args = build_parser().parse_args(argv) args = build_parser().parse_args(argv)
try: try:
if args.command == "validate": if args.command == "validate":
config = load_config(args.config) config = load_config(SHARED_CONFIG_PATH)
result: dict[str, Any] = { result: dict[str, Any] = {
"ok": True, "ok": True,
"config": str(args.config.absolute()), "config": str(SHARED_CONFIG_PATH),
"clis": list(CLIS),
"levels": list(LEVELS), "levels": list(LEVELS),
"stages": list(STAGES), "stages": list(STAGES),
"cliPolicy": config["cliPolicy"],
"defaultLevel": config["defaultLevel"], "defaultLevel": config["defaultLevel"],
} }
elif args.command == "resolve": elif args.command == "resolve":
result = build_launch_plan( result = build_launch_plan(
args.config,
project_root=args.project_root, project_root=args.project_root,
worktree=args.worktree, worktree=args.worktree,
stage=args.stage, stage=args.stage,
host_cli=args.host_cli,
global_level=args.global_level, global_level=args.global_level,
stage_level=args.stage_level, stage_level=args.stage_level,
model_auth=args.model_auth, model_auth=args.model_auth,
-36
View File
@@ -1,36 +0,0 @@
{
"version": 1,
"defaultLevel": "mid",
"stageDefaults": {
"code": "high",
"release": "mid",
"deb": "low",
"docker": "low"
},
"allowedWorktrees": [
"."
],
"profiles": {
"low": {
"cli": "codex",
"model": "gpt-5.6-luna",
"reasoningEffort": "low",
"permissionMode": "workspace-write",
"approvalPolicy": "never"
},
"mid": {
"cli": "codex",
"model": "gpt-5.6-terra",
"reasoningEffort": "medium",
"permissionMode": "workspace-write",
"approvalPolicy": "never"
},
"high": {
"cli": "codex",
"model": "gpt-5.6-sol",
"reasoningEffort": "high",
"permissionMode": "workspace-write",
"approvalPolicy": "never"
}
}
}
+166 -42
View File
@@ -18,7 +18,7 @@ from unittest import mock
REPO_ROOT = Path(__file__).resolve().parents[1] REPO_ROOT = Path(__file__).resolve().parents[1]
ORC_DIR = REPO_ROOT / "skills" / "orc" ORC_DIR = REPO_ROOT / "skills" / "orc"
SCRIPT = ORC_DIR / "scripts" / "resolve_profile.py" SCRIPT = ORC_DIR / "scripts" / "resolve_profile.py"
CONFIG = ORC_DIR / "templates" / "config.template.yaml" CONFIG = ORC_DIR / "config.yaml"
spec = importlib.util.spec_from_file_location("orc_resolve_profile", SCRIPT) spec = importlib.util.spec_from_file_location("orc_resolve_profile", SCRIPT)
assert spec is not None and spec.loader is not None assert spec is not None and spec.loader is not None
@@ -36,7 +36,13 @@ class OrcSkillTests(unittest.TestCase):
self.assertIn("low", skill) self.assertIn("low", skill)
self.assertIn("mid", skill) self.assertIn("mid", skill)
self.assertIn("high", skill) self.assertIn("high", skill)
self.assertIn("只支持 `cli: codex`", skill) self.assertIn("`codex` 和 `cursor-agent`", skill)
self.assertIn("<orc-skill-dir>/config.yaml", skill)
self.assertIn("不得在项目中创建 `docs/orc/config.yaml`", skill)
self.assertIn("薄路由器", skill)
self.assertIn("不做领域判断", skill)
self.assertNotIn("用户未指定档位时采用以下判断", skill)
self.assertNotIn("若该档位不足以安全完成", skill)
for child in ("$ack", "$manage-release", "$deb-publisher", "$publish-docker-image"): for child in ("$ack", "$manage-release", "$deb-publisher", "$publish-docker-image"):
self.assertIn(child, routing) self.assertIn(child, routing)
@@ -64,12 +70,20 @@ class OrcSkillTests(unittest.TestCase):
self.assertIn("最多到", routing) self.assertIn("最多到", routing)
self.assertIn("review_ready", routing) self.assertIn("review_ready", routing)
self.assertIn("当前 task 粒度无法安全表达", routing) self.assertIn("当前 task 粒度无法安全表达", routing)
self.assertRegex(
routing,
r"不判断版本号、\s*实现方案、发布风险或产物策略",
)
def test_template_and_cli_validate(self) -> None: def test_shared_config_and_cli_validate(self) -> None:
config = orc_profiles.load_config(CONFIG) config = orc_profiles.load_config(CONFIG)
self.assertEqual(set(config["profiles"]), {"low", "mid", "high"}) self.assertEqual(config["version"], 2)
self.assertEqual(config["cliPolicy"], "current-host")
self.assertEqual(set(config["profiles"]), {"codex", "cursor-agent"})
for cli in ("codex", "cursor-agent"):
self.assertEqual(set(config["profiles"][cli]), {"low", "mid", "high"})
completed = subprocess.run( completed = subprocess.run(
[sys.executable, str(SCRIPT), "validate", str(CONFIG)], [sys.executable, str(SCRIPT), "validate"],
text=True, text=True,
capture_output=True, capture_output=True,
check=False, check=False,
@@ -77,8 +91,16 @@ class OrcSkillTests(unittest.TestCase):
self.assertEqual(completed.returncode, 0, completed.stderr) self.assertEqual(completed.returncode, 0, completed.stderr)
self.assertIn('"ok": true', completed.stdout) self.assertIn('"ok": true', completed.stdout)
project_config_argument = subprocess.run(
[sys.executable, str(SCRIPT), "validate", str(CONFIG)],
text=True,
capture_output=True,
check=False,
)
self.assertEqual(project_config_argument.returncode, 2)
clean_python = subprocess.run( clean_python = subprocess.run(
[sys.executable, "-I", "-S", str(SCRIPT), "validate", str(CONFIG)], [sys.executable, "-I", "-S", str(SCRIPT), "validate"],
text=True, text=True,
capture_output=True, capture_output=True,
check=False, check=False,
@@ -88,24 +110,39 @@ class OrcSkillTests(unittest.TestCase):
def test_level_precedence_is_stage_then_global_then_config(self) -> None: def test_level_precedence_is_stage_then_global_then_config(self) -> None:
config = orc_profiles.load_config(CONFIG) config = orc_profiles.load_config(CONFIG)
stage = orc_profiles.resolve_profile( stage = orc_profiles.resolve_profile(
config, stage="docker", global_level="mid", stage_level="high" config,
stage="docker",
host_cli="codex",
global_level="mid",
stage_level="high",
) )
global_choice = orc_profiles.resolve_profile( global_choice = orc_profiles.resolve_profile(
config, stage="docker", global_level="mid" config, stage="docker", host_cli="codex", global_level="mid"
)
configured = orc_profiles.resolve_profile(
config, stage="docker", host_cli="codex"
) )
configured = orc_profiles.resolve_profile(config, stage="docker")
fallback_config = copy.deepcopy(config) fallback_config = copy.deepcopy(config)
del fallback_config["stageDefaults"]["docker"] del fallback_config["stageDefaults"]["docker"]
fallback = orc_profiles.resolve_profile(fallback_config, stage="docker") fallback = orc_profiles.resolve_profile(
fallback_config, stage="docker", host_cli="codex"
)
self.assertEqual((stage["level"], stage["selectionSource"]), ("high", "request.stage")) self.assertEqual((stage["level"], stage["selectionSource"]), ("high", "request.stage"))
self.assertEqual((global_choice["level"], global_choice["selectionSource"]), ("mid", "request.global")) self.assertEqual((global_choice["level"], global_choice["selectionSource"]), ("mid", "request.global"))
self.assertEqual((configured["level"], configured["selectionSource"]), ("low", "config.stageDefaults.docker")) self.assertEqual((configured["level"], configured["selectionSource"]), ("low", "config.stageDefaults.docker"))
self.assertEqual((fallback["level"], fallback["selectionSource"]), ("mid", "config.defaultLevel")) self.assertEqual((fallback["level"], fallback["selectionSource"]), ("mid", "config.defaultLevel"))
self.assertEqual(configured["cli"], "codex")
self.assertEqual(configured["cliSelectionSource"], "runtime.host")
with self.assertRaisesRegex(orc_profiles.ConfigError, "host CLI is required"):
orc_profiles.resolve_profile(config, stage="docker")
def test_resolver_builds_fixed_worker_args_without_free_command_fields(self) -> None: def test_resolver_builds_fixed_worker_args_without_free_command_fields(self) -> None:
config = orc_profiles.load_config(CONFIG) config = orc_profiles.load_config(CONFIG)
plan = orc_profiles.resolve_profile(config, stage="release") plan = orc_profiles.resolve_profile(
config, stage="release", host_cli="codex"
)
self.assertEqual(plan["workerArgs"][:2], ["--model", "gpt-5.6-terra"]) self.assertEqual(plan["workerArgs"][:2], ["--model", "gpt-5.6-terra"])
self.assertIn("--strict-config", plan["workerArgs"]) self.assertIn("--strict-config", plan["workerArgs"])
@@ -113,6 +150,40 @@ class OrcSkillTests(unittest.TestCase):
self.assertNotIn("env", plan["profile"]) self.assertNotIn("env", plan["profile"])
self.assertNotIn("command", plan["profile"]) self.assertNotIn("command", plan["profile"])
cursor = orc_profiles.resolve_profile(
config,
stage="release",
host_cli="cursor-agent",
stage_level="low",
)
self.assertEqual(cursor["cli"], "cursor-agent")
self.assertEqual(cursor["cliSelectionSource"], "runtime.host")
self.assertEqual(cursor["modelAuth"], "cursor-login")
self.assertEqual(
cursor["workerArgs"],
[
"--model",
"auto",
"--auto-review",
"--sandbox",
"enabled",
],
)
with self.assertRaisesRegex(orc_profiles.ConfigError, "not valid for cursor-agent"):
orc_profiles.resolve_profile(
config,
stage="release",
host_cli="cursor-agent",
model_auth="openai",
)
with self.assertRaisesRegex(orc_profiles.ConfigError, "not valid for codex"):
orc_profiles.resolve_profile(
config,
stage="release",
host_cli="codex",
model_auth="cursor-api-key",
)
def test_worker_environment_selects_one_model_and_remote_auth(self) -> None: def test_worker_environment_selects_one_model_and_remote_auth(self) -> None:
ambient = { ambient = {
"OPENAI_API_KEY": "openai-secret", "OPENAI_API_KEY": "openai-secret",
@@ -145,6 +216,19 @@ class OrcSkillTests(unittest.TestCase):
): ):
self.assertNotIn(rejected, environment) self.assertNotIn(rejected, environment)
cursor_ambient = {
"CURSOR_API_KEY": "cursor-secret",
"OPENAI_API_KEY": "openai-secret",
}
with mock.patch.dict(os.environ, cursor_ambient, clear=True):
cursor_environment = orc_profiles.worker_environment(
"release",
model_auth="cursor-api-key",
remote_auth="none",
)
self.assertEqual(cursor_environment["CURSOR_API_KEY"], "cursor-secret")
self.assertNotIn("OPENAI_API_KEY", cursor_environment)
with self.assertRaisesRegex(orc_profiles.ConfigError, "deb remote auth"): with self.assertRaisesRegex(orc_profiles.ConfigError, "deb remote auth"):
orc_profiles.worker_environment( orc_profiles.worker_environment(
"deb", "deb",
@@ -240,35 +324,60 @@ class OrcSkillTests(unittest.TestCase):
base = orc_profiles.load_config(CONFIG) base = orc_profiles.load_config(CONFIG)
free_command = copy.deepcopy(base) free_command = copy.deepcopy(base)
free_command["profiles"]["low"]["command"] = "codex --dangerously-bypass" free_command["profiles"]["codex"]["low"]["command"] = "codex --dangerously-bypass"
with self.assertRaisesRegex(orc_profiles.ConfigError, "unknown fields"): with self.assertRaisesRegex(orc_profiles.ConfigError, "unknown fields"):
orc_profiles.validate_config(free_command) orc_profiles.validate_config(free_command)
full_access = copy.deepcopy(base) full_access = copy.deepcopy(base)
full_access["profiles"]["high"]["permissionMode"] = "danger-full-access" full_access["profiles"]["codex"]["high"]["permissionMode"] = "danger-full-access"
with self.assertRaisesRegex(orc_profiles.ConfigError, "workspace-write in ORC v1"): with self.assertRaisesRegex(orc_profiles.ConfigError, "workspace-write in ORC v2"):
orc_profiles.validate_config(full_access) orc_profiles.validate_config(full_access)
read_only = copy.deepcopy(base) read_only = copy.deepcopy(base)
read_only["profiles"]["low"]["permissionMode"] = "read-only" read_only["profiles"]["codex"]["low"]["permissionMode"] = "read-only"
with self.assertRaisesRegex(orc_profiles.ConfigError, "workspace-write in ORC v1"): with self.assertRaisesRegex(orc_profiles.ConfigError, "workspace-write in ORC v2"):
orc_profiles.validate_config(read_only) orc_profiles.validate_config(read_only)
missing_level = copy.deepcopy(base) missing_level = copy.deepcopy(base)
del missing_level["profiles"]["mid"] del missing_level["profiles"]["cursor-agent"]["mid"]
with self.assertRaisesRegex(orc_profiles.ConfigError, "missing fields: mid"): with self.assertRaisesRegex(orc_profiles.ConfigError, "missing fields: mid"):
orc_profiles.validate_config(missing_level) orc_profiles.validate_config(missing_level)
option_model = copy.deepcopy(base) option_model = copy.deepcopy(base)
option_model["profiles"]["low"]["model"] = "--model" option_model["profiles"]["codex"]["low"]["model"] = "--model"
with self.assertRaisesRegex(orc_profiles.ConfigError, "safe exact model ID"): with self.assertRaisesRegex(orc_profiles.ConfigError, "safe exact model ID"):
orc_profiles.validate_config(option_model) orc_profiles.validate_config(option_model)
list_policy = copy.deepcopy(base) list_policy = copy.deepcopy(base)
list_policy["profiles"]["low"]["approvalPolicy"] = ["never"] list_policy["profiles"]["codex"]["low"]["approvalPolicy"] = ["never"]
with self.assertRaisesRegex(orc_profiles.ConfigError, "approvalPolicy"): with self.assertRaisesRegex(orc_profiles.ConfigError, "approvalPolicy"):
orc_profiles.validate_config(list_policy) orc_profiles.validate_config(list_policy)
cursor_effort = copy.deepcopy(base)
cursor_effort["profiles"]["cursor-agent"]["low"]["reasoningEffort"] = "low"
with self.assertRaisesRegex(orc_profiles.ConfigError, "Cursor requires null"):
orc_profiles.validate_config(cursor_effort)
mislabeled_cursor = copy.deepcopy(base)
mislabeled_cursor["profiles"]["cursor-agent"]["low"]["model"] = (
"gpt-5.6-sol-high"
)
with self.assertRaisesRegex(orc_profiles.ConfigError, "encode the low"):
orc_profiles.validate_config(mislabeled_cursor)
project_specific = copy.deepcopy(base)
project_specific["worktreePolicy"] = ["."]
with self.assertRaisesRegex(
orc_profiles.ConfigError,
"registered-same-repository",
):
orc_profiles.validate_config(project_specific)
default_cli = copy.deepcopy(base)
default_cli["cliPolicy"] = "default-codex"
with self.assertRaisesRegex(orc_profiles.ConfigError, "current-host"):
orc_profiles.validate_config(default_cli)
def test_config_reader_rejects_symlinks_and_redacts_parser_input(self) -> None: def test_config_reader_rejects_symlinks_and_redacts_parser_input(self) -> None:
with tempfile.TemporaryDirectory() as temporary: with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary) root = Path(temporary)
@@ -282,16 +391,11 @@ class OrcSkillTests(unittest.TestCase):
malformed = root / "malformed.yaml" malformed = root / "malformed.yaml"
secret_marker = "PRIVATE_MATERIAL_MUST_NOT_APPEAR" secret_marker = "PRIVATE_MATERIAL_MUST_NOT_APPEAR"
malformed.write_text(f"{secret_marker}: [unterminated\n", encoding="utf-8") malformed.write_text(f"{secret_marker}: [unterminated\n", encoding="utf-8")
completed = subprocess.run( with self.assertRaises(orc_profiles.ConfigError) as error:
[sys.executable, str(SCRIPT), "validate", str(malformed)], orc_profiles.load_config(malformed)
text=True, self.assertNotIn(secret_marker, str(error.exception))
capture_output=True,
check=False,
)
self.assertEqual(completed.returncode, 1)
self.assertNotIn(secret_marker, completed.stderr)
def test_bound_plan_enforces_registered_allowlisted_worktree(self) -> None: def test_bound_plan_enforces_registered_same_repository_worktree(self) -> None:
git = shutil.which("git") git = shutil.which("git")
self.assertIsNotNone(git) self.assertIsNotNone(git)
assert git is not None assert git is not None
@@ -306,13 +410,14 @@ class OrcSkillTests(unittest.TestCase):
check=True, check=True,
capture_output=True, capture_output=True,
) )
config_path = root / "docs" / "orc" / "config.yaml"
config_path.parent.mkdir(parents=True)
config_path.write_text(CONFIG.read_text(encoding="utf-8"), encoding="utf-8")
fake_codex = root / "trusted-codex" / "codex" fake_codex = root / "trusted-codex" / "codex"
fake_codex.parent.mkdir() fake_codex.parent.mkdir()
fake_codex.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8") fake_codex.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8")
fake_codex.chmod(0o700) fake_codex.chmod(0o700)
fake_cursor = root / "trusted-cursor" / "cursor-agent"
fake_cursor.parent.mkdir()
fake_cursor.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8")
fake_cursor.chmod(0o700)
fake_orca = root / "trusted-orca" / "orca" fake_orca = root / "trusted-orca" / "orca"
fake_orca.parent.mkdir() fake_orca.parent.mkdir()
fake_orca.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8") fake_orca.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8")
@@ -322,9 +427,20 @@ class OrcSkillTests(unittest.TestCase):
return { return {
"git": git_path, "git": git_path,
"codex": fake_codex, "codex": fake_codex,
"cursor-agent": fake_cursor,
"orca": fake_orca, "orca": fake_orca,
}[name] }[name]
def executable_facts(path: Path) -> dict[str, object]:
return {
"path": str(path),
"device": 1,
"inode": 2,
"size": 3,
"mtimeNs": 4,
"version": f"{path.name}-test 1",
}
with ( with (
mock.patch.object( mock.patch.object(
orc_profiles, orc_profiles,
@@ -339,25 +455,33 @@ class OrcSkillTests(unittest.TestCase):
mock.patch.object( mock.patch.object(
orc_profiles, orc_profiles,
"_executable_facts", "_executable_facts",
return_value={ side_effect=executable_facts,
"path": str(fake_codex),
"device": 1,
"inode": 2,
"size": 3,
"mtimeNs": 4,
"version": "codex-test 1",
},
), ),
): ):
plan = orc_profiles.build_launch_plan( plan = orc_profiles.build_launch_plan(
config_path,
project_root=root, project_root=root,
worktree=root, worktree=root,
stage="code", stage="code",
host_cli="codex",
stage_level="high", stage_level="high",
) )
cursor_plan = orc_profiles.build_launch_plan(
project_root=root,
worktree=root,
stage="release",
host_cli="cursor-agent",
stage_level="low",
)
self.assertEqual(plan["argv"][0], str(fake_codex)) self.assertEqual(plan["argv"][0], str(fake_codex))
self.assertEqual(plan["config"]["path"], str(CONFIG))
self.assertEqual(cursor_plan["argv"][0], str(fake_cursor))
self.assertEqual(cursor_plan["executable"]["path"], str(fake_cursor))
self.assertEqual(cursor_plan["argv"][1:3], ["--model", "auto"])
self.assertIn("--auto-review", cursor_plan["argv"])
self.assertEqual(cursor_plan["modelAuth"], "cursor-login")
self.assertIn("--host-cli", cursor_plan["launcherArgv"])
self.assertEqual(plan["terminalCreateArgv"][0], str(fake_orca)) self.assertEqual(plan["terminalCreateArgv"][0], str(fake_orca))
self.assertEqual(plan["worktree"], str(root)) self.assertEqual(plan["worktree"], str(root))
self.assertEqual(plan["selectionSource"], "request.stage") self.assertEqual(plan["selectionSource"], "request.stage")
@@ -392,10 +516,10 @@ class OrcSkillTests(unittest.TestCase):
): ):
with self.assertRaisesRegex( with self.assertRaisesRegex(
orc_profiles.ConfigError, orc_profiles.ConfigError,
"not in allowedWorktrees", "not registered in the project repository",
): ):
orc_profiles.validate_worktree( orc_profiles.validate_worktree(
orc_profiles.load_config(config_path), orc_profiles.load_config(CONFIG),
project_root_value=root, project_root_value=root,
worktree_value=outside, worktree_value=outside,
) )