ruvnet-brain 4.2.2-dev → 4.3.1
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 +15 -14
- package/bin/install.mjs +11 -2
- 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/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 +7 -2
- package/scripts/build-bundle.mjs +82 -6
- package/scripts/card-from-source.mjs +32 -6
- package/scripts/corpus-aggregates.mjs +194 -0
- package/scripts/corpus-candidate.mjs +56 -36
- package/scripts/corpus-reconcile.mjs +295 -28
- package/scripts/corpus-seed-publish.mjs +3 -3
- 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 +5 -0
- package/scripts/metaharness-gate.mjs +58 -0
- package/scripts/nightly-gists.sh +3 -3
- package/scripts/nightly-wrapper.sh +75 -69
- 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 +50 -5
- package/scripts/release-projection.mjs +86 -0
- package/scripts/release-transaction-provider.mjs +43 -102
- package/scripts/release-transaction.mjs +139 -51
- package/scripts/release.mjs +15 -7
- 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 +9 -3
- 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
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
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
|
+
export const CAPABILITY_INVENTORY_SCHEMA_VERSION = 1;
|
|
8
|
+
const SHA256 = /^[a-f0-9]{64}$/;
|
|
9
|
+
const RUVNET_TERM = /\b(?:ruvnet|ruflo|ruvector|agentdb|rulake|ruview|agentic(?:-flow|-qe|ow)?|rvf|qudag|safla)\b/i;
|
|
10
|
+
const CLAIM_PATTERN = /([A-Za-z0-9@._:/+-]+(?:\s+[A-Za-z0-9@._:/+-]+){0,7})\s+((?:is|are)\s+not|isn't|aren't|is|are)\s+(?:currently\s+)?(installed|registered|present)\b/gi;
|
|
11
|
+
|
|
12
|
+
const sha256 = (value) => crypto.createHash('sha256').update(value).digest('hex');
|
|
13
|
+
const digest = (value) => sha256(JSON.stringify(value));
|
|
14
|
+
const unique = (values) => [...new Set(values.filter(Boolean))];
|
|
15
|
+
const normalizeTokens = (value) => unique(String(value || '').toLowerCase().split(/[^a-z0-9]+/).filter(Boolean));
|
|
16
|
+
|
|
17
|
+
function defaultRoots({ home, host }) {
|
|
18
|
+
const codexHome = process.env.CODEX_HOME || path.join(home, '.codex');
|
|
19
|
+
const hostRoots = host === 'codex'
|
|
20
|
+
? [path.join(codexHome, 'plugins', 'cache', 'ruflo'), path.join(codexHome, 'plugins', 'cache', 'ruvnet-brain'), path.join(codexHome, 'skills')]
|
|
21
|
+
: [path.join(home, '.claude', 'plugins', 'cache', 'ruflo'), path.join(home, '.claude', 'plugins', 'cache', 'ruvnet-brain'), path.join(home, '.claude', 'skills')];
|
|
22
|
+
return unique([...hostRoots, path.join(home, '.agents', 'skills'), process.env.CLAUDE_PLUGIN_ROOT]);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function parseSkillName(bytes) {
|
|
26
|
+
const frontMatter = /^---\s*\n([\s\S]*?)\n---(?:\s*\n|$)/.exec(bytes)?.[1] || '';
|
|
27
|
+
const raw = /^name:\s*(.+?)\s*$/m.exec(frontMatter)?.[1] || '';
|
|
28
|
+
return raw.replace(/^['"]|['"]$/g, '').trim();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function skillRef(relative, name) {
|
|
32
|
+
const parts = relative.split(/[\\/]/).filter(Boolean);
|
|
33
|
+
const skillIndex = parts.lastIndexOf('skills');
|
|
34
|
+
if (skillIndex <= 0) return name;
|
|
35
|
+
const before = parts[skillIndex - 1];
|
|
36
|
+
const plugin = /^v?\d+(?:\.\d+){1,3}(?:[-+].*)?$/.test(before)
|
|
37
|
+
? parts[skillIndex - 2]
|
|
38
|
+
: before;
|
|
39
|
+
return plugin && plugin !== name ? `${plugin}:${name}` : name;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function walkSkillFiles(root, errors, { maxEntries = 20_000 } = {}) {
|
|
43
|
+
const files = [];
|
|
44
|
+
const pending = [root];
|
|
45
|
+
const visited = new Set();
|
|
46
|
+
let entries = 0;
|
|
47
|
+
while (pending.length) {
|
|
48
|
+
const current = pending.pop();
|
|
49
|
+
let real;
|
|
50
|
+
try { real = fs.realpathSync(current); } catch (error) {
|
|
51
|
+
errors.push(`${current}: ${error.code || error.message}`);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (visited.has(real)) continue;
|
|
55
|
+
visited.add(real);
|
|
56
|
+
let children;
|
|
57
|
+
try { children = fs.readdirSync(real, { withFileTypes: true }); } catch (error) {
|
|
58
|
+
errors.push(`${current}: ${error.code || error.message}`);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
for (const child of children) {
|
|
62
|
+
entries += 1;
|
|
63
|
+
if (entries > maxEntries) {
|
|
64
|
+
errors.push(`${root}: inventory exceeded ${maxEntries} filesystem entries`);
|
|
65
|
+
return files;
|
|
66
|
+
}
|
|
67
|
+
const file = path.join(real, child.name);
|
|
68
|
+
if (child.isDirectory() || child.isSymbolicLink()) pending.push(file);
|
|
69
|
+
else if (child.isFile() && child.name === 'SKILL.md') files.push(file);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return files;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function buildCapabilityInventoryReceipt({
|
|
76
|
+
host = process.env.RUVNET_HOOK_HOST || 'claude',
|
|
77
|
+
home = process.env.HOME || process.env.USERPROFILE || os.homedir(),
|
|
78
|
+
roots = process.env.RUVNET_CAPABILITY_ROOTS
|
|
79
|
+
? process.env.RUVNET_CAPABILITY_ROOTS.split(path.delimiter).filter(Boolean)
|
|
80
|
+
: defaultRoots({ home, host }),
|
|
81
|
+
now = new Date().toISOString(),
|
|
82
|
+
} = {}) {
|
|
83
|
+
if (!['claude', 'codex'].includes(host)) throw new Error(`unsupported capability inventory host: ${host}`);
|
|
84
|
+
const errors = [];
|
|
85
|
+
const observedRoots = [];
|
|
86
|
+
const entries = [];
|
|
87
|
+
for (const unresolved of unique(roots.map((root) => path.resolve(root)))) {
|
|
88
|
+
let stat;
|
|
89
|
+
try { stat = fs.statSync(unresolved); } catch (error) {
|
|
90
|
+
if (error?.code === 'ENOENT') { observedRoots.push({ path: unresolved, present: false }); continue; }
|
|
91
|
+
errors.push(`${unresolved}: ${error.code || error.message}`);
|
|
92
|
+
observedRoots.push({ path: unresolved, present: true, readable: false });
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (!stat.isDirectory()) {
|
|
96
|
+
errors.push(`${unresolved}: capability root is not a directory`);
|
|
97
|
+
observedRoots.push({ path: unresolved, present: true, readable: false });
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
observedRoots.push({ path: unresolved, present: true, readable: true });
|
|
101
|
+
for (const file of walkSkillFiles(unresolved, errors)) {
|
|
102
|
+
let bytes;
|
|
103
|
+
try { bytes = fs.readFileSync(file); } catch (error) {
|
|
104
|
+
errors.push(`${file}: ${error.code || error.message}`);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const name = parseSkillName(bytes.toString('utf8'));
|
|
108
|
+
if (!name) {
|
|
109
|
+
errors.push(`${file}: SKILL.md has no parseable name`);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
const relative = path.relative(unresolved, file).split(path.sep).join('/');
|
|
113
|
+
entries.push({
|
|
114
|
+
type: 'skill',
|
|
115
|
+
name,
|
|
116
|
+
ref: skillRef(relative, name),
|
|
117
|
+
sourcePath: file,
|
|
118
|
+
sha256: sha256(bytes),
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
entries.sort((a, b) => a.ref.localeCompare(b.ref) || a.sourcePath.localeCompare(b.sourcePath));
|
|
123
|
+
const deduplicated = entries.filter((entry, index) => index === 0
|
|
124
|
+
|| entry.ref !== entries[index - 1].ref
|
|
125
|
+
|| entry.sha256 !== entries[index - 1].sha256);
|
|
126
|
+
const unsigned = {
|
|
127
|
+
schemaVersion: CAPABILITY_INVENTORY_SCHEMA_VERSION,
|
|
128
|
+
kind: 'ruvnet-brain-capability-inventory',
|
|
129
|
+
host,
|
|
130
|
+
observedAt: now,
|
|
131
|
+
completeness: errors.length ? 'unknown' : 'complete',
|
|
132
|
+
roots: observedRoots,
|
|
133
|
+
entries: deduplicated,
|
|
134
|
+
errors,
|
|
135
|
+
};
|
|
136
|
+
return { ...unsigned, inventoryDigest: digest(unsigned) };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function validateCapabilityInventoryReceipt(receipt) {
|
|
140
|
+
const { inventoryDigest, ...unsigned } = receipt || {};
|
|
141
|
+
if (receipt?.schemaVersion !== CAPABILITY_INVENTORY_SCHEMA_VERSION
|
|
142
|
+
|| receipt?.kind !== 'ruvnet-brain-capability-inventory'
|
|
143
|
+
|| !['claude', 'codex'].includes(receipt?.host)
|
|
144
|
+
|| !['complete', 'unknown'].includes(receipt?.completeness)
|
|
145
|
+
|| !Array.isArray(receipt?.roots)
|
|
146
|
+
|| !Array.isArray(receipt?.entries)
|
|
147
|
+
|| !Array.isArray(receipt?.errors)
|
|
148
|
+
|| !SHA256.test(String(inventoryDigest || ''))
|
|
149
|
+
|| digest(unsigned) !== inventoryDigest) {
|
|
150
|
+
throw new Error('capability inventory receipt digest or schema is invalid');
|
|
151
|
+
}
|
|
152
|
+
if (receipt.entries.some((entry) => entry?.type !== 'skill' || !entry.name || !entry.ref
|
|
153
|
+
|| !path.isAbsolute(entry.sourcePath || '') || !SHA256.test(String(entry.sha256 || '')))) {
|
|
154
|
+
throw new Error('capability inventory receipt contains an invalid entry');
|
|
155
|
+
}
|
|
156
|
+
return receipt;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function extractInstallationClaims(message) {
|
|
160
|
+
const claims = [];
|
|
161
|
+
const prose = String(message || '')
|
|
162
|
+
.replace(/```[\s\S]*?```/g, ' ')
|
|
163
|
+
.replace(/`[^`\n]*`/g, ' ')
|
|
164
|
+
.replace(/"[^"\n]*"|'[^'\n]*'/g, ' ')
|
|
165
|
+
.replace(/^\s*>.*$/gm, ' ');
|
|
166
|
+
for (const match of prose.matchAll(CLAIM_PATTERN)) {
|
|
167
|
+
const prefix = prose.slice(Math.max(0, match.index - 16), match.index);
|
|
168
|
+
if (/\b(?:if|unless|whether)\s*$/i.test(prefix)) continue;
|
|
169
|
+
const words = match[1].trim().split(/\s+/);
|
|
170
|
+
const firstRuv = words.findIndex((word) => RUVNET_TERM.test(word));
|
|
171
|
+
if (firstRuv < 0) continue;
|
|
172
|
+
if (words.slice(0, firstRuv).some((word) => /^(?:if|unless|whether)$/i.test(word))) continue;
|
|
173
|
+
const subject = words.slice(firstRuv).join(' ');
|
|
174
|
+
claims.push({
|
|
175
|
+
text: match[0],
|
|
176
|
+
subject,
|
|
177
|
+
tokens: normalizeTokens(subject),
|
|
178
|
+
polarity: /\bnot\b|n't/i.test(match[2]) ? 'absent' : 'present',
|
|
179
|
+
predicate: match[3].toLowerCase(),
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
return claims;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function matchEntry(claim, entries) {
|
|
186
|
+
let best = null;
|
|
187
|
+
for (const entry of entries) {
|
|
188
|
+
const tokens = normalizeTokens(`${entry.ref} ${entry.name}`);
|
|
189
|
+
const matched = claim.tokens.length === 1
|
|
190
|
+
? tokens.includes(claim.tokens[0])
|
|
191
|
+
: tokens.every((token) => claim.tokens.includes(token));
|
|
192
|
+
if (!matched) continue;
|
|
193
|
+
if (!best || tokens.length > best.tokens.length) best = { entry, tokens };
|
|
194
|
+
}
|
|
195
|
+
return best?.entry || null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function auditCapabilityClaims(message, receipt) {
|
|
199
|
+
validateCapabilityInventoryReceipt(receipt);
|
|
200
|
+
const claims = extractInstallationClaims(message);
|
|
201
|
+
const contradictions = [];
|
|
202
|
+
const unresolved = [];
|
|
203
|
+
for (const claim of claims) {
|
|
204
|
+
const matched = matchEntry(claim, receipt.entries);
|
|
205
|
+
if (claim.polarity === 'absent' && matched) {
|
|
206
|
+
contradictions.push({ ...claim, matchedRef: matched.ref, sourcePath: matched.sourcePath,
|
|
207
|
+
reason: 'absence claim contradicts installed source bytes' });
|
|
208
|
+
} else if (claim.polarity === 'present' && !matched && receipt.completeness === 'complete') {
|
|
209
|
+
contradictions.push({ ...claim, matchedRef: null,
|
|
210
|
+
reason: 'presence claim has no matching entry in the complete inventory' });
|
|
211
|
+
} else if (!matched && receipt.completeness !== 'complete') {
|
|
212
|
+
unresolved.push({ ...claim, reason: 'capability inventory is incomplete' });
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return {
|
|
216
|
+
schemaVersion: 1,
|
|
217
|
+
kind: 'ruvnet-brain-capability-claim-audit',
|
|
218
|
+
inventoryDigest: receipt.inventoryDigest,
|
|
219
|
+
host: receipt.host,
|
|
220
|
+
claims,
|
|
221
|
+
contradictions,
|
|
222
|
+
unresolved,
|
|
223
|
+
verdict: contradictions.length ? 'FAIL' : unresolved.length ? 'UNKNOWN' : 'PASS',
|
|
224
|
+
};
|
|
225
|
+
}
|