From f1d09ad4d1a5b30bfc7bbb84851d5fe34fcc24c8 Mon Sep 17 00:00:00 2001 From: Yichen Jiang Date: Fri, 7 Aug 2026 02:33:53 +0800 Subject: [PATCH] fix(web): keep host-plane services out of the preset, and pin the lane's skill roots MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moving the agent plane behind per-session presets took five rows with it that the host still owns, and the Web surface stopped booting: `host-apiproxy` injects `subagents`, so with the registry disabled here the entry never activated and `dsh web` died at plugin-tree load. The criterion is injection, not subject matter. A host row that injects a service resolves it before any session exists, so there is no agent to key by: `bash-env` (which `apps/cli/src/web.ts` injects to publish `DSH_WEB_URL`), the `subagents` registry and its spawn/fork backends (a process singleton whose cross-session queries the api-proxy serves, and whose provider names are globally unique), and `tool-subagent-report` (a continuable setup on that singleton, registered once per live session by a list that is not scope-aware) all stay host-plane. What a preset chooses is which delegation TOOLS it sees. The browser lane needs the second half: skill roots now resolve inside a preset, a subtree the lane's include patches cannot reach, so the row's documented environment fallback is pinned for the whole scaffold lifetime — presets mount when a session is created, not at boot. Without it a developer's real ~/.dsh/skills enters replay requests and goldens while CI sees none. --- .../agent-presets/core-web/agent.cordis.yml | 20 ++----- .../agent-presets/standard/agent.cordis.yml | 58 ++++++++----------- apps/web/tests/scaffold.ts | 27 +++++++++ packages/bundle/web-app/cordis.patch.yml | 28 +++++---- 4 files changed, 72 insertions(+), 61 deletions(-) diff --git a/apps/cli/config/agent-presets/core-web/agent.cordis.yml b/apps/cli/config/agent-presets/core-web/agent.cordis.yml index 9a251461c5..e9003aa975 100644 --- a/apps/cli/config/agent-presets/core-web/agent.cordis.yml +++ b/apps/cli/config/agent-presets/core-web/agent.cordis.yml @@ -15,20 +15,12 @@ text: >- You are a coding agent powered by the {{model}} model. Your working directory is {{cwd}}. -# `tool-bash` provides the `bashEnv` service, so it needs a realm even alone. -- id: shell - name: cordis:group - group: true - isolate: - bashEnv: true - config: - # The registry and its consumer share the realm: a consumer left outside - # would resolve the host's `bashEnv`, which this plane no longer provides. - - id: bash-env - name: '@deepseek-ai/dsh-bash-env' - - - id: tool-bash - name: '@deepseek-ai/dsh-tool-bash' +# `bash-env` stays in the HOST composition: `apps/cli/src/web.ts` injects it to +# publish `DSH_WEB_URL`/`DSH_WEB_MODE`, and a host row that injects a service is +# the criterion for host-plane ownership. `tool-bash` consumes that host +# registry from here. +- id: tool-bash + name: '@deepseek-ai/dsh-tool-bash' - 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 407e4d7d27..0a211bc97f 100644 --- a/apps/cli/config/agent-presets/standard/agent.cordis.yml +++ b/apps/cli/config/agent-presets/standard/agent.cordis.yml @@ -31,22 +31,15 @@ # ── shell ─────────────────────────────────────────────────────────────────── -# `tool-bash` reads as a tool but provides the `bashEnv` service, so it needs a -# realm like any other provider. The executor behind it (`bash-sandbox`) stays -# in the host composition, where the sandbox policy owns it. -- id: shell - name: cordis:group - group: true - isolate: - bashEnv: true - config: - # The registry and its consumer share the realm: a consumer left outside - # would resolve the host's `bashEnv`, which this plane no longer provides. - - id: bash-env - name: '@deepseek-ai/dsh-bash-env' - - - id: tool-bash - name: '@deepseek-ai/dsh-tool-bash' +# `bash-env` stays in the HOST composition: `apps/cli/src/web.ts` injects it to +# publish `DSH_WEB_URL`/`DSH_WEB_MODE`, and a host row that injects a service is +# the criterion for host-plane ownership — injection resolves before any session +# exists, so there is no agent to key by. Behind a preset realm those variables +# 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. +- id: tool-bash + name: '@deepseek-ai/dsh-tool-bash' # ── filesystem ────────────────────────────────────────────────────────────── @@ -172,29 +165,21 @@ # ── delegation and workflows ──────────────────────────────────────────────── -# Every backend and every tool that reaches `subagents` or `workflows` shares -# one realm: a consumer left outside it would resolve the host's registry -# instead, which this preset does not populate. +# The `subagents` registry and its spawn/fork backends live in the HOST +# composition: the registry is a process singleton whose cross-session queries +# the api-proxy serves to the browser, and a provider name may only be +# registered once. This preset contributes the delegation TOOLS, which resolve +# that host registry. +# +# `workflows` is different — nothing outside an agent reads it — so every row +# that reaches it shares one entry-local realm here, and a consumer left +# outside would resolve a host registry this preset does not populate. - id: delegation name: cordis:group group: true isolate: - subagents: true workflows: true config: - - id: subagent - name: '@deepseek-ai/dsh-subagent' - - - id: subagent-spawn - name: '@deepseek-ai/dsh-subagent-spawn' - config: - providerName: spawn - - - id: subagent-fork - name: '@deepseek-ai/dsh-subagent-fork' - config: - providerName: fork - - id: tool-subagent-control name: '@deepseek-ai/dsh-tool-subagent-control' @@ -215,8 +200,11 @@ toolName: subagent_fork backgroundMode: continuable - - id: tool-subagent-report - name: '@deepseek-ai/dsh-tool-subagent-report' + # `tool-subagent-report` is host-plane for the same reason as the registry, + # not because a preset may not want it: it registers a CONTINUABLE SETUP on + # that singleton rather than a tool this agent calls, and the setup list is + # not scope-aware — one copy per mounted preset means every child gets + # `report` registered once per live session, which throws on the second. - id: workflow-workerthread name: '@deepseek-ai/dsh-workflow-workerthread' diff --git a/apps/web/tests/scaffold.ts b/apps/web/tests/scaffold.ts index 8d8f866740..b1c6d66bf9 100644 --- a/apps/web/tests/scaffold.ts +++ b/apps/web/tests/scaffold.ts @@ -240,6 +240,31 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise [key, process.env[key]]), + ) + let skillRootEnvironmentRestored = false + const restoreSkillRootEnvironment = (): void => { + if (skillRootEnvironmentRestored) return + skillRootEnvironmentRestored = true + for (const [key, value] of Object.entries(originalSkillRootEnvironment)) { + if (value === undefined) Reflect.deleteProperty(process.env, key) + else process.env[key] = value + } + } + Object.assign(process.env, skillRootEnvironment) let persistenceRoot: string try { persistenceRoot = await mkdtemp(join(tmpdir(), 'dsh-web-e2e-sessions-')) @@ -415,6 +440,7 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise 0) { throw new AggregateError([error, ...cleanupFailures], 'web scaffold setup failed and cleanup was incomplete') } @@ -462,6 +488,7 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise 0) throw new AggregateError(failures, 'web scaffold teardown failed') }, diff --git a/packages/bundle/web-app/cordis.patch.yml b/packages/bundle/web-app/cordis.patch.yml index 356d0a95a7..2fe73e7dd5 100644 --- a/packages/bundle/web-app/cordis.patch.yml +++ b/packages/bundle/web-app/cordis.patch.yml @@ -204,8 +204,11 @@ # absent from a surface overlay would silently reappear the day someone reorders # the composition. -- id: bash-env - disabled: true +# `bash-env` STAYS in the host plane: `apps/cli/src/web.ts` injects it to +# publish `DSH_WEB_URL`/`DSH_WEB_MODE`, and a host row that injects a service is +# the criterion for host-plane ownership — injection resolves before any session +# exists, so there is no agent to key by. Behind a preset realm those variables +# would never reach the model's shell at all. - id: tool-bash disabled: true @@ -261,14 +264,12 @@ - id: tool-result-prune disabled: true -- id: subagent - disabled: true - -- id: subagent-spawn - disabled: true - -- id: subagent-fork - disabled: true +# The subagent registry and its backends STAY in the host plane. `subagents` is +# a process singleton with a cross-session query surface (`listChildren`, +# `followup`) that the host api-proxy serves to the browser, and a provider +# registers under a globally unique name, so a per-session copy would both +# starve that host row and collide on the second session. What a preset +# chooses is which delegation TOOLS its agent sees, below. - id: tool-subagent-control disabled: true @@ -282,8 +283,11 @@ - id: tool-subagent-fork disabled: true -- id: tool-subagent-report - disabled: true +# `tool-subagent-report` is host-plane for the same reason as the registry, not +# because a preset may not want it: it registers a CONTINUABLE SETUP on that +# singleton rather than a tool this agent calls, and the setup list is not +# scope-aware — one copy per mounted preset means every child gets `report` +# registered once per live session, which throws on the second. - id: workflow-workerthread disabled: true