ruvnet-brain 4.3.9 → 4.3.11
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 +26 -9
- package/bin/install.mjs +646 -366
- package/bin/nightly-refresh.mjs +115 -0
- package/kb/brain-profile.mjs +118 -32
- package/kb/lifecycle-evidence-retention.mjs +237 -0
- package/kb/refresh-run.mjs +367 -0
- package/kb/retrieval-result.mjs +39 -0
- package/kb/update-storage-transaction.mjs +439 -0
- package/package.json +12 -3
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/hooks/codex-hooks.json +2 -165
- package/plugin/hooks/hook-contracts.json +4 -65
- package/plugin/hooks/hooks.json +2 -208
- package/plugin/scripts/adr-currency-gate.mjs +29 -19
- package/plugin/scripts/capability-registry.mjs +10 -83
- package/plugin/scripts/codex-hook-adapter.mjs +62 -9
- package/plugin/scripts/codex-hook-wrapper.mjs +19 -1
- package/plugin/scripts/continuation-gate.mjs +30 -28
- package/plugin/scripts/continuation-objective.mjs +34 -0
- package/plugin/scripts/development-maintenance.mjs +49 -0
- package/plugin/scripts/hook-shim.mjs +3 -0
- package/plugin/scripts/learn-flush.mjs +6 -1
- package/plugin/scripts/lesson-gate.mjs +5 -2
- package/plugin/scripts/lesson-presentation.mjs +5 -0
- package/plugin/scripts/md-stamp.mjs +109 -6
- package/plugin/scripts/memory-doctor.mjs +3 -7
- package/plugin/scripts/nightly-controller.mjs +9 -27
- package/plugin/scripts/nightly-scheduler.mjs +494 -0
- package/plugin/scripts/project-progression-contract.mjs +4 -4
- package/plugin/scripts/project-progression-store.mjs +35 -6
- package/plugin/scripts/ruflo-bin.mjs +20 -0
- package/plugin/scripts/session-snapshot-contract.mjs +6 -1
- package/plugin/scripts/version-bump-gate.sh +14 -2
- package/plugin/skills/release-proof/SKILL.md +6 -2
- package/plugin/skills/release-proof/references/receipt-contract.md +31 -2
- package/plugin/skills/release-proof/scripts/release-proof.mjs +11 -2
- package/plugin/skills/ruvnet-brain/PLAYBOOK.md +15 -23
- package/scripts/build-bundle.mjs +8 -0
- package/scripts/build-primer.mjs +2 -3
- package/scripts/candidate-host-evidence.mjs +59 -41
- package/scripts/ci/build-fixture-kb.mjs +59 -2
- package/scripts/ci/mutate-hook-timeout.mjs +24 -47
- package/scripts/ci/stranger-scenario.mjs +13 -28
- package/scripts/claims-verify.mjs +219 -21
- package/scripts/console-engine.mjs +4 -18
- package/scripts/console-runtime-identity.mjs +6 -0
- package/scripts/development-maintenance.mjs +47 -0
- package/scripts/development-push-check.mjs +35 -0
- package/scripts/distill-project.mjs +24 -47
- package/scripts/doc-currency.mjs +91 -19
- package/scripts/git-hooks/pre-push +4 -162
- package/scripts/health-repair.mjs +22 -4
- package/scripts/hook-retirement-check.mjs +22 -0
- package/scripts/host-install-matrix.mjs +125 -61
- package/scripts/integration-evidence.mjs +11 -6
- package/scripts/learning-replay-fixture.mjs +23 -2
- package/scripts/nightly-package-observation.mjs +67 -0
- package/scripts/nightly-two-run-proof.mjs +489 -0
- package/scripts/nightly-watchdog.mjs +17 -2
- package/scripts/npm-invocation.mjs +20 -0
- package/scripts/prepublication-evidence.mjs +42 -6
- package/scripts/primer-grounding.mjs +20 -0
- package/scripts/product-integrity-contract.mjs +22 -2
- package/scripts/public-verification-abandon.mjs +101 -0
- package/scripts/public-verification-aggregate.mjs +61 -19
- package/scripts/public-verification-finalizer.mjs +8 -2
- package/scripts/public-verification-lane.mjs +64 -8
- package/scripts/publication-receipt.mjs +156 -50
- package/scripts/published-surface-probe.mjs +10 -2
- package/scripts/qa-contract.mjs +57 -0
- package/scripts/qa-lanes.mjs +40 -0
- package/scripts/qa-runner.mjs +54 -64
- package/scripts/qe/ux-suite.mjs +10 -34
- package/scripts/qualified-candidate-check.mjs +124 -0
- package/scripts/release-abort-stale.mjs +2 -2
- package/scripts/release-projection.mjs +20 -1
- package/scripts/release-qualification-contract.mjs +87 -0
- package/scripts/release-qualification.mjs +135 -0
- package/scripts/release-transaction.mjs +81 -1
- package/scripts/remedy-registry.mjs +6 -13
- package/scripts/retrieval-canary.mjs +91 -18
- package/scripts/selfcheck.mjs +12 -4
- package/scripts/snapshot-freshness.mjs +58 -0
- package/scripts/stack-sync.mjs +41 -43
- package/scripts/staged-host-verifier.mjs +63 -14
- package/scripts/wired-check.mjs +31 -2
|
@@ -5,7 +5,10 @@ import path from 'node:path';
|
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { aggregateEvidence } from './release-evidence-aggregate.mjs';
|
|
7
7
|
import { canonicalJson } from './release-transaction.mjs';
|
|
8
|
-
import {
|
|
8
|
+
import { RELEASE_QUALIFICATION_POLICY } from './integration-evidence.mjs';
|
|
9
|
+
import { readCandidateRetrieval } from './staged-host-verifier.mjs';
|
|
10
|
+
import { validateRetrievalCanaryReceipt } from './retrieval-canary.mjs';
|
|
11
|
+
import { payloadIdFor } from './release-payload.mjs';
|
|
9
12
|
|
|
10
13
|
const sha256 = (bytes) => crypto.createHash('sha256').update(bytes).digest('hex');
|
|
11
14
|
|
|
@@ -44,10 +47,13 @@ export function buildPrepublicationEvidence({
|
|
|
44
47
|
manifestFile,
|
|
45
48
|
payloadProofFile,
|
|
46
49
|
hostFile,
|
|
50
|
+
runtimeCensusFile,
|
|
47
51
|
ciFile,
|
|
48
52
|
integrationFile,
|
|
49
53
|
uxFiles,
|
|
50
54
|
strangerFile,
|
|
55
|
+
planFile,
|
|
56
|
+
coverageFile,
|
|
51
57
|
}) {
|
|
52
58
|
if (!/^[a-f0-9]{40}$/.test(sha || '') || !/^\d+\.\d+\.\d+$/.test(version || '') || !Number.isSafeInteger(runId) || runId <= 0) {
|
|
53
59
|
throw new Error('prepublication identity is malformed');
|
|
@@ -60,10 +66,30 @@ export function buildPrepublicationEvidence({
|
|
|
60
66
|
throw new Error('candidate manifest identity mismatch');
|
|
61
67
|
}
|
|
62
68
|
if (!/^[a-f0-9]{64}$/.test(payload.payloadId || '')) throw new Error('payload identity is malformed');
|
|
69
|
+
if (payload.payloadId !== payloadIdFor(manifest)) throw new Error('candidate manifest payload identity mismatch');
|
|
70
|
+
const retrieval = readCandidateRetrieval({ manifest, planFile, coverageFile });
|
|
71
|
+
|
|
72
|
+
const runtimeCensus = readJson(runtimeCensusFile);
|
|
73
|
+
const census = runtimeCensus.value;
|
|
74
|
+
const result = census.result;
|
|
75
|
+
const artifacts = result?.provenance?.artifacts;
|
|
76
|
+
const candidate = result?.provenance?.candidate;
|
|
77
|
+
const memberHash = (role) => manifest.members.find((member) => member.role === role)?.sha256;
|
|
78
|
+
if (census.schemaVersion !== 1 || census.kind !== 'ruvnet-brain-runtime-census-evidence'
|
|
79
|
+
|| census.sourceSha !== sha || census.version !== version || census.runId !== runId
|
|
80
|
+
|| census.payloadId !== payload.payloadId || census.payloadManifestSha256 !== sha256(manifestBytes)
|
|
81
|
+
|| result?.status !== 'PASS' || result.diagnostic !== false || result.qualification !== 'staged-candidate'
|
|
82
|
+
|| result.provenance?.projectionVerified !== true || candidate?.sourceSha !== sha || candidate?.version !== version
|
|
83
|
+
|| candidate?.coverageSha256 !== sha256(fs.readFileSync(coverageFile))
|
|
84
|
+
|| artifacts?.payloadId !== payload.payloadId || artifacts.claimSourceSha !== sha
|
|
85
|
+
|| artifacts.packageSha256 !== memberHash('npm') || artifacts.bundleSha256 !== memberHash('bundle')
|
|
86
|
+
|| !(artifacts.sidecarsVerified > 0) || !(artifacts.surfacesVerified > 0)) {
|
|
87
|
+
throw new Error('runtime census receipt is not an exact candidate-bound PASS');
|
|
88
|
+
}
|
|
63
89
|
|
|
64
90
|
const host = readJson(hostFile);
|
|
65
91
|
if (host.value.schemaVersion !== 1 || host.value.sha !== sha || host.value.payloadId !== payload.payloadId
|
|
66
|
-
||
|
|
92
|
+
|| host.value.artifactSha256 !== retrieval.artifactSha256 || host.value.leaves?.length !== 3) {
|
|
67
93
|
throw new Error('candidate host evidence identity or shape mismatch');
|
|
68
94
|
}
|
|
69
95
|
requireExactSet(host.value.leaves.map(({ name }) => name), ['claude-only', 'codex-only', 'dual-host'], 'candidate host leaves');
|
|
@@ -77,10 +103,11 @@ export function buildPrepublicationEvidence({
|
|
|
77
103
|
|| leaf.artifactSha256 !== host.value.artifactSha256) {
|
|
78
104
|
throw new Error(`candidate host leaf is not an exact PASS: ${leaf.name || '(missing)'}`);
|
|
79
105
|
}
|
|
106
|
+
validateRetrievalCanaryReceipt(leaf.retrieval, { plan: retrieval.plan });
|
|
80
107
|
}
|
|
81
108
|
|
|
82
109
|
const ci = readJson(ciFile);
|
|
83
|
-
const requiredCiJobs = ['candidate-preflight', '
|
|
110
|
+
const requiredCiJobs = ['candidate-preflight', 'release-acceptance-linux', 'release-acceptance-windows', 'release-acceptance-macos', 'release-qe'];
|
|
84
111
|
if (ci.value.schemaVersion !== 1 || ci.value.kind !== 'ruvnet-brain-candidate-ci-evidence'
|
|
85
112
|
|| ci.value.sourceSha !== sha || ci.value.version !== version || ci.value.payloadId !== payload.payloadId
|
|
86
113
|
|| ci.value.payloadManifestSha256 !== sha256(manifestBytes) || ci.value.workflow !== 'ci'
|
|
@@ -89,6 +116,10 @@ export function buildPrepublicationEvidence({
|
|
|
89
116
|
}
|
|
90
117
|
requireExactSet(ci.value.jobs?.map(({ name }) => name) || [], requiredCiJobs, 'candidate CI jobs');
|
|
91
118
|
if (ci.value.jobs.some(({ conclusion }) => conclusion !== 'success')) throw new Error('candidate CI receipt contains a non-success job');
|
|
119
|
+
requireExactSet(ci.value.acceptanceReceipts?.map(({ platform }) => platform) || [], ['linux', 'macos', 'windows'], 'release acceptance platforms');
|
|
120
|
+
if (ci.value.acceptanceReceipts.some(row => row.sourceSha !== sha || !(row.passed > 0)
|
|
121
|
+
|| !/^[a-f0-9]{64}$/.test(row.receiptSha256 || ''))) throw new Error('release acceptance receipt identity mismatch');
|
|
122
|
+
|
|
92
123
|
|
|
93
124
|
const integration = readJson(integrationFile);
|
|
94
125
|
if (integration.value.schemaVersion !== 1 || integration.value.kind !== 'ruvnet-brain-integration-evidence'
|
|
@@ -102,7 +133,9 @@ export function buildPrepublicationEvidence({
|
|
|
102
133
|
|| integration.value.skippedTests.some((name) => typeof name !== 'string' || !name.trim())) {
|
|
103
134
|
throw new Error('integration receipt is not an exact, fully accounted PASS');
|
|
104
135
|
}
|
|
105
|
-
if (integration.value.exclusionPolicy !==
|
|
136
|
+
if (integration.value.exclusionPolicy !== RELEASE_QUALIFICATION_POLICY
|
|
137
|
+
|| integration.value.skipped !== 0 || integration.value.todo !== 0
|
|
138
|
+
|| !/^[a-f0-9]{64}$/.test(integration.value.qualificationReceiptSha256 || '')
|
|
106
139
|
|| !Array.isArray(integration.value.todoTests)
|
|
107
140
|
|| integration.value.todoTests.length !== Number(integration.value.todo || 0)
|
|
108
141
|
|| !/^[a-f0-9]{64}$/.test(integration.value.exclusionsSha256 || '')) {
|
|
@@ -133,9 +166,9 @@ export function buildPrepublicationEvidence({
|
|
|
133
166
|
|
|
134
167
|
const common = { sha, payloadId: payload.payloadId, runId };
|
|
135
168
|
const leaves = [
|
|
136
|
-
passLeaf({ ...common, name: 'source-quality', source: 'candidate-ci-receipt:
|
|
169
|
+
passLeaf({ ...common, name: 'source-quality', source: 'candidate-ci-receipt:release-acceptance-linux', receiptSha256: ci.digest }),
|
|
137
170
|
passLeaf({ ...common, name: 'ux-qe', source: 'ux-qe-receipts:darwin,linux,win32', receiptSha256: uxDigest }),
|
|
138
|
-
passLeaf({ ...common, name: 'release-qe', source: 'candidate-ci-receipt:release-qe', receiptSha256: ci.digest }),
|
|
171
|
+
passLeaf({ ...common, name: 'release-qe', source: 'candidate-ci-receipt:release-qe', receiptSha256: ci.digest, runtimeCensusSha256: runtimeCensus.digest }),
|
|
139
172
|
passLeaf({ ...common, name: 'integration-linux', source: 'integration-receipt', receiptSha256: integration.digest,
|
|
140
173
|
skipped: integration.value.skipped, todo: integration.value.todo }),
|
|
141
174
|
passLeaf({ ...common, name: 'stranger-linux', source: 'stranger-receipt:ubuntu', receiptSha256: stranger.digest }),
|
|
@@ -161,6 +194,9 @@ if (isCli) {
|
|
|
161
194
|
manifestFile: path.resolve(arg('--manifest')),
|
|
162
195
|
payloadProofFile: path.resolve(arg('--payload-proof')),
|
|
163
196
|
hostFile: path.resolve(arg('--hosts')),
|
|
197
|
+
runtimeCensusFile: path.resolve(arg('--runtime-census')),
|
|
198
|
+
planFile: arg('--plan'),
|
|
199
|
+
coverageFile: arg('--coverage'),
|
|
164
200
|
ciFile: path.resolve(arg('--ci')),
|
|
165
201
|
integrationFile: path.resolve(arg('--integration')),
|
|
166
202
|
uxFiles,
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
// Citation presence is an admission check, not semantic verification of each generated claim.
|
|
5
|
+
export function countPrimerReferences(primer, sourcePaths) {
|
|
6
|
+
const paths = [...new Set(sourcePaths)].filter((value) => typeof value === 'string' && value);
|
|
7
|
+
const citations = new Set([...primer.matchAll(/`([^`\n]+)`/g)].map((match) => match[1]));
|
|
8
|
+
const basenames = new Map();
|
|
9
|
+
for (const file of paths) basenames.set(path.basename(file), (basenames.get(path.basename(file)) || 0) + 1);
|
|
10
|
+
return paths.filter((file) => citations.has(file)
|
|
11
|
+
|| (basenames.get(path.basename(file)) === 1 && citations.has(path.basename(file))));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function writeGroundedPrimer({ primer, sourcePaths, output, minimum = 6 }) {
|
|
15
|
+
if (!Number.isInteger(minimum) || minimum < 1) throw new Error('invalid primer citation minimum');
|
|
16
|
+
const refs = countPrimerReferences(primer, sourcePaths);
|
|
17
|
+
if (refs.length < minimum) throw new Error(`THIN primer: ${refs.length} distinct source citations; requires ${minimum}. Existing primer preserved.`);
|
|
18
|
+
fs.writeFileSync(output, primer);
|
|
19
|
+
return refs;
|
|
20
|
+
}
|
|
@@ -43,7 +43,7 @@ const make = (id, statement, owner, contributors, implementation, tests, receipt
|
|
|
43
43
|
export const PRODUCT_INTEGRITY_OBLIGATIONS = Object.freeze([
|
|
44
44
|
make('S-1', 'One complete public corpus', 'CorpusGeneration', ['SourceCoverage'], ['scripts/source-coverage.mjs', 'scripts/corpus-reconcile.mjs', 'scripts/corpus-candidate.mjs'], ['tests/unit/source-coverage.test.mjs', 'tests/unit/corpus-reconcile.test.mjs'], ['ruvnet-brain-source-observation', 'ruvnet-brain-corpus-candidate']),
|
|
45
45
|
make('S-2', 'One immutable public release projection', 'ReleaseProjection', ['CorpusGeneration'], ['scripts/build-bundle.mjs', 'scripts/coverage-integrity.mjs', 'scripts/public-inventory.mjs'], ['tests/integration/build-bundle-fence.test.mjs', 'tests/unit/public-inventory.test.mjs'], ['ruvnet-brain-release-coverage']),
|
|
46
|
-
make('S-3', 'Recall at 10 is at least 98 percent and delta citations are complete', 'PublicVerification', ['SourceCoverage', 'CorpusGeneration'], ['scripts/retrieval-canary.mjs', 'scripts/
|
|
46
|
+
make('S-3', 'Recall at 10 is at least 98 percent and delta citations are complete', 'PublicVerification', ['SourceCoverage', 'CorpusGeneration'], ['scripts/retrieval-canary.mjs', 'scripts/candidate-host-evidence.mjs', 'scripts/prepublication-evidence.mjs', 'scripts/host-install-matrix.mjs', 'kb/retrieval-result.mjs'], ['tests/unit/retrieval-canary.test.mjs', 'tests/unit/candidate-host-evidence.test.mjs', 'tests/unit/candidate-retrieval-matrix.test.mjs', 'tests/unit/retrieval-result-boundary.test.mjs'], ['ruvnet-brain-retrieval-canary-plan', 'ruvnet-brain-retrieval-canary-receipt']),
|
|
47
47
|
make('S-4', 'Native nightly runs are ordered, idempotent, and retained within budget', 'RefreshLifecycle', [], ['bin/nightly-refresh.mjs', 'plugin/scripts/nightly-scheduler.mjs', 'scripts/nightly-two-run-proof.mjs', 'kb/refresh-run.mjs'], ['tests/unit/nightly-scheduler.test.mjs', 'tests/unit/nightly-two-run-proof.test.mjs'], ['ruvnet-brain-refresh-run', 'ruvnet-brain-native-two-run-nightly-proof']),
|
|
48
48
|
make('S-5', 'One active corpus preserves private stores and bounded recovery', 'RefreshLifecycle', ['CorpusGeneration'], ['kb/update-storage-transaction.mjs', 'kb/forge-update.mjs', 'kb/brain-profile.mjs', 'bin/install.mjs'], ['tests/unit/update-storage-transaction.test.mjs', 'tests/unit/brain-profile.test.mjs'], ['ruvnet-brain-update-storage-transaction', 'ruvnet-brain-installed-profile']),
|
|
49
49
|
make('S-6', 'Every supported OS and host loader converges on exact public bytes', 'HostConvergence', ['PublicVerification'], ['scripts/host-registry.mjs', 'scripts/host-install-matrix.mjs', 'scripts/public-verification-aggregate.mjs'], ['tests/unit/host-registry.test.mjs', 'tests/integration/dual-host-install.test.mjs'], ['ruvnet-brain-host-registry', 'ruvnet-brain-public-verification-leaf']),
|
|
@@ -141,9 +141,29 @@ export function renderProductIntegrityTraceMarkdown(contract = validateProductIn
|
|
|
141
141
|
}
|
|
142
142
|
const valueAfter = (argv, flag) => { const index = argv.indexOf(flag); return index >= 0 ? argv[index + 1] : null; };
|
|
143
143
|
export function runProductIntegrityCli(argv = process.argv.slice(2), io = { stdout: process.stdout }) {
|
|
144
|
-
const known = new Set(['--markdown', '--check-markdown', '--trace', '--source-sha', '--out', '--verify-trace']);
|
|
144
|
+
const known = new Set(['--markdown', '--check-markdown', '--check-source', '--trace', '--source-sha', '--out', '--verify-trace']);
|
|
145
145
|
if (argv.some((arg) => arg.startsWith('--') && !known.has(arg))) throw new Error('unknown product integrity argument');
|
|
146
146
|
if (!argv.length) { io.stdout.write(`${canonicalJson(validateProductIntegrityContract())}\n`); return 0; }
|
|
147
|
+
if (argv.length === 1 && argv[0] === '--check-source') {
|
|
148
|
+
const root = fs.realpathSync(io.root || fileURLToPath(new URL('..', import.meta.url)));
|
|
149
|
+
const missing = [], invalid = [];
|
|
150
|
+
const files = productIntegrityGovernedPaths();
|
|
151
|
+
for (const file of files) {
|
|
152
|
+
const absolute = path.join(root, file);
|
|
153
|
+
try {
|
|
154
|
+
const stat = fs.lstatSync(absolute);
|
|
155
|
+
const relative = path.relative(root, fs.realpathSync(absolute));
|
|
156
|
+
if (!stat.isFile() || stat.isSymbolicLink() || relative.startsWith(`..${path.sep}`)
|
|
157
|
+
|| relative === '..' || path.isAbsolute(relative)) invalid.push(file);
|
|
158
|
+
} catch (error) {
|
|
159
|
+
(error.code === 'ENOENT' ? missing : invalid).push(file);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const verdict = missing.length || invalid.length ? 'FAIL' : 'PASS';
|
|
163
|
+
io.stdout.write(`${canonicalJson({ verdict, scope: 'source-presence', behaviorVerified: false,
|
|
164
|
+
checked: files.length, missing, invalid })}\n`);
|
|
165
|
+
return verdict === 'PASS' ? 0 : 1;
|
|
166
|
+
}
|
|
147
167
|
if (argv.length === 1 && argv[0] === '--markdown') { io.stdout.write(renderProductIntegrityTraceMarkdown()); return 0; }
|
|
148
168
|
const check = valueAfter(argv, '--check-markdown');
|
|
149
169
|
if (check) { if (fs.readFileSync(path.resolve(check), 'utf8') !== renderProductIntegrityTraceMarkdown()) throw new Error('generated traceability Markdown differs'); io.stdout.write('product integrity Markdown: PASS\n'); return 0; }
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import crypto from 'node:crypto';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { execFileSync } from 'node:child_process';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { extractZip } from '../kb/zip-extract.mjs';
|
|
9
|
+
import { abandonPublicVerificationTransaction } from './release-transaction.mjs';
|
|
10
|
+
import { liveReleaseProvider } from './release-transaction-provider.mjs';
|
|
11
|
+
|
|
12
|
+
function regularJson(file, max = 131072) {
|
|
13
|
+
const stat = fs.lstatSync(file);
|
|
14
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > max) throw new Error('untrusted or oversized closure input');
|
|
15
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
16
|
+
}
|
|
17
|
+
const api = (endpoint) => JSON.parse(execFileSync('gh', ['api', endpoint], { encoding: 'utf8', timeout: 30000 }));
|
|
18
|
+
|
|
19
|
+
export function closureProvenance({ event, env, identity, failure, run, artifact, workflow }) {
|
|
20
|
+
const input = event.client_payload;
|
|
21
|
+
if (env.GITHUB_EVENT_NAME !== 'repository_dispatch' || event.action !== 'abandon-public-verification'
|
|
22
|
+
|| event.sender?.login !== env.GITHUB_ACTOR || event.repository?.full_name !== identity.repository
|
|
23
|
+
|| env.GITHUB_REPOSITORY !== identity.repository || input?.authorization !== 'abandon-published-not-verified') {
|
|
24
|
+
throw new Error('closure requires the explicitly authorized abandonment dispatch');
|
|
25
|
+
}
|
|
26
|
+
if (run.event !== 'repository_dispatch' || !Number.isSafeInteger(run.repository?.id)
|
|
27
|
+
|| run.repository.id !== event.repository.id || run.head_repository?.id !== run.repository.id
|
|
28
|
+
|| run.head_repository?.full_name !== identity.repository
|
|
29
|
+
|| !Number.isSafeInteger(workflow?.id) || run.workflow_id !== workflow.id || workflow.path !== run.path
|
|
30
|
+
|| run.id !== input.recovery_run_id || run.status !== 'completed' || run.conclusion !== 'failure'
|
|
31
|
+
|| run.path !== '.github/workflows/recover-public-verification.yml'
|
|
32
|
+
|| run.repository?.full_name !== identity.repository || !/^[a-f0-9]{40}$/.test(run.head_sha || '')
|
|
33
|
+
|| artifact.id !== input.failure_artifact_id || artifact.expired !== false
|
|
34
|
+
|| artifact.workflow_run?.id !== run.id || artifact.workflow_run?.head_sha !== run.head_sha
|
|
35
|
+
|| artifact.name !== `recovered-public-verification-${failure.os}-${identity.candidateSha}-${run.id}`) {
|
|
36
|
+
throw new Error('GitHub failed-run artifact provenance mismatch');
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
expected: input.expected,
|
|
40
|
+
reason: input.reason,
|
|
41
|
+
authorization: { actor: env.GITHUB_ACTOR, reference: input.authorization_reference,
|
|
42
|
+
event: env.GITHUB_EVENT_NAME, action: event.action,
|
|
43
|
+
dispatchRunId: Number(env.GITHUB_RUN_ID), verifierSha: env.GITHUB_SHA },
|
|
44
|
+
recovery: { repository: identity.repository, workflow: run.path, workflowId: workflow.id, repositoryId: run.repository.id, event: run.event, runId: run.id,
|
|
45
|
+
verifierSha: run.head_sha, conclusion: run.conclusion, artifactId: artifact.id, artifactName: artifact.name },
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Download the named GitHub artifact independently; a local JSON lookalike is not provenance.
|
|
50
|
+
async function failureFromArtifact(repository, artifactId) {
|
|
51
|
+
if (!Number.isSafeInteger(artifactId) || artifactId <= 0) throw new Error('invalid failed artifact ID');
|
|
52
|
+
const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'abandon-failed-artifact-'));
|
|
53
|
+
try {
|
|
54
|
+
const zip = path.join(temp, 'failure.zip');
|
|
55
|
+
fs.writeFileSync(zip, execFileSync('gh', ['api', `repos/${repository}/actions/artifacts/${artifactId}/zip`],
|
|
56
|
+
{ encoding: null, timeout: 60000, maxBuffer: 2 * 1024 * 1024 }), { flag: 'wx' });
|
|
57
|
+
const output = path.join(temp, 'contents');
|
|
58
|
+
const extracted = await extractZip(zip, output);
|
|
59
|
+
if (extracted.entryNames.length !== 1 || !/^(linux|macos|windows)\.json$/.test(extracted.entryNames[0])) {
|
|
60
|
+
throw new Error('failure artifact must contain exactly one OS failure receipt');
|
|
61
|
+
}
|
|
62
|
+
return regularJson(path.join(output, extracted.entryNames[0]));
|
|
63
|
+
} finally { fs.rmSync(temp, { recursive: true, force: true }); }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function abandonPublicVerification({ identityFile, outputFile, env = process.env,
|
|
67
|
+
readApi = api, readFailure = failureFromArtifact, adapter = liveReleaseProvider({ root: process.cwd() }),
|
|
68
|
+
publicKeyFile = 'keys/ruvnet-brain-signing.pub.pem' } = {}) {
|
|
69
|
+
if (!identityFile || !outputFile || !env.GITHUB_EVENT_PATH || !env.RUVNET_SIGNING_KEY) {
|
|
70
|
+
throw new Error('identity, output, dispatch event, and signing key are required');
|
|
71
|
+
}
|
|
72
|
+
if (fs.existsSync(outputFile)) throw new Error('refusing to overwrite closure output');
|
|
73
|
+
const identity = regularJson(identityFile);
|
|
74
|
+
if (identity.repository !== 'stuinfla/ruvnet-brain' || identity.package !== 'ruvnet-brain') throw new Error('unexpected release repository');
|
|
75
|
+
const event = regularJson(env.GITHUB_EVENT_PATH);
|
|
76
|
+
const input = event.client_payload;
|
|
77
|
+
if (![input?.recovery_run_id, input?.failure_artifact_id].every((id) => Number.isSafeInteger(id) && id > 0)) {
|
|
78
|
+
throw new Error('explicit recovery run and failed artifact IDs are required');
|
|
79
|
+
}
|
|
80
|
+
const run = await readApi(`repos/${identity.repository}/actions/runs/${input.recovery_run_id}`);
|
|
81
|
+
const artifact = await readApi(`repos/${identity.repository}/actions/artifacts/${input.failure_artifact_id}`);
|
|
82
|
+
const workflow = await readApi(`repos/${identity.repository}/actions/workflows/recover-public-verification.yml`);
|
|
83
|
+
const failure = await readFailure(identity.repository, input.failure_artifact_id);
|
|
84
|
+
const provenance = closureProvenance({ event, env, identity, failure, run, artifact, workflow });
|
|
85
|
+
const receipt = await abandonPublicVerificationTransaction({ identity, failure, ...provenance, adapter,
|
|
86
|
+
privateKey: crypto.createPrivateKey(env.RUVNET_SIGNING_KEY),
|
|
87
|
+
publicKey: crypto.createPublicKey(fs.readFileSync(publicKeyFile, 'utf8')) });
|
|
88
|
+
fs.mkdirSync(path.dirname(path.resolve(outputFile)), { recursive: true });
|
|
89
|
+
fs.writeFileSync(outputFile, `${JSON.stringify(receipt, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
|
|
90
|
+
return receipt;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const direct = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
94
|
+
if (direct) {
|
|
95
|
+
const arg = (name) => { const index = process.argv.indexOf(name); return index < 0 ? null : process.argv[index + 1]; };
|
|
96
|
+
try {
|
|
97
|
+
const receipt = await abandonPublicVerification({ identityFile: arg('--identity'), outputFile: arg('--out') });
|
|
98
|
+
console.log(JSON.stringify({ state: receipt.state, verdict: receipt.observation.verdict,
|
|
99
|
+
transactionId: receipt.transactionId, sequence: receipt.sequence, receiptDigest: receipt.receiptDigest }));
|
|
100
|
+
} catch (error) { console.error(`public-verification-abandon: ${error.message}`); process.exitCode = 1; }
|
|
101
|
+
}
|
|
@@ -6,6 +6,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
6
6
|
import { canonicalJson, digest } from './coverage-integrity.mjs';
|
|
7
7
|
import { retrievalOracleExpectationFromPlan, validateRetrievalOracleReview } from './independent-review-receipt.mjs';
|
|
8
8
|
import { validateRetrievalCanaryPlan, validateRetrievalCanaryReceipt } from './retrieval-canary.mjs';
|
|
9
|
+
import { validateNightlyProofReceipt } from './nightly-two-run-proof.mjs';
|
|
9
10
|
|
|
10
11
|
export const PUBLIC_VERIFICATION_OS = Object.freeze(['linux', 'macos', 'windows']);
|
|
11
12
|
export const PUBLIC_VERIFICATION_MODES = Object.freeze(['claude', 'codex', 'dual']);
|
|
@@ -19,7 +20,10 @@ function unsignedLeaf(leaf) {
|
|
|
19
20
|
return payload;
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
export function validatePublicVerificationLeaf(leaf) {
|
|
23
|
+
export function validatePublicVerificationLeaf(leaf, { publicKey } = {}) {
|
|
24
|
+
if (Object.hasOwn(leaf || {}, 'verifierSha') && !HEX40.test(String(leaf.verifierSha))) {
|
|
25
|
+
throw new Error('public verification leaf verifier SHA is invalid');
|
|
26
|
+
}
|
|
23
27
|
if (leaf?.schemaVersion !== 1 || leaf?.kind !== 'ruvnet-brain-public-verification-leaf'
|
|
24
28
|
|| !PUBLIC_VERIFICATION_OS.includes(leaf.os) || !PUBLIC_VERIFICATION_MODES.includes(leaf.mode)
|
|
25
29
|
|| !HEX40.test(String(leaf.sourceSha || '')) || !HEX64.test(String(leaf.artifactSha256 || ''))
|
|
@@ -33,6 +37,14 @@ export function validatePublicVerificationLeaf(leaf) {
|
|
|
33
37
|
if (leaf.publicBytes?.npmExact !== true || leaf.publicBytes?.githubExact !== true
|
|
34
38
|
|| leaf.publicBytes?.bundleExact !== true || leaf.installed?.version !== leaf.version
|
|
35
39
|
|| leaf.installed?.loaderVerified !== true) throw new Error(`${leaf.os}/${leaf.mode} public bytes or loader are unverified`);
|
|
40
|
+
if (leaf.acceptancePolicy !== undefined || leaf.searchTiming !== undefined) {
|
|
41
|
+
const timing = leaf.searchTiming;
|
|
42
|
+
if (leaf.acceptancePolicy !== 'stabilization-public-deadline-v1' || timing?.deadlineMs !== 30_000
|
|
43
|
+
|| ![timing.firstSearchMs, timing.broadMs].every((value) => typeof value === 'number'
|
|
44
|
+
&& Number.isFinite(value) && value >= 0 && value <= timing.deadlineMs)) {
|
|
45
|
+
throw new Error(`${leaf.os}/${leaf.mode} public search acceptance policy or timing is invalid`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
36
48
|
if (leaf.coverage?.verified !== true || leaf.coverage.eligibleCurrent !== leaf.coverage.eligibleTotal
|
|
37
49
|
|| leaf.coverage.gistCurrent !== leaf.coverage.gistTotal) throw new Error(`${leaf.os}/${leaf.mode} coverage is incomplete`);
|
|
38
50
|
if (!Array.isArray(leaf.untested) || leaf.untested.length || leaf.skipped !== 0 || leaf.unknown !== 0) {
|
|
@@ -49,12 +61,23 @@ export function validatePublicVerificationLeaf(leaf) {
|
|
|
49
61
|
|| retrieval.coverageGeneration !== leaf.coverageGeneration || retrieval.planSha256 !== leaf.canaryPlanSha256) {
|
|
50
62
|
throw new Error(`${leaf.os}/${leaf.mode} retrieval identity differs`);
|
|
51
63
|
}
|
|
64
|
+
if (leaf.mode === 'dual') {
|
|
65
|
+
if (!/^[1-9][0-9]*$/.test(String(leaf.workflowRunId || ''))) throw new Error('native nightly workflow run identity is missing');
|
|
66
|
+
const native = validateNightlyProofReceipt(leaf.nativeNightly, {
|
|
67
|
+
platform: { linux: 'linux', macos: 'darwin', windows: 'win32' }[leaf.os],
|
|
68
|
+
version: leaf.version, sourceSha: leaf.sourceSha, workflowRunId: leaf.workflowRunId,
|
|
69
|
+
packageSha256: leaf.artifactSha256, bundleSha256: leaf.bundleSha256, publicKey,
|
|
70
|
+
});
|
|
71
|
+
if (!native.ok) throw new Error(`${leaf.os}/dual native nightly proof failed: ${native.failures.join('; ')}`);
|
|
72
|
+
} else if (leaf.nativeNightly !== undefined) {
|
|
73
|
+
throw new Error('native nightly proof belongs only to the dual-host leaf');
|
|
74
|
+
}
|
|
52
75
|
return leaf;
|
|
53
76
|
}
|
|
54
77
|
|
|
55
|
-
export function createPublicVerificationLeaf(input) {
|
|
78
|
+
export function createPublicVerificationLeaf(input, options = {}) {
|
|
56
79
|
const payload = { schemaVersion: 1, kind: 'ruvnet-brain-public-verification-leaf', ...input };
|
|
57
|
-
return validatePublicVerificationLeaf({ ...payload, leafSha256: digest(payload) });
|
|
80
|
+
return validatePublicVerificationLeaf({ ...payload, leafSha256: digest(payload) }, options);
|
|
58
81
|
}
|
|
59
82
|
|
|
60
83
|
function aggregatePayload(aggregate) {
|
|
@@ -122,6 +145,7 @@ function validateReviewOracleAgainstPlan(review, retrievalPlan) {
|
|
|
122
145
|
|
|
123
146
|
const identityOf = (leaf) => ({
|
|
124
147
|
sourceSha: leaf.sourceSha,
|
|
148
|
+
...(Object.hasOwn(leaf, 'verifierSha') ? { verifierSha: leaf.verifierSha } : {}),
|
|
125
149
|
version: leaf.version,
|
|
126
150
|
tag: leaf.tag,
|
|
127
151
|
artifactSha256: leaf.artifactSha256,
|
|
@@ -133,11 +157,13 @@ const identityOf = (leaf) => ({
|
|
|
133
157
|
releaseTransactionId: leaf.releaseTransactionId,
|
|
134
158
|
});
|
|
135
159
|
|
|
136
|
-
export function buildPublicVerificationAggregate(leaves) {
|
|
160
|
+
export function buildPublicVerificationAggregate(leaves, options = {}) {
|
|
137
161
|
if (!Array.isArray(leaves) || leaves.length !== PUBLIC_VERIFICATION_OS.length * PUBLIC_VERIFICATION_MODES.length) {
|
|
138
162
|
throw new Error('public verification requires exactly nine leaves');
|
|
139
163
|
}
|
|
140
|
-
leaves.forEach(validatePublicVerificationLeaf);
|
|
164
|
+
leaves.forEach((leaf) => validatePublicVerificationLeaf(leaf, options));
|
|
165
|
+
const nativeRunIds = new Set(leaves.filter((leaf) => leaf.mode === 'dual').map((leaf) => String(leaf.workflowRunId)));
|
|
166
|
+
if (nativeRunIds.size !== 1) throw new Error('native nightly proofs span different workflow runs');
|
|
141
167
|
const byLane = new Map(leaves.map((leaf) => [`${leaf.os}/${leaf.mode}`, leaf]));
|
|
142
168
|
const required = PUBLIC_VERIFICATION_OS.flatMap((os) => PUBLIC_VERIFICATION_MODES.map((mode) => `${os}/${mode}`));
|
|
143
169
|
if (byLane.size !== required.length || required.some((lane) => !byLane.has(lane))) {
|
|
@@ -178,6 +204,7 @@ export function buildPublicVerificationAggregate(leaves) {
|
|
|
178
204
|
schemaVersion: 1,
|
|
179
205
|
kind: 'ruvnet-brain-public-verification-aggregate',
|
|
180
206
|
identity,
|
|
207
|
+
workflowRunId: [...nativeRunIds][0],
|
|
181
208
|
coverage: first.coverage,
|
|
182
209
|
lanes: required.map((lane) => ({ lane, leafSha256: byLane.get(lane).leafSha256 })),
|
|
183
210
|
evidence: { leaves: required.map((lane) => structuredClone(byLane.get(lane))) },
|
|
@@ -193,7 +220,7 @@ export function buildPublicVerificationAggregate(leaves) {
|
|
|
193
220
|
}
|
|
194
221
|
|
|
195
222
|
export function signPublicVerificationAggregate({ leaves }, privateKey) {
|
|
196
|
-
const payload = buildPublicVerificationAggregate(leaves);
|
|
223
|
+
const payload = buildPublicVerificationAggregate(leaves, { publicKey: crypto.createPublicKey(privateKey) });
|
|
197
224
|
const aggregateSha256 = digest(payload);
|
|
198
225
|
const signed = { ...payload, aggregateSha256 };
|
|
199
226
|
return { ...signed, signature: crypto.sign(null, Buffer.from(canonicalJson(signed)), privateKey).toString('base64') };
|
|
@@ -210,14 +237,14 @@ export function verifyPublicVerificationAggregate(aggregate, publicKey, expected
|
|
|
210
237
|
if (!Array.isArray(aggregate.evidence?.leaves)) {
|
|
211
238
|
throw new Error('public verification aggregate lacks raw leaf evidence');
|
|
212
239
|
}
|
|
213
|
-
const rebuilt = buildPublicVerificationAggregate(aggregate.evidence.leaves);
|
|
214
|
-
if (canonicalJson(rebuilt) !== canonicalJson(payload)) {
|
|
215
|
-
throw new Error('public verification aggregate differs from rebuilt raw evidence');
|
|
216
|
-
}
|
|
217
240
|
const signed = { ...payload, aggregateSha256: aggregate.aggregateSha256 };
|
|
218
241
|
if (!crypto.verify(null, Buffer.from(canonicalJson(signed)), publicKey, Buffer.from(aggregate.signature, 'base64'))) {
|
|
219
242
|
throw new Error('public verification aggregate signature mismatch');
|
|
220
243
|
}
|
|
244
|
+
const rebuilt = buildPublicVerificationAggregate(aggregate.evidence.leaves, { publicKey });
|
|
245
|
+
if (canonicalJson(rebuilt) !== canonicalJson(payload)) {
|
|
246
|
+
throw new Error('public verification aggregate differs from rebuilt raw evidence');
|
|
247
|
+
}
|
|
221
248
|
const required = PUBLIC_VERIFICATION_OS.flatMap((os) => PUBLIC_VERIFICATION_MODES.map((mode) => `${os}/${mode}`));
|
|
222
249
|
if (aggregate.lanes?.length !== required.length || new Set(aggregate.lanes.map(({ lane }) => lane)).size !== required.length
|
|
223
250
|
|| required.some((lane) => !aggregate.lanes.some((row) => row.lane === lane && HEX64.test(String(row.leafSha256 || ''))))) {
|
|
@@ -234,7 +261,11 @@ export function verifyPublicVerificationAggregate(aggregate, publicKey, expected
|
|
|
234
261
|
|| aggregate.retrievalOracle?.recordCount !== aggregate.evidence.leaves[0].retrievalPlan.denominator.eligibleStores.length) {
|
|
235
262
|
throw new Error('public verification aggregate retrieval oracle identity is incomplete');
|
|
236
263
|
}
|
|
237
|
-
if (expectedIdentity &&
|
|
264
|
+
if (expectedIdentity?.workflowRunId !== undefined && String(expectedIdentity.workflowRunId) !== aggregate.workflowRunId) {
|
|
265
|
+
throw new Error('public verification aggregate workflow run differs from the expected run');
|
|
266
|
+
}
|
|
267
|
+
const { workflowRunId: _expectedRun, ...expectedReleaseIdentity } = expectedIdentity || {};
|
|
268
|
+
if (expectedIdentity && canonicalJson(aggregate.identity) !== canonicalJson(expectedReleaseIdentity)) {
|
|
238
269
|
throw new Error('public verification aggregate identity differs from the release transaction');
|
|
239
270
|
}
|
|
240
271
|
return aggregate;
|
|
@@ -245,13 +276,13 @@ function parseCliArgs(argv) {
|
|
|
245
276
|
for (let index = 0; index < argv.length; index += 2) {
|
|
246
277
|
const flag = argv[index];
|
|
247
278
|
const value = argv[index + 1];
|
|
248
|
-
if (!['--lanes', '--out'].includes(flag) || !value) {
|
|
249
|
-
throw new Error('usage: public-verification-aggregate.mjs --lanes <dir> --out <file>');
|
|
279
|
+
if (!['--lanes', '--out', '--verifier-sha', '--workflow-run-id'].includes(flag) || !value) {
|
|
280
|
+
throw new Error('usage: public-verification-aggregate.mjs --lanes <dir> --out <file> --workflow-run-id <id>');
|
|
250
281
|
}
|
|
251
282
|
parsed[flag.slice(2)] = value;
|
|
252
283
|
}
|
|
253
|
-
if (!parsed.lanes || !parsed.out ||
|
|
254
|
-
throw new Error('usage: public-verification-aggregate.mjs --lanes <dir> --out <file>');
|
|
284
|
+
if (!parsed.lanes || !parsed.out || !/^[1-9][0-9]*$/.test(parsed['workflow-run-id'] || '')) {
|
|
285
|
+
throw new Error('usage: public-verification-aggregate.mjs --lanes <dir> --out <file> --workflow-run-id <id>');
|
|
255
286
|
}
|
|
256
287
|
return parsed;
|
|
257
288
|
}
|
|
@@ -265,20 +296,24 @@ function readExactJsonFiles(directory, count, label) {
|
|
|
265
296
|
.map((entry) => JSON.parse(fs.readFileSync(path.join(directory, entry.name), 'utf8')));
|
|
266
297
|
}
|
|
267
298
|
|
|
268
|
-
function readLaneLeaves(directory) {
|
|
299
|
+
function readLaneLeaves(directory, options = {}) {
|
|
269
300
|
const wrappers = readExactJsonFiles(directory, PUBLIC_VERIFICATION_OS.length, 'lane');
|
|
270
301
|
const byOs = new Map();
|
|
271
302
|
for (const wrapper of wrappers) {
|
|
272
303
|
const payload = { schemaVersion: wrapper?.schemaVersion, kind: wrapper?.kind, os: wrapper?.os,
|
|
304
|
+
...(Object.hasOwn(wrapper || {}, 'verifierSha') ? { verifierSha: wrapper.verifierSha, sourceSha: wrapper.sourceSha } : {}),
|
|
273
305
|
leaves: wrapper?.leaves };
|
|
274
306
|
if (payload.schemaVersion !== 1 || payload.kind !== 'ruvnet-brain-public-verification-os-lane'
|
|
275
307
|
|| !PUBLIC_VERIFICATION_OS.includes(payload.os) || byOs.has(payload.os)
|
|
276
308
|
|| !Array.isArray(payload.leaves) || payload.leaves.length !== PUBLIC_VERIFICATION_MODES.length
|
|
277
309
|
|| payload.leaves.some((leaf) => leaf?.os !== payload.os)
|
|
310
|
+
|| (Object.hasOwn(payload, 'verifierSha') && (!HEX40.test(String(payload.verifierSha))
|
|
311
|
+
|| !HEX40.test(String(payload.sourceSha))
|
|
312
|
+
|| payload.leaves.some((leaf) => leaf.verifierSha !== payload.verifierSha || leaf.sourceSha !== payload.sourceSha)))
|
|
278
313
|
|| digest(payload) !== wrapper.laneSha256) {
|
|
279
314
|
throw new Error('public verification OS lane wrapper is malformed, duplicated, or tampered');
|
|
280
315
|
}
|
|
281
|
-
payload.leaves.forEach(validatePublicVerificationLeaf);
|
|
316
|
+
payload.leaves.forEach((leaf) => validatePublicVerificationLeaf(leaf, options));
|
|
282
317
|
byOs.set(payload.os, payload.leaves);
|
|
283
318
|
}
|
|
284
319
|
if (PUBLIC_VERIFICATION_OS.some((osName) => !byOs.has(osName))) {
|
|
@@ -287,11 +322,16 @@ function readLaneLeaves(directory) {
|
|
|
287
322
|
return PUBLIC_VERIFICATION_OS.flatMap((osName) => byOs.get(osName));
|
|
288
323
|
}
|
|
289
324
|
|
|
290
|
-
export function generatePublicVerificationAggregate({ lanesDirectory, outputFile, privateKey }) {
|
|
325
|
+
export function generatePublicVerificationAggregate({ lanesDirectory, outputFile, privateKey, verifierSha, workflowRunId }) {
|
|
326
|
+
if (!/^[1-9][0-9]*$/.test(String(workflowRunId || ''))) throw new Error('expected workflow run ID is required');
|
|
291
327
|
if (!privateKey) throw new Error('RUVNET_SIGNING_KEY is required');
|
|
292
328
|
if (fs.existsSync(outputFile)) throw new Error(`refusing to overwrite existing aggregate: ${outputFile}`);
|
|
293
|
-
const leaves = readLaneLeaves(lanesDirectory);
|
|
329
|
+
const leaves = readLaneLeaves(lanesDirectory, { publicKey: crypto.createPublicKey(privateKey) });
|
|
294
330
|
const aggregate = signPublicVerificationAggregate({ leaves }, privateKey);
|
|
331
|
+
if (aggregate.workflowRunId !== String(workflowRunId)) throw new Error('public verification aggregate workflow run differs from the expected run');
|
|
332
|
+
if (verifierSha !== undefined && (!HEX40.test(String(verifierSha)) || aggregate.identity.verifierSha !== verifierSha)) {
|
|
333
|
+
throw new Error('public verification aggregate verifier SHA differs from the expected verifier');
|
|
334
|
+
}
|
|
295
335
|
fs.writeFileSync(outputFile, `${JSON.stringify(aggregate, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
|
|
296
336
|
return aggregate;
|
|
297
337
|
}
|
|
@@ -301,6 +341,8 @@ async function main() {
|
|
|
301
341
|
const aggregate = generatePublicVerificationAggregate({
|
|
302
342
|
lanesDirectory: options.lanes,
|
|
303
343
|
outputFile: options.out,
|
|
344
|
+
verifierSha: options['verifier-sha'],
|
|
345
|
+
workflowRunId: options['workflow-run-id'],
|
|
304
346
|
privateKey: process.env.RUVNET_SIGNING_KEY,
|
|
305
347
|
});
|
|
306
348
|
process.stdout.write(`${JSON.stringify({ ok: true, aggregateSha256: aggregate.aggregateSha256,
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import crypto from 'node:crypto';
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
5
6
|
import { finalizeReleaseTransaction } from './release-transaction.mjs';
|
|
6
7
|
import { liveReleaseProvider } from './release-transaction-provider.mjs';
|
|
7
8
|
|
|
@@ -22,6 +23,8 @@ export async function finalizePublicVerification({
|
|
|
22
23
|
identityFile,
|
|
23
24
|
aggregateFile,
|
|
24
25
|
outputFile,
|
|
26
|
+
verifierSha,
|
|
27
|
+
workflowRunId,
|
|
25
28
|
privatePem = process.env.RUVNET_SIGNING_KEY,
|
|
26
29
|
publicKeyFile = 'keys/ruvnet-brain-signing.pub.pem',
|
|
27
30
|
adapter = liveReleaseProvider({ root: process.cwd() }),
|
|
@@ -33,7 +36,7 @@ export async function finalizePublicVerification({
|
|
|
33
36
|
const identity = regularJson(identityFile, 'release identity');
|
|
34
37
|
const aggregate = regularJson(aggregateFile, 'public verification aggregate');
|
|
35
38
|
const publicKey = crypto.createPublicKey(fs.readFileSync(path.resolve(publicKeyFile), 'utf8'));
|
|
36
|
-
const receipt = await finalizeReleaseTransaction({ identity, aggregate, adapter,
|
|
39
|
+
const receipt = await finalizeReleaseTransaction({ identity, aggregate, adapter, verifierSha, workflowRunId,
|
|
37
40
|
privateKey: crypto.createPrivateKey(privatePem), publicKey, aggregatePublicKey: publicKey });
|
|
38
41
|
fs.mkdirSync(path.dirname(output), { recursive: true });
|
|
39
42
|
fs.writeFileSync(output, `${JSON.stringify(receipt, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
|
|
@@ -46,6 +49,8 @@ export async function main(args = process.argv.slice(2)) {
|
|
|
46
49
|
identityFile: argument(args, '--identity'),
|
|
47
50
|
aggregateFile: argument(args, '--aggregate'),
|
|
48
51
|
outputFile: argument(args, '--out'),
|
|
52
|
+
verifierSha: argument(args, '--verifier-sha') ?? undefined,
|
|
53
|
+
workflowRunId: argument(args, '--workflow-run-id'),
|
|
49
54
|
});
|
|
50
55
|
process.stdout.write(`${JSON.stringify({ state: receipt.state, transactionId: receipt.transactionId,
|
|
51
56
|
receiptDigest: receipt.receiptDigest }, null, 2)}\n`);
|
|
@@ -56,4 +61,5 @@ export async function main(args = process.argv.slice(2)) {
|
|
|
56
61
|
}
|
|
57
62
|
}
|
|
58
63
|
|
|
59
|
-
|
|
64
|
+
const invokedDirectly = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
65
|
+
if (invokedDirectly) process.exitCode = await main();
|