feat(orc): add tiered engineering orchestration
This commit is contained in:
@@ -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 才会报告整个流程完成。
|
||||
@@ -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` 所在目录,记为 `<orc-skill-dir>`;解析真实项目根目录,
|
||||
优先使用 `git rev-parse --show-toplevel`。项目配置固定为
|
||||
`<project-root>/docs/orc/config.yaml`。
|
||||
|
||||
## 选择模式
|
||||
|
||||
- 用户要求初始化 ORC:执行“初始化”。
|
||||
- 用户要求检查 ORC、档位或运行环境:执行“检查”。
|
||||
- 用户要求用 ORC 完成任务:执行“编排”。
|
||||
|
||||
不要静默初始化,也不要在配置缺失或无效时退回裸命令或当前会话直接执行。
|
||||
|
||||
## 初始化
|
||||
|
||||
1. 若 `docs/orc/config.yaml` 已存在,停止创建并转入“检查”,不得覆盖。
|
||||
2. 确认 `docs/orc/` 和目标文件都不是 symlink,再从
|
||||
`<orc-skill-dir>/templates/config.template.yaml` 以 create-only 方式创建配置,不覆盖
|
||||
或跟随既有路径。ORC v1 配置必须保持 JSON-compatible YAML,只由隔离的 Python
|
||||
标准库解析;模板中的 `.` 表示当前项目根。模型 ID 必须由用户或项目的可信配置确认,
|
||||
不能从任务文本猜测。
|
||||
3. 运行:
|
||||
|
||||
```bash
|
||||
/usr/bin/python3 -I -S <orc-skill-dir>/scripts/resolve_profile.py validate \
|
||||
<project-root>/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 <orc-skill-dir>/scripts/resolve_profile.py resolve \
|
||||
<project-root>/docs/orc/config.yaml --stage <stage> \
|
||||
--project-root <absolute-project-root> \
|
||||
--worktree <absolute-target-worktree> \
|
||||
[--global-level <low|mid|high>] [--stage-level <low|mid|high>] \
|
||||
[--model-auth <codex-login|openai|azure-openai>] \
|
||||
[--remote-auth <exact-provider-or-transport>]
|
||||
```
|
||||
|
||||
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 的反向引用。
|
||||
@@ -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
|
||||
@@ -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
|
||||
<resolver-terminalCreateShellCommand>
|
||||
```
|
||||
|
||||
2. 使用返回的具体 handle 等待 Agent TUI 可接收输入:
|
||||
|
||||
```bash
|
||||
orca terminal wait --terminal <handle> --for tui-idle \
|
||||
--timeout-ms 60000 --json
|
||||
```
|
||||
|
||||
3. 先为前置阶段创建 task,再用其 ID 构造依赖数组创建后置 task。task spec 使用
|
||||
`routing.md` 的 worker prompt 契约。
|
||||
4. 对已就绪 task 执行:
|
||||
|
||||
```bash
|
||||
orca orchestration dispatch --task <task-id> --to <handle> --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 的安全规则处理。
|
||||
@@ -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 和必要依赖结果。
|
||||
Executable
+889
@@ -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())
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user