fix(scope): harden merged tool and skill boundaries

This commit is contained in:
Tianyi Cui
2026-07-11 23:41:37 +08:00
parent 172005e0e6
commit cf255eebb1
14 changed files with 283 additions and 42 deletions

View File

@@ -4,8 +4,9 @@ import { join } from 'node:path'
import { tmpdir } from 'node:os'
import { Context } from 'cordis'
import { CallId, type Message } from '@deepseek-ai/dsh-llm'
import { createScope, type Scope } from '@deepseek-ai/dsh-scope'
import SystemPrompt, { renderPrompt } from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry from '@deepseek-ai/dsh-tools'
import ToolRegistry, { defineTool } from '@deepseek-ai/dsh-tools'
import { agentEvents, type Agent } from '@deepseek-ai/dsh-agent'
import SkillService from '@deepseek-ai/dsh-skill'
import * as SkillLocal from '@deepseek-ai/dsh-skill-local'
@@ -36,7 +37,10 @@ function agentForCwd(cwd: string): Agent {
}
async function composePrefix(ctx: Context, cwd: string, signal = new AbortController().signal): Promise<Message[]> {
const agent = agentForCwd(cwd)
return await composePrefixForAgent(ctx, agentForCwd(cwd), signal)
}
async function composePrefixForAgent(ctx: Context, agent: Agent, signal = new AbortController().signal): Promise<Message[]> {
const empty: Message[] = []
return await agentEvents(ctx, agent).waterfall(
'agent/session-prefix', empty, signal,
@@ -44,6 +48,15 @@ async function composePrefix(ctx: Context, cwd: string, signal = new AbortContro
)
}
async function mintAgentScope(ctx: Context, cwd: string): Promise<{ agent: Agent; scope: Scope }> {
const agent = agentForCwd(cwd)
let scope!: Scope
await ctx.plugin(Object.assign((inner: Context) => { scope = createScope(inner, agent) }, {
inject: ['tools'],
}))
return { agent, scope }
}
describe('dsh-tool-skill', () => {
it('registers the skill tool schema and removes it on dispose', async () => {
const ctx = new Context()
@@ -154,6 +167,39 @@ describe('dsh-tool-skill', () => {
expect(await composePrefix(ctx, '/workspace')).toEqual([])
})
it('omits catalog guidance when the calling agent restricts away the shipped skill tool', async () => {
const home = await tempDir('tool-restricted-catalog')
const ctx = await setup(home)
ctx.skills.register({ name: 'listed-skill', description: 'Listed', source: 'runtime', content: 'body' })
const { agent, scope } = await mintAgentScope(ctx, '/workspace')
scope.ctx.tools.restrict({ deny: ['skill'] })
expect(ctx.tools.get('skill', agent)).toBeUndefined()
expect(await composePrefixForAgent(ctx, agent)).toEqual([])
expect(await composePrefix(ctx, '/workspace')).toHaveLength(1)
await scope.dispose()
})
it('does not attach shipped catalog guidance to a scoped same-name tool shadow', async () => {
const home = await tempDir('tool-shadowed-catalog')
const ctx = await setup(home)
ctx.skills.register({ name: 'listed-skill', description: 'Listed', source: 'runtime', content: 'body' })
const { agent, scope } = await mintAgentScope(ctx, '/workspace')
scope.ctx.tools.register(defineTool({
name: 'skill',
description: 'A scoped tool with unrelated semantics.',
parameters: {},
execute() {
return Promise.resolve([{ type: 'text', text: 'shadow' }])
},
}))
expect(ctx.tools.get('skill', agent)).not.toBe(ctx.tools.get('skill'))
expect(await composePrefixForAgent(ctx, agent)).toEqual([])
expect(await composePrefix(ctx, '/workspace')).toHaveLength(1)
await scope.dispose()
})
it('validates the catalog description cap', async () => {
const home = await tempDir('tool-invalid-catalog-cap')
const ctx = new Context()