@tokensapi/dsh-plugin-check 0.3.2 → 0.3.4

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.md CHANGED
@@ -1,91 +1,114 @@
1
- # dsh-plugin-check
2
-
3
- TokensCowork / DSH 插件合格性体检。给所有插件开发者的统一标准:一条命令,当场知道你的插件**能否安全装入宿主、会不会把用户的 Cowork 搞崩**。同一套规则同时用于:开发者本地自查、插件仓库 CI、插件市场上架门禁。
4
-
5
- ## 用法
6
-
7
- ```bash
8
- # 在插件目录里
9
- npx @tokensapi/dsh-plugin-check
10
-
11
- # 指定目标运行时(启用兼容范围判定;当前产品运行时版本见市场公告)
12
- npx @tokensapi/dsh-plugin-check --runtime 0.1.3-alpha.1
13
-
14
- # 只跑离线清单体检(秒级,无网络)
15
- npx @tokensapi/dsh-plugin-check --skip-smoke
16
-
17
- # 机器可读输出(CI / 市场门禁)
18
- npx @tokensapi/dsh-plugin-check --json
19
-
20
- # 体检一个已发布的 npm 包(市场上架审核用)
21
- npx @tokensapi/dsh-plugin-check --package @scope/name@1.2.3 --runtime 0.1.3-alpha.1
22
- ```
23
-
24
- 退出码:`0` 合格,`1` 存在 error(`--strict` 时 warning 也计入)。冒烟失败时加 `--keep-workspace` 保留临时工作区排查。
25
-
26
- ## 体检分两个阶段
27
-
28
- **阶段① 清单体检**(纯静态、离线、秒级):检查 package.json 与 cordis 补丁的声明是否合规。
29
- **阶段② 隔离启动冒烟**(联网、约 1–3 分钟):在临时工作区按你声明的 peer 范围装出真实运行时,然后逐补丁行做真实网关启动时做的事——解析入口 → `import` → `new Context()` + `ctx.plugin()` 应用。三段都过,你的插件就不会以"装上即崩"的方式杀死用户的 Cowork。全程 `--ignore-scripts`,你的代码只在一次性子进程里运行,超时即杀。
30
-
31
- 清单存在 error 时冒烟不执行——先修再跑。
32
-
33
- ## 规则清单
34
-
35
- 每条规则都对应一种真实发生过的宿主故障。
36
-
37
- | 规则 | 级别 | 内容 | 拦的是什么事故 |
38
- |---|---|---|---|
39
- | M1-manifest | error/warning | package.json 合法,name/version 齐全;预发布版本给出上架提示 | 无法入库 |
40
- | M2-lifecycle | error | 禁止 `preinstall/install/postinstall/prepare` 脚本(构建用 `prepack`) | 安装即执行任意代码;市场受控安装的既有红线 |
41
- | M3-core-peer | error | `@deepseek-ai/cordis*`、`@deepseek-ai/dsh*` 只能是 peerDependencies | 双内核实例 Symbol 不等 → 服务注册对不上 → **Cowork 启动失败** |
42
- | M4-bundle | error | `dsh.bundle.patch` 必填、文件存在、能解析出插件行 | 宿主无法把插件挂进加载树 |
43
- | M5-row-scope | error | 补丁行只能指向本包(或其子路径) | 插件行劫持挂载其他包 |
44
- | M6-engine | error/warning | `dsh.engine` 声明目标运行时 SemVer 范围;与 `--runtime` 不相交为 error(未声明当前为 warning,将转必填) | 装进不适配的宿主版本 |
45
- | M7-peer-range | warning | `dsh-*` peer 范围应包含目标运行时 | 宿主升级后接口错配,运行时断裂 |
46
- | M8-node-engines | warning | 建议声明 `engines.node`(宿主为 Node 22+) | 语法/API 不可用 |
47
- | M9-license | warning | 建议声明 license | 分发合规 |
48
- | M10-secrets | error/warning | 发布内容不得包含 `.env`、私钥等凭据样式文件 | 凭据泄露 |
49
- | S1-pack | error | `npm pack --ignore-scripts` 必须成功 | 包本身发布不出来 |
50
- | S2-install | error/warning | 按你声明的依赖必须装得出来;宿主内核包(`@deepseek-ai/*`)的内部版本在公开源取不到时降级为 warning 并跳过冒烟 | 用户受控安装会同样失败 |
51
- | S3/S4-apply | error | 每个补丁行:入口可解析、`import` 不抛、`ctx.plugin()` 应用不抛(声明 `inject` 等待服务注入属正常,不算失败) | **启动链击穿**——一行 import 失败会拖死整棵插件树 |
52
-
53
- ## 会话内体检(装进 Cowork)
54
-
55
- 本包同时是一个 Cowork 插件。装进宿主后会注册 `plugin_check` 工具,在会话里直接问"这个插件能装吗":
56
-
57
- ```
58
- plugin_check(package="@scope/name", version="1.2.3")
59
- ```
60
-
61
- 会话内跑的是**清单规则 + 包内补丁行检查**:工具会把已发布的 tarball 取下来,解出 `cordis.patch.yml` 真读补丁行——只看 registry 清单会漏掉"补丁行 `name` 写成 cordis 插件名而不是 npm 包名"这类装上即让整棵插件树崩溃的缺陷。取不到包时如实标注补丁未检查(warning),不会因网络问题把插件判成不合格。
62
-
63
- 隔离启动冒烟无法在宿主进程里执行,完整体检仍走 CLI 或 Plugin Check workflow。
64
-
65
- ## `dsh.engine` 字段
66
-
67
- 在 package.json 声明你适配的 DSH 运行时范围:
68
-
69
- ```json
70
- {
71
- "dsh": {
72
- "engine": ">=0.1.3-alpha.1 <0.2.0",
73
- "bundle": { "patch": "./cordis.patch.yml" }
74
- }
75
- }
76
- ```
77
-
78
- 宿主升级越过你的范围时,市场会把你的插件标记为"待适配"而不是硬载崩溃。
79
-
80
- ## CI 集成示例
81
-
82
- ```yaml
83
- # .github/workflows/check.yml
84
- - run: npx @tokensapi/dsh-plugin-check --runtime 0.1.3-alpha.1 --strict
85
- ```
86
-
87
- ## 上架流程中的位置
88
-
89
- 1. 开发者:发布 npm 版本前本地跑体检,全绿再发。
90
- 2. 管理员:在仓库 Actions 的 **Plugin Check** workflow 填「包名@版本」手动触发,体检报告生成在该次运行的 Summary。
91
- 3. 上架:后台 publish 对话框把这次运行的链接填入「检查记录链接」——链接的红/绿即审核证据,`reviewed_version` 随上架落库。存在 error 的版本不满足上架条件。
1
+ # dsh-plugin-check
2
+
3
+ TokensCowork / DSH 插件合格性体检。给所有插件开发者的统一标准:一条命令,当场知道你的插件**能否安全装入宿主、会不会把用户的 Cowork 搞崩**。同一套规则同时用于:开发者本地自查、插件仓库 CI、插件市场上架门禁。
4
+
5
+ ## 用法
6
+
7
+ ```bash
8
+ # 在插件目录里
9
+ npx @tokensapi/dsh-plugin-check
10
+
11
+ # 指定目标运行时(启用兼容范围判定;当前产品运行时版本见市场公告)
12
+ npx @tokensapi/dsh-plugin-check --runtime 0.1.3-alpha.1
13
+
14
+ # 只跑离线清单体检(秒级,无网络)
15
+ npx @tokensapi/dsh-plugin-check --skip-smoke
16
+
17
+ # 机器可读输出(CI / 市场门禁)
18
+ npx @tokensapi/dsh-plugin-check --json
19
+
20
+ # 体检一个已发布的 npm 包(市场上架审核用)
21
+ npx @tokensapi/dsh-plugin-check --package @scope/name@1.2.3 --runtime 0.1.3-alpha.1
22
+ ```
23
+
24
+ 退出码:`0` 合格,`1` 存在 error(`--strict` 时 warning 也计入)。冒烟失败时加 `--keep-workspace` 保留临时工作区排查。
25
+
26
+ ## 体检分两个阶段
27
+
28
+ **阶段① 清单体检**(纯静态、离线、秒级):检查 package.json 与 cordis 补丁的声明是否合规。
29
+ **阶段② 隔离启动冒烟**(联网、约 1–3 分钟):在临时工作区按你声明的 peer 范围装出真实运行时,然后逐补丁行做真实网关启动时做的事——解析入口 → `import` → `new Context()` + `ctx.plugin()` 应用。三段都过,你的插件就不会以"装上即崩"的方式杀死用户的 Cowork。全程 `--ignore-scripts`,你的代码只在一次性子进程里运行,超时即杀。
30
+
31
+ 清单存在 error 时冒烟不执行——先修再跑。
32
+
33
+ ## 规则清单
34
+
35
+ 每条规则都对应一种真实发生过的宿主故障。**每条规则在判什么、为什么是这个级别、锚在上游哪一段代码,完整写在 [`docs/CHECKS.md`](docs/CHECKS.md)。**
36
+
37
+ | 规则 | 级别 | 内容 | 拦的是什么事故 |
38
+ |---|---|---|---|
39
+ | C0-contract | warning | `--runtime` 不在本工具核对过的版本里时提示 | 拿过时的上游口径当结论 |
40
+ | M1-manifest | error/warning | package.json 合法,name/version 齐全;预发布版本给出上架提示 | 无法入库 |
41
+ | M2-lifecycle | warning | 不要声明 `preinstall/install/postinstall/prepare`(构建用 `prepack`) | 受控安装未加 `--ignore-scripts`:脚本要么在用户机器上执行任意代码,要么被包管理器默认策略拦下、脚本产物缺失导致装完即坏 |
42
+ | M3-core-peer | error | `@deepseek-ai/cordis*`、`@deepseek-ai/dsh*` 只能是 peerDependencies | 双内核实例 → Symbol 不等 → 服务注册对不上 → **Cowork 启动失败** |
43
+ | M4-bundle | error | `dsh.bundle.patch` 必填、文件存在、能解析出插件行 | 宿主无法把插件挂进加载树 |
44
+ | M5-row-scope | error | 补丁行只能指向本包(或其子路径) | 插件行劫持挂载其他包 |
45
+ | M6-engine | error/warning | `dsh.engine` 声明目标运行时 SemVer 范围;与 `--runtime` 不相交为 error(未声明为 warning——上游目前不强制读取此字段) | 装进不适配的宿主版本 |
46
+ | M7-peer-range | warning | `dsh-*` peer 范围应包含目标运行时 | 宿主升级后接口错配,运行时断裂 |
47
+ | M8-node-engines | warning | 建议声明 `engines.node`(宿主为 Node 22+) | 语法/API 不可用 |
48
+ | M9-license | warning | 建议声明 license | 分发合规 |
49
+ | M10-secrets | error/warning | 发布内容不得包含 `.env`、私钥等凭据样式文件 | 凭据泄露 |
50
+ | S1-pack | error | `npm pack --ignore-scripts` 必须成功 | 包本身发布不出来 |
51
+ | S2-install | error/warning | 按你声明的依赖必须装得出来;宿主内核包(`@deepseek-ai/*`)的内部版本在公开源取不到时降级为 warning 并跳过冒烟 | 用户受控安装会同样失败 |
52
+ | S3/S4-apply | error | 每个补丁行:入口可解析、`import` 不抛、`ctx.plugin()` 应用不抛(声明 `inject` 等待服务注入属正常,不算失败) | **启动链击穿**——一行 import 失败会拖死整棵插件树 |
53
+
54
+ ## 适配的上游版本(契约基线)
55
+
56
+ 规则不是写法偏好,是照着**某个上游版本的真实行为**写下的判定。上游一变,这个检查器
57
+ 可能要改,也可能完全不用改 —— 所以基线是机器可读的,写在 `lib/contract.mjs`,并跟着
58
+ 每次体检结果一起输出:CLI 抬头一行、`--json` 的 `contract` 字段、会话内工具结果的
59
+ `contract` 字段。
60
+
61
+ ```
62
+ 契约基线 2026-09-10 · desktop 2.0.5 / market 0.1.0-dev.0 · 已核对运行时 0.1.0-rc.8、0.1.3-alpha.1
63
+ ```
64
+
65
+ `--runtime` 传了核对范围之外的版本,会多一条 **C0-contract** warning,明说"这个版本
66
+ 没核对过,结论可能过时或过严",不假装照样有效。
67
+
68
+ 每条规则锚在上游哪个文件/符号、上游升级后逐条怎么复核,见
69
+ [`docs/CHECKS.md` §5](docs/CHECKS.md)。
70
+
71
+ ## 会话内体检(装进 Cowork)
72
+
73
+ 本包同时是一个 Cowork 插件。装进宿主后会注册 `plugin_check` 工具,在会话里直接问"这个插件能装吗",不必记命令行:
74
+
75
+ ```
76
+ # 正在开发、还没发布的插件 —— 直接读工作区
77
+ plugin_check(path="D:/code/my-plugin")
78
+
79
+ # 已经发布到 npm 的包
80
+ plugin_check(package="@scope/name", version="1.2.3")
81
+ ```
82
+
83
+ **发版前用 `path`**:读的是你本地的 `package.json` 与 `cordis.patch.yml`,不需要先 `npm publish`,改一行问一次都行。已发布的包用 `package`,跑的是**清单规则 + 包内补丁行检查**:工具会把已发布的 tarball 取下来,解出 `cordis.patch.yml` 真读补丁行——只看 registry 清单会漏掉"补丁行 `name` 写成 cordis 插件名而不是 npm 包名"这类装上即让整棵插件树崩溃的缺陷。取不到包时如实标注补丁未检查(warning),不会因网络问题把插件判成不合格。
84
+
85
+ 隔离启动冒烟要装依赖、起子进程,不在宿主进程里执行;完整体检仍走 CLI 或 Plugin Check workflow。
86
+
87
+ ## `dsh.engine` 字段
88
+
89
+ 在 package.json 声明你适配的 DSH 运行时范围:
90
+
91
+ ```json
92
+ {
93
+ "dsh": {
94
+ "engine": ">=0.1.3-alpha.1 <0.2.0",
95
+ "bundle": { "patch": "./cordis.patch.yml" }
96
+ }
97
+ }
98
+ ```
99
+
100
+ 这个字段目前**没有被上游强制读取**(2026-09-10 核对),它的作用是让体检能判断你与目标
101
+ 运行时是否相容:声明了,`--runtime` 越出范围时会直接报 error;不声明,这一层就判不了。
102
+
103
+ ## CI 集成示例
104
+
105
+ ```yaml
106
+ # .github/workflows/check.yml
107
+ - run: npx @tokensapi/dsh-plugin-check --runtime 0.1.3-alpha.1 --strict
108
+ ```
109
+
110
+ ## 上架流程中的位置
111
+
112
+ 1. 开发者:发布 npm 版本前本地跑体检,全绿再发。
113
+ 2. 管理员:在仓库 Actions 的 **Plugin Check** workflow 填「包名@版本」手动触发,体检报告生成在该次运行的 Summary。
114
+ 3. 上架:后台 publish 对话框把这次运行的链接填入「检查记录链接」——链接的红/绿即审核证据,`reviewed_version` 随上架落库。存在 error 的版本不满足上架条件。
@@ -18,6 +18,7 @@ import { resolve } from 'node:path'
18
18
  import { checkManifest } from '../lib/manifest-check.mjs'
