@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 +10 -0
- package/README.md +2 -1
- package/lib/engine.js +60 -2
- package/lib/index.js +17 -2
- package/package.json +52 -52
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 #
|
|
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
|
-
|
|
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, {
|
|
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.
|
|
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
|
+
}
|