cli-validator 7.0.34 → 7.0.36

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/README.md CHANGED
@@ -36,3 +36,40 @@ Brain Client 服务端在同一次 runtime 请求的事务中绑定真实响应
36
36
  未配置 signer 时只返回 local TestEvidence,独立终审仍为 incomplete。可选 `validator.runner-signer/1.0` 配置包含外置 privateKeyPath、keyId(Ed25519 SPKI DER SHA-256)、receiptTtlMs(最长 10 分钟);配置和私钥同样必须为外部 0600 文件。签名绑定 subject、退出结果与日志指纹,消费方使用已配置公钥核验。密钥配置不是进程隔离:同 UID 子进程可能读取同账户文件,生产可信服务仍须独立 UID/容器及权限隔离;本工具不自动部署隔离、不创建可信密钥,也不改变验证器的信任配置。
37
37
 
38
38
  本地运行仅支持 POSIX;无 shell、显式子进程环境、受限输出与时限。信号终止保留 exitCode=null,不制造整数退出码或成功 receipt。超时、输出超限、非零退出或运行中完整性变化均失败;原始执行记录与日志摘要可审计。此进程执行器不是 OS 沙箱,不授予任意磁盘或网络访问。
39
+
40
+ ## Brain planning verification
41
+
42
+ IDE execution reports remain `reported` until a trusted runner verifies them. To prepare an execution plan from the server's complete report response:
43
+
44
+ ```sh
45
+ cli-validator brain prepare /absolute/trusted-workspace report-response.json > prepared-runner.json
46
+ ```
47
+
48
+ The trusted operator supplies the existing external, mode-0600 runner approval and signer configuration. The approval binds the exact `planSha256` printed by preparation. The runner input contains `reportResponse`, `approvalPath`, and `signerConfigPath`.
49
+
50
+ ```sh
51
+ cli-validator brain run /absolute/trusted-workspace brain-runner-input.json > brain-validation.json
52
+ cli-aimlock brain validate /absolute/ide-workspace brain-validation.json
53
+ ```
54
+
55
+ Run this on a dedicated isolated runner, never on the production application host. Brain verification requires a privileged supervisor and executes the frozen checks as UID/GID 65534. The signing key, approved Node executable and runner controls remain protected and root owned. Frozen control files must be regular mode-0600 files; control directories must not be writable by the check process. Provide writable build-output directories separately when a check needs them. A Linux container supervisor needs SETUID, SETGID and KILL capabilities for identity separation and process-group cleanup. Do not mount a host Docker socket or production secrets.
56
+
57
+ The fixed adapter executes every frozen check with `shell: false` and an explicit PATH. Its source and complete check manifest are included in the signed file manifest. The supervisor bounds the whole run to five minutes and one MiB of output. A failed, timed-out, modified or incomplete run cannot become `verified`.
58
+
59
+ The API accepts `planId`, `reportDigest`, `executionPlan`, the runner's real `subject`, and `receipts`. It reconstructs and compares the member, plan, report, complete checks, files, adapter, frozen contract and policy before validating the signature. The server uses only the configured `CLITAX_VALIDATOR_RECEIPT_PUBLIC_KEY`, an Ed25519 SPKI DER public key encoded as base64. Test fixture keys must never be added to production trust.
60
+
61
+ ## Isolated integration verification
62
+
63
+ The normal server test suite includes protocol, substitution, signature and unprivileged-runner rejection checks. The separate integration test runs real commands in a disposable root-to-unprivileged container:
64
+
65
+ ```sh
66
+ docker run --rm --network none --read-only --cap-drop ALL \
67
+ --cap-add SETUID --cap-add SETGID --cap-add KILL \
68
+ --security-opt no-new-privileges --pids-limit 64 --memory 256m --cpus 1 \
69
+ --tmpfs /tmp:rw,nosuid,size=64m \
70
+ --mount type=bind,source=/absolute/source,target=/code,readonly \
71
+ --workdir /code node:24-alpine \
72
+ node --test scripts/brain-validator-isolated-integration.mjs
73
+ ```
74
+
75
+ Use a source fixture containing only the needed first-party modules, not a directory containing environment files or credentials. The fixture generates a temporary signing key inside the disposable container and verifies that the unprivileged checks cannot read it.
@@ -0,0 +1,9 @@
1
+ export const BRAIN_RUNNER_ADAPTER_SOURCE: string
2
+ export const BRAIN_RUNNER_ADAPTER_PATH: string
3
+ export function brainRunnerContext(userId: string, plan: unknown, report: unknown,
4
+ frozenAt: string, reportDigest: string, reportedAt: string): Record<string, unknown>
5
+ export function assertBrainRunnerSubmission(context: unknown, executionPlan: unknown,
6
+ subject: unknown): Record<string, unknown>
7
+ export function buildBrainRunnerPlan(context: unknown, engine: unknown): Record<string, unknown>
8
+ export function canonicalBrainRunnerJson(value: unknown): string
9
+ export function brainRunnerBytesSha256(bytes: string | Buffer): string
@@ -0,0 +1,151 @@
1
+ import { createHash } from 'node:crypto'
2
+ import { basename, isAbsolute } from 'node:path'
3
+ import { validatorArtifactSubject, validatorReceiptSubject } from './validator-runtime.mjs'
4
+
5
+ export const BRAIN_RUNNER_CONTEXT_SCHEMA = 'brain.runner-context/1.0'
6
+ export const BRAIN_RUNNER_CONTRACT_SCHEMA = 'brain.frozen-runner/1.0'
7
+ export const BRAIN_RUNNER_ADAPTER_PATH = '.aimlock/brain-validation/check-runner.mjs'
8
+ export const BRAIN_RUNNER_MAX_TIMEOUT_MS = 300_000
9
+ export const BRAIN_RUNNER_MAX_OUTPUT_BYTES = 1_048_576
10
+ const BRAIN_RUNNER_OVERHEAD_MS = 5_000
11
+ const SHA256 = /^[0-9a-f]{64}$/
12
+
13
+ export const BRAIN_RUNNER_ADAPTER_SOURCE = String.raw`import { readFile } from 'node:fs/promises'
14
+ import { spawn } from 'node:child_process'
15
+ import { resolve } from 'node:path'
16
+
17
+ if (typeof process.getuid !== 'function' || process.getuid() !== 0) throw new Error('Trusted Brain checks require an isolated root runner with a separate unprivileged UID')
18
+ const path = process.argv[2]
19
+ if (process.argv.length !== 3 || typeof path !== 'string') throw new Error('A frozen checks manifest is required')
20
+ const manifest = JSON.parse(await readFile(resolve(path), 'utf8'))
21
+ if (manifest.schemaVersion !== 'brain.trusted-checks/1.0' || !Array.isArray(manifest.checks)
22
+ || manifest.checks.length === 0 || manifest.checks.length > 16) throw new Error('Invalid frozen checks manifest')
23
+ if (typeof process.env.PATH !== 'string') throw new Error('An explicit executable search path is required')
24
+
25
+ function executeCheck(check) {
26
+ if (typeof check.id !== 'string' || typeof check.executable !== 'string'
27
+ || !Array.isArray(check.args) || check.args.some((arg) => typeof arg !== 'string')
28
+ || !Number.isSafeInteger(check.timeoutMs) || check.timeoutMs < 1) throw new Error('Invalid frozen check')
29
+ return new Promise((resolveCheck, reject) => {
30
+ const child = spawn(check.executable, check.args, {
31
+ cwd: process.cwd(), shell: false, detached: false, uid: 65534, gid: 65534,
32
+ env: { PATH: process.env.PATH }, stdio: ['ignore', 'inherit', 'inherit'],
33
+ })
34
+ let timedOut = false
35
+ const timer = setTimeout(() => {
36
+ timedOut = true
37
+ process.kill(-process.pid, 'SIGKILL')
38
+ }, check.timeoutMs)
39
+ child.once('error', (error) => { clearTimeout(timer); reject(error) })
40
+ child.once('close', (exitCode, signal) => {
41
+ clearTimeout(timer)
42
+ resolveCheck({ checkId: check.id, exitCode, signal, timedOut })
43
+ })
44
+ })
45
+ }
46
+
47
+ const results = []
48
+ for (const check of manifest.checks) results.push(await executeCheck(check))
49
+ const complete = new Set(results.map((result) => result.checkId)).size === manifest.checks.length
50
+ const passed = complete && results.every((result) => result.exitCode === 0 && result.signal === null && !result.timedOut)
51
+ process.stdout.write(JSON.stringify({ schemaVersion: 'brain.trusted-check-results/1.0', results, passed }) + '\n')
52
+ process.exitCode = passed ? 0 : 1
53
+ `
54
+
55
+ export function canonicalBrainRunnerJson(value) {
56
+ if (value === null || typeof value === 'string' || typeof value === 'boolean') return JSON.stringify(value)
57
+ if (typeof value === 'number' && Number.isFinite(value)) return JSON.stringify(value)
58
+ if (Array.isArray(value)) return '[' + value.map(canonicalBrainRunnerJson).join(',') + ']'
59
+ if (!value || typeof value !== 'object') throw new Error('Brain runner value must be JSON')
60
+ return '{' + Object.keys(value).sort().map((key) => JSON.stringify(key) + ':' + canonicalBrainRunnerJson(value[key])).join(',') + '}'
61
+ }
62
+
63
+ export function brainRunnerBytesSha256(bytes) {
64
+ return createHash('sha256').update(bytes).digest('hex')
65
+ }
66
+
67
+ export function brainRunnerContext(userId, plan, report, frozenAt, reportDigest, reportedAt) {
68
+ const files = [...report.files].sort((left, right) => left.path < right.path ? -1 : 1)
69
+ if (files.some((file) => file.path.startsWith('.aimlock/brain-validation/'))) throw new Error('Brain runner control paths are reserved')
70
+ return {
71
+ schemaVersion: BRAIN_RUNNER_CONTEXT_SCHEMA, memberId: brainRunnerBytesSha256(userId).slice(0, 32),
72
+ chainId: 'chn-' + report.reportId, validationRunId: 'brain-' + report.reportId,
73
+ planId: plan.planId, reportId: report.reportId, planDigest: validatorReceiptSubject(plan), reportDigest,
74
+ checks: plan.checks, files, frozenAt, reportedAt,
75
+ scopeContractSha256: validatorReceiptSubject(plan.contract), snapshotSha256: validatorReceiptSubject(plan.targets),
76
+ }
77
+ }
78
+
79
+ export function brainRunnerManifest(context) {
80
+ return {
81
+ schemaVersion: 'brain.trusted-checks/1.0', planId: context.planId, reportId: context.reportId,
82
+ planDigest: context.planDigest, reportDigest: context.reportDigest, checks: context.checks,
83
+ }
84
+ }
85
+
86
+ export function brainRunnerControlFiles(context) {
87
+ return [
88
+ { path: BRAIN_RUNNER_ADAPTER_PATH, content: BRAIN_RUNNER_ADAPTER_SOURCE },
89
+ { path: '.aimlock/brain-validation/' + context.reportId + '/checks.json', content: canonicalBrainRunnerJson(brainRunnerManifest(context)) },
90
+ ]
91
+ }
92
+
93
+ export function buildBrainRunnerPlan(context, engine) {
94
+ if (context.schemaVersion !== BRAIN_RUNNER_CONTEXT_SCHEMA) throw new Error('A server frozen Brain runner context is required')
95
+ if (typeof engine.executable !== 'string' || !isAbsolute(engine.executable) || basename(engine.executable) !== 'node'
96
+ || !SHA256.test(engine.executableSha256) || typeof engine.path !== 'string' || engine.path.length === 0) {
97
+ throw new Error('An approved Node executable, hash, and explicit PATH are required')
98
+ }
99
+ const controls = brainRunnerControlFiles(context)
100
+ const manifestPath = controls[1].path
101
+ const files = [...context.files, ...controls.map((file) => ({ path: file.path, sha256: brainRunnerBytesSha256(file.content) }))]
102
+ .sort((left, right) => left.path < right.path ? -1 : 1)
103
+ const tests = [{ testId: 'brain-all-checks', path: manifestPath }]
104
+ return {
105
+ schemaVersion: 'validator.execution-plan/1.0', frozen: true, planId: context.planId,
106
+ validationRunId: context.validationRunId, memberId: context.memberId, chainId: context.chainId,
107
+ artifactSha256: validatorArtifactSubject(files), files, tests,
108
+ policy: { executable: engine.executable, executableSha256: engine.executableSha256,
109
+ args: [BRAIN_RUNNER_ADAPTER_PATH, manifestPath], environment: { PATH: engine.path },
110
+ timeoutMs: Math.min(BRAIN_RUNNER_MAX_TIMEOUT_MS,
111
+ context.checks.reduce((total, check) => total + check.timeoutMs, BRAIN_RUNNER_OVERHEAD_MS)),
112
+ maxOutputBytes: BRAIN_RUNNER_MAX_OUTPUT_BYTES, requiredExitCode: 0 },
113
+ goldenBaseline: { schemaVersion: 'validator.golden-baseline/1.0', baselineId: 'brain-' + context.planId,
114
+ source: { kind: 'approved-record', locator: 'brain-plan:' + context.planId, digestSha256: context.planDigest },
115
+ version: '1.0', frozen: true, frozenAt: context.frozenAt, frozenBy: 'brain-planning-server',
116
+ testsSha256: validatorReceiptSubject(tests) },
117
+ contracts: { brain: { schemaVersion: BRAIN_RUNNER_CONTRACT_SCHEMA,
118
+ planId: context.planId, planSha256: context.planDigest, reportId: context.reportId, reportSha256: context.reportDigest,
119
+ checksSha256: validatorReceiptSubject(context.checks), scopeContractSha256: context.scopeContractSha256,
120
+ snapshotSha256: context.snapshotSha256, adapterSha256: brainRunnerBytesSha256(BRAIN_RUNNER_ADAPTER_SOURCE) } },
121
+ }
122
+ }
123
+
124
+ export function assertBrainRunnerSubmission(context, executionPlan, subject) {
125
+ if (!executionPlan || typeof executionPlan !== 'object' || !executionPlan.policy || typeof executionPlan.policy !== 'object') {
126
+ throw new Error('A frozen runner execution plan is required')
127
+ }
128
+ const policy = executionPlan.policy
129
+ const expected = buildBrainRunnerPlan(context, {
130
+ executable: policy.executable, executableSha256: policy.executableSha256, path: policy.environment?.PATH,
131
+ })
132
+ if (canonicalBrainRunnerJson(expected) !== canonicalBrainRunnerJson(executionPlan)) {
133
+ throw new Error('Runner plan does not execute the complete frozen Brain checks')
134
+ }
135
+ if (!subject || typeof subject !== 'object' || typeof subject.executedAt !== 'string'
136
+ || !Number.isFinite(Date.parse(subject.executedAt)) || Date.parse(subject.executedAt) < Date.parse(context.reportedAt)) {
137
+ throw new Error('Runner execution must follow the frozen Brain report')
138
+ }
139
+ const expectedSubject = {
140
+ schemaVersion: 'validator.validation-subject/1.0', artifactSha256: expected.artifactSha256,
141
+ memberId: expected.memberId, chainId: expected.chainId, executedAt: subject.executedAt, files: expected.files,
142
+ validationRunId: expected.validationRunId, planId: expected.planId, tests: expected.tests,
143
+ policy: { command: [policy.executable, ...policy.args].map((part) => JSON.stringify(part)).join(' '),
144
+ requiredExitCode: 0, executionPlanSha256: brainRunnerBytesSha256(canonicalBrainRunnerJson(expected)) },
145
+ goldenBaseline: expected.goldenBaseline, contracts: expected.contracts,
146
+ }
147
+ if (canonicalBrainRunnerJson(expectedSubject) !== canonicalBrainRunnerJson(subject)) {
148
+ throw new Error('Signed runner subject does not match the member, plan, report, artifact, and checks')
149
+ }
150
+ return expectedSubject
151
+ }
@@ -0,0 +1,92 @@
1
+ import { constants } from 'node:fs'
2
+ import { lstat, mkdir, open, readFile, realpath } from 'node:fs/promises'
3
+ import { resolve } from 'node:path'
4
+ import { readRegularFile } from './validator-runner-plan.mjs'
5
+ import { runApprovedValidatorPlan } from './validator-local-runner.mjs'
6
+ import {
7
+ assertBrainRunnerSubmission, brainRunnerBytesSha256, brainRunnerControlFiles,
8
+ buildBrainRunnerPlan, canonicalBrainRunnerJson,
9
+ } from './brain-validator-contract.mjs'
10
+
11
+ async function checkedProjectPath(root, relativePath, createParents) {
12
+ if (typeof relativePath !== 'string' || relativePath.startsWith('/') || relativePath.includes('\\')
13
+ || relativePath.split('/').some((part) => !part || part === '.' || part === '..')) throw new Error('Invalid runner project path')
14
+ let directory = root
15
+ const parts = relativePath.split('/')
16
+ for (const part of parts.slice(0, -1)) {
17
+ directory = resolve(directory, part)
18
+ if (createParents) {
19
+ try { await mkdir(directory, { mode: 0o700 }) }
20
+ catch (error) { if (!error || typeof error !== 'object' || error.code !== 'EEXIST') throw error }
21
+ }
22
+ const status = await lstat(directory)
23
+ if (!status.isDirectory() || status.isSymbolicLink()) throw new Error('Runner paths cannot traverse symlinks')
24
+ }
25
+ return resolve(directory, parts[parts.length - 1])
26
+ }
27
+
28
+ async function writeFrozenControl(root, relativePath, content) {
29
+ const path = await checkedProjectPath(root, relativePath, true)
30
+ let handle
31
+ try { handle = await open(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600) }
32
+ catch (error) {
33
+ if (!error || typeof error !== 'object' || error.code !== 'EEXIST') throw error
34
+ const existing = await readRegularFile(path, 1_048_576, true)
35
+ if (existing.sha256 !== brainRunnerBytesSha256(content)) throw new Error('A frozen runner control file already has different content')
36
+ return path
37
+ }
38
+ try { await handle.writeFile(content) } finally { await handle.close() }
39
+ return path
40
+ }
41
+
42
+ export async function prepareBrainValidation(repositoryRoot, reportResponse, environment = process.env) {
43
+ if (reportResponse.status !== 'reported' || !reportResponse.report || reportResponse.report.checksPassed !== true
44
+ || !reportResponse.report.validationContext) throw new Error('A complete successful Brain report is required')
45
+ if (typeof environment.PATH !== 'string' || !environment.PATH) throw new Error('An explicit PATH is required')
46
+ const rootStatus = await lstat(repositoryRoot)
47
+ if (!rootStatus.isDirectory() || rootStatus.isSymbolicLink()) throw new Error('Runner workspace must be a real directory')
48
+ const root = await realpath(repositoryRoot)
49
+ const context = reportResponse.report.validationContext
50
+ if (context.planId !== reportResponse.planId || context.reportDigest !== reportResponse.reportDigest) {
51
+ throw new Error('Brain report identity does not match its runner context')
52
+ }
53
+ for (const file of context.files) {
54
+ const actual = await readRegularFile(await checkedProjectPath(root, file.path, false), 16_777_216, false)
55
+ if (actual.sha256 !== file.sha256) throw new Error('Reported project file changed before validation: ' + file.path)
56
+ }
57
+ for (const file of brainRunnerControlFiles(context)) await writeFrozenControl(root, file.path, file.content)
58
+ const executable = await realpath(process.execPath)
59
+ const executionPlan = buildBrainRunnerPlan(context, {
60
+ executable, executableSha256: brainRunnerBytesSha256(await readFile(executable)), path: environment.PATH,
61
+ })
62
+ const planPath = '.aimlock/brain-validation/' + context.reportId + '/execution-plan.json'
63
+ const planContent = canonicalBrainRunnerJson(executionPlan)
64
+ await writeFrozenControl(root, planPath, planContent)
65
+ return { repositoryRoot: root, planPath, executionPlan, planSha256: brainRunnerBytesSha256(planContent), context }
66
+ }
67
+
68
+ export async function runBrainApprovedValidation(repositoryRoot, input, environment = process.env) {
69
+ if (typeof input.approvalPath !== 'string' || typeof input.signerConfigPath !== 'string') {
70
+ throw new Error('An external approved runner authority and signer configuration are required')
71
+ }
72
+ const prepared = await prepareBrainValidation(repositoryRoot, input.reportResponse, environment)
73
+ const execution = await runApprovedValidatorPlan({
74
+ repositoryRoot: prepared.repositoryRoot, planPath: prepared.planPath,
75
+ approvalPath: input.approvalPath, signerConfigPath: input.signerConfigPath,
76
+ })
77
+ if (execution.status !== 'succeeded' || !execution.receipt) throw new Error('All Brain checks require a successful trusted runner receipt')
78
+ const subject = assertBrainRunnerSubmission(prepared.context, prepared.executionPlan, execution.subject)
79
+ return { planId: prepared.context.planId, reportDigest: prepared.context.reportDigest,
80
+ executionPlan: prepared.executionPlan, subject, receipts: [execution.receipt] }
81
+ }
82
+
83
+ export async function runBrainValidatorCli(args) {
84
+ if (args.length !== 3 || !['prepare', 'run'].includes(args[0])) {
85
+ throw new Error('Usage: cli-validator brain <prepare|run> <repositoryRoot> <inputJsonFile>')
86
+ }
87
+ const inputFile = await readRegularFile(resolve(args[2]), 256_000, false)
88
+ const input = JSON.parse(inputFile.bytes.toString('utf8'))
89
+ const output = args[0] === 'prepare' ? await prepareBrainValidation(resolve(args[1]), input)
90
+ : await runBrainApprovedValidation(resolve(args[1]), input)
91
+ process.stdout.write(JSON.stringify(output, null, 2) + '\n')
92
+ }
package/cli.mjs CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { runBrainValidatorCli } from './brain-validator-plan.mjs'
2
3
  import { dirname } from 'node:path'
