dsh-standards 1.2.1 → 1.3.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/40-checker/README.md +25 -3
- package/40-checker/bin/dsh-levels.mjs +7 -1
- package/40-checker/scripts/build-offline-kit.mjs +284 -0
- package/40-checker/scripts/build-sidecar.mjs +484 -0
- package/40-checker/scripts/flow-collect.mjs +147 -22
- package/40-checker/scripts/level-audit.mjs +57 -9
- package/40-checker/scripts/run-tests.mjs +17 -0
- package/40-checker/src/author-cli.mjs +139 -0
- package/40-checker/src/author-report.mjs +260 -0
- package/40-checker/src/cli.mjs +73 -8
- package/40-checker/src/flow-evidence.mjs +189 -3
- package/40-checker/src/index.mjs +3 -3
- package/40-checker/src/report.mjs +125 -24
- package/40-checker/src/sandbox-run.mjs +403 -0
- package/CHANGELOG.md +26 -0
- package/README.en.md +352 -0
- package/README.es.md +352 -0
- package/README.hi.md +352 -0
- package/README.md +300 -82
- package/README.pt.md +352 -0
- package/bin/dsh-standards.mjs +13 -2
- package/docs/assets/dsh-standards-flow-dark.svg +38 -0
- package/docs/assets/dsh-standards-flow-light.svg +38 -0
- package/docs/assets/dsh-standards-flow-v3-dark.svg +87 -0
- package/docs/assets/dsh-standards-flow-v3-light.svg +87 -0
- package/docs/assets/dsh-standards-flow-v4-dark.svg +79 -0
- package/docs/assets/dsh-standards-flow-v4-light.svg +79 -0
- package/docs/assets/dsh-standards-hero-en-dark.webp +0 -0
- package/docs/assets/dsh-standards-hero-en-light.webp +0 -0
- package/docs/assets/dsh-standards-hero-zh-dark.webp +0 -0
- package/docs/assets/dsh-standards-hero-zh-light.webp +0 -0
- package/package.json +2 -2
- package/spec/README.md +2 -0
- package/spec/flow/README.md +19 -6
- package/spec/flow-evidence.schema.json +76 -6
- package/spec/schemas/advice.schema.json +92 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Author-facing renderers for a `dsh-conformance/1` report.
|
|
3
|
+
*
|
|
4
|
+
* Markdown is the default `dsh-standards check` output; HTML is a single
|
|
5
|
+
* self-contained file with inline CSS, no scripts, no external assets, and
|
|
6
|
+
* every dynamic value escaped. Both read the same report object the checker
|
|
7
|
+
* already produced, so the verdict and exit code never change here.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const DISCLAIMER =
|
|
11
|
+
'Non-official report. This is not a certification, security audit, endorsement, or ranking. '
|
|
12
|
+
+ 'Hygiene warnings are not a clean pass; items under "Not checked" were not inspected.'
|
|
13
|
+
|
|
14
|
+
function text(value) {
|
|
15
|
+
return String(value ?? '').replace(/\s+/g, ' ').trim()
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function mdCell(value) {
|
|
19
|
+
return text(value).replace(/\|/g, '\\|')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function escapeHtml(value) {
|
|
23
|
+
return String(value ?? '').replace(/[&<>"']/g, character => ({
|
|
24
|
+
'&': '&',
|
|
25
|
+
'<': '<',
|
|
26
|
+
'>': '>',
|
|
27
|
+
'"': '"',
|
|
28
|
+
"'": ''',
|
|
29
|
+
}[character]))
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function headlineOf(report) {
|
|
33
|
+
return text(report?.conclusion?.headline) || text(report?.conclusion?.reason) || 'no conclusion'
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function priorityFixes(report, limit = 3) {
|
|
37
|
+
const advice = Array.isArray(report?.advice) ? report.advice : []
|
|
38
|
+
const seen = new Set()
|
|
39
|
+
const fixes = []
|
|
40
|
+
for (const item of advice) {
|
|
41
|
+
const key = item?.key ?? item?.title
|
|
42
|
+
if (key === undefined || seen.has(key)) continue
|
|
43
|
+
seen.add(key)
|
|
44
|
+
fixes.push(item)
|
|
45
|
+
if (fixes.length >= limit) break
|
|
46
|
+
}
|
|
47
|
+
if (fixes.length > 0) return fixes
|
|
48
|
+
// Advice may be disabled; fall back to the FAIL/WARN rows themselves.
|
|
49
|
+
const rows = [...(report?.buckets?.blocking ?? []), ...(report?.buckets?.hygiene ?? [])]
|
|
50
|
+
for (const row of rows) {
|
|
51
|
+
if (fixes.length >= limit) break
|
|
52
|
+
fixes.push({ key: row.id, gate: row.result, severity: row.result, source: row.id, fix: row.reason, why: null, docsUrl: null })
|
|
53
|
+
}
|
|
54
|
+
return fixes
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function passedIds(report) {
|
|
58
|
+
return (report?.checks ?? []).filter(item => item.result === 'PASS').map(item => item.id)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function adviceRows(report) {
|
|
62
|
+
return Array.isArray(report?.advice) ? report.advice : []
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const BUCKETS = [
|
|
66
|
+
{ key: 'blocking', title: 'Blocking (FAIL)' },
|
|
67
|
+
{ key: 'hygiene', title: 'Hygiene (WARN)' },
|
|
68
|
+
{ key: 'notChecked', title: 'Not checked (NA / not-run / ERROR)' },
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
function bucketRows(report, key) {
|
|
72
|
+
return Array.isArray(report?.buckets?.[key]) ? report.buckets[key] : []
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function renderMarkdown(report) {
|
|
76
|
+
const subject = report?.subject ?? {}
|
|
77
|
+
const conclusion = report?.conclusion ?? {}
|
|
78
|
+
const coverage = report?.coverage ?? {}
|
|
79
|
+
const metrics = report?.metrics ?? {}
|
|
80
|
+
const anchor = report?.anchor ?? {}
|
|
81
|
+
const lines = []
|
|
82
|
+
lines.push(`# DSH standards check — ${text(subject.name) || '<unknown>'}@${text(subject.version) || '?'}`)
|
|
83
|
+
lines.push('')
|
|
84
|
+
lines.push(`**Conclusion:** ${headlineOf(report)}`)
|
|
85
|
+
lines.push('')
|
|
86
|
+
lines.push(`- Status: **${text(conclusion.status) || 'unknown'}** (exit ${conclusion.exitCode ?? '?'})`)
|
|
87
|
+
if (conclusion.level) lines.push(`- Level: ${text(conclusion.level)}`)
|
|
88
|
+
lines.push(`- Anchor: dsh=${text(anchor.dsh) || '?'}${anchor.runtimeSource ? ` (source=${anchor.runtimeSource}${anchor.suspect ? ', suspect=true' : ''})` : ''}`)
|
|
89
|
+
lines.push(`- Coverage: ${metrics.checked ?? coverage.checked ?? 0}/${metrics.total ?? coverage.total ?? (report?.checks ?? []).length} checks evaluated · declared fields ${metrics.declaredFields ?? 0}/5`)
|
|
90
|
+
lines.push('')
|
|
91
|
+
|
|
92
|
+
lines.push('## Priority fixes (up to 3)')
|
|
93
|
+
lines.push('')
|
|
94
|
+
const fixes = priorityFixes(report)
|
|
95
|
+
if (fixes.length === 0) {
|
|
96
|
+
lines.push('_None: no FAIL or WARN finding produced advice._')
|
|
97
|
+
} else {
|
|
98
|
+
fixes.forEach((item, index) => {
|
|
99
|
+
lines.push(`${index + 1}. **${text(item.severity ?? item.gate ?? 'WARN')} ${text(item.key) || text(item.title)}** (${text(item.source)}) — ${text(item.fix ?? item.title ?? item.reason)}`)
|
|
100
|
+
if (item.why) lines.push(` - Why: ${text(item.why)}`)
|
|
101
|
+
if (item.docsUrl) lines.push(` - Docs: ${text(item.docsUrl)}`)
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
lines.push('')
|
|
105
|
+
|
|
106
|
+
for (const bucket of BUCKETS) {
|
|
107
|
+
const rows = bucketRows(report, bucket.key)
|
|
108
|
+
lines.push(`## ${bucket.title} — ${rows.length}`)
|
|
109
|
+
lines.push('')
|
|
110
|
+
if (rows.length === 0) {
|
|
111
|
+
lines.push('_None._')
|
|
112
|
+
} else {
|
|
113
|
+
lines.push('| id | clause | result | coverage | reason |')
|
|
114
|
+
lines.push('|---|---|---|---|---|')
|
|
115
|
+
for (const row of rows) {
|
|
116
|
+
const tags = [row.declaration ? 'declaration' : null, row.advisory ? 'advisory' : null].filter(Boolean)
|
|
117
|
+
const reason = tags.length > 0 ? `${text(row.reason)} [${tags.join(',')}]` : text(row.reason)
|
|
118
|
+
lines.push(`| ${mdCell(row.id)} | ${mdCell(row.clause ?? '-')} | ${mdCell(row.result)} | ${mdCell(row.coverage ?? '-')} | ${mdCell(reason)} |`)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
lines.push('')
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const passed = passedIds(report)
|
|
125
|
+
lines.push(`## Passed — ${passed.length}`)
|
|
126
|
+
lines.push('')
|
|
127
|
+
lines.push(passed.length > 0 ? passed.join(', ') : '_None._')
|
|
128
|
+
lines.push('')
|
|
129
|
+
|
|
130
|
+
const advice = adviceRows(report)
|
|
131
|
+
lines.push(`## Advice (non-blocking) — ${advice.length}`)
|
|
132
|
+
lines.push('')
|
|
133
|
+
if (advice.length === 0) {
|
|
134
|
+
lines.push('_None._')
|
|
135
|
+
} else {
|
|
136
|
+
for (const item of advice) {
|
|
137
|
+
const missing = item.ruleMissing ? ' [ruleMissing]' : ''
|
|
138
|
+
lines.push(`- ${text(item.severity)} ${text(item.key)} (${text(item.source)}${missing}): ${text(item.fix ?? item.title ?? '')}`)
|
|
139
|
+
if (item.docsUrl) lines.push(` ${text(item.docsUrl)}`)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
lines.push('')
|
|
143
|
+
|
|
144
|
+
const notChecked = bucketRows(report, 'notChecked')
|
|
145
|
+
lines.push('## What this report does not check')
|
|
146
|
+
lines.push('')
|
|
147
|
+
if (notChecked.length === 0) {
|
|
148
|
+
lines.push('- No check was skipped in this run.')
|
|
149
|
+
} else {
|
|
150
|
+
for (const row of notChecked) {
|
|
151
|
+
lines.push(`- ${text(row.id)}${row.clause ? ` (${text(row.clause)})` : ''}: ${text(row.reason) || 'not checked'}`)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
lines.push(`- Runtime execution beyond the checks listed above is not observed by this report${anchor.suspect ? '; the anchor differs from the host and is flagged suspect' : ''}.`)
|
|
155
|
+
lines.push('- It does not certify, rank, audit security, or endorse the package.')
|
|
156
|
+
lines.push('')
|
|
157
|
+
lines.push('---')
|
|
158
|
+
lines.push('')
|
|
159
|
+
lines.push(DISCLAIMER)
|
|
160
|
+
lines.push('')
|
|
161
|
+
return `${lines.join('\n')}\n`
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function renderHtml(report) {
|
|
165
|
+
const subject = report?.subject ?? {}
|
|
166
|
+
const conclusion = report?.conclusion ?? {}
|
|
167
|
+
const metrics = report?.metrics ?? {}
|
|
168
|
+
const anchor = report?.anchor ?? {}
|
|
169
|
+
const title = `DSH standards check — ${text(subject.name) || '<unknown>'}@${text(subject.version) || '?'}`
|
|
170
|
+
const fixes = priorityFixes(report)
|
|
171
|
+
const advice = adviceRows(report)
|
|
172
|
+
const passed = passedIds(report)
|
|
173
|
+
const notChecked = bucketRows(report, 'notChecked')
|
|
174
|
+
|
|
175
|
+
const table = (key) => {
|
|
176
|
+
const rows = bucketRows(report, key)
|
|
177
|
+
if (rows.length === 0) return '<p class="none">None.</p>'
|
|
178
|
+
const body = rows.map(row => {
|
|
179
|
+
const tags = [row.declaration ? 'declaration' : null, row.advisory ? 'advisory' : null].filter(Boolean)
|
|
180
|
+
const reason = tags.length > 0 ? `${text(row.reason)} [${tags.join(',')}]` : text(row.reason)
|
|
181
|
+
return `<tr><td>${escapeHtml(row.id)}</td><td>${escapeHtml(row.clause ?? '-')}</td><td class="result ${escapeHtml(String(row.result).toLowerCase())}">${escapeHtml(row.result)}</td><td>${escapeHtml(row.coverage ?? '-')}</td><td>${escapeHtml(reason)}</td></tr>`
|
|
182
|
+
}).join('\n')
|
|
183
|
+
return `<table><thead><tr><th>id</th><th>clause</th><th>result</th><th>coverage</th><th>reason</th></tr></thead><tbody>\n${body}\n</tbody></table>`
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const fixList = fixes.length === 0
|
|
187
|
+
? '<p class="none">None: no FAIL or WARN finding produced advice.</p>'
|
|
188
|
+
: `<ol>${fixes.map(item => {
|
|
189
|
+
const why = item.why ? `<br><span class="muted">Why: ${escapeHtml(text(item.why))}</span>` : ''
|
|
190
|
+
const docs = item.docsUrl ? `<br><span class="muted">Docs: ${link(item.docsUrl)}</span>` : ''
|
|
191
|
+
return `<li><strong>${escapeHtml(text(item.severity ?? item.gate ?? 'WARN'))} ${escapeHtml(text(item.key) || text(item.title))}</strong> (${escapeHtml(text(item.source))}) — ${escapeHtml(text(item.fix ?? item.title ?? item.reason))}${why}${docs}</li>`
|
|
192
|
+
}).join('\n')}</ol>`
|
|
193
|
+
|
|
194
|
+
const adviceList = advice.length === 0
|
|
195
|
+
? '<p class="none">None.</p>'
|
|
196
|
+
: `<ul>${advice.map(item => `<li><strong>${escapeHtml(text(item.severity))} ${escapeHtml(text(item.key))}</strong> (${escapeHtml(text(item.source))}${item.ruleMissing ? ' [ruleMissing]' : ''}): ${escapeHtml(text(item.fix ?? item.title ?? ''))}${item.docsUrl ? ` ${link(item.docsUrl)}` : ''}</li>`).join('\n')}</ul>`
|
|
197
|
+
|
|
198
|
+
const notCheckedList = notChecked.length === 0
|
|
199
|
+
? '<li>No check was skipped in this run.</li>'
|
|
200
|
+
: notChecked.map(row => `<li><strong>${escapeHtml(text(row.id))}</strong>${row.clause ? ` (${escapeHtml(text(row.clause))})` : ''}: ${escapeHtml(text(row.reason) || 'not checked')}</li>`).join('\n')
|
|
201
|
+
|
|
202
|
+
return `<!doctype html>
|
|
203
|
+
<html lang="en">
|
|
204
|
+
<head>
|
|
205
|
+
<meta charset="utf-8">
|
|
206
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
207
|
+
<title>${escapeHtml(title)}</title>
|
|
208
|
+
<style>
|
|
209
|
+
:root{color-scheme:light dark}
|
|
210
|
+
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"PingFang SC","Microsoft YaHei",sans-serif;max-width:980px;margin:2rem auto;padding:0 1rem;line-height:1.55;color:#1f2328;background:#fff}
|
|
211
|
+
h1{font-size:1.45rem;margin:0 0 .35rem}
|
|
212
|
+
h2{font-size:1.1rem;margin:1.8rem 0 .5rem;border-bottom:1px solid #d0d7de;padding-bottom:.25rem}
|
|
213
|
+
.headline{font-size:1.05rem;font-weight:600;background:#f6f8fa;border:1px solid #d0d7de;border-radius:6px;padding:.65rem .8rem}
|
|
214
|
+
.meta{margin:.6rem 0 0;padding-left:1.1rem;color:#57606a}
|
|
215
|
+
.muted{color:#57606a;font-size:.9rem}
|
|
216
|
+
table{border-collapse:collapse;width:100%;margin:.4rem 0 1rem;font-size:.9rem}
|
|
217
|
+
th,td{border:1px solid #d0d7de;padding:.3rem .5rem;text-align:left;vertical-align:top}
|
|
218
|
+
th{background:#f6f8fa}
|
|
219
|
+
td.result{font-weight:600;white-space:nowrap}
|
|
220
|
+
td.result.fail{color:#b42318}
|
|
221
|
+
td.result.warn{color:#9a6700}
|
|
222
|
+
td.result.na,td.result.error,td.result.not-run{color:#57606a}
|
|
223
|
+
.none{color:#57606a}
|
|
224
|
+
footer{margin-top:2rem;padding-top:.8rem;border-top:1px solid #d0d7de;color:#57606a;font-size:.9rem}
|
|
225
|
+
code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
|
|
226
|
+
@media (prefers-color-scheme:dark){body{background:#0d1117;color:#e6edf3}h2,.headline,th,td,footer{border-color:#30363d}.headline,th{background:#161b22}.meta,.muted,.none,footer{color:#8b949e}td.result.fail{color:#ff7b72}td.result.warn{color:#d29922}}
|
|
227
|
+
</style>
|
|
228
|
+
</head>
|
|
229
|
+
<body>
|
|
230
|
+
<h1>${escapeHtml(title)}</h1>
|
|
231
|
+
<p class="headline">${escapeHtml(headlineOf(report))}</p>
|
|
232
|
+
<ul class="meta">
|
|
233
|
+
<li>Status: <strong>${escapeHtml(text(conclusion.status) || 'unknown')}</strong> (exit ${escapeHtml(conclusion.exitCode ?? '?')})${conclusion.level ? ` · Level: ${escapeHtml(text(conclusion.level))}` : ''}</li>
|
|
234
|
+
<li>Anchor: dsh=${escapeHtml(text(anchor.dsh) || '?')}${anchor.runtimeSource ? ` (source=${escapeHtml(anchor.runtimeSource)}${anchor.suspect ? ', suspect=true' : ''})` : ''}</li>
|
|
235
|
+
<li>Coverage: ${escapeHtml(metrics.checked ?? 0)}/${escapeHtml(metrics.total ?? (report?.checks ?? []).length)} checks evaluated · declared fields ${escapeHtml(metrics.declaredFields ?? 0)}/5</li>
|
|
236
|
+
</ul>
|
|
237
|
+
<h2>Priority fixes (up to 3)</h2>
|
|
238
|
+
${fixList}
|
|
239
|
+
${BUCKETS.map(bucket => `<h2>${escapeHtml(bucket.title)} — ${bucketRows(report, bucket.key).length}</h2>\n${table(bucket.key)}`).join('\n')}
|
|
240
|
+
<h2>Passed — ${passed.length}</h2>
|
|
241
|
+
<p>${passed.length > 0 ? passed.map(escapeHtml).join(', ') : '<span class="none">None.</span>'}</p>
|
|
242
|
+
<h2>Advice (non-blocking) — ${advice.length}</h2>
|
|
243
|
+
${adviceList}
|
|
244
|
+
<h2>What this report does not check</h2>
|
|
245
|
+
<ul>
|
|
246
|
+
${notCheckedList}
|
|
247
|
+
<li>Runtime execution beyond the checks listed above is not observed by this report${anchor.suspect ? '; the anchor differs from the host and is flagged suspect' : ''}.</li>
|
|
248
|
+
<li>It does not certify, rank, audit security, or endorse the package.</li>
|
|
249
|
+
</ul>
|
|
250
|
+
<footer>${escapeHtml(DISCLAIMER)}</footer>
|
|
251
|
+
</body>
|
|
252
|
+
</html>
|
|
253
|
+
`
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function link(url) {
|
|
257
|
+
const value = text(url)
|
|
258
|
+
if (!/^https:\/\//i.test(value)) return escapeHtml(value)
|
|
259
|
+
return `<a href="${escapeHtml(value)}" rel="noreferrer noopener">${escapeHtml(value)}</a>`
|
|
260
|
+
}
|
package/40-checker/src/cli.mjs
CHANGED
|
@@ -12,19 +12,28 @@ import { buildReport, writeReport } from './report.mjs'
|
|
|
12
12
|
const VERSION = '0.1.0'
|
|
13
13
|
const DEFAULT_COMMIT = 'ce7073d51254c26cd63b22a60835b8f948deb447'
|
|
14
14
|
const DEFAULT_CORDIS = '4.0.6-alpha.1'
|
|
15
|
+
const HOST_RUNTIME_TIMEOUT_MS = 5000
|
|
16
|
+
|
|
17
|
+
function binName() {
|
|
18
|
+
const brand = process.env.DSH_CHECKER_BRAND
|
|
19
|
+
if (typeof brand === 'string' && brand.trim() !== '') return brand.trim().split(/\s+/)[0]
|
|
20
|
+
return 'dsh-plugin-verify'
|
|
21
|
+
}
|
|
15
22
|
|
|
16
23
|
function usage() {
|
|
24
|
+
const bin = binName()
|
|
17
25
|
return `${process.env.DSH_CHECKER_BRAND || 'dsh-plugin-verify ' + VERSION} — DS-C conformance checker (MVP)
|
|
18
26
|
|
|
19
27
|
Usage:
|
|
20
|
-
|
|
28
|
+
${bin} check [dir] [--json] [--html <file>] author-facing report (Markdown by default)
|
|
29
|
+
${bin} <package-dir> [options] raw conformance report
|
|
21
30
|
|
|
22
31
|
Options:
|
|
23
32
|
--format text|json|sarif output format (default: text)
|
|
24
33
|
--scope mvp|ds-c|entry mvp = 8 MVP ids; ds-c = full claim (default); entry = compatibility gate
|
|
25
34
|
--profile package|listing|both evaluation profile (default: package; listing if --market-index is set)
|
|
26
35
|
--runtime <path> path to a built @deepseek-ai/cordis lib/index.js; enables L1-L3
|
|
27
|
-
--runtime-version <v> dsh runtime version used for C-2 (default: ${DEFAULT_RUNTIME_VERSION})
|
|
36
|
+
--runtime-version <v> dsh runtime version used for C-2 (default: host via \`dsh --version\`, else ${DEFAULT_RUNTIME_VERSION})
|
|
28
37
|
--cordis-version <v> cordis version recorded in the anchor (default: ${DEFAULT_CORDIS})
|
|
29
38
|
--commit <sha> harness commit recorded in the anchor (default: ${DEFAULT_COMMIT.slice(0, 12)})
|
|
30
39
|
--install-spec <spec> recommended install spec for C-3 (listing/install check)
|
|
@@ -61,7 +70,7 @@ function parseArgs(argv) {
|
|
|
61
70
|
scope: 'ds-c',
|
|
62
71
|
profile: null,
|
|
63
72
|
runtime: null,
|
|
64
|
-
runtimeVersion:
|
|
73
|
+
runtimeVersion: null,
|
|
65
74
|
cordisVersion: DEFAULT_CORDIS,
|
|
66
75
|
commit: DEFAULT_COMMIT,
|
|
67
76
|
installSpec: null,
|
|
@@ -223,14 +232,66 @@ async function entryRunCheck(kind, runtimeChecks, options, dir) {
|
|
|
223
232
|
return { ...base, result: 'ERROR', coverage: 'not-run', reason: c4?.reason || 'no runtime evidence' }
|
|
224
233
|
}
|
|
225
234
|
|
|
226
|
-
|
|
235
|
+
/**
|
|
236
|
+
* Read the runtime version the host actually runs. The anchor used to be a
|
|
237
|
+
* hard-coded `0.2.2-alpha.2`, which manufactured dozens of C-2.2 failures on a
|
|
238
|
+
* host that had already moved on. Detection is best-effort: when `dsh` is not
|
|
239
|
+
* usable the anchor falls back to the checked-in constant and says so.
|
|
240
|
+
*/
|
|
241
|
+
export function detectHostRuntime({ bin = process.env.DSH_BIN || (process.platform === 'win32' ? 'dsh.cmd' : 'dsh'), timeoutMs = HOST_RUNTIME_TIMEOUT_MS } = {}) {
|
|
242
|
+
const run = spawnSync(bin, ['--version'], { encoding: 'utf8', timeout: timeoutMs })
|
|
243
|
+
if (run.error || run.status !== 0) return { version: null, bin, error: run.error?.message ?? `exit ${run.status}` }
|
|
244
|
+
const text = `${run.stdout ?? ''}\n${run.stderr ?? ''}`
|
|
245
|
+
const match = /(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?)/.exec(text)
|
|
246
|
+
if (!match) return { version: null, bin, error: 'no semver in `dsh --version` output' }
|
|
247
|
+
return { version: match[1], bin, error: null }
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Resolve what `anchor.dsh` means:
|
|
252
|
+
* cli the caller passed --runtime-version; suspect when it differs from the host
|
|
253
|
+
* host no flag: the host version is the anchor, so it cannot be suspect
|
|
254
|
+
* fallback dsh unavailable: keep the constant, flagged as not host-derived
|
|
255
|
+
*/
|
|
256
|
+
export function resolveRuntimeAnchor(requestedVersion = null, hostVersion = null) {
|
|
257
|
+
if (typeof requestedVersion === 'string' && requestedVersion.trim() !== '') {
|
|
258
|
+
const requested = requestedVersion.trim()
|
|
259
|
+
return {
|
|
260
|
+
dsh: requested,
|
|
261
|
+
runtimeSource: 'cli',
|
|
262
|
+
hostVersion: hostVersion ?? null,
|
|
263
|
+
suspect: typeof hostVersion === 'string' && hostVersion !== '' && hostVersion !== requested,
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if (typeof hostVersion === 'string' && hostVersion !== '') {
|
|
267
|
+
return { dsh: hostVersion, runtimeSource: 'host', hostVersion, suspect: false }
|
|
268
|
+
}
|
|
269
|
+
return { dsh: DEFAULT_RUNTIME_VERSION, runtimeSource: 'fallback', hostVersion: null, suspect: false }
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** The five optional declaration surfaces, counted but never scored. */
|
|
273
|
+
export function declarationsOf(manifest) {
|
|
274
|
+
const dsh = manifest?.dsh ?? {}
|
|
275
|
+
const present = []
|
|
276
|
+
if (Array.isArray(dsh.capabilities) && dsh.capabilities.length > 0) present.push('capabilities')
|
|
277
|
+
if (Array.isArray(dsh.labels) && dsh.labels.length > 0) present.push('labels')
|
|
278
|
+
if (dsh.commitments && typeof dsh.commitments === 'object' && !Array.isArray(dsh.commitments)) present.push('commitments')
|
|
279
|
+
if (dsh.disclosure && typeof dsh.disclosure === 'object' && !Array.isArray(dsh.disclosure)) present.push('disclosure')
|
|
280
|
+
if (typeof manifest?.engines?.dsh === 'string' && manifest.engines.dsh.trim() !== '') present.push('engines.dsh')
|
|
281
|
+
return { total: 5, present }
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function anchorOf(options, runtimeInfo, runtimePath) {
|
|
227
285
|
return {
|
|
228
286
|
repo: 'deepseek-ai/deepseek-harness',
|
|
229
287
|
commit: options.commit,
|
|
230
288
|
scanner: `dsh-plugin-verify@${VERSION}`,
|
|
231
|
-
dsh:
|
|
289
|
+
dsh: runtimeInfo.dsh,
|
|
232
290
|
cordis: options.cordisVersion,
|
|
233
|
-
runtime:
|
|
291
|
+
runtime: runtimePath ? path.resolve(runtimePath) : null,
|
|
292
|
+
runtimeSource: runtimeInfo.runtimeSource,
|
|
293
|
+
hostVersion: runtimeInfo.hostVersion,
|
|
294
|
+
suspect: runtimeInfo.suspect,
|
|
234
295
|
}
|
|
235
296
|
}
|
|
236
297
|
|
|
@@ -261,8 +322,11 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
261
322
|
return 3
|
|
262
323
|
}
|
|
263
324
|
|
|
325
|
+
const host = detectHostRuntime()
|
|
326
|
+
const runtimeInfo = resolveRuntimeAnchor(options.runtimeVersion, host.version)
|
|
327
|
+
|
|
264
328
|
const { manifest, kind, kindInferred, entry, patch, g4, checks: manifestChecks } = runManifestChecks(dir, {
|
|
265
|
-
runtimeVersion:
|
|
329
|
+
runtimeVersion: runtimeInfo.dsh,
|
|
266
330
|
installSpec: options.installSpec,
|
|
267
331
|
marketIndex: options.marketIndex,
|
|
268
332
|
pluginName: options.pluginName,
|
|
@@ -341,7 +405,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
341
405
|
}
|
|
342
406
|
const report = buildReport({
|
|
343
407
|
subject,
|
|
344
|
-
anchor: anchorOf(options, options.runtime),
|
|
408
|
+
anchor: anchorOf(options, runtimeInfo, options.runtime),
|
|
345
409
|
checks,
|
|
346
410
|
g4,
|
|
347
411
|
scope: options.scope,
|
|
@@ -351,6 +415,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
351
415
|
entryRuntime: options.entryRuntime,
|
|
352
416
|
entryClaim: options.entryClaim,
|
|
353
417
|
advice: options.advice,
|
|
418
|
+
declarations: declarationsOf(manifest),
|
|
354
419
|
})
|
|
355
420
|
const output = writeReport(report, options.format)
|
|
356
421
|
if (options.out) {
|
|
@@ -19,10 +19,15 @@
|
|
|
19
19
|
* `producer.kind === 'sandbox'`) may be reported as `flow.state = 'runtime'`.
|
|
20
20
|
* `self-attested`/`ci-attested` documents keep their `passed` count but stay
|
|
21
21
|
* `not-run`, because structural validation cannot prove where commands ran.
|
|
22
|
+
* A `sandbox-degraded` producer (any isolation item missing) is never runtime,
|
|
23
|
+
* and a document whose `subject.treeStable` is false is structurally invalid:
|
|
24
|
+
* a collector that changed the inspected tree has no trustworthy evidence.
|
|
22
25
|
*/
|
|
23
26
|
import crypto from 'node:crypto'
|
|
24
27
|
import fs from 'node:fs'
|
|
28
|
+
import os from 'node:os'
|
|
25
29
|
import path from 'node:path'
|
|
30
|
+
import { spawnSync } from 'node:child_process'
|
|
26
31
|
|
|
27
32
|
export const FLOW_IDS = ['J1', 'J2', 'J3', 'J4', 'J5', 'J6', 'J7', 'J8']
|
|
28
33
|
export const FLOW_EVIDENCE_SCHEMA = 'dsh-flow-evidence/2'
|
|
@@ -141,6 +146,103 @@ export function treeSha256(dir, { exclude = [] } = {}) {
|
|
|
141
146
|
return { hash: sha256Value(rows.join('\n')), files: rows.length, bytes }
|
|
142
147
|
}
|
|
143
148
|
|
|
149
|
+
const NPM = process.platform === 'win32' ? 'npm.cmd' : 'npm'
|
|
150
|
+
|
|
151
|
+
function subjectFields(subject) {
|
|
152
|
+
if (!isPlainObject(subject)) return null
|
|
153
|
+
return {
|
|
154
|
+
name: subject.name ?? null,
|
|
155
|
+
version: subject.version ?? null,
|
|
156
|
+
integrity: subject.integrity ?? null,
|
|
157
|
+
tarballSha256: subject.tarballSha256 ?? null,
|
|
158
|
+
treeSha256: subject.treeSha256 ?? null,
|
|
159
|
+
commit: subject.commit ?? null,
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Recompute the subject binding from a package directory: package.json
|
|
165
|
+
* name/version, the deterministic tree hash, and the npm-pack tarball hash +
|
|
166
|
+
* integrity. npm packs the same tree byte-identically, so this is the same
|
|
167
|
+
* binding `dsh-flow-collect` recorded.
|
|
168
|
+
*/
|
|
169
|
+
export function recomputeSubject(dir, { exclude = [], timeoutMs = 120_000 } = {}) {
|
|
170
|
+
const root = path.resolve(dir)
|
|
171
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'))
|
|
172
|
+
const tree = treeSha256(root, { exclude })
|
|
173
|
+
const destination = fs.mkdtempSync(path.join(os.tmpdir(), 'dsh-subject-'))
|
|
174
|
+
try {
|
|
175
|
+
const run = spawnSync(NPM, ['pack', '--ignore-scripts', '--json', '--pack-destination', destination], {
|
|
176
|
+
cwd: root, encoding: 'utf8', timeout: timeoutMs, maxBuffer: 16 * 1024 * 1024,
|
|
177
|
+
})
|
|
178
|
+
if (run.error) throw new Error(`npm pack failed to start: ${run.error.message}`)
|
|
179
|
+
if (run.status !== 0) throw new Error(`npm pack failed (exit ${run.status}): ${String(run.stderr ?? '').replace(/\s+/g, ' ').slice(0, 300)}`)
|
|
180
|
+
let parsed
|
|
181
|
+
try { parsed = JSON.parse(run.stdout) } catch { throw new Error('npm pack did not print JSON') }
|
|
182
|
+
const info = Array.isArray(parsed) ? parsed[0] : parsed
|
|
183
|
+
if (!info?.filename) throw new Error('npm pack did not report a tarball filename')
|
|
184
|
+
return {
|
|
185
|
+
name: typeof manifest.name === 'string' ? manifest.name : null,
|
|
186
|
+
version: typeof manifest.version === 'string' ? manifest.version : null,
|
|
187
|
+
treeSha256: tree.hash,
|
|
188
|
+
tarballSha256: sha256File(path.join(destination, info.filename)),
|
|
189
|
+
integrity: typeof info.integrity === 'string' ? info.integrity : null,
|
|
190
|
+
}
|
|
191
|
+
} finally {
|
|
192
|
+
fs.rmSync(destination, { recursive: true, force: true })
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Bind a flow-evidence document to a package directory. Any mismatch in
|
|
198
|
+
* name/version/treeSha256/tarballSha256 (or a declared integrity) is a hard
|
|
199
|
+
* failure: the audit must not accept evidence for a different subject.
|
|
200
|
+
*/
|
|
201
|
+
export function bindEvidenceSubject(document, dir, options = {}) {
|
|
202
|
+
const declared = isPlainObject(document?.subject) ? document.subject : null
|
|
203
|
+
const base = { dir: path.resolve(dir), declared: subjectFields(declared), recomputed: null, mismatches: [], errors: [] }
|
|
204
|
+
if (!declared) return { ...base, ok: false, errors: ['subject is missing'] }
|
|
205
|
+
let recomputed
|
|
206
|
+
try {
|
|
207
|
+
recomputed = recomputeSubject(dir, {
|
|
208
|
+
exclude: Array.isArray(declared.treeExclude) ? declared.treeExclude : [],
|
|
209
|
+
timeoutMs: options.timeoutMs,
|
|
210
|
+
})
|
|
211
|
+
} catch (error) {
|
|
212
|
+
return { ...base, ok: false, errors: [error.message] }
|
|
213
|
+
}
|
|
214
|
+
const mismatches = []
|
|
215
|
+
for (const key of ['name', 'version', 'treeSha256', 'tarballSha256']) {
|
|
216
|
+
if (declared[key] !== recomputed[key]) {
|
|
217
|
+
mismatches.push(`${key}: declared ${declared[key] ?? 'null'} != recomputed ${recomputed[key] ?? 'null'}`)
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (typeof declared.integrity === 'string' && recomputed.integrity !== null && declared.integrity !== recomputed.integrity) {
|
|
221
|
+
mismatches.push('integrity: declared value != recomputed value')
|
|
222
|
+
}
|
|
223
|
+
return { ...base, ok: mismatches.length === 0, recomputed, mismatches }
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Plain CI attribution for display. These fields are read from the runner
|
|
228
|
+
* environment and are self-declared: CI attribution is not proof.
|
|
229
|
+
*/
|
|
230
|
+
export function ciAttribution(env = process.env) {
|
|
231
|
+
const read = key => (typeof env?.[key] === 'string' ? env[key].trim() : '')
|
|
232
|
+
const runId = read('GITHUB_RUN_ID')
|
|
233
|
+
const repository = read('GITHUB_REPOSITORY')
|
|
234
|
+
const commit = read('GITHUB_SHA')
|
|
235
|
+
if (!runId && !repository && !commit) return null
|
|
236
|
+
const server = (read('GITHUB_SERVER_URL') || 'https://github.com').replace(/\/+$/, '')
|
|
237
|
+
return {
|
|
238
|
+
provider: 'github-actions',
|
|
239
|
+
runId: runId || null,
|
|
240
|
+
repository: repository || null,
|
|
241
|
+
commit: commit || null,
|
|
242
|
+
runUrl: runId && repository ? `${server}/${repository}/actions/runs/${runId}` : null,
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
144
246
|
export function isIsoTimestamp(value) {
|
|
145
247
|
return typeof value === 'string' && ISO_RE.test(value) && Number.isFinite(Date.parse(value))
|
|
146
248
|
}
|
|
@@ -178,6 +280,11 @@ export function journeyCountsAsPass(journey) {
|
|
|
178
280
|
*/
|
|
179
281
|
export function isSandboxAttested(document) {
|
|
180
282
|
if (!isPlainObject(document)) return false
|
|
283
|
+
// Degradation is disqualifying even if a claim was left at sandbox-attested:
|
|
284
|
+
// the producer admits an isolation item was missing.
|
|
285
|
+
if (document.producer?.kind === 'sandbox-degraded') return false
|
|
286
|
+
if (document.sandbox?.mode === 'sandbox-degraded') return false
|
|
287
|
+
if (Array.isArray(document.sandbox?.degraded) && document.sandbox.degraded.length > 0) return false
|
|
181
288
|
return document.claims?.claimLevel === 'sandbox-attested' || document.producer?.kind === 'sandbox'
|
|
182
289
|
}
|
|
183
290
|
|
|
@@ -268,12 +375,18 @@ function validateV2(document) {
|
|
|
268
375
|
if (!Array.isArray(command) || command.length === 0 || command.some(item => typeof item !== 'string')) {
|
|
269
376
|
errors.push('producer.command must be a non-empty array of strings')
|
|
270
377
|
}
|
|
271
|
-
if ('kind' in document.producer && document.producer.kind !== null && !['local', 'ci', 'sandbox'].includes(document.producer.kind)) {
|
|
272
|
-
errors.push('producer.kind must be local, ci, or sandbox')
|
|
378
|
+
if ('kind' in document.producer && document.producer.kind !== null && !['local', 'ci', 'sandbox', 'sandbox-degraded'].includes(document.producer.kind)) {
|
|
379
|
+
errors.push('producer.kind must be local, ci, sandbox, or sandbox-degraded')
|
|
273
380
|
}
|
|
274
|
-
for (const key of ['runner', 'actor', 'runUrl']) {
|
|
381
|
+
for (const key of ['runner', 'actor', 'runUrl', 'provider', 'runId', 'repository', 'commit']) {
|
|
275
382
|
if (key in document.producer && document.producer[key] !== null) requireString(document.producer[key], `producer.${key}`)
|
|
276
383
|
}
|
|
384
|
+
if (typeof document.producer.commit === 'string' && !COMMIT_RE.test(document.producer.commit)) {
|
|
385
|
+
errors.push('producer.commit must be a 40-hex commit')
|
|
386
|
+
}
|
|
387
|
+
if (typeof document.producer.runUrl === 'string' && !/^https:\/\//.test(document.producer.runUrl)) {
|
|
388
|
+
errors.push('producer.runUrl must be an https URL')
|
|
389
|
+
}
|
|
277
390
|
}
|
|
278
391
|
|
|
279
392
|
if (requireObject(document.subject, 'subject')) {
|
|
@@ -292,6 +405,12 @@ function validateV2(document) {
|
|
|
292
405
|
}
|
|
293
406
|
}
|
|
294
407
|
if ('source' in subject) requireString(subject.source, 'subject.source')
|
|
408
|
+
if (subject.treeStable === false) {
|
|
409
|
+
errors.push('subject.treeStable is false: the package tree changed during collection')
|
|
410
|
+
}
|
|
411
|
+
if ('treeSha256After' in subject && subject.treeSha256After && subject.treeSha256After !== subject.treeSha256 && subject.treeStable !== false) {
|
|
412
|
+
errors.push('subject.treeSha256After differs from subject.treeSha256: the package tree changed during collection')
|
|
413
|
+
}
|
|
295
414
|
}
|
|
296
415
|
|
|
297
416
|
if (requireObject(document.host, 'host')) {
|
|
@@ -344,6 +463,8 @@ function validateV2(document) {
|
|
|
344
463
|
if ('method' in claims) requireString(claims.method, 'claims.method')
|
|
345
464
|
}
|
|
346
465
|
|
|
466
|
+
validateSandbox(document, errors)
|
|
467
|
+
|
|
347
468
|
if (requireObject(document.chain, 'chain')) {
|
|
348
469
|
requireHash(document.chain.evidenceHash, 'chain.evidenceHash')
|
|
349
470
|
for (const key of Object.keys(document.chain)) if (key !== 'evidenceHash') errors.push(`chain.${key} is not allowed`)
|
|
@@ -389,9 +510,74 @@ function validateV2(document) {
|
|
|
389
510
|
conclusion: document.conclusion ?? null,
|
|
390
511
|
claimLevel: isPlainObject(document.claims) ? document.claims.claimLevel ?? null : null,
|
|
391
512
|
producerKind: isPlainObject(document.producer) ? document.producer.kind ?? null : null,
|
|
513
|
+
sandbox: isPlainObject(document.sandbox)
|
|
514
|
+
? {
|
|
515
|
+
mode: document.sandbox.mode ?? null,
|
|
516
|
+
degraded: Array.isArray(document.sandbox.degraded) ? [...document.sandbox.degraded] : [],
|
|
517
|
+
}
|
|
518
|
+
: null,
|
|
392
519
|
sandboxAttested: isSandboxAttested(document),
|
|
393
520
|
}
|
|
394
521
|
|
|
522
|
+
function validateSandbox(doc, errors) {
|
|
523
|
+
const kind = isPlainObject(doc.producer) ? doc.producer.kind ?? null : null
|
|
524
|
+
const declaredSandbox = 'sandbox' in doc
|
|
525
|
+
if (!declaredSandbox && kind !== 'sandbox' && kind !== 'sandbox-degraded') return
|
|
526
|
+
if (!declaredSandbox) {
|
|
527
|
+
errors.push('sandbox is required when producer.kind is sandbox or sandbox-degraded')
|
|
528
|
+
return
|
|
529
|
+
}
|
|
530
|
+
const sandbox = doc.sandbox
|
|
531
|
+
if (!isPlainObject(sandbox)) {
|
|
532
|
+
errors.push('sandbox must be an object')
|
|
533
|
+
return
|
|
534
|
+
}
|
|
535
|
+
if (!['sandbox', 'sandbox-degraded'].includes(sandbox.mode)) {
|
|
536
|
+
errors.push('sandbox.mode must be sandbox or sandbox-degraded')
|
|
537
|
+
}
|
|
538
|
+
const degraded = Array.isArray(sandbox.degraded) ? sandbox.degraded : null
|
|
539
|
+
if (!degraded || degraded.some(item => typeof item !== 'string' || item.length === 0)) {
|
|
540
|
+
errors.push('sandbox.degraded must be an array of non-empty strings')
|
|
541
|
+
}
|
|
542
|
+
const list = degraded ?? []
|
|
543
|
+
if (sandbox.mode === 'sandbox' && list.length > 0) errors.push('sandbox.mode is sandbox but sandbox.degraded is not empty')
|
|
544
|
+
if (sandbox.mode === 'sandbox-degraded' && list.length === 0) errors.push('sandbox.mode is sandbox-degraded but sandbox.degraded is empty')
|
|
545
|
+
if ('env' in sandbox) {
|
|
546
|
+
if (!isPlainObject(sandbox.env)) errors.push('sandbox.env must be an object')
|
|
547
|
+
else if (typeof sandbox.env.absent !== 'boolean') errors.push('sandbox.env.absent must be a boolean')
|
|
548
|
+
}
|
|
549
|
+
if ('network' in sandbox && (!isPlainObject(sandbox.network) || typeof sandbox.network.isolated !== 'boolean')) {
|
|
550
|
+
errors.push('sandbox.network.isolated must be a boolean')
|
|
551
|
+
}
|
|
552
|
+
if ('limits' in sandbox) {
|
|
553
|
+
if (!isPlainObject(sandbox.limits)) errors.push('sandbox.limits must be an object')
|
|
554
|
+
else {
|
|
555
|
+
if ('available' in sandbox.limits && typeof sandbox.limits.available !== 'boolean') errors.push('sandbox.limits.available must be a boolean')
|
|
556
|
+
if ('applied' in sandbox.limits && (!Array.isArray(sandbox.limits.applied) || sandbox.limits.applied.some(item => typeof item !== 'string'))) {
|
|
557
|
+
errors.push('sandbox.limits.applied must be an array of strings')
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
if ('home' in sandbox && (!isPlainObject(sandbox.home) || typeof sandbox.home.isolated !== 'boolean')) {
|
|
562
|
+
errors.push('sandbox.home.isolated must be a boolean')
|
|
563
|
+
}
|
|
564
|
+
if ('executed' in sandbox && typeof sandbox.executed !== 'boolean') errors.push('sandbox.executed must be a boolean')
|
|
565
|
+
if (kind === 'sandbox') {
|
|
566
|
+
if (sandbox.mode !== 'sandbox') errors.push('producer.kind=sandbox requires sandbox.mode=sandbox')
|
|
567
|
+
if (list.length > 0) errors.push('producer.kind=sandbox requires an empty sandbox.degraded list')
|
|
568
|
+
if (sandbox.executed !== true) errors.push('producer.kind=sandbox requires sandbox.executed=true')
|
|
569
|
+
if (sandbox.env?.absent !== true) errors.push('producer.kind=sandbox requires sandbox.env.absent=true')
|
|
570
|
+
if (sandbox.network?.isolated !== true) errors.push('producer.kind=sandbox requires sandbox.network.isolated=true')
|
|
571
|
+
if (sandbox.limits?.available !== true) errors.push('producer.kind=sandbox requires sandbox.limits.available=true')
|
|
572
|
+
if (sandbox.home?.isolated !== true) errors.push('producer.kind=sandbox requires sandbox.home.isolated=true')
|
|
573
|
+
}
|
|
574
|
+
if (kind === 'sandbox-degraded') {
|
|
575
|
+
if (sandbox.mode !== 'sandbox-degraded') errors.push('producer.kind=sandbox-degraded requires sandbox.mode=sandbox-degraded')
|
|
576
|
+
if (list.length === 0) errors.push('producer.kind=sandbox-degraded requires at least one sandbox.degraded reason')
|
|
577
|
+
if (doc.claims?.claimLevel === 'sandbox-attested') errors.push('producer.kind=sandbox-degraded cannot claim sandbox-attested')
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
395
581
|
function validateCheck(check, label) {
|
|
396
582
|
if (!isPlainObject(check)) {
|
|
397
583
|
errors.push(`${label} must be an object`)
|
package/40-checker/src/index.mjs
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/** Programmatic API for dsh-plugin-verify. */
|
|
2
|
-
export { main, parseArgs, usage, VERSION } from './cli.mjs'
|
|
2
|
+
export { main, parseArgs, usage, VERSION, detectHostRuntime, resolveRuntimeAnchor, declarationsOf } from './cli.mjs'
|
|
3
3
|
export { runManifestChecks, checkMarketEntry, DEFAULT_RUNTIME_VERSION, inferKind, resolveEntry } from './manifest.mjs'
|
|
4
4
|
export { checkCapabilities, checkCredentials, checkLabels, checkImportTimeAdvisory, CAPABILITIES, LABELS } from './declarations.mjs'
|
|
5
5
|
export { runRuntimeChecks } from './runtime.mjs'
|
|
6
|
-
export { buildReport, formatText, formatSarif, writeReport } from './report.mjs'
|
|
6
|
+
export { buildReport, formatText, formatSarif, writeReport, summarizeBlocking, DECLARATION_CHECKS } from './report.mjs'
|
|
7
7
|
export { joinAdvice, DOCS_BASE } from './remediation.mjs'
|
|
8
8
|
export { satisfies, validRange, parseVersion, compareVersions } from './semver.mjs'
|
|
9
9
|
export { parseInstallSpec } from './install-spec.mjs'
|
|
10
|
-
export { FLOW_IDS, FLOW_EVIDENCE_SCHEMA, evidenceHash, finalizeEvidence, journeyAggregate, isSandboxAttested, treeSha256, validateFlowEvidence } from './flow-evidence.mjs'
|
|
10
|
+
export { FLOW_IDS, FLOW_EVIDENCE_SCHEMA, evidenceHash, finalizeEvidence, journeyAggregate, isSandboxAttested, treeSha256, validateFlowEvidence, recomputeSubject, bindEvidenceSubject, ciAttribution } from './flow-evidence.mjs'
|
|
11
11
|
export { sanitizedEnv, scrubProcessEnv, isSecretEnvName, isAllowedEnvName, envPolicy } from './sandbox-env.mjs'
|