@deepseek-ai/dsh-app-boot 0.1.7-alpha.1 → 0.1.7-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.i18n.yaml CHANGED
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # pnpm run verify-translation-pairing --write packages/boot/app-boot/README.md
5
- README.md: 5bbe75d641eeeaf3b87c31221b3491ad1408934e
6
- README.zh.md: b3ac89c1cb082880622175b494d540888b19e804
5
+ README.md: c32f8e4dd9065897a7dcbe4b7a2fd2e067b25e46
6
+ README.zh.md: 0bbdab2a5d51deeb6b63bf972e6550f9b4034db1
package/README.md CHANGED
@@ -40,7 +40,7 @@ installFailLoud('dsh')
40
40
  const ctx = await boot('dsh', resolveConfigPath(argv[2], process.env.DSH_SNAPSHOT))
41
41
  ```
42
42
 
43
- With that entry point, startup keeps every plugin that can activate. An enabled failed plugin produces a labelled warning. A failed required entry makes startup dispose the whole app and exit nonzero; required ids absent from a profile and disabled required entries do not affect startup. The global required list covers shared Agent execution, application endpoints, and Web bootstrap/transport: `agent-loop`, `webserver`, `modules`, `connection`, `headless-runner`, `acp`, and `sdk-jsonrpc-server`.
43
+ `installFailLoud` writes one labelled `util.inspect` diagnostic to stderr for an unhandled rejection or an uncaught exception, awaits the surface's release hook under a fixed timeout, and exits 1; control never returns to the failed operation, because only the throw site knows which state is intact, and the event loop runs only until the release settles or times out. With that entry point, startup keeps every plugin that can activate. An enabled failed plugin produces a labelled warning. A failed required entry makes startup dispose the whole app and exit nonzero; required ids absent from a profile and disabled required entries do not affect startup. The global required list covers shared Agent execution, application endpoints, and Web bootstrap/transport: `agent-loop`, `webserver`, `modules`, `connection`, `headless-runner`, `acp`, and `sdk-jsonrpc-server`.
44
44
 
45
45
  <a id="profiles"></a>
46
46
  ### Profiles
@@ -49,6 +49,12 @@ Import profile and bundle declaration types from [`@deepseek-ai/dsh-package-mani
49
49
 
50
50
  A profile is how one dsh installation ships different app surfaces: `web`, `headless`, `acp`, `sdk`, and `sdk-minimal` start distinct compositions from the same launcher. A profile lives at `$DSH_HOME/profiles/<name>` and combines installable bundles with its own `cordis.patch.yml`. A bundle's `dsh.bundle.patch` names one patch file or an ordered list of files; `bundlePatchFiles` validates the declaration and `bundlePatchPaths` resolves it to absolute paths; the layer concatenates their patch lists in that order. The YAML composition enables or disables HMR. The shipped `web` template uses live reload, while the other shipped templates apply patches only at startup. `sdk-minimal` names only its standalone bundle; the other templates retain base-plus-mode stacks. `dsh --profile <name> --from-default-profile <template>` creates a custom profile at a new non-shipped name from one shipped template, while `dsh plugin` initializes a base-backed profile and manages its installed bundles. Bundle resolution, manifest, and patch-loading failures print a diagnostic and skip that bundle without changing its selection. Remaining bundles keep their order; profile and user-patch errors still fail startup. Skipping a bundle does not guarantee that the remaining composition can provide the required services. Application-owned npm projects, such as Electron's reserved Desktop profile, use `loadProfileDirectory` to load an already initialized directory without exposing it through CLI profile lookup.
51
51
 
52
+ Before a profile imports a plugin, DSH checks its `peerDependencies` on `@deepseek-ai/dsh` and `@deepseek-ai/dsh-*` against the single runtime version returned by `getDshRuntimeVersion()`. Every declared range must match; prereleases participate in range matching. Source-workspace `workspace:^`, `workspace:~`, and `workspace:*` refer to that same runtime. Missing DSH peers impose no constraint; invalid ranges are incompatible. These checks use peer declarations, not `engines.dsh`, and are not a sandbox against malicious package code.
53
+
54
+ Admission happens only at composition boundaries DSH owns, and a denied row is denied in the launcher's own copy of the composition: no profile patch layer, dependency manifest, or bundle list changes. `prepareProfilePatches` composes over the launcher's empty profile root and runs while the root Include is mounted and on every profile recomposition, so a denied plugin never imports its module. `prepareProfileEntries` does the same for a preset's rows. A denied ordinary row becomes a detached `disabled: true` row; a native group stays mounted and its denied children do not load; a native Include that reaches a denied plugin is omitted whole, because its file is never rewritten. A row denied by policy keeps its configured `disabled` value in the profile, and every refusal is reported with the package, versions, and risk. A bundle is not a row, so `loadProfileDirectory` checks each bundle's own DSH peers when it loads the profile's bundle layers, at startup and on every recomposition; an incompatible bundle without an exemption is skipped like an unreadable one. These boundaries do not cover plugins another embedder mounts through its own `ctx.plugin` call. Two edits made directly to files during a session are judged only at the next recomposition or startup: a running plugin's own `package.json` peer declaration, and an entry-list file the Loader reads itself, such as the launcher's root config or a nested `cordis:include` file. `--dump-config` reports the configured composition, so a denied plugin row still appears there while a denied bundle contributes no rows, and `--dump-config-schema` imports each composed module to read its schema; run it only against a profile whose plugins you already trust.
55
+
56
+ Exact-version exemptions live in the profile's own `compatibility.json`, not in its `package.json`, so an exemption write never touches the dependency manifest, the bundle list, or Cordis patch files. It maps exact `package-name@version` keys to lists of exact DSH runtime versions; neither a plugin upgrade nor a DSH upgrade inherits permission. A missing file means no exemptions. A malformed one never stops the profile from starting: records the reader accepts still apply, each rejected record is reported on stderr beside the plugin denials, and the file is then treated as read-only, so a grant or revocation refuses and asks the user to repair it by hand instead of overwriting their content. [Plugin Manager](../plugin-manager/README.md#version-compatibility-and-exemptions) owns the grant, revocation, and risk confirmation each change requires.
57
+
52
58
  Your machine-local preferences also live in the Harness home:
53
59
 
54
60
  - **`.env`** — your ordinary environment layers: the invoking directory's file outranks the Harness-home file, and both sit below the inherited environment. Variables that decide how the process starts (`PATH`, `DSH_*`, `XDG_*` and similar) are rejected from files: export them instead. The four proxy names (`HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`, `NO_PROXY`) are accepted from the Harness-home file only, never from the invoking directory's, which arrives with a clone. For a non-product bin that just wants one directory's `.env`, a missing file is fine and an unloadable one prints one labelled warning line.
@@ -97,6 +103,7 @@ After the Loader settles, app-boot warns when only optional entries are inactive
97
103
  | An injected service is unavailable | Warn; continue while the entry waits for its dependencies | Stop startup | Keep the entry waiting; adding the missing provider can activate it |
98
104
  | HTTP port binding fails | Warn; continue without that endpoint | Stop startup | Keep the process running without the failed endpoint; corrected config can restore it |
99
105
  | Detached asynchronous work outside the `apply()` return Promise produces an unhandled rejection | Fatal: dispose the app and exit nonzero | Fatal: dispose the app and exit nonzero | Fatal: dispose the app and exit nonzero, regardless of entry id |
106
+ | A synchronous callback (a stream `'data'` listener, a timer) throws an uncaught exception at any point in the process lifetime | Fatal: dispose the app and exit nonzero; the failed operation is not resumed | Fatal: dispose the app and exit nonzero; the failed operation is not resumed | Fatal: dispose the app and exit nonzero, regardless of entry id |
100
107
  | Entry is absent or explicitly disabled | Ignore it | Ignore it | Do not activate it; no required-startup audit |
101
108
 
102
109
  The required list above includes `modules` and `connection`; Web startup cannot succeed when either enabled entry fails. Failure of an optional provider can also prevent a required consumer from activating. Schema rejection before an existing entry updates is not a transactional rollback of sibling changes.
@@ -121,6 +128,7 @@ This section explains how the outcomes above are realized and points at the code
121
128
 
122
129
  ### Design notes
123
130
 
131
+ - **Runtime version.** `getDshRuntimeVersion()` reads this package's manifest through a filesystem path, including the executable's virtual filesystem; missing or invalid versions fail rather than bypassing compatibility checks.
124
132
  - **Profile launch data.** `ctx.profileContext` contains only profile locations, startup bundle names, parsed invocation overlays and the telemetry opt-out value. `readProfilePatches()` composes the supplied startup profile or reads current files at those locations; callers schedule and apply the result.
125
133
  - **Process-local module resolution.** The launcher installs the runtime resolution on Node's internal ESM and CommonJS resolvers before profile rows mount. Node still owns exports, conditions, subpaths, module caches, and error codes; routed ESM failures report the original importer. Explicit CommonJS `paths` always retain native lookup, including paths inside profiles.
126
134
  - **Linked directories.** A profile link to an external directory admits its importers to peer-aware ancestor lookup, even without its own `package.json`. At each `D/node_modules` position, current `D/package.json` peer names present in the runtime table use the runtime package; other names use the physical candidate. A nearer physical package precedes a later peer declaration, and a peer position needs no physical `node_modules`. Installation-scope package directories are excluded from linked interception; overlapping roots do not change the importer's lookup order ([rule](../../../.agents/notes/implemented/architecture/2026-09-19-profile-resolution-lookup-order.md)).
@@ -191,7 +199,7 @@ These limits describe when this boot library is a poor fit or needs special care
191
199
 
192
200
  - **Runtime resolution depends on Node internals** — supported Node versions require the native builtin-access addon and executable compatibility coverage. Only built Harness-owned Workers receive the runtime resolution bootstrap; third-party Workers and custom `vm` linkers keep native resolution.
193
201
  - **Relinking a profile package needs a restart** — Node caches real paths, so changing the target of a profile link or a dependency link requires a process restart.
