From 838667eca1d2435db3ad096e64f096138a701569 Mon Sep 17 00:00:00 2001 From: bboonnee Date: Fri, 31 Jul 2026 16:29:05 +0800 Subject: [PATCH] fix: Validator's disallowedTools was silently disabling Bash entirely --- agents/validator/AGENTS.md | 2 +- docs/sdd/05-实施约束与依赖.md | 7 +++++++ src/agent_profile.py | 26 ++++++++++++++++++-------- src/test_agent_profile.py | 13 ++++++++++++- 4 files changed, 38 insertions(+), 10 deletions(-) diff --git a/agents/validator/AGENTS.md b/agents/validator/AGENTS.md index 5cff6f4..2e1867d 100644 --- a/agents/validator/AGENTS.md +++ b/agents/validator/AGENTS.md @@ -41,7 +41,7 @@ ## 安全与停止条件 -- 不提交、推送、reset、清理或覆盖用户工作树(本 Agent 的 `disallowedTools` 已禁用 `Bash(git commit:*)`/`Bash(git push:*)`/`Bash(git reset:*)`/`Bash(git clean:*)`)。 +- 不提交、推送、reset、清理或覆盖用户工作树(**2026-07-31 订正**:本来想在 `disallowedTools` 里用 `Bash(git commit:*)` 这类按子命令限定的写法禁用,但实测发现 Claude Code 的 subagent frontmatter 里只要出现任意 `Bash(...)` 限定项,`Bash` 工具会被整体禁用,不是只挡住那个子命令——代价是 Validator 完全跑不了任何测试,比放行 `git commit` 的风险大得多。已经去掉这个失效的限定写法,现在这条约束只靠这句话本身,不是工具层强制的)。 - 不做生产写入、真实支付/消息、破坏性迁移或权限提升。 - 不把命令失败、0 tests、skip/xfail、flaky、超时或缺失依赖解释成通过。 - 无法可靠测试时写入真实阻塞证据(对应 `inconclusive_escalate` 分支,见 04-P),不猜测、不伪造成功。 diff --git a/docs/sdd/05-实施约束与依赖.md b/docs/sdd/05-实施约束与依赖.md index 5f8fb4f..0666fba 100644 --- a/docs/sdd/05-实施约束与依赖.md +++ b/docs/sdd/05-实施约束与依赖.md @@ -39,6 +39,13 @@ - **应对方式**:需要"某个 Org Agent 身份 + 结构化输出"同时成立时(如 `c_work_graph_plan`,见 03-C 3.4 节/U-011),不要依赖 `--json-schema`/`structured_output`,改为在 prompt 里明确要求模型把结果包在单个 ```` ```json ```` 围栏代码块里,再从 `result` 字段用正则提取解析,对"整段就是 JSON"和"围栏代码块"两种情况都做兜底解析(参考实现:`src/work_graph_plan.py` 的 `_extract_graph_manifest`) - 未在 Anthropic 官方文档里找到这条限制的说明,是靠实测发现的,不排除后续版本行为会变——如果这里升级过 Claude Code 版本,值得重新验证一次 +## 5.4.1.2 subagent frontmatter 的 `disallowedTools` 不支持按子命令限定(发现,2026-07-31,U-014 真实闭环跑出来的) + +- 实测确认:`.claude/agents/.md` frontmatter 里的 `disallowedTools`,只要出现任意 `Bash(某子命令:*)` 这种按子命令限定的写法(如 `Bash(git commit:*)`),`Bash` 工具会被**整体**禁用,不是只挡住那一条子命令——即使同一份 frontmatter 的 `tools:` 字段里明确写了 `Bash`。用一个只有 `tools:[Read,Bash]`、`disallowedTools:[Edit,Write]`(不含任何 `Bash(...)`)的最小测试 agent 验证过 Bash 能正常跑;一旦给 `disallowedTools` 加一条 `Bash(git commit:*)`,同一个 agent 立刻连 `echo` 都跑不了 +- `claude --help` 里 `--disallowedTools`/`--allowedTools` **CLI 参数**本身明确支持这种按子命令限定的写法(帮助文本示例就是 `Bash(git *) Edit`),但 subagent **frontmatter** 里的同名字段不支持同一种语法——是两个不同的解析路径,行为不一致,官方文档没有说明这个差异 +- **实际后果**:U-004 迁移 Validator 人设时,原本想用 `disallowedTools: [Edit, Write, Bash(git commit:*), Bash(git push:*), Bash(git reset:*), Bash(git clean:*)]` 把"不准改代码"和"不准动 git 历史"都在工具层锁死,结果是 Validator 真实调用时完全没有 Bash 权限,无法运行任何测试——这个问题一直存在到 U-019 之后才通过跑 U-014 完整真实闭环时暴露出来(Validator 给出 INCONCLUSIVE 判定时明确说"没有 Bash 工具")。已订正为 `disallowedTools: [Edit, Write]`(不再限定 Bash 子命令),"不准动 git 历史"这条约束现在只靠 AGENTS.md 里的文字指令,不是工具层强制——一个跑不了任何测试的 Validator 比一个理论上可能跑 `git commit` 的 Validator 风险大得多,两权相害取其轻 +- 同 §5.4.1.1:这条限制没在官方文档里找到说明,是实测发现的,不排除后续版本行为会变——升级 Claude Code 版本后值得重新验证 + ## 5.4.2 Claude Code plan 额度等待重试参数(架构文档 7.5 节决策,2026-07-29) - 退避轮询间隔(5→10→20→30 分钟,上限 30 分钟)、超时兜底阈值(session 8 小时、weekly 9 天)是 MVP 阶段的硬编码默认值,不在管理后台开放用户配置,和 `log_retention_config` 的"保留天数可配、清理频率不可配"是同一个 MVP 简化原则 diff --git a/src/agent_profile.py b/src/agent_profile.py index 55dd323..c1c9502 100644 --- a/src/agent_profile.py +++ b/src/agent_profile.py @@ -73,14 +73,24 @@ FRONTMATTER = { "model": "claude-sonnet-5", "skills": ["qa-task-validation", "unattended-agent-operations"], "tools": ["Read", "Bash", "Grep", "Glob"], - "disallowedTools": [ - "Edit", - "Write", - "Bash(git commit:*)", - "Bash(git push:*)", - "Bash(git reset:*)", - "Bash(git clean:*)", - ], + # 2026-07-31 fix, found for real running U-014's full closed loop: + # scoped Bash patterns here ("Bash(git commit:*)") don't just block + # that one subcommand -- they silently disable Bash *entirely* for + # the agent (confirmed by isolating it: a plain ["Edit","Write"] + # disallowedTools list leaves Bash working fine; adding any + # "Bash(...)" entry breaks it, even though the CLI's own + # --disallowedTools flag documents this exact scoped syntax as + # supported -- it just doesn't work the same way in subagent + # frontmatter). A Validator that can't run tests at all is a much + # worse outcome than the residual risk of it running `git commit`, + # so the git-mutation restriction now relies on the prose instruction + # already in the migrated AGENTS.md ("不提交、推送、reset、清理或覆盖用户 + # 工作树") instead of a tool-level block that doesn't actually work. + # ponytail: this is a real gap (no enforced ceiling on Bash git + # mutations, just an asked-nicely one) -- upgrade path is whatever + # Claude Code's correct scoped-disallowedTools frontmatter syntax + # turns out to be, if this gets fixed/documented upstream. + "disallowedTools": ["Edit", "Write"], }, } diff --git a/src/test_agent_profile.py b/src/test_agent_profile.py index a3c9f5e..270f3b2 100644 --- a/src/test_agent_profile.py +++ b/src/test_agent_profile.py @@ -29,7 +29,18 @@ class RenderAgentDefinitionTest(unittest.TestCase): self.assertIn("disallowedTools:", content) self.assertIn("- Edit", content) self.assertIn("- Write", content) - self.assertIn("Bash(git commit:*)", content) + + def test_validator_disallowed_tools_has_no_scoped_bash_pattern(self): + # 2026-07-31: confirmed for real that any "Bash(...)" entry in a + # subagent's disallowedTools silently disables Bash *entirely*, not + # just that pattern -- must never reappear in the actual frontmatter + # (see agent_profile.py's FRONTMATTER["validator"] comment for the + # full story; the migrated AGENTS.md body is allowed to *mention* + # "Bash(...)" in prose explaining why it's no longer used there). + content = agent_profile.render_agent_definition("validator") + frontmatter = content.split("---", 2)[1] + self.assertNotIn("Bash(", frontmatter) + self.assertIn("- Bash", frontmatter) def test_planner_and_coder_have_no_disallowed_tools(self): for org_node in ("planner", "coder"): -- libgit2 0.21.4