19
19
  import { checkSmoke } from '../lib/smoke-check.mjs'
20
20
  import { fetchPackage } from '../lib/fetch-package.mjs'
21
+ import { CONTRACT, contractLine, checkContractBaseline } from '../lib/contract.mjs'
21
22
 
22
23
  const args = process.argv.slice(2)
23
24
  const options = { dir: process.cwd(), package: undefined, runtime: undefined, skipSmoke: false, keepWorkspace: false, strict: false, json: false }
@@ -53,6 +54,9 @@ if (options.package !== undefined) {
53
54
  options.dir = fetched.dir
54
55
  }
55
56
 
57
+ // C0 先跑:目标运行时没核对过时,后面所有结论都要打个折扣。
58
+ findings.push(...checkContractBaseline(options.runtime))
59
+
56
60
  const manifestResult = checkManifest(options.dir, { runtime: options.runtime })
57
61
  findings.push(...manifestResult.findings)
58
62
  phases.push({ phase: 'manifest', findings: manifestResult.findings })
@@ -83,6 +87,7 @@ if (options.json) {
83
87
  plugin: manifestResult.manifest?.name,
84
88
  version: manifestResult.manifest?.version,
85
89
  runtime: options.runtime ?? null,
90
+ contract: CONTRACT,
86
91
  errors,
87
92
  warnings,
88
93
  phases,
@@ -95,6 +100,7 @@ const title = manifestResult.manifest?.name !== undefined
95
100
  : options.dir
96
101
  process.stdout.write(`\ndsh-plugin-check · ${title}\n`)
97
102
  if (options.runtime !== undefined) process.stdout.write(`目标运行时: ${options.runtime}\n`)
103
+ process.stdout.write(`${contractLine()}\n`)
98
104
  process.stdout.write('\n')
99
105
  for (const finding of errors) process.stdout.write(` ❌ [${finding.rule}] ${finding.message}\n`)
100
106
  for (const finding of warnings) process.stdout.write(` ⚠️ [${finding.rule}] ${finding.message}\n`)
package/docs/CHECKS.md ADDED
@@ -0,0 +1,236 @@
1
+ # 校验逻辑与上游契约
2
+
3
+ 这份文档说明 `@tokensapi/dsh-plugin-check` **每一条规则在判什么、判到什么程度、
4
+ 为什么这么判、锚在上游哪一段代码**,以及**上游变了之后怎么逐条复核**。
5
+
6
+ 规则不是写法偏好。每一条都对应「插件装进用户的 TokensCowork 之后会出什么事」的
7
+ 一条具体路径,而那条路径由宿主/市场的代码决定 —— 所以本文档给每条规则都标了
8
+ 上游锚点(文件 + 符号),复核时照着看一遍就知道该不该改。
9
+
10
+ ---
11
+
12
+ ## 0. 契约基线(先看这个)
13
+
14
+ 规则是**照着某个上游版本的行为**写的。基线写在 `lib/contract.mjs`,并随每次体检
15
+ 结果一起输出(CLI 抬头一行、`--json` 的 `contract` 字段、会话内工具结果的
16
+ `contract` 字段)。
17
+
18
+ | 项 | 值 |
19
+ | --- | --- |
20
+ | 核对日期 | 2026-09-10 |
21
+ | `dsh-plugin-desktop` | 2.0.5 |
22
+ | `dsh-community-market` | 0.1.0-dev.0 |
23
+ | `@deepseek-ai/cordis` | 4.0.2 |
24
+ | `@deepseek-ai/dsh-tools` | 0.1.3-alpha.1 |
25
+ | 受控安装的包管理器 | pnpm 11.8.0 |
26
+ | 已核对的 DSH 运行时 | `0.1.0-rc.8`、`0.1.3-alpha.1` |
27
+ | 核对过的运行时区间 | `>=0.1.0-rc.8 <0.2.0` |
28
+
29
+ `--runtime` 传了核对范围之外的版本时,会多出一条 **C0-contract** warning:明确
30
+ 告诉你「这个版本没核对过,结论可能过时或过严」,而不是假装照样有效。
31
+
32
+ > 基线只保证「这些规则在这些上游版本上是对的」。上游升级后请走
33
+ > [§5 上游变动后怎么复核](#5-上游变动后怎么复核)。
34
+
35
+ ---
36
+
37
+ ## 1. 三条执行路径(判定同源,能做的事不同)
38
+
39
+ | 入口 | 拿到的是什么 | 跑哪些规则 |
40
+ | --- | --- | --- |
41
+ | CLI `dsh-plugin-check [目录]` | 本地工作区文件 | M1–M10 + S1–S4(完整) |
42
+ | CLI `--package <name@ver>` | `npm pack` 下载并解包的目录 | M1–M10 + S1–S4(完整) |
43
+ | 会话内 `plugin_check(path=…)` | 本地工作区文件 | M1–M10 |
44
+ | 会话内 `plugin_check(package=…)` | registry 版本清单 + tarball 里抽出的 `cordis.patch.yml` | M1–M10(除 M10 的目录扫描) |
45
+
46
+ 会话内不跑冒烟:冒烟要装依赖、起子进程、执行被测插件代码,这些不能发生在用户的
47
+ Cowork 宿主进程里。所以会话内的结论只覆盖清单侧,输出里的 `note` 会如实说明这
48
+ 一点,不冒充完整体检。
49
+
50
+ **M4/M5 只有一份实现**(`lib/patch-rows.mjs` 的 `checkPatchContent`),目录体检和
51
+ 会话内体检共用。这一点是踩过坑之后加的:两边曾经各写各的,会话内看不到 M5,把
52
+ 「装上即崩」的插件判成了合格。
53
+
54
+ ---
55
+
56
+ ## 2. 清单规则(M 系列)
57
+
58
+ 判定实现:`lib/manifest-check.mjs`(读磁盘)与 `lib/registry-check.mjs`(读 registry
59
+ 清单)。两份的语义相同,后者不依赖文件系统,因此内置了一个够用的迷你 SemVer。
60
+
61
+ ### M1-manifest — 清单本身可用
62
+
63
+ - **error**:`package.json` 不存在 / 不是合法 JSON / `name` 缺失 / `version` 不是
64
+ 合法 SemVer。
65
+ - **warning**:`version` 是预发布版(如 `0.2.0-beta.1`)。
66
+ - **为什么只是 warning**:上游市场的 npm 验证器要求 `latest` 是**稳定的精确版**,
67
+ 预发布版拿不到一键安装按钮,只会退回手动命令 —— 但插件完全可以把预发布发在
68
+ `next` 之类的 dist-tag 上,让 `latest` 保持稳定。工具看到的是清单里的版本号,
69
+ 看不到它会不会被打成 `latest`,所以不越权判死。
70
+ - **上游锚点**:`dsh-community-market/src/install/service.ts` → `stableExactVersion()`
71
+ (`valid(v) === v && prerelease(v) === null`),在 `createNpmRegistryVerifier` 里强制。
72
+
73
+ ### M2-lifecycle — 安装期生命周期脚本
74
+
75
+ - **warning**:声明了 `preinstall` / `install` / `postinstall` / `prepare`。
76
+ - **判什么**:受控安装是 `pnpm add --save-exact --registry=…`,**没有**
77
+ `--ignore-scripts`;Profile 的 `pnpm-workspace.yaml` 也没有配
78
+ `onlyBuiltDependencies` 允许名单。于是两种结局,都不好:
79
+ - 脚本真执行 → 安装即在用户机器上跑任意代码;
80
+ - 脚本被包管理器默认策略拦下 → 依赖脚本产物的插件**装完即坏**。
81
+
82
+ `prepare` 另说:从 npm tarball 装**不执行**,从 `github:` 源装**执行** —— 同一个
83
+ 包两条安装路径行为不一致,本身就是缺陷。构建请改用 `prepack`。
84
+ - **为什么是 warning 而不是 error(2026-09-10 改的)**:这条以前是 error,理由是
85
+ 「市场受控安装会拒绝」。核对下来这个理由**已经不成立**:上游验证器现在明确接受
86
+ 带生命周期脚本的包。它仍然是个真问题,但不再是上架阻断项,所以降级并改了措辞。
87
+ - **上游锚点**:`dsh-community-market/src/install/service.ts` → `createNpmRegistryVerifier`
88
+ (只校验 name / 稳定版本 / `dsh.bundle.patch`,不看 `scripts`)、`installOptions()`
89
+ (`--save-exact --registry=…`)、`executeInstall()` 里的 `pnpm add`;测试
90
+ `dsh-community-market/tests/market-install.spec.ts` →
91
+ `'uses npm latest and accepts lifecycle, deprecated, and unrelated repository metadata'`。
92
+
93
+ ### M3-core-peer — 双内核
94
+
95
+ - **error**:`@deepseek-ai/cordis`、`@deepseek-ai/cordis-*`、`@deepseek-ai/dsh*` 出现在
96
+ `dependencies`。
97
+ - **判什么**:这些包携带**身份语义** —— cordis 的 Context 与 dsh 的服务注册靠模块
98
+ 实例 identity 对齐。插件自己装一份,宿主一份、插件一份,Symbol 不等,注册对不上,
99
+ 典型症状是整个 Cowork 启动失败(不是这个插件坏,是全都起不来)。必须声明为
100
+ `peerDependencies`,由宿主提供唯一实例。
101
+ - **不在此列**:`schemastery`、`cosmokit` 等纯工具库没有身份语义,插件可以正常依赖。
102
+
103
+ ### M4-bundle — 补丁存在且可解析
104
+
105
+ - **error**:`dsh.bundle.patch` 缺失;指向的文件不存在(目录体检)或不在发布内容里
106
+ (会话内体检,常见原因是 `files` 白名单漏了它);补丁不是合法 YAML;补丁没声明
107
+ 任何插件行。
108
+ - **warning**:会话内体检取不到 tarball(离线/超时/过大)—— 如实标注「补丁行本次
109
+ 未检查」,绝不因网络问题把插件判成不合格。
110
+ - **判什么**:宿主靠 `dsh.bundle.patch` 把插件挂进 cordis 加载树。取不到补丁,插件
111
+ 根本挂不上。
112
+ - **上游锚点**:`dsh-community-market/src/install/service.ts` → `safeBundlePatch()`
113
+ (非空、≤512 字节、无 NUL、相对路径、不含反斜杠、路径段不为 `.` / `..` / 不含冒号),
114
+ 在 `createNpmRegistryVerifier` 里强制;宿主侧 `dsh-plugin-desktop/src/profile.ts` 的
115
+ `DESKTOP_PATCH_PATH` 与 `profile-checkpoint.ts` 里的 `cordis.patch.yml` 条目。
116
+
117
+ ### M5-row-scope — 补丁行只能挂自己
118
+
119
+ - **error**:补丁行的 `name` 既不等于本包包名,也不是 `<包名>/子路径`。
120
+ - **判什么**:这是**最容易写错、后果最重**的一条。补丁行里:
121
+ - `id` = 加载器条目 id(惯例上取插件自己 `export const name` 的那个 cordis 插件名);
122
+ - `name` = **用于模块解析的 npm 包说明符**。
123
+
124
+ 把 `name` 写成 cordis 插件名(而不是 npm 包名),宿主会拿着这个名字去 Profile 的
125
+ `node_modules` 找包,找不到就抛 `PackageOverlayNotFoundError` —— 而这个异常发生在
126
+ 加载树构建期,**整棵插件树都起不来**,不只是这一个插件。用户看到的是 Cowork 打不开。
127
+ - **为什么允许 `<包名>/子路径`**:一个包挂多个入口(`@scope/pkg/worker`)是合法用法。
128
+ - **上游锚点**:`dsh-plugin-desktop/src/module-resolution.ts` → `selectedOverlayCandidate()`,
129
+ `PackageOverlayNotFoundError` 在第 360 / 371 / 380 / 492 / 545 行抛出。
130
+
131
+ ### M6-engine — 目标运行时声明
132
+
133
+ - **warning**:`dsh.engine` 未声明。
134
+ - **error**:`dsh.engine` 不是合法 SemVer 范围;或给了 `--runtime` 而该范围不覆盖它。
135
+ - **诚实说明**:核对下来,**上游目前没有任何代码读 `dsh.engine`** —— 它是约定,不是
136
+ 被强制的契约。所以「未声明」只给 warning。一旦声明了,就按声明判:自己写了范围又
137
+ 不覆盖目标运行时,是插件自己的矛盾,判 error 不冤。
138
+
139
+ ### M7-peer-range — peer 范围与运行时的交集
140
+
141
+ - **error**:`@deepseek-ai/dsh*` 的 peer 范围不合法/不可识别。
142
+ - **warning**:给了 `--runtime` 而 peer 范围不含它。
143
+ - **为什么只是 warning**:范围不含目标运行时,不代表当场就崩 —— 它预示的是宿主升级
144
+ 之后的接口错配。判死会挡住「暂时还能跑、只是没来得及放宽范围」的插件。
145
+
146
+ ### M8-node-engines / M9-license
147
+
148
+ - **warning**:`engines.node` 未声明(宿主跑在 Node 22+)、`license` 未声明。
149
+ - 都是发布卫生问题,不影响能不能装,所以都不判死。
150
+
151
+ ### M10-secrets — 别把凭据发上去
152
+
153
+ - **error**:`files` 白名单里出现疑似凭据文件(`.env*`、`id_rsa`、`*.pem`、`*.p12`、`*.key`)。
154
+ - **warning**:目录里有 `.env` / `id_rsa` **且**没有用 `files` 收口 —— `npm pack` 很可能
155
+ 把它带上。
156
+ - 只有目录体检能做后半条(要看文件系统);会话内 `package=` 模式看不到工作区。
157
+
158
+ ---
159
+
160
+ ## 3. 隔离冒烟(S 系列,只在 CLI)
161
+
162
+ 实现:`lib/smoke-check.mjs`。在系统临时目录建一次性工作区,全程 `--ignore-scripts`
163
+ (被测代码没有机会在检查器进程里跑安装钩子),插件代码只在一次性子进程里运行,
164
+ 超时即杀。
165
+
166
+ | 步骤 | 做什么 | 判定 |
167
+ | --- | --- | --- |
168
+ | **S1-pack** | `npm pack --ignore-scripts` | 打不出包 → error |
169
+ | **S2-install** | 按插件自己声明的 peer 范围装出一套真实运行时,插件本体走刚打出的 tgz(等价用户侧受控安装) | 装不出来 → error;例外见 §4 |
170
+ | **S3-rows** | 取补丁里 `disabled !== true` 的行 | 没有启用行 → warning(冒烟无事可做) |
171
+ | **S4-apply** | 子进程里逐行 `require.resolve` → `import` → `new Context()` → `ctx.plugin(…)` | 任一阶段抛异常 → error,并标出是哪一段 |
172
+
173
+ S4 之所以要在子进程里做,是因为它**真的执行插件代码**:顶层异常、缺模块、ESM 形状
174
+ 不对、apply 里抛出,都会在这里现形 —— 这正是「装进用户 Cowork 会在启动链同点崩溃」
175
+ 的那个点。子进程结果写文件而不是 stdout,避免插件打日志污染判定。
176
+
177
+ ---
178
+
179
+ ## 4. 通用性:为什么这套规则不因插件而异
180
+
181
+ **规则判的全是「插件与宿主之间的契约」,不是插件的业务行为。** 无论插件是做笔记、
182
+ 调模型还是控浏览器,它挂进宿主的方式完全一样:一份 `package.json`、一份
183
+ `cordis.patch.yml`、一次 `pnpm add`、一次加载树构建。M 系列查的就是这条通路上的
184
+ 声明,S 系列执行的就是这条通路本身。所以这套规则对所有插件同样适用。
185
+
186
+ 真正会因插件而异的是**业务是否正确**(接口对不对、功能好不好用)—— 那不在体检范围
187
+ 内,本工具不碰。
188
+
189
+ 体检里唯一「看插件长相」的地方是 S4:它会真的 `import` 并 `apply` 你的插件。为了不
190
+ 因为插件的正常差异而误判,有**四处刻意不致命**:
191
+
192
+ 1. **网络取不到** —— 会话内取 tarball 失败、CLI 装不上公开源上确实没有的包,都只给
193
+ warning 并如实标注「未检查」,不判不合格。检查环境的限制不该算在插件头上。
194
+ 2. **宿主内核的私有版本** —— `@deepseek-ai/*` 的内部版本本就不发公开源,S2 会先探测
195
+ 一次(`npm view <name>@<range>`),确认是「内核私有版本取不到」才降级为 warning;
196
+ 其它包解析不到,仍然是插件自己范围写错,照判 error。
197
+ 3. **补丁配置含动态表达式** —— `cordis.patch.yml` 可以带 `!!js` 之类的自定义标签。解析
198
+ 用的是宽容 schema(未知标签收敛成占位对象),行照常提取,该行的 config 标记为
199
+ 不可静态求值,冒烟时**以空配置 apply**,并给一条 warning 说明。不因为看不懂配置
200
+ 就拒绝这个插件。
201
+ 4. **`inject` 声明的服务没注入** —— 插件声明了依赖服务而冒烟环境里没有,cordis 会
202
+ 正常停车等待。这是预期行为,不算失败。
203
+
204
+ ---
205
+
206
+ ## 5. 上游变动后怎么复核
207
+
208
+ 上游一个版本升上去,这个检查器**可能要改,也可能完全不用改**。别猜,照下表看一遍
209
+ 就有答案。每格里的锚点都是本次(2026-09-10)实际读过的代码位置。
210
+
211
+ | 规则 | 去看什么 | 什么情况下必须改 |
212
+ | --- | --- | --- |
213
+ | M1 | market `src/install/service.ts` → `stableExactVersion` | 稳定版要求放宽/收紧 → 调级别与措辞 |
214
+ | M2 | 同上 → `createNpmRegistryVerifier` 是否重新检查 `scripts`;`installOptions()` 是否加了 `--ignore-scripts`;Profile 的 `pnpm-workspace.yaml` 是否配了 `onlyBuiltDependencies` | 验证器重新拒绝脚本 → 升回 error;安装加了 `--ignore-scripts` → 改措辞 |
215
+ | M3 | `@deepseek-ai/cordis` 主版本、dsh 服务注册方式 | 身份对齐机制改变 → 重新划 `isIdentityCore` 的范围 |
216
+ | M4 | 同上 → `safeBundlePatch`;`dsh-plugin-desktop/src/profile.ts` 的补丁路径 | 补丁路径形状约束变化 → 同步 |
217
+ | M5 | `dsh-plugin-desktop/src/module-resolution.ts` → `selectedOverlayCandidate` / `PackageOverlayNotFoundError` | 行 `name` 的解析语义变化 → 这条必须跟着改 |
218
+ | M6 | 全仓搜 `dsh.engine` 有没有被真正读取 | 一旦上游开始强制 → 「未声明」从 warning 升 error |
219
+ | M7 | 宿主提供的 `@deepseek-ai/dsh*` 版本 | 无需改代码,更新 `CONTRACT.runtimes` 即可 |
220
+ | M8/M9 | 宿主 Node 版本、上架元数据要求 | 要求变化 → 同步措辞 |
221
+ | M10 | 无上游依赖(发布卫生) | 基本不用改 |
222
+ | S1–S4 | cordis 的 `new Context()` / `ctx.plugin()` API | cordis 主版本升级 → 复核 `RUNNER_SOURCE` |
223
+
224
+ 复核完成后,请一并更新 `lib/contract.mjs` 里的 `verifiedAt`、`upstream`、`runtimes`
225
+ 和 `runtimeRange`,再 bump 版本发布 —— 这样用户从输出的基线一行就能看出「这套结论
226
+ 是照着哪个上游核对的」。
227
+
228
+ ---
229
+
230
+ ## 6. 判定级别的含义
231
+
232
+ - **error** — 装进用户的 Cowork 会坏(启动失败、挂不上、装不出来)。修好再发。
233
+ - **warning** — 现在能用,但预示未来断裂,或者拿不到受控安装这类更好的路径。
234
+ - **info** — 只是过程说明(比如冒烟通过了几行)。
235
+
236
+ `--strict` 会把 warning 也计入失败,退出码 1。默认只有 error 才失败。
@@ -0,0 +1,80 @@
1
+ /* ============================================================
2
+ * 上游契约基线
3
+ * ============================================================
4
+ * 这个工具的每条规则都不是写法偏好,而是照着某个上游版本的真实
5
+ * 行为写下的判定。上游一变,规则可能要跟着变,也可能完全不用变
6
+ * —— 区别只有"回去看一眼"才知道。所以把「照着哪个版本核对的」
7
+ * 做成机器可读的一份数据,跟着每次体检结果一起输出;目标运行时
8
+ * 落在核对过的范围之外时明确说"没核对过",而不是假装照样有效。
9
+ *
10
+ * 每条规则锚在上游哪个文件/行:见 docs/CHECKS.md。
11
+ * 上游升级后的逐条复核流程:见同一文档末尾「上游变动后怎么复核」。
12
+ * ============================================================ */
13
+
14
+ /** 上游契约基线:最近一次逐条核对上游代码时的事实快照。 */
15
+ export const CONTRACT = {
16
+ /** 最近一次逐条核对上游代码的日期。 */
17
+ verifiedAt: '2026-09-10',
18
+ /** 核对时上游各包的版本;规则的判定口径照着这些版本的代码写。 */
19
+ upstream: {
20
+ 'dsh-plugin-desktop': '2.0.5',
21
+ 'dsh-community-market': '0.1.0-dev.0',
22
+ '@deepseek-ai/cordis': '4.0.2',
23
+ '@deepseek-ai/dsh-tools': '0.1.3-alpha.1',
24
+ pnpm: '11.8.0',
25
+ },
26
+ /** 实际核对过的 DSH 运行时版本。 */
27
+ runtimes: ['0.1.0-rc.8', '0.1.3-alpha.1'],
28
+ /** 核对过的运行时区间;超出只代表未核对,不代表不可用。 */
29
+ runtimeRange: '>=0.1.0-rc.8 <0.2.0',
30
+ }
31
+
32
+ /** 一行人读的基线摘要,供 CLI 抬头与会话内结果展示。 */
33
+ export function contractLine() {
34
+ const { upstream } = CONTRACT
35
+ return `契约基线 ${CONTRACT.verifiedAt} · desktop ${upstream['dsh-plugin-desktop']}`
36
+ + ` / market ${upstream['dsh-community-market']}`
37
+ + ` · 已核对运行时 ${CONTRACT.runtimes.join('、')}`
38
+ }
39
+
40
+ /**
41
+ * 目标运行时是否在核对过的范围内。
42
+ * 不做区间数学:只认"确实核对过的那几个版本",别的一律如实说
43
+ * 没核对过 —— 这条提示的价值就在于不替上游的未知变动打包票。
44
+ * @param {string | undefined} runtime - 目标 DSH 运行时版本。
45
+ * @returns {Array<{level: 'warning', rule: string, message: string}>}
46
+ */
47
+ export function checkContractBaseline(runtime) {
48
+ if (runtime === undefined || runtime === '') return []
49
+ if (CONTRACT.runtimes.includes(runtime)) return []
50
+ return [{
51
+ level: 'warning',
52
+ rule: 'C0-contract',
53
+ message: `目标运行时 ${runtime} 不在本工具核对过的版本里`
54
+ + `(已核对 ${CONTRACT.runtimes.join('、')},核对日期 ${CONTRACT.verifiedAt});`
55
+ + `规则仍按 ${CONTRACT.runtimeRange} 的上游行为判定,上游若已改动,结论可能过时或过严。`
56
+ + `复核清单见 docs/CHECKS.md`,
57
+ }]
58
+ }
59
+
60
+ /**
61
+ * 安装期生命周期脚本。曾经是市场硬红线,核对下来现在不是了。
62
+ * 上游 dsh-community-market 0.1.0-dev.0 的 npm 验证器已不再看
63
+ * scripts;受控安装是 `pnpm add --save-exact --registry=…`,没有
64
+ * --ignore-scripts,而 Profile 也没有配 onlyBuiltDependencies 允许
65
+ * 名单。两种结局都不好,但都不是"上架被拒",所以降为 warning:
66
+ * - 脚本真执行 → 安装即在用户机器上跑任意代码;
67
+ * - 脚本被包管理器默认策略拦下 → 依赖脚本产物的插件装完即坏。
68
+ */
69
+ export const LIFECYCLE_SCRIPTS = ['preinstall', 'install', 'postinstall', 'prepare']
70
+
71
+ /** M2 的措辞:两条安装路径行为不同,分开说清楚。 */
72
+ export function lifecycleMessage(script) {
73
+ if (script === 'prepare') {
74
+ return 'prepare 脚本:从 npm tarball 安装时不执行,从 github: 源安装时执行;'
75
+ + '同一个包两条安装路径行为不一致,构建请改用 prepack'
76
+ }
77
+ return `${script} 脚本:宿主受控安装是 pnpm add(未加 --ignore-scripts),`
78
+ + `要么在用户机器上执行任意代码,要么被包管理器默认策略拦下、脚本产物缺失导致插件装完即坏;`
79
+ + `构建请改用 prepack`
80
+ }