mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
feat(tools): require cancellation signal on every invocation
This commit is contained in:
@@ -366,7 +366,7 @@ export function apply(ctx: Context, config: Config = {}): void {
|
||||
toolName: 'bash',
|
||||
callId: exec.callId,
|
||||
reason: `escalate sandbox to ${mode}: ${justification}`,
|
||||
...exec.signal ? { signal: exec.signal } : {},
|
||||
signal: exec.signal,
|
||||
})
|
||||
switch (outcome) {
|
||||
case 'allowed-once': return mode as SandboxMode
|
||||
@@ -437,8 +437,6 @@ export function apply(ctx: Context, config: Config = {}): void {
|
||||
if (tasks === undefined) {
|
||||
throw new Error('background tasks unavailable: load @deepseek-ai/dsh-tasks and @deepseek-ai/dsh-tool-tasks')
|
||||
}
|
||||
// Reject pre-start cancellation; returned tasks use their own lifecycle.
|
||||
if (exec.signal?.aborted) throw new Error('command aborted')
|
||||
// Task preflight finishes before the starter can spawn a process.
|
||||
const id = tasks.start({
|
||||
kind: 'bash',
|
||||
@@ -457,7 +455,7 @@ export function apply(ctx: Context, config: Config = {}): void {
|
||||
}
|
||||
const result = await ctx.bash.run(ctx.bash.resolve({
|
||||
...request,
|
||||
...exec.signal ? { signal: exec.signal } : {},
|
||||
signal: exec.signal,
|
||||
}))
|
||||
if (result.aborted) throw new Error('command aborted')
|
||||
return [{ type: 'text', text: renderResult(result, escalationModes) }]
|
||||
|
||||
@@ -7,10 +7,13 @@ import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import type { ToolExecution } from '@deepseek-ai/dsh-tools'
|
||||
import { BashEnvRegistry } from '@deepseek-ai/dsh-tool-bash'
|
||||
|
||||
const testToolSignal = new AbortController().signal
|
||||
|
||||
afterEach(() => vi.unstubAllEnvs())
|
||||
|
||||
function execution(sessionId?: string): ToolExecution {
|
||||
return {
|
||||
signal: testToolSignal,
|
||||
token: Symbol('bash-env-test') as ToolExecution['token'],
|
||||
callId: CallId('bash-env-call'),
|
||||
name: 'bash',
|
||||
|
||||
@@ -7,7 +7,7 @@ import { CallId } from '@deepseek-ai/dsh-llm'
|
||||
import { BashExecutor } from '@deepseek-ai/dsh-bash'
|
||||
import type { BashExecRequest, BashExecSpec, BashProcess, BashProcessRead, BashRunResult } from '@deepseek-ai/dsh-bash'
|
||||
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
|
||||
import ToolRegistry from '@deepseek-ai/dsh-tools'
|
||||
import ToolRegistry, { TOOL_ABORTED_BEFORE_DISPATCH } from '@deepseek-ai/dsh-tools'
|
||||
import AgentRegistry from '@deepseek-ai/dsh-agent'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
|
||||
@@ -21,6 +21,8 @@ import * as ToolBash from '@deepseek-ai/dsh-tool-bash'
|
||||
import { processOutcome } from '../src/background.ts'
|
||||
import { renderProcessRead, renderResult } from '../src/render.ts'
|
||||
|
||||
const testToolSignal = new AbortController().signal
|
||||
|
||||
const spillDir = mkdtempSync(join(tmpdir(), 'dsh-tool-bash-spec-'))
|
||||
|
||||
/** Foreground-only harness: no task runtime (backgrounding fails loud here). */
|
||||
@@ -67,7 +69,7 @@ function registerFakeAgent(ctx: Context, sessionId: string, inject: (...args: un
|
||||
}
|
||||
let callCounter = 0
|
||||
function call(ctx: Context, name: string, args: unknown, agent?: Agent) {
|
||||
return ctx.tools.execute({ callId: CallId(`call-${++callCounter}`), name, arguments: args, ...agent ? { agent } : {} })
|
||||
return ctx.tools.execute({ signal: testToolSignal, callId: CallId(`call-${++callCounter}`), name, arguments: args, ...agent ? { agent } : {} })
|
||||
}
|
||||
|
||||
function text(result: { content: { type: string; text?: string }[] }): string {
|
||||
@@ -451,7 +453,7 @@ describe('background execution through the task runtime', () => {
|
||||
expect(text(result)).toContain('background tasks unavailable: load @deepseek-ai/dsh-tasks and @deepseek-ai/dsh-tool-tasks')
|
||||
})
|
||||
|
||||
it('a pre-aborted call refuses to start: isError, no process spawned', async () => {
|
||||
it('a pre-aborted call is skipped before the process starts', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SystemPrompt)
|
||||
await ctx.plugin(ToolRegistry)
|
||||
@@ -470,7 +472,8 @@ describe('background execution through the task runtime', () => {
|
||||
signal: controller.signal,
|
||||
})
|
||||
expect(result.isError).toBe(true)
|
||||
expect(text(result)).toContain('command aborted')
|
||||
expect(result.error).toEqual({ name: 'AbortError', code: TOOL_ABORTED_BEFORE_DISPATCH })
|
||||
expect(text(result)).toBe('Error: tool call aborted before dispatch')
|
||||
expect((ctx.bash as CountingStartExecutor).starts).toBe(0)
|
||||
})
|
||||
|
||||
@@ -730,7 +733,7 @@ describe('session-cwd routing (per-session workdir)', () => {
|
||||
it('falls back to the executor default when the agent has no session cwd', async () => {
|
||||
const ctx = await setup()
|
||||
// No exec.agent at all → executor uses its config/process.cwd() default.
|
||||
const result = await ctx.tools.execute({ callId: CallId('cwd-noagent'), name: 'bash', arguments: { command: 'pwd', description: 'pwd' } })
|
||||
const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('cwd-noagent'), name: 'bash', arguments: { command: 'pwd', description: 'pwd' } })
|
||||
expect(result.isError).toBe(false)
|
||||
expect(text(result).trim().length).toBeGreaterThan(0)
|
||||
})
|
||||
@@ -1012,6 +1015,7 @@ describe('the model-facing bash tool builds its request from named args only (no
|
||||
const path = ctx.sessionPersistence.locate(agent.session.header)?.path
|
||||
|
||||
await ctx.tools.execute({
|
||||
signal: testToolSignal,
|
||||
callId: CallId('session-env-fg'),
|
||||
name: 'bash',
|
||||
arguments: { command: 'true', description: 'run command' },
|
||||
@@ -1032,6 +1036,7 @@ describe('the model-facing bash tool builds its request from named args only (no
|
||||
const path = ctx.sessionPersistence.locate(agent.session.header)?.path
|
||||
|
||||
await ctx.tools.execute({
|
||||
signal: testToolSignal,
|
||||
callId: CallId('session-env-bg'),
|
||||
name: 'bash',
|
||||
arguments: {
|
||||
@@ -1058,6 +1063,7 @@ describe('the model-facing bash tool builds its request from named args only (no
|
||||
const ambient = process.env.DSH_SESSION_ID
|
||||
|
||||
await ctx.tools.execute({
|
||||
signal: testToolSignal,
|
||||
callId: CallId('session-env-id-only'),
|
||||
name: 'bash',
|
||||
arguments: { command: 'true', description: 'run command' },
|
||||
@@ -1079,6 +1085,7 @@ describe('the model-facing bash tool builds its request from named args only (no
|
||||
|
||||
for (const [callId, agent] of [['parent', parent], ['child', child]] as const) {
|
||||
await ctx.tools.execute({
|
||||
signal: testToolSignal,
|
||||
callId: CallId(`session-env-${callId}`),
|
||||
name: 'bash',
|
||||
arguments: { command: 'true', description: 'run command' },
|
||||
@@ -1109,6 +1116,7 @@ describe('the model-facing bash tool builds its request from named args only (no
|
||||
// This preserves the request shape; it is not a security boundary because shell syntax can
|
||||
// already set environment variables or feed stdin.
|
||||
await ctx.tools.execute({
|
||||
signal: testToolSignal,
|
||||
callId: CallId('no-forward-1'),
|
||||
name: 'bash',
|
||||
arguments: {
|
||||
@@ -1130,6 +1138,7 @@ describe('the model-facing bash tool builds its request from named args only (no
|
||||
it('a background bash call likewise carries no trusted-only fields', async () => {
|
||||
const { ctx, bash } = await setupRecording()
|
||||
const result = await ctx.tools.execute({
|
||||
signal: testToolSignal,
|
||||
callId: CallId('no-forward-2'),
|
||||
name: 'bash',
|
||||
arguments: {
|
||||
|
||||
Reference in New Issue
Block a user