cli-validator 7.0.33 → 7.0.34

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
@@ -24,3 +24,15 @@ Source: https://github.com/88208555/Validator-clitax.git
24
24
  Brain Client 服务端在同一次 runtime 请求的事务中绑定真实响应、生成并持久化权威评分与评语,再返回已提交回执。broker 只验证 `feedbackReceiptId`、`feedbackInvocationId` 和权威摘要,不发起第二次评价写入,也不生成分数或评语。`not-reported`、验证不完整、P0/P1 findings、`blocked` 或 `failed` 都不得生成好评;缺凭证、缺回执、摘要不匹配、响应非法或 HTTP 失败都会显式失败。
25
25
 
26
26
  本地 CLI 不提供手工评分或评语提交命令,人类不能选择技能分数或填写技能评价。日常聊天不属于评价协议。
27
+
28
+ ## 本地受限执行器
29
+
30
+ 先运行 `cli-validator local capabilities` 读取输入合同与限额,再以 JSON stdin 调用 `cli-validator local run-approved-plan`。输入必须完整包含 `repositoryRoot`(绝对目录)、`planPath`(工作区相对路径)、`approvalPath`(外部绝对路径)与 `signerConfigPath`(外部绝对路径或显式 null)。
31
+
32
+ 冻结计划采用 `validator.execution-plan/1.0`:frozen=true,planId/validationRunId/memberId/chainId,排序且唯一的 files[{path,sha256}] 与 artifactSha256,tests[{testId,path}],GoldenBaseline 与 Aimlock 合同。policy 明确 executable(绝对真实文件)及 executableSha256、args、environment 字符串字典、timeoutMs、maxOutputBytes、requiredExitCode=0。每个测试文件必须列入 manifest 并作为真实命令参数传入;组合测试可使用受审计的入口脚本,不能把未执行文件假报为测试。执行前后校验产物、计划、可执行文件与外部授权。
33
+
34
+ 外部授权为 `validator.runner-approval/1.0`,包含 repositoryRoot、planSha256(冻结计划文件原始字节 SHA-256)、approvedBy、approvedAt、expiresAt,最长 24 小时。文件必须归执行账户所有、0600、非符号链接且位于被测工作区之外。它记录外部批准;智能体不得伪造批准或用测试 fixture 授权真实工作。
35
+
36
+ 未配置 signer 时只返回 local TestEvidence,独立终审仍为 incomplete。可选 `validator.runner-signer/1.0` 配置包含外置 privateKeyPath、keyId(Ed25519 SPKI DER SHA-256)、receiptTtlMs(最长 10 分钟);配置和私钥同样必须为外部 0600 文件。签名绑定 subject、退出结果与日志指纹,消费方使用已配置公钥核验。密钥配置不是进程隔离:同 UID 子进程可能读取同账户文件,生产可信服务仍须独立 UID/容器及权限隔离;本工具不自动部署隔离、不创建可信密钥,也不改变验证器的信任配置。
37
+
38
+ 本地运行仅支持 POSIX;无 shell、显式子进程环境、受限输出与时限。信号终止保留 exitCode=null,不制造整数退出码或成功 receipt。超时、输出超限、非零退出或运行中完整性变化均失败;原始执行记录与日志摘要可审计。此进程执行器不是 OS 沙箱,不授予任意磁盘或网络访问。
package/cli.mjs CHANGED
@@ -2,6 +2,7 @@
2
2
  import { dirname } from 'node:path'
3
3
  import { fileURLToPath } from 'node:url'
4
4
  import { dispatchOfficialSkillCli, runIntakeHandshake } from './installer.mjs'
5
+ import { runValidatorLocalCli } from './validator-local-cli.mjs'
5
6
 
