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.
Files changed (83) hide show
  1. package/README.md +26 -9
  2. package/bin/install.mjs +646 -366
  3. package/bin/nightly-refresh.mjs +115 -0
  4. package/kb/brain-profile.mjs +118 -32
  5. package/kb/lifecycle-evidence-retention.mjs +237 -0
  6. package/kb/refresh-run.mjs +367 -0
  7. package/kb/retrieval-result.mjs +39 -0
  8. package/kb/update-storage-transaction.mjs +439 -0
  9. package/package.json +11 -2
  10. package/plugin/.claude-plugin/plugin.json +1 -1
  11. package/plugin/.codex-plugin/plugin.json +1 -1
  12. package/plugin/hooks/codex-hooks.json +2 -165
  13. package/plugin/hooks/hook-contracts.json +4 -65
  14. package/plugin/hooks/hooks.json +2 -208
  15. package/plugin/scripts/adr-currency-gate.mjs +29 -19
  16. package/plugin/scripts/capability-registry.mjs +10 -83
  17. package/plugin/scripts/codex-hook-adapter.mjs +62 -9
  18. package/plugin/scripts/codex-hook-wrapper.mjs +19 -1
  19. package/plugin/scripts/continuation-gate.mjs +30 -28
  20. package/plugin/scripts/continuation-objective.mjs +34 -0
  21. package/plugin/scripts/development-maintenance.mjs +49 -0
  22. package/plugin/scripts/hook-shim.mjs +3 -0
  23. package/plugin/scripts/learn-flush.mjs +6 -1
  24. package/plugin/scripts/lesson-gate.mjs +5 -2
  25. package/plugin/scripts/lesson-presentation.mjs +5 -0
  26. package/plugin/scripts/md-stamp.mjs +109 -6
  27. package/plugin/scripts/memory-doctor.mjs +3 -7
  28. package/plugin/scripts/nightly-controller.mjs +9 -27
  29. package/plugin/scripts/nightly-scheduler.mjs +472 -0
  30. package/plugin/scripts/project-progression-contract.mjs +4 -4
  31. package/plugin/scripts/project-progression-store.mjs +35 -6
  32. package/plugin/scripts/ruflo-bin.mjs +20 -0
  33. package/plugin/scripts/session-snapshot-contract.mjs +6 -1
  34. package/plugin/scripts/version-bump-gate.sh +14 -2
  35. package/plugin/skills/ruvnet-brain/PLAYBOOK.md +15 -23
  36. package/scripts/build-bundle.mjs +8 -0
  37. package/scripts/build-primer.mjs +2 -3
  38. package/scripts/candidate-host-evidence.mjs +59 -41
  39. package/scripts/ci/build-fixture-kb.mjs +59 -2
  40. package/scripts/ci/mutate-hook-timeout.mjs +24 -47
  41. package/scripts/ci/stranger-scenario.mjs +13 -28
  42. package/scripts/claims-verify.mjs +219 -21
  43. package/scripts/console-engine.mjs +4 -18
  44. package/scripts/console-runtime-identity.mjs +6 -0
  45. package/scripts/development-maintenance.mjs +47 -0
  46. package/scripts/development-push-check.mjs +35 -0
  47. package/scripts/distill-project.mjs +24 -47
  48. package/scripts/doc-currency.mjs +91 -19
  49. package/scripts/git-hooks/pre-push +4 -162
  50. package/scripts/health-repair.mjs +22 -4
  51. package/scripts/hook-retirement-check.mjs +22 -0
  52. package/scripts/host-install-matrix.mjs +125 -61
  53. package/scripts/integration-evidence.mjs +11 -6
  54. package/scripts/learning-replay-fixture.mjs +23 -2
  55. package/scripts/nightly-two-run-proof.mjs +415 -0
  56. package/scripts/nightly-watchdog.mjs +17 -2
  57. package/scripts/npm-invocation.mjs +20 -0
  58. package/scripts/prepublication-evidence.mjs +42 -6
  59. package/scripts/primer-grounding.mjs +20 -0
  60. package/scripts/product-integrity-contract.mjs +22 -2
  61. package/scripts/public-verification-abandon.mjs +101 -0
  62. package/scripts/public-verification-aggregate.mjs +53 -19
  63. package/scripts/public-verification-finalizer.mjs +8 -2
  64. package/scripts/public-verification-lane.mjs +63 -8
  65. package/scripts/publication-receipt.mjs +109 -45
  66. package/scripts/published-surface-probe.mjs +10 -2
  67. package/scripts/qa-contract.mjs +57 -0
  68. package/scripts/qa-lanes.mjs +40 -0
  69. package/scripts/qa-runner.mjs +54 -64
  70. package/scripts/qe/ux-suite.mjs +10 -34
  71. package/scripts/qualified-candidate-check.mjs +124 -0
  72. package/scripts/release-abort-stale.mjs +2 -2
  73. package/scripts/release-projection.mjs +20 -1
  74. package/scripts/release-qualification-contract.mjs +87 -0
  75. package/scripts/release-qualification.mjs +135 -0
  76. package/scripts/release-transaction.mjs +81 -1
  77. package/scripts/remedy-registry.mjs +6 -13
  78. package/scripts/retrieval-canary.mjs +91 -18
  79. package/scripts/selfcheck.mjs +12 -4
  80. package/scripts/snapshot-freshness.mjs +58 -0
  81. package/scripts/stack-sync.mjs +41 -43
  82. package/scripts/staged-host-verifier.mjs +63 -14
  83. 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/packed-retrieval-canary.mjs'], ['tests/unit/retrieval-canary.test.mjs', 'tests/unit/packed-retrieval-canary.test.mjs'], ['ruvnet-brain-retrieval-canary-plan', 'ruvnet-brain-retrieval-canary-receipt']),
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 && canonicalJson(aggregate.identity) !== canonicalJson(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 || Object.keys(parsed).length !== 2) {
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
- if (path.resolve(process.argv[1] || '') === path.resolve(new URL(import.meta.url).pathname)) process.exitCode = await main();
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
- adapter = livePublicationAdapter({ root }),
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
- const payload = { schemaVersion: 1, kind: 'ruvnet-brain-public-verification-os-lane', os, leaves };
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 (error.publicVerificationFailure && !fs.existsSync(output)) {
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(error.publicVerificationFailure, null, 2)}\n`,
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'),