From 5d86a284e548ccfac0557cd2ea4ff106ac9e1306 Mon Sep 17 00:00:00 2001 From: Yichen Jiang Date: Mon, 10 Aug 2026 14:18:41 +0800 Subject: [PATCH] fix(web-app,agent-presets): keep the task registry on the host plane MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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 --- .../agent-presets/code/agent.cordis.yml | 20 +++--- .../agent-presets/cordis/agent.cordis.yml | 20 +++--- .../agent-presets/minimal/agent.cordis.yml | 8 +++ .../agent-presets/standard/agent.cordis.yml | 20 +++--- apps/web/tests/shipped-composition.e2e.ts | 62 +++++++++++++++++++ packages/bundle/web-app/cordis.patch.yml | 13 +++- 6 files changed, 107 insertions(+), 36 deletions(-) diff --git a/apps/cli/config/agent-presets/code/agent.cordis.yml b/apps/cli/config/agent-presets/code/agent.cordis.yml index 65d2716458..d068e00dea 100644 --- a/apps/cli/config/agent-presets/code/agent.cordis.yml +++ b/apps/cli/config/agent-presets/code/agent.cordis.yml @@ -70,17 +70,15 @@ # ── background tasks ──────────────────────────────────────────────────────── -- id: tasks - name: cordis:group - group: true - isolate: - tasks: true - config: - - id: tasks-local - name: '@deepseek-ai/dsh-tasks-local' - - - id: tool-tasks - name: '@deepseek-ai/dsh-tool-tasks' +# Only the model-facing controls. The task REGISTRY stays on the host plane: +# its producers sit outside any realm this file could put it in — `tool-bash` +# above resolves it with `ctx.get`, and an entry-local realm here is invisible +# to every sibling row, so `run_in_background` would answer "background tasks +# unavailable" while these controls sat in the catalog. The registry is keyed by +# owning agent anyway, so one host instance serves every session. What a preset +# chooses is whether its agent can collect and stop background work at all. +- id: tool-tasks + name: '@deepseek-ai/dsh-tool-tasks' # ── skills ────────────────────────────────────────────────────────────────── diff --git a/apps/cli/config/agent-presets/cordis/agent.cordis.yml b/apps/cli/config/agent-presets/cordis/agent.cordis.yml index f2cdeea159..91fa28a31d 100644 --- a/apps/cli/config/agent-presets/cordis/agent.cordis.yml +++ b/apps/cli/config/agent-presets/cordis/agent.cordis.yml @@ -64,17 +64,15 @@ # ── background tasks ──────────────────────────────────────────────────────── -- id: tasks - name: cordis:group - group: true - isolate: - tasks: true - config: - - id: tasks-local - name: '@deepseek-ai/dsh-tasks-local' - - - id: tool-tasks - name: '@deepseek-ai/dsh-tool-tasks' +# Only the model-facing controls. The task REGISTRY stays on the host plane: +# its producers sit outside any realm this file could put it in — `tool-bash` +# above resolves it with `ctx.get`, and an entry-local realm here is invisible +# to every sibling row, so `run_in_background` would answer "background tasks +# unavailable" while these controls sat in the catalog. The registry is keyed by +# owning agent anyway, so one host instance serves every session. What a preset +# chooses is whether its agent can collect and stop background work at all. +- id: tool-tasks + name: '@deepseek-ai/dsh-tool-tasks' # ── goals ─────────────────────────────────────────────────────────────────── diff --git a/apps/cli/config/agent-presets/minimal/agent.cordis.yml b/apps/cli/config/agent-presets/minimal/agent.cordis.yml index 8ca6f0dcdf..cbccafe160 100644 --- a/apps/cli/config/agent-presets/minimal/agent.cordis.yml +++ b/apps/cli/config/agent-presets/minimal/agent.cordis.yml @@ -22,8 +22,16 @@ # 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 # 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 name: '@deepseek-ai/dsh-tool-bash' + config: + enableRunInBackground: false - id: tool-str-replace-editor name: '@deepseek-ai/dsh-tool-str-replace-editor' diff --git a/apps/cli/config/agent-presets/standard/agent.cordis.yml b/apps/cli/config/agent-presets/standard/agent.cordis.yml index 66407faf1d..f73f4b3fba 100644 --- a/apps/cli/config/agent-presets/standard/agent.cordis.yml +++ b/apps/cli/config/agent-presets/standard/agent.cordis.yml @@ -63,17 +63,15 @@ # ── background tasks ──────────────────────────────────────────────────────── -- id: tasks - name: cordis:group - group: true - isolate: - tasks: true - config: - - id: tasks-local - name: '@deepseek-ai/dsh-tasks-local' - - - id: tool-tasks - name: '@deepseek-ai/dsh-tool-tasks' +# Only the model-facing controls. The task REGISTRY stays on the host plane: +# its producers sit outside any realm this file could put it in — `tool-bash` +# above resolves it with `ctx.get`, and an entry-local realm here is invisible +# to every sibling row, so `run_in_background` would answer "background tasks +# unavailable" while these controls sat in the catalog. The registry is keyed by +# owning agent anyway, so one host instance serves every session. What a preset +# chooses is whether its agent can collect and stop background work at all. +- id: tool-tasks + name: '@deepseek-ai/dsh-tool-tasks' # ── skills ────────────────────────────────────────────────────────────────── diff --git a/apps/web/tests/shipped-composition.e2e.ts b/apps/web/tests/shipped-composition.e2e.ts index 5d929044b3..8241dfc0bc 100644 --- a/apps/web/tests/shipped-composition.e2e.ts +++ b/apps/web/tests/shipped-composition.e2e.ts @@ -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) diff --git a/packages/bundle/web-app/cordis.patch.yml b/packages/bundle/web-app/cordis.patch.yml index e4c4935a2a..ed08451f1b 100644 --- a/packages/bundle/web-app/cordis.patch.yml +++ b/packages/bundle/web-app/cordis.patch.yml @@ -218,10 +218,17 @@ - id: tool-bash disabled: true -- id: tool-tasks - disabled: true +# The background-task REGISTRY stays on the host plane; only the model-facing +# `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 - id: tool-fs