dsh-standards 1.1.2 → 1.2.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.
Files changed (42) hide show
  1. package/40-checker/README.md +15 -0
  2. package/40-checker/bin/dsh-flow-collect.mjs +3 -0
  3. package/40-checker/bin/dsh-levels.mjs +13 -2
  4. package/40-checker/scripts/bundle-smoke.mjs +3 -0
  5. package/40-checker/scripts/entry18-ci.mjs +4 -3
  6. package/40-checker/scripts/entry18-web.mjs +4 -1
  7. package/40-checker/scripts/flow-collect.mjs +494 -0
  8. package/40-checker/scripts/flow-sample-ci.mjs +4 -4
  9. package/40-checker/scripts/host-farm.mjs +2 -1
  10. package/40-checker/scripts/journey-config-observe.mjs +5 -3
  11. package/40-checker/scripts/journey-runtime.mjs +208 -83
  12. package/40-checker/scripts/level-audit.mjs +119 -19
  13. package/40-checker/scripts/m2-runner.mjs +4 -3
  14. package/40-checker/scripts/runtime-child.mjs +4 -0
  15. package/40-checker/src/cli.mjs +10 -2
  16. package/40-checker/src/flow-evidence.mjs +455 -0
  17. package/40-checker/src/index.mjs +3 -0
  18. package/40-checker/src/remediation.mjs +137 -0
  19. package/40-checker/src/report.mjs +19 -2
  20. package/40-checker/src/sandbox-env.mjs +78 -0
  21. package/CHANGELOG.md +22 -0
  22. package/bin/dsh-flow-collect.mjs +4 -0
  23. package/package.json +1 -2
  24. package/spec/README.md +2 -0
  25. package/spec/flow/README.md +27 -2
  26. package/spec/flow-evidence.schema.json +221 -0
  27. package/spec/levels.json +2 -0
  28. package/spec/remediation.json +478 -0
  29. package/40-checker/test/attestation.test.mjs +0 -59
  30. package/40-checker/test/checks.test.mjs +0 -414
  31. package/40-checker/test/claim.test.mjs +0 -28
  32. package/40-checker/test/client-shape.test.mjs +0 -35
  33. package/40-checker/test/dco.test.mjs +0 -51
  34. package/40-checker/test/entry-validate.test.mjs +0 -172
  35. package/40-checker/test/governance.test.mjs +0 -76
  36. package/40-checker/test/label.test.mjs +0 -97
  37. package/40-checker/test/level-audit.test.mjs +0 -99
  38. package/40-checker/test/levels.test.mjs +0 -31
  39. package/40-checker/test/ops.test.mjs +0 -117
  40. package/40-checker/test/permissions.test.mjs +0 -40
  41. package/40-checker/test/semver.test.mjs +0 -32
  42. package/40-checker/test/verify.test.mjs +0 -57
@@ -1,4 +1,6 @@
1
1
  import path from 'node:path'
2
+ import remediationSpec from '../../spec/remediation.json' with { type: 'json' }
3
+ import { DOCS_BASE, joinAdvice } from './remediation.mjs'
2
4
 
