@bushizhanghan/dsh-publish-gate 0.5.0 → 0.7.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 +83 -0
- package/README.md +6 -2
- package/lib/engine.js +192 -6
- package/lib/index.js +2 -0
- package/package.json +4 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.7.0
|
|
4
|
+
|
|
5
|
+
- `tool-output-invalid` (error): a tool result its own `output.schema` rejects. The host validates every
|
|
6
|
+
result and throws `ToolOutputError`, so the tool is unusable from the first call. Only checked when the
|
|
7
|
+
caller names the tool in `call`, because calling a tool can launch applications or write files.
|
|
8
|
+
- `npm-not-logged-in` (error) / `npm-scope-mismatch` (warn): ask npm who it is before spending a publish.
|
|
9
|
+
ENEEDAUTH and the 403 that names neither the token nor the scope both become a message instead of a loss.
|
|
10
|
+
- `readme-wrong-package` (error): the README install line names a different package — a README copied from
|
|
11
|
+
another plugin. Whoever follows it installs something else, and nothing fails because that package exists.
|
|
12
|
+
## 0.6.0
|
|
13
|
+
|
|
14
|
+
- New rule `tool-undeclared-param`: a tool whose `execute` reads `args.x` that
|
|
15
|
+
`parameters.properties` never declares. The host renders `parameters` into the
|
|
16
|
+
prompt and passes the model nothing else, so that value is undefined on every
|
|
17
|
+
real call — the code path exists and never runs, and nothing throws.
|
|
18
|
+
Covers `args.x`, `args['x']` and `const { x } = args`.
|
|
19
|
+
|
|
20
|
+
## 0.5.0
|
|
21
|
+
|
|
22
|
+
- New layer: traceability. `git-uncommitted` (files that ship in the tarball are
|
|
23
|
+
not committed), `git-tag-missing` (no `v<version>` tag on HEAD),
|
|
24
|
+
`git-tag-elsewhere` (the tag exists but points at another commit).
|
|
25
|
+
- The incident behind it: a 1.2.7 went to npm while the GitHub release still
|
|
26
|
+
said 1.2.6 and the market entry still pointed at the old tarball.
|
|
27
|
+
|
|
28
|
+
## 0.4.0
|
|
29
|
+
|
|
30
|
+
- New layer: dependency truth. `undeclared-import` (lib/ imports a package
|
|
31
|
+
package.json never declares), `dep-in-dev-only` (only devDependencies satisfy
|
|
32
|
+
it — npm never installs those for the consumer), `unused-dependency` (info).
|
|
33
|
+
- New rule `version-behind-latest`: publishing under the live `latest` is
|
|
34
|
+
accepted by npm and reaches nobody.
|
|
35
|
+
|
|
36
|
+
## 0.3.4
|
|
37
|
+
|
|
38
|
+
- `tool-call-slow` split from `tool-call-throws`: a timeout is not a defect.
|
|
39
|
+
|
|
40
|
+
## 0.3.3
|
|
41
|
+
|
|
42
|
+
- `publish_gate_tools` accepts `call: ["name"]` to really invoke a tool, opt-in
|
|
43
|
+
because some tools open applications or write files.
|
|
44
|
+
|
|
45
|
+
## 0.3.2
|
|
46
|
+
|
|
47
|
+
- New rule `node-too-old`: `engines.node` says one thing, `lib/` uses an API from
|
|
48
|
+
a newer Node. npm only warns on `engines`, so the plugin installs and then
|
|
49
|
+
throws at import.
|
|
50
|
+
|
|
51
|
+
## 0.3.1
|
|
52
|
+
|
|
53
|
+
- `publish_gate_commands` emits commands that actually run on this machine
|
|
54
|
+
(`cmd /c "npm publish --access public"` on Windows) and pairs the publish step
|
|
55
|
+
with a `publish_gate_verify` shasum check.
|
|
56
|
+
|
|
57
|
+
## 0.3.0
|
|
58
|
+
|
|
59
|
+
- New layer: the browser half. `checkClientHalf` / `checkClientRuntime` load the
|
|
60
|
+
bundle against a fake `window.__ModuleLoader__` and report a bundle that never
|
|
61
|
+
loads, never returns `apply`, throws, registers nothing, or uses a service its
|
|
62
|
+
`inject` array does not declare.
|
|
63
|
+
- New layer: market readiness — the repo is already listed, another entry
|
|
64
|
+
publishes the same npm name, or the repo is under 24 hours old (the gate fails
|
|
65
|
+
and reruns itself).
|
|
66
|
+
- New rules: `client-without-platform`, `pack-omits-client`, `pack-ships-secret`,
|
|
67
|
+
`private-field-set`, `publish-config-registry`, `tool-bad-parameter-type`,
|
|
68
|
+
`entry-path-missing`, `bundle-patch-not-declared`, `inject-unused-service`.
|
|
69
|
+
- New tool `publish_gate_client` (9 tools).
|
|
70
|
+
|
|
71
|
+
## 0.2.0
|
|
72
|
+
|
|
73
|
+
- New layers: artifact (`npm pack --dry-run` — what npm will really ship) and
|
|
74
|
+
runtime (import the entry in an isolated child process, call `apply`, report a
|
|
75
|
+
throwing import, a throwing apply, missing execute, duplicate tool names, a
|
|
76
|
+
throwing `presentCall`, and card kinds the host does not render).
|
|
77
|
+
- New tools `publish_gate_tools` and `publish_gate_verify`.
|
|
78
|
+
- Fixed: `bad-repo-url` never fired for the object form of `repository`.
|
|
79
|
+
|
|
80
|
+
## 0.1.0
|
|
81
|
+
|
|
82
|
+
- First release: manifest rules, `cordis.patch.yml` insert checks, README and
|
|
83
|
+
test presence, npm name and registry state.
|
package/README.md
CHANGED
|
@@ -68,9 +68,11 @@ dsh plugin --profile desktop add @bushizhanghan/dsh-publish-gate
|
|
|
68
68
|
| `version-taken` | 这个版本已经在 registry 上,再发是 403 |
|
|
69
69
|
| `version-behind-latest` | 本地版本比 registry 上的 `latest` **旧**。npm 会照收,但 `latest` 继续指向新版本——包上传了,按名字装的人永远拿不到,且没有任何报错 |
|
|
70
70
|
| `registry-unreachable` | registry 回了 500/429 —— 版本检查**没跑**,所以别把它当干净 |
|
|
71
|
+
| `npm-not-logged-in` | 这台机器上 npm 没有可用登录(`npm whoami` 报 ENEEDAUTH)。`npm publish` 会在 tarball 打好之后才失败 |
|
|
72
|
+
| `readme-wrong-package` | README 的安装命令写的是**另一个包名**(从别的插件抄过来没改)。照着装的人装到别的插件上,而且不会报错——那个包真的存在 |
|
|
71
73
|
|
|
72
74
|
### 值得在发布前知道的(warn / info)
|
|
73
|
-
`unscoped-name`(granular token 的 403)、`repo-missing`、`patch-name-unresolvable`(junction 放错层级)、`inject-unused-service`(`inject` 里写了个拼错的服务名,cordis 一直等它,插件静默不激活)、`no-publish-config-access`(scoped 包没写 `publishConfig.access`,只是提醒,因为命令里本来就带 `--access public`)、`publish-config-registry`(`publishConfig.registry` 指到内网 registry,版本发到别人装不到的地方)、`no-readme` / `readme-no-install` / `readme-no-profile`、`license-mismatch`、`no-tests` / `no-test-files` / `no-test-script`、`no-engines`、`bad-repo-url`、`no-git` / `no-gitignore`、`readme-tool-undocumented`(README 没提这个工具)/ `readme-test-count`(README 写的测试数和真实数不一致),以及市场条目的 `market-*` 全系列。
|
|
75
|
+
`npm-scope-mismatch`(登录的账号不是包 scope 的所有者,PUT 会 403,而错误里既不说 token 也不说 scope——我为一个 403 花了一小时)、`unscoped-name`(granular token 的 403)、`repo-missing`、`patch-name-unresolvable`(junction 放错层级)、`inject-unused-service`(`inject` 里写了个拼错的服务名,cordis 一直等它,插件静默不激活)、`no-publish-config-access`(scoped 包没写 `publishConfig.access`,只是提醒,因为命令里本来就带 `--access public`)、`publish-config-registry`(`publishConfig.registry` 指到内网 registry,版本发到别人装不到的地方)、`no-readme` / `readme-no-install` / `readme-no-profile`、`license-mismatch`、`no-tests` / `no-test-files` / `no-test-script`、`no-engines`、`bad-repo-url`、`no-git` / `no-gitignore`、`readme-tool-undocumented`(README 没提这个工具)/ `readme-test-count`(README 写的测试数和真实数不一致),以及市场条目的 `market-*` 全系列。
|
|
74
76
|
|
|
75
77
|
### 产物层:npm 到底会发出什么
|
|
76
78
|
|
|
@@ -102,6 +104,8 @@ dsh plugin --profile desktop add @bushizhanghan/dsh-publish-gate
|
|
|
102
104
|
| `tool-present-call-throws` | `presentCall` 抛——调用记录渲染时炸 |
|
|
103
105
|
| `tool-bad-card-kind` | `card: 'generic'` 的 `kind` 不在宿主认识的那一组里(`other` / `execute` / `search` / `read` / `edit` / `delete` / `fetch`)。注册不报错,卡片渲染坏掉——我五个插件一起踩过 |
|
|
104
106
|
| `tool-bad-parameter-type` | `parameters` 里某个 `type` 写错(`"strin"`)。宿主把 `parameters` 渲染进 prompt、但**从不校验**调用值,所以这个 typo 只是悄悄把类型提示弄丢 |
|
|
107
|
+
| `tool-undeclared-param` | `execute` 读了 `args.x`,但 `parameters.properties` 里没有 `x`。模型只能传 schema 声明过的参数,所以这个值在真实调用里永远是 `undefined`——代码在,永远不跑,也不报错。`args.x`、`args['x']`、`const { x } = args` 三种写法都认 |
|
|
108
|
+
| `tool-output-invalid` | 工具返回的东西**自己的 output schema 不收**。宿主每个结果都拿 schema 校验,不匹配就抛 `ToolOutputError`——工具从第一次调用起就不可用,而报错只给一个 JSON 路径,不给是哪行代码返回的。只有点名 `call` 真的调了才检查。这个插件自己踩过:schema 收紧成 `additionalProperties: false`,但有一条错误分支还在返回 `{ ok, error }` |
|
|
105
109
|
|
|
106
110
|
### 浏览器半边:侧栏面板到底会不会出现
|
|
107
111
|
|
|
@@ -210,7 +214,7 @@ dsh plugin --profile desktop add @bushizhanghan/dsh-publish-gate
|
|
|
210
214
|
## 开发
|
|
211
215
|
|
|
212
216
|
```sh
|
|
213
|
-
node --test #
|
|
217
|
+
node --test # 88 个引擎单测
|
|
214
218
|
node test/integration.test.mjs # 26 条集成断言(9 个真实插件作 fixture)
|
|
215
219
|
```
|
|
216
220
|
|
package/lib/engine.js
CHANGED
|
@@ -517,6 +517,33 @@ export function checkDocs(dir) {
|
|
|
517
517
|
if (text && !/dsh plugin .*add .*--profile/.test(text) && !/dsh plugin --profile \S+ add/.test(text)) {
|
|
518
518
|
out.push(F('readme-no-profile', 'info', 'README install line omits --profile', 'Omitting it installs into the default profile, which is not what every user wants.', 'Show `dsh plugin --profile desktop add <pkg>`.'))
|
|
519
519
|
}
|
|
520
|
+
// A README copied from another plugin keeps the old package name, and the
|
|
521
|
+
// install line then installs something else entirely — with no error, since
|
|
522
|
+
// that package really does exist.
|
|
523
|
+
const pkg = readPkgJson(dir)
|
|
524
|
+
if (text && pkg?.name) {
|
|
525
|
+
const specs = [...text.matchAll(/dsh plugin\b[^\n]*?\badd\s+(?:--profile\s+\S+\s+)?(\S+)/g)]
|
|
526
|
+
.map((m) => m[1].replace(/[`'"]/g, '').trim())
|
|
527
|
+
.filter((s) => s && !s.startsWith('-'))
|
|
528
|
+
const wrong = [...new Set(specs)].filter((s) => {
|
|
529
|
+
if (s === pkg.name || s.startsWith('github:') || s.includes('://')) return false
|
|
530
|
+
// A different install FORM is not a copy-paste slip: an unscoped name in
|
|
531
|
+
// a scoped package's README is usually just shorthand for a dependency.
|
|
532
|
+
if (s.startsWith('@') !== pkg.name.startsWith('@')) return false
|
|
533
|
+
return /^@[\w.-]+\/[\w.-]+$|^[\w.-]+$/.test(s)
|
|
534
|
+
})
|
|
535
|
+
for (const spec of wrong) {
|
|
536
|
+
out.push(
|
|
537
|
+
F(
|
|
538
|
+
'readme-wrong-package',
|
|
539
|
+
'error',
|
|
540
|
+
`README install line says "${spec}" but the package is "${pkg.name}"`,
|
|
541
|
+
'A README copied from another plugin keeps the old name. Whoever follows it installs a different package — and nothing fails, because that package exists too.',
|
|
542
|
+
`Change the install line to ${pkg.name}.`,
|
|
543
|
+
),
|
|
544
|
+
)
|
|
545
|
+
}
|
|
546
|
+
}
|
|
520
547
|
}
|
|
521
548
|
if (!existsSync(join(dir, 'LICENSE'))) {
|
|
522
549
|
out.push(F('no-license', 'error', 'LICENSE is missing', 'npm warns on publish and the market listing shows an unknown license.', 'Add a LICENSE file (MIT is the ecosystem norm).'))
|
|
@@ -533,6 +560,66 @@ export function checkDocs(dir) {
|
|
|
533
560
|
// npm / GitHub / market state
|
|
534
561
|
// =====================================================================
|
|
535
562
|
|
|
563
|
+
/**
|
|
564
|
+
* Who npm thinks you are on this machine.
|
|
565
|
+
*
|
|
566
|
+
* A publish fails at the very last step with ENEEDAUTH or a 403 that names
|
|
567
|
+
* neither the token nor the scope. Asking first turns that into a message
|
|
568
|
+
* instead of a spent version number.
|
|
569
|
+
*/
|
|
570
|
+
export async function checkPublishIdentity(dir, { timeoutMs = 15000, whoami } = {}) {
|
|
571
|
+
const out = []
|
|
572
|
+
let who = whoami
|
|
573
|
+
if (who === undefined) {
|
|
574
|
+
try {
|
|
575
|
+
const isWin = process.platform === 'win32'
|
|
576
|
+
const cmd = isWin ? process.env.ComSpec || 'cmd.exe' : 'npm'
|
|
577
|
+
const args = isWin ? ['/d', '/s', '/c', 'npm whoami'] : ['whoami']
|
|
578
|
+
const { stdout } = await runFile(cmd, args, { timeout: timeoutMs, windowsHide: true })
|
|
579
|
+
who = stdout.trim() || null
|
|
580
|
+
} catch (error) {
|
|
581
|
+
const text = String(error?.stderr || error?.message || error)
|
|
582
|
+
if (/ENEEDAUTH|E401|not logged in|need auth/i.test(text)) {
|
|
583
|
+
out.push(
|
|
584
|
+
F(
|
|
585
|
+
'npm-not-logged-in',
|
|
586
|
+
'error',
|
|
587
|
+
'npm has no usable login on this machine',
|
|
588
|
+
'`npm publish` will fail with ENEEDAUTH after the tarball has been built. A granular token that lacks publish rights fails later still, with a 403 that names neither the token nor the scope.',
|
|
589
|
+
'Run `npm login`, or put a publish-capable token in ~/.npmrc.',
|
|
590
|
+
),
|
|
591
|
+
)
|
|
592
|
+
} else {
|
|
593
|
+
out.push(
|
|
594
|
+
F(
|
|
595
|
+
'npm-whoami-unavailable',
|
|
596
|
+
'info',
|
|
597
|
+
'could not ask npm who is logged in',
|
|
598
|
+
'The publish-identity check did not run, so nothing is claimed about it.',
|
|
599
|
+
'Make sure npm is on PATH and re-run.',
|
|
600
|
+
),
|
|
601
|
+
)
|
|
602
|
+
}
|
|
603
|
+
return out
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
const pkg = readPkgJson(dir)
|
|
608
|
+
const scope = /^@([^/]+)\//.exec(pkg?.name || '')?.[1]
|
|
609
|
+
if (who && scope && who !== scope) {
|
|
610
|
+
out.push(
|
|
611
|
+
F(
|
|
612
|
+
'npm-scope-mismatch',
|
|
613
|
+
'warn',
|
|
614
|
+
`logged in as "${who}" but the package is under @${scope}`,
|
|
615
|
+
`Publishing under a scope needs a token that owns that scope. A granular token scoped to another owner answers 403 on the PUT — the error names neither the token nor the scope, which is why this takes an hour to diagnose.`,
|
|
616
|
+
`Log in as ${scope}, or mint a token with publish rights for @${scope}.`,
|
|
617
|
+
),
|
|
618
|
+
)
|
|
619
|
+
}
|
|
620
|
+
return out
|
|
621
|
+
}
|
|
622
|
+
|
|
536
623
|
export function checkNpmState(dir) {
|
|
537
624
|
const out = []
|
|
538
625
|
const json = readPkgJson(dir)
|
|
@@ -1057,6 +1144,31 @@ const walkParams = (schema, path) => {
|
|
|
1057
1144
|
}
|
|
1058
1145
|
if (schema.items) walkParams(schema.items, path + '[]')
|
|
1059
1146
|
}
|
|
1147
|
+
// The model only ever sends the arguments \`parameters\` declares — the host
|
|
1148
|
+
// renders that schema into the prompt and passes nothing else. So an execute
|
|
1149
|
+
// that reads args.dir while the schema never mentions dir is unreachable code:
|
|
1150
|
+
// every call sees undefined, and the tool looks broken with no error anywhere.
|
|
1151
|
+
const ARG_DOT = /\\bargs\\s*\\??\\.\\s*([A-Za-z_$][\\w$]*)/g
|
|
1152
|
+
const ARG_INDEX = /\\bargs\\s*\\[\\s*['"]([^'"]+)['"]\\s*\\]/g
|
|
1153
|
+
const ARG_DESTRUCTURE = /\\{\\s*([^{}]+?)\\s*\\}\\s*=\\s*args\\b/g
|
|
1154
|
+
const undeclaredArgs = (def) => {
|
|
1155
|
+
const props = def?.parameters?.properties
|
|
1156
|
+
if (!props || typeof props !== 'object') return []
|
|
1157
|
+
const declared = Object.keys(props)
|
|
1158
|
+
if (typeof def.execute !== 'function') return []
|
|
1159
|
+
let src = ''
|
|
1160
|
+
try { src = def.execute.toString() } catch { return [] }
|
|
1161
|
+
const reads = []
|
|
1162
|
+
for (const m of src.matchAll(ARG_DOT)) reads.push(m[1])
|
|
1163
|
+
for (const m of src.matchAll(ARG_INDEX)) reads.push(m[1])
|
|
1164
|
+
for (const m of src.matchAll(ARG_DESTRUCTURE)) {
|
|
1165
|
+
for (const part of m[1].split(',')) {
|
|
1166
|
+
const key = part.split(':')[0].split('=')[0].replace(/\\.\\.\\./, '').trim()
|
|
1167
|
+
if (/^[A-Za-z_$][\\w$]*$/.test(key)) reads.push(key)
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
return [...new Set(reads)].filter((r) => !declared.includes(r))
|
|
1171
|
+
}
|
|
1060
1172
|
const noopService = () => new Proxy(function () {}, {
|
|
1061
1173
|
get: (target, prop) => {
|
|
1062
1174
|
// Own properties win, or ctx.tools would resolve to another no-op proxy
|
|
@@ -1085,12 +1197,58 @@ ctx.tools = {
|
|
|
1085
1197
|
name: typeof def?.name === 'string' ? def.name : null,
|
|
1086
1198
|
hasExecute: typeof def?.execute === 'function',
|
|
1087
1199
|
hasOutputSchema: Boolean(def && def.output && def.output.schema),
|
|
1200
|
+
schema: (def && def.output && def.output.schema) || null,
|
|
1088
1201
|
cardKind: card,
|
|
1202
|
+
undeclaredParams: undeclaredArgs(def),
|
|
1089
1203
|
run: typeof def?.execute === 'function' ? def.execute : null,
|
|
1090
1204
|
})
|
|
1091
1205
|
walkParams(def?.parameters, typeof def?.name === 'string' ? def.name : '(unnamed)')
|
|
1092
1206
|
},
|
|
1093
1207
|
}
|
|
1208
|
+
// The host validates every tool result against output.schema and throws
|
|
1209
|
+
// ToolOutputError when it does not match — the tool is then unusable, and the
|
|
1210
|
+
// failure names a JSON path rather than the code that produced it. This gate
|
|
1211
|
+
// shipped exactly that bug: a permissive schema was tightened to
|
|
1212
|
+
// additionalProperties:false while one error path still returned { ok, error }.
|
|
1213
|
+
const validateValue = (schema, value, path) => {
|
|
1214
|
+
if (!schema || typeof schema !== 'object') return null
|
|
1215
|
+
const at = path || '$'
|
|
1216
|
+
if (typeof schema.type === 'string' && schema.type !== 'json') {
|
|
1217
|
+
const t = schema.type
|
|
1218
|
+
const ok = t === 'array' ? Array.isArray(value)
|
|
1219
|
+
: t === 'null' ? value === null
|
|
1220
|
+
: t === 'integer' ? Number.isInteger(value)
|
|
1221
|
+
: t === 'object' ? (value !== null && typeof value === 'object' && !Array.isArray(value))
|
|
1222
|
+
: typeof value === t
|
|
1223
|
+
if (!ok) return at + ' must be ' + t + ', got ' + (value === null ? 'null' : Array.isArray(value) ? 'array' : typeof value)
|
|
1224
|
+
}
|
|
1225
|
+
if (Array.isArray(schema.enum) && !schema.enum.some((e) => JSON.stringify(e) === JSON.stringify(value))) {
|
|
1226
|
+
return at + ' is not one of ' + JSON.stringify(schema.enum)
|
|
1227
|
+
}
|
|
1228
|
+
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
|
1229
|
+
for (const key of Array.isArray(schema.required) ? schema.required : []) {
|
|
1230
|
+
if (!(key in value)) return at + '.' + key + ' is required but missing'
|
|
1231
|
+
}
|
|
1232
|
+
if (schema.additionalProperties === false && schema.properties && typeof schema.properties === 'object') {
|
|
1233
|
+
const extra = Object.keys(value).filter((k) => !(k in schema.properties))
|
|
1234
|
+
if (extra.length) return at + ' has key(s) the schema does not declare: ' + extra.join(', ')
|
|
1235
|
+
}
|
|
1236
|
+
if (schema.properties && typeof schema.properties === 'object') {
|
|
1237
|
+
for (const [key, sub] of Object.entries(schema.properties)) {
|
|
1238
|
+
if (!(key in value)) continue
|
|
1239
|
+
const bad = validateValue(sub, value[key], at + '.' + key)
|
|
1240
|
+
if (bad) return bad
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
if (Array.isArray(value) && schema.items) {
|
|
1245
|
+
for (let i = 0; i < value.length; i++) {
|
|
1246
|
+
const bad = validateValue(schema.items, value[i], at + '[' + i + ']')
|
|
1247
|
+
if (bad) return bad
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
return null
|
|
1251
|
+
}
|
|
1094
1252
|
const emit = (payload) => process.stdout.write('\\n@@PG@@' + JSON.stringify(payload) + '\\n')
|
|
1095
1253
|
let stage = 'import'
|
|
1096
1254
|
try {
|
|
@@ -1108,11 +1266,11 @@ try {
|
|
|
1108
1266
|
for (const entry of registered) {
|
|
1109
1267
|
if (!wanted.has(entry.name) || !entry.run) continue
|
|
1110
1268
|
try {
|
|
1111
|
-
await Promise.race([
|
|
1269
|
+
const value = await Promise.race([
|
|
1112
1270
|
Promise.resolve(entry.run({}, { signal: undefined })),
|
|
1113
1271
|
new Promise((_, reject) => setTimeout(() => reject(new Error('timed out')), budget)),
|
|
1114
1272
|
])
|
|
1115
|
-
calls.push({ name: entry.name, ok: true })
|
|
1273
|
+
calls.push({ name: entry.name, ok: true, invalid: validateValue(entry.schema, value) })
|
|
1116
1274
|
} catch (error) {
|
|
1117
1275
|
const message = String((error && error.message) || error)
|
|
1118
1276
|
// A slow tool is not a broken tool: of_env shells out to WSL and takes
|
|
@@ -1185,7 +1343,20 @@ export async function checkToolRegistrations(dir, opts = {}) {
|
|
|
1185
1343
|
|
|
1186
1344
|
const seen = new Set()
|
|
1187
1345
|
for (const c of Array.isArray(payload.calls) ? payload.calls : []) {
|
|
1188
|
-
if (c.ok)
|
|
1346
|
+
if (c.ok) {
|
|
1347
|
+
if (c.invalid) {
|
|
1348
|
+
out.push(
|
|
1349
|
+
F(
|
|
1350
|
+
'tool-output-invalid',
|
|
1351
|
+
'error',
|
|
1352
|
+
`tool "${c.name}" returns a value its own output schema rejects: ${c.invalid}`,
|
|
1353
|
+
'The host validates every result against output.schema and throws ToolOutputError on a mismatch. The tool is unusable from the first call, and the error names a JSON path rather than the line that produced it.',
|
|
1354
|
+
'Either widen the schema or change what execute returns so the two agree.',
|
|
1355
|
+
),
|
|
1356
|
+
)
|
|
1357
|
+
}
|
|
1358
|
+
continue
|
|
1359
|
+
}
|
|
1189
1360
|
if (c.timedOut) {
|
|
1190
1361
|
out.push(
|
|
1191
1362
|
F(
|
|
@@ -1229,6 +1400,21 @@ export async function checkToolRegistrations(dir, opts = {}) {
|
|
|
1229
1400
|
),
|
|
1230
1401
|
)
|
|
1231
1402
|
}
|
|
1403
|
+
|
|
1404
|
+
// The model only sends what `parameters` declares, so a read of anything
|
|
1405
|
+
// else is unreachable: args.dir is undefined on every real call, and the
|
|
1406
|
+
// tool looks broken while nothing throws.
|
|
1407
|
+
for (const name of Array.isArray(t.undeclaredParams) ? t.undeclaredParams : []) {
|
|
1408
|
+
out.push(
|
|
1409
|
+
F(
|
|
1410
|
+
'tool-undeclared-param',
|
|
1411
|
+
'warn',
|
|
1412
|
+
`tool "${label}" reads args.${name}, which parameters never declares`,
|
|
1413
|
+
'The host renders parameters into the prompt and passes the model nothing else, so this value is always undefined on a real call. The code path exists and never runs.',
|
|
1414
|
+
`Add "${name}" to parameters.properties, or stop reading it.`,
|
|
1415
|
+
),
|
|
1416
|
+
)
|
|
1417
|
+
}
|
|
1232
1418
|
}
|
|
1233
1419
|
const badParams = Array.isArray(payload.badParamTypes) ? payload.badParamTypes : []
|
|
1234
1420
|
for (const where of badParams) {
|
|
@@ -1918,14 +2104,14 @@ const RULE_ORDER = [
|
|
|
1918
2104
|
'inject-missing-tools', 'inject-missing-web', 'inject-missing-webServer', 'inject-missing-timer',
|
|
1919
2105
|
'inject-unused-service',
|
|
1920
2106
|
'tool-missing-name', 'tool-duplicate-name', 'tool-missing-execute', 'tool-present-call-throws',
|
|
1921
|
-
'tool-bad-card-kind', 'tool-missing-output-schema', 'tool-none-registered', 'tool-bad-parameter-type', 'tool-call-throws', 'tool-call-slow',
|
|
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',
|
|
1922
2108
|
'lib-debugger', 'lib-console-log', 'lib-hardcoded-path', 'lib-url-pathname', 'node-too-old', 'stale-build', 'hand-edited-build',
|
|
1923
2109
|
'undeclared-import', 'dep-in-dev-only', 'unused-dependency',
|
|
1924
2110
|
'pack-omits-lib', 'pack-omits-patch', 'pack-omits-client', 'pack-oversize', 'pack-omits-readme', 'pack-omits-license', 'pack-ships-secret',
|
|
1925
2111
|
'pack-unexpected-dir', 'pack-script-build', 'pack-unavailable',
|
|
1926
|
-
'no-license', 'license-mismatch', 'version-taken', 'version-behind-latest', 'registry-unreachable', 'publish-not-served',
|
|
2112
|
+
'no-license', 'license-mismatch', 'version-taken', 'version-behind-latest', 'npm-not-logged-in', 'npm-scope-mismatch', 'npm-whoami-unavailable', 'registry-unreachable', 'publish-not-served',
|
|
1927
2113
|
'publish-served-mismatch', 'publish-served', 'market-empty', 'market-no-url', 'market-url-not-github',
|
|
1928
|
-
'market-no-name', 'market-name-shape', 'market-no-category', 'market-bad-category', 'market-no-desc', 'market-no-en', 'market-no-zh', 'unscoped-name', 'no-git', 'no-readme', 'readme-no-install', 'readme-no-profile',
|
|
2114
|
+
'market-no-name', 'market-name-shape', 'market-no-category', 'market-bad-category', 'market-no-desc', 'market-no-en', 'market-no-zh', 'unscoped-name', 'no-git', 'no-readme', 'readme-no-install', 'readme-no-profile', 'readme-wrong-package',
|
|
1929
2115
|
'market-no-repo', 'market-unavailable', 'market-duplicate-entry', 'market-npm-name-collision', 'market-repo-too-new',
|
|
1930
2116
|
'git-uncommitted', 'git-tag-missing', 'git-tag-elsewhere',
|
|
1931
2117
|
'readme-tool-undocumented', 'readme-test-count',
|
package/lib/index.js
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
checkTests,
|
|
23
23
|
checkToolRegistrations,
|
|
24
24
|
checkClientRuntime,
|
|
25
|
+
checkPublishIdentity,
|
|
25
26
|
checkVersionTaken,
|
|
26
27
|
compareVersions,
|
|
27
28
|
latestPublished,
|
|
@@ -64,6 +65,7 @@ export function apply(ctx, config = {}) {
|
|
|
64
65
|
if (pkgJson?.name && pkgJson?.version) {
|
|
65
66
|
findings.push(...(await checkVersionTaken(pkgJson.name, pkgJson.version, { registry: cfg.registry })))
|
|
66
67
|
}
|
|
68
|
+
findings.push(...(await checkPublishIdentity(dir, { timeoutMs: cfg.networkTimeoutMs })))
|
|
67
69
|
// is the GitHub repo there?
|
|
68
70
|
if (pkgJson?.repository) {
|
|
69
71
|
const url = repoUrl(pkgJson.repository)
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bushizhanghan/dsh-publish-gate",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Pre-publish gate for DSH plugins
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"description": "Pre-publish gate for DSH plugins: manifest, dependencies, artifact, runtime, browser half, traceability and market readiness. 发布前的七层门禁。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
"lib",
|
|
14
14
|
"cordis.patch.yml",
|
|
15
15
|
"README.md",
|
|
16
|
-
"LICENSE"
|
|
16
|
+
"LICENSE",
|
|
17
|
+
"CHANGELOG.md"
|
|
17
18
|
],
|
|
18
19
|
"keywords": [
|
|
19
20
|
"dsh",
|