Browse Source

Merge pull request #4059 from deepseek-harness/worktree/publish-all-experimental

feat(release): publish all experimental packages
Tianyi Cui 1 month ago
parent
commit
c2a68638b2
89 changed files with 3076 additions and 204 deletions
  1. 2 2
      .agents/notes/implemented/architecture/2026-07-31-ptc-runtime-python-fd3-protocol.i18n.yaml
  2. 2 2
      .agents/notes/implemented/architecture/2026-07-31-ptc-runtime-python-fd3-protocol.md
  3. 2 2
      .agents/notes/implemented/architecture/2026-07-31-ptc-runtime-python-fd3-protocol.zh.md
  4. 2 2
      .agents/notes/implemented/architecture/2026-08-18-experimental-agent-teams-packages.i18n.yaml
  5. 4 4
      .agents/notes/implemented/architecture/2026-08-18-experimental-agent-teams-packages.md
  6. 4 4
      .agents/notes/implemented/architecture/2026-08-18-experimental-agent-teams-packages.zh.md
  7. 2 2
      .agents/notes/implemented/feature/2026-08-28-auto-review.i18n.yaml
  8. 1 1
      .agents/notes/implemented/feature/2026-08-28-auto-review.md
  9. 1 1
      .agents/notes/implemented/feature/2026-08-28-auto-review.zh.md
  10. 2 2
      .agents/notes/implemented/process/2026-08-10-npm-release-sequences.i18n.yaml
  11. 1 1
      .agents/notes/implemented/process/2026-08-10-npm-release-sequences.md
  12. 1 1
      .agents/notes/implemented/process/2026-08-10-npm-release-sequences.zh.md
  13. 6 0
      .agents/notes/implemented/process/2026-09-12-default-product-experimental-isolation.i18n.yaml
  14. 39 0
      .agents/notes/implemented/process/2026-09-12-default-product-experimental-isolation.md
  15. 39 0
      .agents/notes/implemented/process/2026-09-12-default-product-experimental-isolation.zh.md
  16. 6 0
      .agents/notes/implemented/process/2026-09-12-experimental-publication-denylist.i18n.yaml
  17. 27 0
      .agents/notes/implemented/process/2026-09-12-experimental-publication-denylist.md
  18. 27 0
      .agents/notes/implemented/process/2026-09-12-experimental-publication-denylist.zh.md
  19. 6 0
      .agents/notes/implemented/process/2026-09-12-publish-all-experimental-packages.i18n.yaml
  20. 29 0
      .agents/notes/implemented/process/2026-09-12-publish-all-experimental-packages.md
  21. 29 0
      .agents/notes/implemented/process/2026-09-12-publish-all-experimental-packages.zh.md
  22. 1 1
      AGENTS.md
  23. 185 0
      apps/cli/tests/profiles/web/tests/default-web-process.ts
  24. 38 0
      apps/cli/tests/profiles/web/tests/fixtures/runtime-roster-observer.ts
  25. 95 0
      apps/cli/tests/profiles/web/tests/runtime-roster.ts
  26. 42 0
      apps/cli/tests/profiles/web/tests/web-default-isolation.expected.e2e.ts
  27. 105 0
      apps/web/product-isolation.ts
  28. 112 0
      apps/web/tests/default-product-isolation.e2e.ts
  29. 1 0
      apps/web/tsconfig.json
  30. 17 6
      apps/web/vite.config.ts
  31. 1 0
      package.json
  32. 2 2
      packages/README.i18n.yaml
  33. 2 2
      packages/README.md
  34. 2 2
      packages/README.zh.md
  35. 70 5
      packages/client/tsdown.client.ts
  36. 4 4
      packages/experimental/AGENTS.md
  37. 2 2
      packages/experimental/README.i18n.yaml
  38. 3 3
      packages/experimental/README.md
  39. 3 3
      packages/experimental/README.zh.md
  40. 2 2
      packages/experimental/auto-review/README.i18n.yaml
  41. 4 4
      packages/experimental/auto-review/README.md
  42. 4 4
      packages/experimental/auto-review/README.zh.md
  43. 3 1
      packages/experimental/auto-review/package.json
  44. 2 2
      packages/experimental/inspector/README.i18n.yaml
  45. 1 1
      packages/experimental/inspector/README.md
  46. 1 1
      packages/experimental/inspector/README.zh.md
  47. 4 1
      packages/experimental/inspector/package.json
  48. 69 34
      packages/experimental/inspector/tests/built-lib.e2e.ts
  49. 2 2
      packages/experimental/ptc-runtime-python/README.i18n.yaml
  50. 3 3
      packages/experimental/ptc-runtime-python/README.md
  51. 3 3
      packages/experimental/ptc-runtime-python/README.zh.md
  52. 3 1
      packages/experimental/ptc-runtime-python/package.json
  53. 2 2
      packages/experimental/webworker-packer/README.i18n.yaml
  54. 3 0
      packages/experimental/webworker-packer/README.md
  55. 3 0
      packages/experimental/webworker-packer/README.zh.md
  56. 3 1
      packages/experimental/webworker-packer/package.json
  57. 1 2
      packages/experimental/webworker-packer/src/pack.ts
  58. 100 0
      packages/experimental/webworker-packer/tests/built-lib.e2e.ts
  59. 2 2
      packages/experimental/webworker-runtime/README.i18n.yaml
  60. 2 0
      packages/experimental/webworker-runtime/README.md
  61. 2 0
      packages/experimental/webworker-runtime/README.zh.md
  62. 3 1
      packages/experimental/webworker-runtime/package.json
  63. 2 0
      packages/experimental/webworker-runtime/src/index.ts
  64. 49 8
      scripts/browser-bundled-externals.spec.ts
  65. 8 5
      scripts/browser-bundled-externals.ts
  66. 95 0
      scripts/bundle-input-isolation.ts
  67. 37 23
      scripts/check-workspace-constraints.spec.ts
  68. 13 5
      scripts/check-workspace-constraints.ts
  69. 186 4
      scripts/client-bundle-purity.spec.ts
  70. 10 15
      scripts/experimental-package-policy.ts
  71. 73 0
      scripts/npm-baseline-packages.spec.ts
  72. 26 0
      scripts/npm-baseline-packages.ts
  73. 2 9
      scripts/publish-npm-baseline.ts
  74. 25 2
      scripts/release/families.spec.ts
  75. 1 3
      scripts/release/families.ts
  76. 93 0
      scripts/release/installed-product-isolation.spec.ts
  77. 65 0
      scripts/release/installed-product-isolation.ts
  78. 4 0
      scripts/release/verify-packed-install.ts
  79. 12 1
      scripts/run-gates.spec.ts
  80. 2 0
      scripts/run-gates.ts
  81. 3 3
      scripts/verify-application-entrypoints.ts
  82. 19 0
      scripts/verify-client-packages.spec.ts
  83. 7 5
      scripts/verify-client-packages.ts
  84. 373 0
      scripts/verify-default-product-isolation.spec.ts
  85. 372 0
      scripts/verify-default-product-isolation.ts
  86. 258 0
      scripts/web-product-bundle-isolation.spec.ts
  87. 199 0
      scripts/web-product-bundle-isolation.ts
  88. 1 0
      tsconfig.client.json
  89. 2 0
      tsconfig.host.json

+ 2 - 2
.agents/notes/implemented/architecture/2026-07-31-ptc-runtime-python-fd3-protocol.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-07-31-ptc-runtime-python-fd3-protocol.md
-2026-07-31-ptc-runtime-python-fd3-protocol.md: 18ce7b44b116eee3a11528a5b515c4e02e955ee6
-2026-07-31-ptc-runtime-python-fd3-protocol.zh.md: d5eea4cfd80f10dc1d99c7c43090ca6b0ff6e19e
+2026-07-31-ptc-runtime-python-fd3-protocol.md: 097bcfd55f333a0973084f378a6c97cb630a4bec
+2026-07-31-ptc-runtime-python-fd3-protocol.zh.md: ac656e421bd822994c50c33d0dfaf65ae46aa817

+ 2 - 2
.agents/notes/implemented/architecture/2026-07-31-ptc-runtime-python-fd3-protocol.md

@@ -2,7 +2,7 @@
 
 Status: implemented
 
-The CPython PTC runtime now lives at `packages/experimental/ptc-runtime-python` (private, npm name `@deepseek-ai/dsh-experimental-ptc-runtime-python`); promotion to a released package follows the experimental-packages decision.
+The CPython PTC runtime lives at `packages/experimental/ptc-runtime-python` and publishes as `@deepseek-ai/dsh-experimental-ptc-runtime-python`; the [publication decision](../process/2026-09-12-publish-all-experimental-packages.md) preserves its experimental status.
 
 English | [中文](2026-07-31-ptc-runtime-python-fd3-protocol.zh.md)
 
@@ -10,7 +10,7 @@ English | [中文](2026-07-31-ptc-runtime-python-fd3-protocol.zh.md)
 
 `@deepseek-ai/dsh-experimental-ptc-runtime-python` owns the wire protocol intended for a CPython ptc-runtime provider. Such a provider runs each model program in a fresh `python3 -I` subprocess and bridges binding calls and completion values over the child's fd 3. The host cannot trust that channel: model code has full access to fd 3 and can forge any frame, so every inbound frame is hostile input that the host must validate and rebuild before reading. The protocol also has to carry lossless JSON without the depth limit `JSON.stringify` and `json.dumps` impose, because the seam's `PtcJsonValue` is depth-unbounded.
 
