@bushizhanghan/dsh-publish-gate 0.7.1 → 0.8.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.8.0
4
+
5
+ - `tool-input-throws` (error): `publish_gate_tools` gained `fuzz` and `skip`. With `fuzz: true` it drives
6
+ every declared parameter with a wrong-typed value and reports the tools that throw instead of answering.
7
+ The host never validates a call against `parameters`, so those values really arrive — this rule exists
8
+ because the same sweep found 12 real crashes across this workspace's own plugins (a number reaching
9
+ `path.join` via `session`, `profile`, `casePath` and `workspace`), all of which handed the caller a raw
10
+ Node TypeError. Only a throw counts: a filter that ignores a number and returns the unfiltered list still
11
+ answers, and every optional boolean treats any truthy value the same way.
12
+ - One finding per tool+parameter, not one per wrong value.
3
13
  ## 0.7.1
4
14
 
5
15
  - Fixed: a non-string `dir` made every path tool throw a raw Node `TypeError`
package/README.md CHANGED
@@ -106,6 +106,7 @@ dsh plugin --profile desktop add @bushizhanghan/dsh-publish-gate
106
106
  | `tool-bad-parameter-type` | `parameters` 里某个 `type` 写错(`"strin"`)。宿主把 `parameters` 渲染进 prompt、但**从不校验**调用值,所以这个 typo 只是悄悄把类型提示弄丢 |
107
107
  | `tool-undeclared-param` | `execute` 读了 `args.x`,但 `parameters.properties` 里没有 `x`。模型只能传 schema 声明过的参数,所以这个值在真实调用里永远是 `undefined`——代码在,永远不跑,也不报错。`args.x`、`args['x']`、`const { x } = args` 三种写法都认 |
108
108
  | `tool-output-invalid` | 工具返回的东西**自己的 output schema 不收**。宿主每个结果都拿 schema 校验,不匹配就抛 `ToolOutputError`——工具从第一次调用起就不可用,而报错只给一个 JSON 路径,不给是哪行代码返回的。只有点名 `call` 真的调了才检查。这个插件自己踩过:schema 收紧成 `additionalProperties: false`,但有一条错误分支还在返回 `{ ok, error }` |
109
+ | `tool-input-throws` | 把每个声明过的参数**塞一个错类型的值**去调,工具直接抛 `TypeError` 而不是回答。宿主从不拿 `parameters` 校验入参,所以这个值真的会到你的代码里——调用方拿到的是 `The "path" argument must be of type string`,插件日志里没有一行说是哪个工具。这条规则是在自己的工作区里跑出 **12 个真实崩溃** 之后才加的(`session`/`profile`/`casePath`/`workspace` 各一个入口),现在九个插件全绿。默认关:它会把每个工具都调一遍,用 `skip` 点名排除那些会开程序、写文件的 |
109
110
 
110
111
  ### 浏览器半边:侧栏面板到底会不会出现
111
112
 
@@ -214,7 +215,7 @@ dsh plugin --profile desktop add @bushizhanghan/dsh-publish-gate
214
215
  ## 开发
215
216
 
216
217
  ```sh
217
- node --test # 88 个引擎单测
218
+ node --test # 90 个引擎单测
218
219
  node test/integration.test.mjs # 27 条集成断言(9 个真实插件作 fixture)
219
220
  ```
220
221
 
package/lib/engine.js CHANGED
@@ -1198,6 +1198,7 @@ ctx.tools = {
1198
1198
  hasExecute: typeof def?.execute === 'function',
1199
1199
  hasOutputSchema: Boolean(def && def.output && def.output.schema),
1200
1200
  schema: (def && def.output && def.output.schema) || null,
1201
+ parameters: (def && def.parameters) || null,
1201
1202
  cardKind: card,
1202
1203
  undeclaredParams: undeclaredArgs(def),
1203
1204
  run: typeof def?.execute === 'function' ? def.execute : null,
@@ -1278,7 +1279,45 @@ try {
1278
1279
  calls.push({ name: entry.name, ok: false, timedOut: message === 'timed out', error: message })
1279
1280
  }
1280
1281
  }
