feat(ack): add grok workers and allow --always-approve
Grok is a first-class worker CLI. Launcher argv includes --always-approve so unattended tool calls are not blocked; sandbox stays required.
This commit is contained in:
+36
-16
@@ -9,8 +9,9 @@ ACK 是一个显式调用的 Agent Skill,用三种独立角色运行工程协
|
||||
关键约束是验证者不等于实现者。每个任务最多修复三轮,仍未通过时记录为
|
||||
`leftover`,然后继续处理其它任务。
|
||||
|
||||
项目还可以声明一个可选的交付阶段:任务全部验证后,ACK 按项目维护的 profile
|
||||
构建 DEB 或镜像、发布产物、创建 PR,并在授权范围内部署。交付配置默认关闭,
|
||||
项目还可以在同一份 `docs/ack/delivery.yaml` 里声明测试环境部署和版本发布。
|
||||
用户告诉 ACK 这两件事怎么做之后,再说「重新布测试环境」或「发布一个版本」,
|
||||
ACK 按对应 intent 执行。任务全部验证后仍可按 profile 做常规交付。配置默认关闭,
|
||||
稳定发布与生产部署始终保留人工批准点。
|
||||
|
||||
## 安装
|
||||
@@ -70,8 +71,8 @@ skills/ack/
|
||||
`docs/ack/project.md` 只保存当前项目的命令、路径和权限差异;
|
||||
`docs/ack/tasks.yaml` 保存当前任务状态;`docs/ack/knowledge.yaml` 保存跨任务复用、
|
||||
已经独立验证的项目知识护栏。
|
||||
`docs/ack/delivery.yaml` 声明项目特有的构建、发布和部署能力;每次执行结果另记在
|
||||
`tasks.yaml.deliveryRuns`,配置与运行状态不会混在一起。
|
||||
`docs/ack/delivery.yaml` 是测试环境部署和版本发布的唯一契约,也声明常规构建、
|
||||
发布和部署能力;每次执行结果另记在 `tasks.yaml.deliveryRuns`。
|
||||
|
||||
## 检查项目状态
|
||||
|
||||
@@ -139,16 +140,29 @@ python3 <ack-skill-dir>/scripts/run_verification.py \
|
||||
|
||||
## 配置与运行交付
|
||||
|
||||
用户可以直接向 `/ack` 描述项目差异,例如“这个项目用 `make deb` 构建 DEB,推到
|
||||
preview APT 源,再部署到开发机”。ACK 会把它维护成
|
||||
`docs/ack/delivery.yaml` 中的声明式 entrypoint、artifact、destination、environment
|
||||
和 profile,校验后展示 diff;首次配置保持关闭,确认后才启用。
|
||||
用户可以直接向 `/ack` 说明两件独立操作,并写进同一份契约:
|
||||
|
||||
```text
|
||||
/ack 测试时先 go build -o garden ./cmd/garden,再启动这个二进制;
|
||||
发版方式以后再告诉你。
|
||||
```
|
||||
|
||||
ACK 把它维护成 `docs/ack/delivery.yaml` 的 `intents.testEnvironment` /
|
||||
`intents.release`、entrypoint、artifact、environment 和 profile。首次配置保持
|
||||
关闭,确认后才启用。之后用户可以说:
|
||||
|
||||
```text
|
||||
/ack 重新布一下测试环境,我要测试
|
||||
/ack 发布一个版本
|
||||
```
|
||||
|
||||
对应 intent 未配置时先问清楚并写回同一文件,不猜测。intent 运行不要求当前有
|
||||
`verified` 任务;`deliveryRuns.intent` 记录是测试环境还是发版。
|
||||
|
||||
交付配置只允许声明式工具 target 或仓库内可执行脚本,不接受自由 shell,也不保存
|
||||
凭据值。ACK 在任务进入 `verified` 后,按用户确认的 profile 执行,并把 revision、
|
||||
PR、产物摘要、部署目标、健康检查和日志引用写入 `tasks.yaml.deliveryRuns`。默认
|
||||
profile 只能停在 `review_ready`;稳定发布或生产部署必须经过对应 approval 步骤。
|
||||
具体契约见 `references/delivery.md`。
|
||||
凭据值。任务进入 `verified` 后的常规交付仍按确认过的 profile 执行。默认
|
||||
profile 只能停在 `validation_ready` 或 `review_ready`;稳定发布或生产部署必须经过
|
||||
对应 approval 步骤。具体契约见 `references/delivery.md`。
|
||||
|
||||
## 启动 Worker
|
||||
|
||||
@@ -191,8 +205,10 @@ identity。Coordinator 将 receipt 追加到顶层 `workerReceipts`,再把 rec
|
||||
不能跨任务或跨轮次改挂。
|
||||
|
||||
v0.10 自动 launcher 仅支持 `read-only` 与 `workspace-write`。full-access、
|
||||
Codex bypass、Cursor YOLO/force 和关闭 sandbox 都会 fail closed;在有可信平台
|
||||
审批或独立签发通道之前,不用项目文件伪装成用户授权。旧任务板没有结构化
|
||||
Codex bypass、Cursor YOLO/force、Grok `--yolo` / bypassPermissions 和关闭
|
||||
sandbox 都会 fail closed;在有可信平台审批或独立签发通道之前,不用项目文件
|
||||
伪装成用户授权。Grok worker 由 launcher 固定带 `--always-approve`,避免工具调用
|
||||
停在确认框,sandbox 仍必须启用。v0.17 起 `cli: grok` 是一等 worker CLI。旧任务板没有结构化
|
||||
`project.orchestration` 时仍可读取和手动协作,但不得自动创建 worker。
|
||||
|
||||
持久化 `receiptHash` 是无密钥 checksum,不是 launcher 身份证明。ACK 只复用同一轮
|
||||
@@ -243,5 +259,9 @@ ACK 会自动读取 `delivery.yaml`,无需再逐步提醒它构建、上传、
|
||||
`ackVersion`。从 `0.10.0` 起,`project.orchestration` 与顶层 `workerReceipts` 必须
|
||||
同时存在;从 `0.11.0` 起,新项目还会生成默认关闭的 `delivery.yaml`,并在任务板声明
|
||||
`project.deliveryFile` 与 `deliveryRuns`;从 `0.13.0` 起,Coordinator 使用
|
||||
`select_tasks.py` 获取有预算的任务上下文,不再把完整任务板注入模型。旧项目可以不迁移
|
||||
而继续使用原闭环。旧项目的 `kitVersion` 可以继续读取,但建议迁移为 `ackVersion`。
|
||||
`select_tasks.py` 获取有预算的任务上下文,不再把完整任务板注入模型;从 `0.16.0` 起,
|
||||
`delivery.yaml` 可用 `intents.testEnvironment` 与 `intents.release` 把测试环境部署和
|
||||
版本发布写成用户可单独触发的操作;从 `0.17.0` 起,结构化 worker 路由支持
|
||||
`cli: grok`(与 Codex、Cursor 并列);从 `0.17.1` 起 Grok worker argv 固定带
|
||||
`--always-approve`,sandbox 仍必开。旧项目可以不迁移而继续使用原闭环。旧项目的
|
||||
`kitVersion` 可以继续读取,但建议迁移为 `ackVersion`。
|
||||
|
||||
+36
-8
@@ -3,7 +3,7 @@ name: ack
|
||||
description: >-
|
||||
初始化、检查并运行 ACK 三角色协作闭环。仅在用户显式调用 /ack 或 $ack,并要求
|
||||
初始化 ACK、检查 docs/ack 配置、按 ACK 规划需求、指挥 Coordinator/Developer/Test
|
||||
工作,或配置并执行任务验证后的项目交付流程时使用。
|
||||
工作,配置测试环境与发版方式,重新部署测试环境,或发布版本时使用。
|
||||
---
|
||||
|
||||
# ACK 项目协作入口
|
||||
@@ -21,7 +21,11 @@ description: >-
|
||||
- 用户要求初始化、接入或安装 ACK:执行“初始化”。
|
||||
- 用户要求检查 ACK 是否可用、配置是否完整:执行“检查”。
|
||||
- 用户要求用 ACK 做需求、修复问题或继续任务:执行“工作”。
|
||||
- 用户用自然语言要求增加、修改或关闭项目交付流程:执行“交付配置维护”。
|
||||
- 用户用自然语言说明怎么部署测试环境、怎么发布版本,或要求增加、修改、关闭交付
|
||||
流程:执行“交付配置维护”。测试环境和发版必须写进同一份
|
||||
`docs/ack/delivery.yaml`。
|
||||
- 用户要求部署、重新部署测试环境,或按已配置方式开始测试:执行“运行测试环境”。
|
||||
- 用户要求发布版本:执行“运行版本发布”。
|
||||
|
||||
始终先解析真实项目根目录。优先使用 `git rev-parse --show-toplevel`;不是 Git
|
||||
项目时使用用户指定目录或当前目录。不要修改项目的 `AGENTS.md`、`CLAUDE.md`
|
||||
@@ -169,8 +173,9 @@ description: >-
|
||||
任一条件不符、清理能力不存在或无法确认清理成功时创建 fresh worker。持久化
|
||||
receipt 只作审计与 dispatch 关联,不能单独授权复用。当前 Orca 终端接口不能提供
|
||||
可验证的历史消息清理,因此使用 Orca 时仍走 fresh worker。
|
||||
6. 用户已确认的任务按 ACK 闭环执行:Developer 实现与白盒验证,Test 独立黑盒
|
||||
复测,Coordinator 读取证据终检并唯一写入 `tasks.yaml`。Developer 回报
|
||||
6. 用户已确认的任务按 ACK 闭环执行:Developer 实现与白盒验证;若
|
||||
`intents.testEnvironment` 已启用,Coordinator 先按「运行测试环境」拉起服务,再
|
||||
派 Test 独立黑盒复测。Coordinator 读取证据终检并唯一写入 `tasks.yaml`。Developer 回报
|
||||
`knowledgeApplied` 和 `knowledgeCandidates`,Test 回报 `knowledgeChecks`;
|
||||
`candidate` 只有在独立验证和 gate 后才能由 Coordinator 写入或激活。
|
||||
7. 执行知识项的 `verification.ref` 时,只调用
|
||||
@@ -202,8 +207,10 @@ description: >-
|
||||
## 交付配置维护
|
||||
|
||||
1. 读取 `references/delivery.md`、模板、schema、现有 `delivery.yaml`、项目构建/发布
|
||||
入口和 CI;把用户自然语言描述转换为结构化 entrypoint、artifact、destination、
|
||||
environment 与 profile。配置只引用仓库内脚本或声明式工具 target,不保存 shell。
|
||||
入口和 CI;把用户自然语言描述转换为结构化 `intents`、entrypoint、artifact、
|
||||
destination、environment 与 profile。测试环境部署和版本发布都写进这一份
|
||||
`delivery.yaml`,不要拆成第二份文档。配置只引用仓库内脚本或声明式工具 target,
|
||||
不保存 shell。
|
||||
2. 若旧项目首次启用,生成 `docs/ack/delivery.yaml`,在 `tasks.yaml.project` 增加
|
||||
`deliveryFile: docs/ack/delivery.yaml`,并增加顶层 `deliveryRuns: []`;不改写其它
|
||||
项目状态。首次生成保持 `enabled: false`,先展示 diff 和解析出的执行顺序。
|
||||
@@ -213,13 +220,34 @@ description: >-
|
||||
执行的 run 使用开始时审阅的 commit/config revision 快照,不能借当前分支修改
|
||||
扩大权限。
|
||||
|
||||
## 运行测试环境
|
||||
|
||||
1. 读取 `docs/ack/delivery.yaml` 与 `references/delivery.md`。
|
||||
2. `enabled` 不为 true,或 `intents.testEnvironment` 为 null:停止,请用户说明如何
|
||||
部署测试环境,转入交付配置维护。不猜测编译或启动命令。
|
||||
3. 不要求任务已 `verified`。按该 profile 执行 build → deploy → health-check。
|
||||
4. 把访问地址交给用户或随后的 Test 黑盒。证据写入 `deliveryRuns`,
|
||||
`intent: testEnvironment`,`taskIds` 可为空。
|
||||
5. 派发 Test 前若该 intent 已启用,必须先完成本步骤。
|
||||
|
||||
## 运行版本发布
|
||||
|
||||
1. 读取同一份 `docs/ack/delivery.yaml` 与 `references/delivery.md`。
|
||||
2. `enabled` 不为 true,或 `intents.release` 为 null:停止,请用户说明如何发版,
|
||||
写入同一文件后再执行。
|
||||
3. 按该 profile 顺序执行。stable 发布和生产部署的 `approval` 不能用口头「发版」
|
||||
代替。
|
||||
4. 证据写入 `deliveryRuns`,`intent: release`;绑定了任务时 `taskIds` 仍只能引用
|
||||
`verified` 任务。
|
||||
|
||||
## 边界
|
||||
|
||||
- 不修改或追加任何项目 Agent 指令文件,包括 `AGENTS.md`。
|
||||
- 不在项目中维护第二份 ACK 通用规范、模板或任务 schema。
|
||||
- 不猜测项目命令、服务地址、worker handle 或模型名称。
|
||||
- 不把 full-access、bypass、YOLO/force、关闭 sandbox 或项目内“授权”字段当成
|
||||
v0.10 自动 worker 的合法配置;当前一律 fail closed。
|
||||
- 不把 full-access、bypass、YOLO/force、Grok `--yolo` / bypassPermissions、
|
||||
关闭 sandbox 或项目内“授权”字段当成 v0.10 自动 worker 的合法配置;当前一律
|
||||
fail closed。Grok worker 由 launcher 固定带 `--always-approve`,仍必须带 sandbox。
|
||||
- 不把无密钥 `receiptHash` 或 Orca live metadata 当作旧终端的启动 attestation;
|
||||
没有可信空闲状态、配置匹配和历史消息清理证明时不复用既有 worker。
|
||||
- launcher 返回 `indeterminate` 或 `reconcile required` 时,不直接重试;先按
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
0.15.0
|
||||
0.17.1
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
interface:
|
||||
display_name: "ACK"
|
||||
short_description: "初始化、检查并运行 ACK 开发、验证与可选交付闭环"
|
||||
default_prompt: "Use $ack to initialize or check ACK, coordinate verified work, maintain project delivery configuration, or run an approved delivery profile."
|
||||
default_prompt: "Use $ack to initialize or check ACK, coordinate verified work, record how to deploy the test environment and publish a release in delivery.yaml, redeploy the test environment, or publish a version."
|
||||
policy:
|
||||
allow_implicit_invocation: false
|
||||
|
||||
@@ -6,6 +6,10 @@ project:
|
||||
enabled: true
|
||||
defaultProfile: "review"
|
||||
|
||||
intents:
|
||||
testEnvironment: test-env
|
||||
release: null
|
||||
|
||||
entrypoints:
|
||||
verify:
|
||||
kind: make
|
||||
@@ -100,6 +104,19 @@ environments:
|
||||
mutex: "notes-test-deploy"
|
||||
|
||||
profiles:
|
||||
test-env:
|
||||
stopAt: validation_ready
|
||||
steps:
|
||||
- id: build-deb
|
||||
action: build
|
||||
artifact: service-deb
|
||||
- id: deploy-test
|
||||
action: deploy
|
||||
artifact: service-deb
|
||||
environment: test-server
|
||||
- id: smoke-test
|
||||
action: health-check
|
||||
environment: test-server
|
||||
review:
|
||||
stopAt: review_ready
|
||||
steps:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# notes-web Agent 协作协议(示例,项目覆盖层)
|
||||
|
||||
> 本项目基于 ack v0.15.0。
|
||||
> 本项目基于 ack v0.17.1。
|
||||
> 通用规范由 `/ack` 从 Skill 自身的 `references/` 读取,本文件只填项目差异。
|
||||
> 覆盖层文件放在 `docs/ack/project.md`,不占用 `AGENTS.md`。
|
||||
> ACK 不会自动修改 `AGENTS.md`、`CLAUDE.md` 或其它 Agent 指令文件。
|
||||
@@ -39,7 +39,8 @@
|
||||
| Test | `codex-test-standard` | standard |
|
||||
| Developer 升级 | `codex-dev-strong` | strong |
|
||||
|
||||
本文件不保存或拼接 worker shell 命令。
|
||||
项目如改用 Cursor 或 Grok,应修改结构化 profile、allowlist 和 defaults,再运行
|
||||
任务板校验。本文件不保存或拼接 worker shell 命令。
|
||||
|
||||
## 路径权限
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
version: 1
|
||||
updatedAt: "2026-07-06T09:40:00+08:00"
|
||||
source: "Coordinator (PM) Agent"
|
||||
ackVersion: "0.15.0"
|
||||
ackVersion: "0.17.1"
|
||||
project:
|
||||
name: "notes-web"
|
||||
baseUrl: "http://localhost:5173"
|
||||
@@ -27,6 +27,12 @@ project:
|
||||
standard: ["auto"]
|
||||
test:
|
||||
standard: ["auto"]
|
||||
grok:
|
||||
developer:
|
||||
standard: ["grok-4.5"]
|
||||
strong: ["grok-4.6"]
|
||||
test:
|
||||
standard: ["grok-4.5"]
|
||||
profiles:
|
||||
codex-dev-standard:
|
||||
role: "developer"
|
||||
@@ -63,6 +69,27 @@ project:
|
||||
model: "auto"
|
||||
reasoningEffort: null
|
||||
permissionMode: "workspace-write"
|
||||
grok-dev-standard:
|
||||
role: "developer"
|
||||
cli: "grok"
|
||||
tier: "standard"
|
||||
model: "grok-4.5"
|
||||
reasoningEffort: "medium"
|
||||
permissionMode: "workspace-write"
|
||||
grok-test-standard:
|
||||
role: "test"
|
||||
cli: "grok"
|
||||
tier: "standard"
|
||||
model: "grok-4.5"
|
||||
reasoningEffort: "low"
|
||||
permissionMode: "workspace-write"
|
||||
grok-dev-strong:
|
||||
role: "developer"
|
||||
cli: "grok"
|
||||
tier: "strong"
|
||||
model: "grok-4.6"
|
||||
reasoningEffort: "high"
|
||||
permissionMode: "workspace-write"
|
||||
defaults:
|
||||
developer: "codex-dev-standard"
|
||||
test: "codex-test-standard"
|
||||
|
||||
@@ -48,6 +48,8 @@
|
||||
## 可选交付
|
||||
|
||||
- [ ] `delivery.yaml` 首次生成保持 `enabled: false`,没有根据 README/CI 自动启用。
|
||||
- [ ] 测试环境部署和版本发布都写在同一份 `delivery.yaml` 的 `intents` 中,没有第二份
|
||||
操作文档。未说明的 intent 保持 `null`。
|
||||
- [ ] entrypoint 只使用声明式工具 target 或仓库内无 symlink 的可执行脚本;没有
|
||||
shell、自由 command、凭据值或环境变量值。
|
||||
- [ ] artifact、destination、environment 和 profile 引用均通过
|
||||
|
||||
@@ -41,6 +41,8 @@ Coordinator 发现或读取 open 任务
|
||||
-> dispatch 给 Developer(--to <worker handle>)
|
||||
-> wait:Developer 的 worker_done / escalation(含 knowledgeApplied / knowledgeCandidates)
|
||||
-> writeback fixed_by_dev
|
||||
-> 若 delivery.yaml intents.testEnvironment 已启用:Coordinator 先执行该 profile
|
||||
拉起待测服务,再派 Test;Test 不发明编译或启动命令
|
||||
-> 为 Test 独立解析安全 profile;安全重置同角色空闲 worker,或重新 plan/launch fresh worker
|
||||
-> dispatch 给 Test(retesting)
|
||||
-> wait:Test 的 retest_result(含 knowledgeChecks 和 candidate 独立证据)
|
||||
@@ -265,7 +267,7 @@ frontendDir:
|
||||
worktreePath:
|
||||
```
|
||||
|
||||
如果开发在 `<dev_worktree>` 修复,但服务跑的是另一个 worktree,必须**停止并重启正确服务**后再测。长跑服务或静态前端尤其要确认加载的是最新构建产物。
|
||||
如果开发在 `<dev_worktree>` 修复,但服务跑的是另一个 worktree,必须**停止并重启正确服务**后再测。长跑服务或静态前端尤其要确认加载的是最新构建产物。若项目配置了 `intents.testEnvironment`,重启方式以该 profile 为准,不另写一套启动命令。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -52,6 +52,31 @@ channel、environment 或 source revision 漂移时重新确认。
|
||||
凭据系统或执行环境注入。复杂逻辑放在受版本控制的项目入口中。entrypoint 使用 argv
|
||||
语义执行,不能拼成 `sh -c` 字符串。
|
||||
|
||||
## 3.1 测试环境与发版写在同一份契约
|
||||
|
||||
`docs/ack/delivery.yaml` 是测试环境部署和版本发布的唯一文档。不要另写操作手册,
|
||||
也不要把其中一项写进 `project.md`。用户用自然语言说明「怎么布测试环境」或
|
||||
「怎么发版」时,Coordinator 把两者都维护进这份文件的 `intents`、entrypoint、
|
||||
artifact、environment 和 profile。
|
||||
|
||||
```yaml
|
||||
intents:
|
||||
testEnvironment: local-binary # profile ID,或 null
|
||||
release: null # profile ID,或 null
|
||||
```
|
||||
|
||||
- `testEnvironment` 指向 `stopAt: validation_ready` 的 profile:build 产物、部署到
|
||||
development/staging、健康检查。用户说「重新布测试环境」「我要测试」时执行它;
|
||||
派发 Test 复测前,若该 intent 已配置且 `enabled: true`,Coordinator 也先执行它。
|
||||
不要求当前有 `verified` 任务。Test 不对这个 intent 发明编译或启动命令。
|
||||
- `release` 指向 `stopAt: released` 的 profile。用户说「发布一个版本」时执行它。
|
||||
口头「发版」不能代替 stable/production 的 `approval` 步骤。
|
||||
- 对应 intent 为 `null` 或交付未启用:停止,请用户说明怎么做,按「交付配置维护」
|
||||
写入同一文件后再执行。不猜测 Makefile、镜像仓库或发布通道。
|
||||
- 用户触发的 intent 运行写入 `tasks.yaml.deliveryRuns`,`intent` 填
|
||||
`testEnvironment` 或 `release`,`taskIds` 可为空。绑定任务的常规交付 run 不填
|
||||
`intent`,仍只能引用 `verified` 任务。
|
||||
|
||||
## 4. 运行前检查
|
||||
|
||||
1. 从 `tasks.yaml.project.deliveryFile` 解析文件;未引用或 `enabled=false` 时保持旧 ACK
|
||||
|
||||
@@ -116,10 +116,11 @@ candidate 留在任务证据中,不会被派发。只有 Test 独立验证且
|
||||
|
||||
## 初始化项目交付
|
||||
|
||||
新项目的 `docs/ack/delivery.yaml` 保持 `enabled: false`、空能力表和空 profile。
|
||||
不要根据 README 或 CI 自动推断并启用发布/部署。用户用自然语言描述交付要求后,
|
||||
Coordinator 按 `delivery.md` 维护声明式配置:工具 target 与仓库脚本分开引用,产物、
|
||||
目的地、环境和有序 profile 使用稳定 ID。配置中不保存 shell、环境变量值或凭据
|
||||
新项目的 `docs/ack/delivery.yaml` 保持 `enabled: false`、空能力表、空 profile,以及
|
||||
`intents.testEnvironment: null` 与 `intents.release: null`。
|
||||
不要根据 README 或 CI 自动推断并启用发布/部署。用户用自然语言说明测试环境或发版
|
||||
方式后,Coordinator 按 `delivery.md` 把两者都写入这一份契约:`intents` 指向对应
|
||||
profile,工具 target 与仓库脚本分开引用。配置中不保存 shell、环境变量值或凭据
|
||||
正文;稳定发布和生产部署必须有显式 approval 步骤。
|
||||
|
||||
## 校验
|
||||
|
||||
@@ -37,7 +37,8 @@
|
||||
Developer/Test profile;优先选择同一轮内角色/profile/worktree 匹配的空闲 worker,
|
||||
只有历史消息已可信清理并取得新会话身份才复用,否则审阅 plan 后用 expected
|
||||
fingerprint 创建 fresh worker;
|
||||
dispatch 开发 → worker_done → dispatch 测试独立复测 → 你读证据终检 → 回写 tasks.yaml;
|
||||
dispatch 开发 → worker_done → 若 intents.testEnvironment 已启用则先拉起测试环境 →
|
||||
dispatch 测试独立复测 → 你读证据终检 → 回写 tasks.yaml;
|
||||
每个任务最多三轮有效产品复验,三轮不过记 leftover 并升级我复盘;环境失败单独
|
||||
记录、恢复并告诉我下一步,不占产品复验轮次。
|
||||
7. 所选任务都 verified 后,只有本次计划包含交付时才按 profile 顺序执行并写
|
||||
@@ -154,7 +155,7 @@ worktree 走同一套 `plan` -> 带 expected fingerprint 的 `launch`。在调
|
||||
task-create → dispatch 给 DEV → 等 worker_done
|
||||
→ 每个角色先检查可安全重置的空闲 worker;不符合即通过 plan + expected fingerprint launch fresh worker
|
||||
→ 每轮使用 Coordinator 分配的稳定 <task-id>-A<round>
|
||||
→ 回写 fixed_by_dev → dispatch 给 TEST 复测 → 等 retest_result
|
||||
→ 回写 fixed_by_dev → 若 intents.testEnvironment 已启用则先拉起测试环境 → dispatch 给 TEST 复测 → 等 retest_result
|
||||
→ Developer 回 knowledgeApplied / knowledgeCandidates,Test 回 knowledgeChecks
|
||||
→ 环境无法完成:记录 environmentIncidents,报告影响与用户下一步,恢复后重新复验(不计轮次)
|
||||
→ Coordinator 读证据终检 → 过则 verified,产品失败则 failed_retest 再派 DEV(最多累计 3 轮)
|
||||
@@ -173,6 +174,9 @@ Coordinator 只内联本轮 `knowledgeRefs` 指向的少量知识,不要求 wo
|
||||
|
||||
## 第 5 步:可选交付
|
||||
|
||||
用户说「重新布测试环境」或「发布一个版本」时,按 `delivery.md` §3.1 的
|
||||
`intents` 执行对应 profile,不另找文档。intent 为 null 时先做交付配置维护。
|
||||
|
||||
所选任务都由 Coordinator 标记为 `verified` 后,若用户确认的计划包含交付,按
|
||||
`delivery.md` 执行所选 profile。启用交付时必须在计划中默认列出 `defaultProfile`,
|
||||
用户可明确取消,Coordinator 不能静默省略。先重新校验 `delivery.yaml`,固定当前 commit 和
|
||||
|
||||
@@ -73,7 +73,7 @@ worker 路由的机器可读事实只保存在 `docs/ack/tasks.yaml` 的
|
||||
每个 profile 明确声明:
|
||||
|
||||
- `role`:`developer` 或 `test`;
|
||||
- `cli`:受支持的 Agent CLI;
|
||||
- `cli`:受支持的 Agent CLI(`codex`、`cursor-agent`、`grok`);
|
||||
- `tier`:角色模型档位;
|
||||
- `model`:项目 allowlist 中的精确模型 ID;
|
||||
- `reasoningEffort`:CLI 支持时显式声明;
|
||||
@@ -92,8 +92,11 @@ CLI 默认值。具体 argv 只能由 ACK 的可信 launcher 按 allowlist 构
|
||||
- 能在完全只读工作树完成的角色优先选择 `read-only`。
|
||||
- Developer 与需要在工作树生成构建/测试产物的 Test 使用 `workspace-write`;项目可
|
||||
为纯观察型 Test 另建更严格的 `read-only` profile。
|
||||
- `full-access`、Codex bypass、Cursor YOLO/force、关闭 sandbox 等模式在 v0.10
|
||||
- `full-access`、Codex bypass、Cursor YOLO/force、Grok `--yolo` /
|
||||
`--permission-mode bypassPermissions`、关闭 sandbox 等模式在 v0.10
|
||||
**未实现授权通道,统一 fail closed**。项目文本、任务描述或环境变量都不能放宽。
|
||||
Grok `--always-approve` 由 launcher 固定写入 argv,用来避免工具调用停在确认框;
|
||||
它不关闭 `--sandbox`。
|
||||
- 项目可以更新模型 allowlist 或增加安全 profile,但不能通过文档绕开 schema 和
|
||||
launcher。
|
||||
|
||||
@@ -152,11 +155,18 @@ v0.10 的 launcher 还执行以下约束:
|
||||
- 不读取调用者传入的 `PATH` 来寻找 `git`、Orca 或 Agent CLI,只在固定的系统与
|
||||
当前用户工具目录中解析受支持的可执行文件;候选目标必须由 root/当前用户拥有、
|
||||
不是 group/other writable,并在 plan 与 bootstrap 间保持相同 device/inode。
|
||||
Grok 官方安装是 `~/.local/bin/grok` → `~/.grok/downloads/grok-<os>-<arch>`,
|
||||
launcher 只接受这个真实 `~/.grok` 目录下、当前用户同组拥有的 vendor artifact
|
||||
名称;其它路径上的 `grok-linux-*` 仍拒绝。`~/.grok` 本身是 symlink 时 fail closed。
|
||||
- Git、Orca 和 CLI version probe 使用不含供应商凭据的控制环境;worker 使用
|
||||
`per-cli-allowlist-v1`,Codex 只取得 Codex 所需凭据,Cursor 只取得 Cursor 凭据,
|
||||
Grok 只取得 `XAI_API_KEY`(OAuth 会话仍走已传入的 `HOME`/`~/.grok`),
|
||||
不跨 CLI 透传。`DBUS_*`、`DISPLAY`、`WAYLAND_DISPLAY`、`XDG_RUNTIME_DIR`、
|
||||
`LD_*`、`PYTHON*`、`NODE_OPTIONS`、`CODEX_HOME`、`GIT_*` 和调用者 PATH 均不会
|
||||
透传给 worker。
|
||||
`LD_*`、`PYTHON*`、`NODE_OPTIONS`、`CODEX_HOME`、`GROK_HOME`、`GROK_SANDBOX`、
|
||||
`GIT_*` 和调用者 PATH 均不会透传给 worker。Grok argv 只允许
|
||||
`--model`、`--reasoning-effort`、`--permission-mode plan|acceptEdits`、
|
||||
`--always-approve`、`--sandbox read-only|workspace` 和 `--cwd`;不得带 `--yolo`
|
||||
或 `bypassPermissions`,也不得省略 sandbox。
|
||||
- worktree 除了命中项目 allowlist、属于同一 Git common-dir,还必须精确出现在
|
||||
`git worktree list --porcelain -z` 中;`.git` symlink 冒充的目录会失败。
|
||||
- Orca 的 `--command` 只含固定 bootstrap 与随机 launch ID。终端创建后,父进程先
|
||||
|
||||
@@ -128,7 +128,7 @@ Developer 本轮声称(仅供参考,不作数):
|
||||
- <K-014@2>: <directive + rationale + verification.ref + resolved path/args>
|
||||
|
||||
复测要求(见 roles-and-permissions.md §三角色能力清单 · Test):
|
||||
- 先对齐运行环境(pwd / 分支 / commit / 服务 worktree,见 closed-loop.md),避免测错实例或旧构建;网站类先确认服务已按新代码重启。
|
||||
- 先对齐运行环境(pwd / 分支 / commit / 服务 worktree,见 closed-loop.md),避免测错实例或旧构建。测试环境由 Coordinator 按 `delivery.yaml` 的 `intents.testEnvironment` 拉起;不要自行发明编译或启动命令。网站类确认 Base URL 已指向这次产物后再测。
|
||||
- 网站类任务优先用浏览器复测真实交互,其次才是 API / 脚本。
|
||||
- 逐条验证下列验收信号,不要只看静态文案,要验证交互后的真实状态:
|
||||
1. <observable signal 1>
|
||||
|
||||
@@ -39,8 +39,10 @@ if str(SCRIPT_DIR) not in sys.path:
|
||||
|
||||
from validate_tasks import load_document, validate_builtin # noqa: E402
|
||||
from worker_profiles import ( # noqa: E402
|
||||
GROK_EXECUTABLE_NAME_RE,
|
||||
LAUNCH_PROTOCOL_VERSION,
|
||||
canonical_sha256,
|
||||
executable_basename_matches_cli,
|
||||
profile_hash,
|
||||
render_worker_argv,
|
||||
validate_routing_document,
|
||||
@@ -81,6 +83,12 @@ WORKER_ENVIRONMENT_NAMES = CONTROL_ENVIRONMENT_NAMES | {
|
||||
WORKER_CREDENTIAL_NAMES = {
|
||||
"codex": frozenset({"AZURE_OPENAI_API_KEY", "OPENAI_API_KEY"}),
|
||||
"cursor-agent": frozenset({"CURSOR_API_KEY"}),
|
||||
"grok": frozenset({"XAI_API_KEY"}),
|
||||
}
|
||||
CLI_TITLE_LABELS = {
|
||||
"codex": "CODEX",
|
||||
"cursor-agent": "CURSOR",
|
||||
"grok": "GROK",
|
||||
}
|
||||
INHERITED_ENVIRONMENT_PREFIXES = (
|
||||
"LC_",
|
||||
@@ -263,8 +271,41 @@ def load_authoritative_board(project_root_value: str) -> tuple[Path, dict]:
|
||||
return project_root, board
|
||||
|
||||
|
||||
def _is_under_real_grok_home(resolved: Path) -> bool:
|
||||
"""Return True when ``resolved`` is under a non-symlink ``$HOME/.grok``."""
|
||||
|
||||
home, _ = account_identity()
|
||||
grok_root = home / ".grok"
|
||||
try:
|
||||
root_metadata = os.lstat(grok_root)
|
||||
if stat.S_ISLNK(root_metadata.st_mode) or not stat.S_ISDIR(root_metadata.st_mode):
|
||||
return False
|
||||
grok_root.resolve(strict=True)
|
||||
resolved.relative_to(grok_root.resolve(strict=True))
|
||||
except (OSError, ValueError):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _is_trusted_grok_executable(resolved: Path, metadata: os.stat_result) -> bool:
|
||||
"""Accept Grok's vendor artifact names under ``~/.grok``, or a 0755 ``grok``."""
|
||||
|
||||
if GROK_EXECUTABLE_NAME_RE.fullmatch(resolved.name) is None:
|
||||
return False
|
||||
if not executable_basename_matches_cli(str(resolved), "grok"):
|
||||
return False
|
||||
mode = stat.S_IMODE(metadata.st_mode)
|
||||
if mode & 0o002:
|
||||
return False
|
||||
if resolved.name == "grok" and not (mode & 0o020):
|
||||
return True
|
||||
if not _is_under_real_grok_home(resolved):
|
||||
return False
|
||||
return metadata.st_uid == os.getuid() and metadata.st_gid == os.getgid()
|
||||
|
||||
|
||||
def resolve_executable(name: str) -> Path:
|
||||
supported = {"codex", "cursor-agent", "git", "orca"}
|
||||
supported = {"codex", "cursor-agent", "grok", "git", "orca"}
|
||||
if name not in supported:
|
||||
raise LaunchError(f"不支持的可执行文件: {name}")
|
||||
search_paths = trusted_path_entries()
|
||||
@@ -289,6 +330,10 @@ def resolve_executable(name: str) -> Path:
|
||||
continue
|
||||
if metadata.st_uid not in {0, os.getuid()}:
|
||||
continue
|
||||
if name == "grok":
|
||||
if _is_trusted_grok_executable(resolved, metadata):
|
||||
return resolved
|
||||
continue
|
||||
if stat.S_IMODE(metadata.st_mode) & 0o022:
|
||||
continue
|
||||
if resolved.name != name:
|
||||
@@ -697,7 +742,10 @@ def build_plan(
|
||||
"slot": slot,
|
||||
}
|
||||
)
|
||||
cli_label = "CODEX" if profile["cli"] == "codex" else "CURSOR"
|
||||
try:
|
||||
cli_label = CLI_TITLE_LABELS[str(profile["cli"])]
|
||||
except KeyError as exc:
|
||||
raise LaunchError(f"不支持的 worker CLI: {profile['cli']}") from exc
|
||||
role_label = "DEV" if role == "developer" else "TEST"
|
||||
digest_short = launch_fingerprint.split(":", 1)[-1][:10]
|
||||
title = (
|
||||
|
||||
@@ -43,6 +43,7 @@ TOP_LEVEL_FIELDS = {
|
||||
"project",
|
||||
"enabled",
|
||||
"defaultProfile",
|
||||
"intents",
|
||||
"entrypoints",
|
||||
"artifacts",
|
||||
"destinations",
|
||||
@@ -99,6 +100,11 @@ CHANNELS = {"preview", "staging", "stable"}
|
||||
ENVIRONMENT_TYPES = {"ssh-host", "docker-compose", "kubernetes", "custom"}
|
||||
CLASSIFICATIONS = {"development", "staging", "production"}
|
||||
STOP_POINTS = {"verified", "validation_ready", "review_ready", "released"}
|
||||
INTENT_FIELDS = {"testEnvironment", "release"}
|
||||
INTENT_STOP_AT = {
|
||||
"testEnvironment": "validation_ready",
|
||||
"release": "released",
|
||||
}
|
||||
ACTIONS = {
|
||||
"verify",
|
||||
"pull-request",
|
||||
@@ -675,6 +681,38 @@ def _validate_profiles(
|
||||
errors.append(f"{where}: defaultProfile 不能部署 production 环境")
|
||||
|
||||
|
||||
def _validate_intents(
|
||||
values: Any,
|
||||
profiles: dict[str, Any],
|
||||
errors: list[str],
|
||||
) -> None:
|
||||
if values is None:
|
||||
return
|
||||
if not _mapping(values):
|
||||
errors.append("intents: 必须是对象")
|
||||
return
|
||||
_reject_unknown(values, INTENT_FIELDS, "intents", errors)
|
||||
for field in sorted(INTENT_FIELDS):
|
||||
if field not in values:
|
||||
errors.append(f"intents.{field}: 必填")
|
||||
continue
|
||||
profile_id = values[field]
|
||||
if profile_id is None:
|
||||
continue
|
||||
if not isinstance(profile_id, str) or ID_RE.fullmatch(profile_id) is None:
|
||||
errors.append(f"intents.{field}: 必须是 null 或小写连字符 profile ID")
|
||||
continue
|
||||
profile = profiles.get(profile_id)
|
||||
if profile is None:
|
||||
errors.append(f"intents.{field}: 未定义 profile {profile_id!r}")
|
||||
continue
|
||||
expected_stop = INTENT_STOP_AT[field]
|
||||
if _mapping(profile) and profile.get("stopAt") != expected_stop:
|
||||
errors.append(
|
||||
f"intents.{field}: profile {profile_id!r} 必须 stopAt {expected_stop}"
|
||||
)
|
||||
|
||||
|
||||
def validate_builtin(data: dict[str, Any], project_root: Path | None = None) -> list[str]:
|
||||
errors: list[str] = []
|
||||
_reject_unknown(data, TOP_LEVEL_FIELDS, "<root>", errors)
|
||||
@@ -720,6 +758,7 @@ def validate_builtin(data: dict[str, Any], project_root: Path | None = None) ->
|
||||
environments=environments,
|
||||
errors=errors,
|
||||
)
|
||||
_validate_intents(data.get("intents"), profiles, errors)
|
||||
|
||||
if enabled:
|
||||
if default_profile not in profiles:
|
||||
|
||||
@@ -103,6 +103,8 @@ DELIVERY_RUN_FIELDS = {
|
||||
"evidence",
|
||||
"updatedAt",
|
||||
}
|
||||
DELIVERY_RUN_OPTIONAL_FIELDS = {"intent"}
|
||||
DELIVERY_RUN_INTENTS = {"testEnvironment", "release"}
|
||||
DELIVERY_STATUSES = {
|
||||
"planned",
|
||||
"running",
|
||||
@@ -481,7 +483,9 @@ def validate_delivery_runs(
|
||||
if not isinstance(run, dict):
|
||||
errors.append(f"{where}: 必须是对象")
|
||||
continue
|
||||
reject_unknown_fields(run, DELIVERY_RUN_FIELDS, where, errors)
|
||||
reject_unknown_fields(
|
||||
run, DELIVERY_RUN_FIELDS | DELIVERY_RUN_OPTIONAL_FIELDS, where, errors
|
||||
)
|
||||
missing = sorted(DELIVERY_RUN_FIELDS - set(run))
|
||||
for field in missing:
|
||||
errors.append(f"{where}.{field}: 必填")
|
||||
@@ -501,13 +505,23 @@ def validate_delivery_runs(
|
||||
if status not in DELIVERY_STATUSES:
|
||||
errors.append(f"{where}.status: 必须是 {sorted(DELIVERY_STATUSES)}")
|
||||
|
||||
intent = run.get("intent")
|
||||
if "intent" in run and intent not in DELIVERY_RUN_INTENTS:
|
||||
errors.append(
|
||||
f"{where}.intent: 必须是 {sorted(DELIVERY_RUN_INTENTS)}"
|
||||
)
|
||||
task_ids = run.get("taskIds")
|
||||
allow_empty_tasks = intent in DELIVERY_RUN_INTENTS
|
||||
if (
|
||||
not isinstance(task_ids, list)
|
||||
or not task_ids
|
||||
or any(not _nonempty_string(task_id) for task_id in task_ids)
|
||||
or (not task_ids and not allow_empty_tasks)
|
||||
or any(not _nonempty_string(task_id) for task_id in (task_ids or []))
|
||||
):
|
||||
errors.append(f"{where}.taskIds: 必须是非空任务 ID 列表")
|
||||
errors.append(
|
||||
f"{where}.taskIds: 必须是任务 ID 列表"
|
||||
if allow_empty_tasks
|
||||
else f"{where}.taskIds: 必须是非空任务 ID 列表"
|
||||
)
|
||||
task_ids = []
|
||||
elif len(task_ids) != len(set(task_ids)):
|
||||
errors.append(f"{where}.taskIds: 不能包含重复值")
|
||||
|
||||
@@ -24,12 +24,27 @@ LAUNCH_PROTOCOL_VERSION = 1
|
||||
MAX_ROUNDS = 3
|
||||
|
||||
ROLES = frozenset({"developer", "test"})
|
||||
CLIS = frozenset({"codex", "cursor-agent"})
|
||||
CLIS = frozenset({"codex", "cursor-agent", "grok"})
|
||||
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"})
|
||||
CLI_REQUIRES_REASONING_EFFORT = frozenset({"codex", "grok"})
|
||||
CLI_REQUIRES_NULL_REASONING_EFFORT = frozenset({"cursor-agent"})
|
||||
|
||||
|
||||
def _cli_choice_text() -> str:
|
||||
return "/".join(sorted(CLIS))
|
||||
|
||||
|
||||
def executable_basename_matches_cli(executable: str, cli: str) -> bool:
|
||||
"""Return whether a resolved executable basename is valid for ``cli``."""
|
||||
|
||||
name = Path(executable).name
|
||||
if name == cli:
|
||||
return True
|
||||
return cli == "grok" and GROK_EXECUTABLE_NAME_RE.fullmatch(name) is not None
|
||||
|
||||
ORCHESTRATION_FIELDS = frozenset(
|
||||
{
|
||||
@@ -114,6 +129,9 @@ ROLE_DISPATCH_FIELDS = frozenset(
|
||||
|
||||
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}$")
|
||||
GROK_EXECUTABLE_NAME_RE = re.compile(
|
||||
r"^grok(?:-(?:linux|darwin|windows)-(?:x86_64|aarch64|arm64))?$"
|
||||
)
|
||||
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}$")
|
||||
@@ -214,7 +232,7 @@ def validate_profile(profile: Any, *, where: str = "profile") -> list[str]:
|
||||
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")
|
||||
errors.append(f"{where}.cli: must be {_cli_choice_text()}")
|
||||
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:
|
||||
@@ -223,12 +241,17 @@ def validate_profile(profile: Any, *, where: str = "profile") -> list[str]:
|
||||
errors.append(
|
||||
f"{where}.permissionMode: must be read-only/workspace-write"
|
||||
)
|
||||
if cli == "codex":
|
||||
if isinstance(cli, str) and cli in CLI_REQUIRES_REASONING_EFFORT:
|
||||
if not isinstance(effort, str) or effort not in REASONING_EFFORTS:
|
||||
label = "Codex" if cli == "codex" else "Grok"
|
||||
errors.append(
|
||||
f"{where}.reasoningEffort: Codex requires low/medium/high/xhigh"
|
||||
f"{where}.reasoningEffort: {label} requires low/medium/high/xhigh"
|
||||
)
|
||||
elif cli == "cursor-agent" and effort is not None:
|
||||
elif (
|
||||
isinstance(cli, str)
|
||||
and cli in CLI_REQUIRES_NULL_REASONING_EFFORT
|
||||
and effort is not None
|
||||
):
|
||||
errors.append(f"{where}.reasoningEffort: Cursor requires null")
|
||||
|
||||
if role == "test" and tier != "standard":
|
||||
@@ -446,14 +469,15 @@ def render_worker_argv(
|
||||
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"]:
|
||||
if not executable_basename_matches_cli(executable, 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":
|
||||
cli = profile["cli"]
|
||||
if cli == "codex":
|
||||
return [
|
||||
executable,
|
||||
"--strict-config",
|
||||
@@ -469,6 +493,7 @@ def render_worker_argv(
|
||||
worktree,
|
||||
]
|
||||
|
||||
if cli == "cursor-agent":
|
||||
argv = [executable, "--model", model]
|
||||
if permission == "read-only":
|
||||
argv.extend(["--mode", "plan"])
|
||||
@@ -477,6 +502,25 @@ def render_worker_argv(
|
||||
argv.extend(["--sandbox", "enabled", "--workspace", worktree])
|
||||
return argv
|
||||
|
||||
if cli != "grok":
|
||||
raise ValueError(f"unsupported cli: {cli}")
|
||||
grok_permission = "plan" if permission == "read-only" else "acceptEdits"
|
||||
grok_sandbox = "read-only" if permission == "read-only" else "workspace"
|
||||
return [
|
||||
executable,
|
||||
"--model",
|
||||
model,
|
||||
"--reasoning-effort",
|
||||
profile["reasoningEffort"],
|
||||
"--permission-mode",
|
||||
grok_permission,
|
||||
"--always-approve",
|
||||
"--sandbox",
|
||||
grok_sandbox,
|
||||
"--cwd",
|
||||
worktree,
|
||||
]
|
||||
|
||||
|
||||
def receipt_hash(receipt: dict[str, Any]) -> str:
|
||||
"""Hash every receipt field except the self-referential ``receiptHash``."""
|
||||
@@ -532,25 +576,34 @@ def _validate_requested(value: Any, where: str) -> list[str]:
|
||||
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")
|
||||
errors.append(f"{where}.cli: must be {_cli_choice_text()}")
|
||||
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 (
|
||||
if isinstance(cli, str) and cli in CLI_REQUIRES_REASONING_EFFORT 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:
|
||||
label = "Codex" if cli == "codex" else "Grok"
|
||||
errors.append(f"{where}.reasoningEffort: invalid {label} effort")
|
||||
if (
|
||||
isinstance(cli, str)
|
||||
and cli in CLI_REQUIRES_NULL_REASONING_EFFORT
|
||||
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:
|
||||
elif (
|
||||
isinstance(cli, str)
|
||||
and cli in CLIS
|
||||
and not executable_basename_matches_cli(str(executable), 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")
|
||||
@@ -995,6 +1048,7 @@ __all__ = [
|
||||
"profile_hash",
|
||||
"receipt_hash",
|
||||
"render_worker_argv",
|
||||
"executable_basename_matches_cli",
|
||||
"validate_profile",
|
||||
"validate_orchestration",
|
||||
"validate_worker_receipt",
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"type": ["string", "null"],
|
||||
"pattern": "^[a-z][a-z0-9-]{0,63}$"
|
||||
},
|
||||
"intents": { "$ref": "#/definitions/intents" },
|
||||
"entrypoints": {
|
||||
"type": "object",
|
||||
"propertyNames": { "$ref": "#/definitions/id" },
|
||||
@@ -63,6 +64,21 @@
|
||||
"type": "string",
|
||||
"pattern": "^[a-z][a-z0-9-]{0,63}$"
|
||||
},
|
||||
"intents": {
|
||||
"type": "object",
|
||||
"required": ["testEnvironment", "release"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"testEnvironment": {
|
||||
"type": ["string", "null"],
|
||||
"pattern": "^[a-z][a-z0-9-]{0,63}$"
|
||||
},
|
||||
"release": {
|
||||
"type": ["string", "null"],
|
||||
"pattern": "^[a-z][a-z0-9-]{0,63}$"
|
||||
}
|
||||
}
|
||||
},
|
||||
"relativePath": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
|
||||
@@ -7,6 +7,11 @@ project:
|
||||
enabled: false
|
||||
defaultProfile: null
|
||||
|
||||
# 测试环境部署和版本发布都写在本文件。null 表示用户尚未说明该操作。
|
||||
intents:
|
||||
testEnvironment: null
|
||||
release: null
|
||||
|
||||
# 所有可执行入口都使用结构化 argv 语义;requiredSecrets 只保存名称,不保存值。
|
||||
# 不在此保存 shell、env 或凭据正文。
|
||||
entrypoints: {}
|
||||
|
||||
@@ -48,7 +48,7 @@ receipt 全部以 `docs/ack/tasks.yaml` 的 `project.orchestration` 与顶层
|
||||
| Test | `codex-test-standard` | standard |
|
||||
| Developer 升级 | `codex-dev-strong` | strong |
|
||||
|
||||
项目如改用 Cursor,应修改结构化 profile、allowlist 和 defaults,再运行任务板
|
||||
项目如改用 Cursor 或 Grok,应修改结构化 profile、allowlist 和 defaults,再运行任务板
|
||||
校验;不能在这里粘贴 `orca --command`、Agent CLI 参数或自由 shell。
|
||||
每次自动派发必须先审阅 launcher plan,再把其中的 `launchFingerprint` 作为
|
||||
`--expected-launch-fingerprint` 启动 fresh worker。`receiptHash` 只作审计 checksum,
|
||||
@@ -79,7 +79,7 @@ Developer 白盒验证:
|
||||
<local_run_command>
|
||||
```
|
||||
|
||||
Test 黑盒复测:
|
||||
Test 黑盒复测(服务启动以 `delivery.yaml` 的 `intents.testEnvironment` 为准):
|
||||
|
||||
```bash
|
||||
<preflight_command>
|
||||
@@ -97,7 +97,8 @@ Skill 的 `scripts/run_verification.py` 执行,不直接拼接 path/args。检
|
||||
|
||||
项目状态校验由 `/ack` 使用 Skill 自带的 `scripts/validate_tasks.py`、
|
||||
`scripts/validate_knowledge.py` 和 `scripts/validate_delivery.py` 执行。
|
||||
构建/发布/部署的机器入口以 `delivery.yaml` 为准;本文件不维护第二套交付命令。
|
||||
构建、测试环境部署和版本发布的机器入口以 `delivery.yaml` 的 `intents` 为准;
|
||||
本文件不维护第二套交付命令。
|
||||
|
||||
## 硬规则(其余见 references/)
|
||||
|
||||
@@ -118,8 +119,9 @@ Skill 的 `scripts/run_verification.py` 执行,不直接拼接 path/args。检
|
||||
- Developer 回报 `knowledgeApplied` 与 `knowledgeCandidates`,Test 回报
|
||||
`knowledgeChecks`。关键约束应下沉为测试、lint、CI 或正式规范。
|
||||
- ACK 不自动修改 `AGENTS.md`、`CLAUDE.md` 或其它 Agent 指令文件。
|
||||
- `delivery.yaml` 默认关闭,只描述能力,不自动授权提交、推送、发布或部署;交付仅在
|
||||
任务 `verified` 且本次 profile/目标/停止点得到确认后运行。
|
||||
- `delivery.yaml` 默认关闭,只描述能力,不自动授权提交、推送、发布或部署。测试环境
|
||||
与发版都写在这份契约的 `intents` 里;用户明确要求重新部署测试环境或发布版本时
|
||||
才执行对应 intent。常规交付仍在任务 `verified` 且本次 profile 得到确认后运行。
|
||||
- 默认交付 profile 最多到 `validation_ready` 或 `review_ready`;stable 发布或 production 部署必须有
|
||||
approval 步骤并再次获得明确批准。配置变更只影响下一次 run。
|
||||
- 每个任务最多派发 3 轮,仍不过标记 `leftover` 并继续下一个。
|
||||
|
||||
@@ -338,7 +338,8 @@
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"codex": { "$ref": "#/definitions/modelRoleAllowlist" },
|
||||
"cursor-agent": { "$ref": "#/definitions/modelRoleAllowlist" }
|
||||
"cursor-agent": { "$ref": "#/definitions/modelRoleAllowlist" },
|
||||
"grok": { "$ref": "#/definitions/modelRoleAllowlist" }
|
||||
}
|
||||
},
|
||||
"workerProfile": {
|
||||
@@ -359,7 +360,7 @@
|
||||
},
|
||||
"cli": {
|
||||
"type": "string",
|
||||
"enum": ["codex", "cursor-agent"]
|
||||
"enum": ["codex", "cursor-agent", "grok"]
|
||||
},
|
||||
"tier": {
|
||||
"type": "string",
|
||||
@@ -517,7 +518,7 @@
|
||||
"properties": {
|
||||
"cli": {
|
||||
"type": "string",
|
||||
"enum": ["codex", "cursor-agent"]
|
||||
"enum": ["codex", "cursor-agent", "grok"]
|
||||
},
|
||||
"tier": {
|
||||
"type": "string",
|
||||
@@ -989,9 +990,14 @@
|
||||
"type": "string",
|
||||
"pattern": "^[a-z][a-z0-9-]{0,63}$"
|
||||
},
|
||||
"intent": {
|
||||
"type": "string",
|
||||
"enum": ["testEnvironment", "release"],
|
||||
"description": "用户触发的测试环境或发版操作;有 intent 时 taskIds 可为 []"
|
||||
},
|
||||
"taskIds": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"minItems": 0,
|
||||
"uniqueItems": true,
|
||||
"items": { "type": "string", "minLength": 1 }
|
||||
},
|
||||
|
||||
@@ -44,6 +44,12 @@ project:
|
||||
standard: ["auto"]
|
||||
test:
|
||||
standard: ["auto"]
|
||||
grok:
|
||||
developer:
|
||||
standard: ["grok-4.5"]
|
||||
strong: ["grok-4.6"]
|
||||
test:
|
||||
standard: ["grok-4.5"]
|
||||
profiles:
|
||||
codex-dev-standard:
|
||||
role: "developer"
|
||||
@@ -80,6 +86,27 @@ project:
|
||||
model: "auto"
|
||||
reasoningEffort: null
|
||||
permissionMode: "workspace-write"
|
||||
grok-dev-standard:
|
||||
role: "developer"
|
||||
cli: "grok"
|
||||
tier: "standard"
|
||||
model: "grok-4.5"
|
||||
reasoningEffort: "medium"
|
||||
permissionMode: "workspace-write"
|
||||
grok-test-standard:
|
||||
role: "test"
|
||||
cli: "grok"
|
||||
tier: "standard"
|
||||
model: "grok-4.5"
|
||||
reasoningEffort: "low"
|
||||
permissionMode: "workspace-write"
|
||||
grok-dev-strong:
|
||||
role: "developer"
|
||||
cli: "grok"
|
||||
tier: "strong"
|
||||
model: "grok-4.6"
|
||||
reasoningEffort: "high"
|
||||
permissionMode: "workspace-write"
|
||||
defaults:
|
||||
developer: "codex-dev-standard"
|
||||
test: "codex-test-standard"
|
||||
|
||||
@@ -303,6 +303,7 @@ class AckDeliveryValidationTests(unittest.TestCase):
|
||||
{
|
||||
"enabled": False,
|
||||
"defaultProfile": None,
|
||||
"intents": {"testEnvironment": None, "release": None},
|
||||
"entrypoints": {},
|
||||
"artifacts": {},
|
||||
"destinations": {},
|
||||
@@ -313,6 +314,42 @@ class AckDeliveryValidationTests(unittest.TestCase):
|
||||
|
||||
self.assertEqual(validate_delivery.validate_builtin(contract), [])
|
||||
|
||||
def test_intents_must_point_at_matching_stop_points(self) -> None:
|
||||
contract = valid_contract()
|
||||
contract["intents"] = {
|
||||
"testEnvironment": "review",
|
||||
"release": None,
|
||||
}
|
||||
|
||||
errors = validate_delivery.validate_builtin(contract)
|
||||
self.assertTrue(
|
||||
any("intents.testEnvironment" in item and "validation_ready" in item for item in errors)
|
||||
)
|
||||
|
||||
contract["intents"]["testEnvironment"] = "local-validation"
|
||||
contract["profiles"]["local-validation"] = {
|
||||
"stopAt": "validation_ready",
|
||||
"steps": [
|
||||
{"id": "build-local", "action": "build", "artifact": "service-deb"},
|
||||
{
|
||||
"id": "deploy-local",
|
||||
"action": "deploy",
|
||||
"artifact": "service-deb",
|
||||
"environment": "test-server",
|
||||
},
|
||||
{
|
||||
"id": "health-local",
|
||||
"action": "health-check",
|
||||
"environment": "test-server",
|
||||
},
|
||||
],
|
||||
}
|
||||
self.assertEqual(validate_delivery.validate_builtin(contract), [])
|
||||
|
||||
contract["intents"]["release"] = "missing-release"
|
||||
errors = validate_delivery.validate_builtin(contract)
|
||||
self.assertTrue(any("未定义 profile 'missing-release'" in item for item in errors))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -324,6 +324,9 @@ class EnvironmentAndExecutableTests(unittest.TestCase):
|
||||
"OPENAI_API_KEY": "test-token",
|
||||
"AZURE_OPENAI_API_KEY": "azure-token",
|
||||
"CURSOR_API_KEY": "cursor-token",
|
||||
"XAI_API_KEY": "xai-token",
|
||||
"GROK_HOME": "/tmp/evil-grok",
|
||||
"GROK_SANDBOX": "off",
|
||||
"ANTHROPIC_API_KEY": "anthropic-token",
|
||||
"DBUS_SESSION_BUS_ADDRESS": "unix:path=/tmp/dbus",
|
||||
"DISPLAY": ":99",
|
||||
@@ -336,17 +339,24 @@ class EnvironmentAndExecutableTests(unittest.TestCase):
|
||||
control = launch_worker.control_environment()
|
||||
codex = launch_worker.worker_environment("codex")
|
||||
cursor = launch_worker.worker_environment("cursor-agent")
|
||||
grok = launch_worker.worker_environment("grok")
|
||||
|
||||
for environment in (control, codex, cursor):
|
||||
for environment in (control, codex, cursor, grok):
|
||||
self.assertNotIn(hostile_path, environment["PATH"].split(os.pathsep))
|
||||
self.assertEqual(environment["LANG"], "C.UTF-8")
|
||||
self.assertNotIn("OPENAI_API_KEY", control)
|
||||
self.assertNotIn("CURSOR_API_KEY", control)
|
||||
self.assertNotIn("XAI_API_KEY", control)
|
||||
self.assertEqual(codex["OPENAI_API_KEY"], "test-token")
|
||||
self.assertEqual(codex["AZURE_OPENAI_API_KEY"], "azure-token")
|
||||
self.assertNotIn("CURSOR_API_KEY", codex)
|
||||
self.assertNotIn("XAI_API_KEY", codex)
|
||||
self.assertEqual(cursor["CURSOR_API_KEY"], "cursor-token")
|
||||
self.assertNotIn("OPENAI_API_KEY", cursor)
|
||||
self.assertNotIn("XAI_API_KEY", cursor)
|
||||
self.assertEqual(grok["XAI_API_KEY"], "xai-token")
|
||||
self.assertNotIn("OPENAI_API_KEY", grok)
|
||||
self.assertNotIn("CURSOR_API_KEY", grok)
|
||||
for forbidden in (
|
||||
"ANTHROPIC_API_KEY",
|
||||
"DBUS_SESSION_BUS_ADDRESS",
|
||||
@@ -356,11 +366,13 @@ class EnvironmentAndExecutableTests(unittest.TestCase):
|
||||
"NODE_OPTIONS",
|
||||
"PYTHONPATH",
|
||||
"CODEX_HOME",
|
||||
"GROK_HOME",
|
||||
"GROK_SANDBOX",
|
||||
"GIT_SSH_COMMAND",
|
||||
"WAYLAND_DISPLAY",
|
||||
"XDG_RUNTIME_DIR",
|
||||
):
|
||||
for environment in (control, codex, cursor):
|
||||
for environment in (control, codex, cursor, grok):
|
||||
self.assertNotIn(forbidden, environment)
|
||||
|
||||
def test_executable_resolution_ignores_hostile_path(self) -> None:
|
||||
@@ -373,6 +385,51 @@ class EnvironmentAndExecutableTests(unittest.TestCase):
|
||||
|
||||
self.assertEqual(resolved, Path("/usr/bin/git"))
|
||||
|
||||
def test_grok_vendor_layout_is_trusted_and_outside_artifacts_are_not(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
home = Path(temporary) / "home"
|
||||
downloads = home / ".grok" / "downloads"
|
||||
downloads.mkdir(parents=True)
|
||||
artifact = downloads / "grok-linux-x86_64"
|
||||
artifact.write_text("#!/bin/sh\n", encoding="utf-8")
|
||||
artifact.chmod(0o775)
|
||||
bindir = home / ".local" / "bin"
|
||||
bindir.mkdir(parents=True)
|
||||
(bindir / "grok").symlink_to(artifact)
|
||||
|
||||
hostile_dir = Path(temporary) / "tmp"
|
||||
hostile_dir.mkdir()
|
||||
hostile = hostile_dir / "grok-linux-x86_64"
|
||||
hostile.write_text("#!/bin/sh\n", encoding="utf-8")
|
||||
hostile.chmod(0o775)
|
||||
hostile_bin = Path(temporary) / "hostile-bin"
|
||||
hostile_bin.mkdir()
|
||||
(hostile_bin / "grok").symlink_to(hostile)
|
||||
|
||||
with mock.patch.object(
|
||||
launch_worker,
|
||||
"account_identity",
|
||||
return_value=(home.resolve(strict=True), "ace"),
|
||||
), mock.patch.object(
|
||||
launch_worker,
|
||||
"trusted_path_entries",
|
||||
return_value=[bindir.resolve(strict=True)],
|
||||
):
|
||||
resolved = launch_worker.resolve_executable("grok")
|
||||
self.assertEqual(resolved, artifact.resolve(strict=True))
|
||||
|
||||
with mock.patch.object(
|
||||
launch_worker,
|
||||
"account_identity",
|
||||
return_value=(home.resolve(strict=True), "ace"),
|
||||
), mock.patch.object(
|
||||
launch_worker,
|
||||
"trusted_path_entries",
|
||||
return_value=[hostile_bin.resolve(strict=True)],
|
||||
):
|
||||
with self.assertRaises(launch_worker.LaunchError):
|
||||
launch_worker.resolve_executable("grok")
|
||||
|
||||
|
||||
class PlanTests(unittest.TestCase):
|
||||
def test_authoritative_board_is_derived_from_project_root_without_repo_path(self) -> None:
|
||||
|
||||
@@ -27,6 +27,9 @@ class AckSkillContentTests(unittest.TestCase):
|
||||
"references/kickoff.md",
|
||||
"不要修改项目的 `AGENTS.md`",
|
||||
"当前会话担任 Coordinator",
|
||||
"intents.testEnvironment",
|
||||
"运行测试环境",
|
||||
"运行版本发布",
|
||||
):
|
||||
self.assertIn(expected, content)
|
||||
|
||||
@@ -128,7 +131,7 @@ class AckSkillContentTests(unittest.TestCase):
|
||||
):
|
||||
self.assertTrue((ack_dir / relative_path).is_file(), relative_path)
|
||||
version = (ack_dir / "VERSION").read_text(encoding="utf-8").strip()
|
||||
self.assertEqual(version, "0.15.0")
|
||||
self.assertEqual(version, "0.17.1")
|
||||
self.assertIn(
|
||||
f'ackVersion: "{version}"',
|
||||
(ack_dir / "examples" / "tasks.example.yaml").read_text(encoding="utf-8"),
|
||||
|
||||
@@ -438,6 +438,47 @@ class AckTaskValidationTests(unittest.TestCase):
|
||||
|
||||
self.assert_board_accepted_in_all_modes(board)
|
||||
|
||||
def test_intent_delivery_run_allows_empty_task_ids(self) -> None:
|
||||
board = valid_manual_routing_board()
|
||||
board["project"]["deliveryFile"] = "docs/ack/delivery.yaml"
|
||||
board["deliveryRuns"] = [
|
||||
{
|
||||
"id": "DR-test-env-1",
|
||||
"profile": "test-local",
|
||||
"intent": "testEnvironment",
|
||||
"taskIds": [],
|
||||
"status": "validation_ready",
|
||||
"sourceRevision": "a" * 40,
|
||||
"configRevision": "b" * 40,
|
||||
"pullRequest": None,
|
||||
"artifacts": [
|
||||
{
|
||||
"id": "garden-bin",
|
||||
"type": "file",
|
||||
"reference": "garden",
|
||||
"digest": "sha256:" + "c" * 64,
|
||||
}
|
||||
],
|
||||
"deployments": [
|
||||
{
|
||||
"environment": "local-write",
|
||||
"result": "succeeded",
|
||||
"evidence": "GET /login returned 200",
|
||||
}
|
||||
],
|
||||
"evidence": ["http://write.localhost:8080/ ready"],
|
||||
"updatedAt": "2026-08-23T00:57:00+08:00",
|
||||
}
|
||||
]
|
||||
|
||||
self.assert_board_accepted_in_all_modes(board)
|
||||
|
||||
del board["deliveryRuns"][0]["intent"]
|
||||
self.assert_board_rejected_in_all_modes(
|
||||
board,
|
||||
"taskIds: 必须是非空任务 ID 列表",
|
||||
)
|
||||
|
||||
def test_delivery_runs_and_delivery_file_must_appear_together(self) -> None:
|
||||
board = valid_manual_routing_board()
|
||||
board["deliveryRuns"] = []
|
||||
|
||||
@@ -30,6 +30,13 @@ def valid_orchestration() -> dict:
|
||||
"developer": {"standard": ["cursor-auto"]},
|
||||
"test": {"standard": ["cursor-auto"]},
|
||||
},
|
||||
"grok": {
|
||||
"developer": {
|
||||
"standard": ["grok-4.5"],
|
||||
"strong": ["grok-4.6"],
|
||||
},
|
||||
"test": {"standard": ["grok-4.5"]},
|
||||
},
|
||||
},
|
||||
"profiles": {
|
||||
"codex-dev-standard": {
|
||||
@@ -64,6 +71,30 @@ def valid_orchestration() -> dict:
|
||||
"reasoningEffort": None,
|
||||
"permissionMode": "workspace-write",
|
||||
},
|
||||
"grok-dev-standard": {
|
||||
"role": "developer",
|
||||
"cli": "grok",
|
||||
"tier": "standard",
|
||||
"model": "grok-4.5",
|
||||
"reasoningEffort": "medium",
|
||||
"permissionMode": "workspace-write",
|
||||
},
|
||||
"grok-dev-strong": {
|
||||
"role": "developer",
|
||||
"cli": "grok",
|
||||
"tier": "strong",
|
||||
"model": "grok-4.6",
|
||||
"reasoningEffort": "high",
|
||||
"permissionMode": "workspace-write",
|
||||
},
|
||||
"grok-test-readonly": {
|
||||
"role": "test",
|
||||
"cli": "grok",
|
||||
"tier": "standard",
|
||||
"model": "grok-4.5",
|
||||
"reasoningEffort": "low",
|
||||
"permissionMode": "read-only",
|
||||
},
|
||||
},
|
||||
"defaults": {
|
||||
"developer": "codex-dev-standard",
|
||||
@@ -278,12 +309,16 @@ class ProfileValidationTests(unittest.TestCase):
|
||||
codex["profiles"]["codex-dev-standard"]["reasoningEffort"] = None
|
||||
cursor = valid_orchestration()
|
||||
cursor["profiles"]["cursor-test-standard"]["reasoningEffort"] = "low"
|
||||
grok = valid_orchestration()
|
||||
grok["profiles"]["grok-dev-standard"]["reasoningEffort"] = None
|
||||
|
||||
codex_errors = worker_profiles.validate_orchestration(codex)
|
||||
cursor_errors = worker_profiles.validate_orchestration(cursor)
|
||||
grok_errors = worker_profiles.validate_orchestration(grok)
|
||||
|
||||
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))
|
||||
self.assertTrue(any("Grok requires" in error for error in grok_errors))
|
||||
|
||||
def test_test_cannot_use_strong_tier(self) -> None:
|
||||
routing = valid_orchestration()
|
||||
@@ -444,6 +479,86 @@ class ArgvRendererTests(unittest.TestCase):
|
||||
self.assertNotIn("--yolo", argv)
|
||||
self.assertNotIn("--force", argv)
|
||||
|
||||
def test_grok_workspace_write_exact_safe_argv(self) -> None:
|
||||
profile = valid_orchestration()["profiles"]["grok-dev-standard"]
|
||||
|
||||
argv = worker_profiles.render_worker_argv(
|
||||
profile,
|
||||
"/usr/local/bin/grok",
|
||||
"/repo/demo",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
argv,
|
||||
[
|
||||
"/usr/local/bin/grok",
|
||||
"--model",
|
||||
"grok-4.5",
|
||||
"--reasoning-effort",
|
||||
"medium",
|
||||
"--permission-mode",
|
||||
"acceptEdits",
|
||||
"--always-approve",
|
||||
"--sandbox",
|
||||
"workspace",
|
||||
"--cwd",
|
||||
"/repo/demo",
|
||||
],
|
||||
)
|
||||
for forbidden in (
|
||||
"--yolo",
|
||||
"bypassPermissions",
|
||||
"auto",
|
||||
"dontAsk",
|
||||
"off",
|
||||
):
|
||||
self.assertNotIn(forbidden, argv)
|
||||
|
||||
def test_grok_read_only_uses_plan_and_read_only_sandbox(self) -> None:
|
||||
profile = valid_orchestration()["profiles"]["grok-test-readonly"]
|
||||
|
||||
argv = worker_profiles.render_worker_argv(
|
||||
profile,
|
||||
"/usr/local/bin/grok",
|
||||
"/repo/demo",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
argv,
|
||||
[
|
||||
"/usr/local/bin/grok",
|
||||
"--model",
|
||||
"grok-4.5",
|
||||
"--reasoning-effort",
|
||||
"low",
|
||||
"--permission-mode",
|
||||
"plan",
|
||||
"--always-approve",
|
||||
"--sandbox",
|
||||
"read-only",
|
||||
"--cwd",
|
||||
"/repo/demo",
|
||||
],
|
||||
)
|
||||
self.assertNotIn("--yolo", argv)
|
||||
self.assertNotIn("bypassPermissions", argv)
|
||||
|
||||
def test_grok_accepts_vendor_artifact_basename(self) -> None:
|
||||
profile = valid_orchestration()["profiles"]["grok-dev-standard"]
|
||||
artifact = "/home/ace/.grok/downloads/grok-linux-x86_64"
|
||||
|
||||
argv = worker_profiles.render_worker_argv(profile, artifact, "/repo/demo")
|
||||
|
||||
self.assertEqual(argv[0], artifact)
|
||||
self.assertTrue(
|
||||
worker_profiles.executable_basename_matches_cli(artifact, "grok")
|
||||
)
|
||||
self.assertFalse(
|
||||
worker_profiles.executable_basename_matches_cli(
|
||||
artifact, "cursor-agent"
|
||||
)
|
||||
)
|
||||
|
||||
def test_renderer_rejects_wrong_executable_or_unsafe_worktree(self) -> None:
|
||||
profile = valid_orchestration()["profiles"]["codex-dev-standard"]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user