194
- - **Linked scope follows recorded real directories** — a hoisted dependency outside every linked root uses native Node. Current peer reads do not invalidate Node caches, watch files, or validate peer version ranges.
202
+ - **Linked scope follows recorded real directories** — a hoisted dependency outside every linked root uses native Node. A peer read never invalidates Node caches or changes which files are watched.
195
203
  - **Source launches use an ESM-only hook** — CommonJS requests still need the JavaScript files selected by package exports; resolution does not supply missing build outputs.
196
204
  - **Snapshot replay swapping is basename-specific** — only a config ending in `cordis.yml` or `cordis.yaml` maps to the sibling `cordis.snapshot.yml`; custom config names require caller-managed selection.
197
205
  - **Environment discovery is launch-scoped** — `loadLayeredEnv` reads only the invocation directory and Harness home once; it does not search parents or follow a workspace selected later. `loadEnv` remains the one-directory helper for non-product bins.
package/README.zh.md CHANGED
@@ -40,7 +40,7 @@ installFailLoud('dsh')
40
40
  const ctx = await boot('dsh', resolveConfigPath(argv[2], process.env.DSH_SNAPSHOT))
41
41
  ```
42
42
 
43
- 有了这个入口,启动会保留所有能够激活的插件。启用但失败的插件会产生带标签的警告。required entry 失败时,启动会拆卸整个应用并以非零码退出;profile 中不存在的 required id 和已禁用的 required entry 不影响启动。全局 required list 覆盖共享 Agent 执行、应用 endpoint,以及 Web 启动与传输:`agent-loop`、`webserver`、`modules`、`connection`、`headless-runner`、`acp` 和 `sdk-jsonrpc-server`。
43
+ `installFailLoud` 会为未处理 rejection 或未捕获异常向 stderr 写一条带标签的 `util.inspect` 诊断,在固定超时内等待界面的 release 钩子,然后以 1 退出;控制流不会回到失败的操作,因为只有抛出点知道哪些状态仍然完整,事件循环只运行到 release 结束或超时。有了这个入口,启动会保留所有能够激活的插件。启用但失败的插件会产生带标签的警告。required entry 失败时,启动会拆卸整个应用并以非零码退出;profile 中不存在的 required id 和已禁用的 required entry 不影响启动。全局 required list 覆盖共享 Agent 执行、应用 endpoint,以及 Web 启动与传输:`agent-loop`、`webserver`、`modules`、`connection`、`headless-runner`、`acp` 和 `sdk-jsonrpc-server`。
44
44
 
45
45
  <a id="profiles"></a>
46
46
  ### Profile
@@ -49,6 +49,12 @@ Profile 与组合包的声明类型从 [`@deepseek-ai/dsh-package-manifest`](../
49
49
 
50
50
  profile 是同一套 dsh 安装提供不同应用界面的方式:`web`、`headless`、`acp`、`sdk` 与 `sdk-minimal` 从同一 launcher 启动不同组合。profile 位于 `$DSH_HOME/profiles/<name>`,由可安装组合包和自身 `cordis.patch.yml` 组成。组合包的 `dsh.bundle.patch` 指定一个 patch 文件或一个有序的文件列表;`bundlePatchFiles` 校验该声明,`bundlePatchPaths` 把它解析为绝对路径;该层按此顺序拼接各文件的 patch 列表。YAML 组合决定是否启用 HMR。随产品交付的 `web` 模板实时重载,其他随附模板只在启动时应用 patch。`sdk-minimal` 只列出自身的独立组合包,其他模板保留 base 加模式的组合包栈。`dsh --profile <name> --from-default-profile <template>` 从一个随附模板,在新的非内置名称处创建自定义 profile;`dsh plugin` 则初始化以 base 为基础的 profile,并管理其中安装的组合包。组合包解析、manifest 读取或 patch 加载失败时会输出诊断并跳过该组合包,不改变其选择状态。其余组合包保持原顺序;profile 和用户 patch 错误仍会导致启动失败。跳过组合包不保证剩余组合能够提供所需服务。由应用持有的 npm 项目(例如 Electron 保留的 Desktop profile)通过 `loadProfileDirectory` 加载已经初始化的目录,而不会将它暴露给 CLI profile 查找。
51
51
 
52
+ profile 导入插件前,DSH 会检查其 `peerDependencies` 中对 `@deepseek-ai/dsh` 和 `@deepseek-ai/dsh-*` 的依赖,与 `getDshRuntimeVersion()` 返回的唯一运行时版本比较。每个声明的版本范围都必须匹配;预发布版本参与范围匹配。源码工作区的 `workspace:^`、`workspace:~` 和 `workspace:*` 指向同一个运行时。未声明 DSH peer 时不施加版本约束;无效范围视为不兼容。这些检查使用 peer 声明,而不是 `engines.dsh`,也不是防范恶意包代码的沙箱。
53
+
54
+ 检查只发生在 DSH 自己持有的组合入口,改写的是启动器自己的那份组合:profile 的 patch 层、依赖清单与组合包列表都不会改变。`prepareProfilePatches` 在启动器的空 profile 根之上组合,并在挂载根 Include 时以及每次 profile 重新组合时执行,因此被拒绝的插件永远不会导入其模块;`prepareProfileEntries` 对 preset 行做同样的事。被拒绝的普通行会变成游离的 `disabled: true` 行;原生 group 保持挂载,其被拒绝的子行不会加载;若某个原生 Include 会到达被拒绝的插件,则整体省略该 Include,因为它的文件不会被改写。被策略拒绝的行在 profile 中保留其配置的 `disabled` 值,每次拒绝都会报告包名、版本与风险。组合包本身不是行,因此 `loadProfileDirectory` 在启动和每次重新组合加载 profile 的组合包层时,检查每个组合包自己声明的 DSH peer;没有豁免的不兼容组合包会像无法读取的组合包一样被跳过。这些入口不覆盖其他嵌入方通过自己的 `ctx.plugin` 调用挂载的插件。会话中直接改文件的两类编辑只在下一次重新组合或启动时才被判定:正在运行的插件自己的 `package.json` peer 声明,以及 Loader 自己读取的入口清单文件(如启动器的根配置或嵌套的 `cordis:include` 文件)。`--dump-config` 报告的是配置出的组合,因此被拒绝的插件行仍会出现在其中,而被拒绝的组合包不提供任何行;`--dump-config-schema` 会导入每个组合模块以读取其 schema,请只对已经信任其插件的 profile 运行。
55
+
56
+ 精确版本豁免保存在 profile 自己的 `compatibility.json` 中,而不是 `package.json`,因此写豁免不会触及依赖清单、组合包列表或 Cordis patch 文件。它把精确的 `package-name@version` 键映射到精确 DSH 运行时版本列表;插件升级和 DSH 升级都不继承授权。文件缺失表示没有豁免。文件损坏绝不会阻止 profile 启动:读取器接受的记录仍然生效,每条被拒绝的记录会与插件拒绝信息一起输出到 stderr,此后该文件被视为只读,因此授予或撤销会拒绝执行并要求用户手工修复,而不是覆盖用户的内容。[插件管理器](../plugin-manager/README.zh.md#version-compatibility-and-exemptions)负责每次变更所需的授权、撤销与风险确认。
57
+
52
58
  你的机器本地偏好同样位于 harness home 中:
53
59
 
54
60
  - **`.env`**——你的普通环境层:调用目录的文件优先于 harness home 的文件,两者都低于继承环境。在文件中设置的进程启动变量(如 `PATH`、`DSH_*`、`XDG_*`)会被拒绝:请改为导出这些变量。四个代理名(`HTTP_PROXY`、`HTTPS_PROXY`、`ALL_PROXY`、`NO_PROXY`)只从 harness home 的文件接受,绝不从调用目录的文件接受——后者随 clone 一起到来。对于只想加载某个目录 `.env` 的非产品 bin,文件缺失不影响启动,文件无法加载时输出一行带标签的警告。
@@ -97,6 +103,7 @@ Loader 结算后,app-boot 在仅 optional 条目未激活时输出警告。如
97
103
  | 注入的服务不可用 | 警告;继续,条目等待依赖 | 终止启动 | 条目继续等待;补上缺失的提供方后可以激活 |
98
104
  | HTTP 端口绑定失败 | 警告;继续,但该端点不可用 | 终止启动 | 进程继续运行,但失败的端点不可用;修正配置后可以恢复 |
99
105
  | 脱离 `apply()` 返回 Promise 的异步任务产生未处理 rejection | 致命错误:释放应用并以非零码退出 | 致命错误:释放应用并以非零码退出 | 致命错误:释放应用并以非零码退出,与条目 id 无关 |
106
+ | 同步回调(流的 `'data'` 监听器、定时器)在进程生命周期任一时刻抛出未捕获异常 | 致命错误:释放应用并以非零码退出;失败的操作不会被恢复 | 致命错误:释放应用并以非零码退出;失败的操作不会被恢复 | 致命错误:释放应用并以非零码退出,与条目 id 无关 |
100
107
  | 条目缺失或被显式禁用 | 忽略 | 忽略 | 不激活该条目;不执行 required 启动审计 |
101
108
 
102
109
  上面的 required 列表包含 `modules` 与 `connection`;只要其中一个已启用条目失败,Web 就无法成功启动。Optional 提供方失败也可能使 required 消费方无法激活。现有条目的新配置在更新前被 schema 校验拒绝,并不等于对兄弟插件的变更做事务回滚。
@@ -121,6 +128,7 @@ Loader 结算后,app-boot 在仅 optional 条目未激活时输出警告。如
121
128
 
122
129
  ### 设计说明
123
130
 
131
+ - **运行时版本。** `getDshRuntimeVersion()` 通过文件系统路径读取本包清单,也支持可执行文件内的虚拟文件系统;版本缺失或无效时会失败,而不会绕过兼容性检查。
124
132
  - **Profile 启动数据。** `ctx.profileContext` 只包含 profile 位置、启动时组合包名称、已解析的调用级 overlay 与遥测退出值。`readProfilePatches()` 组合传入的启动 profile,或读取这些位置上的当前文件;调用方负责调度和应用结果。
125
133
  - **进程内模块解析。** launcher 在挂载 profile 条目前,将 runtime resolution 安装到 Node 的 ESM 与 CommonJS 内部 resolver。exports、conditions、子路径、模块缓存和错误码仍由 Node 负责;路由后的 ESM 失败报告原始 importer。显式 CommonJS `paths` 始终保留原生查询,包括指向 profile 内的路径。
126
134
  - **链接目录。** profile 链接到树外目录时,其下的 importer 参与逐层 peer 查询,即使目标没有自身的 `package.json`。在每个 `D/node_modules` 位置,当前 `D/package.json` 的 peer 包名若存在于运行时表,就使用运行时包;其他包名查询物理候选。更近的物理包先于后续 peer 声明,peer 位置无需物理 `node_modules`。installation 作用域包目录不参与 linked 拦截,重叠 root 不改变 importer 的查询顺序([规则](../../../.agents/notes/implemented/architecture/2026-09-19-profile-resolution-lookup-order.zh.md))。
@@ -191,7 +199,7 @@ Loader 结算后,app-boot 在仅 optional 条目未激活时输出警告。如
191
199
 
192
200
  - **运行时解析依赖 Node 内部机制**——受支持的 Node 版本需要 native builtin access addon 和可执行兼容验证。只有构建后的 Harness 自有 Worker 接收 runtime resolution bootstrap;第三方 Worker 与自定义 `vm` linker 保持原生解析。
193
201
  - **重新链接 profile 包需要重启**——Node 缓存真实路径,因此改变 profile 链接或依赖链接的目标需要重启进程。
194
- - **链接作用域以记录的真实目录为准**——提升后的依赖若在所有 linked root 之外,就使用原生 Node。实时读取 peer 不会使 Node 缓存失效、监视文件或校验 peer 版本范围。
202
+ - **链接作用域以记录的真实目录为准**——提升后的依赖若在所有 linked root 之外,就使用原生 Node。每次读取 peer 都不会使 Node 缓存失效,也不会改变被监视的文件。
195
203
  - **源码启动只安装 ESM 钩子**——CommonJS 请求仍需要 package exports 选中的 JavaScript 文件,解析器不会补出缺失的构建产物。
196
204
  - **快照回放替换仅识别特定 basename**——只有以 `cordis.yml` 或 `cordis.yaml` 结尾的配置会映射到同级 `cordis.snapshot.yml`;自定义配置名称需要调用方自行选择。
197
205
  - **环境发现以启动为界**——`loadLayeredEnv` 只读取一次调用目录与 harness home 中的 `.env`;它不搜索父目录,也不跟随之后选择的 workspace。`loadEnv` 仍是非产品 bin 使用的单目录 helper。
package/lib/index.js CHANGED
@@ -1,16 +1,19 @@
1
1
  import { createRequire, isBuiltin } from "node:module";
2
2
  import { fileURLToPath, pathToFileURL } from "node:url";
3
- import { existsSync, mkdirSync, readFileSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
4
- import { parseEnv } from "node:util";
3
+ import fs, { existsSync, mkdirSync, readFileSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
4
+ import { inspect, parseEnv } from "node:util";
5
5
  import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep, win32 } from "node:path";
6
6
  import * as yaml from "js-yaml";
7
+ import { load } from "js-yaml";
7
8
  import { Context, Service } from "@deepseek-ai/cordis";
8
9
  import Loader, { EntryGroup, EntryTree, ModuleLoader, isJsExpr } from "@deepseek-ai/cordis-plugin-loader";
9
- import { access, constants, readFile, realpath, rename, writeFile } from "node:fs/promises";
10
+ import { access, constants, mkdir, readFile, realpath, rename, writeFile } from "node:fs/promises";
10
11
  import { setTimeout as setTimeout$1 } from "node:timers/promises";
11
12
  import Group from "@deepseek-ai/cordis-plugin-group";
12
13
  import { dshHomePath, resolveDshHome } from "@deepseek-ai/dsh-home-paths";
13
14
  import { createLaunchEnvironmentSnapshot } from "@deepseek-ai/dsh-launch-environment";
15
+ import semver, { parse } from "semver";
16
+ import { withFileLock, writeFileAtomic } from "@deepseek-ai/dsh-atomic-write";
14
17
  import { getEnvironmentData, setEnvironmentData } from "node:worker_threads";
15
18
  import { imports } from "resolve.exports";
16
19
  //#region ../../../vendor/include/src/index.ts
@@ -245,6 +248,198 @@ var Include = class extends EntryTree {
245
248
  }
246
249
  };
247
250
  //#endregion
251
+ //#region lib/types/plugin-compatibility.js
252
+ /** Evaluate plugin dsh peer requirements without importing plugin code. */
253
+ function objectOf$1(value, field) {
254
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${field} must be an object`);
255
+ return value;
256
+ }
257
+ function runtimeVersionOf(value) {
258
+ if (typeof value !== "string" || semver.valid(value) === null) throw new Error(`Invalid dsh runtime version: ${JSON.stringify(value)}; expected a semantic version`);
259
+ return value;
260
+ }
261
+ function identityField(manifest, field) {
262
+ const value = Object.hasOwn(manifest, field) ? manifest[field] : void 0;
263
+ if (typeof value !== "string" || value.trim() === "") throw new Error(`Plugin manifest ${field} must be a non-empty string when dsh peers are incompatible`);
264
+ return value;
265
+ }
266
+ /**
267
+ * Read this app-boot package's version in both source and bundled installations.
268
+ * @returns the validated runtime semantic version, preserving its exact spelling.
269
+ * @throws if package.json cannot be read or its version is missing or invalid.
270
+ */
271
+ function getDshRuntimeVersion() {
272
+ const filename = fileURLToPath(new URL("../package.json", import.meta.url));
273
+ const manifest = objectOf$1(JSON.parse(fs.readFileSync(filename, "utf8")), "app-boot package.json");
274
+ return runtimeVersionOf(Object.hasOwn(manifest, "version") ? manifest.version : void 0);
275
+ }
276
+ /**
277
+ * Check every @deepseek-ai/dsh or @deepseek-ai/dsh-* peer against the runtime.
278
+ * Prereleases participate in ranges. workspace:^, workspace:~, and workspace:*
279
+ * refer to the current runtime; other invalid ranges are incompatible.
280
+ * @param manifest - parsed plugin package.json; inherited fields are ignored.
281
+ * @param exemptions - exact plugin name@version keys mapped to exact runtime versions.
282
+ * @param runtimeVersion - running dsh version, defaulting to this app-boot package.
283
+ * @returns incompatible peers and exemption status, or undefined when none are incompatible.
284
+ * @throws for malformed manifest peer fields, invalid runtime versions, or missing identity on a mismatch.
285
+ */
286
+ function evaluatePluginCompatibility(manifest, exemptions = {}, runtimeVersion = getDshRuntimeVersion()) {
287
+ runtimeVersionOf(runtimeVersion);
288
+ const fields = objectOf$1(manifest, "Plugin manifest");
289
+ if (!Object.hasOwn(fields, "peerDependencies")) return void 0;
290
+ const dependencies = objectOf$1(fields.peerDependencies, "Plugin manifest peerDependencies");
291
+ const peers = {};
292
+ for (const [name, range] of Object.entries(dependencies)) {
293
+ if (typeof range !== "string") throw new Error(`Plugin manifest peerDependencies[${JSON.stringify(name)}] must be a string`);
294
+ if (name !== "@deepseek-ai/dsh" && !name.startsWith("@deepseek-ai/dsh-")) continue;
295
+ const requirement = [
296
+ "workspace:^",
297
+ "workspace:~",
298
+ "workspace:*"
299
+ ].includes(range) ? runtimeVersion : range;
300
+ if (requirement.trim() === "" || !semver.satisfies(runtimeVersion, requirement, { includePrerelease: true })) peers[name] = range;
301
+ }
302
+ if (Object.keys(peers).length === 0) return void 0;
303
+ const name = identityField(fields, "name");
304
+ const version = identityField(fields, "version");
305
+ const key = `${name}@${version}`;
306
+ return {
307
+ name,
308
+ version,
309
+ runtimeVersion,
310
+ peers,
311
+ exempted: (Object.hasOwn(exemptions, key) ? exemptions[key] : void 0)?.includes(runtimeVersion) === true
312
+ };
313
+ }
314
+ /**
315
+ * Describe incompatible peers, their risk, and the exact-version remedy.
316
+ * Surfaces with their own grant mechanism or locale render the structured result themselves.
317
+ * @param issue - incompatible plugin/runtime result, including exempted mismatches.
318
+ * @returns an English diagnostic for logs and stderr.
319
+ */
320
+ function pluginCompatibilityWarning(issue) {
321
+ const key = `${issue.name}@${issue.version}`;
322
+ return `Plugin ${key} is incompatible with dsh ${issue.runtimeVersion}: peerDependencies ${JSON.stringify(issue.peers)}. Running it may cause crashes or data loss. Update the plugin or install a plugin version compatible with this dsh runtime. To accept this risk explicitly, grant the exact-version exemption for ${key} on dsh ${issue.runtimeVersion} with \`dsh plugin allow-version\` or the plugin manager, then retry the installation or restart dsh. Exact-version exemption: ${issue.exempted ? "active" : "not active"}.`;
323
+ }
324
+ //#endregion
325
+ //#region lib/types/profile-compatibility.js
326
+ /** Profile-local compatibility permissions readable before any Cordis plugins load. */
327
+ /** Independent profile metadata; neither package manifests nor Cordis patches carry grants. */
328
+ const PROFILE_COMPATIBILITY_FILENAME = "compatibility.json";
329
+ /** Published and scoped npm package names, as npm accepts them in a manifest dependency key. */
330
+ const PACKAGE_NAME = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/;
331
+ /** Whether a decoded record accepts only exact DSH versions. */
332
+ function isVersionList(value) {
333
+ return Array.isArray(value) && value.every((version) => typeof version === "string" && isExactPluginVersion(version));
334
+ }
335
+ /** Test whether an exemption names a canonical exact SemVer, including build metadata.
336
+ * @param value Version supplied by a manifest or user.
337
+ * @returns False for ranges, prefixes, whitespace, and malformed versions.
338
+ */
339
+ function isExactPluginVersion(value) {
340
+ const parsed = parse(value);
341
+ return parsed !== null && value === `${parsed.version}${parsed.build.length === 0 ? "" : `+${parsed.build.join(".")}`}`;
342
+ }
343
+ /** Validate one explicit exemption without granting it.
344
+ * @param packageVersion Exact npm package-name@version, never an installation spec or range.
345
+ * @param runtimeVersion Exact DSH version, including prerelease and build metadata.
346
+ * @throws When either identity is not canonical.
347
+ */
348
+ function validatePluginVersionExemption(packageVersion, runtimeVersion) {
349
+ const separator = packageVersion.lastIndexOf("@");
350
+ if (separator <= 0 || !PACKAGE_NAME.test(packageVersion.slice(0, separator)) || !isExactPluginVersion(packageVersion.slice(separator + 1)) || !isExactPluginVersion(runtimeVersion)) throw new Error("Version exemptions require an exact npm package-name@version and an exact DSH runtime version");
351
+ }
352
+ /** Read the profile's independent compatibility file without loading plugins.
353
+ * A missing file authorizes nothing. An unreadable or unparsable file authorizes nothing and is
354
+ * reported instead of failing, so a bad file can never make the profile unusable; rejected records
355
+ * are skipped while the remaining valid ones still apply.
356
+ * @param profileDir Absolute profile directory.
357
+ * @returns Accepted exemptions plus every problem found; no manifest fallback is used.
358
+ */
359
+ function readProfileCompatibility(profileDir) {
360
+ const filename = join(profileDir, PROFILE_COMPATIBILITY_FILENAME);
361
+ const unreadable = (reason) => ({
362
+ exemptions: {},
363
+ warnings: [`${filename} ${reason}; treating the profile as having no exemptions`],
364
+ rewritable: false
365
+ });
366
+ let text;
367
+ try {
368
+ text = readFileSync(filename, "utf8");
369
+ } catch (error) {
370
+ if (error.code === "ENOENT") return {
371
+ exemptions: {},
372
+ warnings: [],
373
+ rewritable: true
374
+ };
375
+ return unreadable(`cannot be read (${String(error)})`);
376
+ }
377
+ let value;
378
+ try {
379
+ value = JSON.parse(text);
380
+ } catch (error) {
381
+ return unreadable(`is not valid JSON (${String(error)})`);
382
+ }
383
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return unreadable("must map exact package@version keys to DSH version lists");
384
+ const exemptions = {};
385
+ const warnings = [];
386
+ for (const [key, versions] of Object.entries(value)) {
387
+ const separator = key.lastIndexOf("@");
388
+ if (separator <= 0 || !PACKAGE_NAME.test(key.slice(0, separator)) || !isExactPluginVersion(key.slice(separator + 1))) {
389
+ warnings.push(`${filename}: ${JSON.stringify(key)} is not an exact package-name@version key; the record is ignored`);
390
+ continue;
391
+ }
392
+ if (!isVersionList(versions)) {
393
+ warnings.push(`${filename}: ${key} must contain a list of exact DSH versions; the record is ignored`);
394
+ continue;
395
+ }
396
+ exemptions[key] = versions;
397
+ }
398
+ return {
399
+ exemptions,
400
+ warnings,
401
+ rewritable: warnings.length === 0
402
+ };
403
+ }
404
+ /** Read only the accepted exemptions of a profile.
405
+ * @param profileDir Absolute profile directory.
406
+ * @returns Exact package-name@version keys mapped to their allowed DSH versions.
407
+ */
408
+ function readProfileVersionExemptions(profileDir) {
409
+ return readProfileCompatibility(profileDir).exemptions;
410
+ }
411
+ /** Persist one informed grant or revocation under the compatibility file's own lock.
412
+ * @param profileDir Profile directory; no package manifest is created or modified.
413
+ * @param packageVersion Exact manifest package-name@version.
414
+ * @param runtimeVersion Exact DSH version; grants must name the current runtime, revocations may name historical ones.
415
+ * @param enabled Whether to grant rather than revoke.
416
+ * @param acceptRisk Required true for grants after explicit acknowledgement of possible crashes or data loss.
417
+ * @returns After the atomic write. Existing plugin instances are not reloaded by this operation.
418
+ * @throws For invalid identities, missing consent, a stale runtime, or a file the reader rejected,
419
+ * which the user must repair by hand because rewriting it would discard their content.
420
+ */
421
+ async function setProfileVersionExemption(profileDir, packageVersion, runtimeVersion, enabled, acceptRisk) {
422
+ validatePluginVersionExemption(packageVersion, runtimeVersion);
423
+ if (enabled && !acceptRisk) throw new Error("Incompatible plugins may cause crashes or data loss. To grant this exact-version exemption, explicitly acknowledge the risk with --accept-risk (acceptRisk: true).");
424
+ const current = getDshRuntimeVersion();
425
+ if (enabled && runtimeVersion !== current) throw new Error(`Cannot approve DSH ${runtimeVersion}: this application runs DSH ${current}. Use --dsh-version ${current}.`);
426
+ await mkdir(profileDir, { recursive: true });
427
+ const filename = join(profileDir, PROFILE_COMPATIBILITY_FILENAME);
428
+ await withFileLock(filename, async () => {
429
+ const current = readProfileCompatibility(profileDir);
430
+ if (!current.rewritable) throw new Error(`${PROFILE_COMPATIBILITY_FILENAME} must be repaired before exemptions change:\n${current.warnings.join("\n")}`);
431
+ const exemptions = current.exemptions;
432
+ const versions = exemptions[packageVersion] ?? [];
433
+ if (enabled) exemptions[packageVersion] = [...new Set([...versions, runtimeVersion])];
434
+ else {
435
+ const retained = versions.filter((version) => version !== runtimeVersion);
436
+ if (retained.length) exemptions[packageVersion] = retained;
437
+ else Reflect.deleteProperty(exemptions, packageVersion);
438
+ }
439
+ await writeFileAtomic(filename, JSON.stringify(exemptions, void 0, 2) + "\n", { mode: 384 });
440
+ });
441
+ }
442
+ //#endregion
248
443
  //#region lib/types/profile-resolution/legacy-links.js