1281
- emit({ ok: true, tools: registered.map(({ run, ...rest }) => rest), badParamTypes, calls })
1282
+ // The host never validates tool input against \`parameters\`, so a number really
1283
+ // does reach a path argument. Fuzzing every declared parameter with the wrong
1284
+ // type found 12 crashes across this workspace's own plugins — each one a raw
1285
+ // TypeError handed to the caller instead of an answer.
1286
+ stage = 'fuzz'
1287
+ const fuzzIssues = []
1288
+ if (process.env.PG_FUZZ === '1') {
1289
+ const skip = new Set(JSON.parse(process.env.PG_FUZZ_SKIP || '[]'))
1290
+ const WRONG = {
1291
+ string: [123, true, { a: 1 }, [1]],
1292
+ boolean: ['yes', 1, {}],
1293
+ number: ['n', {}, []],
1294
+ array: ['x', 1, {}],
1295
+ object: ['x', 1, []],
1296
+ }
1297
+ for (const entry of registered) {
1298
+ if (!entry.run || skip.has(entry.name)) continue
1299
+ const props = entry.parameters && entry.parameters.properties
1300
+ if (!props || typeof props !== 'object') continue
1301
+ for (const [param, spec] of Object.entries(props)) {
1302
+ for (const bad of WRONG[spec && spec.type] || [123]) {
1303
+ try {
1304
+ await Promise.race([
1305
+ Promise.resolve(entry.run({ [param]: bad }, { signal: undefined })),
1306
+ new Promise((_, reject) => setTimeout(() => reject(new Error('timed out')), budget)),
1307
+ ])
1308
+ } catch (error) {
1309
+ // Only a throw is a defect. A filter parameter that ignores a number
1310
+ // and returns the unfiltered list still answers, and every optional
1311
+ // boolean treats any truthy value the same way.
1312
+ const message = String((error && error.message) || error)
1313
+ if (message === 'timed out') continue
1314
+ fuzzIssues.push({ tool: entry.name, param, value: JSON.stringify(bad), problem: message.slice(0, 200) })
1315
+ }
1316
+ }
1317
+ }
1318
+ }
1319
+ }
1320
+ emit({ ok: true, tools: registered.map(({ run, ...rest }) => rest), badParamTypes, calls, fuzzIssues })
1282
1321
  } catch (error) {
1283
1322
  emit({ ok: false, stage, error: String((error && error.message) || error) })
1284
1323
  }
@@ -1297,6 +1336,8 @@ export async function checkToolRegistrations(dir, opts = {}) {
1297
1336
  ...process.env,
1298
1337
  PG_CALLS: JSON.stringify(Array.isArray(opts.calls) ? opts.calls : []),
1299
1338
  PG_CALL_MS: String(opts.callTimeoutMs ?? 8000),
1339
+ PG_FUZZ: opts.fuzz ? '1' : '0',
1340
+ PG_FUZZ_SKIP: JSON.stringify(Array.isArray(opts.fuzzSkip) ? opts.fuzzSkip : []),
1300
1341
  },
1301
1342
  })
1302
1343
  const line = String(stdout).split('\n').filter((l) => l.startsWith('@@PG@@')).pop()
@@ -1428,6 +1469,23 @@ export async function checkToolRegistrations(dir, opts = {}) {
1428
1469
  ),
1429
1470
  )
1430
1471
  }
1472
+ // One finding per tool+parameter: the first wrong value is enough to prove the
1473
+ // guard is missing, and four findings for the same line is noise.
1474
+ const fuzzSeen = new Set()
1475
+ for (const issue of Array.isArray(payload.fuzzIssues) ? payload.fuzzIssues : []) {
1476
+ const key = `${issue.tool}\u0000${issue.param}`
1477
+ if (fuzzSeen.has(key)) continue
1478
+ fuzzSeen.add(key)
1479
+ out.push(
1480
+ F(
1481
+ 'tool-input-throws',
1482
+ 'error',
1483
+ `${issue.tool}(${issue.param}: ${issue.value}) — ${issue.problem}`,
1484
+ 'The host never validates a call against parameters, so this really reaches your code with the wrong type. The caller gets a raw Node TypeError instead of an answer, and nothing in the plugin log says which tool was at fault.',
1485
+ `Guard the top of ${issue.tool}: if (typeof args.${issue.param} !== 'string') return { ok: false, error: '${issue.param} must be a string' }.`,
1486
+ ),
1487
+ )
1488
+ }
1431
1489
  return { findings: out, tools }
