ruvnet-brain 4.3.9 → 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 +63 -8
- package/scripts/publication-receipt.mjs +109 -45
- 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
|
@@ -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,
|
|
@@ -29,12 +30,13 @@ function failedCanaryCases(retrieval) {
|
|
|
29
30
|
status !== 'COMPLETED' || retrievalHit !== true || (cohort === 'delta' && citationResolved !== true));
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
function publicVerificationFailure({ os, identity, failures, completedLeaves }) {
|
|
33
|
+
function publicVerificationFailure({ os, identity, verifierSha, failures, completedLeaves }) {
|
|
33
34
|
const payload = {
|
|
34
35
|
schemaVersion: 1,
|
|
35
36
|
kind: 'ruvnet-brain-public-verification-failure',
|
|
36
37
|
os,
|
|
37
38
|
sourceSha: identity.candidateSha,
|
|
39
|
+
...(verifierSha === undefined ? {} : { verifierSha }),
|
|
38
40
|
artifactSha256: identity.packageSha256,
|
|
39
41
|
bundleSha256: identity.bundleSha256,
|
|
40
42
|
failures: failures.map(({ mode, reason, retrieval }) => ({
|
|
@@ -102,6 +104,9 @@ function coverageProjection(coverage) {
|
|
|
102
104
|
|
|
103
105
|
export async function createPublicVerificationLane({
|
|
104
106
|
os,
|
|
107
|
+
verifierSha,
|
|
108
|
+
workflowRunId,
|
|
109
|
+
publicKey,
|
|
105
110
|
candidate,
|
|
106
111
|
publication,
|
|
107
112
|
identity,
|
|
@@ -112,6 +117,8 @@ export async function createPublicVerificationLane({
|
|
|
112
117
|
adapter,
|
|
113
118
|
} = {}) {
|
|
114
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');
|
|
115
122
|
validateReleaseIdentity(identity);
|
|
116
123
|
validatePublicEvidence({ candidate, publication, identity });
|
|
117
124
|
const registry = validateHostRegistry(hostRegistry);
|
|
@@ -132,7 +139,9 @@ export async function createPublicVerificationLane({
|
|
|
132
139
|
throw new Error('public verification lane adapter is incomplete');
|
|
133
140
|
}
|
|
134
141
|
const common = {
|
|
142
|
+
workflowRunId: String(workflowRunId),
|
|
135
143
|
sourceSha: identity.candidateSha,
|
|
144
|
+
...(verifierSha === undefined ? {} : { verifierSha }),
|
|
136
145
|
version: identity.version,
|
|
137
146
|
tag: identity.tag,
|
|
138
147
|
artifactSha256: identity.packageSha256,
|
|
@@ -169,8 +178,14 @@ export async function createPublicVerificationLane({
|
|
|
169
178
|
failures.push({ mode, reason: error.message, retrieval });
|
|
170
179
|
continue;
|
|
171
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
|
+
}
|
|
172
186
|
leaves.push(createPublicVerificationLeaf({
|
|
173
187
|
...common,
|
|
188
|
+
...(nativeNightly === undefined ? {} : { nativeNightly }),
|
|
174
189
|
os,
|
|
175
190
|
mode,
|
|
176
191
|
status: verifiedRetrieval.receiptSha256 === retrieval.receiptSha256 ? 'completed' : 'failed',
|
|
@@ -183,11 +198,11 @@ export async function createPublicVerificationLane({
|
|
|
183
198
|
untested: [],
|
|
184
199
|
skipped: 0,
|
|
185
200
|
unknown: 0,
|
|
186
|
-
}));
|
|
201
|
+
}, { publicKey }));
|
|
187
202
|
}
|
|
188
203
|
if (failures.length) {
|
|
189
204
|
const error = new Error(`retrieval canary acceptance failed for ${failures.map(({ mode }) => mode).join(', ')}`);
|
|
190
|
-
error.publicVerificationFailure = publicVerificationFailure({ os, identity, failures, completedLeaves: leaves });
|
|
205
|
+
error.publicVerificationFailure = publicVerificationFailure({ os, identity, verifierSha, failures, completedLeaves: leaves });
|
|
191
206
|
throw error;
|
|
192
207
|
}
|
|
193
208
|
if (canonicalJson(leaves.map(({ mode }) => mode)) !== canonicalJson(PUBLIC_VERIFICATION_MODES)) {
|
|
@@ -206,8 +221,22 @@ function regularJson(file, label) {
|
|
|
206
221
|
return { absolute, bytes: fs.readFileSync(absolute), value: JSON.parse(fs.readFileSync(absolute, 'utf8')) };
|
|
207
222
|
}
|
|
208
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
|
+
|
|
209
236
|
export async function generatePublicVerificationLane({
|
|
210
237
|
os,
|
|
238
|
+
verifierSha: expectedVerifierSha,
|
|
239
|
+
workflowRunId,
|
|
211
240
|
candidatePath,
|
|
212
241
|
identityPath,
|
|
213
242
|
coveragePath,
|
|
@@ -215,10 +244,19 @@ export async function generatePublicVerificationLane({
|
|
|
215
244
|
registryPath,
|
|
216
245
|
outPath,
|
|
217
246
|
root = process.cwd(),
|
|
218
|
-
|
|
247
|
+
candidateRoot = root,
|
|
248
|
+
adapter = livePublicationAdapter({ root, candidateRoot }),
|
|
219
249
|
} = {}) {
|
|
220
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);
|
|
221
258
|
const candidate = regularJson(candidatePath, 'candidate receipt');
|
|
259
|
+
validateCandidateSource(candidateRoot, candidate.value);
|
|
222
260
|
const identity = regularJson(identityPath, 'release identity');
|
|
223
261
|
const releaseCoverage = regularJson(coveragePath, 'release coverage');
|
|
224
262
|
const retrievalPlan = regularJson(planPath, 'retrieval plan');
|
|
@@ -234,6 +272,8 @@ export async function generatePublicVerificationLane({
|
|
|
234
272
|
const publication = JSON.parse(fs.readFileSync(publicationPath, 'utf8'));
|
|
235
273
|
const leaves = await createPublicVerificationLane({
|
|
236
274
|
os,
|
|
275
|
+
verifierSha,
|
|
276
|
+
workflowRunId,
|
|
237
277
|
candidate: candidate.value,
|
|
238
278
|
publication,
|
|
239
279
|
identity: identity.value,
|
|
@@ -244,15 +284,27 @@ export async function generatePublicVerificationLane({
|
|
|
244
284
|
hostRegistry: hostRegistry.value,
|
|
245
285
|
adapter,
|
|
246
286
|
});
|
|
247
|
-
|
|
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 };
|
|
248
291
|
const receipt = { ...payload, laneSha256: digest(payload) };
|
|
249
292
|
fs.mkdirSync(path.dirname(output), { recursive: true });
|
|
250
293
|
fs.writeFileSync(output, `${JSON.stringify(receipt, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
|
|
251
294
|
return receipt;
|
|
252
295
|
} catch (error) {
|
|
253
|
-
if (
|
|
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
|
+
})();
|
|
254
306
|
fs.mkdirSync(path.dirname(output), { recursive: true });
|
|
255
|
-
fs.writeFileSync(output, `${JSON.stringify(
|
|
307
|
+
fs.writeFileSync(output, `${JSON.stringify(failure, null, 2)}\n`,
|
|
256
308
|
{ flag: 'wx', mode: 0o600 });
|
|
257
309
|
}
|
|
258
310
|
throw error;
|
|
@@ -271,6 +323,9 @@ export async function main(args = process.argv.slice(2)) {
|
|
|
271
323
|
try {
|
|
272
324
|
const receipt = await generatePublicVerificationLane({
|
|
273
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,
|
|
274
329
|
candidatePath: argument(args, '--candidate'),
|
|
275
330
|
identityPath: argument(args, '--identity'),
|
|
276
331
|
coveragePath: argument(args, '--coverage'),
|