3
5
  export const PACKAGE_MANDATORY = [
4
6
  'DSC-01', 'DSC-02', 'DSC-04', 'DSC-05', 'DSC-06', 'DSC-07', 'DSC-08', 'DSC-09', 'DSC-11', 'DSC-12', 'DSC-13',
@@ -37,7 +39,7 @@ function allowedNa(check, scope, profile, kind) {
37
39
  return false
38
40
  }
39
41
 
40
- export function buildReport({ subject, anchor, checks, g4 = null, scope, profile, kind, staticOnly = false, entryRuntime = 'require', entryClaim = 'runtime' }) {
42
+ export function buildReport({ subject, anchor, checks, g4 = null, scope, profile, kind, staticOnly = false, entryRuntime = 'require', entryClaim = 'runtime', advice = true }) {
41
43
  const mandatory = mandatoryFor(scope, profile, staticOnly, entryClaim)
42
44
  const adjustedChecks = scope === 'entry' ? checks.map(item => adjustEntryCheck(item, entryClaim)) : checks
43
45
  const runtimeIds = new Set(['DSC-04', 'DSC-05', 'DSC-06', 'DSC-07', 'DSC-08', 'DSC-09'])
@@ -85,7 +87,7 @@ export function buildReport({ subject, anchor, checks, g4 = null, scope, profile
85
87
  if (warnings.length > 0) reasonParts.push(`WARN: ${warnings.map(item => item.id).join(', ')}`)
86
88
  if (advisoryWarnings.length > 0) reasonParts.push(`advisory: ${advisoryWarnings.map(item => item.id).join(', ')}`)
87
89
 
88
- return {
90
+ const report = {
89
91
  schema: 'dsh-conformance/1',
90
92
  standard: { id: 'DS-C', version: '1.0.0' },
91
93
  subject,
@@ -120,6 +122,12 @@ export function buildReport({ subject, anchor, checks, g4 = null, scope, profile
120
122
  reason: reasonParts.join('; '),
121
123
  },
122
124
  }
125
+ // Additive only: `advice` is a sibling of `checks`, never part of a check,
126
+ // and the verdict is computed above without reading it.
127
+ if (advice !== false) {
128
+ report.advice = joinAdvice(report, remediationSpec, DOCS_BASE)
129
+ }
130
+ return report
123
131
  }
124
132
 
125
133
  const ICON = { PASS: 'PASS', WARN: 'WARN', FAIL: 'FAIL', NA: 'NA ', ERROR: 'ERR ' }
@@ -145,6 +153,15 @@ export function formatText(report) {
145
153
  lines.push(`coverage: mandatory=${coverage.mandatory} pass=${coverage.pass} warn=${coverage.warn} fail=${coverage.fail} na=${coverage.na} notRun=${coverage.notRun}`)
146
154
  if (conclusion.reason) lines.push(`reason: ${conclusion.reason}`)
147
155
  if (conclusion.status === 'incomplete') lines.push('NOT A DS-C PASS: at least one mandatory check did not run (see notRun).')
156
+ if (Array.isArray(report.advice) && report.advice.length > 0) {
157
+ lines.push('')
158
+ lines.push('advice (non-blocking):')
159
+ for (const item of report.advice) {
160
+ const missing = item.ruleMissing ? ' [ruleMissing]' : ''
161
+ lines.push(` ${String(item.severity).padEnd(4)} ${String(item.key).padEnd(7)} ${item.source}${missing}: ${String(item.fix ?? item.title ?? '').replace(/\s+/g, ' ').slice(0, 160)}`)
162
+ if (item.docsUrl) lines.push(` ${item.docsUrl}`)
163
+ }
164
+ }
148
165
  lines.push('')
149
166
  return `${lines.join('\n')}\n`
150
167
  }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Environment policy for child processes that may load third-party plugin code.
3
+ *
4
+ * A plugin boots on first import: anything it can read from `process.env` is
5
+ * already exfiltratable before a single check runs. `...process.env` therefore
6
+ * must never be forwarded to `dsh plugin add`, a host boot, or a runtime child.
7
+ * The policy is an allowlist (not a denylist) so a newly named credential does
8
+ * not silently leak; the secret-shaped name filter stays as a second gate in
9
+ * case an allowlisted name is ambiguous.
10
+ *
11
+ * Callers that need isolation (temporary HOME, DSH_HOME, TMPDIR) pass those as
12
+ * explicit overrides. Overrides are the caller's own values, never inherited.
13
+ */
14
+
15
+ const ALLOWED = new Set([
16
+ // Portable process essentials.
17
+ 'PATH', 'HOME', 'USERPROFILE', 'TMPDIR', 'TEMP', 'TMP', 'PWD', 'SHELL',
18
+ 'USER', 'LOGNAME', 'LANG', 'LANGUAGE', 'LC_ALL', 'LC_CTYPE', 'LC_MESSAGES',
19
+ 'TZ', 'TERM', 'NO_COLOR', 'FORCE_COLOR', 'COLORTERM',
20
+ // Windows needs these to locate node/npm/tmp; they carry no credentials.
21
+ 'SYSTEMROOT', 'SYSTEMDRIVE', 'WINDIR', 'COMSPEC', 'PATHEXT', 'OS',
22
+ 'NUMBER_OF_PROCESSORS', 'PROCESSOR_ARCHITECTURE', 'PROCESSOR_ARCHITEW6432',
23
+ 'APPDATA', 'LOCALAPPDATA', 'PROGRAMDATA', 'PROGRAMFILES',
24
+ 'PROGRAMFILES(X86)', 'COMMONPROGRAMFILES', 'HOMEDRIVE', 'HOMEPATH',
25
+ ])
26
+
27
+ const SECRET_NAME = /(TOKEN|SECRET|PASSWORD|PASSWD|API_?KEY|PRIVATE_?KEY|CREDENTIAL|AUTH|COOKIE|SESSION|_KEY$)/i
28
+
29
+ export function isSecretEnvName(name) {
30
+ return SECRET_NAME.test(String(name ?? ''))
31
+ }
32
+
33
+ export function isAllowedEnvName(name) {
34
+ return ALLOWED.has(String(name)) && !isSecretEnvName(name)
35
+ }
36
+
37
+ /**
38
+ * Build the child environment: inherited allowlisted keys + caller overrides.
39
+ * Overrides are trusted call values (paths the caller just created), not
40
+ * inherited state, so they are not filtered.
41
+ */
42
+ export function sanitizedEnv(overrides = {}) {
43
+ const env = {}
44
+ for (const [key, value] of Object.entries(process.env)) {
45
+ if (value === undefined || !isAllowedEnvName(key)) continue
46
+ env[key] = value
47
+ }
48
+ for (const [key, value] of Object.entries(overrides)) {
49
+ if (value === undefined || value === null) continue
50
+ env[key] = String(value)
51
+ }
52
+ return env
53
+ }
54
+
55
+ /**
56
+ * In-process variant for scripts that boot a plugin in this very process
57
+ * (bundle smoke, runtime child). It deletes everything not allowlisted, so a
58
+ * direct `node script.mjs` invocation gets the same policy as a spawned one.
59
+ * Returns the removed key names (never values) for logging.
60
+ */
61
+ export function scrubProcessEnv() {
62
+ const removed = []
63
+ for (const key of Object.keys(process.env)) {
64
+ if (isAllowedEnvName(key)) continue
65
+ removed.push(key)
66
+ delete process.env[key]
67
+ }
68
+ return removed
69
+ }
70
+
71
+ export function envPolicy() {
72
+ return {
73
+ mode: 'allowlist',
74
+ allow: [...ALLOWED].sort(),
75
+ denyNamePattern: SECRET_NAME.source,
76
+ note: 'tokens/keys/secrets/credentials are dropped even if a future allowlist entry matches',
77
+ }
78
+ }
package/CHANGELOG.md CHANGED
@@ -1,3 +1,19 @@
1
+ ## 1.2.1 — 2026-11-06
2
+
3
+ - **V2 核验修复**:`level-audit` 不再把结构自洽的 self-attested/ci-attested `dsh-flow-evidence/2` 判成 FLOW `runtime`。只有 `claims.claimLevel=sandbox-attested` 或 `producer.kind=sandbox` 才允许 `runtime`;self/ci 的 `passed` 计数保留但 state 记 `not-run`,reason 为 `<claimLevel> evidence; runtime requires sandbox-attested`。`spec/flow-evidence.schema.json` 的 `claims`/`producer.kind` 补充了该判定说明。`advice[]` 新增 `gate`(该 check 的最终结果 PASS/WARN/FAIL),`severity` 跟随 `gate`:ENTRY static claim 把 DSC-02 降为 WARN 时,C-2.1 的 FAIL detail 仍保留在 `reason`,但 advice severity 为 WARN。测试 150 → 152。
4
+ - **FLOW 证据链**:新增 `spec/flow-evidence.schema.json`(`dsh-flow-evidence/2`)与 `40-checker/src/flow-evidence.mjs`。`level-audit --flow-evidence` 只接受 v2;v1(`dsh-flow-journey/1`、`dsh-flow-runtime/1`)与裸 `{Jx:{status}}` 标 `deprecated`,不计 pass。每个 J 须有 `checks[]` 且全部 `exitCode === 0`、断言成立、至少一条非 `static` 观测检查才计 pass(`warn`/`blocked`/`na`/`static` 不计 8/8)。v2 的 `chain.evidenceHash` 由 audit 重算,改动一个字节即 `invalid`。
5
+ - **collector**:新增 `40-checker/bin/dsh-flow-collect.mjs`(根入口 `bin/dsh-flow-collect.mjs`,并支持 `dsh-levels flow collect`)。只读副本 + 前后 `treeSha256` 一致、`npm pack --ignore-scripts` 的 integrity/tarballSha256、`git rev-parse` commit、临时 HOME/DSH_HOME、失败五要素提示(子检查/命令/exitCode/stderr 末 20 行/日志绝对路径 + 条款锚);前置缺失记 `blocked` 且 exit 0,真失败 exit 1。
6
+ - **安全**:新增 `40-checker/src/sandbox-env.mjs` 白名单 env。`src/cli.mjs` 的 runtime-child/bundle-smoke 子进程、`bundle-smoke.mjs`/`runtime-child.mjs` 进程内 boot、`m2-runner.mjs`、`journey-runtime.mjs`、`journey-config-observe.mjs`、`flow-sample-ci.mjs`、`host-farm.mjs`、`entry18-web.mjs` 不再把 `process.env` 透传给被检插件/宿主 boot。
7
+ - 测试 137 → 149(新增 `40-checker/test/flow-evidence.test.mjs`:v1 不再 pass、v2 伪造 checks/exitCode 不 pass、篡改链路 invalid、blocked 不判 fail、collector 产物可被 audit 读取、失败五要素提示、env 白名单)。
8
+ - 发布包不再包含 `40-checker/test`:该目录依赖仓库内 fixtures,装到用户环境后无法自测,`npm test` 会在 tarball 内失败。`npm test` 152/152。
9
+
10
+ ## 1.2.0 — 2026-11-06
11
+
12
+ - 新增**建议管道**:`spec/remediation.json` 提供 68 条规则(`details[].item` ∪ `checks[].clause` ∪ G0–G4),`40-checker/src/remediation.mjs` 的纯函数 `joinAdvice(report, rules, docsBase)` 把 FAIL/WARN detail 连接成 `advice[]`;无 detail 的运行时 FAIL 用 `check.clause` 兜底(C-4/C-14)。
13
+ - `buildReport()` 输出新增**加性** `advice[]`(key/severity/title/why/fix/docsUrl/source/version/evidence),不改任何 check 构造、不改判定与 exit code;CLI 新增 `--advice`(默认开)与 `--no-advice`。
14
+ - 建议文档统一指向 `dsh-plugin-guide` 的 v0.4.3:`docsUrl = https://github.com/PerryLink/dsh-plugin-guide/blob/v0.4.3/ + docs`;规则表 `docs` 只存 `文件#锚点`。
15
+ - 新增 `40-checker/test/remediation.test.mjs`:规则表覆盖全集、fixture 无 `ruleMissing`、本地 guide 锚点存在、severity/merge/兜底/纯函数回归。测试 128 → 137。
16
+
1
17
  ## 1.1.2 — 2026-11-04
2
18
 
3
19
  - 修复 1.1.1 的发布失败:vendored `@babel/parser` 的 `node_modules/` 被根 `.gitignore` 忽略,CI checkout 缺依赖导致 `npm test` 失败。现在 force-add 四个 vendored 包(约 2 MB),并加 1.1.2 的回归说明。
@@ -65,6 +81,12 @@
65
81
 
66
82
  # Changelog
67
83
 
84
+ ## 2026-11-04 — 0.2.2-alpha.3 baseline in the three-level docs
85
+
86
+ - Add the 0.2.2-alpha.3 baseline table and six PR-review evidence gates to `35`.
87
+ - Carry the baseline into `31` (ENTRY), `32` (FLOW R6) and the `33` BOOST card.
88
+ - Fix the ENTRY checklist: Node `>=24.9.0`, `engines.dsh` precedence from 0.2.1-alpha.4, frozen lockfile / release-age evidence.
89
+
68
90
  ## 2026-11-03 — Node 24.9 baseline
69
91
  - Require Node >=24.9.0 to match DeepSeek Harness 0.2.2-alpha.3.
70
92
 
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ // Public umbrella entry point for the FLOW evidence collector.
3
+ // The implementation lives in 40-checker/.
4
+ import '../40-checker/bin/dsh-flow-collect.mjs'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-standards",
3
- "version": "1.1.2",
3
+ "version": "1.2.1",
4
4
  "description": "DSH plugin standards and conformance checks: ENTRY/FLOW checks plus BOOST guidance. Non-official; no certification or ranking.",
5
5
  "type": "module",
6
6
  "main": "40-checker/src/index.mjs",
@@ -20,7 +20,6 @@
20
20
  "40-checker/scripts",
21
21
  "40-checker/samples",
22
22
  "40-checker/fixtures",
23
- "40-checker/test",
24
23
  "40-checker/package.json",
25
24
  "40-checker/README.md",
26
25
  "spec",
package/spec/README.md CHANGED
@@ -14,7 +14,9 @@ Machine-readable:
14
14
  - [levels.json](levels.json) — levels, gates, evidence states, crosswalk.
15
15
  - [clauses.json](clauses.json) — stable clause IDs mapped to gates (`clause-map.mjs`).
16
16
  - [reference-suite.json](reference-suite.json) — versioned samples, anchors, change rules.
17
+ - [remediation.json](remediation.json) — 68-key advice table for the additive `advice[]` report field (docs anchors + severity hints).
17
18
  - [schemas/evidence.schema.json](schemas/evidence.schema.json) — `dsh-levels/1` record.
19
+ - [flow-evidence.schema.json](flow-evidence.schema.json) — `dsh-flow-evidence/2` FLOW chain (J1–J8 checks, subject/host binding, `evidenceHash`). v1 and bare `{Jx:{status}}` rows are deprecated and never count as a pass.
18
20
  - [schemas/attestation.schema.json](schemas/attestation.schema.json) — `dsh-attestation/1` quality record.
19
21
  - [schemas/report.schema.json](schemas/report.schema.json) — `dsh-report/1` inspection report (no grade).
20
22
  - [../40-checker/scripts/level-audit.mjs](../40-checker/scripts/level-audit.mjs) — one-command audit.
@@ -4,5 +4,30 @@ Usability surface: install, configure, first success, permissions, upgrade,
4
4
  uninstall, diagnostics, client/server states.
5
5
 
6
6
  - Normative text: [../../32-DSH-FLOW顺畅规范.md](../../32-DSH-FLOW顺畅规范.md)
7
- - Journeys: J1–J8; only J1 has a runtime harness (`40-checker/scripts/journey-runtime.mjs`).
8
- - Current: J1 6 PASS + 1 WARN; J2–J8 not-run.
7
+ - Journeys: J1–J8 (R1–R8 in the normative text).
8
+ - Runtime probe: `40-checker/scripts/journey-runtime.mjs` (J1/J2/J4/J6); J3/J5/J7/J8
9
+ are static preflight or manual and never count as a runtime pass.
10
+ - Evidence: `dsh-flow-evidence/2` ([../flow-evidence.schema.json](../flow-evidence.schema.json)).
11
+ Collect it with `node 40-checker/bin/dsh-flow-collect.mjs --package <dir> --out flow-evidence.json`.
12
+
13
+ ## Evidence chain rules
14
+
15
+ - `level-audit.mjs --flow-evidence` accepts only `dsh-flow-evidence/2`. v1
16
+ (`dsh-flow-journey/1`, `dsh-flow-runtime/1`) and bare `{ "J1": { "status": "pass" } }`
17
+ rows are reported `deprecated` and never count as a pass.
18
+ - A journey counts as pass only when it has at least one `checks[]` element,
19
+ every check has `exitCode === 0`, every assertion holds, and at least one
20
+ check is an observed (non-`static`) command. `warn`, `blocked`, `na`,
21
+ `static` and `not-run` never count toward 8/8.
22
+ - `static` preflight is not runtime: a static PASS is a preflight result only.
23
+ - The collector copies the package to a temp dir, runs against the copy, and
24
+ records `subject.treeSha256` before/after; a changed tree fails the run.
25
+ - Child environments are built from an allowlist; tokens/keys/secrets never
26
+ reach the inspected plugin.
27
+ - `claims.claimLevel` is `self-attested` for a local collector run,
28
+ `ci-attested` with `--ci`. Neither is `verified`, a certification, or a
29
+ security audit; only a sandbox run may be described as runtime-attested.
30
+ - `level-audit.mjs` reports FLOW `runtime` only when `claims.claimLevel` is
31
+ `sandbox-attested` or `producer.kind` is `sandbox`. A `self-attested`/
32
+ `ci-attested` document keeps its `passed` count but is reported `not-run`;
33
+ structural validation cannot prove where the commands ran.
@@ -0,0 +1,221 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/PerryLink/dsh-standards/spec/flow-evidence.schema.json",
4
+ "title": "DSH FLOW evidence chain",
5
+ "description": "One collected FLOW result (J1-J8) bound to a package, a host, a timestamp, and a chain hash. Structural validation only: it does not prove the commands really ran. v1 (dsh-flow-journey/1, dsh-flow-runtime/1) and bare {Jx:{status}} documents are deprecated and never count as a pass. `level-audit.mjs --flow-evidence` accepts this document only, and reports `runtime` only when `claims.claimLevel` is `sandbox-attested` or `producer.kind` is `sandbox`; self-attested/ci-attested keeps its pass count but stays not-run.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": [
9
+ "schema",
10
+ "generatedAt",
11
+ "producer",
12
+ "subject",
13
+ "host",
14
+ "journeys",
15
+ "claims",
16
+ "chain"
17
+ ],
18
+ "properties": {
19
+ "schema": {
20
+ "const": "dsh-flow-evidence/2"
21
+ },
22
+ "evidenceId": {
23
+ "type": "string",
24
+ "minLength": 1
25
+ },
26
+ "generatedAt": {
27
+ "type": "string",
28
+ "format": "date-time"
29
+ },
30
+ "producer": {
31
+ "type": "object",
32
+ "description": "Who collected the evidence. `kind: sandbox` is the producer-side equivalent of `claims.claimLevel: sandbox-attested` and is one of the two ways a document may be reported as FLOW runtime.",
33
+ "additionalProperties": false,
34
+ "required": ["command", "at"],
35
+ "properties": {
36
+ "command": {
37
+ "type": "array",
38
+ "minItems": 1,
39
+ "items": { "type": "string" }
40
+ },
41
+ "at": {
42
+ "type": "string",
43
+ "format": "date-time"
44
+ },
45
+ "kind": {
46
+ "enum": ["local", "ci", "sandbox"],
47
+ "description": "`local`/`ci` are structural evidence only; `sandbox` may count toward FLOW runtime."
48
+ },
49
+ "runner": { "type": "string" },
50
+ "actor": { "type": "string" },
51
+ "runUrl": { "type": "string" }
52
+ }
53
+ },
54
+ "subject": {
55
+ "type": "object",
56
+ "additionalProperties": false,
57
+ "required": ["name", "version", "integrity", "tarballSha256", "commit", "treeSha256"],
58
+ "properties": {
59
+ "name": { "type": "string", "minLength": 1 },
60
+ "version": { "type": "string", "minLength": 1 },
61
+ "integrity": {
62
+ "oneOf": [
63
+ { "type": "string", "pattern": "^(sha512|sha256)-[A-Za-z0-9+/=]+$" },
64
+ { "type": "null" }
65
+ ]
66
+ },
67
+ "tarballSha256": { "type": "string", "pattern": "^sha256:[0-9a-f]{64}$" },
68
+ "commit": {
69
+ "oneOf": [
70
+ { "type": "string", "pattern": "^[0-9a-f]{40}$" },
71
+ { "type": "null" }
72
+ ]
73
+ },
74
+ "treeSha256": { "type": "string", "pattern": "^sha256:[0-9a-f]{64}$" },
75
+ "treeSha256After": {
76
+ "oneOf": [
77
+ { "type": "string", "pattern": "^sha256:[0-9a-f]{64}$" },
78
+ { "type": "null" }
79
+ ]
80
+ },
81
+ "treeStable": { "type": "boolean" },
82
+ "treeExclude": {
83
+ "type": "array",
84
+ "items": { "type": "string" }
85
+ },
86
+ "source": { "type": "string" }
87
+ }
88
+ },
89
+ "host": {
90
+ "type": "object",
91
+ "additionalProperties": false,
92
+ "required": ["dsh", "node", "platform"],
93
+ "properties": {
94
+ "dsh": {
95
+ "oneOf": [
96
+ { "type": "string", "minLength": 1 },
97
+ { "type": "null" }
98
+ ]
99
+ },
100
+ "dshCommit": {
101
+ "oneOf": [
102
+ { "type": "string", "pattern": "^[0-9a-f]{7,40}$" },
103
+ { "type": "null" }
104
+ ]
105
+ },
106
+ "cordis": { "type": "string" },
107
+ "node": { "type": "string", "minLength": 1 },
108
+ "platform": { "type": "string", "minLength": 1 }
109
+ }
110
+ },
111
+ "journeys": {
112
+ "type": "object",
113
+ "additionalProperties": false,
114
+ "required": ["J1", "J2", "J3", "J4", "J5", "J6", "J7", "J8"],
115
+ "patternProperties": {
116
+ "^J[1-8]$": { "$ref": "#/$defs/journey" }
117
+ }
118
+ },
119
+ "claims": {
120
+ "type": "object",
121
+ "description": "Attestation level of this evidence. Passing journeys are not runtime by themselves: `level-audit` may report `runtime` only when `claimLevel` is `sandbox-attested` (or `producer.kind` is `sandbox`). `self-attested` (local collector) and `ci-attested` (`--ci`) keep their `passed` count but are reported `not-run`, because structural validation cannot prove the commands ran, let alone in a sandbox.",
122
+ "additionalProperties": false,
123
+ "required": ["claimLevel"],
124
+ "properties": {
125
+ "claimLevel": {
126
+ "enum": ["self-attested", "ci-attested", "sandbox-attested"],
127
+ "description": "`sandbox-attested` is the only claim level that can yield FLOW runtime; `self-attested` and `ci-attested` are never runtime."
128
+ },
129
+ "notClaimed": {
130
+ "type": "array",
131
+ "items": { "type": "string" }
132
+ },
133
+ "method": { "type": "string" }
134
+ }
135
+ },
136
+ "chain": {
137
+ "type": "object",
138
+ "additionalProperties": false,
139
+ "required": ["evidenceHash"],
140
+ "properties": {
141
+ "evidenceHash": { "$ref": "#/$defs/sha256" }
142
+ }
143
+ },
144
+ "conclusion": {
145
+ "enum": ["flow-pass", "flow-warn", "flow-blocked", "flow-fail"]
146
+ },
147
+ "signature": {
148
+ "oneOf": [{ "type": "object" }, { "type": "null" }]
149
+ }
150
+ },
151
+ "$defs": {
152
+ "sha256": {
153
+ "type": "string",
154
+ "pattern": "^sha256:[0-9a-f]{64}$"
155
+ },
156
+ "log": {
157
+ "type": "object",
158
+ "additionalProperties": false,
159
+ "required": ["path", "sha256"],
160
+ "properties": {
161
+ "path": { "type": "string", "minLength": 1 },
162
+ "sha256": { "$ref": "#/$defs/sha256" }
163
+ }
164
+ },
165
+ "assertion": {
166
+ "type": "object",
167
+ "additionalProperties": false,
168
+ "required": ["claim", "observed"],
169
+ "properties": {
170
+ "claim": { "type": "string", "minLength": 1 },
171
+ "observed": { "type": "boolean" },
172
+ "detail": { "type": "string" }
173
+ }
174
+ },
175
+ "check": {
176
+ "type": "object",
177
+ "additionalProperties": false,
178
+ "required": ["id", "command", "exitCode", "startedAt", "endedAt", "stdout", "assertions"],
179
+ "properties": {
180
+ "id": { "type": "string", "pattern": "^J[1-8](?:\\.[A-Za-z0-9._-]+)?$" },
181
+ "kind": { "enum": ["observed", "static", "manual"] },
182
+ "command": {
183
+ "type": "array",
184
+ "minItems": 1,
185
+ "items": { "type": "string" }
186
+ },
187
+ "exitCode": { "type": "integer", "minimum": 0, "maximum": 255 },
188
+ "startedAt": { "type": "string", "format": "date-time" },
189
+ "endedAt": { "type": "string", "format": "date-time" },
190
+ "stdout": { "$ref": "#/$defs/log" },
191
+ "stderr": { "$ref": "#/$defs/log" },
192
+ "assertions": {
193
+ "type": "array",
194
+ "minItems": 1,
195
+ "items": { "$ref": "#/$defs/assertion" }
196
+ },
197
+ "durationMs": { "type": "number", "minimum": 0 }
198
+ }
199
+ },
200
+ "journey": {
201
+ "type": "object",
202
+ "additionalProperties": false,
203
+ "required": ["checks"],
204
+ "properties": {
205
+ "status": {
206
+ "enum": ["pass", "fail", "warn", "blocked", "static", "na", "not-run"]
207
+ },
208
+ "reason": { "type": "string" },
209
+ "kind": { "type": "string" },
210
+ "limitations": {
211
+ "type": "array",
212
+ "items": { "type": "string" }
213
+ },
214
+ "checks": {
215
+ "type": "array",
216
+ "items": { "$ref": "#/$defs/check" }
217
+ }
218
+ }
219
+ }
220
+ }
221
+ }
package/spec/levels.json CHANGED
@@ -156,6 +156,8 @@
156
156
  "machine": {
157
157
  "journey": "40-checker/scripts/journey.mjs",
158
158
  "journeyRuntime": "40-checker/scripts/journey-runtime.mjs",
159
+ "collector": "40-checker/bin/dsh-flow-collect.mjs",
160
+ "flowEvidence": "spec/flow-evidence.schema.json",
159
161
  "audit": "40-checker/scripts/level-audit.mjs"
160
162
  }
161
163
  },