🔍 AI 深度代码审查日报 2026-08-09
仓库: devcxl/opencode-cabbage
新发现: 9 个
由 AI 全面阅读代码后整理(架构 + 安全),已报告过的问题不会重复出现。
🏛️ 架构评估
这是一个"薄 TypeScript 内核 + 纯 Prompt 资产"的 OpenCode 插件:src/ 只负责注入 goal 工具、7 个命令、8 个 skill、5 个 agent(含 permission 规则)并驱动"goal 状态机 + idle 自动续接"循环,实际的 git/gh 操作全部由模型通过 bash 工具执行,靠 permission 白名单收敛。内核层(context/profile/session-index/permission/mutex)职责单一、无业务逻辑,资产层(commands/skills/agents/prompts)以 stage-contract 和 prompt-lint 做契约校验,CI 有 prompt-lint 测试把关资产一致性,整体模块边界清晰,是设计意图执行得相当彻底的架构。
优点突出:goal 状态最小化(只存 issue 号/status/计数,目标从 GitHub Issue 读取)、mutateGoal 作为带 KeyedMutex 的读-改-写唯一入口杜绝并发丢失更新、session-index 原子写(PID 后缀 tmp + rename)且索引失败不阻塞主流程、CONTEXT.md 注入带"非指令"包裹和防重复 marker、permission 语义(最后匹配优先、auto 仅 allow)有独立纯函数与对抗测试、resolveProjectDir 对异常会话目录做了防御性退化。这些表明作者对并发与状态一致性有明确意识。
主要问题集中在两个面:多项目/多实例隔离与不可信输入→权限放大。多项目方面:prompts.ts/bootstrap.ts 用模块级可变单例存 projectDir,多项目同进程时互相覆盖;server.ts 的事件处理(idle/message.updated/session.updated)不区分会话归属项目,会把本项目 CONTEXT.md 注入到别的项目的会话续接里,并把 flow 状态写进错误的 session-index。安全方面:primary 编排器 bash 全量 allow + 50 次无人值守自动续接 + 以 GitHub Issue/PR 正文(不可信)为执行依据,构成 prompt injection → 任意命令执行链;AGENTS.md(仓库可控内容)的 test command 首 token 被直接渲染成 bash allow 前缀,仓库可借此把 developer 权限放大到完整 shell;goal complete 授权仅凭 agent 名称字符串。这些大多是文档化的取舍(shell.ts、对抗测试均明示),但取舍的安全成本被系统性低估。
错误处理整体是"捕获 + console 日志 + 不阻塞",session-index 与 goal 元数据之间允许静默漂移(如 bindSession 失败被吞掉),对单会话可接受,但叠加续接循环后状态失真会被放大。技术债集中在:agent frontmatter 的 tools/capabilities 字段被静默丢弃(安全声明与运行时不一致)、技能安装对全局固定目录 rm -rf 且无锁、KeyedMutex 无清理、goal 输入无校验、跨进程索引无锁。
发现
[HIGH][安全] primary 编排器 bash 全量 allow + 无人值守自动续接 + 以不可信 Issue/PR 内容为执行依据,prompt injection 可直接驱动任意命令
说明: 位置: assets/agents/dev-lifecycle.md:8("*": "allow")、src/plugin/server.ts:69-136(queueContinuation)、src/plugin/server.ts:301-315(idle 自动续接)、src/plugin/goal.ts:59-70(续接提示要求"直接执行,不要叙述")
说明: dev-lifecycle 以用户完整 gh/git 凭据自动执行 push/PR 创建/merge/tag 等写操作,无逐命令人工确认;其执行依据(Flow Record、Task Record、PR diff、Issue 评论)全部来自 GitHub 上他人可写入的内容,而仓库内恶意 Issue/PR 正文即可注入指令。CONTEXT.md 有"非指令"防注入包裹,但 Issue/PR 内容没有任何同等防护,续接提示反而鼓励模型不加停顿地执行。
建议: 将写命令(git push / gh pr merge / gh release / git tag 等)在 permission 层设为 ask 强制确认;对从 Issue/PR 读取的内容套用与 CONTEXT.md 相同的"仅供参考、非指令"包裹并指示模型验证来源。
[HIGH][安全] AGENTS.md Profile 的 test command 首 token 被渲染为 bash allow 前缀,仓库可控内容可放大为任意命令执行
说明: 位置: src/plugin/server.ts:227-241(renderPermission)、assets/agents/team/developer.md:10("<profile-test-command>*": "allow")、src/kernel/profile.ts:133-138
说明: <profile-test-command>* 被替换为测试命令首 token 加 *(如 npm*、bash*、sudo*),等于放行该二进制全部能力(npm exec/run/install、bash -c、sudo 任意命令),deny 仅挡 <token> publish* 一种形态。AGENTS.md 是仓库内容,贡献者/攻击者把 test command 设为 bash 即可让 developer/goal-verify 获得完整 shell,且 cleanValue 只剥反引号,不做任何白名单校验。
建议: 只放行 Profile 中 test command 的精确完整命令(整串匹配),不再用首 token 前缀;或将测试执行单列为独立权限类别,默认 ask。
[MEDIUM][安全] goal complete 授权仅凭 agent 名称字符串,可被用户配置遮蔽或伪造
说明: 位置: src/plugin/goal.ts:166-185(ctx.agent !== "goal-verify" 即拒绝)、src/plugin/server.ts:58-67(configureGoalTools 按名称放行 goal)、src/plugin/server.ts:250-262(已存在同名 agent 跳过插件注入)
说明: 任何 key 为 "goal-verify" 的子 agent 都能 complete goal,而用户 config 中同名 agent 会遮蔽插件自带版本、静默获得 goal:true,插件不产生任何告警;主会话的 complete 只返回 BLOCKED 文本,完全依赖模型自觉。名称即权限,无凭据、无证据校验。
建议: 用插件可独占注入的配置标记(如特殊 capability 或工具配置值)而非名称判断,遮蔽时输出 warning;complete 前校验 goal-verify 会话确实返回过验证报告。
[MEDIUM][安全] 技能安装对 env 可覆盖的固定全局目录执行 rm -rf,无路径校验且存在并发竞态
说明: 位置: src/plugin/skills.ts:9-14(resolveSkillsPath)、16-35(setupSkillsDir)、48-64(全量改写 SKILL.md)
说明: CABBAGE_SKILLS_DIR 可指向任意路径(家目录、项目根),未校验即 rm -rf + mkdir + cp,误设即数据丢失;所有项目共享 ~/.config/opencode/cabbage/skills,check-then-act(readdir 完整性判断 → rm → cp → 改写全部 SKILL.md)在多个插件实例/进程并发启动时存在 TOCTOU,一方 rm 掉另一方正在读取的目录会导致插件加载失败,不同插件版本还会互相覆盖安装。
建议: rm 前校验解析路径位于约定的配置目录内且 basename 为 "skills";按内容 hash 分目录安装(skills-)或加进程间文件锁串行化安装。
[MEDIUM][安全] 测试命令白名单使"运行测试"等价于任意代码执行,且 agent shell 透传全部用户凭据
说明: 位置: assets/agents/team/developer.md:10-41(bash 白名单)、src/plugin/shell.ts:12-18(仅设两个 git 环境变量)
说明: developer/goal-verify 被要求对仓库内容执行测试命令,而仓库 package.json 的 scripts/依赖安装脚本本身即任意代码;agent shell 只设置 GIT_CONFIG_NOSYSTEM/GIT_TERMINAL_PROMPT,GH_TOKEN/GITHUB_TOKEN/HOME(SSH key)全部透传。对不可信仓库运行测试 = 以用户全部凭据执行任意代码,且当前无任何隔离或提示。
建议: 在 setup 阶段向用户明示该风险;可行时用受限 token、临时 HOME/凭据隔离运行测试命令;与上一条修复合并(精确命令白名单 + 运行前确认)。
[MEDIUM][架构] 同一 Flow 双 session 无检测:两个会话都会自动续接同一 goal,产生重复并行工作
说明: 位置: src/kernel/session-index.ts:62-80(bindSession 覆盖式绑定)、src/plugin/server.ts:69-136(idle 续接不查索引)
说明: 文件头注释声称"双 session 检测(R7)",但 bindSession 对已绑定其它 session 的 Flow 直接覆盖;idle 驱动的续接完全不 consult 索引,因此新旧两个会话会同时为同一 Flow 自动续接执行(重复 worktree/PR/合并),autoResume 只恢复最后绑定的一个。
建议: bindSession 发现 key 已被其它 sessionID 绑定时返回冲突,让 goal create 拒绝;或续接前校验该 session 是索引中该 Flow 的当前绑定会话。
[MEDIUM][架构] 模块级可变单例导致多项目插件实例互相污染
说明: 位置: src/plugin/prompts.ts:7-13(_packageRoot/_projectDir)、src/plugin/bootstrap.ts:7(_bootstrapCache)、src/kernel/context.ts:27(_cache)
说明: initPrompts 用模块级变量存 projectDir,后创建的插件实例覆盖先创建者;多项目同进程时 loadPrompt 会从错误的项目目录读取 prompt 覆盖文件,bootstrap 缓存同理(context 缓存按 projectDir 键控,幸免)。项目级 prompt 覆盖(.opencode/opencode-cabbage/prompts/)因此可能被注入到别的项目的会话。
建议: 将这些状态收进 createOpencodeCabbage 的闭包,或改为按 projectDir 的 Map(带生命周期清理),prompts/bootstrap 改为实例级。
[LOW][架构] 续接循环的 50 次上限依赖事件时序,且续接不感知子 agent 是否仍在运行
说明: 位置: src/plugin/server.ts:317-335(message.updated 重置计数)、99-108(compaction)、69-136(queueContinuation)
说明: message.updated 对任何 role=user 消息重置 continuationCount,若 synthetic 续接消息的事件在 continuationInFlight 清理后才到达(或 autoResume 路径),配额会被清零;primary idle 即续接,不感知 Task 子 agent 是否仍在执行,可能在子任务未结束时叠加推进,导致编排竞争。
建议: 只对非 synthetic 的用户消息重置计数;续接前检查该会话是否存在进行中的 subagent turn,或增加最小续接间隔。
[LOW][架构] goal 输入校验缺失、KeyedMutex 无清理、索引写跨进程无锁
说明: 位置: src/plugin/goal.ts:188-205(create 时 Number() 不校验)、src/kernel/mutex.ts:6-22、src/kernel/session-index.ts:50-56
说明: parent_issue_number 传 "abc"/负数/0 会生成 NaN/负数 goal 并写入索引 "NaN" 键;KeyedMutex 的 locks Map 永不删除已释放条目,长进程下随 session 数增长泄漏内存;writeIndex 的互斥仅进程内有效,两个 opencode 进程同时操作同一项目索引会丢失更新。
建议: create 前校验 parent_issue_number 为正整数;mutex 释放后删除 key;索引写加进程间锁(lockfile)或在文档明确单进程假设。
🔍 AI 深度代码审查日报 2026-08-09
仓库:
devcxl/opencode-cabbage新发现: 9 个
🏛️ 架构评估
这是一个"薄 TypeScript 内核 + 纯 Prompt 资产"的 OpenCode 插件:
src/只负责注入 goal 工具、7 个命令、8 个 skill、5 个 agent(含 permission 规则)并驱动"goal 状态机 + idle 自动续接"循环,实际的 git/gh 操作全部由模型通过 bash 工具执行,靠 permission 白名单收敛。内核层(context/profile/session-index/permission/mutex)职责单一、无业务逻辑,资产层(commands/skills/agents/prompts)以 stage-contract 和 prompt-lint 做契约校验,CI 有 prompt-lint 测试把关资产一致性,整体模块边界清晰,是设计意图执行得相当彻底的架构。优点突出:goal 状态最小化(只存 issue 号/status/计数,目标从 GitHub Issue 读取)、
mutateGoal作为带 KeyedMutex 的读-改-写唯一入口杜绝并发丢失更新、session-index 原子写(PID 后缀 tmp + rename)且索引失败不阻塞主流程、CONTEXT.md 注入带"非指令"包裹和防重复 marker、permission 语义(最后匹配优先、auto 仅 allow)有独立纯函数与对抗测试、resolveProjectDir对异常会话目录做了防御性退化。这些表明作者对并发与状态一致性有明确意识。主要问题集中在两个面:多项目/多实例隔离与不可信输入→权限放大。多项目方面:
prompts.ts/bootstrap.ts用模块级可变单例存 projectDir,多项目同进程时互相覆盖;server.ts 的事件处理(idle/message.updated/session.updated)不区分会话归属项目,会把本项目 CONTEXT.md 注入到别的项目的会话续接里,并把 flow 状态写进错误的 session-index。安全方面:primary 编排器 bash 全量 allow + 50 次无人值守自动续接 + 以 GitHub Issue/PR 正文(不可信)为执行依据,构成 prompt injection → 任意命令执行链;AGENTS.md(仓库可控内容)的 test command 首 token 被直接渲染成 bash allow 前缀,仓库可借此把 developer 权限放大到完整 shell;goal complete 授权仅凭 agent 名称字符串。这些大多是文档化的取舍(shell.ts、对抗测试均明示),但取舍的安全成本被系统性低估。错误处理整体是"捕获 + console 日志 + 不阻塞",session-index 与 goal 元数据之间允许静默漂移(如 bindSession 失败被吞掉),对单会话可接受,但叠加续接循环后状态失真会被放大。技术债集中在:agent frontmatter 的
tools/capabilities字段被静默丢弃(安全声明与运行时不一致)、技能安装对全局固定目录 rm -rf 且无锁、KeyedMutex 无清理、goal 输入无校验、跨进程索引无锁。发现
[HIGH][安全] primary 编排器 bash 全量 allow + 无人值守自动续接 + 以不可信 Issue/PR 内容为执行依据,prompt injection 可直接驱动任意命令
说明: 位置: assets/agents/dev-lifecycle.md:8(
"*": "allow")、src/plugin/server.ts:69-136(queueContinuation)、src/plugin/server.ts:301-315(idle 自动续接)、src/plugin/goal.ts:59-70(续接提示要求"直接执行,不要叙述")说明: dev-lifecycle 以用户完整 gh/git 凭据自动执行 push/PR 创建/merge/tag 等写操作,无逐命令人工确认;其执行依据(Flow Record、Task Record、PR diff、Issue 评论)全部来自 GitHub 上他人可写入的内容,而仓库内恶意 Issue/PR 正文即可注入指令。CONTEXT.md 有"非指令"防注入包裹,但 Issue/PR 内容没有任何同等防护,续接提示反而鼓励模型不加停顿地执行。
建议: 将写命令(git push / gh pr merge / gh release / git tag 等)在 permission 层设为
ask强制确认;对从 Issue/PR 读取的内容套用与 CONTEXT.md 相同的"仅供参考、非指令"包裹并指示模型验证来源。[HIGH][安全] AGENTS.md Profile 的 test command 首 token 被渲染为 bash allow 前缀,仓库可控内容可放大为任意命令执行
说明: 位置: src/plugin/server.ts:227-241(renderPermission)、assets/agents/team/developer.md:10(
"<profile-test-command>*": "allow")、src/kernel/profile.ts:133-138说明:
<profile-test-command>*被替换为测试命令首 token 加*(如npm*、bash*、sudo*),等于放行该二进制全部能力(npm exec/run/install、bash -c、sudo 任意命令),deny 仅挡<token> publish*一种形态。AGENTS.md 是仓库内容,贡献者/攻击者把test command设为bash即可让 developer/goal-verify 获得完整 shell,且cleanValue只剥反引号,不做任何白名单校验。建议: 只放行 Profile 中 test command 的精确完整命令(整串匹配),不再用首 token 前缀;或将测试执行单列为独立权限类别,默认 ask。
[MEDIUM][安全] goal complete 授权仅凭 agent 名称字符串,可被用户配置遮蔽或伪造
说明: 位置: src/plugin/goal.ts:166-185(
ctx.agent !== "goal-verify"即拒绝)、src/plugin/server.ts:58-67(configureGoalTools 按名称放行 goal)、src/plugin/server.ts:250-262(已存在同名 agent 跳过插件注入)说明: 任何 key 为 "goal-verify" 的子 agent 都能 complete goal,而用户 config 中同名 agent 会遮蔽插件自带版本、静默获得 goal:true,插件不产生任何告警;主会话的 complete 只返回 BLOCKED 文本,完全依赖模型自觉。名称即权限,无凭据、无证据校验。
建议: 用插件可独占注入的配置标记(如特殊 capability 或工具配置值)而非名称判断,遮蔽时输出 warning;complete 前校验 goal-verify 会话确实返回过验证报告。
[MEDIUM][安全] 技能安装对 env 可覆盖的固定全局目录执行 rm -rf,无路径校验且存在并发竞态
说明: 位置: src/plugin/skills.ts:9-14(resolveSkillsPath)、16-35(setupSkillsDir)、48-64(全量改写 SKILL.md)
说明: CABBAGE_SKILLS_DIR 可指向任意路径(家目录、项目根),未校验即
rm -rf+ mkdir + cp,误设即数据丢失;所有项目共享~/.config/opencode/cabbage/skills,check-then-act(readdir 完整性判断 → rm → cp → 改写全部 SKILL.md)在多个插件实例/进程并发启动时存在 TOCTOU,一方 rm 掉另一方正在读取的目录会导致插件加载失败,不同插件版本还会互相覆盖安装。建议: rm 前校验解析路径位于约定的配置目录内且 basename 为 "skills";按内容 hash 分目录安装(skills-)或加进程间文件锁串行化安装。
[MEDIUM][安全] 测试命令白名单使"运行测试"等价于任意代码执行,且 agent shell 透传全部用户凭据
说明: 位置: assets/agents/team/developer.md:10-41(bash 白名单)、src/plugin/shell.ts:12-18(仅设两个 git 环境变量)
说明: developer/goal-verify 被要求对仓库内容执行测试命令,而仓库 package.json 的 scripts/依赖安装脚本本身即任意代码;agent shell 只设置 GIT_CONFIG_NOSYSTEM/GIT_TERMINAL_PROMPT,GH_TOKEN/GITHUB_TOKEN/HOME(SSH key)全部透传。对不可信仓库运行测试 = 以用户全部凭据执行任意代码,且当前无任何隔离或提示。
建议: 在 setup 阶段向用户明示该风险;可行时用受限 token、临时 HOME/凭据隔离运行测试命令;与上一条修复合并(精确命令白名单 + 运行前确认)。
[MEDIUM][架构] 同一 Flow 双 session 无检测:两个会话都会自动续接同一 goal,产生重复并行工作
说明: 位置: src/kernel/session-index.ts:62-80(bindSession 覆盖式绑定)、src/plugin/server.ts:69-136(idle 续接不查索引)
说明: 文件头注释声称"双 session 检测(R7)",但 bindSession 对已绑定其它 session 的 Flow 直接覆盖;idle 驱动的续接完全不 consult 索引,因此新旧两个会话会同时为同一 Flow 自动续接执行(重复 worktree/PR/合并),autoResume 只恢复最后绑定的一个。
建议: bindSession 发现 key 已被其它 sessionID 绑定时返回冲突,让 goal create 拒绝;或续接前校验该 session 是索引中该 Flow 的当前绑定会话。
[MEDIUM][架构] 模块级可变单例导致多项目插件实例互相污染
说明: 位置: src/plugin/prompts.ts:7-13(_packageRoot/_projectDir)、src/plugin/bootstrap.ts:7(_bootstrapCache)、src/kernel/context.ts:27(_cache)
说明: initPrompts 用模块级变量存 projectDir,后创建的插件实例覆盖先创建者;多项目同进程时 loadPrompt 会从错误的项目目录读取 prompt 覆盖文件,bootstrap 缓存同理(context 缓存按 projectDir 键控,幸免)。项目级 prompt 覆盖(
.opencode/opencode-cabbage/prompts/)因此可能被注入到别的项目的会话。建议: 将这些状态收进 createOpencodeCabbage 的闭包,或改为按 projectDir 的 Map(带生命周期清理),prompts/bootstrap 改为实例级。
[LOW][架构] 续接循环的 50 次上限依赖事件时序,且续接不感知子 agent 是否仍在运行
说明: 位置: src/plugin/server.ts:317-335(message.updated 重置计数)、99-108(compaction)、69-136(queueContinuation)
说明: message.updated 对任何 role=user 消息重置 continuationCount,若 synthetic 续接消息的事件在 continuationInFlight 清理后才到达(或 autoResume 路径),配额会被清零;primary idle 即续接,不感知 Task 子 agent 是否仍在执行,可能在子任务未结束时叠加推进,导致编排竞争。
建议: 只对非 synthetic 的用户消息重置计数;续接前检查该会话是否存在进行中的 subagent turn,或增加最小续接间隔。
[LOW][架构] goal 输入校验缺失、KeyedMutex 无清理、索引写跨进程无锁
说明: 位置: src/plugin/goal.ts:188-205(create 时 Number() 不校验)、src/kernel/mutex.ts:6-22、src/kernel/session-index.ts:50-56
说明: parent_issue_number 传 "abc"/负数/0 会生成 NaN/负数 goal 并写入索引 "NaN" 键;KeyedMutex 的 locks Map 永不删除已释放条目,长进程下随 session 数增长泄漏内存;writeIndex 的互斥仅进程内有效,两个 opencode 进程同时操作同一项目索引会丢失更新。
建议: create 前校验 parent_issue_number 为正整数;mutex 释放后删除 key;索引写加进程间锁(lockfile)或在文档明确单进程假设。