249
444
  /** Package directory canonicalization through the active runtime carrier's filesystem. */
250
445
  /**
@@ -710,7 +905,8 @@ function resolveBundleDir(binName, packageName, installAnchor, profileDir) {
710
905
  * Load an already initialized profile directory without resolving it through
711
906
  * the shared Harness home. This is used by application-owned profiles whose
712
907
  * package project and lifecycle belong to that application.
713
- * Unreadable bundles are reported on stderr and skipped without changing the manifest.
908
+ * Unreadable bundles, and bundles whose own dsh peers the profile does not exempt, are reported
909
+ * on stderr and skipped without changing the manifest.
714
910
  * @param binName - the diagnostic prefix on thrown errors.
715
911
  * @param dir - absolute profile package directory.
716
912
  * @param installAnchor - absolute path of the owning dsh app's package.json.
@@ -720,10 +916,14 @@ function resolveBundleDir(binName, packageName, installAnchor, profileDir) {
720
916
  function loadProfileDirectory(binName, dir, installAnchor, options = {}) {
721
917
  const bundles = readProfileManifest(binName, dir).dsh?.profile?.bundles ?? [];
722
918
  const layers = [];
919
+ const exemptions = bundles.length === 0 ? {} : readProfileVersionExemptions(dir);
723
920
  for (const packageName of bundles) try {
724
921
  const packageDir = resolveBundleDir(binName, packageName, installAnchor, dir);
725
- const bundle = readProfileManifest(binName, packageDir).dsh?.bundle;
922
+ const bundleManifest = readProfileManifest(binName, packageDir);
923
+ const bundle = bundleManifest.dsh?.bundle;
726
924
  if (bundle === void 0) throw new Error(`${binName}: profile bundle ${JSON.stringify(packageName)} declares no dsh.bundle in its package.json`);
925
+ const issue = evaluatePluginCompatibility(bundleManifest, exemptions);
926
+ if (issue !== void 0 && !issue.exempted) throw new Error(pluginCompatibilityWarning(issue));
727
927
  const patchPaths = bundlePatchPaths(packageDir, bundle);
728
928
  const patches = patchPaths.flatMap((patchPath) => loadOverlayPatches(binName, patchPath));
729
929
  layers.push({
@@ -747,8 +947,8 @@ function loadProfileDirectory(binName, dir, installAnchor, options = {}) {
747
947
  }
748
948
  /**
749
949
  * Load a profile: resolve every `dsh.profile.bundles` entry to its patch
750
- * layer and parse the profile's own patch file. Unreadable bundles are reported
751
- * on stderr and skipped; profile manifest and user patch errors still throw.
950
+ * layer and parse the profile's own patch file. Unreadable or incompatible bundles
951
+ * are reported on stderr and skipped; profile manifest and user patch errors still throw.
752
952
  * @param binName - the diagnostic prefix on thrown errors.
753
953
  * @param name - the profile name.
754
954
  * @param installAnchor - absolute path of the dsh app's package.json (first resolution anchor).
@@ -1779,6 +1979,161 @@ function readPluginMeta(specifier, parentURL) {
1779
1979
  }
1780
1980
  }
1781
1981
  //#endregion
1982
+ //#region lib/types/compatibility-preflight.js
1983
+ /** Compatibility checks at the composition entry points DSH owns; no Loader instrumentation. */
1984
+ function readManifest(filename) {
1985
+ return JSON.parse(readFileSync(filename, "utf8"));
1986
+ }
1987
+ /** Render the patch wording an Include would report, through the profile's logger. */
1988
+ function patchWarning(ctx) {
1989
+ return (message, ...args) => {
1990
+ let index = 0;
1991
+ ctx.logger.warn(message.replace(/%C/g, () => JSON.stringify(args[index++])));
1992
+ };
1993
+ }
1994
+ function manifestOf(ctx, name, parentURL) {
1995
+ if (name.startsWith("cordis:")) return void 0;
1996
+ const specifier = name.startsWith("#") ? resolvePluginResource(name, parentURL) : name;
1997
+ const packages = ctx.get("pluginPackages");
1998
+ const pkg = packages?.packageOf(specifier, parentURL);
1999
+ if (pkg !== void 0) return readManifest(pkg.manifestPath);
2000
+ const bare = barePackageName(specifier);
2001
+ if (bare !== void 0 && packages === void 0) for (const path of createRequire(parentURL).resolve.paths(bare)) {
2002
+ const filename = join(path, bare, "package.json");
2003
+ if (existsSync(filename)) return readManifest(filename);
2004
+ }
2005
+ if (!isAbsolute(specifier) && !specifier.startsWith(".") && !specifier.startsWith("file:")) return void 0;
2006
+ const url = isAbsolute(specifier) ? pathToFileURL(specifier) : new URL(specifier, parentURL);
2007
+ let dir;
2008
+ try {
2009
+ dir = dirname(realpathSync(fileURLToPath(url)));
2010
+ } catch (error) {
2011
+ if ([
2012
+ "ENOENT",
2013
+ "ENOTDIR",
2014
+ "EISDIR"
2015
+ ].includes(String(error.code))) return void 0;
2016
+ throw error;
2017
+ }
2018
+ while (true) try {
2019
+ return readManifest(join(dir, "package.json"));
2020
+ } catch (error) {
2021
+ if (error.code !== "ENOENT") throw error;
2022
+ const parent = dirname(dir);
2023
+ if (parent === dir) return void 0;
2024
+ dir = parent;
2025
+ }
2026
+ }
2027
+ /** Prepare profile or preset rows before the owning DSH caller passes them to Loader.
2028
+ * Only a compatibility conflict denies a row: an entry whose manifest cannot be resolved keeps
2029
+ * the Loader's own import failure. A denied ordinary row gains `disabled`; a native Include that
2030
+ * reaches a denied plugin is denied as a whole, because its file is never rewritten.
2031
+ * @param ctx Context carrying launcher-owned profile facts; non-profile contexts retain their rows.
2032
+ * @param entries Complete effective entry list, after patch composition.
2033
+ * @param parentURL Resolution base of the tree that will import these rows.
2034
+ * @param binName Diagnostic prefix for a denied row; defaults to `dsh`.
2035
+ * @returns Detached rows with incompatible entries denied; reads the profile compatibility file once.
2036
+ * @throws For malformed compatibility permissions or a profile composition without a resolution base.
2037
+ */
2038
+ function prepareProfileEntries(ctx, entries, parentURL, binName = "dsh") {
2039
+ return preflight(ctx, entries, parentURL, (row, reason) => {
2040
+ const label = typeof row.id === "string" ? `row ${JSON.stringify(row.id)}` : row.name;
2041
+ process.stderr.write(`${binName}: disabling profile plugin ${label}: ${reason}\n`);
2042
+ }).rows;
2043
+ }
2044
+ function preflight(ctx, entries, parentURL, report) {
2045
+ const rows = structuredClone(entries);
2046
+ const profile = ctx.get("profileContext");
2047
+ if (profile === void 0) return {
2048
+ rows,
2049
+ blocked: false
2050
+ };
2051
+ if (parentURL === void 0) throw new Error("Profile compatibility preflight requires a resolution base");
2052
+ const { exemptions, warnings } = readProfileCompatibility(profile.dir);
2053
+ for (const warning of warnings) process.stderr.write(`${warning}\n`);
2054
+ const includes = /* @__PURE__ */ new Set();
2055
+ /** Only a compatibility conflict denies a row; every other failure keeps the Loader's own diagnosis. */
2056
+ const denial = (row, base) => {
2057
+ try {
2058
+ const manifest = manifestOf(ctx, row.name, base);
2059
+ if (manifest === void 0) return void 0;
2060
+ const issue = evaluatePluginCompatibility(manifest, exemptions);
2061
+ return issue === void 0 || issue.exempted ? void 0 : pluginCompatibilityWarning(issue);
2062
+ } catch (error) {
2063
+ return `its declared peer dependencies cannot be validated: ${error instanceof Error ? error.message : String(error)}`;
2064
+ }
2065
+ };
2066
+ const deny = (row, reason) => {
2067
+ row.disabled = true;
2068
+ if (row.group) row.group = false;
2069
+ report(row, reason);
2070
+ };
2071
+ const check = (rows, base) => {
2072
+ let blocked = false;
2073
+ for (const row of rows) {
2074
+ if (row.disabled === true && !row.group) continue;
2075
+ const conflict = denial(row, base);
2076
+ if (conflict !== void 0) {
2077
+ deny(row, conflict);
2078
+ blocked = true;
2079
+ continue;
2080
+ }
2081
+ if ((row.group === true || row.name === "cordis:group" || row.name === "@deepseek-ai/cordis-plugin-group") && Array.isArray(row.config) && check(row.config, base)) blocked = true;
2082
+ if (row.name !== "cordis:include" && row.name !== "@deepseek-ai/cordis-plugin-include") continue;
2083
+ const reached = includedConflicts(row, base);
2084
+ if (reached !== void 0) {
2085
+ deny(row, reached);
2086
+ blocked = true;
2087
+ }
2088
+ }
2089
+ return blocked;
2090
+ };
2091
+ /** Whether a native Include reaches a denied plugin; an unreadable one has no compatibility opinion. */
2092
+ const includedConflicts = (row, base) => {
2093
+ const config = row.config;
2094
+ if (typeof config?.path !== "string" || ![
2095
+ ".yml",
2096
+ ".yaml",
2097
+ ".json"
2098
+ ].includes(extname(config.path))) return;
2099
+ const requested = isAbsolute(config.path) ? config.path : fileURLToPath(new URL(config.path, base));
2100
+ const filename = existsSync(requested) ? realpathSync(requested) : requested;
2101
+ if (includes.has(filename)) return void 0;
2102
+ includes.add(filename);
2103
+ try {
2104
+ let data;
2105
+ try {
2106
+ data = load(readFileSync(filename, "utf8"), { schema: entryListSchema });
2107
+ } catch (error) {
2108
+ if (error.code !== "ENOENT" || config.initial === void 0) return void 0;
2109
+ data = config.initial;
2110
+ }
2111
+ if (!Array.isArray(data)) return void 0;
2112
+ return check(applyEntryPatches(data, config.patches, patchWarning(ctx)), pathToFileURL(filename).href) ? `its included file ${filename} reaches an incompatible plugin, and that file is never rewritten` : void 0;
2113
+ } finally {
2114
+ includes.delete(filename);
2115
+ }
2116
+ };
2117
+ return {
2118
+ rows,
2119
+ blocked: check(rows, parentURL)
2120
+ };
2121
+ }
2122
+ /** Apply compatibility policy to the complete patch composition over a profile's empty root.
2123
+ * The caller passes every patch layer of the profile, because this returns one insertion patch for
2124
+ * that empty root: a non-empty root config would lose the caller's override patches by id.
2125
+ * @param ctx Profile context prepared by the launcher.
2126
+ * @param patches Original ordered profile patches; these remain unchanged.
2127
+ * @param parentURL Root Include's resolution base.
2128
+ * @param binName Diagnostic prefix for a denied row; defaults to `dsh`.
2129
+ * @returns One prepared insertion patch for profiles, or the original patches for non-profile callers.
2130
+ */
2131
+ function prepareProfilePatches(ctx, patches, parentURL, binName = "dsh") {
2132
+ if (ctx.get("profileContext") === void 0) return patches;
2133
+ const rows = prepareProfileEntries(ctx, applyEntryPatches([], patches, patchWarning(ctx)), parentURL, binName);
2134
+ return rows.length === 0 ? [] : [{ insert: rows }];
2135
+ }
2136
+ //#endregion
1782
2137
  //#region lib/types/config-schema/native.js
