From 50c46be9485767382a2639a93ed19e71329e8390 Mon Sep 17 00:00:00 2001 From: Yichen Jiang Date: Thu, 6 Aug 2026 20:11:44 +0800 Subject: [PATCH] fix(cli): isolate Web runtime context from headless --- ...026-07-28-web-agent-runtime-context.i18n.yaml | 4 ++-- .../2026-07-28-web-agent-runtime-context.md | 2 +- .../2026-07-28-web-agent-runtime-context.zh.md | 2 +- apps/cli/config/web.cordis.yml | 8 +++++--- apps/cli/src/app-cli-entry.ts | 16 ++++++++++++---- apps/cli/src/dump-config.ts | 2 ++ apps/cli/src/web.ts | 8 ++++++++ apps/cli/tests/built-bin.e2e.ts | 12 ++++++++++++ apps/web/tests/scaffold.ts | 4 +++- 9 files changed, 46 insertions(+), 12 deletions(-) diff --git a/.agents/notes/implemented/bug-fix/2026-07-28-web-agent-runtime-context.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-07-28-web-agent-runtime-context.i18n.yaml index 8f186c0689..aabcee452e 100644 --- a/.agents/notes/implemented/bug-fix/2026-07-28-web-agent-runtime-context.i18n.yaml +++ b/.agents/notes/implemented/bug-fix/2026-07-28-web-agent-runtime-context.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-07-28-web-agent-runtime-context.md -2026-07-28-web-agent-runtime-context.md: c0e01ab60f2c2eef8e4a021c274e6a8fe9b8ed6f -2026-07-28-web-agent-runtime-context.zh.md: 1f4deb9cf4f4b4fd135cf907323765cf4869a714 +2026-07-28-web-agent-runtime-context.md: 68f57e46296f34d8cf9d70f49ca18687d9573bca +2026-07-28-web-agent-runtime-context.zh.md: cabfc70962be0b99dc07dc5576e470d41793301b diff --git a/.agents/notes/implemented/bug-fix/2026-07-28-web-agent-runtime-context.md b/.agents/notes/implemented/bug-fix/2026-07-28-web-agent-runtime-context.md index c0e01ab60f..68f57e4629 100644 --- a/.agents/notes/implemented/bug-fix/2026-07-28-web-agent-runtime-context.md +++ b/.agents/notes/implemented/bug-fix/2026-07-28-web-agent-runtime-context.md @@ -10,7 +10,7 @@ The shared CLI base configured an empty deployment persona, the Web overlay did ## Decision -The shared Web/headless overlay (`apps/cli/config/web.cordis.yml`) supplies a concise coding-agent persona containing the resolved `{{model}}` and session `{{cwd}}`. Before mounting that tree, `dsh web` registers a launcher-provided `cordis:web-runtime-context` builtin; the ordinary Web overlay mounts it to resolve the harness checkout from the launcher's module URL, install the existing `harness:source` section, and add an `app:web-surface` section. A profile that owns its complete prompt can disable the builtin row, while every mounted prompt contribution still activates before later consumers such as the agent loop can emit a request header. The [source-checkout/workdir decision](2026-07-30-source-checkout-workdir-distinction.md) owns the source section's wording and its warning not to infer one path from the other. +The shared Web/headless overlay (`apps/cli/config/web.cordis.yml`) supplies a concise coding-agent persona containing the resolved `{{model}}` and session `{{cwd}}`. Its launcher-provided `cordis:web-runtime-context` row is disabled by default because Headless shares the tree without registering that builtin. Before mounting the tree, `dsh web` registers the builtin and applies an enable patch ahead of personal or explicit configuration; the mounted plugin resolves the harness checkout from the launcher's module URL, installs the existing `harness:source` section, and adds an `app:web-surface` section. A profile that owns its complete prompt can disable the row in the later configuration layer, while every mounted prompt contribution still activates before consumers such as the agent loop can emit a request header. The [source-checkout/workdir decision](2026-07-30-source-checkout-workdir-distinction.md) owns the source section's wording and its warning not to infer one path from the other. The Web section treats unqualified references to “this page,” “this GUI,” or “this app” as references to the DeepSeek Harness Web GUI. It also states that the browser provides no implicit DOM, route, or screenshot context, so the model can identify the product without claiming visual state it did not receive. The assembled text is logged in `request/header`, preserving the model-visible/logged invariant. diff --git a/.agents/notes/implemented/bug-fix/2026-07-28-web-agent-runtime-context.zh.md b/.agents/notes/implemented/bug-fix/2026-07-28-web-agent-runtime-context.zh.md index 1f4deb9cf4..cabfc70962 100644 --- a/.agents/notes/implemented/bug-fix/2026-07-28-web-agent-runtime-context.zh.md +++ b/.agents/notes/implemented/bug-fix/2026-07-28-web-agent-runtime-context.zh.md @@ -10,7 +10,7 @@ CLI 共享 base 配置了空的部署 persona,Web overlay 没有替换它, ## 决策 -`apps/cli/config/web.cordis.yml` 这份 Web/无头共享 overlay 提供一段简洁的编码 agent persona,其中包含解析后的 `{{model}}` 与会话 `{{cwd}}`。挂载该配置树前,`dsh web` 会注册一个由启动器提供的 `cordis:web-runtime-context` builtin;常规 Web overlay 会挂载它,以根据启动器模块的 URL 解析 harness checkout、安装现有的 `harness:source` 提示词段并添加 `app:web-surface` 提示词段。拥有完整提示词的 profile 可以禁用该 builtin 配置行,而每项已挂载的提示词贡献仍会在 agent loop(智能体循环)等后续消费方发出 request header 前激活。源码提示词段的措辞,以及其中不得从一条路径推断另一条路径的警告,均由另行记录的[源码 checkout 与工作目录区分决策](2026-07-30-source-checkout-workdir-distinction.md)负责。 +`apps/cli/config/web.cordis.yml` 这份 Web/无头共享 overlay 提供一段简洁的编码 agent persona,其中包含解析后的 `{{model}}` 与会话 `{{cwd}}`。由于无头模式共享该配置树但不会注册这个 builtin,其中由启动器提供的 `cordis:web-runtime-context` 配置行默认禁用。挂载配置树前,`dsh web` 会注册该 builtin,并先于个人配置或显式配置应用一项启用 patch;挂载后的插件会根据启动器模块的 URL 解析 harness checkout、安装现有的 `harness:source` 提示词段并添加 `app:web-surface` 提示词段。拥有完整提示词的 profile 可以在后续配置层中禁用该配置行,而每项已挂载的提示词贡献仍会在 agent loop(智能体循环)等消费方发出 request header 前激活。源码提示词段的措辞,以及其中不得从一条路径推断另一条路径的警告,均由另行记录的[源码 checkout 与工作目录区分决策](2026-07-30-source-checkout-workdir-distinction.md)负责。 Web 提示词段把未限定的「这个页面」「这个 GUI」或「这个应用」解释为 DeepSeek Harness Web GUI。同时,它会明确说明浏览器不会隐式提供 DOM、路由或截图上下文,使模型能够识别产品,但不会声称掌握未收到的视觉状态。组装后的文本会记录在 `request/header` 中,从而保持「模型可见内容必须有日志记录」这一不变量。 diff --git a/apps/cli/config/web.cordis.yml b/apps/cli/config/web.cordis.yml index 2bf9a9d89d..54b5157e97 100644 --- a/apps/cli/config/web.cordis.yml +++ b/apps/cli/config/web.cordis.yml @@ -46,11 +46,13 @@ # `dshClient` rows are the browser roster the modules node half scans into # window.__DSH_BOOT__; the modules row is simultaneously a host row. - insert: - # Launcher-provided builtin: owns Web GUI orientation in the model prompt - # and the matching managed Bash variables. Profiles with a complete prompt - # contract disable this row instead of relying on launcher special cases. + # Launcher-provided builtin: Headless keeps this shared row disabled, while + # `dsh web` enables it before the later personal/--config layer. Profiles + # with a complete prompt contract can therefore disable both Web GUI + # orientation and its matching managed Bash variables. - id: web-runtime-context name: cordis:web-runtime-context + disabled: true - id: session-projection name: '@deepseek-ai/dsh-session-projection' diff --git a/apps/cli/src/app-cli-entry.ts b/apps/cli/src/app-cli-entry.ts index 651dbca3f7..eb62f63992 100644 --- a/apps/cli/src/app-cli-entry.ts +++ b/apps/cli/src/app-cli-entry.ts @@ -141,9 +141,15 @@ export interface AppCLIEntryOptions { */ overlayPath: string /** - * Optional explicit overlay applied after {@link overlayPath} and before + * Launcher-owned patches applied after {@link overlayPath} and before the + * personal or explicit overlay, so user configuration can still override + * surface activation choices. + */ + launcherPatches?: readonly PatchOptions[] + /** + * Optional explicit overlay applied after {@link launcherPatches} and before * this entry's own profile/flag patches. When absent, the personal - * `$DSH_HOME/config.yaml` overlay is applied instead. + * `$DSH_HOME/config.yaml` overlay is applied in the same position instead. */ extraOverlayPath?: string /** Whether to append client-bundle HMR (the Web surface's prod/dev difference). */ @@ -261,10 +267,12 @@ export class AppCLIEntry { private async bootTree(): Promise { // One include of the shared base with every overlay as a sibling patch // list: patches never cross an include boundary, so nesting them would - // silently stop reaching base rows. The surface overlay applies first, then - // this entry's profile-json and CLI-flag patches, which therefore win. + // silently stop reaching base rows. The shared surface overlay applies + // first, then launcher activation, user configuration, and finally this + // entry's profile-json and CLI-flag patches. const compose = (overlay: PatchOptions[]): PatchOptions[] => [ ...loadOverlayPatches('dsh', this.options.overlayPath), + ...(this.options.launcherPatches ?? []), ...overlay, ...this.patches, ] diff --git a/apps/cli/src/dump-config.ts b/apps/cli/src/dump-config.ts index cb88e8d655..a60344c591 100644 --- a/apps/cli/src/dump-config.ts +++ b/apps/cli/src/dump-config.ts @@ -14,6 +14,7 @@ import { type ConfigDumpLayer, } from '@deepseek-ai/dsh-app-boot' import { resolveDshHome } from '@deepseek-ai/dsh-paths' +import { WEB_RUNTIME_CONTEXT_ENABLE_PATCH } from './web.ts' const NAME = 'dsh' const BASE_CONFIG = fileURLToPath(new URL('../config/base.cordis.yml', import.meta.url)) @@ -36,6 +37,7 @@ export function runDumpConfig(surface: 'config' | 'web', defaultOnly: boolean, c } } else { layers.push({ label: basename(WEB_OVERLAY), patches: loadOverlayPatches(NAME, WEB_OVERLAY) }) + layers.push({ label: 'dsh web launcher', patches: [WEB_RUNTIME_CONTEXT_ENABLE_PATCH] }) if (!defaultOnly) { if (config === undefined) { const personal = loadPersonalPatches(NAME) diff --git a/apps/cli/src/web.ts b/apps/cli/src/web.ts index f1fcb72fc6..3d19f2fbb9 100644 --- a/apps/cli/src/web.ts +++ b/apps/cli/src/web.ts @@ -8,6 +8,7 @@ import { fileURLToPath } from 'node:url' import type { Context } from 'cordis' +import type { PatchOptions } from '@cordisjs/plugin-include' import { addHarnessSourceSection, resolveConfigPath } from '@deepseek-ai/dsh-app-boot' import type {} from '@deepseek-ai/dsh-host-webserver' import type {} from '@deepseek-ai/dsh-system-prompt' @@ -24,6 +25,12 @@ const DSH_WEB_URL = 'DSH_WEB_URL' as const const DSH_WEB_MODE = 'DSH_WEB_MODE' as const const WEB_RUNTIME_CONTEXT_BUILTIN = 'web-runtime-context' as const +/** Web-launcher activation applied before personal or explicit configuration. */ +export const WEB_RUNTIME_CONTEXT_ENABLE_PATCH = { + id: WEB_RUNTIME_CONTEXT_BUILTIN, + disabled: false, +} as const satisfies PatchOptions + type WebMode = 'production' | 'development' // Display-only mirror of the webserver schema's loopback host: the address the @@ -125,6 +132,7 @@ export async function runWeb( const entry = new AppCLIEntry({ configPath: BASE_CONFIG, overlayPath: WEB_OVERLAY, + launcherPatches: [WEB_RUNTIME_CONTEXT_ENABLE_PATCH], ...config !== undefined && { extraOverlayPath: resolveConfigPath(config, undefined) }, dev, prepare: (ctx) => { prepareWebRuntimeContext(ctx, SOURCE_ROOT, mode) }, diff --git a/apps/cli/tests/built-bin.e2e.ts b/apps/cli/tests/built-bin.e2e.ts index fcfe8b3829..c9ee640612 100644 --- a/apps/cli/tests/built-bin.e2e.ts +++ b/apps/cli/tests/built-bin.e2e.ts @@ -10,6 +10,7 @@ const repoRoot = fileURLToPath(new URL('../../../', import.meta.url)) const dshBin = join(repoRoot, 'apps/cli/lib/bin.js') const rawOverlay = fileURLToPath(new URL('./fixtures/raw-overlay.cordis.yml', import.meta.url)) const rawInvalidProvider = fileURLToPath(new URL('./fixtures/raw-invalid-provider.cordis.yml', import.meta.url)) +const coreWebOverlay = fileURLToPath(new URL('../config/core-web.cordis.yml', import.meta.url)) async function runBuiltBin( args: readonly string[] = [], @@ -200,6 +201,17 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)', expect(code).toBe(0) expect(stdout).toContain("name: '@deepseek-ai/dsh-host-webserver'") expect(stdout).toContain('provider: personal-provider') + expect(stdout).toMatch(/- id: web-runtime-context\n name: cordis:web-runtime-context\n disabled: false/u) + }, 30_000) + + it('lets an explicit Web profile override launcher activation', async () => { + const { stdout, code, stderr } = await runBuiltBin( + ['web', '--dump-config', '--config', coreWebOverlay], + { DSH_HOME: home }, + ) + expect(code).toBe(0) + expect(stderr).toBe('') + expect(stdout).toMatch(/- id: web-runtime-context\n name: cordis:web-runtime-context\n disabled: true/u) }, 30_000) }) }) diff --git a/apps/web/tests/scaffold.ts b/apps/web/tests/scaffold.ts index 52eb7f151d..2cf1b2dd95 100644 --- a/apps/web/tests/scaffold.ts +++ b/apps/web/tests/scaffold.ts @@ -53,7 +53,7 @@ import * as ToolCordis from '@deepseek-ai/dsh-tool-cordis' // Empty type imports carry the httpServer/agents/sessionPersistence Context merges. import type {} from '@deepseek-ai/dsh-host-webserver' import type {} from '@deepseek-ai/dsh-agent' -import { prepareWebRuntimeContext } from '../../cli/src/web.ts' +import { prepareWebRuntimeContext, WEB_RUNTIME_CONTEXT_ENABLE_PATCH } from '../../cli/src/web.ts' import { DIST_INDEX, REPO_ROOT, requireDist } from './support.ts' /** Snapshot mode for the lane, from $DSH_SNAPSHOT (same vocabulary as the other snapshot suites). */ @@ -249,6 +249,8 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise