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:
@@ -70,17 +70,15 @@
|
|||||||
|
|
||||||
# ── background tasks ────────────────────────────────────────────────────────
|
# ── background tasks ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
- id: tasks
|
# Only the model-facing controls. The task REGISTRY stays on the host plane:
|
||||||
name: cordis:group
|
# its producers sit outside any realm this file could put it in — `tool-bash`
|
||||||
group: true
|
# above resolves it with `ctx.get`, and an entry-local realm here is invisible
|
||||||
isolate:
|
# to every sibling row, so `run_in_background` would answer "background tasks
|
||||||
tasks: true
|
# unavailable" while these controls sat in the catalog. The registry is keyed by
|
||||||
config:
|
# owning agent anyway, so one host instance serves every session. What a preset
|
||||||
- id: tasks-local
|
# chooses is whether its agent can collect and stop background work at all.
|
||||||
name: '@deepseek-ai/dsh-tasks-local'
|
- id: tool-tasks
|
||||||
|
name: '@deepseek-ai/dsh-tool-tasks'
|
||||||
- id: tool-tasks
|
|
||||||
name: '@deepseek-ai/dsh-tool-tasks'
|
|
||||||
|
|
||||||
# ── skills ──────────────────────────────────────────────────────────────────
|
# ── skills ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
@@ -64,17 +64,15 @@
|
|||||||
|
|
||||||
# ── background tasks ────────────────────────────────────────────────────────
|
# ── background tasks ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
- id: tasks
|
# Only the model-facing controls. The task REGISTRY stays on the host plane:
|
||||||
name: cordis:group
|
# its producers sit outside any realm this file could put it in — `tool-bash`
|
||||||
group: true
|
# above resolves it with `ctx.get`, and an entry-local realm here is invisible
|
||||||
isolate:
|
# to every sibling row, so `run_in_background` would answer "background tasks
|
||||||
tasks: true
|
# unavailable" while these controls sat in the catalog. The registry is keyed by
|
||||||
config:
|
# owning agent anyway, so one host instance serves every session. What a preset
|
||||||
- id: tasks-local
|
# chooses is whether its agent can collect and stop background work at all.
|
||||||
name: '@deepseek-ai/dsh-tasks-local'
|
- id: tool-tasks
|
||||||
|
name: '@deepseek-ai/dsh-tool-tasks'
|
||||||
- id: tool-tasks
|
|
||||||
name: '@deepseek-ai/dsh-tool-tasks'
|
|
||||||
|
|
||||||
# ── goals ───────────────────────────────────────────────────────────────────
|
# ── goals ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
@@ -22,8 +22,16 @@
|
|||||||
# never reached the model's shell at all. `tool-bash` consumes the host registry
|
# never reached the model's shell at all. `tool-bash` consumes the host registry
|
||||||
# from here; the executor behind it (`bash-sandbox`) is host-plane too, where the
|
# from here; the executor behind it (`bash-sandbox`) is host-plane too, where the
|
||||||
# sandbox policy owns it.
|
# sandbox policy owns it.
|
||||||
|
#
|
||||||
|
# `run_in_background` is off because this preset mounts no `tool-tasks`: the
|
||||||
|
# host task registry gates starts on SOME control surface being attached, and
|
||||||
|
# that set is process-wide, so another preset's controls would let this agent
|
||||||
|
# start work it has no `task_output` to collect. Disabling drops the parameter
|
||||||
|
# from the schema too, which is the honest surface for a two-tool benchmark.
|
||||||
- id: tool-bash
|
- id: tool-bash
|
||||||
name: '@deepseek-ai/dsh-tool-bash'
|
name: '@deepseek-ai/dsh-tool-bash'
|
||||||
|
config:
|
||||||
|
enableRunInBackground: false
|
||||||
|
|
||||||
- id: tool-str-replace-editor
|
- id: tool-str-replace-editor
|
||||||
name: '@deepseek-ai/dsh-tool-str-replace-editor'
|
name: '@deepseek-ai/dsh-tool-str-replace-editor'
|
||||||
|
|||||||
@@ -63,17 +63,15 @@
|
|||||||
|
|
||||||
# ── background tasks ────────────────────────────────────────────────────────
|
# ── background tasks ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
- id: tasks
|
# Only the model-facing controls. The task REGISTRY stays on the host plane:
|
||||||
name: cordis:group
|
# its producers sit outside any realm this file could put it in — `tool-bash`
|
||||||
group: true
|
# above resolves it with `ctx.get`, and an entry-local realm here is invisible
|
||||||
isolate:
|
# to every sibling row, so `run_in_background` would answer "background tasks
|
||||||
tasks: true
|
# unavailable" while these controls sat in the catalog. The registry is keyed by
|
||||||
config:
|
# owning agent anyway, so one host instance serves every session. What a preset
|
||||||
- id: tasks-local
|
# chooses is whether its agent can collect and stop background work at all.
|
||||||
name: '@deepseek-ai/dsh-tasks-local'
|
- id: tool-tasks
|
||||||
|
name: '@deepseek-ai/dsh-tool-tasks'
|
||||||
- id: tool-tasks
|
|
||||||
name: '@deepseek-ai/dsh-tool-tasks'
|
|
||||||
|
|
||||||
# ── skills ──────────────────────────────────────────────────────────────────
|
# ── skills ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
// surface itself.
|
// surface itself.
|
||||||
import { tmpdir } from 'node:os'
|
import { tmpdir } from 'node:os'
|
||||||
import { afterEach, expect, it } from 'vitest'
|
import { afterEach, expect, it } from 'vitest'
|
||||||
|
import { CallId } from '@deepseek-ai/dsh-llm'
|
||||||
import { canonicalPath, writableRoots } from '@deepseek-ai/dsh-sandbox'
|
import { canonicalPath, writableRoots } from '@deepseek-ai/dsh-sandbox'
|
||||||
import { SessionId } from '@deepseek-ai/dsh-session'
|
import { SessionId } from '@deepseek-ai/dsh-session'
|
||||||
// Empty type imports carry the tools/sandboxPolicy/approval Context merges.
|
// 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()
|
await commandHandle.dispose()
|
||||||
}
|
}
|
||||||
}, 120_000)
|
}, 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)
|
||||||
|
|||||||
@@ -218,10 +218,17 @@
|
|||||||
- id: tool-bash
|
- id: tool-bash
|
||||||
disabled: true
|
disabled: true
|
||||||
|
|
||||||
- id: tool-tasks
|
# The background-task REGISTRY stays on the host plane; only the model-facing
|
||||||
disabled: true
|
# `task_*` controls move. Its producers — `tool-bash` here, `tool-pty` and a
|
||||||
|
# non-continuable `tool-subagent` elsewhere — are preset rows that resolve it
|
||||||
|
# with `ctx.get`, and an entry-local realm around the registry is invisible to
|
||||||
|
# every sibling row outside that realm, so `run_in_background` answered
|
||||||
|
# "background tasks unavailable" while the controls sat in the catalog. 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. The registry is keyed by owning
|
||||||
|
# agent, so one host instance serves every session exactly as before presets.
|
||||||
|
|
||||||
- id: tasks
|
- id: tool-tasks
|
||||||
disabled: true
|
disabled: true
|
||||||
|
|
||||||
- id: tool-fs
|
- id: tool-fs
|
||||||
|
|||||||
Reference in New Issue
Block a user