mirror of
https://github.com/ruvnet/RuView
synced 2026-07-29 18:31:44 +00:00
feat(ruview): secure community metaharness flywheel (#1467)
* feat(ruview): add secure community metaharness flywheel * fix(ruview): canonicalize manifest line endings
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env node
|
||||
import { createHash } from 'node:crypto';
|
||||
import { readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
||||
import { dirname, join, relative } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { gateFingerprint } from '../flywheel/gate.mjs';
|
||||
import { loadBrain } from '../src/brain.js';
|
||||
|
||||
const ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
||||
const quiet = process.argv.includes('--quiet');
|
||||
const INCLUDE = ['package.json', 'bin', 'src', 'skills', '.claude', '.mcp', '.harness/claims.json', '.harness/mcp-policy.json', 'brain', 'flywheel', 'scripts', 'CLAUDE.md', 'README.md', 'LICENSE'];
|
||||
const sha = (value) => createHash('sha256').update(value).digest('hex');
|
||||
const canonicalFile = (path) => readFileSync(path, 'utf8').replace(/\r\n/g, '\n');
|
||||
const files = [];
|
||||
function walk(path) {
|
||||
const stat = statSync(path);
|
||||
if (stat.isDirectory()) {
|
||||
for (const name of readdirSync(path).sort()) walk(join(path, name));
|
||||
} else files.push(path);
|
||||
}
|
||||
for (const entry of INCLUDE) walk(join(ROOT, entry));
|
||||
const hashes = Object.fromEntries(files.sort().map((path) => [relative(ROOT, path).replaceAll('\\', '/'), sha(canonicalFile(path))]));
|
||||
const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8'));
|
||||
const manifest = {
|
||||
schema: 2,
|
||||
generator: 'RuView metaharness provenance v2',
|
||||
template: 'vertical:ruview',
|
||||
name: pkg.name,
|
||||
version: pkg.version,
|
||||
hosts: ['claude-code', 'codex'],
|
||||
toolPolicy: 'default-deny-mutations',
|
||||
files: hashes,
|
||||
filesDigest: sha(JSON.stringify(hashes)),
|
||||
brainDigest: loadBrain().digest,
|
||||
gateFingerprint: gateFingerprint(),
|
||||
developmentPins: pkg.devDependencies,
|
||||
meta: { surface: 'cli+mcp+brain+flywheel', adr: 'ADR-182/263' },
|
||||
};
|
||||
const json = `${JSON.stringify(manifest, null, 2)}\n`;
|
||||
writeFileSync(join(ROOT, '.harness', 'manifest.json'), json);
|
||||
writeFileSync(join(ROOT, '.harness', 'manifest.sha256'), `${sha(json)} manifest.json\n`);
|
||||
if (!quiet) console.log(JSON.stringify({ ok: true, files: files.length, digest: sha(json) }));
|
||||
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env node
|
||||
import { createHash } from 'node:crypto';
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
const ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
||||
const quiet = process.argv.includes('--quiet');
|
||||
const sha = (value) => createHash('sha256').update(value).digest('hex');
|
||||
const canonicalFile = (path) => readFileSync(path, 'utf8').replace(/\r\n/g, '\n');
|
||||
const path = join(ROOT, '.harness', 'manifest.json');
|
||||
const raw = readFileSync(path);
|
||||
const manifest = JSON.parse(raw);
|
||||
const findings = [];
|
||||
for (const [name, expected] of Object.entries(manifest.files || {})) {
|
||||
const target = join(ROOT, name);
|
||||
if (!existsSync(target)) findings.push(`${name}:missing`);
|
||||
else if (sha(canonicalFile(target)) !== expected) findings.push(`${name}:hash-mismatch`);
|
||||
}
|
||||
const expectedOuter = readFileSync(join(ROOT, '.harness', 'manifest.sha256'), 'utf8').trim().split(/\s+/)[0];
|
||||
if (sha(raw) !== expectedOuter) findings.push('manifest.sha256:mismatch');
|
||||
if (!quiet) console.log(JSON.stringify({ ok: findings.length === 0, files: Object.keys(manifest.files || {}).length, findings }, null, 2));
|
||||
process.exit(findings.length ? 1 : 0);
|
||||
Reference in New Issue
Block a user