1783
2138
  /** Identity checks at the plugin-export and lazy-builder boundaries. */
1784
2139
  /**
@@ -3107,9 +3462,11 @@ async function reconcileProfilePatches(ctx, patches, binName, requiredIds = [])
3107
3462
  failed: row.fiber.state === FIBER_FAILED || row.fiber.state === FIBER_DISPOSED
3108
3463
  }]);
3109
3464
  const { patches: _previous, ...includeConfig } = entry.options.config;
3465
+ const parentURL = new URL(".", new URL(includeConfig.path, entry.parent.tree.ctx.baseUrl)).href;
3466
+ const prepared = prepareProfilePatches(ctx, patches, parentURL, binName);
3110
3467
  await entry.update({ config: {
3111
3468
  ...includeConfig,
3112
- patches
3469
+ patches: prepared
3113
3470
  } });
3114
3471
  const results = await Promise.allSettled(previousFibers.map(({ fiber }) => fiber.await()));
3115
3472
  await ctx.loader.await();
@@ -3196,14 +3553,15 @@ function parsePatchList(binName, file, content, label) {
3196
3553
  return anchorInsertedPluginNames(parsed, file);
3197
3554
  }
3198
3555
  /**
3199
- * Compose the effective entry list exactly as `boot()` would mount it: parse
3200
- * the base config file with the include's entry-list dialect, apply every
3201
- * layer's patches as ONE flattened list through the include's own patch
3202
- * algorithm (`applyEntryPatches`) — the same single call `boot()` makes, so
3203
- * even patch-visibility corner cases (a later layer targeting a group child a
3204
- * plain config replacement introduced, which the single-pass id index never
3205
- * sees) compose identically — then render the result as YAML in the same
3206
- * dialect (`!!js` expressions print verbatim, unevaluated).
3556
+ * Compose the configured entry list: parse the base config file with the
3557
+ * include's entry-list dialect, apply every layer's patches as ONE flattened
3558
+ * list through the include's own patch algorithm (`applyEntryPatches`) — the
3559
+ * same call `boot()` makes, so even patch-visibility corner cases (a later
3560
+ * layer targeting a group child a plain config replacement introduced, which
3561
+ * the single-pass id index never sees) compose identically — then render the
3562
+ * result as YAML in the same dialect (`!!js` expressions print verbatim,
3563
+ * unevaluated). Row admission is a later stage: a plugin row the compatibility
3564
+ * policy denies still appears here, while a denied bundle contributes no layer.
3207
3565
  *
3208
3566
  * Every run of rows from the same file and patch layers is preceded by a `# ==` comment
3209
3567
  * naming the file that contributed the rows and any layers that patched them,
@@ -3308,11 +3666,12 @@ function groupedDump(composed, entryOrigins) {
3308
3666
  * @param patches - initial app and user patches, applied in order.
3309
3667
  * @param bareModuleBaseUrl - optional installed-host base for bare package
3310
3668
  * names; relative names continue to resolve beside the configuration file.
3669
+ * @param binName - diagnostic prefix for a profile plugin denied by compatibility policy; defaults to `dsh`.
3311
3670
  * @returns the created root Include entry, or `undefined` when a surface
3312
3671
  * disposed the whole tree (taking the Loader service with it) while the
3313
3672
  * entry creation was in flight.
3314
3673
  */
