Procházet zdrojové kódy

fix(tools): resolve the collapse through the scope, not the deployment default

`collapses()` read `defaultMode`, so the collapse only applied when the
DEPLOYMENT was `code`. An agent handed `code` by an agent preset under a
native default announced `[run_code]` on the wire and still executed a
model-direct native call -- the bypass this collapse exists to close,
reopened for exactly the composition `dsh-agent-tool-mode` produces.

`modeFor(scope)` is the same resolution `wireSchemas` and the SDK section
already use, so presentation and execution cannot disagree, and a mode
inherited from a standing preset scope collapses like a declared one.

The per-agent and preset tests asserted only the wire, which is why the
regression passed them. They now assert through the executor: the body
never runs, the call resolves UNKNOWN_TOOL, and the native sibling beside
it still executes.
Yichen Jiang před 1 měsícem
rodič
revize
e258cf7a2d

+ 2 - 2
docs/config-catalog.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/config-catalog.md
-config-catalog.md: 6362022dd1b80e17997684574d9dc7f14372e23a
-config-catalog.zh.md: 3fd335ee2b1ec6d1526f75ed9667f1d9286faafe
+config-catalog.md: 39433b697588ede7b36f878490f4e0b095e8d67a
+config-catalog.zh.md: d76b289f8a7ffbc434b2adeefd5f16142fdffc99

+ 1 - 1
docs/config-catalog.md

