Repository navigation
Replies: 4 comments
补充案例:没有打断,同样没有拦截为避免把问题误解为「只发生在被打断时」,这里补充一次完全没有打断的会话。 环境相同:DSH 会话特征
写操作分布前 23 步做了常规的文件读取与环境探测(
全部返回成功,无一次被拦截或询问。 第二类敏感操作也未被阻挡其中一次操作复制了浏览器 Cookies 数据库: 这类操作在计划模式下同样没有触发任何确认或拒绝。 结论把这个案例与主贴的场景放在一起看:
说明拦截缺失是普遍性的,打断只是让问题更早暴露,并不是触发条件。 如果后续修复只针对「打断」这一路径(例如恢复队列顺序),而不同时加上工具层的 一点背景说明本案例中用户的原始指令是「请阅读这个计划,完成还未完成的剩余内容」—— 从意图上看,用户确实希望 Agent 动手执行。 因此本案例不主张「模型违背了用户意图」。它要说明的是另一件事:当用户指令本身就是执行类任务时,计划模式理应先引导 Agent 产出一份计划待审批,而不是直接开始写。 目前 |
补充:机制侧核查 —— 拦截能力是现成的,只是 plan-mode 没有使用前两条评论描述的是现象。这一条补充机制侧的核查结论,帮助排除几个常见的归因方向。 1.
|
| 包 | 用途 |
|---|---|
dsh-tools |
管道本体 |
dsh-tool-bash |
bash 工具 |
dsh-tool-jobs |
输出限额 |
dsh-tool-cordis |
工具目录 |
dsh-bash-local |
本地执行器 |
dsh-workspace-changes |
变更捕获 |
dsh-experimental-auto-review |
自动审查 |
dsh-tool-jobs 的实际用法:
ctx.on("tools/pre-execute", (exec, next) => {
const maxBytes = visibleOutputLimit(ctx, exec);
if (maxBytes !== void 0) outputLimits.set(exec, maxBytes);
return next();
}, { prepend: true });dsh-workspace-changes 则同时挂了三个:
ctx.on("fs/write-intent", async (target, actor, next) => { const intent = await next(); await capture(target, actor); return intent; }, { prepend: true });
ctx.on("fs/edit-intent", async (target, actor, next) => { const intent = await next(); await capture(target, actor); return intent; }, { prepend: true });
ctx.on("tools/pre-execute", async (exec, next) => { ... });而 dsh-tool-fs 本身已经会发出 fs/write-intent / fs/edit-intent 事件。
也就是说:拦截机制、事件通道、以及触发这些事件的写入工具,三者在当前版本里都是完整可用的。 问题不在机制缺失。
2. dsh-plan-mode 对上述任何一个都没有注册
对 @deepseek-ai/dsh-plan-mode/lib/index.js 的同一次扫描:
| 关注点 | 出现次数 |
|---|---|
tools/pre-execute |
0 |
fs/write-intent |
0 |
fs/edit-intent |
0 |
唯一的约束手段是 systemPrompt.section('plan:policy')。
3. 官方源码自己写明了正确的位置
@deepseek-ai/dsh-tool-bash/lib/index.js 的模块注释:
/**
* TODO(permissions): deployment policy belongs in `tools/pre-execute` and
* sandboxing executors; see docs/architecture.md § Where new behavior goes.
* @module @deepseek-ai/dsh-tool-bash
*/即:工具级策略应该落在 tools/pre-execute 与 sandbox 执行器里。这正是 plan-mode 目前缺失的环节。
4. 约束提示词确实已注入到请求中
为排除「模型没有被正确告知」这一可能,从两个复现会话的 system message 中提取了实际注入的策略文本:
You are in plan mode. Stay in plan mode until the user approves your plan through
exit_plan_modeor switches the session mode. Imperative language to implement changes means plan the implementation, not execute it. ...Explore first. Use non-mutating reads, searches, static analysis, and checks to ground the plan in the actual repository. Do not edit or write files, change configuration, run formatters or code generation that rewrites tracked files, commit, or otherwise carry out the plan. ...
The tool catalog stays the same across modes for request-cache stability. These plan-mode rules override any later tool description or guidance that suggests using mutation tools; those tools remain listed to keep the tool catalog unchanged. ...
文本完整、措辞明确,且明确声明了「覆盖后续任何鼓励使用变更工具的描述」。
另外,两次复现分别走的是不同的 provider 路由(xdpool-global 与 xdpool),行为一致,因此也排除了某个特定 provider 造成的可能。
小结
| 归因方向 | 结论 |
|---|---|
| 第三方插件冲突 | 不成立 —— 拦截机制由官方包独占使用,plan-mode 未注册任何钩子 |
| 机制不可用 | 不成立 —— 钩子、事件通道、写入工具三者均在 |
| 模型未被告知 | 不成立 —— 策略文本已注入且措辞明确 |
| plan-mode 只做了提示词层,缺工具层兜底 | 成立 |
这不是「某个模型不够听话」,而是只读边界缺少第二道保险。提示词是建议,拦截才是边界。常见做法是两层并存:提示词负责引导,工具层负责在越界时给出明确错误。
建议的最小修复
// 示意:在 plan-mode 内注册守卫
ctx.on('tools/pre-execute', (exec, next) => {
const active = this.pendingIntents.get(exec.agent?.session)?.active
?? this.loggedActive(exec.agent?.session);
if (!active) return next();
if (MUTATING_TOOLS.has(exec.name))
return { kind: 'deny', reason: 'plan mode: file mutations are not allowed' };
return next();
});需要注意一点:仅靠方案 A 会漏掉 pwsh / bash 内的间接写入(Copy-Item、Set-Content、Out-File、> 重定向等),这在本次补充案例中确实发生过。若要让只读边界真正成立,建议同时把计划模式期间的 sandbox 钉为 read-only,用既有的 enforcement 机制兜住间接写入。
两者结合,才能覆盖:直接文件工具 → 工具层守卫;任意 shell → sandbox 层。
补充:系统提示词内部存在指令冲突前两条评论分别讲了现象和机制。这一条补充一个此前未提及、但解释力更强的直接诱因:计划策略与紧随其后的指令自相矛盾。 计划策略声明自己优先级最高system message 中的
注意最后一句:它明确声明「覆盖后续任何鼓励使用变更工具的指引」。 但紧随其后的段落就在鼓励编辑同一条 system message 中,计划策略之后不到 2500 字符处:
也就是说,模型在同一条系统提示里,先被告知「不要写文件,这条优先于后面所有内容」,再被告知「改文件请优先用 read/edit/write」。 位置实测(两个会话一致)对 system message 做字符偏移定位:
两个会话中,鼓励编辑的指令都稳定出现在计划策略之后。 这一条的意义即使补上了工具层拦截,这个冲突仍值得修:
可能的最小修法
|
补充:模型侧的责任,以及为什么这更说明需要工具层兜底前三条评论聚焦在宿主侧机制。这一条补上模型侧的观察 —— 为了保持结论客观,先把不属于宿主的问题讲清楚。 两个会话中模型的行为差异明显对两个复现会话做统计:
会话 A 中模型是知道正确收尾方式的 —— 它调用了 4 次 会话 B 中模型一次都没有调用,直接在计划模式下写了 17 个文件。 所以这里存在一个不应回避的事实:模型在会话 B 中没有遵守明确的禁令。 system message 里写得很清楚:
没有任何歧义,模型仍然越界了。这部分责任在模型侧,不在宿主。 但正是这个对比,说明提示词层单独不可靠两个会话使用同一份
也就是说:同一份提示词,同一模型,结果不稳定。 这不是「提示词写得不够好」能解决的问题 —— 会话 A 证明了提示词是有效的。问题在于提示词是概率性约束,不是确定性边界。 责任归属
关键区别在于:
其他主流编码 Agent 在实现只读模式时都采用两层:提示词引导模型,工具层在越界时给出明确错误。DSH 目前只有第一层 —— 这意味着只要模型那次判断失误,用户的文件就会被实际修改,而且用户完全不知道。 这也是为什么本贴把重心放在宿主侧:不是认为模型没问题,而是模型的不稳定恰恰是需要工具层兜底的理由。如果模型 100% 可靠,提示词就够了;正因为不是,才需要第二道保险。 对修复的补充建议结合前面的分析,建议按以下优先级处理:
第 1、2 条解决「越界无代价」,第 3 条降低「越界概率」。三者叠加才能让计划模式的只读承诺真正成立。 一点说明本贴不主张「模型完全无责」,也不主张「只要模型更聪明就不需要拦截」。两者都需要:提示词负责引导,拦截负责边界。目前缺的是后者。 |
Uh oh!
There was an error while loading. Please reload this page.
概述
计划模式(
/plan)的只读承诺只由系统提示词约束,工具执行层没有任何拦截。当用户在 Agent 探索阶段发送新消息打断时,该消息会被立即插入当前 turn(而非排队到下一 turn),提示词约束随之失效,Agent 会直接开始修改文件。实测:计划模式全程保持开启(
plan/mode事件一直是active: true,从未退出),期间发生了 8 次文件写入调用,其中 7 次成功落盘。复现
edit修改文件,无任何拦截、无任何提示实测数据
环境:DSH
0.2.1-rc.2/ Windows / Electron 44。两次计划模式窗口(16:05:44–17:18:22、17:26:01–18:13:01)内共 8 次
edit调用:profiles/desktop/cordis.patch.ymlprofiles/desktop/cordis.patch.ymlplugins/dsh-permission-settings-fix/client/client.js唯一的 FAILED 是文件版本冲突(
file changed since it was read),不是计划模式的拦截。打断机制(会话日志原样)
也就是说:新消息不会等到当前 turn 结束,而是被重新投递到
next-step,在当前 turn 中途就进入上下文。根因
@deepseek-ai/dsh-plan-mode(lib/index.js)对当前源码的统计:tools/pre-executefs/writesystemPrompt.section('plan:policy')约束完全来自这一段系统提示文本:
只要上下文在 turn 中途被
next-step重新铺设,模型注意力转移到新消息上,这段提示词的约束力就会漂移 —— 而没有任何工具层兜底。建议
write/edit/pwsh(含写文件命令)/ops_replace注册tools/pre-execute守卫,计划模式激活时返回{ kind: "deny" }或{ kind: "ask" }。read-only,用既有的 enforcement 机制而不是提示词。与已有讨论的关系
以下讨论都围绕计划模式,但问题不同,本贴不重复:
exit_plan_mode在后台 subagent 未落定时就提交计划本贴聚焦的是:计划模式没有任何工具层的写操作拦截,且被立即消息打断时会实际写文件。
影响
计划模式的核心承诺是「只讨论、不动手」。用户在计划模式下发送一句补充说明,本意是继续讨论,但文件可能已被修改。这不是体验问题,是用户对只读边界的信任被打破。
All reactions