From 337f1a90980964e5eacecb21323bc782d8e8deb1 Mon Sep 17 00:00:00 2001 From: laily Date: Sat, 1 Aug 2026 17:49:25 +0800 Subject: [PATCH] feat(orc): add tiered engineering orchestration --- AGENTS.md | 1 + README.md | 1 + skills/orc/README.md | 59 ++ skills/orc/SKILL.md | 133 ++++ skills/orc/agents/openai.yaml | 6 + skills/orc/references/orca-adapter.md | 95 +++ skills/orc/references/routing.md | 74 ++ skills/orc/scripts/resolve_profile.py | 889 ++++++++++++++++++++++ skills/orc/templates/config.template.yaml | 36 + tests/test_orc_skill.py | 405 ++++++++++ 10 files changed, 1699 insertions(+) create mode 100644 skills/orc/README.md create mode 100644 skills/orc/SKILL.md create mode 100644 skills/orc/agents/openai.yaml create mode 100644 skills/orc/references/orca-adapter.md create mode 100644 skills/orc/references/routing.md create mode 100755 skills/orc/scripts/resolve_profile.py create mode 100644 skills/orc/templates/config.template.yaml create mode 100644 tests/test_orc_skill.py diff --git a/AGENTS.md b/AGENTS.md index 9819a03..56ce32e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -59,6 +59,7 @@ AGENTS.md # 本文档 | Skill | 说明 | | ---------------------------------------------------------------------- | ------------------------------------------------- | +| [orc](skills/orc/SKILL.md) | ORC 入口:显式编排开发、版本发布与产物任务,支持 Agent 分档 | | [ack](skills/ack/SKILL.md) | ACK 入口:显式初始化、检查并运行项目三角色协作闭环 | | [skiff](skills/skiff/SKILL.md) | 本项目工作流:创建、使用、反馈与更新 builtin skill | | [declarative-openspec-loop](skills/declarative-openspec-loop/SKILL.md) | 声明式编程循环:用户提供校验方式,Agent 自动 propose/apply/校验并迭代直到通过 | diff --git a/README.md b/README.md index 8b5d940..9f52789 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ AGENTS.md # 详细规范与架构说明 | Skill | 说明 | |-------|------| +| [orc](skills/orc/SKILL.md) | 显式编排开发、版本发布和产物任务,支持 low/mid/high Agent 档位 | | [ack](skills/ack/SKILL.md) | 显式初始化、检查并运行 ACK 三角色协作及可选交付闭环 | | [skiff](skills/skiff/SKILL.md) | 在项目中创建、安装、反馈和维护 builtin skill | | [declarative-openspec-loop](skills/declarative-openspec-loop/SKILL.md) | 声明式编程循环:用户提供校验方式,Agent 自动迭代直到通过 | diff --git a/skills/orc/README.md b/skills/orc/README.md new file mode 100644 index 0000000..b336a18 --- /dev/null +++ b/skills/orc/README.md @@ -0,0 +1,59 @@ +# orc + +ORC 是显式调用的工程编排入口:把开发、版本发布、DEB 和 Docker 任务拆成阶段, +交给对应 Skill,并为每个执行 Agent 选择 `low`、`mid` 或 `high` 档位。 + +## 什么时候使用 + +- 一个请求同时包含写代码、发版本和构建产物。 +- 希望由 ORC 监督多个 Agent,并按阶段控制成本与推理能力。 +- 需要继续中断的多阶段工程流程并保留依赖关系。 + +只做单一领域任务时可以直接调用对应 Skill;ORC 不替代它们的安全规则。 + +## 使用前准备 + +- Orca 正在运行并启用了 orchestration。 +- 安装本次需要的 `$ack`、`$manage-release`、`$deb-publisher` 或 + `$publish-docker-image`。 +- 运行 `$orc 初始化` 生成 `docs/orc/config.yaml`,确认三档对应的精确模型。 +- `/usr/bin/python3`;ORC v1 配置必须保持为内置模板使用的 JSON-compatible YAML。 + resolver 以 `-I -S` 隔离模式运行,不加载项目模块、用户 site-packages 或第三方解析器。 +- ORC v1 worker 使用 `workspace-write` sandbox,以便发送 Orca lifecycle 消息; + 只读任务会在阶段 prompt 中禁止文件修改。 + +## Agent 档位 + +| 档位 | 典型任务 | +|------|----------| +| `low` | 输入明确的测试、构建、打包和上传 | +| `mid` | 常规版本发布与范围清晰的工程任务 | +| `high` | 需求理解、跨系统改动、异常恢复和高风险裁决 | + +档位不是权限。三个档位仍受各自 profile 和下游 Skill 的授权边界约束。 +当前 ORC v1 的结构化启动适配器支持 Codex worker;其它 Agent CLI 需要独立适配器, +不会通过自由命令接入。 + +## 使用示例 + +```text +$orc high 修复登录问题,验证通过后发布新版本。 +$orc code=high release=mid docker=low,完成修复、发版并推送镜像。 +$orc mid 继续上次中断的 v1.4.0 发布流程。 +``` + +阶段级档位优先于全局档位。用户显式指定后,ORC 不会静默改档;能力不足时会暂停并 +请求确认。 + +## Agent 会做什么 + +1. 识别开发、源码发布、DEB 与 Docker 阶段及其依赖。 +2. 解析每个阶段的 Agent profile,把配置快照、可信 Codex executable 和真实 Git + worktree 绑定成 launch fingerprint,并展示计划和外部写入边界。 +3. 通过 Orca 分发给对应 Skill,监督完成消息、异常和决策门。 +4. 汇总每个阶段的实际状态、证据和安全恢复入口。 + +## 如何判断完成 + +最终结果会逐阶段列出所用 Skill、Agent 档位、源 revision、远端或产物状态,以及任何 +未完成项。只有所有必要阶段都通过各自验证时,ORC 才会报告整个流程完成。 diff --git a/skills/orc/SKILL.md b/skills/orc/SKILL.md new file mode 100644 index 0000000..93843cd --- /dev/null +++ b/skills/orc/SKILL.md @@ -0,0 +1,133 @@ +--- +name: orc +description: >- + 显式编排开发、源码版本发布、DEB 和 Docker 产物任务,把阶段分发给对应 Skill, + 并用 low、mid、high 选择执行 Agent 档位。仅在用户显式调用 $orc 或 /orc,要求 + 跨阶段协调、指定 Agent 级别、监督多个 worker 或继续 ORC 编排时使用。 +--- + +# ORC 工程编排入口 + +当前会话担任 Coordinator:拆分阶段、选择档位、派发 worker、监督依赖与决策门, +但不替代下游 Skill 执行其领域流程。 + +开始时解析当前 `SKILL.md` 所在目录,记为 ``;解析真实项目根目录, +优先使用 `git rev-parse --show-toplevel`。项目配置固定为 +`/docs/orc/config.yaml`。 + +## 选择模式 + +- 用户要求初始化 ORC:执行“初始化”。 +- 用户要求检查 ORC、档位或运行环境:执行“检查”。 +- 用户要求用 ORC 完成任务:执行“编排”。 + +不要静默初始化,也不要在配置缺失或无效时退回裸命令或当前会话直接执行。 + +## 初始化 + +1. 若 `docs/orc/config.yaml` 已存在,停止创建并转入“检查”,不得覆盖。 +2. 确认 `docs/orc/` 和目标文件都不是 symlink,再从 + `/templates/config.template.yaml` 以 create-only 方式创建配置,不覆盖 + 或跟随既有路径。ORC v1 配置必须保持 JSON-compatible YAML,只由隔离的 Python + 标准库解析;模板中的 `.` 表示当前项目根。模型 ID 必须由用户或项目的可信配置确认, + 不能从任务文本猜测。 +3. 运行: + + ```bash + /usr/bin/python3 -I -S /scripts/resolve_profile.py validate \ + /docs/orc/config.yaml + ``` + +4. 报告三个档位的 CLI、模型、reasoning、权限和阶段默认值。除非用户明确要求, + 不安装下游 Skill、不创建终端、不修改 Agent 全局配置。 + +## 检查 + +1. 校验 `docs/orc/config.yaml`,确认只存在 `low`、`mid`、`high` 三档。 +2. 检查 `orca status --json`,并确认 orchestration 命令可用。 +3. 确认本次所需下游 Skill 已安装:`ack`、`manage-release`、`deb-publisher`、 + `publish-docker-image`。只检查实际会用到的项。 +4. 解析 profile 时把项目根和目标 worktree 一并交给 resolver;只有 resolver 验证目标 + 命中 `allowedWorktrees`、属于当前 Git 仓库且身份稳定后才可创建终端。不得只做文本 + 比较或跳过机器校验。 +5. 任何 profile、Skill、运行时或 worktree 不可用时 fail closed;不得选择相邻档位、 + 复用身份不明的终端或手写替代流程。 + +## 编排 + +1. 读取 [routing.md](references/routing.md),把请求拆成 `code`、`release`、`deb`、 + `docker` 阶段。没有匹配下游 Skill 的工作留在范围外并明确报告。 +2. 锁定用户授权的最远动作、目标版本、产物目标、源 commit/tag 与停止点。ORC 的调用 + 本身不扩大 push、合并、打 tag、上传或部署权限;每个下游 Skill 的授权边界继续生效。 +3. 解析档位:阶段级指定 > 全局指定 > `stageDefaults` > `defaultLevel`。只接受 + `low`、`mid`、`high`;用户显式指定后不得静默升降级。若该档位不足以安全完成, + 建立 decision gate,等待用户改档或缩小范围。 +4. 用户未指定档位时采用以下判断:清晰、机械的构建或上传可用 `low`;常规版本流程 + 用 `mid`;需求理解、跨系统改动、恢复中断流程、目标含糊或高风险裁决用 `high`。 +5. 对每个阶段运行 profile resolver。全局档位用 `--global-level`,阶段档位用 + `--stage-level`;项目根与目标 worktree 必须使用规范绝对路径。模型认证默认复用 + `codex-login`,只有明确使用对应环境凭据时才选 `openai` 或 `azure-openai`。远端认证 + 默认 `none`;只有目标 provider 与 transport 已确认时,才选择一个精确的 + `github-token`、`gitlab-token`、`gitea-token`、`forgejo-token`、`ssh-agent` 或 + `deb-token`。不得把多个 provider 凭据一起交给 worker。resolver 不存在静默 + fallback: + + ```bash + /usr/bin/python3 -I -S /scripts/resolve_profile.py resolve \ + /docs/orc/config.yaml --stage \ + --project-root \ + --worktree \ + [--global-level ] [--stage-level ] \ + [--model-auth ] \ + [--remote-auth ] + ``` + +6. 核对 resolver 返回的 `launchFingerprint`、绝对 executable、worktree identity 和选择 + 来源,再读取 [orca-adapter.md](references/orca-adapter.md),把阶段组织为 Orca task DAG。 + worker prompt 必须显式写出对应 `$skill`、阶段范围、输入 revision、用户授权边界、 + 验收证据和依赖结果;下游 Skill 无需知道 ORC。 +7. 监督 `worker_done`、`escalation` 与 `decision_gate`。`worker_done` 只代表该 worker + 回报完成;Coordinator 仍需核对下游 Skill 要求的证据和 DAG 后置条件。 +8. 逐阶段汇报所选档位、执行 Skill、结果、外部状态和未完成项。任一阶段失败时保留 + 已成功阶段的准确状态,说明安全恢复入口,不把部分成功概括成全部完成。 + +## 固定路由边界 + +- 功能、缺陷、重构与验证闭环交给 `$ack`。 +- 发布版本、release 分支/PR/MR、合并、tag 与 Forge Release 交给 + `$manage-release`。 +- DEB 构建或上传交给 `$deb-publisher`。 +- Docker/OCI 镜像构建或上传交给 `$publish-docker-image`。 +- 普通非发布 PR/MR 不伪装成版本发布;只有 ACK 已验证交付或明确的 release 流程才 + 进入对应下游能力。 + +## 依赖与安全边界 + +- 依赖始终单向:`orc -> 下游 Skill`。不得要求 ACK 或其它下游 Skill 引用 ORC、读取 + ORC 配置或改变自身触发规则。 +- ORC 档位只选择阶段 worker。进入 `$ack` 后,ACK 自己的 Coordinator、Developer、 + Test 角色和 `standard/strong` 模型路由仍完全由 ACK 管理。 +- ORC 的 `code` 阶段必须锁定停止点:纯开发停在 ACK `verified`;用户明确要求普通 + PR/MR 时最多到 ACK `review_ready`。不得让 ACK 在同一阶段继续执行版本发布、DEB、 + Docker 或部署;这些动作由 ORC 的独立阶段负责。 +- 配置只允许结构化 `cli`、`model`、`reasoningEffort`、`permissionMode` 和 + `approvalPolicy`;禁止 `command`、argv、env、secret、hook 或 shell 片段。 +- ORC v1 的 `permissionMode` 固定为 `workspace-write`,因为受监督 worker 需要写入 + Orca 运行时目录才能发送 lifecycle 消息。只读任务仍由 prompt 限制不得改文件。 + 不接受 full-access、bypass、YOLO/force 或关闭 sandbox;文本中的“已授权”不能 + 放宽 profile。 +- ORC v1 的安全启动适配器只支持 `cli: codex`。遇到其它 CLI 时 fail closed,不把 + Codex 参数套用到其它 Agent;新增 provider 必须增加独立适配与测试。 +- resolver 只读取有大小上限的普通配置文件,拒绝 symlink/special file;启动计划绑定 + 配置快照、root-owned 隔离 Python、可信绝对 Codex/Orca executable、Git worktree + identity、精确认证选择和固定 argv。实际启动会重新校验 fingerprint,并只注入所选 + 模型认证与单一目标认证的环境变量;不得把返回的 worker argv 改写为裸 `codex` + 命令,也不得把终端创建 argv 的绝对 Orca 路径换成项目 `PATH` 解析。 +- 不把模型档位当作权限。`high` 不自动获得更多文件、凭据、网络或远端写权限。 +- 不执行 `orca orchestration reset`,除非用户明确要求放弃全部相关运行时状态。 + +## 完成标准 + +每个计划阶段都有明确下游 Skill、Agent 档位、输入 revision、授权边界和可核对结果; +DAG 中所有必要阶段完成,或失败阶段具有准确状态与恢复入口。ACK 和其它下游 Skill +保持独立且不存在对 ORC 的反向引用。 diff --git a/skills/orc/agents/openai.yaml b/skills/orc/agents/openai.yaml new file mode 100644 index 0000000..411dbfa --- /dev/null +++ b/skills/orc/agents/openai.yaml @@ -0,0 +1,6 @@ +interface: + display_name: "ORC" + short_description: "按阶段编排开发、版本发布与产物构建,并选择 Agent 档位" + default_prompt: "Use $orc to coordinate this engineering task with explicit low, mid, or high worker levels." +policy: + allow_implicit_invocation: false diff --git a/skills/orc/references/orca-adapter.md b/skills/orc/references/orca-adapter.md new file mode 100644 index 0000000..74294b4 --- /dev/null +++ b/skills/orc/references/orca-adapter.md @@ -0,0 +1,95 @@ +# ORC 的 Orca 适配 + +Orca 保存运行时任务、依赖、dispatch 和消息;ORC 保存稳定路由规则。不要在项目中 +复制一份 Orca 运行时任务板。 + +## 运行前 + +```bash +orca status --json +orca orchestration task-list --json +orca orchestration inbox --limit 20 --json +``` + +确认 runtime 可达、orchestration 可用,并识别是否存在与当前请求相同的活跃任务。 +恢复流程时复用身份匹配的 task;身份不明时不要猜测或清空全局状态。 + +## 解析安全启动计划 + +对每个阶段调用 `resolve_profile.py resolve`,同时传入规范绝对 `--project-root` 和 +`--worktree`,以及本阶段精确的 `--model-auth`、`--remote-auth`。JSON 结果包含选择来源、 +结构化 profile、认证选择、配置与 worktree identity、可信 Python/Codex/Orca executable、 +`launchFingerprint`、固定 worker/launcher argv,以及完整的 `terminalCreateArgv` 和 +`terminalCreateShellCommand`。 + +- 优先把返回的 `terminalCreateArgv` 原样交给 argv-capable 进程工具;若工具只接受 shell + 字符串,则执行完整的 `terminalCreateShellCommand`。不得再插值、包引号或追加参数。 +- `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 变量。 +- resolver 失败时停止;不改用默认模型、邻近档位或已有未知终端。 +- 不重复手写 worktree 检查;resolver 已拒绝 root、不存在、非规范、含 symlink、未注册、 + 跨仓库或不在 allowlist 的目标,并把身份写入 fingerprint。 +- ORC v1 profile 必须使用 `workspace-write`。`read-only` sandbox 可能阻止 Orca CLI + 写入自身运行时或挂载目录,导致 worker 无法发送 `worker_done`;只读工作通过 task + spec 限制,而不是换成无法完成 lifecycle 的 sandbox。 + +## 创建与派发 + +每个需要 fresh worker 的阶段依次执行: + +1. 使用解析结果创建目标 worktree 内的终端。下面整行代表 resolver 返回的一个完整值, + 不是待替换的参数模板: + + ```bash + + ``` + +2. 使用返回的具体 handle 等待 Agent TUI 可接收输入: + + ```bash + orca terminal wait --terminal --for tui-idle \ + --timeout-ms 60000 --json + ``` + +3. 先为前置阶段创建 task,再用其 ID 构造依赖数组创建后置 task。task spec 使用 + `routing.md` 的 worker prompt 契约。 +4. 对已就绪 task 执行: + + ```bash + orca orchestration dispatch --task --to --inject --json + ``` + +不要把同一 task 同时派给多个写 worker。并行阶段必须使用不会竞争同一工作树写入的 +独立 worker;否则串行执行。`terminal create` 超时或连接中断时状态不确定:先用 +`terminalTitle`、worktree 和 fingerprint 核对 `orca terminal list --json`,没有完成 +reconciliation 前不得重试创建。 + +## 监督循环 + +```bash +orca orchestration check --wait \ + --types worker_done,escalation,decision_gate \ + --timeout-ms 900000 --json +``` + +- timeout 或空结果是检查点,不等于失败;任务仍活跃时继续滚动等待。 +- 收到 `decision_gate` 时只回答当前问题,不扩大用户授权。 +- 收到 `escalation` 时保留已完成状态,判断是改档、补输入还是停止。 +- 收到 `worker_done` 后核对 task/dispatch ID、下游完成证据和外部状态,再把依赖 task + 标记完成并派发新就绪阶段。 +- 三次连续同因失败后停止重派,报告恢复入口;不要用新终端掩盖同一阻塞。 + +## ACK 特例 + +`code` worker 是运行 `$ack` 的阶段 Coordinator。ORC 的 `low/mid/high` 只决定这个 +worker 的启动 profile;ACK 启动 Developer/Test 时继续读取自己的项目配置与 +`standard/strong` 规则。ORC 不读取、改写或覆盖 ACK 的内部 worker profiles。 + +## 收尾 + +不要自动关闭仍承担恢复入口的终端,不自动删除 worktree 或清空 runtime-global task。 +只有用户明确要求清理时,才按 Orca 和下游 Skill 的安全规则处理。 diff --git a/skills/orc/references/routing.md b/skills/orc/references/routing.md new file mode 100644 index 0000000..01440d1 --- /dev/null +++ b/skills/orc/references/routing.md @@ -0,0 +1,74 @@ +# ORC 阶段路由 + +ORC 只负责拆分、依赖、档位和监督。领域步骤、授权检查和完成标准由下游 Skill 自己 +决定。 + +## 路由表 + +| 阶段 | 下游 Skill | 包含 | 不包含 | +|------|------------|------|--------| +| `code` | `$ack` | 功能、缺陷、重构、测试、三角色验证闭环,以及用户明确要求的普通非发布 PR/MR | 版本发布、单独上传产物 | +| `release` | `$manage-release` | 版本号、release worktree/分支、release PR/MR、合并、tag、Forge Release、恢复发布 | 普通非发布 PR/MR、构建或上传 DEB/Docker | +| `deb` | `$deb-publisher` | DEB 构建、校验、上传与仓库可见性 | 源码 tag、Docker 镜像 | +| `docker` | `$publish-docker-image` | Docker/OCI 构建、push、digest 与平台验证 | 源码版本生命周期、DEB | + +没有匹配项时不要临时扩写某个 Skill 的职责,也不要让 ORC 自己模仿领域流程。报告缺少 +的能力,由用户决定直接执行、安装新 Skill 或另行设计。 + +## 拆分规则 + +1. 先从用户请求提取最终结果,再拆出真正需要的阶段;不要因为安装了某个 Skill 就 + 自动增加发布或上传。 +2. 为每个阶段锁定输入:项目、worktree、源 commit/tag、版本、目标和用户授权的最远 + 写操作。 +3. 同一领域的连续动作保留在一个下游任务中。例如版本号、release PR、合并和 tag + 属于一个 `manage-release` 生命周期,不拆成多个互相争抢状态的 worker。 +4. 只有输入 revision 完全相同且互不修改同一工作树时,才并行执行 DEB 与 Docker。 +5. 普通代码改动进入 ACK。若项目尚未初始化 ACK,`code` 阶段停在前置条件,不由 ORC + 静默初始化。 +6. `code` 阶段默认停在 ACK `verified`;用户明确要求普通 PR/MR 时最多到 + `review_ready`。worker prompt 必须禁止继续执行版本发布、DEB、Docker 或部署。 + +## 常见 DAG + +完整交付: + +```text +code ($ack) + -> release ($manage-release) + -> deb ($deb-publisher) + -> docker ($publish-docker-image) +``` + +只从当前 commit 构建产物: + +```text +deb ($deb-publisher) || docker ($publish-docker-image) +``` + +仅发布源码版本: + +```text +release ($manage-release) +``` + +依赖不是固定模板,但 ORC v1 不拆分一个 `manage-release` 生命周期。若项目要求在打开 +release PR 与合并之间插入 DEB/Docker gate,当前 task 粒度无法安全表达该中间里程碑; +在打开 PR 前建立 decision gate 并报告该流程暂不支持,不得用循环依赖或两个 release +worker 临时拼接。 + +## Worker prompt 契约 + +每个 worker prompt 至少包含: + +- 第一条指令显式调用唯一的下游 Skill,例如 `Use $manage-release ...`。 +- 阶段目标与明确的非目标。 +- 项目/worktree、输入 commit/tag 和前置阶段的可核对结果。 +- 用户已经授予的最远动作;未授权动作明确禁止。 +- 要求遵循项目 Agent 指令和下游 Skill 自身的停止条件。 +- 完成证据,以及通过 live dispatch preamble 回报 `worker_done` 的要求。 +- `code` prompt 还必须写明 ACK 停止点是 `verified` 或普通 PR 的 `review_ready`,并禁止 + ACK 路由版本、DEB、Docker 或部署动作。 + +不要把 ORC 的 profile、路由器内部规则或其它下游 Skill 注入 worker。worker 只需要 +当前阶段、对应 Skill 和必要依赖结果。 diff --git a/skills/orc/scripts/resolve_profile.py b/skills/orc/scripts/resolve_profile.py new file mode 100755 index 0000000..5ca5ec6 --- /dev/null +++ b/skills/orc/scripts/resolve_profile.py @@ -0,0 +1,889 @@ +#!/usr/bin/env python3 +"""Validate ORC config and build a worktree-bound Codex launch plan.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import pwd +import re +import shlex +import stat +import subprocess +import sys +from pathlib import Path +from typing import Any + +LEVELS = ("low", "mid", "high") +STAGES = ("code", "release", "deb", "docker") +EXPECTED_EFFORT = {"low": "low", "mid": "medium", "high": "high"} +TOP_LEVEL_KEYS = { + "version", + "defaultLevel", + "stageDefaults", + "allowedWorktrees", + "profiles", +} +PROFILE_KEYS = { + "cli", + "model", + "reasoningEffort", + "permissionMode", + "approvalPolicy", +} +MODEL_RE = re.compile(r"[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}\Z") +MAX_CONFIG_SIZE = 64 * 1024 +MAX_CONTROL_OUTPUT = 64 * 1024 +CONTROL_TIMEOUT_SECONDS = 15 +CONFIG_RELATIVE_PATH = Path("docs/orc/config.yaml") +COMMON_ENVIRONMENT_NAMES = { + "COLORTERM", + "LANG", + "LC_ALL", + "NO_COLOR", + "TERM", + "TZ", +} +MODEL_AUTH_ENVIRONMENT = { + "codex-login": frozenset(), + "openai": frozenset({"OPENAI_API_KEY"}), + "azure-openai": frozenset({"AZURE_OPENAI_API_KEY"}), +} +REMOTE_AUTH_ENVIRONMENT = { + "none": frozenset(), + "github-token": frozenset({"GITHUB_TOKEN"}), + "gitlab-token": frozenset({"GITLAB_TOKEN"}), + "gitea-token": frozenset({"GITEA_TOKEN"}), + "forgejo-token": frozenset({"FORGEJO_TOKEN"}), + "ssh-agent": frozenset({"SSH_AUTH_SOCK"}), + "deb-token": frozenset({"DEB_TOKEN"}), +} +STAGE_REMOTE_AUTH = { + "code": frozenset( + { + "none", + "github-token", + "gitlab-token", + "gitea-token", + "forgejo-token", + "ssh-agent", + } + ), + "release": frozenset( + { + "none", + "github-token", + "gitlab-token", + "gitea-token", + "forgejo-token", + "ssh-agent", + } + ), + "deb": frozenset({"none", "deb-token", "ssh-agent"}), + "docker": frozenset({"none"}), +} + + +class ConfigError(ValueError): + """ORC configuration or launch state is invalid or unsafe.""" + + +def _json_object(pairs: list[tuple[str, Any]]) -> dict[str, Any]: + result: dict[str, Any] = {} + for key, value in pairs: + if key in result: + raise ConfigError("JSON-compatible YAML contains a duplicate key") + result[key] = value + return result + + +def _read_bounded_regular_file(path: Path) -> tuple[str, dict[str, int]]: + absolute = path.absolute() + flags = os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0) + descriptor: int | None = None + try: + descriptor = os.open(absolute, flags) + with os.fdopen(descriptor, "rb") as stream: + descriptor = None + before = os.fstat(stream.fileno()) + if not stat.S_ISREG(before.st_mode): + raise ConfigError(f"config must be a regular file: {absolute}") + if before.st_size > MAX_CONFIG_SIZE: + raise ConfigError(f"config exceeds {MAX_CONFIG_SIZE} bytes") + content = stream.read(MAX_CONFIG_SIZE + 1) + after = os.fstat(stream.fileno()) + except OSError as exc: + raise ConfigError(f"cannot safely read config {absolute}: {exc}") from exc + finally: + if descriptor is not None: + os.close(descriptor) + + if len(content) > MAX_CONFIG_SIZE: + raise ConfigError(f"config exceeds {MAX_CONFIG_SIZE} bytes") + identity_before = (before.st_dev, before.st_ino, before.st_size, before.st_mtime_ns) + identity_after = (after.st_dev, after.st_ino, after.st_size, after.st_mtime_ns) + if identity_before != identity_after: + raise ConfigError("config changed while it was being read") + try: + text = content.decode("utf-8") + except UnicodeDecodeError as exc: + raise ConfigError("config must be valid UTF-8") from exc + return text, { + "device": before.st_dev, + "inode": before.st_ino, + "size": before.st_size, + "mtimeNs": before.st_mtime_ns, + } + + +def _mapping(value: Any, path: str) -> dict[str, Any]: + if not isinstance(value, dict): + raise ConfigError(f"{path} must be a mapping") + return value + + +def _exact_keys( + value: dict[str, Any], + *, + required: set[str], + path: str, +) -> None: + missing = sorted(required - set(value)) + unknown = sorted(set(value) - required) + if missing: + raise ConfigError(f"{path} is missing fields: {', '.join(missing)}") + if unknown: + raise ConfigError(f"{path} has unknown fields: {', '.join(unknown)}") + + +def _enum(value: Any, allowed: tuple[str, ...] | set[str], path: str) -> str: + if not isinstance(value, str) or value not in allowed: + raise ConfigError(f"{path} must be one of: {', '.join(sorted(allowed))}") + return value + + +def validate_config(document: Any) -> dict[str, Any]: + config = _mapping(document, "config") + _exact_keys(config, required=TOP_LEVEL_KEYS, path="config") + + version = config["version"] + if isinstance(version, bool) or version != 1: + raise ConfigError("version must be integer 1") + + _enum(config["defaultLevel"], LEVELS, "defaultLevel") + + stage_defaults = _mapping(config["stageDefaults"], "stageDefaults") + unknown_stages = sorted(set(stage_defaults) - set(STAGES)) + if unknown_stages: + raise ConfigError( + f"stageDefaults has unknown fields: {', '.join(unknown_stages)}" + ) + for stage, level in stage_defaults.items(): + _enum(level, LEVELS, f"stageDefaults.{stage}") + + allowed = config["allowedWorktrees"] + if not isinstance(allowed, list) or not allowed: + raise ConfigError("allowedWorktrees must be a non-empty list") + 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") + _exact_keys(profiles, required=set(LEVELS), path="profiles") + for level in LEVELS: + profile = _mapping(profiles[level], f"profiles.{level}") + _exact_keys(profile, required=PROFILE_KEYS, path=f"profiles.{level}") + if profile["cli"] != "codex": + raise ConfigError(f"profiles.{level}.cli must be codex in ORC v1") + model = profile["model"] + if not isinstance(model, str) or not MODEL_RE.fullmatch(model): + raise ConfigError(f"profiles.{level}.model is not a safe exact model ID") + expected_effort = EXPECTED_EFFORT[level] + if profile["reasoningEffort"] != expected_effort: + raise ConfigError( + f"profiles.{level}.reasoningEffort must be {expected_effort}" + ) + if profile["permissionMode"] != "workspace-write": + raise ConfigError( + f"profiles.{level}.permissionMode must be workspace-write in ORC v1" + ) + _enum( + profile["approvalPolicy"], + {"untrusted", "on-request", "never"}, + f"profiles.{level}.approvalPolicy", + ) + return config + + +def load_config_snapshot(path: Path) -> tuple[dict[str, Any], dict[str, Any]]: + raw, identity = _read_bounded_regular_file(path) + try: + document = json.loads(raw, object_pairs_hook=_json_object) + except ConfigError: + raise + except Exception as exc: + raise ConfigError("invalid JSON-compatible YAML config") from exc + config = validate_config(document) + snapshot = { + "sha256": hashlib.sha256(raw.encode("utf-8")).hexdigest(), + **identity, + } + return config, snapshot + + +def load_config(path: Path) -> dict[str, Any]: + config, _ = load_config_snapshot(path) + return config + + +def resolve_profile( + config: dict[str, Any], + *, + stage: str, + global_level: str | None = None, + stage_level: str | None = None, + model_auth: str = "codex-login", + remote_auth: str = "none", +) -> dict[str, Any]: + _enum(stage, STAGES, "stage") + if global_level is not None: + _enum(global_level, LEVELS, "global level") + if stage_level is not None: + _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") + + if stage_level is not None: + level, source = stage_level, "request.stage" + elif global_level is not None: + level, source = global_level, "request.global" + elif stage in config["stageDefaults"]: + level, source = config["stageDefaults"][stage], f"config.stageDefaults.{stage}" + else: + level, source = config["defaultLevel"], "config.defaultLevel" + + profile = config["profiles"].get(level) + if profile is None: + raise ConfigError(f"requested profile does not exist: {level}") + worker_args = [ + "--model", + profile["model"], + "-c", + f'model_reasoning_effort="{profile["reasoningEffort"]}"', + "--sandbox", + profile["permissionMode"], + "--ask-for-approval", + profile["approvalPolicy"], + "--strict-config", + ] + return { + "stage": stage, + "level": level, + "selectionSource": source, + "modelAuth": model_auth, + "remoteAuth": remote_auth, + "profile": dict(profile), + "workerArgs": worker_args, + } + + +def _path_has_parent_reference(value: str) -> bool: + return ".." in Path(value).parts + + +def _assert_no_symlink_components(path: Path, label: str) -> None: + if not path.is_absolute(): + raise ConfigError(f"{label} must be absolute: {path}") + current = Path(path.anchor) + for part in path.parts[1:]: + current /= part + try: + metadata = os.lstat(current) + except OSError as exc: + raise ConfigError(f"{label} does not exist: {current}") from exc + if stat.S_ISLNK(metadata.st_mode): + raise ConfigError(f"{label} must not contain symlinks: {current}") + + +def canonical_directory(value: str | Path, label: str) -> Path: + text = str(value) + if ( + not text + or text != text.strip() + or any(character in text for character in ("\x00", "\n", "\r")) + or _path_has_parent_reference(text) + ): + raise ConfigError(f"{label} must be a safe canonical absolute path") + raw = Path(text) + _assert_no_symlink_components(raw, label) + try: + resolved = raw.resolve(strict=True) + except OSError as exc: + raise ConfigError(f"{label} does not exist: {raw}") from exc + if resolved != raw or resolved == Path(resolved.anchor) or not resolved.is_dir(): + raise ConfigError(f"{label} must be a canonical non-root directory: {resolved}") + return resolved + + +def account_identity() -> tuple[Path, str]: + account = pwd.getpwuid(os.getuid()) + account_home = Path(account.pw_dir).resolve(strict=True) + if not account_home.is_dir(): + raise ConfigError("current account home is unavailable") + return account_home, account.pw_name + + +def trusted_path_entries() -> list[Path]: + account_home, _ = account_identity() + candidates = [ + account_home / ".local" / "bin", + account_home / ".local" / "share" / "mise" / "shims", + account_home / ".cargo" / "bin", + Path("/home/linuxbrew/.linuxbrew/bin"), + Path("/usr/local/bin"), + Path("/usr/bin"), + Path("/bin"), + ] + result: list[Path] = [] + for candidate in candidates: + try: + resolved = candidate.resolve(strict=True) + except OSError: + continue + if resolved.is_dir() and resolved not in result: + result.append(resolved) + return result + + +def _trusted_executable(path: Path, expected_name: str) -> Path | None: + try: + candidate_metadata = os.lstat(path) + resolved = path.resolve(strict=True) + metadata = resolved.stat() + except OSError: + return None + if not ( + stat.S_ISREG(candidate_metadata.st_mode) + or stat.S_ISLNK(candidate_metadata.st_mode) + ): + return None + if not stat.S_ISREG(metadata.st_mode) or not os.access(resolved, os.X_OK): + return None + if metadata.st_uid not in {0, os.getuid()}: + return None + if stat.S_IMODE(metadata.st_mode) & 0o022: + return None + if expected_name == "python3": + if not resolved.name.startswith("python3"): + return None + elif resolved.name != expected_name: + return None + return resolved + + +def resolve_trusted_executable(name: str) -> Path: + if name not in {"codex", "git", "orca"}: + raise ConfigError(f"unsupported executable: {name}") + search_paths = trusted_path_entries() + if name == "git": + search_paths = [ + path + for path in search_paths + if str(path) in {"/usr/local/bin", "/usr/bin", "/bin"} + ] + for directory in search_paths: + resolved = _trusted_executable(directory / name, name) + if resolved is not None: + return resolved + raise ConfigError(f"trusted {name} executable was not found in fixed directories") + + +def resolve_trusted_python() -> Path: + for candidate in (Path("/usr/bin/python3"), Path("/usr/local/bin/python3")): + resolved = _trusted_executable(candidate, "python3") + if resolved is not None and resolved.stat().st_uid == 0: + return resolved + raise ConfigError("a root-owned Python executable was not found in fixed paths") + + +def control_environment() -> dict[str, str]: + account_home, username = account_identity() + result = { + "HOME": str(account_home), + "LOGNAME": username, + "PATH": os.pathsep.join(str(path) for path in trusted_path_entries()), + "USER": username, + } + for name, value in os.environ.items(): + if ( + name in COMMON_ENVIRONMENT_NAMES or name.startswith("LC_") + ) and "\x00" not in value: + result[name] = value + return result + + +def selected_credential_names( + stage: str, + *, + model_auth: str, + remote_auth: str, +) -> frozenset[str]: + _enum(stage, STAGES, "stage") + _enum(model_auth, set(MODEL_AUTH_ENVIRONMENT), "model auth") + _enum(remote_auth, STAGE_REMOTE_AUTH[stage], f"{stage} remote auth") + names = MODEL_AUTH_ENVIRONMENT[model_auth] | REMOTE_AUTH_ENVIRONMENT[remote_auth] + for name in names: + value = os.environ.get(name) + if not value or "\x00" in value: + raise ConfigError( + f"selected authentication variable is unavailable: {name}" + ) + if name == "SSH_AUTH_SOCK": + socket_path = Path(value) + if ( + not socket_path.is_absolute() + or value != value.strip() + or any(character in value for character in ("\n", "\r")) + or _path_has_parent_reference(value) + ): + raise ConfigError("selected SSH_AUTH_SOCK is not a safe absolute path") + _assert_no_symlink_components(socket_path, "selected SSH_AUTH_SOCK") + try: + metadata = os.lstat(socket_path) + except OSError as exc: + raise ConfigError("selected SSH_AUTH_SOCK is unavailable") from exc + if ( + not stat.S_ISSOCK(metadata.st_mode) + or metadata.st_uid != os.getuid() + or stat.S_IMODE(metadata.st_mode) & 0o022 + ): + raise ConfigError("selected SSH_AUTH_SOCK is not a trusted user socket") + return names + + +def worker_environment( + stage: str, + *, + model_auth: str, + remote_auth: str, +) -> dict[str, str]: + result = control_environment() + for name in selected_credential_names( + stage, + model_auth=model_auth, + remote_auth=remote_auth, + ): + result[name] = os.environ[name] + return result + + +def _run_control(argv: list[str], label: str) -> str: + try: + completed = subprocess.run( + argv, + shell=False, + check=False, + capture_output=True, + text=True, + timeout=CONTROL_TIMEOUT_SECONDS, + env=control_environment(), + ) + except (OSError, subprocess.TimeoutExpired) as exc: + raise ConfigError(f"{label} could not be executed safely") from exc + if completed.returncode != 0: + raise ConfigError(f"{label} failed with exit {completed.returncode}") + output = completed.stdout.strip() + if not output or len(output.encode("utf-8")) > MAX_CONTROL_OUTPUT: + raise ConfigError(f"{label} returned invalid output") + return output + + +def _git_path(value: str, cwd: Path, label: str) -> Path: + candidate = Path(value) + if not candidate.is_absolute(): + candidate = cwd / candidate + _assert_no_symlink_components(candidate, label) + try: + resolved = candidate.resolve(strict=True) + except OSError as exc: + raise ConfigError(f"{label} is invalid") from exc + if not resolved.is_dir(): + raise ConfigError(f"{label} is not a directory") + return resolved + + +def validate_worktree( + config: dict[str, Any], + *, + project_root_value: str | Path, + worktree_value: str | Path, +) -> dict[str, Any]: + project_root = canonical_directory(project_root_value, "project root") + worktree = canonical_directory(worktree_value, "target worktree") + git = resolve_trusted_executable("git") + + top_level = canonical_directory( + _run_control( + [str(git), "-C", str(project_root), "rev-parse", "--show-toplevel"], + "Git project-root check", + ), + "Git project root", + ) + if top_level != project_root: + raise ConfigError("project root is not the repository top level") + + common_dir = _git_path( + _run_control( + [str(git), "-C", str(project_root), "rev-parse", "--git-common-dir"], + "Git common-directory check", + ), + project_root, + "Git common directory", + ) + listed = _run_control( + [str(git), "-C", str(project_root), "worktree", "list", "--porcelain"], + "Git worktree listing", + ) + registered: set[Path] = set() + for line in listed.splitlines(): + if line.startswith("worktree "): + try: + registered.add( + canonical_directory(line[9:], "registered worktree") + ) + except ConfigError: + continue + + allowed: set[Path] = set() + for entry in config["allowedWorktrees"]: + allowed.add( + project_root + 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( + _run_control( + [str(git), "-C", str(worktree), "rev-parse", "--show-toplevel"], + "Git target-worktree check", + ), + "target Git worktree", + ) + target_common = _git_path( + _run_control( + [str(git), "-C", str(worktree), "rev-parse", "--git-common-dir"], + "target Git common-directory check", + ), + worktree, + "target Git common directory", + ) + if target_top != worktree or target_common != common_dir: + raise ConfigError("target worktree does not belong to the project repository") + + worktree_metadata = worktree.stat() + common_metadata = common_dir.stat() + return { + "projectRoot": str(project_root), + "worktree": str(worktree), + "gitCommonDir": str(common_dir), + "worktreeIdentity": { + "device": worktree_metadata.st_dev, + "inode": worktree_metadata.st_ino, + "gitCommonDevice": common_metadata.st_dev, + "gitCommonInode": common_metadata.st_ino, + }, + } + + +def _file_facts(path: Path) -> dict[str, Any]: + metadata = path.stat() + return { + "path": str(path), + "device": metadata.st_dev, + "inode": metadata.st_ino, + "size": metadata.st_size, + "mtimeNs": metadata.st_mtime_ns, + } + + +def _executable_facts(path: Path) -> dict[str, Any]: + return { + **_file_facts(path), + "version": _run_control([str(path), "--version"], "Codex version check"), + } + + +def _python_facts(path: Path) -> dict[str, Any]: + version = _run_control( + [ + str(path), + "-I", + "-S", + "-c", + "import sys; print(sys.version.split()[0])", + ], + "Python version check", + ) + return { + **_file_facts(path), + "version": version, + } + + +def _fingerprint(value: dict[str, Any]) -> str: + canonical = json.dumps( + value, + ensure_ascii=False, + sort_keys=True, + separators=(",", ":"), + ).encode("utf-8") + return "sha256:" + hashlib.sha256(canonical).hexdigest() + + +def build_launch_plan( + config_path: Path, + *, + project_root: str | Path, + worktree: str | Path, + stage: str, + global_level: str | None = None, + stage_level: str | None = None, + model_auth: str = "codex-login", + remote_auth: str = "none", +) -> dict[str, Any]: + project = canonical_directory(project_root, "project root") + expected_config = project / CONFIG_RELATIVE_PATH + if config_path.absolute() != expected_config: + 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( + config, + project_root_value=project, + worktree_value=worktree, + ) + profile = resolve_profile( + config, + stage=stage, + global_level=global_level, + stage_level=stage_level, + model_auth=model_auth, + remote_auth=remote_auth, + ) + selected_credential_names( + stage, + model_auth=model_auth, + remote_auth=remote_auth, + ) + codex = resolve_trusted_executable("codex") + executable = _executable_facts(codex) + orca = resolve_trusted_executable("orca") + orca_executable = _file_facts(orca) + python = resolve_trusted_python() + python_executable = _python_facts(python) + script = Path(__file__).resolve(strict=True) + script_metadata = script.stat() + script_sha256 = hashlib.sha256(script.read_bytes()).hexdigest() + launch_facts = { + **profile, + **worktree_facts, + "config": { + "path": str(expected_config), + **config_snapshot, + }, + "executable": executable, + "orca": orca_executable, + "python": python_executable, + "launcher": { + "path": str(script), + "device": script_metadata.st_dev, + "inode": script_metadata.st_ino, + "size": script_metadata.st_size, + "mtimeNs": script_metadata.st_mtime_ns, + "sha256": script_sha256, + }, + } + fingerprint = _fingerprint(launch_facts) + worker_argv = [str(codex), *profile["workerArgs"]] + launcher_argv = [ + str(python), + "-I", + "-S", + str(script), + "_launch", + str(expected_config), + "--project-root", + str(project), + "--worktree", + str(worktree_facts["worktree"]), + "--stage", + stage, + "--expected-fingerprint", + fingerprint, + ] + if global_level is not None: + launcher_argv.extend(["--global-level", global_level]) + if stage_level is not None: + launcher_argv.extend(["--stage-level", stage_level]) + if model_auth != "codex-login": + launcher_argv.extend(["--model-auth", model_auth]) + if remote_auth != "none": + launcher_argv.extend(["--remote-auth", remote_auth]) + terminal_command = shlex.join(launcher_argv) + worktree_selector = "path:" + str(worktree_facts["worktree"]) + terminal_title = f"ORC-{stage}-{profile['level']}-{fingerprint[7:15]}" + terminal_create_argv = [ + str(orca), + "terminal", + "create", + "--worktree", + worktree_selector, + "--title", + terminal_title, + "--command", + terminal_command, + "--json", + ] + return { + **launch_facts, + "argv": worker_argv, + "launchFingerprint": fingerprint, + "launcherArgv": launcher_argv, + "terminalCommand": terminal_command, + "worktreeSelector": worktree_selector, + "terminalTitle": terminal_title, + "terminalCreateArgv": terminal_create_argv, + "terminalCreateShellCommand": shlex.join(terminal_create_argv), + } + + +def execute_launch(args: argparse.Namespace) -> int: + plan = build_launch_plan( + args.config, + project_root=args.project_root, + worktree=args.worktree, + stage=args.stage, + global_level=args.global_level, + stage_level=args.stage_level, + model_auth=args.model_auth, + remote_auth=args.remote_auth, + ) + if plan["launchFingerprint"] != args.expected_fingerprint: + raise ConfigError("launch fingerprint changed; resolve the profile again") + current = canonical_directory(Path.cwd(), "launcher working directory") + if current != Path(plan["worktree"]): + raise ConfigError("launcher working directory does not match target worktree") + current_python = Path(sys.executable).resolve(strict=True) + if current_python != Path(plan["python"]["path"]): + raise ConfigError("launcher Python does not match the resolved interpreter") + if _python_facts(current_python) != plan["python"]: + raise ConfigError("Python executable changed before launch") + executable = Path(plan["executable"]["path"]) + if _executable_facts(executable) != plan["executable"]: + raise ConfigError("Codex executable changed before launch") + try: + os.execve( + executable, + plan["argv"], + worker_environment( + plan["stage"], + model_auth=plan["modelAuth"], + remote_auth=plan["remoteAuth"], + ), + ) + except OSError as exc: + raise ConfigError("Codex worker could not be launched") from exc + return 1 # pragma: no cover - os.execve does not return on success + + +def _add_resolution_arguments(parser: argparse.ArgumentParser) -> None: + parser.add_argument("config", type=Path) + parser.add_argument("--project-root", required=True) + parser.add_argument("--worktree", required=True) + parser.add_argument("--stage", required=True, choices=STAGES) + parser.add_argument("--global-level", choices=LEVELS) + parser.add_argument("--stage-level", choices=LEVELS) + parser.add_argument( + "--model-auth", + choices=tuple(MODEL_AUTH_ENVIRONMENT), + default="codex-login", + ) + parser.add_argument( + "--remote-auth", + choices=tuple(REMOTE_AUTH_ENVIRONMENT), + default="none", + ) + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description=__doc__) + subparsers = parser.add_subparsers(dest="command", required=True) + + 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") + _add_resolution_arguments(resolve) + + launch = subparsers.add_parser("_launch", help=argparse.SUPPRESS) + _add_resolution_arguments(launch) + launch.add_argument("--expected-fingerprint", required=True) + return parser + + +def main(argv: list[str] | None = None) -> int: + args = build_parser().parse_args(argv) + try: + if args.command == "validate": + config = load_config(args.config) + result: dict[str, Any] = { + "ok": True, + "config": str(args.config.absolute()), + "levels": list(LEVELS), + "stages": list(STAGES), + "defaultLevel": config["defaultLevel"], + } + elif args.command == "resolve": + result = build_launch_plan( + args.config, + project_root=args.project_root, + worktree=args.worktree, + stage=args.stage, + global_level=args.global_level, + stage_level=args.stage_level, + model_auth=args.model_auth, + remote_auth=args.remote_auth, + ) + else: + return execute_launch(args) + except ConfigError as exc: + print(f"ORC config error: {exc}", file=sys.stderr) + return 1 + print(json.dumps(result, ensure_ascii=False, sort_keys=True)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/skills/orc/templates/config.template.yaml b/skills/orc/templates/config.template.yaml new file mode 100644 index 0000000..da0c413 --- /dev/null +++ b/skills/orc/templates/config.template.yaml @@ -0,0 +1,36 @@ +{ + "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" + } + } +} diff --git a/tests/test_orc_skill.py b/tests/test_orc_skill.py new file mode 100644 index 0000000..f1ee2b6 --- /dev/null +++ b/tests/test_orc_skill.py @@ -0,0 +1,405 @@ +from __future__ import annotations + +import copy +import importlib.util +import json +import os +import shlex +import shutil +import socket +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path +from unittest import mock + + +REPO_ROOT = Path(__file__).resolve().parents[1] +ORC_DIR = REPO_ROOT / "skills" / "orc" +SCRIPT = ORC_DIR / "scripts" / "resolve_profile.py" +CONFIG = ORC_DIR / "templates" / "config.template.yaml" + +spec = importlib.util.spec_from_file_location("orc_resolve_profile", SCRIPT) +assert spec is not None and spec.loader is not None +orc_profiles = importlib.util.module_from_spec(spec) +spec.loader.exec_module(orc_profiles) + + +class OrcSkillTests(unittest.TestCase): + def test_orc_is_explicit_and_routes_to_independent_skills(self) -> None: + skill = (ORC_DIR / "SKILL.md").read_text(encoding="utf-8") + routing = (ORC_DIR / "references" / "routing.md").read_text(encoding="utf-8") + metadata = (ORC_DIR / "agents" / "openai.yaml").read_text(encoding="utf-8") + + self.assertIn("allow_implicit_invocation: false", metadata) + self.assertIn("low", skill) + self.assertIn("mid", skill) + self.assertIn("high", skill) + self.assertIn("只支持 `cli: codex`", skill) + for child in ("$ack", "$manage-release", "$deb-publisher", "$publish-docker-image"): + self.assertIn(child, routing) + + def test_children_do_not_reference_orc(self) -> None: + for child in ("ack", "manage-release", "deb-publisher", "publish-docker-image"): + for path in (REPO_ROOT / "skills" / child).rglob("*"): + if not path.is_file() or "__pycache__" in path.parts: + continue + content = path.read_text(encoding="utf-8", errors="ignore") + self.assertNotIn("$orc", content, str(path)) + self.assertNotRegex( + content, + r"(? None: + routing = (ORC_DIR / "references" / "routing.md").read_text( + encoding="utf-8" + ) + + self.assertIn("普通非发布 PR/MR", routing) + self.assertIn("最多到", routing) + self.assertIn("review_ready", routing) + self.assertIn("当前 task 粒度无法安全表达", routing) + + def test_template_and_cli_validate(self) -> None: + config = orc_profiles.load_config(CONFIG) + self.assertEqual(set(config["profiles"]), {"low", "mid", "high"}) + completed = subprocess.run( + [sys.executable, str(SCRIPT), "validate", str(CONFIG)], + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(completed.returncode, 0, completed.stderr) + self.assertIn('"ok": true', completed.stdout) + + clean_python = subprocess.run( + [sys.executable, "-I", "-S", str(SCRIPT), "validate", str(CONFIG)], + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(clean_python.returncode, 0, clean_python.stderr) + + def test_level_precedence_is_stage_then_global_then_config(self) -> None: + config = orc_profiles.load_config(CONFIG) + stage = orc_profiles.resolve_profile( + config, stage="docker", global_level="mid", stage_level="high" + ) + global_choice = orc_profiles.resolve_profile( + config, stage="docker", global_level="mid" + ) + configured = orc_profiles.resolve_profile(config, stage="docker") + fallback_config = copy.deepcopy(config) + del fallback_config["stageDefaults"]["docker"] + fallback = orc_profiles.resolve_profile(fallback_config, stage="docker") + + self.assertEqual((stage["level"], stage["selectionSource"]), ("high", "request.stage")) + self.assertEqual((global_choice["level"], global_choice["selectionSource"]), ("mid", "request.global")) + self.assertEqual((configured["level"], configured["selectionSource"]), ("low", "config.stageDefaults.docker")) + self.assertEqual((fallback["level"], fallback["selectionSource"]), ("mid", "config.defaultLevel")) + + def test_resolver_builds_fixed_worker_args_without_free_command_fields(self) -> None: + config = orc_profiles.load_config(CONFIG) + plan = orc_profiles.resolve_profile(config, stage="release") + + self.assertEqual(plan["workerArgs"][:2], ["--model", "gpt-5.6-terra"]) + self.assertIn("--strict-config", plan["workerArgs"]) + self.assertNotIn("danger-full-access", " ".join(plan["workerArgs"])) + self.assertNotIn("env", plan["profile"]) + self.assertNotIn("command", plan["profile"]) + + def test_worker_environment_selects_one_model_and_remote_auth(self) -> None: + ambient = { + "OPENAI_API_KEY": "openai-secret", + "AZURE_OPENAI_API_KEY": "azure-secret", + "GITHUB_TOKEN": "github-secret", + "GITLAB_TOKEN": "gitlab-secret", + "GITEA_TOKEN": "gitea-secret", + "FORGEJO_TOKEN": "forgejo-secret", + "DEB_TOKEN": "deb-secret", + "SSH_AUTH_SOCK": "/tmp/agent.sock", + "HTTPS_PROXY": "https://user:secret@proxy.example", + } + with mock.patch.dict(os.environ, ambient, clear=True): + environment = orc_profiles.worker_environment( + "release", + model_auth="openai", + remote_auth="forgejo-token", + ) + + self.assertEqual(environment["OPENAI_API_KEY"], "openai-secret") + self.assertEqual(environment["FORGEJO_TOKEN"], "forgejo-secret") + for rejected in ( + "AZURE_OPENAI_API_KEY", + "GITHUB_TOKEN", + "GITLAB_TOKEN", + "GITEA_TOKEN", + "DEB_TOKEN", + "SSH_AUTH_SOCK", + "HTTPS_PROXY", + ): + self.assertNotIn(rejected, environment) + + with self.assertRaisesRegex(orc_profiles.ConfigError, "deb remote auth"): + orc_profiles.worker_environment( + "deb", + model_auth="codex-login", + remote_auth="forgejo-token", + ) + + with mock.patch.dict(os.environ, {}, clear=True): + with self.assertRaisesRegex( + orc_profiles.ConfigError, + "selected authentication variable is unavailable", + ): + orc_profiles.worker_environment( + "release", + model_auth="openai", + remote_auth="none", + ) + + with mock.patch.dict(os.environ, {"OPENAI_API_KEY": ""}, clear=True): + with self.assertRaisesRegex( + orc_profiles.ConfigError, + "selected authentication variable is unavailable", + ): + orc_profiles.worker_environment( + "release", + model_auth="openai", + remote_auth="none", + ) + + def test_ssh_auth_requires_a_trusted_user_socket(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + socket_path = root / "agent.sock" + with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as agent: + agent.bind(str(socket_path)) + socket_path.chmod(0o600) + with mock.patch.dict( + os.environ, + {"SSH_AUTH_SOCK": str(socket_path)}, + clear=True, + ): + environment = orc_profiles.worker_environment( + "release", + model_auth="codex-login", + remote_auth="ssh-agent", + ) + self.assertEqual(environment["SSH_AUTH_SOCK"], str(socket_path)) + + regular_file = root / "not-a-socket" + regular_file.write_text("not a socket", encoding="utf-8") + regular_file.chmod(0o600) + with mock.patch.dict( + os.environ, + {"SSH_AUTH_SOCK": str(regular_file)}, + clear=True, + ): + with self.assertRaisesRegex( + orc_profiles.ConfigError, + "not a trusted user socket", + ): + orc_profiles.worker_environment( + "release", + model_auth="codex-login", + remote_auth="ssh-agent", + ) + + def test_trusted_orca_ignores_ambient_path(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + trusted = root / "trusted" + hostile = root / "hostile" + trusted.mkdir() + hostile.mkdir() + trusted_orca = trusted / "orca" + hostile_orca = hostile / "orca" + for executable in (trusted_orca, hostile_orca): + executable.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8") + executable.chmod(0o700) + + with ( + mock.patch.object( + orc_profiles, + "trusted_path_entries", + return_value=[trusted], + ), + mock.patch.dict(os.environ, {"PATH": str(hostile)}, clear=True), + ): + resolved = orc_profiles.resolve_trusted_executable("orca") + + self.assertEqual(resolved, trusted_orca) + + def test_config_rejects_unsafe_or_ambiguous_profiles(self) -> None: + base = orc_profiles.load_config(CONFIG) + + free_command = copy.deepcopy(base) + free_command["profiles"]["low"]["command"] = "codex --dangerously-bypass" + with self.assertRaisesRegex(orc_profiles.ConfigError, "unknown fields"): + orc_profiles.validate_config(free_command) + + full_access = copy.deepcopy(base) + full_access["profiles"]["high"]["permissionMode"] = "danger-full-access" + with self.assertRaisesRegex(orc_profiles.ConfigError, "workspace-write in ORC v1"): + orc_profiles.validate_config(full_access) + + read_only = copy.deepcopy(base) + read_only["profiles"]["low"]["permissionMode"] = "read-only" + with self.assertRaisesRegex(orc_profiles.ConfigError, "workspace-write in ORC v1"): + orc_profiles.validate_config(read_only) + + missing_level = copy.deepcopy(base) + del missing_level["profiles"]["mid"] + with self.assertRaisesRegex(orc_profiles.ConfigError, "missing fields: mid"): + orc_profiles.validate_config(missing_level) + + option_model = copy.deepcopy(base) + option_model["profiles"]["low"]["model"] = "--model" + with self.assertRaisesRegex(orc_profiles.ConfigError, "safe exact model ID"): + orc_profiles.validate_config(option_model) + + list_policy = copy.deepcopy(base) + list_policy["profiles"]["low"]["approvalPolicy"] = ["never"] + with self.assertRaisesRegex(orc_profiles.ConfigError, "approvalPolicy"): + orc_profiles.validate_config(list_policy) + + def test_config_reader_rejects_symlinks_and_redacts_parser_input(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + real = root / "real.yaml" + real.write_text(CONFIG.read_text(encoding="utf-8"), encoding="utf-8") + linked = root / "linked.yaml" + linked.symlink_to(real) + with self.assertRaisesRegex(orc_profiles.ConfigError, "safely read"): + orc_profiles.load_config(linked) + + malformed = root / "malformed.yaml" + secret_marker = "PRIVATE_MATERIAL_MUST_NOT_APPEAR" + malformed.write_text(f"{secret_marker}: [unterminated\n", encoding="utf-8") + completed = subprocess.run( + [sys.executable, str(SCRIPT), "validate", str(malformed)], + text=True, + 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: + git = shutil.which("git") + self.assertIsNotNone(git) + assert git is not None + git_path = Path(git).resolve() + + with tempfile.TemporaryDirectory(prefix="orc shell $(id) ' ") as temporary: + temporary_root = Path(temporary).resolve() + root = temporary_root / "project" + root.mkdir() + subprocess.run( + [str(git_path), "init", "-q", str(root)], + check=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.parent.mkdir() + fake_codex.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8") + fake_codex.chmod(0o700) + fake_orca = root / "trusted-orca" / "orca" + fake_orca.parent.mkdir() + fake_orca.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8") + fake_orca.chmod(0o700) + + def executable(name: str) -> Path: + return { + "git": git_path, + "codex": fake_codex, + "orca": fake_orca, + }[name] + + with ( + mock.patch.object( + orc_profiles, + "resolve_trusted_executable", + side_effect=executable, + ), + mock.patch.object( + orc_profiles, + "resolve_trusted_python", + return_value=Path(sys.executable).resolve(), + ), + mock.patch.object( + orc_profiles, + "_executable_facts", + return_value={ + "path": str(fake_codex), + "device": 1, + "inode": 2, + "size": 3, + "mtimeNs": 4, + "version": "codex-test 1", + }, + ), + ): + plan = orc_profiles.build_launch_plan( + config_path, + project_root=root, + worktree=root, + stage="code", + stage_level="high", + ) + + self.assertEqual(plan["argv"][0], str(fake_codex)) + self.assertEqual(plan["terminalCreateArgv"][0], str(fake_orca)) + self.assertEqual(plan["worktree"], str(root)) + self.assertEqual(plan["selectionSource"], "request.stage") + self.assertTrue(plan["launchFingerprint"].startswith("sha256:")) + self.assertIn("_launch", plan["launcherArgv"]) + self.assertEqual(plan["launcherArgv"][1:3], ["-I", "-S"]) + self.assertIn("python", plan) + self.assertEqual(plan["worktreeSelector"], f"path:{root}") + self.assertNotIn("PRIVATE", json.dumps(plan)) + self.assertEqual(shlex.split(plan["terminalCommand"]), plan["launcherArgv"]) + self.assertEqual( + shlex.split(plan["terminalCreateShellCommand"]), + plan["terminalCreateArgv"], + ) + command_index = plan["terminalCreateArgv"].index("--command") + self.assertEqual( + plan["terminalCreateArgv"][command_index + 1], + plan["terminalCommand"], + ) + + outside = temporary_root / "outside" + outside.mkdir() + subprocess.run( + [str(git_path), "init", "-q", str(outside)], + check=True, + capture_output=True, + ) + with mock.patch.object( + orc_profiles, + "resolve_trusted_executable", + side_effect=executable, + ): + with self.assertRaisesRegex( + orc_profiles.ConfigError, + "not in allowedWorktrees", + ): + orc_profiles.validate_worktree( + orc_profiles.load_config(config_path), + project_root_value=root, + worktree_value=outside, + ) + + +if __name__ == "__main__": + unittest.main()