ruvnet-brain 4.0.7 → 4.0.8

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.
@@ -10,6 +10,7 @@ import { Readable } from 'node:stream';
10
10
  import { spawn, spawnSync } from 'node:child_process';
11
11
  import { pathToFileURL } from 'node:url';
12
12
  import { evaluateCandidateReceipt, evaluatePublicationReceipt } from './release-proof.mjs';
13
+ import { verifyPayload } from './release-payload.mjs';
13
14
 
14
15
  const REPO = 'stuinfla/ruvnet-brain';
15
16
  const PACKAGE = 'ruvnet-brain';
@@ -28,6 +29,22 @@ function command(name, args, options = {}) {
28
29
  return String(result.stdout || '').trim();
29
30
  }
30
31
 
32
+ function locate(name) {
33
+ try { return command('which', [name]); } catch { return null; }
34
+ }
35
+
36
+ function isolatedPath(mode, temp) {
37
+ const bin = path.join(temp, `bin-${mode}`);
38
+ fs.mkdirSync(bin);
39
+ const hosts = mode === 'claudeOnly' ? ['claude'] : mode === 'codexOnly' ? ['codex'] : ['claude', 'codex'];
40
+ for (const name of ['node', 'npm', ...hosts]) {
41
+ const target = locate(name);
42
+ if (!target) throw new Error(`${name} CLI unavailable for ${mode} public host fixture`);
43
+ fs.symlinkSync(target, path.join(bin, name));
44
+ }
45
+ return `${bin}:/usr/bin:/bin`;
46
+ }
47
+
31
48
  async function download(url, destination, headers = {}) {
32
49
  const response = await fetch(url, {
33
50
  redirect: 'follow',
@@ -66,6 +83,23 @@ function findManifest(root, relative, version) {
66
83
  return match;
67
84
  }
68
85
 
86
+ function findMcpServer(home) {
87
+ const matches = [];
88
+ const visit = (directory, depth = 0) => {
89
+ if (depth > 12 || !fs.existsSync(directory)) return;
90
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
91
+ const child = path.join(directory, entry.name);
92
+ if (entry.isSymbolicLink()) continue;
93
+ if (entry.isDirectory()) visit(child, depth + 1);
94
+ else if (entry.isFile() && entry.name === 'server.mjs' && child.includes(`${path.sep}mcp${path.sep}`)) matches.push(child);
95
+ }
96
+ };
97
+ visit(home);
98
+ const server = matches.find((file) => file.includes('ruvnet-brain'));
99
+ if (!server) throw new Error('installed host registration has no ruvnet-brain MCP server');
100
+ return server;
101
+ }
102
+
69
103
  export function assertInstalledPayload(sourceRoot, installedRoot) {
70
104
  let checked = 0;
71
105
  const visit = (source, relative = '') => {
@@ -143,6 +177,7 @@ function rpcSearch(server, env, query, timeoutMs = DEADLINE_MS) {
143
177
 
144
178
  export function livePublicationAdapter({ root = process.cwd() } = {}) {
145
179
  let installContext = null;
180
+ let installTemp = null;
146
181
  return {
147
182
  async downloadNpm({ version, destination }) {
148
183
  const metadata = JSON.parse(command('npm', ['view', `${PACKAGE}@${version}`, '--json']));
@@ -169,42 +204,66 @@ export function livePublicationAdapter({ root = process.cwd() } = {}) {
169
204
 
170
205
  async installHosts({ artifactPath, artifactSha256, version }) {
171
206
  const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'ruvnet-public-install-'));
207
+ installTemp = temp;
172
208
  const packageRoot = path.join(temp, 'package');
173
209
  command('tar', ['-xzf', artifactPath, '-C', temp]);
174
- const home = path.join(temp, 'home');
175
- const codexHome = path.join(home, '.codex');
176
- const brainHome = path.join(home, '.cache', 'ruvnet-brain');
177
- const kb = path.join(brainHome, 'kb');
178
- fs.mkdirSync(codexHome, { recursive: true });
179
- const env = {
180
- ...process.env,
181
- HOME: home,
182
- CODEX_HOME: codexHome,
183
- RUVNET_BRAIN_HOME: brainHome,
184
- RUVNET_BRAIN_KB: kb,
185
- RUVNET_STRICT_INSTALL: '1',
186
- RUVNET_BRAIN_PROFILE: 'complete',
187
- CI: 'true',
188
- };
189
- command(process.execPath, [
190
- path.join(packageRoot, 'bin', 'install.mjs'), '--yes', '--force', '--version', `v${version}`,
191
- '--no-nightly-prompt', '--no-telemetry', '--no-stack', '--no-enhance', '--no-statusline',
192
- ], { env, cwd: packageRoot, timeout: 1_200_000, maxBuffer: 32 * 1024 * 1024 });
193
-
194
- const claudeManifest = findManifest(path.join(home, '.claude'), path.join('.claude-plugin', 'plugin.json'), version);
195
- const codexManifest = findManifest(path.join(home, '.codex'), path.join('.codex-plugin', 'plugin.json'), version);
196
210
  const sealedPlugin = path.join(packageRoot, 'plugin');
197
- const claudeFiles = assertInstalledPayload(sealedPlugin, path.dirname(path.dirname(claudeManifest)));
198
- const codexFiles = assertInstalledPayload(sealedPlugin, path.dirname(path.dirname(codexManifest)));
199
- const config = fs.readFileSync(path.join(codexHome, 'config.toml'), 'utf8');
200
- if (!/\[mcp_servers\.ruvnet-brain\]/.test(config)) throw new Error('virgin Codex home is not wired to ruvnet-brain');
201
- const source = readJson(path.join(kb, 'SOURCE.json'));
202
- if (source.brainVersion !== version || source.releaseTag !== `v${version}`) throw new Error('installed public Brain bundle version mismatch');
203
- installContext = { temp, home, codexHome, brainHome, kb, env, packageRoot };
211
+ const results = {};
212
+ let bundle = null;
213
+ for (const mode of ['claudeOnly', 'codexOnly', 'dual']) {
214
+ const home = path.join(temp, `home-${mode}`);
215
+ const codexHome = path.join(home, '.codex');
216
+ const brainHome = path.join(home, '.cache', 'ruvnet-brain');
217
+ const kb = path.join(brainHome, 'kb');
218
+ fs.mkdirSync(path.join(home, '.claude'), { recursive: true });
219
+ if (mode !== 'claudeOnly') fs.mkdirSync(codexHome, { recursive: true });
220
+ const env = {
221
+ ...process.env,
222
+ HOME: home,
223
+ CODEX_HOME: codexHome,
224
+ RUVNET_BRAIN_HOME: brainHome,
225
+ RUVNET_BRAIN_KB: kb,
226
+ RUVNET_STRICT_INSTALL: '1',
227
+ RUVNET_BRAIN_PROFILE: 'complete',
228
+ CI: 'true',
229
+ PATH: isolatedPath(mode, temp),
230
+ };
231
+ const installer = path.join(packageRoot, 'bin', 'install.mjs');
232
+ command(process.execPath, [
233
+ installer, '--yes', '--force', '--version', `v${version}`,
234
+ '--no-nightly-prompt', '--no-telemetry', '--no-stack', '--no-enhance', '--no-statusline',
235
+ ], { env, cwd: packageRoot, timeout: 1_200_000, maxBuffer: 32 * 1024 * 1024 });
236
+ command(process.execPath, [installer, '--doctor', '--hooks'], {
237
+ env, cwd: packageRoot, timeout: 300_000, maxBuffer: 32 * 1024 * 1024,
238
+ });
239
+
240
+ const verified = {};
241
+ if (mode !== 'codexOnly') {
242
+ const manifest = findManifest(path.join(home, '.claude'), path.join('.claude-plugin', 'plugin.json'), version);
243
+ verified.claudeFiles = assertInstalledPayload(sealedPlugin, path.dirname(path.dirname(manifest)));
244
+ }
245
+ if (mode !== 'claudeOnly') {
246
+ const manifest = findManifest(codexHome, path.join('.codex-plugin', 'plugin.json'), version);
247
+ verified.codexFiles = assertInstalledPayload(sealedPlugin, path.dirname(path.dirname(manifest)));
248
+ const config = fs.readFileSync(path.join(codexHome, 'config.toml'), 'utf8');
249
+ if (!/\[mcp_servers\.ruvnet-brain\]/.test(config)) throw new Error(`${mode} virgin Codex home is not wired to ruvnet-brain`);
250
+ }
251
+ const source = readJson(path.join(kb, 'SOURCE.json'));
252
+ if (source.brainVersion !== version || source.releaseTag !== `v${version}`) {
253
+ throw new Error(`${mode} installed public Brain bundle version mismatch`);
254
+ }
255
+ bundle ||= { brainVersion: source.brainVersion, releaseTag: source.releaseTag };
256
+ const searched = await rpcSearch(findMcpServer(home), env, 'How does RuvNet Brain prove a public release artifact?', DEADLINE_MS);
257
+ results[mode] = {
258
+ status: 'PASS', doctorExit: 0, version, artifactSha256,
259
+ functionalSearch: true, searchMs: searched.broadMs, hostsOnPath: mode,
260
+ ...verified,
261
+ };
262
+ if (mode === 'dual') installContext = { temp, home, codexHome, brainHome, kb, env, packageRoot };
263
+ }
204
264
  return {
205
- claude: { status: 'PASS', version, artifactSha256, payloadFilesVerified: claudeFiles },
206
- codex: { status: 'PASS', version, artifactSha256, payloadFilesVerified: codexFiles },
207
- bundle: { brainVersion: source.brainVersion, releaseTag: source.releaseTag },
265
+ ...results,
266
+ bundle,
208
267
  };
209
268
  },
210
269
 
@@ -231,6 +290,12 @@ export function livePublicationAdapter({ root = process.cwd() } = {}) {
231
290
  if (result.status !== 0 || parsed.verdict !== 'PASS') throw new Error(`published-surface-probe is ${parsed.verdict || `exit ${result.status}`}`);
232
291
  return { name: 'published-surface-probe', status: 'completed', conclusion: 'success', sha };
233
292
  },
293
+
294
+ async dispose() {
295
+ if (installTemp) fs.rmSync(installTemp, { recursive: true, force: true });
296
+ installTemp = null;
297
+ installContext = null;
298
+ },
234
299
  };
235
300
  }
236
301
 
@@ -246,15 +311,35 @@ export async function generatePublicationReceipt({
246
311
  const candidateResult = evaluateCandidateReceipt(candidate);
247
312
  if (candidateResult.verdict !== 'PASS') throw new Error(`candidate seal failed: ${candidateResult.failures.map(({ code }) => code).join(',')}`);
248
313
  const digest = receiptDigest(candidate);
314
+ const evidenceRoot = path.dirname(path.resolve(candidatePath));
315
+ const payloadManifestPath = path.join(evidenceRoot, 'payload-manifest.json');
316
+ const payloadSignaturePath = path.join(evidenceRoot, 'payload-manifest.sig');
317
+ const payloadManifest = readJson(payloadManifestPath);
318
+ const payloadProof = verifyPayload({
319
+ manifest: payloadManifest,
320
+ signature: fs.readFileSync(payloadSignaturePath, 'utf8'),
321
+ publicKey: crypto.createPublicKey(fs.readFileSync(path.join(root, 'keys', 'ruvnet-brain-signing.pub.pem'), 'utf8')),
322
+ root: evidenceRoot,
323
+ });
324
+ if (payloadManifest.version !== candidate.version || payloadManifest.tag !== candidate.tag
325
+ || payloadManifest.candidateSha !== candidate.sha) throw new Error('signed payload identity differs from candidate seal');
326
+ const npmMember = payloadManifest.members.find(({ role }) => role === 'npm');
327
+ const bundleMember = payloadManifest.members.find(({ role }) => role === 'bundle');
328
+ if (!npmMember || !bundleMember || npmMember.sha256 !== digest) throw new Error('signed payload member set differs from candidate artifact');
249
329
  const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'ruvnet-public-evidence-'));
250
330
  try {
251
331
  const assetName = path.basename(candidate.artifact.path);
252
332
  const npm = await adapter.downloadNpm({ version: candidate.version, destination: path.join(temp, `npm-${assetName}`) });
253
333
  const github = await adapter.downloadGithub({ tag: candidate.tag, assetName, destination: path.join(temp, `github-${assetName}`) });
334
+ const githubBundle = await adapter.downloadGithub({
335
+ tag: candidate.tag, assetName: bundleMember.name, destination: path.join(temp, `github-${bundleMember.name}`),
336
+ });
254
337
  const npmDigest = sha256(npm.path);
255
338
  const githubDigest = sha256(github.path);
256
339
  if (npmDigest !== digest) throw new Error(`npm public byte mismatch: ${npmDigest} != ${digest}`);
257
340
  if (githubDigest !== digest) throw new Error(`GitHub public byte mismatch: ${githubDigest} != ${digest}`);
341
+ const githubBundleDigest = sha256(githubBundle.path);
342
+ if (githubBundleDigest !== bundleMember.sha256) throw new Error(`GitHub public bundle byte mismatch: ${githubBundleDigest} != ${bundleMember.sha256}`);
258
343
  if (npm.version !== candidate.version || (npm.sha && npm.sha !== candidate.sha)) throw new Error('npm public identity mismatch');
259
344
  if (github.tag !== candidate.tag || github.sha !== candidate.sha) throw new Error('GitHub public identity mismatch');
260
345
 
@@ -262,17 +347,23 @@ export async function generatePublicationReceipt({
262
347
  const brain = await adapter.probeBrain({ sha: candidate.sha, artifactSha256: digest, version: candidate.version });
263
348
  const surface = await adapter.probePublishedSurface({ sha: candidate.sha, artifactSha256: digest, version: candidate.version });
264
349
  const publication = {
265
- schemaVersion: 1,
350
+ schemaVersion: 2,
266
351
  phase: 'publication',
267
352
  sha: candidate.sha,
268
353
  artifactSha256: digest,
269
354
  version: candidate.version,
355
+ payloadId: payloadProof.payloadId,
356
+ bundleArtifactSha256: githubBundleDigest,
270
357
  // npm does not guarantee gitHead metadata. Exact public-byte equality with the candidate's
271
358
  // SHA-bound sealed artifact is the identity proof; a present but conflicting gitHead is red.
272
359
  npm: { version: npm.version, sha: candidate.sha, artifactSha256: npmDigest },
273
360
  githubRelease: { tag: github.tag, sha: github.sha, artifactSha256: githubDigest },
274
361
  bundle: installed.bundle,
275
- installed: { claude: installed.claude, codex: installed.codex },
362
+ installed: {
363
+ claudeOnly: installed.claudeOnly,
364
+ codexOnly: installed.codexOnly,
365
+ dual: installed.dual,
366
+ },
276
367
  brain,
277
368
  postPublicationChecks: [surface],
278
369
  };
@@ -282,6 +373,7 @@ export async function generatePublicationReceipt({
282
373
  fs.writeFileSync(outPath, `${JSON.stringify(publication, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
283
374
  return result;
284
375
  } finally {
376
+ await adapter.dispose?.();
285
377
  fs.rmSync(temp, { recursive: true, force: true });
286
378
  }
287
379
  }
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env node
2
+ import crypto from 'node:crypto';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { canonicalJson } from './release-transaction.mjs';
7
+
8
+ export const REQUIRED_RELEASE_LEAVES = Object.freeze([
9
+ 'source-quality',
10
+ 'ux-qe',
11
+ 'release-qe',
12
+ 'integration-linux',
13
+ 'stranger-linux',
14
+ 'stranger-macos',
15
+ 'stranger-windows',
16
+ 'claude-only',
17
+ 'codex-only',
18
+ 'dual-host',
19
+ ]);
20
+
21
+ export function aggregateEvidence({ sha, payloadId, leaves }) {
22
+ if (!/^[a-f0-9]{40}$/.test(sha || '') || !/^[a-f0-9]{64}$/.test(payloadId || '')) {
23
+ throw new Error('aggregate identity is malformed');
24
+ }
25
+ const byName = new Map();
26
+ for (const leaf of leaves || []) {
27
+ if (!leaf?.name || byName.has(leaf.name)) throw new Error(`duplicate or unnamed evidence leaf: ${leaf?.name || '(missing)'}`);
28
+ if (leaf.sha !== sha || leaf.payloadId !== payloadId) throw new Error(`evidence identity mismatch: ${leaf.name}`);
29
+ if (leaf.status !== 'completed' || leaf.conclusion !== 'success' || leaf.verdict !== 'PASS') {
30
+ throw new Error(`evidence leaf is not fail-closed PASS: ${leaf.name}`);
31
+ }
32
+ byName.set(leaf.name, leaf);
33
+ }
34
+ const missing = REQUIRED_RELEASE_LEAVES.filter((name) => !byName.has(name));
35
+ if (missing.length) throw new Error(`required evidence leaves missing: ${missing.join(', ')}`);
36
+ const normalized = [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
37
+ const envelope = { schemaVersion: 1, sha, payloadId, verdict: 'PASS', leaves: normalized };
38
+ return { ...envelope, evidenceDigest: crypto.createHash('sha256').update(canonicalJson(envelope)).digest('hex') };
39
+ }
40
+
41
+ const isCli = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
42
+ if (isCli) {
43
+ const arg = (name) => {
44
+ const index = process.argv.indexOf(name);
45
+ return index >= 0 ? process.argv[index + 1] : null;
46
+ };
47
+ const out = path.resolve(arg('--out') || 'release-evidence/aggregate-envelope.json');
48
+ const envelope = aggregateEvidence({
49
+ sha: arg('--sha'),
50
+ payloadId: arg('--payload-id'),
51
+ leaves: JSON.parse(fs.readFileSync(path.resolve(arg('--leaves')), 'utf8')),
52
+ });
53
+ fs.writeFileSync(out, `${canonicalJson(envelope)}\n`, { flag: 'wx', mode: 0o600 });
54
+ process.stdout.write(`${JSON.stringify({ verdict: 'PASS', evidenceDigest: envelope.evidenceDigest, out })}\n`);
55
+ }
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env node
2
+ import crypto from 'node:crypto';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { canonicalJson } from './release-transaction.mjs';
7
+
8
+ const sha256 = (bytes) => crypto.createHash('sha256').update(bytes).digest('hex');
9
+ const digestFile = (file) => sha256(fs.readFileSync(file));
10
+
11
+ export function createPayloadManifest({ version, tag, candidateSha, producer, members }) {
12
+ const normalized = members.map(({ name, file, role }) => {
13
+ const stat = fs.statSync(file);
14
+ if (!stat.isFile() || stat.size === 0) throw new Error(`payload member is absent or empty: ${file}`);
15
+ return { name, role, size: stat.size, sha256: digestFile(file) };
16
+ }).sort((a, b) => a.name.localeCompare(b.name));
17
+ const names = new Set(normalized.map(({ name }) => name));
18
+ if (names.size !== normalized.length) throw new Error('payload member names must be unique');
19
+ return { schemaVersion: 1, version, tag, candidateSha, producer, members: normalized };
20
+ }
21
+
22
+ export const payloadIdFor = (manifest) => sha256(Buffer.from(canonicalJson(manifest)));
23
+
24
+ export function signPayloadManifest(manifest, privateKey) {
25
+ return crypto.sign(null, Buffer.from(canonicalJson(manifest)), privateKey).toString('base64');
26
+ }
27
+
28
+ export function verifyPayloadMembers({ manifest, root }) {
29
+ if (!manifest || manifest.schemaVersion !== 1 || !Array.isArray(manifest.members)) {
30
+ throw new Error('invalid payload manifest');
31
+ }
32
+ for (const member of manifest.members) {
33
+ if (!member.name || path.basename(member.name) !== member.name) throw new Error(`unsafe payload member name: ${member.name}`);
34
+ const file = path.join(root, member.name);
35
+ const stat = fs.statSync(file);
36
+ if (stat.size !== member.size || digestFile(file) !== member.sha256) {
37
+ throw new Error(`payload member mismatch: ${member.name}`);
38
+ }
39
+ }
40
+ return { payloadId: payloadIdFor(manifest), manifest, root };
41
+ }
42
+
43
+ export function verifyPayload({ manifest, signature, publicKey, root }) {
44
+ if (!crypto.verify(null, Buffer.from(canonicalJson(manifest)), publicKey, Buffer.from(signature.trim(), 'base64'))) {
45
+ throw new Error('payload manifest signature mismatch');
46
+ }
47
+ return verifyPayloadMembers({ manifest, root });
48
+ }
49
+
50
+ const arg = (name) => {
51
+ const index = process.argv.indexOf(name);
52
+ return index >= 0 ? process.argv[index + 1] : null;
53
+ };
54
+ const command = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)
55
+ ? process.argv[2]
56
+ : null;
57
+
58
+ if (command === 'create') {
59
+ const out = path.resolve(arg('--out') || 'release-evidence/payload-manifest.json');
60
+ const memberArgs = process.argv.filter((value, index) => process.argv[index - 1] === '--member');
61
+ const members = memberArgs.map((entry) => {
62
+ const [role, file] = entry.split('=', 2);
63
+ if (!role || !file) throw new Error(`invalid --member ${entry}; expected role=path`);
64
+ return { role, file: path.resolve(file), name: path.basename(file) };
65
+ });
66
+ const manifest = createPayloadManifest({
67
+ version: arg('--version'),
68
+ tag: arg('--tag'),
69
+ candidateSha: arg('--sha'),
70
+ producer: {
71
+ workflow: process.env.GITHUB_WORKFLOW || 'local',
72
+ runId: process.env.GITHUB_RUN_ID || 'local',
73
+ runAttempt: process.env.GITHUB_RUN_ATTEMPT || '1',
74
+ },
75
+ members,
76
+ });
77
+ fs.writeFileSync(out, `${canonicalJson(manifest)}\n`, { flag: 'wx', mode: 0o600 });
78
+ process.stdout.write(`${JSON.stringify({ payloadId: payloadIdFor(manifest), manifest: out })}\n`);
79
+ } else if (command === 'sign') {
80
+ const manifestPath = path.resolve(arg('--manifest'));
81
+ const out = path.resolve(arg('--out'));
82
+ if (fs.existsSync(out)) throw new Error(`refusing to overwrite persisted payload signature: ${out}`);
83
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
84
+ const pem = process.env.RUVNET_SIGNING_KEY;
85
+ if (!pem) throw new Error('RUVNET_SIGNING_KEY is required');
86
+ fs.writeFileSync(out, `${signPayloadManifest(manifest, crypto.createPrivateKey(pem))}\n`, { flag: 'wx', mode: 0o600 });
87
+ process.stdout.write(`${JSON.stringify({ payloadId: payloadIdFor(manifest), signature: out })}\n`);
88
+ } else if (command === 'verify' || command === 'verify-members') {
89
+ const manifestPath = path.resolve(arg('--manifest'));
90
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
91
+ const result = command === 'verify'
92
+ ? verifyPayload({
93
+ manifest,
94
+ signature: fs.readFileSync(path.resolve(arg('--signature')), 'utf8'),
95
+ publicKey: crypto.createPublicKey(fs.readFileSync(path.resolve(arg('--public-key')), 'utf8')),
96
+ root: path.dirname(manifestPath),
97
+ })
98
+ : verifyPayloadMembers({ manifest, root: path.dirname(manifestPath) });
99
+ process.stdout.write(`${JSON.stringify({ verdict: 'PASS', payloadId: result.payloadId })}\n`);
100
+ }