Skip to content

[BUG] PTC 模式下 hide-official 未调用 tools.restrict 隐藏所选官方工具:模型在 tools:sdk (ToolArgsMap) 与提示词中仍能看到 read/glob/grep,但调用即被 refusal 拦截 #415

Open

Description

@TomXPRIME

背景与现象

在 #365 / PR #366 之后,dsh-ops/hide-official 对 run_code(PTC)作用域采取了**“不隐藏(不调用 tools.restrict),但在执行守卫 refusal 中一律拒绝所选官方工具”**的策略(见 src/official-tool-hiding.ts:140-179 与 test/presentation-readiness.test.mjs:52 的 'a collapsed scope hides nothing')。

这在实际运行中会导致一个直接面向模型的矛盾现象:在 PTC 模式下,即便启用了 dsh-ops/hide-official 并配置了 replaceTools: ['read', 'glob', 'grep'],模型在系统提示词里依然能完整看到官方 read、glob、grep 的工具说明和 run_code 的 TypeScript SDK 声明(ToolArgsMap / ToolOutputMap),但只要模型照着提示词在 run_code 中调用 await tools.read(...),就会立刻撞上 READINESS_BLOCKED 报错,白烧一轮工具调用与上下文 Token。


为什么模型在 PTC 里还能看到被替换的官方工具?(根因拆解)

1. 宿主 dsh-tools 的 PTC 提示词与 SDK 声明是从 view(scope).visible 生成的,而不是从顶层 result.tools 生成的

PR #366 的注释(src/official-tool-hiding.ts:164-169)假设:既然 PTC 作用域组装出的顶层 Wire 工具数组 result.tools 只剩 ['run_code'],那么官方 read/glob/grep 就没有暴露给模型,因此不需要下达 entry.context.tools.restrict({ deny })。

但宿主 @deepseek-ai/dsh-tools(packages/core/tools/src/index.ts)在 PTC 模式下的真实行为分两路:

  1. 顶层 Wire 工具栏(wireSchemas,第 1019-1037 行):折叠为只剩 [{ name: 'run_code' }]。
  2. 系统提示词中的 SDK 声明(sdkSection / sdkSchemas,第 899-916、1273-1287 行)与官方工具提示词段落:遍历的是 this.view(scope).visible.values()(即该作用域下所有未被 tools.restrict({ deny }) 屏蔽的工具)!

因为在 PTC 模式下:

  • dsh-ops/ptc(src/ptc/index.ts:164)调用 claimPtcSurface() 撤下了 NativeProvider,改发 PTC 专属定义;
  • official-tool-hiding.ts:52-58 的 readiness() 只认 nativeProviders(ctx, agent),在 PTC 下恒为 false;
  • prepare()(第 75 行)返回 { deny: [], failure: ... },reconcile()(第 100 行)直接 clear(entry) 跳过了 entry.context.tools.restrict({ deny: plan.deny })。

结果:官方 read、glob、grep 在底层的 view(scope).visible 中完全没有被屏蔽,宿主 sdkSchemas() 把它们原封不动地印进了系统提示词的 interface ToolArgsMap / ToolOutputMap 里,同时官方工具自己的提示词规则(如 Use the read tool...、Use the glob tool...、Use the grep tool...)也照常渲染。模型看着说明书去调 await tools.read(...),却在 official-tool-hiding.ts:145-146 被 refusal 守卫以 READINESS_BLOCKED: official tool read is selected for replacement 拦下。


连锁引发的另外两个问题

2. run_code 执行期间若触发 reconcile() 重算,连未选中的正常工具子调用也会被 entry.error 误杀

在 src/official-tool-hiding.ts:134-147 中:

const refusal: ToolGuard = exec => {
  if (stopped) return blocked('component disposed')
  if (!exec?.agent?.ctx) return blocked('calling agent scope unavailable')
  attach(exec.agent)
  reconcile() // Authority is live, not a startup snapshot.
  const entry = entries.get(exec.agent)
  const transport = entry?.collapsed === true && exec.name === RUN_CODE
  if (entry?.error && !transport) return entry.error
  return hideable(exec.name)
    ? blocked(`official tool ${exec.name} is selected for replacement`) : undefined
}
  • system-prompt/assemble 结束时(第 174 行)虽然因为 collapsed === true 把 entry.error 清成了 undefined,但这只是单次修改。
  • 一旦在 assemble 之后发生过任何使 snapshot() 变化的事件(例如并行会话的工具变化),当 run_code 内部发起任何合法的 SDK 子调用(如 await tools.ops_inspect_local_file(...) 或 await tools.write(...))时:
    1. reconcile() 重新执行 prepare(agent, entry),将 entry.error 重新置为 READINESS_BLOCKED ... missing OPS surface;
    2. 由于子调用的 exec.name 是具体工具名而非 'run_code',transport 为 false;
    3. if (entry?.error && !transport) return entry.error 直接命中,导致 run_code 内所有子调用全部被拒。

3. 与 DSH 0.2.1 的 system-prompt/assemble 契约(#413 / PR #414)冲突

在 DSH 0.2.1(9d0c97b9d2)下,system-prompt/assemble 瀑布不再携带 result.tools(result.tools 为 undefined):

  1. official-tool-hiding.ts:163-169 中 wire 为 [],导致 collapsed 恒为 false,第 173 行 if (!collapsed) throw new Error(entry.error) 在 PTC 作用域下必抛 READINESS_BLOCKED;
  2. 即便如 PR fix(hide-official): verify the assembled surface without a tool array #414 在第 178 行回退到 replaceTools.filter(tool => tools.get(tool, context.scope) !== undefined),由于 PTC 作用域下并未对 replaceTools 下达 tools.restrict,tools.get('read', context.scope) 依然非空,会在第 184 行立即触发 assembled surface still contains hidden official tools。

最小复现

在 test/lib/official-tool-hiding.mjs 的 a run_code-presented scope is not gated by native readiness 用例(第 193 行)中查看组装出的 tools:sdk 提示词段落:

const { ctx } = await bootHost({ toolsMode: 'ptc' })
ctx.provide('ptcRuntime', { language: 'typescript' })
for (const name of ['read', 'glob', 'grep', 'foreign']) ctx.tools.register(tool(name))
await mount(ctx, { replaceTools: ['read', 'glob', 'grep'] })
const agent = await mint(ctx, 'ptc-scope')
const assembly = await assemble(ctx, agent)
const sdkText = assembly.sections.find(s => s.name === 'tools:sdk')?.text ?? ''
// 1. 模型看到的 SDK 声明里仍然包含 read / glob / grep:
console.log(sdkText.includes('read:'), sdkText.includes('glob:'), sdkText.includes('grep:')) // true, true, true
// 2. 但模型在 run_code 内调用它们时被 refusal 拒绝:
const nestedRefused = await ctx.waterfall('tools/pre-execute', { agent, name: 'read', parent: {} }, () => ({ kind: 'allow' }))
console.log(nestedRefused.kind) // 'deny'

建议修复方向

既然出于凭据围栏(credential fence)考虑,所选的官方 read/glob/grep 在 PTC 作用域内也不允许被执行(PR #366 review comment),那么可见性层与执行层应当保持一致(凡是拒绝执行的官方工具,在 PTC 的 view(scope).visible 与 tools:sdk 中也应当通过 tools.restrict({ deny }) 隐藏):

  1. 在 PTC 作用域同样校验对应 OPS PTC 工具的就绪状态并下达 tools.restrict({ deny }):
  2. 或者如果替换契约确实只针对 Native 呈现生效:

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions