Просмотр исходного кода

fix(boot): judge a package's cordis copy by package directory

The probe compared two resolved URLs, which differ for one package under a
source launch (src against lib), across a symlink, and behind the proxy a
packaged executable writes; it now walks each URL up to the cordis manifest,
follows a dsh module proxy to its target, and compares real package
directories, with a probe test for the linked and the proxied case. The README
records that the probe child itself cannot run from a packaged executable.
Yichen Jiang 3 недель назад
Родитель
Сommit
792a68468f

+ 2 - 2
.agents/notes/implemented/architecture/2026-09-04-boot-scoped-fail-loud-and-package-probe.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/architecture/2026-09-04-boot-scoped-fail-loud-and-package-probe.md
-2026-09-04-boot-scoped-fail-loud-and-package-probe.md: 940b500f9f0b5c531e0c0eaf849a8a8000a90a6a
-2026-09-04-boot-scoped-fail-loud-and-package-probe.zh.md: 2205a1abfba92c06b3e65745a9d79bb09421bdb2
+2026-09-04-boot-scoped-fail-loud-and-package-probe.md: 2b892cacf14a9f62921b0fce015f14970a2b5a06
+2026-09-04-boot-scoped-fail-loud-and-package-probe.zh.md: f7004f83f417282670fb90effadf257716066e67

+ 1 - 1
.agents/notes/implemented/architecture/2026-09-04-boot-scoped-fail-loud-and-package-probe.md

@@ -8,7 +8,7 @@ English | [中文](2026-09-04-boot-scoped-fail-loud-and-package-probe.zh.md)
 
 `installFailLoud` registered a process-wide `unhandledRejection` handler that wrote `fatal load failure` and exited, and the launcher discarded the uninstaller it returned, so the handler lived for the whole process. That is right during startup, where an unhandled rejection is a load failure nobody else will report. After boot it meant that any plugin's stray continuation — one rejected promise a community bundle forgot to await — took every session down with it, and no `uncaughtException` handler existed at all, so a synchronous throw in a timer callback crashed the process with Node's default trace and no origin. Mounting third-party code at runtime, which the plugin manager exists to do, was a bet on the process with these two defaults in place.
 
-Separately, nothing could say what an installed package was without importing it into the host: whether it declared a bundle layer or exported a plugin, which rows its patch would insert, whether it resolved `@deepseek-ai/cordis` to the harness's copy or to one of its own — the actual shape of a "dependency conflict" under the profile's hoisted linker with `autoInstallPeers: false` — and what `Config` schema its main export carried.
+Separately, nothing could say what an installed package was without importing it into the host: whether it declared a bundle layer or exported a plugin, which rows its patch would insert, whether it resolved `@deepseek-ai/cordis` to the harness's copy (judged by package directory, through links and a packaged executable's proxy) or to one of its own — the actual shape of a "dependency conflict" under the profile's hoisted linker with `autoInstallPeers: false` — and what `Config` schema its main export carried.
 
 ## Decision
 

+ 1 - 1
.agents/notes/implemented/architecture/2026-09-04-boot-scoped-fail-loud-and-package-probe.zh.md

@@ -8,7 +8,7 @@ Status: implemented
 
 `installFailLoud` 注册了一个进程级的 `unhandledRejection` 处理器,写出 `fatal load failure` 后退出,而 launcher 丢弃了它返回的卸载函数,于是这个处理器活到进程结束。启动期间这是对的:未处理的 rejection 就是没人会报告的加载失败。启动之后它意味着任何插件的漏网延续——社区组合包忘了 await 的一个被拒 promise——都会把每个会话一起拖下去,而且根本没有 `uncaughtException` 处理器,定时器回调里的一次同步 throw 会让进程带着 Node 的默认堆栈崩掉,没有来源。插件管理器存在的目的正是在运行时挂载三方代码,在这两个默认行为之下这是拿进程赌。
 
-另外,不把包 import 进宿主就没法知道一个已安装的包是什么:它是声明了组合包层还是导出了插件,它的 patch 会插入哪些行,它把 `@deepseek-ai/cordis` 解析到 harness 的那份还是自己的一份——在 profile 的 hoisted linker 与 `autoInstallPeers: false` 之下这才是"依赖冲突"的真实形态——以及它的主导出带什么 `Config` schema。
+另外,不把包 import 进宿主就没法知道一个已安装的包是什么:它是声明了组合包层还是导出了插件,它的 patch 会插入哪些行,它把 `@deepseek-ai/cordis` 解析到 harness 的那份还是自己的一份——在 profile 的 hoisted linker 与 `autoInstallPeers: false` 之下这才是"依赖冲突"的真实形态——以及它的主导出带什么 `Config` schema(按包目录判断,穿过符号链接与打包可执行文件的代理包)。
 
 ## 决定
 

+ 2 - 2
packages/boot/app-boot/README.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 packages/boot/app-boot/README.md
-README.md: 2da207fb7a04db7fc73eb1b04a921d3e2ddd10dc
-README.zh.md: 217881ccf67bb3213eaff4cd442ff386d5897304
+README.md: 9c91ff8dba9385567c5607ffccd7f8cfe23f038d
+README.zh.md: 1106e7541f27d1d30fe1d93a4d472b1e10ea45c9

+ 2 - 1
packages/boot/app-boot/README.md

@@ -93,7 +93,7 @@ This section explains how the outcomes above are realized and points at the code
 - **External bundles are groups.** The vendored `EntryGroup.update` is all-or-nothing, so `composeExternalLayer` wraps each `runtime`-stage external layer's inserts in one `cordis:contained-group` under the ids the bundle declares; the group's `create()` records a failed row on the root's `pluginFailures` registry instead of rejecting, a group that updates drops the records of rows it no longer configures and one that unmounts drops them all, and `assertEntriesActivated` exempts recorded rows while still failing a built-in row left pending.
 - **Row ids are owned, not rewritten.** Entry ids are unique per tree and a `create()` that finds an existing id re-parents that entry instead of rejecting, so `composeProfileStack` decides ownership before anything mounts: built-in and boot-staged layers claim first and a duplicate among them fails the boot, a contained bundle that collides is left out whole, a user insert of a taken id is dropped, and every such row is a `conflict` record in `pluginFailures`. A config override may restate a row under the group that already holds it; the same id twice in one config list, or set under another group, counts as declared twice — the Loader would reject the first at mount and silently move the second — so a built-in layer fails the boot and a contained bundle is left out. Boot, live recomposition, and `--dump-config` compose through the same function.
 - **Fail-loud is boot-scoped.** `installFailLoud` exits on any unhandled rejection because during startup one is a load failure; the launcher uninstalls it once the tree is up and installs `installRuntimeGuards`, which reports a rejection and keeps running and exits on an uncaught exception. Nested fibers (a `ctx.inject()` continuation) that fail under a built-in entry are reported by `warnNestedFiberFailures` as advisory lines.
-- **The probe never runs a package in the host.** `probePackage` reads an installed package's manifest here and imports it in a child process that reports over an IPC channel, so a package that throws, exits, hangs, prints at import, or brings its own copy of cordis costs one child and yields a record with the reason; the child's report and the cached record are validated field by field before either is trusted. The child runs with the harness's credential-shaped variables scrubbed (`SENSITIVE_ENV_PATTERN` from `dsh-launch-environment`), its report is the one message that echoes a per-run token it removed from its environment before importing — the imported code finds no `process.send` either — the failure text keeps the last 16 KiB of its stderr, and the probe settles only once the child closed. It calls a package a `plugin` only when the package declares itself to dsh — a `dsh` section or a dependency on `@deepseek-ai/cordis` — and its main export is plugin-shaped; a bare function export (`lodash`) is a `library`. Records are cached under the profile's `.dsh-plugins/` with a format number, so a record an older probe wrote is probed again rather than trusted.
+- **The probe never runs a package in the host.** `probePackage` reads an installed package's manifest here and imports it in a child process that reports over an IPC channel, so a package that throws, exits, hangs, prints at import, or brings its own copy of cordis costs one child and yields a record with the reason — the cordis check compares package directories, through links and through the proxy a packaged executable writes, so `src` and `lib` resolutions of one package agree; the child's report and the cached record are validated field by field before either is trusted. The child runs with the harness's credential-shaped variables scrubbed (`SENSITIVE_ENV_PATTERN` from `dsh-launch-environment`), its report is the one message that echoes a per-run token it removed from its environment before importing — the imported code finds no `process.send` either — the failure text keeps the last 16 KiB of its stderr, and the probe settles only once the child closed. It calls a package a `plugin` only when the package declares itself to dsh — a `dsh` section or a dependency on `@deepseek-ai/cordis` — and its main export is plugin-shaped; a bare function export (`lodash`) is a `library`. Records are cached under the profile's `.dsh-plugins/` with a format number, so a record an older probe wrote is probed again rather than trusted.
 - **Profile module fallback.** Bare plugin specifiers resolve through the Loader from the config directory. Plain Node maintains one symlink per package in the installation dependency closure. A packaged executable instead reads each installed export map with Node ESM conditions and writes real proxy packages that re-export virtual module URLs, because an operating-system symlink cannot enter pkg's `/snapshot` tree. Missing exports stay unavailable, malformed maps fail startup, and a cross-process writer lock replaces stale entries without exposing partial proxies. A selected external bundle absent from the installation closure receives a profile-local `.dsh-module-fallback` link; existing pnpm entries win, projected links are excluded from later closure discovery, and cleanup removes only dsh-owned links.
 - **One rejection checkpoint.** `assertEntriesActivated` keeps the exact reasons it folds into the boot diagnostic visible through the next process rejection checkpoint, so `installFailLoud` coalesces Loader's duplicate notification while unrelated unhandled rejections remain fatal.
 - **Two-stage failure labels.** `boot()` distinguishes `host preparation failed` — `prepare` threw before any config-tree entry mounted — from `plugin tree failed to load`, and appends the deepest plugin error's stack so the startup diagnostic preserves the original activation error instead of only the wrap chain.
@@ -157,6 +157,7 @@ These limits describe when this boot library is a poor fit or needs special care
 - **An external bundle's overrides are not isolated** — a patch it applies to a built-in row edits that row in place, so its effect stays when the bundle's own rows fail and it is the one thing a bundle can break outside its group.
 - **A conflict is decided by order, not merit** — among external bundles the earlier layer in `dsh.profile.bundles` keeps a contested id, so uninstalling that bundle lets the later one mount on the next boot; the plugin list shows which bundle lost and to whom.
 - **The nested-fiber audit is advisory** — a failed `ctx.inject()` continuation under a built-in entry is reported, not fatal, until shipped compositions are known clean.
+- **The probe cannot run from a packaged executable** — `probe-child.js` sits inside the executable's `/snapshot` tree, which the plain Node child cannot read, so `probePackage` rejects there until the child entry is materialized outside the executable the way module proxies are.
 
 <a id="dev-note"></a>
 ### Dev Note

+ 2 - 1
packages/boot/app-boot/README.zh.md

@@ -93,7 +93,7 @@ profile 是同一套 dsh 安装提供不同应用界面的方式:`web`、`head
 - **外部组合包即组。** vendored 的 `EntryGroup.update` 是整组事务,因此 `composeExternalLayer` 把每个 `runtime` 阶段外部层的插入行按组合包声明的 id 包进一个 `cordis:contained-group`;该组的 `create()` 把失败的行记录到根上的 `pluginFailures` 注册表而不是 reject,组更新时丢掉配置里不再有的行的记录,卸载时全部丢掉,`assertEntriesActivated` 豁免已记录的行,但内置行停在 pending 时仍然失败。
 - **行 id 归属而非改写。** entry id 在整棵树内唯一,而 `create()` 遇到已有 id 时会把那个 entry 挪到自己名下而不是 reject,所以 `composeProfileStack` 在任何行挂载之前先判定归属:内置层与 boot 阶段的层先占有 id,它们之间重复即启动失败;撞名的受控组合包整层排除;用户层插入已被占用的 id 时该行丢弃;每一条被排除的行都是 `pluginFailures` 里的一条 `conflict` 记录。config 覆盖可以在已持有某行的组下重述它;同一 config 列表里出现两次、或改放到别的组下,都算声明了两次——前者会在挂载时被 Loader 拒绝,后者会被静默挪走——因此内置层启动失败、受控组合包整层排除。启动、运行时重组与 `--dump-config` 走同一个函数。
 - **fail-loud 只在启动期。** `installFailLoud` 对任何未处理 rejection 退出,因为启动期间它就是加载失败;树起来后 launcher 卸载它并安装 `installRuntimeGuards`:rejection 被报告并继续运行,未捕获异常被报告并退出。内置条目下失败的嵌套 fiber(`ctx.inject()` 的延续)由 `warnNestedFiberFailures` 以提示行报告。
-- **探针从不在宿主内运行包。** `probePackage` 在本进程读取已安装包的 manifest,在子进程里 import 它并经 IPC 通道接收报告,因此抛错、退出、挂起、import 时打印或自带 cordis 副本的包只消耗一个子进程,得到一条带原因的记录;子进程的报告与缓存记录都逐字段校验之后才被信任。子进程拿到的是剔除了密钥形态变量的宿主环境(`dsh-launch-environment` 的 `SENSITIVE_ENV_PATTERN`),报告是唯一回显本次 token 的那条消息,token 在 import 之前就从子进程环境里删掉、被 import 的代码也找不到 `process.send`;失败文本只留 stderr 的最后 16 KiB;探针要等子进程关闭后才结算。只有包向 dsh 声明了自己——有 `dsh` 段或依赖 `@deepseek-ai/cordis`——且主导出是插件形状时才判为 `plugin`;光是导出一个函数(`lodash`)的包是 `library`。记录缓存在 profile 的 `.dsh-plugins/` 下并带格式号,旧版探针写的记录会重新探测而不是被信任。
+- **探针从不在宿主内运行包。** `probePackage` 在本进程读取已安装包的 manifest,在子进程里 import 它并经 IPC 通道接收报告,因此抛错、退出、挂起、import 时打印或自带 cordis 副本的包只消耗一个子进程,得到一条带原因的记录——cordis 判定比较的是包目录,穿过符号链接与打包可执行文件写出的代理包,因此同一个包的 `src` 与 `lib` 解析结果一致;子进程的报告与缓存记录都逐字段校验之后才被信任。子进程拿到的是剔除了密钥形态变量的宿主环境(`dsh-launch-environment` 的 `SENSITIVE_ENV_PATTERN`),报告是唯一回显本次 token 的那条消息,token 在 import 之前就从子进程环境里删掉、被 import 的代码也找不到 `process.send`;失败文本只留 stderr 的最后 16 KiB;探针要等子进程关闭后才结算。只有包向 dsh 声明了自己——有 `dsh` 段或依赖 `@deepseek-ai/cordis`——且主导出是插件形状时才判为 `plugin`;光是导出一个函数(`lodash`)的包是 `library`。记录缓存在 profile 的 `.dsh-plugins/` 下并带格式号,旧版探针写的记录会重新探测而不是被信任。
 - **Profile 模块后备机制。** 裸插件 specifier 由 Loader 从配置目录解析。普通 Node 会为安装依赖闭包中的每个包维护一个符号链接。打包可执行文件无法让操作系统符号链接进入 pkg 的 `/snapshot` 树,因此会按 Node ESM 条件读取已安装包的 export map,并写入重新导出虚拟模块 URL 的真实代理包。缺失 export 保持不可用,错误 export map 会让启动失败,跨进程 writer lock 则会在不暴露部分代理的情况下替换陈旧条目。所选外部 bundle 若不在安装闭包中,则会获得 profile 本地的 `.dsh-module-fallback` 链接;已有 pnpm 条目优先,后续闭包发现会排除投影链接,清理也只删除 dsh 自有链接。
 - **单一 rejection 检查点。** `assertEntriesActivated` 把折入启动诊断的确切原因保持到下一个进程级 rejection 检查点可见,使 `installFailLoud` 能合并 Loader 的重复通知,而所有无关的未处理 rejection 仍然致命。
 - **两阶段失败标签。** `boot()` 区分 `host preparation failed`(`prepare` 在任何配置树条目挂载前抛出)与 `plugin tree failed to load`(此后的一切失败),并追加最深层插件错误的堆栈,使启动诊断保留原始激活错误,而不只是包装链。
@@ -157,6 +157,7 @@ profile 是同一套 dsh 安装提供不同应用界面的方式:`web`、`head
 - **外部组合包的覆盖不被隔离**——它对内置行施加的 patch 原地修改那一行,所以即使该组合包自己的行失败,效果仍然保留;这是组合包唯一能在自己的组之外造成影响的地方。
 - **冲突按顺序判定,不看是非**——外部组合包之间,`dsh.profile.bundles` 里靠前的那层保住争议 id,卸掉它之后靠后的那层在下次启动时挂上;插件列表显示谁输给了谁。
 - **嵌套 fiber 审计只是提示**——内置条目下失败的 `ctx.inject()` 延续会被报告而非致命,直到确认随附组合都没有这类失败。
+- **探针无法从打包可执行文件运行**——`probe-child.js` 位于可执行文件的 `/snapshot` 树内,普通 Node 子进程读不到它,因此 `probePackage` 在那里会 reject,直到子进程入口像模块代理那样被物化到可执行文件之外。
 
 <a id="dev-note"></a>
 ### 开发备注

+ 54 - 8
packages/boot/app-boot/src/probe.ts

@@ -11,8 +11,8 @@
 
 import { spawn } from 'node:child_process'
 import { randomUUID } from 'node:crypto'
-import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
-import { join } from 'node:path'
+import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs'
+import { dirname, join } from 'node:path'
 import { fileURLToPath } from 'node:url'
 import { withoutSensitiveEnv } from '@deepseek-ai/dsh-launch-environment'
 import { loadOverlayPatches } from './index.ts'
@@ -262,8 +262,7 @@ export async function probePackage(options: ProbeOptions): Promise<PluginProbe>
     : describeBundlePatch(options.binName, join(packageDir, declaredBundle))
   const hasMain = manifest.main !== undefined || manifest.exports !== undefined
   const report = await runChild(options, packageDir, hasMain ? options.packageName : '', declaredAddable.map(entry => entry.name))
-  const harnessCordis = resolveHarnessCordis()
-  const cordisSameCopy = report.cordis === null || harnessCordis === undefined ? null : report.cordis === harnessCordis
+  const cordisSameCopy = sameCordisCopy(report.cordis, resolveHarnessCordis())
   const kind: PluginProbe['kind'] = declaredBundle !== undefined
     ? 'bundle'
     : report.main.isPlugin && declaresDsh(manifest) ? 'plugin' : 'library'
@@ -305,13 +304,13 @@ export async function probePackage(options: ProbeOptions): Promise<PluginProbe>
 }
 
 /**
- * The copy of `@deepseek-ai/cordis` this harness runs: the one this module
- * resolves, which is the one every built-in plugin shares. Undefined only in
- * an environment that cannot resolve it at all.
+ * The package directory of the `@deepseek-ai/cordis` copy this harness runs:
+ * the one this module resolves, which every built-in plugin shares. Undefined
+ * only in an environment that cannot resolve it at all.
  */
 function resolveHarnessCordis(): string | undefined {
   try {
-    return import.meta.resolve('@deepseek-ai/cordis')
+    return cordisPackageDir(import.meta.resolve('@deepseek-ai/cordis'))
   } catch {
     // Only an embedder without cordis on its module path lands here; the
     // harness itself always resolves its own peer.
@@ -320,6 +319,53 @@ function resolveHarnessCordis(): string | undefined {
   }
 }
 
+/**
+ * Whether the package's cordis is the harness's: the two package directories
+ * compare equal. Null when the package resolves no cordis, when the harness
+ * cannot resolve its own, or when no cordis manifest encloses what the
+ * package resolved, since none of those says the package brought a copy.
+ */
+function sameCordisCopy(reported: string | null, harness: string | undefined): boolean | null {
+  if (reported === null) return null
+  /* v8 ignore next -- the harness resolves its own peer; only an embedder without cordis lands here */
+  if (harness === undefined) return null
+  const dir = cordisPackageDir(reported)
+  return dir === undefined ? null : dir === harness
+}
+
+/** The manifest fields that tell a dsh module proxy from the package it stands for. */
+interface ProxyAwareManifest {
+  name?: unknown
+  dsh?: { moduleFallback?: { targets?: Record<string, string> } }
+}
+
+/**
+ * The directory of the `@deepseek-ai/cordis` package a resolved module URL
+ * belongs to, with symlinks resolved and a dsh module proxy followed to the
+ * package it re-exports. Two resolutions of one installed package compare
+ * equal this way whether they landed on `src` or `lib`, on a link or its
+ * target, or on the proxy a packaged executable writes for its peers; the
+ * URLs themselves differ in every one of those cases.
+ * @param resolved - the URL `import.meta.resolve('@deepseek-ai/cordis')` gave.
+ * @returns the real package directory, or undefined when no cordis manifest encloses the URL.
+ */
+export function cordisPackageDir(resolved: string): string | undefined {
+  let dir = dirname(fileURLToPath(resolved))
+  for (;;) {
+    const manifestPath = join(dir, 'package.json')
+    if (existsSync(manifestPath)) {
+      const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) as ProxyAwareManifest
+      if (manifest.name === '@deepseek-ai/cordis') {
+        const target = Object.values(manifest.dsh?.moduleFallback?.targets ?? {})[0]
+        return target === undefined ? realpathSync(dir) : cordisPackageDir(target)
+      }
+    }
+    const parent = dirname(dir)
+    if (parent === dir) return undefined
+    dir = parent
+  }
+}
+
 /** The cache file for one package under a profile. */
 function probeCachePath(profileDir: string, packageName: string): string {
   return join(profileDir, PLUGIN_PROBE_DIR, `${packageName.replaceAll('/', '__')}.json`)

+ 42 - 2
packages/boot/app-boot/tests/probe.spec.ts

@@ -3,12 +3,12 @@
  * a child process, and the per-profile cache.
  */
 
-import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs'
+import { mkdirSync, mkdtempSync, symlinkSync, writeFileSync } from 'node:fs'
 import { tmpdir } from 'node:os'
 import { join } from 'node:path'
 import { describe, expect, it } from 'vitest'
 import { PLUGIN_PROBE_DIR, PLUGIN_PROBE_FORMAT, probePackage, readProbeCache, writeProbeCache, type PluginProbe } from '../src/index.ts'
-import { parseProbeRecord } from '../src/probe.ts'
+import { cordisPackageDir, parseProbeRecord } from '../src/probe.ts'
 import { parseChildReport, type ChildReport } from '../src/probe-report.ts'
 
 const NAME = 'dsh-test-bin'
@@ -141,6 +141,46 @@ describe('probePackage', () => {
     expect(own.reason).toContain('own copy of @deepseek-ai/cordis')
   })
 
+  it('recognizes the harness\'s cordis through a link and through a packaged executable\'s proxy', async () => {
+    const harnessCordis = cordisPackageDir(import.meta.resolve('@deepseek-ai/cordis')) as string
+    const harnessEntry = import.meta.resolve('@deepseek-ai/cordis')
+    const { profileDir, installAnchor } = stage({
+      'linked': { main: 'export function apply() {}\n', manifest: { peerDependencies: { '@deepseek-ai/cordis': '*' } } },
+      // A cordis directory whose manifest names no package: an unknown copy, not a second one.
+      'unnamed': {
+        main: 'export function apply() {}\n',
+        manifest: { peerDependencies: { '@deepseek-ai/cordis': '*' } },
+        files: {
+          'node_modules/@deepseek-ai/cordis/package.json': JSON.stringify({ version: '0.0.0', type: 'module', main: './index.js' }),
+          'node_modules/@deepseek-ai/cordis/index.js': 'export const Context = class {}\n',
+        },
+      },
+      'proxied': {
+        main: 'export function apply() {}\n',
+        manifest: { peerDependencies: { '@deepseek-ai/cordis': '*' } },
+        files: {
+          // The proxy a packaged executable writes: a manifest naming its targets, and an entry that re-exports one.
+          'node_modules/@deepseek-ai/cordis/package.json': JSON.stringify({
+            name: '@deepseek-ai/cordis', version: '0.0.0', type: 'module', exports: { '.': './entry-0.js' },
+            dsh: { moduleFallback: { targets: { '.': harnessEntry } } },
+          }),
+          'node_modules/@deepseek-ai/cordis/entry-0.js': `export * from ${JSON.stringify(harnessEntry)}\n`,
+        },
+      },
+    })
+    // The profile links the harness's own cordis, as plain Node installs do.
+    mkdirSync(join(profileDir, 'node_modules', '@deepseek-ai'), { recursive: true })
+    symlinkSync(harnessCordis, join(profileDir, 'node_modules', '@deepseek-ai', 'cordis'), 'dir')
+    const linked = await probePackage({ binName: NAME, profileDir, installAnchor, packageName: 'linked' })
+    expect(linked).toMatchObject({ kind: 'plugin', ok: true, cordisSameCopy: true })
+    const proxied = await probePackage({ binName: NAME, profileDir, installAnchor, packageName: 'proxied' })
+    expect(proxied).toMatchObject({ kind: 'plugin', ok: true, cordisSameCopy: true })
+    const unnamed = await probePackage({ binName: NAME, profileDir, installAnchor, packageName: 'unnamed' })
+    expect(unnamed).toMatchObject({ kind: 'plugin', ok: true, cordisSameCopy: null })
+    // A URL no cordis manifest encloses is an unknown copy, not a different one.
+    expect(cordisPackageDir(`file://${tmpdir()}/nowhere/index.js`)).toBeUndefined()
+  })
+
   it('probes a package with the minimal manifest and no main export', async () => {
     const { profileDir, installAnchor } = stage({
       'minimal': { manifest: { version: undefined } },