6
7
  const INTAKE_QUESTIONS = [
7
8
  { id: 'goal', prompt: 'What is being validated? Describe the deliverable and expected behavior.', required: true, example: '电商运营仪表盘计算工具:输入访客数/订单数/GMV/广告费,输出转化率/客单价/ROAS。' },
@@ -10,8 +11,10 @@ const INTAKE_QUESTIONS = [
10
11
  { id: 'targetFiles', prompt: 'Files to validate (or "auto" to scan all).', required: false, example: 'auto' },
11
12
  ]
12
13
 
13
- await dispatchOfficialSkillCli({
14
+ if (process.argv[2] === 'local') await runValidatorLocalCli(process.argv.slice(3))
15
+ else await dispatchOfficialSkillCli({
14
16
  packageRoot: dirname(fileURLToPath(import.meta.url)),
17
+ extraUsageLines: [' cli-validator local capabilities', ' cli-validator local run-approved-plan < runner-input.json'],
15
18
  runCommand: (context) => runIntakeHandshake(context, {
16
19
  questions: INTAKE_QUESTIONS,
17
20
  outputFile: 'VALIDATOR-REQUIREMENTS.json',
package/package.json CHANGED
@@ -3,13 +3,23 @@
3
3
  "cli-validator": "./cli.mjs"
4
4
  },
5
5
  "description": "Validator skill installer for CLI.Tax: delivery quality gate with three defense lines and golden baseline testing.",
6
+ "exports": {
7
+ "./local-runner": "./validator-local-runner.mjs",
8
+ "./runner-plan": "./validator-runner-plan.mjs",
9
+ "./runtime": "./validator-runtime.mjs"
10
+ },
6
11
  "files": [
7
12
  "cli.mjs",
8
13
  "installer.mjs",
9
14
  "broker.mjs",
10
15
  "README.md",
11
16
  "skill/SKILL.md",
12
- "skill/skill.json"
17
+ "skill/skill.json",
18
+ "validator-runner-plan.mjs",
19
+ "validator-local-runner.mjs",
20
+ "validator-local-cli.mjs",
21
+ "validator-runtime.mjs",
22
+ "validator-compliance.mjs"
13
23
  ],
14
24
  "license": "UNLICENSED",
15
25
  "name": "cli-validator",
@@ -18,5 +28,5 @@
18
28
  "url": "https://github.com/88208555/Validator-clitax.git"
19
29
  },
20
30
  "type": "module",
21
- "version": "7.0.33"
31
+ "version": "7.0.34"
22
32
  }
package/skill/SKILL.md CHANGED
@@ -5,7 +5,7 @@ description: '交付前质量门禁:三道防线(静态/动态/对抗)递
5
5
 
6
6
  # Validator
7
7
 
8
- Package version: v7.0.33
8
+ Package version: v7.0.34
9
9
 
10
10
  Validator 是技能链最后一站,只消费冻结目标和真实执行证据;模型解释没有裁判权。
11
11
 
@@ -107,3 +107,15 @@ GoldenBaseline 只有 `frozen: true` 才有效。来源只允许 `repository-com
107
107
  - 本地 CLI 不提供手工评分或评语提交命令,人类不得选择技能分数或填写技能评价;日常聊天不属于评价协议。
108
108
 
109
109
  调用示例:`npx cli-validator@latest invoke <operation> '<JSON对象>'`。IDE 集成可向 `npx cli-validator@latest broker` 的 stdin 发送 `{"operation":"capabilities","input":{}}`。
110
+
111
+ ## 本地受限执行器
112
+
113
+ 先运行 `cli-validator local capabilities` 读取输入合同与限额,再以 JSON stdin 调用 `cli-validator local run-approved-plan`。输入必须完整包含 `repositoryRoot`(绝对目录)、`planPath`(工作区相对路径)、`approvalPath`(外部绝对路径)与 `signerConfigPath`(外部绝对路径或显式 null)。
114
+
115
+ 冻结计划采用 `validator.execution-plan/1.0`:frozen=true,planId/validationRunId/memberId/chainId,排序且唯一的 files[{path,sha256}] 与 artifactSha256,tests[{testId,path}],GoldenBaseline 与 Aimlock 合同。policy 明确 executable(绝对真实文件)及 executableSha256、args、environment 字符串字典、timeoutMs、maxOutputBytes、requiredExitCode=0。每个测试文件必须列入 manifest 并作为真实命令参数传入;组合测试可使用受审计的入口脚本,不能把未执行文件假报为测试。执行前后校验产物、计划、可执行文件与外部授权。
116
+
117
+ 外部授权为 `validator.runner-approval/1.0`,包含 repositoryRoot、planSha256(冻结计划文件原始字节 SHA-256)、approvedBy、approvedAt、expiresAt,最长 24 小时。文件必须归执行账户所有、0600、非符号链接且位于被测工作区之外。它记录外部批准;智能体不得伪造批准或用测试 fixture 授权真实工作。
118
+
119
+ 未配置 signer 时只返回 local TestEvidence,独立终审仍为 incomplete。可选 `validator.runner-signer/1.0` 配置包含外置 privateKeyPath、keyId(Ed25519 SPKI DER SHA-256)、receiptTtlMs(最长 10 分钟);配置和私钥同样必须为外部 0600 文件。签名绑定 subject、退出结果与日志指纹,消费方使用已配置公钥核验。密钥配置不是进程隔离:同 UID 子进程可能读取同账户文件,生产可信服务仍须独立 UID/容器及权限隔离;本工具不自动部署隔离、不创建可信密钥,也不改变验证器的信任配置。
120
+
121
+ 本地运行仅支持 POSIX;无 shell、显式子进程环境、受限输出与时限。信号终止保留 exitCode=null,不制造整数退出码或成功 receipt。超时、输出超限、非零退出或运行中完整性变化均失败;原始执行记录与日志摘要可审计。此进程执行器不是 OS 沙箱,不授予任意磁盘或网络访问。
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.33",
8
+ "version": "v7.0.34",
9
9
  "schemaVersion": "validator.skill.request/1.0"
10
10
  }
@@ -0,0 +1,158 @@
1
+ const PUBLIC_BIND_ADDRESSES = new Set(['0.0.0.0', '::', '[::]', '0:0:0:0:0:0:0:0'])
2
+ const BIND_OPTION = /^(?:host|hostname|bind|bindhost|bindaddress|listenhost|listenaddress)$/i
3
+ const BIND_METHODS = new Set(['listen', 'bind'])
4
+ const SCRIPT_EXTENSION = /\.(?:[cm]?[jt]sx?)$/i
5
+
6
+ function decodeHostLiteral(value) {
7
+ return value.replace(/\\(?:u\{([0-9a-f]+)\}|u([0-9a-f]{4})|x([0-9a-f]{2})|(.))/gi,
8
+ (match, point, unicode, byte, character) => {
9
+ const digits = point ?? unicode ?? byte
10
+ if (digits === undefined) return character
11
+ const code = parseInt(digits, 16)
12
+ return code <= 0x10ffff ? String.fromCodePoint(code) : match
13
+ })
14
+ }
15
+
16
+ function quotedToken(source, start) {
17
+ const quote = source[start]
18
+ let end = start + 1
19
+ while (end < source.length) {
20
+ if (source[end] === '\\') { end += 2; continue }
21
+ if (source[end] === quote) {
22
+ return { kind: 'literal', value: decodeHostLiteral(source.slice(start + 1, end)), start, end: end + 1 }
23
+ }
24
+ end += 1
25
+ }
26
+ return { kind: 'invalid', value: source.slice(start), start, end: source.length }
27
+ }
28
+
29
+ function templateTokens(source, start) {
30
+ const tokens = []
31
+ let index = start + 1
32
+ while (index < source.length) {
33
+ if (source[index] === '\\') { index += 2; continue }
34
+ if (source[index] === '`') {
35
+ if (tokens.length === 0) tokens.push(quotedToken(source, start))
36
+ return { tokens, end: index + 1 }
37
+ }
38
+ if (source[index] === '$' && source[index + 1] === '{') {
39
+ const expression = codeTokens(source, index + 2, true)
40
+ tokens.push(...expression.tokens)
41
+ index = expression.end
42
+ continue
43
+ }
44
+ index += 1
45
+ }
46
+ return { tokens, end: source.length }
47
+ }
48
+
49
+ function codeTokens(source, start = 0, templateExpression = false) {
50
+ const tokens = []
51
+ let depth = 0
52
+ for (let index = start; index < source.length;) {
53
+ const tail = source.slice(index)
54
+ const ignored = tail.match(/^(?:\s+|\/\/[^\n]*|\/\*[\s\S]*?(?:\*\/|$))/)
55
+ if (ignored) { index += ignored[0].length; continue }
56
+ if (source[index] === '`') {
57
+ const template = templateTokens(source, index)
58
+ tokens.push(...template.tokens)
59
+ index = template.end
60
+ continue
61
+ }
62
+ if (['"', "'"].includes(source[index])) {
63
+ const token = quotedToken(source, index)
64
+ tokens.push(token)
65
+ index = token.end
66
+ continue
67
+ }
68
+ if (source[index] === '}' && templateExpression && depth === 0) return { tokens, end: index + 1 }
69
+ if (source[index] === '{') depth += 1
70
+ if (source[index] === '}') depth -= 1
71
+ const word = tail.match(/^[A-Za-z_$][\w$]*/)
72
+ const value = word === null ? source[index] : word[0]
73
+ tokens.push({ kind: word === null ? 'symbol' : 'word', value, start: index, end: index + value.length })
74
+ index += value.length
75
+ }
76
+ return { tokens, end: source.length }
77
+ }
78
+
79
+ function bindHost(token, aliases) {
80
+ if (token === undefined) return false
81
+ return token.kind === 'literal' ? PUBLIC_BIND_ADDRESSES.has(token.value)
82
+ : token.kind === 'word' && aliases.has(token.value)
83
+ }
84
+
85
+ function publicAliases(tokens) {
86
+ const aliases = new Set()
87
+ let changed = true
88
+ while (changed) {
89
+ changed = false
90
+ for (let index = 0; index < tokens.length - 2; index += 1) {
91
+ const token = tokens[index]
92
+ if (token.kind === 'word' && tokens[index + 1].value === '='
93
+ && bindHost(tokens[index + 2], aliases) && !aliases.has(token.value)) {
94
+ aliases.add(token.value)
95
+ changed = true
96
+ }
97
+ }
98
+ }
99
+ return aliases
100
+ }
101
+
102
+ function callArguments(tokens, opening) {
103
+ const args = [[]]
104
+ let depth = 0
105
+ for (let index = opening + 1; index < tokens.length; index += 1) {
106
+ const token = tokens[index]
107
+ if (token.value === ')' && depth === 0) return args
108
+ if (token.value === ',' && depth === 0) { args.push([]); continue }
109
+ args[args.length - 1].push(token)
110
+ if (['(', '[', '{'].includes(token.value)) depth += 1
111
+ if ([')', ']', '}'].includes(token.value)) depth -= 1
112
+ }
113
+ return []
114
+ }
115
+
116
+ function scriptBindSites(source) {
117
+ const { tokens } = codeTokens(source)
118
+ const aliases = publicAliases(tokens)
119
+ const sites = []
120
+ for (let index = 0; index < tokens.length; index += 1) {
121
+ const token = tokens[index]
122
+ const normalized = token.value.replaceAll('_', '').replaceAll('-', '')
123
+ if (BIND_OPTION.test(normalized) && [':', '='].includes(tokens[index + 1]?.value)
124
+ && bindHost(tokens[index + 2], aliases)) {
125
+ sites.push({ offset: token.start, context: 'bind-option', address: tokens[index + 2].value })
126
+ }
127
+ if (!BIND_METHODS.has(token.value)) continue
128
+ let opening = index + 1
129
+ if (tokens[index - 1]?.value === '[' && tokens[opening]?.value === ']') opening += 1
130
+ if (tokens[opening]?.value === '?' && tokens[opening + 1]?.value === '.') opening += 2
131
+ if (tokens[opening]?.value !== '(') continue
132
+ const args = callArguments(tokens, opening)
133
+ const hosts = token.value === 'bind' ? args.slice(0, 2).flat() : args[1]
134
+ if (hosts === undefined) continue
135
+ const publicHost = hosts.find((candidate) => bindHost(candidate, aliases))
136
+ if (publicHost) sites.push({ offset: token.start, context: `${token.value}-argument`, address: publicHost.value })
137
+ }
138
+ return sites
139
+ }
140
+
141
+ function configurationBindSites(source) {
142
+ const sites = []
143
+ const pattern = /(?:\b(?:host|hostname|bind(?:[_-]?(?:host|address))?|listen(?:[_-]?(?:host|address))?)\b["']?\s*(?:[:=]\s*|\s+)|--(?:host|bind|listen)(?:=|\s+))["']?(0\.0\.0\.0|\[::\]|::)(?=["'\s:;,]|$)/gi
144
+ for (const match of source.matchAll(pattern)) {
145
+ const lineStart = source.lastIndexOf('\n', match.index) + 1
146
+ if (/^\s*(?:#|\/\/)/.test(source.slice(lineStart, match.index))) continue
147
+ sites.push({ offset: match.index, context: 'bind-configuration', address: match[1] })
148
+ }
149
+ return sites
150
+ }
151
+
152
+ /** Inspect bind sinks and host configuration; address lists and outbound targets are not listeners. */
153
+ export function publicBindSites(path, source) {
154
+ const sites = SCRIPT_EXTENSION.test(path) ? scriptBindSites(source) : configurationBindSites(source)
155
+ return sites.map((site) => ({
156
+ ...site, line: source.slice(0, site.offset).split('\n').length,
157
+ }))
158
+ }
@@ -0,0 +1,49 @@
1
+ import { stdin, stdout } from 'node:process'
2
+ import { runApprovedValidatorPlan } from './validator-local-runner.mjs'
3
+ import { RUNNER_LIMITS, RUNNER_PLAN_SCHEMA, RUNNER_APPROVAL_SCHEMA, RUNNER_SIGNER_SCHEMA } from './validator-runner-plan.mjs'
4
+
5
+ const CAPABILITIES = Object.freeze({
6
+ schemaVersion: 'validator.local-runner/1.0',
7
+ operations: ['capabilities', 'run-approved-plan'],
8
+ operationSchemas: {
9
+ capabilities: { type: 'object', additionalProperties: false, properties: {} },
10
+ 'run-approved-plan': {
11
+ type: 'object', additionalProperties: false,
12
+ required: ['repositoryRoot', 'planPath', 'approvalPath', 'signerConfigPath'],
13
+ properties: {
14
+ repositoryRoot: { type: 'string', minLength: 1 }, planPath: { type: 'string', minLength: 1 },
15
+ approvalPath: { type: 'string', minLength: 1 }, signerConfigPath: { type: ['string', 'null'] },
16
+ },
17
+ },
18
+ },
19
+ planSchema: RUNNER_PLAN_SCHEMA, approvalSchema: RUNNER_APPROVAL_SCHEMA, signerSchema: RUNNER_SIGNER_SCHEMA,
20
+ limits: RUNNER_LIMITS,
21
+ boundary: 'POSIX subprocess runner; external preapproval required. Signed receipts require separately configured trust and isolation.',
22
+ })
23
+
24
+ async function readInput() {
25
+ const chunks = []
26
+ let bytes = 0
27
+ for await (const chunk of stdin) {
28
+ bytes += Buffer.byteLength(chunk)
29
+ if (bytes > RUNNER_LIMITS.maxControlBytes) throw new Error('Validator runner input exceeds its limit')
30
+ chunks.push(Buffer.from(chunk))
31
+ }
32
+ return JSON.parse(Buffer.concat(chunks).toString('utf8'))
33
+ }
34
+
35
+ export async function runValidatorLocalCli(args) {
36
+ try {
37
+ if (args.length !== 1 || !CAPABILITIES.operations.includes(args[0])) {
38
+ throw new Error('Usage: cli-validator local capabilities | local run-approved-plan < runner-input.json')
39
+ }
40
+ const output = args[0] === 'capabilities' ? CAPABILITIES : await runApprovedValidatorPlan(await readInput())
41
+ stdout.write(JSON.stringify(output) + '\n')
42
+ if (output.status === 'failed') process.exitCode = 1
43
+ } catch (error) {
44
+ const failure = { status: 'failed', error: error instanceof Error ? error.message : String(error) }
45
+ if (error instanceof Error && error.execution) failure.execution = error.execution
46
+ stdout.write(JSON.stringify(failure) + '\n')
47
+ process.exitCode = 1
48
+ }
49
+ }
@@ -0,0 +1,170 @@
1
+ import { spawn } from 'node:child_process'
2
+ import { createHash, randomUUID, sign } from 'node:crypto'
3
+ import { performance } from 'node:perf_hooks'
4
+ import {
5
+ bytesSha256, loadApprovedExecutionPlan, loadApprovedSigner, validationSubjectForPlan, verifyPlanFiles,
6
+ } from './validator-runner-plan.mjs'
7
+ import { validatorReceiptPayload, validatorReceiptSubject } from './validator-runtime.mjs'
8
+
9
+ const TERMINATION_GRACE_MS = 250
10
+ const REAP_DEADLINE_MS = 1_000
11
+ const EXECUTION_SCHEMA = 'validator.runner-execution/1.0'
12
+ const EVIDENCE_SCHEMA = 'cli.tax.test-evidence/1.0'
13
+ const RECEIPT_SCHEMA = 'validator.execution-receipt/1.0'
14
+
15
+ function signalProcessGroup(child, signal) {
16
+ if (!Number.isInteger(child.pid)) throw new Error('Runner child has no process identifier')
17
+ try {
18
+ process.kill(-child.pid, signal)
19
+ return { signal, status: 'delivered' }
20
+ } catch (error) {
21
+ if (!(error instanceof Error) || !('code' in error) || error.code !== 'ESRCH') throw error
22
+ return { signal, status: 'already-exited' }
23
+ }
24
+ }
25
+
26
+ function executeBoundedCommand(authority) {
27
+ const { policy } = authority.plan
28
+ return new Promise((resolve, reject) => {
29
+ const started = performance.now()
30
+ const child = spawn(policy.executable, policy.args, {
31
+ cwd: authority.root, env: { ...policy.environment },
32
+ shell: false, detached: true, stdio: ['ignore', 'pipe', 'pipe'],
33
+ })
34
+ const termination = []
35
+ const stdout = []
36
+ const stderr = []
37
+ const stdoutHash = createHash('sha256')
38
+ const stderrHash = createHash('sha256')
39
+ let capturedBytes = 0
40
+ let droppedBytes = 0
41
+ let timedOut = false
42
+ let outputLimitExceeded = false
43
+ let terminating = false
44
+ let escalation = null
45
+ let failure = null
46
+ const terminate = () => {
47
+ if (terminating) return
48
+ terminating = true
49
+ try {
50
+ termination.push(signalProcessGroup(child, 'SIGTERM'))
51
+ escalation = setTimeout(() => {
52
+ try { termination.push(signalProcessGroup(child, 'SIGKILL')) } catch (error) { failure = error }
53
+ }, TERMINATION_GRACE_MS)
54
+ } catch (error) {
55
+ failure = error
56
+ }
57
+ }
58
+ const timer = setTimeout(() => { timedOut = true; terminate() }, policy.timeoutMs)
59
+ const hardLimit = setTimeout(() => {
60
+ clearTimeout(timer)
61
+ if (escalation !== null) clearTimeout(escalation)
62
+ try { if (Number.isInteger(child.pid)) termination.push(signalProcessGroup(child, 'SIGKILL')) } catch (error) { failure = error }
63
+ child.stdout.destroy()
64
+ child.stderr.destroy()
65
+ child.unref()
66
+ reject(new Error('Validator process could not be reaped within its hard deadline', { cause: failure }))
67
+ }, policy.timeoutMs + TERMINATION_GRACE_MS + REAP_DEADLINE_MS)
68
+ const consume = (target, hash, chunk) => {
69
+ const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
70
+ hash.update(bytes)
71
+ const remaining = policy.maxOutputBytes - capturedBytes
72
+ const retained = Math.min(bytes.length, remaining)
73
+ if (retained > 0) target.push(Buffer.from(bytes.subarray(0, retained)))
74
+ capturedBytes += retained
75
+ droppedBytes += bytes.length - retained
76
+ if (droppedBytes > 0) { outputLimitExceeded = true; terminate() }
77
+ }
78
+ child.stdout.on('data', (chunk) => consume(stdout, stdoutHash, chunk))
79
+ child.stderr.on('data', (chunk) => consume(stderr, stderrHash, chunk))
80
+ child.once('error', (error) => { failure = error })
81
+ child.once('close', (exitCode, signal) => {
82
+ clearTimeout(timer)
83
+ clearTimeout(hardLimit)
84
+ if (escalation !== null) clearTimeout(escalation)
85
+ try { if (Number.isInteger(child.pid)) termination.push(signalProcessGroup(child, 'SIGKILL')) } catch (error) { failure = error }
86
+ if (failure !== null) return reject(new Error('Validator subprocess lifecycle failed', { cause: failure }))
87
+ resolve({
88
+ exitCode, signal, timedOut, outputLimitExceeded,
89
+ durationMs: Math.max(0, Math.round(performance.now() - started)),
90
+ stdout: Buffer.concat(stdout).toString('utf8'), stderr: Buffer.concat(stderr).toString('utf8'),
91
+ stdoutSha256: stdoutHash.digest('hex'), stderrSha256: stderrHash.digest('hex'),
92
+ capturedBytes, droppedBytes, termination,
93
+ })
94
+ })
95
+ })
96
+ }
97
+
98
+ function executionSummary(result) {
99
+ if (result.timedOut) return 'Approved validation command exceeded its deadline'
100
+ if (result.outputLimitExceeded) return 'Approved validation command exceeded its output limit'
101
+ if (result.signal !== null) return `Approved validation command ended on signal ${result.signal}`
102
+ return `Approved validation command exited with code ${result.exitCode}`
103
+ }
104
+
105
+ export function validatorExecutionLogsDigest(result) {
106
+ return bytesSha256(JSON.stringify({
107
+ stdout: result.stdout, stderr: result.stderr, stdoutSha256: result.stdoutSha256, stderrSha256: result.stderrSha256,
108
+ capturedBytes: result.capturedBytes, droppedBytes: result.droppedBytes,
109
+ }))
110
+ }
111
+
112
+ function receiptSummary(result) {
113
+ return JSON.stringify({
114
+ message: executionSummary(result), logsSha256: validatorExecutionLogsDigest(result),
115
+ stdoutSha256: result.stdoutSha256, stderrSha256: result.stderrSha256,
116
+ capturedBytes: result.capturedBytes, droppedBytes: result.droppedBytes,
117
+ })
118
+ }
119
+
120
+ function localEvidence(subject, result, summary) {
121
+ if (!Number.isInteger(result.exitCode)) return null
122
+ return {
123
+ schemaVersion: EVIDENCE_SCHEMA, evidenceId: `${subject.validationRunId}:process`,
124
+ kind: 'test', runner: 'local', command: subject.policy.command,
125
+ exitCode: result.exitCode, durationMs: result.durationMs, summary,
126
+ artifactSha256: subject.artifactSha256, subject, subjectDigest: validatorReceiptSubject(subject),
127
+ }
128
+ }
129
+
130
+ function signedEvidence(subject, result, summary, signer, passed) {
131
+ const issuedAt = new Date()
132
+ const unsigned = {
133
+ schemaVersion: RECEIPT_SCHEMA, keyId: signer.keyId, nonce: `receipt-${randomUUID()}`,
134
+ subjectDigest: validatorReceiptSubject(subject), issuedAt: issuedAt.toISOString(),
135
+ expiresAt: new Date(issuedAt.getTime() + signer.receiptTtlMs).toISOString(),
136
+ result: { runner: 'trusted-runner', passed, exitCode: result.exitCode,
137
+ durationMs: result.durationMs, summary },
138
+ }
139
+ const receipt = { ...unsigned,
140
+ signature: sign(null, Buffer.from(validatorReceiptPayload(unsigned)), signer.key).toString('base64url') }
141
+ return { ...localEvidence(subject, result, summary), runner: 'trusted-runner', receipt }
142
+ }
143
+
144
+ export async function runApprovedValidatorPlan(input) {
145
+ const authority = await loadApprovedExecutionPlan(input)
146
+ const subject = validationSubjectForPlan(authority.plan, authority.planSha256, new Date().toISOString())
147
+ const result = await executeBoundedCommand(authority)
148
+ try {
149
+ await verifyPlanFiles(authority)
150
+ } catch (error) {
151
+ const failure = new Error('Validator integrity changed during execution', { cause: error })
152
+ failure.execution = { schemaVersion: EXECUTION_SCHEMA, status: 'failed', subject, process: result, evidence: [] }
153
+ throw failure
154
+ }
155
+ const passed = result.exitCode === authority.plan.policy.requiredExitCode
156
+ && result.signal === null && !result.timedOut && !result.outputLimitExceeded
157
+ const summary = receiptSummary(result)
158
+ const signer = Number.isInteger(result.exitCode) ? await loadApprovedSigner(authority) : null
159
+ const evidence = signer === null ? localEvidence(subject, result, summary)
160
+ : signedEvidence(subject, result, summary, signer, passed)
161
+ return {
162
+ schemaVersion: EXECUTION_SCHEMA, status: passed ? 'succeeded' : 'failed',
163
+ planSha256: authority.planSha256,
164
+ approvalSha256: authority.approval.sha256, approvedBy: authority.approval.approval.approvedBy,
165
+ subject, process: result, evidence: evidence === null ? [] : [evidence],
166
+ receipt: evidence !== null && signer !== null ? evidence.receipt : null,
167
+ signingBoundary: signer === null ? 'local-evidence-only'
168
+ : 'configured-signature-only; independent-uid-or-container-isolation-required-for-production-trust',
169
+ }
170
+ }
@@ -0,0 +1,284 @@
1
+ import { constants } from 'node:fs'
2
+ import { lstat, open, realpath } from 'node:fs/promises'
3
+ import { createHash, createPrivateKey, createPublicKey } from 'node:crypto'
4
+ import { isAbsolute, relative, resolve, sep } from 'node:path'
5
+ import {
6
+ run as validateProtocol, validatorArtifactSubject,
7
+ } from './validator-runtime.mjs'
8
+
9
+ export const RUNNER_PLAN_SCHEMA = 'validator.execution-plan/1.0'
10
+ export const RUNNER_APPROVAL_SCHEMA = 'validator.runner-approval/1.0'
11
+ export const RUNNER_SIGNER_SCHEMA = 'validator.runner-signer/1.0'
12
+ export const RUNNER_LIMITS = Object.freeze({
13
+ maxFiles: 512, maxFileBytes: 16 * 1024 * 1024, maxManifestBytes: 64 * 1024 * 1024,
14
+ maxControlBytes: 1024 * 1024, maxExecutableBytes: 256 * 1024 * 1024, maxTimeoutMs: 300_000, maxOutputBytes: 1024 * 1024,
15
+ maxApprovalLifetimeMs: 24 * 60 * 60 * 1000, maxReceiptLifetimeMs: 600_000,
16
+ })
17
+ const SHA = /^[0-9a-f]{64}$/
18
+ const IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/
19
+ const ENVIRONMENT_KEY = /^[A-Za-z_][A-Za-z0-9_]*$/
20
+
21
+ export function bytesSha256(value) {
22
+ return createHash('sha256').update(value).digest('hex')
23
+ }
24
+
25
+ function exactObject(value, fields, label) {
26
+ if (!value || typeof value !== 'object' || Array.isArray(value)
27
+ || Object.keys(value).sort().join('|') !== [...fields].sort().join('|')) {
28
+ throw new Error(`${label} has invalid fields`)
29
+ }
30
+ return value
31
+ }
32
+
33
+ function positiveInteger(value, maximum, label) {
34
+ if (!Number.isSafeInteger(value) || value <= 0 || value > maximum) {
35
+ throw new Error(`${label} must be a positive bounded integer`)
36
+ }
37
+ }
38
+
39
+ function relativeFile(value) {
40
+ if (typeof value !== 'string' || !value || value !== value.normalize('NFC')
41
+ || isAbsolute(value) || /^[A-Za-z]:/.test(value) || value.includes('\\')
42
+ || /[\u0000-\u001f\u007f]/.test(value)
43
+ || value.split('/').some((part) => !part || part === '.' || part === '..')) {
44
+ throw new Error('Manifest paths must be normalized relative files')
45
+ }
46
+ return value
47
+ }
48
+
49
+ function inside(root, target) {
50
+ const path = relative(root, target)
51
+ return path === '' || (!isAbsolute(path) && path !== '..' && !path.startsWith(`..${sep}`))
52
+ }
53
+
54
+ export async function readRegularFile(path, maximum, privateFile) {
55
+ const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW)
56
+ try {
57
+ const before = await handle.stat()
58
+ if (!before.isFile() || before.size > maximum) throw new Error('File is not regular or exceeds its limit')
59
+ if (privateFile && (before.uid !== process.getuid() || (before.mode & 0o777) !== 0o600
60
+ || before.nlink !== 1)) throw new Error('Runner control files must be owned by the runner and mode 0600')
61
+ const bytes = await handle.readFile()
62
+ const after = await handle.stat()
63
+ if (before.size !== after.size || before.mtimeMs !== after.mtimeMs
64
+ || bytes.length !== after.size) throw new Error('File changed while being read')
65
+ return { bytes, byteLength: bytes.length, sha256: bytesSha256(bytes), device: before.dev, inode: before.ino }
66
+ } finally {
67
+ await handle.close()
68
+ }
69
+ }
70
+
71
+ async function hashRegularFile(path, maximum) {
72
+ const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW)
73
+ try {
74
+ const before = await handle.stat()
75
+ if (!before.isFile() || before.size > maximum) throw new Error('Hash target is not a bounded regular file')
76
+ const hash = createHash('sha256')
77
+ const buffer = Buffer.alloc(64 * 1024)
78
+ let byteLength = 0
79
+ while (true) {
80
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, null)
81
+ if (bytesRead === 0) break
82
+ byteLength += bytesRead
83
+ if (byteLength > maximum) throw new Error('Hash target grew beyond its limit')
84
+ hash.update(buffer.subarray(0, bytesRead))
85
+ }
86
+ const after = await handle.stat()
87
+ if (before.size !== after.size || before.mtimeMs !== after.mtimeMs
88
+ || byteLength !== after.size) throw new Error('Hash target changed while being read')
89
+ return { byteLength, sha256: hash.digest('hex'), device: before.dev, inode: before.ino }
90
+ } finally {
91
+ await handle.close()
92
+ }
93
+ }
94
+
95
+ async function externalFile(root, path, maximum) {
96
+ if (typeof path !== 'string' || !isAbsolute(path)) throw new Error('External runner control path must be absolute')
97
+ const status = await lstat(path)
98
+ if (status.isSymbolicLink()) throw new Error('External runner control cannot be a symlink')
99
+ const canonical = await realpath(path)
100
+ if (inside(root, canonical)) throw new Error('Runner controls and keys must be outside the tested workspace')
101
+ return { path: canonical, ...(await readRegularFile(canonical, maximum, true)) }
102
+ }
103
+
104
+ async function projectFile(root, path, contents) {
105
+ const name = relativeFile(path)
106
+ let target = root
107
+ for (const [index, part] of name.split('/').entries()) {
108
+ target = resolve(target, part)
109
+ const status = await lstat(target)
110
+ if (status.isSymbolicLink() || (index < name.split('/').length - 1 && !status.isDirectory())) {
111
+ throw new Error('Workspace files cannot traverse symlinks or non-directory parents')
112
+ }
113
+ }
114
+ if (!inside(root, await realpath(target))) throw new Error('Workspace file escapes its root')
115
+ const data = contents ? await readRegularFile(target, RUNNER_LIMITS.maxFileBytes, false)
116
+ : await hashRegularFile(target, RUNNER_LIMITS.maxFileBytes)
117
+ return { path: target, ...data }
118
+ }
119
+
120
+ function validateExecutionPolicy(policy, tests, files) {
121
+ exactObject(policy, ['executable', 'executableSha256', 'args', 'environment',
122
+ 'timeoutMs', 'maxOutputBytes', 'requiredExitCode'], 'Execution policy')
123
+ if (typeof policy.executable !== 'string' || !isAbsolute(policy.executable)
124
+ || !SHA.test(policy.executableSha256)) throw new Error('Executable requires an absolute path and SHA-256')
125
+ if (!Array.isArray(policy.args) || policy.args.length > 128
126
+ || policy.args.some((value) => typeof value !== 'string' || value.includes('\0') || value.length > 4096)) {
127
+ throw new Error('Executable args must be a bounded string array')
128
+ }
129
+ if (!policy.environment || typeof policy.environment !== 'object' || Array.isArray(policy.environment)
130
+ || Object.keys(policy.environment).length > 64
131
+ || Object.entries(policy.environment).some(([key, value]) => !ENVIRONMENT_KEY.test(key)
132
+ || typeof value !== 'string' || value.includes('\0') || value.length > 16_384)) {
133
+ throw new Error('Child environment must be explicit and bounded')
134
+ }
135
+ positiveInteger(policy.timeoutMs, RUNNER_LIMITS.maxTimeoutMs, 'timeoutMs')
136
+ positiveInteger(policy.maxOutputBytes, RUNNER_LIMITS.maxOutputBytes, 'maxOutputBytes')
137
+ if (policy.requiredExitCode !== 0) throw new Error('Validation requires exit code zero')
138
+ if (!Array.isArray(tests) || !tests.length || tests.length > RUNNER_LIMITS.maxFiles) {
139
+ throw new Error('Frozen tests must be non-empty and bounded')
140
+ }
141
+ for (const item of tests) {
142
+ exactObject(item, ['testId', 'path'], 'Frozen test')
143
+ if (!IDENTIFIER.test(item.testId) || !files.some((file) => file.path === item.path)
144
+ || !policy.args.includes(relativeFile(item.path))) {
145
+ throw new Error('Each test must be in the manifest and passed explicitly as a command argument')
146
+ }
147
+ }
148
+ }
149
+
150
+ function validateFrozenPlan(plan) {
151
+ exactObject(plan, ['schemaVersion', 'frozen', 'planId', 'validationRunId', 'memberId', 'chainId',
152
+ 'artifactSha256', 'files', 'tests', 'policy', 'goldenBaseline', 'contracts'], 'Frozen execution plan')
153
+ if (plan.schemaVersion !== RUNNER_PLAN_SCHEMA || plan.frozen !== true
154
+ || !SHA.test(plan.artifactSha256) || !Array.isArray(plan.files)
155
+ || !plan.files.length || plan.files.length > RUNNER_LIMITS.maxFiles) {
156
+ throw new Error('Frozen execution plan is invalid')
157
+ }
158
+ for (const [index, file] of plan.files.entries()) {
159
+ exactObject(file, ['path', 'sha256'], 'Manifest entry')
160
+ relativeFile(file.path)
161
+ if (!SHA.test(file.sha256) || (index > 0 && plan.files[index - 1].path >= file.path)) {
162
+ throw new Error('Manifest paths must be unique, sorted and SHA-bound')
163
+ }
164
+ }
165
+ if (validatorArtifactSubject(plan.files) !== plan.artifactSha256) throw new Error('Artifact manifest digest mismatch')
166
+ validateExecutionPolicy(plan.policy, plan.tests, plan.files)
167
+ if (plan.contracts?.archguard?.driftStatus === 'red') throw new Error('ArchGuard red blocks execution')
168
+ return plan
169
+ }
170
+
171
+ export function validationSubjectForPlan(plan, planSha256, executedAt) {
172
+ return {
173
+ schemaVersion: 'validator.validation-subject/1.0',
174
+ artifactSha256: plan.artifactSha256, memberId: plan.memberId, chainId: plan.chainId,
175
+ executedAt, files: plan.files, validationRunId: plan.validationRunId, planId: plan.planId,
176
+ tests: plan.tests,
177
+ policy: { command: [plan.policy.executable, ...plan.policy.args].map((value) => JSON.stringify(value)).join(' '),
178
+ requiredExitCode: plan.policy.requiredExitCode, executionPlanSha256: planSha256 },
179
+ goldenBaseline: plan.goldenBaseline, contracts: plan.contracts,
180
+ }
181
+ }
182
+
183
+ async function validateApproval(root, approvalPath, planSha256) {
184
+ const file = await externalFile(root, approvalPath, RUNNER_LIMITS.maxControlBytes)
185
+ const approval = exactObject(JSON.parse(file.bytes.toString('utf8')),
186
+ ['schemaVersion', 'repositoryRoot', 'planSha256', 'approvedBy', 'approvedAt', 'expiresAt'],
187
+ 'Runner approval')
188
+ const approvedAt = Date.parse(approval.approvedAt)
189
+ const expiresAt = Date.parse(approval.expiresAt)
190
+ if (approval.schemaVersion !== RUNNER_APPROVAL_SCHEMA || approval.repositoryRoot !== root
191
+ || approval.planSha256 !== planSha256 || !IDENTIFIER.test(approval.approvedBy)
192
+ || !Number.isFinite(approvedAt) || !Number.isFinite(expiresAt)
193
+ || approvedAt > Date.now() || expiresAt <= Date.now() || expiresAt <= approvedAt
194
+ || expiresAt - approvedAt > RUNNER_LIMITS.maxApprovalLifetimeMs) {
195
+ throw new Error('External approval is expired or does not match the frozen plan')
196
+ }
197
+ return { ...file, approval }
198
+ }
199
+
200
+ async function signerAuthority(root, signerConfigPath, policy) {
201
+ if (signerConfigPath === null) return null
202
+ const file = await externalFile(root, signerConfigPath, RUNNER_LIMITS.maxControlBytes)
203
+ const config = exactObject(JSON.parse(file.bytes.toString('utf8')),
204
+ ['schemaVersion', 'privateKeyPath', 'keyId', 'receiptTtlMs'], 'Runner signer')
205
+ if (config.schemaVersion !== RUNNER_SIGNER_SCHEMA || !SHA.test(config.keyId)) {
206
+ throw new Error('External runner signer configuration is invalid')
207
+ }
208
+ positiveInteger(config.receiptTtlMs, RUNNER_LIMITS.maxReceiptLifetimeMs, 'receiptTtlMs')
209
+ const privatePath = await realpath(config.privateKeyPath)
210
+ if (!isAbsolute(config.privateKeyPath) || inside(root, privatePath)
211
+ || JSON.stringify([policy.args, policy.environment]).includes(config.privateKeyPath)
212
+ || JSON.stringify([policy.args, policy.environment]).includes(privatePath)
213
+ || JSON.stringify([policy.args, policy.environment]).includes(file.path)) {
214
+ throw new Error('Signer paths cannot be inside the workspace or passed to the child')
215
+ }
216
+ const keyStatus = await lstat(config.privateKeyPath)
217
+ if (keyStatus.isSymbolicLink() || !keyStatus.isFile() || keyStatus.uid !== process.getuid()
218
+ || (keyStatus.mode & 0o777) !== 0o600 || keyStatus.nlink !== 1) {
219
+ throw new Error('Runner private key must be a regular owned mode-0600 external file')
220
+ }
221
+ return { ...file, config: { ...config, privateKeyPath: privatePath } }
222
+ }
223
+
224
+ export async function verifyPlanFiles(authority) {
225
+ let bytes = 0
226
+ for (const file of authority.plan.files) {
227
+ const actual = await projectFile(authority.root, file.path, false)
228
+ bytes += actual.byteLength
229
+ if (bytes > RUNNER_LIMITS.maxManifestBytes || actual.sha256 !== file.sha256) {
230
+ throw new Error(`Artifact changed or exceeds its bound: ${file.path}`)
231
+ }
232
+ }
233
+ const executable = await hashRegularFile(authority.plan.policy.executable, RUNNER_LIMITS.maxExecutableBytes)
234
+ if (executable.sha256 !== authority.plan.policy.executableSha256) throw new Error('Executable SHA-256 mismatch')
235
+ const plan = await projectFile(authority.root, authority.planPath, true)
236
+ if (plan.sha256 !== authority.planSha256) throw new Error('Frozen plan changed')
237
+ const approval = await validateApproval(authority.root, authority.approval.path, authority.planSha256)
238
+ if (approval.sha256 !== authority.approval.sha256) throw new Error('External approval changed')
239
+ if (authority.signer !== null) {
240
+ const signer = await externalFile(authority.root, authority.signer.path, RUNNER_LIMITS.maxControlBytes)
241
+ if (signer.sha256 !== authority.signer.sha256) throw new Error('External signer configuration changed')
242
+ }
243
+ }
244
+
245
+ export async function loadApprovedExecutionPlan(input) {
246
+ if (process.platform === 'win32' || typeof process.getuid !== 'function') {
247
+ throw new Error('This bounded subprocess runner requires a POSIX host')
248
+ }
249
+ exactObject(input, ['repositoryRoot', 'planPath', 'approvalPath', 'signerConfigPath'], 'Runner input')
250
+ if (typeof input.repositoryRoot !== 'string' || !isAbsolute(input.repositoryRoot)) {
251
+ throw new Error('repositoryRoot must be absolute')
252
+ }
253
+ const rootStatus = await lstat(input.repositoryRoot)
254
+ if (!rootStatus.isDirectory() || rootStatus.isSymbolicLink()) throw new Error('Workspace root must be a real directory')
255
+ const root = await realpath(input.repositoryRoot)
256
+ const planFile = await projectFile(root, input.planPath, true)
257
+ if (planFile.bytes.length > RUNNER_LIMITS.maxControlBytes) throw new Error('Frozen plan is too large')
258
+ const plan = validateFrozenPlan(JSON.parse(planFile.bytes.toString('utf8')))
259
+ const approval = await validateApproval(root, input.approvalPath, planFile.sha256)
260
+ const signer = await signerAuthority(root, input.signerConfigPath, plan.policy)
261
+ const authority = { root, plan, planPath: input.planPath, planSha256: planFile.sha256, approval, signer }
262
+ await verifyPlanFiles(authority)
263
+ const subject = validationSubjectForPlan(plan, planFile.sha256, new Date().toISOString())
264
+ const validation = await validateProtocol({ schemaVersion: 'validator.skill.request/1.0',
265
+ requestId: 'runner-subject-validation', operation: 'verdict',
266
+ input: { expectedSubject: subject, evidence: [], findings: [] } })
267
+ if (validation.status !== 'succeeded') throw new Error('Frozen validation subject fails the Validator protocol')
268
+ return authority
269
+ }
270
+
271
+ export async function loadApprovedSigner(authority) {
272
+ if (authority.signer === null) return null
273
+ const { config } = authority.signer
274
+ const file = await externalFile(authority.root, config.privateKeyPath, 16_384)
275
+ try {
276
+ const key = createPrivateKey(file.bytes)
277
+ if (key.asymmetricKeyType !== 'ed25519') throw new Error('Runner signer requires Ed25519')
278
+ const publicDer = createPublicKey(key).export({ format: 'der', type: 'spki' })
279
+ if (bytesSha256(publicDer) !== config.keyId) throw new Error('Runner signer public key fingerprint mismatch')
280
+ return { key, keyId: config.keyId, receiptTtlMs: config.receiptTtlMs }
281
+ } finally {
282
+ file.bytes.fill(0)
283
+ }
284
+ }
@@ -0,0 +1,498 @@
1
+ import { publicBindSites } from "./validator-compliance.mjs";
2
+ import { createHash, createPublicKey, verify as verifySignature } from "node:crypto";
3
+
4
+ const REQ = "validator.skill.request/1.0";
5
+ const RES = "validator.skill.response/1.0";
6
+ const ERR = "validator.skill.error/1.0";
7
+ const NAME = "validator";
8
+ const COMPILER_VERSION = "v7.0.34";
9
+ const CATALOG_SCHEMA = "cli.tax.skill-catalog/1.0";
10
+ const RECEIPT_SCHEMA = "validator.execution-receipt/1.0";
11
+ const VALIDATION_SUBJECT_SCHEMA = "validator.validation-subject/1.0";
12
+ const GOLDEN_BASELINE_SCHEMA = "validator.golden-baseline/1.0";
13
+ const TEST_EVIDENCE_SCHEMA = "cli.tax.test-evidence/1.0";
14
+ const RECEIPT_PUBLIC_KEY_ENV = "CLITAX_VALIDATOR_RECEIPT_PUBLIC_KEY";
15
+ const MAX_RECEIPT_LIFETIME_MS = 10 * 60 * 1000;
16
+ const ID_PATTERN = "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$";
17
+ const MEMBER_PATTERN = "^[A-Za-z0-9][A-Za-z0-9_-]{0,31}$";
18
+ const CHAIN_PATTERN = "^chn-[0-9a-f-]{36}$";
19
+ const SHA_PATTERN = "^[0-9a-f]{64}$";
20
+ const idRegex = new RegExp(ID_PATTERN);
21
+ const memberRegex = new RegExp(MEMBER_PATTERN);
22
+ const chainRegex = new RegExp(CHAIN_PATTERN);
23
+ const shaRegex = new RegExp(SHA_PATTERN);
24
+
25
+ const OPS = ["capabilities","help","intake","plan","validate-structure","security-scan","compliance-audit","functional-verify","sandbox-run","fuzz-input","perf-benchmark","intrusive-test","verdict"];
26
+ const PURE = new Set(["capabilities","help","intake","plan","validate-structure","security-scan","compliance-audit","functional-verify","verdict"]);
27
+ const LOCAL_ONLY = new Set(["sandbox-run","fuzz-input","perf-benchmark","intrusive-test"]);
28
+ const CATALOG = OPS.map((operation) => ({ operation, summary: operation }));
29
+
30
+ const stringSchema = (extra = {}) => ({ type: "string", ...extra });
31
+ const arraySchema = (items, extra = {}) => ({ type: "array", items, ...extra });
32
+ const objectSchema = (properties, required = [], extra = {}) => ({
33
+ type: "object", properties, required, additionalProperties: false, ...extra,
34
+ });
35
+ const anyObjectSchema = { type: "object" };
36
+ const findingSchema = objectSchema({
37
+ severity: { enum: ["P0", "P1", "P2"] }, ruleId: stringSchema({ minLength: 1 }),
38
+ entityRef: stringSchema({ minLength: 1 }), message: stringSchema({ minLength: 1 }),
39
+ evidence: anyObjectSchema,
40
+ }, ["severity", "ruleId", "entityRef", "message", "evidence"]);
41
+ const receiptSchema = objectSchema({
42
+ schemaVersion: { const: RECEIPT_SCHEMA }, keyId: stringSchema({ pattern: SHA_PATTERN }),
43
+ nonce: stringSchema({ pattern: ID_PATTERN, minLength: 16, maxLength: 128 }), subjectDigest: stringSchema({ pattern: SHA_PATTERN }),
44
+ issuedAt: stringSchema({ format: "date-time" }), expiresAt: stringSchema({ format: "date-time" }),
45
+ result: objectSchema({ runner: { const: "trusted-runner" }, passed: { type: "boolean" },
46
+ exitCode: { type: "integer" }, durationMs: { type: "number", minimum: 0 },
47
+ summary: stringSchema({ minLength: 1 }) }, ["runner", "passed", "exitCode", "durationMs", "summary"]),
48
+ signature: stringSchema({ minLength: 1 }),
49
+ }, ["schemaVersion", "keyId", "nonce", "subjectDigest", "issuedAt", "expiresAt", "result", "signature"]);
50
+ const baselineSchema = objectSchema({
51
+ schemaVersion: { const: GOLDEN_BASELINE_SCHEMA }, baselineId: stringSchema({ pattern: ID_PATTERN }),
52
+ source: objectSchema({ kind: { enum: ["repository-commit", "artifact", "approved-record"] },
53
+ locator: stringSchema({ minLength: 1 }), digestSha256: stringSchema({ pattern: SHA_PATTERN }) },
54
+ ["kind", "locator", "digestSha256"]),
55
+ version: stringSchema({ pattern: ID_PATTERN }), frozen: { const: true },
56
+ frozenAt: stringSchema({ format: "date-time" }), frozenBy: stringSchema({ pattern: ID_PATTERN }),
57
+ testsSha256: stringSchema({ pattern: SHA_PATTERN }),
58
+ }, ["schemaVersion", "baselineId", "source", "version", "frozen", "frozenAt", "frozenBy", "testsSha256"]);
59
+ const contractsSchema = objectSchema({
60
+ aimlock: objectSchema({ goalId: stringSchema({ pattern: ID_PATTERN }),
61
+ scopeContractSha256: stringSchema({ pattern: SHA_PATTERN }), snapshotSha256: stringSchema({ pattern: SHA_PATTERN }) },
62
+ ["goalId", "scopeContractSha256", "snapshotSha256"]),
63
+ blueprint: objectSchema({ blueprintId: stringSchema({ pattern: ID_PATTERN }),
64
+ acceptanceReportSha256: stringSchema({ pattern: SHA_PATTERN }) }, ["blueprintId", "acceptanceReportSha256"]),
65
+ archguard: objectSchema({ contractSha256: stringSchema({ pattern: SHA_PATTERN }),
66
+ ledgerSha256: stringSchema({ pattern: SHA_PATTERN }),
67
+ driftStatus: { enum: ["green", "yellow", "red"] } },
68
+ ["contractSha256", "ledgerSha256", "driftStatus"]),
69
+ });
70
+ const validationFileSchema = objectSchema({
71
+ path: stringSchema({ minLength: 1, maxLength: 500 }),
72
+ sha256: stringSchema({ pattern: SHA_PATTERN }),
73
+ }, ["path", "sha256"]);
74
+ const subjectSchema = objectSchema({
75
+ schemaVersion: { const: VALIDATION_SUBJECT_SCHEMA }, artifactSha256: stringSchema({ pattern: SHA_PATTERN }),
76
+ memberId: stringSchema({ pattern: MEMBER_PATTERN }), chainId: stringSchema({ pattern: CHAIN_PATTERN }),
77
+ executedAt: stringSchema({ format: "date-time" }), files: arraySchema(validationFileSchema, { minItems: 1 }),
78
+ validationRunId: stringSchema({ pattern: ID_PATTERN }), planId: stringSchema({ pattern: ID_PATTERN }),
79
+ tests: arraySchema(anyObjectSchema, { minItems: 1 }), policy: anyObjectSchema,
80
+ goldenBaseline: baselineSchema, contracts: contractsSchema,
81
+ }, ["schemaVersion", "artifactSha256", "validationRunId", "planId", "tests", "policy", "goldenBaseline"]);
82
+ const testEvidenceProperties = {
83
+ schemaVersion: { const: TEST_EVIDENCE_SCHEMA }, evidenceId: stringSchema({ pattern: ID_PATTERN }),
84
+ kind: { enum: ["test", "build", "lint", "security", "benchmark"] }, command: stringSchema({ minLength: 1 }),
85
+ exitCode: { type: "integer" }, durationMs: { type: "number", minimum: 0 },
86
+ summary: stringSchema({ minLength: 1 }), artifactSha256: stringSchema({ pattern: SHA_PATTERN }),
87
+ subject: subjectSchema, subjectDigest: stringSchema({ pattern: SHA_PATTERN }), receipt: receiptSchema,
88
+ };
89
+ const testEvidenceRequired = ["schemaVersion", "evidenceId", "kind", "runner", "command", "exitCode", "durationMs", "summary"];
90
+ const testEvidenceSchema = {
91
+ oneOf: [
92
+ objectSchema({ ...testEvidenceProperties, runner: { const: "local" } }, testEvidenceRequired),
93
+ objectSchema({ ...testEvidenceProperties, runner: { const: "trusted-runner" } },
94
+ [...testEvidenceRequired, "artifactSha256", "subject", "subjectDigest", "receipt"]),
95
+ ],
96
+ };
97
+ const riskEntrySchema = objectSchema({
98
+ riskId: stringSchema({ pattern: ID_PATTERN }), findingRuleId: stringSchema({ minLength: 1 }),
99
+ findingEntityRef: stringSchema({ minLength: 1 }), owner: stringSchema({ pattern: ID_PATTERN }),
100
+ mitigation: stringSchema({ minLength: 1 }), acceptedBy: stringSchema({ pattern: ID_PATTERN }),
101
+ acceptedAt: stringSchema({ format: "date-time" }),
102
+ }, ["riskId", "findingRuleId", "findingEntityRef", "owner", "mitigation", "acceptedBy", "acceptedAt"]);
103
+ const repairCategorySchema = {
104
+ enum: ["structure-schema", "formula-calculation", "scope-drift", "execution-dispatch", "validator-self"],
105
+ };
106
+ const fileSchema = objectSchema({ path: stringSchema({ minLength: 1 }), content: stringSchema(), schema: anyObjectSchema }, ["path", "content"]);
107
+ const nextSchema = objectSchema({ operation: { type: ["string", "null"] }, instruction: stringSchema() }, ["operation", "instruction"]);
108
+ const responseSchema = (properties, required) => objectSchema({
109
+ schemaVersion: { const: RES }, requestId: stringSchema({ minLength: 1 }), status: { enum: ["succeeded", "blocked", "failed"] }, ...properties,
110
+ }, ["schemaVersion", "requestId", "status", ...required]);
111
+ const operationSchema = (input, inputRequired, output, outputRequired) => ({
112
+ input: objectSchema(input, inputRequired), output: responseSchema(output, outputRequired),
113
+ });
114
+ const SCHEMAS = Object.freeze({
115
+ capabilities: operationSchema({}, [], { capabilities: anyObjectSchema, skill: anyObjectSchema, operationSchemas: anyObjectSchema, nextStep: nextSchema }, ["capabilities", "skill", "operationSchemas", "nextStep"]),
116
+ help: operationSchema({}, [], { help: anyObjectSchema, operationSchemas: anyObjectSchema, nextStep: nextSchema }, ["help", "operationSchemas", "nextStep"]),
117
+ intake: operationSchema({ goal: stringSchema({ minLength: 1 }), riskLevel: { enum: ["low", "medium", "high"] }, complianceReqs: arraySchema(stringSchema()), targetFiles: arraySchema(stringSchema()) }, ["goal", "riskLevel"], { intake: anyObjectSchema, nextStep: nextSchema }, ["intake", "nextStep"]),
118
+ plan: operationSchema({ findings: arraySchema(findingSchema), findingCategories: arraySchema(repairCategorySchema), intakeResult: anyObjectSchema, availableSkills: arraySchema(stringSchema()) }, ["intakeResult"], { plan: anyObjectSchema, nextStep: nextSchema }, ["plan", "nextStep"]),
119
+ "validate-structure": operationSchema({ files: arraySchema(fileSchema, { minItems: 1 }), rules: arraySchema(anyObjectSchema) }, ["files"], { findings: arraySchema(findingSchema), summary: anyObjectSchema, nextStep: nextSchema }, ["findings", "summary", "nextStep"]),
120
+ "security-scan": operationSchema({ files: arraySchema(fileSchema, { minItems: 1 }), rules: arraySchema(anyObjectSchema) }, ["files"], { findings: arraySchema(findingSchema), summary: anyObjectSchema, nextStep: nextSchema }, ["findings", "summary", "nextStep"]),
121
+ "compliance-audit": operationSchema({ files: arraySchema(fileSchema, { minItems: 1 }), template: stringSchema({ minLength: 1 }), requirements: arraySchema(stringSchema()) }, ["files", "template"], { findings: arraySchema(findingSchema), template: stringSchema(), nextStep: nextSchema }, ["findings", "template", "nextStep"]),
122
+ "functional-verify": operationSchema({ validationContext: subjectSchema, receipts: arraySchema(receiptSchema, { minItems: 1 }) }, ["validationContext", "receipts"], { subject: subjectSchema, subjectDigest: stringSchema({ pattern: SHA_PATTERN }), results: arraySchema(anyObjectSchema), summary: anyObjectSchema, findings: arraySchema(findingSchema), evidence: arraySchema(testEvidenceSchema), nextStep: nextSchema }, ["subject", "subjectDigest", "results", "summary", "findings", "evidence", "nextStep"]),
123
+ "sandbox-run": operationSchema({ command: stringSchema({ minLength: 1 }), files: arraySchema(fileSchema), timeout: { type: "number", minimum: 0 }, networkPolicy: { enum: ["block", "allow"] } }, ["command", "networkPolicy"], { sandbox: anyObjectSchema, runner: { const: "local-only" }, evidence: arraySchema(testEvidenceSchema), nextStep: nextSchema }, ["sandbox", "runner", "evidence", "nextStep"]),
124
+ "fuzz-input": operationSchema({ targetFile: stringSchema({ minLength: 1 }), cases: arraySchema(anyObjectSchema), maxCases: { type: "integer", minimum: 1 } }, ["targetFile", "maxCases"], { fuzz: anyObjectSchema, runner: { const: "local-only" }, evidence: arraySchema(testEvidenceSchema), nextStep: nextSchema }, ["fuzz", "runner", "evidence", "nextStep"]),
125
+ "perf-benchmark": operationSchema({ command: stringSchema({ minLength: 1 }), baseline: baselineSchema, threshold: { type: "number", minimum: 0 } }, ["command", "baseline", "threshold"], { benchmark: anyObjectSchema, runner: { const: "local-only" }, evidence: arraySchema(testEvidenceSchema), nextStep: nextSchema }, ["benchmark", "runner", "evidence", "nextStep"]),
126
+ "intrusive-test": operationSchema({ authorization: { type: "boolean" }, tests: arraySchema(anyObjectSchema, { minItems: 1 }), sandbox: { const: true } }, ["authorization", "tests", "sandbox"], { intrusive: anyObjectSchema, runner: { const: "local-only" }, evidence: arraySchema(testEvidenceSchema), nextStep: nextSchema }, ["intrusive", "runner", "evidence", "nextStep"]),
127
+ verdict: operationSchema({ expectedSubject: subjectSchema, validationContext: subjectSchema, findings: arraySchema(findingSchema), evidence: arraySchema(testEvidenceSchema), riskLedger: arraySchema(riskEntrySchema) }, [], { report: anyObjectSchema, findings: arraySchema(findingSchema), evidence: arraySchema(testEvidenceSchema), nextStep: nextSchema }, ["report", "findings", "evidence", "nextStep"]),
128
+ });
129
+
130
+ function text(value) { return String(value ?? ""); }
131
+ function isObj(value) { return value !== null && typeof value === "object" && !Array.isArray(value); }
132
+ function finding(severity, ruleId, entityRef, message, evidence) {
133
+ return { severity, ruleId, entityRef, message, evidence: evidence === undefined ? {} : { example: evidence } };
134
+ }
135
+ function ok(requestId, payload) { return { schemaVersion: RES, requestId, status: "succeeded", ...payload }; }
136
+ function blocked(requestId, findings) { return { schemaVersion: RES, requestId, status: "blocked", validation: { valid: false, guarantee: "blocked", findings } }; }
137
+ function failed(requestId, code, message) { return { schemaVersion: RES, requestId, status: "failed", errorSchema: ERR, error: { code, message } }; }
138
+ function requiredText(input, key) {
139
+ const value = text(input[key]).trim();
140
+ return value ? { value } : { error: finding("P0", "REQUIRED", `input.${key}`, `${key} is required`) };
141
+ }
142
+ function requiredArray(input, key, minimum = 0) {
143
+ if (!Array.isArray(input[key]) || input[key].length < minimum) return { error: finding("P0", "REQUIRED", `input.${key}`, `${key} must contain at least ${minimum} item(s)`) };
144
+ return { value: input[key] };
145
+ }
146
+ function canonicalJson(value) {
147
+ if (value === null || typeof value === "string" || typeof value === "boolean") return JSON.stringify(value);
148
+ if (typeof value === "number") {
149
+ if (!Number.isFinite(value)) throw new TypeError("Evidence must contain finite numbers");
150
+ return JSON.stringify(value);
151
+ }
152
+ if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
153
+ if (!isObj(value)) throw new TypeError("Evidence must be JSON serializable");
154
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(",")}}`;
155
+ }
156
+ export function validatorReceiptSubject(value) { return createHash("sha256").update(canonicalJson(value)).digest("hex"); }
157
+ export function validatorArtifactSubject(files) {
158
+ const manifest = files.map((file) => `${file.path}\0${file.sha256}`).join("\n");
159
+ return createHash("sha256").update(`validator.file-manifest/1.0\n${manifest}`).digest("hex");
160
+ }
161
+ export function validatorReceiptPayload(receipt) {
162
+ if (!isObj(receipt)) throw new TypeError("Execution receipt must be an object");
163
+ const { signature: _signature, ...payload } = receipt;
164
+ return canonicalJson(payload);
165
+ }
166
+
167
+ function validateGoldenBaseline(value, entityRef, tests) {
168
+ const findings = [];
169
+ if (!isObj(value)) return [finding("P0", "GOLDEN-BASELINE-REQUIRED", entityRef, "A frozen golden baseline is required")];
170
+ if (value.schemaVersion !== GOLDEN_BASELINE_SCHEMA) findings.push(finding("P0", "GOLDEN-BASELINE-SCHEMA", `${entityRef}.schemaVersion`, `Expected ${GOLDEN_BASELINE_SCHEMA}`));
171
+ if (!idRegex.test(text(value.baselineId)) || !idRegex.test(text(value.version)) || !idRegex.test(text(value.frozenBy))) findings.push(finding("P0", "GOLDEN-BASELINE-ID", entityRef, "baselineId, version, and frozenBy must be stable identifiers"));
172
+ if (value.frozen !== true || !Number.isFinite(Date.parse(text(value.frozenAt)))) findings.push(finding("P0", "GOLDEN-BASELINE-FROZEN", entityRef, "Baseline must be frozen with a valid timestamp"));
173
+ if (!isObj(value.source) || !["repository-commit", "artifact", "approved-record"].includes(value.source.kind)
174
+ || !text(value.source.locator).trim() || !shaRegex.test(text(value.source.digestSha256))) findings.push(finding("P0", "GOLDEN-BASELINE-SOURCE", `${entityRef}.source`, "Baseline requires traceable source, locator, and SHA-256"));
175
+ if (!shaRegex.test(text(value.testsSha256)) || value.testsSha256 !== validatorReceiptSubject(tests)) findings.push(finding("P0", "GOLDEN-BASELINE-TESTS", `${entityRef}.testsSha256`, "Frozen tests digest does not match subject tests"));
176
+ return findings;
177
+ }
178
+ function readValidationSubject(value, entityRef) {
179
+ if (!isObj(value)) return { findings: [finding("P0", "VALIDATION-SUBJECT-REQUIRED", entityRef, "A validation subject is required")] };
180
+ const findings = [];
181
+ const allowed = new Set(["schemaVersion", "memberId", "chainId", "executedAt", "files", "artifactSha256", "validationRunId", "planId", "tests", "policy", "goldenBaseline", "contracts"]);
182
+ const unknown = Object.keys(value).filter((key) => !allowed.has(key));
183
+ if (unknown.length) findings.push(finding("P0", "VALIDATION-SUBJECT-FIELDS", entityRef, `Unknown fields: ${unknown.join(", ")}`));
184
+ if (value.schemaVersion !== VALIDATION_SUBJECT_SCHEMA) findings.push(finding("P0", "VALIDATION-SUBJECT-SCHEMA", `${entityRef}.schemaVersion`, `Expected ${VALIDATION_SUBJECT_SCHEMA}`));
185
+ if (!shaRegex.test(text(value.artifactSha256))) findings.push(finding("P0", "VALIDATION-SUBJECT-ARTIFACT", `${entityRef}.artifactSha256`, "artifactSha256 must be lowercase SHA-256"));
186
+ for (const key of ["validationRunId", "planId"]) if (!idRegex.test(text(value[key]))) findings.push(finding("P0", "VALIDATION-SUBJECT-ID", `${entityRef}.${key}`, `${key} must be stable`));
187
+ if (!Array.isArray(value.tests) || value.tests.length === 0) findings.push(finding("P0", "VALIDATION-SUBJECT-TESTS", `${entityRef}.tests`, "tests must be non-empty"));
188
+ if (!isObj(value.policy) || !text(value.policy.command).trim() || !Number.isInteger(value.policy.requiredExitCode)) findings.push(finding("P0", "VALIDATION-SUBJECT-POLICY", `${entityRef}.policy`, "policy requires command and requiredExitCode"));
189
+ const hardenedFields = ["memberId", "chainId", "executedAt", "files"];
190
+ const hardened = hardenedFields.some((key) => value[key] !== undefined);
191
+ if (hardened) {
192
+ if (!memberRegex.test(text(value.memberId))) findings.push(finding("P0", "VALIDATION-SUBJECT-MEMBER", `${entityRef}.memberId`, "memberId must be a stable member identifier"));
193
+ if (!chainRegex.test(text(value.chainId))) findings.push(finding("P0", "VALIDATION-SUBJECT-CHAIN", `${entityRef}.chainId`, "chainId must be a valid member chain identifier"));
194
+ if (!Number.isFinite(Date.parse(text(value.executedAt)))) findings.push(finding("P0", "VALIDATION-SUBJECT-EXECUTED-AT", `${entityRef}.executedAt`, "executedAt must be a valid timestamp"));
195
+ if (!Array.isArray(value.files) || value.files.length === 0) findings.push(finding("P0", "VALIDATION-SUBJECT-FILES", `${entityRef}.files`, "files must be a non-empty normalized manifest"));
196
+ else {
197
+ for (const [index, file] of value.files.entries()) {
198
+ const path = text(file?.path);
199
+ const normalized = path === path.normalize("NFC") && !/[\u0000-\u001f\u007f]/.test(path)
200
+ && !path.startsWith("/") && !path.includes("\\")
201
+ && path.split("/").every((segment) => segment && segment !== "." && segment !== "..");
202
+ if (!isObj(file) || !normalized || !shaRegex.test(text(file.sha256))) findings.push(finding("P0", "VALIDATION-SUBJECT-FILE", `${entityRef}.files[${index}]`, "Each file needs a normalized relative path and SHA-256"));
203
+ if (index > 0 && text(value.files[index - 1]?.path) >= path) findings.push(finding("P0", "VALIDATION-SUBJECT-FILE-ORDER", `${entityRef}.files[${index}].path`, "File paths must be unique and sorted"));
204
+ }
205
+ if (!findings.some((item) => item.ruleId.startsWith("VALIDATION-SUBJECT-FILE"))
206
+ && value.artifactSha256 !== validatorArtifactSubject(value.files)) findings.push(finding("P0", "VALIDATION-SUBJECT-ARTIFACT-MANIFEST", `${entityRef}.artifactSha256`, "Artifact digest does not match the file manifest"));
207
+ }
208
+ }
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"));
222
+ if (Array.isArray(value.tests)) findings.push(...validateGoldenBaseline(value.goldenBaseline, `${entityRef}.goldenBaseline`, value.tests));
223
+ try { canonicalJson(value); } catch (error) { findings.push(finding("P0", "VALIDATION-SUBJECT-JSON", entityRef, error instanceof Error ? error.message : "Invalid JSON")); }
224
+ return findings.length ? { findings } : { value };
225
+ }
226
+ function expectedValidationSubject(input) {
227
+ if (input.expectedSubject === undefined && input.validationContext === undefined) return readValidationSubject(undefined, "input.expectedSubject");
228
+ const expected = input.expectedSubject === undefined ? null : readValidationSubject(input.expectedSubject, "input.expectedSubject");
229
+ const context = input.validationContext === undefined ? null : readValidationSubject(input.validationContext, "input.validationContext");
230
+ const findings = [...(expected?.findings ?? []), ...(context?.findings ?? [])];
231
+ if (findings.length) return { findings };
232
+ const value = expected?.value ?? context.value;
233
+ const digest = validatorReceiptSubject(value);
234
+ if (expected?.value && context?.value && digest !== validatorReceiptSubject(context.value)) return { findings: [finding("P0", "VALIDATION-SUBJECT-CONFLICT", "input.validationContext", "Targets differ")] };
235
+ return { value, digest };
236
+ }
237
+
238
+ function configuredReceiptKey() {
239
+ const encoded = text(process.env[RECEIPT_PUBLIC_KEY_ENV]).trim();
240
+ if (!/^[A-Za-z0-9+/]+={0,2}$/.test(encoded)) return null;
241
+ try {
242
+ const der = Buffer.from(encoded, "base64");
243
+ const key = createPublicKey({ key: der, format: "der", type: "spki" });
244
+ return key.asymmetricKeyType === "ed25519" ? { key, keyId: createHash("sha256").update(der).digest("hex") } : null;
245
+ } catch { return null; }
246
+ }
247
+ function verifiedReceipt(receipt, subjectDigest, subject) {
248
+ const configured = configuredReceiptKey();
249
+ if (!configured || !isObj(receipt) || receipt.schemaVersion !== RECEIPT_SCHEMA) return null;
250
+ const result = receipt.result;
251
+ const issuedAt = Date.parse(text(receipt.issuedAt));
252
+ const expiresAt = Date.parse(text(receipt.expiresAt));
253
+ const executedAt = subject.executedAt === undefined ? null : Date.parse(text(subject.executedAt));
254
+ const now = Date.now();
255
+ if (receipt.keyId !== configured.keyId || receipt.subjectDigest !== subjectDigest || !idRegex.test(text(receipt.nonce))
256
+ || !Number.isFinite(issuedAt) || !Number.isFinite(expiresAt) || issuedAt > now + 60_000 || expiresAt <= now
257
+ || (executedAt !== null && (!Number.isFinite(executedAt) || executedAt > issuedAt
258
+ || issuedAt - executedAt > MAX_RECEIPT_LIFETIME_MS))
259
+ || expiresAt <= issuedAt || expiresAt - issuedAt > MAX_RECEIPT_LIFETIME_MS || !isObj(result)
260
+ || result.runner !== "trusted-runner" || typeof result.passed !== "boolean" || !Number.isInteger(result.exitCode)
261
+ || typeof result.durationMs !== "number" || result.durationMs < 0 || !text(result.summary).trim()) return null;
262
+ try {
263
+ const signature = Buffer.from(text(receipt.signature), "base64url");
264
+ return verifySignature(null, Buffer.from(validatorReceiptPayload(receipt)), configured.key, signature) ? receipt : null;
265
+ } catch { return null; }
266
+ }
267
+ function createTestEvidence(receipt, subject, subjectDigest, index) {
268
+ return { schemaVersion: TEST_EVIDENCE_SCHEMA, evidenceId: `${subject.validationRunId}:${index}`, kind: "test",
269
+ runner: "trusted-runner", command: subject.policy.command, exitCode: receipt.result.exitCode,
270
+ durationMs: receipt.result.durationMs, summary: receipt.result.summary, artifactSha256: subject.artifactSha256,
271
+ subject, subjectDigest, receipt };
272
+ }
273
+ function evidenceState(evidence, subject, subjectDigest) {
274
+ if (!isObj(evidence) || evidence.schemaVersion !== TEST_EVIDENCE_SCHEMA || evidence.runner === "local") return "unverifiable";
275
+ let evidenceDigest;
276
+ try { evidenceDigest = validatorReceiptSubject(evidence.subject); } catch { return "unverifiable"; }
277
+ if (!idRegex.test(text(evidence.evidenceId)) || !["test", "build", "lint", "security", "benchmark"].includes(evidence.kind)
278
+ || evidence.runner !== "trusted-runner" || evidence.command !== subject.policy.command
279
+ || evidence.artifactSha256 !== subject.artifactSha256 || evidence.subjectDigest !== subjectDigest
280
+ || !isObj(evidence.subject) || evidenceDigest !== subjectDigest) return "unverifiable";
281
+ const receipt = verifiedReceipt(evidence.receipt, subjectDigest, subject);
282
+ if (!receipt || evidence.exitCode !== receipt.result.exitCode || evidence.durationMs !== receipt.result.durationMs
283
+ || evidence.summary !== receipt.result.summary) return "unverifiable";
284
+ return receipt.result.passed && receipt.result.exitCode === subject.policy.requiredExitCode ? "valid" : "failed";
285
+ }
286
+
287
+ const DEFAULT_SECURITY_RULES = [
288
+ { id: "SEC-EVAL", pattern: /\beval\s*\(|new\s+Function\b|\bFunction\s*\(/g, severity: "P0", fix: "Use controlled AST evaluation", executableOnly: true },
289
+ { id: "SEC-SECRETS", pattern: /(?:api[_-]?key|token|password|secret)\s*[:=]\s*['"][A-Za-z0-9_\-]{8,}/gi, severity: "P0", fix: "Remove hardcoded credentials" },
290
+ { id: "SEC-SQLI", pattern: /(?:SELECT|INSERT|UPDATE|DELETE)\s+.*\+\s*(?:req\.|input\.|params\.)/gi, severity: "P0", fix: "Use parameterized queries" },
291
+ { id: "SEC-XSS", pattern: /innerHTML\s*=\s*(?!\s*['"`]\s*['"`])/g, severity: "P1", fix: "Use sanitized output" },
292
+ ];
293
+ const DEFAULT_STRUCTURE_RULES = ["references-closed", "required-fields", "type-correct", "no-cycle"];
294
+ function executableJavaScript(path) { return /\.(?:[cm]?[jt]sx?)$/i.test(path); }
295
+ function stripJavaScriptInertText(source) {
296
+ let output = "";
297
+ let state = "code";
298
+ let quote = "";
299
+ for (let index = 0; index < source.length; index += 1) {
300
+ const character = source[index];
301
+ const next = source[index + 1];
302
+ if (state === "line") {
303
+ if (character === "\n") { state = "code"; output += "\n"; }
304
+ continue;
305
+ }
306
+ if (state === "block") {
307
+ if (character === "*" && next === "/") { state = "code"; index += 1; }
308
+ else if (character === "\n") output += "\n";
309
+ continue;
310
+ }
311
+ if (state === "string") {
312
+ if (character === "\\") { index += 1; continue; }
313
+ if (character === quote) { state = "code"; quote = ""; }
314
+ else if (character === "\n") output += "\n";
315
+ continue;
316
+ }
317
+ if (character === "/" && next === "/") { state = "line"; index += 1; continue; }
318
+ if (character === "/" && next === "*") { state = "block"; index += 1; continue; }
319
+ if (["'", '"', "`"].includes(character)) { state = "string"; quote = character; continue; }
320
+ output += character;
321
+ }
322
+ return output;
323
+ }
324
+ function runSecurityScan(files, rules = DEFAULT_SECURITY_RULES) {
325
+ const findings = [];
326
+ for (const file of files) for (const rule of rules) {
327
+ const path = text(file.path);
328
+ const content = rule.executableOnly
329
+ ? executableJavaScript(path) ? stripJavaScriptInertText(text(file.content)) : ""
330
+ : text(file.content);
331
+ const matches = content.match(rule.pattern instanceof RegExp ? rule.pattern : new RegExp(rule.pattern, "g"));
332
+ if (matches) findings.push(finding(rule.severity, rule.id, text(file.path), `${matches.length} match(es): ${rule.fix}`, { sample: matches[0] }));
333
+ }
334
+ return findings;
335
+ }
336
+ function runStructureValidation(files) {
337
+ const findings = [];
338
+ for (const file of files) {
339
+ const path = text(file.path);
340
+ if (path.endsWith(".json")) try { JSON.parse(text(file.content)); } catch (error) { findings.push(finding("P0", "STR-JSON", path, error.message)); }
341
+ if (file.schema && path.endsWith(".json")) try {
342
+ const data = JSON.parse(text(file.content));
343
+ for (const field of file.schema.required ?? []) if (data[field] === undefined) findings.push(finding("P0", "STR-REQ", `${path}.${field}`, "Required field is missing"));
344
+ } catch { /* invalid JSON is reported above */ }
345
+ }
346
+ return findings;
347
+ }
348
+ function validateReq(request) {
349
+ const findings = [];
350
+ if (!isObj(request)) return [finding("P0", "REQ_OBJECT", "request", "request must be an object")];
351
+ if (request.schemaVersion !== REQ) findings.push(finding("P0", "REQ_SCHEMA", "request.schemaVersion", `Expected ${REQ}`));
352
+ if (!text(request.requestId).trim()) findings.push(finding("P0", "REQ_FIELD", "request.requestId", "requestId is required"));
353
+ if (!OPS.includes(request.operation)) findings.push(finding("P0", "REQ_OPERATION", "request.operation", "operation is unsupported"));
354
+ if (!isObj(request.input)) findings.push(finding("P0", "REQ_INPUT", "request.input", "input must be an object"));
355
+ return findings;
356
+ }
357
+ function severitySummary(findings) {
358
+ return { total: findings.length, p0: findings.filter((item) => item.severity === "P0").length,
359
+ p1: findings.filter((item) => item.severity === "P1").length,
360
+ p2: findings.filter((item) => item.severity === "P2").length };
361
+ }
362
+ const REPAIR_ROUTES = Object.freeze([
363
+ { category: "structure-schema", skill: "blueprint", action: "recompile-contract", trigger: "structure or schema mismatch", requiresHumanConfirmation: false },
364
+ { category: "formula-calculation", skill: "calctool", action: "repair-formula-engine", trigger: "formula or calculation mismatch", requiresHumanConfirmation: false },
365
+ { category: "scope-drift", skill: "aimlock", action: "relock-scope", trigger: "scope contract violation", requiresHumanConfirmation: false },
366
+ { category: "execution-dispatch", skill: "swarm", action: "repair-dispatch", trigger: "execution or dispatch failure", requiresHumanConfirmation: false },
367
+ { category: "validator-self", skill: "validator", action: "propose-validator-patch", trigger: "Validator rule or engine defect", requiresHumanConfirmation: true },
368
+ ]);
369
+
370
+ function runMeta(requestId, operation) {
371
+ const operationStatus = { implementedPure: [...PURE], localRunnerRequired: [...LOCAL_ONLY], planned: ["mutation-testing"] };
372
+ const goldenPathExample = { operation: "functional-verify", input: { validationContext: "frozen ValidationSubject", receipts: "signed trusted-runner receipt[]" }, next: "verdict with cli.tax.test-evidence/1.0" };
373
+ if (operation === "capabilities") return ok(requestId, { capabilities: { pure: false, stateless: true, operationStatus,
374
+ verdictLevels: ["pass", "pass-with-risk", "blocked", "incomplete"], testEvidenceSchema: TEST_EVIDENCE_SCHEMA,
375
+ goldenBaselineSchema: GOLDEN_BASELINE_SCHEMA, catalogSchema: CATALOG_SCHEMA }, operationSchemas: SCHEMAS,
376
+ goldenPathExample, skill: { name: NAME, version: COMPILER_VERSION }, nextStep: { operation: "intake", instruction: "Collect validation requirements." } });
377
+ return ok(requestId, { help: { name: NAME, version: COMPILER_VERSION, operations: CATALOG, operationStatus, goldenPathExample },
378
+ operationSchemas: SCHEMAS, nextStep: { operation: "intake", instruction: "Collect validation requirements." } });
379
+ }
380
+ function runPlanning(requestId, operation, input) {
381
+ if (operation === "intake") {
382
+ const goal = requiredText(input, "goal");
383
+ const risk = requiredText(input, "riskLevel");
384
+ if (goal.error || risk.error) return blocked(requestId, [goal.error, risk.error].filter(Boolean));
385
+ if (!["low", "medium", "high"].includes(risk.value)) return blocked(requestId, [finding("P0", "RISK-LEVEL", "input.riskLevel", "riskLevel must be low, medium, or high")]);
386
+ return ok(requestId, { intake: { goal: goal.value, riskLevel: risk.value, complianceReqs: input.complianceReqs, targetFiles: input.targetFiles }, nextStep: { operation: "plan", instruction: "Generate validation plan." } });
387
+ }
388
+ const risk = text(input.intakeResult?.riskLevel);
389
+ if (!["low", "medium", "high"].includes(risk)) return blocked(requestId, [finding("P0", "INTAKE-RESULT", "input.intakeResult", "A valid intakeResult is required")]);
390
+ const modules = ["validate-structure", "security-scan", "functional-verify"];
391
+ if (risk === "high") modules.push("compliance-audit", "sandbox-run", "intrusive-test");
392
+ else modules.push("fuzz-input");
393
+ modules.push("perf-benchmark", "verdict");
394
+ const available = Array.isArray(input.availableSkills) ? input.availableSkills : [];
395
+ const requestedCategories = Array.isArray(input.findingCategories) ? input.findingCategories : [];
396
+ const invalidCategory = requestedCategories.find((category) => !REPAIR_ROUTES.some((route) => route.category === category));
397
+ if (invalidCategory) return blocked(requestId, [finding("P0", "REPAIR-CATEGORY", "input.findingCategories", `Unsupported repair category: ${invalidCategory}`)]);
398
+ const selectedRoutes = requestedCategories.length
399
+ ? REPAIR_ROUTES.filter((route) => requestedCategories.includes(route.category)) : REPAIR_ROUTES;
400
+ const routing = selectedRoutes.map((route) => ({
401
+ ...route,
402
+ available: route.skill === "validator" || available.includes(route.skill),
403
+ invoke: route.skill !== "validator" && available.includes(route.skill),
404
+ }));
405
+ return ok(requestId, { plan: { modules, routing, routingPolicy: "deterministic-category-map",
406
+ riskLevel: risk, totalSteps: modules.length }, nextStep: { operation: modules[0], instruction: `Execute ${modules[0]}.` } });
407
+ }
408
+ function runStatic(requestId, operation, input) {
409
+ const files = requiredArray(input, "files", 1);
410
+ if (files.error) return blocked(requestId, [files.error]);
411
+ if (operation === "validate-structure") {
412
+ const findings = runStructureValidation(files.value);
413
+ return ok(requestId, { findings, summary: severitySummary(findings), line: "static", nextStep: { operation: "security-scan", instruction: "Run security scan." } });
414
+ }
415
+ if (operation === "security-scan") {
416
+ const findings = runSecurityScan(files.value, input.rules);
417
+ return ok(requestId, { findings, summary: severitySummary(findings), line: "static", nextStep: { operation: "functional-verify", instruction: "Run frozen golden baseline." } });
418
+ }
419
+ const template = requiredText(input, "template");
420
+ if (template.error) return blocked(requestId, [template.error]);
421
+ const findings = files.value.flatMap((file) => publicBindSites(text(file.path), text(file.content)).map((site) => finding("P1", "COMP-PORT", text(file.path), "Public bind requires review", site)));
422
+ return ok(requestId, { findings, template: template.value, nextStep: { operation: "functional-verify", instruction: "Run frozen golden baseline." } });
423
+ }
424
+ function runFunctional(requestId, input) {
425
+ const subject = readValidationSubject(input.validationContext, "input.validationContext");
426
+ if (subject.findings) return blocked(requestId, subject.findings);
427
+ const receipts = requiredArray(input, "receipts", 1);
428
+ if (receipts.error) return blocked(requestId, [finding("P0", "TRUSTED-RECEIPT-REQUIRED", "input.receipts", "Signed trusted-runner receipts are required")]);
429
+ const subjectDigest = validatorReceiptSubject(subject.value);
430
+ const verified = receipts.value.map((receipt) => verifiedReceipt(receipt, subjectDigest, subject.value));
431
+ if (verified.some((receipt) => receipt === null)) return blocked(requestId, [finding("P0", "INVALID-EXECUTION-RECEIPT", "input.receipts", "Receipt signature, lifetime, result, or subject is invalid")]);
432
+ const results = verified.map((receipt) => receipt.result);
433
+ const findings = results.flatMap((result, index) => result.passed && result.exitCode === subject.value.policy.requiredExitCode ? [] : [finding("P1", "GOLDEN-FAIL", `receipt:${index}`, "Frozen golden baseline failed", result)]);
434
+ const evidence = verified.map((receipt, index) => createTestEvidence(receipt, subject.value, subjectDigest, index));
435
+ return ok(requestId, { subject: subject.value, subjectDigest, results,
436
+ summary: { total: results.length, passed: results.length - findings.length, failed: findings.length },
437
+ runner: "trusted-runner", findings, evidence, nextStep: { operation: "verdict", instruction: "Render verdict from TestEvidence." } });
438
+ }
439
+
440
+ function runLocalProtocol(requestId, operation, input) {
441
+ if (operation === "intrusive-test" && input.authorization !== true) return blocked(requestId, [finding("P0", "INTRUSIVE-NO-AUTH", "input.authorization", "Explicit authorization is required")]);
442
+ const descriptor = operation === "fuzz-input" ? requiredText(input, "targetFile") : requiredText(input, "command");
443
+ if (descriptor.error && operation !== "intrusive-test") return blocked(requestId, [descriptor.error]);
444
+ const pending = { operation, status: "pending-execution", requestedTarget: descriptor.value };
445
+ const key = operation === "sandbox-run" ? "sandbox" : operation === "fuzz-input" ? "fuzz"
446
+ : operation === "perf-benchmark" ? "benchmark" : "intrusive";
447
+ return ok(requestId, { [key]: pending, runner: "local-only", evidence: [],
448
+ pendingEvidenceRequirements: { schemaVersion: TEST_EVIDENCE_SCHEMA, trustedReceiptRequiredForFinalPass: true },
449
+ nextStep: { operation: "verdict", instruction: "Local runner must return signed TestEvidence; pending is incomplete." } });
450
+ }
451
+ function validRiskLedger(findings, ledger) {
452
+ if (!Array.isArray(ledger)) return false;
453
+ return findings.filter((item) => item.severity === "P1").every((item) => ledger.some((entry) => isObj(entry)
454
+ && idRegex.test(text(entry.riskId)) && entry.findingRuleId === item.ruleId
455
+ && entry.findingEntityRef === item.entityRef
456
+ && idRegex.test(text(entry.owner)) && text(entry.mitigation).trim()
457
+ && idRegex.test(text(entry.acceptedBy)) && Number.isFinite(Date.parse(text(entry.acceptedAt)))));
458
+ }
459
+ function runVerdict(requestId, input) {
460
+ const expected = expectedValidationSubject(input);
461
+ if (expected.findings) return blocked(requestId, expected.findings);
462
+ const findings = Array.isArray(input.findings) ? input.findings : [];
463
+ const evidence = Array.isArray(input.evidence) ? input.evidence : [];
464
+ const states = evidence.map((item) => evidenceState(item, expected.value, expected.digest));
465
+ const failedCount = states.filter((state) => state === "failed").length;
466
+ const p0 = findings.filter((item) => item.severity === "P0").length;
467
+ const p1 = findings.filter((item) => item.severity === "P1").length;
468
+ const allValid = states.length > 0 && states.every((state) => state === "valid");
469
+ const riskLedgerValid = p1 === 0 || validRiskLedger(findings, input.riskLedger);
470
+ const level = p0 > 0 || failedCount > 0 ? "blocked"
471
+ : !allValid || !riskLedgerValid ? "incomplete" : p1 > 0 ? "pass-with-risk" : "pass";
472
+ const report = { verdict: level, subjectDigest: expected.digest, findings: severitySummary(findings),
473
+ evidenceCount: evidence.length, evidenceValid: allValid,
474
+ evidenceSummary: { valid: states.filter((state) => state === "valid").length, failed: failedCount,
475
+ pending: 0, unverifiable: states.filter((state) => state === "unverifiable").length },
476
+ riskLedgerValid, riskLedger: input.riskLedger };
477
+ return ok(requestId, { report, findings, evidence,
478
+ nextStep: level === "blocked" ? { operation: "plan", instruction: "Repair blocking failures." }
479
+ : level === "incomplete" ? { operation: "verdict", instruction: "Provide trusted evidence and complete P1 risk ledger." }
480
+ : { operation: null, instruction: `Verdict: ${level}.` } });
481
+ }
482
+
483
+ export async function run(request) {
484
+ const validationFindings = validateReq(request);
485
+ if (validationFindings.length) return { ...blocked(request?.requestId ?? "unknown", validationFindings), errorSchema: ERR };
486
+ const { requestId, operation, input } = request;
487
+ if (operation === "capabilities" || operation === "help") return runMeta(requestId, operation);
488
+ if (operation === "intake" || operation === "plan") return runPlanning(requestId, operation, input);
489
+ if (["validate-structure", "security-scan", "compliance-audit"].includes(operation)) return runStatic(requestId, operation, input);
490
+ if (operation === "functional-verify") return runFunctional(requestId, input);
491
+ if (LOCAL_ONLY.has(operation)) return runLocalProtocol(requestId, operation, input);
492
+ if (operation === "verdict") return runVerdict(requestId, input);
493
+ return failed(requestId, "UNSUPPORTED_OPERATION", `Unsupported operation: ${operation}`);
494
+ }
495
+
496
+ export { COMPILER_VERSION, NAME, OPS, PURE, CATALOG, SCHEMAS, GOLDEN_BASELINE_SCHEMA,
497
+ TEST_EVIDENCE_SCHEMA, DEFAULT_SECURITY_RULES, DEFAULT_STRUCTURE_RULES,
498
+ runSecurityScan, runStructureValidation };