1432
1490
  }
1433
1491
 
@@ -2104,7 +2162,7 @@ const RULE_ORDER = [
2104
2162
  'inject-missing-tools', 'inject-missing-web', 'inject-missing-webServer', 'inject-missing-timer',
2105
2163
  'inject-unused-service',
2106
2164
  'tool-missing-name', 'tool-duplicate-name', 'tool-missing-execute', 'tool-present-call-throws',
2107
- 'tool-bad-card-kind', 'tool-missing-output-schema', 'tool-none-registered', 'tool-bad-parameter-type', 'tool-undeclared-param', 'tool-call-throws', 'tool-output-invalid', 'tool-call-slow',
2165
+ 'tool-bad-card-kind', 'tool-missing-output-schema', 'tool-none-registered', 'tool-bad-parameter-type', 'tool-undeclared-param', 'tool-input-throws', 'tool-call-throws', 'tool-output-invalid', 'tool-call-slow',
2108
2166
  'lib-debugger', 'lib-console-log', 'lib-hardcoded-path', 'lib-url-pathname', 'node-too-old', 'stale-build', 'hand-edited-build',
2109
2167
  'undeclared-import', 'dep-in-dev-only', 'unused-dependency',
2110
2168
  'pack-omits-lib', 'pack-omits-patch', 'pack-omits-client', 'pack-oversize', 'pack-omits-readme', 'pack-omits-license', 'pack-ships-secret',
package/lib/index.js CHANGED
@@ -132,7 +132,7 @@ export function apply(ctx, config = {}) {
132
132
  {
133
133
  name: 'publish_gate_tools',
134
134
  description:
135
- 'Import a plugin entry in an isolated child process, call apply(ctx) with a no-op context, and report what it actually registered. Catches the failures the manifest cannot show: the entry throws when imported, apply throws, a tool has no execute function, two tools share a name, presentCall throws, and a card kind the host does not render (the host only knows other/execute/search/read/edit/delete/fetch — anything else registers fine and renders a broken card). 20 s timeout, plugin code never runs in the host process. Returns {ok, tools:[{name,hasExecute,hasOutputSchema,cardKind}], findings, report}.',
135
+ 'Import a plugin entry in an isolated child process, call apply(ctx) with a no-op context, and report what it actually registered. Catches the failures the manifest cannot show: the entry throws when imported, apply throws, a tool has no execute function, two tools share a name, presentCall throws, and a card kind the host does not render (the host only knows other/execute/search/read/edit/delete/fetch — anything else registers fine and renders a broken card). Optional deeper modes: `call` runs named tools with {} and validates each result against its own output schema; `fuzz` drives every declared parameter with a wrong-typed value and reports the tools that throw a raw TypeError instead of answering — the host never validates a call against parameters, so those values really arrive. 20 s timeout, plugin code never runs in the host process. Returns {ok, tools:[{name,hasExecute,hasOutputSchema,cardKind}], findings, report}.',
136
136
  parameters: {
137
137
  type: 'object',
138
138
  properties: {
@@ -144,12 +144,27 @@ export function apply(ctx, config = {}) {
144
144
  description:
145
145
  'Tool names to actually call with empty arguments, e.g. ["page_watch_list"]. Off by default: some tools open applications or write files, so you name the ones you consider safe. A tool that throws on {} is a bug no other check can see, because registration never runs execute.',
146
146
  },
147
+ fuzz: {
148
+ type: 'boolean',
149
+ description:
150
+ 'Drive every declared parameter with wrong-typed values (a number where a string is declared, and so on) and report the tools that throw instead of answering. The host never validates a call against parameters, so these values really reach your code. Off by default because it calls every tool — pass skip for the ones that launch applications or write files.',
151
+ },
152
+ skip: {
153
+ type: 'array',
154
+ items: { type: 'string' },
155
+ description: 'Tool names the fuzz must not call, e.g. ["weapp_open","of_run","page_watch_add"].',
156
+ },
147
157
  },
148
158
  },
149
159
  async execute(args) {
150
160
  const dir = resolveDir(args.dir)
151
161
  if (!dir) return { ok: false, error: 'no plugin directory given (dir must be a string path)' }
152
- const { findings, tools } = await checkToolRegistrations(dir, { timeoutMs: args.timeoutMs, calls: args.call })
162
+ const { findings, tools } = await checkToolRegistrations(dir, {
163
+ timeoutMs: args.timeoutMs,
164
+ calls: args.call,
165
+ fuzz: args.fuzz,
166
+ fuzzSkip: args.skip,
167
+ })
153
168
  return { ok: true, tools, findings, report: renderReport(findings, { title: `runtime gate · ${dir}` }) }
154
169
  },
155
170
  },
package/package.json CHANGED
@@ -1,52 +1,52 @@
1
- {
2
- "name": "@bushizhanghan/dsh-publish-gate",
3
- "version": "0.7.1",
4
- "description": "Pre-publish gate for DSH plugins: manifest, dependencies, artifact, runtime, browser half, traceability and market readiness. 发布前的七层门禁。",
5
- "type": "module",
6
- "main": "lib/index.js",
7
- "exports": {
8
- ".": "./lib/index.js",
9
- "./engine": "./lib/engine.js",
10
- "./package.json": "./package.json"
11
- },
12
- "files": [
13
- "lib",
14
- "cordis.patch.yml",
15
- "README.md",
16
- "LICENSE",
17
- "CHANGELOG.md"
18
- ],
19
- "keywords": [
20
- "dsh",
21
- "dsh-plugin",
22
- "deepseek-harness",
23
- "cordis",
24
- "cordis-plugin",
25
- "publish",
26
- "preflight",
27
- "npm",
28
- "market",
29
- "agent-tools"
30
- ],
31
- "author": "bushizhanghan",
32
- "license": "MIT",
33
- "repository": {
34
- "type": "git",
35
- "url": "git+https://github.com/bushizhanghan/dsh-publish-gate.git"
36
- },
37
- "bugs": {
38
- "url": "https://github.com/bushizhanghan/dsh-publish-gate/issues"
39
- },
40
- "homepage": "https://github.com/bushizhanghan/dsh-publish-gate#readme",
41
- "engines": {
42
- "node": ">=22"
43
- },
44
- "dsh": {
45
- "bundle": {
46
- "patch": "./cordis.patch.yml"
47
- }
48
- },
49
- "scripts": {
50
- "test": "node --test"
51
- }
52
- }
1
+ {
2
+ "name": "@bushizhanghan/dsh-publish-gate",
3
+ "version": "0.8.0",
4
+ "description": "Pre-publish gate for DSH plugins: manifest, dependencies, artifact, runtime, browser half, traceability and market readiness — plus an input fuzzer that drives every declared parameter with the wrong type. 发布前的七层门禁 + 入参模糊测试。",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "exports": {
8
+ ".": "./lib/index.js",
9
+ "./engine": "./lib/engine.js",
10
+ "./package.json": "./package.json"
11
+ },
12
+ "files": [
13
+ "lib",
14
+ "cordis.patch.yml",
15
+ "README.md",
16
+ "LICENSE",
17
+ "CHANGELOG.md"
18
+ ],
19
+ "keywords": [
20
+ "dsh",
21
+ "dsh-plugin",
22
+ "deepseek-harness",
23
+ "cordis",
24
+ "cordis-plugin",
25
+ "publish",
26
+ "preflight",
27
+ "npm",
28
+ "market",
29
+ "agent-tools"
30
+ ],
31
+ "author": "bushizhanghan",
32
+ "license": "MIT",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/bushizhanghan/dsh-publish-gate.git"
36
+ },
37
+ "bugs": {
38
+ "url": "https://github.com/bushizhanghan/dsh-publish-gate/issues"
39
+ },
40
+ "homepage": "https://github.com/bushizhanghan/dsh-publish-gate#readme",
41
+ "engines": {
42
+ "node": ">=22"
43
+ },
44
+ "dsh": {
45
+ "bundle": {
46
+ "patch": "./cordis.patch.yml"
47
+ }
48
+ },
49
+ "scripts": {
50
+ "test": "node --test"
51
+ }
52
+ }