From fac3fc090f203b54a66e7f6fd88613e209a1a806 Mon Sep 17 00:00:00 2001 From: Turtle Date: Tue, 21 Jul 2026 14:43:38 +0800 Subject: [PATCH] fix(paths): treat empty DSH_HOME as unset; isolate telemetry env test Review fixes for #462: - resolveDshHome now treats an empty or whitespace-only $DSH_HOME as unset, so a blank override never resolves the home to cwd via resolve(''). Restores the guard telemetry's old resolver carried. - The default-env telemetry test asserts only that globalConfigDir() returns an absolute path, so a machine DSH_HOME without a .dsh suffix cannot break it. --- .../2026-07-24-single-harness-home-resolver.i18n.yaml | 4 ++-- .../2026-07-24-single-harness-home-resolver.md | 2 +- .../2026-07-24-single-harness-home-resolver.zh.md | 2 +- packages/sdk/telemetry/tests/anonymous-id.spec.ts | 6 ++++-- packages/util/paths/src/index.ts | 7 +++++-- packages/util/paths/tests/paths.spec.ts | 5 +++++ 6 files changed, 18 insertions(+), 8 deletions(-) diff --git a/.agents/notes/implemented/architecture/2026-07-24-single-harness-home-resolver.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-24-single-harness-home-resolver.i18n.yaml index 2dd6b4e934..081e9ee4ca 100644 --- a/.agents/notes/implemented/architecture/2026-07-24-single-harness-home-resolver.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-24-single-harness-home-resolver.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 -2026-07-24-single-harness-home-resolver.md: ec733dedc812ac029fb8aace074c35b7c3ccb873 -2026-07-24-single-harness-home-resolver.zh.md: 61bdee119f6cb729f751c8a65b4360da5a259028 +2026-07-24-single-harness-home-resolver.md: 9212a424db5fa0b77c0b482e29527a72f1656a0c +2026-07-24-single-harness-home-resolver.zh.md: 33f3fea5145497924a6a6d9738e076bedb781943 diff --git a/.agents/notes/implemented/architecture/2026-07-24-single-harness-home-resolver.md b/.agents/notes/implemented/architecture/2026-07-24-single-harness-home-resolver.md index ec733dedc8..9212a424db 100644 --- a/.agents/notes/implemented/architecture/2026-07-24-single-harness-home-resolver.md +++ b/.agents/notes/implemented/architecture/2026-07-24-single-harness-home-resolver.md @@ -22,7 +22,7 @@ One resolver owns the harness home, in `@deepseek-ai/dsh-paths`, single-root: explicit configured path > $DSH_HOME > ~/.dsh ``` -The harness keeps all user data under one root; there is no XDG config/data/cache split. `dshHomeDisplay()` names a resolved root symbolically for user-facing paths — `~/.dsh` for the default home, `$DSH_HOME` for any configured home — so the user-global `AGENTS.md` label never leaks an absolute machine path. It replaces workspace-context's bespoke default-vs-`$DSH_HOME` check. +An empty or whitespace-only `$DSH_HOME` is treated as unset, matching the guard telemetry's old resolver carried: without it `resolve('')` would silently place the home at the current working directory. The harness keeps all user data under one root; there is no XDG config/data/cache split. `dshHomeDisplay()` names a resolved root symbolically for user-facing paths — `~/.dsh` for the default home, `$DSH_HOME` for any configured home — so the user-global `AGENTS.md` label never leaks an absolute machine path. It replaces workspace-context's bespoke default-vs-`$DSH_HOME` check. `@deepseek-ai/dsh-home` is deleted. Its three importers (`dsh-tool-bash`, `dsh-skill-local`, `dsh-agent-spine-demo`) now import `resolveDshHome` from `dsh-paths`. `dsh-telemetry`'s `globalConfigDir` delegates to `resolveDshHome`, dropping its second resolver, the `DSH_CONFIG_HOME` override, the XDG/`%APPDATA%` branches, and the `deepseek-harness` namespace; the anonymous id now lives directly under the harness home. diff --git a/.agents/notes/implemented/architecture/2026-07-24-single-harness-home-resolver.zh.md b/.agents/notes/implemented/architecture/2026-07-24-single-harness-home-resolver.zh.md index 61bdee119f..33f3fea514 100644 --- a/.agents/notes/implemented/architecture/2026-07-24-single-harness-home-resolver.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-24-single-harness-home-resolver.zh.md @@ -22,7 +22,7 @@ Status: implemented explicit configured path > $DSH_HOME > ~/.dsh ``` -harness 把所有用户数据都放在同一个根目录下;不存在 XDG 的 config/data/cache 拆分。`dshHomeDisplay()` 为面向用户的路径以符号形式命名已解析的根目录——默认 home 显示为 `~/.dsh`,任何已配置的 home 显示为 `$DSH_HOME`——这样面向用户全局的 `AGENTS.md` 标签就绝不会泄露机器上的绝对路径。它取代了 workspace-context 中自定义的"默认值 vs `$DSH_HOME`"判断。 +空或仅含空白的 `$DSH_HOME` 被当作未设置处理,这与 telemetry 旧解析器所带的保护一致:若无此保护,`resolve('')` 会悄悄把 home 落在当前工作目录。harness 把所有用户数据都放在同一个根目录下;不存在 XDG 的 config/data/cache 拆分。`dshHomeDisplay()` 为面向用户的路径以符号形式命名已解析的根目录——默认 home 显示为 `~/.dsh`,任何已配置的 home 显示为 `$DSH_HOME`——这样面向用户全局的 `AGENTS.md` 标签就绝不会泄露机器上的绝对路径。它取代了 workspace-context 中自定义的"默认值 vs `$DSH_HOME`"判断。 `@deepseek-ai/dsh-home` 被删除。它的三个引用方(`dsh-tool-bash`、`dsh-skill-local`、`dsh-agent-spine-demo`)现在从 `dsh-paths` 导入 `resolveDshHome`。`dsh-telemetry` 的 `globalConfigDir` 转而委托给 `resolveDshHome`,去掉了它的第二个解析器、`DSH_CONFIG_HOME` 覆盖项、XDG/`%APPDATA%` 分支以及 `deepseek-harness` 命名空间;匿名 id 现在直接存放在 harness home 之下。 diff --git a/packages/sdk/telemetry/tests/anonymous-id.spec.ts b/packages/sdk/telemetry/tests/anonymous-id.spec.ts index 9f2ab8f41e..7bd5fb1924 100644 --- a/packages/sdk/telemetry/tests/anonymous-id.spec.ts +++ b/packages/sdk/telemetry/tests/anonymous-id.spec.ts @@ -1,6 +1,6 @@ import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' -import { join, resolve } from 'node:path' +import { isAbsolute, join, resolve } from 'node:path' import { defaultDshHome } from '@deepseek-ai/dsh-paths' import { afterEach, describe, expect, it } from 'vitest' import { @@ -34,7 +34,9 @@ describe('globalConfigDir', () => { it('reads process.env by default', () => { // No override supplied: the call must not throw and must return an absolute path. - expect(globalConfigDir()).toContain('.dsh') + // The ambient DSH_HOME is unknown here, so assert only the invariant the + // resolver guarantees rather than a specific location. + expect(isAbsolute(globalConfigDir())).toBe(true) }) }) diff --git a/packages/util/paths/src/index.ts b/packages/util/paths/src/index.ts index 41c07cabe1..c54a5e0a5f 100644 --- a/packages/util/paths/src/index.ts +++ b/packages/util/paths/src/index.ts @@ -39,13 +39,16 @@ export function expandHomePath(path: string): string { * Resolve the single-root DeepSeek Harness home. * * Precedence, highest first: an explicit configured path, `$DSH_HOME`, then - * `~/.dsh`. The harness keeps all user data under one root. + * `~/.dsh`. The harness keeps all user data under one root. An empty or + * whitespace-only `$DSH_HOME` is treated as unset, so a blank override never + * resolves the home to the current working directory. * @param configured - explicit harness-home override, which has highest precedence. * @param env - environment mapping used to read `DSH_HOME`. * @returns the normalized absolute harness home path. */ export function resolveDshHome(configured?: string, env: Record = process.env): string { - const selected = configured ?? env[DSH_HOME_ENV] ?? defaultDshHome() + const fromEnv = env[DSH_HOME_ENV] + const selected = configured ?? (fromEnv !== undefined && fromEnv.trim().length > 0 ? fromEnv : defaultDshHome()) return resolve(expandHomePath(selected)) } diff --git a/packages/util/paths/tests/paths.spec.ts b/packages/util/paths/tests/paths.spec.ts index 7682956e8d..e7b3804b23 100644 --- a/packages/util/paths/tests/paths.spec.ts +++ b/packages/util/paths/tests/paths.spec.ts @@ -33,6 +33,11 @@ describe('dsh path helpers', () => { expect(resolveDshHome(undefined, {})).toBe(defaultDshHome()) }) + it('treats an empty or whitespace-only DSH_HOME as unset', () => { + expect(resolveDshHome(undefined, { DSH_HOME: '' })).toBe(defaultDshHome()) + expect(resolveDshHome(undefined, { DSH_HOME: ' ' })).toBe(defaultDshHome()) + }) + it('labels a resolved home by whether it is the default root', () => { expect(dshHomeDisplay(resolve(defaultDshHome()))).toBe('~/.dsh') expect(dshHomeDisplay('/some/other/root')).toBe('$DSH_HOME')