ruvnet-brain 4.0.90-dev → 4.3.1-dev
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 +31 -7
- package/bin/install.mjs +153 -22
- package/console/architecture.html +12 -14
- package/console/install-mockup.html +3 -3
- package/kb/zip-extract.mjs +8 -2
- package/package.json +7 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/hooks/codex-hooks.json +1 -1
- package/plugin/hooks/hooks.json +10 -0
- package/plugin/host-adapters/claude.json +11 -0
- package/plugin/host-adapters/codex.json +11 -0
- package/plugin/mcp/managed-cli-interface.mjs +77 -10
- package/plugin/mcp/server.mjs +2 -1
- package/plugin/scripts/anticipate.sh +30 -1
- package/plugin/scripts/capability-claim-evidence.mjs +431 -0
- package/plugin/scripts/capability-inventory-receipt.mjs +225 -0
- package/plugin/scripts/capability-registry.mjs +6 -0
- package/plugin/scripts/capability-routing.mjs +70 -0
- package/plugin/scripts/continuation-gate.mjs +68 -2
- package/plugin/scripts/coverage-integrity.mjs +575 -0
- package/plugin/scripts/decision-outcomes.mjs +0 -0
- package/plugin/scripts/ground-ruvnet.sh +2 -2
- package/plugin/scripts/nightly-controller.mjs +19 -2
- package/plugin/scripts/project-progression-contract.mjs +409 -0
- package/plugin/scripts/project-progression-hook.mjs +196 -0
- package/plugin/scripts/project-progression-outbox.mjs +95 -0
- package/plugin/scripts/project-progression-session-start.mjs +183 -0
- package/plugin/scripts/project-progression-store.mjs +248 -0
- package/plugin/scripts/project-store-resolver.mjs +106 -0
- package/plugin/scripts/session-snapshot-hook.mjs +29 -1
- package/plugin/scripts/session-start-core.mjs +22 -18
- package/plugin/scripts/spend-guard.mjs +6 -1
- package/scripts/adr-072-completion.mjs +98 -0
- package/scripts/behavioral-l1-l4.mjs +11 -8
- package/scripts/brain-score.mjs +23 -9
- package/scripts/build-bundle.mjs +94 -8
- package/scripts/card-from-source.mjs +140 -0
- package/scripts/corpus-aggregates.mjs +194 -0
- package/scripts/corpus-candidate.mjs +314 -0
- package/scripts/corpus-reconcile.mjs +540 -0
- package/scripts/corpus-seed-publish.mjs +110 -0
- package/scripts/coverage-integrity.mjs +3 -0
- package/scripts/gist-receipts.mjs +183 -0
- package/scripts/git-hooks/pre-push +21 -0
- package/scripts/host-registry.mjs +118 -0
- package/scripts/independent-review-receipt.mjs +499 -0
- package/scripts/ingest-gists.mjs +34 -5
- package/scripts/ingest-new-repos.mjs +127 -0
- package/scripts/metaharness-gate.mjs +58 -0
- package/scripts/nightly-gists.sh +3 -3
- package/scripts/nightly-wrapper.sh +105 -64
- package/scripts/product-integrity-contract.mjs +156 -0
- package/scripts/public-inventory.mjs +4 -0
- package/scripts/public-verification-aggregate.mjs +342 -0
- package/scripts/public-verification-finalizer.mjs +59 -0
- package/scripts/public-verification-inputs.mjs +473 -0
- package/scripts/public-verification-lane.mjs +244 -0
- package/scripts/publication-receipt.mjs +50 -6
- package/scripts/rebuild-gists-from-receipts.mjs +291 -0
- package/scripts/release-projection.mjs +86 -0
- package/scripts/release-transaction-provider.mjs +43 -102
- package/scripts/release-transaction.mjs +165 -49
- package/scripts/release.mjs +158 -3
- package/scripts/restore-local-ingests.mjs +2 -2
- package/scripts/retrieval-canary.mjs +533 -0
- package/scripts/routing-flywheel.mjs +12 -1
- package/scripts/rvf-generation.mjs +50 -0
- package/scripts/self-update.mjs +19 -44
- package/scripts/source-coverage.mjs +415 -0
- package/scripts/source-scope-receipt.mjs +84 -0
- package/scripts/sync-census.mjs +0 -0
- package/scripts/wired-check.mjs +114 -24
- package/scripts/worktree-integrity.mjs +123 -0
|
@@ -3,6 +3,7 @@ import fs from 'node:fs';
|
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { loadRuntimePreferences, runtimeChildEnv } from '../scripts/runtime-preferences.mjs';
|
|
6
|
+
import { recordManagedCliObservation, recordRegistryLatestObservation } from '../scripts/capability-claim-evidence.mjs';
|
|
6
7
|
|
|
7
8
|
export const MANAGED_EXECUTABLES = Object.freeze([
|
|
8
9
|
'ruflo',
|
|
@@ -15,6 +16,15 @@ export const MANAGED_EXECUTABLES = Object.freeze([
|
|
|
15
16
|
]);
|
|
16
17
|
|
|
17
18
|
const MANAGED = new Set(MANAGED_EXECUTABLES);
|
|
19
|
+
const REGISTRY_PACKAGES = Object.freeze({
|
|
20
|
+
ruflo: 'ruflo',
|
|
21
|
+
'claude-flow': '@claude-flow/cli',
|
|
22
|
+
'agentic-flow': 'agentic-flow',
|
|
23
|
+
'agentic-qe': 'agentic-qe',
|
|
24
|
+
ruvector: 'ruvector',
|
|
25
|
+
'agent-browser': 'agent-browser',
|
|
26
|
+
'ruv-swarm': 'ruv-swarm',
|
|
27
|
+
});
|
|
18
28
|
const SUBCOMMAND = /^[a-z][a-z0-9-]*$/;
|
|
19
29
|
const MAX_ARGS = 256;
|
|
20
30
|
const MAX_ARG_BYTES = 8192;
|
|
@@ -29,6 +39,17 @@ const executableSchema = {
|
|
|
29
39
|
};
|
|
30
40
|
|
|
31
41
|
export const MANAGED_CLI_TOOLS = Object.freeze([
|
|
42
|
+
{
|
|
43
|
+
name: 'ruvnet_registry_latest',
|
|
44
|
+
description: 'Read the exact npm registry latest version for a managed RuvNet executable and record a content-bound public-registry receipt.',
|
|
45
|
+
inputSchema: {
|
|
46
|
+
type: 'object',
|
|
47
|
+
additionalProperties: false,
|
|
48
|
+
properties: { executable: executableSchema },
|
|
49
|
+
required: ['executable'],
|
|
50
|
+
},
|
|
51
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
52
|
+
},
|
|
32
53
|
{
|
|
33
54
|
name: 'ruvnet_cli_help',
|
|
34
55
|
description: 'Read a managed CLI interface from the executable itself. Runs only the supplied subcommand path plus --help and records a fresh stamp only after exit 0.',
|
|
@@ -146,20 +167,43 @@ function writeStamps(executable, argv, env) {
|
|
|
146
167
|
export function resolveManagedExecutable(executable, env = process.env) {
|
|
147
168
|
if (executable !== 'ruflo') return executable;
|
|
148
169
|
const home = env.HOME || os.homedir();
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
return
|
|
153
|
-
}
|
|
154
|
-
|
|
170
|
+
const candidates = [path.join(home, '.npm-global', 'bin', 'ruflo')];
|
|
171
|
+
if (process.platform === 'win32') candidates.push(`${candidates[0]}.cmd`);
|
|
172
|
+
return candidates.find((candidate) => {
|
|
173
|
+
try { fs.accessSync(candidate, fs.constants.X_OK); return true; } catch { return false; }
|
|
174
|
+
}) || executable;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function quoteWindowsCommandArg(value) {
|
|
178
|
+
// cmd.exe is required for npm's .cmd shims. Keep the child_process call itself shell:false,
|
|
179
|
+
// and quote every literal token so shell metacharacters remain data rather than syntax.
|
|
180
|
+
// Delayed expansion is disabled by default; doubling percent signs prevents environment
|
|
181
|
+
// expansion while cmd parses the /c command line.
|
|
182
|
+
return `"${value.replace(/%/g, '%%').replace(/"/g, '""')}"`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function spawnSpec(executable, argv, env) {
|
|
186
|
+
const resolved = resolveManagedExecutable(executable, env);
|
|
187
|
+
if (process.platform !== 'win32' || !resolved.toLowerCase().endsWith('.cmd')) {
|
|
188
|
+
return { file: resolved, args: argv, windowsVerbatimArguments: false };
|
|
155
189
|
}
|
|
190
|
+
// `/s /c` removes the first and last quote characters from its command string. The outer
|
|
191
|
+
// envelope therefore preserves the inner quotes around a shim path containing spaces.
|
|
192
|
+
const command = `"${[resolved, ...argv].map(quoteWindowsCommandArg).join(' ')}"`;
|
|
193
|
+
return {
|
|
194
|
+
file: env.ComSpec || env.COMSPEC || 'cmd.exe',
|
|
195
|
+
args: ['/d', '/s', '/c', command],
|
|
196
|
+
windowsVerbatimArguments: true,
|
|
197
|
+
};
|
|
156
198
|
}
|
|
157
199
|
|
|
158
200
|
function execute(executable, argv, env) {
|
|
159
201
|
return new Promise((resolve) => {
|
|
160
|
-
const
|
|
202
|
+
const spec = spawnSpec(executable, argv, env);
|
|
203
|
+
const child = spawn(spec.file, spec.args, {
|
|
161
204
|
env,
|
|
162
205
|
shell: false,
|
|
206
|
+
windowsVerbatimArguments: spec.windowsVerbatimArguments,
|
|
163
207
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
164
208
|
});
|
|
165
209
|
const stdout = [];
|
|
@@ -213,16 +257,37 @@ function resultOf(executable, argv, result) {
|
|
|
213
257
|
};
|
|
214
258
|
}
|
|
215
259
|
|
|
216
|
-
export async function callManagedCli(toolName, args, env = process.env) {
|
|
260
|
+
export async function callManagedCli(toolName, args, env = process.env, fetchImpl = globalThis.fetch) {
|
|
217
261
|
try {
|
|
218
262
|
const executable = assertExecutable(args?.executable);
|
|
219
|
-
const argv = literalArgv(args?.argv);
|
|
263
|
+
const argv = literalArgv(args?.argv ?? []);
|
|
264
|
+
|
|
265
|
+
if (toolName === 'ruvnet_registry_latest') {
|
|
266
|
+
const packageName = REGISTRY_PACKAGES[executable];
|
|
267
|
+
const registryUrl = `https://registry.npmjs.org/${packageName.replace('/', '%2F')}/latest`;
|
|
268
|
+
const response = await fetchImpl(registryUrl, {
|
|
269
|
+
headers: { accept: 'application/json' },
|
|
270
|
+
signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS),
|
|
271
|
+
});
|
|
272
|
+
const body = await response.text();
|
|
273
|
+
if (!response.ok) throw new Error(`registry latest lookup failed with HTTP ${response.status}`);
|
|
274
|
+
let metadata;
|
|
275
|
+
try { metadata = JSON.parse(body); } catch { throw new Error('registry latest response was not JSON'); }
|
|
276
|
+
const receipt = recordRegistryLatestObservation({
|
|
277
|
+
executable, packageName, version: metadata?.version, registryUrl, responseBody: body, env,
|
|
278
|
+
});
|
|
279
|
+
return {
|
|
280
|
+
content: [{ type: 'text', text: `${packageName} latest version: ${receipt.observedVersion} (registry receipt ${receipt.receiptSha256})` }],
|
|
281
|
+
isError: false,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
220
284
|
|
|
221
285
|
if (toolName === 'ruvnet_cli_help') {
|
|
222
286
|
stampKeysForHelp(executable, argv);
|
|
223
287
|
const commandArgv = [...argv, '--help'];
|
|
224
288
|
const execution = await execute(executable, commandArgv, env);
|
|
225
289
|
if (execution.code === 0 && !execution.error) writeStamps(executable, argv, env);
|
|
290
|
+
recordManagedCliObservation({ toolName, executable, argv: commandArgv, execution, env });
|
|
226
291
|
return resultOf(executable, commandArgv, execution);
|
|
227
292
|
}
|
|
228
293
|
|
|
@@ -263,7 +328,9 @@ export async function callManagedCli(toolName, args, env = process.env) {
|
|
|
263
328
|
const childEnv = (executable === 'agentic-flow' || executable === 'agentic-qe')
|
|
264
329
|
? runtimeChildEnv({ env, cwd: env.RUVNET_BRAIN_PROJECT_DIR || process.cwd() })
|
|
265
330
|
: env;
|
|
266
|
-
|
|
331
|
+
const execution = await execute(executable, argv, childEnv);
|
|
332
|
+
recordManagedCliObservation({ toolName, executable, argv, execution, env });
|
|
333
|
+
return resultOf(executable, argv, execution);
|
|
267
334
|
}
|
|
268
335
|
|
|
269
336
|
return {
|
package/plugin/mcp/server.mjs
CHANGED
|
@@ -327,7 +327,8 @@ async function handleClient(msg) {
|
|
|
327
327
|
return clientOk(id, { tools: FALLBACK_TOOLS });
|
|
328
328
|
}
|
|
329
329
|
case 'tools/call': {
|
|
330
|
-
if (params?.name === 'ruvnet_cli_help' || params?.name === 'ruvnet_cli_run'
|
|
330
|
+
if (params?.name === 'ruvnet_cli_help' || params?.name === 'ruvnet_cli_run'
|
|
331
|
+
|| params?.name === 'ruvnet_registry_latest') {
|
|
331
332
|
return clientOk(id, await callManagedCli(params.name, params.arguments || {}));
|
|
332
333
|
}
|
|
333
334
|
if (params?.name !== 'search_ruvnet') return clientErr(id, -32602, `unknown tool: ${params?.name}`);
|
|
@@ -195,6 +195,7 @@ import { pathToFileURL } from 'node:url';
|
|
|
195
195
|
const MODE = process.env.RUVNET_ANTICIPATE_MODE || 'suggest';
|
|
196
196
|
const ARG = process.env.RUVNET_ANTICIPATE_ARG || '';
|
|
197
197
|
const SELF = process.env.RUVNET_ANTICIPATE_SELF || 'anticipate.sh';
|
|
198
|
+
const SELF_DIR = path.dirname(SELF);
|
|
198
199
|
|
|
199
200
|
// CANDIDATE MODE (ADR-040 / DDD-0004 "the enforcement chokepoint"). Set by unprompted-runtime.mjs on
|
|
200
201
|
// every producer child. When on, this hook writes ZERO user-facing prose: it emits ONE advocacy
|
|
@@ -414,13 +415,18 @@ const sessions = st.sessions && typeof st.sessions === 'object' ? st.sessions :
|
|
|
414
415
|
const said = new Set(strings(sessions[sid]?.said));
|
|
415
416
|
if (said.size >= MAX_PER_SESSION) quit();
|
|
416
417
|
|
|
417
|
-
let auditAll, matchGoal, floor;
|
|
418
|
+
let auditAll, matchGoal, floor, buildCapabilityRoutingReceipt, hasToolPreference;
|
|
418
419
|
try { ({ auditAll } = await import(pathToFileURL(process.env.RUVNET_CAPABILITY_REGISTRY).href)); } catch { quit(); }
|
|
419
420
|
try {
|
|
420
421
|
const gm = await import(pathToFileURL(process.env.RUVNET_GOAL_MATCH).href);
|
|
421
422
|
matchGoal = gm.matchGoal;
|
|
422
423
|
floor = gm.CONFIDENCE_FLOOR;
|
|
423
424
|
} catch { quit(); }
|
|
425
|
+
try {
|
|
426
|
+
const routingPath = process.env.RUVNET_CAPABILITY_ROUTING_MODULE
|
|
427
|
+
|| path.join(SELF_DIR, 'capability-routing.mjs');
|
|
428
|
+
({ buildCapabilityRoutingReceipt, hasToolPreference } = await import(pathToFileURL(routingPath).href));
|
|
429
|
+
} catch { /* older packed hosts predate the optional routing receipt */ }
|
|
424
430
|
if (typeof auditAll !== 'function' || typeof matchGoal !== 'function') quit();
|
|
425
431
|
const MIN_CONFIDENCE = typeof floor === 'number' && Number.isFinite(floor) ? floor : FALLBACK_CONFIDENCE_FLOOR;
|
|
426
432
|
|
|
@@ -459,6 +465,28 @@ let matches = [];
|
|
|
459
465
|
try { matches = matchGoal(prompt, dormant) || []; } catch { quit(); }
|
|
460
466
|
if (!Array.isArray(matches) || !matches.length) quit();
|
|
461
467
|
|
|
468
|
+
// Only an evidence-bound route to an existing RuvNet building block may leave this hook.
|
|
469
|
+
let routingReceipt;
|
|
470
|
+
try {
|
|
471
|
+
routingReceipt = typeof buildCapabilityRoutingReceipt === 'function'
|
|
472
|
+
? buildCapabilityRoutingReceipt({ prompt, matches, capabilities: rows }) : null;
|
|
473
|
+
// Test/host adapters may provide synthetic capabilities outside the shipped registry. Preserve
|
|
474
|
+
// their existing protocol; real RuvNet capabilities are fail-closed when no route receipt exists.
|
|
475
|
+
const knownRoute = typeof hasToolPreference === 'function' && matches.some((match) => {
|
|
476
|
+
const key = match?.capability?.key || match?.capability;
|
|
477
|
+
const liveRow = rows.find((row) => row?.key === key);
|
|
478
|
+
return hasToolPreference(key) && typeof liveRow?.evidence === 'string' && liveRow.evidence.trim()
|
|
479
|
+
&& typeof liveRow?.evidenceDigest === 'string' && liveRow.evidenceDigest.length === 64;
|
|
480
|
+
});
|
|
481
|
+
if (!routingReceipt && knownRoute) quit();
|
|
482
|
+
if (!routingReceipt) routingReceipt = { receiptSha256: 'synthetic-adapter' };
|
|
483
|
+
const receiptFile = process.env.RUVNET_CAPABILITY_ROUTING_RECEIPTS;
|
|
484
|
+
if (receiptFile) {
|
|
485
|
+
fs.mkdirSync(path.dirname(receiptFile), { recursive: true });
|
|
486
|
+
fs.appendFileSync(receiptFile, `${JSON.stringify(routingReceipt)}\n`, { mode: 0o600 });
|
|
487
|
+
}
|
|
488
|
+
} catch { quit(); }
|
|
489
|
+
|
|
462
490
|
// The matcher is trusted to rank, never to assert existence: every match is re-resolved against the
|
|
463
491
|
// dormant rows THIS audit produced. A capability the matcher names that is not dormant right now is
|
|
464
492
|
// dropped, so a stale or over-eager matcher can only ever cause silence, never a false claim.
|
|
@@ -534,6 +562,7 @@ if (EMIT_CANDIDATES) {
|
|
|
534
562
|
findingId: best.row.key,
|
|
535
563
|
severity: best.row.severity || 'normal',
|
|
536
564
|
observationHash: stateHashOf(best.row.evidence),
|
|
565
|
+
routingReceiptSha256: routingReceipt.receiptSha256,
|
|
537
566
|
}));
|
|
538
567
|
quit();
|
|
539
568
|
}
|
|
@@ -0,0 +1,431 @@
|
|
|
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
|
+
|
|
7
|
+
const HEX40 = /^[a-f0-9]{40}$/;
|
|
8
|
+
const HEX64 = /^[a-f0-9]{64}$/;
|
|
9
|
+
const HOSTS = Object.freeze(['claude', 'codex']);
|
|
10
|
+
const OS_LANES = Object.freeze(['linux', 'macos', 'windows']);
|
|
11
|
+
const CLAIM_CLASSES = Object.freeze(['installation', 'behavior', 'currentVersion', 'latestVersion', 'health']);
|
|
12
|
+
const VERSION = /\bv?(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)\b/;
|
|
13
|
+
const RUVNET_TOOL = '(?:Ruflo|Claude Flow|Agentic Flow|Agentic QE|RuVector|Agent Browser|Ruv Swarm|AgentDB|RuLake|RuvNet Brain)';
|
|
14
|
+
const MAX_AGE_MS = 10 * 60_000;
|
|
15
|
+
const MAX_LEDGER_BYTES = 1024 * 1024;
|
|
16
|
+
const STOPWORDS = new Set(['a', 'an', 'and', 'are', 'can', 'could', 'does', 'for', 'has', 'have',
|
|
17
|
+
'is', 'it', 'not', 'of', 'or', 'the', 'this', 'to', 'version', 'with', 'would']);
|
|
18
|
+
|
|
19
|
+
const sha256 = (value) => crypto.createHash('sha256').update(value).digest('hex');
|
|
20
|
+
export const canonicalJson = (value) => {
|
|
21
|
+
if (Array.isArray(value)) return `[${value.map((item) => canonicalJson(item)).join(',')}]`;
|
|
22
|
+
if (value && typeof value === 'object') {
|
|
23
|
+
return `{${Object.keys(value).sort().filter((key) => value[key] !== undefined)
|
|
24
|
+
.map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(',')}}`;
|
|
25
|
+
}
|
|
26
|
+
return JSON.stringify(value);
|
|
27
|
+
};
|
|
28
|
+
const digest = (value) => sha256(canonicalJson(value));
|
|
29
|
+
const normalize = (value) => String(value || '').replace(/\s+/g, ' ').replace(/[.!?]+$/g, '').trim().toLowerCase();
|
|
30
|
+
const tokens = (value) => String(value || '').replace(/([a-z0-9])([A-Z])/g, '$1 $2').toLowerCase()
|
|
31
|
+
.match(/[a-z0-9][a-z0-9-]*[a-z0-9]|[a-z0-9]/g) || [];
|
|
32
|
+
const significant = (value) => tokens(value).filter((token) => !STOPWORDS.has(token));
|
|
33
|
+
const overlap = (left, right) => {
|
|
34
|
+
const rightSet = new Set(significant(right));
|
|
35
|
+
return significant(left).filter((token) => rightSet.has(token));
|
|
36
|
+
};
|
|
37
|
+
const safeMessage = (message) => String(message || '')
|
|
38
|
+
.replace(/```[\s\S]*?```/g, ' ')
|
|
39
|
+
.replace(/`[^`\n]*`/g, ' ')
|
|
40
|
+
.replace(/"[^"\n]*"|'[^'\n]*'/g, ' ')
|
|
41
|
+
.replace(/^\s*>.*$/gm, ' ');
|
|
42
|
+
const toolId = (name) => String(name || '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
43
|
+
const executableFor = (name) => ({
|
|
44
|
+
ruflo: 'ruflo', 'claude-flow': 'claude-flow', 'agentic-flow': 'agentic-flow',
|
|
45
|
+
'agentic-qe': 'agentic-qe', ruvector: 'ruvector', 'agent-browser': 'agent-browser',
|
|
46
|
+
'ruv-swarm': 'ruv-swarm',
|
|
47
|
+
})[toolId(name)] || null;
|
|
48
|
+
|
|
49
|
+
function liveEvidenceFile(env = process.env) {
|
|
50
|
+
if (env.RUVNET_CAPABILITY_LIVE_EVIDENCE) return path.resolve(env.RUVNET_CAPABILITY_LIVE_EVIDENCE);
|
|
51
|
+
const home = env.HOME || env.USERPROFILE || os.homedir();
|
|
52
|
+
const brainHome = env.RUVNET_BRAIN_HOME || path.join(home, '.cache', 'ruvnet-brain');
|
|
53
|
+
return path.join(brainHome, 'capability-live-evidence.jsonl');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function groundingEvidenceFile(env = process.env) {
|
|
57
|
+
if (env.RUVNET_EVIDENCE_FILE) return path.resolve(env.RUVNET_EVIDENCE_FILE);
|
|
58
|
+
const home = env.HOME || env.USERPROFILE || os.homedir();
|
|
59
|
+
const brainHome = env.RUVNET_BRAIN_HOME || path.join(home, '.cache', 'ruvnet-brain');
|
|
60
|
+
return path.join(brainHome, 'evidence.jsonl');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function sourcePayload(receipt) {
|
|
64
|
+
const { receiptSha256: _receiptSha256, ...payload } = receipt || {};
|
|
65
|
+
return payload;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function validateSourceClaimReceipt(receipt) {
|
|
69
|
+
if (receipt?.schemaVersion !== 1 || receipt?.kind !== 'ruvnet-brain-source-claim'
|
|
70
|
+
|| receipt.verdict !== 'PASS' || !HEX64.test(String(receipt.claimDigest || ''))
|
|
71
|
+
|| !HEX64.test(String(receipt.groundingReceiptDigest || ''))
|
|
72
|
+
|| !HEX64.test(String(receipt.source?.sha256 || '')) || !receipt.source?.repo || !receipt.source?.path
|
|
73
|
+
|| !HEX64.test(String(receipt.receiptSha256 || ''))
|
|
74
|
+
|| digest(sourcePayload(receipt)) !== receipt.receiptSha256) {
|
|
75
|
+
throw new Error('source claim receipt is malformed or digest-invalid');
|
|
76
|
+
}
|
|
77
|
+
return receipt;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function sourceBindsClaim(claim, source, query) {
|
|
81
|
+
const negative = /\b(?:cannot|can't|does\s+not|doesn't|not\s+(?:support|provide|implement|export|expose))\b/i.test(claim);
|
|
82
|
+
if (negative) {
|
|
83
|
+
return (source.negatives || []).some(({ symbol, quote }) =>
|
|
84
|
+
(symbol && normalize(claim).includes(normalize(symbol))) || overlap(claim, quote).length >= 2);
|
|
85
|
+
}
|
|
86
|
+
const boundQuery = source.claimBinding?.query || query;
|
|
87
|
+
if (source.claimBinding?.method === 'tight-source-token-pair' && overlap(claim, boundQuery).length >= 2) return true;
|
|
88
|
+
if ((source.symbols || []).some(({ name }) => normalize(claim).includes(normalize(name)))) return true;
|
|
89
|
+
return (source.posture || []).some((posture) => overlap(claim, posture).length >= 2);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function buildSourceClaimReceipt({ claim, groundingReceipt, sourcePath, observedAt = new Date().toISOString() } = {}) {
|
|
93
|
+
if (typeof claim !== 'string' || !claim.trim() || !groundingReceipt || !Array.isArray(groundingReceipt.sources)) {
|
|
94
|
+
throw new Error('source claim receipt requires a claim and grounding receipt');
|
|
95
|
+
}
|
|
96
|
+
const source = groundingReceipt.sources.find((row) => `${row.repo}/${row.path}` === sourcePath);
|
|
97
|
+
if (!source || !HEX64.test(String(source.sha || '')) || source.enforceable !== true) {
|
|
98
|
+
throw new Error('grounding source lacks a full content identity or enforceable evidence');
|
|
99
|
+
}
|
|
100
|
+
if (!sourceBindsClaim(claim, source, groundingReceipt.query)) throw new Error('grounding source does not bind the claim');
|
|
101
|
+
const payload = {
|
|
102
|
+
schemaVersion: 1,
|
|
103
|
+
kind: 'ruvnet-brain-source-claim',
|
|
104
|
+
observedAt,
|
|
105
|
+
claim: claim.trim(),
|
|
106
|
+
claimDigest: digest(normalize(claim)),
|
|
107
|
+
groundingReceiptId: String(groundingReceipt.id || ''),
|
|
108
|
+
groundingReceiptDigest: digest(groundingReceipt),
|
|
109
|
+
source: { repo: source.repo, path: source.path, sha256: source.sha },
|
|
110
|
+
binding: source.claimBinding?.method || ((source.negatives || []).length ? 'explicit-negative' : 'explicit-source-fact'),
|
|
111
|
+
verdict: 'PASS',
|
|
112
|
+
};
|
|
113
|
+
return validateSourceClaimReceipt({ ...payload, receiptSha256: digest(payload) });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function livePayload(receipt) {
|
|
117
|
+
const { receiptSha256: _receiptSha256, ...payload } = receipt || {};
|
|
118
|
+
return payload;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function validateLiveSurfaceReceipt(receipt) {
|
|
122
|
+
if (receipt?.schemaVersion !== 1 || receipt?.kind !== 'ruvnet-brain-live-surface'
|
|
123
|
+
|| !['claude', 'codex', 'shared'].includes(receipt.host) || !receipt.executable
|
|
124
|
+
|| !['current-version', 'latest-version', 'health'].includes(receipt.observationClass)
|
|
125
|
+
|| !HEX64.test(String(receipt.outputSha256 || '')) || !HEX64.test(String(receipt.receiptSha256 || ''))
|
|
126
|
+
|| digest(livePayload(receipt)) !== receipt.receiptSha256) {
|
|
127
|
+
throw new Error('live surface receipt is malformed or digest-invalid');
|
|
128
|
+
}
|
|
129
|
+
if (receipt.observationClass === 'current-version' && !VERSION.test(String(receipt.observedVersion || ''))) {
|
|
130
|
+
throw new Error('live version receipt has no observed semantic version');
|
|
131
|
+
}
|
|
132
|
+
if (receipt.observationClass === 'latest-version'
|
|
133
|
+
&& (receipt.host !== 'shared' || !VERSION.test(String(receipt.observedVersion || ''))
|
|
134
|
+
|| !receipt.registryPackage || !/^https:\/\/registry\.npmjs\.org\//.test(String(receipt.registryUrl || '')))) {
|
|
135
|
+
throw new Error('registry latest receipt has no exact public semantic version observation');
|
|
136
|
+
}
|
|
137
|
+
if (receipt.observationClass === 'health' && !['PASS', 'FAIL', 'UNKNOWN'].includes(receipt.healthVerdict)) {
|
|
138
|
+
throw new Error('live health receipt has no typed verdict');
|
|
139
|
+
}
|
|
140
|
+
return receipt;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function appendReceipt(file, receipt) {
|
|
144
|
+
try {
|
|
145
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
146
|
+
fs.appendFileSync(file, `${JSON.stringify(receipt)}\n`, { mode: 0o600 });
|
|
147
|
+
if (fs.statSync(file).size > MAX_LEDGER_BYTES) {
|
|
148
|
+
const lines = fs.readFileSync(file, 'utf8').split('\n').filter(Boolean).slice(-200);
|
|
149
|
+
const temporary = `${file}.tmp-${process.pid}-${Date.now()}`;
|
|
150
|
+
fs.writeFileSync(temporary, `${lines.join('\n')}\n`, { flag: 'wx', mode: 0o600 });
|
|
151
|
+
fs.renameSync(temporary, file);
|
|
152
|
+
}
|
|
153
|
+
} catch { /* evidence capture cannot break the managed CLI boundary */ }
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function recordManagedCliObservation({ toolName, executable, argv, execution, env = process.env,
|
|
157
|
+
observedAt = new Date().toISOString() } = {}) {
|
|
158
|
+
try {
|
|
159
|
+
const output = [execution?.stdout, execution?.stderr].filter(Boolean).join('\n');
|
|
160
|
+
const reachable = execution?.code === 0 && !execution?.error;
|
|
161
|
+
const version = VERSION.exec(output)?.[1] || null;
|
|
162
|
+
let observationClass = null;
|
|
163
|
+
let fields = {};
|
|
164
|
+
if (reachable && version && (toolName === 'ruvnet_cli_help'
|
|
165
|
+
|| (argv || []).some((arg) => ['--version', '-V', 'version'].includes(arg)))) {
|
|
166
|
+
observationClass = 'current-version';
|
|
167
|
+
fields = { observedVersion: version };
|
|
168
|
+
} else if (['doctor', 'status'].includes(String(argv?.[0] || ''))) {
|
|
169
|
+
observationClass = 'health';
|
|
170
|
+
const negative = /\b(?:critical|degraded|error|failed|failure|unhealthy)\b/i.test(output);
|
|
171
|
+
const positive = /\b(?:all checks passed|healthy|overall\s*[:=-]?\s*(?:ok|pass)|status\s*[:=-]\s*(?:ok|pass|healthy))\b/i.test(output);
|
|
172
|
+
fields = { healthVerdict: negative ? 'FAIL' : reachable && positive ? 'PASS' : 'UNKNOWN', reachable };
|
|
173
|
+
}
|
|
174
|
+
if (!observationClass) return null;
|
|
175
|
+
const payload = {
|
|
176
|
+
schemaVersion: 1,
|
|
177
|
+
kind: 'ruvnet-brain-live-surface',
|
|
178
|
+
host: HOSTS.includes(env.RUVNET_HOOK_HOST) ? env.RUVNET_HOOK_HOST : 'shared',
|
|
179
|
+
observedAt,
|
|
180
|
+
toolName,
|
|
181
|
+
executable,
|
|
182
|
+
argv: [...(argv || [])],
|
|
183
|
+
observationClass,
|
|
184
|
+
outputSha256: sha256(output),
|
|
185
|
+
...fields,
|
|
186
|
+
};
|
|
187
|
+
const receipt = validateLiveSurfaceReceipt({ ...payload, receiptSha256: digest(payload) });
|
|
188
|
+
appendReceipt(liveEvidenceFile(env), receipt);
|
|
189
|
+
return receipt;
|
|
190
|
+
} catch { return null; }
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function recordRegistryLatestObservation({ executable, packageName, version, registryUrl, responseBody,
|
|
194
|
+
env = process.env, observedAt = new Date().toISOString() } = {}) {
|
|
195
|
+
const matched = VERSION.exec(String(version || ''));
|
|
196
|
+
const normalizedVersion = String(version || '').replace(/^v/, '');
|
|
197
|
+
if (!executable || !packageName || !matched || matched[1] !== normalizedVersion) {
|
|
198
|
+
throw new Error('registry latest receipt requires an exact semantic version');
|
|
199
|
+
}
|
|
200
|
+
const payload = {
|
|
201
|
+
schemaVersion: 1,
|
|
202
|
+
kind: 'ruvnet-brain-live-surface',
|
|
203
|
+
host: 'shared',
|
|
204
|
+
observedAt,
|
|
205
|
+
toolName: 'ruvnet_registry_latest',
|
|
206
|
+
executable,
|
|
207
|
+
argv: [packageName],
|
|
208
|
+
observationClass: 'latest-version',
|
|
209
|
+
outputSha256: sha256(String(responseBody || '')),
|
|
210
|
+
observedVersion: normalizedVersion,
|
|
211
|
+
registryPackage: packageName,
|
|
212
|
+
registryUrl,
|
|
213
|
+
};
|
|
214
|
+
const receipt = validateLiveSurfaceReceipt({ ...payload, receiptSha256: digest(payload) });
|
|
215
|
+
appendReceipt(liveEvidenceFile(env), receipt);
|
|
216
|
+
return receipt;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function readLiveSurfaceReceipts({ file = null, env = process.env, limit = 100 } = {}) {
|
|
220
|
+
try {
|
|
221
|
+
const lines = fs.readFileSync(file || liveEvidenceFile(env), 'utf8').split('\n').filter(Boolean);
|
|
222
|
+
const receipts = [];
|
|
223
|
+
for (let index = lines.length - 1; index >= 0 && receipts.length < limit; index -= 1) {
|
|
224
|
+
try { receipts.push(validateLiveSurfaceReceipt(JSON.parse(lines[index]))); } catch { /* reject torn/tampered rows */ }
|
|
225
|
+
}
|
|
226
|
+
return receipts;
|
|
227
|
+
} catch { return []; }
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function extractClaims(message) {
|
|
231
|
+
const claims = [];
|
|
232
|
+
for (const sentence of safeMessage(message).split(/(?<=[.!?])\s+|\n+/).map((value) => value.trim()).filter(Boolean)) {
|
|
233
|
+
let match = new RegExp(`\\b(${RUVNET_TOOL})\\b[^.!?]{0,48}\\b(?:current|installed)\\s+version\\s+(?:is\\s+)?v?(\\d+\\.\\d+\\.\\d+(?:[-+][0-9A-Za-z.-]+)?)`, 'i').exec(sentence);
|
|
234
|
+
if (match) { claims.push({ class: 'current-version', text: sentence, tool: match[1], version: match[2] }); continue; }
|
|
235
|
+
match = new RegExp(`\\b(${RUVNET_TOOL})\\b[^.!?]{0,32}\\bv?(\\d+\\.\\d+\\.\\d+(?:[-+][0-9A-Za-z.-]+)?)\\b[^.!?]{0,32}\\blatest\\s+version`, 'i').exec(sentence);
|
|
236
|
+
if (match) { claims.push({ class: 'latest-version', text: sentence, tool: match[1], version: match[2] }); continue; }
|
|
237
|
+
match = new RegExp(`\\b(${RUVNET_TOOL})\\b\\s+is\\s+(healthy|unhealthy|reachable|unreachable|available|down)\\b`, 'i').exec(sentence);
|
|
238
|
+
if (match) { claims.push({ class: 'health', text: sentence, tool: match[1], state: match[2].toLowerCase() }); continue; }
|
|
239
|
+
match = new RegExp(`\\b(${RUVNET_TOOL})\\b\\s+(?:can(?:not|'t)?|supports?|does\\s+not\\s+support|provides?|does\\s+not\\s+provide|implements?|does\\s+not\\s+implement|exports?|does\\s+not\\s+export|exposes?|does\\s+not\\s+expose|requires?|uses?)\\s+([^.!?]{1,160})`, 'i').exec(sentence);
|
|
240
|
+
if (match) claims.push({ class: 'behavior', text: sentence, tool: match[1] });
|
|
241
|
+
}
|
|
242
|
+
return claims;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function deriveSourceClaimReceipts(message, { file = null, env = process.env,
|
|
246
|
+
now = new Date().toISOString(), maxAgeMs = 24 * 60 * 60_000 } = {}) {
|
|
247
|
+
const behaviors = extractClaims(message).filter(({ class: claimClass }) => claimClass === 'behavior');
|
|
248
|
+
if (!behaviors.length) return [];
|
|
249
|
+
let lines;
|
|
250
|
+
try { lines = fs.readFileSync(file || groundingEvidenceFile(env), 'utf8').split('\n').filter(Boolean); }
|
|
251
|
+
catch { return []; }
|
|
252
|
+
const currentTime = Date.parse(now);
|
|
253
|
+
const grounding = [];
|
|
254
|
+
for (let index = lines.length - 1; index >= 0 && grounding.length < 60; index -= 1) {
|
|
255
|
+
try {
|
|
256
|
+
const receipt = JSON.parse(lines[index]);
|
|
257
|
+
const age = currentTime - Date.parse(receipt.ts);
|
|
258
|
+
if (receipt?.v === 1 && Array.isArray(receipt.sources) && Number.isFinite(age) && age >= 0 && age <= maxAgeMs) {
|
|
259
|
+
grounding.push(receipt);
|
|
260
|
+
}
|
|
261
|
+
} catch { /* torn or malformed evidence cannot authorize a claim */ }
|
|
262
|
+
}
|
|
263
|
+
const sourceClaims = [];
|
|
264
|
+
for (const claim of behaviors) {
|
|
265
|
+
let bound = null;
|
|
266
|
+
for (const receipt of grounding) {
|
|
267
|
+
for (const source of receipt.sources || []) {
|
|
268
|
+
try {
|
|
269
|
+
bound = buildSourceClaimReceipt({ claim: claim.text, groundingReceipt: receipt,
|
|
270
|
+
sourcePath: `${source.repo}/${source.path}`, observedAt: now });
|
|
271
|
+
} catch { /* this source does not bind this exact claim */ }
|
|
272
|
+
if (bound) break;
|
|
273
|
+
}
|
|
274
|
+
if (bound) break;
|
|
275
|
+
}
|
|
276
|
+
if (bound) sourceClaims.push(bound);
|
|
277
|
+
}
|
|
278
|
+
return sourceClaims;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function auditCurrentCapabilityEvidence(message, { env = process.env,
|
|
282
|
+
now = new Date().toISOString() } = {}) {
|
|
283
|
+
return auditEvidenceBoundCapabilityClaims(message, {
|
|
284
|
+
sourceClaims: deriveSourceClaimReceipts(message, { env, now }),
|
|
285
|
+
liveSurfaces: readLiveSurfaceReceipts({ env }),
|
|
286
|
+
host: HOSTS.includes(env.RUVNET_HOOK_HOST) ? env.RUVNET_HOOK_HOST : null,
|
|
287
|
+
now,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export function auditEvidenceBoundCapabilityClaims(message, {
|
|
292
|
+
sourceClaims = [], liveSurfaces = [], host = null,
|
|
293
|
+
now = new Date().toISOString(), maxAgeMs = MAX_AGE_MS,
|
|
294
|
+
} = {}) {
|
|
295
|
+
const claims = extractClaims(message);
|
|
296
|
+
const contradictions = [];
|
|
297
|
+
const unresolved = [];
|
|
298
|
+
const passed = [];
|
|
299
|
+
const currentTime = Date.parse(now);
|
|
300
|
+
const live = liveSurfaces.flatMap((receipt) => {
|
|
301
|
+
try {
|
|
302
|
+
validateLiveSurfaceReceipt(receipt);
|
|
303
|
+
const age = currentTime - Date.parse(receipt.observedAt);
|
|
304
|
+
const hostMatches = !host || receipt.host === 'shared' || receipt.host === host;
|
|
305
|
+
return hostMatches && Number.isFinite(age) && age >= 0 && age <= maxAgeMs ? [receipt] : [];
|
|
306
|
+
} catch { return []; }
|
|
307
|
+
});
|
|
308
|
+
const sources = sourceClaims.flatMap((receipt) => { try { return [validateSourceClaimReceipt(receipt)]; } catch { return []; } });
|
|
309
|
+
for (const claim of claims) {
|
|
310
|
+
if (claim.class === 'behavior') {
|
|
311
|
+
const match = sources.find((receipt) => receipt.claimDigest === digest(normalize(claim.text)));
|
|
312
|
+
(match ? passed : unresolved).push({ ...claim, evidence: match?.receiptSha256,
|
|
313
|
+
reason: match ? 'exact behavior claim is source-bound' : 'behavior claim has no exact source receipt' });
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
const executable = executableFor(claim.tool);
|
|
317
|
+
const candidates = live.filter((receipt) => receipt.executable === executable);
|
|
318
|
+
if (claim.class === 'current-version') {
|
|
319
|
+
const observed = candidates.find((receipt) => receipt.observationClass === 'current-version');
|
|
320
|
+
if (!observed) unresolved.push({ ...claim, reason: 'current version has no fresh live receipt' });
|
|
321
|
+
else if (observed.observedVersion !== claim.version) contradictions.push({ ...claim,
|
|
322
|
+
reason: `live current version is ${observed.observedVersion}`, evidence: observed.receiptSha256 });
|
|
323
|
+
else passed.push({ ...claim, evidence: observed.receiptSha256, reason: 'current version matches live receipt' });
|
|
324
|
+
} else if (claim.class === 'latest-version') {
|
|
325
|
+
const observed = candidates.find((receipt) => receipt.observationClass === 'latest-version');
|
|
326
|
+
if (!observed) unresolved.push({ ...claim, reason: 'latest version has no fresh registry receipt' });
|
|
327
|
+
else if (observed.observedVersion !== claim.version) contradictions.push({ ...claim,
|
|
328
|
+
reason: `public registry latest version is ${observed.observedVersion}`, evidence: observed.receiptSha256 });
|
|
329
|
+
else passed.push({ ...claim, evidence: observed.receiptSha256, reason: 'latest version matches registry receipt' });
|
|
330
|
+
} else {
|
|
331
|
+
const observed = candidates.find((receipt) => receipt.observationClass === 'health');
|
|
332
|
+
let state = 'UNKNOWN';
|
|
333
|
+
if (observed && claim.state === 'healthy') state = observed.healthVerdict === 'PASS' ? 'PASS'
|
|
334
|
+
: observed.healthVerdict === 'FAIL' ? 'FAIL' : 'UNKNOWN';
|
|
335
|
+
else if (observed && claim.state === 'unhealthy') state = observed.healthVerdict === 'FAIL' ? 'PASS'
|
|
336
|
+
: observed.healthVerdict === 'PASS' ? 'FAIL' : 'UNKNOWN';
|
|
337
|
+
else if (observed && ['reachable', 'available'].includes(claim.state)) state = observed.reachable === true ? 'PASS' : 'FAIL';
|
|
338
|
+
else if (observed && ['unreachable', 'down'].includes(claim.state)) state = observed.reachable === false ? 'PASS' : 'FAIL';
|
|
339
|
+
if (state === 'PASS') passed.push({ ...claim, evidence: observed.receiptSha256 });
|
|
340
|
+
else if (state === 'FAIL') contradictions.push({ ...claim, evidence: observed.receiptSha256,
|
|
341
|
+
reason: 'fresh live health receipt contradicts the claim' });
|
|
342
|
+
else unresolved.push({ ...claim, reason: 'health or reachability claim has no conclusive fresh live receipt' });
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return { schemaVersion: 1, kind: 'ruvnet-brain-evidence-bound-capability-audit', claims, passed,
|
|
346
|
+
contradictions, unresolved, verdict: contradictions.length ? 'FAIL' : unresolved.length ? 'UNKNOWN' : 'PASS' };
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function aggregatePayload(aggregate) {
|
|
350
|
+
const { aggregateSha256: _aggregateSha256, signature: _signature, ...payload } = aggregate || {};
|
|
351
|
+
return payload;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function validateAggregatePayload(payload) {
|
|
355
|
+
if (payload?.schemaVersion !== 1 || payload?.kind !== 'ruvnet-brain-capability-claim-aggregate'
|
|
356
|
+
|| !HEX40.test(String(payload.identity?.sourceSha || '')) || !HEX64.test(String(payload.identity?.artifactSha256 || ''))
|
|
357
|
+
|| !Array.isArray(payload.lanes) || !Array.isArray(payload.untested)
|
|
358
|
+
|| !Array.isArray(payload.sourceClaimDigests) || !Array.isArray(payload.liveSurfaceDigests)
|
|
359
|
+
|| payload.sourceClaimDigests.some((value) => !HEX64.test(String(value)))
|
|
360
|
+
|| payload.liveSurfaceDigests.some((value) => !HEX64.test(String(value)))) {
|
|
361
|
+
throw new Error('capability claim aggregate payload is malformed');
|
|
362
|
+
}
|
|
363
|
+
const laneIds = new Set();
|
|
364
|
+
for (const lane of payload.lanes) {
|
|
365
|
+
const id = `${lane.os}/${lane.host}`;
|
|
366
|
+
const claims = lane.claims || {};
|
|
367
|
+
const claimVerdicts = CLAIM_CLASSES.map((name) => claims[name]);
|
|
368
|
+
const derivedVerdict = claimVerdicts.includes('FAIL') ? 'FAIL'
|
|
369
|
+
: claimVerdicts.includes('UNKNOWN') ? 'PARTIAL' : 'PASS';
|
|
370
|
+
if (!OS_LANES.includes(lane.os) || !HOSTS.includes(lane.host) || laneIds.has(id)
|
|
371
|
+
|| claimVerdicts.some((verdict) => !['PASS', 'FAIL', 'UNKNOWN'].includes(verdict))
|
|
372
|
+
|| lane.verdict !== derivedVerdict
|
|
373
|
+
|| !HEX64.test(String(lane.receiptSha256 || ''))) throw new Error('capability claim aggregate lane is incomplete');
|
|
374
|
+
laneIds.add(id);
|
|
375
|
+
}
|
|
376
|
+
const os = [...new Set(payload.lanes.map(({ os: laneOs }) => laneOs))].sort();
|
|
377
|
+
const hosts = [...new Set(payload.lanes.map(({ host }) => host))].sort();
|
|
378
|
+
if (canonicalJson(os) !== canonicalJson(payload.os) || canonicalJson(hosts) !== canonicalJson(payload.hosts)) {
|
|
379
|
+
throw new Error('capability claim aggregate lane projection differs');
|
|
380
|
+
}
|
|
381
|
+
const complete = OS_LANES.every((laneOs) => HOSTS.every((host) => laneIds.has(`${laneOs}/${host}`)));
|
|
382
|
+
const laneVerdicts = payload.lanes.map(({ verdict }) => verdict);
|
|
383
|
+
const derivedVerdict = laneVerdicts.includes('FAIL') ? 'FAIL'
|
|
384
|
+
: !complete || payload.untested.length || laneVerdicts.includes('PARTIAL') ? 'PARTIAL' : 'PASS';
|
|
385
|
+
if (payload.verdict !== derivedVerdict) throw new Error('capability claim aggregate verdict differs from its lanes');
|
|
386
|
+
if (payload.verdict === 'PASS' && (!complete || payload.untested.length)) {
|
|
387
|
+
throw new Error('capability claim PASS lacks the complete OS and host matrix');
|
|
388
|
+
}
|
|
389
|
+
if (payload.verdict === 'PARTIAL' && !payload.untested.length && !laneVerdicts.includes('PARTIAL')) {
|
|
390
|
+
throw new Error('capability claim PARTIAL has no disclosed incomplete scope');
|
|
391
|
+
}
|
|
392
|
+
return payload;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
export function signCapabilityClaimAggregate(input, privateKey) {
|
|
396
|
+
const lanes = [...(input.lanes || [])].sort((a, b) => `${a.os}/${a.host}`.localeCompare(`${b.os}/${b.host}`));
|
|
397
|
+
const complete = OS_LANES.every((laneOs) => HOSTS.every((host) =>
|
|
398
|
+
lanes.some((lane) => lane.os === laneOs && lane.host === host)));
|
|
399
|
+
const laneVerdicts = lanes.map(({ verdict }) => verdict);
|
|
400
|
+
const untested = [...new Set(input.untested || [])].sort();
|
|
401
|
+
const verdict = laneVerdicts.includes('FAIL') ? 'FAIL'
|
|
402
|
+
: !complete || untested.length || laneVerdicts.includes('PARTIAL') ? 'PARTIAL' : 'PASS';
|
|
403
|
+
const payload = validateAggregatePayload({
|
|
404
|
+
schemaVersion: 1, kind: 'ruvnet-brain-capability-claim-aggregate', identity: input.identity,
|
|
405
|
+
os: [...new Set(lanes.map(({ os: laneOs }) => laneOs))].sort(),
|
|
406
|
+
hosts: [...new Set(lanes.map(({ host }) => host))].sort(), lanes,
|
|
407
|
+
sourceClaimDigests: [...new Set(input.sourceClaimDigests || [])].sort(),
|
|
408
|
+
liveSurfaceDigests: [...new Set(input.liveSurfaceDigests || [])].sort(),
|
|
409
|
+
verdict,
|
|
410
|
+
untested,
|
|
411
|
+
});
|
|
412
|
+
const aggregateSha256 = digest(payload);
|
|
413
|
+
const signed = { ...payload, aggregateSha256 };
|
|
414
|
+
return { ...signed, signature: crypto.sign(null, Buffer.from(canonicalJson(signed)), privateKey).toString('base64') };
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
export function verifyCapabilityClaimAggregate(aggregate, publicKey, expectedIdentity = null) {
|
|
418
|
+
if (!HEX64.test(String(aggregate?.aggregateSha256 || '')) || typeof aggregate?.signature !== 'string') {
|
|
419
|
+
throw new Error('capability claim aggregate signature envelope is malformed');
|
|
420
|
+
}
|
|
421
|
+
const payload = validateAggregatePayload(aggregatePayload(aggregate));
|
|
422
|
+
if (digest(payload) !== aggregate.aggregateSha256) throw new Error('capability claim aggregate digest mismatch');
|
|
423
|
+
const signed = { ...payload, aggregateSha256: aggregate.aggregateSha256 };
|
|
424
|
+
if (!crypto.verify(null, Buffer.from(canonicalJson(signed)), publicKey, Buffer.from(aggregate.signature, 'base64'))) {
|
|
425
|
+
throw new Error('capability claim aggregate signature mismatch');
|
|
426
|
+
}
|
|
427
|
+
if (expectedIdentity && canonicalJson(expectedIdentity) !== canonicalJson(aggregate.identity)) {
|
|
428
|
+
throw new Error('capability claim aggregate identity differs');
|
|
429
|
+
}
|
|
430
|
+
return aggregate;
|
|
431
|
+
}
|