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.
This commit is contained in:
@@ -2,5 +2,5 @@
|
|||||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
# 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:
|
# after editing either side, bring the other along and re-record with:
|
||||||
# pnpm run verify-translation-pairing --write docs/config-catalog.md
|
# pnpm run verify-translation-pairing --write docs/config-catalog.md
|
||||||
config-catalog.md: 6362022dd1b80e17997684574d9dc7f14372e23a
|
config-catalog.md: 39433b697588ede7b36f878490f4e0b095e8d67a
|
||||||
config-catalog.zh.md: 3fd335ee2b1ec6d1526f75ed9667f1d9286faafe
|
config-catalog.zh.md: d76b289f8a7ffbc434b2adeefd5f16142fdffc99
|
||||||
|
|||||||
@@ -2505,7 +2505,7 @@ export interface Config {
|
|||||||
export type ToolPresentationMode = 'native' | 'code' | 'both'
|
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`
|
## `@deepseek-ai/dsh-typert-loader`
|
||||||
|
|
||||||
|
|||||||
@@ -2506,7 +2506,7 @@ export interface Config {
|
|||||||
export type ToolPresentationMode = 'native' | 'code' | 'both'
|
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`
|
## `@deepseek-ai/dsh-typert-loader`
|
||||||
|
|
||||||
|
|||||||
@@ -1221,7 +1221,7 @@ export class ToolRegistry extends Service {
|
|||||||
private resolveExecution(name: string, scope: ScopeKey | undefined, nested: boolean): ToolDefinition | undefined {
|
private resolveExecution(name: string, scope: ScopeKey | undefined, nested: boolean): ToolDefinition | undefined {
|
||||||
const tool = this.get(name, scope)
|
const tool = this.get(name, scope)
|
||||||
if (tool === undefined) return undefined
|
if (tool === undefined) return undefined
|
||||||
if (this.collapses(name, nested)) return undefined
|
if (this.collapses(name, scope, nested)) return undefined
|
||||||
return tool
|
return tool
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1311,11 +1311,18 @@ export class ToolRegistry extends Service {
|
|||||||
* `parent` token set) bypass the collapse. One home for the
|
* `parent` token set) bypass the collapse. One home for the
|
||||||
* security-relevant predicate, shared by {@link resolveExecution} and
|
* security-relevant predicate, shared by {@link resolveExecution} and
|
||||||
* {@link createExecution} so the two can never drift apart.
|
* {@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 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.
|
* @param nested - whether the call is a transport sub-dispatch, not a model-direct call.
|
||||||
*/
|
*/
|
||||||
private collapses(name: string, nested: boolean): boolean {
|
private collapses(name: string, scope: ScopeKey | undefined, nested: boolean): boolean {
|
||||||
return !nested && this.defaultMode === 'code' && name !== RUN_CODE_NAME
|
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
|
// keeps the historical dispatch-stage `UNKNOWN_TOOL` path so policy
|
||||||
// listeners still see every name that reaches the registry.
|
// listeners still see every name that reaches the registry.
|
||||||
const visible = this.get(name, agent)
|
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 concludingExecutions = this.concludingExecutions
|
||||||
const base = {
|
const base = {
|
||||||
token,
|
token,
|
||||||
|
|||||||
@@ -1635,7 +1635,7 @@ describe('the run_code dispatch bridge', () => {
|
|||||||
describe('per-agent presentation', () => {
|
describe('per-agent presentation', () => {
|
||||||
it('gives one agent Code Mode while the deployment stays native', async () => {
|
it('gives one agent Code Mode while the deployment stays native', async () => {
|
||||||
const { ctx, systemPrompt } = await setup({ mode: 'native' })
|
const { ctx, systemPrompt } = await setup({ mode: 'native' })
|
||||||
registerEcho(ctx)
|
const calls = registerEcho(ctx)
|
||||||
const { scope, agent } = await mintAgentScope(ctx)
|
const { scope, agent } = await mintAgentScope(ctx)
|
||||||
|
|
||||||
scope.ctx.tools.presentAs('code')
|
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.tools.map(tool => tool.name)).toEqual([RUN_CODE_NAME])
|
||||||
expect(coded.sections.find(section => section.name === 'tools:sdk')?.text)
|
expect(coded.sections.find(section => section.name === 'tools:sdk')?.text)
|
||||||
.toContain('echo')
|
.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 —
|
// The deployment default is untouched: an agent that declared nothing —
|
||||||
// and the global view behind it — still sees the native catalog.
|
// and the global view behind it — still sees the native catalog.
|
||||||
const native = await systemPrompt.assemble()
|
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 () => {
|
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 { bindScopeParent } = await import('@deepseek-ai/dsh-scope')
|
||||||
const { ctx, systemPrompt } = await setup({ mode: 'native' })
|
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 preset's standing scope declares once; the agent only PARENTS to it
|
||||||
// (the per-preset standing mount configuration has no per-agent declaration).
|
// (the per-preset standing mount configuration has no per-agent declaration).
|
||||||
const standing = await mintAgentScope(ctx, 'preset:code-like')
|
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()
|
expect(ctx.tools.get(RUN_CODE_NAME, joined.agent)).toBeDefined()
|
||||||
const coded = await systemPrompt.assemble({ scope: joined.agent })
|
const coded = await systemPrompt.assemble({ scope: joined.agent })
|
||||||
expect(coded.tools.map(tool => tool.name)).toEqual([RUN_CODE_NAME])
|
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.
|
// A sibling that never parented stays native, as does the global view.
|
||||||
expect(ctx.tools.get(RUN_CODE_NAME, loner.agent)).toBeUndefined()
|
expect(ctx.tools.get(RUN_CODE_NAME, loner.agent)).toBeUndefined()
|
||||||
const native = await systemPrompt.assemble({ scope: loner.agent })
|
const native = await systemPrompt.assemble({ scope: loner.agent })
|
||||||
expect(native.tools.map(tool => tool.name)).toEqual(['echo'])
|
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 () => {
|
it('keeps run_code out of a native agent\'s dispatch table', async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user