fix(scope): harden final ownership boundaries

This commit is contained in:
Tianyi Cui
2026-07-12 05:13:17 +08:00
parent 36b8370027
commit a9cb70d896
52 changed files with 2839 additions and 514 deletions

View File

@@ -7,7 +7,7 @@ import ApprovalService, { type ApprovalOutcome, type ApprovalRequest } from '@de
import ToolRegistry, {
defineTool, schemaSpecToJsonSchema, validateArgs, ToolArgsError, ToolNotFoundError,
type DefineToolOptions, type InferArgs, type SchemaSpec, type PreToolDecision, type PostToolDecision,
type ToolDefinition, type ToolExecution, type ToolExecutionResult, type ToolGuard,
type ToolDefinition, type ToolExecution, type ToolExecutionInput, type ToolExecutionResult, type ToolGuard,
} from '@deepseek-ai/dsh-tools'
async function setup() {
@@ -83,6 +83,95 @@ describe('ToolRegistry', () => {
expect(result).toEqual({ callId: CallId('c1'), content: [{ type: 'text', text: 'hi' }], isError: false })
})
it.each([
{ field: 'callId', value: 1n },
{ field: 'callId', value: 123 },
{ field: 'name', value: 1n },
{ field: 'name', value: 123 },
] as const)('rejects a non-string $field before final observation', async ({ field, value }) => {
const ctx = await setup()
let observed = 0
ctx.on('tools/result', () => { observed += 1 })
const input: Record<string, unknown> = {
callId: CallId('valid-call'),
name: 'missing',
arguments: {},
}
input[field] = value
await expect(ctx.tools.execute(input as unknown as ToolExecutionInput))
.rejects.toThrow(`tool execution ${field} must be a string`)
expect(observed).toBe(0)
})
it('reads correlation accessors once and normalizes a later hostile accessor', async () => {
const ctx = await setup()
const reads = { callId: 0, name: 0, arguments: 0 }
let observed: { callId: unknown; name: unknown; isError: boolean } | undefined
ctx.on('tools/result', (exec, result) => {
observed = { callId: exec.callId, name: exec.name, isError: result.isError }
})
const input = Object.defineProperties({}, {
callId: {
enumerable: true,
get: () => {
reads.callId += 1
if (reads.callId > 1) throw new Error('callId reread')
return CallId('one-read-call')
},
},
name: {
enumerable: true,
get: () => {
reads.name += 1
if (reads.name > 1) throw new Error('name reread')
return 'missing'
},
},
arguments: {
enumerable: true,
get: () => {
reads.arguments += 1
throw new Error('arguments accessor broke')
},
},
}) as unknown as ToolExecutionInput
const result = await ctx.tools.execute(input)
expect(reads).toEqual({ callId: 1, name: 1, arguments: 1 })
expect(result).toMatchObject({ callId: CallId('one-read-call'), isError: true })
expect(result.content[0]).toMatchObject({ text: 'Error: arguments accessor broke' })
expect(observed).toEqual({ callId: CallId('one-read-call'), name: 'missing', isError: true })
})
it('reads callId once before a hostile name accessor rejects correlation', async () => {
const ctx = await setup()
const reads = { callId: 0, name: 0 }
let observed = 0
ctx.on('tools/result', () => { observed += 1 })
const input = Object.defineProperties({ arguments: {} }, {
callId: {
enumerable: true,
get: () => {
reads.callId += 1
return CallId('hostile-name')
},
},
name: {
enumerable: true,
get: () => {
reads.name += 1
throw new Error('name accessor broke')
},
},
}) as unknown as ToolExecutionInput
await expect(ctx.tools.execute(input)).rejects.toThrow('name accessor broke')
expect(reads).toEqual({ callId: 1, name: 1 })
expect(observed).toBe(0)
})
it('threads a tool-attached meta (object return form) onto the result', async () => {
const ctx = await setup()
ctx.tools.register({