Parcourir la source

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
Yichen Jiang il y a 1 mois
Parent
commit
5d86a284e5

+ 9 - 11
apps/cli/config/agent-presets/code/agent.cordis.yml

@@ -70,17 +70,15 @@
 
 
 # ── background tasks ────────────────────────────────────────────────────────
 # ── 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 ──────────────────────────────────────────────────────────────────
 # ── skills ──────────────────────────────────────────────────────────────────
 
 

+ 9 - 11
apps/cli/config/agent-presets/cordis/agent.cordis.yml

@@ -64,17 +64,15 @@
 
 
 # ── background tasks ────────────────────────────────────────────────────────
 # ── 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 ───────────────────────────────────────────────────────────────────
 # ── goals ───────────────────────────────────────────────────────────────────
 
 

+ 8 - 0
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
 # 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'

+ 9 - 11
apps/cli/config/agent-presets/standard/agent.cordis.yml

@@ -63,17 +63,15 @@
 
 
 # ── background tasks ────────────────────────────────────────────────────────
 # ── 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 ──────────────────────────────────────────────────────────────────
 # ── skills ──────────────────────────────────────────────────────────────────
 
 

+ 62 - 0
apps/web/tests/shipped-composition.e2e.ts

@@ -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)

+ 10 - 3
packages/bundle/web-app/cordis.patch.yml

@@ -218,10 +218,17 @@
 - id: tool-bash
 - id: tool-bash
   disabled: true
   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
   disabled: true
 
 
 - id: tool-fs
 - id: tool-fs