@@ -2505,7 +2505,7 @@ export interface Config {
 export type ToolPresentationMode = 'native' | 'code' | 'both'
 ```
 
-Source: [`packages/core/tools/src/index.ts:629`](../packages/core/tools/src/index.ts)
+Source: [`packages/core/tools/src/index.ts:654`](../packages/core/tools/src/index.ts)
 
 ## `@deepseek-ai/dsh-typert-loader`
 

+ 1 - 1
docs/config-catalog.zh.md

@@ -2506,7 +2506,7 @@ export interface Config {
 export type ToolPresentationMode = 'native' | 'code' | 'both'
 ```
 
-来源:[`packages/core/tools/src/index.ts:617`](../packages/core/tools/src/index.ts)
+来源:[`packages/core/tools/src/index.ts:654`](../packages/core/tools/src/index.ts)
 
 ## `@deepseek-ai/dsh-typert-loader`
 

+ 11 - 4
packages/core/tools/src/index.ts

@@ -1221,7 +1221,7 @@ export class ToolRegistry extends Service {
   private resolveExecution(name: string, scope: ScopeKey | undefined, nested: boolean): ToolDefinition | undefined {
     const tool = this.get(name, scope)
     if (tool === undefined) return undefined
-    if (this.collapses(name, nested)) return undefined
+    if (this.collapses(name, scope, nested)) return undefined
     return tool
   }
 
@@ -1311,11 +1311,18 @@ export class ToolRegistry extends Service {
    * `parent` token set) bypass the collapse. One home for the
    * security-relevant predicate, shared by {@link resolveExecution} and
    * {@link createExecution} so the two can never drift apart.
+   *
+   * Resolved through {@link modeFor}, NOT `defaultMode`: an agent given `code`
+   * by an agent preset under a native deployment is the composition
+   * `dsh-agent-tool-mode` exists for, and reading the deployment default would
+   * leave exactly that agent uncollapsed — announcing one surface while
+   * executing another, which is the bypass this collapse closes.
    * @param name - the tool name as registered.
+   * @param scope - the viewing scope whose effective presentation mode applies.
    * @param nested - whether the call is a transport sub-dispatch, not a model-direct call.
    */
-  private collapses(name: string, nested: boolean): boolean {
-    return !nested && this.defaultMode === 'code' && name !== RUN_CODE_NAME
+  private collapses(name: string, scope: ScopeKey | undefined, nested: boolean): boolean {
+    return !nested && this.modeFor(scope) === 'code' && name !== RUN_CODE_NAME
   }
 
   /**
@@ -1371,7 +1378,7 @@ export class ToolRegistry extends Service {
     // keeps the historical dispatch-stage `UNKNOWN_TOOL` path so policy
     // listeners still see every name that reaches the registry.
     const visible = this.get(name, agent)
-    const collapsed = visible !== undefined && this.collapses(name, parent !== undefined)
+    const collapsed = visible !== undefined && this.collapses(name, agent, parent !== undefined)
     const concludingExecutions = this.concludingExecutions
     const base = {
       token,

+ 43 - 2
packages/core/tools/tests/code-mode.spec.ts

@@ -1635,7 +1635,7 @@ describe('the run_code dispatch bridge', () => {
 describe('per-agent presentation', () => {
   it('gives one agent Code Mode while the deployment stays native', async () => {
     const { ctx, systemPrompt } = await setup({ mode: 'native' })
-    registerEcho(ctx)
+    const calls = registerEcho(ctx)
     const { scope, agent } = await mintAgentScope(ctx)
 
     scope.ctx.tools.presentAs('code')
@@ -1644,6 +1644,17 @@ describe('per-agent presentation', () => {
     expect(coded.tools.map(tool => tool.name)).toEqual([RUN_CODE_NAME])
     expect(coded.sections.find(section => section.name === 'tools:sdk')?.text)
       .toContain('echo')
+    // Announced surface and callable surface must agree for THIS agent, whose
+    // mode is its own rather than the deployment's.
+    const denied = await ctx.tools.execute({
+      signal: testToolSignal,
+      callId: CallId('coded-direct'),
+      name: 'echo',
+      arguments: { value: 'coded' },
+      agent,
+    })
+    expect(denied.error?.info).toEqual({ name: 'ToolNotFoundError', code: 'UNKNOWN_TOOL' })
+    expect(calls).toEqual([])
     // The deployment default is untouched: an agent that declared nothing —
     // and the global view behind it — still sees the native catalog.
     const native = await systemPrompt.assemble()
@@ -1654,7 +1665,7 @@ describe('per-agent presentation', () => {
   it('inherits a STANDING preset scope\'s mode down the chain, agents beside it unaffected', async () => {
     const { bindScopeParent } = await import('@deepseek-ai/dsh-scope')
     const { ctx, systemPrompt } = await setup({ mode: 'native' })
-    registerEcho(ctx)
+    const calls = registerEcho(ctx)
     // The preset's standing scope declares once; the agent only PARENTS to it
     // (the per-preset standing mount configuration has no per-agent declaration).
     const standing = await mintAgentScope(ctx, 'preset:code-like')
@@ -1666,10 +1677,40 @@ describe('per-agent presentation', () => {
     expect(ctx.tools.get(RUN_CODE_NAME, joined.agent)).toBeDefined()
     const coded = await systemPrompt.assemble({ scope: joined.agent })
     expect(coded.tools.map(tool => tool.name)).toEqual([RUN_CODE_NAME])
+    // Through the EXECUTOR, not just the wire: the deployment default is
+    // `native` here, so a collapse predicate reading it instead of this
+    // scope's effective mode would announce [run_code] and still execute the
+    // native call — the bypass, reopened for exactly the preset composition
+    // `dsh-agent-tool-mode` produces.
+    expect(ctx.tools.executionMode({
+      signal: testToolSignal,
+      callId: CallId('preset-coded-schedule'),
+      name: 'echo',
+      arguments: { value: 'joined' },
+      agent: joined.agent,
+    })).toEqual({ kind: 'exclusive' })
+    const denied = await ctx.tools.execute({
+      signal: testToolSignal,
+      callId: CallId('preset-coded-direct'),
+      name: 'echo',
+      arguments: { value: 'joined' },
+      agent: joined.agent,
+    })
+    expect(denied.error?.info).toEqual({ name: 'ToolNotFoundError', code: 'UNKNOWN_TOOL' })
+    expect(calls).toEqual([])
     // A sibling that never parented stays native, as does the global view.
     expect(ctx.tools.get(RUN_CODE_NAME, loner.agent)).toBeUndefined()
     const native = await systemPrompt.assemble({ scope: loner.agent })
     expect(native.tools.map(tool => tool.name)).toEqual(['echo'])
+    const allowed = await ctx.tools.execute({
+      signal: testToolSignal,
+      callId: CallId('native-sibling-direct'),
+      name: 'echo',
+      arguments: { value: 'loner' },
+      agent: loner.agent,
+    })
+    expect(allowed).toMatchObject({ isError: false, value: 'echo:loner' })
+    expect(calls).toEqual([{ value: 'loner' }])
   })
 
   it('keeps run_code out of a native agent\'s dispatch table', async () => {