-The private experimental package contains both the protocol and runtime implementation: `PythonPtcRuntime` (the plugin's default export), the `python3 -I` subprocess path, and the Python-side JSON codec all live in `@deepseek-ai/dsh-experimental-ptc-runtime-python`. The protocol builds on the [portable identifier seam](../../archived/architecture/2026-07-31-code-runtime-portable-identifier-seam.md).
+The experimental package contains both the protocol and runtime implementation: `PythonPtcRuntime` (the plugin's default export), the `python3 -I` subprocess path, and the Python-side JSON codec all live in `@deepseek-ai/dsh-experimental-ptc-runtime-python`. The protocol builds on the [portable identifier seam](../../archived/architecture/2026-07-31-code-runtime-portable-identifier-seam.md).
 
 ## Decision
 

+ 2 - 2
.agents/notes/implemented/architecture/2026-07-31-ptc-runtime-python-fd3-protocol.zh.md

@@ -2,7 +2,7 @@
 
 Status: implemented
 
-CPython PTC 运行时现在位于 `packages/experimental/ptc-runtime-python`(私有,npm 名 `@deepseek-ai/dsh-experimental-ptc-runtime-python`);提升为发布包遵循 experimental-packages 决策。
+CPython PTC 运行时位于 `packages/experimental/ptc-runtime-python`,以 `@deepseek-ai/dsh-experimental-ptc-runtime-python` 名称发布;[发布决策](../process/2026-09-12-publish-all-experimental-packages.zh.md)保留其实验性状态。
 
 [English](2026-07-31-ptc-runtime-python-fd3-protocol.md) | 中文
 
@@ -10,7 +10,7 @@ CPython PTC 运行时现在位于 `packages/experimental/ptc-runtime-python`(
 
 `@deepseek-ai/dsh-experimental-ptc-runtime-python` 负责供 CPython ptc-runtime 提供方使用的 wire protocol。这样的提供方会在全新的 `python3 -I` 子进程中运行每个模型程序,并通过子进程 fd 3 桥接 binding 调用与完成值。Host 不能信任这条通道:模型代码可以完全访问 fd 3 并伪造任意帧,因此 host 必须把每个入站帧视为敌意输入,先校验并重建后才能读取。协议还必须承载无深度限制的 lossless JSON,因为 seam 的 `PtcJsonValue` 深度无界,而 `JSON.stringify` 和 `json.dumps` 都有递归深度限制。
 
-这个私有实验包同时包含协议与 runtime 实现:`PythonPtcRuntime`(插件的默认导出)、`python3 -I` 子进程路径与 Python 侧 JSON codec 都在 `@deepseek-ai/dsh-experimental-ptc-runtime-python` 中。协议建立在[可移植标识符 seam](../../archived/architecture/2026-07-31-code-runtime-portable-identifier-seam.md)之上。
+这个实验包同时包含协议与 runtime 实现:`PythonPtcRuntime`(插件的默认导出)、`python3 -I` 子进程路径与 Python 侧 JSON codec 都在 `@deepseek-ai/dsh-experimental-ptc-runtime-python` 中。协议建立在[可移植标识符 seam](../../archived/architecture/2026-07-31-code-runtime-portable-identifier-seam.md)之上。
 
 ## Decision
 

+ 2 - 2
.agents/notes/implemented/architecture/2026-08-18-experimental-agent-teams-packages.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-08-18-experimental-agent-teams-packages.md
-2026-08-18-experimental-agent-teams-packages.md: c36760587979b234ffd7990035178b0acee53e35
-2026-08-18-experimental-agent-teams-packages.zh.md: 9502ad15ee776564423ccd398d5f77a98a51b730
+2026-08-18-experimental-agent-teams-packages.md: 2a00b651e0073434a5a68df13b9716adcab5fccf
+2026-08-18-experimental-agent-teams-packages.zh.md: df73ee5c05fd3a25bf843bee10b06535a1512783

+ 4 - 4
.agents/notes/implemented/architecture/2026-08-18-experimental-agent-teams-packages.md

@@ -8,13 +8,13 @@ English | [中文](2026-08-18-experimental-agent-teams-packages.zh.md)
 
 Agent Teams needs the real Session log, subagent lifecycle, tools, examples, snapshots, and repository checks while its service and tool contracts continue to change. Users also need to install the complete Team composition from npm without building a source checkout.
 
-Moving the packages into product-role groups would remove their experimental names and imply stable-package ownership. Publishing every package under `packages/experimental/` would instead expose unrelated internal prototypes. The release policy needs an explicit Agent Teams exception while preserving the private default.
+Moving the packages into product-role groups would remove their experimental names and imply stable-package ownership. Publishing every package under `packages/experimental/` would instead expose unrelated internal prototypes. The release policy must let users install Agent Teams while keeping internal-only prototypes private.
 
 ## Decision
 
-`packages/experimental/agent-team`, `packages/experimental/tool-agent-team`, `packages/experimental/agent-team-profile`, `packages/experimental/client-ui-agent-team`, and `packages/experimental/agent-team-web-profile` are public workspace packages. They retain their existing `@deepseek-ai/dsh-experimental-*` names and join the dsh release family. The [experimental package rules](../../../../packages/experimental/AGENTS.md) own the private default, this exception, and later promotion.
+`packages/experimental/agent-team`, `packages/experimental/tool-agent-team`, `packages/experimental/agent-team-profile`, `packages/experimental/client-ui-agent-team`, and `packages/experimental/agent-team-web-profile` are public workspace packages. They retain their existing `@deepseek-ai/dsh-experimental-*` names and join the dsh release family. The [publication denylist decision](../process/2026-09-12-experimental-publication-denylist.md) owns the public default and private exceptions; the [experimental package rules](../../../../packages/experimental/AGENTS.md) own dependency isolation and later promotion.
 
-The dsh pack and publish set and the local baseline publisher include the explicit experimental allowlist. These five Agent Teams directories and the [Cua Driver provider exceptions](2026-09-12-computer-use-provider-registration.md) omit `private`, set `publishConfig.access` to `public`, and keep the experimental npm prefix. Unlisted experimental packages remain private and excluded from publication by default. Release packages and apps outside the experimental group, plus the Python runtime, cannot name experimental packages in `dependencies`, `optionalDependencies`, or `peerDependencies`; experimental packages may depend on release packages and each other.
+The dsh pack and publish set and the local baseline publisher include these five Agent Teams directories and the [Cua Driver providers](2026-09-12-computer-use-provider-registration.md). Workspace constraints require them to omit `private`, set `publishConfig.access` to `public`, and keep the experimental npm prefix. Release packages and apps outside the experimental group, plus the Python runtime, cannot name experimental packages in `dependencies`, `optionalDependencies`, or `peerDependencies`; experimental packages may depend on release packages and each other.
 
 The generic caller-reserved continuable child identity and selective direct-child drain remain in the stable Subagent service. They own Subagent identity and Activation lifecycle without importing or naming Agent Teams; the experimental Team service consumes them in the permitted direction.
 
@@ -38,4 +38,4 @@ Experimental status changes compatibility and support expectations, not publicat
 
 Agent Teams publishes as five installable tarballs in the dsh release family without changing package names or enabling Team in a shipped profile. Public availability does not make the packages stable or supported by default, and stable release packages cannot take runtime dependencies on them.
 
-The release family carries explicitly named experimental exceptions. Promotion still creates path and npm-name churn as specified by the experimental package rules.
+The release family carries the experimental npm names. Promotion still creates path and npm-name churn as specified by the experimental package rules.

+ 4 - 4
.agents/notes/implemented/architecture/2026-08-18-experimental-agent-teams-packages.zh.md

@@ -8,13 +8,13 @@ Status: implemented
 
 Agent Teams 的服务与工具约定仍在变化,但它需要使用真实 Session 日志、subagent 生命周期、工具、示例、快照和仓库检查。用户还需要直接从 npm 安装完整 Team 组合,而无需构建源码 checkout。
 
-把这些包移入产品职责组会移除实验性名称,并暗示稳定包 owner 已经就位。发布 `packages/experimental/` 下的所有包又会暴露无关的内部原型。发布策略需要为 Agent Teams 设置显式例外,同时保留默认私有原则。
+把这些包移入产品职责组会移除实验性名称,并暗示稳定包 owner 已经就位。发布 `packages/experimental/` 下的所有包又会暴露无关的内部原型。发布策略必须让用户安装 Agent Teams,同时让内部专用原型保持私有。
 
 ## 决策
 
-`packages/experimental/agent-team`、`packages/experimental/tool-agent-team`、`packages/experimental/agent-team-profile`、`packages/experimental/client-ui-agent-team` 与 `packages/experimental/agent-team-web-profile` 是公开 workspace 包。它们保留现有 `@deepseek-ai/dsh-experimental-*` 名称并加入 dsh 发布系列。[实验性包规则](../../../../packages/experimental/AGENTS.md)负责默认私有原则、本例外与后续 promotion。
+`packages/experimental/agent-team`、`packages/experimental/tool-agent-team`、`packages/experimental/agent-team-profile`、`packages/experimental/client-ui-agent-team` 与 `packages/experimental/agent-team-web-profile` 是公开 workspace 包。它们保留现有 `@deepseek-ai/dsh-experimental-*` 名称并加入 dsh 发布系列。[发布拒绝列表决策](../process/2026-09-12-experimental-publication-denylist.zh.md)负责默认公开与私有例外;[实验性包规则](../../../../packages/experimental/AGENTS.md)负责依赖隔离与后续 promotion。
 
-dsh 打包与发布集合以及本地基线发布器包含显式实验包允许列表。这五个 Agent Teams 目录以及 [Cua Driver 提供方例外](2026-09-12-computer-use-provider-registration.zh.md)省略 `private`、将 `publishConfig.access` 设为 `public`,并保留实验性 npm 前缀。未列出的实验包默认保持私有且不参与发布。实验组之外的发布包、应用和 Python 运行时不能在 `dependencies`、`optionalDependencies` 或 `peerDependencies` 中引用实验包;实验包可以依赖发布包和彼此。
+dsh 打包与发布集合以及本地基线发布器包含这五个 Agent Teams 目录和 [Cua Driver 提供方](2026-09-12-computer-use-provider-registration.zh.md)。workspace 约束要求它们省略 `private`、将 `publishConfig.access` 设为 `public`,并保留实验性 npm 前缀。实验组之外的发布包、应用和 Python 运行时不能在 `dependencies`、`optionalDependencies` 或 `peerDependencies` 中引用实验包;实验包可以依赖发布包和彼此。
 
 通用的调用方预留 continuable child 身份和精确 direct-child drain 仍属于稳定 Subagent 服务。它们负责 Subagent 身份与 Activation 生命周期,不 import 或命名 Agent Teams;实验性 Team 服务沿允许的方向消费这些能力。
 
@@ -38,4 +38,4 @@ profile 安装通过自身 package manager 解析每个公开 bundle 及其依
 
 Agent Teams 会作为 dsh 发布系列中的五个可安装 tarball 发布,同时保持包名不变,也不会在随附 profile 中启用 Team。公开可用不代表这些包稳定或默认受支持,稳定发布包也不能对其建立运行时依赖。
 
-发布系列需要维护显式命名的实验性例外。promotion 仍会按照实验性包规则产生路径和 npm 名改动。
+发布系列保留实验性 npm 名称。promotion 仍会按照实验性包规则产生路径和 npm 名改动。

+ 2 - 2
.agents/notes/implemented/feature/2026-08-28-auto-review.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/feature/2026-08-28-auto-review.md
-2026-08-28-auto-review.md: a5a0b94540a1c47a0f8dea988476653b0e60bfae
-2026-08-28-auto-review.zh.md: 076380c638fa09e8ef1a4a9adaa0b2e6e0ea58e9
+2026-08-28-auto-review.md: 13b3769b97c27b2ef4ac5cf4f6c2c2dde6323e7f
+2026-08-28-auto-review.zh.md: bf7d70b85be43a0b9f451dd20e3bcd5c4b879cd3

+ 1 - 1
.agents/notes/implemented/feature/2026-08-28-auto-review.md

@@ -10,7 +10,7 @@ Full access lets useful project work proceed without repeated approvals, but it
 
 ## Decision
 
-[`dsh-experimental-auto-review`](../../../../packages/experimental/auto-review/README.md) is a private, explicitly installed source Web layer. Default Web retains Read Only, Workspace Write, and Full access. The layer contributes current-session `auto`, whose only durable identity is `permission/preset:auto`; it shares Full access's unchanged `danger-full-access + never` knobs and tool definitions. Official payloads, Headless, General settings, and new-session defaults exclude the integration.
+[`dsh-experimental-auto-review`](../../../../packages/experimental/auto-review/README.md) is an explicitly installed experimental Web layer, published under the [experimental package publication decision](../process/2026-09-12-publish-all-experimental-packages.md). Default Web retains Read Only, Workspace Write, and Full access. The layer contributes current-session `auto`, whose only durable identity is `permission/preset:auto`; it shares Full access's unchanged `danger-full-access + never` knobs and tool definitions. Headless, General settings, and new-session defaults exclude the integration.
 
 Every native call and started PTC `tools.*` inner call receives one review before its body. The outer `run_code` transport and direct Node effects in a PTC program remain outside this guarantee. There are no tool-name exemptions, cached grants, retries, configurable policy, second authorization check, or manual fallback. A repeated call receives a fresh review.
 

+ 1 - 1
.agents/notes/implemented/feature/2026-08-28-auto-review.zh.md

@@ -10,7 +10,7 @@ Full access 让有用的项目工作无需反复审批即可继续,但也允
 
 ## 决策
 
-[`dsh-experimental-auto-review`](../../../../packages/experimental/auto-review/README.zh.md)是显式安装的私有源码 Web 层。默认 Web 保持 Read Only、Workspace Write 与 Full access。此层贡献仅限当前会话的 `auto`,唯一持久身份为 `permission/preset:auto`;它共用 Full access 未改变的 `danger-full-access + never` 旋钮与工具定义。正式 payload、Headless、通用设置与新会话默认值都排除此 integration。
+[`dsh-experimental-auto-review`](../../../../packages/experimental/auto-review/README.zh.md)是显式安装的实验性 Web 层,按[实验包发布决策](../process/2026-09-12-publish-all-experimental-packages.zh.md)参与发布。默认 Web 保持 Read Only、Workspace Write 与 Full access。此层贡献仅限当前会话的 `auto`,唯一持久身份为 `permission/preset:auto`;它共用 Full access 未改变的 `danger-full-access + never` 旋钮与工具定义。Headless、通用设置与新会话默认值都排除此 integration。
 
 每个原生调用与已开始的 PTC `tools.*` inner call 都在 body 前接受一次审查。外层 `run_code` transport 与 PTC 程序内直接 Node 效果不在保证范围内。不提供按工具名豁免、缓存 grant、重试、可配置策略、第二授权检查或人工 fallback。重复调用也重新审查。
 

+ 2 - 2
.agents/notes/implemented/process/2026-08-10-npm-release-sequences.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/process/2026-08-10-npm-release-sequences.md
-2026-08-10-npm-release-sequences.md: 3a52c01494921cacd5df3053fb515461831d6007
-2026-08-10-npm-release-sequences.zh.md: f973b442f2bbf6400ef0b75650392744a489ace3
+2026-08-10-npm-release-sequences.md: 77b73b8c461238ad80afd7abd609a68df0bcf058
+2026-08-10-npm-release-sequences.zh.md: f56234fc50d776658d2d9191056b0b04e1609f1e

+ 1 - 1
.agents/notes/implemented/process/2026-08-10-npm-release-sequences.md

@@ -22,7 +22,7 @@ Two hard blockers sat in the way. All 217 workspace manifests set `private: true
 
 | Sequence | Members | Version baseline | Tag | Workflow |
 |---|---|---|---|---|
-| dsh | Publish set: non-experimental `packages/*/*` + `apps/*`; private experimental packages join only the shared version bump | one version for the publish set, private dsh packages, and workspace root, `0.0.x` | `dsh-v<version>` | `release.yml` (pack) / `release-publish.yml` (publish) |
+| dsh | Publish set: public members of `packages/*/*` + `apps/*`, with [private experimental exceptions](2026-09-12-experimental-publication-denylist.md); private packages join only the shared version bump | one version for the publish set, private dsh packages, and workspace root, `0.0.x` | `dsh-v<version>` | `release.yml` (pack) / `release-publish.yml` (publish) |
 | vendored framework | the nine `vendor/*` packages | each package on its own version line | `vendor-<package>-v<version>` (one per package) | `release-vendor.yml` (pack) / `release-vendor-publish.yml` (publish) |
 | native | `native/system/packages/*` | its own `0.0.x` | `node-addon-system-v<version>` | `node-addon-system-release.yml` |
 

+ 1 - 1
.agents/notes/implemented/process/2026-08-10-npm-release-sequences.zh.md

@@ -22,7 +22,7 @@ Status: implemented
 
 | 序列 | 成员 | 版本基线 | tag | workflow |
 |---|---|---|---|---|
-| dsh | 发布集:非 experimental 的 `packages/*/*` + `apps/*`;私有实验性包仅加入共享版本 bump | 发布集、私有 dsh 包与 workspace 根共用一个 `0.0.x` | `dsh-v<版本>` | `release.yml`(pack)/ `release-publish.yml`(发布) |
+| dsh | 发布集:`packages/*/*` + `apps/*` 中的公开成员,保留[私有实验包例外](2026-09-12-experimental-publication-denylist.zh.md);私有包仅加入共享版本 bump | 发布集、私有 dsh 包与 workspace 根共用一个 `0.0.x` | `dsh-v<版本>` | `release.yml`(pack)/ `release-publish.yml`(发布) |
 | vendored framework | `vendor/*` 九个包 | 每包各自一条版本线 | `vendor-<包名>-v<版本>`(每包一个) | `release-vendor.yml`(pack)/ `release-vendor-publish.yml`(发布) |
 | native | `native/system/packages/*` | 自己的 `0.0.x` | `node-addon-system-v<版本>` | `node-addon-system-release.yml` |
 

+ 6 - 0
.agents/notes/implemented/process/2026-09-12-default-product-experimental-isolation.i18n.yaml

@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# 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/process/2026-09-12-default-product-experimental-isolation.md
+2026-09-12-default-product-experimental-isolation.md: d2312d6061f372e910bfd625512230ccd48a586a
+2026-09-12-default-product-experimental-isolation.zh.md: 6ea86f1f3e859719ffd8287e66ebe63df77d64c2

+ 39 - 0
.agents/notes/implemented/process/2026-09-12-default-product-experimental-isolation.md

@@ -0,0 +1,39 @@
+# Agent Note: Keep experimental packages outside the default product
+
+Status: implemented
+
+English | [中文](2026-09-12-default-product-experimental-isolation.zh.md)
+
+## Problem
+
+Public npm availability does not make an experimental package part of the default product. Direct manifest checks miss dependency aliases, transitive installation paths, runtime imports declared only for development, and plugins loaded by configuration. The release smoke installs every tarball together, so the presence of experimental packages in its consumer directory does not identify what the default product requires.
+
+## Decision
+
+[`verify-default-product-isolation`](../../../../scripts/verify-default-product-isolation.ts) runs in static CI and package hygiene. It follows runtime dependencies, optional dependencies, and peers from every app and the Python runtime, resolves workspace and npm aliases, and identifies experimental packages by their npm prefix or repository directory. Publication denylist membership has no effect on this classification.
+
+The source check also reads runtime imports in the selected packages, installation-owned profile bundle lists, bundle patches, shipped agent presets, and declared configuration trees. It loads the default Web layers with the production patch parser and composes them with the same patch engine used at boot. The effective rows and patched Include trees are checked, so an id-only patch cannot hide a replacement group's plugins. Disabled plugin rows remain checked; ordinary plugin configuration data is not interpreted as another Loader entry list. Missing default roots fail the check.
+
+The default Web source graph starts at the module scripts in its actual HTML entry, including inline modules and locally referenced Worker entries. The separate experimental preview can exist without joining this graph. Importing it from the default entry fails the check. Source references to Cordis configuration files include the Desktop patch in the same proof.
+
+The [Host startup smoke](../../../../apps/cli/tests/profiles/web/tests/web-default-isolation.expected.e2e.ts) launches the real Web profile from built `dsh` exports in a private Harness home. Its test-only observer reads actual Loader rows, registered fibers, modules associated with registered callbacks, and Node's loaded-module cache after readiness. The [Chromium smoke](../../../../apps/web/tests/default-product-isolation.e2e.ts) boots the delivered page and reads the actual Client Loader, registry, and module cache, requiring every delivered entry to be active. Both checks reject a real experimental plugin mounted by their explicit negative-control command. The existing expected-output and Web CI lanes own these tests after the complete build.
+
+Build-time input checks cover both browser bundling stages. The [client preset](../../../../packages/client/tsdown.client.ts) rejects experimental inputs in non-experimental outputs before their original paths disappear inside `lib/client.js`. The [Web graph check](../../../../scripts/web-product-bundle-isolation.ts) follows actual Vite output edges from `index.html`, including lazy chunks, workers, CSS dependencies, and assets. The separate preview remains outside the product graph. The build fails when required module or asset input records are missing; a failed Web build cannot produce a successful complete client-build record. The notices generator uses an explicit analysis marker for its partial graph; that marker requires output writing to be disabled. Other in-memory product builds retain the checks.
+
+[`verify-packed-install`](../../../../scripts/release/verify-packed-install.ts) follows the installed dependency graph from `@deepseek-ai/dsh`, using resolved manifest names to detect aliases and external transitive dependencies. Development dependencies and unrelated tarballs installed beside the product are excluded. Missing required dependencies fail; omitted optional dependencies remain allowed unless they name an experimental package.
+
+This check enforces the existing [experimental dependency isolation rules](../architecture/2026-08-18-experimental-agent-teams-packages.md). The [publication policy](2026-09-12-experimental-publication-denylist.md) independently determines which experimental packages explicit consumers may install.
+
+## Alternatives considered
+
+**Check only direct dependency names.** Aliases, runtime source imports, and configuration-loaded plugins can bypass that check.
+
+**Reject every installed experimental tarball.** The release smoke intentionally installs the whole release family, including opt-in packages. Only the default entry's dependency graph answers whether those packages ship as product requirements.
+
+**Scan every Web source as a default entry.** This would reject the separate experimental preview even when the default HTML and runtime imports never reach it.
+
+**Check only the final Vite module paths.** A stable `lib/client.js` path can contain code bundled earlier from an experimental package. Each bundling stage must check its actual inputs.
+
+## Consequences
+
+Experimental packages may publish without joining default installations or compositions. Source, effective composition, build-input, installed-dependency, and runtime-registry checks provide independent evidence. The SDK's [source-launch compatibility patch](../../../../apps/cli/src/sdk-source.cordis.patch.yml) is selected by a computed filesystem path and is outside static configuration discovery. Runtime smokes observe startup and Client activation; they do not replace tests for every later user-triggered execution path or for explicitly installed profile extensions.

+ 39 - 0
.agents/notes/implemented/process/2026-09-12-default-product-experimental-isolation.zh.md

@@ -0,0 +1,39 @@
+# Agent Note: 将实验包隔离在默认产品之外
+
+Status: implemented
+
+[English](2026-09-12-default-product-experimental-isolation.md) | 中文
+
+## Problem
+
+在 npm 上公开可用不意味着实验包属于默认产品。直接 manifest 检查会漏掉依赖别名、传递安装路径、仅声明为开发依赖的运行时导入,以及由配置加载的插件。发布 smoke 会一起安装所有 tarball,因此消费者目录中存在实验包并不能说明默认产品需要它。
+
+## Decision
+
+[`verify-default-product-isolation`](../../../../scripts/verify-default-product-isolation.ts) 在静态 CI 和包 hygiene 中运行。它从所有应用与 Python runtime 出发,遍历运行时依赖、可选依赖和 peer,解析 workspace 与 npm 别名,并按 npm 前缀或仓库目录识别实验包。发布 denylist 的成员关系不影响此分类。
+
+源码检查还读取所选包的运行时导入、安装自带的 profile bundle 列表、bundle patch、随产品提供的 Agent preset,以及声明的配置树。它使用生产 patch 解析器加载默认 Web 各层,并使用启动时的同一个 patch 引擎完成组合。检查对象包括最终 entry 和应用 patch 后的 Include 树,因此仅按 id 覆盖 group 的 patch 也无法隐藏替换后的插件。禁用的插件行仍纳入检查;普通插件配置数据不会被解释为另一个 Loader entry 列表。默认入口缺失会使检查失败。
+
+默认 Web 源码图从实际 HTML 入口中的 module script 出发,包括内联模块和本地引用的 Worker 入口。独立的实验预览可以存在而不加入此图;默认入口导入它时检查会失败。源码中的 Cordis 配置文件引用会将 Desktop patch 纳入同一证明。
+
+[Host 启动 smoke](../../../../apps/cli/tests/profiles/web/tests/web-default-isolation.expected.e2e.ts) 在独立 Harness home 中通过构建后的 `dsh` 导出启动真实 Web profile。测试专用观察器在就绪后读取实际 Loader 行、注册的 fiber、注册回调所属模块和 Node 已加载模块缓存。[Chromium smoke](../../../../apps/web/tests/default-product-isolation.e2e.ts) 启动实际交付的页面,读取真实 Client Loader、registry 和模块缓存,并要求所有交付 entry 均已激活。两项检查都会拒绝其显式负例命令挂载的真实实验插件。现有 expected-output 和 Web CI lane 在完整构建后执行这些测试。
+
+构建输入检查覆盖浏览器打包的两个阶段。[Client preset](../../../../packages/client/tsdown.client.ts) 在原始路径被折叠进 `lib/client.js` 前,拒绝非实验输出中的实验输入。[Web 图检查](../../../../scripts/web-product-bundle-isolation.ts) 从 `index.html` 沿真实 Vite 输出边遍历,覆盖延迟加载 chunk、Worker、CSS 依赖和资源。独立 preview 不属于产品图。缺少必需的模块或资源输入记录会使构建失败;失败的 Web 构建无法产生成功的完整 Client 构建记录。Notices 生成器通过显式分析标记检查其部分依赖图,该标记要求禁用输出写入。其他内存内产品构建仍执行这些检查。
+
+[`verify-packed-install`](../../../../scripts/release/verify-packed-install.ts) 从 `@deepseek-ai/dsh` 遍历已安装依赖图,通过解析后的 manifest 名称识别别名和外部传递依赖。开发依赖以及安装在产品旁边的不相关 tarball 不参与遍历。缺失必需依赖会失败;允许省略可选依赖,但其名称不得指向实验包。
+
+此检查执行现有的[实验包依赖隔离规则](../architecture/2026-08-18-experimental-agent-teams-packages.zh.md)。[发布策略](2026-09-12-experimental-publication-denylist.zh.md) 独立决定显式消费者可以安装哪些实验包。
+
+## Alternatives considered
+
+**只检查直接依赖名称。** 别名、运行时源码导入和配置加载的插件可以绕过该检查。
+
+**拒绝任何已安装的实验 tarball。** 发布 smoke 有意安装整个发布族,包括可选启用的包。只有默认入口的依赖图能够回答这些包是否成为产品依赖。
+
+**将全部 Web 源码视为默认入口。** 即使默认 HTML 和运行时导入从未到达独立的实验预览,这也会错误拒绝它。
+
+**只检查最终 Vite 模块路径。** 稳定的 `lib/client.js` 路径可能包含前一阶段从实验包打入的代码。每个打包阶段都必须检查实际输入。
+
+## Consequences
+
+实验包可以发布而不加入默认安装或组合。源码、最终组合、构建输入、安装依赖和运行时 registry 检查提供相互独立的证据。SDK 的[源码启动兼容 patch](../../../../apps/cli/src/sdk-source.cordis.patch.yml) 通过计算出的文件路径选择,不在静态配置发现范围内。运行时 smoke 观察启动和 Client 激活;它们不替代后续每条用户触发路径的测试,也不替代显式安装的 profile 扩展测试。

+ 6 - 0
.agents/notes/implemented/process/2026-09-12-experimental-publication-denylist.i18n.yaml

@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# 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/process/2026-09-12-experimental-publication-denylist.md
+2026-09-12-experimental-publication-denylist.md: f19faa1f391628574fd47e3064130e00c26c831a
+2026-09-12-experimental-publication-denylist.zh.md: 92a1c6d5cd0f23615f89d4779f2b9a977b1a8438

+ 27 - 0
.agents/notes/implemented/process/2026-09-12-experimental-publication-denylist.md

@@ -0,0 +1,27 @@
+# Agent Note: Publish experimental packages with explicit private exceptions
+
+Status: implemented
+
+English | [中文](2026-09-12-experimental-publication-denylist.zh.md)
+
+## Problem
+
+An allowlist for public experimental packages requires a policy edit whenever a new installable prototype joins the repository. Experimental status describes compatibility and support expectations, but internal-only prototypes still need an explicit publication exclusion.
+
+## Decision
+
+The local npm baseline publisher and the public dsh release family discover experimental packages by default. [`PRIVATE_EXPERIMENTAL_PACKAGE_DIRECTORIES`](../../../../scripts/experimental-package-policy.ts) owns explicit private exclusions. The [complete experimental publication decision](2026-09-12-publish-all-experimental-packages.md) leaves this denylist empty; all current experimental packages publish.
+
+Every experimental directory outside the denylist is public by default. Workspace constraints require public packages to omit `private` and set `publishConfig.access: public`; all experimental packages retain the `@deepseek-ai/dsh-experimental-*` npm prefix. Adding a private prototype requires a denylist entry as well as its private manifest.
+
+This decision supersedes the private publication default in the [Agent Teams package decision](../architecture/2026-08-18-experimental-agent-teams-packages.md). Its dependency isolation, opt-in composition, engineering requirements, and promotion rules remain active. Publication grants neither stability nor a support promise.
+
+## Alternatives considered
+
+**Retain a public allowlist.** Every new public experimental package needs another policy entry, even though the default publication path can discover it.
+
+**Publish every experimental package immediately.** This would expose the existing internal-only prototypes. An explicit private denylist changes the default for new packages while preserving their current publication status. The [complete publication decision](2026-09-12-publish-all-experimental-packages.md) records their separate publication.
+
+## Consequences
+
+New experimental packages join both npm publication paths without an allowlist edit. Private exceptions have one shared owner, and workspace validation rejects manifests that disagree with it.

+ 27 - 0
.agents/notes/implemented/process/2026-09-12-experimental-publication-denylist.zh.md

@@ -0,0 +1,27 @@
+# Agent Note: 默认发布实验性包并保留显式私有例外
+
+Status: implemented
+
+[English](2026-09-12-experimental-publication-denylist.md) | 中文
+
+## 问题
+
+公开实验性包的允许列表要求每个新加入仓库的可安装原型都修改一次策略。实验性状态描述兼容性与支持预期,但内部专用原型仍需要显式排除发布。
+
+## 决策
+
+本地 npm baseline 发布器与公开 dsh 发布系列默认发现实验性包。[`PRIVATE_EXPERIMENTAL_PACKAGE_DIRECTORIES`](../../../../scripts/experimental-package-policy.ts) 负责显式私有排除项。[完整实验包发布决策](2026-09-12-publish-all-experimental-packages.zh.md)使该拒绝列表保持为空;所有当前实验性包都参与发布。
+
+拒绝列表以外的每个实验性目录默认公开。workspace 约束要求公开包省略 `private` 并设置 `publishConfig.access: public`;所有实验性包保留 `@deepseek-ai/dsh-experimental-*` npm 前缀。添加私有原型需要拒绝列表条目及其私有 manifest(元数据清单)。
+
+本决策取代 [Agent Teams 包决策](../architecture/2026-08-18-experimental-agent-teams-packages.zh.md)中的默认私有发布原则。该决策的依赖隔离、显式启用组合、工程要求与 promotion 规则继续生效。发布不提供稳定性或支持承诺。
+
+## 曾考虑的替代方案
+
+**保留公开允许列表。** 即使默认发布路径能够发现新公开实验性包,每个包仍需要额外的策略条目。
+
+**立即发布所有实验性包。** 这会暴露现有的内部专用原型。显式私有拒绝列表改变新包的默认策略,同时保留这些原型的当前发布状态。[完整发布决策](2026-09-12-publish-all-experimental-packages.zh.md)记录了对这些包的单独发布决定。
+
+## 后果
+
+新实验性包无需修改允许列表即可加入两条 npm 发布路径。私有例外有一个共享 owner,workspace 校验会拒绝与之不符的 manifest。

+ 6 - 0
.agents/notes/implemented/process/2026-09-12-publish-all-experimental-packages.i18n.yaml

@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# 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/process/2026-09-12-publish-all-experimental-packages.md
+2026-09-12-publish-all-experimental-packages.md: 8bef877297d096159fcce60e6b479e12461357f2
+2026-09-12-publish-all-experimental-packages.zh.md: 6163e6b4ac11c46dac3897361585d2fcc29ef746

+ 29 - 0
.agents/notes/implemented/process/2026-09-12-publish-all-experimental-packages.md

@@ -0,0 +1,29 @@
+# Agent Note: Publish every current experimental package
+
+Status: implemented
+
+English | [中文](2026-09-12-publish-all-experimental-packages.zh.md)
+
+## Problem
+
+Users need npm access to Auto review, the Inspector, the CPython PTC backend, and browser-worker libraries without promoting their experimental APIs. Source-checkout tests alone do not establish that their tarballs contain the runtime files and imports needed by installed consumers.
+
+## Decision
+
+Every current package under `packages/experimental/` publishes in the dsh release family and the local npm baseline. Each manifest omits `private` and sets `publishConfig.access: public`. The [publication denylist](2026-09-12-experimental-publication-denylist.md) remains available for future private exceptions but contains no directories. Experimental npm names, opt-in composition, dependency isolation, compatibility expectations, and support limits remain unchanged.
+
+The Inspector tarball includes `lib/worker.js`, which its Host entry starts by a sibling URL. Its built-artifact test packs and extracts the package before starting that Worker. The WebWorker packer imports module-proxy and replacement-package tables through the runtime's public library entry; its compiled repository chunk requires no runtime TypeScript source. Its packed-consumer test imports both extracted tarballs under plain Node and mounts the resulting base image and data overlay without either package's source tree.
+
+The CPython PTC backend ships its Python scripts and still requires a supported external interpreter on Unix. The browser-worker packages expose installed library APIs; the packer's repository CLI still requires its documented built checkout. Publication does not add these packages to any shipped profile or make the browser preview a product launcher.
+
+## Alternatives considered
+
+**Keep the excluded packages private.** This prevents installed use even when the selected composition explicitly accepts experimental behavior.
+
+**Change only the manifest access fields.** The Inspector would omit its Worker, and the compiled WebWorker packer would import source files absent from the runtime tarball. Packed-consumer checks must cover the runtime files that source tests can mask.
+
+**Promote the packages into product groups.** Installation does not require removing the experimental npm names or accepting stable ownership and support obligations.
+
+## Consequences
+
+All current experimental packages are available to explicit consumers. The release set and package authors take on complete public payloads while keeping default product compositions isolated. The empty denylist retains fixture-backed validation for future private exclusions.

+ 29 - 0
.agents/notes/implemented/process/2026-09-12-publish-all-experimental-packages.zh.md

@@ -0,0 +1,29 @@
+# Agent Note: 发布所有当前实验性包
+
+Status: implemented
+
+[English](2026-09-12-publish-all-experimental-packages.md) | 中文
+
+## 问题
+
+用户需要通过 npm 使用 Auto review、Inspector、CPython PTC 后端与浏览器 worker 库,同时保留其实验性 API 状态。仅在源码 checkout 中测试,不能证明 tarball 包含已安装消费方所需的运行时文件与 import。
+
+## 决策
+
+`packages/experimental/` 下的所有当前包都通过 dsh 发布系列与本地 npm baseline 发布。各 manifest(元数据清单)省略 `private`,并设置 `publishConfig.access: public`。[发布拒绝列表](2026-09-12-experimental-publication-denylist.zh.md)仍可用于未来的私有例外,但不包含任何目录。实验性 npm 名称、显式启用组合、依赖隔离、兼容性预期与支持限制保持不变。
+
+Inspector tarball 包含 `lib/worker.js`,Host 入口通过相邻 URL 启动它。其构建产物测试先打包并解压包,再启动该 Worker。WebWorker 打包器通过运行时的公开库入口导入模块代理与替换包表;编译后的 repository 分片不需要运行时 TypeScript 源码。其已打包消费方测试在普通 Node 下导入两个解压后的 tarball,并在不使用这两个包源码目录的情况下挂载生成的基础镜像与数据叠加层。
+
+CPython PTC 后端携带 Python 脚本,在 Unix 上仍需要受支持的外部解释器。浏览器 worker 包提供已安装库 API;打包器的仓库 CLI(命令行界面)仍需要文档要求的已构建 checkout。发布不会把这些包加入任何随附 profile,也不会让浏览器预览成为产品启动入口。
+
+## 曾考虑的替代方案
+
+**让被排除的包继续保持私有。** 即使所选组合显式接受实验性行为,用户仍无法安装使用。
+
+**只修改 manifest 访问字段。** Inspector 会遗漏其 Worker,编译后的 WebWorker 打包器会导入运行时 tarball 中不存在的源文件。已打包消费方检查必须覆盖可能被源码测试掩盖的运行时文件。
+
+**将这些包提升到产品组。** 安装不需要移除实验性 npm 名称,也不需要接受稳定 owner 与支持义务。
+
+## 后果
+
+所有当前实验性包都可由显式消费方使用。发布集合与包作者承担完整公开产物的责任,同时保持默认产品组合隔离。空拒绝列表通过 fixture(测试前置数据)继续验证未来私有排除项。

+ 1 - 1
AGENTS.md

@@ -62,7 +62,7 @@ packages/    @deepseek-ai/dsh-<pkg> workspaces at packages/<group>/<pkg>/
   host/                 GUI host
   client/               GUI client
   mcp/                  external tools
-  experimental/         pre-stable prototypes; private by default with explicit public exceptions
+  experimental/         pre-stable prototypes; public by default with explicit private exceptions
   test-support/         test infrastructure
   runtime-diagnostics/  runtime invariants
   util/                 zero-dependency utilities

+ 185 - 0
apps/cli/tests/profiles/web/tests/default-web-process.ts

@@ -0,0 +1,185 @@
+/** Built-process owner shared by the Host and browser default-Web isolation smokes. */
+
+import { spawn } from 'node:child_process'
+import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
+import { tmpdir } from 'node:os'
+import { dirname, join } from 'node:path'
+import { fileURLToPath, pathToFileURL } from 'node:url'
+import { get } from 'node:http'
+import type { IncomingHttpHeaders } from 'node:http'
+import { resolveExampleLaunch } from '@deepseek-ai/dsh-loader-smoke'
+import ts from 'typescript'
+import { expect } from 'vitest'
+import type { TestContext } from 'vitest'
+import { PROCESS_SHUTDOWN_TIMEOUT_MS } from '../../../../src/process-shutdown.ts'
+import type { RuntimeRoster } from './runtime-roster.ts'
+
+const repoRoot = fileURLToPath(new URL('../../../../../../', import.meta.url))
+
+/** Live process whose IPC observer reports independently collected runtime state. */
+interface DefaultWeb {
+  url: string
+  request: (command: 'roster' | 'mount-experimental' | 'mount-experimental-entry') => Promise<RuntimeRoster>
+}
+
+/**
+ * Boot the built Web profile under plain Node and dispose it to quiescence after an assertion callback.
+ * @param test - owning Vitest case, including its timeout, cancellation, and cleanup hooks.
+ * @param inspect - assertions against the running process and its ephemeral loopback URL.
+ */
+export async function withDefaultWeb(test: TestContext, inspect: (app: DefaultWeb) => Promise<void>): Promise<void> {
+  const root = await mkdtemp(join(tmpdir(), 'dsh-web-default-isolation-'))
+  let removal: Promise<void> | undefined
+  const removeRoot = (): Promise<void> => removal ??= rm(root, { recursive: true, force: true })
+  test.onTestFinished(removeRoot)
+  try {
+    await writeFile(join(root, 'package.json'), JSON.stringify({ type: 'module' }) + '\n')
+    for (const relative of ['runtime-roster.ts', 'fixtures/runtime-roster-observer.ts']) {
+      const source = await readFile(new URL(relative, import.meta.url), 'utf8')
+      const output = ts.transpileModule(source, {
+        fileName: relative,
+        compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.ESNext, rewriteRelativeImportExtensions: true },
+      }).outputText
+      const path = join(root, relative.replace(/\.ts$/, '.js'))
+      await mkdir(dirname(path), { recursive: true })
+      await writeFile(path, output)
+    }
+    const patch = join(root, 'observer.patch.yml')
+    await writeFile(patch, JSON.stringify([{ insert: [{
+      id: 'runtime-roster-observer',
+      name: pathToFileURL(join(root, 'fixtures/runtime-roster-observer.js')).href,
+      config: { negativeControl: pathToFileURL(join(repoRoot, 'packages/experimental/client-ui-agent-team/lib/index.js')).href },
+    }] }]) + '\n')
+    const launch = resolveExampleLaunch({
+      srcBin: join(repoRoot, 'apps/cli/src/bin.ts'),
+      mode: 'lib',
+      configArgs: ['--profile', 'web', '--patch', patch, '--host', '127.0.0.1', '--port', '0', '--no-open'],
+      env: {
+        NODE_OPTIONS: undefined,
+        NODE_PATH: undefined,
+        TSX_TSCONFIG_PATH: undefined,
+        DSH_HOME: join(root, 'home'),
+        DSH_AGENTS_HOME: join(root, '.agents'),
+        DSH_TELEMETRY_DISABLED: '1',
+        DEEPSEEK_API_KEY: 'keyless-default-web-no-call',
+        NODE_NO_WARNINGS: '1',
+      },
+    })
+    test.signal.throwIfAborted()
+    const args = ['--no-experimental-strip-types', ...launch.args]
+    const child = spawn(launch.command, args, {
+      cwd: root,
+      env: { ...process.env, ...launch.env },
+      stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
+    })
+    let stdout = ''
+    let stderr = ''
+    child.stdout!.setEncoding('utf8').on('data', (chunk: string) => { stdout += chunk })
+    child.stderr!.setEncoding('utf8').on('data', (chunk: string) => { stderr += chunk })
+    const completion = Promise.withResolvers<{ code: number | null; signal: NodeJS.Signals | null }>()
+    let exited = false
+    let processError: Error | undefined
+    const pending = new Map<string, ReturnType<typeof Promise.withResolvers<RuntimeRoster>>>()
+    const rejectPending = (error: Error): void => {
+      for (const request of pending.values()) request.reject(error)
+      pending.clear()
+    }
+    child.once('error', (error) => { processError = error; rejectPending(error) })
+    child.once('close', (code, signal) => {
+      exited = true
+      rejectPending(new Error(`Web exited (code ${String(code)}, signal ${String(signal)})\n${stdout}\n${stderr}`))
+      completion.resolve({ code, signal })
+    })
+    child.once('disconnect', () => { rejectPending(new Error(`Web IPC disconnected\n${stdout}\n${stderr}`)) })
+    child.on('message', (message: { command?: string; roster?: RuntimeRoster; error?: string }) => {
+      if (message.command === undefined) return
+      const request = pending.get(message.command)
+      if (request === undefined) return
+      pending.delete(message.command)
+      if (message.error !== undefined) request.reject(new Error(message.error))
+      else if (message.roster !== undefined) request.resolve(message.roster)
+      else request.reject(new Error('Web observer returned no runtime roster'))
+    })
+    const send = (command: string): Promise<void> => new Promise((resolve, reject) => {
+      child.send(command, (error: Error | null) => { if (error) reject(error); else resolve() })
+    })
+    const request = async (command: string): Promise<RuntimeRoster> => {
+      test.signal.throwIfAborted()
+      if (exited || !child.connected) throw new Error(`Web is not connected during ${command}`)
+      const reply = Promise.withResolvers<RuntimeRoster>()
+      pending.set(command, reply)
+      void send(command).catch((error: unknown) => {
+        if (pending.get(command) !== reply) return
+        pending.delete(command)
+        reply.reject(error)
+      })
+      return reply.promise
+    }
+    let forced = false
+    let closing: Promise<Awaited<typeof completion.promise>> | undefined
+    const close = (): Promise<Awaited<typeof completion.promise>> => closing ??= (async () => {
+      const force = (): void => {
+        if (exited || child.pid === undefined) return
+        forced = true
+        child.kill('SIGKILL')
+      }
+      // The CLI owns its 5-second graceful deadline; the outer watchdog also allows that process exit to settle.
+      const kill = setTimeout(force, PROCESS_SHUTDOWN_TIMEOUT_MS * 2)
+      try {
+        if (!exited && child.connected) {
+          try { await send('stop') } catch (_closedChannel: unknown) { force() }
+        } else force()
+        return await completion.promise
+      } finally { clearTimeout(kill) }
+    })()
+    const abort = (): void => {
+      rejectPending(new Error('Web isolation test was cancelled', { cause: test.signal.reason }))
+      void close()
+    }
+    test.signal.addEventListener('abort', abort, { once: true })
+    test.onTestFinished(async () => { await close() })
+    try {
+      await expect.poll(() => {
+        test.signal.throwIfAborted()
+        if (exited) throw new Error(`Web exited before readiness\n${stdout}\n${stderr}`)
+        return /dsh web: (http:\/\/[^\s]+)/u.exec(stdout)?.[1]
+      }, { timeout: test.task.timeout }).toBeDefined()
+      const url = /dsh web: (http:\/\/[^\s]+)/u.exec(stdout)![1]!
+      await inspect({ url, request })
+    } finally {
+      const result = await close()
+      test.signal.removeEventListener('abort', abort)
+      expect(test.signal.aborted, stderr).toBe(false)
+      expect(processError, stderr).toBeUndefined()
+      expect(forced, stderr).toBe(false)
+      expect(result.signal, stderr).toBeNull()
+      expect(result.code, stderr).toBe(0)
+    }
+  } finally {
+    await removeRoot()
+  }
+}
+
+/**
+ * Read an owned loopback response without inheriting Node's process-start proxy dispatcher.
+ * @param url - URL of the test-owned Web process.
+ * @param signal - owning test cancellation signal.
+ * @param headers - optional authentication cookie.
+ * @returns complete response after its stream ends.
+ */
+export function webGet(url: string | URL, signal: AbortSignal, headers: Record<string, string> = {}): Promise<{
+  status: number | undefined
+  headers: IncomingHttpHeaders
+  text: string
+}> {
+  return new Promise((resolve, reject) => {
+    const request = get(url, { headers, agent: false, signal }, (response) => {
+      response.setEncoding('utf8')
+      let text = ''
+      response.on('data', (chunk: string) => { text += chunk })
+      response.once('error', reject)
+      response.once('end', () => { resolve({ status: response.statusCode, headers: response.headers, text }) })
+    })
+    request.once('error', reject)
+  })
+}

+ 38 - 0
apps/cli/tests/profiles/web/tests/fixtures/runtime-roster-observer.ts

@@ -0,0 +1,38 @@
+/** Test-only IPC observer mounted beside the unchanged default Web composition. */
+
+import type { Context } from '@deepseek-ai/cordis'
+import { runtimeRoster } from '../runtime-roster.ts'
+
+/** Services whose live state the observer reads. */
+export const inject = ['loader', 'clientModules']
+
+/**
+ * Observe the shipped process and optionally mount the real negative-control plugin dynamically.
+ * @param ctx - context in the real Loader tree.
+ * @param config - absolute module URL used only by the explicit negative-control command.
+ */
+export function apply(ctx: Context, config: { negativeControl: string }): void {
+  if (process.send === undefined) throw new Error('Runtime roster observer requires an IPC channel')
+  const receive = (message: unknown): void => {
+    if (message === 'stop') {
+      process.emit('SIGTERM')
+      return
+    }
+    if (message !== 'roster' && message !== 'mount-experimental' && message !== 'mount-experimental-entry') return
+    void (async () => {
+      if (message === 'mount-experimental') {
+        const namespace: unknown = await import(config.negativeControl)
+        await ctx.plugin(ctx.loader.unwrapExports(namespace))
+      }
+      if (message === 'mount-experimental-entry') await ctx.loader.create({ name: config.negativeControl })
+      await ctx.loader.await()
+      process.send!({ command: message, roster: runtimeRoster(ctx) })
+    })().catch((error: unknown) => {
+      process.send!({ command: message, error: error instanceof Error ? error.stack : String(error) })
+    })
+  }
+  ctx.effect(() => {
+    process.on('message', receive)
+    return () => { process.off('message', receive) }
+  })
+}

+ 95 - 0
apps/cli/tests/profiles/web/tests/runtime-roster.ts

@@ -0,0 +1,95 @@
+/** Runtime evidence collected from the real Web process for default-product isolation. */
+
+import { existsSync, readFileSync } from 'node:fs'
+import { createRequire } from 'node:module'
+import { dirname, join } from 'node:path'
+import { fileURLToPath, pathToFileURL } from 'node:url'
+import type { Context, Plugin } from '@deepseek-ai/cordis'
+import type {} from '@deepseek-ai/cordis-plugin-loader'
+import type { WebBootGraph } from '@deepseek-ai/dsh-client-modules/client'
+import type {} from '@deepseek-ai/dsh-client-modules'
+
+/** Observed Loader entries, registered plugin instances, loaded modules, and delivered Client entries. */
+export interface RuntimeRoster {
+  entries: Array<{ id: string; name: string; state: number | undefined }>
+  plugins: Array<{ name: string; owner: string | undefined; state: number; modules: string[] }>
+  modules: string[]
+  client: WebBootGraph
+}
+
+/**
+ * Find a loaded file's npm package without importing additional application code.
+ * @param url - file module URL from the running process.
+ * @returns owning npm package name, if the module belongs to a package.
+ */
+export function modulePackage(url: string): string | undefined {
+  if (!url.startsWith('file:')) return undefined
+  let directory = dirname(fileURLToPath(url))
+  for (;;) {
+    const manifest = join(directory, 'package.json')
+    if (existsSync(manifest)) {
+      const parsed = JSON.parse(readFileSync(manifest, 'utf8')) as { name?: string }
+      return parsed.name
+    }
+    const parent = dirname(directory)
+    if (parent === directory) return undefined
+    directory = parent
+  }
+}
+
+/**
+ * Collect the settled process's actual Loader, Cordis registry, and Node module cache.
+ * @param ctx - observer context inside the shipped Web profile.
+ * @returns independently inspectable runtime records; unavailable module introspection fails the smoke.
+ */
+export function runtimeRoster(ctx: Context): RuntimeRoster {
+  const internal = ctx.loader.internal
+  if (!internal) throw new Error('Web isolation smoke requires Node module-cache introspection')
+  const modules = new Set<string>([...internal.loadCache.keys()])
+  const callbacks = new Map<object, string[]>()
+  for (const [url, records] of internal.loadCache) {
+    for (const job of Object.values(records)) {
+      if (!job?.module) continue
+      const namespace: unknown = job.module.getNamespace()
+      if (typeof namespace !== 'object' || namespace === null) continue
+      for (const value of Object.values(namespace)) {
+        if (typeof value !== 'function' && (typeof value !== 'object' || value === null)) continue
+        const callback = ctx.registry.resolve(value as Plugin)
+        if (callback === undefined) continue
+        const urls = callbacks.get(callback) ?? []
+        if (!urls.includes(url)) urls.push(url)
+        callbacks.set(callback, urls)
+      }
+    }
+  }
+  for (const path of Object.keys(createRequire(import.meta.url).cache)) modules.add(pathToFileURL(path).href)
+  return {
+    entries: [...ctx.loader.entries()].map(entry => ({ id: entry.id, name: entry.options.name, state: entry.fiber?.state })),
+    plugins: [...ctx.registry.values()].flatMap(runtime => [...runtime.fibers].map(fiber => ({
+      name: runtime.name ?? runtime.callback.name,
+      owner: fiber.entry?.options.name,
+      state: fiber.state,
+      modules: callbacks.get(runtime.callback) ?? [],
+    }))),
+    modules: [...modules].sort(),
+    client: ctx.clientModules.graph(),
+  }
+}
+
+/**
+ * Locate experimental package identities and experimental source/artifact paths in runtime evidence.
+ * @param roster - independently collected Web runtime records.
+ * @returns diagnostic evidence for every forbidden package reference.
+ */
+export function experimentalRuntimeReferences(roster: RuntimeRoster): string[] {
+  const references = new Set([
+    ...roster.entries.map(entry => entry.name),
+    ...roster.plugins.flatMap(plugin => [plugin.owner ?? '', ...plugin.modules]),
+    ...roster.modules,
+    ...roster.client.entries.flatMap(entry => [entry.id, ...entry.inject ?? [], ...entry.external ?? []]),
+    ...roster.client.batches.flatMap(batch => batch.entries),
+  ])
+  return [...references].filter(reference => reference.includes('@deepseek-ai/dsh-experimental-')
+    || (reference.startsWith('file:') && (fileURLToPath(reference).replaceAll('\\', '/').includes('/packages/experimental/')
+      || modulePackage(reference)?.startsWith('@deepseek-ai/dsh-experimental-')))).sort()
+}

+ 42 - 0
apps/cli/tests/profiles/web/tests/web-default-isolation.expected.e2e.ts

@@ -0,0 +1,42 @@
+/** Real Web startup, mounted plugin package identities, and delivered Client graph isolation. */
+
+import { FiberState } from '@deepseek-ai/cordis'
+import type { WebBootGraph } from '@deepseek-ai/dsh-client-modules/client'
+import { expect, it } from 'vitest'
+import { experimentalRuntimeReferences, modulePackage } from './runtime-roster.ts'
+import { withDefaultWeb, webGet } from './default-web-process.ts'
+
+const experimentalName = '@deepseek-ai/dsh-experimental-client-ui-agent-team'
+
+it('boots the default Web profile without experimental Host modules, mounted plugins, or Client entries', async (test) => {
+  await withDefaultWeb(test, async ({ url, request }) => {
+    const auth = await webGet(url, test.signal)
+    const cookie = auth.headers['set-cookie']?.[0]?.split(';', 1)[0]
+    expect(cookie).toBeDefined()
+    const page = await webGet(new URL('/', url), test.signal, { cookie: cookie! })
+    expect(page.status).toBe(200)
+    const html = page.text
+    const rawBoot = /globalThis\["__DSH_BOOT__"\] = ([\s\S]*?)<\/script>/u.exec(html)?.[1]
+    expect(rawBoot, html).toBeDefined()
+    const delivered = JSON.parse(rawBoot!) as WebBootGraph
+    const roster = await request('roster')
+    expect(roster.client).toEqual(delivered)
+    expect(roster.entries).toEqual(expect.arrayContaining([
+      expect.objectContaining({ name: '@deepseek-ai/dsh-host-webserver', state: FiberState.ACTIVE }),
+      expect.objectContaining({ name: '@deepseek-ai/dsh-client-modules', state: FiberState.ACTIVE }),
+    ]))
+    expect(roster.entries.some(entry => entry.name.endsWith('/runtime-roster-observer.js') && entry.state === FiberState.ACTIVE)).toBe(true)
+    expect(roster.plugins.length).toBeGreaterThan(roster.entries.length)
+    expect(roster.modules.some(url => modulePackage(url) === '@deepseek-ai/dsh')).toBe(true)
+    expect(roster.client.entries.length).toBeGreaterThan(0)
+    expect(experimentalRuntimeReferences(roster)).toEqual([])
+
+    const contaminated = await request('mount-experimental')
+    expect(contaminated.entries.some(entry => entry.name === experimentalName)).toBe(false)
+    const mounted = contaminated.plugins.filter(plugin => plugin.modules.some(url => modulePackage(url) === experimentalName))
+    expect(mounted).toEqual([expect.objectContaining({ state: FiberState.ACTIVE })])
+    expect(experimentalRuntimeReferences(contaminated)).toEqual(expect.arrayContaining([
+      expect.stringContaining('/packages/experimental/client-ui-agent-team/'),
+    ]))
+  })
+})

+ 105 - 0
apps/web/product-isolation.ts

@@ -0,0 +1,105 @@
+/** Vite adapters for recording product chunk, asset, CSS, and worker inputs. */
+
+import type { Plugin } from 'vite'
+import { WebProductBundleIsolation } from '../../scripts/web-product-bundle-isolation.ts'
+
+const DEPENDENCY_ANALYSIS_PLUGIN = 'dsh-browser-dependency-analysis'
+
+/**
+ * Identify a dependency-disclosure walk that deliberately leaves third-party imports external.
+ * @returns Analysis intent that refuses builds which can write output.
+ */
+export function browserDependencyAnalysis(): Plugin {
+  return {
+    name: DEPENDENCY_ANALYSIS_PLUGIN,
+    apply: 'build',
+    configResolved(config) { requireNonWritingAnalysis(config.build.write) },
+  }
+}
+
+function requireNonWritingAnalysis(write: boolean): void {
+  if (write !== false) throw new Error('browser dependency analysis requires build.write: false')
+}
+
+/**
+ * Reject experimental inputs reachable from the built default Web page.
+ * @param repository - repository root supplying package ownership.
+ * @param webRoot - Vite root containing index.html and public assets.
+ * @returns Build plugins that preserve Vite's output and fail before publication on a violation.
+ */
+export function productWebBundleIsolation(repository: string, webRoot: string): Plugin[] {
+  const inputs = new WebProductBundleIsolation(repository, webRoot)
+  let dependencyAnalysis = false
+  return [{
+    name: 'dsh-product-web-chunk-inputs',
+    apply: 'build',
+    generateBundle: {
+      order: 'pre',
+      handler(_options, bundle) {
+        if (!dependencyAnalysis) inputs.captureChunks(bundle)
+      },
+    },
+  }, {
+    name: 'dsh-product-web-bundle-isolation',
+    apply: 'build',
+    enforce: 'post',
+    config(config) {
+      const renderBuiltUrl = config.experimental?.renderBuiltUrl
+      const workerPlugins = config.worker?.plugins
+      return {
+        experimental: {
+          renderBuiltUrl(filename, context) {
+            inputs.assetReference(filename, context.hostId, context.type)
+            return renderBuiltUrl?.(filename, context)
+          },
+        },
+        worker: {
+          plugins: () => [...workerPlugins?.() ?? [], {
+            name: 'dsh-worker-build-inputs',
+            generateBundle: {
+              order: 'post',
+              handler(_options, bundle) { inputs.workerBundle(bundle, this.getWatchFiles()) },
+            },
+          }],
+        },
+      }
+    },
+    configResolved(config) {
+      dependencyAnalysis = config.plugins.some(plugin => plugin.name === DEPENDENCY_ANALYSIS_PLUGIN)
+      if (dependencyAnalysis) {
+        requireNonWritingAnalysis(config.build.write)
+        return
+      }
+      const cssPlugins = config.plugins.filter(plugin => plugin.name === 'vite:css')
+      const css = cssPlugins[0]
+      if (cssPlugins.length !== 1 || css?.transform === undefined) {
+        throw new Error('Web product isolation: Vite CSS input instrumentation is unavailable')
+      }
+      const transform = typeof css.transform === 'function' ? css.transform : css.transform.handler
+      css.transform = {
+        ...typeof css.transform === 'function' ? {} : css.transform,
+        handler(code, id, options) {
+          inputs.cssTransform(id)
+          const context = new Proxy(this, {
+            get(target, property) {
+              if (property === 'addWatchFile') return (file: string): void => {
+                inputs.cssDependency(id, file)
+                target.addWatchFile(file)
+              }
+              const value: unknown = Reflect.get(target, property, target)
+              return typeof value === 'function' ? value.bind(target) : value
+            },
+          })
+          return transform.call(context, code, id, options)
+        },
+      }
+    },
+    buildStart() { inputs.reset() },
+    generateBundle: {
+      order: 'post',
+      handler(_options, bundle) {
+        if (!dependencyAnalysis) inputs.verify(bundle, id => this.getModuleInfo(id))
+      },
+    },
+  }]
+}

+ 112 - 0
apps/web/tests/default-product-isolation.e2e.ts

@@ -0,0 +1,112 @@
+/** Chromium acceptance of the shipped Web profile's actual Client plugin and module registries. */
+
+import { FiberState } from '@deepseek-ai/cordis'
+import type { Context, Plugin, RegistryService } from '@deepseek-ai/cordis'
+import type { ClientModuleLoader, ClientModuleLoaderTarget } from '@deepseek-ai/dsh-client-modules/client'
+import { chromium } from 'playwright'
+import { expect, it } from 'vitest'
+import { withDefaultWeb } from '../../cli/tests/profiles/web/tests/default-web-process.ts'
+import { newEnglishPage } from './support.ts'
+
+interface ClientObservation {
+  ctx?: Context
+  modules?: ClientModuleLoader
+}
+
+async function readClientRoster() {
+  const observation = Reflect.get(globalThis, '__dshIsolationObservation') as ClientObservation
+  const { ctx, modules } = observation
+  if (ctx === undefined || modules === undefined) throw new Error('Real Client registry was not observed')
+  await ctx.loader.await()
+  const callbacks = new Map<object, string[]>()
+  for (const [id, record] of modules.loadCache) {
+    if (typeof record.exports !== 'object' || record.exports === null) continue
+    for (const value of Object.values(record.exports)) {
+      if (typeof value !== 'function' && (typeof value !== 'object' || value === null)) continue
+      const callback = ctx.registry.resolve(value as Plugin)
+      if (callback !== undefined) callbacks.set(callback, [...callbacks.get(callback) ?? [], id])
+    }
+  }
+  return {
+    entries: [...ctx.loader.entries()].map(entry => ({ name: entry.options.name, state: entry.fiber?.state })),
+    plugins: [...ctx.registry.values()].flatMap(runtime => [...runtime.fibers].map(fiber => ({
+      name: runtime.name ?? runtime.callback.name,
+      owner: fiber.entry?.options.name,
+      state: fiber.state,
+      modules: callbacks.get(runtime.callback) ?? [],
+    }))),
+    modules: [...modules.loadCache].flatMap(([id, record]) => [id, ...record.edges]),
+  }
+}
+
+function experimentalClientReferences(roster: Awaited<ReturnType<typeof readClientRoster>>): string[] {
+  return [
+    ...roster.entries.map(entry => entry.name),
+    ...roster.plugins.flatMap(plugin => [plugin.owner ?? '', ...plugin.modules]),
+    ...roster.modules,
+  ].filter(name => name.startsWith('@deepseek-ai/dsh-experimental-'))
+}
+
+it('activates the actual default Client registry without experimental packages', async (test) => {
+  await withDefaultWeb(test, async ({ url, request }) => {
+    const browser = await chromium.launch({ timeout: test.task.timeout })
+    test.onTestFinished(async () => { await browser.close() })
+    try {
+      const page = await newEnglishPage(browser)
+      page.setDefaultTimeout(test.task.timeout)
+      const errors: string[] = []
+      page.on('pageerror', error => errors.push(error.message))
+      await page.addInitScript(() => {
+        const observation: ClientObservation = {}
+        Reflect.set(globalThis, '__dshIsolationObservation', observation)
+        Object.defineProperty(globalThis, '__ModuleLoader__', {
+          configurable: true,
+          set(target: ClientModuleLoaderTarget) {
+            Object.defineProperty(globalThis, '__ModuleLoader__', { configurable: true, writable: true, value: target })
+            const create = target.create.bind(target)
+            target.create = function (options) {
+              const cordis = options.staticModules['@deepseek-ai/cordis'] as { RegistryService: typeof RegistryService }
+              const prototype = cordis.RegistryService.prototype
+              // eslint-disable-next-line @typescript-eslint/unbound-method -- apply() preserves the runtime registry receiver.
+              const plugin = prototype.plugin
+              // Capture the first real root and immediately restore the registry method.
+              prototype.plugin = function (...args: Parameters<RegistryService['plugin']>) {
+                prototype.plugin = plugin
+                observation.ctx = this.ctx.root
+                return plugin.apply(this, args)
+              }
+              const modules = create(options)
+              observation.modules = modules
+              return modules
+            }
+          },
+        })
+      })
+      const navigation = await page.goto(url)
+      expect(navigation?.status()).toBe(200)
+      await page.getByRole('tree', { name: 'Sessions' }).waitFor({ state: 'visible' })
+      const roster = await page.evaluate(readClientRoster)
+      const host = await request('roster')
+      expect(roster.entries.map(entry => entry.name).sort()).toEqual(host.client.entries.map(entry => entry.id).sort())
+      expect(roster.entries.every(entry => entry.state === FiberState.ACTIVE)).toBe(true)
+      expect(roster.plugins.length).toBeGreaterThan(roster.entries.length)
+      expect(roster.plugins.some(plugin => plugin.modules.includes('@deepseek-ai/dsh-client-ui-layout'))).toBe(true)
+      expect(experimentalClientReferences(roster)).toEqual([])
+      const contaminatedHost = await request('mount-experimental-entry')
+      const experimentalName = '@deepseek-ai/dsh-experimental-client-ui-agent-team'
+      expect(contaminatedHost.client.entries.map(entry => entry.id)).toContain(experimentalName)
+      await page.reload()
+      await expect.poll(async () => {
+        const current = await page.evaluate(readClientRoster)
+        return current.entries.some(entry => entry.name === experimentalName && entry.state === FiberState.ACTIVE)
+      }, { timeout: test.task.timeout }).toBe(true)
+      const contaminated = await page.evaluate(readClientRoster)
+      expect(contaminated.plugins.some(plugin => plugin.modules.includes(experimentalName)
+        && plugin.state === FiberState.ACTIVE)).toBe(true)
+      expect(experimentalClientReferences(contaminated)).toContain(experimentalName)
+      expect(errors).toEqual([])
+    } finally {
+      await browser.close()
+    }
+  })
+})

+ 1 - 0
apps/web/tsconfig.json

@@ -22,6 +22,7 @@
   // this client-registered project must not also hold them (one program
   // cannot see both sides of the cordis Context merges).
   "exclude": [
+    "tests/default-product-isolation.e2e.ts",
     "tests/scaffold.ts",
     "tests/auto-review-fixture.ts",
     "tests/scaffold-generation.spec.ts",

+ 17 - 6
apps/web/vite.config.ts

@@ -1,9 +1,11 @@
 import { readFile, writeFile } from 'node:fs/promises'
+import { resolve } from 'node:path'
 import { fileURLToPath } from 'node:url'
 import { defineConfig } from 'vite'
 import type { Plugin } from 'vite'
 import react from '@vitejs/plugin-react'
 import { clientBuildEnvironmentDefines } from '../../scripts/client-build-environment.ts'
+import { productWebBundleIsolation } from './product-isolation.ts'
 
 const src = (rel: string): string => fileURLToPath(new URL(rel, import.meta.url))
 const STANDALONE_ERROR = 'apps/web is not a standalone application: bare Vite cannot inject window.__DSH_BOOT__. '
@@ -47,10 +49,17 @@ function rejectStandaloneServe(): Plugin {
 function emitPreviewPage(): Plugin {
   let bootstrapFile: string | undefined
   let write = true
+  let written = false
+  let outputDirectory = ''
   return {
     name: 'dsh-emit-preview-page',
     configResolved(config) {
       write = config.build.write
+      outputDirectory = resolve(config.root, config.build.outDir)
+    },
+    buildStart() {
+      bootstrapFile = undefined
+      written = false
     },
     generateBundle(_options, bundle) {
       if (!write) return
@@ -59,15 +68,14 @@ function emitPreviewPage(): Plugin {
       }
       if (bootstrapFile === undefined) throw new Error('vite: preview bootstrap entry missing from the bundle')
     },
+    writeBundle() { written = true },
     async closeBundle() {
-      if (!write) return
-      // A build that failed before generateBundle has no page to splice.
-      if (bootstrapFile === undefined) return
-      const page = await readFile(src('./dist/index.html'), 'utf8')
+      if (!write || !written || bootstrapFile === undefined) return
+      const page = await readFile(resolve(outputDirectory, 'index.html'), 'utf8')
       const anchor = page.indexOf('<script type="module"')
       if (anchor === -1) throw new Error('vite: built index.html lost its module entry tag')
       const tag = `<script type="module" crossorigin src="./${bootstrapFile}"></script>`
-      await writeFile(src('./dist/preview.html'), `${page.slice(0, anchor)}${tag}${page.slice(anchor)}`)
+      await writeFile(resolve(outputDirectory, 'preview.html'), `${page.slice(0, anchor)}${tag}${page.slice(anchor)}`)
     },
   }
 }
@@ -147,7 +155,10 @@ export default defineConfig({
   // Relative asset URLs: preview.html mounts the same output under any base
   // directory, and the served index resolves identically from the site root.
   base: './',
-  plugins: [rejectStandaloneServe(), clientDocumentTitle(), react(), emitPreviewPage()],
+  plugins: [
+    rejectStandaloneServe(), clientDocumentTitle(), react(), emitPreviewPage(),
+    productWebBundleIsolation(src('../..'), src('.')),
+  ],
   build: {
     // The worker bootstrap holds its page at top-level await; Vite's default
     // `modules` target (es2020-era) rejects that syntax.

+ 1 - 0
package.json

@@ -130,6 +130,7 @@
     "verify-node-next-types": "tsx scripts/verify-node-next-types.ts",
     "verify-optional-dependency-imports": "tsx scripts/verify-optional-dependency-imports.ts",
     "verify-runtime-closure": "tsx scripts/verify-runtime-closure.ts",
+    "verify-default-product-isolation": "tsx scripts/verify-default-product-isolation.ts",
     "verify-application-entrypoints": "tsx scripts/verify-application-entrypoints.ts",
     "verify-package-dependencies": "tsx scripts/verify-package-dependencies.ts",
     "verify-npm-install-layout": "tsx scripts/verify-npm-install-layout.ts",

+ 2 - 2
packages/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/README.md
-README.md: eaea9ac6c9c18ebba9e01bcbaff3ae070b9a78de
-README.zh.md: 7941839c7fb924baaeb7df227c085265b66dac76
+README.md: b15d9b11822772702b92fac9be24afdbdd885785
+README.zh.md: 1f572953fffc2bbd7a55a9417ee179e43b64f300

+ 2 - 2
packages/README.md

@@ -50,7 +50,7 @@ Every package lives in exactly one group; new packages join existing groups, and
 | [`context/`](context/README.md) | Model-visible request context: workspace instructions, time context, references |
 | [`subagent/`](subagent/README.md) | Subagent capability family: provider-registry contract and model-facing delegation tools |
 | [`jobs/`](jobs/README.md) | Generic background-job runtime and model-facing job control tools |
-| [`experimental/`](experimental/README.md) | Experimental prototypes with explicit public npm exceptions |
+| [`experimental/`](experimental/README.md) | Pre-stable prototypes with explicit private exceptions |
 | [`workflow/`](workflow/README.md) | Workflow seam, worker-thread engine, and model-facing `workflow`/`ralph` tools |
 | [`webhook/`](webhook/README.md) | Verified external events, trusted rules, and fire-and-forget Workspace Sessions |
 | [`web/`](web/README.md) | Web capability family: seam, search/fetch providers, model-facing web tools |
@@ -85,7 +85,7 @@ Every package lives in exactly one group; new packages join existing groups, and
 <a id="release-expectations"></a>
 ## Release expectations
 
-Most groups are product — stable API. The exceptions: `experimental/` publishes only explicitly allowlisted packages, and `test-support/`, `runtime-diagnostics/`, and `util/` are support with lower compatibility expectations.
+Most groups are product — stable API. The exceptions: `experimental/` publishes without stability or support promises, and `test-support/`, `runtime-diagnostics/`, and `util/` are support with lower compatibility expectations.
 
 -----
 

+ 2 - 2
packages/README.zh.md

@@ -50,7 +50,7 @@ harness 由 `packages/` 下的 npm 包组装而成,按能力系列分组:会
 | [`context/`](context/README.zh.md) | 模型可见请求上下文:workspace 指令、时间上下文、引用 |
 | [`subagent/`](subagent/README.zh.md) | subagent 能力系列:提供方注册表约定和面向模型的委托工具 |
 | [`jobs/`](jobs/README.zh.md) | 通用后台任务运行时和面向模型的作业控制工具 |
-| [`experimental/`](experimental/README.zh.md) | 实验性原型和显式公开 npm 例外 |
+| [`experimental/`](experimental/README.zh.md) | 预稳定原型,包含显式私有例外 |
 | [`workflow/`](workflow/README.zh.md) | 工作流 seam、worker 线程引擎、面向模型的 `workflow`/`ralph` 工具 |
 | [`webhook/`](webhook/README.zh.md) | 已验证外部事件、受信规则与即发即弃 Workspace 会话 |
 | [`web/`](web/README.zh.md) | Web 能力系列:seam、搜索/获取提供方、面向模型的 Web 工具 |
@@ -85,7 +85,7 @@ harness 由 `packages/` 下的 npm 包组装而成,按能力系列分组:会
 <a id="release-expectations"></a>
 ## 发布预期
 
-大多数组属于产品组,提供稳定 API。例外:`experimental/` 仅发布显式允许列表中的包,`test-support/`、`runtime-diagnostics/` 与 `util/` 是兼容性预期较低的支持组。
+大多数组属于产品组,提供稳定 API。例外:`experimental/` 发布时不提供稳定性或支持承诺,`test-support/`、`runtime-diagnostics/` 与 `util/` 是兼容性预期较低的支持组。
 
 -----
 

+ 70 - 5
packages/client/tsdown.client.ts

@@ -7,17 +7,20 @@
  * and injects a tagged style at factory execution, while `x.css?inline`
  * exports compiled text for a plugin-owned lifecycle effect. The virtual
  * loaders register each real stylesheet as a watch dependency.
+ * Non-experimental client outputs reject experimental module and stylesheet
+ * inputs, including origins recorded by chained source maps.
  */
 import { readFile } from 'node:fs/promises'
 import { existsSync, globSync, readFileSync } from 'node:fs'
 import { isBuiltin } from 'node:module'
 import { basename, dirname, isAbsolute, relative, resolve as resolvePath, sep } from 'node:path'
 import { fileURLToPath } from 'node:url'
-import type { UserConfig } from 'tsdown'
+import type { TsdownPlugin, UserConfig } from 'tsdown'
 import { transform } from 'lightningcss'
 import { optionalStringArray } from './modules/src/client/manifest.ts'
 import { PLATFORM_MODULES, PRELOADED_CLIENT_EXTERNALS } from './web/src/platform.ts'
 import { clientBuildEnvironmentDefines } from '../../scripts/client-build-environment.ts'
+import { BundleInputIsolation, physicalBundleInput } from '../../scripts/bundle-input-isolation.ts'
 
 /**
  * Virtual-id wrapper keeping module CSS away from tsdown's own css pipeline
@@ -255,6 +258,7 @@ interface AssetEmitter {
 
 function staticLinkedConfig(id: string, entry: string, outputName = basename(entry, '.js')): UserConfig {
   const emitted = new Set<string>()
+  const isolation = clientInputIsolation(id)
   return {
     name: id,
     entry: { [outputName]: entry },
@@ -268,7 +272,10 @@ function staticLinkedConfig(id: string, entry: string, outputName = basename(ent
     // The shell compiles this artifact, so its map is the only path from a
     // browser stack frame back to the TSX (tsc emits the lib/types half).
     sourcemap: true,
-    outputOptions: { sourcemapExcludeSources: false },
+    outputOptions: {
+      sourcemapExcludeSources: false,
+      sourcemapPathTransform: isolation.sourcePath,
+    },
     plugins: [{
       // Contract 1. `pre` because tsdown's own deps plugin would otherwise
       // resolve and inline every specifier missing from the npm production
@@ -283,7 +290,7 @@ function staticLinkedConfig(id: string, entry: string, outputName = basename(ent
           return isBareSpecifier(source) ? { id: source, external: true } : null
         },
       },
-    }, tscSourceMapPlugin(), {
+    }, tscSourceMapPlugin(), isolation.plugin, {
       // Contract 4. The import survives verbatim and the sheet lands beside the
       // JavaScript, so the shell's CSS Modules pipeline sees a real stylesheet.
       name: 'dsh-css-asset',
@@ -427,6 +434,7 @@ function matchesSpecifier(patterns: readonly RegExp[], specifier: string): boole
 
 function clientConfig(id: string, entry: string): UserConfig {
   const isRequested = (specifier: string): boolean => clientExternals(id).has(specifier)
+  const isolation = clientInputIsolation(id)
   return {
     name: `${id}/client`,
     entry: { client: entry },
@@ -498,7 +506,7 @@ function clientConfig(id: string, entry: string): UserConfig {
           + '(type-only imports are erased and never reach this gate)',
         )
       },
-    }, tscSourceMapPlugin(), {
+    }, tscSourceMapPlugin(), isolation.plugin, {
       name: 'dsh-css-modules-inline',
       resolveId(source: string, importer: string | undefined) {
         if (!source.endsWith('.module.css')) return null
@@ -562,7 +570,10 @@ function clientConfig(id: string, entry: string): UserConfig {
       // browser resolves its local sources back into URLs that mirror the
       // /packages/<group>/<package>/src directories; sourcesContent keeps them usable
       // without exposing that tree as an HTTP route.
-      sourcemapPathTransform: browserSourcePath,
+      sourcemapPathTransform(source, mapPath) {
+        isolation.sourcePath(source, mapPath)
+        return browserSourcePath(source, mapPath)
+      },
       banner: `window.__ModuleLoader__.load({ id: ${JSON.stringify(id)}, factory: (require) => {`,
       footer: 'return module.exports; } });',
       intro: 'var module = { exports: {} }; var exports = module.exports;',
@@ -570,6 +581,60 @@ function clientConfig(id: string, entry: string): UserConfig {
   }
 }
 
+/** Check browser bundle inputs before their original package identity is folded into an artifact. */
+function clientInputIsolation(id: string): {
+  plugin: TsdownPlugin
+  sourcePath: (source: string, mapPath: string) => string
+} {
+  const experimental = id.startsWith('@deepseek-ai/dsh-experimental-')
+  const inputs = new BundleInputIsolation(REPOSITORY_ROOT, `client bundle isolation (${id})`)
+  return {
+    plugin: {
+      name: 'dsh-client-input-isolation',
+      buildStart() { inputs.reset() },
+      generateBundle(_options, bundle) {
+        if (experimental) return
+        for (const output of Object.values(bundle)) {
+          if (output.type === 'chunk') {
+            for (const module of Object.keys(output.modules)) {
+              inputs.assertInput(clientInputFile(module))
+              const info = this.getModuleInfo(module)
+              if (info === null) {
+                // Rolldown's runtime helper is compiler-generated and has no source module record.
+                if (module === '\0rolldown/runtime.js') continue
+                throw new Error(`client bundle isolation (${id}): module ${module} has no bundler module record`)
+              }
+              for (const dependency of [...info.importedIds, ...info.dynamicallyImportedIds]) {
+                inputs.assertInput(clientInputFile(dependency))
+              }
+            }
+            for (const external of [...output.imports, ...output.dynamicImports]) {
+              if (!(external in bundle)) inputs.assertInput(external)
+            }
+          } else {
+            for (const original of output.originalFileNames) inputs.assertInput(original)
+          }
+        }
+      },
+    },
+    sourcePath(source, mapPath) {
+      if (!experimental) {
+        const decoded = clientInputFile(source)
+        const file = physicalBundleInput(decoded) ?? resolvePath(dirname(mapPath), decoded)
+        inputs.assertSourceMapInput(file)
+      }
+      return source
+    },
+  }
+}
+
+/** CSS loader ids append a JavaScript suffix to the physical stylesheet path. */
+function clientInputFile(id: string): string {
+  const prefix = [CSS_VIRTUAL_PREFIX, GLOBAL_CSS_VIRTUAL_PREFIX, INLINE_CSS_VIRTUAL_PREFIX]
+    .find(prefix => id.startsWith(prefix))
+  return prefix === undefined ? id : id.slice(prefix.length, -CSS_VIRTUAL_SUFFIX.length)
+}
+
 /** Chain tsc's emitted maps into any Client bundle that consumes `lib/types`. */
 function tscSourceMapPlugin() {
   return {

+ 4 - 4
packages/experimental/AGENTS.md

@@ -1,9 +1,9 @@
 # AGENTS.md — Experimental packages
 
-These rules supplement the [package rules](../AGENTS.md). The [experimental Agent Teams package decision](../../.agents/notes/implemented/architecture/2026-08-18-experimental-agent-teams-packages.md) owns the rationale.
+These rules supplement the [package rules](../AGENTS.md). The [experimental publication decision](../../.agents/notes/implemented/process/2026-09-12-experimental-publication-denylist.md) owns the publication policy; the [Agent Teams package decision](../../.agents/notes/implemented/architecture/2026-08-18-experimental-agent-teams-packages.md) owns dependency isolation and promotion rationale.
 
 - A package belongs here only when its complete public contract is experimental or internal-only. An experimental option inside a release package stays with its owning product role.
-- Every package here uses the `@deepseek-ai/dsh-experimental-*` npm prefix. Packages are private by default and omit `publishConfig`; the allowlisted Agent Teams and computer-use provider packages are explicit public exceptions that keep their experimental names, set `publishConfig.access: public`, and join the dsh release family.
-- Release packages and apps outside this group must not name experimental packages in `dependencies`, `optionalDependencies`, or `peerDependencies`. Experimental packages may depend on release packages and each other. Tests may use experimental packages through `devDependencies`; examples may load them explicitly.
+- Every package here uses the `@deepseek-ai/dsh-experimental-*` npm prefix and joins the dsh release family by default: omit `private` and set `publishConfig.access: public`. Only directories in `PRIVATE_EXPERIMENTAL_PACKAGE_DIRECTORIES` in [the publication policy](../../scripts/experimental-package-policy.ts) stay private: set `private: true` and omit `publishConfig`.
+- Release packages and apps outside this group must not name experimental packages in `dependencies`, `optionalDependencies`, or `peerDependencies`. [Default-product isolation](../../.agents/notes/implemented/process/2026-09-12-default-product-experimental-isolation.md) also checks transitive installations, runtime imports, and shipped compositions in CI. Experimental packages may depend on release packages and each other. Tests may use experimental packages through `devDependencies`; examples may load them explicitly.
 - Experimental status does not relax engineering, security, documentation, lifecycle, testing, invariant, or snapshot requirements.
-- Publishing an explicit exception does not promote it or add a stability promise. Promotion moves a package to its product-role group and removes `experimental-` from its npm name; update every import and configuration row atomically, then review its public contract, limitations, test evidence, release payload, runtime dependents, and named stable owner.
+- Publishing an experimental package does not promote it or add a stability promise. Promotion moves a package to its product-role group and removes `experimental-` from its npm name; update every import and configuration row atomically, then review its public contract, limitations, test evidence, release payload, runtime dependents, and named stable owner.

+ 2 - 2
packages/experimental/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/experimental/README.md
-README.md: a3bba024b5f2dfa3609c54e9a6288463ac475412
-README.zh.md: 0b2ee06ec301c41ae73140300ace9d74510e9efc
+README.md: c0f9c7e97df5eda82eea4c18276671549749c786
+README.zh.md: c020d2592b65aa7973e0ae761a4f7341b5aa7b5a

+ 3 - 3
packages/experimental/README.md

@@ -1,5 +1,5 @@
 ---
-description: "The experimental group map: pre-stable prototypes that are private by default, with explicit public Agent Teams and computer-use packages."
+description: "The experimental group map: publicly installable pre-stable prototypes."
 kind: "package-group"
 ---
 
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
 
 ## Summary
 
-The experimental group contains prototype capabilities whose contracts can change and carry no support promise. Packages are private by default; the allowlisted Agent Teams and Cua Driver packages are published opt-in exceptions under their existing `@deepseek-ai/dsh-experimental-*` names. The group also holds the private cross-realm Inspector, CPython subprocess backend, and browser-worker preview packages. Released products outside this group must not depend on experimental packages.
+The experimental group contains prototype capabilities whose contracts can change and carry no support promise. All current packages publish under their `@deepseek-ai/dsh-experimental-*` names, including the opt-in Agent Teams composition, Auto review, Cua Driver providers, cross-realm Inspector, CPython PTC backend, and browser-worker preview libraries. Released products outside this group must not depend on experimental packages.
 
 ## Table of Contents
 
@@ -42,7 +42,7 @@ The experimental group contains prototype capabilities whose contracts can chang
 <a id="related-documentation"></a>
 ## Related documentation
 
-- [Experimental package decision](../../.agents/notes/implemented/architecture/2026-08-18-experimental-agent-teams-packages.md) — private defaults, public Agent Teams exceptions, and dependency isolation.
+- [Experimental publication decision](../../.agents/notes/implemented/process/2026-09-12-experimental-publication-denylist.md) — public defaults and private exceptions.
 - [Computer use](../../docs/subsystems/computer-use.md) — desktop provider choices.
 - [Agent Teams subsystem](../../docs/subsystems/agent-team.md) — durable Team types and the `ctx.agentTeams` service API.
 - [Experimental subtree rules](AGENTS.md) — what experimental status does and does not relax.

+ 3 - 3
packages/experimental/README.zh.md

@@ -1,5 +1,5 @@
 ---
-description: "实验组地图:默认私有的预稳定原型,以及显式公开发布的 Agent Teams 和计算机操作包。"
+description: "实验组地图:可公开安装的预稳定原型。"
 kind: "package-group"
 ---
 
@@ -9,7 +9,7 @@ kind: "package-group"
 
 ## 概述
 
-实验组包含约定可能变更且不提供支持承诺的原型能力。包默认私有;允许列表中的 Agent Teams 和 Cua Driver 包是显式公开发布的例外,并保留现有 `@deepseek-ai/dsh-experimental-*` 名称。本组还包含私有的跨 realm Inspector、CPython 子进程后端与浏览器 worker 预览包。组外已发布产品不得依赖实验性包。
+实验组包含约定可能变更且不提供支持承诺的原型能力。所有当前包都以 `@deepseek-ai/dsh-experimental-*` 名称发布,包括显式启用的 Agent Teams 组合、Auto review、Cua Driver 提供方、跨 realm Inspector、CPython PTC 后端与浏览器 worker 预览库。组外已发布产品不得依赖实验性包。
 
 ## 目录
 
@@ -42,7 +42,7 @@ kind: "package-group"
 <a id="related-documentation"></a>
 ## 相关文档
 
-- [实验包决策](../../.agents/notes/implemented/architecture/2026-08-18-experimental-agent-teams-packages.zh.md)——默认私有、Agent Teams 公开例外与依赖隔离。
+- [实验包发布决策](../../.agents/notes/implemented/process/2026-09-12-experimental-publication-denylist.zh.md)——默认公开与私有例外。
 - [计算机操作](../../docs/subsystems/computer-use.zh.md)——桌面提供方选择。
 - [Agent Teams 子系统](../../docs/subsystems/agent-team.zh.md)——持久 Team 类型与 `ctx.agentTeams` 服务 API。
 - [实验子树规则](AGENTS.md)——实验状态放宽了什么、不放宽什么。

+ 2 - 2
packages/experimental/auto-review/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/experimental/auto-review/README.md
-README.md: f3d0cdac9af4325f40289176683a3a2dd6073e0e
-README.zh.md: c6dd276d05b22878e8a0da76492b57a3c5d97cd2
+README.md: 857ea568577e5071d2ea282849cf4b817a26a1b4
+README.zh.md: e05e81b0765743e444912c98b0170fc1bb746503

+ 4 - 4
packages/experimental/auto-review/README.md

@@ -1,5 +1,5 @@
 ---
-description: "Add experimental per-call Auto review to a source-checkout Web profile, using the current agent's model before tools execute with Full access."
+description: "Add experimental per-call Auto review to a Web profile, using the current agent's model before tools execute with Full access."
 kind: "package-bundle"
 ---
 
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
 
 ## Summary
 
-Add Auto review to the current-session permission pickers in a source-checkout Web profile. Before each native or PTC inner tool call, the current agent's provider and model assess the pending action; an allowed call executes with Full access. Default Web keeps its three permission modes until this layer is explicitly installed. Auto review is experimental: it can allow unsafe actions, deny useful work, and spend additional tokens. Official releases exclude this private package.
+Add Auto review to the current-session permission pickers in a Web profile. Before each native or PTC inner tool call, the current agent's provider and model assess the pending action; an allowed call executes with Full access. Default Web keeps its three permission modes until this layer is explicitly installed. Auto review is experimental: it can allow unsafe actions, deny useful work, and spend additional tokens.
 
 ## Table of Contents
 
@@ -70,7 +70,7 @@ No runtime invariant companion is published: this single effect owns selection a
 <a id="further-exploration"></a>
 ## Further Exploration
 
-- [Experimental packages](../README.md) — release exclusion and dependency isolation.
+- [Experimental packages](../README.md) — publication policy and dependency isolation.
 - [Web bundle](../../bundle/web-app/README.md) — the stable profile this patch extends.
 - [Auto review decision](../../../.agents/notes/implemented/feature/2026-08-28-auto-review.md) — fixed risk policy, authority, and lifecycle.
 - [Tools](../../core/tools/README.md) — execution, cancellation, and PTC result propagation.
@@ -112,7 +112,7 @@ The denial appends an ordinary tool result; it does not rewrite earlier context
 
 <a id="known-limitations-and-deferred-work"></a>
 
-- Auto is available only through an explicitly installed source Web layer; it is absent from official payloads, Headless, General settings, and new-session defaults.
+- Auto requires an explicitly installed Web layer; it is absent from default Web, Headless, General settings, and new-session defaults.
 - Auto provides no file sandbox. The outer `run_code` transport and direct Node effects inside a PTC program do not pass through inner-tool review.
 - Model classification can be wrong. There are no deterministic tool exemptions, persistent grants, manual fallback, configurable policy, or retry layer.
 - In-process Auto children review their own calls. Out-of-process children retain their native permission systems after the parent delegation call is allowed.

+ 4 - 4
packages/experimental/auto-review/README.zh.md

@@ -1,5 +1,5 @@
 ---
-description: "为源码 checkout 的 Web profile 添加实验性逐调用 Auto review,在工具以 Full access 执行前使用当前 agent 的模型审查。"
+description: "为 Web profile 添加实验性逐调用 Auto review,在工具以 Full access 执行前使用当前 agent 的模型审查。"
 kind: "package-bundle"
 ---
 
@@ -9,7 +9,7 @@ kind: "package-bundle"
 
 ## 概述
 
-为源码 checkout 的 Web profile 当前会话权限选择器添加 Auto review。每次原生或 PTC inner 工具调用前,当前 agent 的 provider 与模型会评估待执行动作;获准调用以 Full access 执行。在显式安装此层之前,默认 Web 保持三种权限模式。Auto review 是实验功能:它可能误放行不安全动作、误拒绝有用操作,并消耗额外 token。正式发布不包含此私有包。
+为 Web profile 当前会话权限选择器添加 Auto review。每次原生或 PTC inner 工具调用前,当前 agent 的 provider 与模型会评估待执行动作;获准调用以 Full access 执行。在显式安装此层之前,默认 Web 保持三种权限模式。Auto review 是实验功能:它可能误放行不安全动作、误拒绝有用操作,并消耗额外 token。
 
 ## 目录
 
@@ -70,7 +70,7 @@ Reviewer 从当前 Session surface 与待执行调用重建五个分区:固定
 <a id="further-exploration"></a>
 ## 进一步探索
 
-- [实验包](../README.zh.md)——发布排除与依赖隔离。
+- [实验包](../README.zh.md)——发布策略与依赖隔离。
 - [Web bundle](../../bundle/web-app/README.zh.md)——此 patch 扩展的稳定 profile。
 - [Auto review 决策](../../../.agents/notes/implemented/feature/2026-08-28-auto-review.zh.md)——固定风险策略、权威与生命周期。
 - [Tools](../../core/tools/README.zh.md)——执行、取消与 PTC 结果传播。
@@ -112,7 +112,7 @@ Reviewer 使用最新 `request/header.config` 的 provider 与模型,并沿用
 
 <a id="known-limitations-and-deferred-work"></a>
 
-- Auto 仅通过显式安装的源码 Web 层提供;正式 payload、Headless、通用设置与新会话默认值都不包含它。
+- Auto 需要显式安装 Web 层;默认 Web、Headless、通用设置与新会话默认值都不包含它。
 - Auto 不提供文件沙箱。外层 `run_code` transport 及PTC 程序内直接 Node 效果不经过 inner-tool review。
 - 模型分类可能出错。不提供确定性工具豁免、持久 grant、人工 fallback、可配置策略或重试层。
 - 进程内 Auto child 独立审查自身调用。进程外 child 在父委派调用获准后保留原生权限系统。

+ 3 - 1
packages/experimental/auto-review/package.json

@@ -67,7 +67,9 @@
     "@deepseek-ai/dsh-user-approval": "workspace:^",
     "@deepseek-ai/cordis": "workspace:^"
   },
-  "private": true,
+  "publishConfig": {
+    "access": "public"
+  },
   "dsh": {
     "bundle": {
       "patch": "./cordis.patch.yml"

+ 2 - 2
packages/experimental/inspector/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/experimental/inspector/README.md
-README.md: 956a316430b69cc04535296136542ea5ba4580de
-README.zh.md: bed5143cecb16d93d09e81a341f815f8cf69f305
+README.md: 2b2986d49bbc970c70cc7bc85de38ba6fb6d9593
+README.zh.md: a24b17d3a8a88736fe5efe83b4185de9379b5903

+ 1 - 1
packages/experimental/inspector/README.md

@@ -11,7 +11,7 @@ English | [中文](README.zh.md)
 
 Use this experimental inspector to inspect one running dsh Host and its browser Clients in Chrome DevTools. It exposes Host and Client Console contexts, Host Sources and debugging, captured Host fetches, and a shared Cordis tree while keeping all CDP state in a Worker.
 
-The package is private and excluded from releases. The Worker never accesses live Cordis objects: the shared Host/Client collector projects them into validated snapshots before transport. Cordis also owns plugin composition, `ctx.inspector` registration, bootstrap injection, and disposal.
+The package publishes under its experimental name and requires an explicit composition. The Worker never accesses live Cordis objects: the shared Host/Client collector projects them into validated snapshots before transport. Cordis also owns plugin composition, `ctx.inspector` registration, bootstrap injection, and disposal.
 
 ## Table of Contents
 

+ 1 - 1
packages/experimental/inspector/README.zh.md

@@ -11,7 +11,7 @@ kind: "package-reference"
 
 使用这个实验性 Inspector,可以在 Chrome DevTools 中检查一个运行中的 dsh Host 及其浏览器 Client。它提供 Host 与 Client Console context、Host Sources 与调试、Host fetch 采集和共享 Cordis 树,并让 Worker 独占全部 CDP 状态。
 
-本包为私有包,不进入正式发布。Worker 不访问实时 Cordis 对象;共享 Host/Client collector 会在传输前把它们投影成已验证快照。Cordis 还负责插件组合、注册 `ctx.inspector`、注入 bootstrap 和 dispose(资源释放)。
+本包以实验性名称发布,需要显式组合。Worker 不访问实时 Cordis 对象;共享 Host/Client collector 会在传输前把它们投影成已验证快照。Cordis 还负责插件组合、注册 `ctx.inspector`、注入 bootstrap 和 dispose(资源释放)。
 
 ## 目录
 

+ 4 - 1
packages/experimental/inspector/package.json

@@ -2,7 +2,9 @@
   "name": "@deepseek-ai/dsh-experimental-inspector",
   "description": "Experimental cross-realm CDP hub for Host debugging and Client Runtime inspection",
   "version": "0.1.5-rc.2",
-  "private": true,
+  "publishConfig": {
+    "access": "public"
+  },
   "repository": {
     "type": "git",
     "url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
@@ -33,6 +35,7 @@
   "files": [
     "lib/index.js",
     "lib/client.js",
+    "lib/worker.js",
     "lib/types/**/*.d.ts"
   ],
   "license": "MIT",

+ 69 - 34
packages/experimental/inspector/tests/built-lib.e2e.ts

@@ -1,56 +1,91 @@
 import { existsSync } from 'node:fs'
+import { mkdtemp, readdir, rm, symlink, unlink } from 'node:fs/promises'
+import { tmpdir } from 'node:os'
 import { join } from 'node:path'
 import { fileURLToPath } from 'node:url'
 import { execa } from 'execa'
 import { describe, expect, it } from 'vitest'
+import { pnpmInvocation } from '../../../../scripts/pnpm-invocation.ts'
 
 const packageDirectory = fileURLToPath(new URL('..', import.meta.url))
 const built = [
   'lib/index.js',
-  'lib/worker.js',
   'node_modules/@deepseek-ai/schemastery/lib/index.mjs',
 ].every(file => existsSync(join(packageDirectory, file)))
 
 describe.skipIf(!built)('experimental Inspector built artifact', () => {
-  it('starts its sibling Worker and evaluates the Host through plain Node', async () => {
+  it('packs its sibling Worker and evaluates the Host from the tarball through plain Node', { retry: 0 }, async (test) => {
+    const root = await mkdtemp(join(tmpdir(), 'dsh-inspector-packed-'))
+    const consumer = join(root, 'package')
+    const dependencies = join(consumer, 'node_modules')
+    let linked = false
+    let pending: Promise<string> = Promise.resolve('')
+    test.onTestFinished(async () => {
+      try {
+        await pending
+      } finally {
+        if (linked) await unlink(dependencies)
+        await rm(root, { recursive: true, force: true })
+      }
+    })
+    const run = (command: string, args: string[], cwd: string): Promise<string> => {
+      pending = execa(command, args, {
+        cwd,
+        stdin: 'ignore',
+        timeout: test.task.timeout,
+        cancelSignal: test.signal,
+        killSignal: 'SIGKILL',
+        reject: false,
+      }).then((result) => {
+        expect(result.timedOut, `stderr:\n${result.stderr}`).toBe(false)
+        expect(result.isCanceled, `stderr:\n${result.stderr}`).toBe(false)
+        expect(result.signal, `stderr:\n${result.stderr}`).toBeUndefined()
+        expect(result.exitCode, `stderr:\n${result.stderr}`).toBe(0)
+        return result.stdout
+      })
+      return pending
+    }
+    const invocation = pnpmInvocation(['pack', '--pack-destination', root])
+    await run(invocation.command, invocation.args, packageDirectory)
+    const archives = (await readdir(root)).filter(name => name.endsWith('.tgz'))
+    expect(archives).toHaveLength(1)
+    await run('tar', ['-xzf', join(root, archives[0]!), '-C', root], root)
+    expect(existsSync(join(consumer, 'lib/worker.js'))).toBe(true)
+    await symlink(join(packageDirectory, 'node_modules'), dependencies, process.platform === 'win32' ? 'junction' : 'dir')
+    linked = true
     const script = `
       const { startInspector } = await import('@deepseek-ai/dsh-experimental-inspector')
       const { default: WebSocket } = await import('ws')
       globalThis.__builtInspectorProbe = 42
-      const inspector = await startInspector({ port: 0, captureFetch: false })
+      const inspector = await startInspector({ port: 0, captureFetch: false, startupTimeoutMs: ${String(test.task.timeout)} })
       const socket = new WebSocket(inspector.endpoint.webSocketDebuggerUrl)
-      await new Promise((resolve, reject) => {
-        socket.once('open', resolve)
-        socket.once('error', reject)
-      })
-      const response = new Promise((resolve, reject) => {
-        const timer = setTimeout(() => reject(new Error('CDP response timeout')), 5000)
-        socket.on('message', data => {
-          const message = JSON.parse(Buffer.from(data).toString('utf8'))
-          if (message.id !== 1) return
-          clearTimeout(timer)
-          resolve(message)
+      try {
+        await new Promise((resolve, reject) => {
+          socket.once('open', resolve)
+          socket.once('error', reject)
         })
-      })
-      socket.send(JSON.stringify({
-        id: 1,
-        method: 'Runtime.evaluate',
-        params: { expression: 'globalThis.__builtInspectorProbe', returnByValue: true },
-      }))
-      const message = await response
-      socket.close()
-      await inspector.close()
-      console.log(JSON.stringify(message.result.result))
+        const response = new Promise((resolve, reject) => {
+          const timer = setTimeout(() => reject(new Error('CDP response timeout')), 5000)
+          socket.on('message', data => {
+            const message = JSON.parse(Buffer.from(data).toString('utf8'))
+            if (message.id !== 1) return
+            clearTimeout(timer)
+            resolve(message)
+          })
+        })
+        socket.send(JSON.stringify({
+          id: 1,
+          method: 'Runtime.evaluate',
+          params: { expression: 'globalThis.__builtInspectorProbe', returnByValue: true },
+        }))
+        const message = await response
+        console.log(JSON.stringify(message.result.result))
+      } finally {
+        socket.terminate()
+        await inspector.close()
+      }
     `
-    const result = await execa(process.execPath, ['--input-type=module', '-e', script], {
-      cwd: packageDirectory,
-      stdin: 'ignore',
-      timeout: 20_000,
-      killSignal: 'SIGKILL',
-      reject: false,
-    })
-
-    expect(result.exitCode, `stderr:\n${result.stderr}`).toBe(0)
-    expect(JSON.parse(result.stdout.trim()) as unknown).toEqual({ type: 'number', value: 42, description: '42' })
+    const stdout = await run(process.execPath, ['--input-type=module', '-e', script], consumer)
+    expect(JSON.parse(stdout.trim()) as unknown).toEqual({ type: 'number', value: 42, description: '42' })
   })
 })

+ 2 - 2
packages/experimental/ptc-runtime-python/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/experimental/ptc-runtime-python/README.md
-README.md: 53fded2e42be72c87a9675beb1e48d8b8ed59c6d
-README.zh.md: 8c9a18d5cc910960700deeb65bf1c77e60439115
+README.md: 879fef9cbc9f9bded5aabb2860a13b0d9586367f
+README.zh.md: 4f5a9e602e7264b7f906ce55d10ce763f2cbf8cc

+ 3 - 3
packages/experimental/ptc-runtime-python/README.md

@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
 
 ## Summary
 
-This private experimental package lets source-checkout compositions run model-generated Python in a fresh CPython 3.10+ subprocess for each request. Programs can use top-level `await` and `return`, call configured bindings, and write normal stdout/stderr while receiving explicit completion or failure results. Resource budgets and process-group teardown contain runaway work, but the subprocess is not a security boundary: direct Python operations have no filesystem sandbox, no state persists across runs, and no shipped profile enables this runtime.
+This experimental package lets explicit compositions run model-generated Python in a fresh CPython 3.10+ subprocess for each request. Programs can use top-level `await` and `return`, call configured bindings, and write normal stdout/stderr while receiving explicit completion or failure results. Resource budgets and process-group teardown contain runaway work, but the subprocess is not a security boundary: direct Python operations have no filesystem sandbox, no state persists across runs, and no shipped profile enables this runtime.
 
 ## Table of Contents
 
@@ -25,7 +25,7 @@ This private experimental package lets source-checkout compositions run model-ge
 <a id="use-this-package"></a>
 ## Use this package
 
-Choose this private experimental package only in an explicit source-checkout composition. Register `PythonPtcRuntime` beside `dsh-tools`; `run(resolve(request))` executes each program in a fresh CPython 3.10+ subprocess, resolving with `result.value` on success and `result.error` on failure (the orthogonal `PtcRunFailure.kind` taxonomy classifies parse failures, thrown exceptions, invalid completions, output overflows, budget expiry, aborts, and substrate death). It rejects only for seam misuse — a malformed binding namespace, or a call after disposal. Configuration is rejected at load: a non-Unix platform; an explicit `pythonBin` that is not an executable regular file or a bare name that does not resolve on `PATH`; a non-CPython, pre-3.10, or probe-failing interpreter; a non-positive or non-integer budget; a `maxLogBytes` below the truncation-marker floor (64); a timer value `setTimeout` would clamp; a budget larger than the effective fd-3 frame cap (lowered when the host heap cannot safely parse a near-cap frame); or an `addressSpaceMb`/output-budget pair whose worst-case peak would breach `RLIMIT_AS`.
+Choose this published experimental package only in an explicit composition. Register `PythonPtcRuntime` beside `dsh-tools`; `run(resolve(request))` executes each program in a fresh CPython 3.10+ subprocess, resolving with `result.value` on success and `result.error` on failure (the orthogonal `PtcRunFailure.kind` taxonomy classifies parse failures, thrown exceptions, invalid completions, output overflows, budget expiry, aborts, and substrate death). It rejects only for seam misuse — a malformed binding namespace, or a call after disposal. Configuration is rejected at load: a non-Unix platform; an explicit `pythonBin` that is not an executable regular file or a bare name that does not resolve on `PATH`; a non-CPython, pre-3.10, or probe-failing interpreter; a non-positive or non-integer budget; a `maxLogBytes` below the truncation-marker floor (64); a timer value `setTimeout` would clamp; a budget larger than the effective fd-3 frame cap (lowered when the host heap cannot safely parse a near-cap frame); or an `addressSpaceMb`/output-budget pair whose worst-case peak would breach `RLIMIT_AS`.
 
 `resolve(request)` accepts an absolute `cwd` and uses the provider's configured `maxWallMs` deadline (600,000 ms by default). Explicit `timeoutMs` overrides and sandbox policies are unsupported and reject before execution. This provider does not advertise `sandboxMode` or return confinement facts.
 
@@ -99,7 +99,7 @@ Read these when the runtime contract is not enough. They move from the seam defi
 <a id="model-experience"></a>
 ## Model Experience
 
-Indirectly, through PTC mode in `dsh-tools` when an explicit source-checkout composition mounts this provider; it renders the program's completion value or failure into a retained `run_code` result, and no shipped profile mounts this private package.
+Indirectly, through PTC mode in `dsh-tools` when an explicit composition mounts this provider; it renders the program's completion value or failure into a retained `run_code` result, and no shipped profile mounts this experimental package.
 
 #### KV Cache effect
 

+ 3 - 3
packages/experimental/ptc-runtime-python/README.zh.md

@@ -9,7 +9,7 @@ kind: "package-reference"
 
 ## 概述
 
-这个私有实验包可让源码检出组合在每次请求时都用全新的 CPython 3.10+ 子进程运行模型生成的 Python。程序可以使用顶层 `await` 和 `return`、调用已配置的 binding、正常写入 stdout/stderr,并获得明确的完成或失败结果。资源预算和进程组拆卸会约束失控的工作,但子进程不是安全边界:直接 Python 操作没有文件系统沙箱,运行之间不保留状态,且没有已发布 profile 启用此 runtime。
+这个实验包可让显式组合在每次请求时都用全新的 CPython 3.10+ 子进程运行模型生成的 Python。程序可以使用顶层 `await` 和 `return`、调用已配置的 binding、正常写入 stdout/stderr,并获得明确的完成或失败结果。资源预算和进程组拆卸会约束失控的工作,但子进程不是安全边界:直接 Python 操作没有文件系统沙箱,运行之间不保留状态,且没有已发布 profile 启用此 runtime。
 
 ## 目录
 
@@ -25,7 +25,7 @@ kind: "package-reference"
 <a id="use-this-package"></a>
 ## 使用本包
 
-仅在显式源码检出组合中选择这个私有实验包。将 `PythonPtcRuntime` 与 `dsh-tools` 一起注册后,`run(resolve(request))` 会在全新的 CPython 3.10+ 子进程中执行每个程序;成功时以 `result.value` resolve,失败时以 `result.error` resolve(正交的 `PtcRunFailure.kind` 分类涵盖解析失败、抛出异常、无效完成值、输出溢出、预算到期、中止与执行基底终止)。仅有 seam 误用会 reject——binding 命名空间不合法,或在 dispose 后调用。配置在加载期拒绝:非 Unix 平台;不是可执行普通文件的显式 `pythonBin`,或无法在 `PATH` 上解析的裸名;非 CPython、低于 3.10 或探测失败的解释器;非正或非整数预算;低于截断标记下限(64)的 `maxLogBytes`;会被 `setTimeout` 截断的定时器值;超过有效 fd-3 帧上限的预算(宿主堆无法安全解析接近上限的帧时,该上限会降低);或最坏峰值会突破 `RLIMIT_AS` 的 `addressSpaceMb`/输出预算组合。
+仅在显式组合中选择这个已发布的实验包。将 `PythonPtcRuntime` 与 `dsh-tools` 一起注册后,`run(resolve(request))` 会在全新的 CPython 3.10+ 子进程中执行每个程序;成功时以 `result.value` resolve,失败时以 `result.error` resolve(正交的 `PtcRunFailure.kind` 分类涵盖解析失败、抛出异常、无效完成值、输出溢出、预算到期、中止与执行基底终止)。仅有 seam 误用会 reject——binding 命名空间不合法,或在 dispose 后调用。配置在加载期拒绝:非 Unix 平台;不是可执行普通文件的显式 `pythonBin`,或无法在 `PATH` 上解析的裸名;非 CPython、低于 3.10 或探测失败的解释器;非正或非整数预算;低于截断标记下限(64)的 `maxLogBytes`;会被 `setTimeout` 截断的定时器值;超过有效 fd-3 帧上限的预算(宿主堆无法安全解析接近上限的帧时,该上限会降低);或最坏峰值会突破 `RLIMIT_AS` 的 `addressSpaceMb`/输出预算组合。
 
 `resolve(request)` 接受绝对 `cwd`,并使用提供方配置的 `maxWallMs` 截止时间(默认 600,000 ms)。显式 `timeoutMs` 覆盖与沙箱策略不受支持,会在执行前拒绝。本提供方不声明 `sandboxMode`,也不返回约束事实。
 
@@ -99,7 +99,7 @@ kind: "package-reference"
 <a id="model-experience"></a>
 ## 模型体验
 
-间接地,通过 `dsh-tools` 中的 PTC mode;当显式的源码 checkout 组合挂载本提供方时,它会把程序的完成值或失败渲染成保留的 `run_code` 结果,且已发布 profile 均不挂载这个私有包。
+间接地,通过 `dsh-tools` 中的 PTC mode;当显式组合挂载本提供方时,它会把程序的完成值或失败渲染成保留的 `run_code` 结果,且已发布 profile 均不挂载这个实验包。
 
 #### KV Cache 效应
 

+ 3 - 1
packages/experimental/ptc-runtime-python/package.json

@@ -38,5 +38,7 @@
   "dependencies": {
     "@deepseek-ai/schemastery": "workspace:^"
   },
-  "private": true
+  "publishConfig": {
+    "access": "public"
+  }
 }

+ 2 - 2
packages/experimental/webworker-packer/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/experimental/webworker-packer/README.md
-README.md: f3863d89712ccb3cb522e4b4908b43602af5c997
-README.zh.md: f3ab2c5e3dee64c76d58436f939c483664b61c46
+README.md: 90723b7718393ed32005bd6146764044a8ec80b1
+README.zh.md: 9d7df43bf2b49d7b4a0438fcdb96fdbe44cc326a

+ 3 - 0
packages/experimental/webworker-packer/README.md

@@ -23,6 +23,8 @@ The VFS image packer: turns one composed profile into the gzip-compressed base t
 <a id="use-this-package"></a>
 ## Use this package
 
+The public npm package exposes `packVfsImage` and `packVfsOverlay` through its library entry. Callers supply the composition, package directories, and resolution root; no profile layer is installed. The runtime's library entry supplies the module-proxy and replacement tables used during packing.
+
 The packer owns the internal `dsh.configTrees` declaration, its validation, and source directory resolution in [`src/repository.ts`](src/repository.ts). This field is not part of the public plugin manifest API.
 
 The pack is a three-layer standard stack:
@@ -54,6 +56,7 @@ None; this package neither assembles nor sends a provider request.
 - **Reachability infers only exact request forms** — computed `import` and `require` arguments, stored `createRequire` results, CommonJS-obtained `createRequire`, and bases other than `import.meta.url` resolve only at runtime and fail loud if the target was otherwise pruned; a target reachable only through those forms needs an explicit image entry seed.
 - **Vendored package sources (`src/*.ts`) are excluded** — nothing resolves them at runtime; a future in-worker source-inspection feature would need a dedicated include rule.
 - **The packer assumes built `lib/` artifacts are current**: it never compiles, so a stale workspace build packs stale bytes. Run the repository build first.
+- **The CLI requires its original repository location**: `dsh-pack-vfs-image` finds the checkout relative to its own installed file and reads the source CLI, configuration trees, and preview fixtures there. An npm installation supports the parameterized library API; the CLI and repository helpers require a complete, built DeepSeek Harness checkout.
 
 
 <a id="dev-note"></a>

+ 3 - 0
packages/experimental/webworker-packer/README.zh.md

@@ -23,6 +23,8 @@ VFS 镜像打包器:把一份合成 profile 变成浏览器 worker 挂载为
 <a id="use-this-package"></a>
 ## 使用本包
 
+公开 npm 包通过库入口导出 `packVfsImage` 和 `packVfsOverlay`。调用方提供合成配置、包目录和解析根目录;本包不安装 profile 层。运行时的库入口提供打包所用的模块代理表与替换表。
+
 打包器在 [`src/repository.ts`](src/repository.ts) 中拥有内部 `dsh.configTrees` 声明、校验和源目录解析。该字段不属于公共插件 manifest API。
 
 打包是三层标准栈:
@@ -54,6 +56,7 @@ VFS 镜像打包器:把一份合成 profile 变成浏览器 worker 挂载为
 - **可达性只推断精确请求形式**——计算得到的 `import` 与 `require` 参数、保存下来的 `createRequire` 结果、经 CommonJS 获取的 `createRequire`,以及基准不是 `import.meta.url` 的调用只在运行时解析;若目标已被裁掉就会显式失败。只能通过这些形式触达的目标需要显式镜像入口种子。
 - **vendored 包源码(`src/*.ts`)被排除**——运行时无人解析它们;未来若有 worker 内源码巡检功能需要专门的 include 规则。
 - **打包器假定构建产物 `lib/` 是新鲜的**:它从不编译,工作区构建陈旧就会打包陈旧字节。先跑仓库构建。
+- **CLI 必须位于原有仓库位置**:`dsh-pack-vfs-image` 相对于自身安装文件定位检出目录,并从中读取源码 CLI、配置树和预览 fixture。npm 安装支持参数化的库 API;CLI 和仓库辅助函数需要完整且已构建的 DeepSeek Harness 检出目录。
 
 
 <a id="dev-note"></a>

+ 3 - 1
packages/experimental/webworker-packer/package.json

@@ -2,7 +2,9 @@
   "name": "@deepseek-ai/dsh-experimental-webworker-packer",
   "description": "Build-time packer for the browser runtime's base VFS image and ordered data-overlay archives",
   "version": "0.1.5-rc.2",
-  "private": true,
+  "publishConfig": {
+    "access": "public"
+  },
   "repository": {
     "type": "git",
     "url": "git+https://github.com/deepseek-ai/deepseek-harness.git",

+ 1 - 2
packages/experimental/webworker-packer/src/pack.ts

@@ -20,12 +20,11 @@ import {
   lowerModuleSource, MemoryVfs, packTar, WorkerModuleLoader,
   DEFAULT_ROOT, IMAGE_CONFIG_PATH, IMAGE_EMPTY_DIRECTORIES, IMAGE_MANIFEST_PATH,
   IMAGE_OVERLAY_DIRECTORIES,
+  MODULE_PROXIES, MODULE_PROXY_PREFIXES, REPLACED_EXTERNAL_PACKAGES,
 } from '@deepseek-ai/dsh-experimental-webworker-runtime'
 import picomatch from 'picomatch'
 import yaml from 'js-yaml'
 import { entryListSchema } from '@deepseek-ai/cordis-plugin-include'
-import { REPLACED_EXTERNAL_PACKAGES } from '@deepseek-ai/dsh-experimental-webworker-runtime/src/node/external_packages/replaced-externals.ts'
-import { MODULE_PROXIES, MODULE_PROXY_PREFIXES } from '@deepseek-ai/dsh-experimental-webworker-runtime/src/module-proxies.ts'
 import { WRAPPER_CONTRACT, type ImageFiles, type TransformOutcome } from './transform-image.ts'
 import { EXCLUDE, EXCLUDE_WORKSPACE, IMAGE_ENTRY_SEEDS, PAGE_ASSETS } from './rules.ts'
 

+ 100 - 0
packages/experimental/webworker-packer/tests/built-lib.e2e.ts

@@ -0,0 +1,100 @@
+/** Installed packer/runtime interoperability without either package's source tree. */
+import { existsSync } from 'node:fs'
+import { mkdir, mkdtemp, readFile, readdir, realpath, rm, symlink, unlink, writeFile } from 'node:fs/promises'
+import { tmpdir } from 'node:os'
+import { dirname, join } from 'node:path'
+import { fileURLToPath } from 'node:url'
+import { execa } from 'execa'
+import { expect, it } from 'vitest'
+import { pnpmInvocation } from '../../../../scripts/pnpm-invocation.ts'
+
+const experimentalDirectory = fileURLToPath(new URL('../..', import.meta.url))
+const packages = ['webworker-runtime', 'webworker-packer']
+const packageNames = new Set(packages.map(name => `@deepseek-ai/dsh-experimental-${name}`))
+
+it('loads both tarballs through plain Node and mounts their base image and overlay', { retry: 0 }, async (test) => {
+  const root = await mkdtemp(join(tmpdir(), 'dsh-webworker-packed-'))
+  const links: string[] = []
+  let pending: Promise<string> = Promise.resolve('')
+  test.onTestFinished(async () => {
+    try {
+      await pending
+    } finally {
+      for (const path of links.reverse()) await unlink(path)
+      await rm(root, { recursive: true, force: true })
+    }
+  })
+  const run = (command: string, args: string[], cwd: string): Promise<string> => {
+    pending = execa(command, args, {
+      cwd,
+      env: { NODE_OPTIONS: undefined, NODE_PATH: undefined, TSX_TSCONFIG_PATH: undefined },
+      stdin: 'ignore',
+      timeout: test.task.timeout,
+      cancelSignal: test.signal,
+      killSignal: 'SIGKILL',
+      reject: false,
+    }).then((result) => {
+      expect(result.timedOut, `stderr:\n${result.stderr}`).toBe(false)
+      expect(result.isCanceled, `stderr:\n${result.stderr}`).toBe(false)
+      expect(result.signal, `stderr:\n${result.stderr}`).toBeUndefined()
+      expect(result.exitCode, `stderr:\n${result.stderr}`).toBe(0)
+      return result.stdout
+    })
+    return pending
+  }
+  await writeFile(join(root, 'package.json'), JSON.stringify({ private: true, type: 'module' }))
+  for (const name of packages) {
+    const source = join(experimentalDirectory, name)
+    const directory = join(root, 'node_modules/@deepseek-ai', `dsh-experimental-${name}`)
+    const packRoot = join(root, name)
+    await mkdir(packRoot, { recursive: true })
+    await mkdir(directory, { recursive: true })
+    const invocation = pnpmInvocation(['pack', '--pack-destination', packRoot])
+    await run(invocation.command, invocation.args, source)
+    const archives = (await readdir(packRoot)).filter(file => file.endsWith('.tgz'))
+    expect(archives).toHaveLength(1)
+    await run('tar', ['-xzf', join(packRoot, archives[0]!), '-C', directory, '--strip-components=1'], root)
+    expect(existsSync(join(directory, 'src'))).toBe(false)
+    const manifest = JSON.parse(await readFile(join(directory, 'package.json'), 'utf8')) as {
+      dependencies?: Record<string, string>
+      peerDependencies?: Record<string, string>
+    }
+    // Only the two subjects resolve from tarballs; their declared external dependencies use the built checkout.
+    for (const dependency of Object.keys({ ...manifest.dependencies, ...manifest.peerDependencies })) {
+      if (packageNames.has(dependency)) continue
+      const target = join(root, 'node_modules', dependency)
+      if (existsSync(target)) continue
+      await mkdir(dirname(target), { recursive: true })
+      await symlink(await realpath(join(source, 'node_modules', dependency)), target, process.platform === 'win32' ? 'junction' : 'dir')
+      links.push(target)
+    }
+  }
+  const script = `
+    import assert from 'node:assert/strict'
+    import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
+    import { fileURLToPath } from 'node:url'
+    import * as packer from '@deepseek-ai/dsh-experimental-webworker-packer'
+    import * as runtime from '@deepseek-ai/dsh-experimental-webworker-runtime'
+    import * as client from '@deepseek-ai/dsh-experimental-webworker-runtime/client'
+    for (const name of ['webworker-packer', 'webworker-runtime']) {
+      assert.equal(import.meta.resolve('@deepseek-ai/dsh-experimental-' + name),
+        new URL('./node_modules/@deepseek-ai/dsh-experimental-' + name + '/lib/index.js', import.meta.url).href)
+    }
+    assert.equal(typeof client.connectWorkerHost, 'function')
+    const worker = fileURLToPath(import.meta.resolve('@deepseek-ai/dsh-experimental-webworker-runtime/worker'))
+    assert.ok(existsSync(worker))
+    assert.equal(readFileSync(worker, 'utf8').match(/^import[ \\t]/m), null)
+    const base = packer.packVfsImage({ config: '[]\\n', profile: 'packed-consumer', workspaces: new Map(), resolveFrom: process.cwd(), entries: [] })
+    assert.deepEqual(base.missing, [])
+    const vfs = runtime.loadVfsImage(await runtime.inflateImage(base.image, 'packed base'))
+    assert.ok(vfs.existsSync('/dsh/' + packer.MANIFEST_PATH))
+    mkdirSync('overlay')
+    writeFileSync('overlay/hello.txt', 'packed worker pair\\n')
+    const overlay = packer.packVfsOverlay([{ mount: 'workspace', directory: fileURLToPath(new URL('./overlay', import.meta.url)) }])
+    runtime.loadVfsOverlay(await runtime.inflateImage(overlay.image, 'packed overlay'), '/dsh', vfs)
+    assert.equal(vfs.readFileSync('/dsh/workspace/hello.txt', 'utf8'), 'packed worker pair\\n')
+    console.log('packed image and overlay mounted')
+  `
+  await writeFile(join(root, 'consumer.mjs'), script)
+  expect((await run(process.execPath, ['consumer.mjs'], root)).trim()).toBe('packed image and overlay mounted')
+})

+ 2 - 2
packages/experimental/webworker-runtime/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/experimental/webworker-runtime/README.md
-README.md: 15b30b87d3e82ad78ad91dc24133113bcc6c4773
-README.zh.md: 6b89b729973b92278d1a488d8d0d4e4578997097
+README.md: ab54ec689664adc1c0e01cc3387777c77f36bc66
+README.zh.md: d6593eeff22e642e2423fa9348f64d751d5de62f

+ 2 - 0
packages/experimental/webworker-runtime/README.md

@@ -23,6 +23,8 @@ The browser worker host: the whole harness plugin tree runs inside one dedicated
 <a id="use-this-package"></a>
 ## Use this package
 
+The public npm package exposes the runtime library at its root, the self-contained browser Worker at `./worker`, and the page connector at `./client`. Deployments supply and serve the VFS image separately through the [packer](../webworker-packer/README.md); installing this library does not add a profile layer. The root also exports the module-proxy and replacement tables used by the packer.
+
 Three build artifacts and one source-owned process layer:
 
 - **`lib/index.js` (assembly library)** — `createWorkerHost`/`startWorkerHost` mount the base image and any ordered data overlays (`storage/`), install the module loader (`module-system/`) and the `process` shim, boot the tree through the image's own `dsh-app-boot`, and hand the tunnel its serving seams. Overlays may replace files only under `home/` and `workspace/`; they cannot replace the base manifest, configuration, or modules. The image layout contract (`image-layout.ts`: virtual root, config/manifest paths, empty directories, the `lowered` wrapper-contract gate) is shared with the packer. Boot patches force the deployment-shaped rows: frontend serving off, JSONL session logs on the plaintext path, preset roots onto the image's `config/agent-presets`.

+ 2 - 0
packages/experimental/webworker-runtime/README.zh.md

@@ -23,6 +23,8 @@ kind: "package-library"
 <a id="use-this-package"></a>
 ## 使用本包
 
+公开 npm 包在根入口导出运行时库,通过 `./worker` 提供自包含的浏览器 Worker,通过 `./client` 提供页面连接器。部署方通过 [打包器](../webworker-packer/README.zh.md) 单独提供和托管 VFS 镜像;安装此库不会增加 profile 层。根入口还导出打包器所用的模块代理表与替换表。
+
 一条 tsdown 管线产出三个构建产物;另有一层由源码维护的进程实现:
 
 - **`lib/index.js`(装配库)**——`createWorkerHost`/`startWorkerHost` 挂载基础镜像和按序排列的数据 overlays(`storage/`)、安装模块加载器(`module-system/`)与 `process` shim、经镜像自带的 `dsh-app-boot` 启动插件树,并把服务 seam 交给隧道。Overlay 只能替换 `home/` 与 `workspace/` 下的文件,不能替换基础 manifest、配置或模块。镜像布局契约(`image-layout.ts`:虚拟根、config/manifest 路径、空目录、`lowered` 包装契约门)与 packer 共享。boot patch 强制部署形态行:关前端静态服务、JSONL 会话日志走明文、preset 根指向镜像内 `config/agent-presets`。

+ 3 - 1
packages/experimental/webworker-runtime/package.json

@@ -2,7 +2,9 @@
   "name": "@deepseek-ai/dsh-experimental-webworker-runtime",
   "description": "Browser-only harness runtime: in-memory VFS, module transform and loader, postMessage tunnel, and the dedicated Web Worker assembly, with the Node-compatibility layer that lets the host tree run unchanged",
   "version": "0.1.5-rc.2",
-  "private": true,
+  "publishConfig": {
+    "access": "public"
+  },
   "repository": {
     "type": "git",
     "url": "git+https://github.com/deepseek-ai/deepseek-harness.git",

+ 2 - 0
packages/experimental/webworker-runtime/src/index.ts

@@ -19,6 +19,8 @@ export {
   type Resolution, type StaticModuleFactory, type WorkerModuleLoaderOptions, type WorkerRequire,
 } from './module-system/module-loader.ts'
 export * as posixPath from './module-system/posix-path.ts'
+export { MODULE_PROXIES, MODULE_PROXY_PREFIXES } from './module-proxies.ts'
+export { REPLACED_EXTERNAL_PACKAGES } from './node/external_packages/replaced-externals.ts'
 export {
   createSyntheticExchange,
   type RequestListener, type ResponseSink, type SyntheticExchange,

+ 49 - 8
scripts/browser-bundled-externals.spec.ts

@@ -1,18 +1,35 @@
-import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, unlinkSync, writeFileSync } from 'node:fs'
+import { existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, symlinkSync, unlinkSync, writeFileSync } from 'node:fs'
+import { createRequire } from 'node:module'
 import { tmpdir } from 'node:os'
 import { dirname, join, resolve } from 'node:path'
-import { afterEach, describe, expect, it } from 'vitest'
+import { afterEach, describe, expect, it, vi } from 'vitest'
 import { browserBundledExternals, browserPackageOfFile, browserSourceAliases } from './browser-bundled-externals.ts'
 
+const filesystem = vi.hoisted(() => ({ preservedAlias: undefined as string | undefined }))
+vi.mock('node:fs', async (importOriginal) => {
+  const actual = await importOriginal<typeof import('node:fs')>()
+  return {
+    ...actual,
+    realpathSync: Object.assign((...args: Parameters<typeof actual.realpathSync>) => {
+      // Windows' JavaScript realpath can retain an 8.3 alias that Vite's native resolution expands.
+      if (typeof args[0] === 'string' && args[0] === filesystem.preservedAlias) return args[0]
+      return actual.realpathSync(...args)
+    }, { native: actual.realpathSync.native }),
+  }
+})
+
 const roots: string[] = []
+const dependencyLinks: string[] = []
 const repositoryRoot = resolve(import.meta.dirname, '..')
 
 afterEach(() => {
+  filesystem.preservedAlias = undefined
+  for (const link of dependencyLinks.splice(0)) unlinkSync(link)
   for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true })
 })
 
 function fixture(): string {
-  const root = mkdtempSync(join(tmpdir(), 'dsh-browser-notices-'))
+  const root = realpathSync.native(mkdtempSync(join(tmpdir(), 'dsh-browser-notices-')))
   roots.push(root)
   write(root, 'package.json', '{"type":"module"}')
   write(root, 'tsconfig.base.json', JSON.stringify({
@@ -78,7 +95,10 @@ describe('browser dependency discovery', () => {
     await expect(browserBundledExternals(root)).rejects.toThrow('has no browser build config')
   })
 
-  it.each([false, true])('follows shell aliases, CSS and lazy imports without writing output (symlinked root: %s)', async (linked) => {
+  it.each([
+    ['Rollup', false, false], ['Rollup', true, false], ['Rollup', true, true],
+    ['Rolldown', false, false], ['Rolldown', true, false], ['Rolldown', true, true],
+  ] as const)('follows shell aliases, CSS and lazy imports without writing output (%s, symlinked root: %s, retained alias: %s)', async (bundler, linked, preserveAlias) => {
     const root = fixture()
     library(root, 'shell-lib')
     library(root, 'lazy-lib')
@@ -96,19 +116,40 @@ describe('browser dependency discovery', () => {
     ].join('\n'))
     const app = join(root, 'apps/web')
     write(root, 'apps/web/package.json', '{"name":"@fixture/web","type":"module","exports":{"./dist/*":"./dist/*"}}')
-    symlinkSync(resolve(repositoryRoot, 'apps/web/node_modules'), join(app, 'node_modules'), 'junction')
+    const owner = bundler === 'Rollup' ? resolve(repositoryRoot, 'apps/web/package.json')
+      : createRequire(resolve(repositoryRoot, 'package.json')).resolve('vitest/package.json')
+    const viteDirectory = dirname(createRequire(owner).resolve('vite/package.json'))
+    const viteLink = join(app, 'node_modules/vite')
+    mkdirSync(dirname(viteLink), { recursive: true })
+    symlinkSync(viteDirectory, viteLink, 'junction')
+    dependencyLinks.push(viteLink)
     write(root, 'apps/web/index.html', '<script type="module" src="./main.ts"></script>')
     write(root, 'apps/web/main.ts', 'import { output, lazy } from "@fixture/static"; console.log(output); void lazy()')
-    write(root, 'apps/web/vite.config.ts', `export default {
-      build: { rollupOptions: { input: { index: ${JSON.stringify(join(app, 'index.html'))}, preview: "missing-preview.ts" } } }
-    }`)
+    write(root, 'apps/web/vite.config.ts', `
+      import { productWebBundleIsolation } from ${JSON.stringify(resolve(repositoryRoot, 'apps/web/product-isolation.ts').replaceAll('\\', '/'))}
+      export default ({ mode }) => {
+        if (mode !== 'production') throw new Error('notice discovery must use the production build mode')
+        return {
+          plugins: [...productWebBundleIsolation(${JSON.stringify(root)}, ${JSON.stringify(app)}), {
+            name: 'fixture-bundler-engine',
+            generateBundle() {
+              const actual = typeof this.meta.rolldownVersion === 'string' ? 'Rolldown' : 'Rollup'
+              if (actual !== ${JSON.stringify(bundler)}) throw new Error('unexpected bundler: ' + actual)
+            },
+          }],
+          build: { rollupOptions: { input: { index: ${JSON.stringify(join(app, 'index.html'))}, preview: "missing-preview.ts" } } }
+        }
+      }
+    `)
     write(root, 'apps/web/dist/sentinel.txt', 'untouched')
 
     const scanRoot = linked ? join(fixture(), 'linked') : root
     if (linked) symlinkSync(root, scanRoot, 'junction')
+    if (preserveAlias) filesystem.preservedAlias = scanRoot
     try {
       expect(await browserBundledExternals(scanRoot)).toEqual(new Set(['shell-lib', 'lazy-lib', 'asset-lib']))
     } finally {
+      filesystem.preservedAlias = undefined
       if (linked) unlinkSync(scanRoot)
     }
     expect(readFileSync(join(app, 'dist/sentinel.txt'), 'utf8')).toBe('untouched')

+ 8 - 5
scripts/browser-bundled-externals.ts

@@ -6,6 +6,7 @@ import { dirname, resolve } from 'node:path'
 import { pathToFileURL } from 'node:url'
 import { Rolldown, type UserConfigExport } from 'tsdown'
 import ts from 'typescript'
+import { browserDependencyAnalysis } from '../apps/web/product-isolation.ts'
 
 interface Manifest {
   name: string
@@ -117,7 +118,9 @@ interface ShellConfig {
 }
 
 interface ViteApi {
-  resolveConfig(config: Record<string, unknown>, command: 'build'): Promise<ShellConfig>
+  resolveConfig(
+    config: Record<string, unknown>, command: 'build', defaultMode: string, defaultNodeEnv: string,
+  ): Promise<ShellConfig>
   build(config: Record<string, unknown>): Promise<unknown>
 }
 
@@ -132,7 +135,7 @@ async function collectShell(
     if (manifest.private === true || manifest.exports?.['./dist/*'] === undefined) continue
     const vitePath = createRequire(resolve(dir, 'package.json')).resolve('vite')
     const vite = await import(pathToFileURL(vitePath).href) as ViteApi
-    const config = await vite.resolveConfig({ root: dir, logLevel: 'error' }, 'build')
+    const config = await vite.resolveConfig({ root: dir, logLevel: 'error' }, 'build', 'production', 'production')
     const input = config.build.rollupOptions?.input
     const entries = typeof input === 'string' ? [input] : Object.values(input ?? {})
     const pages = entries.filter(entry => entry.endsWith('.html'))
@@ -140,7 +143,7 @@ async function collectShell(
     await vite.build({
       root: dir,
       logLevel: 'error',
-      plugins: [recorder(seen, workspaceNames, true)],
+      plugins: [browserDependencyAnalysis(), recorder(seen, workspaceNames, true)],
       resolve: { alias: browserSourceAliases(root) },
       build: {
         write: false,
@@ -163,8 +166,8 @@ async function collectShell(
  * @returns Names of distributed browser inputs, excluding workspace packages and erased types.
  */
 export async function browserBundledExternals(root: string): Promise<Set<string>> {
-  // Vite resolves HTML inputs to real paths, so its root must use the same spelling.
-  root = realpathSync(root)
+  // Vite resolves HTML through native realpath, including Windows 8.3 alias expansion.
+  root = realpathSync.native(root)
   const manifests = new Map<string, Manifest>()
   for (const glob of ['packages/*/*/package.json', 'vendor/*/package.json']) {
     for (const path of globSync(glob, { cwd: root }).sort()) {

+ 95 - 0
scripts/bundle-input-isolation.ts

@@ -0,0 +1,95 @@
+/** Experimental ownership of the actual filesystem and package inputs supplied to a bundler. */
+
+import { existsSync, readFileSync, realpathSync } from 'node:fs'
+import { dirname, isAbsolute, relative, resolve, sep } from 'node:path'
+import { fileURLToPath } from 'node:url'
+
+/** Package identities are cached only within one build. */
+export class BundleInputIsolation {
+  private readonly packages = new Map<string, string | undefined>()
+  private readonly repository: string
+  private readonly label: string
+
+  constructor(repository: string, label: string) {
+    this.repository = repository
+    this.label = label
+  }
+
+  /** Discard package metadata before a new build or watch rebuild. */
+  reset(): void { this.packages.clear() }
+
+  /**
+   * Require a recorded input to have non-experimental physical and package ownership.
+   * @param id - bundler module id, filesystem path, or filesystem URL.
+   */
+  assertInput(id: string): void {
+    this.checkInput(id, false)
+  }
+
+  /**
+   * Check declared source-map ownership; dependency tarballs may omit their upstream sources.
+   * @param id - source-map source resolved relative to its map.
+   */
+  assertSourceMapInput(id: string): void {
+    this.checkInput(id, true)
+  }
+
+  private checkInput(id: string, allowMissing: boolean): void {
+    if (/(?:^|[/:\u0000])@deepseek-ai\/dsh-experimental-[^/?#]+/.test(id)) {
+      throw new Error(`${this.label}: experimental input ${id}`)
+    }
+    const file = physicalBundleInput(id)
+    if (file === undefined) return
+    const lexical = relative(resolve(this.repository, 'packages/experimental'), file)
+    if (lexical === '' || lexical !== '..' && !lexical.startsWith(`..${sep}`) && !isAbsolute(lexical)) {
+      throw new Error(`${this.label}: experimental input ${id}`)
+    }
+    let existing = file
+    if (!allowMissing && !existsSync(file)) throw new Error(`${this.label}: input ${id} is missing`)
+    while (!existsSync(existing)) {
+      const parent = dirname(existing)
+      if (parent === existing) throw new Error(`${this.label}: input ${id} has no existing filesystem root`)
+      existing = parent
+    }
+    const canonical = resolve(realpathSync(existing), relative(existing, file))
+    if (canonical !== file) this.checkInput(canonical, allowMissing)
+    const name = this.packageName(dirname(canonical))
+    if (name?.startsWith('@deepseek-ai/dsh-experimental-')) {
+      throw new Error(`${this.label}: ${id} belongs to experimental package ${name}`)
+    }
+  }
+
+  private packageName(directory: string): string | undefined {
+    if (this.packages.has(directory)) return this.packages.get(directory)
+    const manifest = resolve(directory, 'package.json')
+    let name: string | undefined
+    if (existsSync(manifest)) {
+      const value: unknown = JSON.parse(readFileSync(manifest, 'utf8'))
+      if (value !== null && typeof value === 'object' && 'name' in value && typeof value.name === 'string') name = value.name
+    }
+    if (name === undefined && dirname(directory) !== directory) name = this.packageName(dirname(directory))
+    this.packages.set(directory, name)
+    return name
+  }
+}
+
+/**
+ * Recover physical ownership from queries, filesystem URLs, and virtual path wrappers.
+ * @param id - bundler module id or recorded filesystem input.
+ * @returns absolute file path, or undefined for a virtual id without a physical path.
+ */
+export function physicalBundleInput(id: string): string | undefined {
+  let clean = id.replaceAll('\\', '/').split(/[?#]/, 1)[0] ?? ''
+  clean = clean.replace(/^\u0000/, '')
+  if (clean.startsWith('file://')) return fileURLToPath(clean)
+  if (clean.startsWith('/@fs/')) {
+    clean = clean.slice('/@fs/'.length)
+    if (!isAbsolute(clean) && !/^[a-zA-Z]:\//.test(clean)) clean = `/${clean}`
+  }
+  if (!isAbsolute(clean) && !/^[a-zA-Z]:\//.test(clean)) {
+    const wrapped = /:(\/.*|[a-zA-Z]:\/.*)$/.exec(clean)?.[1]
+    if (wrapped === undefined) return undefined
+    clean = wrapped
+  }
+  return resolve(clean)
+}

+ 37 - 23
scripts/check-workspace-constraints.spec.ts

@@ -1,6 +1,10 @@
 /** Experimental-package publication and dependency constraints. */
 
 import { describe, expect, it } from 'vitest'
+import {
+  isPublicExperimentalPackageDirectory,
+  PRIVATE_EXPERIMENTAL_PACKAGE_DIRECTORIES,
+} from './experimental-package-policy.ts'
 import {
   checkDshFamilyVersion,
   checkExperimentalDependencyIsolation,
@@ -9,18 +13,13 @@ import {
   type WorkspaceManifest,
 } from './check-workspace-constraints.ts'
 
-const experimental: WorkspaceManifest = {
+const experimental = {
   dir: 'packages/experimental/prototype',
-  manifest: { name: '@deepseek-ai/dsh-experimental-prototype', private: true },
-}
-
-const publicExperimental: WorkspaceManifest = {
-  dir: 'packages/experimental/agent-team',
   manifest: {
-    name: '@deepseek-ai/dsh-experimental-agent-team',
+    name: '@deepseek-ai/dsh-experimental-prototype',
     publishConfig: { access: 'public' },
   },
-}
+} satisfies WorkspaceManifest
 
 describe('experimental workspace constraints', () => {
   it('requires the experimental package-name prefix', () => {
@@ -32,31 +31,46 @@ describe('experimental workspace constraints', () => {
     ])
   })
 
-  it('requires private manifests without publication metadata', () => {
+  it('requires public metadata for unlisted experimental packages', () => {
     expect(checkExperimentalManifest(experimental)).toEqual([])
     expect(checkExperimentalManifest({
       ...experimental,
-      manifest: { ...experimental.manifest, private: false, publishConfig: { access: 'public' } },
+      manifest: { name: experimental.manifest.name, private: true },
     })).toEqual([
-      '@deepseek-ai/dsh-experimental-prototype: experimental package must set "private": true',
-      '@deepseek-ai/dsh-experimental-prototype: experimental package must omit publishConfig',
+      '@deepseek-ai/dsh-experimental-prototype: public experimental package must not set "private": true',
+      '@deepseek-ai/dsh-experimental-prototype: public experimental package must set publishConfig.access to "public"',
     ])
   })
 
-  it('requires public metadata only for the Agent Teams exceptions', () => {
-    expect(checkExperimentalManifest(publicExperimental)).toEqual([])
-    expect(checkExperimentalManifest({
-      ...publicExperimental,
-      manifest: {
-        name: '@deepseek-ai/dsh-experimental-agent-team',
-        private: true,
-      },
-    })).toEqual([
-      '@deepseek-ai/dsh-experimental-agent-team: public experimental package must not set "private": true',
-      '@deepseek-ai/dsh-experimental-agent-team: public experimental package must set publishConfig.access to "public"',
+  it('requires private metadata for an explicitly excluded prototype', () => {
+    const { dir, manifest: { name } } = experimental
+    const privateDirectories = [dir]
+    expect(isPublicExperimentalPackageDirectory(dir, privateDirectories)).toBe(false)
+    expect(checkExperimentalManifest({ dir, manifest: { name, private: true } }, privateDirectories)).toEqual([])
+    expect(checkExperimentalManifest(experimental, privateDirectories)).toEqual([
+      `${name}: experimental package must set "private": true`,
+      `${name}: experimental package must omit publishConfig`,
     ])
   })
 
+  it('keeps the current experimental publication set unrestricted', () => {
+    expect(PRIVATE_EXPERIMENTAL_PACKAGE_DIRECTORIES).toEqual([])
+  })
+
+  it('limits the public default to experimental package directories', () => {
+    expect(isPublicExperimentalPackageDirectory(experimental.dir)).toBe(true)
+    for (const dir of [
+      'packages/core/session',
+      'apps/cli',
+      'vendor/cordis',
+      'packages/experimental',
+      'packages/experimental/prototype/src',
+      ...PRIVATE_EXPERIMENTAL_PACKAGE_DIRECTORIES,
+    ]) {
+      expect(isPublicExperimentalPackageDirectory(dir)).toBe(false)
+    }
+  })
+
   it.each(['dependencies', 'optionalDependencies', 'peerDependencies'] as const)(
     'rejects release %s on an experimental package',
     (section) => {

+ 13 - 5
scripts/check-workspace-constraints.ts

@@ -8,7 +8,10 @@
 import { existsSync, readdirSync, readFileSync } from 'node:fs'
 import { join, relative, resolve } from 'node:path'
 import { pathToFileURL } from 'node:url'
-import { isPublicExperimentalPackageDirectory } from './experimental-package-policy.ts'
+import {
+  isPublicExperimentalPackageDirectory,
+  PRIVATE_EXPERIMENTAL_PACKAGE_DIRECTORIES,
+} from './experimental-package-policy.ts'
 import { hasTypertRemoteNavigation, isForbiddenPublicationFile } from './publication-payload.ts'
 import { collectProjectReferenceFaceViolations } from './project-reference-faces.ts'
 
@@ -62,7 +65,7 @@ const appPackageFiles: Readonly<Record<string, readonly string[]>> = {
     'config/desktop.cordis.patch.yml',
   ],
   // Sourcemaps stay out by payload policy; the worker-preview surface
-  // (dist/preview.html and dist/preview/) backs private experimental
+  // (dist/preview.html and dist/preview/) backs opt-in experimental
   // packages and is not published.
   '@deepseek-ai/dsh-web-frontend': ['dist', '!dist/**/*.map', '!dist/preview.html', '!dist/preview'],
 }
@@ -158,6 +161,8 @@ const packageFileExtras: Readonly<Record<string, readonly string[]>> = {
   '@deepseek-ai/dsh-experimental-ptc-runtime-python': ['py/**/*.py'],
   // The isolated Node bootstrap is a separately launched bundle.
   '@deepseek-ai/dsh-ptc-runtime-node': ['lib/process.js'],
+  // The Host entry starts its sibling Worker by URL rather than a package export.
+  '@deepseek-ai/dsh-experimental-inspector': ['lib/worker.js'],
   // The shipped preset compositions travel inside the roster package.
   '@deepseek-ai/dsh-agent-presets': ['presets'],
   // The Web Host mounts the default-off settings owner independently of each
@@ -276,15 +281,18 @@ function usesEmittedTreeDefaults(manifest: PackageManifest): boolean {
     exportDefault(manifest, subpath)?.startsWith('./lib/types/') === true)
 }
 
-/** Experimental manifest requirements, including explicit public exceptions. */
-export function checkExperimentalManifest({ dir, manifest }: WorkspaceManifest): string[] {
+/** Experimental manifest requirements, including explicit private exceptions. */
+export function checkExperimentalManifest(
+  { dir, manifest }: WorkspaceManifest,
+  privateDirectories: readonly string[] = PRIVATE_EXPERIMENTAL_PACKAGE_DIRECTORIES,
+): string[] {
   if (!experimentalPackageDirectory.test(dir)) return []
   const label = manifest.name ?? dir
   const errors: string[] = []
   if (manifest.name?.startsWith(experimentalPackageNamePrefix) !== true) {
     errors.push(`${label}: experimental package name must start with ${JSON.stringify(experimentalPackageNamePrefix)}`)
   }
-  if (isPublicExperimentalPackageDirectory(dir)) {
+  if (isPublicExperimentalPackageDirectory(dir, privateDirectories)) {
     if (manifest.private === true) errors.push(`${label}: public experimental package must not set "private": true`)
     if (manifest.publishConfig?.access !== 'public') {
       errors.push(`${label}: public experimental package must set publishConfig.access to "public"`)

+ 186 - 4
scripts/client-bundle-purity.spec.ts

@@ -2,12 +2,13 @@
  * Pins shared client-bundle preset rules: module-edge purity, source-map
  * chaining, and physical watch dependencies hidden behind virtual CSS Modules.
  */
-import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
+import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
 import { tmpdir } from 'node:os'
-import { join } from 'node:path'
+import { dirname, join, relative } from 'node:path'
 import { fileURLToPath } from 'node:url'
-import { describe, expect, it, vi } from 'vitest'
-import { clientBundle, requestedExternals } from '../packages/client/tsdown.client.ts'
+import { build, type TsdownBundle, type UserConfig } from 'tsdown'
+import { describe, expect, it, onTestFinished, vi } from 'vitest'
+import { clientBundle, requestedExternals, staticLinked } from '../packages/client/tsdown.client.ts'
 
 type ResolveId = (source: string) => null | { id: string; external: boolean }
 
@@ -22,6 +23,18 @@ interface SourceMapPlugin {
   load?: (id: string) => Promise<unknown>
 }
 
+interface InputIsolationPlugin {
+  name: string
+  generateBundle: (this: {
+    getModuleInfo(id: string): { importedIds: string[]; dynamicallyImportedIds: string[] } | null
+  }, options: unknown, bundle: Record<string, {
+      type: 'chunk'
+      modules: Record<string, object>
+      imports: string[]
+      dynamicImports: string[]
+    }>) => void
+}
+
 /** A representative dynamic bundle using the shared client baseline. */
 const REQUESTING_PACKAGE = '@deepseek-ai/dsh-client-ui-conversation'
 
@@ -147,6 +160,175 @@ describe('client bundle purity gate', () => {
   })
 })
 
+describe('client bundle experimental input isolation', () => {
+  const experimental = '@deepseek-ai/dsh-experimental-client-ui-agent-team'
+
+  function fixture() {
+    const root = mkdtempSync(join(tmpdir(), 'dsh-client-inputs-'))
+    onTestFinished(() => { rmSync(root, { recursive: true, force: true }) })
+    const owner = join(root, 'client')
+    const entry = join(owner, 'lib/types/client/index.js')
+    const prototype = join(root, 'prototype/src/index.js')
+    for (const file of [entry, prototype]) mkdirSync(dirname(file), { recursive: true })
+    writeFileSync(join(owner, 'package.json'), JSON.stringify({ name: REQUESTING_PACKAGE, type: 'module' }))
+    writeFileSync(join(root, 'prototype/package.json'), JSON.stringify({ name: experimental, type: 'module' }))
+    writeFileSync(prototype, 'export const marker = "experimental sentinel"\n')
+    return { root, owner, entry, prototype }
+  }
+
+  function config(kind: 'static' | 'dynamic', id = REQUESTING_PACKAGE): UserConfig {
+    const configs = kind === 'static'
+      ? staticLinked(id, ['lib/types/client/index.js'])({ env: { DSH_BUILD_FACE: 'client' } })
+      : clientConfigs(id)
+    const browser = configs.find(config => config.platform === 'browser')
+    if (browser === undefined) throw new Error('client config missing')
+    return browser
+  }
+
+  async function bundle(owner: string, config: UserConfig): Promise<string> {
+    let builds: TsdownBundle[] = []
+    try {
+      builds = await build({
+        ...config, cwd: owner, config: false, tsconfig: false,
+        write: false, clean: false, exports: false, report: false, logLevel: 'silent',
+      })
+      return builds.flatMap(build => build.chunks.filter(chunk => chunk.type === 'chunk').map(chunk => chunk.code)).join('\n')
+    } finally {
+      for (const build of builds) await build[Symbol.asyncDispose]()
+    }
+  }
+
+  function importPath(from: string, to: string): string {
+    return relative(dirname(from), to).replaceAll('\\', '/')
+  }
+
+  function checkCompilerModule(module: string, imports?: string[]): void {
+    const plugins = config('dynamic').plugins as InputIsolationPlugin[]
+    const plugin = plugins.find(plugin => plugin.name === 'dsh-client-input-isolation')
+    if (plugin === undefined) throw new Error('client input isolation plugin missing')
+    plugin.generateBundle.call({
+      getModuleInfo: () => imports === undefined ? null : { importedIds: imports, dynamicallyImportedIds: [] },
+    }, {}, {
+      'client.js': { type: 'chunk', modules: { [module]: {} }, imports: [], dynamicImports: [] },
+    })
+  }
+
+  it('accepts the exact compiler runtime helper without a source module record', () => {
+    expect(() => { checkCompilerModule('\0rolldown/runtime.js') }).not.toThrow()
+  })
+
+  it.each(['\0rolldown/other.js', '\0rolldown/runtime.js?user', '\0other/runtime.js'])(
+    'rejects the unrecorded module %j',
+    (module) => {
+      expect(() => { checkCompilerModule(module) }).toThrow(/has no bundler module record/)
+    },
+  )
+
+  it('checks runtime helper dependencies when the compiler supplies a module record', () => {
+    expect(() => { checkCompilerModule('\0rolldown/runtime.js', [experimental]) })
+      .toThrow(/client bundle isolation.*experimental/)
+  })
+
+  it.each(['static', 'dynamic'] as const)('rejects experimental files folded into a %s client artifact', async (kind) => {
+    const { owner, entry, prototype } = fixture()
+    writeFileSync(entry, `export { marker } from ${JSON.stringify(importPath(entry, prototype))}\n`)
+
+    await expect(bundle(owner, config(kind))).rejects.toThrow(/client bundle isolation.*experimental/)
+    expect(existsSync(join(owner, 'lib/client.js'))).toBe(false)
+    expect(existsSync(join(owner, 'lib/index.js'))).toBe(false)
+  })
+
+  it('proves the static library otherwise hides the experimental input in its emitted JavaScript', async () => {
+    const { owner, entry, prototype } = fixture()
+    writeFileSync(entry, `export { marker } from ${JSON.stringify(importPath(entry, prototype))}\n`)
+    const guarded = config('static')
+    const plugins = guarded.plugins as Array<{ name: string }>
+    const unguarded: UserConfig = {
+      ...guarded,
+      plugins: plugins.filter(plugin => plugin.name !== 'dsh-client-input-isolation'),
+      outputOptions: { sourcemapExcludeSources: false },
+    }
+
+    await expect(bundle(owner, unguarded)).resolves.toContain('experimental sentinel')
+    await expect(bundle(owner, guarded)).rejects.toThrow(/client bundle isolation.*experimental/)
+  })
+
+  it.each(['static', 'dynamic'] as const)('allows an experimental %s artifact to keep its own inputs', async (kind) => {
+    const { owner, entry, prototype } = fixture()
+    writeFileSync(entry, `export { marker } from ${JSON.stringify(importPath(entry, prototype))}\n`)
+
+    await expect(bundle(owner, config(kind, experimental))).resolves.toContain('experimental sentinel')
+  })
+
+  it('rejects experimental ownership preserved only by an emitted compiler source map', async () => {
+    const { owner, entry, prototype } = fixture()
+    writeFileSync(entry, 'export const marker = "experimental sentinel"\n//# sourceMappingURL=index.js.map\n')
+    writeFileSync(`${entry}.map`, JSON.stringify({
+      version: 3, names: [], mappings: 'AAAA', sources: [importPath(entry, prototype)],
+      sourcesContent: ['export const marker = "experimental sentinel"\n'],
+    }))
+
+    await expect(bundle(owner, config('static'))).rejects.toThrow(/client bundle isolation.*experimental/)
+  })
+
+  it('checks the resolved module when an import alias conceals its experimental package', async () => {
+    const { owner, entry, prototype } = fixture()
+    writeFileSync(entry, 'export { marker } from "./ordinary.js"\n')
+
+    await expect(bundle(owner, { ...config('static'), alias: { './ordinary.js': prototype } }))
+      .rejects.toThrow(/client bundle isolation.*experimental/)
+  })
+
+  it('rejects an experimental runtime import left external by a static client library', async () => {
+    const { owner, entry } = fixture()
+    writeFileSync(entry, `export * from ${JSON.stringify(experimental)}\n`)
+
+    await expect(bundle(owner, config('static'))).rejects.toThrow(/client bundle isolation.*experimental/)
+  })
+
+  it.each([false, true])('checks omitted source-map files against their package owner (experimental: %s)', async (experimentalSource) => {
+    const { root, owner, entry } = fixture()
+    const dependency = join(root, 'dependency')
+    mkdirSync(dependency)
+    writeFileSync(join(dependency, 'package.json'), JSON.stringify({ name: experimentalSource ? experimental : 'ordinary-library' }))
+    writeFileSync(entry, 'export const marker = "mapped sentinel"\n//# sourceMappingURL=index.js.map\n')
+    writeFileSync(`${entry}.map`, JSON.stringify({
+      version: 3, names: [], mappings: 'AAAA', sourceRoot: importPath(entry, dependency),
+      sources: ['unshipped.ts'], sourcesContent: ['export const marker = "mapped sentinel"\n'],
+    }))
+
+    const result = bundle(owner, config('static'))
+    if (experimentalSource) await expect(result).rejects.toThrow(/client bundle isolation.*experimental/)
+    else await expect(result).resolves.toContain('mapped sentinel')
+  })
+
+  it.each(['.css', '.module.css', '.css?inline'])('rejects experimental %s inputs behind client CSS virtual loaders', async (extension) => {
+    const { owner, entry, prototype } = fixture()
+    const css = join(dirname(prototype), `style${extension.replace('?inline', '')}`)
+    writeFileSync(css, 'body { color: red; }\n')
+    const specifier = `${importPath(entry, css)}${extension.endsWith('?inline') ? '?inline' : ''}`
+    writeFileSync(entry, extension === '.css'
+      ? `import ${JSON.stringify(specifier)}\nexport const marker = true\n`
+      : `export { default as style } from ${JSON.stringify(specifier)}\n`)
+
+    await expect(bundle(owner, config('dynamic'))).rejects.toThrow(/client bundle isolation.*experimental/)
+  })
+
+  it.each(['static', 'dynamic'] as const)('keeps ordinary %s bundles and source maps buildable', async (kind) => {
+    const { owner, entry } = fixture()
+    const source = join(owner, 'src/client/index.ts')
+    mkdirSync(dirname(source), { recursive: true })
+    writeFileSync(source, 'export const marker = "stable sentinel"\n')
+    writeFileSync(entry, 'export const marker = "stable sentinel"\n//# sourceMappingURL=index.js.map\n')
+    writeFileSync(`${entry}.map`, JSON.stringify({
+      version: 3, names: [], mappings: 'AAAA', sources: [importPath(entry, source)],
+      sourcesContent: ['export const marker = "stable sentinel"\n'],
+    }))
+
+    await expect(bundle(owner, config(kind))).resolves.toContain('stable sentinel')
+  })
+})
+
 describe('client bundle module requests', () => {
   it('requests what the declaration lists', () => {
     const requests = requestedExternals('@deepseek-ai/dsh-client-fixture', {

+ 10 - 15
scripts/experimental-package-policy.ts

@@ -1,21 +1,16 @@
-/** Experimental packages that publish with the dsh release family without changing names. */
-export const PUBLIC_EXPERIMENTAL_PACKAGE_DIRECTORIES = [
-  'packages/experimental/agent-team',
-  'packages/experimental/agent-team-profile',
-  'packages/experimental/agent-team-web-profile',
-  'packages/experimental/client-ui-agent-team',
-  'packages/experimental/tool-agent-team',
-  'packages/experimental/computer-use-cua-driver-mcp',
-  'packages/experimental/computer-use-cua-driver-native',
-] as const
-
-const publicExperimentalPackageDirectories = new Set<string>(PUBLIC_EXPERIMENTAL_PACKAGE_DIRECTORIES)
+/** Experimental packages excluded from public releases and npm baselines. */
+export const PRIVATE_EXPERIMENTAL_PACKAGE_DIRECTORIES: readonly string[] = []
 
 /**
- * Whether an experimental package is an explicit public-release exception.
+ * Whether an experimental package publishes under the default-public policy.
  * @param directory - repository-relative package directory.
+ * @param privateDirectories - experimental directories excluded from publication.
  * @returns Whether the package publishes with the dsh family.
  */
-export function isPublicExperimentalPackageDirectory(directory: string): boolean {
-  return publicExperimentalPackageDirectories.has(directory)
+export function isPublicExperimentalPackageDirectory(
+  directory: string,
+  privateDirectories: readonly string[] = PRIVATE_EXPERIMENTAL_PACKAGE_DIRECTORIES,
+): boolean {
+  return /^packages\/experimental\/[^/]+$/.test(directory)
+    && !privateDirectories.includes(directory)
 }

+ 73 - 0
scripts/npm-baseline-packages.spec.ts

@@ -0,0 +1,73 @@
+/** Baseline package selection through Node's real filesystem glob implementation. */
+
+import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
+import { tmpdir } from 'node:os'
+import { dirname, join } from 'node:path'
+import { describe, expect, it, onTestFinished } from 'vitest'
+import { PRIVATE_EXPERIMENTAL_PACKAGE_DIRECTORIES } from './experimental-package-policy.ts'
+import { discoverNpmBaselineManifests } from './npm-baseline-packages.ts'
+
+function fixture(manifests: readonly string[]): string {
+  const root = mkdtempSync(join(tmpdir(), 'dsh-baseline-discovery-'))
+  onTestFinished(() => { rmSync(root, { recursive: true, force: true }) })
+  for (const manifest of manifests) {
+    const file = join(root, manifest)
+    mkdirSync(dirname(file), { recursive: true })
+    writeFileSync(file, '{}\n')
+  }
+  return root
+}
+
+describe('npm baseline package discovery', () => {
+  it('discovers ordinary package roots and unlisted experimental packages', () => {
+    const expected = [
+      'apps/cli/package.json',
+      'packages/core/session/package.json',
+      'packages/experimental/agent-team/package.json',
+      'packages/experimental/new-prototype/package.json',
+      'vendor/cordis/package.json',
+    ]
+    const root = fixture([
+      ...expected,
+      'package.json',
+      'packages/experimental/new-prototype/nested/package.json',
+      'website/package.json',
+    ])
+
+    expect(discoverNpmBaselineManifests(root)).toEqual(expected)
+  })
+
+  it('excludes every directory in the current publication denylist', () => {
+    const allowed = 'packages/experimental/new-prototype/package.json'
+    const root = fixture([
+      allowed,
+      ...PRIVATE_EXPERIMENTAL_PACKAGE_DIRECTORIES.map(directory => `${directory}/package.json`),
+    ])
+
+    expect(discoverNpmBaselineManifests(root)).toEqual([allowed])
+  })
+
+  it('excludes an exact configured directory without excluding nearby names', () => {
+    const root = fixture([
+      'packages/experimental/internal/package.json',
+      'packages/experimental/internal-tools/package.json',
+      'packages/experimental/other/package.json',
+    ])
+    expect(discoverNpmBaselineManifests(root, ['packages/experimental/internal'])).toEqual([
+      'packages/experimental/internal-tools/package.json',
+      'packages/experimental/other/package.json',
+    ])
+  })
+
+  it('retains experimental packages when the configured denylist is empty', () => {
+    const manifests = [
+      'packages/experimental/inspector/package.json',
+      'packages/experimental/new-prototype/package.json',
+    ]
+    expect(discoverNpmBaselineManifests(fixture(manifests), [])).toEqual(manifests)
+  })
+
+  it('leaves an empty package set for the baseline caller to reject', () => {
+    expect(discoverNpmBaselineManifests(fixture([]))).toEqual([])
+  })
+})

+ 26 - 0
scripts/npm-baseline-packages.ts

@@ -0,0 +1,26 @@
+/** Manifest discovery for the npm baseline's vendor, harness, and application packages. */
+
+import { globSync } from 'node:fs'
+import { PRIVATE_EXPERIMENTAL_PACKAGE_DIRECTORIES } from './experimental-package-policy.ts'
+
+const PACKAGE_PATTERNS = [
+  'vendor/*/package.json',
+  'packages/*/*/package.json',
+  'apps/*/package.json',
+] as const
+
+/**
+ * Discover baseline manifests while excluding the configured private experimental directories.
+ * @param root - repository root to scan.
+ * @param privateDirectories - repository-relative experimental directories excluded from publication.
+ * @returns Sorted repository-relative manifest paths with forward slashes.
+ */
+export function discoverNpmBaselineManifests(
+  root: string,
+  privateDirectories: readonly string[] = PRIVATE_EXPERIMENTAL_PACKAGE_DIRECTORIES,
+): string[] {
+  return globSync(PACKAGE_PATTERNS, {
+    cwd: root,
+    exclude: privateDirectories.map(directory => `${directory}/package.json`),
+  }).map(path => path.replaceAll('\\', '/')).sort()
+}

+ 2 - 9
scripts/publish-npm-baseline.ts

@@ -4,7 +4,6 @@ import { spawnSync, type SpawnSyncReturns } from 'node:child_process'
 import { createHash } from 'node:crypto'
 import {
   existsSync,
-  globSync,
   mkdirSync,
   mkdtempSync,
   readFileSync,
@@ -18,17 +17,11 @@ import { basename, dirname, isAbsolute, join, normalize, relative, resolve, sep
 import { createInterface } from 'node:readline/promises'
 import { pathToFileURL } from 'node:url'
 import { parseArgs } from 'node:util'
-import { PUBLIC_EXPERIMENTAL_PACKAGE_DIRECTORIES } from './experimental-package-policy.ts'
+import { discoverNpmBaselineManifests } from './npm-baseline-packages.ts'
 import { validateTarballPayload } from './publication-payload.ts'
 
 const DEFAULT_REGISTRY = 'https://registry.npm.harnessment.com'
 const DEFAULT_OUTPUT_DIRECTORY = '.artifacts/npm-baseline'
-const PACKAGE_PATTERNS = [
-  'vendor/*/package.json',
-  'packages/!(experimental)/*/package.json',
-  'apps/*/package.json',
-  ...PUBLIC_EXPERIMENTAL_PACKAGE_DIRECTORIES.map(directory => `${directory}/package.json`),
-] as const
 const DEPENDENCY_SECTIONS = [
   'dependencies',
   'devDependencies',
@@ -244,7 +237,7 @@ class WorkspacePackageSet {
   ) {}
 
   static discover(root: string): WorkspacePackageSet {
-    const manifestPaths = globSync(PACKAGE_PATTERNS, { cwd: root }).sort()
+    const manifestPaths = discoverNpmBaselineManifests(root)
     if (manifestPaths.length === 0) {
       throw new Error('no package manifests found under vendor/, packages/, or apps/')
     }

+ 25 - 2
scripts/release/families.spec.ts

@@ -42,7 +42,7 @@ afterEach(() => {
 })
 
 describe('release families', () => {
-  it('publishes allowlisted experimental packages while excluding private prototypes', () => {
+  it('publishes all current experimental packages', () => {
     const members = releaseFamily('dsh').members(resolve(import.meta.dirname, '../..'))
 
     expect(members
@@ -51,12 +51,16 @@ describe('release families', () => {
       '@deepseek-ai/dsh-experimental-agent-team-profile',
       '@deepseek-ai/dsh-experimental-agent-team-web-profile',
       '@deepseek-ai/dsh-experimental-agent-team',
+      '@deepseek-ai/dsh-experimental-auto-review',
       '@deepseek-ai/dsh-experimental-client-ui-agent-team',
       '@deepseek-ai/dsh-experimental-computer-use-cua-driver-mcp',
       '@deepseek-ai/dsh-experimental-computer-use-cua-driver-native',
+      '@deepseek-ai/dsh-experimental-inspector',
+      '@deepseek-ai/dsh-experimental-ptc-runtime-python',
       '@deepseek-ai/dsh-experimental-tool-agent-team',
+      '@deepseek-ai/dsh-experimental-webworker-packer',
+      '@deepseek-ai/dsh-experimental-webworker-runtime',
     ])
-    expect(members.map(member => member.name)).not.toContain('@deepseek-ai/dsh-experimental-inspector')
   })
 
   it('excludes private applications from the publish set', () => {
@@ -68,6 +72,25 @@ describe('release families', () => {
     expect(releaseFamily('dsh').members(root).map(entry => entry.name)).toEqual(['@deepseek-ai/dsh-public'])
   })
 
+  it('publishes unlisted experimental packages while retaining private exclusions', () => {
+    const root = mkdtempSync(join(tmpdir(), 'dsh-release-experimental-'))
+    roots.push(root)
+    write(join(root, 'packages/experimental/prototype/package.json'), JSON.stringify({
+      name: '@deepseek-ai/dsh-experimental-prototype',
+      version: '0.0.1',
+      publishConfig: { access: 'public' },
+    }))
+    write(join(root, 'packages/experimental/inspector/package.json'), JSON.stringify({
+      name: '@deepseek-ai/dsh-experimental-inspector',
+      version: '0.0.1',
+      private: true,
+    }))
+
+    expect(releaseFamily('dsh').members(root).map(entry => entry.name)).toEqual([
+      '@deepseek-ai/dsh-experimental-prototype',
+    ])
+  })
+
   it('bumps private dsh workspaces without adding release tags', () => {
     const root = mkdtempSync(join(tmpdir(), 'dsh-release-version-'))
     roots.push(root)

+ 1 - 3
scripts/release/families.ts

@@ -15,7 +15,6 @@ import {
   officialClientBuildEnvironment,
   readClientBuildRecord,
 } from '../client-build-environment.ts'
-import { PUBLIC_EXPERIMENTAL_PACKAGE_DIRECTORIES } from '../experimental-package-policy.ts'
 import { validateTarballPayload } from '../publication-payload.ts'
 
 /**
@@ -323,9 +322,8 @@ export abstract class ReleaseFamily {
 class DshFamily extends ReleaseFamily {
   readonly id = 'dsh'
   readonly patterns = [
-    'packages/!(experimental)/*/package.json',
+    'packages/*/*/package.json',
     'apps/*/package.json',
-    ...PUBLIC_EXPERIMENTAL_PACKAGE_DIRECTORIES.map(directory => `${directory}/package.json`),
   ] as const
   readonly tagPrefix = 'dsh-v'
 

+ 93 - 0
scripts/release/installed-product-isolation.spec.ts

@@ -0,0 +1,93 @@
+/** Installed-product isolation uses resolved package identities and runtime edges. */
+
+import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
+import { tmpdir } from 'node:os'
+import { join } from 'node:path'
+import { afterEach, describe, expect, it } from 'vitest'
+import { verifyInstalledProductIsolation } from './installed-product-isolation.ts'
+
+const roots: string[] = []
+const experimental = '@deepseek-ai/dsh-experimental-example'
+
+function fixture(): string {
+  const root = mkdtempSync(join(tmpdir(), 'dsh-installed-isolation-'))
+  roots.push(root)
+  return root
+}
+
+function writePackage(root: string, key: string, manifest: Record<string, unknown>): string {
+  const directory = join(root, 'node_modules', key)
+  mkdirSync(directory, { recursive: true })
+  writeFileSync(join(directory, 'package.json'), `${JSON.stringify({ name: key, ...manifest })}\n`)
+  return directory
+}
+
+afterEach(() => {
+  for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true })
+})
+
+describe('installed default-product isolation', () => {
+  it('ignores development dependencies and unrelated installed experimental packages', () => {
+    const root = fixture()
+    const entry = writePackage(root, '@deepseek-ai/dsh', {
+      dependencies: { core: '1.0.0' },
+      devDependencies: { [experimental]: '1.0.0' },
+    })
+    writePackage(root, 'core', { peerDependencies: { '@deepseek-ai/dsh': '1.0.0' } })
+    writePackage(root, experimental, {})
+
+    expect(verifyInstalledProductIsolation(entry)).toBe(2)
+  })
+
+  it.each(['dependencies', 'optionalDependencies', 'peerDependencies'])(
+    'rejects a transitive experimental %s',
+    (section) => {
+      const root = fixture()
+      const entry = writePackage(root, '@deepseek-ai/dsh', { dependencies: { thirdParty: '1.0.0' } })
+      writePackage(root, 'thirdParty', { [section]: { [experimental]: '1.0.0' } })
+
+      expect(() => verifyInstalledProductIsolation(entry)).toThrow(
+        `@deepseek-ai/dsh -> thirdParty -> ${experimental}`,
+      )
+    },
+  )
+
+  it('rejects experimental identities hidden behind an installed alias', () => {
+    const root = fixture()
+    const entry = writePackage(root, '@deepseek-ai/dsh', { dependencies: { safeName: 'file:../prototype' } })
+    writePackage(root, 'safeName', { name: experimental })
+
+    expect(() => verifyInstalledProductIsolation(entry)).toThrow(experimental)
+  })
+
+  it('rejects experimental npm aliases even when an optional package is omitted', () => {
+    const root = fixture()
+    const entry = writePackage(root, '@deepseek-ai/dsh', {
+      optionalDependencies: { safeName: `npm:${experimental}@1.0.0` },
+    })
+
+    expect(() => verifyInstalledProductIsolation(entry)).toThrow(experimental)
+  })
+
+  it('follows nested installations rather than an unrelated hoisted package', () => {
+    const root = fixture()
+    const entry = writePackage(root, '@deepseek-ai/dsh', { dependencies: { core: '1.0.0' } })
+    writePackage(root, 'core', {})
+    writePackage(entry, 'core', { dependencies: { [experimental]: '1.0.0' } })
+
+    expect(() => verifyInstalledProductIsolation(entry)).toThrow(experimental)
+  })
+
+  it('permits missing optional packages and optional peers but rejects missing dependencies', () => {
+    const root = fixture()
+    const entry = writePackage(root, '@deepseek-ai/dsh', {
+      dependencies: { optional: '1.0.0' },
+      optionalDependencies: { optional: '1.0.0' },
+      peerDependencies: { peer: '1.0.0' },
+      peerDependenciesMeta: { peer: { optional: true } },
+    })
+    expect(verifyInstalledProductIsolation(entry)).toBe(1)
+    writePackage(root, '@deepseek-ai/dsh', { dependencies: { missing: '1.0.0' } })
+    expect(() => verifyInstalledProductIsolation(entry)).toThrow('dependency is missing')
+  })
+})

+ 65 - 0
scripts/release/installed-product-isolation.ts

@@ -0,0 +1,65 @@
+/** Experimental-package isolation for the installed default product's dependency graph. */
+
+import { existsSync, readFileSync, realpathSync } from 'node:fs'
+import { createRequire } from 'node:module'
+import { join } from 'node:path'
+
+interface InstalledManifest {
+  name: string
+  dependencies?: Record<string, string>
+  optionalDependencies?: Record<string, string>
+  peerDependencies?: Record<string, string>
+  peerDependenciesMeta?: Record<string, { optional?: boolean }>
+}
+
+/**
+ * Reject experimental dependencies reachable from one installed product entry.
+ * Other packages installed beside the entry and development dependencies do not join its graph.
+ * @param directory - installed entry package directory.
+ * @returns number of distinct installed packages visited.
+ */
+export function verifyInstalledProductIsolation(directory: string): number {
+  const visited = new Set<string>()
+  const queue = [{ directory, chain: [] as string[] }]
+  for (const item of queue) {
+    const canonical = realpathSync(item.directory)
+    if (visited.has(canonical)) continue
+    visited.add(canonical)
+    const manifest = JSON.parse(readFileSync(join(canonical, 'package.json'), 'utf8')) as InstalledManifest
+    const chain = [...item.chain, manifest.name]
+    rejectExperimental(manifest.name, chain)
+    for (const section of ['dependencies', 'optionalDependencies', 'peerDependencies'] as const) {
+      for (const [name, range] of Object.entries(manifest[section] ?? {})) {
+        rejectExperimental(name, [...chain, name])
+        if (range.startsWith('npm:')) {
+          rejectExperimental(range.slice(4), [...chain, `${name} (${range})`])
+        }
+        const dependency = installedPackage(canonical, name)
+        if (dependency === undefined) {
+          if (section === 'optionalDependencies'
+            || Object.hasOwn(manifest.optionalDependencies ?? {}, name)
+            || section === 'peerDependencies' && manifest.peerDependenciesMeta?.[name]?.optional === true) continue
+          throw new Error(`default product dependency is missing: ${[...chain, name].join(' -> ')}`)
+        }
+        queue.push({ directory: dependency, chain })
+      }
+    }
+  }
+  return visited.size
+}
+
+function rejectExperimental(name: string, chain: readonly string[]): void {
+  if (name.startsWith('@deepseek-ai/dsh-experimental-')) {
+    throw new Error(`default product includes an experimental package: ${chain.join(' -> ')}`)
+  }
+}
+
+/** Resolve a dependency directory through the installed package's ancestor node_modules. */
+function installedPackage(from: string, name: string): string | undefined {
+  const resolver = createRequire(join(from, 'package.json'))
+  for (const directory of resolver.resolve.paths(name) ?? []) {
+    const candidate = join(directory, name)
+    if (existsSync(join(candidate, 'package.json'))) return candidate
+  }
+  return undefined
+}

+ 4 - 0
scripts/release/verify-packed-install.ts

@@ -24,6 +24,7 @@ import { parseArgs } from 'node:util'
 import { releaseFamily } from './families.ts'
 import { capture, isEntry } from './process.ts'
 import { packedIdentity } from './tarball.ts'
+import { verifyInstalledProductIsolation } from './installed-product-isolation.ts'
 
 /**
  * Environment for the installed artifact: no host Node hooks, no host DeepSeek
@@ -107,6 +108,9 @@ function main(): void {
     capture('npm', ['install', '--no-audit', '--no-fund', '--package-lock=false', '--omit=optional', '--loglevel=http'],
       { cwd: consumerRoot, env: environment })
 
+    const installedEntry = join(consumerRoot, 'node_modules', entry.packageName)
+    const packageCount = verifyInstalledProductIsolation(installedEntry)
+    console.log(`release verify-packed-install: ${String(packageCount)} default-product packages exclude experimental packages`)
     const bin = join(consumerRoot, 'node_modules', ...entry.packageName.split('/'), entry.binPath)
     const version = capture(process.execPath, [bin, '--version'], { cwd: consumerRoot, env: environment })
     if (version !== expected.version) {

+ 12 - 1
scripts/run-gates.spec.ts

@@ -245,7 +245,7 @@ describe('gate graph validation', () => {
     const ids = withPnpmEntrypoint(() => gatesForMode('hygiene').map(subject => subject.id))
 
     expect(ids).toEqual([
-      'rescope-vendor', 'publint', 'constraints', 'package-dependencies', 'application-entrypoints',
+      'rescope-vendor', 'publint', 'constraints', 'default-product-isolation', 'package-dependencies', 'application-entrypoints',
       'dsh-package-licenses', 'package-invariants', 'built-package-invariants', 'node-next-types',
       'optional-dependency-imports', 'client-packages', 'client-ui-i18n', 'no-bare-dispatcher', 'cordis-config',
       'runtime-closure',
@@ -293,6 +293,17 @@ describe('gate graph validation', () => {
     },
   )
 
+  it.each(['ci-primary', 'ci-static', 'check-all', 'hygiene'] as const)(
+    'executes default-product experimental isolation in %s',
+    (mode) => {
+      const gate = withPnpmEntrypoint(() => gatesForMode(mode)
+        .find(subject => subject.id === 'default-product-isolation'))
+
+      expect(gate?.args).toContain('verify-default-product-isolation')
+      expect(gate?.allowFailure).not.toBe(true)
+    },
+  )
+
   it.each(['ci-primary', 'ci-static', 'check-all'] as const)(
     'keeps the client dependency policy in %s',
     (mode) => {

+ 2 - 0
scripts/run-gates.ts

@@ -297,6 +297,7 @@ export function gatesForMode(selected: Mode): Gate[] {
 function ciSharedStaticGates(): Gate[] {
   return [
     pnpmScript('runtime-closure', 'verify-runtime-closure', { label: 'runtime closure' }),
+    pnpmScript('default-product-isolation', 'verify-default-product-isolation', { label: 'default product isolation' }),
     pnpmScript('application-entrypoints', 'verify-application-entrypoints', { label: 'application entrypoints' }),
     pnpmScript('constraints', 'constraints'),
     pnpmScript('package-dependencies', 'verify-package-dependencies', { label: 'package dependencies' }),
@@ -693,6 +694,7 @@ function hygieneLeafGates(options: { artifactNeeds?: string[] } = {}): Gate[] {
     pnpmScript('rescope-vendor', 'rescope-vendor:check', { label: 'vendor rescope' }),
     pnpmScript('publint', 'publint', artifactOptions),
     pnpmScript('constraints', 'constraints'),
+    pnpmScript('default-product-isolation', 'verify-default-product-isolation', { label: 'default product isolation' }),
     pnpmScript('package-dependencies', 'verify-package-dependencies', { label: 'package dependencies' }),
     pnpmScript('application-entrypoints', 'verify-application-entrypoints', { label: 'application entrypoints' }),
     pnpmScript('dsh-package-licenses', 'verify-dsh-package-licenses', { label: 'DSH package licenses' }),

+ 3 - 3
scripts/verify-application-entrypoints.ts

@@ -23,7 +23,7 @@ interface DemoPolicy {
   readonly wrapper?: string
 }
 
-/** Public product launcher plus the private build-only WebWorker packer. */
+/** Public product launcher plus the build-only WebWorker packer. */
 const MANIFEST_BIN_ALLOWLIST = new Map<string, ManifestBin>([
   ['apps/cli/package.json', { dsh: 'lib/bin.js' }],
   ['packages/experimental/webworker-packer/package.json', { 'dsh-pack-vfs-image': './bin.js' }],
@@ -33,8 +33,8 @@ const MANIFEST_BIN_ALLOWLIST = new Map<string, ManifestBin>([
 const EXECUTABLE_SOURCE_ALLOWLIST = new Map<string, string>([
   ['apps/cli/src/bin.ts', 'supported dsh application launcher'],
   ['packages/context/time-context/tests/fixtures/driver.ts', 'test-only subprocess driver'],
-  ['packages/experimental/webworker-packer/bin.js', 'private build-only wrapper'],
-  ['packages/experimental/webworker-packer/src/bin.ts', 'private build-only implementation'],
+  ['packages/experimental/webworker-packer/bin.js', 'build-only wrapper'],
+  ['packages/experimental/webworker-packer/src/bin.ts', 'build-only implementation'],
   ['packages/sdk/client/tests/fake-runtime.ts', 'test-only SDK runtime peer'],
   ['packages/session/session-telemetry-otel/tests/fixtures/driver.ts', 'test-only subprocess driver'],
   ['packages/shell/tool-pwsh/tests/fixtures/loader/driver.ts', 'test-only subprocess driver'],

+ 19 - 0
scripts/verify-client-packages.spec.ts

@@ -7,6 +7,7 @@ import { afterEach, describe, expect, it } from 'vitest'
 import {
   collectClientPackageViolations,
   collectLocalSourceSpecifiers,
+  collectRuntimeLocalSourceSpecifiers,
   collectRuntimeSourcePackageUses,
   collectRuntimeSourceSpecifiers,
   collectSourcePackageUses,
@@ -75,6 +76,24 @@ function facts(
 }
 
 describe('source package uses', () => {
+  it('includes root-relative runtime requests only when explicitly requested', () => {
+    const source = [
+      "import './local.ts'",
+      "import '/src/value.ts'",
+      "export * from '/src/export.ts'",
+      "const load = () => import('/src/lazy.ts')",
+      "const legacy = require('/src/legacy.ts')",
+      "import type { A } from '/src/import-type.ts'",
+      "import { type B } from '/src/import-type-only.ts'",
+      "export type { A } from '/src/export-type.ts'",
+      "export { type B } from '/src/export-type-only.ts'",
+    ].join('\n')
+    expect([...collectRuntimeLocalSourceSpecifiers('feature.ts', source)]).toEqual(['./local.ts'])
+    expect([...collectRuntimeLocalSourceSpecifiers('feature.ts', source, true)]).toEqual([
+      './local.ts', '/src/value.ts', '/src/export.ts', '/src/lazy.ts', '/src/legacy.ts',
+    ])
+  })
+
   it('counts type imports, module augmentations, dynamic imports, and JSX', () => {
     const uses = collectSourcePackageUses('feature.tsx', [
       "import type { A } from '@deepseek-ai/dsh-a/subpath'",

+ 7 - 5
scripts/verify-client-packages.ts

@@ -101,14 +101,15 @@ export function collectLocalSourceSpecifiers(path: string, source: string): Set<
 }
 
 /**
- * Collect relative module specifiers retained by one production source file.
+ * Collect local module specifiers retained by one production source file.
  * @param path - File path used to select TypeScript's parser mode.
  * @param source - Source text to inspect.
- * @returns Relative imports, exports, and requires that survive compilation.
+ * @param includeRootRelative - Include absolute paths, such as Vite's Web-root imports.
+ * @returns Local imports, exports, and requires that survive compilation.
  */
-export function collectRuntimeLocalSourceSpecifiers(path: string, source: string): Set<string> {
+export function collectRuntimeLocalSourceSpecifiers(path: string, source: string, includeRootRelative = false): Set<string> {
   const sourceFile = ts.createSourceFile(path, source, ts.ScriptTarget.Latest, true)
-  return collectSourceFileUses(sourceFile, true, 'local')
+  return collectSourceFileUses(sourceFile, true, 'local', includeRootRelative)
 }
 
 function importCarriesRuntimeValue(node: ts.ImportDeclaration): boolean {
@@ -134,13 +135,14 @@ function collectSourceFileUses(
   sourceFile: ts.SourceFile,
   runtimeOnly: boolean,
   key: 'local' | 'package' | 'specifier',
+  includeRootRelative = false,
 ): Set<string> {
   const uses = new Set<string>()
 
   const add = (specifier: ts.Expression | undefined): void => {
     if (specifier === undefined || !ts.isStringLiteralLike(specifier)) return
     if (key === 'local') {
-      if (specifier.text.startsWith('.')) uses.add(specifier.text)
+      if (specifier.text.startsWith('.') || includeRootRelative && specifier.text.startsWith('/')) uses.add(specifier.text)
       return
     }
     if (!isBareSpecifier(specifier.text)) return

+ 373 - 0
scripts/verify-default-product-isolation.spec.ts

@@ -0,0 +1,373 @@
+/** Source, installation, and composition regressions for default-product isolation. */
+
+import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
+import { tmpdir } from 'node:os'
+import { dirname, join } from 'node:path'
+import { pathToFileURL } from 'node:url'
+import { afterEach, describe, expect, it } from 'vitest'
+import { verifyDefaultProductIsolation } from './verify-default-product-isolation.ts'
+
+const roots: string[] = []
+const experimental = '@deepseek-ai/dsh-experimental-prototype'
+const core = '@deepseek-ai/dsh-core'
+const base = '@deepseek-ai/dsh-base'
+const profile = 'packages/boot/app-boot/src/profile.ts'
+const preset = 'packages/preset/agent-presets/presets/standard/agent.cordis.yml'
+const patch = 'packages/bundle/base/cordis.patch.yml'
+
+function write(root: string, path: string, value: unknown): void {
+  const target = join(root, path)
+  mkdirSync(dirname(target), { recursive: true })
+  writeFileSync(target, typeof value === 'string' ? value : `${JSON.stringify(value)}\n`)
+}
+
+function manifest(root: string, path: string, fields: Record<string, unknown>): void {
+  const existing = JSON.parse(readFileSync(join(root, path), 'utf8')) as Record<string, unknown>
+  write(root, path, { ...existing, ...fields })
+}
+
+function fixture(): string {
+  const root = mkdtempSync(join(tmpdir(), 'dsh.default-isolation-'))
+  roots.push(root)
+  write(root, 'apps/cli/package.json', { name: '@deepseek-ai/dsh', dependencies: { [core]: 'workspace:^' } })
+  write(root, 'apps/cli/src/bin.ts', 'export {}\n')
+  write(root, 'apps/web/package.json', { name: '@deepseek-ai/dsh-web-frontend' })
+  write(root, 'apps/web/index.html', '<script type="module" src="/src/main.ts"></script>')
+  write(root, 'apps/web/src/main.ts', 'export {}\n')
+  write(root, 'python/sdk-runtime/package.json', { name: '@deepseek-ai/dsh-python-runtime' })
+  write(root, 'packages/core/core/package.json', { name: core })
+  write(root, 'packages/core/core/src/index.ts', 'export {}\n')
+  write(root, 'packages/bundle/base/package.json', { name: base, dsh: { bundle: { patch: './cordis.patch.yml' } } })
+  write(root, patch, [{ insert: [{ name: core }] }])
+  write(root, preset, [{ name: core }])
+  write(root, profile, `export const PROFILE_TEMPLATES = { web: { bundles: ['${base}'], patchReload: 'live' } }\n`
+    + `export const DEFAULT_PROFILE_BUNDLES = ['${base}']\n`)
+  write(root, 'packages/experimental/prototype/package.json', { name: experimental })
+  return root
+}
+
+afterEach(() => {
+  for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true })
+})
+
+describe('default product isolation', () => {
+  it('allows development dependencies, type imports, and separate experimental preview entries', () => {
+    const root = fixture()
+    manifest(root, 'apps/cli/package.json', { devDependencies: { [experimental]: 'workspace:^' } })
+    write(root, 'apps/cli/src/bin.ts', `import type { Options } from '${experimental}'\nexport type { Options }\n`)
+    write(root, 'apps/web/src/preview.ts', `import '${experimental}'\n`)
+    write(root, 'packages/experimental/prototype/cordis.patch.yml', [{ name: experimental }])
+
+    expect(verifyDefaultProductIsolation(root)).toMatchObject({ failures: [], packageCount: 5, configCount: 2 })
+  })
+
+  it.each(['dependencies', 'optionalDependencies', 'peerDependencies'])(
+    'rejects transitive experimental %s',
+    (section) => {
+      const root = fixture()
+      manifest(root, 'packages/core/core/package.json', { [section]: { [experimental]: 'workspace:^' } })
+
+      expect(verifyDefaultProductIsolation(root).failures.join('\n')).toContain(`${core} ${section} -> ${experimental}`)
+    },
+  )
+
+  it.each([
+    `npm:${experimental}@1.0.0`, `workspace:${experimental}@*`,
+    'file:../../experimental/prototype', 'link:../../experimental/prototype',
+    'workspace:../../experimental/prototype',
+  ])('rejects a safe-looking dependency alias targeting %s', (range) => {
+    const root = fixture()
+    manifest(root, 'packages/core/core/package.json', { dependencies: { safe: range } })
+
+    expect(verifyDefaultProductIsolation(root).failures.join('\n')).toContain(experimental)
+  })
+
+  it('rejects experimental names absent from the inventory and paths with another package name', () => {
+    const root = fixture()
+    manifest(root, 'apps/cli/package.json', { dependencies: { '@deepseek-ai/dsh-experimental-missing': '*' } })
+    manifest(root, 'packages/experimental/prototype/package.json', { name: '@fixture/innocent' })
+    manifest(root, 'python/sdk-runtime/package.json', { dependencies: { '@fixture/innocent': '*' } })
+
+    const failures = verifyDefaultProductIsolation(root).failures.join('\n')
+    expect(failures).toContain('@deepseek-ai/dsh-experimental-missing')
+    expect(failures).toContain('@fixture/innocent')
+  })
+
+  it('follows private application intermediaries and terminates cycles', () => {
+    const root = fixture()
+    write(root, 'apps/desktop/package.json', { name: '@fixture/desktop', private: true,
+      dependencies: { [core]: '*', [experimental]: '*' } })
+    manifest(root, 'packages/core/core/package.json', { peerDependencies: { '@fixture/desktop': '*' } })
+
+    expect(verifyDefaultProductIsolation(root).failures.join('\n')).toContain(experimental)
+  })
+
+  it.each([
+    `import '${experimental}'`,
+    `export * from '${experimental}'`,
+    `await import('${experimental}/client')`,
+    `const x = require('${experimental}')`,
+    'import \'./../../../packages/experimental/prototype/src/index.ts\'',
+  ])('rejects runtime source reference %s', (source) => {
+    const root = fixture()
+    write(root, 'apps/cli/src/bin.ts', source)
+    write(root, 'packages/experimental/prototype/src/index.ts', 'export {}\n')
+
+    expect(verifyDefaultProductIsolation(root).failures.join('\n')).toContain(experimental)
+  })
+
+  it.each([
+    "import './preview.ts'", "import './preview.ts?worker'", "new Worker(new URL('./preview.ts', import.meta.url))",
+  ])('rejects default Web entry reaching preview via %s', (source) => {
+    const root = fixture()
+    write(root, 'apps/web/src/main.ts', source)
+    write(root, 'apps/web/src/preview.ts', `import '${experimental}'\n`)
+
+    expect(verifyDefaultProductIsolation(root).failures.join('\n')).toContain(experimental)
+  })
+
+  it.each([
+    "import '/src/preview.ts'",
+    "export * from '/src/preview.ts'",
+    "await import('/src/preview.ts')",
+    "new Worker(new URL('/src/preview.ts', import.meta.url))",
+  ])('follows Web-root runtime reference %s', (source) => {
+    const root = fixture()
+    write(root, 'apps/web/src/main.ts', source)
+    write(root, 'apps/web/src/preview.ts', `import '${experimental}'\n`)
+
+    expect(verifyDefaultProductIsolation(root).failures.join('\n')).toContain(experimental)
+  })
+
+  it('does not follow type-only Web-root references', () => {
+    const root = fixture()
+    write(root, 'apps/web/src/main.ts', "import type { Value } from '/src/preview.ts'\nexport type { Value } from '/src/preview.ts'\n")
+    write(root, 'apps/web/src/preview.ts', `import '${experimental}'\n`)
+
+    expect(verifyDefaultProductIsolation(root).failures).toEqual([])
+  })
+
+  it.each(['.js', '.jsx', '.mjs', '.ts', '.tsx', '.mts'])('follows extensionless Web imports to %s in a dotted checkout', (extension) => {
+    const root = fixture()
+    write(root, 'apps/web/src/main.ts', "import './preview'\n")
+    write(root, `apps/web/src/preview${extension}`, `import '${experimental}'\n`)
+
+    expect(verifyDefaultProductIsolation(root).failures.join('\n')).toContain(experimental)
+  })
+
+  it.each(['.js', '.jsx', '.mjs'])('follows Web directory imports to index%s', (extension) => {
+    const root = fixture()
+    write(root, 'apps/web/src/main.ts', "import './preview'\n")
+    write(root, `apps/web/src/preview/index${extension}`, `import '${experimental}'\n`)
+
+    expect(verifyDefaultProductIsolation(root).failures.join('\n')).toContain(experimental)
+  })
+
+  it.each([['.mjs', '.mts'], ['.cjs', '.cts']])('follows %s runtime requests to their %s source', (request, source) => {
+    const root = fixture()
+    write(root, 'apps/web/src/main.ts', `import './preview${request}'\n`)
+    write(root, `apps/web/src/preview${source}`, `import '${experimental}'\n`)
+
+    expect(verifyDefaultProductIsolation(root).failures.join('\n')).toContain(experimental)
+  })
+
+  it('follows a Web directory import whose directory name contains a dot', () => {
+    const root = fixture()
+    write(root, 'apps/web/src/main.ts', "import './preview.v1'\n")
+    write(root, 'apps/web/src/preview.v1/index.js', `import '${experimental}'\n`)
+
+    expect(verifyDefaultProductIsolation(root).failures.join('\n')).toContain(experimental)
+  })
+
+  it('identifies a relative package-directory import using its exact directory owner', () => {
+    const root = fixture()
+    write(root, 'apps/web/src/main.ts', "import '../../../packages/experimental/prototype'\n")
+    manifest(root, 'packages/experimental/prototype/package.json', { main: 'src/entry.js' })
+    write(root, 'packages/experimental/prototype/src/entry.js', 'export {}\n')
+
+    expect(verifyDefaultProductIsolation(root).failures.join('\n')).toContain(experimental)
+  })
+
+  it.each([
+    [{ name: experimental, disabled: true }],
+    [{ group: true, config: [{ name: experimental }] }],
+    [{ insert: [{ name: experimental }] }],
+    [{ name: '@deepseek-ai/cordis-plugin-group', config: [{ name: experimental }] }],
+    [{ name: '@deepseek-ai/cordis-plugin-include', config: { patches: [{ insert: [{ name: experimental }] }] } }],
+  ].map(entries => ({ entries })))('rejects experimental plugin rows in $entries', ({ entries }) => {
+    const root = fixture()
+    write(root, patch, entries)
+
+    expect(verifyDefaultProductIsolation(root).failures.join('\n')).toContain(experimental)
+  })
+
+  it('follows Include files while ignoring ordinary plugin config data', () => {
+    const root = fixture()
+    write(root, patch, [{ insert: [{ name: core, config: { name: experimental, insert: [{ name: experimental }] } }] }])
+    expect(verifyDefaultProductIsolation(root).failures).toEqual([])
+    write(root, patch, [{ name: '@deepseek-ai/cordis-plugin-include', config: { path: './nested.yml' } }])
+    write(root, 'packages/bundle/base/nested.yml', [{ name: experimental }])
+    expect(verifyDefaultProductIsolation(root).failures.join('\n')).toContain(experimental)
+  })
+
+  it.each([false, true])('checks Include.initial when its file exists: %s', (existing) => {
+    const root = fixture()
+    const included = join(root, 'included.yml')
+    if (existing) write(root, 'included.yml', [{ name: core }])
+    write(root, patch, [{ insert: [{ name: 'cordis:include', config: {
+      path: pathToFileURL(included).href,
+      initial: [{ name: experimental }],
+    } }] }])
+
+    expect(verifyDefaultProductIsolation(root).failures.join('\n')).toContain(experimental)
+  })
+
+  it('accepts a missing Include file initialized with stable entries and applies its patches', () => {
+    const root = fixture()
+    write(root, patch, [{ insert: [{ name: 'cordis:include', config: {
+      path: pathToFileURL(join(root, 'included.yml')).href,
+      initial: [{ id: 'feature', group: true, config: [{ name: core }] }],
+    } }] }])
+    expect(verifyDefaultProductIsolation(root).failures).toEqual([])
+    write(root, patch, [{ insert: [{ name: 'cordis:include', config: {
+      path: pathToFileURL(join(root, 'included.yml')).href,
+      initial: [{ id: 'feature', group: true, config: [{ name: core }] }],
+      patches: [{ id: 'feature', config: [{ name: experimental }] }],
+    } }] }])
+    expect(verifyDefaultProductIsolation(root).failures.join('\n')).toContain(experimental)
+  })
+
+  it('rejects an Include.initial tree that includes its own generated file', () => {
+    const root = fixture()
+    const path = pathToFileURL(join(root, 'included.yml')).href
+    write(root, patch, [{ insert: [{ name: 'cordis:include', config: {
+      path, initial: [{ name: 'cordis:include', config: { path } }],
+    } }] }])
+
+    expect(verifyDefaultProductIsolation(root).failures.join('\n')).toContain('cyclic Include path included.yml')
+  })
+
+  it('checks default profile bundle names and declared config trees', () => {
+    const root = fixture()
+    write(root, profile, `export const PROFILE_TEMPLATES = { web: { bundles: ['${experimental}'] } }\n`
+      + `export const DEFAULT_PROFILE_BUNDLES = ['${base}']\n`)
+    manifest(root, 'apps/cli/package.json', { dsh: { configTrees: [{ path: './config' }] } })
+    write(root, 'apps/cli/config/extra.cordis.yml', [{ name: experimental }])
+    const failures = verifyDefaultProductIsolation(root).failures.join('\n')
+    expect(failures).toContain(`${profile} -> ${experimental}`)
+    expect(failures).toContain('apps/cli/config/extra.cordis.yml')
+  })
+
+  it.each([false, true])('rejects a declared config tree without composition files when its directory exists: %s', (existing) => {
+    const root = fixture()
+    manifest(root, 'apps/cli/package.json', { dsh: { configTrees: [{ path: './config' }] } })
+    if (existing) write(root, 'apps/cli/config/README.i18n.yaml', 'en: test\n')
+
+    expect(verifyDefaultProductIsolation(root).failures.join('\n')).toContain('apps/cli/config')
+  })
+
+  it('does not classify ordinary YAML as a Cordis config because of an ancestor directory name', () => {
+    const root = fixture()
+    write(root, 'packages/core/core/src/cordis/index.ts', "new URL('./ordinary.yml', import.meta.url)\n")
+    write(root, 'packages/core/core/src/cordis/ordinary.yml', 'ordinary: data\n')
+
+    expect(verifyDefaultProductIsolation(root).failures).toEqual([])
+  })
+
+  it('checks desktop configuration reached through a source URL', () => {
+    const root = fixture()
+    write(root, 'apps/desktop-host/package.json', { name: '@fixture/desktop-host', private: true })
+    write(root, 'apps/desktop-host/src/index.ts', "new URL('../config/desktop.cordis.patch.yml', import.meta.url)")
+    write(root, 'apps/desktop-host/config/desktop.cordis.patch.yml', [{ insert: [{ name: experimental }] }])
+
+    expect(verifyDefaultProductIsolation(root).failures.join('\n')).toContain(experimental)
+  })
+
+  it('checks group contents after an id-only patch changes the composed Web tree', () => {
+    const root = fixture()
+    const web = '@deepseek-ai/dsh-web-app'
+    write(root, 'packages/bundle/web-app/package.json', { name: web, dsh: { bundle: { patch: './cordis.patch.yml' } } })
+    write(root, patch, [{ insert: [{ id: 'feature-group', group: true, config: [{ name: core }] }] }])
+    write(root, 'packages/bundle/web-app/cordis.patch.yml', [
+      { id: 'feature-group', config: [{ name: experimental }] },
+    ])
+    write(root, profile, `export const PROFILE_TEMPLATES = { web: { bundles: ['${base}', '${web}'] } }\n`
+      + `export const DEFAULT_PROFILE_BUNDLES = ['${base}']\n`)
+
+    expect(verifyDefaultProductIsolation(root).failures.join('\n')).toContain(`${profile} -> ${experimental}`)
+  })
+
+  it('composes Include patches before checking their effective plugin rows', () => {
+    const root = fixture()
+    const included = join(root, 'included tree', 'cordis.yml')
+    write(root, 'included tree/cordis.yml', [{ id: 'feature-group', group: true, config: [{ name: core }] }])
+    write(root, patch, [{ insert: [{ name: 'cordis:include', config: {
+      path: pathToFileURL(included).href,
+      patches: [{ id: 'feature-group', config: [{ name: experimental }] }],
+    } }] }])
+
+    expect(verifyDefaultProductIsolation(root).failures.join('\n')).toContain(`included tree/cordis.yml -> ${experimental}`)
+  })
+
+  it('checks the replacement Include path after an id-only Web patch', () => {
+    const root = fixture()
+    write(root, 'original.yml', [{ name: core }])
+    write(root, 'replacement.yml', [{ name: experimental }])
+    write(root, patch, [
+      { insert: [{ id: 'feature-include', name: 'cordis:include', config: { path: pathToFileURL(join(root, 'original.yml')).href } }] },
+      { id: 'feature-include', config: { path: pathToFileURL(join(root, 'replacement.yml')).href } },
+    ])
+
+    expect(verifyDefaultProductIsolation(root).failures.join('\n')).toContain(`replacement.yml -> ${experimental}`)
+  })
+
+  it('reports an empty effective Web tree even when the raw patch names a package', () => {
+    const root = fixture()
+    write(root, patch, [{ id: 'missing-target', name: core }])
+
+    expect(verifyDefaultProductIsolation(root).failures).toContain(`${profile}: composed Web profile contains no plugins`)
+  })
+
+  it('rejects a file-URL plugin name anchored by the real patch loader', () => {
+    const root = fixture()
+    write(root, patch, [{ insert: [{ name: '../../experimental/prototype/src/index.ts' }] }])
+    write(root, 'packages/experimental/prototype/src/index.ts', 'export {}\n')
+
+    expect(verifyDefaultProductIsolation(root).failures.join('\n')).toContain(`${profile} -> ${experimental}`)
+  })
+
+  it('checks installation-owned profile tuples as well as new-profile defaults', () => {
+    const root = fixture()
+    const source = readFileSync(join(root, profile), 'utf8')
+    write(root, profile, `${source}\nconst INSTALLATION_OWNED_PROFILE_TUPLES = { headless: ['${experimental}'] }\n`)
+
+    expect(verifyDefaultProductIsolation(root).failures.join('\n')).toContain(experimental)
+  })
+
+  it('checks inline modules alongside the default Web script', () => {
+    const root = fixture()
+    write(root, 'apps/web/index.html', '<script type="module" src="/src/main.ts"></script>'
+      + `<script type="module">import '${experimental}'</script>`)
+
+    expect(verifyDefaultProductIsolation(root).failures.join('\n')).toContain(experimental)
+  })
+
+  it.each(['apps/cli/package.json', 'apps/web/index.html', 'python/sdk-runtime/package.json', preset])(
+    'fails closed when required input %s is missing',
+    (path) => {
+      const root = fixture()
+      rmSync(join(root, path))
+      expect(verifyDefaultProductIsolation(root).failures.length).toBeGreaterThan(0)
+    },
+  )
+
+  it.each([
+    '',
+    'export const PROFILE_TEMPLATES = {}; export const DEFAULT_PROFILE_BUNDLES = []',
+    'export const PROFILE_TEMPLATES = computedProfiles(); export const DEFAULT_PROFILE_BUNDLES = []',
+  ])('rejects absent, empty, or uninspectable default profile definitions', (source) => {
+    const root = fixture()
+    write(root, profile, source)
+    expect(() => verifyDefaultProductIsolation(root)).toThrow('profile')
+  })
+})

+ 372 - 0
scripts/verify-default-product-isolation.ts

@@ -0,0 +1,372 @@
+/** Keep experimental packages outside default installations, runtime imports, and shipped compositions. */
+
+import { existsSync, globSync, readFileSync, statSync } from 'node:fs'
+import { basename, dirname, extname, relative, resolve } from 'node:path'
+import { fileURLToPath } from 'node:url'
+import { JSDOM } from 'jsdom'
+import ts from 'typescript'
+import { applyEntryPatches, type PatchOptions } from '@deepseek-ai/cordis-plugin-include'
+import type { EntryOptions } from '@deepseek-ai/cordis-plugin-loader'
+import { loadOverlayPatches } from '../packages/boot/app-boot/src/index.ts'
+import { composeEntries } from '../packages/boot/app-boot/src/profile.ts'
+import { isCordisGroupEntry, loadCordisYaml } from './cordis-yaml.ts'
+import {
+  collectRuntimeLocalSourceSpecifiers,
+  collectRuntimeSourceSpecifiers,
+} from './verify-client-packages.ts'
+
+const EXPERIMENTAL_PREFIX = '@deepseek-ai/dsh-experimental-'
+const PROFILE_SOURCE = 'packages/boot/app-boot/src/profile.ts'
+const PRESET_PATTERN = 'packages/preset/agent-presets/presets/*/agent.cordis.yml'
+const RUNTIME_SECTIONS = ['dependencies', 'optionalDependencies', 'peerDependencies'] as const
+
+interface Manifest {
+  name: string
+  dependencies?: Record<string, string>
+  optionalDependencies?: Record<string, string>
+  peerDependencies?: Record<string, string>
+  devDependencies?: Record<string, string>
+  dsh?: { bundle?: { patch?: string }; configTrees?: Array<{ path: string }> }
+}
+
+interface Package {
+  directory: string
+  manifest: Manifest
+}
+
+/** Counts and violations from the default product's source and configuration inputs. */
+export interface ProductIsolationResult {
+  failures: string[]
+  packageCount: number
+  sourceCount: number
+  configCount: number
+  webPluginCount: number
+}
+
+/**
+ * Check default app installations and their authored runtime/configuration inputs.
+ * Experimental opt-in packages and separate Web preview entries are outside these roots.
+ * @param root - repository root; no build outputs or installed workspace links are read.
+ * @returns violations and the sizes of the checked package, source, and configuration sets.
+ */
+export function verifyDefaultProductIsolation(root: string): ProductIsolationResult {
+  const failures: string[] = []
+  const packages = new Map<string, Package>()
+  const directories = new Map<string, Package>()
+  const display = (path: string): string => relative(root, path).replaceAll('\\', '/')
+  for (const path of globSync([
+    'apps/*/package.json', 'packages/*/*/package.json', 'vendor/*/package.json',
+    'native/system/packages/*/package.json', 'python/sdk-runtime/package.json',
+  ], { cwd: root }).sort()) {
+    const manifest = JSON.parse(readFileSync(resolve(root, path), 'utf8')) as Manifest
+    if (typeof manifest.name !== 'string' || manifest.name === '') throw new Error(`${path}: missing package name`)
+    if (packages.has(manifest.name)) throw new Error(`${path}: duplicate package name ${manifest.name}`)
+    const pkg = { directory: dirname(resolve(root, path)), manifest }
+    packages.set(manifest.name, pkg)
+    directories.set(pkg.directory, pkg)
+  }
+  for (const path of ['apps/cli/package.json', 'apps/web/package.json', 'python/sdk-runtime/package.json']) {
+    if (!existsSync(resolve(root, path))) failures.push(`missing default product root ${path}`)
+  }
+  if (directories.get(resolve(root, 'apps/cli'))?.manifest.name !== '@deepseek-ai/dsh') {
+    failures.push('apps/cli/package.json must identify @deepseek-ai/dsh')
+  }
+
+  const queue: Package[] = []
+  const visited = new Set<string>()
+  const sources = new Set<string>()
+  const configs = new Set<string>()
+  let webPluginCount = 0
+  const isExperimental = (pkg: Package): boolean => pkg.manifest.name.startsWith(EXPERIMENTAL_PREFIX)
+    || display(pkg.directory).startsWith('packages/experimental/')
+  const add = (pkg: Package, origin: string): void => {
+    if (isExperimental(pkg)) {
+      failures.push(`${origin} -> ${pkg.manifest.name}: default product must not include experimental packages`)
+    } else if (!visited.has(pkg.directory)) {
+      visited.add(pkg.directory)
+      queue.push(pkg)
+    }
+  }
+  const ownerOf = (path: string): Package | undefined => {
+    let directory = dirname(path)
+    for (;;) {
+      const pkg = directories.get(directory)
+      if (pkg !== undefined) return pkg
+      const parent = dirname(directory)
+      if (parent === directory) return undefined
+      directory = parent
+    }
+  }
+  const reference = (name: string, origin: string, owner?: Package): void => {
+    if (name.startsWith('.') || name.startsWith('/') || /^(?:file|link):/.test(name)) {
+      const target = name.startsWith('file://') ? fileURLToPath(name)
+        : resolve(owner?.directory ?? root, name.replace(/^(?:file|link):/, ''))
+      const pkg = directories.get(target) ?? ownerOf(target)
+      if (pkg !== undefined) add(pkg, origin)
+      return
+    }
+    const packageName = barePackageName(name)
+    if (packageName.startsWith(EXPERIMENTAL_PREFIX)) {
+      failures.push(`${origin} -> ${name}: default product must not include experimental packages`)
+      return
+    }
+    const pkg = packages.get(packageName)
+    if (pkg !== undefined) add(pkg, origin)
+    else if (packageName.startsWith('@deepseek-ai/')) failures.push(`${origin}: unknown workspace package ${name}`)
+  }
+  const dependency = (name: string, range: string, owner: Package, origin: string): void => {
+    reference(name, origin, owner)
+    if (/^(?:file:|link:|workspace:\.)/.test(range)) {
+      reference(range.replace(/^workspace:/, ''), origin, owner)
+    } else if (/^(?:npm:|workspace:)(?:@|[a-zA-Z])/.test(range)) {
+      reference(range.replace(/^(?:npm:|workspace:)/, ''), origin, owner)
+    }
+  }
+  const sourceReference = (specifier: string, path: string): void => {
+    const owner = ownerOf(path)
+    if (owner === undefined) return
+    reference(specifier, display(path), owner)
+    const name = barePackageName(specifier)
+    const range = [...RUNTIME_SECTIONS, 'devDependencies' as const]
+      .map(section => owner.manifest[section]?.[name]).find(value => value !== undefined)
+    if (range !== undefined) dependency(name, range, owner, display(path))
+  }
+  const scanSource = (path: string, followLocal = false, inlineSource?: string): void => {
+    if (sources.has(path)) return
+    sources.add(path)
+    const source = inlineSource ?? readFileSync(path, 'utf8')
+    for (const specifier of collectRuntimeSourceSpecifiers(path, source)) sourceReference(specifier, path)
+    for (const specifier of runtimeLocalSpecifiers(path, source)) {
+      const local = followLocal && specifier.startsWith('/')
+        ? resolve(root, 'apps/web', `.${specifier.replace(/[?#].*$/, '')}`)
+        : resolve(dirname(path), specifier.replace(/[?#].*$/, ''))
+      const target = resolveSource(local)
+      const resolved = target ?? local
+      const owner = directories.get(resolved) ?? ownerOf(resolved)
+      if (owner !== undefined && isExperimental(owner)) add(owner, display(path))
+      if (/cordis[^/]*\.ya?ml$/.test(basename(resolved))) scanConfig(resolved)
+      if (followLocal && target !== undefined) scanSource(target, true)
+    }
+  }
+  const scanEntries = (
+    document: unknown[], path: string, composedWeb = false, includeStack: ReadonlySet<string> = new Set(),
+  ): void => {
+    const visit = (entry: unknown): void => {
+      if (!isRecord(entry)) return
+      if (typeof entry.name === 'string') {
+        if (composedWeb) webPluginCount += 1
+        const owner = ownerOf(path)
+        if (entry.name.startsWith('.')) {
+          const target = resolve(dirname(path), entry.name)
+          const targetOwner = ownerOf(target)
+          if (targetOwner !== undefined) add(targetOwner, display(path))
+        } else reference(entry.name, display(path), owner)
+      }
+      if (isCordisGroupEntry(entry) || entry.name === 'cordis:group' && Array.isArray(entry.config)) {
+        (entry.config as unknown[]).forEach(visit)
+      }
+      if (Array.isArray(entry.insert)) entry.insert.forEach(visit)
+      if ((entry.name === '@deepseek-ai/cordis-plugin-include' || entry.name === 'cordis:include') && isRecord(entry.config)) {
+        if (!composedWeb && Array.isArray(entry.config.patches)) entry.config.patches.forEach(visit)
+        const included = entry.config.path
+        if (typeof included !== 'string') return
+        const filename = included.startsWith('file:') ? fileURLToPath(included) : resolve(dirname(path), included)
+        if (includeStack.has(filename)) {
+          failures.push(`${display(path)}: cyclic Include path ${display(filename)}`)
+          return
+        }
+        const nestedStack = new Set([...includeStack, filename])
+        const initial = Array.isArray(entry.config.initial) ? entry.config.initial : undefined
+        if (initial !== undefined) scanEntries(initial, filename, false, nestedStack)
+        if (!composedWeb) {
+          if (existsSync(filename) || initial === undefined) scanConfig(filename)
+          return
+        }
+        const content = existsSync(filename) ? loadCordisYaml(readFileSync(filename, 'utf8')) : initial
+        if (!Array.isArray(content)) {
+          failures.push(`${display(filename)}: included composition must contain an entry array`)
+          return
+        }
+        const entries = applyEntryPatches(content as EntryOptions[], entry.config.patches as PatchOptions[] | undefined, () => {})
+        scanEntries(entries, filename, true, nestedStack)
+      }
+    }
+    document.forEach(visit)
+  }
+  const scanConfig = (path: string): void => {
+    if (configs.has(path)) return
+    configs.add(path)
+    const document = loadCordisYaml(readFileSync(path, 'utf8'))
+    if (!Array.isArray(document)) {
+      failures.push(`${display(path)}: shipped composition must contain an entry array`)
+      return
+    }
+    scanEntries(document, path)
+  }
+
+  for (const pkg of packages.values()) {
+    const path = display(pkg.directory)
+    if (path.startsWith('apps/') || path === 'python/sdk-runtime') add(pkg, path)
+  }
+  const profilePath = resolve(root, PROFILE_SOURCE)
+  if (existsSync(profilePath)) {
+    const selection = profilePackages(readFileSync(profilePath, 'utf8'))
+    for (const name of selection.packages) {
+      reference(name, PROFILE_SOURCE)
+      if (packages.get(name)?.manifest.dsh?.bundle?.patch === undefined) {
+        failures.push(`${PROFILE_SOURCE}: default bundle ${name} must declare dsh.bundle.patch`)
+      }
+    }
+    const webLayers = selection.webBundles.flatMap((name) => {
+      const pkg = packages.get(name)
+      const patch = pkg?.manifest.dsh?.bundle?.patch
+      if (pkg === undefined || patch === undefined) return []
+      return [loadOverlayPatches('verify-default-product-isolation', resolve(pkg.directory, patch))]
+    })
+    if (webLayers.length !== selection.webBundles.length) {
+      failures.push(`${PROFILE_SOURCE}: default Web bundle layers are incomplete`)
+    } else {
+      const entries = composeEntries(webLayers)
+      scanEntries(entries, profilePath, true)
+      if (webPluginCount === 0) failures.push(`${PROFILE_SOURCE}: composed Web profile contains no plugins`)
+    }
+  } else failures.push(`missing default profile source ${PROFILE_SOURCE}`)
+  const presets = globSync(PRESET_PATTERN, { cwd: root }).sort()
+  if (presets.length === 0) failures.push(`no shipped presets matched ${PRESET_PATTERN}`)
+  for (const path of presets) scanConfig(resolve(root, path))
+
+  const html = resolve(root, 'apps/web/index.html')
+  if (existsSync(html)) {
+    const dom = new JSDOM(readFileSync(html, 'utf8'))
+    try {
+      const entries = [...dom.window.document.querySelectorAll('script[type="module"]')]
+      if (entries.length === 0) failures.push('apps/web/index.html: missing default module entry')
+      for (const [index, entry] of entries.entries()) {
+        const src = entry.getAttribute('src')
+        if (src === null) {
+          scanSource(`${html}.inline-${String(index)}.js`, true, entry.textContent)
+          continue
+        }
+        if (/^(?:https?:)?\/\//.test(src)) {
+          failures.push(`apps/web/index.html: external default module entry cannot be checked: ${src}`)
+          continue
+        }
+        scanSource(resolve(root, 'apps/web', src.replace(/^\//, '')), true)
+      }
+    } finally {
+      dom.window.close()
+    }
+  } else failures.push('missing default Web entry apps/web/index.html')
+
+  for (const pkg of queue) {
+    const { manifest } = pkg
+    for (const section of RUNTIME_SECTIONS) {
+      for (const [name, range] of Object.entries(manifest[section] ?? {})) {
+        dependency(name, range, pkg, `${manifest.name} ${section}`)
+      }
+    }
+    if (manifest.dsh?.bundle?.patch !== undefined) scanConfig(resolve(pkg.directory, manifest.dsh.bundle.patch))
+    for (const tree of manifest.dsh?.configTrees ?? []) {
+      const treePath = resolve(pkg.directory, tree.path)
+      const files = existsSync(treePath) && statSync(treePath).isDirectory()
+        ? globSync('**/*.{yml,yaml}', { cwd: treePath, exclude: ['**/*.i18n.yaml', '**/preset.yml'] }) : []
+      if (files.length === 0) failures.push(`${display(treePath)}: declared config tree has no composition files`)
+      for (const path of files) scanConfig(resolve(treePath, path))
+    }
+    if (display(pkg.directory) === 'apps/web') continue
+    const files = globSync('src/**/*.{ts,tsx,mts,cts,js,mjs,cjs}', { cwd: pkg.directory,
+      exclude: ['**/*.spec.*', '**/*.test.*', '**/*.d.ts', '**/tests/**', '**/__tests__/**'] })
+    if (display(pkg.directory) === 'apps/cli' && files.length === 0) failures.push('apps/cli: no default runtime sources')
+    for (const path of files) scanSource(resolve(pkg.directory, path))
+  }
+  return { failures: [...new Set(failures)], packageCount: visited.size,
+    sourceCount: sources.size, configCount: configs.size, webPluginCount }
+}
+
+function barePackageName(specifier: string): string {
+  return /^(?:@[^/]+\/)?[^/@]+/.exec(specifier)?.[0] ?? specifier
+}
+
+function resolveSource(path: string): string | undefined {
+  const extension = extname(path)
+  if (extension !== '' && !/^\.[cm]?[jt]sx?$/.test(extension)
+    && !(existsSync(path) && statSync(path).isDirectory())) return undefined
+  const extensions = ['.mjs', '.js', '.mts', '.ts', '.jsx', '.tsx']
+  const typed = extension === '.js' ? [path.replace(/\.js$/, '.ts'), path.replace(/\.js$/, '.tsx')]
+    : extension === '.mjs' ? [path.replace(/\.mjs$/, '.mts')]
+      : extension === '.cjs' ? [path.replace(/\.cjs$/, '.cts')] : []
+  return [path, ...typed, ...extension === '' ? extensions.map(suffix => `${path}${suffix}`) : [],
+    ...extensions.map(suffix => resolve(path, `index${suffix}`))]
+    .find(candidate => existsSync(candidate) && statSync(candidate).isFile())
+}
+
+function runtimeLocalSpecifiers(path: string, source: string): Set<string> {
+  const specifiers = collectRuntimeLocalSourceSpecifiers(path, source, true)
+  const file = ts.createSourceFile(path, source, ts.ScriptTarget.Latest, true)
+  const visit = (node: ts.Node): void => {
+    if (ts.isNewExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === 'URL') {
+      const first = node.arguments?.[0]
+      if (first !== undefined && ts.isStringLiteralLike(first)
+        && (first.text.startsWith('.') || first.text.startsWith('/'))) specifiers.add(first.text)
+    }
+    ts.forEachChild(node, visit)
+  }
+  visit(file)
+  return specifiers
+}
+
+function isRecord(value: unknown): value is Record<string, unknown> {
+  return typeof value === 'object' && value !== null && !Array.isArray(value)
+}
+
+/** Read the literal package lists that define installation-owned profile defaults. */
+function profilePackages(source: string): { packages: string[]; webBundles: string[] } {
+  const file = ts.createSourceFile(PROFILE_SOURCE, source, ts.ScriptTarget.Latest, true)
+  const required = new Set(['PROFILE_TEMPLATES', 'DEFAULT_PROFILE_BUNDLES'])
+  const found = new Set<string>()
+  const packages: string[] = []
+  const webBundles: string[] = []
+  const literals = (node: ts.Node, path: string[]): void => {
+    if (ts.isStringLiteralLike(node)) {
+      if (node.text.startsWith('@')) packages.push(node.text)
+      if (path.join('.') === 'PROFILE_TEMPLATES.web.bundles') webBundles.push(node.text)
+    } else if (ts.isArrayLiteralExpression(node)) node.elements.forEach((child) => { literals(child, path) })
+    else if (ts.isObjectLiteralExpression(node)) node.properties.forEach((child) => { literals(child, path) })
+    else if (ts.isPropertyAssignment(node)) {
+      if (!ts.isIdentifier(node.name) && !ts.isStringLiteralLike(node.name)) {
+        throw new Error(`${PROFILE_SOURCE}: default profile keys must be literal names`)
+      }
+      literals(node.initializer, [...path, node.name.text])
+    }
+    else if (ts.isAsExpression(node) || ts.isSatisfiesExpression(node) || ts.isParenthesizedExpression(node)) {
+      literals(node.expression, path)
+    } else throw new Error(`${PROFILE_SOURCE}: default profile packages must use static literal lists`)
+  }
+  for (const statement of file.statements) {
+    if (!ts.isVariableStatement(statement)) continue
+    for (const declaration of statement.declarationList.declarations) {
+      if (!ts.isIdentifier(declaration.name)
+        || !required.has(declaration.name.text) && declaration.name.text !== 'INSTALLATION_OWNED_PROFILE_TUPLES') continue
+      if (declaration.initializer === undefined) continue
+      if (required.has(declaration.name.text)) found.add(declaration.name.text)
+      const before = packages.length
+      literals(declaration.initializer, [declaration.name.text])
+      if (packages.length === before) throw new Error(`${PROFILE_SOURCE}: ${declaration.name.text} has no default bundles`)
+    }
+  }
+  if (found.size !== required.size) throw new Error(`${PROFILE_SOURCE}: missing default profile declarations`)
+  if (webBundles.length === 0) throw new Error(`${PROFILE_SOURCE}: missing default Web bundle list`)
+  return { packages, webBundles }
+}
+
+if (import.meta.main) {
+  const result = verifyDefaultProductIsolation(resolve(import.meta.dirname, '..'))
+  if (result.failures.length > 0) {
+    for (const failure of result.failures) console.error(`verify-default-product-isolation: ${failure}`)
+    process.exitCode = 1
+  } else {
+    console.log(`verify-default-product-isolation: ${String(result.packageCount)} packages, `
+      + `${String(result.sourceCount)} runtime sources, ${String(result.configCount)} configurations, `
+      + `${String(result.webPluginCount)} composed Web plugins exclude experimental packages.`)
+  }
+}

+ 258 - 0
scripts/web-product-bundle-isolation.spec.ts

@@ -0,0 +1,258 @@
+/** Exercise real Vite build inputs, including files absent from ordinary chunk imports. */
+
+import { existsSync, mkdtempSync, mkdirSync, realpathSync, rmSync, symlinkSync, unlinkSync, writeFileSync } from 'node:fs'
+import { createRequire } from 'node:module'
+import { tmpdir } from 'node:os'
+import { dirname, join } from 'node:path'
+import { pathToFileURL } from 'node:url'
+import { describe, expect, it, onTestFinished, vi } from 'vitest'
+import { browserDependencyAnalysis, productWebBundleIsolation } from '../apps/web/product-isolation.ts'
+import { WebProductBundleIsolation } from './web-product-bundle-isolation.ts'
+import { BundleInputIsolation } from './bundle-input-isolation.ts'
+
+const filesystem = vi.hoisted(() => ({ missingRoot: false }))
+vi.mock('node:fs', async (importOriginal) => {
+  const actual = await importOriginal<typeof import('node:fs')>()
+  return {
+    ...actual,
+    existsSync: (path: Parameters<typeof actual.existsSync>[0]) => !filesystem.missingRoot && actual.existsSync(path),
+  }
+})
+
+// Vite is owned by the Web app rather than the repository's scripting dependencies.
+const vite = createRequire(new URL('../apps/web/package.json', import.meta.url))('vite') as {
+  build(config: Record<string, unknown>): Promise<unknown>
+}
+
+function fixture() {
+  // Vite resolves inputs with native realpath, which expands Windows short names.
+  const root = realpathSync.native(mkdtempSync(join(tmpdir(), 'dsh-web-bundle-isolation-')))
+  const web = join(root, 'apps/web')
+  const links: string[] = []
+  onTestFinished(() => {
+    for (const path of links) unlinkSync(path)
+    rmSync(root, { recursive: true, force: true })
+  })
+  const write = (path: string, content: string): void => {
+    mkdirSync(dirname(join(root, path)), { recursive: true })
+    writeFileSync(join(root, path), content)
+  }
+  write('package.json', '{"name":"isolation-fixture","type":"module"}')
+  write('apps/web/package.json', '{"name":"@deepseek-ai/dsh-web-frontend","type":"module"}')
+  write('apps/web/index.html', '<script type="module" src="/src/main.js"></script>')
+  write('apps/web/src/main.js', 'globalThis.product = true')
+  write('apps/web/src/preview.js', 'import "../../../packages/experimental/prototype/index.js"')
+  write('packages/experimental/prototype/package.json', '{"name":"@deepseek-ai/dsh-experimental-prototype","type":"module"}')
+  write('packages/experimental/prototype/index.js', 'globalThis.experimental = true')
+  write('packages/experimental/prototype/style.css', '.experimental { color: red }')
+  write('packages/experimental/prototype/tiny.svg', '<svg xmlns="http://www.w3.org/2000/svg"/>')
+  const run = (extra: Record<string, unknown> = {}): Promise<unknown> => vite.build({
+    configFile: false,
+    root: web,
+    logLevel: 'silent',
+    plugins: productWebBundleIsolation(root, web),
+    build: {
+      write: false,
+      minify: false,
+      rollupOptions: {
+        input: { index: join(web, 'index.html'), bootstrap: join(web, 'src/preview.js') },
+      },
+    },
+    ...extra,
+  })
+  return { root, web, write, run, links }
+}
+
+describe('default Web bundle input isolation', () => {
+  it('requires explicit analysis intent before allowing externalized inputs in a non-writing build', async () => {
+    const test = fixture()
+    test.write('apps/web/src/main.js', 'import "external-lib"; globalThis.product = true')
+    const externalize = {
+      name: 'fixture-external-analysis',
+      resolveId(id: string) { return id === 'external-lib' ? { id, external: true } : null },
+    }
+    await expect(test.run({ plugins: [...productWebBundleIsolation(test.root, test.web), externalize] }))
+      .rejects.toThrow(/external module external-lib has no bundled input proof/)
+    await expect(test.run({ plugins: [
+      ...productWebBundleIsolation(test.root, test.web), browserDependencyAnalysis(), externalize,
+    ] })).resolves.toBeDefined()
+    expect(existsSync(join(test.web, 'dist'))).toBe(false)
+  })
+
+  it('refuses output writing when dependency analysis bypasses the product check', async () => {
+    const test = fixture()
+    const output = join(test.root, 'analysis-output')
+    await expect(test.run({
+      plugins: [...productWebBundleIsolation(test.root, test.web), browserDependencyAnalysis()],
+      build: { write: true, outDir: output },
+    })).rejects.toThrow(/browser dependency analysis requires build.write: false/)
+    expect(existsSync(output)).toBe(false)
+  })
+
+  it('allows experimental code in the separately emitted preview entry', async () => {
+    await expect(fixture().run()).resolves.toMatchObject({
+      output: expect.arrayContaining([
+        expect.objectContaining({ type: 'asset', fileName: 'index.html' }),
+        expect.objectContaining({ type: 'chunk', name: 'bootstrap' }),
+      ]) as unknown,
+    })
+  })
+
+  it.each([
+    ['static import', 'import "../../../packages/experimental/prototype/index.js"'],
+    ['dynamic import', 'void import("../../../packages/experimental/prototype/index.js")'],
+    ['raw query import', 'import text from "../../../packages/experimental/prototype/index.js?raw"; console.log(text)'],
+  ])('rejects a product %s of experimental input', async (_name, source) => {
+    const test = fixture()
+    test.write('apps/web/src/main.js', source)
+    await expect(test.run()).rejects.toThrow(/Web product isolation: experimental input/)
+  })
+
+  it('checks inline HTML modules after Vite resolves and bundles them', async () => {
+    const test = fixture()
+    test.write('apps/web/index.html', '<script type="module">import "../../packages/experimental/prototype/index.js"</script>')
+    await expect(test.run()).rejects.toThrow(/Web product isolation: experimental input/)
+  })
+
+  it('checks resolver aliases by the actual file owner', async () => {
+    const test = fixture()
+    test.write('apps/web/src/main.js', 'import "feature"')
+    await expect(test.run({ resolve: { alias: { feature: join(test.root, 'packages/experimental/prototype/index.js') } } }))
+      .rejects.toThrow(/Web product isolation: experimental input/)
+  })
+
+  it('checks npm alias package identity independently of its directory name', async () => {
+    const test = fixture()
+    test.write('apps/web/src/main.js', 'import "ordinary-name"')
+    test.write('apps/web/node_modules/ordinary-name/package.json',
+      '{"name":"@deepseek-ai/dsh-experimental-aliased","type":"module","main":"index.js"}')
+    test.write('apps/web/node_modules/ordinary-name/index.js', 'globalThis.aliased = true')
+    await expect(test.run()).rejects.toThrow(/belongs to experimental package/)
+  })
+
+  it('follows virtual module edges to their resolved inputs', async () => {
+    const test = fixture()
+    test.write('apps/web/src/main.js', 'import "virtual:feature"')
+    await expect(test.run({ plugins: [...productWebBundleIsolation(test.root, test.web), {
+      name: 'fixture-virtual-module',
+      resolveId(id: string) { return id === 'virtual:feature' ? '\u0000virtual:feature' : null },
+      load(id: string) {
+        return id === '\u0000virtual:feature'
+          ? `import ${JSON.stringify(join(test.root, 'packages/experimental/prototype/index.js'))}` : null
+      },
+    }] })).rejects.toThrow(/Web product isolation: experimental input/)
+  })
+
+  it('rejects experimental inputs merged into a product-reachable shared chunk', async () => {
+    const test = fixture()
+    test.write('apps/web/src/main.js', 'import "./shared.js"; globalThis.product = true')
+    test.write('apps/web/src/shared.js', 'globalThis.shared = true')
+    await expect(test.run({ build: {
+      write: false,
+      minify: false,
+      rollupOptions: {
+        input: { index: join(test.web, 'index.html'), bootstrap: join(test.web, 'src/preview.js') },
+        output: { manualChunks: (id: string) => id.endsWith('/shared.js') || id.includes('/packages/experimental/')
+          ? 'shared' : undefined },
+      },
+    } })).rejects.toThrow(/Web product isolation: experimental input/)
+  })
+
+  it('checks symlinked package ownership', async () => {
+    const test = fixture()
+    test.write('apps/web/src/main.js', 'import "ordinary-name/index.js"')
+    const link = join(test.web, 'node_modules/ordinary-name')
+    mkdirSync(dirname(link), { recursive: true })
+    symlinkSync(join(test.root, 'packages/experimental/prototype'), link, process.platform === 'win32' ? 'junction' : 'dir')
+    test.links.push(link)
+    await expect(test.run({ resolve: { preserveSymlinks: true } }))
+      .rejects.toThrow(/Web product isolation: experimental input/)
+  })
+
+  it.each([
+    ['stylesheet import', '@import "../../../packages/experimental/prototype/style.css";'],
+    ['inlined image', '.product { background: url("../../../packages/experimental/prototype/tiny.svg") }'],
+    ['emitted image', '.product { background: url("../../../packages/experimental/prototype/tiny.svg?no-inline") }'],
+  ])('checks the actual CSS transform inputs for %s', async (_name, css) => {
+    const test = fixture()
+    test.write('apps/web/src/main.js', 'import "./style.css"; globalThis.product = true')
+    test.write('apps/web/src/style.css', css)
+    await expect(test.run()).rejects.toThrow(/Web product isolation: experimental input/)
+  })
+
+  it('allows experimental CSS only used by preview', async () => {
+    const test = fixture()
+    test.write('apps/web/src/preview.js', 'import "./preview.css"; globalThis.preview = true')
+    test.write('apps/web/src/preview.css', '@import "../../../packages/experimental/prototype/style.css";')
+    await expect(test.run()).resolves.toBeDefined()
+  })
+
+  it.each([
+    ['URL worker', 'new Worker(new URL("./worker.js", import.meta.url), { type: "module" })'],
+    ['inline worker', 'import Worker from "./worker.js?worker&inline"; new Worker()'],
+  ])('checks %s subbuild inputs when the product loads it', async (_name, source) => {
+    const test = fixture()
+    test.write('apps/web/src/main.js', source)
+    test.write('apps/web/src/worker.js', 'import "../../../packages/experimental/prototype/index.js"')
+    await expect(test.run()).rejects.toThrow(/Web product isolation: experimental input/)
+  })
+
+  it('allows an experimental worker loaded only by preview', async () => {
+    const test = fixture()
+    test.write('apps/web/src/preview.js', 'new Worker(new URL("./worker.js", import.meta.url), { type: "module" })')
+    test.write('apps/web/src/worker.js', 'import "../../../packages/experimental/prototype/index.js"')
+    await expect(test.run()).resolves.toBeDefined()
+  })
+
+  it('rejects an emitted entry without its original HTML input', () => {
+    const test = fixture()
+    const inputs = new WebProductBundleIsolation(test.root, test.web)
+    expect(() => { inputs.verify({}, () => null) }).toThrow(/index.html is missing its original HTML input/)
+  })
+
+  it('rejects a worker asset whose subbuild was not observed', () => {
+    const test = fixture()
+    const inputs = new WebProductBundleIsolation(test.root, test.web)
+    inputs.assetReference('worker.js', 'index.html', 'asset')
+    expect(() => { inputs.verify({
+      'index.html': { type: 'asset', fileName: 'index.html', names: ['index.html'], originalFileNames: ['index.html'] },
+      'worker.js': { type: 'asset', fileName: 'worker.js', names: [], originalFileNames: [] },
+    }, () => null) }).toThrow(/worker.js has no recorded original files/)
+  })
+})
+
+describe('bundler input ownership', () => {
+  it('refuses source-map paths whose filesystem root is unavailable', () => {
+    const test = fixture()
+    const ownership = new BundleInputIsolation(test.root, 'fixture')
+    filesystem.missingRoot = true
+    try {
+      expect(() => { ownership.assertSourceMapInput(join(test.web, 'missing/source.js')) })
+        .toThrow(/has no existing filesystem root/)
+    } finally {
+      filesystem.missingRoot = false
+    }
+  })
+
+  it('retains ownership through queries, filesystem URLs, and virtual path wrappers', () => {
+    const test = fixture()
+    const ownership = new BundleInputIsolation(test.root, 'fixture')
+    const file = join(test.root, 'packages/experimental/prototype/index.js')
+    for (const id of [file, `${file}?raw`, `${pathToFileURL(file).href}?import`, `\u0000${file}?commonjs-proxy`,
+      `virtual:${file}`, `/@fs/${file.replaceAll('\\', '/')}`, file.replaceAll('/', '\\')]) {
+      expect(() => { ownership.assertInput(id) }, id).toThrow(/experimental input/)
+    }
+  })
+
+  it('distinguishes missing actual inputs from omitted upstream source-map files', () => {
+    const test = fixture()
+    const ownership = new BundleInputIsolation(test.root, 'fixture')
+    const file = join(test.web, 'node_modules/upstream/src/omitted.js')
+    test.write('apps/web/node_modules/upstream/package.json', '{"name":"upstream"}')
+    expect(() => { ownership.assertInput(file) }).toThrow(/is missing/)
+    expect(() => { ownership.assertSourceMapInput(file) }).not.toThrow()
+    test.write('apps/web/node_modules/upstream/package.json', '{"name":"@deepseek-ai/dsh-experimental-upstream"}')
+    ownership.reset()
+    expect(() => { ownership.assertSourceMapInput(file) }).toThrow(/belongs to experimental package/)
+  })
+})

+ 199 - 0
scripts/web-product-bundle-isolation.ts

@@ -0,0 +1,199 @@
+/** Experimental-package isolation over Vite's emitted product graph and recorded file inputs. */
+
+import { basename, resolve } from 'node:path'
+import { BundleInputIsolation, physicalBundleInput } from './bundle-input-isolation.ts'
+
+/** The emitted chunk fields used to follow the product's actual output graph. */
+export interface WebOutputChunk {
+  type: 'chunk'
+  fileName: string
+  preliminaryFileName?: string
+  facadeModuleId: string | null
+  modules: Record<string, unknown>
+  imports: string[]
+  dynamicImports: string[]
+  implicitlyLoadedBefore: string[]
+  referencedFiles: string[]
+  viteMetadata?: { importedCss: Set<string>; importedAssets: Set<string> }
+}
+
+/** Original file names provide ownership for emitted, non-CSS assets. */
+export interface WebOutputAsset {
+  type: 'asset'
+  fileName: string
+  names: string[]
+  originalFileNames: string[]
+}
+
+/** Vite's output bundle, restricted to the fields needed by this check. */
+export type WebOutputBundle = Record<string, WebOutputChunk | WebOutputAsset>
+
+/** Retained source edges, including CSS modules whose pure output chunks Vite removes. */
+export interface WebModuleInfo {
+  importedIds: readonly string[]
+  dynamicallyImportedIds: readonly string[]
+  isExternal: boolean
+  isIncluded: boolean | null
+}
+
+interface AssetReference {
+  host: string
+  file: string
+  type: 'asset' | 'public'
+}
+
+/** Output graphs are rebuilt each time; cached transforms retain their file-input observations. */
+export class WebProductBundleIsolation {
+  private readonly cssInputs = new Map<string, Set<string>>()
+  private readonly chunks = new Map<string, WebOutputChunk>()
+  private readonly references: AssetReference[] = []
+  private readonly workerInputs = new Map<string, Set<string>>()
+  private readonly inputs: BundleInputIsolation
+  private readonly webRoot: string
+
+  constructor(repository: string, webRoot: string) {
+    this.webRoot = webRoot
+    this.inputs = new BundleInputIsolation(repository, 'Web product isolation')
+  }
+
+  /** Discard output graphs and package metadata before a build or watch rebuild. */
+  reset(): void {
+    this.chunks.clear()
+    this.references.length = 0
+    this.workerInputs.clear()
+    this.inputs.reset()
+  }
+
+  /** Replace dependency observations whenever Vite transforms a module again. */
+  cssTransform(module: string): void { this.cssInputs.set(module, new Set<string>()) }
+
+  /** Record the files the actual CSS transform asks Rollup to watch. */
+  cssDependency(module: string, file: string): void {
+    const inputs = this.cssInputs.get(module) ?? new Set<string>()
+    inputs.add(file)
+    this.cssInputs.set(module, inputs)
+  }
+
+  /** Record Vite's asset URL edges without changing their rendered values. */
+  assetReference(file: string, host: string, type: 'asset' | 'public'): void {
+    this.references.push({ file, host, type })
+  }
+
+  /** Preserve pure CSS chunk inputs before Vite removes their JavaScript wrappers. */
+  captureChunks(bundle: WebOutputBundle): void {
+    for (const item of Object.values(bundle)) {
+      if (item.type !== 'chunk') continue
+      this.chunks.set(item.fileName, {
+        ...item,
+        modules: { ...item.modules },
+        imports: [...item.imports],
+        dynamicImports: [...item.dynamicImports],
+        implicitlyLoadedBefore: [...item.implicitlyLoadedBefore],
+        referencedFiles: [...item.referencedFiles],
+        ...(item.viteMetadata === undefined ? {} : { viteMetadata: {
+          importedCss: new Set(item.viteMetadata.importedCss),
+          importedAssets: new Set(item.viteMetadata.importedAssets),
+        } }),
+      })
+    }
+  }
+
+  /** Each Vite worker subbuild has one entry and owns its complete watched input set. */
+  workerBundle(bundle: WebOutputBundle, watchedFiles: readonly string[]): void {
+    const inputs = new Set(watchedFiles)
+    const entries: string[] = []
+    for (const item of Object.values(bundle)) {
+      if (item.type === 'chunk') {
+        for (const id of Object.keys(item.modules)) inputs.add(id)
+        if (item.facadeModuleId !== null) entries.push(physicalBundleInput(item.facadeModuleId) ?? item.facadeModuleId)
+      }
+    }
+    for (const file of [...Object.keys(bundle), ...entries]) {
+      this.workerInputs.set(file, new Set([...this.workerInputs.get(file) ?? [], ...inputs]))
+    }
+  }
+
+  /** Reject experimental ownership in everything the emitted index page can load. */
+  verify(bundle: WebOutputBundle, moduleInfo: (id: string) => WebModuleInfo | null): void {
+    const html = bundle['index.html']
+    if (html?.type !== 'asset' || !html.originalFileNames.some(file =>
+      resolve(this.webRoot, file) === resolve(this.webRoot, 'index.html'))) {
+      throw new Error('Web product isolation: emitted index.html is missing its original HTML input')
+    }
+    const outputs = new Map(Object.entries(bundle))
+    const aliases = new Map<string, string>()
+    const cssOwners = new Map<string, Set<string>>()
+    for (const [file, chunk] of this.chunks) {
+      aliases.set(chunk.preliminaryFileName ?? file, file)
+      for (const css of chunk.viteMetadata?.importedCss ?? []) {
+        const owners = cssOwners.get(css) ?? new Set<string>()
+        for (const id of Object.keys(chunk.modules)) owners.add(id)
+        cssOwners.set(css, owners)
+      }
+    }
+    for (const item of Object.values(bundle)) {
+      if (item.type === 'asset') {
+        for (const name of item.names) aliases.set(name, item.fileName)
+      }
+    }
+    const outputName = (name: string): string => outputs.has(name) ? name
+      : aliases.get(name) ?? aliases.get(basename(name)) ?? name
+    const references = new Map<string, AssetReference[]>()
+    for (const reference of this.references) {
+      const host = outputName(reference.host)
+      references.set(host, [...references.get(host) ?? [], reference])
+    }
+    const visitedOutputs = new Set<string>()
+    const visitedModules = new Set<string>()
+    const queue = ['index.html']
+    const checkWorker = (key: string): void => {
+      const inputs = this.workerInputs.get(key)
+      if (inputs === undefined) throw new Error(`Web product isolation: worker ${key} has no recorded build inputs`)
+      for (const input of inputs) this.inputs.assertInput(input)
+    }
+    const checkModule = (id: string): void => {
+      if (visitedModules.has(id)) return
+      visitedModules.add(id)
+      this.inputs.assertInput(id)
+      const info = moduleInfo(id)
+      if (info === null) throw new Error(`Web product isolation: module ${id} has no Rollup module record`)
+      if (info.isExternal) throw new Error(`Web product isolation: external module ${id} has no bundled input proof`)
+      if (/\.(?:css|less|sass|scss|styl|stylus|pcss|postcss|sss)(?:$|\?)/i.test(id) && !this.cssInputs.has(id)) {
+        throw new Error(`Web product isolation: stylesheet ${id} has no recorded transform inputs`)
+      }
+      if (/[?&](?:sharedworker|worker)(?:&|$)/.test(id)) checkWorker(physicalBundleInput(id) ?? id)
+      for (const file of this.cssInputs.get(id) ?? []) this.inputs.assertInput(file)
+      for (const child of [...info.importedIds, ...info.dynamicallyImportedIds]) {
+        const childInfo = moduleInfo(child)
+        if (childInfo === null) throw new Error(`Web product isolation: module ${child} has no Rollup module record`)
+        if (childInfo.isIncluded || childInfo.isExternal) checkModule(child)
+      }
+    }
+    for (const name of queue) {
+      const file = outputName(name)
+      if (visitedOutputs.has(file)) continue
+      visitedOutputs.add(file)
+      const item = outputs.get(file)
+      if (item === undefined) throw new Error(`Web product isolation: referenced output ${file} is missing`)
+      if (this.workerInputs.has(file)) checkWorker(file)
+      else if (item.type === 'chunk') {
+        for (const id of Object.keys(item.modules)) checkModule(id)
+        queue.push(...item.imports, ...item.dynamicImports, ...item.implicitlyLoadedBefore, ...item.referencedFiles)
+        queue.push(...item.viteMetadata?.importedCss ?? [], ...item.viteMetadata?.importedAssets ?? [])
+      } else if (cssOwners.has(file)) {
+        for (const id of cssOwners.get(file) ?? []) checkModule(id)
+      } else {
+        if (item.originalFileNames.length === 0) {
+          throw new Error(`Web product isolation: asset ${file} has no recorded original files`)
+        }
+        for (const original of item.originalFileNames) this.inputs.assertInput(resolve(this.webRoot, original))
+      }
+      for (const reference of references.get(file) ?? []) {
+        if (reference.type === 'public') this.inputs.assertInput(resolve(this.webRoot, 'public', reference.file))
+        else queue.push(reference.file)
+      }
+    }
+    if (visitedModules.size === 0) throw new Error('Web product isolation: index.html has no recorded reachable modules')
+  }
+
+}

+ 1 - 0
tsconfig.client.json

@@ -30,6 +30,7 @@
     "packages/extensions/ui-cordis/src/css-modules.d.ts",
     "packages/client/tsdown.client.ts",
     "scripts/client-build-environment.ts",
+    "scripts/bundle-input-isolation.ts",
     "scripts/*.client.spec.ts",
     "scripts/client-bundle-css.spec.ts",
     "scripts/client-bundle-purity.spec.ts"

+ 2 - 0
tsconfig.host.json

@@ -8,6 +8,8 @@
     "rewriteRelativeImportExtensions": false
   },
   "include": [
+    "apps/web/product-isolation.ts",
+    "apps/web/tests/default-product-isolation.e2e.ts",
     "apps/web/tests/scaffold.ts",
     "apps/web/tests/auto-review-fixture.ts",
     "apps/web/tests/auto-review-denial.e2e.ts",