cli-validator 7.0.34 → 7.0.35
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 +37 -0
- package/brain-validator-contract.d.mts +9 -0
- package/brain-validator-contract.mjs +151 -0
- package/brain-validator-plan.mjs +92 -0
- package/cli.mjs +4 -2
- package/package.json +9 -3
- package/skill/SKILL.md +1 -1
- package/skill/skill.json +1 -1
- package/validator-brain-contract.mjs +47 -0
- package/validator-runner-plan.mjs +22 -0
- package/validator-runtime.mjs +4 -14
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] === '
|
|
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.
|
|
37
|
+
"version": "7.0.35"
|
|
32
38
|
}
|
package/skill/SKILL.md
CHANGED
package/skill/skill.json
CHANGED
|
@@ -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)
|
package/validator-runtime.mjs
CHANGED
|
@@ -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.
|
|
9
|
+
const COMPILER_VERSION = "v7.0.35";
|
|
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
|
-
|
|
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 };
|