fix(web-app,agent-presets): keep the task registry on the host plane
`tool-bash` resolves the background-task registry with `ctx.get('tasks')`,
and it sits at the preset's top level. The registry sat inside an
entry-local `isolate: { tasks: true }` realm, which is invisible to every
sibling row outside it, while the Web surface disabled the host row — so
both lookups missed and every `run_in_background` call answered
"background tasks unavailable" with `task_output`, `task_list`, and
`task_kill` still listed in the catalog. `task_list` returning
"(no background tasks)" is what made the outage read as an empty queue
rather than a severed producer.
That is the `goals` criterion read from inside the preset: a Service a row
outside its realm READS belongs to the plane both can see. `tasks` already
keys access by owning agent (`assertAccess` compares `task.owner.id`) and
mints an independent token per `attachSurface` call, so one host instance
serves every session exactly as before presets — the per-preset-standing-mounts
note records that sharing `tasks-local` is a return to its design.
`minimal` mounts no `tool-tasks`, and the `start()` control-surface gate is
a service-wide set that another preset's controls would open for it, so its
`tool-bash` disables `run_in_background` and drops the parameter from the
schema.
Fixes #2141
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
// surface itself.
|
||||
import { tmpdir } from 'node:os'
|
||||
import { afterEach, expect, it } from 'vitest'
|
||||
import { CallId } from '@deepseek-ai/dsh-llm'
|
||||
import { canonicalPath, writableRoots } from '@deepseek-ai/dsh-sandbox'
|
||||
import { SessionId } from '@deepseek-ai/dsh-session'
|
||||
// Empty type imports carry the tools/sandboxPolicy/approval Context merges.
|
||||
@@ -114,3 +115,64 @@ it('assembles the shipped Web catalog with the confined access default', async (
|
||||
await commandHandle.dispose()
|
||||
}
|
||||
}, 120_000)
|
||||
|
||||
it('lets a preset producer reach the background-task registry', async () => {
|
||||
scaffold = await launchWebScaffold()
|
||||
const ctx = scaffold.ctx
|
||||
const handle = await ctx.agents.create({
|
||||
sessionId: SessionId('shipped-background-task'),
|
||||
meta: { cwd: scaffold.workspaceCwd },
|
||||
setup: agentCtx => ctx.agentPresets.mount(agentCtx).then(() => undefined),
|
||||
})
|
||||
try {
|
||||
const signal = new AbortController().signal
|
||||
// `tool-bash` is a preset row and `tasks` is a host registry; the producer
|
||||
// resolves it with `ctx.get`, so a registry hidden behind a preset realm
|
||||
// fails here — with every task control still listed in the catalog above.
|
||||
const started = await ctx.tools.execute({
|
||||
signal,
|
||||
callId: CallId('shipped-bash-background'),
|
||||
name: 'bash',
|
||||
arguments: {
|
||||
command: 'printf SHIPPED_BACKGROUND_OK',
|
||||
description: 'shipped background probe',
|
||||
run_in_background: true,
|
||||
},
|
||||
agent: handle.agent,
|
||||
})
|
||||
expect({ isError: started.isError, content: started.content }).toEqual({
|
||||
isError: false,
|
||||
content: [{ type: 'text', text: 'started background task bash-1' }],
|
||||
})
|
||||
|
||||
// The control surface reads what the producer started: same registry, one
|
||||
// owner. A per-preset registry would list nothing here even on success.
|
||||
const listed = await ctx.tools.execute({
|
||||
signal,
|
||||
callId: CallId('shipped-task-list'),
|
||||
name: 'task_list',
|
||||
arguments: {},
|
||||
agent: handle.agent,
|
||||
})
|
||||
expect(listed.isError).toBe(false)
|
||||
expect(listed.content).toEqual([
|
||||
{ type: 'text', text: expect.stringContaining('bash-1 [bash]') as unknown as string },
|
||||
])
|
||||
|
||||
// The full round trip: the output a host-plane producer wrote is collected
|
||||
// through a preset-plane control, which is the linkage the realm severed.
|
||||
const collected = await ctx.tools.execute({
|
||||
signal,
|
||||
callId: CallId('shipped-task-output'),
|
||||
name: 'task_output',
|
||||
arguments: { task_id: 'bash-1', wait: true },
|
||||
agent: handle.agent,
|
||||
})
|
||||
expect(collected.isError).toBe(false)
|
||||
expect(collected.content).toEqual([
|
||||
{ type: 'text', text: expect.stringContaining('SHIPPED_BACKGROUND_OK') as unknown as string },
|
||||
])
|
||||
} finally {
|
||||
await handle.dispose()
|
||||
}
|
||||
}, 120_000)
|
||||
|
||||
Reference in New Issue
Block a user