ruvnet-brain 4.3.8 → 4.3.10
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 +11 -2
- 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 +472 -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/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-two-run-proof.mjs +415 -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 +53 -19
- package/scripts/public-verification-finalizer.mjs +8 -2
- package/scripts/public-verification-lane.mjs +108 -8
- package/scripts/publication-receipt.mjs +173 -66
- 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-publication-handoff.mjs +101 -0
- package/scripts/release-qualification-contract.mjs +87 -0
- package/scripts/release-qualification.mjs +135 -0
- package/scripts/release-transaction.mjs +81 -1
- package/scripts/release.mjs +10 -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
|
@@ -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 || ''))
|
|
@@ -49,12 +53,23 @@ export function validatePublicVerificationLeaf(leaf) {
|
|
|
49
53
|
|| retrieval.coverageGeneration !== leaf.coverageGeneration || retrieval.planSha256 !== leaf.canaryPlanSha256) {
|
|
50
54
|
throw new Error(`${leaf.os}/${leaf.mode} retrieval identity differs`);
|
|
51
55
|
}
|
|
56
|
+
if (leaf.mode === 'dual') {
|
|
57
|
+
if (!/^[1-9][0-9]*$/.test(String(leaf.workflowRunId || ''))) throw new Error('native nightly workflow run identity is missing');
|
|
58
|
+
const native = validateNightlyProofReceipt(leaf.nativeNightly, {
|
|
59
|
+
platform: { linux: 'linux', macos: 'darwin', windows: 'win32' }[leaf.os],
|
|
60
|
+
version: leaf.version, sourceSha: leaf.sourceSha, workflowRunId: leaf.workflowRunId,
|
|
61
|
+
packageSha256: leaf.artifactSha256, bundleSha256: leaf.bundleSha256, publicKey,
|
|
62
|
+
});
|
|
63
|
+
if (!native.ok) throw new Error(`${leaf.os}/dual native nightly proof failed: ${native.failures.join('; ')}`);
|
|
64
|
+
} else if (leaf.nativeNightly !== undefined) {
|
|
65
|
+
throw new Error('native nightly proof belongs only to the dual-host leaf');
|
|
66
|
+
}
|
|
52
67
|
return leaf;
|
|
53
68
|
}
|
|
54
69
|
|
|
55
|
-
export function createPublicVerificationLeaf(input) {
|
|
70
|
+
export function createPublicVerificationLeaf(input, options = {}) {
|
|
56
71
|
const payload = { schemaVersion: 1, kind: 'ruvnet-brain-public-verification-leaf', ...input };
|
|
57
|
-
return validatePublicVerificationLeaf({ ...payload, leafSha256: digest(payload) });
|
|
72
|
+
return validatePublicVerificationLeaf({ ...payload, leafSha256: digest(payload) }, options);
|
|
58
73
|
}
|
|
59
74
|
|
|
60
75
|
function aggregatePayload(aggregate) {
|
|
@@ -122,6 +137,7 @@ function validateReviewOracleAgainstPlan(review, retrievalPlan) {
|
|
|
122
137
|
|
|
123
138
|
const identityOf = (leaf) => ({
|
|
124
139
|
sourceSha: leaf.sourceSha,
|
|
140
|
+
...(Object.hasOwn(leaf, 'verifierSha') ? { verifierSha: leaf.verifierSha } : {}),
|
|
125
141
|
version: leaf.version,
|
|
126
142
|
tag: leaf.tag,
|
|
127
143
|
artifactSha256: leaf.artifactSha256,
|
|
@@ -133,11 +149,13 @@ const identityOf = (leaf) => ({
|
|
|
133
149
|
releaseTransactionId: leaf.releaseTransactionId,
|
|
134
150
|
});
|
|
135
151
|
|
|
136
|
-
export function buildPublicVerificationAggregate(leaves) {
|
|
152
|
+
export function buildPublicVerificationAggregate(leaves, options = {}) {
|
|
137
153
|
if (!Array.isArray(leaves) || leaves.length !== PUBLIC_VERIFICATION_OS.length * PUBLIC_VERIFICATION_MODES.length) {
|
|
138
154
|
throw new Error('public verification requires exactly nine leaves');
|
|
139
155
|
}
|
|
140
|
-
leaves.forEach(validatePublicVerificationLeaf);
|
|
156
|
+
leaves.forEach((leaf) => validatePublicVerificationLeaf(leaf, options));
|
|
157
|
+
const nativeRunIds = new Set(leaves.filter((leaf) => leaf.mode === 'dual').map((leaf) => String(leaf.workflowRunId)));
|
|
158
|
+
if (nativeRunIds.size !== 1) throw new Error('native nightly proofs span different workflow runs');
|
|
141
159
|
const byLane = new Map(leaves.map((leaf) => [`${leaf.os}/${leaf.mode}`, leaf]));
|
|
142
160
|
const required = PUBLIC_VERIFICATION_OS.flatMap((os) => PUBLIC_VERIFICATION_MODES.map((mode) => `${os}/${mode}`));
|
|
143
161
|
if (byLane.size !== required.length || required.some((lane) => !byLane.has(lane))) {
|
|
@@ -178,6 +196,7 @@ export function buildPublicVerificationAggregate(leaves) {
|
|
|
178
196
|
schemaVersion: 1,
|
|
179
197
|
kind: 'ruvnet-brain-public-verification-aggregate',
|
|
180
198
|
identity,
|
|
199
|
+
workflowRunId: [...nativeRunIds][0],
|
|
181
200
|
coverage: first.coverage,
|
|
182
201
|
lanes: required.map((lane) => ({ lane, leafSha256: byLane.get(lane).leafSha256 })),
|
|
183
202
|
evidence: { leaves: required.map((lane) => structuredClone(byLane.get(lane))) },
|
|
@@ -193,7 +212,7 @@ export function buildPublicVerificationAggregate(leaves) {
|
|
|
193
212
|
}
|
|
194
213
|
|
|
195
214
|
export function signPublicVerificationAggregate({ leaves }, privateKey) {
|
|
196
|
-
const payload = buildPublicVerificationAggregate(leaves);
|
|
215
|
+
const payload = buildPublicVerificationAggregate(leaves, { publicKey: crypto.createPublicKey(privateKey) });
|
|
197
216
|
const aggregateSha256 = digest(payload);
|
|
198
217
|
const signed = { ...payload, aggregateSha256 };
|
|
199
218
|
return { ...signed, signature: crypto.sign(null, Buffer.from(canonicalJson(signed)), privateKey).toString('base64') };
|
|
@@ -210,14 +229,14 @@ export function verifyPublicVerificationAggregate(aggregate, publicKey, expected
|
|
|
210
229
|
if (!Array.isArray(aggregate.evidence?.leaves)) {
|
|
211
230
|
throw new Error('public verification aggregate lacks raw leaf evidence');
|
|
212
231
|
}
|
|
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
232
|
const signed = { ...payload, aggregateSha256: aggregate.aggregateSha256 };
|
|
218
233
|
if (!crypto.verify(null, Buffer.from(canonicalJson(signed)), publicKey, Buffer.from(aggregate.signature, 'base64'))) {
|
|
219
234
|
throw new Error('public verification aggregate signature mismatch');
|
|
220
235
|
}
|
|
236
|
+
const rebuilt = buildPublicVerificationAggregate(aggregate.evidence.leaves, { publicKey });
|
|
237
|
+
if (canonicalJson(rebuilt) !== canonicalJson(payload)) {
|
|
238
|
+
throw new Error('public verification aggregate differs from rebuilt raw evidence');
|
|
239
|
+
}
|
|
221
240
|
const required = PUBLIC_VERIFICATION_OS.flatMap((os) => PUBLIC_VERIFICATION_MODES.map((mode) => `${os}/${mode}`));
|
|
222
241
|
if (aggregate.lanes?.length !== required.length || new Set(aggregate.lanes.map(({ lane }) => lane)).size !== required.length
|
|
223
242
|
|| required.some((lane) => !aggregate.lanes.some((row) => row.lane === lane && HEX64.test(String(row.leafSha256 || ''))))) {
|
|
@@ -234,7 +253,11 @@ export function verifyPublicVerificationAggregate(aggregate, publicKey, expected
|
|
|
234
253
|
|| aggregate.retrievalOracle?.recordCount !== aggregate.evidence.leaves[0].retrievalPlan.denominator.eligibleStores.length) {
|
|
235
254
|
throw new Error('public verification aggregate retrieval oracle identity is incomplete');
|
|
236
255
|
}
|
|
237
|
-
if (expectedIdentity &&
|
|
256
|
+
if (expectedIdentity?.workflowRunId !== undefined && String(expectedIdentity.workflowRunId) !== aggregate.workflowRunId) {
|
|
257
|
+
throw new Error('public verification aggregate workflow run differs from the expected run');
|
|
258
|
+
}
|
|
259
|
+
const { workflowRunId: _expectedRun, ...expectedReleaseIdentity } = expectedIdentity || {};
|
|
260
|
+
if (expectedIdentity && canonicalJson(aggregate.identity) !== canonicalJson(expectedReleaseIdentity)) {
|
|
238
261
|
throw new Error('public verification aggregate identity differs from the release transaction');
|
|
239
262
|
}
|
|
240
263
|
return aggregate;
|
|
@@ -245,13 +268,13 @@ function parseCliArgs(argv) {
|
|
|
245
268
|
for (let index = 0; index < argv.length; index += 2) {
|
|
246
269
|
const flag = argv[index];
|
|
247
270
|
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>');
|
|
271
|
+
if (!['--lanes', '--out', '--verifier-sha', '--workflow-run-id'].includes(flag) || !value) {
|
|
272
|
+
throw new Error('usage: public-verification-aggregate.mjs --lanes <dir> --out <file> --workflow-run-id <id>');
|
|
250
273
|
}
|
|
251
274
|
parsed[flag.slice(2)] = value;
|
|
252
275
|
}
|
|
253
|
-
if (!parsed.lanes || !parsed.out ||
|
|
254
|
-
throw new Error('usage: public-verification-aggregate.mjs --lanes <dir> --out <file>');
|
|
276
|
+
if (!parsed.lanes || !parsed.out || !/^[1-9][0-9]*$/.test(parsed['workflow-run-id'] || '')) {
|
|
277
|
+
throw new Error('usage: public-verification-aggregate.mjs --lanes <dir> --out <file> --workflow-run-id <id>');
|
|
255
278
|
}
|
|
256
279
|
return parsed;
|
|
257
280
|
}
|
|
@@ -265,20 +288,24 @@ function readExactJsonFiles(directory, count, label) {
|
|
|
265
288
|
.map((entry) => JSON.parse(fs.readFileSync(path.join(directory, entry.name), 'utf8')));
|
|
266
289
|
}
|
|
267
290
|
|
|
268
|
-
function readLaneLeaves(directory) {
|
|
291
|
+
function readLaneLeaves(directory, options = {}) {
|
|
269
292
|
const wrappers = readExactJsonFiles(directory, PUBLIC_VERIFICATION_OS.length, 'lane');
|
|
270
293
|
const byOs = new Map();
|
|
271
294
|
for (const wrapper of wrappers) {
|
|
272
295
|
const payload = { schemaVersion: wrapper?.schemaVersion, kind: wrapper?.kind, os: wrapper?.os,
|
|
296
|
+
...(Object.hasOwn(wrapper || {}, 'verifierSha') ? { verifierSha: wrapper.verifierSha, sourceSha: wrapper.sourceSha } : {}),
|
|
273
297
|
leaves: wrapper?.leaves };
|
|
274
298
|
if (payload.schemaVersion !== 1 || payload.kind !== 'ruvnet-brain-public-verification-os-lane'
|
|
275
299
|
|| !PUBLIC_VERIFICATION_OS.includes(payload.os) || byOs.has(payload.os)
|
|
276
300
|
|| !Array.isArray(payload.leaves) || payload.leaves.length !== PUBLIC_VERIFICATION_MODES.length
|
|
277
301
|
|| payload.leaves.some((leaf) => leaf?.os !== payload.os)
|
|
302
|
+
|| (Object.hasOwn(payload, 'verifierSha') && (!HEX40.test(String(payload.verifierSha))
|
|
303
|
+
|| !HEX40.test(String(payload.sourceSha))
|
|
304
|
+
|| payload.leaves.some((leaf) => leaf.verifierSha !== payload.verifierSha || leaf.sourceSha !== payload.sourceSha)))
|
|
278
305
|
|| digest(payload) !== wrapper.laneSha256) {
|
|
279
306
|
throw new Error('public verification OS lane wrapper is malformed, duplicated, or tampered');
|
|
280
307
|
}
|
|
281
|
-
payload.leaves.forEach(validatePublicVerificationLeaf);
|
|
308
|
+
payload.leaves.forEach((leaf) => validatePublicVerificationLeaf(leaf, options));
|
|
282
309
|
byOs.set(payload.os, payload.leaves);
|
|
283
310
|
}
|
|
284
311
|
if (PUBLIC_VERIFICATION_OS.some((osName) => !byOs.has(osName))) {
|
|
@@ -287,11 +314,16 @@ function readLaneLeaves(directory) {
|
|
|
287
314
|
return PUBLIC_VERIFICATION_OS.flatMap((osName) => byOs.get(osName));
|
|
288
315
|
}
|
|
289
316
|
|
|
290
|
-
export function generatePublicVerificationAggregate({ lanesDirectory, outputFile, privateKey }) {
|
|
317
|
+
export function generatePublicVerificationAggregate({ lanesDirectory, outputFile, privateKey, verifierSha, workflowRunId }) {
|
|
318
|
+
if (!/^[1-9][0-9]*$/.test(String(workflowRunId || ''))) throw new Error('expected workflow run ID is required');
|
|
291
319
|
if (!privateKey) throw new Error('RUVNET_SIGNING_KEY is required');
|
|
292
320
|
if (fs.existsSync(outputFile)) throw new Error(`refusing to overwrite existing aggregate: ${outputFile}`);
|
|
293
|
-
const leaves = readLaneLeaves(lanesDirectory);
|
|
321
|
+
const leaves = readLaneLeaves(lanesDirectory, { publicKey: crypto.createPublicKey(privateKey) });
|
|
294
322
|
const aggregate = signPublicVerificationAggregate({ leaves }, privateKey);
|
|
323
|
+
if (aggregate.workflowRunId !== String(workflowRunId)) throw new Error('public verification aggregate workflow run differs from the expected run');
|
|
324
|
+
if (verifierSha !== undefined && (!HEX40.test(String(verifierSha)) || aggregate.identity.verifierSha !== verifierSha)) {
|
|
325
|
+
throw new Error('public verification aggregate verifier SHA differs from the expected verifier');
|
|
326
|
+
}
|
|
295
327
|
fs.writeFileSync(outputFile, `${JSON.stringify(aggregate, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
|
|
296
328
|
return aggregate;
|
|
297
329
|
}
|
|
@@ -301,6 +333,8 @@ async function main() {
|
|
|
301
333
|
const aggregate = generatePublicVerificationAggregate({
|
|
302
334
|
lanesDirectory: options.lanes,
|
|
303
335
|
outputFile: options.out,
|
|
336
|
+
verifierSha: options['verifier-sha'],
|
|
337
|
+
workflowRunId: options['workflow-run-id'],
|
|
304
338
|
privateKey: process.env.RUVNET_SIGNING_KEY,
|
|
305
339
|
});
|
|
306
340
|
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();
|
|
@@ -3,11 +3,12 @@ import crypto from 'node:crypto';
|
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import osModule from 'node:os';
|
|
5
5
|
import path from 'node:path';
|
|
6
|
+
import { execFileSync } from 'node:child_process';
|
|
6
7
|
import { fileURLToPath } from 'node:url';
|
|
7
8
|
import { canonicalJson, digest, validateCoverageLedger } from './coverage-integrity.mjs';
|
|
8
9
|
import { validateHostRegistry } from './host-registry.mjs';
|
|
9
10
|
import { RECEIPT_MODE_NAMES } from './host-install-matrix.mjs';
|
|
10
|
-
import { generatePublicationReceipt, livePublicationAdapter } from './publication-receipt.mjs';
|
|
11
|
+
import { generatePublicationReceipt, livePublicationAdapter, validateCandidateSource } from './publication-receipt.mjs';
|
|
11
12
|
import {
|
|
12
13
|
createPublicVerificationLeaf,
|
|
13
14
|
PUBLIC_VERIFICATION_MODES,
|
|
@@ -24,6 +25,33 @@ const HEX40 = /^[a-f0-9]{40}$/;
|
|
|
24
25
|
const HEX64 = /^[a-f0-9]{64}$/;
|
|
25
26
|
const normalizedDigest = (value) => String(value || '').replace(/^sha256:/, '');
|
|
26
27
|
|
|
28
|
+
function failedCanaryCases(retrieval) {
|
|
29
|
+
return retrieval.cases.filter(({ cohort, status, retrievalHit, citationResolved }) =>
|
|
30
|
+
status !== 'COMPLETED' || retrievalHit !== true || (cohort === 'delta' && citationResolved !== true));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function publicVerificationFailure({ os, identity, verifierSha, failures, completedLeaves }) {
|
|
34
|
+
const payload = {
|
|
35
|
+
schemaVersion: 1,
|
|
36
|
+
kind: 'ruvnet-brain-public-verification-failure',
|
|
37
|
+
os,
|
|
38
|
+
sourceSha: identity.candidateSha,
|
|
39
|
+
...(verifierSha === undefined ? {} : { verifierSha }),
|
|
40
|
+
artifactSha256: identity.packageSha256,
|
|
41
|
+
bundleSha256: identity.bundleSha256,
|
|
42
|
+
failures: failures.map(({ mode, reason, retrieval }) => ({
|
|
43
|
+
mode,
|
|
44
|
+
reason,
|
|
45
|
+
metrics: retrieval.metrics,
|
|
46
|
+
failedCaseIds: failedCanaryCases(retrieval).map(({ id }) => id),
|
|
47
|
+
failedCases: failedCanaryCases(retrieval),
|
|
48
|
+
retrievalReceiptSha256: retrieval.receiptSha256,
|
|
49
|
+
})),
|
|
50
|
+
completedLeaves: completedLeaves.map(({ mode, leafSha256 }) => ({ mode, leafSha256 })),
|
|
51
|
+
};
|
|
52
|
+
return { ...payload, failureSha256: digest(payload) };
|
|
53
|
+
}
|
|
54
|
+
|
|
27
55
|
function validateReleaseIdentity(identity) {
|
|
28
56
|
if (identity?.repository !== 'stuinfla/ruvnet-brain' || identity?.package !== 'ruvnet-brain'
|
|
29
57
|
|| !HEX40.test(String(identity.candidateSha || '')) || !HEX64.test(String(identity.payloadId || ''))
|
|
@@ -76,6 +104,9 @@ function coverageProjection(coverage) {
|
|
|
76
104
|
|
|
77
105
|
export async function createPublicVerificationLane({
|
|
78
106
|
os,
|
|
107
|
+
verifierSha,
|
|
108
|
+
workflowRunId,
|
|
109
|
+
publicKey,
|
|
79
110
|
candidate,
|
|
80
111
|
publication,
|
|
81
112
|
identity,
|
|
@@ -86,6 +117,8 @@ export async function createPublicVerificationLane({
|
|
|
86
117
|
adapter,
|
|
87
118
|
} = {}) {
|
|
88
119
|
if (!PUBLIC_VERIFICATION_OS.includes(os)) throw new Error(`unsupported public verification OS: ${os || '(missing)'}`);
|
|
120
|
+
if (verifierSha !== undefined && !HEX40.test(String(verifierSha))) throw new Error('verifier SHA must be a full commit identity');
|
|
121
|
+
if (!/^[1-9][0-9]*$/.test(String(workflowRunId || ''))) throw new Error('workflow run ID is required');
|
|
89
122
|
validateReleaseIdentity(identity);
|
|
90
123
|
validatePublicEvidence({ candidate, publication, identity });
|
|
91
124
|
const registry = validateHostRegistry(hostRegistry);
|
|
@@ -106,7 +139,9 @@ export async function createPublicVerificationLane({
|
|
|
106
139
|
throw new Error('public verification lane adapter is incomplete');
|
|
107
140
|
}
|
|
108
141
|
const common = {
|
|
142
|
+
workflowRunId: String(workflowRunId),
|
|
109
143
|
sourceSha: identity.candidateSha,
|
|
144
|
+
...(verifierSha === undefined ? {} : { verifierSha }),
|
|
110
145
|
version: identity.version,
|
|
111
146
|
tag: identity.tag,
|
|
112
147
|
artifactSha256: identity.packageSha256,
|
|
@@ -118,6 +153,7 @@ export async function createPublicVerificationLane({
|
|
|
118
153
|
releaseTransactionId: transactionIdFor(identity),
|
|
119
154
|
};
|
|
120
155
|
const leaves = [];
|
|
156
|
+
const failures = [];
|
|
121
157
|
for (const mode of PUBLIC_VERIFICATION_MODES) {
|
|
122
158
|
const installed = publication.installed?.[RECEIPT_MODE_NAMES[mode]];
|
|
123
159
|
if (installed?.status !== 'PASS' || installed.doctorExit !== 0 || installed.version !== identity.version
|
|
@@ -132,12 +168,24 @@ export async function createPublicVerificationLane({
|
|
|
132
168
|
search: ({ query, k }) => adapter.searchInstalled({ mode, query, k }),
|
|
133
169
|
citationResolver: (matched, expected) => adapter.resolveInstalledCitation({ mode, matched, expected }),
|
|
134
170
|
});
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
171
|
+
let verifiedRetrieval;
|
|
172
|
+
try {
|
|
173
|
+
verifiedRetrieval = validateRetrievalCanaryReceipt(
|
|
174
|
+
retrieval,
|
|
175
|
+
{ plan: retrievalPlan, requireAcceptance: true },
|
|
176
|
+
);
|
|
177
|
+
} catch (error) {
|
|
178
|
+
failures.push({ mode, reason: error.message, retrieval });
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
let nativeNightly;
|
|
182
|
+
if (mode === 'dual') {
|
|
183
|
+
if (typeof adapter.runNativeNightly !== 'function') throw new Error('native nightly proof adapter is required');
|
|
184
|
+
nativeNightly = await adapter.runNativeNightly({ identity, workflowRunId: String(workflowRunId) });
|
|
185
|
+
}
|
|
139
186
|
leaves.push(createPublicVerificationLeaf({
|
|
140
187
|
...common,
|
|
188
|
+
...(nativeNightly === undefined ? {} : { nativeNightly }),
|
|
141
189
|
os,
|
|
142
190
|
mode,
|
|
143
191
|
status: verifiedRetrieval.receiptSha256 === retrieval.receiptSha256 ? 'completed' : 'failed',
|
|
@@ -150,7 +198,12 @@ export async function createPublicVerificationLane({
|
|
|
150
198
|
untested: [],
|
|
151
199
|
skipped: 0,
|
|
152
200
|
unknown: 0,
|
|
153
|
-
}));
|
|
201
|
+
}, { publicKey }));
|
|
202
|
+
}
|
|
203
|
+
if (failures.length) {
|
|
204
|
+
const error = new Error(`retrieval canary acceptance failed for ${failures.map(({ mode }) => mode).join(', ')}`);
|
|
205
|
+
error.publicVerificationFailure = publicVerificationFailure({ os, identity, verifierSha, failures, completedLeaves: leaves });
|
|
206
|
+
throw error;
|
|
154
207
|
}
|
|
155
208
|
if (canonicalJson(leaves.map(({ mode }) => mode)) !== canonicalJson(PUBLIC_VERIFICATION_MODES)) {
|
|
156
209
|
throw new Error('public verification lane modes are incomplete');
|
|
@@ -168,8 +221,22 @@ function regularJson(file, label) {
|
|
|
168
221
|
return { absolute, bytes: fs.readFileSync(absolute), value: JSON.parse(fs.readFileSync(absolute, 'utf8')) };
|
|
169
222
|
}
|
|
170
223
|
|
|
224
|
+
export function resolveVerifierSha(root, expected) {
|
|
225
|
+
const options = { cwd: root, encoding: 'utf8' };
|
|
226
|
+
const actual = execFileSync('git', ['rev-parse', 'HEAD'], options).trim();
|
|
227
|
+
if (!HEX40.test(actual) || (expected !== undefined && expected !== actual)) {
|
|
228
|
+
throw new Error('verifier SHA differs from the executing checkout');
|
|
229
|
+
}
|
|
230
|
+
if (execFileSync('git', ['status', '--porcelain', '--untracked-files=no'], options).trim()) {
|
|
231
|
+
throw new Error('verifier checkout has tracked changes');
|
|
232
|
+
}
|
|
233
|
+
return actual;
|
|
234
|
+
}
|
|
235
|
+
|
|
171
236
|
export async function generatePublicVerificationLane({
|
|
172
237
|
os,
|
|
238
|
+
verifierSha: expectedVerifierSha,
|
|
239
|
+
workflowRunId,
|
|
173
240
|
candidatePath,
|
|
174
241
|
identityPath,
|
|
175
242
|
coveragePath,
|
|
@@ -177,10 +244,19 @@ export async function generatePublicVerificationLane({
|
|
|
177
244
|
registryPath,
|
|
178
245
|
outPath,
|
|
179
246
|
root = process.cwd(),
|
|
180
|
-
|
|
247
|
+
candidateRoot = root,
|
|
248
|
+
adapter = livePublicationAdapter({ root, candidateRoot }),
|
|
181
249
|
} = {}) {
|
|
182
250
|
if (os !== hostOperatingSystem()) throw new Error(`runner OS ${hostOperatingSystem() || process.platform} cannot produce ${os || '(missing)'}`);
|
|
251
|
+
const rootDirectory = fs.statSync(root, { bigint: true });
|
|
252
|
+
const scriptDirectory = fs.statSync(fileURLToPath(new URL('..', import.meta.url)), { bigint: true });
|
|
253
|
+
if (!rootDirectory.isDirectory() || rootDirectory.ino <= 0n
|
|
254
|
+
|| rootDirectory.dev !== scriptDirectory.dev || rootDirectory.ino !== scriptDirectory.ino) {
|
|
255
|
+
throw new Error('verifier root differs from the executing script checkout');
|
|
256
|
+
}
|
|
257
|
+
const verifierSha = resolveVerifierSha(root, expectedVerifierSha);
|
|
183
258
|
const candidate = regularJson(candidatePath, 'candidate receipt');
|
|
259
|
+
validateCandidateSource(candidateRoot, candidate.value);
|
|
184
260
|
const identity = regularJson(identityPath, 'release identity');
|
|
185
261
|
const releaseCoverage = regularJson(coveragePath, 'release coverage');
|
|
186
262
|
const retrievalPlan = regularJson(planPath, 'retrieval plan');
|
|
@@ -196,6 +272,8 @@ export async function generatePublicVerificationLane({
|
|
|
196
272
|
const publication = JSON.parse(fs.readFileSync(publicationPath, 'utf8'));
|
|
197
273
|
const leaves = await createPublicVerificationLane({
|
|
198
274
|
os,
|
|
275
|
+
verifierSha,
|
|
276
|
+
workflowRunId,
|
|
199
277
|
candidate: candidate.value,
|
|
200
278
|
publication,
|
|
201
279
|
identity: identity.value,
|
|
@@ -206,11 +284,30 @@ export async function generatePublicVerificationLane({
|
|
|
206
284
|
hostRegistry: hostRegistry.value,
|
|
207
285
|
adapter,
|
|
208
286
|
});
|
|
209
|
-
|
|
287
|
+
resolveVerifierSha(root, verifierSha);
|
|
288
|
+
validateCandidateSource(candidateRoot, candidate.value);
|
|
289
|
+
const payload = { schemaVersion: 1, kind: 'ruvnet-brain-public-verification-os-lane', os,
|
|
290
|
+
verifierSha, sourceSha: identity.value.candidateSha, leaves };
|
|
210
291
|
const receipt = { ...payload, laneSha256: digest(payload) };
|
|
211
292
|
fs.mkdirSync(path.dirname(output), { recursive: true });
|
|
212
293
|
fs.writeFileSync(output, `${JSON.stringify(receipt, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
|
|
213
294
|
return receipt;
|
|
295
|
+
} catch (error) {
|
|
296
|
+
if (!fs.existsSync(output)) {
|
|
297
|
+
const failure = error.publicVerificationFailure || (() => {
|
|
298
|
+
const payload = { schemaVersion: 1, kind: 'ruvnet-brain-public-verification-failure',
|
|
299
|
+
os, verifierSha, sourceSha: identity.value.candidateSha,
|
|
300
|
+
artifactSha256: identity.value.packageSha256, bundleSha256: identity.value.bundleSha256,
|
|
301
|
+
workflowRunId: String(workflowRunId || ''),
|
|
302
|
+
failures: [{ stage: 'public-install-and-native-update', reason: String(error.message).slice(0, 2000) }],
|
|
303
|
+
completedLeaves: [] };
|
|
304
|
+
return { ...payload, failureSha256: digest(payload) };
|
|
305
|
+
})();
|
|
306
|
+
fs.mkdirSync(path.dirname(output), { recursive: true });
|
|
307
|
+
fs.writeFileSync(output, `${JSON.stringify(failure, null, 2)}\n`,
|
|
308
|
+
{ flag: 'wx', mode: 0o600 });
|
|
309
|
+
}
|
|
310
|
+
throw error;
|
|
214
311
|
} finally {
|
|
215
312
|
await adapter.dispose?.();
|
|
216
313
|
fs.rmSync(temp, { recursive: true, force: true });
|
|
@@ -226,6 +323,9 @@ export async function main(args = process.argv.slice(2)) {
|
|
|
226
323
|
try {
|
|
227
324
|
const receipt = await generatePublicVerificationLane({
|
|
228
325
|
os: argument(args, '--os'),
|
|
326
|
+
verifierSha: argument(args, '--verifier-sha') ?? undefined,
|
|
327
|
+
workflowRunId: argument(args, '--workflow-run-id'),
|
|
328
|
+
candidateRoot: argument(args, '--candidate-root') ?? undefined,
|
|
229
329
|
candidatePath: argument(args, '--candidate'),
|
|
230
330
|
identityPath: argument(args, '--identity'),
|
|
231
331
|
coveragePath: argument(args, '--coverage'),
|