3
4
  import { fileURLToPath } from 'node:url'
4
5
  import { dispatchOfficialSkillCli, runIntakeHandshake } from './installer.mjs'
@@ -11,10 +12,11 @@ const INTAKE_QUESTIONS = [
11
12
  { id: 'targetFiles', prompt: 'Files to validate (or "auto" to scan all).', required: false, example: 'auto' },
12
13
  ]
13
14
 
14
- if (process.argv[2] === 'local') await runValidatorLocalCli(process.argv.slice(3))
15
+ if (process.argv[2] === 'brain') await runBrainValidatorCli(process.argv.slice(3))
16
+ else if (process.argv[2] === 'local') await runValidatorLocalCli(process.argv.slice(3))
15
17
  else await dispatchOfficialSkillCli({
16
18
  packageRoot: dirname(fileURLToPath(import.meta.url)),
17
- extraUsageLines: [' cli-validator local capabilities', ' cli-validator local run-approved-plan < runner-input.json'],
19
+ extraUsageLines: [' cli-validator brain prepare <repositoryRoot> <reportResponse.json>', ' cli-validator brain run <repositoryRoot> <runnerInput.json>', ' cli-validator local capabilities', ' cli-validator local run-approved-plan < runner-input.json'],
18
20
  runCommand: (context) => runIntakeHandshake(context, {
19
21
  questions: INTAKE_QUESTIONS,
20
22
  outputFile: 'VALIDATOR-REQUIREMENTS.json',
package/package.json CHANGED
@@ -6,7 +6,9 @@
6
6
  "exports": {
7
7
  "./local-runner": "./validator-local-runner.mjs",
8
8
  "./runner-plan": "./validator-runner-plan.mjs",
9
- "./runtime": "./validator-runtime.mjs"
9
+ "./runtime": "./validator-runtime.mjs",
10
+ "./brain-contract": "./brain-validator-contract.mjs",
11
+ "./brain-runner": "./brain-validator-plan.mjs"
10
12
  },
11
13
  "files": [
12
14
  "cli.mjs",
@@ -19,7 +21,11 @@
19
21
  "validator-local-runner.mjs",
20
22
  "validator-local-cli.mjs",
21
23
  "validator-runtime.mjs",
22
- "validator-compliance.mjs"
24
+ "validator-compliance.mjs",
25
+ "brain-validator-contract.mjs",
26
+ "brain-validator-contract.d.mts",
27
+ "brain-validator-plan.mjs",
28
+ "validator-brain-contract.mjs"
23
29
  ],
24
30
  "license": "UNLICENSED",
25
31
  "name": "cli-validator",
@@ -28,5 +34,5 @@
28
34
  "url": "https://github.com/88208555/Validator-clitax.git"
29
35
  },
30
36
  "type": "module",
31
- "version": "7.0.34"
37
+ "version": "7.0.36"
32
38
  }
package/skill/SKILL.md CHANGED
@@ -5,7 +5,7 @@ description: '交付前质量门禁:三道防线(静态/动态/对抗)递
5
5
 
6
6
  # Validator
7
7
 
8
- Package version: v7.0.34
8
+ Package version: v7.0.36
9
9
 
10
10
  Validator 是技能链最后一站,只消费冻结目标和真实执行证据;模型解释没有裁判权。
11
11
 
package/skill/skill.json CHANGED
@@ -5,6 +5,6 @@
5
5
  "method": "POST",
6
6
  "name": "validator",
7
7
  "type": "Skill",
8
- "version": "v7.0.34",
8
+ "version": "v7.0.36",
9
9
  "schemaVersion": "validator.skill.request/1.0"
10
10
  }
@@ -0,0 +1,47 @@
1
+ const SHA_PATTERN = '^[0-9a-f]{64}$'
2
+ const UUID_PATTERN = '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'
3
+ const SHA = new RegExp(SHA_PATTERN)
4
+ const UUID = new RegExp(UUID_PATTERN)
5
+ const FIELDS = ['schemaVersion', 'planId', 'planSha256', 'reportId', 'reportSha256',
6
+ 'checksSha256', 'scopeContractSha256', 'snapshotSha256', 'adapterSha256']
7
+
8
+ export const brainRunnerContractSchema = {
9
+ type: 'object', additionalProperties: false, required: FIELDS,
10
+ properties: {
11
+ schemaVersion: { const: 'brain.frozen-runner/1.0' },
12
+ planId: { type: 'string', pattern: UUID_PATTERN }, reportId: { type: 'string', pattern: UUID_PATTERN },
13
+ ...Object.fromEntries(FIELDS.filter((field) => field.endsWith('Sha256'))
14
+ .map((field) => [field, { type: 'string', pattern: SHA_PATTERN }])),
15
+ },
16
+ }
17
+
18
+ export function validBrainRunnerContract(value, subject) {
19
+ if (!value || typeof value !== 'object' || Array.isArray(value)
20
+ || Object.keys(value).sort().join('|') !== [...FIELDS].sort().join('|')
21
+ || value.schemaVersion !== 'brain.frozen-runner/1.0'
22
+ || !UUID.test(value.planId) || !UUID.test(value.reportId)
23
+ || value.planId !== subject.planId || subject.chainId !== 'chn-' + value.reportId
24
+ || subject.validationRunId !== 'brain-' + value.reportId) return false
25
+ return FIELDS.filter((field) => field.endsWith('Sha256')).every((field) => typeof value[field] === 'string' && SHA.test(value[field]))
26
+ }
27
+
28
+ export function validateBridgeContracts(value, entityRef, hardened, utilities) {
29
+ const { isObj, text, idRegex, shaRegex, finding } = utilities
30
+ const findings = []
31
+ if (hardened && (!isObj(value.contracts) || (!isObj(value.contracts.aimlock)
32
+ && !validBrainRunnerContract(value.contracts.brain, value)))) findings.push(finding("P0", "VALIDATION-SUBJECT-SNAPSHOT", `${entityRef}.contracts`, "Hardened validation requires an Aimlock snapshot or server frozen Brain contract binding"));
33
+ if (value.contracts?.brain !== undefined && !validBrainRunnerContract(value.contracts.brain, value)) findings.push(finding("P0", "VALIDATION-SUBJECT-BRAIN", `${entityRef}.contracts.brain`, "Brain runner contract is invalid or bound to another execution"));
34
+ if (value.contracts !== undefined && (!isObj(value.contracts)
35
+ || (value.contracts.aimlock !== undefined && (!isObj(value.contracts.aimlock)
36
+ || !idRegex.test(text(value.contracts.aimlock.goalId))
37
+ || !shaRegex.test(text(value.contracts.aimlock.scopeContractSha256))
38
+ || !shaRegex.test(text(value.contracts.aimlock.snapshotSha256))))
39
+ || (value.contracts.blueprint !== undefined && (!isObj(value.contracts.blueprint)
40
+ || !idRegex.test(text(value.contracts.blueprint.blueprintId))
41
+ || !shaRegex.test(text(value.contracts.blueprint.acceptanceReportSha256))))
42
+ || (value.contracts.archguard !== undefined && (!isObj(value.contracts.archguard)
43
+ || !shaRegex.test(text(value.contracts.archguard.contractSha256))
44
+ || !shaRegex.test(text(value.contracts.archguard.ledgerSha256))
45
+ || !["green", "yellow", "red"].includes(value.contracts.archguard.driftStatus))))) findings.push(finding("P0", "VALIDATION-SUBJECT-CONTRACTS", `${entityRef}.contracts`, "Aimlock, Blueprint, and ArchGuard bridge contracts require stable ids, SHA-256 digests, and a valid drift status"));
46
+ return findings
47
+ }
@@ -221,7 +221,29 @@ async function signerAuthority(root, signerConfigPath, policy) {
221
221
  return { ...file, config: { ...config, privateKeyPath: privatePath } }
222
222
  }
223
223
 
224
+ async function verifyBrainControlAuthority(authority) {
225
+ if (authority.plan.contracts.brain === undefined) return
226
+ if (process.getuid() !== 0) throw new Error('Brain validation requires an isolated privileged runner')
227
+ const executable = await lstat(authority.plan.policy.executable)
228
+ if (executable.uid !== 0 || (executable.mode & 0o022) !== 0) throw new Error('Brain runner executable must be root owned and protected')
229
+ for (const name of authority.plan.policy.args) {
230
+ const parts = relativeFile(name).split('/')
231
+ let target = authority.root
232
+ for (const [index, part] of ['', ...parts].entries()) {
233
+ if (part) target = resolve(target, part)
234
+ const status = await lstat(target)
235
+ if (status.uid !== 0 || status.isSymbolicLink() || (status.mode & 0o022) !== 0) {
236
+ throw new Error('Brain runner control paths must be root owned and protected')
237
+ }
238
+ if (index === parts.length && (!status.isFile() || status.nlink !== 1 || (status.mode & 0o777) !== 0o600)) {
239
+ throw new Error('Brain runner controls require root owned mode-0600 regular files')
240
+ }
241
+ }
242
+ }
243
+ }
244
+
224
245
  export async function verifyPlanFiles(authority) {
246
+ await verifyBrainControlAuthority(authority)
225
247
  let bytes = 0
226
248
  for (const file of authority.plan.files) {
227
249
  const actual = await projectFile(authority.root, file.path, false)
@@ -1,3 +1,4 @@
1
+ import { brainRunnerContractSchema, validateBridgeContracts } from "./validator-brain-contract.mjs";
1
2
  import { publicBindSites } from "./validator-compliance.mjs";
2
3
  import { createHash, createPublicKey, verify as verifySignature } from "node:crypto";
3
4
 
@@ -5,7 +6,7 @@ const REQ = "validator.skill.request/1.0";
5
6
  const RES = "validator.skill.response/1.0";
6
7
  const ERR = "validator.skill.error/1.0";
7
8
  const NAME = "validator";
8
- const COMPILER_VERSION = "v7.0.34";
9
+ const COMPILER_VERSION = "v7.0.36";
9
10
  const CATALOG_SCHEMA = "cli.tax.skill-catalog/1.0";
10
11
  const RECEIPT_SCHEMA = "validator.execution-receipt/1.0";
11
12
  const VALIDATION_SUBJECT_SCHEMA = "validator.validation-subject/1.0";
@@ -57,6 +58,7 @@ const baselineSchema = objectSchema({
57
58
  testsSha256: stringSchema({ pattern: SHA_PATTERN }),
58
59
  }, ["schemaVersion", "baselineId", "source", "version", "frozen", "frozenAt", "frozenBy", "testsSha256"]);
59
60
  const contractsSchema = objectSchema({
61
+ brain: brainRunnerContractSchema,
60
62
  aimlock: objectSchema({ goalId: stringSchema({ pattern: ID_PATTERN }),
61
63
  scopeContractSha256: stringSchema({ pattern: SHA_PATTERN }), snapshotSha256: stringSchema({ pattern: SHA_PATTERN }) },
62
64
  ["goalId", "scopeContractSha256", "snapshotSha256"]),
@@ -206,19 +208,7 @@ function readValidationSubject(value, entityRef) {
206
208
  && value.artifactSha256 !== validatorArtifactSubject(value.files)) findings.push(finding("P0", "VALIDATION-SUBJECT-ARTIFACT-MANIFEST", `${entityRef}.artifactSha256`, "Artifact digest does not match the file manifest"));
207
209
  }
208
210
  }
209
- if (hardened && (!isObj(value.contracts) || !isObj(value.contracts.aimlock))) findings.push(finding("P0", "VALIDATION-SUBJECT-SNAPSHOT", `${entityRef}.contracts.aimlock`, "Hardened validation requires an Aimlock snapshot binding"));
210
- if (value.contracts !== undefined && (!isObj(value.contracts)
211
- || (value.contracts.aimlock !== undefined && (!isObj(value.contracts.aimlock)
212
- || !idRegex.test(text(value.contracts.aimlock.goalId))
213
- || !shaRegex.test(text(value.contracts.aimlock.scopeContractSha256))
214
- || !shaRegex.test(text(value.contracts.aimlock.snapshotSha256))))
215
- || (value.contracts.blueprint !== undefined && (!isObj(value.contracts.blueprint)
216
- || !idRegex.test(text(value.contracts.blueprint.blueprintId))
217
- || !shaRegex.test(text(value.contracts.blueprint.acceptanceReportSha256))))
218
- || (value.contracts.archguard !== undefined && (!isObj(value.contracts.archguard)
219
- || !shaRegex.test(text(value.contracts.archguard.contractSha256))
220
- || !shaRegex.test(text(value.contracts.archguard.ledgerSha256))
221
- || !["green", "yellow", "red"].includes(value.contracts.archguard.driftStatus))))) findings.push(finding("P0", "VALIDATION-SUBJECT-CONTRACTS", `${entityRef}.contracts`, "Aimlock, Blueprint, and ArchGuard bridge contracts require stable ids, SHA-256 digests, and a valid drift status"));
211
+ findings.push(...validateBridgeContracts(value, entityRef, hardened, { isObj, text, idRegex, shaRegex, finding }));
222
212
  if (Array.isArray(value.tests)) findings.push(...validateGoldenBaseline(value.goldenBaseline, `${entityRef}.goldenBaseline`, value.tests));
223
213
  try { canonicalJson(value); } catch (error) { findings.push(finding("P0", "VALIDATION-SUBJECT-JSON", entityRef, error instanceof Error ? error.message : "Invalid JSON")); }
224
214
  return findings.length ? { findings } : { value };