3315
- async function mountRootInclude(ctx, absoluteConfigPath, patches = [], bareModuleBaseUrl) {
3674
+ async function mountRootInclude(ctx, absoluteConfigPath, patches = [], bareModuleBaseUrl, binName = "dsh") {
3316
3675
  ctx.loader.builtins.include = bareModuleBaseUrl === void 0 ? Include : class HostResolvedRootInclude extends Include {
3317
3676
  import(name, getOuterStack) {
3318
3677
  const specifier = isAbsolute(name) ? pathToFileURL(name).href : name;
@@ -3325,12 +3684,13 @@ async function mountRootInclude(ctx, absoluteConfigPath, patches = [], bareModul
3325
3684
  }
3326
3685
  };
3327
3686
  ctx.loader.builtins.group = Group;
3687
+ const prepared = prepareProfilePatches(ctx, [...patches], pathToFileURL(dirname(absoluteConfigPath)).href + "/", binName);
3328
3688
  const rootInclude = {
3329
3689
  id: "include",
3330
3690
  name: "cordis:include",
3331
3691
  config: {
3332
3692
  path: pathToFileURL(absoluteConfigPath).href,
3333
- ...patches.length > 0 ? { patches: [...patches] } : {}
3693
+ ...prepared.length > 0 ? { patches: prepared } : {}
3334
3694
  }
3335
3695
  };
3336
3696
  const includeId = await ctx.loader.create(rootInclude);
@@ -3363,11 +3723,22 @@ async function observeLoaderRejectionCheckpoint(reasons) {
3363
3723
  */
3364
3724
  const FAIL_LOUD_RELEASE_TIMEOUT_MS = 2e3;
3365
3725
  /**
3366
- * Install before boot to turn a late unhandled plugin-init rejection into one
3367
- * labelled stderr diagnostic and `exit(1)`. A rejection already included by
3368
- * {@link auditStartupEntries} is ignored during its process checkpoint;
3369
- * every other rejection remains fatal. Stdout remains untouched for ACP; the
3370
- * returned function removes the handler.
3726
+ * Install before boot to turn an unhandled rejection or an uncaught exception,
3727
+ * at any point in the process lifetime, into one labelled stderr diagnostic and
3728
+ * `exit(1)`. A rejection already included by {@link auditStartupEntries} is
3729
+ * ignored during its process checkpoint; every other rejection and every
3730
+ * uncaught exception remains fatal. Control never returns to the failed
3731
+ * operation after either: only the throw site knows which state is intact, and
3732
+ * a listener that threw mid-update (a stream `'data'` handler, a half-applied
3733
+ * registry write) leaves silently wrong results behind if it were resumed. The
3734
+ * event loop keeps running only until the release hook settles or times out.
3735
+ * Stdout remains untouched for ACP; the returned function removes both handlers.
3736
+ *
3737
+ * The diagnostic is `util.inspect(err)`, not `err.stack`: a `node:fs` error's
3738
+ * `code`, `syscall`, and `path` and any `cause` chain are enumerable properties
3739
+ * that the stack line omits, and they are what a crash report needs. Once a
3740
+ * handler is installed Node prints nothing of its own, so this line is the
3741
+ * only record of the failure.
3371
3742
  *
3372
3743
  * The Loader mounts entries concurrently, so a surface that owns the terminal
3373
3744
  * can already hold it when a sibling entry rejects. Exiting straight from the
@@ -3389,15 +3760,17 @@ const FAIL_LOUD_RELEASE_TIMEOUT_MS = 2e3;
3389
3760
  * @param release - optional teardown awaited before exit, used by a
3390
3761
  * terminal-owning surface to restore the terminal. Its own failure is
3391
3762
  * swallowed because the pending fatal exit already owns the outcome.
3392
- * @returns the uninstaller that removes the rejection handler.
3763
+ * @returns the uninstaller that removes both handlers.
3393
3764
  */
3394
3765
  function installFailLoud(binName, proc = process, release) {
3395
3766
  let exiting = false;
3396
- const handler = (err) => {
3397
- if (assembledActivationRejections.has(err)) return;
3767
+ const report = (err, label) => {
3398
3768
  if (exiting) return;
3399
3769
  exiting = true;
3400
- proc.stderr.write(`${binName}: fatal load failure: ${err instanceof Error ? err.stack ?? err.message : String(err)}\n`);
3770
+ proc.stderr.write(`${binName}: ${label}: ${inspect(err, {
3771
+ depth: 4,
3772
+ maxArrayLength: 50
3773
+ })}\n`);
3401
3774
  if (release === void 0) {
3402
3775
  proc.exit(1);
3403
3776
  return;
@@ -3413,8 +3786,19 @@ function installFailLoud(binName, proc = process, release) {
3413
3786
  proc.exit(1);
3414
3787
  })();
3415
3788
  };
3416
- const uninstall = () => void proc.off("unhandledRejection", handler);
3417
- proc.on("unhandledRejection", handler);
3789
+ const onRejection = (err) => {
3790
+ if (assembledActivationRejections.has(err)) return;
3791
+ report(err, "fatal load failure");
3792
+ };
3793
+ const onException = (err) => {
3794
+ report(err, "fatal uncaught exception");
3795
+ };
3796
+ const uninstall = () => {
3797
+ proc.off("unhandledRejection", onRejection);
3798
+ proc.off("uncaughtException", onException);
3799
+ };
3800
+ proc.on("unhandledRejection", onRejection);
3801
+ proc.on("uncaughtException", onException);
3418
3802
  return uninstall;
3419
3803
  }
3420
3804
  /**
@@ -3679,7 +4063,7 @@ async function boot(binName, absoluteConfigPath, patches, prepare, bareModuleBas
3679
4063
  await ctx.plugin(Loader);
3680
4064
  await prepare?.(ctx);
3681
4065
  stage = "plugin tree failed to load";
3682
- await mountRootInclude(ctx, absoluteConfigPath, patches, bareModuleBaseUrl);
4066
+ await mountRootInclude(ctx, absoluteConfigPath, patches, bareModuleBaseUrl, binName);
3683
4067
  await ctx.get("loader")?.await();
3684
4068
  if (ctx.get("loader") === void 0) return ctx;
3685
4069
  await auditStartupEntries(ctx, binName);
@@ -3733,4 +4117,4 @@ function addHarnessSourceSection(ctx, sourceRoot) {
3733
4117
  });
3734
4118
  }
3735
4119
  //#endregion
3736
- export { DEFAULT_PROFILE_BUNDLES, FAIL_LOUD_RELEASE_TIMEOUT_MS, HARNESS_SOURCE_SECTION, LOADER_EXPRESSION_SCHEMA, OPTIONAL_BUNDLES, PROFILES_DIR, PROFILE_PATCH_FILENAME, PROFILE_TEMPLATES, PluginPackages, StartupError, addHarnessSourceSection, auditStartupEntries, boot, bundlePatchFiles, bundlePatchPaths, composeEntries, createConfigProjector, createRuntimeResolution, generateConfigSchema, initProfile, installFailLoud, isNativeConfigSchema, loadEnv, loadLayeredEnv, loadOptionalPatches, loadOverlayPatches, loadProfile, loadProfileDirectory, mountRootInclude, readPluginMeta, readProfileManifest, readProfilePatches, readProfilePlugins, reconcileProfilePatches, reconcileProfilePlugins, removeLinkProjections, renderConfigDump, resolveBundleDir, resolveConfigPath, resolveProfileDir, resolveTelemetryPatch, sanitizeProfile, writeProfileBundles, writeProfileManifest };
4120
+ export { DEFAULT_PROFILE_BUNDLES, FAIL_LOUD_RELEASE_TIMEOUT_MS, HARNESS_SOURCE_SECTION, LOADER_EXPRESSION_SCHEMA, OPTIONAL_BUNDLES, PROFILES_DIR, PROFILE_COMPATIBILITY_FILENAME, PROFILE_PATCH_FILENAME, PROFILE_TEMPLATES, PluginPackages, StartupError, addHarnessSourceSection, auditStartupEntries, boot, bundlePatchFiles, bundlePatchPaths, composeEntries, createConfigProjector, createRuntimeResolution, evaluatePluginCompatibility, generateConfigSchema, getDshRuntimeVersion, initProfile, installFailLoud, isNativeConfigSchema, loadEnv, loadLayeredEnv, loadOptionalPatches, loadOverlayPatches, loadProfile, loadProfileDirectory, mountRootInclude, pluginCompatibilityWarning, prepareProfileEntries, prepareProfilePatches, readPluginMeta, readProfileCompatibility, readProfileManifest, readProfilePatches, readProfilePlugins, readProfileVersionExemptions, reconcileProfilePatches, reconcileProfilePlugins, removeLinkProjections, renderConfigDump, resolveBundleDir, resolveConfigPath, resolveProfileDir, resolveTelemetryPatch, sanitizeProfile, setProfileVersionExemption, writeProfileBundles, writeProfileManifest };
@@ -0,0 +1,26 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ import type { EntryOptions } from '@deepseek-ai/cordis-plugin-loader';
3
+ import { type PatchOptions } from '@deepseek-ai/cordis-plugin-include';
4
+ /** Prepare profile or preset rows before the owning DSH caller passes them to Loader.
5
+ * Only a compatibility conflict denies a row: an entry whose manifest cannot be resolved keeps
6
+ * the Loader's own import failure. A denied ordinary row gains `disabled`; a native Include that
7
+ * reaches a denied plugin is denied as a whole, because its file is never rewritten.
8
+ * @param ctx Context carrying launcher-owned profile facts; non-profile contexts retain their rows.
9
+ * @param entries Complete effective entry list, after patch composition.
10
+ * @param parentURL Resolution base of the tree that will import these rows.
11
+ * @param binName Diagnostic prefix for a denied row; defaults to `dsh`.
12
+ * @returns Detached rows with incompatible entries denied; reads the profile compatibility file once.
13
+ * @throws For malformed compatibility permissions or a profile composition without a resolution base.
14
+ */
15
+ export declare function prepareProfileEntries(ctx: Context, entries: readonly EntryOptions[], parentURL: string | undefined, binName?: string): EntryOptions[];
16
+ /** Apply compatibility policy to the complete patch composition over a profile's empty root.
17
+ * The caller passes every patch layer of the profile, because this returns one insertion patch for
18
+ * that empty root: a non-empty root config would lose the caller's override patches by id.
19
+ * @param ctx Profile context prepared by the launcher.
20
+ * @param patches Original ordered profile patches; these remain unchanged.
21
+ * @param parentURL Root Include's resolution base.
22
+ * @param binName Diagnostic prefix for a denied row; defaults to `dsh`.
23
+ * @returns One prepared insertion patch for profiles, or the original patches for non-profile callers.
24
+ */
25
+ export declare function prepareProfilePatches(ctx: Context, patches: PatchOptions[], parentURL: string, binName?: string): PatchOptions[];
26
+ //# sourceMappingURL=compatibility-preflight.d.ts.map
@@ -12,6 +12,9 @@ import { dshHomePath } from '@deepseek-ai/dsh-home-paths';
12
12
  import { type LaunchEnvironmentSnapshot } from '@deepseek-ai/dsh-launch-environment';
13
13
  export { readProfilePatches, resolveTelemetryPatch, type ProfileContext, type ProfilePnpmInvocation } from './profile-context.ts';
14
14
  export { sanitizeProfile } from './profile-sanitize.ts';
15
+ export { getDshRuntimeVersion, evaluatePluginCompatibility, pluginCompatibilityWarning, type PluginCompatibility } from './plugin-compatibility.ts';
16
+ export { PROFILE_COMPATIBILITY_FILENAME, readProfileCompatibility, readProfileVersionExemptions, setProfileVersionExemption, type ProfileCompatibility, } from './profile-compatibility.ts';
17
+ export { prepareProfileEntries, prepareProfilePatches } from './compatibility-preflight.ts';
15
18
  export { readPluginMeta } from './package-meta.ts';
16
19
  export { generateConfigSchema, type ConfigSchemaDump, type NativeConfigSchema } from './config-schema/index.ts';
17
20
  export { createConfigProjector, LOADER_EXPRESSION_SCHEMA, type ConfigProjection } from './config-schema/projector.ts';
@@ -101,14 +104,15 @@ export interface ConfigDumpLayer {
101
104
  patches: PatchOptions[];
102
105
  }
103
106
  /**
104
- * Compose the effective entry list exactly as `boot()` would mount it: parse
105
- * the base config file with the include's entry-list dialect, apply every
106
- * layer's patches as ONE flattened list through the include's own patch
107
- * algorithm (`applyEntryPatches`) — the same single call `boot()` makes, so
108
- * even patch-visibility corner cases (a later layer targeting a group child a
109
- * plain config replacement introduced, which the single-pass id index never
110
- * sees) compose identically — then render the result as YAML in the same
111
- * dialect (`!!js` expressions print verbatim, unevaluated).
107
+ * Compose the configured entry list: parse the base config file with the
108
+ * include's entry-list dialect, apply every layer's patches as ONE flattened
109
+ * list through the include's own patch algorithm (`applyEntryPatches`) — the
110
+ * same call `boot()` makes, so even patch-visibility corner cases (a later
111
+ * layer targeting a group child a plain config replacement introduced, which
112
+ * the single-pass id index never sees) compose identically — then render the
113
+ * result as YAML in the same dialect (`!!js` expressions print verbatim,
114
+ * unevaluated). Row admission is a later stage: a plugin row the compatibility
115
+ * policy denies still appears here, while a denied bundle contributes no layer.
112
116
  *
113
117
  * Every run of rows from the same file and patch layers is preceded by a `# ==` comment
114
118
  * naming the file that contributed the rows and any layers that patched them,
@@ -139,18 +143,21 @@ export declare function renderConfigDump(binName: string, absoluteConfigPath: st
139
143
  * @param patches - initial app and user patches, applied in order.
140
144
  * @param bareModuleBaseUrl - optional installed-host base for bare package
141
145
  * names; relative names continue to resolve beside the configuration file.
146
+ * @param binName - diagnostic prefix for a profile plugin denied by compatibility policy; defaults to `dsh`.
142
147
  * @returns the created root Include entry, or `undefined` when a surface
143
148
  * disposed the whole tree (taking the Loader service with it) while the
144
149
  * entry creation was in flight.
145
150
  */
146
- export declare function mountRootInclude(ctx: Context, absoluteConfigPath: string, patches?: readonly PatchOptions[], bareModuleBaseUrl?: string): Promise<Entry | undefined>;
151
+ export declare function mountRootInclude(ctx: Context, absoluteConfigPath: string, patches?: readonly PatchOptions[], bareModuleBaseUrl?: string, binName?: string): Promise<Entry | undefined>;
152
+ /** The two process events {@link installFailLoud} turns into a fatal exit. */
153
+ export type FailLoudEvent = 'unhandledRejection' | 'uncaughtException';
147
154
  /**
148
155
  * The slice of `process` {@link installFailLoud} needs — injectable so tests
149
156
  * exercise the handler without registering on (or exiting) the real process.
150
157
  */
151
158
  export interface FailLoudProcess {
152
- on(event: 'unhandledRejection', handler: (err: unknown) => void): unknown;
153
- off(event: 'unhandledRejection', handler: (err: unknown) => void): unknown;
159
+ on(event: FailLoudEvent, handler: (err: unknown) => void): unknown;
160
+ off(event: FailLoudEvent, handler: (err: unknown) => void): unknown;
154
161
  stderr: {
155
162
  write(chunk: string): unknown;
156
163
  };
@@ -167,11 +174,22 @@ export interface FailLoudProcess {
167
174
  */
168
175
  export declare const FAIL_LOUD_RELEASE_TIMEOUT_MS = 2000;
169
176
  /**
170
- * Install before boot to turn a late unhandled plugin-init rejection into one
171
- * labelled stderr diagnostic and `exit(1)`. A rejection already included by
172
- * {@link auditStartupEntries} is ignored during its process checkpoint;
173
- * every other rejection remains fatal. Stdout remains untouched for ACP; the
174
- * returned function removes the handler.
177
+ * Install before boot to turn an unhandled rejection or an uncaught exception,
178
+ * at any point in the process lifetime, into one labelled stderr diagnostic and
179
+ * `exit(1)`. A rejection already included by {@link auditStartupEntries} is
180
+ * ignored during its process checkpoint; every other rejection and every
181
+ * uncaught exception remains fatal. Control never returns to the failed
182
+ * operation after either: only the throw site knows which state is intact, and
183
+ * a listener that threw mid-update (a stream `'data'` handler, a half-applied
184
+ * registry write) leaves silently wrong results behind if it were resumed. The
185
+ * event loop keeps running only until the release hook settles or times out.
186
+ * Stdout remains untouched for ACP; the returned function removes both handlers.
187
+ *
188
+ * The diagnostic is `util.inspect(err)`, not `err.stack`: a `node:fs` error's
189
+ * `code`, `syscall`, and `path` and any `cause` chain are enumerable properties
190
+ * that the stack line omits, and they are what a crash report needs. Once a
191
+ * handler is installed Node prints nothing of its own, so this line is the
192
+ * only record of the failure.
175
193
  *
176
194
  * The Loader mounts entries concurrently, so a surface that owns the terminal
177
195
  * can already hold it when a sibling entry rejects. Exiting straight from the
@@ -193,7 +211,7 @@ export declare const FAIL_LOUD_RELEASE_TIMEOUT_MS = 2000;
193
211
  * @param release - optional teardown awaited before exit, used by a
194
212
  * terminal-owning surface to restore the terminal. Its own failure is
195
213
  * swallowed because the pending fatal exit already owns the outcome.
196
- * @returns the uninstaller that removes the rejection handler.
214
+ * @returns the uninstaller that removes both handlers.
197
215
  */
198
216
  export declare function installFailLoud(binName: string, proc?: FailLoudProcess, release?: () => Promise<void> | void): () => void;
199
217
  interface InactiveEntry {
@@ -0,0 +1,35 @@
1
+ /** Evaluate plugin dsh peer requirements without importing plugin code. */
2
+ /** Incompatible dsh peers and the exact plugin/runtime exemption decision. */
3
+ export interface PluginCompatibility {
4
+ name: string;
5
+ version: string;
6
+ runtimeVersion: string;
7
+ /** Only peer requirements not satisfied by the running dsh version. */
8
+ peers: Record<string, string>;
9
+ exempted: boolean;
10
+ }
11
+ /**
12
+ * Read this app-boot package's version in both source and bundled installations.
13
+ * @returns the validated runtime semantic version, preserving its exact spelling.
14
+ * @throws if package.json cannot be read or its version is missing or invalid.
15
+ */
16
+ export declare function getDshRuntimeVersion(): string;
17
+ /**
18
+ * Check every @deepseek-ai/dsh or @deepseek-ai/dsh-* peer against the runtime.
19
+ * Prereleases participate in ranges. workspace:^, workspace:~, and workspace:*
20
+ * refer to the current runtime; other invalid ranges are incompatible.
21
+ * @param manifest - parsed plugin package.json; inherited fields are ignored.
22
+ * @param exemptions - exact plugin name@version keys mapped to exact runtime versions.
23
+ * @param runtimeVersion - running dsh version, defaulting to this app-boot package.
24
+ * @returns incompatible peers and exemption status, or undefined when none are incompatible.
25
+ * @throws for malformed manifest peer fields, invalid runtime versions, or missing identity on a mismatch.
26
+ */
27
+ export declare function evaluatePluginCompatibility(manifest: object, exemptions?: Readonly<Record<string, readonly string[]>>, runtimeVersion?: string): PluginCompatibility | undefined;
28
+ /**
29
+ * Describe incompatible peers, their risk, and the exact-version remedy.
30
+ * Surfaces with their own grant mechanism or locale render the structured result themselves.
31
+ * @param issue - incompatible plugin/runtime result, including exempted mismatches.
32
+ * @returns an English diagnostic for logs and stderr.
33
+ */
34
+ export declare function pluginCompatibilityWarning(issue: PluginCompatibility): string;
35
+ //# sourceMappingURL=plugin-compatibility.d.ts.map
@@ -0,0 +1,50 @@
1
+ /** Independent profile metadata; neither package manifests nor Cordis patches carry grants. */
2
+ export declare const PROFILE_COMPATIBILITY_FILENAME = "compatibility.json";
3
+ /** Test whether an exemption names a canonical exact SemVer, including build metadata.
4
+ * @param value Version supplied by a manifest or user.
5
+ * @returns False for ranges, prefixes, whitespace, and malformed versions.
6
+ */
7
+ export declare function isExactPluginVersion(value: string): boolean;
8
+ /** Validate one explicit exemption without granting it.
9
+ * @param packageVersion Exact npm package-name@version, never an installation spec or range.
10
+ * @param runtimeVersion Exact DSH version, including prerelease and build metadata.
11
+ * @throws When either identity is not canonical.
12
+ */
13
+ export declare function validatePluginVersionExemption(packageVersion: string, runtimeVersion: string): void;
14
+ /** What one profile's compatibility file currently authorizes, and what is wrong with it. */
15
+ export interface ProfileCompatibility {
16
+ /** Accepted exact package-name@version keys mapped to their allowed DSH versions. */
17
+ readonly exemptions: Record<string, string[]>;
18
+ /** Human-readable problems; empty when every record was accepted. */
19
+ readonly warnings: string[];
20
+ /**
21
+ * Whether the file holds nothing this reader rejected, so a grant or revocation may rewrite it.
22
+ * A false value means writing would discard content the user must repair by hand.
23
+ */
24
+ readonly rewritable: boolean;
25
+ }
26
+ /** Read the profile's independent compatibility file without loading plugins.
27
+ * A missing file authorizes nothing. An unreadable or unparsable file authorizes nothing and is
28
+ * reported instead of failing, so a bad file can never make the profile unusable; rejected records
29
+ * are skipped while the remaining valid ones still apply.
30
+ * @param profileDir Absolute profile directory.
31
+ * @returns Accepted exemptions plus every problem found; no manifest fallback is used.
32
+ */
33
+ export declare function readProfileCompatibility(profileDir: string): ProfileCompatibility;
34
+ /** Read only the accepted exemptions of a profile.
35
+ * @param profileDir Absolute profile directory.
36
+ * @returns Exact package-name@version keys mapped to their allowed DSH versions.
37
+ */
38
+ export declare function readProfileVersionExemptions(profileDir: string): Record<string, string[]>;
39
+ /** Persist one informed grant or revocation under the compatibility file's own lock.
40
+ * @param profileDir Profile directory; no package manifest is created or modified.
41
+ * @param packageVersion Exact manifest package-name@version.
42
+ * @param runtimeVersion Exact DSH version; grants must name the current runtime, revocations may name historical ones.
43
+ * @param enabled Whether to grant rather than revoke.
44
+ * @param acceptRisk Required true for grants after explicit acknowledgement of possible crashes or data loss.
45
+ * @returns After the atomic write. Existing plugin instances are not reloaded by this operation.
46
+ * @throws For invalid identities, missing consent, a stale runtime, or a file the reader rejected,
47
+ * which the user must repair by hand because rewriting it would discard their content.
48
+ */
49
+ export declare function setProfileVersionExemption(profileDir: string, packageVersion: string, runtimeVersion: string, enabled: boolean, acceptRisk: boolean): Promise<void>;
50
+ //# sourceMappingURL=profile-compatibility.d.ts.map
@@ -196,7 +196,8 @@ export declare function resolveBundleDir(binName: string, packageName: string, i
196
196
  * Load an already initialized profile directory without resolving it through
197
197
  * the shared Harness home. This is used by application-owned profiles whose
198
198
  * package project and lifecycle belong to that application.
199
- * Unreadable bundles are reported on stderr and skipped without changing the manifest.
199
+ * Unreadable bundles, and bundles whose own dsh peers the profile does not exempt, are reported
200
+ * on stderr and skipped without changing the manifest.
200
201
  * @param binName - the diagnostic prefix on thrown errors.
201
202
  * @param dir - absolute profile package directory.
202
203
  * @param installAnchor - absolute path of the owning dsh app's package.json.
@@ -208,8 +209,8 @@ export declare function loadProfileDirectory(binName: string, dir: string, insta
208
209
  }): Profile;
209
210
  /**
210
211
  * Load a profile: resolve every `dsh.profile.bundles` entry to its patch
211
- * layer and parse the profile's own patch file. Unreadable bundles are reported
212
- * on stderr and skipped; profile manifest and user patch errors still throw.
212
+ * layer and parse the profile's own patch file. Unreadable or incompatible bundles
213
+ * are reported on stderr and skipped; profile manifest and user patch errors still throw.
213
214
  * @param binName - the diagnostic prefix on thrown errors.
214
215
  * @param name - the profile name.
215
216
  * @param installAnchor - absolute path of the dsh app's package.json (first resolution anchor).
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@deepseek-ai/dsh-app-boot",
3
3
  "description": "Shared boot glue for the app bins: .env loading, fail-loud Loader guards, snapshot-aware config resolution, and the Loader boot sequence",
4
- "version": "0.1.7-alpha.1",
4
+ "version": "0.1.7-rc.1",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -37,27 +37,30 @@
37
37
  "js-yaml": "^4.2.0",
38
38
  "node-addon-require-builtin": "^0.1.6",
39
39
  "resolve.exports": "^2.0.3",
40
- "@deepseek-ai/dsh-package-manifest": "^0.1.7-alpha.1"
40
+ "semver": "^7.8.5",
41
+ "@deepseek-ai/dsh-atomic-write": "0.1.7-rc.1",
42
+ "@deepseek-ai/dsh-package-manifest": "0.1.7-rc.1"
41
43
  },
42
44
  "peerDependencies": {
43
- "@deepseek-ai/cordis-plugin-include": "^1.0.8",
44
- "@deepseek-ai/cordis-plugin-group": "^1.0.3",
45
- "@deepseek-ai/dsh-launch-environment": "^0.1.7-alpha.1",
46
- "@deepseek-ai/dsh-system-prompt": "^0.1.7-alpha.1",
47
- "@deepseek-ai/cordis": "^4.0.3",
48
- "@deepseek-ai/dsh-home-paths": "^0.1.7-alpha.1",
49
- "@deepseek-ai/cordis-plugin-loader": "^1.0.4"
45
+ "@deepseek-ai/cordis-plugin-include": "~1.0.9",
46
+ "@deepseek-ai/cordis-plugin-loader": "~1.0.5",
47
+ "@deepseek-ai/dsh-launch-environment": "0.1.7-rc.1",
48
+ "@deepseek-ai/dsh-home-paths": "0.1.7-rc.1",
49
+ "@deepseek-ai/dsh-system-prompt": "0.1.7-rc.1",
50
+ "@deepseek-ai/cordis": "~4.0.4",
51
+ "@deepseek-ai/cordis-plugin-group": "~1.0.4"
50
52
  },
51
53
  "devDependencies": {
52
54
  "@types/js-yaml": "^4.0.9",
53
- "@deepseek-ai/schemastery": "^3.18.3",
54
- "@deepseek-ai/cordis-plugin-group": "^1.0.3",
55
- "@deepseek-ai/cordis-plugin-include": "^1.0.8",
56
- "@deepseek-ai/cordis-plugin-loader": "^1.0.4",
57
- "@deepseek-ai/dsh-launch-environment": "^0.1.7-alpha.1",
58
- "@deepseek-ai/cordis-plugin-timer": "^1.1.5",
59
- "@deepseek-ai/dsh-home-paths": "^0.1.7-alpha.1",
60
- "@deepseek-ai/dsh-system-prompt": "^0.1.7-alpha.1",
61
- "@deepseek-ai/cordis": "^4.0.3"
55
+ "@types/semver": "^7.8.0",
56
+ "@deepseek-ai/schemastery": "~3.18.4",
57
+ "@deepseek-ai/cordis-plugin-group": "~1.0.4",
58
+ "@deepseek-ai/cordis-plugin-loader": "~1.0.5",
59
+ "@deepseek-ai/cordis-plugin-timer": "~1.1.6",
60
+ "@deepseek-ai/dsh-launch-environment": "0.1.7-rc.1",
61
+ "@deepseek-ai/cordis-plugin-include": "~1.0.9",
62
+ "@deepseek-ai/dsh-home-paths": "0.1.7-rc.1",
63
+ "@deepseek-ai/dsh-system-prompt": "0.1.7-rc.1",
64
+ "@deepseek-ai/cordis": "~4.0.4"
62
65
  }
63
66
  }