From 921fcc1341f437b8b02422528c6ca5e780264d4c Mon Sep 17 00:00:00 2001 From: Yichen Jiang Date: Sat, 8 Aug 2026 23:20:55 +0800 Subject: [PATCH 1/2] fix(cli): patch the agent-preset roots for every dsh launcher The roots are an assembly fact (the shipped set beside this app's config, the user's own under $DSH_HOME) but only `dsh web` patched them in, so the merged `dsh run` booted the roster with no roots and failed resolving `standard`. The shared profile boot now owns the patch for every launcher, and master's interrupt_agent tool joins the standard composition's exact catalog. The roster helpers the wire layer added (standingKeyFor, serviceForAgent's chain guards, the direct mountPreset boundary) gain the unit coverage the per-file gate requires. --- apps/cli/src/profile-boot.ts | 25 ++++++++++++ apps/cli/src/web.ts | 20 ++-------- apps/cli/tests/web-agent-presets.e2e.ts | 2 +- .../preset/agent-presets/tests/mount.spec.ts | 39 ++++++++++++++++++- 4 files changed, 67 insertions(+), 19 deletions(-) diff --git a/apps/cli/src/profile-boot.ts b/apps/cli/src/profile-boot.ts index 4730ec7073..c62c2f140d 100644 --- a/apps/cli/src/profile-boot.ts +++ b/apps/cli/src/profile-boot.ts @@ -12,6 +12,7 @@ import { join, resolve } from 'node:path' import { fileURLToPath } from 'node:url' import type { Context } from 'cordis' import type { PatchOptions } from '@cordisjs/plugin-include' +import { dshHomePath } from '@deepseek-ai/dsh-paths' import { boot, composeEntries, @@ -25,6 +26,12 @@ import { type Profile, } from '@deepseek-ai/dsh-app-boot' import { resolveDshHome } from '@deepseek-ai/dsh-paths' + +/** Shipped agent-preset root: beside this app's own config, in both source and built layouts. */ +const SHIPPED_PRESET_ROOT = fileURLToPath(new URL('../config/agent-presets/', import.meta.url)) + +/** Harness-home directory holding locally authored agent presets. */ +const USER_PRESET_DIR = '.agent-presets' import { DSH_ENVIRONMENT_KEY, type EnvironmentSnapshot } from '@deepseek-ai/dsh-environment' import type { HeadlessIo } from '@deepseek-ai/dsh-headless' import { createProcessShutdown, type ProcessShutdown } from './process-shutdown.ts' @@ -147,6 +154,24 @@ function composeProfile( if (typeof row.id === 'string') rows.set(row.id, row) } const overlayAndFlags = [...overlays, ...deriveFlagPatches(rows)] + // The agent-preset roots are an assembly fact of every dsh launcher, not a + // patch author's choice: the shipped set sits beside this app's config and + // the user's own under the Harness home. Resolved per boot ($DSH_HOME may + // differ per run) and only patched when the composed tree actually mounts + // the roster — a one-shot `dsh run` composes agents from the same roster + // `dsh web` offers. + if (rows.has('agent-presets')) { + overlayAndFlags.push({ + id: 'agent-presets', + config: { + ...(rows.get('agent-presets')?.config ?? {}) as Record, + roots: [ + { path: SHIPPED_PRESET_ROOT, trust: 'system' }, + { path: dshHomePath(USER_PRESET_DIR), trust: 'user' }, + ], + }, + }) + } const telemetryPatch = resolveTelemetryPatch(process.env.DSH_TELEMETRY_DISABLED, rows.has(TELEMETRY_ROW_ID)) if (telemetryPatch !== undefined) overlayAndFlags.push(telemetryPatch) return { profile, bundlePatches, homePatches, overlayAndFlags, rows } diff --git a/apps/cli/src/web.ts b/apps/cli/src/web.ts index 2cf9b8229a..bdf301e2ae 100644 --- a/apps/cli/src/web.ts +++ b/apps/cli/src/web.ts @@ -10,7 +10,6 @@ import { networkInterfaces } from 'node:os' import { fileURLToPath } from 'node:url' -import { dshHomePath } from '@deepseek-ai/dsh-paths' import type { Context } from 'cordis' import type { PatchOptions } from '@cordisjs/plugin-include' import { addHarnessSourceSection } from '@deepseek-ai/dsh-app-boot' @@ -19,12 +18,6 @@ import { runProfile, type ProfileRows } from './profile-boot.ts' const SOURCE_ROOT = fileURLToPath(new URL('../../..', import.meta.url)) -/** Shipped agent-preset root: beside this app's own config, in both source and built layouts. */ -const SHIPPED_PRESET_ROOT = fileURLToPath(new URL('../config/agent-presets/', import.meta.url)) - -/** Harness-home directory holding locally authored agent presets. */ -const USER_PRESET_DIR = '.agent-presets' - /** The webserver schema's all-interfaces bind literal: gates LAN-authority derivation. */ const ALL_INTERFACES_HOST = '0.0.0.0' @@ -104,16 +97,9 @@ function deriveWebFlagPatches( // inserts the client-hmr row), never pass-throughs of composed values. put('web-runtime', 'mode', flags.dev ? 'development' : 'production') put('web-runtime', 'lanAddresses', lanAddresses) - // The agent-preset roots are an assembly fact, like the values above: the - // shipped set sits beside this app's config and the user's own under the - // Harness home, and neither location is something a patch author chooses. - // Only patched when the composed tree actually mounts the roster. - if (rows.has('agent-presets')) { - put('agent-presets', 'roots', [ - { path: SHIPPED_PRESET_ROOT, trust: 'system' }, - { path: dshHomePath(USER_PRESET_DIR), trust: 'user' }, - ]) - } + // The agent-preset roots are patched by the shared profile boot: they are + // an assembly fact of every dsh launcher, and `dsh run` composes agents + // from the same roster this alias offers. const patches = [...overrides.entries()].map(([id, bag]): PatchOptions => { const composed = rows.get(id) if (composed === undefined) throw new Error(`dsh: patch target row "${id}" not found in the web profile composition`) diff --git a/apps/cli/tests/web-agent-presets.e2e.ts b/apps/cli/tests/web-agent-presets.e2e.ts index 381b30503b..2ba21ed020 100644 --- a/apps/cli/tests/web-agent-presets.e2e.ts +++ b/apps/cli/tests/web-agent-presets.e2e.ts @@ -115,7 +115,7 @@ describe('the shipped Web composition', () => { // depend on ripgrep being present on the machine. expect(toolNames(ctx, handle.agent).filter(name => name !== 'glob' && name !== 'grep')).toEqual([ 'ask_user_question', 'bash', 'create_goal', 'edit', 'exit_plan_mode', - 'get_goal', 'list_agents', 'ralph', 'read', 'send_message', 'skill', + 'get_goal', 'interrupt_agent', 'list_agents', 'ralph', 'read', 'send_message', 'skill', 'str_replace_editor', 'subagent', 'subagent_fork', 'task_kill', 'task_list', 'task_output', 'todo_write', 'update_goal', 'web_search', 'workflow', 'write', diff --git a/packages/preset/agent-presets/tests/mount.spec.ts b/packages/preset/agent-presets/tests/mount.spec.ts index 49bdf94d64..dabb3c8320 100644 --- a/packages/preset/agent-presets/tests/mount.spec.ts +++ b/packages/preset/agent-presets/tests/mount.spec.ts @@ -12,7 +12,10 @@ import ToolRegistry from '@deepseek-ai/dsh-tools' import AgentRegistry, { assembleContextFor, type Agent } from '@deepseek-ai/dsh-agent' import AgentLoop from '@deepseek-ai/dsh-agent-loop' import { beforeEach, describe, expect, it } from 'vitest' -import AgentPresets, { COMPOSITION_FILE, leakedServices, livePresetMounts } from '@deepseek-ai/dsh-agent-presets' +import AgentPresets, { + COMPOSITION_FILE, leakedServices, livePresetMounts, mountPreset, serviceForAgent, +} from '@deepseek-ai/dsh-agent-presets' +import { createScope, scopeOf, setScopeParent } from '@deepseek-ai/dsh-scope' declare module 'cordis' { interface Context { @@ -182,11 +185,45 @@ describe('rejecting a composition that cannot be used', () => { }) it('answers undefined for a service the agent\'s preset does not mount', async () => { + // The isolated preset's standing instance exists in the same runtime, so + // the lookup finds the NAME and must still refuse it: the instance lives + // under another mount's fiber, not this agent's composition. + await agentOn(ctx, 'sess-reach-other', 'isolated') const agent = await agentOn(ctx, 'sess-reach-none', 'standard') expect(ctx.agentPresets.serviceFor(agent, 'fixtureIsolatedSvc')).toBeUndefined() }) + it('answers undefined for an agent outside the scope machinery', async () => { + // Unscoped, scoped-but-unparented, and parented to a key no live mount + // owns are the three ways a context can fail to name a standing mount; + // each is an answer, not a throw, because the caller asked a question. + expect(serviceForAgent(ctx, { ctx }, 'fixtureIsolatedSvc')).toBeUndefined() + const loner = createScope(ctx, { test: 'loner' }) + expect(serviceForAgent(ctx, { ctx: loner.ctx }, 'fixtureIsolatedSvc')).toBeUndefined() + const orphan = createScope(ctx, { test: 'orphan' }) + setScopeParent(scopeOf(orphan.ctx)!, { agentPreset: 'never-mounted' }) + expect(serviceForAgent(ctx, { ctx: orphan.ctx }, 'fixtureIsolatedSvc')).toBeUndefined() + }) + + it('refuses to mount a preset directly into an unscoped context', async () => { + // The service's own mount() guards this before delegating; the exported + // function is callable on its own, so the boundary holds there too. + const preset = await ctx.agentPresets.resolve('standard') + + await expect(mountPreset(ctx, preset)).rejects.toThrow(/unscoped context/) + }) + + it('hands a host reader the standing key without starting an agent', async () => { + const key = await ctx.agentPresets.standingKeyFor('minimal') + + // The mount exists for the reader; no agent, session, or turn started. + expect(key).toEqual({ agentPreset: 'minimal' }) + expect(ctx.agents.get(SessionId('minimal'))).toBeUndefined() + // A second reader resolves the same generation, not a new mount. + expect(await ctx.agentPresets.standingKeyFor('minimal')).toBe(key) + }) + it('reports the known ids when a preset is unknown', async () => { await expect(ctx.agentPresets.resolve('nope')) .rejects.toThrow(/preset "nope" not found \(available: .*standard/) From d58d3c9747ebce034596af55331a79af60829139 Mon Sep 17 00:00:00 2001 From: Yichen Jiang Date: Sat, 8 Aug 2026 23:35:16 +0800 Subject: [PATCH 2/2] test(web): re-record the master-lane goldens with the header preset label --- apps/web/tests/snapshots/skill-user-invoke/ui.expected.md | 3 +++ .../snapshots/subagent-interrupt/offline-composer.expected.md | 1 + 2 files changed, 4 insertions(+) diff --git a/apps/web/tests/snapshots/skill-user-invoke/ui.expected.md b/apps/web/tests/snapshots/skill-user-invoke/ui.expected.md index c77081584a..f8e7cf3c6c 100644 --- a/apps/web/tests/snapshots/skill-user-invoke/ui.expected.md +++ b/apps/web/tests/snapshots/skill-user-invoke/ui.expected.md @@ -25,6 +25,9 @@ - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write +- button "standard": + - text: standard + - img - button "Select model, current DeepSeek-V4-Flash": - text: DeepSeek-V4-Flash - img diff --git a/apps/web/tests/snapshots/subagent-interrupt/offline-composer.expected.md b/apps/web/tests/snapshots/subagent-interrupt/offline-composer.expected.md index a977afbbea..a265f84a85 100644 --- a/apps/web/tests/snapshots/subagent-interrupt/offline-composer.expected.md +++ b/apps/web/tests/snapshots/subagent-interrupt/offline-composer.expected.md @@ -19,5 +19,6 @@ - button "Commands" [disabled]: - img - 'button "Access mode, current: Workspace Write" [disabled]': Workspace Write +- text: standard - button "Stop generating" - button "Send message" [disabled]