dsh-standards 0.0.3 → 1.0.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/40-checker/README.md +251 -0
- package/40-checker/bin/dsh-levels.mjs +15 -0
- package/40-checker/bin/dsh-plugin-verify.mjs +8 -0
- package/40-checker/fixtures/bad/index.mjs +12 -0
- package/40-checker/fixtures/bad/package.json +15 -0
- package/40-checker/fixtures/bad-manifest/index.js +1 -0
- package/40-checker/fixtures/bad-manifest/package.json +5 -0
- package/40-checker/fixtures/bundle/cordis.patch.yml +3 -0
- package/40-checker/fixtures/bundle/package.json +15 -0
- package/40-checker/fixtures/bundle/server.mjs +7 -0
- package/40-checker/fixtures/bundle-v0/cordis.patch.yml +3 -0
- package/40-checker/fixtures/bundle-v0/package.json +21 -0
- package/40-checker/fixtures/bundle-v0/server.mjs +7 -0
- package/40-checker/fixtures/client-missing/client.js +1 -0
- package/40-checker/fixtures/client-missing/index.js +1 -0
- package/40-checker/fixtures/client-missing/package.json +6 -0
- package/40-checker/fixtures/flow-config/index.js +9 -0
- package/40-checker/fixtures/flow-config/package.json +10 -0
- package/40-checker/fixtures/fs-write/index.mjs +7 -0
- package/40-checker/fixtures/fs-write/package.json +10 -0
- package/40-checker/fixtures/good/index.mjs +10 -0
- package/40-checker/fixtures/good/package.json +19 -0
- package/40-checker/fixtures/hang/index.mjs +5 -0
- package/40-checker/fixtures/hang/package.json +10 -0
- package/40-checker/fixtures/known-answer/expected.json +18 -0
- package/40-checker/fixtures/market-index-real.json +18 -0
- package/40-checker/fixtures/market-index.json +18 -0
- package/40-checker/fixtures/negative/bad-entry/package.json +1 -0
- package/40-checker/fixtures/negative/no-entry/package.json +1 -0
- package/40-checker/fixtures/negative/no-patch/package.json +1 -0
- package/40-checker/fixtures/negative/root-missing/cordis.patch.yml +2 -0
- package/40-checker/fixtures/negative/root-missing/package.json +1 -0
- package/40-checker/fixtures/negative/workspace/package.json +1 -0
- package/40-checker/package.json +22 -0
- package/40-checker/samples/README.md +8 -0
- package/40-checker/samples/entry18.json +139 -0
- package/40-checker/samples/frame.json +485 -0
- package/40-checker/scripts/apply-overlay.mjs +59 -0
- package/40-checker/scripts/attestation-check.mjs +151 -0
- package/40-checker/scripts/bundle-smoke.mjs +126 -0
- package/40-checker/scripts/claim-check.mjs +145 -0
- package/40-checker/scripts/clause-map.mjs +94 -0
- package/40-checker/scripts/collect-fixtures.mjs +44 -0
- package/40-checker/scripts/dashboard.mjs +85 -0
- package/40-checker/scripts/dco-check.mjs +147 -0
- package/40-checker/scripts/dependabot-check.mjs +40 -0
- package/40-checker/scripts/doc-commands-check.mjs +92 -0
- package/40-checker/scripts/entry-scan.mjs +190 -0
- package/40-checker/scripts/entry-validate.mjs +291 -0
- package/40-checker/scripts/entry18-attribution.mjs +66 -0
- package/40-checker/scripts/entry18-ci.mjs +685 -0
- package/40-checker/scripts/entry18-realweb-summary.mjs +71 -0
- package/40-checker/scripts/entry18-web.mjs +307 -0
- package/40-checker/scripts/evidence-chain.mjs +100 -0
- package/40-checker/scripts/flow-index.mjs +82 -0
- package/40-checker/scripts/flow-sample-ci.mjs +529 -0
- package/40-checker/scripts/governance-check.mjs +210 -0
- package/40-checker/scripts/host-farm.mjs +165 -0
- package/40-checker/scripts/journey-config-observe.mjs +109 -0
- package/40-checker/scripts/journey-disclosure-shape.mjs +83 -0
- package/40-checker/scripts/journey-runtime.mjs +178 -0
- package/40-checker/scripts/journey.mjs +76 -0
- package/40-checker/scripts/label-drift.mjs +74 -0
- package/40-checker/scripts/label-sync.mjs +87 -0
- package/40-checker/scripts/label-verify.mjs +90 -0
- package/40-checker/scripts/landlock-exec.c +101 -0
- package/40-checker/scripts/level-audit.mjs +336 -0
- package/40-checker/scripts/m1-sidecar.mjs +126 -0
- package/40-checker/scripts/m2-runner.mjs +213 -0
- package/40-checker/scripts/round-robin.mjs +88 -0
- package/40-checker/scripts/sample-frame.mjs +72 -0
- package/40-checker/scripts/sample20-ci.mjs +25 -0
- package/40-checker/scripts/sample60-ci.mjs +25 -0
- package/40-checker/scripts/stale-check.mjs +59 -0
- package/40-checker/scripts/surveillance.mjs +88 -0
- package/40-checker/scripts/web-probe.mjs +504 -0
- package/40-checker/src/cli.mjs +300 -0
- package/40-checker/src/client-shape.mjs +37 -0
- package/40-checker/src/declarations.mjs +242 -0
- package/40-checker/src/index.mjs +8 -0
- package/40-checker/src/install-spec.mjs +67 -0
- package/40-checker/src/label.mjs +130 -0
- package/40-checker/src/manifest.mjs +381 -0
- package/40-checker/src/permissions.mjs +60 -0
- package/40-checker/src/report.mjs +204 -0
- package/40-checker/src/result.mjs +60 -0
- package/40-checker/src/runtime.mjs +299 -0
- package/40-checker/src/semver.mjs +194 -0
- package/40-checker/src/util.mjs +148 -0
- package/40-checker/test/attestation.test.mjs +59 -0
- package/40-checker/test/checks.test.mjs +172 -0
- package/40-checker/test/claim.test.mjs +28 -0
- package/40-checker/test/client-shape.test.mjs +35 -0
- package/40-checker/test/dco.test.mjs +51 -0
- package/40-checker/test/entry-validate.test.mjs +172 -0
- package/40-checker/test/governance.test.mjs +76 -0
- package/40-checker/test/label.test.mjs +97 -0
- package/40-checker/test/level-audit.test.mjs +99 -0
- package/40-checker/test/levels.test.mjs +31 -0
- package/40-checker/test/ops.test.mjs +117 -0
- package/40-checker/test/permissions.test.mjs +40 -0
- package/40-checker/test/semver.test.mjs +32 -0
- package/40-checker/test/verify.test.mjs +57 -0
- package/CHANGELOG.md +24 -0
- package/LICENSE +28 -0
- package/LICENSE-APACHE +190 -0
- package/LICENSE-CC-BY +23 -0
- package/NOTICE +14 -0
- package/NOTICE.md +11 -0
- package/README.md +146 -3
- package/bin/dsh-levels.mjs +4 -0
- package/bin/dsh-standards.mjs +19 -0
- package/docs/BOOST-FIXTURES-2026-11-02.md +38 -0
- package/docs/DEVELOPER-JOURNEY.md +40 -0
- package/docs/HOST-J3-BUG-REPORT-DRAFT.md +27 -0
- package/docs/HOST-J3-CONFIG-REPRO-2026-11-02.md +36 -0
- package/docs/NAMING-FALLBACK.md +11 -0
- package/docs/fork-policy.md +12 -0
- package/docs/full-test-plan-2026-11-02.md +207 -0
- package/docs/incident.md +37 -0
- package/docs/onboarding.md +27 -0
- package/docs/privacy.md +12 -0
- package/docs/release.md +16 -0
- package/docs/scope-security.md +62 -0
- package/docs/status-taxonomy.md +54 -0
- package/docs/sunset.md +44 -0
- package/docs/translating.md +12 -0
- package/package.json +58 -12
- package/spec/README.md +27 -0
- package/spec/boost/README.md +8 -0
- package/spec/clauses.json +253 -0
- package/spec/entry/README.md +9 -0
- package/spec/flow/README.md +8 -0
- package/spec/levels.json +371 -0
- package/spec/levels.schema.json +45 -0
- package/spec/reference-suite.json +35 -0
- package/spec/schemas/attestation.schema.json +284 -0
- package/spec/schemas/evidence.schema.json +331 -0
- package/spec/schemas/report.schema.json +64 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import { runManifestChecks, DEFAULT_RUNTIME_VERSION } from './manifest.mjs'
|
|
5
|
+
import { checkCapabilities, checkCredentials, checkImportTimeAdvisory, checkLabels } from './declarations.mjs'
|
|
6
|
+
import { runRuntimeChecks } from './runtime.mjs'
|
|
7
|
+
import { spawnSync } from 'node:child_process'
|
|
8
|
+
import { readSources } from './util.mjs'
|
|
9
|
+
import { buildReport, writeReport } from './report.mjs'
|
|
10
|
+
|
|
11
|
+
const VERSION = '0.1.0'
|
|
12
|
+
const DEFAULT_COMMIT = 'ce7073d51254c26cd63b22a60835b8f948deb447'
|
|
13
|
+
const DEFAULT_CORDIS = '4.0.6-alpha.1'
|
|
14
|
+
|
|
15
|
+
function usage() {
|
|
16
|
+
return `${process.env.DSH_CHECKER_BRAND || 'dsh-plugin-verify ' + VERSION} — DS-C conformance checker (MVP)
|
|
17
|
+
|
|
18
|
+
Usage:
|
|
19
|
+
dsh-plugin-verify <package-dir> [options]
|
|
20
|
+
|
|
21
|
+
Options:
|
|
22
|
+
--format text|json|sarif output format (default: text)
|
|
23
|
+
--scope mvp|ds-c|entry mvp = 8 MVP ids; ds-c = full claim (default); entry = compatibility gate
|
|
24
|
+
--profile package|listing|both evaluation profile (default: package; listing if --market-index is set)
|
|
25
|
+
--runtime <path> path to a built @deepseek-ai/cordis lib/index.js; enables L1-L3
|
|
26
|
+
--runtime-version <v> dsh runtime version used for C-2 (default: ${DEFAULT_RUNTIME_VERSION})
|
|
27
|
+
--cordis-version <v> cordis version recorded in the anchor (default: ${DEFAULT_CORDIS})
|
|
28
|
+
--commit <sha> harness commit recorded in the anchor (default: ${DEFAULT_COMMIT.slice(0, 12)})
|
|
29
|
+
--install-spec <spec> recommended install spec for C-3 (listing/install check)
|
|
30
|
+
--market-index <file> format-1 market index JSON for C-3 listing mode
|
|
31
|
+
--plugin-name <name> plugin name inside the market index
|
|
32
|
+
--timeout <ms> L1-L3 timeout (default: 10000)
|
|
33
|
+
--no-exec skip L1-L3 entirely (reported as not-run/NA, never PASS)
|
|
34
|
+
--static-only M1 mode: listing profile, only C-1/C-2/C-3/C-11/C-12/C-13; C-4..C-9 are out of scope
|
|
35
|
+
--treat-as-plugin run L1-L3 on a bundle's server entry (lib/index.js, index.js, lib/public-entry.js, src/index.ts)
|
|
36
|
+
--entry-runtime require|server-entry entry scope: require full G0-G3 (default) or accept server-entry proxy (not bundle)
|
|
37
|
+
--entry-claim runtime|static entry scope: runtime (default, needs DSC-14) or static recognized (no runtime claim)
|
|
38
|
+
--host <file> app-boot lib/index.js; enables bundle G0-G3 smoke
|
|
39
|
+
--base <file> optional base cordis.patch.yml for bundle smoke
|
|
40
|
+
--web <file> optional web-app cordis.patch.yml for bundle smoke
|
|
41
|
+
--bare <dir> optional bare-module base for bundle smoke (host farm)
|
|
42
|
+
--out <file> write the report to a file (stdout otherwise)
|
|
43
|
+
--help show this help
|
|
44
|
+
--version show the checker version
|
|
45
|
+
|
|
46
|
+
Exit codes:
|
|
47
|
+
0 completed, no FAIL (PASS or WARN; WARN does not claim a clean PASS)
|
|
48
|
+
1 completed, at least one FAIL
|
|
49
|
+
2 incomplete: a mandatory check did not run, or the checker itself failed
|
|
50
|
+
3 usage error
|
|
51
|
+
`
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function parseArgs(argv) {
|
|
55
|
+
const options = {
|
|
56
|
+
target: null,
|
|
57
|
+
format: 'text',
|
|
58
|
+
scope: 'ds-c',
|
|
59
|
+
profile: null,
|
|
60
|
+
runtime: null,
|
|
61
|
+
runtimeVersion: DEFAULT_RUNTIME_VERSION,
|
|
62
|
+
cordisVersion: DEFAULT_CORDIS,
|
|
63
|
+
commit: DEFAULT_COMMIT,
|
|
64
|
+
installSpec: null,
|
|
65
|
+
marketIndex: null,
|
|
66
|
+
pluginName: null,
|
|
67
|
+
timeoutMs: 10_000,
|
|
68
|
+
noExec: false,
|
|
69
|
+
staticOnly: false,
|
|
70
|
+
treatAsPlugin: false,
|
|
71
|
+
entryRuntime: 'require',
|
|
72
|
+
entryClaim: 'runtime',
|
|
73
|
+
host: null,
|
|
74
|
+
base: null,
|
|
75
|
+
web: null,
|
|
76
|
+
bare: null,
|
|
77
|
+
out: null,
|
|
78
|
+
help: false,
|
|
79
|
+
version: false,
|
|
80
|
+
}
|
|
81
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
82
|
+
const arg = argv[index]
|
|
83
|
+
if (arg === '--help' || arg === '-h') { options.help = true; continue }
|
|
84
|
+
if (arg === '--version' || arg === '-v') { options.version = true; continue }
|
|
85
|
+
if (!arg.startsWith('--')) {
|
|
86
|
+
if (options.target !== null) throw new Error(`unexpected positional argument: ${arg}`)
|
|
87
|
+
options.target = arg
|
|
88
|
+
continue
|
|
89
|
+
}
|
|
90
|
+
const [key, inline] = arg.split('=', 2)
|
|
91
|
+
const value = () => {
|
|
92
|
+
if (inline !== undefined) return inline
|
|
93
|
+
const next = argv[index + 1]
|
|
94
|
+
if (next === undefined || next.startsWith('--')) throw new Error(`missing value for ${key}`)
|
|
95
|
+
index += 1
|
|
96
|
+
return next
|
|
97
|
+
}
|
|
98
|
+
switch (key) {
|
|
99
|
+
case '--format': options.format = value(); break
|
|
100
|
+
case '--scope': options.scope = value(); break
|
|
101
|
+
case '--profile': options.profile = value(); break
|
|
102
|
+
case '--runtime': options.runtime = value(); break
|
|
103
|
+
case '--runtime-version': options.runtimeVersion = value(); break
|
|
104
|
+
case '--cordis-version': options.cordisVersion = value(); break
|
|
105
|
+
case '--commit': options.commit = value(); break
|
|
106
|
+
case '--install-spec': options.installSpec = value(); break
|
|
107
|
+
case '--market-index': options.marketIndex = value(); break
|
|
108
|
+
case '--plugin-name': options.pluginName = value(); break
|
|
109
|
+
case '--timeout': options.timeoutMs = Number(value()); break
|
|
110
|
+
case '--out': options.out = value(); break
|
|
111
|
+
case '--no-exec': options.noExec = true; break
|
|
112
|
+
case '--static-only': options.staticOnly = true; break
|
|
113
|
+
case '--treat-as-plugin': options.treatAsPlugin = true; break
|
|
114
|
+
case '--entry-runtime': options.entryRuntime = value(); break
|
|
115
|
+
case '--entry-claim': options.entryClaim = value(); break
|
|
116
|
+
case '--host': options.host = value(); break
|
|
117
|
+
case '--base': options.base = value(); break
|
|
118
|
+
case '--web': options.web = value(); break
|
|
119
|
+
case '--bare': options.bare = value(); break
|
|
120
|
+
default: throw new Error(`unknown option: ${key}`)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (!['text', 'json', 'sarif'].includes(options.format)) throw new Error(`unsupported --format: ${options.format}`)
|
|
124
|
+
if (!['mvp', 'ds-c', 'entry'].includes(options.scope)) throw new Error(`unsupported --scope: ${options.scope}`)
|
|
125
|
+
if (!['require', 'server-entry'].includes(options.entryRuntime)) throw new Error('--entry-runtime must be require or server-entry')
|
|
126
|
+
if (!['runtime', 'static'].includes(options.entryClaim)) throw new Error('--entry-claim must be runtime or static')
|
|
127
|
+
if (options.staticOnly) { options.scope = 'ds-c'; options.profile = 'listing' }
|
|
128
|
+
if (!options.profile) options.profile = options.marketIndex ? 'listing' : 'package'
|
|
129
|
+
if (!['package', 'listing', 'both'].includes(options.profile)) throw new Error(`unsupported --profile: ${options.profile}`)
|
|
130
|
+
if (options.staticOnly && options.treatAsPlugin) throw new Error('--static-only and --treat-as-plugin are mutually exclusive')
|
|
131
|
+
if (!Number.isFinite(options.timeoutMs) || options.timeoutMs < 100 || options.timeoutMs > 120_000) throw new Error('--timeout must be between 100 and 120000 ms')
|
|
132
|
+
if (options.target === null) options.target = '.'
|
|
133
|
+
return options
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function entryRunCheck(kind, runtimeChecks, options, dir) {
|
|
137
|
+
const byId = new Map(runtimeChecks.map(item => [item.id, item]))
|
|
138
|
+
const c4 = byId.get('DSC-04')
|
|
139
|
+
const base = {
|
|
140
|
+
id: 'DSC-14', clause: 'C-14', layer: 'L1', method: 'M', advisory: false,
|
|
141
|
+
details: [], evidence: c4?.evidence ?? [],
|
|
142
|
+
}
|
|
143
|
+
if (options.entryClaim === 'static') {
|
|
144
|
+
return { ...base, result: 'NA', coverage: 'not-run', reason: 'entry static claim: no runtime evidence' }
|
|
145
|
+
}
|
|
146
|
+
if (['client', 'preset', 'skill', 'theme', 'mcp-config', 'unknown'].includes(kind)) {
|
|
147
|
+
return { ...base, result: 'NA', coverage: 'not-run', reason: `kind=${kind}; no runtime claim` }
|
|
148
|
+
}
|
|
149
|
+
if (kind === 'bundle' && options.entryRuntime !== 'server-entry') {
|
|
150
|
+
if (!options.host) return { ...base, result: 'ERROR', coverage: 'not-run', reason: 'bundle runtime requires --host <app-boot/lib/index.js> for G0-G3' }
|
|
151
|
+
const script = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'scripts', 'bundle-smoke.mjs')
|
|
152
|
+
const args = [script, '--package', dir, '--host', options.host, '--json', '--timeout', String(options.timeoutMs)]
|
|
153
|
+
if (options.base) args.push('--base', options.base)
|
|
154
|
+
if (options.web) args.push('--web', options.web)
|
|
155
|
+
if (options.bare) args.push('--bare', options.bare)
|
|
156
|
+
const run = spawnSync(process.execPath, args, { encoding: 'utf8', timeout: options.timeoutMs + 5000, maxBuffer: 8 * 1024 * 1024 })
|
|
157
|
+
if (run.error) return { ...base, result: 'ERROR', coverage: 'not-run', reason: `bundle smoke failed to start: ${run.error.message}` }
|
|
158
|
+
let smoke
|
|
159
|
+
try { smoke = JSON.parse(run.stdout) } catch { return { ...base, result: 'ERROR', coverage: 'not-run', reason: `bundle smoke produced no JSON: ${(run.stderr || '').slice(0, 300)}` } }
|
|
160
|
+
const state = smoke.g3?.state
|
|
161
|
+
if (smoke.g2?.compatibility === 'reject') return { ...base, result: 'FAIL', coverage: 'full', reason: 'G2 gate rejected: host would skip this bundle', details: [{ item: 'G2', result: 'FAIL', reason: JSON.stringify(smoke.g2) }] }
|
|
162
|
+
if (state === 'verified' && smoke.g4?.ok === false) return { ...base, result: 'FAIL', coverage: 'full', reason: 'G4 failed: dsh.client has no package-root row or missing client export', details: [{ item: 'G4', result: 'FAIL', reason: JSON.stringify(smoke.g4) }] }
|
|
163
|
+
if (state === 'verified') return { ...base, result: 'PASS', coverage: 'full', reason: `G0-G3 verified; rows=${smoke.g3.active}/${smoke.g3.rows.length}${smoke.g4?.hasClient ? '; G4 static ok' : ''}`, details: [{ item: 'G0', result: 'PASS', reason: 'patch parsed' }, { item: 'G1', result: 'PASS', reason: `rows ${smoke.g1.targetRows}` }, { item: 'G3', result: 'PASS', reason: `rows ${smoke.g3.active}/${smoke.g3.rows.length} ACTIVE` }] }
|
|
164
|
+
if (state === 'partial') return { ...base, result: 'ERROR', coverage: 'not-run', reason: `G3 partial: ${smoke.g3.active}/${smoke.g3.rows.length} ACTIVE; target profile does not provide all services`, details: [{ item: 'G3', result: 'WARN', reason: JSON.stringify(smoke.g3.rows) }] }
|
|
165
|
+
return { ...base, result: 'FAIL', coverage: 'full', reason: `bundle smoke failed: ${JSON.stringify(smoke.g3?.rows ?? [])}` }
|
|
166
|
+
}
|
|
167
|
+
if (c4?.result === 'PASS') {
|
|
168
|
+
return { ...base, result: 'PASS', coverage: 'full', reason: kind === 'bundle' ? 'server-entry ACTIVE only; bundle G0-G4 not run' : '' }
|
|
169
|
+
}
|
|
170
|
+
if (c4?.result === 'FAIL') return { ...base, result: 'FAIL', coverage: 'full', reason: c4.reason || 'runtime failed' }
|
|
171
|
+
return { ...base, result: 'ERROR', coverage: 'not-run', reason: c4?.reason || 'no runtime evidence' }
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function anchorOf(options, runtime) {
|
|
175
|
+
return {
|
|
176
|
+
repo: 'deepseek-ai/deepseek-harness',
|
|
177
|
+
commit: options.commit,
|
|
178
|
+
scanner: `dsh-plugin-verify@${VERSION}`,
|
|
179
|
+
dsh: options.runtimeVersion,
|
|
180
|
+
cordis: options.cordisVersion,
|
|
181
|
+
runtime: runtime ? path.resolve(runtime) : null,
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export async function main(argv = process.argv.slice(2)) {
|
|
186
|
+
let options
|
|
187
|
+
try {
|
|
188
|
+
options = parseArgs(argv)
|
|
189
|
+
} catch (error) {
|
|
190
|
+
process.stderr.write(`dsh-plugin-verify: ${error.message}\n`)
|
|
191
|
+
process.stderr.write(usage())
|
|
192
|
+
return 3
|
|
193
|
+
}
|
|
194
|
+
if (options.help) { process.stdout.write(usage()); return 0 }
|
|
195
|
+
if (options.version) { process.stdout.write(`${VERSION}\n`); return 0 }
|
|
196
|
+
|
|
197
|
+
const dir = path.resolve(options.target)
|
|
198
|
+
let stat
|
|
199
|
+
try { stat = fs.statSync(dir) } catch {
|
|
200
|
+
process.stderr.write(`dsh-plugin-verify: target not found: ${dir}\n`)
|
|
201
|
+
return 3
|
|
202
|
+
}
|
|
203
|
+
if (!stat.isDirectory()) {
|
|
204
|
+
process.stderr.write(`dsh-plugin-verify: target must be a package directory (tarball support is not in the MVP): ${dir}\n`)
|
|
205
|
+
return 3
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const { manifest, kind, kindInferred, entry, patch, g4, checks: manifestChecks } = runManifestChecks(dir, {
|
|
209
|
+
runtimeVersion: options.runtimeVersion,
|
|
210
|
+
installSpec: options.installSpec,
|
|
211
|
+
marketIndex: options.marketIndex,
|
|
212
|
+
pluginName: options.pluginName,
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
let sources = []
|
|
216
|
+
try { sources = readSources(dir) } catch { sources = [] }
|
|
217
|
+
const declarationChecks = manifest ? [
|
|
218
|
+
checkCapabilities(manifest, sources, patch),
|
|
219
|
+
checkCredentials(manifest, sources),
|
|
220
|
+
checkLabels(manifest, sources),
|
|
221
|
+
] : [
|
|
222
|
+
{ id: 'DSC-11', clause: 'C-11', layer: 'L0', result: 'ERROR', coverage: 'not-run', reason: 'manifest unavailable', details: [], evidence: [], method: 'M', advisory: false },
|
|
223
|
+
{ id: 'DSC-12', clause: 'C-12', layer: 'L0', result: 'ERROR', coverage: 'not-run', reason: 'manifest unavailable', details: [], evidence: [], method: 'M', advisory: false },
|
|
224
|
+
{ id: 'DSC-13', clause: 'C-13', layer: 'L0', result: 'ERROR', coverage: 'not-run', reason: 'manifest unavailable', details: [], evidence: [], method: 'M', advisory: false },
|
|
225
|
+
]
|
|
226
|
+
const advisoryChecks = [checkImportTimeAdvisory(sources)]
|
|
227
|
+
|
|
228
|
+
if (options.scope === 'entry' && options.entryRuntime === 'server-entry' && kind !== 'plugin') options.treatAsPlugin = true
|
|
229
|
+
let runtimeKind = kind
|
|
230
|
+
let runtimeEntry = entry
|
|
231
|
+
if (options.treatAsPlugin && manifest) {
|
|
232
|
+
runtimeKind = 'plugin'
|
|
233
|
+
if (!runtimeEntry?.exists) {
|
|
234
|
+
const candidates = ['lib/index.js', 'index.js', 'lib/public-entry.js', 'src/index.ts', 'src/index.js', 'src/index.mjs']
|
|
235
|
+
for (const relative of candidates) {
|
|
236
|
+
const absolute = path.resolve(dir, relative)
|
|
237
|
+
if (fs.existsSync(absolute)) { runtimeEntry = { exists: true, absolute, relative, source: '--treat-as-plugin' }; break }
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
let runtimeChecks
|
|
243
|
+
if (options.noExec || !manifest) {
|
|
244
|
+
const reason = options.noExec ? '--no-exec' : 'manifest unavailable'
|
|
245
|
+
runtimeChecks = ['DSC-04', 'DSC-05', 'DSC-06', 'DSC-07', 'DSC-08', 'DSC-09'].map(id => ({
|
|
246
|
+
id, clause: { 'DSC-04': 'C-4', 'DSC-05': 'C-5', 'DSC-06': 'C-6', 'DSC-07': 'C-7', 'DSC-08': 'C-8', 'DSC-09': 'C-9' }[id],
|
|
247
|
+
layer: { 'DSC-04': 'L1', 'DSC-05': 'L1', 'DSC-06': 'L1+L2', 'DSC-07': 'L2', 'DSC-08': 'L3', 'DSC-09': 'L1' }[id],
|
|
248
|
+
result: 'ERROR', coverage: 'not-run', reason, details: [], evidence: [], method: 'M', advisory: false,
|
|
249
|
+
}))
|
|
250
|
+
} else {
|
|
251
|
+
runtimeChecks = await runRuntimeChecks({
|
|
252
|
+
dir, manifest, kind: runtimeKind, entry: runtimeEntry,
|
|
253
|
+
runtimePath: options.runtime, timeoutMs: options.timeoutMs, sources,
|
|
254
|
+
})
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const runCheck = await entryRunCheck(kind, runtimeChecks, options, dir)
|
|
258
|
+
const checks = [
|
|
259
|
+
manifestChecks[0], manifestChecks[1], manifestChecks[2],
|
|
260
|
+
...runtimeChecks,
|
|
261
|
+
runCheck,
|
|
262
|
+
advisoryChecks[0],
|
|
263
|
+
...declarationChecks,
|
|
264
|
+
]
|
|
265
|
+
|
|
266
|
+
const subject = {
|
|
267
|
+
mode: options.profile === 'listing' || options.profile === 'both' ? 'listing' : 'package',
|
|
268
|
+
profile: options.profile,
|
|
269
|
+
kind,
|
|
270
|
+
kindInferred,
|
|
271
|
+
kindEvaluated: runtimeKind,
|
|
272
|
+
name: manifest?.name ?? null,
|
|
273
|
+
version: manifest?.version ?? null,
|
|
274
|
+
path: dir,
|
|
275
|
+
}
|
|
276
|
+
const report = buildReport({
|
|
277
|
+
subject,
|
|
278
|
+
anchor: anchorOf(options, options.runtime),
|
|
279
|
+
checks,
|
|
280
|
+
g4,
|
|
281
|
+
scope: options.scope,
|
|
282
|
+
profile: options.profile,
|
|
283
|
+
kind,
|
|
284
|
+
staticOnly: options.staticOnly,
|
|
285
|
+
entryRuntime: options.entryRuntime,
|
|
286
|
+
entryClaim: options.entryClaim,
|
|
287
|
+
})
|
|
288
|
+
const output = writeReport(report, options.format)
|
|
289
|
+
if (options.out) {
|
|
290
|
+
const outPath = path.resolve(options.out)
|
|
291
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true })
|
|
292
|
+
fs.writeFileSync(outPath, output)
|
|
293
|
+
process.stderr.write(`dsh-plugin-verify: wrote ${outPath}\n`)
|
|
294
|
+
} else {
|
|
295
|
+
process.stdout.write(output)
|
|
296
|
+
}
|
|
297
|
+
return report.conclusion.exitCode
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export { parseArgs, usage, VERSION }
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static G4 check: when `dsh.client` exists, the package must expose
|
|
3
|
+
* `exports["./client"]` and declare the client at the package root.
|
|
4
|
+
*
|
|
5
|
+
* This is a static shape check. It does not prove the client module loads or
|
|
6
|
+
* renders; that is a runtime/UI probe and is reported separately.
|
|
7
|
+
*/
|
|
8
|
+
export function checkClientShape(manifest) {
|
|
9
|
+
const hasClient = manifest?.dsh?.client !== undefined
|
|
10
|
+
if (!hasClient) {
|
|
11
|
+
return {
|
|
12
|
+
hasClient: false,
|
|
13
|
+
ok: true,
|
|
14
|
+
result: 'NA',
|
|
15
|
+
reason: 'no dsh.client declaration',
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
const client = manifest.dsh.client
|
|
19
|
+
if (client === null || client === undefined || client === '') {
|
|
20
|
+
return { hasClient: true, ok: false, result: 'FAIL', reason: 'dsh.client is empty' }
|
|
21
|
+
}
|
|
22
|
+
const exported = manifest.exports?.['./client']
|
|
23
|
+
if (!exported) {
|
|
24
|
+
return {
|
|
25
|
+
hasClient: true,
|
|
26
|
+
ok: false,
|
|
27
|
+
result: 'FAIL',
|
|
28
|
+
reason: 'dsh.client exists but exports["./client"] is missing; a package-root row cannot resolve it',
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
hasClient: true,
|
|
33
|
+
ok: true,
|
|
34
|
+
result: 'PASS',
|
|
35
|
+
reason: 'dsh.client + exports["./client"] present (static only)',
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { isObject, regexHits } from './util.mjs'
|
|
2
|
+
import { check, detail, worst } from './result.mjs'
|
|
3
|
+
import { validateCommitments, validateDisclosure } from './permissions.mjs'
|
|
4
|
+
|
|
5
|
+
export const CAPABILITIES = new Set([
|
|
6
|
+
'fs:read', 'fs:write', 'shell', 'subprocess', 'network:egress', 'network:ingress',
|
|
7
|
+
'credentials:read', 'credentials:list', 'credentials:provider', 'llm:adapter',
|
|
8
|
+
'sessions:read', 'system-prompt:write', 'tools:register', 'remote:expose', 'plugin:install',
|
|
9
|
+
])
|
|
10
|
+
|
|
11
|
+
export const LABELS = new Set([
|
|
12
|
+
'prompt-modification', 'red-team', 'third-party-model-provider', 'remote-access', 'mirror',
|
|
13
|
+
])
|
|
14
|
+
|
|
15
|
+
export const FORBIDDEN_LABELS = new Set(['official', 'verified', 'certified', 'secure', 'audited'])
|
|
16
|
+
|
|
17
|
+
const TRIGGERS = [
|
|
18
|
+
{ cap: 'network:ingress', pattern: /ctx\.(?:webServer|webhookRuntime)\b/g },
|
|
19
|
+
{ cap: 'plugin:install', pattern: /\b(?:installBundle|removeBundle)\s*\(/g },
|
|
20
|
+
{ cap: 'llm:adapter', pattern: /registerAdapter\s*\(|ctx\.llm\.register/g },
|
|
21
|
+
{ cap: 'system-prompt:write', pattern: /ctx\.systemPrompt\.(?:section|context|variable)\s*\(/g },
|
|
22
|
+
{ cap: 'tools:register', pattern: /ctx\.tools\.register\s*\(/g },
|
|
23
|
+
{ cap: 'remote:expose', pattern: /@Remote(?:Scope)?\s*\(/g },
|
|
24
|
+
{ cap: 'credentials:list', pattern: /\.listRecords\s*\(/g },
|
|
25
|
+
{ cap: 'sessions:read', pattern: /ctx\.(?:sessions|sessionQuery)\b/g },
|
|
26
|
+
{ cap: 'shell', pattern: /ctx\.(?:shell|bash)\b/g },
|
|
27
|
+
{ cap: 'subprocess', pattern: /ctx\.subprocess\b/g },
|
|
28
|
+
{ cap: 'fs:read', pattern: /ctx\.fs\b/g },
|
|
29
|
+
{ cap: 'network:egress', pattern: /ctx\.web\b|\bfetch\s*\(/g },
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
function capabilityList(manifest) {
|
|
33
|
+
const value = manifest?.dsh?.capabilities
|
|
34
|
+
if (value === undefined) return { defined: false, list: [], error: null }
|
|
35
|
+
if (!Array.isArray(value) || value.some(item => typeof item !== 'string')) {
|
|
36
|
+
return { defined: true, list: [], error: 'dsh.capabilities must be a string array' }
|
|
37
|
+
}
|
|
38
|
+
return { defined: true, list: value, error: null }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function labelList(manifest) {
|
|
42
|
+
const value = manifest?.dsh?.labels
|
|
43
|
+
if (value === undefined) return { defined: false, list: [], error: null }
|
|
44
|
+
if (!Array.isArray(value) || value.some(item => typeof item !== 'string')) {
|
|
45
|
+
return { defined: true, list: [], error: 'dsh.labels must be a string array' }
|
|
46
|
+
}
|
|
47
|
+
return { defined: true, list: value, error: null }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function hitEvidence(hits, limit = 3) {
|
|
51
|
+
return hits.slice(0, limit).map(hit => `${hit.file}:${hit.line}`)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** DSC-10: advisory only; never FAIL. */
|
|
55
|
+
export function checkImportTimeAdvisory(sources) {
|
|
56
|
+
const patterns = [
|
|
57
|
+
{ name: 'top-level await', pattern: /^await\s/m },
|
|
58
|
+
{ name: 'top-level setInterval', pattern: /^setInterval\s*\(/m },
|
|
59
|
+
{ name: 'top-level process.on', pattern: /^process\.on\s*\(/m },
|
|
60
|
+
{ name: 'top-level process.env assignment', pattern: /^process\.env\.[A-Za-z_][A-Za-z0-9_]*\s*=/m },
|
|
61
|
+
{ name: 'top-level globalThis assignment', pattern: /^globalThis\.[A-Za-z_][A-Za-z0-9_]*\s*=/m },
|
|
62
|
+
]
|
|
63
|
+
const hits = []
|
|
64
|
+
for (const item of patterns) {
|
|
65
|
+
for (const hit of regexHits(sources, item.pattern)) hits.push({ ...hit, pattern: item.name })
|
|
66
|
+
}
|
|
67
|
+
const result = hits.length > 0 ? 'WARN' : sources.length === 0 ? 'WARN' : 'PASS'
|
|
68
|
+
return check({
|
|
69
|
+
id: 'DSC-10',
|
|
70
|
+
clause: 'C-10',
|
|
71
|
+
layer: 'L0',
|
|
72
|
+
result,
|
|
73
|
+
advisory: true,
|
|
74
|
+
reason: hits.length > 0 ? `advisory: ${hits.length} import-time side-effect clue(s)` : sources.length === 0 ? 'no source files scanned' : '',
|
|
75
|
+
details: hits.slice(0, 20).map(hit => detail('C-10.1/2', 'WARN', `${hit.pattern} at ${hit.file}:${hit.line}`, [`${hit.file}:${hit.line}`])),
|
|
76
|
+
evidence: hitEvidence(hits),
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** DSC-11: dsh.capabilities declaration + high-signal static triggers. */
|
|
81
|
+
export function checkCapabilities(manifest, sources, patch) {
|
|
82
|
+
const caps = capabilityList(manifest)
|
|
83
|
+
const details = []
|
|
84
|
+
if (caps.error) details.push(detail('C-11.1', 'FAIL', caps.error, ['package.json#dsh.capabilities']))
|
|
85
|
+
if (!caps.defined) {
|
|
86
|
+
details.push(detail('C-11.1', 'WARN', 'dsh.capabilities is not declared (transitional WARN; host does not read it yet)', ['package.json#dsh.capabilities']))
|
|
87
|
+
} else if (!caps.error) {
|
|
88
|
+
for (const id of caps.list) {
|
|
89
|
+
if (!CAPABILITIES.has(id)) details.push(detail('C-11.1', 'FAIL', `unknown capability id: ${id}`, ['package.json#dsh.capabilities']))
|
|
90
|
+
}
|
|
91
|
+
if (caps.list.length === 0) details.push(detail('C-11.1', 'WARN', 'dsh.capabilities is empty', ['package.json#dsh.capabilities']))
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const triggered = new Map()
|
|
95
|
+
for (const trigger of TRIGGERS) {
|
|
96
|
+
const hits = regexHits(sources, trigger.pattern)
|
|
97
|
+
if (hits.length > 0) triggered.set(trigger.cap, hits)
|
|
98
|
+
}
|
|
99
|
+
if (!caps.defined) {
|
|
100
|
+
for (const [cap, hits] of triggered) {
|
|
101
|
+
details.push(detail('C-11.2', 'WARN', `trigger for ${cap} observed but dsh.capabilities is absent`, hitEvidence(hits)))
|
|
102
|
+
}
|
|
103
|
+
} else if (!caps.error) {
|
|
104
|
+
for (const [cap, hits] of triggered) {
|
|
105
|
+
const satisfied = caps.list.includes(cap) || (cap === 'fs:read' && caps.list.includes('fs:write'))
|
|
106
|
+
if (!satisfied) {
|
|
107
|
+
details.push(detail('C-11.2', 'FAIL', `static trigger ${cap} is not declared in dsh.capabilities`, hitEvidence(hits)))
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
for (const cap of caps.list) {
|
|
111
|
+
const observed = triggered.has(cap) || (cap === 'fs:write' && triggered.has('fs:read'))
|
|
112
|
+
if (!observed) details.push(detail('C-11.5', 'WARN', `declared capability ${cap} was not observed statically`, ['package.json#dsh.capabilities']))
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (patch?.enabledPluginManagerTools) {
|
|
117
|
+
details.push(detail('C-11.3', 'FAIL', 'bundle patch enables @deepseek-ai/dsh-plugin-manager/tools', ['dsh.bundle.patch']))
|
|
118
|
+
}
|
|
119
|
+
const dangerHits = regexHits(sources, /danger-full-access/g)
|
|
120
|
+
if (dangerHits.length > 0) details.push(detail('C-11.4', 'WARN', 'danger-full-access literal found', hitEvidence(dangerHits)))
|
|
121
|
+
if (details.length === 0) details.push(detail('C-11.1', 'PASS', 'dsh.capabilities is declared and consistent with static triggers'))
|
|
122
|
+
|
|
123
|
+
const result = worst(details.map(item => item.result))
|
|
124
|
+
return check({
|
|
125
|
+
id: 'DSC-11', clause: 'C-11', layer: 'L0', result,
|
|
126
|
+
reason: details.filter(item => item.result !== 'PASS').map(item => `${item.item}: ${item.reason}`).join(' | '),
|
|
127
|
+
details,
|
|
128
|
+
evidence: ['package.json#dsh.capabilities', 'dsh.bundle.patch'],
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** DSC-12: direct credential backing-file access / listRecords. */
|
|
133
|
+
export function checkCredentials(manifest, sources) {
|
|
134
|
+
const caps = capabilityList(manifest)
|
|
135
|
+
const details = []
|
|
136
|
+
const backingPatterns = [
|
|
137
|
+
/\.credentials\.yaml/g,
|
|
138
|
+
/\bCREDENTIALS_FILENAME\b/g,
|
|
139
|
+
/@deepseek-ai\/dsh-credentials-local/g,
|
|
140
|
+
/\$DSH_HOME[^\n]{0,80}credentials/gi,
|
|
141
|
+
]
|
|
142
|
+
const backingHits = []
|
|
143
|
+
for (const pattern of backingPatterns) backingHits.push(...regexHits(sources, pattern))
|
|
144
|
+
const provider = caps.defined && caps.list.includes('credentials:provider')
|
|
145
|
+
const hardHits = backingHits.filter(hit => {
|
|
146
|
+
if (/credentials-local/.test(hit.text)) return true
|
|
147
|
+
const source = sources.find(item => item.relative === hit.file)
|
|
148
|
+
if (!source) return false
|
|
149
|
+
const lines = source.text.split('\n')
|
|
150
|
+
const window = lines.slice(Math.max(0, hit.line - 6), hit.line + 6).join('\n')
|
|
151
|
+
return /readFile|readFileSync|createReadStream|openSync|\.load\s*\(|\.parse\s*\(/.test(window)
|
|
152
|
+
})
|
|
153
|
+
if (backingHits.length === 0) {
|
|
154
|
+
details.push(detail('C-12.1', 'PASS', 'no credential backing-file reference found'))
|
|
155
|
+
} else if (provider) {
|
|
156
|
+
details.push(detail('C-12.1', 'PASS', 'credential backing-file reference is declared as credentials:provider', hitEvidence(backingHits)))
|
|
157
|
+
} else if (hardHits.length > 0) {
|
|
158
|
+
details.push(detail('C-12.1', 'FAIL', 'credential backing-file access near a file-read API without credentials:provider', hitEvidence(hardHits)))
|
|
159
|
+
} else {
|
|
160
|
+
details.push(detail('C-12.1', 'PASS', 'credential backing-file path appears in text/UI only; no file-read API found nearby', hitEvidence(backingHits)))
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const listHits = regexHits(sources, /\.listRecords\s*\(/g)
|
|
164
|
+
if (listHits.length > 0) {
|
|
165
|
+
if (!caps.defined) details.push(detail('C-12.2', 'WARN', 'listRecords() used but dsh.capabilities is absent (transitional WARN)', hitEvidence(listHits)))
|
|
166
|
+
else if (!caps.list.includes('credentials:list')) details.push(detail('C-12.2', 'FAIL', 'listRecords() used without credentials:list', hitEvidence(listHits)))
|
|
167
|
+
else details.push(detail('C-12.2', 'PASS', 'listRecords() is declared as credentials:list'))
|
|
168
|
+
} else {
|
|
169
|
+
details.push(detail('C-12.2', 'PASS', 'listRecords() not used'))
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const providerHits = regexHits(sources, /ctx\.provide\(\s*['"](?:shell|credentials|sandboxPolicy|sandbox|deepseekAccount)['"]/g)
|
|
173
|
+
if (providerHits.length > 0) details.push(detail('C-12.3', 'WARN', 'global seam provider registration detected; declaration ownership needs review', hitEvidence(providerHits)))
|
|
174
|
+
const dangerHits = regexHits(sources, /danger-full-access|--add-dir/g)
|
|
175
|
+
if (dangerHits.length > 0) details.push(detail('C-12.4', 'WARN', 'danger-full-access/--add-dir literal found', hitEvidence(dangerHits)))
|
|
176
|
+
details.push(...validateCommitments(manifest), ...validateDisclosure(manifest))
|
|
177
|
+
|
|
178
|
+
const result = worst(details.map(item => item.result))
|
|
179
|
+
return check({
|
|
180
|
+
id: 'DSC-12', clause: 'C-12', layer: 'L0', result,
|
|
181
|
+
reason: details.filter(item => item.result !== 'PASS' && item.result !== 'NA').map(item => `${item.item}: ${item.reason}`).join(' | '),
|
|
182
|
+
details,
|
|
183
|
+
evidence: ['package.json#dsh.capabilities'],
|
|
184
|
+
})
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** DSC-13: high-risk labels + mirror/upstream. */
|
|
188
|
+
export function checkLabels(manifest, sources) {
|
|
189
|
+
const caps = capabilityList(manifest)
|
|
190
|
+
const labels = labelList(manifest)
|
|
191
|
+
const details = []
|
|
192
|
+
if (labels.error) details.push(detail('C-13.1', 'FAIL', labels.error, ['package.json#dsh.labels']))
|
|
193
|
+
if (!labels.defined) {
|
|
194
|
+
details.push(detail('C-13.1', 'WARN', 'dsh.labels is not declared', ['package.json#dsh.labels']))
|
|
195
|
+
} else if (!labels.error) {
|
|
196
|
+
for (const label of labels.list) {
|
|
197
|
+
if (!LABELS.has(label)) details.push(detail('C-13.1', 'FAIL', `unknown label: ${label}`, ['package.json#dsh.labels']))
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
for (const label of labels.list) {
|
|
202
|
+
if (FORBIDDEN_LABELS.has(label)) details.push(detail('C-13.4', 'FAIL', `forbidden endorsement label: ${label}`, ['package.json#dsh.labels']))
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const upstream = manifest?.dsh?.upstream
|
|
206
|
+
if (upstream !== undefined) {
|
|
207
|
+
let url = null
|
|
208
|
+
try { url = new URL(String(upstream)) } catch { /* handled below */ }
|
|
209
|
+
if (!url || url.protocol !== 'https:') {
|
|
210
|
+
details.push(detail('C-13.3', 'FAIL', 'dsh.upstream must be an https URL', ['package.json#dsh.upstream']))
|
|
211
|
+
} else if (isObject(manifest.repository) && typeof manifest.repository.url === 'string' && manifest.repository.url.includes(url.hostname + url.pathname)) {
|
|
212
|
+
details.push(detail('C-13.3', 'FAIL', 'dsh.upstream points at the package itself', ['package.json#dsh.upstream']))
|
|
213
|
+
} else if (!labels.list.includes('mirror')) {
|
|
214
|
+
details.push(detail('C-13.2', 'FAIL', 'dsh.upstream is declared but the mirror label is missing', ['package.json#dsh.labels']))
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (!caps.defined) {
|
|
219
|
+
details.push(detail('C-13.2', 'WARN', 'dsh.capabilities is absent; trigger-to-label mapping cannot be evaluated', ['package.json#dsh.capabilities']))
|
|
220
|
+
} else if (!caps.error) {
|
|
221
|
+
const required = new Set()
|
|
222
|
+
if (caps.list.includes('system-prompt:write')) required.add('prompt-modification')
|
|
223
|
+
if (caps.list.includes('llm:adapter') && caps.list.includes('network:egress')) required.add('third-party-model-provider')
|
|
224
|
+
if (caps.list.includes('network:ingress')) required.add('remote-access')
|
|
225
|
+
for (const label of required) {
|
|
226
|
+
if (!labels.list.includes(label)) details.push(detail('C-13.2', 'FAIL', `capability trigger requires label ${label}`, ['package.json#dsh.labels']))
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (labels.list.includes('red-team') && manifest?.dsh?.usageScope === undefined) {
|
|
231
|
+
details.push(detail('C-13.5', 'WARN', 'red-team label without dsh.usageScope', ['package.json#dsh.labels']))
|
|
232
|
+
}
|
|
233
|
+
if (details.length === 0) details.push(detail('C-13.1', 'PASS', 'dsh.labels is declared and consistent with capability triggers'))
|
|
234
|
+
|
|
235
|
+
const result = worst(details.map(item => item.result))
|
|
236
|
+
return check({
|
|
237
|
+
id: 'DSC-13', clause: 'C-13', layer: 'L0', result,
|
|
238
|
+
reason: details.filter(item => item.result !== 'PASS').map(item => `${item.item}: ${item.reason}`).join(' | '),
|
|
239
|
+
details,
|
|
240
|
+
evidence: ['package.json#dsh.labels', 'package.json#dsh.upstream'],
|
|
241
|
+
})
|
|
242
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Programmatic API for dsh-plugin-verify. */
|
|
2
|
+
export { main, parseArgs, usage, VERSION } from './cli.mjs'
|
|
3
|
+
export { runManifestChecks, checkMarketEntry, DEFAULT_RUNTIME_VERSION, inferKind, resolveEntry } from './manifest.mjs'
|
|
4
|
+
export { checkCapabilities, checkCredentials, checkLabels, checkImportTimeAdvisory, CAPABILITIES, LABELS } from './declarations.mjs'
|
|
5
|
+
export { runRuntimeChecks } from './runtime.mjs'
|
|
6
|
+
export { buildReport, formatText, formatSarif, writeReport } from './report.mjs'
|
|
7
|
+
export { satisfies, validRange, parseVersion, compareVersions } from './semver.mjs'
|
|
8
|
+
export { parseInstallSpec } from './install-spec.mjs'
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal port of plugin-manager/src/install-spec.ts:69 (parseInstallSpec).
|
|
3
|
+
* The checker must not import the host TypeScript source (it is not built in
|
|
4
|
+
* a plugin checkout), so this file reimplements the accepted forms and
|
|
5
|
+
* refusal rules. It is deliberately narrower: it also requires a pinned
|
|
6
|
+
* reference for git specs, which DS-C C-3 requires.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const PACKAGE_NAME = /^(?:@[a-z0-9][a-z0-9._~-]*\/)?[a-z0-9][a-z0-9._~-]*$/
|
|
10
|
+
const PACKAGE_NAME_MAX_LENGTH = 214
|
|
11
|
+
const GIT_SHORTHAND = /^(?:github|gitlab|bitbucket|gist):/i
|
|
12
|
+
const GIT_URL = /^git(?:\+[a-z]+)?:\/\/|^git@[^:]+:/i
|
|
13
|
+
const HOSTED_REPOSITORY_URL = /^https?:\/\/[^/]+\/[^/]+\/[^/#]+(?:\.git)?(?:#.*)?$/i
|
|
14
|
+
const TARBALL_SPEC = /\.(?:tgz|tar\.gz)(?:#.*)?$/i
|
|
15
|
+
const COMMIT = /^[0-9a-f]{40}$/
|
|
16
|
+
|
|
17
|
+
export class InvalidInstallSpec extends Error {
|
|
18
|
+
constructor(spec, reason) {
|
|
19
|
+
super(`invalid install spec: ${reason}: ${spec}`)
|
|
20
|
+
this.name = 'InvalidInstallSpec'
|
|
21
|
+
this.spec = spec
|
|
22
|
+
this.reason = reason
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function fail(spec, reason) {
|
|
27
|
+
throw new InvalidInstallSpec(spec, reason)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @param {string} raw
|
|
32
|
+
* @returns {{kind:'registry'|'path'|'tarball'|'git', spec:string, name?:string, range?:string, path?:string, host?:string, commit?:string, pinned:boolean}}
|
|
33
|
+
*/
|
|
34
|
+
export function parseInstallSpec(raw) {
|
|
35
|
+
if (typeof raw !== 'string') fail(String(raw), 'the spec must be a string')
|
|
36
|
+
const spec = raw.trim()
|
|
37
|
+
if (spec === '') fail(spec, 'the package spec must not be empty')
|
|
38
|
+
|
|
39
|
+
const path = spec.replace(/^(?:file|link):/, '')
|
|
40
|
+
if (path !== spec || /^(?:\/|[A-Za-z]:[\\/])/.test(spec)) {
|
|
41
|
+
if (!/^(?:\/|[A-Za-z]:[\\/])/.test(path)) fail(spec, 'a local path must be absolute')
|
|
42
|
+
if (TARBALL_SPEC.test(path)) return { kind: 'tarball', spec, path, pinned: true }
|
|
43
|
+
return { kind: 'path', spec, path, pinned: true }
|
|
44
|
+
}
|
|
45
|
+
if (/^\.{1,2}(?:[\\/]|$)/.test(spec)) fail(spec, 'a local path must be absolute')
|
|
46
|
+
|
|
47
|
+
const git = GIT_SHORTHAND.test(spec) || GIT_URL.test(spec) || HOSTED_REPOSITORY_URL.test(spec)
|
|
48
|
+
if (git && !TARBALL_SPEC.test(spec)) {
|
|
49
|
+
if (!/#[0-9a-f]{40}$/.test(spec)) fail(spec, 'a git spec must pin a 40-hex-digit commit (no HEAD/branch/tag)')
|
|
50
|
+
return { kind: 'git', spec, host: 'git', commit: spec.slice(-40), pinned: true }
|
|
51
|
+
}
|
|
52
|
+
if (/^https?:\/\//i.test(spec)) {
|
|
53
|
+
if (TARBALL_SPEC.test(spec)) return { kind: 'tarball', spec, host: new URL(spec).host, pinned: true }
|
|
54
|
+
fail(spec, 'a URL must point at a git repository or a tarball')
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const at = spec.indexOf('@', 1)
|
|
58
|
+
const name = at === -1 ? spec : spec.slice(0, at)
|
|
59
|
+
const range = at === -1 ? undefined : spec.slice(at + 1)
|
|
60
|
+
if (name.length > PACKAGE_NAME_MAX_LENGTH || !PACKAGE_NAME.test(name)) {
|
|
61
|
+
fail(spec, 'not a package name the registry accepts')
|
|
62
|
+
}
|
|
63
|
+
if (range === '') fail(spec, 'a version after @ must not be empty')
|
|
64
|
+
return range === undefined
|
|
65
|
+
? { kind: 'registry', spec, name, pinned: false }
|
|
66
|
+
: { kind: 'registry', spec, name, range, pinned: !/^(?:latest|next|beta|canary|\*|x)$/i.test(range) }
|
|
67
|
+
}
|