feat(ack): add structured worker model routing

This commit is contained in:
2026-07-31 23:34:13 +08:00
parent ee66dbe9ce
commit ae3bce7b5d
23 changed files with 6491 additions and 327 deletions
+119 -4
View File
@@ -1,7 +1,7 @@
---
title: ACK 设计评审记录
date: "2026-07-31T17:15:37+08:00"
updated: "2026-07-31T20:45:09+08:00"
updated: "2026-07-31T23:31:48+08:00"
---
# ACK 设计评审记录
@@ -11,7 +11,8 @@ updated: "2026-07-31T20:45:09+08:00"
本文同时记录后续方案和实施结果。带“建议”“目标”或“待验证”的内容默认是设计
方向,不代表当前版本已经具备;实际运行契约仍以 `skills/ack/SKILL.md`、schema、
校验器和测试为准。知识护栏方案的 v0.9.0 落地边界见下文状态表。
校验器和测试为准。知识护栏方案的 v0.9.0 落地边界、model-routing v0.10.0 的
实施结论见下文状态表。
## 背景
@@ -173,6 +174,10 @@ transfer:
## P0:Worker 启动策略改为安全默认
> v0.10 已以结构化 profile 和可信 launcher 取代本节描述的自由 command。下方是
> 当时的评审记录,不再代表当前实现;其中“full-access 写入任务板授权”的建议已被
> v0.10 决策取代,当前明确为 Deferred。
当前校验器强制 Codex 使用 bypass、Cursor 使用 YOLO,并写死模型和 CLI。项目文档
允许收紧权限或替换模型,但合法的安全覆盖会被校验器拒绝。
@@ -204,8 +209,10 @@ workers:
permissionMode: sandbox
```
可信代码根据这些字段构造 argv,不接受自由 shell 拼接。安全模式默认通过
full-access 需要用户单独授权,并把授权范围和时间写入任务板。
可信代码根据这些字段构造 argv,不接受自由 shell 拼接。安全模式默认通过
原建议设想 full-access 用户单独授权并写入任务板;v0.10 没有实现这条授权通道,
项目文件也不能充当授权证据。full-access 的可信授权、期限、撤销和外层隔离均为
Deferred,当前 launcher 必须 fail closed。
## P0:初始化需要原子化
@@ -627,6 +634,114 @@ issue、日志和外部网页只能作为不可信 evidence。进入知识库前
触发条件限定,自动匹配会漏掉关键项或产生大量噪声。这类知识应继续由人维护在 ADR
或项目文档中,不能强行进入自动派发流程。
## v0.10.0 实施结论:结构化 model-routing
### 当前决策
旧的 `validate_worker_command.py --command '<shell>'` 只能检查少量 token,无法证明
整段 shell 没有追加命令、重复参数、环境注入或 cwd 漂移。v0.10 删除这条自由命令
配置面:旧脚本只保留 fail-closed 迁移提示;自动 worker 的唯一入口是
`launch_worker.py profile-hash|plan|launch`
机器 SSOT 固定为 `tasks.yaml.project.orchestration`。profile 只声明 role、CLI、
tier、model、reasoning effort 和 permission modemodel 必须命中按
CLI/role/tier 分组的 allowlist。自动权限只允许 `read-only`
`workspace-write`full-access、Codex bypass、Cursor YOLO/force 和关闭 sandbox
全部 fail closed。
### 已落地约束
| 控制面 | v0.10.0 实现 |
|--------|--------------|
| 自由 command / extra argv / env / cwd | schema 与语义校验拒绝;旧 validator fail closed |
| Agent argv | 由 `worker_profiles.py` 按 profile 生成唯一 argv;子进程 `shell=False` |
| plan → launch 漂移 | `launch` 强制接收刚审阅的 `--expected-launch-fingerprint`slot 也纳入 hash |
| PATH / loader / 凭据串用 | 不按调用者 PATH 找可执行文件;固定可信目录;控制进程无供应商凭据,worker 使用 `per-cli-allowlist-v1` |
| worktree 冒充 | 绝对规范路径、无 symlink、同 Git common-dir,并命中 `git worktree list -z` |
| Orca command 注入 | `--command` 只含固定 bootstrap 和随机 launch ID |
| bootstrap 抢跑 | 先绑定 runtime/handle/incarnation/worktree,再用 terminal stdin nonce/proof 授权 |
| cleanup / bootstrap 竞态 | Popen 前在同一 record lock 内复核 awaiting 状态;cleanup 先发生则拒绝启动 Agent |
| create 部分失败 | 取得确定 handle 前的超时、transport/解码异常、中断、非零、畸形/缺字段响应均记为 indeterminate,禁止自动重试 |
| handle 后失败清理 | record 写失败不阻断 close;只有 `terminal close --tab --json` 完整匹配且最终 failed 状态可靠落盘才记普通失败,否则保持 indeterminate/reconcile-required |
| receipt 漂移 | 规范化 profileHash、argvHash、slot、launchFingerprint 与 receiptHash 交叉校验 |
| dispatch 旧 receipt 改挂 | 每个角色显式记录 attemptIdreceipt 必须绑定当前 ACK task.id、同 role/profile/attemptreceiptId/attemptId 同空同填 |
| 配置兼容 | ackVersion 必须是合法 SemVerv0.10+ 的 orchestration/workerReceipts 必须同时存在,v0.9 旧板仍可只读校验和手动协作 |
### 不能被弱证明掩盖的边界
`receiptHash` 是无密钥 checksum,不是签名。项目内可写方能修改 receipt 后重算
hash;而当前 Orca terminal metadata 不提供原始 Agent argv、模型或权限
attestation。因此 v0.10 **不根据持久化 receipt 自动复用旧终端**。每次自动派发都
重新执行 plan,并用 expected fingerprint 启动 fresh worker;持久化 receipt 只作
审计和 dispatch 关联。
`launchFingerprint` 只证明完整计划没有漂移,不是一次性授权或幂等键;同一计划重复
执行仍会创建新的 fresh terminal。成功后不得重放,结果不确定时必须先 reconcile。
自动防重放要依赖后端原子 idempotency/claim 或项目外可信 launch intent,不能由
checksum 伪装提供。
任务板里的 dispatch 关联也不是宽松的历史索引。`dispatch.<role>` 显式记录
`profileId``receiptId``attemptId`,校验器要求 receipt 的 `createdFor` 精确绑定
当前 ACK `tasks[].id`、同一角色和同一 attempt,并要求 profile 一致。该约束阻止把
旧任务、旧角色或旧轮次 receipt 改挂到当前 dispatch,但不会把无密钥 receipt 升级成
可信 attestation。
同样,receipt 证明的是 launcher 请求和本地 CLI/runtime 绑定,不证明模型供应商最终
执行的模型。开放自动复用需要 Orca/ACP 提供启动参数 attestation,或项目外可信签发
与校验通道。开放 full-access 还需要不可由项目文本伪造的用户授权、期限、撤销和外层
隔离。这两项均留待后续版本。
威胁模型也明确到 Coordinator 账户边界:v0.10 防止任务板、任务内容、普通环境和
受限 worker 把数据变成第二个启动命令入口;不抵御已经完全控制 Coordinator OS
账户、Orca runtime、受信 Agent/Orca 可执行文件或用户级 Agent 配置/插件/MCP 的
攻击者。`permissionMode` 也不是模型供应商或外部工具能力 attestation;后者必须依赖
独立 OS 身份、受控 Agent 配置或平台能力。
## 后续版本提案:ACP 与可替换编排后端
### 当前决策
ACK 当前版本继续以 **Orca** 作为唯一自动编排后端。闭环语义保持工具无关,但
worker 创建、终端身份绑定、dispatch 和 wait 的可执行适配仍由 Orca 完成。
ACPAgent Client Protocol)作为后续版本的优先候选,当前只记录方案,不加入运行
时依赖、不新增半成品 adapter,也不让现有流程在 Orca 与 ACP 之间自动猜测。
| 能力 | 当前版本 | 后续方向 |
|------|----------|----------|
| 自动创建并监督 worker | Orca | ACP capability negotiation 后可增加 ACP adapter |
| 结构化模型与权限 profile | ACK 自己定义,Orca 只承载启动 | 保持为 ACK 稳定契约,不交给后端自由解释 |
| dispatch / wait / terminal identity | Orca runtime handle | 映射到 ACP session / request / event identity |
| 无自动后端时运行闭环 | 手动模式 | 可增加 PTY / Zellij 类低层 fallback,但不冒充语义协议 |
| 后端选择 | 项目显式配置 | 未来仍需显式配置,不按已安装命令静默切换 |
### 后端无关的最小语义
未来 adapter 只应实现以下能力,不应接管 ACK 的角色、状态机或安全策略:
1. `createWorker(profile, worktree)`:按 ACK 已验证的结构化 profile 创建 worker。
2. `inspectWorker(identity)`:返回可绑定的 runtime、session/incarnation 和活性。
3. `dispatch(identity, taskContext)`:把一个明确任务投递给已验证 worker。
4. `wait(identity, eventTypes)`:等待完成、复测、升级或 decision gate 事件。
5. `closeWorker(identity)`:只关闭本次创建且身份仍匹配的 worker。
模型、reasoning effort、权限模式、worktree 和 receipt hash 仍由 ACK 校验。adapter
不能重新开放自由 shell、任意 argv、任意环境变量或“后端默认模型”作为旁路。
### ACP 采用门槛
满足以下条件后再实现 ACP adapter
- 目标 Agent CLI 对 ACP 的启动、会话身份、取消和事件语义足够稳定。
- 能把 Developer/Test 的独立身份、worktree 和结构化 profile 映射到可核对字段。
- 能区分“已请求的模型/权限”和“运行时可观测事实”,不把客户端请求冒充供应商证明。
- 超时、重连、重复 dispatch 和 runtime 重启具有明确的幂等或恢复语义。
- 与 Orca adapter 使用同一组 ACK conformance tests,且不会降低安全默认。
在这些门槛满足前,ACP 保持 **Deferred**。PTY 或 Zellij 一类方案只能作为较低层的
进程/终端承载,不提供 session 语义、模型证明或任务状态机;如果以后加入,也必须
经过独立 adapter,不能散落为文档中的自由命令。
### 参考依据
- [GitHub 仓库自定义指令](https://docs.github.com/en/copilot/how-tos/copilot-on-github/customize-copilot/add-custom-instructions/add-repository-instructions)
+58 -3
View File
@@ -54,7 +54,7 @@ skills/ack/
├── references/ # 三角色规范、闭环流程和初始化说明
├── templates/ # project.md、tasks.yaml、knowledge.yaml 模板和 schema
├── examples/ # 完整示例
└── scripts/ # 状态校验、知识选择、安全验证执行与 worker 命令工具
└── scripts/ # 状态校验、知识选择、安全验证执行与结构化 worker launcher
```
`SKILL.md` 是 Agent 的工作流入口。`references/` 是按需读取的稳定规范;
@@ -115,6 +115,60 @@ python3 <ack-skill-dir>/scripts/run_verification.py \
执行;关键约束应继续下沉到测试、lint、CI 或正式规范。ACK 不自动修改项目的
`AGENTS.md``CLAUDE.md` 或其它 Agent 指令文件。
## 启动 Worker
worker 的机器配置位于 `tasks.yaml.project.orchestration`:项目显式维护模型
allowlist、结构化 profile、默认 profile 和允许的 worktree。不得在 `project.md`
或任务里保存完整启动命令、额外 argv 或环境变量。
先审阅计划,不产生终端:
```bash
python3 <ack-skill-dir>/scripts/launch_worker.py plan \
--project-root <project-root> \
--task-id <task-id> \
--attempt-id <task-id>-A<round> \
--role developer \
--profile-id codex-dev-standard \
--worktree <allowed-worktree>
```
确认后执行 `launch`,并显式绑定刚审阅的 fingerprint
```bash
python3 <ack-skill-dir>/scripts/launch_worker.py launch \
--project-root <project-root> \
--task-id <task-id> \
--attempt-id <task-id>-A<round> \
--role developer \
--profile-id codex-dev-standard \
--worktree <allowed-worktree> \
--expected-launch-fingerprint <plan 中的 sha256:...>
```
launcher 是自动创建 worker 的唯一入口:它从权威任务板重读 profile,构造固定
argv,忽略调用者 PATH、使用环境 allowlist,并验证真实 Git worktree 注册;然后
通过仓库外的单次启动记录、terminal-bound nonce/proof 和受限 bootstrap 调用 Orca。
返回的 receipt 含 `runtimeId`、handle、incarnation、profile hash、slot 和 worktree
identity。Coordinator 将 receipt 追加到顶层 `workerReceipts`,再把 receipt ID
写入任务的 `dispatch.developer``dispatch.test`,并同步写入本轮 `attemptId`
校验器要求 receipt 与当前 ACK task、角色、profile 和 attempt 完全一致;历史 receipt
不能跨任务或跨轮次改挂。
v0.10 自动 launcher 仅支持 `read-only``workspace-write`。full-access、
Codex bypass、Cursor YOLO/force 和关闭 sandbox 都会 fail closed;在有可信平台
审批或独立签发通道之前,不用项目文件伪装成用户授权。旧任务板没有结构化
`project.orchestration` 时仍可读取和手动协作,但不得自动创建 worker。
持久化 `receiptHash` 是无密钥 checksum,不是 launcher 身份证明。由于 Orca 当前
不能证明旧终端的原始 argv/模型/权限,v0.10 不自动复用既有 worker;每次自动派发
都重新 `plan` 并用 expected fingerprint 启动 fresh worker。
fingerprint 只校验完整计划没有漂移,不是一次性令牌;成功后不得用同一 fingerprint
重复启动,结果不确定时必须先 reconcile。
若创建或关闭回执不完整,或外部 launch record 状态无法可靠持久化,launcher 会返回
`indeterminate/reconcile-required`;必须先核对 record 与 Orca live state,不能
直接重试。
## 开始一个需求
初始化完成后可以直接说:
@@ -128,5 +182,6 @@ Coordinator 会先读取项目状态和 `references/kickoff.md`,生成产品
## 版本
当前 Skill 版本见 `VERSION`。新项目在 `tasks.yaml`记录 `ackVersion`。旧项目的
`kitVersion` 可以继续读取,但建议迁移为 `ackVersion`
当前 Skill 版本见 `VERSION`。新项目在 `tasks.yaml`以合法 SemVer 记录
`ackVersion`。从 `0.10.0` 起,`project.orchestration` 与顶层 `workerReceipts` 必须
同时存在;旧项目的 `kitVersion` 可以继续读取,但建议迁移为 `ackVersion`
+28 -7
View File
@@ -75,7 +75,9 @@ description: >-
- `docs/ack/tasks.yaml`
- `docs/ack/knowledge.yaml`
2. 读取 `<ack-skill-dir>/VERSION`,对比 `tasks.yaml` 的 `ackVersion`。旧项目只有
`kitVersion` 时仍可读取,但建议迁移为 `ackVersion`。
`kitVersion` 时仍可读取,但建议迁移为 `ackVersion`。`ackVersion` 必须是合法
SemVer;从 `0.10.0` 起 `project.orchestration` 与顶层 `workerReceipts` 必须同时
存在。
3. 查找未替换占位符,并核对项目路径、覆盖层路径、Developer 白盒命令、Test
黑盒命令和 Base URL。
4. 使用 `<ack-skill-dir>/scripts/validate_tasks.py` 校验任务板,使用
@@ -84,7 +86,12 @@ description: >-
字段缺失而宣称失败。
5. 检查知识引用能解析到固定 revision,candidate 仍留在任务证据中,且
`stale`、`superseded` 和 `archived` 不会被当作可派发的 `active` 知识。
6. 检查不会自动修复或覆盖现有配置;用户明确要求修复后再修改。
6. 若存在 `project.orchestration`,检查 profile、model allowlist、默认 profile、
允许 worktree、顶层 `workerReceipts` 与 `dispatch.developer/test` 的引用;receipt
必须绑定当前 ACK task、同一 role/profile/attempt`receiptId` 与 `attemptId`
必须同时为空或同时填写。
缺少结构化路由的旧任务板只能使用手动模式,不能自动创建 worker。
7. 检查不会自动修复或覆盖现有配置;用户明确要求修复后再修改。
## 工作
@@ -97,22 +104,30 @@ description: >-
- `<ack-skill-dir>/references/kickoff.md`
- kickoff 指定且与当前任务相关的 references 文件
3. 当前会话担任 Coordinator,遵守项目覆盖层中的命令、路径权限、模型路由和
worker 复用规则。项目覆盖层优先于通用示例命令。按 scope 推荐相关 `active`
worker 启动规则。项目覆盖层优先于通用示例命令。按 scope 推荐相关 `active`
知识,经确认后把固定 revision 的显式 `knowledgeRefs` 写入当前任务上下文;
不全量注入知识库。
4. 新需求先写产品文档、任务拆分与可观测验收信号,更新 `tasks.yaml` 并校验,
然后交给用户确认;确认前不派发实现。
5. 用户已确认的任务按 ACK 闭环执行:Developer 实现与白盒验证,Test 独立黑盒
5. 创建或更换 worker 时,只使用
`<ack-skill-dir>/scripts/launch_worker.py plan|launch` 读取
`tasks.yaml.project.orchestration` 的 profile。不得直接执行
`orca terminal create --command`,不得接受或拼接自由 command、额外 argv、
executable、env 或 cwd。必须先审阅 `plan.launchFingerprint`,再把它作为
`launch --expected-launch-fingerprint` 传入。v0.10 不根据持久化 receipt 自动
复用旧终端;每次自动派发都创建 fresh workerreceipt 只作审计与 dispatch
关联。
6. 用户已确认的任务按 ACK 闭环执行:Developer 实现与白盒验证,Test 独立黑盒
复测,Coordinator 读取证据终检并唯一写入 `tasks.yaml`。Developer 回报
`knowledgeApplied` 和 `knowledgeCandidates`Test 回报 `knowledgeChecks`
`candidate` 只有在独立验证和 gate 后才能由 Coordinator 写入或激活。
6. 执行知识项的 `verification.ref` 时,只调用
7. 执行知识项的 `verification.ref` 时,只调用
`<ack-skill-dir>/scripts/run_verification.py docs/ack/knowledge.yaml
<verification-ref> --project-root <project-root>`。不要直接执行选择器返回的 path/args,
也不要给 runner 注入额外命令或参数。
7. 不把 `worker_done` 或 Test 自报成功直接当作完成。每项最多三轮,仍失败则记录
8. 不把 `worker_done` 或 Test 自报成功直接当作完成。每项最多三轮,仍失败则记录
`leftover` 并继续其它任务。
8. 关键的安全、正确性和兼容性约束应下沉为测试、lint、CI 或正式规范;
9. 关键的安全、正确性和兼容性约束应下沉为测试、lint、CI 或正式规范;
`knowledge.yaml` 只保存触发条件、原因与证据引用,不能替代可执行控制。
## 边界
@@ -120,6 +135,12 @@ description: >-
- 不修改或追加任何项目 Agent 指令文件,包括 `AGENTS.md`。
- 不在项目中维护第二份 ACK 通用规范、模板或任务 schema。
- 不猜测项目命令、服务地址、worker handle 或模型名称。
- 不把 full-access、bypass、YOLO/force、关闭 sandbox 或项目内“授权”字段当成
v0.10 自动 worker 的合法配置;当前一律 fail closed。
- 不把无密钥 `receiptHash` 或 Orca live metadata 当作旧终端的启动 attestation
v0.10 不自动复用既有 worker。
- launcher 返回 `indeterminate` 或 `reconcile required` 时,不直接重试;先按
launch ID、外部 record 和 Orca live state 完成人工核对。
- 不覆盖已有 `docs/ack` 文件,不擅自提交、推送、创建终端或新 worktree。
- 只有 Coordinator 写 `tasks.yaml` 和 `knowledge.yaml`Developer 与 Test 只读,
只能通过回报提名或验证知识。
+1 -1
View File
@@ -1 +1 @@
0.9.0
0.10.0
+17 -7
View File
@@ -1,6 +1,6 @@
# notes-web Agent 协作协议(示例,项目覆盖层)
> 本项目基于 ack v0.9.0。
> 本项目基于 ack v0.10.0。
> 通用规范由 `/ack` 从 Skill 自身的 `references/` 读取,本文件只填项目差异。
> 覆盖层文件放在 `docs/ack/project.md`,不占用 `AGENTS.md`。
> ACK 不会自动修改 `AGENTS.md`、`CLAUDE.md` 或其它 Agent 指令文件。
@@ -26,13 +26,18 @@
- 派发 prompt 模板:`references/prompt-templates.md`
- Orca 编排命令:`references/orca-adapter.md`
## 模型档位
## Worker 路由
| 角色 | 默认档位 | 本项目实际 |
|------|----------|------------|
| Coordinator (PM) | 强模型 | claude-sonnet-5-thinking-high |
| Test | 中低模型 | 默认(中低) |
| Developer | 中低模型 | 默认(中低),架构任务临时升级 |
结构化配置位于 `docs/ack/tasks.yaml``project.orchestration`,启动记录位于顶层
`workerReceipts`。本项目默认使用:
| 角色 | profile ID | 档位 |
|------|------------|------|
| Developer | `codex-dev-standard` | standard |
| Test | `codex-test-standard` | standard |
| Developer 升级 | `codex-dev-strong` | strong |
本文件不保存或拼接 worker shell 命令。
## 路径权限
@@ -77,6 +82,11 @@ path/args。
- 三角色独立:Coordinator 只编排、Test 只验证、Developer 只实现。
- 模型分层:Coordinator 强模型不跑测试,Test/Developer 中低模型(见 references/model-routing.md)。
- 自动 worker 只通过 ACK 的 `scripts/launch_worker.py` 启动;不直接执行自由
`orca --command`。v0.10 的自动 profile 只允许 `read-only`
`workspace-write`,不允许 full-access、bypass、YOLO/force 或关闭 sandbox。
- 每次自动派发都先审阅 plan,再用 `--expected-launch-fingerprint` 启动 fresh
worker;持久化 receipt 只作审计,不授权自动复用旧终端。
- `worker_done` 与复测报告都不等于完成,必须 Test 独立复测 + Coordinator 终检后才能 `verified`
- 只有 Coordinator 写 `tasks.yaml``knowledge.yaml`Test 与 Developer 只读。
- Coordinator 只派发按 scope 命中并显式写入 `knowledgeRefs``active` 知识;
+86 -8
View File
@@ -1,8 +1,9 @@
# 填好的最小示例(虚构的 notes-web 项目)。可用 scripts/validate_tasks.py 校验通过。
# 填好的手动闭环示例(虚构的 notes-web 项目)。历史轮次没有自动 launcher receipt
# 因此使用 manual 模式。可用 scripts/validate_tasks.py 校验通过。
version: 1
updatedAt: "2026-07-06T09:40:00+08:00"
source: "Coordinator (PM) Agent"
ackVersion: "0.9.0"
ackVersion: "0.10.0"
project:
name: "notes-web"
repoPath: "/home/dev/notes-web"
@@ -10,6 +11,65 @@ project:
devWorktree: "/home/dev/notes-web-wt/fix-preview"
overlayFile: "docs/ack/project.md"
knowledgeFile: "docs/ack/knowledge.yaml"
orchestration:
profileVersion: 1
mode: "manual"
allowedWorktrees:
- "/home/dev/notes-web-wt/fix-preview"
modelAllowlist:
codex:
developer:
standard: ["gpt-5.6-terra"]
strong: ["gpt-5.6-sol"]
test:
standard: ["gpt-5.6-luna"]
cursor-agent:
developer:
standard: ["auto"]
test:
standard: ["auto"]
profiles:
codex-dev-standard:
role: "developer"
cli: "codex"
tier: "standard"
model: "gpt-5.6-terra"
reasoningEffort: "medium"
permissionMode: "workspace-write"
codex-test-standard:
role: "test"
cli: "codex"
tier: "standard"
model: "gpt-5.6-luna"
reasoningEffort: "low"
permissionMode: "workspace-write"
codex-dev-strong:
role: "developer"
cli: "codex"
tier: "strong"
model: "gpt-5.6-sol"
reasoningEffort: "high"
permissionMode: "workspace-write"
cursor-dev-standard:
role: "developer"
cli: "cursor-agent"
tier: "standard"
model: "auto"
reasoningEffort: null
permissionMode: "workspace-write"
cursor-test-standard:
role: "test"
cli: "cursor-agent"
tier: "standard"
model: "auto"
reasoningEffort: null
permissionMode: "workspace-write"
defaults:
developer: "codex-dev-standard"
test: "codex-test-standard"
developerUpgraded: "codex-dev-strong"
workerReceipts: []
summary:
verified: ["BUG-002"]
@@ -65,9 +125,18 @@ tasks:
checks:
- "预览后出现 4 行 difftitle/before/after/coverChanged"
dispatch:
taskId: "orca-task-88"
dispatchId: "orca-disp-91"
worker: "dev-worker-1"
developer:
profileId: "codex-dev-standard"
receiptId: null
attemptId: null
taskId: null
dispatchId: null
test:
profileId: "codex-test-standard"
receiptId: null
attemptId: null
taskId: null
dispatchId: null
rounds:
- round: 1
attemptId: "BUG-002-A1"
@@ -130,9 +199,18 @@ tasks:
checks:
- "并发保存时后写方收到冲突提示"
dispatch:
taskId: "orca-task-90"
dispatchId: "orca-disp-95"
worker: "dev-worker-1"
developer:
profileId: "codex-dev-standard"
receiptId: null
attemptId: null
taskId: null
dispatchId: null
test:
profileId: "codex-test-standard"
receiptId: null
attemptId: null
taskId: null
dispatchId: null
rounds:
- round: 1
attemptId: "BUG-003-A1"
+14 -2
View File
@@ -18,6 +18,9 @@
`docs/ack/knowledge.yaml`
- [ ] 技术栈、运行、构建、单测和集成测试命令均来自项目证据。
- [ ] Coordinator、Developer、Test 的模型档位和升级规则已明确。
- [ ] `project.orchestration` 使用受支持的 profileVersion,模型都命中项目
allowlist,默认 profile 与角色/档位一致。
- [ ] `allowedWorktrees` 只列出同一 Git common-dir 下已经核对的绝对 worktree。
## 路径权限
@@ -56,8 +59,17 @@
## 编排
- [ ] 已选择 Orca 或手动模式。
- [ ] 派发前优先复用同 worktree、同角色、同配置的空闲 worker。
- [ ] Developer 与 Test 的启动命令通过校验
- [ ] 自动创建只调用 `launch_worker.py plan|launch`,没有直接拼接
`orca terminal create --command` 或自由 worker 命令
- [ ] `launch` 使用刚审阅的 `plan.launchFingerprint` 作为
`--expected-launch-fingerprint`;漂移时重新 plan。
- [ ] Developer/Test 只使用 `read-only``workspace-write` profile
v0.10 没有 full-access、bypass、YOLO/force 或关闭 sandbox。
- [ ] v0.10 每次自动派发都启动 fresh worker;没有把历史 receipt checksum 或
Orca live metadata 当作自动复用授权。
- [ ] launcher receipt 已追加到顶层 `workerReceipts`,任务分别引用
`dispatch.developer``dispatch.test`;每个引用的 task/role/profile/attempt
都与当前任务、本轮 receipt 完全一致,没有用单一 handle 混淆两个角色。
- [ ] 多 worktree 场景只有一个权威 `tasks.yaml``knowledge.yaml`
- [ ] Test 使用的服务来自正确 worktree。
+50 -11
View File
@@ -2,8 +2,8 @@
本文件定义**与具体编排工具无关**的三角色协作闭环。运行时调度可以用 Orca(见 `orca-adapter.md`),也可以手动跑(见下方「手动模式」)。
原则:调度消息只是运行时载体,任务结论必须回写到 `tasks.yaml`跨任务复用、已经
验证的项目经验必须由 Coordinator 回写到 `knowledge.yaml`。不要把消息当最终记录。
原则:调度消息只是运行时载体,任务结论必须回写到 `tasks.yaml`;已经验证且可跨
任务使用的项目经验必须由 Coordinator 回写到 `knowledge.yaml`。不要把消息当最终记录。
角色定义见 `roles-and-permissions.md`Coordinator 只编排、Test 只验证、
Developer 只实现。
@@ -34,10 +34,13 @@ Coordinator 发现或读取 open 任务
-> 从 knowledge.yaml 按 scope 推荐 active 知识,Coordinator 确认固定 revision 的 knowledgeRefs
-> 为新轮次生成稳定 attemptId<task-id>-A<round>),Developer 与 Test 共用
-> 决定 worktree:当前 worktree 起子 agent,还是新建隔离 worktree(见下节「子任务放哪」)
-> 解析 worker:先复用同 worktree、同角色的空闲 worker;没有可复用项时才校验命令并新建(见 orca-adapter.md
-> 解析安全 profile:机器配置只读 tasks.yaml.project.orchestration
-> 运行 launcher plan 并审阅 launchFingerprint
-> 带 expected fingerprint 启动 fresh worker,把 receipt 写回 tasks.yaml(见 orca-adapter.md
-> dispatch 给 Developer--to <worker handle>
-> waitDeveloper 的 worker_done / escalation(含 knowledgeApplied / knowledgeCandidates
-> writeback fixed_by_dev
-> 为 Test 独立解析安全 profile,重新 plan/launch fresh worker 并写回 receipt
-> dispatch 给 Testretesting
-> waitTest 的 retest_result(含 knowledgeChecks 和 candidate 独立证据)
-> Test 通过:gateCoordinator 读证据对齐意图)
@@ -48,7 +51,9 @@ Coordinator 发现或读取 open 任务
```
一次派发只修一个明确问题(细则见 `optimization-method.md` §「每轮派发只修一个明确问题」)。
「决定 worktree」「解析 worker」两步的决策见下节与 `model-routing.md` / `orca-adapter.md`
「决定 worktree」「解析 profile/receipt」几步的决策见下节与 `model-routing.md` /
`orca-adapter.md`。v0.10 不自动复用历史 receipt 或既有终端;标题、preview、分支名、
worker 自报和无密钥 receipt checksum 都不能代替可信启动 attestation。
Coordinator 默认给新逻辑轮次生成 `<task-id>-A<round>`,并在记录轮次结果时写入
`tasks[].dispatch.rounds[].attemptId`。它独立于编排工具产生的 `taskId`
`dispatchId`:前者用于知识来源追溯,后两者只用于查询运行时。0.9.0 之前的历史
@@ -101,23 +106,48 @@ Test 对显式引用回报 `not_applicable` 时,说明 Coordinator 的选择
Coordinator 派发前先决定 Developer/Test 在哪工作。两种方式:
**方式 1:当前 worktree 复用或起子 agent(终端)**
**方式 1:当前 worktree 起 fresh 子 agent(终端)**
```bash
# 先按 orca-adapter.md 的 worker 复用流程查找空闲终端;没有时才创建
orca terminal create --worktree active --command "cursor-agent --yolo --model auto" --title "ACK-DEV-CURSOR-AUTO-1" --json
# v0.10 不自动复用既有终端;先审阅 plan:
python3 <ack-skill-dir>/scripts/launch_worker.py plan \
--project-root <project-root> --task-id <task-id> \
--attempt-id <task-id>-A<round> --role <developer-or-test> \
--profile-id <profile-id> --worktree <absolute-current-worktree> --slot <1..99>
python3 <ack-skill-dir>/scripts/launch_worker.py launch \
--project-root <project-root> --task-id <task-id> \
--attempt-id <task-id>-A<round> --role <developer-or-test> \
--profile-id <profile-id> --worktree <absolute-current-worktree> --slot <1..99> \
--expected-launch-fingerprint <plan 中的 sha256:...>
```
- 适合:串行闭环、一次一个 Developer 任务、小改动、Test 要复测的正是 Developer 改的那棵树。
- 优点:单一服务实例、无跨 worktree 对齐困扰、开销小。
- 风险:多个 agent 同时改同一棵树会互相踩(半构建状态、git 冲突)。
- 记录:`launch` 成功后把 receipt 追加到权威 `tasks.yaml.workerReceipts`,并把
`receipt.id` 写入 `dispatch.<role>.receiptId`、把 `profileId` 写入
`dispatch.<role>.profileId`、把本轮稳定 ID 写入 `dispatch.<role>.attemptId`
receipt 的 task/role/profile/attempt 必须与当前 dispatch 完全一致,不能只记录
一个裸 handle,也不能引用旧任务或旧轮次 receipt。
**方式 2:新建隔离 worktree,再在其中起子 agent**
创建成功后,先把 Orca 返回的绝对路径加入权威任务板的
`project.orchestration.allowedWorktrees` 并重新运行 `validate_tasks.py`;未进入
allowlist 的路径不能交给 launcher。
```bash
orca worktree create --name <feature> --base-branch <base> --json
# 然后先查该 worktree 的同角色空闲终端;没有时才创建
orca terminal create --worktree path:<new> --command "cursor-agent --yolo --model auto" --title "ACK-DEV-CURSOR-AUTO-1"
# 对返回的绝对路径调用 ACK launcher
python3 <ack-skill-dir>/scripts/launch_worker.py plan \
--project-root <project-root> --task-id <task-id> \
--attempt-id <task-id>-A<round> --role <developer-or-test> \
--profile-id <profile-id> --worktree <absolute-new-worktree> --slot <1..99>
python3 <ack-skill-dir>/scripts/launch_worker.py launch \
--project-root <project-root> --task-id <task-id> \
--attempt-id <task-id>-A<round> --role <developer-or-test> \
--profile-id <profile-id> --worktree <absolute-new-worktree> --slot <1..99> \
--expected-launch-fingerprint <plan 中的 sha256:...>
```
- 适合:并行多个互不依赖的子任务、大/高风险/实验性改动、要保持基线分支干净(如 prod 不动、feature 走独立分支)、要独立 build 或跑独立服务实例、best-of-N 尝试。
@@ -136,8 +166,13 @@ orca terminal create --worktree path:<new> --command "cursor-agent --yolo --mode
**项目状态(SSOT)只落一处**:无论开几个 worktree`tasks.yaml`
`knowledge.yaml` 都只认一个权威副本(通常在基线/协调所在 worktree),由
Coordinator 单写。不要每个 worktree 各留一份会分叉的项目状态。模型固定方式见
`model-routing.md``orca-adapter.md`
Coordinator 单写。`project.orchestration`、顶层 `workerReceipts` 和任务 dispatch
也只写入这个副本;不要每个 worktree 各留一份会分叉的项目状态。profile 解析、
launcher 与 receipt 规则见 `model-routing.md``orca-adapter.md`
两种 worktree 方式都只允许 profile 中的 `read-only` / `workspace-write` 安全权限。
v0.10 的 full-access 授权与撤销机制仍是 Deferredlauncher 遇到 full-access 或等价
的 sandbox 绕过请求必须 fail closed。
---
@@ -158,6 +193,10 @@ Coordinator 单写。不要每个 worktree 各留一份会分叉的项目状态
手动模式下同样遵守:worker_done / 复测报告都不等于最终结论、只有 Coordinator
`tasks.yaml``knowledge.yaml`、三轮失败留档。
手动交给用户已打开的会话不等于产生 ACK receipt。该会话可以完成本次显式 handoff,
但不能因此进入 Orca 自动派发信任路径;后续自动派发仍需重新通过 ACK launcher
创建 fresh worker。
---
## Test 复测(编排无关)
+10 -1
View File
@@ -68,10 +68,14 @@ docs/ack/
编辑 `docs/ack/tasks.yaml`
- `ackVersion` 使用 ACK Skill 的 `VERSION`
- `ackVersion` 使用 ACK Skill 的合法 SemVer `VERSION`;从 `0.10.0`
`project.orchestration` 与顶层 `workerReceipts` 必须同时存在。
- `updatedAt` 使用当前带时区时间。
- `project.name``repoPath``devWorktree``overlayFile``knowledgeFile` 使用
真实值。
- `project.orchestration.allowedWorktrees` 使用已核对的绝对 worktree;模型
allowlist、profiles 和 defaults 使用项目实际允许值。不要把完整启动命令、
`extraArgs``env` 或任意 executable 写进任务板。
- 非服务项目的 `baseUrl` 写为 `n/a`
- 没有真实任务时使用 `tasks: []`,不要保留或虚构示例任务。
@@ -109,6 +113,11 @@ python3 <ack-skill-dir>/scripts/validate_knowledge.py docs/ack/knowledge.yaml --
- `project.overlayFile` 指向真实文件。
- `project.knowledgeFile` 指向 `docs/ack/knowledge.yaml`
- Developer 与 Test 的验证命令可执行。
- `project.orchestration` 的 profile/allowlist/defaults 通过校验,自动模式只允许
`read-only``workspace-write`;旧任务板未迁移时保持手动模式。
- 顶层 `workerReceipts``dispatch.developer/test` 的 task/role/profile/attempt
引用一致;`receiptId``attemptId` 同时为空或同时填写。持久 receipt 只作审计,
v0.10 不自动复用旧终端。
- 网站或 API 项目写清服务启动、重启和 Base URL。
- 任务中的固定 revision `knowledgeRefs` 都能解析,非 `active` 条目没有被派发。
+64 -21
View File
@@ -6,7 +6,11 @@
## 前提:谁是 Coordinator
**你(发起编排的强模型会话)就是 Coordinator (PM) / 产品。** 你负责写文档、拆任务、编排、终检,**不亲自写代码、不亲自跑测试**。开发和测试是另起的 worker agent;具体 CLI、模型、reasoning effort 和执行模式必须先读项目覆盖层 `docs/ack/project.md`,禁止照抄未核对的默认命令。
**你(发起编排的强模型会话)就是 Coordinator (PM) / 产品。** 你负责写文档、拆任务、
编排、终检,**不亲自写代码、不亲自跑测试**。开发和测试是另起的 worker agent
具体 CLI、模型、reasoning effort 和执行模式的机器事实源是
`docs/ack/tasks.yaml``project.orchestration``docs/ack/project.md` 只解释项目
差异,不能提供另一套启动命令。
---
@@ -16,7 +20,7 @@
我要做一个新需求:<一句话需求>。
你作为 ack 的 Coordinator(PM),按 ACK Skill 的 references 规范执行:
1. 先读 docs/ack/project.md、docs/ack/tasks.yaml,校验 docs/ack/knowledge.yaml 并
1. 先读 docs/ack/project.md、docs/ack/tasks.yaml(包括 project.orchestration,校验 docs/ack/knowledge.yaml 并
用 `scripts/select_knowledge.py` 只读取当前任务相关的 active 条目,再读
references/roles-and-permissions.md、closed-loop.md、optimization-method.md。
2. 写产品文档到 docs/(PRD / 交互 / 验收),把需求拆成任务,每个任务的验收写成可观测信号(可见文本 / API 结果 / 交互结果)。
@@ -24,7 +28,8 @@
knowledgeRefs 写入任务;不要派发 candidate 或全量知识库。
4. 把任务写进 docs/ack/tasks.yaml(只有你写),校验 tasks.yaml 和 knowledge.yaml。
5. 先把「产品文档 + 任务拆分 + 验收信号 + 适用知识引用」给我确认,不要急着派发。
6. 我确认后,按 ack 闭环循环:先按 docs/ack/project.md 校验 Developer/Test worker 启动命令,
6. 我确认后,按 ack 闭环循环:先用 `scripts/launch_worker.py` 校验结构化
Developer/Test profile,审阅 plan 后用 expected fingerprint 创建 fresh worker
dispatch 开发 → worker_done → dispatch 测试独立复测 → 你读证据终检 → 回写 tasks.yaml
每个任务最多三轮,三轮不过记 leftover 并升级我复盘。
```
@@ -45,6 +50,9 @@ python3 <ack-skill-dir>/scripts/validate_tasks.py docs/ack/tasks.yaml
python3 <ack-skill-dir>/scripts/validate_knowledge.py docs/ack/knowledge.yaml --tasks docs/ack/tasks.yaml
```
`project.orchestration` 是 worker profile 的机器 SSOT;未知字段、非 allowlist 模型、
不匹配的角色/档位和非安全权限必须在派发前 fail closed。
5. **停下来给人确认**:这是强模型该花时间的地方,不要跳过。
---
@@ -58,26 +66,58 @@ python3 <ack-skill-dir>/scripts/validate_knowledge.py docs/ack/knowledge.yaml --
---
## 第 3 步:复用或启动 worker
## 第 3 步:审阅并启动 worker
先读 `docs/ack/project.md` 的项目覆盖规则,再按 `orca-adapter.md` §「解析并复用 worker」执行:先列出现有终端和活跃任务,优先复用同 worktree、同角色、配置兼容且空闲的 worker。只有没有可复用项时才创建新终端;如果项目提供 `validate_worker_command.py`,校验未通过时不得创建。
先读 `tasks.yaml.project.orchestration` 的结构化 profiles。v0.10 不把历史 receipt
或既有 Orca terminal 当作自动复用授权;每次自动派发都创建 fresh worker。原因和
边界见 `model-routing.md` §「Receipt、审计与复用边界」。
先查看目标 profile hash,确认本次结构化配置。这个 hash 只用于审计和漂移比较,
不能用于匹配或复用旧 receipt / 既有终端:
```bash
# Codex 示例(模型与执行模式以项目覆盖层为准)
DEV_CMD='codex --dangerously-bypass-approvals-and-sandbox -m gpt-5.6-terra -c model_reasoning_effort=medium'
TEST_CMD='codex --dangerously-bypass-approvals-and-sandbox -m gpt-5.6-luna -c model_reasoning_effort=low'
python3 <ack-skill-dir>/scripts/validate_worker_command.py --role developer --command "$DEV_CMD"
python3 <ack-skill-dir>/scripts/validate_worker_command.py --role test --command "$TEST_CMD"
orca terminal create --worktree active --command "$DEV_CMD" --title "ACK-DEV-CODEX-TERRA-1" --json
orca terminal create --worktree active --command "$TEST_CMD" --title "ACK-TEST-CODEX-LUNA-1" --json
# Cursor 示例
CURSOR_CMD='cursor-agent --yolo --model auto'
python3 <ack-skill-dir>/scripts/validate_worker_command.py --role developer --command "$CURSOR_CMD"
orca terminal create --worktree active --command "$CURSOR_CMD" --title "ACK-DEV-CURSOR-AUTO-1" --json
python3 <ack-skill-dir>/scripts/launch_worker.py profile-hash \
--project-root <project-root> --profile-id <profile-id>
```
上面的 `terminal create` 都是“没有空闲兼容 worker”时的兜底,不是每次 kickoff 的固定动作。新 worktree 时先 `orca worktree create --name <feature> --base-branch <base>`,再解析该 worktree 的 worker。选型与升级见 `model-routing.md`。新建终端后、dispatch 前必须读取启动信息,确认实际 CLI、模型和 reasoning effort;派发后把实际 handle 写入对应任务的 `dispatch.worker`
生成只读计划:
```bash
python3 <ack-skill-dir>/scripts/launch_worker.py plan \
--project-root <project-root> --task-id <task-id> \
--attempt-id <task-id>-A<round> --role <developer-or-test> \
--profile-id <profile-id> --worktree <absolute-worktree> --slot <1..99>
```
审阅计划后执行唯一创建入口:
```bash
python3 <ack-skill-dir>/scripts/launch_worker.py launch \
--project-root <project-root> --task-id <task-id> \
--attempt-id <task-id>-A<round> --role <developer-or-test> \
--profile-id <profile-id> --worktree <absolute-worktree> --slot <1..99> \
--expected-launch-fingerprint <plan 中的 sha256:...>
```
`launch` 的 fingerprint 必须来自刚才审阅的 plan;若中间任何关键事实漂移,重新
plan,不得覆盖检查。标准输出是单一 JSON。Coordinator 校验其中的 receipt 后,把
receipt 原样追加到 `tasks.yaml.workerReceipts`,把 `receipt.id` 写入该任务
`dispatch.<role>.receiptId`、把 `profileId` 写入 `dispatch.<role>.profileId`,并把
本轮稳定 ID 写入 `dispatch.<role>.attemptId`。三者必须与 receipt 的当前
task/role/profile/attempt 完全绑定;不得把旧任务或旧轮次的 receipt 改挂到当前
dispatch。后续
紧随其后的本次 Orca dispatch 只使用这次 fresh receipt binding 中的 handle;持久化
receipt 仅用于审计,不能在后续轮次自动复用。不要自行拼装 Agent CLI 或 Orca 的
底层创建命令。
新 worktree 时可先运行
`orca worktree create --name <feature> --base-branch <base> --json`,再对返回的绝对
worktree 走同一套 `plan` -> 带 expected fingerprint 的 `launch`。在调用 launcher
前,先把新
路径加入 `project.orchestration.allowedWorktrees` 并重新运行任务板校验。profile
只允许 `read-only``workspace-write`v0.10 的 full-access 授权通道尚未实现,
任何 bypass、YOLO/force 或关闭 sandbox 的请求都必须失败,不能手写命令兜底。
选型与升级见 `model-routing.md`
---
@@ -85,6 +125,7 @@ orca terminal create --worktree active --command "$CURSOR_CMD" --title "ACK-DEV-
```text
task-create → dispatch 给 DEV → 等 worker_done
→ 每个角色都通过 plan + expected fingerprint launch fresh worker 并写回 receipt
→ 每轮使用 Coordinator 分配的稳定 <task-id>-A<round>
→ 回写 fixed_by_dev → dispatch 给 TEST 复测 → 等 retest_result
→ Developer 回 knowledgeApplied / knowledgeCandidatesTest 回 knowledgeChecks
@@ -110,6 +151,8 @@ Coordinator 只内联本轮 `knowledgeRefs` 指向的少量知识,不要求 wo
## 一句话
产品文档 + 验收信号写在前(你,强模型)→ 确认显式 `knowledgeRefs`按项目
覆盖层校验并启动 DEV/TEST → 核对实际模型 → dispatch / 复测 / 终检循环 → 任务
结论落 `tasks.yaml`,验证后的跨任务知识由 Coordinator 落 `knowledge.yaml`
产品文档 + 验收信号写在前(你,强模型)→ 确认显式 `knowledgeRefs`
`tasks.yaml.project.orchestration` 解析安全 profile → 审阅 plan 并用 expected
fingerprint 启动 fresh DEV/TEST → dispatch / 复测 / 终检循环 → 任务结论落
`tasks.yaml`,验证后的
跨任务知识由 Coordinator 落 `knowledge.yaml`
+164 -41
View File
@@ -1,8 +1,11 @@
# 模型路由(稳定核心)
本文件是**三角色默认模型档位**和**升级规则**的单一事实源(SSOT)。目标:在不牺牲质量的前提下降低 token 和模型成本——把昂贵的强模型留给需要判断的工作,把机械执行交给较弱模型
本文件是**三角色默认模型档位、升级规则和 worker profile 选择规则**的单一规范源
目标是在不牺牲质量的前提下降低 token 和模型成本:把昂贵的强模型留给需要判断的
工作,把机械执行交给较弱模型。
角色定义见 `roles-and-permissions.md`Coordinator 编排 / Test 验证 / Developer 实现)。本文件只补一层正交的「用哪个档位的模型」。
角色定义见 `roles-and-permissions.md`Coordinator 编排 / Test 验证 / Developer
实现)。本文件只补一层正交的「用哪个档位、以什么安全 profile 启动 worker」。
---
@@ -14,7 +17,9 @@
| Test | 中低模型 | 按既定验收信号执行浏览器/API/脚本,主要做观察、记录、逐条 pass/fail |
| Developer | 中低模型(按任务升级) | 多数实现可照规格执行;跨系统、数据迁移、重复失败时再升级 |
关键点:Coordinator 用强模型但**不亲自跑测试**(测试由 Test 承担),所以强模型的 token 花在思考和终检上,而不是反复点击页面、跑 smoke、复制日志。这一分工天然省 token,同时保持「验证者 ≠ 实现者」。
Coordinator 用强模型但**不亲自跑测试**(测试由 Test 承担),所以强模型的 token
花在思考和终检上,而不是反复点击页面、跑 smoke、复制日志。这一分工天然省 token,
同时保持「验证者 != 实现者」。
---
@@ -51,67 +56,183 @@
- 需要设计新的抽象。
- 低档位反复产出表面修复。
升级动作本身由 Coordinator 判断并记录(可写进 `tasks.yaml``dispatch` 备注或 `resolution`)。
升级由 Coordinator 判断并记录原因。升级不是修改一个正在运行的终端:必须选择
对应强档 profile,重新计算 `profileHash`,再通过 launcher 创建新的 worker。
v0.10 不自动复用持久化 receipt 指向的旧终端,Test 也不得使用 Developer 的强档
worker。
---
## 如何给 worker 指定模型(编排相关)
## 结构化 worker profile
**模型不由编排层设置。** Orca 的 `orchestration task-create` / `dispatch` 没有 `--model` 参数——`dispatch` 只是把任务投递给一个已存在的终端 handle。**模型在创建 worker 终端、启动 agent CLI 时用 CLI 自带的 flag 固定**,之后该终端的所有 dispatch 都用这个模型。
### 机器事实源
**默认不跨 Agent CLI 创建 worker。** Coordinator 应按自己所在的运行环境选择同类 worker:Cursor 会话创建 `cursor-agent` workerCodex 会话创建 `codex` worker。不要依赖 Coordinator 凭模型回答来猜测运行环境或精确模型名;以实际 CLI / 终端环境为准。只有项目 overlay 或用户明确指定跨 Agent 时,才混用不同 CLI,并记录原因。
worker 路由的机器可读事实只保存在 `docs/ack/tasks.yaml`
`project.orchestration``project.md` 可以解释项目为何选某个档位,但不能另写一份
完整启动命令或覆盖机器配置。字段结构以 `templates/tasks.schema.json` 为准。
因此「档位 → 具体模型」的映射是 **agent 相关** 的,落地方式见 `orca-adapter.md` §「给 worker 终端固定模型」。常见 CLI
每个 profile 明确声明
| Agent CLI | 指定模型的方式 |
|-----------|----------------|
| Cursor (`cursor-agent`) | `cursor-agent --model <model>``--model auto` 让 Cursor 自动选(推荐给 Test/Developer worker |
| Codex (`codex`) | `codex -m <model> -c model_reasoning_effort=<effort>` |
| 其它(opencode 等) | 用各自 CLI 的模型参数或配置 |
- `role``developer``test`
- `cli`:受支持的 Agent CLI
- `tier`:角色模型档位;
- `model`:项目 allowlist 中的精确模型 ID
- `reasoningEffort`CLI 支持时显式声明;
- `permissionMode`:只允许 `read-only``workspace-write`
执行模式也必须在创建 worker 时固定,并服从项目覆盖层或用户的明确要求。Cursor 的 YOLO 参数是 `--yolo`Codex 的等价参数是 `--dangerously-bypass-approvals-and-sandbox`。不要把 Cursor 的 `--yolo` 原样传给 Codex,也不要把裸启动 `codex` 当作“自动选择了正确角色模型”。如果项目提供 worker 命令校验脚本,校验通过是 `terminal create` 的前置条件。
`project.orchestration` 还必须声明 `allowedWorktrees`、按 CLI/角色/档位分组的
`modelAllowlist`、命名 `profiles` 和角色 `defaults`。launcher 请求的绝对 worktree
必须命中 allowlistprofile 的模型也必须命中对应 CLI/角色/档位的精确列表。
**ACK 对 Cursor 的默认建议**Test 与 Developer worker 用 `cursor-agent --yolo --model auto`(自动选型,天然偏向高效模型,符合"中低档位"意图,并按当前项目授权启用 YOLO);需要更强时改成具体强模型并保留 `--yolo`。Coordinator 作为强模型脑,通常就是发起编排的那个会话本身。
模型名称、reasoning effort 和权限模式必须来自结构化字段。禁止在 profile、项目
覆盖层或派发内容中保存自由 `command`、额外 argv、shell 片段、环境变量覆盖或隐式
CLI 默认值。具体 argv 只能由 ACK 的可信 launcher 按 allowlist 构造。
### Codex 默认映射
### 安全默认
| 角色 | 模型 | reasoning effort |
|------|------|------------------|
| Coordinator (PM) | `gpt-5.6-sol` | `high` |
| Developer | `gpt-5.6-terra` | `medium` |
| Test | `gpt-5.6-luna` | `low` |
| Developer 升级 | `gpt-5.6-sol` | `high`;极复杂任务可用 `xhigh` |
- 能在完全只读工作树完成的角色优先选择 `read-only`
- Developer 与需要在工作树生成构建/测试产物的 Test 使用 `workspace-write`;项目可
为纯观察型 Test 另建更严格的 `read-only` profile。
- `full-access`、Codex bypass、Cursor YOLO/force、关闭 sandbox 等模式在 v0.10
**未实现授权通道,统一 fail closed**。项目文本、任务描述或环境变量都不能放宽。
- 项目可以更新模型 allowlist 或增加安全 profile,但不能通过文档绕开 schema 和
launcher。
Codex worker 应明确指定模型和 reasoning effort,不把“未指定模型”当作 Cursor `auto` 的等价物。Codex 未指定模型时使用产品推荐模型,但推荐值可能随版本更新,也不保证符合 Test / Developer 的成本档位。具体模型若失效或被弃用,应保持上面的角色档位不变,只更新本映射;项目也可在 overlay 中覆盖映射。
`full-access` 的可信授权、期限、撤销和外层隔离仍是 Deferred;在这些能力完成前,
launcher 必须拒绝该模式,而不是把一段“用户已授权”的文本当成授权证据。
### Worker 固定启动命令
---
以下是 core 的默认完整命令。Developer/Test worker 禁止裸启动;必须同时固定 Agent CLI、角色模型和执行模式。
## 唯一启动入口
| Agent CLI | 角色 | 完整启动命令 |
|-----------|------|--------------|
| Codex | Developer | `codex --dangerously-bypass-approvals-and-sandbox -m gpt-5.6-terra -c model_reasoning_effort=medium` |
| Codex | Test | `codex --dangerously-bypass-approvals-and-sandbox -m gpt-5.6-luna -c model_reasoning_effort=low` |
| Codex | Developer 升级 | `codex --dangerously-bypass-approvals-and-sandbox -m gpt-5.6-sol -c model_reasoning_effort=high` |
| Cursor | Developer / Test | `cursor-agent --yolo --model auto` |
worker 的 profile 解析、计划和启动只允许通过:
硬规则:
```bash
python3 <ack-skill-dir>/scripts/launch_worker.py profile-hash \
--project-root <project-root> --profile-id <profile-id>
1. Codex worker 必须带 `--dangerously-bypass-approvals-and-sandbox`Cursor worker 必须带 `--yolo`
2. 禁止用裸 `codex`、裸 `cursor-agent` 或只指定 CLI 而不指定角色模型。
3. 创建终端前先运行项目提供的 worker 命令校验器;校验失败时不得执行 `orca terminal create`
4. 创建终端后、dispatch 前读取启动信息,确认实际 CLI、模型和 reasoning effort 与命令一致。
5. Developer 升级到强模型必须由 Coordinator 记录原因;Test 不得复用 Developer 的强模型终端。
6. YOLO 会跳过确认和沙箱,只能在用户已明确授权、外层运行环境受控时启用;项目覆盖层可以收紧执行模式,不能静默放宽。
python3 <ack-skill-dir>/scripts/launch_worker.py plan \
--project-root <project-root> --task-id <task-id> \
--attempt-id <task-id>-A<round> --role <developer-or-test> \
--profile-id <profile-id> --worktree <absolute-worktree> [--slot <1..99>]
项目可以覆盖模型名称或收紧权限,但必须在 `project.md` 中写出完整替代命令,不能只写“auto”“默认模型”或依赖当前 CLI 的隐式默认值。
python3 <ack-skill-dir>/scripts/launch_worker.py launch \
--project-root <project-root> --task-id <task-id> \
--attempt-id <task-id>-A<round> --role <developer-or-test> \
--profile-id <profile-id> --worktree <absolute-worktree> [--slot <1..99>] \
--expected-launch-fingerprint <plan 中的 sha256:...>
```
`--project-root` 始终指向保存权威 `docs/ack/tasks.yaml` 的项目根;`--worktree` 是本次
worker 实际工作的绝对路径,两者可以不同。后者仍必须属于同一项目并命中
`project.orchestration.allowedWorktrees`
- `profile-hash` 读取 `tasks.yaml` 中的 profile,输出确定性 hash。
- `plan` 只校验并展示将使用的结构化计划,不创建终端。
- `launch` 是唯一允许创建 worker 的入口;它要求传入刚审阅的
`plan.launchFingerprint`,当前 profile、任务/attempt、worktree、CLI 或 argv
任一事实漂移都会在创建前失败。它内部固定 Orca 调用和 Agent argv,标准输出只有
一份 JSON 结果。
- launcher 的内部 bootstrap 属于私有协议,Coordinator 不直接调用、不复制到项目
文档,也不自行拼装 Orca 的底层终端创建文本。
Coordinator 在执行 `launch` 前必须先审阅 `plan`。任何校验失败、字段未知、路径不
匹配、CLI 不可用或权限模式不支持,都停止创建并回报,不回退到裸 Agent 命令。
---
## 可执行信任边界
v0.10 的 launcher 还执行以下约束:
- 不读取调用者传入的 `PATH` 来寻找 `git`、Orca 或 Agent CLI,只在固定的系统与
当前用户工具目录中解析受支持的可执行文件;候选目标必须由 root/当前用户拥有、
不是 group/other writable,并在 plan 与 bootstrap 间保持相同 device/inode。
- Git、Orca 和 CLI version probe 使用不含供应商凭据的控制环境;worker 使用
`per-cli-allowlist-v1`Codex 只取得 Codex 所需凭据,Cursor 只取得 Cursor 凭据,
不跨 CLI 透传。`DBUS_*``DISPLAY``WAYLAND_DISPLAY``XDG_RUNTIME_DIR`
`LD_*``PYTHON*``NODE_OPTIONS``CODEX_HOME``GIT_*` 和调用者 PATH 均不会
透传给 worker。
- worktree 除了命中项目 allowlist、属于同一 Git common-dir,还必须精确出现在
`git worktree list --porcelain -z` 中;`.git` symlink 冒充的目录会失败。
- Orca 的 `--command` 只含固定 bootstrap 与随机 launch ID。终端创建后,父进程先
绑定 runtime/handle/incarnation/worktree,再通过该终端 stdin 发送一次性 nonce
bootstrap 校验 nonce 后才允许启动 Agent CLI,并把 child PID 与 nonce/launch
fingerprint 绑定的 proof 原子写入 ready 状态;父进程看到并核对 proof 后才把
worker 视为可派发。
- `orca terminal create` 一经发出,在取得确定 handle 前遇到超时、transport/解码
异常、进程中断、非零退出、畸形 JSON 或缺字段,一律标记为 `indeterminate`
禁止自动重试。
- 取得 handle 后若任一绑定或握手检查失败,launcher 会先用
`orca terminal close --tab --json` 请求持久关闭,并严格核对 runtime、handle、
tab 和 close mode。只有关闭回执完整匹配才记为普通 `failed`;关闭无法确认时保持
`indeterminate/reconcile-required`,调用方必须先人工核对,不能直接重试。
- bootstrap 在真正启动 Agent 前,会在同一 launch-record 锁内再次确认状态仍为
`awaiting-authorization` 且没有 cleanup;父进程一旦先进入 cleanup/reconcile
bootstrap 必须拒绝 `Popen`,不能让取消后的 workspace-write worker 继续启动。
- 异常路径的 launch record 写入失败不能阻断上述关闭动作,也不能遮蔽原始
`indeterminate`。即使关闭已确认,只要最终状态无法可靠持久化,也保守返回
`indeterminate/reconcile-required`
这些约束防止任务板、任务文本和普通环境变量变成第二个启动命令入口。它们不抵御已经
完全控制 Coordinator 用户账户、Orca 运行时、受信 Agent/Orca 可执行文件或
Coordinator 用户级 Agent 配置/插件/MCP 的攻击者;该场景需要独立 OS 身份、受控
Agent 配置或平台签发能力。`permissionMode` 约束的是 Agent CLI 的 shell/文件系统
执行模式,不是模型供应商或外部工具能力的 attestation。
---
## Receipt、审计与复用边界
成功启动返回的 JSON 含一个可校验 receipt。Coordinator 把 receipt 追加到
`tasks.yaml` 顶层 `workerReceipts`,并把它的 `id` 写入当前任务
`dispatch.<role>.receiptId`、把 `profileId` 写入 `dispatch.<role>.profileId`,同时把
本次 `<task-id>-A<round>` 写入 `dispatch.<role>.attemptId`
这三个字段不是可任意组合的历史索引。任务板校验器要求被引用 receipt 的
`createdFor.taskId` 等于当前 ACK `tasks[].id``createdFor.role` 等于 dispatch
角色,`profileId` 完全相同,且 `createdFor.attemptId` 等于
`dispatch.<role>.attemptId``receiptId: null``attemptId` 必须同为 `null`
`receiptId` 非空时 `attemptId` 必须存在并命中本轮。`dispatch.<role>.taskId` 是 Orca
运行时 task ID,不能拿来替代 ACK task ID 或稳定 attempt ID。
receipt 至少把以下事实绑定在一起:
- `profileHash` 与请求的角色、模型、effort、权限和精确 argv
- `slot`,确保 fingerprint 也绑定审阅过的 worker 槽位和派生标题;
- 任务、attempt、绝对 worktree 及 path/device/inode/git common-dir 等稳定身份;
- Orca `runtimeId`、terminal `handle``incarnationId`
- 创建时间、launcher 指纹和 `receiptHash`
`receiptHash` 是无密钥的规范 JSON checksum,只能发现意外漂移或未同步修改,**不是
launcher 身份证明,也不是复用授权**。项目内有写权限的一方可以修改 receipt 后重算
hash;而当前 Orca metadata 又不能证明终端最初执行的命令、模型和权限。因此 v0.10
明确禁止根据持久化 receipt 自动复用既有终端:每次需要自动派发 worker,都重新走
`plan` → 带 expected fingerprint 的 `launch`,只使用该次 launcher 标准输出中的
fresh handle 完成本次派发。
`launchFingerprint` 是确定性的完整计划漂移校验,不是一次性授权或幂等键。同一份
计划重复执行 `launch` 会创建新的 fresh terminal;成功后不得用同一 fingerprint
再次启动。若结果不确定,必须先 reconcile,不能把重跑当作恢复方式。未来需要自动
防重放时,应由 Orca/ACP 的原子 idempotency/claim 能力或项目外可信 launch intent
提供,而不是把 checksum 冒充成一次性令牌。
持久化 receipt 仍用于审计、dispatch 关联和检测配置漂移;标题、preview、分支名、
worker 自报或单独的 Orca live metadata 都不能把旧终端提升为可信 worker。未来只有
在 Orca/ACP 提供启动参数 attestation,或存在项目外可信签发与校验通道后,才开放
自动复用。CLI / 模型变更仍需更新 allowlist 并重新生成 receipt。
`ackVersion` 必须使用合法 SemVer。`0.10.0` 及以后版本的任务板必须同时存在
`project.orchestration` 与顶层 `workerReceipts`;其中任一字段出现,另一个也必须
出现,避免 schema-only 校验把半迁移任务板误判为可自动派发。
---
## 成本原则
强模型产出高密度、可复用的产物:需求、架构决策、验收信号、任务拆分、失败复盘。
中低模型消费这些产物,产出可核对的执行证据:测试结果、快照、API 响应、构建日志、改动文件清单。
中低模型消费这些产物,产出可核对的执行证据:测试结果、快照、API 响应、构建日志、
改动文件清单。
这样把昂贵推理挡在重复执行之外。
@@ -119,4 +240,6 @@ Codex worker 应明确指定模型和 reasoning effort,不把“未指定模
## 一句话
Coordinator 是脑,Test 是眼,Developer 是手。脑用最强的模型且不做机械测试,眼和手用便宜模型,只有常规闭环卡住时才升级。
Coordinator 是脑,Test 是眼,Developer 是手。脑用模型且不做机械测试,眼和手
默认用安全、较低成本的结构化 profile;只有常规闭环卡住时才升级,并且所有 worker
都必须经 launcher 产生可核对的 receipt。
+57 -53
View File
@@ -23,79 +23,83 @@ orca orchestration task-list --json
orca orchestration inbox --limit 20 --json
```
确认:Orca runtime 可达Coordinator、Developer、Test 三个终端存在;Developer 在正确 worktree;当前没有冲突的活跃编排任务。
确认:Orca runtime 可达并取得本次 `runtimeId`Coordinator 终端存在;当前没有冲突
的活跃编排任务。Developer/Test worker 必须通过本次 launcher 新建,不要求预先存在。
---
## 解析并复用 worker
## 既有 worker 的处理
**默认复用,创建是兜底。** 每次向 Developer 或 Test 派发前都先解析 worker,不能直接照后文示例执行 `terminal create`
1. 运行 `orca terminal list --json`,筛选 `connected=true``writable=true`、目标 `worktreePath` 一致、标题角色和配置一致的终端。稳定标题使用 `ACK-<ROLE>-<CLI>-<TIER>-<N>`,例如 `ACK-DEV-CODEX-TERRA-1``ACK-TEST-CODEX-LUNA-1``ACK-DEV-CURSOR-AUTO-1`;强档示例为 `ACK-DEV-CODEX-SOL-1`
2. 运行 `orca orchestration task-list --status dispatched --json`。若候选 handle 出现在活跃任务的 `assignee_handle`,视为 busy,不复用;不要仅凭终端预览文本或最近输出时间猜忙闲
3. 对空闲候选运行 `orca terminal show --terminal <handle> --json`,确认仍存活且 worktree、角色和 Agent CLI / 模型档位符合本次要求。符合就直接把该 handle 作为 `dispatch --to` 的目标。
4. 只有不存在兼容的空闲候选时才创建。若同角色同配置已有 busy worker,使用下一个未占用序号,例如 `ACK-DEV-CODEX-TERRA-2`;不要创建另一个同名终端。
5. dispatch 成功后,把实际 handle 写入对应 `tasks.yaml` 条目的 `dispatch.worker`。下一轮先尝试该 handle;若它已消失、断开、不可写、worktree/角色/档位不兼容或正忙,再回到第 1 步选择其它 worker。
`tasks.yaml.dispatch.worker` 记录“这个任务实际用了谁”,便于续跑和审计;`orca terminal list` 记录“谁现在还活着”,是运行时存活状态的事实源。不要另建一份永久 worker 池,因为终端关闭后其中的 handle 会过期。
复用决策:
v0.10 **不自动复用任何既有终端**`receiptHash` 是无密钥 checksumOrca 当前的
terminal metadata 也不包含原始 Agent argv、模型或权限 attestation;两者即使完全
匹配,仍不足以证明旧终端由可信 launcher 以目标 profile 创建。标题、preview、分支
名或 worker 自报更不能作为证明
| 情况 | 动作 |
|------|------|
| 任务已记录 worker,且该 handle 存活、兼容、空闲 | 优先复用原 handle |
| 有其它同 worktree、同角色、兼容的空闲 worker | 复用该 handle |
| 同角色 worker 存在但都 busy | 创建下一编号的临时并发 worker |
| worker 已关闭、断开或不存在 | 创建新 worker |
| 模型升级或 worktree 不同 | 不复用不兼容 worker,创建对应档位/路径的新 worker |
| 历史 receipt 与 live metadata 看似完全匹配 | 只作审计,不自动复用;重新 launch |
| 没有 receipt,或只有同名终端 | 复用;重新 launch |
| 需要并发 worker | 使用下一 `slot` 重新 launch |
| Developer 升级模型档位 | 选择强档安全 profile,重新 plan/launch |
只有刚刚成功返回的 `launch` 标准输出中的 fresh handle,才可用于紧随其后的本次
dispatch。未来若 Orca/ACP 增加启动参数 attestation,或 ACK 接入项目外可信签发
通道,再单独设计自动复用;不能用项目文档声明跳过这条限制。
---
## 给 worker 终端固定模型
## 通过 launcher 创建 worker
**编排层不设模型。** `task-create` / `dispatch` 都没有 `--model``dispatch --to <handle>` 只是把任务投给一个已存在的终端,用的是那个终端里 agent 会话启动时的模型。要固定模型,就在 **创建 worker 终端** 时用 agent CLI 的模型参数:
**模型不由 Orca dispatch 设置** `task-create` / `dispatch` 只投递任务;Agent CLI
模型、reasoning effort 和权限由 `tasks.yaml.project.orchestration` 的结构化 profile
决定,并且只能由 ACK launcher 构造。
默认沿用 Coordinator 当前所在的 Agent CLICursor Coordinator 创建 Cursor workerCodex Coordinator 创建 Codex worker。不要通过询问模型来猜运行环境;以当前 CLI / 终端环境为准。除非项目 overlay 或用户明确指定,否则不跨 Agent CLI 创建 worker。
先只读生成计划:
```bash
# CursorTest / Developer worker 用 auto 模型,并按项目要求启用 YOLO
orca terminal create --worktree path:<dev_worktree> \
--command "cursor-agent --yolo --model auto" --title "ACK-DEV-CURSOR-AUTO-1" --json
# 需要更强模型时改成具体模型
orca terminal create --worktree path:<dev_worktree> \
--command "cursor-agent --yolo --model claude-opus-4-8-thinking-high" --title "ACK-DEV-CURSOR-STRONG-1" --json
# CodexDeveloper worker
orca terminal create --worktree path:<dev_worktree> \
--command "codex --dangerously-bypass-approvals-and-sandbox -m gpt-5.6-terra -c model_reasoning_effort=medium" \
--title "ACK-DEV-CODEX-TERRA-1" --json
# CodexTest worker
orca terminal create --worktree path:<test_worktree> \
--command "codex --dangerously-bypass-approvals-and-sandbox -m gpt-5.6-luna -c model_reasoning_effort=low" \
--title "ACK-TEST-CODEX-LUNA-1" --json
# Codex:复杂 Developer 任务升级
orca terminal create --worktree path:<dev_worktree> \
--command "codex --dangerously-bypass-approvals-and-sandbox -m gpt-5.6-sol -c model_reasoning_effort=high" \
--title "ACK-DEV-CODEX-SOL-1" --json
python3 <ack-skill-dir>/scripts/launch_worker.py plan \
--project-root <project-root> --task-id <task-id> \
--attempt-id <task-id>-A<round> --role <developer-or-test> \
--profile-id <profile-id> --worktree <absolute-worktree> --slot <1..99>
```
这些创建命令只在复用流程找不到空闲兼容 worker 时执行。拿到返回的 handle 后再 `task-create` + `dispatch --to <handle>`,并把 handle 写入 `tasks.yaml``dispatch.worker`。模型档位与选型策略见 `model-routing.md`
需要隔离/并行时,先建新 worktree 再在其中起 worker(是否新建见 `closed-loop.md` §「子任务放哪」):
确认计划中的任务、attempt、角色、profile hash、绝对 worktree 和安全权限模式后,再
执行唯一启动入口:
```bash
orca worktree create --name <feature> --base-branch <base> --json
orca terminal create --worktree path:<new_worktree> --command "cursor-agent --yolo --model auto" --title "ACK-DEV-CURSOR-AUTO-1" --json
python3 <ack-skill-dir>/scripts/launch_worker.py launch \
--project-root <project-root> --task-id <task-id> \
--attempt-id <task-id>-A<round> --role <developer-or-test> \
--profile-id <profile-id> --worktree <absolute-worktree> --slot <1..99> \
--expected-launch-fingerprint <plan 中的 sha256:...>
```
- `cursor-agent --list-models` 可列出合法模型;`auto` 表示由 Cursor 自动选型。
- Codex 用 `-m / --model` 指定模型,用 `-c model_reasoning_effort=<effort>` 固定推理档位;不要把省略 `-m` 当作 Cursor `auto` 的等价物。
- 本项目要求 Cursor worker 带 `--yolo`Codex worker 带 `--dangerously-bypass-approvals-and-sandbox`;创建前先运行项目覆盖层指定的校验器。
- 其它 CLI(opencode 等)用各自的模型参数或配置,`--command` 相应替换
- 若 worker 是已在跑的会话(用 `--inject` 投递),模型已由该会话启动时决定,无法在 dispatch 时改;要换模型需新建终端。
`launch` 必须携带刚才审阅的 `plan.launchFingerprint`;若 profile、任务/attempt、
worktree、CLI identity/version 或精确 argv 漂移,创建前就失败。它内部调用当前
Orca 后端并完成终端绑定;Coordinator 不自行执行或拼装 Orca 的底层终端创建文本,
也不直接调用 launcher 的私有 bootstrap
若 launcher 报告 `indeterminate``reconcile required`,说明终端可能已经创建且
持久关闭尚未得到可信确认;此时先按返回的 launch ID/record 和 Orca live state
人工核对,禁止直接重复执行 `launch`
标准输出是单一 JSON。成功后:
1. 校验返回 receipt 的 `receiptHash`
2. 把 receipt 原样追加到 `tasks.yaml.workerReceipts`
3.`receipt.id` 写入当前任务 `dispatch.<role>.receiptId`,把 `profileId` 写入
`dispatch.<role>.profileId`,把本轮 `<task-id>-A<round>` 写入
`dispatch.<role>.attemptId`;校验器会要求 receipt 的 task/role/profile/attempt
与当前 ACK 任务完全一致;
4. 再用 receipt binding 中的 handle 创建/派发 Orca 子任务。
profile 只允许 `read-only``workspace-write`。v0.10 不提供可信的 full-access 授权
通道;bypass、YOLO/force、关闭 sandbox 及其它 full-access 请求必须由 schema 和
launcher fail closed,不能改用手写命令兜底。模型档位与升级规则见
`model-routing.md`
需要隔离/并行时,可以先用 Orca 创建 worktree。Coordinator 必须先把返回的绝对
路径加入 `project.orchestration.allowedWorktrees` 并重新校验 `tasks.yaml`,然后才在
该路径上走 `plan` -> 带 expected fingerprint 的 `launch`,并把 receipt 留作审计。
既有会话可以由用户直接操作,但不能进入 ACK v0.10 的自动派发信任路径。
---
+1636
View File
File diff suppressed because it is too large Load Diff
+44
View File
@@ -31,6 +31,7 @@ from yaml_subset import (
load_yaml_subset,
make_unique_pyyaml_loader,
)
from worker_profiles import validate_routing_document
STATUS_ENUM = {
"open",
@@ -46,7 +47,16 @@ MAX_ROUNDS = 3
KNOWLEDGE_KINDS = {"guardrail", "pitfall", "verification"}
KNOWLEDGE_CHECK_RESULTS = {"passed", "failed", "not_applicable"}
KNOWLEDGE_REF_RE = re.compile(r"^K-[A-Z0-9][A-Z0-9-]*@[1-9][0-9]*$")
TASK_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$")
ATTEMPT_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*-A[1-9][0-9]*$")
SEMVER_RE = re.compile(
r"^(0|[1-9][0-9]*)\."
r"(0|[1-9][0-9]*)\."
r"(0|[1-9][0-9]*)"
r"(?:-(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)"
r"(?:\.(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*)?"
r"(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$"
)
KNOWLEDGE_SCOPE_FIELDS = {
"components",
"paths",
@@ -76,6 +86,14 @@ KNOWLEDGE_CHECK_FIELDS = {
"checkedBy",
"checkedAt",
}
DISPATCH_FIELDS = {
"taskId",
"dispatchId",
"worker",
"developer",
"test",
"rounds",
}
KNOWLEDGE_CANDIDATE_REQUIRED_FIELDS = {
"kind",
"title",
@@ -461,6 +479,22 @@ def validate_builtin(data: dict) -> list[str]:
"project.knowledgeFile 必须固定为 docs/ack/knowledge.yaml"
)
ack_version = data.get("ackVersion")
version_match = SEMVER_RE.fullmatch(ack_version) if isinstance(ack_version, str) else None
if "ackVersion" in data and version_match is None:
errors.append("ackVersion 必须是合法 SemVer(例如 0.10.0")
routing_required = (
(isinstance(project, dict) and "orchestration" in project)
or "workerReceipts" in data
or (
version_match is not None
and (int(version_match.group(1)), int(version_match.group(2)))
>= (0, 10)
)
)
if routing_required:
errors.extend(validate_routing_document(data))
if "summary" in data:
summary = data["summary"]
if not isinstance(summary, dict):
@@ -494,6 +528,10 @@ def validate_builtin(data: dict) -> list[str]:
errors.append(f"{where}: id 必须是非空字符串")
else:
where = f"tasks[{i}] {tid}"
if routing_required and TASK_ID_RE.fullmatch(tid) is None:
errors.append(
f"{where}: v0.10 自动路由 id 只允许字母、数字、点、下划线和连字符"
)
if tid in seen_ids:
errors.append(f"{where}: id 重复")
seen_ids.add(tid)
@@ -533,6 +571,12 @@ def validate_builtin(data: dict) -> list[str]:
dispatch = {}
else:
dispatch = task["dispatch"]
reject_unknown_fields(
dispatch,
DISPATCH_FIELDS,
f"{where}.dispatch",
errors,
)
validate_string_fields(
dispatch,
{"taskId", "dispatchId", "worker"},
+13 -154
View File
@@ -1,171 +1,30 @@
#!/usr/bin/env python3
"""校验 Music Pilot 的 Developer/Test worker 启动命令。
"""Fail-closed migration shim for ACK's removed free-form command validator.
用法
python3 validate_worker_command.py --role developer --command '<command>'
python3 validate_worker_command.py --role test --command '<command>'
python3 validate_worker_command.py --role developer --upgraded --command '<command>'
python3 validate_worker_command.py --self-test
退出码0 通过 / 1 规则不通过 / 2 用法或命令解析错误
ACK v0.10 no longer accepts a shell command as worker configuration. Keep this
file only so older documentation or automation fails with a precise migration
message instead of silently retaining the unsafe path.
"""
from __future__ import annotations
import argparse
import shlex
import sys
from pathlib import Path
CODEX_YOLO = "--dangerously-bypass-approvals-and-sandbox"
CURSOR_YOLO = "--yolo"
def option_value(tokens: list[str], *names: str) -> str | None:
for index, token in enumerate(tokens):
for name in names:
if token == name and index + 1 < len(tokens):
return tokens[index + 1]
prefix = f"{name}="
if token.startswith(prefix):
return token[len(prefix) :]
return None
def codex_effort(tokens: list[str]) -> str | None:
configs: list[str] = []
for index, token in enumerate(tokens):
if token in {"-c", "--config"} and index + 1 < len(tokens):
configs.append(tokens[index + 1])
elif token.startswith("--config="):
configs.append(token.split("=", 1)[1])
for config in configs:
if config.startswith("model_reasoning_effort="):
return config.split("=", 1)[1].strip('"\'')
return None
def validate(role: str, command: str, upgraded: bool = False) -> list[str]:
try:
tokens = shlex.split(command)
except ValueError as exc:
return [f"命令无法解析:{exc}"]
if not tokens:
return ["启动命令不能为空"]
executable = Path(tokens[0]).name
errors: list[str] = []
if upgraded and role != "developer":
errors.append("只有 Developer 可以使用 --upgraded")
if executable == "codex":
if CODEX_YOLO not in tokens:
errors.append(f"Codex worker 必须包含 {CODEX_YOLO}")
model = option_value(tokens, "-m", "--model")
effort = codex_effort(tokens)
if upgraded:
expected_model = "gpt-5.6-sol"
allowed_efforts = {"high", "xhigh"}
elif role == "developer":
expected_model = "gpt-5.6-terra"
allowed_efforts = {"medium"}
else:
expected_model = "gpt-5.6-luna"
allowed_efforts = {"low"}
if model != expected_model:
errors.append(
f"Codex {role} 模型应为 {expected_model},实际为 {model or '未指定'}"
MIGRATION_MESSAGE = (
"ACK v0.10 已停用自由 worker command 校验器;请在 "
"docs/ack/tasks.yaml 的 project.orchestration 中声明 profile,并仅调用 "
"scripts/launch_worker.py profile-hash|plan|launch。"
)
if effort not in allowed_efforts:
expected = "/".join(sorted(allowed_efforts))
errors.append(
f"Codex {role} reasoning effort 应为 {expected},实际为 {effort or '未指定'}"
)
elif executable == "cursor-agent":
if upgraded:
errors.append("Cursor worker 不使用 Codex --upgraded 映射")
if CURSOR_YOLO not in tokens:
errors.append(f"Cursor worker 必须显式包含 {CURSOR_YOLO}")
model = option_value(tokens, "--model")
if model != "auto":
errors.append(f"Cursor {role} 模型应为 auto,实际为 {model or '未指定'}")
else:
errors.append(f"不支持的 worker CLI{executable};只允许 codex 或 cursor-agent")
return errors
def run_self_test() -> int:
cases = [
(
"codex developer",
"developer",
f"codex {CODEX_YOLO} -m gpt-5.6-terra -c model_reasoning_effort=medium",
False,
True,
),
(
"codex test",
"test",
f"codex {CODEX_YOLO} -m gpt-5.6-luna -c model_reasoning_effort=low",
False,
True,
),
(
"codex upgraded developer",
"developer",
f"codex {CODEX_YOLO} -m gpt-5.6-sol -c model_reasoning_effort=high",
True,
True,
),
("cursor worker", "test", "cursor-agent --yolo --model auto", False, True),
("naked codex", "developer", "codex", False, False),
(
"wrong codex role model",
"test",
f"codex {CODEX_YOLO} -m gpt-5.6-terra -c model_reasoning_effort=medium",
False,
False,
),
("cursor without yolo", "developer", "cursor-agent --model auto", False, False),
]
failures: list[str] = []
for name, role, command, upgraded, expected_pass in cases:
passed = not validate(role, command, upgraded)
if passed != expected_pass:
failures.append(name)
if failures:
sys.stderr.write("worker 命令校验器自测失败:" + ", ".join(failures) + "\n")
return 1
print(f"worker 命令校验器自测通过:{len(cases)}")
return 0
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="校验 Developer/Test worker 启动命令")
parser.add_argument("--role", choices=("developer", "test"))
parser.add_argument("--command")
parser.add_argument("--upgraded", action="store_true", help="校验升级后的 Codex Developer")
parser.add_argument("--self-test", action="store_true")
args = parser.parse_args(argv)
if args.self_test:
return run_self_test()
if not args.role or not args.command:
parser.error("非自测模式必须同时提供 --role 和 --command")
errors = validate(args.role, args.command, args.upgraded)
if errors:
sys.stderr.write("worker 启动命令校验失败:\n")
for error in errors:
sys.stderr.write(f" - {error}\n")
return 1
print(f"worker 启动命令校验通过:role={args.role}")
arguments = list(sys.argv[1:] if argv is None else argv)
if arguments == ["--self-test"]:
print("legacy worker command 路径已 fail-closed")
return 0
sys.stderr.write(MIGRATION_MESSAGE + "\n")
return 2
if __name__ == "__main__":
+992
View File
@@ -0,0 +1,992 @@
#!/usr/bin/env python3
"""Strict, zero-dependency primitives for ACK worker routing.
This module deliberately validates structured data rather than accepting a
shell command. It does not invoke Orca, resolve executables, inspect Git, or
write ``tasks.yaml``. A launcher can use the validated profile and the exact
argv renderer below, then persist its externally observed facts as a receipt.
"""
from __future__ import annotations
import hashlib
import json
import os
import re
from datetime import datetime
from pathlib import Path
from typing import Any
PROFILE_VERSION = 1
RECEIPT_VERSION = 1
LAUNCH_PROTOCOL_VERSION = 1
MAX_ROUNDS = 3
ROLES = frozenset({"developer", "test"})
CLIS = frozenset({"codex", "cursor-agent"})
TIERS = frozenset({"standard", "strong"})
REASONING_EFFORTS = frozenset({"low", "medium", "high", "xhigh"})
PERMISSION_MODES = frozenset({"read-only", "workspace-write"})
ORCHESTRATION_MODES = frozenset({"orca", "manual"})
DEFAULT_KEYS = frozenset({"developer", "test", "developerUpgraded"})
ORCHESTRATION_FIELDS = frozenset(
{
"profileVersion",
"mode",
"allowedWorktrees",
"modelAllowlist",
"profiles",
"defaults",
}
)
PROFILE_FIELDS = frozenset(
{
"role",
"cli",
"tier",
"model",
"reasoningEffort",
"permissionMode",
}
)
RECEIPT_FIELDS = frozenset(
{
"receiptVersion",
"id",
"launchId",
"profileId",
"profileHash",
"launchFingerprint",
"slot",
"createdFor",
"worktree",
"requested",
"binding",
"createdAt",
"receiptHash",
}
)
CREATED_FOR_FIELDS = frozenset({"taskId", "attemptId", "role"})
WORKTREE_FIELDS = frozenset(
{
"path",
"device",
"inode",
"gitCommonDir",
"gitCommonDevice",
"gitCommonInode",
}
)
REQUESTED_FIELDS = frozenset(
{
"cli",
"tier",
"model",
"reasoningEffort",
"permissionMode",
"executable",
"executableDevice",
"executableInode",
"cliVersion",
"argv",
"argvHash",
"environmentPolicy",
}
)
BINDING_FIELDS = frozenset(
{
"orchestrator",
"runtimeId",
"handle",
"incarnationId",
"observedWorktreePath",
"connected",
"writable",
"boundAt",
}
)
ROLE_DISPATCH_FIELDS = frozenset(
{"profileId", "receiptId", "attemptId", "taskId", "dispatchId"}
)
PROFILE_ID_RE = re.compile(r"^[a-z][a-z0-9-]{1,63}$")
MODEL_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:/+@-]{0,127}$")
RECEIPT_ID_RE = re.compile(r"^WR-[0-9a-f]{64}$")
LAUNCH_ID_RE = re.compile(r"^[0-9a-f]{64}$")
TASK_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$")
ATTEMPT_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*-A[1-3]$")
SHA256_RE = re.compile(r"^sha256:[0-9a-f]{64}$")
def _canonical_json(value: Any) -> str:
"""Return the one JSON representation used by all hashes in this module."""
try:
return json.dumps(
value,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
allow_nan=False,
)
except (RecursionError, TypeError, ValueError) as exc:
raise ValueError(f"value is not canonical JSON data: {exc}") from exc
def canonical_sha256(value: Any) -> str:
"""Hash canonical UTF-8 JSON and return ``sha256:<lowercase hex>``."""
digest = hashlib.sha256(_canonical_json(value).encode("utf-8")).hexdigest()
return f"sha256:{digest}"
def _unknown_fields(value: dict[str, Any], allowed: frozenset[str], where: str) -> list[str]:
unknown = set(value) - allowed
return [
f"{where}: unknown field {field!r}"
for field in sorted(unknown, key=repr)
]
def _missing_fields(value: dict[str, Any], required: frozenset[str], where: str) -> list[str]:
return [f"{where}: missing field {field!r}" for field in sorted(required - set(value))]
def _is_nonempty_text(value: Any, *, max_length: int = 1024) -> bool:
return (
isinstance(value, str)
and bool(value.strip())
and len(value) <= max_length
and all(ord(character) >= 32 and ord(character) != 127 for character in value)
)
def _is_absolute_safe_path(value: Any) -> bool:
if not _is_nonempty_text(value, max_length=4096):
return False
path = Path(value)
return (
path.is_absolute()
and value != os.path.sep
and not value.startswith("//")
and ".." not in path.parts
and os.path.normpath(value) == value
)
def _is_nonnegative_int(value: Any) -> bool:
return isinstance(value, int) and not isinstance(value, bool) and value >= 0
def _is_positive_int(value: Any) -> bool:
return isinstance(value, int) and not isinstance(value, bool) and value > 0
def _is_timestamp(value: Any) -> bool:
if not isinstance(value, str):
return False
try:
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
except ValueError:
return False
return parsed.tzinfo is not None
def validate_profile(profile: Any, *, where: str = "profile") -> list[str]:
"""Validate one strict worker profile without consulting its allowlist."""
if not isinstance(profile, dict):
return [f"{where}: must be an object"]
errors = _unknown_fields(profile, PROFILE_FIELDS, where)
errors.extend(_missing_fields(profile, PROFILE_FIELDS, where))
role = profile.get("role")
cli = profile.get("cli")
tier = profile.get("tier")
model = profile.get("model")
effort = profile.get("reasoningEffort")
permission = profile.get("permissionMode")
if not isinstance(role, str) or role not in ROLES:
errors.append(f"{where}.role: must be developer/test")
if not isinstance(cli, str) or cli not in CLIS:
errors.append(f"{where}.cli: must be codex/cursor-agent")
if not isinstance(tier, str) or tier not in TIERS:
errors.append(f"{where}.tier: must be standard/strong")
if not isinstance(model, str) or MODEL_ID_RE.fullmatch(model) is None:
errors.append(f"{where}.model: must be a safe model ID")
if not isinstance(permission, str) or permission not in PERMISSION_MODES:
errors.append(
f"{where}.permissionMode: must be read-only/workspace-write"
)
if cli == "codex":
if not isinstance(effort, str) or effort not in REASONING_EFFORTS:
errors.append(
f"{where}.reasoningEffort: Codex requires low/medium/high/xhigh"
)
elif cli == "cursor-agent" and effort is not None:
errors.append(f"{where}.reasoningEffort: Cursor requires null")
if role == "test" and tier != "standard":
errors.append(f"{where}.tier: Test may only use standard")
if tier == "strong" and role != "developer":
errors.append(f"{where}.tier: strong may only be used by Developer")
return errors
def _validate_model_allowlist(value: Any, where: str) -> list[str]:
if not isinstance(value, dict):
return [f"{where}: must be an object"]
errors: list[str] = []
for cli in sorted(set(value) - CLIS, key=repr):
errors.append(f"{where}: unknown CLI {cli!r}")
for cli, roles in value.items():
cli_where = f"{where}.{cli}"
if cli not in CLIS:
continue
if not isinstance(roles, dict):
errors.append(f"{cli_where}: must be an object")
continue
if not roles:
errors.append(f"{cli_where}: must not be empty")
for role in sorted(set(roles) - ROLES, key=repr):
errors.append(f"{cli_where}: unknown role {role!r}")
for role, tiers in roles.items():
role_where = f"{cli_where}.{role}"
if role not in ROLES:
continue
if not isinstance(tiers, dict):
errors.append(f"{role_where}: must be an object")
continue
if not tiers:
errors.append(f"{role_where}: must not be empty")
for tier in sorted(set(tiers) - TIERS, key=repr):
errors.append(f"{role_where}: unknown tier {tier!r}")
if role == "test" and "strong" in tiers:
errors.append(f"{role_where}: Test cannot define a strong allowlist")
for tier, models in tiers.items():
tier_where = f"{role_where}.{tier}"
if tier not in TIERS:
continue
if not isinstance(models, list) or not models:
errors.append(f"{tier_where}: must be a non-empty model list")
continue
seen: set[str] = set()
for index, model in enumerate(models):
item_where = f"{tier_where}[{index}]"
if not isinstance(model, str) or MODEL_ID_RE.fullmatch(model) is None:
errors.append(f"{item_where}: must be a safe model ID")
elif model in seen:
errors.append(f"{item_where}: duplicate model {model!r}")
else:
seen.add(model)
return errors
def _allowed_models(
allowlist: Any,
cli: Any,
role: Any,
tier: Any,
) -> list[str] | None:
if not isinstance(allowlist, dict):
return None
roles = allowlist.get(cli)
if not isinstance(roles, dict):
return None
tiers = roles.get(role)
if not isinstance(tiers, dict):
return None
models = tiers.get(tier)
return models if isinstance(models, list) else None
def validate_orchestration(
orchestration: Any,
*,
where: str = "project.orchestration",
) -> list[str]:
"""Validate the complete strict routing configuration."""
if not isinstance(orchestration, dict):
return [f"{where}: must be an object"]
errors = _unknown_fields(orchestration, ORCHESTRATION_FIELDS, where)
errors.extend(_missing_fields(orchestration, ORCHESTRATION_FIELDS, where))
profile_version = orchestration.get("profileVersion")
mode = orchestration.get("mode")
allowed_worktrees = orchestration.get("allowedWorktrees")
allowlist = orchestration.get("modelAllowlist")
profiles = orchestration.get("profiles")
defaults = orchestration.get("defaults")
if profile_version != PROFILE_VERSION or isinstance(profile_version, bool):
errors.append(f"{where}.profileVersion: must be {PROFILE_VERSION}")
if not isinstance(mode, str) or mode not in ORCHESTRATION_MODES:
errors.append(f"{where}.mode: must be orca/manual")
if not isinstance(allowed_worktrees, list):
errors.append(f"{where}.allowedWorktrees: must be a list")
else:
if mode == "orca" and not allowed_worktrees:
errors.append(f"{where}.allowedWorktrees: Orca mode requires at least one path")
seen_worktrees: set[str] = set()
for index, worktree in enumerate(allowed_worktrees):
item_where = f"{where}.allowedWorktrees[{index}]"
if not _is_absolute_safe_path(worktree):
errors.append(f"{item_where}: must be a safe absolute path other than root")
elif worktree in seen_worktrees:
errors.append(f"{item_where}: duplicate worktree {worktree!r}")
else:
seen_worktrees.add(worktree)
errors.extend(_validate_model_allowlist(allowlist, f"{where}.modelAllowlist"))
valid_profiles: dict[str, dict[str, Any]] = {}
if not isinstance(profiles, dict):
errors.append(f"{where}.profiles: must be an object")
else:
if mode == "orca" and not profiles:
errors.append(f"{where}.profiles: Orca mode requires profiles")
for profile_id, profile in profiles.items():
profile_where = f"{where}.profiles.{profile_id}"
if not isinstance(profile_id, str) or PROFILE_ID_RE.fullmatch(profile_id) is None:
errors.append(f"{where}.profiles: invalid profile ID {profile_id!r}")
continue
profile_errors = validate_profile(profile, where=profile_where)
errors.extend(profile_errors)
if profile_errors or not isinstance(profile, dict):
continue
valid_profiles[profile_id] = profile
models = _allowed_models(
allowlist,
profile.get("cli"),
profile.get("role"),
profile.get("tier"),
)
if profile.get("model") not in (models or []):
errors.append(
f"{profile_where}.model: {profile.get('model')!r} is not allowed "
"for its cli/role/tier"
)
if not isinstance(defaults, dict):
errors.append(f"{where}.defaults: must be an object")
else:
for default_key in sorted(set(defaults) - DEFAULT_KEYS, key=repr):
errors.append(f"{where}.defaults: unknown key {default_key!r}")
if mode == "orca":
for role in sorted(ROLES - set(defaults)):
errors.append(f"{where}.defaults: missing role {role!r}")
for default_key, profile_id in defaults.items():
default_where = f"{where}.defaults.{default_key}"
if default_key not in DEFAULT_KEYS:
continue
if not isinstance(profile_id, str) or PROFILE_ID_RE.fullmatch(profile_id) is None:
errors.append(f"{default_where}: must be a profile ID")
continue
profile = valid_profiles.get(profile_id)
if profile is None:
errors.append(f"{default_where}: unknown or invalid profile {profile_id!r}")
continue
expected_role = "developer" if default_key == "developerUpgraded" else default_key
expected_tier = "strong" if default_key == "developerUpgraded" else "standard"
if profile.get("role") != expected_role:
errors.append(f"{default_where}: profile role must be {expected_role}")
if profile.get("tier") != expected_tier:
errors.append(
f"{default_where}: default profile must use {expected_tier} tier"
)
if profile.get("permissionMode") not in PERMISSION_MODES:
errors.append(f"{default_where}: default profile has unsafe permissions")
return errors
def profile_hash(
profile: dict[str, Any],
*,
profile_version: int = PROFILE_VERSION,
) -> str:
"""Return the canonical hash of a valid strict profile."""
errors = validate_profile(profile)
if errors:
raise ValueError("invalid profile: " + "; ".join(errors))
if (
not isinstance(profile_version, int)
or isinstance(profile_version, bool)
or profile_version < 1
):
raise ValueError("profile_version must be a positive integer")
return canonical_sha256(
{
"profileVersion": profile_version,
"profile": profile,
}
)
def render_worker_argv(
profile: dict[str, Any],
executable: str,
worktree: str,
) -> list[str]:
"""Render the only argv shapes allowed by routing profile version 1."""
errors = validate_profile(profile)
if errors:
raise ValueError("invalid profile: " + "; ".join(errors))
if not _is_absolute_safe_path(executable):
raise ValueError("executable must be a safe absolute path other than root")
if Path(executable).name != profile["cli"]:
raise ValueError("executable basename must match profile.cli")
if not _is_absolute_safe_path(worktree):
raise ValueError("worktree must be a safe absolute path other than root")
model = profile["model"]
permission = profile["permissionMode"]
if profile["cli"] == "codex":
return [
executable,
"--strict-config",
"--model",
model,
"--config",
f"model_reasoning_effort={profile['reasoningEffort']}",
"--sandbox",
permission,
"--ask-for-approval",
"never",
"--cd",
worktree,
]
argv = [executable, "--model", model]
if permission == "read-only":
argv.extend(["--mode", "plan"])
else:
argv.append("--auto-review")
argv.extend(["--sandbox", "enabled", "--workspace", worktree])
return argv
def receipt_hash(receipt: dict[str, Any]) -> str:
"""Hash every receipt field except the self-referential ``receiptHash``."""
if not isinstance(receipt, dict):
raise ValueError("receipt must be an object")
payload = {key: value for key, value in receipt.items() if key != "receiptHash"}
return canonical_sha256(payload)
def _validate_created_for(value: Any, where: str) -> list[str]:
if not isinstance(value, dict):
return [f"{where}: must be an object"]
errors = _unknown_fields(value, CREATED_FOR_FIELDS, where)
errors.extend(_missing_fields(value, CREATED_FOR_FIELDS, where))
task_id = value.get("taskId")
attempt_id = value.get("attemptId")
if not isinstance(task_id, str) or TASK_ID_RE.fullmatch(task_id) is None:
errors.append(f"{where}.taskId: must be a safe task ID")
if not isinstance(attempt_id, str) or ATTEMPT_ID_RE.fullmatch(attempt_id) is None:
errors.append(f"{where}.attemptId: must use <task-id>-A1..A3")
elif isinstance(task_id, str) and not attempt_id.startswith(f"{task_id}-A"):
errors.append(f"{where}.attemptId: must belong to taskId")
role = value.get("role")
if not isinstance(role, str) or role not in ROLES:
errors.append(f"{where}.role: must be developer/test")
return errors
def _validate_worktree_snapshot(value: Any, where: str) -> list[str]:
if not isinstance(value, dict):
return [f"{where}: must be an object"]
errors = _unknown_fields(value, WORKTREE_FIELDS, where)
errors.extend(_missing_fields(value, WORKTREE_FIELDS, where))
for field in ("path", "gitCommonDir"):
if not _is_absolute_safe_path(value.get(field)):
errors.append(f"{where}.{field}: must be a safe absolute path other than root")
for field in ("device", "gitCommonDevice"):
if not _is_nonnegative_int(value.get(field)):
errors.append(f"{where}.{field}: must be a non-negative integer")
for field in ("inode", "gitCommonInode"):
if not _is_positive_int(value.get(field)):
errors.append(f"{where}.{field}: must be a positive integer")
return errors
def _validate_requested(value: Any, where: str) -> list[str]:
if not isinstance(value, dict):
return [f"{where}: must be an object"]
errors = _unknown_fields(value, REQUESTED_FIELDS, where)
errors.extend(_missing_fields(value, REQUESTED_FIELDS, where))
cli = value.get("cli")
tier = value.get("tier")
permission = value.get("permissionMode")
if not isinstance(cli, str) or cli not in CLIS:
errors.append(f"{where}.cli: must be codex/cursor-agent")
if not isinstance(tier, str) or tier not in TIERS:
errors.append(f"{where}.tier: must be standard/strong")
model = value.get("model")
if not isinstance(model, str) or MODEL_ID_RE.fullmatch(model) is None:
errors.append(f"{where}.model: must be a safe model ID")
effort = value.get("reasoningEffort")
if cli == "codex" and (
not isinstance(effort, str) or effort not in REASONING_EFFORTS
):
errors.append(f"{where}.reasoningEffort: invalid Codex effort")
if cli == "cursor-agent" and effort is not None:
errors.append(f"{where}.reasoningEffort: Cursor requires null")
if not isinstance(permission, str) or permission not in PERMISSION_MODES:
errors.append(f"{where}.permissionMode: must be read-only/workspace-write")
executable = value.get("executable")
if not _is_absolute_safe_path(executable):
errors.append(f"{where}.executable: must be a safe absolute path")
elif isinstance(cli, str) and cli in CLIS and Path(executable).name != cli:
errors.append(f"{where}.executable: basename must match cli")
if not _is_nonnegative_int(value.get("executableDevice")):
errors.append(f"{where}.executableDevice: must be a non-negative integer")
if not _is_positive_int(value.get("executableInode")):
errors.append(f"{where}.executableInode: must be a positive integer")
if not _is_nonempty_text(value.get("cliVersion"), max_length=256):
errors.append(f"{where}.cliVersion: must be non-empty single-line text")
argv = value.get("argv")
if (
not isinstance(argv, list)
or len(argv) < 2
or any(not isinstance(arg, str) for arg in argv)
):
errors.append(f"{where}.argv: must be a string array with at least 2 items")
argv_hash = value.get("argvHash")
if not isinstance(argv_hash, str) or SHA256_RE.fullmatch(argv_hash) is None:
errors.append(f"{where}.argvHash: must be a canonical sha256 hex digest")
elif isinstance(argv, list) and all(isinstance(arg, str) for arg in argv):
if argv_hash != canonical_sha256(argv):
errors.append(f"{where}.argvHash: does not match argv")
if value.get("environmentPolicy") != "per-cli-allowlist-v1":
errors.append(
f"{where}.environmentPolicy: must be 'per-cli-allowlist-v1'"
)
return errors
def _validate_binding(value: Any, where: str) -> list[str]:
if not isinstance(value, dict):
return [f"{where}: must be an object"]
errors = _unknown_fields(value, BINDING_FIELDS, where)
errors.extend(_missing_fields(value, BINDING_FIELDS, where))
if value.get("orchestrator") != "orca":
errors.append(f"{where}.orchestrator: must be orca")
for field in ("runtimeId", "handle", "incarnationId"):
if not _is_nonempty_text(value.get(field), max_length=512):
errors.append(f"{where}.{field}: must be non-empty single-line text")
if not _is_absolute_safe_path(value.get("observedWorktreePath")):
errors.append(f"{where}.observedWorktreePath: must be a safe absolute path")
for field in ("connected", "writable"):
if value.get(field) is not True:
errors.append(f"{where}.{field}: must be true")
if not _is_timestamp(value.get("boundAt")):
errors.append(f"{where}.boundAt: must be a timezone-aware ISO 8601 timestamp")
return errors
def validate_worker_receipt(
receipt: Any,
*,
orchestration: dict[str, Any] | None = None,
task_ids: set[str] | None = None,
where: str = "workerReceipt",
) -> list[str]:
"""Validate one audit receipt's structure/checksums and routing references.
This is not origin authentication: ``receiptHash`` is unkeyed, so callers
must never use this result alone to authorize reuse of an old terminal.
"""
if not isinstance(receipt, dict):
return [f"{where}: must be an object"]
errors = _unknown_fields(receipt, RECEIPT_FIELDS, where)
errors.extend(_missing_fields(receipt, RECEIPT_FIELDS, where))
version = receipt.get("receiptVersion")
if version != RECEIPT_VERSION or isinstance(version, bool):
errors.append(f"{where}.receiptVersion: must be {RECEIPT_VERSION}")
for field in ("id", "launchId", "profileId"):
value = receipt.get(field)
matcher = {
"id": RECEIPT_ID_RE,
"launchId": LAUNCH_ID_RE,
"profileId": PROFILE_ID_RE,
}[field]
if not isinstance(value, str) or matcher.fullmatch(value) is None:
errors.append(f"{where}.{field}: invalid identifier")
for field in ("profileHash", "launchFingerprint", "receiptHash"):
value = receipt.get(field)
if not isinstance(value, str) or SHA256_RE.fullmatch(value) is None:
errors.append(f"{where}.{field}: must be a canonical sha256 hex digest")
slot = receipt.get("slot")
if not isinstance(slot, int) or isinstance(slot, bool) or not 1 <= slot <= 99:
errors.append(f"{where}.slot: must be an integer from 1 to 99")
receipt_id = receipt.get("id")
launch_id = receipt.get("launchId")
if (
isinstance(receipt_id, str)
and RECEIPT_ID_RE.fullmatch(receipt_id)
and isinstance(launch_id, str)
and LAUNCH_ID_RE.fullmatch(launch_id)
and receipt_id != f"WR-{launch_id}"
):
errors.append(f"{where}.id: must equal 'WR-' + launchId")
created_for = receipt.get("createdFor")
worktree = receipt.get("worktree")
requested = receipt.get("requested")
binding = receipt.get("binding")
errors.extend(_validate_created_for(created_for, f"{where}.createdFor"))
errors.extend(_validate_worktree_snapshot(worktree, f"{where}.worktree"))
errors.extend(_validate_requested(requested, f"{where}.requested"))
errors.extend(_validate_binding(binding, f"{where}.binding"))
if not _is_timestamp(receipt.get("createdAt")):
errors.append(f"{where}.createdAt: must be a timezone-aware ISO 8601 timestamp")
if isinstance(created_for, dict) and task_ids is not None:
task_id = created_for.get("taskId")
if isinstance(task_id, str) and task_id not in task_ids:
errors.append(f"{where}.createdFor.taskId: unknown task {task_id!r}")
profile: dict[str, Any] | None = None
if orchestration is not None and not isinstance(orchestration, dict):
errors.append(f"{where}: orchestration must be an object")
elif isinstance(orchestration, dict):
profiles = orchestration.get("profiles")
profile_id = receipt.get("profileId")
candidate = profiles.get(profile_id) if (
isinstance(profiles, dict) and isinstance(profile_id, str)
) else None
if isinstance(candidate, dict):
profile = candidate
else:
errors.append(f"{where}.profileId: unknown profile {profile_id!r}")
allowed = orchestration.get("allowedWorktrees")
if isinstance(worktree, dict) and isinstance(allowed, list):
if worktree.get("path") not in allowed:
errors.append(f"{where}.worktree.path: is not in allowedWorktrees")
if profile is not None:
try:
expected_profile_hash = profile_hash(
profile,
profile_version=orchestration.get(
"profileVersion",
PROFILE_VERSION,
),
)
except ValueError:
errors.append(f"{where}.profileId: referenced profile is invalid")
else:
if receipt.get("profileHash") != expected_profile_hash:
errors.append(f"{where}.profileHash: does not match profile")
if isinstance(created_for, dict) and created_for.get("role") != profile.get("role"):
errors.append(f"{where}.createdFor.role: does not match profile")
if isinstance(requested, dict):
for field in (
"cli",
"tier",
"model",
"reasoningEffort",
"permissionMode",
):
if requested.get(field) != profile.get(field):
errors.append(f"{where}.requested.{field}: does not match profile")
executable = requested.get("executable")
worktree_path = worktree.get("path") if isinstance(worktree, dict) else None
if isinstance(executable, str) and isinstance(worktree_path, str):
try:
expected_argv = render_worker_argv(profile, executable, worktree_path)
except ValueError:
errors.append(f"{where}.requested.argv: cannot render referenced profile")
else:
if requested.get("argv") != expected_argv:
errors.append(f"{where}.requested.argv: does not match exact renderer")
if isinstance(binding, dict) and isinstance(worktree, dict):
if binding.get("observedWorktreePath") != worktree.get("path"):
errors.append(f"{where}.binding.observedWorktreePath: does not match worktree.path")
if (
isinstance(created_for, dict)
and isinstance(worktree, dict)
and isinstance(requested, dict)
):
try:
expected_fingerprint = canonical_sha256(
{
"protocolVersion": LAUNCH_PROTOCOL_VERSION,
"backend": "orca",
"profileId": receipt.get("profileId"),
"profileHash": receipt.get("profileHash"),
"createdFor": created_for,
"worktree": worktree,
"requested": requested,
"slot": slot,
}
)
except ValueError:
errors.append(f"{where}.launchFingerprint: cannot hash launch facts")
else:
if receipt.get("launchFingerprint") != expected_fingerprint:
errors.append(
f"{where}.launchFingerprint: does not match launch facts"
)
try:
expected_receipt_hash = receipt_hash(receipt)
except ValueError:
errors.append(f"{where}: must contain canonical JSON data")
else:
if receipt.get("receiptHash") != expected_receipt_hash:
errors.append(f"{where}.receiptHash: does not match receipt")
return errors
def validate_worker_receipts(
receipts: Any,
orchestration: dict[str, Any] | None = None,
*,
task_ids: set[str] | None = None,
where: str = "workerReceipts",
) -> list[str]:
"""Validate the top-level receipt list and reject duplicate identities."""
if not isinstance(receipts, list):
return [f"{where}: must be a list"]
errors: list[str] = []
seen_ids: set[str] = set()
seen_launch_ids: set[str] = set()
for index, receipt in enumerate(receipts):
receipt_where = f"{where}[{index}]"
errors.extend(
validate_worker_receipt(
receipt,
orchestration=orchestration,
task_ids=task_ids,
where=receipt_where,
)
)
if not isinstance(receipt, dict):
continue
receipt_id = receipt.get("id")
if isinstance(receipt_id, str):
if receipt_id in seen_ids:
errors.append(f"{receipt_where}.id: duplicate receipt ID {receipt_id!r}")
seen_ids.add(receipt_id)
launch_id = receipt.get("launchId")
if isinstance(launch_id, str):
if launch_id in seen_launch_ids:
errors.append(f"{receipt_where}.launchId: duplicate launch ID {launch_id!r}")
seen_launch_ids.add(launch_id)
return errors
def _validate_dispatch_links(
tasks: Any,
receipts: Any,
orchestration: Any,
) -> list[str]:
"""Cross-link role dispatch records to persisted audit receipts."""
if not isinstance(tasks, list):
return []
receipt_by_id: dict[str, dict[str, Any]] = {}
if isinstance(receipts, list):
for receipt in receipts:
if not isinstance(receipt, dict):
continue
receipt_id = receipt.get("id")
if isinstance(receipt_id, str) and receipt_id not in receipt_by_id:
receipt_by_id[receipt_id] = receipt
profiles = orchestration.get("profiles") if isinstance(orchestration, dict) else None
errors: list[str] = []
for task_index, task in enumerate(tasks):
if not isinstance(task, dict):
continue
ack_task_id = task.get("id")
dispatch = task.get("dispatch")
if not isinstance(dispatch, dict):
continue
for role in sorted(ROLES):
if role not in dispatch:
continue
role_dispatch = dispatch.get(role)
where = f"tasks[{task_index}].dispatch.{role}"
if not isinstance(role_dispatch, dict):
errors.append(f"{where}: must be an object")
continue
errors.extend(_unknown_fields(role_dispatch, ROLE_DISPATCH_FIELDS, where))
errors.extend(_missing_fields(role_dispatch, ROLE_DISPATCH_FIELDS, where))
profile_id = role_dispatch.get("profileId")
if profile_id is not None:
if not isinstance(profile_id, str) or PROFILE_ID_RE.fullmatch(profile_id) is None:
errors.append(f"{where}.profileId: must be null or a profile ID")
else:
profile = profiles.get(profile_id) if isinstance(profiles, dict) else None
if not isinstance(profile, dict):
errors.append(f"{where}.profileId: unknown profile {profile_id!r}")
elif profile.get("role") != role:
errors.append(f"{where}.profileId: profile role must be {role}")
runtime_values: list[Any] = []
for field in ("taskId", "dispatchId"):
runtime_value = role_dispatch.get(field)
runtime_values.append(runtime_value)
if runtime_value is not None and not _is_nonempty_text(
runtime_value,
max_length=512,
):
errors.append(
f"{where}.{field}: must be null or non-empty single-line text"
)
if (runtime_values[0] is None) != (runtime_values[1] is None):
errors.append(
f"{where}: taskId and dispatchId must both be null or both be set"
)
receipt_id = role_dispatch.get("receiptId")
attempt_id = role_dispatch.get("attemptId")
current_task_attempts = {
f"{ack_task_id}-A{round_number}" for round_number in range(1, 4)
}
if attempt_id is not None:
if (
not isinstance(attempt_id, str)
or ATTEMPT_ID_RE.fullmatch(attempt_id) is None
):
errors.append(
f"{where}.attemptId: must be null or use <task-id>-A1..A3"
)
elif (
isinstance(ack_task_id, str)
and attempt_id not in current_task_attempts
):
errors.append(
f"{where}.attemptId: must belong to current ACK task {ack_task_id!r}"
)
if receipt_id is None:
if attempt_id is not None:
errors.append(
f"{where}.attemptId: must be null when receiptId is null"
)
if any(value is not None for value in runtime_values):
errors.append(
f"{where}.receiptId: is required before runtime dispatch IDs"
)
continue
if attempt_id is None:
errors.append(
f"{where}.attemptId: is required when receiptId is set"
)
if not isinstance(receipt_id, str) or RECEIPT_ID_RE.fullmatch(receipt_id) is None:
errors.append(f"{where}.receiptId: must be null or a receipt ID")
continue
receipt = receipt_by_id.get(receipt_id)
if receipt is None:
errors.append(f"{where}.receiptId: unknown receipt {receipt_id!r}")
continue
if receipt.get("profileId") != profile_id:
errors.append(f"{where}.profileId: does not match referenced receipt")
created_for = receipt.get("createdFor")
receipt_task_id = (
created_for.get("taskId") if isinstance(created_for, dict) else None
)
receipt_role = created_for.get("role") if isinstance(created_for, dict) else None
receipt_attempt_id = (
created_for.get("attemptId") if isinstance(created_for, dict) else None
)
if receipt_task_id != ack_task_id:
errors.append(
f"{where}.receiptId: referenced receipt task must be current "
f"ACK task {ack_task_id!r}"
)
if receipt_role != role:
errors.append(f"{where}.receiptId: referenced receipt role must be {role}")
if receipt_attempt_id != attempt_id:
errors.append(
f"{where}.attemptId: does not match referenced receipt"
)
return errors
def validate_routing_document(data: Any) -> list[str]:
"""Validate routing and receipts embedded in an ACK tasks document."""
if not isinstance(data, dict):
return ["<root>: must be an object"]
project = data.get("project")
if not isinstance(project, dict):
return ["project: must be an object"]
if "orchestration" not in project:
return ["project.orchestration: is required"]
orchestration = project.get("orchestration")
errors = validate_orchestration(orchestration)
task_ids: set[str] = set()
tasks = data.get("tasks")
if isinstance(tasks, list):
for task in tasks:
if isinstance(task, dict) and isinstance(task.get("id"), str):
task_ids.add(task["id"])
receipts = data.get("workerReceipts")
if "workerReceipts" not in data:
errors.append("workerReceipts: is required")
else:
errors.extend(
validate_worker_receipts(
receipts,
orchestration if isinstance(orchestration, dict) else None,
task_ids=task_ids,
)
)
errors.extend(_validate_dispatch_links(tasks, receipts, orchestration))
if isinstance(orchestration, dict) and orchestration.get("mode") == "manual":
receipts = data.get("workerReceipts")
if isinstance(receipts, list) and receipts:
errors.append("workerReceipts: manual orchestration requires an empty list")
return errors
# Short aliases for callers that treat this module as a builtin validator.
validate_routing = validate_orchestration
validate_receipts = validate_worker_receipts
validate_builtin = validate_routing_document
__all__ = [
"PROFILE_VERSION",
"RECEIPT_VERSION",
"LAUNCH_PROTOCOL_VERSION",
"MAX_ROUNDS",
"canonical_sha256",
"profile_hash",
"receipt_hash",
"render_worker_argv",
"validate_profile",
"validate_orchestration",
"validate_worker_receipt",
"validate_worker_receipts",
"validate_routing_document",
"validate_routing",
"validate_receipts",
"validate_builtin",
]
+24 -6
View File
@@ -31,13 +31,25 @@
- 派发 prompt 模板:`references/prompt-templates.md`
- Orca 编排命令(可选):`references/orca-adapter.md`
## 模型档位(项目可覆盖,默认见 references/model-routing.md
## Worker 路由
| 角色 | 默认档位 | 本项目实际 |
|------|----------|------------|
| Coordinator (PM) | 强模型 | `<model_or_default>` |
| Test | 中低模型 | `<model_or_default>` |
| Developer | 中低模型 | `<model_or_default>` |
机器可校验的模型、reasoning effort、权限模式、默认 profile、允许 worktree 和启动
receipt 全部以 `docs/ack/tasks.yaml``project.orchestration` 与顶层
`workerReceipts` 为准。本文件不保存可执行 worker 命令。
默认 profile
| 角色 | profile ID | 档位 |
|------|------------|------|
| Developer | `codex-dev-standard` | standard |
| Test | `codex-test-standard` | standard |
| Developer 升级 | `codex-dev-strong` | strong |
项目如改用 Cursor,应修改结构化 profile、allowlist 和 defaults,再运行任务板
校验;不能在这里粘贴 `orca --command`、Agent CLI 参数或自由 shell。
每次自动派发必须先审阅 launcher plan,再把其中的 `launchFingerprint` 作为
`--expected-launch-fingerprint` 启动 fresh worker。v0.10 的 `receiptHash` 只作
审计 checksum,不授权自动复用旧终端。
## 路径权限(项目覆盖层,必须填实际路径)
@@ -86,6 +98,12 @@ Skill 的 `scripts/run_verification.py` 执行,不直接拼接 path/args。检
- 三角色独立:Coordinator 只编排、Test 只验证、Developer 只实现(验证者 ≠ 实现者)。
- 模型分层:Coordinator 用强模型且不亲自跑测试,Test/Developer 用中低模型,必要时升级(见 references/model-routing.md)。
- 自动 worker 只能由 ACK 的 `scripts/launch_worker.py` 按结构化 profile 启动;
禁止直接拼 `orca terminal create --command`,禁止 `command``extraArgs``env`
等第二配置面。
- v0.10 自动 launcher 只支持 `read-only``workspace-write`。full-access、
bypass、YOLO/force 与关闭 sandbox 均 fail closed;需要可信平台审批通道后再开放。
- v0.10 不自动复用持久化 receipt 指向的旧终端;每次自动派发都重新 plan/launch。
- `worker_done` 与复测报告都不等于完成。必须 Test 独立复测 + Coordinator 终检后才能 `verified`
- 只有 Coordinator 写 `tasks.yaml``knowledge.yaml`Developer 与 Test 都只读,
通过消息回报。
+534 -2
View File
@@ -12,6 +12,7 @@
"source": { "type": "string" },
"ackVersion": {
"type": "string",
"pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(?:-(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(?:\\.(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$",
"description": "接入时所基于的 ack 版本,便于日后 diff 升级"
},
"kitVersion": {
@@ -35,6 +36,9 @@
"type": "string",
"const": "docs/ack/knowledge.yaml",
"description": "项目知识护栏库的唯一权威路径"
},
"orchestration": {
"$ref": "#/definitions/orchestration"
}
}
},
@@ -49,12 +53,538 @@
}
},
"statusReference": { "type": "object" },
"workerReceipts": {
"type": "array",
"items": { "$ref": "#/definitions/workerReceipt" }
},
"tasks": {
"type": "array",
"items": { "$ref": "#/definitions/task" }
}
},
"allOf": [
{
"if": {
"properties": {
"ackVersion": {
"type": "string",
"pattern": "^(?:0\\.[1-9][0-9]+\\.[0-9]+|[1-9][0-9]*\\.[0-9]+\\.[0-9]+)(?:-|\\+|$)"
}
},
"required": ["ackVersion"]
},
"then": {
"required": ["workerReceipts"],
"properties": {
"workerReceipts": {},
"tasks": { "$ref": "#/definitions/launchableTasks" },
"project": {
"type": "object",
"required": ["orchestration"],
"properties": {
"orchestration": {}
}
}
}
}
},
{
"if": {
"properties": {
"project": {
"type": "object",
"required": ["orchestration"],
"properties": {
"orchestration": {}
}
}
},
"required": ["project"]
},
"then": {
"required": ["workerReceipts"],
"properties": {
"workerReceipts": {},
"tasks": { "$ref": "#/definitions/launchableTasks" }
}
}
},
{
"if": {
"required": ["workerReceipts"],
"properties": {
"workerReceipts": {}
}
},
"then": {
"properties": {
"tasks": { "$ref": "#/definitions/launchableTasks" },
"project": {
"type": "object",
"required": ["orchestration"],
"properties": {
"orchestration": {}
}
}
}
}
}
],
"definitions": {
"sha256": {
"type": "string",
"pattern": "^sha256:[0-9a-f]{64}$"
},
"profileId": {
"type": "string",
"pattern": "^[a-z][a-z0-9-]{1,63}$"
},
"launchableTasks": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"pattern": "^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$"
}
}
}
},
"modelList": {
"type": "array",
"minItems": 1,
"uniqueItems": true,
"items": {
"type": "string",
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:/+@-]{0,127}$"
}
},
"modelTierAllowlist": {
"type": "object",
"minProperties": 1,
"additionalProperties": false,
"properties": {
"standard": { "$ref": "#/definitions/modelList" },
"strong": { "$ref": "#/definitions/modelList" }
}
},
"modelRoleAllowlist": {
"type": "object",
"minProperties": 1,
"additionalProperties": false,
"properties": {
"developer": { "$ref": "#/definitions/modelTierAllowlist" },
"test": { "$ref": "#/definitions/modelTierAllowlist" }
}
},
"modelAllowlist": {
"type": "object",
"additionalProperties": false,
"properties": {
"codex": { "$ref": "#/definitions/modelRoleAllowlist" },
"cursor-agent": { "$ref": "#/definitions/modelRoleAllowlist" }
}
},
"workerProfile": {
"type": "object",
"required": [
"role",
"cli",
"tier",
"model",
"reasoningEffort",
"permissionMode"
],
"additionalProperties": false,
"properties": {
"role": {
"type": "string",
"enum": ["developer", "test"]
},
"cli": {
"type": "string",
"enum": ["codex", "cursor-agent"]
},
"tier": {
"type": "string",
"enum": ["standard", "strong"]
},
"model": {
"type": "string",
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:/+@-]{0,127}$"
},
"reasoningEffort": {
"type": ["string", "null"],
"enum": ["low", "medium", "high", "xhigh", null]
},
"permissionMode": {
"type": "string",
"enum": ["read-only", "workspace-write"]
}
}
},
"workerDefaults": {
"type": "object",
"additionalProperties": false,
"properties": {
"developer": { "$ref": "#/definitions/profileId" },
"test": { "$ref": "#/definitions/profileId" },
"developerUpgraded": { "$ref": "#/definitions/profileId" }
}
},
"orchestration": {
"type": "object",
"required": [
"profileVersion",
"mode",
"allowedWorktrees",
"modelAllowlist",
"profiles",
"defaults"
],
"additionalProperties": false,
"properties": {
"profileVersion": {
"type": "integer",
"const": 1
},
"mode": {
"type": "string",
"enum": ["orca", "manual"]
},
"allowedWorktrees": {
"type": "array",
"uniqueItems": true,
"items": {
"type": "string",
"minLength": 1,
"pattern": "^/"
}
},
"modelAllowlist": {
"$ref": "#/definitions/modelAllowlist"
},
"profiles": {
"type": "object",
"propertyNames": {
"$ref": "#/definitions/profileId"
},
"additionalProperties": {
"$ref": "#/definitions/workerProfile"
}
},
"defaults": {
"$ref": "#/definitions/workerDefaults"
}
},
"allOf": [
{
"if": {
"properties": {
"mode": { "const": "orca" }
},
"required": ["mode"]
},
"then": {
"properties": {
"allowedWorktrees": { "type": "array", "minItems": 1 },
"modelAllowlist": { "type": "object", "minProperties": 1 },
"profiles": { "type": "object", "minProperties": 1 },
"defaults": {
"type": "object",
"required": ["developer", "test"],
"properties": {
"developer": {},
"test": {}
}
}
}
}
}
]
},
"worktreeIdentity": {
"type": "object",
"required": [
"path",
"device",
"inode",
"gitCommonDir",
"gitCommonDevice",
"gitCommonInode"
],
"additionalProperties": false,
"properties": {
"path": {
"type": "string",
"pattern": "^/"
},
"device": {
"type": "integer",
"minimum": 0
},
"inode": {
"type": "integer",
"minimum": 1
},
"gitCommonDir": {
"type": "string",
"pattern": "^/"
},
"gitCommonDevice": {
"type": "integer",
"minimum": 0
},
"gitCommonInode": {
"type": "integer",
"minimum": 1
}
}
},
"receiptRequested": {
"type": "object",
"required": [
"cli",
"tier",
"model",
"reasoningEffort",
"permissionMode",
"executable",
"executableDevice",
"executableInode",
"cliVersion",
"argv",
"argvHash",
"environmentPolicy"
],
"additionalProperties": false,
"properties": {
"cli": {
"type": "string",
"enum": ["codex", "cursor-agent"]
},
"tier": {
"type": "string",
"enum": ["standard", "strong"]
},
"model": {
"type": "string",
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:/+@-]{0,127}$"
},
"reasoningEffort": {
"type": ["string", "null"],
"enum": ["low", "medium", "high", "xhigh", null]
},
"permissionMode": {
"type": "string",
"enum": ["read-only", "workspace-write"]
},
"executable": {
"type": "string",
"pattern": "^/"
},
"executableDevice": {
"type": "integer",
"minimum": 0
},
"executableInode": {
"type": "integer",
"minimum": 1
},
"cliVersion": {
"type": "string",
"minLength": 1
},
"argv": {
"type": "array",
"minItems": 2,
"items": {
"type": "string"
}
},
"argvHash": {
"$ref": "#/definitions/sha256"
},
"environmentPolicy": {
"type": "string",
"const": "per-cli-allowlist-v1"
}
}
},
"receiptBinding": {
"type": "object",
"required": [
"orchestrator",
"runtimeId",
"handle",
"incarnationId",
"observedWorktreePath",
"connected",
"writable",
"boundAt"
],
"additionalProperties": false,
"properties": {
"orchestrator": {
"type": "string",
"const": "orca"
},
"runtimeId": {
"type": "string",
"minLength": 1
},
"handle": {
"type": "string",
"minLength": 1
},
"incarnationId": {
"type": "string",
"minLength": 1
},
"observedWorktreePath": {
"type": "string"
},
"connected": {
"type": "boolean"
},
"writable": {
"type": "boolean"
},
"boundAt": {
"type": "string",
"minLength": 1
}
}
},
"workerReceipt": {
"type": "object",
"required": [
"receiptVersion",
"id",
"launchId",
"profileId",
"profileHash",
"launchFingerprint",
"slot",
"createdFor",
"worktree",
"requested",
"binding",
"createdAt",
"receiptHash"
],
"additionalProperties": false,
"properties": {
"receiptVersion": {
"type": "integer",
"const": 1
},
"id": {
"type": "string",
"pattern": "^WR-[0-9a-f]{64}$"
},
"launchId": {
"type": "string",
"pattern": "^[0-9a-f]{64}$"
},
"profileId": {
"$ref": "#/definitions/profileId"
},
"profileHash": {
"$ref": "#/definitions/sha256"
},
"launchFingerprint": {
"$ref": "#/definitions/sha256"
},
"slot": {
"type": "integer",
"minimum": 1,
"maximum": 99
},
"createdFor": {
"type": "object",
"required": ["taskId", "attemptId", "role"],
"additionalProperties": false,
"properties": {
"taskId": {
"type": "string",
"minLength": 1
},
"attemptId": {
"type": "string",
"pattern": "^[A-Za-z0-9][A-Za-z0-9._-]*-A[1-3]$"
},
"role": {
"type": "string",
"enum": ["developer", "test"]
}
}
},
"worktree": {
"$ref": "#/definitions/worktreeIdentity"
},
"requested": {
"$ref": "#/definitions/receiptRequested"
},
"binding": {
"$ref": "#/definitions/receiptBinding"
},
"createdAt": {
"type": "string",
"minLength": 1
},
"receiptHash": {
"$ref": "#/definitions/sha256"
}
}
},
"roleDispatch": {
"type": "object",
"required": ["profileId", "receiptId", "attemptId", "taskId", "dispatchId"],
"additionalProperties": false,
"properties": {
"profileId": {
"type": ["string", "null"],
"pattern": "^[a-z][a-z0-9-]{1,63}$"
},
"receiptId": {
"type": ["string", "null"],
"pattern": "^WR-[0-9a-f]{64}$"
},
"attemptId": {
"type": ["string", "null"],
"pattern": "^[A-Za-z0-9][A-Za-z0-9._-]*-A[1-3]$"
},
"taskId": {
"type": ["string", "null"]
},
"dispatchId": {
"type": ["string", "null"]
}
},
"allOf": [
{
"if": {
"properties": {
"receiptId": { "type": "null" }
},
"required": ["receiptId"]
},
"then": {
"properties": {
"attemptId": { "type": "null" }
}
},
"else": {
"properties": {
"profileId": { "type": "string" },
"attemptId": { "type": "string" }
}
}
}
]
},
"status": {
"type": "string",
"enum": [
@@ -76,7 +606,7 @@
"round": { "type": "integer", "minimum": 1, "maximum": 3 },
"attemptId": {
"type": "string",
"pattern": "^[A-Za-z0-9][A-Za-z0-9._-]*-A[1-9][0-9]*$",
"pattern": "^[A-Za-z0-9][A-Za-z0-9._-]*-A[1-3]$",
"description": "稳定逻辑轮次 ID,应为 <task-id>-A<round>;旧轮次可缺省,但作为知识来源前必须补齐"
},
"result": { "type": "string", "enum": ["passed", "failed"] },
@@ -230,11 +760,13 @@
"verification": { "type": "object" },
"dispatch": {
"type": "object",
"additionalProperties": true,
"additionalProperties": false,
"properties": {
"taskId": { "type": ["string", "null"] },
"dispatchId": { "type": ["string", "null"] },
"worker": { "type": ["string", "null"] },
"developer": { "$ref": "#/definitions/roleDispatch" },
"test": { "$ref": "#/definitions/roleDispatch" },
"rounds": {
"type": "array",
"items": { "$ref": "#/definitions/round" }
+69 -1
View File
@@ -10,6 +10,65 @@ project:
devWorktree: "<dev_worktree>"
overlayFile: "docs/ack/project.md"
knowledgeFile: "docs/ack/knowledge.yaml"
orchestration:
profileVersion: 1
mode: "orca"
allowedWorktrees:
- "<dev_worktree>"
modelAllowlist:
codex:
developer:
standard: ["gpt-5.6-terra"]
strong: ["gpt-5.6-sol"]
test:
standard: ["gpt-5.6-luna"]
cursor-agent:
developer:
standard: ["auto"]
test:
standard: ["auto"]
profiles:
codex-dev-standard:
role: "developer"
cli: "codex"
tier: "standard"
model: "gpt-5.6-terra"
reasoningEffort: "medium"
permissionMode: "workspace-write"
codex-test-standard:
role: "test"
cli: "codex"
tier: "standard"
model: "gpt-5.6-luna"
reasoningEffort: "low"
permissionMode: "workspace-write"
codex-dev-strong:
role: "developer"
cli: "codex"
tier: "strong"
model: "gpt-5.6-sol"
reasoningEffort: "high"
permissionMode: "workspace-write"
cursor-dev-standard:
role: "developer"
cli: "cursor-agent"
tier: "standard"
model: "auto"
reasoningEffort: null
permissionMode: "workspace-write"
cursor-test-standard:
role: "test"
cli: "cursor-agent"
tier: "standard"
model: "auto"
reasoningEffort: null
permissionMode: "workspace-write"
defaults:
developer: "codex-dev-standard"
test: "codex-test-standard"
developerUpgraded: "codex-dev-strong"
workerReceipts: []
summary:
verified: []
@@ -62,9 +121,18 @@ tasks:
- "<visible text or interaction expected>"
dispatch:
developer:
profileId: "codex-dev-standard"
receiptId: null
attemptId: null
taskId: null
dispatchId: null
test:
profileId: "codex-test-standard"
receiptId: null
attemptId: null
taskId: null
dispatchId: null
worker: null
rounds: []
resolution:
File diff suppressed because it is too large Load Diff
+19 -1
View File
@@ -34,6 +34,16 @@ class AckSkillContentTests(unittest.TestCase):
self.assertIn('display_name: "ACK"', metadata)
self.assertIn("allow_implicit_invocation: false", metadata)
def test_model_routing_is_fingerprint_bound_and_does_not_trust_receipt_reuse(self) -> None:
content = (
REPO_ROOT / "skills" / "ack" / "references" / "model-routing.md"
).read_text(encoding="utf-8")
self.assertIn("--expected-launch-fingerprint", content)
self.assertIn("allowlist-v1", content)
self.assertIn("禁止根据持久化 receipt 自动复用", content)
self.assertIn("launcher 身份证明", content)
def test_ack_knowledge_resources_and_version_are_present(self) -> None:
ack_dir = REPO_ROOT / "skills" / "ack"
@@ -44,9 +54,17 @@ class AckSkillContentTests(unittest.TestCase):
"scripts/validate_knowledge.py",
"scripts/select_knowledge.py",
"scripts/run_verification.py",
"scripts/worker_profiles.py",
"scripts/launch_worker.py",
):
self.assertTrue((ack_dir / relative_path).is_file(), relative_path)
self.assertEqual((ack_dir / "VERSION").read_text(encoding="utf-8").strip(), "0.9.0")
self.assertEqual((ack_dir / "VERSION").read_text(encoding="utf-8").strip(), "0.10.0")
self.assertIn(
'ackVersion: "<接入时的 ack skill 版本>"',
(ack_dir / "templates" / "tasks.template.yaml").read_text(
encoding="utf-8"
),
)
if __name__ == "__main__":
+185
View File
@@ -1,6 +1,9 @@
from __future__ import annotations
import copy
import importlib.util
import json
import re
import subprocess
import sys
import tempfile
@@ -12,6 +15,7 @@ from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
VALIDATOR = REPO_ROOT / "skills" / "ack" / "scripts" / "validate_tasks.py"
EXAMPLE = REPO_ROOT / "skills" / "ack" / "examples" / "tasks.example.yaml"
SCHEMA = REPO_ROOT / "skills" / "ack" / "templates" / "tasks.schema.json"
def valid_knowledge_board() -> dict:
@@ -59,6 +63,21 @@ def valid_knowledge_board() -> dict:
}
def valid_manual_routing_board() -> dict:
board = valid_knowledge_board()
board["ackVersion"] = "0.10.0"
board["project"]["orchestration"] = {
"profileVersion": 1,
"mode": "manual",
"allowedWorktrees": [],
"modelAllowlist": {},
"profiles": {},
"defaults": {},
}
board["workerReceipts"] = []
return board
class AckTaskValidationTests(unittest.TestCase):
def run_validator(
self,
@@ -129,6 +148,160 @@ class AckTaskValidationTests(unittest.TestCase):
self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn("任务板校验通过", result.stdout)
def test_v010_requires_structured_routing_but_v009_remains_readable(self) -> None:
current = valid_knowledge_board()
current["ackVersion"] = "0.10.0"
self.assert_board_rejected_in_all_modes(
current,
"project.orchestration: is required",
)
legacy = valid_knowledge_board()
legacy["ackVersion"] = "0.9.0"
self.assert_board_accepted_in_all_modes(legacy)
def test_builtin_rejects_invalid_ack_semver_in_all_modes(self) -> None:
for invalid in ("0.10", "00.10.0", "0.10.01", "v0.10.0", "0.10.0-"):
with self.subTest(ack_version=invalid):
board = valid_knowledge_board()
board["ackVersion"] = invalid
self.assert_board_rejected_in_all_modes(
board,
"ackVersion 必须是合法 SemVer",
)
def test_schema_declares_semver_routing_and_attempt_contracts(self) -> None:
schema = json.loads(SCHEMA.read_text(encoding="utf-8"))
ack_pattern = re.compile(schema["properties"]["ackVersion"]["pattern"])
for valid in ("0.9.0", "0.10.0", "0.11.2-alpha.1+build.7", "1.0.0"):
with self.subTest(valid_semver=valid):
self.assertIsNotNone(ack_pattern.fullmatch(valid))
for invalid in ("0.10", "00.10.0", "0.10.01", "v0.10.0", "0.10.0-"):
with self.subTest(invalid_semver=invalid):
self.assertIsNone(ack_pattern.fullmatch(invalid))
current_gate, orchestration_gate, receipts_gate = schema["allOf"]
current_pattern = re.compile(
current_gate["if"]["properties"]["ackVersion"]["pattern"]
)
for current in ("0.10.0", "0.99.1", "1.0.0", "12.34.56+build"):
self.assertIsNotNone(current_pattern.search(current))
self.assertIsNone(current_pattern.search("0.9.99"))
self.assertIn("workerReceipts", current_gate["then"]["required"])
self.assertIn(
"workerReceipts",
current_gate["then"]["properties"],
)
self.assertIn(
"orchestration",
current_gate["then"]["properties"]["project"]["required"],
)
self.assertIn(
"orchestration",
current_gate["then"]["properties"]["project"]["properties"],
)
self.assertIn("workerReceipts", orchestration_gate["then"]["required"])
self.assertIn(
"workerReceipts",
orchestration_gate["then"]["properties"],
)
self.assertIn(
"orchestration",
receipts_gate["then"]["properties"]["project"]["required"],
)
self.assertIn(
"orchestration",
receipts_gate["then"]["properties"]["project"]["properties"],
)
for gate in (current_gate, orchestration_gate, receipts_gate):
self.assertEqual(
gate["then"]["properties"]["tasks"]["$ref"],
"#/definitions/launchableTasks",
)
role_dispatch = schema["definitions"]["roleDispatch"]
self.assertIn("attemptId", role_dispatch["required"])
receipt_rule = role_dispatch["allOf"][0]
self.assertEqual(
receipt_rule["then"]["properties"]["attemptId"]["type"],
"null",
)
self.assertEqual(
receipt_rule["else"]["properties"]["attemptId"]["type"],
"string",
)
@unittest.skipUnless(
importlib.util.find_spec("jsonschema") is not None,
"jsonschema is required for the schema-only contract test",
)
def test_schema_alone_enforces_v010_and_mutual_routing_presence(self) -> None:
import jsonschema # type: ignore
schema = json.loads(SCHEMA.read_text(encoding="utf-8"))
validator = jsonschema.Draft7Validator(schema)
def messages(board: dict) -> list[str]:
return [error.message for error in validator.iter_errors(board)]
base = {
"version": 1,
"ackVersion": "0.9.0",
"project": {"name": "demo"},
"tasks": [],
}
self.assertEqual(messages(base), [])
for current in ("0.10.0", "0.11.2-alpha.1+build.7", "1.0.0"):
with self.subTest(ack_version=current):
board = copy.deepcopy(base)
board["ackVersion"] = current
errors = messages(board)
self.assertIn("'workerReceipts' is a required property", errors)
self.assertIn("'orchestration' is a required property", errors)
manual = {
"profileVersion": 1,
"mode": "manual",
"allowedWorktrees": [],
"modelAllowlist": {},
"profiles": {},
"defaults": {},
}
orchestration_only = copy.deepcopy(base)
orchestration_only["project"]["orchestration"] = manual
self.assertIn(
"'workerReceipts' is a required property",
messages(orchestration_only),
)
receipts_only = copy.deepcopy(base)
receipts_only["workerReceipts"] = []
self.assertIn(
"'orchestration' is a required property",
messages(receipts_only),
)
complete = copy.deepcopy(orchestration_only)
complete["ackVersion"] = "0.10.0+routing.1"
complete["workerReceipts"] = []
self.assertEqual(messages(complete), [])
def test_v010_task_ids_match_launcher_while_legacy_ids_remain_readable(
self,
) -> None:
current = valid_manual_routing_board()
current["tasks"][0]["id"] = "BUG/001"
self.assert_board_rejected_in_all_modes(
current,
"v0.10 自动路由 id 只允许字母、数字、点、下划线和连字符",
)
legacy = valid_knowledge_board()
legacy["ackVersion"] = "0.9.0"
legacy["tasks"][0]["id"] = "BUG/001"
self.assert_board_accepted_in_all_modes(legacy)
def test_knowledge_applied_and_checks_must_reference_selected_knowledge(self) -> None:
result = self.run_validator(
"""
@@ -582,6 +755,18 @@ class AckTaskValidationTests(unittest.TestCase):
"T-RESOLUTION.resolution: 必须是对象",
)
def test_dispatch_rejects_free_command_field_without_jsonschema(self) -> None:
board = valid_knowledge_board()
board["tasks"][0]["dispatch"] = {
"command": "cursor-agent --yolo",
"rounds": [],
}
self.assert_board_rejected_in_all_modes(
board,
"dispatch: 未知字段 'command'",
)
def test_valid_optional_schema_fields_pass_in_all_modes(self) -> None:
board = valid_knowledge_board()
board.update(
+895
View File
@@ -0,0 +1,895 @@
from __future__ import annotations
import copy
import sys
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
SCRIPTS_DIR = REPO_ROOT / "skills" / "ack" / "scripts"
sys.path.insert(0, str(SCRIPTS_DIR))
import worker_profiles # noqa: E402
def valid_orchestration() -> dict:
return {
"profileVersion": 1,
"mode": "orca",
"allowedWorktrees": ["/repo/demo"],
"modelAllowlist": {
"codex": {
"developer": {
"standard": ["gpt-safe-dev"],
"strong": ["gpt-safe-strong"],
},
"test": {"standard": ["gpt-safe-test"]},
},
"cursor-agent": {
"developer": {"standard": ["cursor-auto"]},
"test": {"standard": ["cursor-auto"]},
},
},
"profiles": {
"codex-dev-standard": {
"role": "developer",
"cli": "codex",
"tier": "standard",
"model": "gpt-safe-dev",
"reasoningEffort": "medium",
"permissionMode": "workspace-write",
},
"codex-dev-strong": {
"role": "developer",
"cli": "codex",
"tier": "strong",
"model": "gpt-safe-strong",
"reasoningEffort": "high",
"permissionMode": "workspace-write",
},
"cursor-test-standard": {
"role": "test",
"cli": "cursor-agent",
"tier": "standard",
"model": "cursor-auto",
"reasoningEffort": None,
"permissionMode": "read-only",
},
"cursor-dev-standard": {
"role": "developer",
"cli": "cursor-agent",
"tier": "standard",
"model": "cursor-auto",
"reasoningEffort": None,
"permissionMode": "workspace-write",
},
},
"defaults": {
"developer": "codex-dev-standard",
"test": "cursor-test-standard",
"developerUpgraded": "codex-dev-strong",
},
}
def valid_receipt(orchestration: dict | None = None) -> dict:
routing = orchestration or valid_orchestration()
profile_id = "codex-dev-standard"
profile = routing["profiles"][profile_id]
executable = "/usr/local/bin/codex"
worktree_path = "/repo/demo"
argv = worker_profiles.render_worker_argv(profile, executable, worktree_path)
launch_id = "a" * 64
requested = {
"cli": profile["cli"],
"tier": profile["tier"],
"model": profile["model"],
"reasoningEffort": profile["reasoningEffort"],
"permissionMode": profile["permissionMode"],
"executable": executable,
"executableDevice": 8,
"executableInode": 201,
"cliVersion": "codex 1.0.0",
"argv": argv,
"argvHash": worker_profiles.canonical_sha256(argv),
"environmentPolicy": "per-cli-allowlist-v1",
}
worktree = {
"path": worktree_path,
"device": 8,
"inode": 101,
"gitCommonDir": "/repo/demo/.git",
"gitCommonDevice": 8,
"gitCommonInode": 102,
}
receipt = {
"receiptVersion": 1,
"id": f"WR-{launch_id}",
"launchId": launch_id,
"profileId": profile_id,
"profileHash": worker_profiles.profile_hash(profile),
"launchFingerprint": worker_profiles.canonical_sha256(
{
"protocolVersion": 1,
"backend": "orca",
"profileId": profile_id,
"profileHash": worker_profiles.profile_hash(profile),
"createdFor": {
"taskId": "TASK-001",
"attemptId": "TASK-001-A1",
"role": "developer",
},
"worktree": worktree,
"requested": requested,
"slot": 1,
}
),
"slot": 1,
"createdFor": {
"taskId": "TASK-001",
"attemptId": "TASK-001-A1",
"role": "developer",
},
"worktree": worktree,
"requested": requested,
"binding": {
"orchestrator": "orca",
"runtimeId": "runtime-001",
"handle": "terminal-001",
"incarnationId": "incarnation-001",
"observedWorktreePath": worktree_path,
"connected": True,
"writable": True,
"boundAt": "2026-07-31T12:00:01+08:00",
},
"createdAt": "2026-07-31T12:00:00+08:00",
"receiptHash": "",
}
receipt["receiptHash"] = worker_profiles.receipt_hash(receipt)
return receipt
class CanonicalHashTests(unittest.TestCase):
def test_hash_is_prefixed_order_independent_and_content_sensitive(self) -> None:
first = worker_profiles.canonical_sha256(
{"b": [2, 1], "a": {"enabled": True}}
)
reordered = worker_profiles.canonical_sha256(
{"a": {"enabled": True}, "b": [2, 1]}
)
changed = worker_profiles.canonical_sha256(
{"a": {"enabled": False}, "b": [2, 1]}
)
self.assertEqual(first, reordered)
self.assertNotEqual(first, changed)
self.assertRegex(first, r"^sha256:[0-9a-f]{64}$")
def test_hash_rejects_non_json_and_non_finite_values(self) -> None:
with self.assertRaises(ValueError):
worker_profiles.canonical_sha256({"bad": object()})
with self.assertRaises(ValueError):
worker_profiles.canonical_sha256({"bad": float("nan")})
cyclic: list = []
cyclic.append(cyclic)
with self.assertRaises(ValueError):
worker_profiles.canonical_sha256(cyclic)
def test_profile_hash_rejects_invalid_profile(self) -> None:
profile = valid_orchestration()["profiles"]["codex-dev-standard"]
profile["command"] = "codex; touch forged"
with self.assertRaises(ValueError):
worker_profiles.profile_hash(profile)
def test_profile_hash_binds_profile_version(self) -> None:
profile = valid_orchestration()["profiles"]["codex-dev-standard"]
self.assertNotEqual(
worker_profiles.profile_hash(profile, profile_version=1),
worker_profiles.profile_hash(profile, profile_version=2),
)
class ProfileValidationTests(unittest.TestCase):
def test_valid_orchestration_passes(self) -> None:
self.assertEqual(
worker_profiles.validate_orchestration(valid_orchestration()),
[],
)
def test_manual_mode_accepts_empty_structured_routing(self) -> None:
routing = {
"profileVersion": 1,
"mode": "manual",
"allowedWorktrees": [],
"modelAllowlist": {},
"profiles": {},
"defaults": {},
}
self.assertEqual(worker_profiles.validate_orchestration(routing), [])
def test_partial_empty_allowlist_nodes_are_rejected(self) -> None:
routing = {
"profileVersion": 1,
"mode": "manual",
"allowedWorktrees": [],
"modelAllowlist": {"codex": {}},
"profiles": {},
"defaults": {},
}
errors = worker_profiles.validate_orchestration(routing)
self.assertTrue(any("modelAllowlist.codex: must not be empty" in error for error in errors))
def test_profile_rejects_all_command_shaped_and_unknown_fields(self) -> None:
forbidden = ("command", "args", "env", "executable", "argv", "extraArgs")
for field in forbidden:
with self.subTest(field=field):
routing = valid_orchestration()
routing["profiles"]["codex-dev-standard"][field] = "forged"
errors = worker_profiles.validate_orchestration(routing)
self.assertTrue(any(f"unknown field '{field}'" in error for error in errors))
def test_routing_rejects_unknown_top_level_fields(self) -> None:
routing = valid_orchestration()
routing["command"] = "codex"
errors = worker_profiles.validate_orchestration(routing)
self.assertIn("project.orchestration: unknown field 'command'", errors)
def test_model_id_rejects_shell_and_whitespace_syntax(self) -> None:
for model in (
"safe;touch-forged",
"safe && forged",
"$(touch-forged)",
"`touch-forged`",
"safe\nforged",
"--dangerously-bypass-approvals-and-sandbox",
):
with self.subTest(model=model):
routing = valid_orchestration()
routing["profiles"]["codex-dev-standard"]["model"] = model
errors = worker_profiles.validate_orchestration(routing)
self.assertTrue(any("safe model ID" in error for error in errors))
def test_only_safe_permission_modes_are_accepted(self) -> None:
for permission in (
"danger-full-access",
"full-access",
"yolo",
"bypass",
"never",
):
with self.subTest(permission=permission):
routing = valid_orchestration()
routing["profiles"]["codex-dev-standard"][
"permissionMode"
] = permission
errors = worker_profiles.validate_orchestration(routing)
self.assertTrue(any("read-only/workspace-write" in error for error in errors))
def test_reasoning_effort_is_required_for_codex_and_null_for_cursor(self) -> None:
codex = valid_orchestration()
codex["profiles"]["codex-dev-standard"]["reasoningEffort"] = None
cursor = valid_orchestration()
cursor["profiles"]["cursor-test-standard"]["reasoningEffort"] = "low"
codex_errors = worker_profiles.validate_orchestration(codex)
cursor_errors = worker_profiles.validate_orchestration(cursor)
self.assertTrue(any("Codex requires" in error for error in codex_errors))
self.assertTrue(any("Cursor requires null" in error for error in cursor_errors))
def test_test_cannot_use_strong_tier(self) -> None:
routing = valid_orchestration()
profile = routing["profiles"]["cursor-test-standard"]
profile["tier"] = "strong"
routing["modelAllowlist"]["cursor-agent"]["test"]["strong"] = [
"cursor-auto"
]
errors = worker_profiles.validate_orchestration(routing)
self.assertTrue(any("Test may only use standard" in error for error in errors))
self.assertTrue(any("Test cannot define a strong allowlist" in error for error in errors))
def test_profile_model_must_match_exact_cli_role_tier_allowlist(self) -> None:
routing = valid_orchestration()
routing["profiles"]["codex-dev-standard"]["model"] = "other-safe-model"
errors = worker_profiles.validate_orchestration(routing)
self.assertTrue(any("is not allowed for its cli/role/tier" in error for error in errors))
def test_defaults_require_matching_role_and_standard_tier(self) -> None:
wrong_role = valid_orchestration()
wrong_role["defaults"]["test"] = "codex-dev-standard"
strong_default = valid_orchestration()
strong_default["defaults"]["developer"] = "codex-dev-strong"
role_errors = worker_profiles.validate_orchestration(wrong_role)
tier_errors = worker_profiles.validate_orchestration(strong_default)
self.assertTrue(any("profile role must be test" in error for error in role_errors))
self.assertTrue(any("default profile must use standard" in error for error in tier_errors))
def test_developer_upgraded_default_requires_developer_strong(self) -> None:
valid = valid_orchestration()
wrong_tier = valid_orchestration()
wrong_tier["defaults"]["developerUpgraded"] = "codex-dev-standard"
wrong_role = valid_orchestration()
wrong_role["defaults"]["developerUpgraded"] = "cursor-test-standard"
self.assertEqual(worker_profiles.validate_orchestration(valid), [])
tier_errors = worker_profiles.validate_orchestration(wrong_tier)
role_errors = worker_profiles.validate_orchestration(wrong_role)
self.assertTrue(any("must use strong tier" in error for error in tier_errors))
self.assertTrue(any("profile role must be developer" in error for error in role_errors))
def test_malformed_scalar_types_return_errors_instead_of_raising(self) -> None:
for field in ("role", "cli", "tier", "reasoningEffort", "permissionMode"):
with self.subTest(profile_field=field):
profile = valid_orchestration()["profiles"]["codex-dev-standard"]
profile[field] = []
self.assertTrue(worker_profiles.validate_profile(profile))
routing = valid_orchestration()
routing["mode"] = []
self.assertTrue(worker_profiles.validate_orchestration(routing))
receipt = valid_receipt()
receipt["profileId"] = []
receipt["requested"]["cli"] = []
receipt["requested"]["tier"] = []
receipt["requested"]["permissionMode"] = []
receipt["receiptHash"] = worker_profiles.receipt_hash(receipt)
self.assertTrue(worker_profiles.validate_worker_receipt(receipt))
self.assertTrue(
worker_profiles.validate_worker_receipt(receipt, orchestration=[])
)
def test_orca_requires_defaults_profiles_and_allowed_worktree(self) -> None:
routing = valid_orchestration()
routing["allowedWorktrees"] = []
routing["profiles"] = {}
routing["defaults"] = {}
errors = worker_profiles.validate_orchestration(routing)
self.assertTrue(any("requires at least one path" in error for error in errors))
self.assertTrue(any("Orca mode requires profiles" in error for error in errors))
self.assertTrue(any("missing role 'developer'" in error for error in errors))
self.assertTrue(any("missing role 'test'" in error for error in errors))
class ArgvRendererTests(unittest.TestCase):
def test_codex_exact_safe_argv(self) -> None:
profile = valid_orchestration()["profiles"]["codex-dev-standard"]
argv = worker_profiles.render_worker_argv(
profile,
"/usr/local/bin/codex",
"/repo/demo",
)
self.assertEqual(
argv,
[
"/usr/local/bin/codex",
"--strict-config",
"--model",
"gpt-safe-dev",
"--config",
"model_reasoning_effort=medium",
"--sandbox",
"workspace-write",
"--ask-for-approval",
"never",
"--cd",
"/repo/demo",
],
)
def test_cursor_read_only_exact_safe_argv(self) -> None:
profile = valid_orchestration()["profiles"]["cursor-test-standard"]
argv = worker_profiles.render_worker_argv(
profile,
"/usr/local/bin/cursor-agent",
"/repo/demo",
)
self.assertEqual(
argv,
[
"/usr/local/bin/cursor-agent",
"--model",
"cursor-auto",
"--mode",
"plan",
"--sandbox",
"enabled",
"--workspace",
"/repo/demo",
],
)
def test_cursor_workspace_write_adds_auto_review_without_yolo(self) -> None:
profile = valid_orchestration()["profiles"]["cursor-dev-standard"]
argv = worker_profiles.render_worker_argv(
profile,
"/usr/local/bin/cursor-agent",
"/repo/demo",
)
self.assertEqual(
argv,
[
"/usr/local/bin/cursor-agent",
"--model",
"cursor-auto",
"--auto-review",
"--sandbox",
"enabled",
"--workspace",
"/repo/demo",
],
)
self.assertNotIn("--yolo", argv)
self.assertNotIn("--force", argv)
def test_renderer_rejects_wrong_executable_or_unsafe_worktree(self) -> None:
profile = valid_orchestration()["profiles"]["codex-dev-standard"]
with self.assertRaises(ValueError):
worker_profiles.render_worker_argv(
profile,
"/tmp/cursor-agent",
"/repo/demo",
)
with self.assertRaises(ValueError):
worker_profiles.render_worker_argv(
profile,
"/usr/local/bin/codex",
"/repo/demo/../outside",
)
for worktree in ("/repo/./demo", "/repo/demo/", "//repo/demo"):
with self.subTest(worktree=worktree), self.assertRaises(ValueError):
worker_profiles.render_worker_argv(
profile,
"/usr/local/bin/codex",
worktree,
)
class ReceiptValidationTests(unittest.TestCase):
def test_valid_receipt_is_bound_to_profile_and_routing(self) -> None:
routing = valid_orchestration()
receipt = valid_receipt(routing)
self.assertEqual(
worker_profiles.validate_worker_receipt(
receipt,
orchestration=routing,
task_ids={"TASK-001"},
),
[],
)
self.assertEqual(receipt["receiptHash"], worker_profiles.receipt_hash(receipt))
def test_receipt_rejects_unknown_fields_at_every_strict_level(self) -> None:
cases = (
((), "command"),
(("createdFor",), "command"),
(("worktree",), "command"),
(("requested",), "command"),
(("binding",), "command"),
)
for path, field in cases:
with self.subTest(path=path):
routing = valid_orchestration()
receipt = valid_receipt(routing)
target = receipt
for component in path:
target = target[component]
target[field] = "forged"
receipt["receiptHash"] = worker_profiles.receipt_hash(receipt)
errors = worker_profiles.validate_worker_receipt(
receipt,
orchestration=routing,
task_ids={"TASK-001"},
)
self.assertTrue(any("unknown field 'command'" in error for error in errors))
def test_receipt_rejects_profile_and_exact_argv_drift(self) -> None:
routing = valid_orchestration()
receipt = valid_receipt(routing)
receipt["requested"]["model"] = "gpt-safe-strong"
receipt["requested"]["argv"][3] = "gpt-safe-strong"
receipt["requested"]["argvHash"] = worker_profiles.canonical_sha256(
receipt["requested"]["argv"]
)
receipt["receiptHash"] = worker_profiles.receipt_hash(receipt)
errors = worker_profiles.validate_worker_receipt(
receipt,
orchestration=routing,
task_ids={"TASK-001"},
)
self.assertTrue(any("requested.model: does not match profile" in error for error in errors))
self.assertTrue(
any(
"requested.argv: does not match exact renderer" in error
for error in errors
)
)
def test_receipt_rejects_tampering_without_recomputed_hash(self) -> None:
routing = valid_orchestration()
receipt = valid_receipt(routing)
receipt["binding"]["handle"] = "forged-handle"
errors = worker_profiles.validate_worker_receipt(
receipt,
orchestration=routing,
task_ids={"TASK-001"},
)
self.assertTrue(any("receiptHash: does not match receipt" in error for error in errors))
def test_receipt_rejects_out_of_budget_attempt_empty_argv_and_id_drift(self) -> None:
routing = valid_orchestration()
attempt = valid_receipt(routing)
attempt["createdFor"]["attemptId"] = "TASK-001-A4"
attempt["receiptHash"] = worker_profiles.receipt_hash(attempt)
attempt_errors = worker_profiles.validate_worker_receipt(
attempt,
orchestration=routing,
task_ids={"TASK-001"},
)
empty_argv = valid_receipt(routing)
empty_argv["requested"]["argv"] = []
empty_argv["requested"]["argvHash"] = worker_profiles.canonical_sha256([])
empty_argv["receiptHash"] = worker_profiles.receipt_hash(empty_argv)
argv_errors = worker_profiles.validate_worker_receipt(
empty_argv,
orchestration=routing,
task_ids={"TASK-001"},
)
wrong_id = valid_receipt(routing)
wrong_id["id"] = "WR-" + "b" * 64
wrong_id["receiptHash"] = worker_profiles.receipt_hash(wrong_id)
id_errors = worker_profiles.validate_worker_receipt(
wrong_id,
orchestration=routing,
task_ids={"TASK-001"},
)
self.assertTrue(any("A1..A3" in error for error in attempt_errors))
self.assertTrue(any("at least 2 items" in error for error in argv_errors))
self.assertTrue(any("must equal 'WR-' + launchId" in error for error in id_errors))
def test_receipt_rejects_rehashed_launch_fingerprint_tampering(self) -> None:
routing = valid_orchestration()
receipt = valid_receipt(routing)
receipt["launchFingerprint"] = worker_profiles.canonical_sha256(
{"forged": True}
)
receipt["receiptHash"] = worker_profiles.receipt_hash(receipt)
errors = worker_profiles.validate_worker_receipt(
receipt,
orchestration=routing,
task_ids={"TASK-001"},
)
self.assertTrue(
any(
"launchFingerprint: does not match launch facts" in error
for error in errors
)
)
def test_receipt_slot_is_bounded_and_bound_into_launch_fingerprint(self) -> None:
routing = valid_orchestration()
invalid = valid_receipt(routing)
invalid["slot"] = 0
invalid["receiptHash"] = worker_profiles.receipt_hash(invalid)
invalid_errors = worker_profiles.validate_worker_receipt(
invalid,
orchestration=routing,
task_ids={"TASK-001"},
)
drifted = valid_receipt(routing)
drifted["slot"] = 2
drifted["receiptHash"] = worker_profiles.receipt_hash(drifted)
drifted_errors = worker_profiles.validate_worker_receipt(
drifted,
orchestration=routing,
task_ids={"TASK-001"},
)
self.assertTrue(any("slot: must be" in error for error in invalid_errors))
self.assertTrue(
any(
"launchFingerprint: does not match launch facts" in error
for error in drifted_errors
)
)
def test_receipt_with_non_json_data_returns_errors(self) -> None:
receipt = valid_receipt()
receipt["requested"]["argv"] = [object()]
errors = worker_profiles.validate_worker_receipt(receipt)
self.assertTrue(any("must contain canonical JSON data" in error for error in errors))
def test_receipt_rejects_wrong_worktree_task_and_binding(self) -> None:
routing = valid_orchestration()
receipt = valid_receipt(routing)
receipt["worktree"]["path"] = "/repo/other"
receipt["binding"]["observedWorktreePath"] = "/repo/elsewhere"
receipt["receiptHash"] = worker_profiles.receipt_hash(receipt)
errors = worker_profiles.validate_worker_receipt(
receipt,
orchestration=routing,
task_ids={"OTHER-TASK"},
)
self.assertTrue(any("unknown task 'TASK-001'" in error for error in errors))
self.assertTrue(any("is not in allowedWorktrees" in error for error in errors))
self.assertTrue(any("does not match worktree.path" in error for error in errors))
def test_receipt_requires_safe_environment_and_live_binding(self) -> None:
routing = valid_orchestration()
receipt = valid_receipt(routing)
receipt["requested"]["environmentPolicy"] = "inherit-all"
receipt["binding"]["connected"] = False
receipt["binding"]["writable"] = False
receipt["receiptHash"] = worker_profiles.receipt_hash(receipt)
errors = worker_profiles.validate_worker_receipt(
receipt,
orchestration=routing,
task_ids={"TASK-001"},
)
self.assertTrue(any("per-cli-allowlist-v1" in error for error in errors))
self.assertTrue(any("binding.connected: must be true" in error for error in errors))
self.assertTrue(any("binding.writable: must be true" in error for error in errors))
def test_receipt_list_rejects_duplicate_receipt_and_launch_ids(self) -> None:
routing = valid_orchestration()
first = valid_receipt(routing)
duplicate = copy.deepcopy(first)
errors = worker_profiles.validate_worker_receipts(
[first, duplicate],
routing,
task_ids={"TASK-001"},
)
self.assertTrue(any("duplicate receipt ID" in error for error in errors))
self.assertTrue(any("duplicate launch ID" in error for error in errors))
def test_document_requires_top_level_receipts_and_routing(self) -> None:
missing = {
"version": 1,
"project": {"name": "demo"},
"tasks": [],
}
errors = worker_profiles.validate_routing_document(missing)
self.assertEqual(errors, ["project.orchestration: is required"])
def test_valid_document_passes_and_manual_receipts_fail(self) -> None:
routing = valid_orchestration()
document = {
"version": 1,
"project": {"name": "demo", "orchestration": routing},
"workerReceipts": [valid_receipt(routing)],
"tasks": [{"id": "TASK-001", "title": "demo", "status": "open"}],
}
self.assertEqual(worker_profiles.validate_routing_document(document), [])
manual = {
"version": 1,
"project": {
"name": "demo",
"orchestration": {
"profileVersion": 1,
"mode": "manual",
"allowedWorktrees": [],
"modelAllowlist": {},
"profiles": {},
"defaults": {},
},
},
"workerReceipts": [valid_receipt(routing)],
"tasks": [{"id": "TASK-001", "title": "demo", "status": "open"}],
}
manual_errors = worker_profiles.validate_routing_document(manual)
self.assertTrue(
any("manual orchestration requires an empty list" in error for error in manual_errors)
)
def test_dispatch_receipt_allows_two_phase_for_exact_task_attempt_binding(self) -> None:
routing = valid_orchestration()
receipt = valid_receipt(routing)
role_dispatch = {
"profileId": receipt["profileId"],
"receiptId": receipt["id"],
"attemptId": "TASK-001-A1",
"taskId": None,
"dispatchId": None,
}
document = {
"version": 1,
"project": {"name": "demo", "orchestration": routing},
"workerReceipts": [receipt],
"tasks": [
{
"id": "TASK-001",
"title": "created for",
"status": "dispatched",
"dispatch": {"developer": role_dispatch},
},
],
}
self.assertEqual(worker_profiles.validate_routing_document(document), [])
role_dispatch["taskId"] = "orca-task-002"
role_dispatch["dispatchId"] = "orca-dispatch-002"
self.assertEqual(worker_profiles.validate_routing_document(document), [])
def test_dispatch_rejects_unknown_or_mismatched_receipt(self) -> None:
routing = valid_orchestration()
receipt = valid_receipt(routing)
role_dispatch = {
"profileId": "codex-dev-strong",
"receiptId": receipt["id"],
"attemptId": "TASK-001-A1",
"taskId": None,
"dispatchId": None,
}
document = {
"version": 1,
"project": {"name": "demo", "orchestration": routing},
"workerReceipts": [receipt],
"tasks": [
{
"id": "TASK-001",
"title": "demo",
"status": "open",
"dispatch": {"developer": role_dispatch},
}
],
}
errors = worker_profiles.validate_routing_document(document)
self.assertTrue(
any("profileId: does not match referenced receipt" in error for error in errors)
)
role_dispatch["receiptId"] = f"WR-{'b' * 64}"
errors = worker_profiles.validate_routing_document(document)
self.assertTrue(any("unknown receipt" in error for error in errors))
def test_dispatch_rejects_cross_task_role_and_attempt_receipts(self) -> None:
routing = valid_orchestration()
receipt = valid_receipt(routing)
link = {
"profileId": receipt["profileId"],
"receiptId": receipt["id"],
"attemptId": "TASK-001-A1",
"taskId": None,
"dispatchId": None,
}
document = {
"version": 1,
"project": {"name": "demo", "orchestration": routing},
"workerReceipts": [receipt],
"tasks": [
{"id": "TASK-001", "title": "receipt owner", "status": "open"},
{
"id": "TASK-002",
"title": "must not reuse receipt",
"status": "dispatched",
"dispatch": {"developer": link},
},
],
}
errors = worker_profiles.validate_routing_document(document)
self.assertTrue(any("must be current ACK task 'TASK-002'" in error for error in errors))
self.assertTrue(any("must belong to current ACK task 'TASK-002'" in error for error in errors))
document["tasks"][1]["id"] = "TASK-001"
link["attemptId"] = "TASK-001-A2"
errors = worker_profiles.validate_routing_document(document)
self.assertTrue(any("does not match referenced receipt" in error for error in errors))
link["attemptId"] = "TASK-001-A1"
document["tasks"][1]["dispatch"] = {"test": link}
errors = worker_profiles.validate_routing_document(document)
self.assertTrue(any("referenced receipt role must be test" in error for error in errors))
def test_dispatch_attempt_presence_tracks_receipt_presence(self) -> None:
routing = valid_orchestration()
receipt = valid_receipt(routing)
link = {
"profileId": receipt["profileId"],
"receiptId": None,
"attemptId": "TASK-001-A1",
"taskId": None,
"dispatchId": None,
}
document = {
"version": 1,
"project": {"name": "demo", "orchestration": routing},
"workerReceipts": [receipt],
"tasks": [
{
"id": "TASK-001",
"title": "demo",
"status": "open",
"dispatch": {"developer": link},
}
],
}
errors = worker_profiles.validate_routing_document(document)
self.assertTrue(any("must be null when receiptId is null" in error for error in errors))
link["receiptId"] = receipt["id"]
link["attemptId"] = None
errors = worker_profiles.validate_routing_document(document)
self.assertTrue(any("is required when receiptId is set" in error for error in errors))
def test_dispatch_requires_receipt_before_paired_runtime_ids(self) -> None:
routing = valid_orchestration()
document = {
"version": 1,
"project": {"name": "demo", "orchestration": routing},
"workerReceipts": [],
"tasks": [
{
"id": "TASK-001",
"title": "demo",
"status": "dispatched",
"dispatch": {
"developer": {
"profileId": "codex-dev-standard",
"receiptId": None,
"attemptId": None,
"taskId": "orca-task-001",
"dispatchId": None,
}
},
}
],
}
errors = worker_profiles.validate_routing_document(document)
self.assertTrue(any("must both be null or both be set" in error for error in errors))
self.assertTrue(any("required before runtime dispatch IDs" in error for error in errors))
if __name__ == "__main__":
unittest.main()