@ruvector/ruvllm-gpu-loop 0.0.0-stage → 1.1.0

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.
@@ -0,0 +1,26 @@
1
+ {
2
+ "schema": "ruvllm-gpu-loop/v1",
3
+ "stateRoot": "/absolute/private/gpu-loop-state",
4
+ "intervalSeconds": 60,
5
+ "maxAttempts": 2,
6
+ "budgetUsd": 0,
7
+ "jobs": [
8
+ {
9
+ "id": "qwen35-4b-paired-evaluation",
10
+ "provider": "vast.ai",
11
+ "model": {
12
+ "repo": "Qwen/Qwen3.5-4B",
13
+ "revision": "851bf6e806efd8d0a36b00ddf55e13ccb7b8cd0a"
14
+ },
15
+ "task": "evaluate",
16
+ "secretRef": "ruos.vast.primary",
17
+ "resource": { "instanceId": "REPLACE_WITH_EXISTING_INSTANCE_ID" },
18
+ "maxCostUsd": 1,
19
+ "timeoutSeconds": 1800,
20
+ "experiment": {
21
+ "configPath": "/absolute/private/operator-profile/runtime.json",
22
+ "planPath": "/absolute/experiment-inputs/plan.json"
23
+ }
24
+ }
25
+ ]
26
+ }
package/package.json CHANGED
@@ -1,6 +1,71 @@
1
1
  {
2
2
  "name": "@ruvector/ruvllm-gpu-loop",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.1.0",
4
+ "type": "module",
5
+ "description": "RuVLLM GPU job loop: bounded weight experiments, a durable GPU job scheduler with a Vast.ai host adapter, adapter archives and verified paired measurements (split out of @ruvector/ruvllm 4.0.x)",
6
+ "license": "Apache-2.0",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/ruvnet/ruvllm.git",
10
+ "directory": "integrations/metaharness"
11
+ },
12
+ "homepage": "https://github.com/ruvnet/ruvllm/tree/main/integrations/metaharness#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/ruvnet/ruvllm/issues"
15
+ },
16
+ "keywords": [
17
+ "ruvllm",
18
+ "ruvllm-gpu-loop",
19
+ "metaharness",
20
+ "lora",
21
+ "adapters",
22
+ "gpu",
23
+ "gpu-scheduler",
24
+ "vast.ai",
25
+ "evaluation",
26
+ "weight-editing",
27
+ "ruvector"
28
+ ],
29
+ "engines": {
30
+ "node": ">=22"
31
+ },
32
+ "bin": {
33
+ "ruvllm-gpu": "src/cli.mjs",
34
+ "ruvllm-gpu-loop": "src/gpu-loop-cli.mjs",
35
+ "ruvllm-adapters": "src/adapters-cli.mjs"
36
+ },
37
+ "exports": {
38
+ ".": "./src/engine.mjs",
39
+ "./adapters": "./src/adapters.mjs",
40
+ "./fixture": "./src/fixture.mjs",
41
+ "./gpu-loop": "./src/gpu-loop.mjs",
42
+ "./gpu-host": "./src/gpu-host.mjs",
43
+ "./gpu-spool-adapter": "./src/gpu-spool-adapter.mjs",
44
+ "./vast-host-adapter": "./src/vast-host-adapter.mjs",
45
+ "./package.json": "./package.json"
46
+ },
47
+ "files": [
48
+ "src/",
49
+ "examples/",
50
+ "README.md",
51
+ "GPU-LOOP.md",
52
+ "LICENSE",
53
+ "NOTICE"
54
+ ],
55
+ "scripts": {
56
+ "guard:name": "node scripts/check-package-name.mjs",
57
+ "test": "node --test test/*.test.mjs",
58
+ "bench": "node bench/run.mjs",
59
+ "prepack": "cp ../../LICENSE ../../NOTICE .",
60
+ "postpack": "rm -f LICENSE NOTICE",
61
+ "prepublishOnly": "node scripts/check-package-name.mjs"
62
+ },
63
+ "publishConfig": {
64
+ "access": "public"
65
+ },
66
+ "dependencies": {
67
+ "@metaharness/darwin": "^0.12.0",
68
+ "@metaharness/flywheel": "^0.1.13",
69
+ "@metaharness/weight-eft": "^0.1.2"
70
+ }
71
+ }
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env node
2
+ import { exportArchive, trainingPlan, selectDevelopmentCandidate } from './adapters.mjs';
3
+
4
+ const operations = { export: exportArchive, 'training-plan': trainingPlan, select: selectDevelopmentCandidate };
5
+ if (process.argv.length !== 3 || !Object.hasOwn(operations, process.argv[2])) {
6
+ process.stderr.write('Usage: node src/adapters-cli.mjs export|training-plan|select < input.json\nJSON data only; no training, inference or activation is executed.\n');
7
+ process.exitCode = process.argv[2] === '--help' ? 0 : 2;
8
+ } else {
9
+ try {
10
+ const chunks = [];
11
+ let bytes = 0;
12
+ for await (const chunk of process.stdin) {
13
+ bytes += chunk.length;
14
+ if (bytes > 16 * 1024 * 1024) throw new Error('Input exceeds 16 MiB');
15
+ chunks.push(chunk);
16
+ }
17
+ const input = JSON.parse(Buffer.concat(chunks).toString('utf8'));
18
+ process.stdout.write(JSON.stringify(await operations[process.argv[2]](input), null, 2) + '\n');
19
+ } catch (error) {
20
+ process.stderr.write(JSON.stringify({ ok: false, error: error.message }) + '\n');
21
+ process.exitCode = 1;
22
+ }
23
+ }
@@ -0,0 +1,91 @@
1
+ // MetaHarness adapters are data-only: no provider requests, trainer launch or activation.
2
+ import { createHash } from 'node:crypto';
3
+ import { exportTrainingData, twoStagePlan } from '@metaharness/weight-eft';
4
+ import { runContrastive, digestDocument } from '@metaharness/darwin/contrastive';
5
+
6
+ const hash = value => createHash('sha256').update(JSON.stringify(value)).digest('hex');
7
+ const require = (condition, message) => { if (!condition) throw new Error(message); };
8
+ const identifier = value => typeof value === 'string' && /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$/.test(value);
9
+
10
+ /** Export reviewed caller-asserted trajectories; held-out instance IDs are always excluded. */
11
+ export function exportArchive({ archive, evalHoldout, maxTokens = 12000 }) {
12
+ require(Array.isArray(archive) && archive.length <= 10000, 'Archive limit is 10000 trajectories');
13
+ require(Array.isArray(evalHoldout) && evalHoldout.every(identifier), 'Explicit eval holdout IDs required');
14
+ require(Number.isInteger(maxTokens) && maxTokens >= 1 && maxTokens <= 32768, 'Invalid token budget');
15
+ require(Buffer.byteLength(JSON.stringify(archive)) <= 16 * 1024 * 1024, 'Archive exceeds 16 MiB');
16
+ for (const row of archive) {
17
+ require(row && identifier(row.instance_id) && typeof row.model === 'string' && row.model.length > 0,
18
+ 'Invalid trajectory identity');
19
+ require(['cheap', 'frontier'].includes(row.tier) && typeof row.resolved === 'boolean', 'Invalid trajectory outcome');
20
+ require(typeof row.model_patch === 'string' && Array.isArray(row.messages) && row.messages.length <= 4096,
21
+ 'Invalid trajectory content');
22
+ for (const message of row.messages) {
23
+ require(message && ['system', 'user', 'assistant', 'tool'].includes(message.role)
24
+ && (typeof message.content === 'string' || message.content === null), 'Invalid message');
25
+ }
26
+ }
27
+ const result = exportTrainingData(archive, {
28
+ evalHoldout, maxTokens, truncateOverLength: false, dropRewardHacked: true,
29
+ });
30
+ return {
31
+ schema: 'ruvllm-training-export/v1', ...result,
32
+ archiveSha256: hash(archive), holdoutSha256: hash(evalHoldout),
33
+ evidenceOrigin: 'caller-asserted-trajectories', trainingExecuted: false,
34
+ productionActivation: false,
35
+ };
36
+ }
37
+
38
+ /** Upstream command is informational; this checkout has no `ruvllm microlora` CLI. */
39
+ export function trainingPlan({ base, sftPath, dpoPath, adapterPrefix }) {
40
+ require(base && typeof base.id === 'string' && Number.isFinite(base.paramsB), 'Invalid model');
41
+ for (const path of [sftPath, dpoPath, adapterPrefix]) require(typeof path === 'string' && path.length > 0, 'Path required');
42
+ return {
43
+ schema: 'ruvllm-training-plan/v1',
44
+ plans: twoStagePlan(base, sftPath, dpoPath, adapterPrefix),
45
+ executionStatus: 'not_executed',
46
+ runnerCompatibility: 'adapter-required',
47
+ reason: 'The upstream ruvllm microlora command is not implemented by this standalone checkout. Use a separately validated trainer; DPO requires an additional trainer implementation.',
48
+ productionActivation: false,
49
+ };
50
+ }
51
+
52
+ /** Replay bounded measured scores through Darwin. This ranks development evidence only. */
53
+ export async function selectDevelopmentCandidate({ baseline, candidates, envelope }) {
54
+ require(envelope && typeof envelope === 'object' && !Array.isArray(envelope), 'Immutable experiment envelope required');
55
+ require(Array.isArray(candidates) && candidates.length > 0 && candidates.length <= 32, 'Expected 1..32 candidates');
56
+ const all = [baseline, ...candidates];
57
+ for (const candidate of all) {
58
+ require(candidate && identifier(candidate.id) && Number.isFinite(candidate.score)
59
+ && candidate.score >= 0 && candidate.score <= 1
60
+ && /^[a-f0-9]{64}$/.test(candidate.evidenceSha256), 'Invalid measured candidate');
61
+ }
62
+ require(new Set(all.map(row => row.id)).size === all.length, 'Duplicate candidate ID');
63
+ const byId = new Map(all.map(row => [row.id, { ...row }]));
64
+ const best = candidates.reduce((a, b) => b.score > a.score ? b : a);
65
+ const document = { components: [{ id: 'weight-candidate', scope: 'domain', content: { id: baseline.id } }] };
66
+ const result = await runContrastive({
67
+ tenantId: 'local', projectId: 'ruvllm', task: { purpose: 'development-candidate-selection' },
68
+ environment: { mode: 'evidence-replay' },
69
+ library: { tenantId: 'local', projectId: 'ruvllm', entries: [] },
70
+ envelope, baseline: document,
71
+ budget: { maxUsd: 0, maxCalls: 5, timeoutMs: 5000, probeUsd: 0, proposeUsd: 0, composeUsd: 0 },
72
+ evaluator: {
73
+ surfaceAware: true,
74
+ async probe({ candidate }) {
75
+ const row = byId.get(candidate.components[0].content.id);
76
+ require(row, 'Unmeasured candidate');
77
+ return { appliedDigest: digestDocument(candidate), score: row.score, costUsd: 0,
78
+ observations: [{ id: 'measured-failure', componentId: 'weight-candidate', message: `Bound evidence ${row.evidenceSha256}` }] };
79
+ },
80
+ },
81
+ proposer: {
82
+ async propose({ scope }) {
83
+ return { edits: scope === 'domain' ? [{ componentId: 'weight-candidate', content: { id: best.id },
84
+ citations: ['measured-failure'] }] : [], costUsd: 0 };
85
+ },
86
+ },
87
+ });
88
+ return { schema: 'ruvllm-development-selection/v1', selectedId: result.candidate.components[0].content.id,
89
+ envelopeSha256: hash(envelope), evidenceSha256: hash(all), evidenceMode: 'score-replay',
90
+ result, independentConfirmation: false, productionActivation: false };
91
+ }
package/src/cli.mjs ADDED
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env node
2
+ import { runExperiment } from './engine.mjs';
3
+ import { createFixture } from './fixture.mjs';
4
+
5
+ const [command, ...args] = process.argv.slice(2);
6
+ function options(allowed) {
7
+ const result = {};
8
+ for (let i = 0; i < args.length; i += 2) {
9
+ if (!allowed.includes(args[i]) || !args[i + 1] || Object.hasOwn(result, args[i])) throw new Error('Unknown, duplicate or missing argument');
10
+ result[args[i]] = args[i + 1];
11
+ }
12
+ return result;
13
+ }
14
+ try {
15
+ let result;
16
+ if (command === 'run') {
17
+ const flags = options(['--config', '--plan']);
18
+ result = await runExperiment({ configPath: flags['--config'], planPath: flags['--plan'] });
19
+ } else if (command === 'fixture') {
20
+ const flags = options(['--dir', '--binary']);
21
+ if (!flags['--dir'] || !flags['--binary']) throw new Error('fixture requires --dir and --binary');
22
+ result = await runExperiment(await createFixture({ directory: flags['--dir'], binaryPath: flags['--binary'] }));
23
+ } else if (command === '--help' || command === 'help') {
24
+ console.log('Usage: ruvllm-gpu run --config ABS --plan ABS\n ruvllm-gpu fixture --dir NEW_ABS --binary ABS');
25
+ process.exit(0);
26
+ } else throw new Error('Expected run, fixture or --help');
27
+ console.log(JSON.stringify(result, null, 2));
28
+ } catch (error) {
29
+ console.error(JSON.stringify({ ok: false, error: error.message, productionActivation: false })); process.exitCode = 1;
30
+ }
package/src/engine.mjs ADDED
@@ -0,0 +1,255 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { spawn } from 'node:child_process';
3
+ import { constants } from 'node:fs';
4
+ import { open, lstat, realpath, mkdir, readFile, writeFile } from 'node:fs/promises';
5
+ import { resolve, dirname, join, relative, isAbsolute, parse } from 'node:path';
6
+ import { meetsPromotionRule, gateFingerprint } from '@metaharness/flywheel';
7
+ import { selectDevelopmentCandidate } from './adapters.mjs';
8
+
9
+ const MAX_JSON = 1024 * 1024;
10
+ const HASH = /^[a-f0-9]{64}$/;
11
+ const inside = (root, path) => path === root || (!relative(root, path).startsWith('..') && !isAbsolute(relative(root, path)));
12
+ const assert = (condition, message) => { if (!condition) throw new Error(message); };
13
+ export const sha256 = bytes => createHash('sha256').update(bytes).digest('hex');
14
+
15
+ function keys(value, required, optional = []) {
16
+ assert(value && typeof value === 'object' && !Array.isArray(value), 'Expected JSON object');
17
+ assert(required.every(k => Object.hasOwn(value, k)), `Missing fields: ${required.join(', ')}`);
18
+ assert(Object.keys(value).every(k => [...required, ...optional].includes(k)), 'Unknown object fields');
19
+ }
20
+
21
+ // Reject duplicate object keys and non-finite numbers before applying schemas.
22
+ export function parseJson(bytes) {
23
+ const text = bytes.toString('utf8'); let index = 0;
24
+ const ws = () => { while (/[ \t\r\n]/.test(text[index] ?? '') && index < text.length) index++; };
25
+ function string() {
26
+ const start = index++; let escaped = false;
27
+ while (index < text.length) {
28
+ const c = text[index++];
29
+ if (c === '"' && !escaped) return JSON.parse(text.slice(start, index));
30
+ if (c === '\\' && !escaped) escaped = true; else escaped = false;
31
+ }
32
+ throw new Error('Unterminated JSON string');
33
+ }
34
+ function value(depth = 0) {
35
+ assert(depth < 64, 'JSON nesting limit'); ws();
36
+ if (text[index] === '"') return string();
37
+ if (text[index] === '{') {
38
+ index++; ws(); const result = Object.create(null); const seen = new Set();
39
+ if (text[index] === '}') { index++; return result; }
40
+ while (true) {
41
+ ws(); assert(text[index] === '"', 'Invalid JSON key'); const key = string();
42
+ assert(!seen.has(key), 'Duplicate JSON key'); seen.add(key); ws();
43
+ assert(text[index++] === ':', 'Invalid JSON separator'); result[key] = value(depth + 1); ws();
44
+ const end = text[index++]; if (end === '}') return result; assert(end === ',', 'Invalid JSON object');
45
+ }
46
+ }
47
+ if (text[index] === '[') {
48
+ index++; ws(); const result = []; if (text[index] === ']') { index++; return result; }
49
+ while (true) { result.push(value(depth + 1)); ws(); const end = text[index++]; if (end === ']') return result; assert(end === ',', 'Invalid JSON array'); }
50
+ }
51
+ const token = /^(?:true|false|null|-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?)/.exec(text.slice(index));
52
+ assert(token, 'Invalid JSON value'); index += token[0].length; const result = JSON.parse(token[0]);
53
+ assert(typeof result !== 'number' || Number.isFinite(result), 'Non-finite JSON number'); return result;
54
+ }
55
+ const result = value(); ws(); assert(index === text.length, 'Trailing JSON data'); return result;
56
+ }
57
+
58
+ export async function checkedPath(path, { directory = false, roots } = {}) {
59
+ assert(typeof path === 'string' && isAbsolute(path) && !path.includes('\0'), 'Expected absolute path');
60
+ assert(path === resolve(path), 'Path must be canonical without traversal');
61
+ let cursor = parse(path).root;
62
+ for (const part of path.slice(cursor.length).split('/').filter(Boolean)) {
63
+ cursor = join(cursor, part); const stat = await lstat(cursor); assert(!stat.isSymbolicLink(), 'Symlinks are forbidden');
64
+ }
65
+ const canonical = await realpath(path); const stat = await lstat(canonical);
66
+ assert(directory ? stat.isDirectory() : stat.isFile(), directory ? 'Expected directory' : 'Expected regular file');
67
+ if (roots) assert(roots.some(root => inside(root, canonical)), 'Path outside configured read roots');
68
+ return canonical;
69
+ }
70
+
71
+ async function boundedRead(path, limit = MAX_JSON) {
72
+ const file = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
73
+ try {
74
+ const stat = await file.stat(); assert(stat.isFile() && stat.size <= limit, 'Control file too large or not regular');
75
+ const bytes = Buffer.alloc(stat.size); const { bytesRead } = await file.read(bytes, 0, bytes.length, 0);
76
+ assert(bytesRead === bytes.length, 'Control file changed during read'); return bytes;
77
+ } finally { await file.close(); }
78
+ }
79
+
80
+ export async function fileDigest(path) {
81
+ const file = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
82
+ try {
83
+ assert((await file.stat()).isFile(), 'Expected regular file');
84
+ const hash = createHash('sha256'); const bytes = Buffer.alloc(1024 * 1024); let position = 0;
85
+ while (true) { const { bytesRead } = await file.read(bytes, 0, bytes.length, position); if (!bytesRead) break; hash.update(bytes.subarray(0, bytesRead)); position += bytesRead; }
86
+ return hash.digest('hex');
87
+ } finally { await file.close(); }
88
+ }
89
+
90
+ // Digest cache for the pinned native binary: re-hash only when the file's
91
+ // identity or change metadata differs (dev, inode, size, mtime, ctime in ns).
92
+ // Any write to the file updates ctime, which callers cannot set.
93
+ const digestCache = new Map();
94
+ async function verifiedDigest(path) {
95
+ const info = await lstat(path, { bigint: true });
96
+ assert(info.isFile(), 'Expected regular file');
97
+ const identity = [info.dev, info.ino, info.size, info.mtimeNs, info.ctimeNs].join(':');
98
+ const cached = digestCache.get(path);
99
+ if (cached?.identity === identity) return cached.digest;
100
+ const digest = await fileDigest(path);
101
+ const after = await lstat(path, { bigint: true });
102
+ if ([after.dev, after.ino, after.size, after.mtimeNs, after.ctimeNs].join(':') === identity) digestCache.set(path, { identity, digest });
103
+ else digestCache.delete(path);
104
+ return digest;
105
+ }
106
+
107
+ async function identicalFiles(a, b) {
108
+ const first = await open(a, constants.O_RDONLY | constants.O_NOFOLLOW), second = await open(b, constants.O_RDONLY | constants.O_NOFOLLOW);
109
+ try {
110
+ const size = (await first.stat()).size; if (size !== (await second.stat()).size) return false;
111
+ const x = Buffer.alloc(Math.min(size, 1024 * 1024)), y = Buffer.alloc(x.length);
112
+ for (let position = 0; position < size; position += x.length) {
113
+ const length = Math.min(x.length, size - position);
114
+ const [rx, ry] = await Promise.all([first.read(x, 0, length, position), second.read(y, 0, length, position)]);
115
+ if (rx.bytesRead !== length || ry.bytesRead !== length || !x.subarray(0, length).equals(y.subarray(0, length))) return false;
116
+ }
117
+ return true;
118
+ } finally { await first.close(); await second.close(); }
119
+ }
120
+
121
+ async function save(path, value) { await writeFile(path, JSON.stringify(value, null, 2) + '\n', { flag: 'wx', mode: 0o600 }); }
122
+
123
+ async function runtime(configPath) {
124
+ configPath = await checkedPath(configPath); const bytes = await boundedRead(configPath); const config = parseJson(bytes);
125
+ keys(config, ['schema', 'binaryPath', 'binarySha256', 'readRoots', 'writeRoot', 'policySha256s', 'maxCandidates', 'timeoutMs']);
126
+ assert(config.schema === 'ruvllm-experiment-runtime/v1', 'Unsupported runtime schema');
127
+ assert(HASH.test(config.binarySha256), 'Invalid binary digest');
128
+ assert(Array.isArray(config.readRoots) && config.readRoots.length > 0 && config.readRoots.length <= 32, 'Invalid read roots');
129
+ assert(Array.isArray(config.policySha256s) && config.policySha256s.length > 0 && config.policySha256s.length <= 128 && config.policySha256s.every(x => HASH.test(x)), 'Invalid approved policy digests');
130
+ assert(Number.isInteger(config.maxCandidates) && config.maxCandidates >= 1 && config.maxCandidates <= 16, 'Candidate limit must be 1..16');
131
+ assert(Number.isInteger(config.timeoutMs) && config.timeoutMs >= 100 && config.timeoutMs <= 300000, 'Invalid timeout');
132
+ config.readRoots = await Promise.all(config.readRoots.map(path => checkedPath(path, { directory: true })));
133
+ config.writeRoot = await checkedPath(config.writeRoot, { directory: true });
134
+ config.binaryPath = await checkedPath(config.binaryPath);
135
+ assert(!inside(config.writeRoot, configPath) && !inside(config.writeRoot, config.binaryPath), 'Runtime config and binary must be outside write root');
136
+ const stat = await lstat(configPath); assert((stat.mode & 0o077) === 0, 'Runtime config must be owner-only');
137
+ assert(((await lstat(config.writeRoot)).mode & 0o077) === 0, 'Write root must be private');
138
+ assert(await verifiedDigest(config.binaryPath) === config.binarySha256, 'Binary digest mismatch');
139
+ return { ...config, configSha256: sha256(bytes) };
140
+ }
141
+
142
+ async function native(config, request) {
143
+ assert(await verifiedDigest(config.binaryPath) === config.binarySha256, 'Binary digest mismatch');
144
+ const input = JSON.stringify(request); assert(Buffer.byteLength(input) <= 64 * 1024, 'Native request limit');
145
+ return new Promise((accept, reject) => {
146
+ const environment = Object.fromEntries(['PATH', 'HOME', 'TMPDIR', 'LANG', 'LC_ALL'].filter(k => process.env[k]).map(k => [k, process.env[k]]));
147
+ const child = spawn(config.binaryPath, [], { stdio: ['pipe', 'pipe', 'pipe'], env: environment, shell: false });
148
+ let stdout = [], stderr = [], outSize = 0, errSize = 0, failure;
149
+ const fail = message => { failure ??= new Error(message); child.kill('SIGKILL'); };
150
+ const timer = setTimeout(() => fail('Native operation timed out'), config.timeoutMs);
151
+ child.stdout.on('data', bytes => { outSize += bytes.length; if (outSize > MAX_JSON) fail('Native stdout limit'); else stdout.push(bytes); });
152
+ child.stderr.on('data', bytes => { errSize += bytes.length; if (errSize > 64 * 1024) fail('Native stderr limit'); else stderr.push(bytes); });
153
+ child.on('error', error => { clearTimeout(timer); reject(error); });
154
+ child.stdin.on('error', () => {});
155
+ child.on('close', code => {
156
+ clearTimeout(timer); if (failure) return reject(failure);
157
+ if (code !== 0) return reject(new Error(`Native ${request.op} rejected: ${Buffer.concat(stderr).toString('utf8').slice(0, 2048)}`));
158
+ try { accept(parseJson(Buffer.concat(stdout))); } catch (error) { reject(error); }
159
+ });
160
+ child.stdin.end(input);
161
+ });
162
+ }
163
+
164
+ export async function runExperiment({ configPath, planPath }) {
165
+ const config = await runtime(configPath); planPath = await checkedPath(planPath, { roots: config.readRoots });
166
+ const planBytes = await boundedRead(planPath), plan = parseJson(planBytes);
167
+ keys(plan, ['schema', 'evidenceMode', 'source', 'policy', 'candidates']);
168
+ assert(plan.schema === 'ruvllm-experiment-plan/v1', 'Unsupported plan schema');
169
+ assert(['fixture', 'external'].includes(plan.evidenceMode), 'Explicit fixture or external evidence mode required');
170
+ assert(Array.isArray(plan.candidates) && plan.candidates.length >= 1 && plan.candidates.length <= config.maxCandidates, 'Candidate limit exceeded');
171
+ const source = await checkedPath(plan.source, { roots: config.readRoots });
172
+ const policyPath = await checkedPath(plan.policy, { roots: config.readRoots });
173
+ const policyBytes = await boundedRead(policyPath), policy = parseJson(policyBytes), policySha256 = sha256(policyBytes);
174
+ assert(config.policySha256s.includes(policySha256), 'Policy is not approved');
175
+ assert(Array.isArray(policy.allowed_tensors) && policy.allowed_tensors.length > 0 && policy.allowed_tensors.length <= 256, 'Invalid tensor selection');
176
+ const ids = new Set(), candidates = [];
177
+ for (const candidate of plan.candidates) {
178
+ keys(candidate, ['id', 'patch'], ['evaluation']);
179
+ assert(typeof candidate.id === 'string' && /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/.test(candidate.id) && !ids.has(candidate.id), 'Invalid or duplicate candidate id'); ids.add(candidate.id);
180
+ const patchPath = await checkedPath(candidate.patch, { roots: config.readRoots }), patchBytes = await boundedRead(patchPath);
181
+ const entry = { ...candidate, patchBytes, patch: parseJson(patchBytes) };
182
+ if (candidate.evaluation) { const path = await checkedPath(candidate.evaluation, { roots: config.readRoots }); entry.evaluationBytes = await boundedRead(path); entry.evaluationValue = parseJson(entry.evaluationBytes); }
183
+ candidates.push(entry);
184
+ }
185
+ const runDirectory = join(config.writeRoot, `run-${randomUUID()}`); await mkdir(runDirectory, { mode: 0o700 });
186
+ await writeFile(join(runDirectory, 'plan.json'), planBytes, { flag: 'wx', mode: 0o600 });
187
+ const copiedPolicy = join(runDirectory, 'policy.json'); await writeFile(copiedPolicy, policyBytes, { flag: 'wx', mode: 0o600 });
188
+ const inspection = await native(config, { op: 'inspect', source, tensors: policy.allowed_tensors });
189
+ const reportPath = join(runDirectory, 'report.json');
190
+ const report = { schema: 'ruvllm-experiment-report/v1', evidenceMode: plan.evidenceMode, sourceSha256: inspection.source_sha256,
191
+ policySha256, planSha256: sha256(planBytes), binarySha256: config.binarySha256, configSha256: config.configSha256,
192
+ gateSha256: gateFingerprint(meetsPromotionRule), runDirectory, reportPath, productionActivation: false, activationAuthorized: false,
193
+ inferencePerformed: false, independentConfirmation: false, evaluationTrust: 'caller-supplied; not independent attestation', costMetered: false, candidates: [], selectedCandidateId: null };
194
+ let sharedEvaluation, baselineScore;
195
+ for (const candidate of candidates) {
196
+ const output = join(runDirectory, candidate.id); await mkdir(output, { mode: 0o700 });
197
+ const patch = join(output, 'patch.json'), checkpoint = join(output, 'candidate.safetensors'), receiptPath = join(output, 'receipt.json');
198
+ await writeFile(patch, candidate.patchBytes, { flag: 'wx', mode: 0o600 });
199
+ const result = { id: candidate.id, status: 'rejected', patchSha256: sha256(candidate.patchBytes), rollbackExact: false, rollbackVerified: false, rvfVerified: false, productionActivation: false };
200
+ let receipt;
201
+ try {
202
+ assert(candidate.patch.source_sha256 === report.sourceSha256, 'Patch source mismatch');
203
+ receipt = await native(config, { op: 'patch', source, output: checkpoint, patch, policy: copiedPolicy });
204
+ assert(receipt.source_sha256 === report.sourceSha256 && receipt.policy_sha256 === policySha256 && receipt.patch_sha256 === result.patchSha256 && receipt.activation_authorized === false, 'Native receipt binding mismatch');
205
+ await save(receiptPath, receipt); result.candidateSha256 = receipt.candidate_sha256; result.status = 'unqualified';
206
+ let evaluationPath;
207
+ if (candidate.evaluationBytes) {
208
+ evaluationPath = join(output, 'evaluation.json'); await writeFile(evaluationPath, candidate.evaluationBytes, { flag: 'wx', mode: 0o600 });
209
+ const value = candidate.evaluationValue;
210
+ result.evaluationSha256 = sha256(candidate.evaluationBytes);
211
+ // All candidates must use the same suite, baseline, runtime and protocol.
212
+ const signature = JSON.stringify([value.suite_sha256, value.protocol_sha256, value.baseline, value.total, value.baseline_correct, value.baseline_truncated, value.baseline_output_sha256]);
213
+ const validation = await native(config, { op: 'evaluate', evaluation: evaluationPath, receipt: receiptPath });
214
+ assert(validation.valid === true && validation.activation_authorized === false, 'Evaluation not valid');
215
+ if (sharedEvaluation) assert(sharedEvaluation === signature, 'Candidates evaluated against different baselines or suites'); else sharedEvaluation = signature;
216
+ const total = value.total;
217
+ assert(Number.isInteger(total) && total > 0, 'Evaluation total must be positive');
218
+ const score = (correct, truncated) => ({ primary: correct / total, noopRate: truncated / total, costPerWin: 0, regressed: false });
219
+ baselineScore = value.baseline_correct / total;
220
+ const gate = meetsPromotionRule({ baseline: score(value.baseline_correct, value.baseline_truncated), candidate: score(value.candidate_correct, value.candidate_truncated) });
221
+ if (value.candidate_correct <= value.baseline_correct) { gate.promote = false; gate.reasons.push('no_strict_accuracy_improvement'); }
222
+ result.gate = { ...gate, meaning: 'research qualification only; no activation authority' };
223
+ result.score = score(value.candidate_correct, value.candidate_truncated);
224
+ result.status = gate.promote ? 'qualified' : 'unqualified';
225
+ } else result.gate = { promote: false, reasons: ['missing_evaluation'] };
226
+ const artifact = join(output, 'evidence.rvf');
227
+ const exported = await native(config, { op: 'export', patch, policy: copiedPolicy, receipt: receiptPath, output: artifact, ...(evaluationPath ? { evaluation: evaluationPath } : {}) });
228
+ const verified = await native(config, { op: 'verify', artifact, expected_sha256: exported.sha256 });
229
+ assert(verified.integrity_verified === true && verified.activation_authorized === false, 'RVF verification failed'); result.rvfSha256 = exported.sha256; result.rvfVerified = true;
230
+ } catch (error) { result.status = 'rejected'; result.error = error.message; }
231
+ finally {
232
+ if (receipt) {
233
+ const restored = join(output, 'restored.safetensors');
234
+ const rollback = await native(config, { op: 'rollback', source: checkpoint, output: restored, patch, policy: copiedPolicy, receipt: receiptPath });
235
+ result.restoredSha256 = rollback.candidate_sha256;
236
+ result.rollbackExact = result.rollbackVerified = rollback.candidate_sha256 === report.sourceSha256 && await identicalFiles(source, restored);
237
+ assert(result.rollbackExact, 'Rollback did not restore original bytes');
238
+ }
239
+ }
240
+ report.candidates.push(result);
241
+ }
242
+ assert(await fileDigest(source) === report.sourceSha256, 'Source checkpoint changed during experiment');
243
+ const qualified = report.candidates.filter(candidate => candidate.status === 'qualified');
244
+ qualified.sort((a, b) => b.score.primary - a.score.primary || a.score.noopRate - b.score.noopRate || a.id.localeCompare(b.id));
245
+ if (qualified.length) {
246
+ report.developmentSelection = await selectDevelopmentCandidate({
247
+ baseline: { id: 'source.baseline', score: baselineScore, evidenceSha256: report.sourceSha256 },
248
+ candidates: qualified.map(candidate => ({ id: candidate.id, score: candidate.score.primary, evidenceSha256: candidate.evaluationSha256 })),
249
+ envelope: Object.fromEntries(['sourceSha256', 'policySha256', 'binarySha256', 'gateSha256', 'planSha256'].map(key => [key, report[key]])),
250
+ });
251
+ assert(qualified.some(candidate => candidate.id === report.developmentSelection.selectedId), 'Darwin selected an unqualified candidate');
252
+ report.selectedCandidateId = report.developmentSelection.selectedId;
253
+ }
254
+ await save(reportPath, report); return report;
255
+ }
@@ -0,0 +1,38 @@
1
+ import { mkdir, writeFile } from 'node:fs/promises';
2
+ import { join, resolve } from 'node:path';
3
+ import { fileDigest, sha256, checkedPath } from './engine.mjs';
4
+
5
+ // This deterministic fixture measures contracts and native I/O, never model quality.
6
+ export async function createFixture({ directory, binaryPath }) {
7
+ directory = resolve(directory); binaryPath = await checkedPath(resolve(binaryPath));
8
+ await mkdir(directory, { mode: 0o700 });
9
+ const inputs = join(directory, 'inputs'), outputs = join(directory, 'outputs');
10
+ await mkdir(inputs, { mode: 0o700 }); await mkdir(outputs, { mode: 0o700 });
11
+ async function json(name, value) {
12
+ const path = join(inputs, name), bytes = Buffer.from(JSON.stringify(value, null, 2) + '\n');
13
+ await writeFile(path, bytes, { flag: 'wx', mode: 0o600 }); return { path, sha256: sha256(bytes) };
14
+ }
15
+ const rawHeader = JSON.stringify({ 'layer.weight': { dtype: 'BF16', shape: [4], data_offsets: [0, 8] } });
16
+ const header = Buffer.from(rawHeader + ' '.repeat((8 - Buffer.byteLength(rawHeader) % 8) % 8));
17
+ const size = Buffer.alloc(8); size.writeBigUInt64LE(BigInt(header.length));
18
+ const values = Buffer.alloc(8); [0x3f80, 0x4000, 0x4040, 0x4080].forEach((value, i) => values.writeUInt16LE(value, i * 2));
19
+ const original = Buffer.concat([size, header, values]), source = join(inputs, 'original.safetensors');
20
+ await writeFile(source, original, { flag: 'wx', mode: 0o600 });
21
+ const candidate = Buffer.from(original); candidate.writeUInt16LE(0x4020, candidate.length - 6);
22
+ const policy = await json('policy.json', { allowed_tensors: ['layer.weight'], max_changed_values: 1, max_l2: 0.5, max_abs_delta: 0.5, max_relative_l2: 1, protected: [] });
23
+ const patch = await json('patch.json', { schema: 'ruvllm.weight_patch.v1', source_sha256: sha256(original), edits: [{ tensor: 'layer.weight', index: 1, old_bits: 0x4000, new_bits: 0x4020 }] });
24
+ const binding = { runtime_sha256: await fileDigest(binaryPath), topology_sha256: '2'.repeat(64), protocol_sha256: '3'.repeat(64), toolchain_sha256: '4'.repeat(64) };
25
+ const evaluation = await json('fixture-evaluation.json', {
26
+ schema: 'ruvllm.weight_evaluation.v1',
27
+ baseline: { ...binding, model_generation: 0, model_sha256: sha256(original), patch_sha256: '0'.repeat(64) },
28
+ candidate: { ...binding, model_generation: 1, model_sha256: sha256(candidate), patch_sha256: patch.sha256 },
29
+ suite_sha256: '5'.repeat(64), protocol_sha256: binding.protocol_sha256,
30
+ baseline_output_sha256: '6'.repeat(64), candidate_output_sha256: '7'.repeat(64),
31
+ evaluator_identity: 'synthetic-fixture-evaluator', proposer_identity: 'synthetic-fixture-proposer',
32
+ total: 2, baseline_correct: 0, candidate_correct: 1, baseline_truncated: 1, candidate_truncated: 0,
33
+ });
34
+ const plan = await json('plan.json', { schema: 'ruvllm-experiment-plan/v1', evidenceMode: 'fixture', source, policy: policy.path, candidates: [{ id: 'fixture-improvement', patch: patch.path, evaluation: evaluation.path }, { id: 'unevaluated-control', patch: patch.path }] });
35
+ const configPath = join(directory, 'runtime.json');
36
+ await writeFile(configPath, JSON.stringify({ schema: 'ruvllm-experiment-runtime/v1', binaryPath, binarySha256: binding.runtime_sha256, readRoots: [inputs], writeRoot: outputs, policySha256s: [policy.sha256], maxCandidates: 16, timeoutMs: 30000 }, null, 2) + '\n', { flag: 'wx', mode: 0o600 });
37
+ return { configPath, planPath: plan.path };
38
+ }
@@ -0,0 +1,97 @@
1
+ // The operator supplies this module locally. Secret resolution and provider APIs
2
+ // remain with that host; this loader never fetches credentials or parses stdout.
3
+ import { createHash } from 'node:crypto';
4
+ import { constants } from 'node:fs';
5
+ import { open } from 'node:fs/promises';
6
+ import nodeModule from 'node:module';
7
+ import { pathToFileURL } from 'node:url';
8
+ import { checkedPath } from './engine.mjs';
9
+
10
+ function failure(code) { const error = new Error(code); error.code = code; return error; }
11
+
12
+ // Import the entry module from the bytes that were hashed, not by re-reading the
13
+ // path: a loader hook answers the pinned URL with those exact bytes, so a file
14
+ // swapped after the check is never executed. Relative imports still resolve
15
+ // next to the original file (they are trusted operator code, as before).
16
+ const ASYNC_HOOKS = 'data:text/javascript,' + encodeURIComponent(`
17
+ const sources = new Map();
18
+ export async function initialize({ port }) {
19
+ port.on('message', ({ href, source }) => { sources.set(href, source); port.postMessage(href); });
20
+ port.unref();
21
+ }
22
+ export async function load(url, context, nextLoad) {
23
+ const source = sources.get(url);
24
+ if (source === undefined) return nextLoad(url, context);
25
+ sources.delete(url);
26
+ return { format: 'module', source, shortCircuit: true };
27
+ }`);
28
+ let asyncPort;
29
+ const pending = new Map();
30
+
31
+ function asyncHooks() {
32
+ if (!asyncPort) {
33
+ const { port1, port2 } = new MessageChannel();
34
+ port1.on('message', href => { pending.get(href)?.(); pending.delete(href); });
35
+ port1.unref();
36
+ nodeModule.register(ASYNC_HOOKS, { data: { port: port2 }, transferList: [port2] });
37
+ asyncPort = port1;
38
+ }
39
+ return asyncPort;
40
+ }
41
+
42
+ async function importVerified(href, bytes) {
43
+ if (typeof nodeModule.registerHooks === 'function') {
44
+ const hooks = nodeModule.registerHooks({
45
+ load(url, context, nextLoad) {
46
+ if (url === href) return { format: 'module', source: bytes, shortCircuit: true };
47
+ return nextLoad(url, context);
48
+ },
49
+ });
50
+ try { return await import(href); } finally { hooks.deregister(); }
51
+ }
52
+ // Node 22 before module.registerHooks (22.15): off-thread loader hooks.
53
+ const port = asyncHooks();
54
+ port.ref();
55
+ try {
56
+ await new Promise(resolve => { pending.set(href, resolve); port.postMessage({ href, source: bytes }); });
57
+ } finally { port.unref(); }
58
+ return import(href);
59
+ }
60
+
61
+ export async function loadHostAdapter({ modulePath, sha256 }) {
62
+ if (typeof sha256 !== 'string' || !/^[a-f0-9]{64}$/.test(sha256)) throw failure('HOST_ADAPTER_INVALID_PIN');
63
+ let path, bytes;
64
+ try {
65
+ path = await checkedPath(modulePath);
66
+ if (!path.endsWith('.mjs')) throw failure('HOST_ADAPTER_INVALID_PATH');
67
+ const file = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
68
+ try {
69
+ const stat = await file.stat();
70
+ if (!stat.isFile() || stat.size > 1024 * 1024 || (stat.mode & 0o077) !== 0
71
+ || typeof process.getuid !== 'function' || stat.uid !== process.getuid()) {
72
+ throw failure('HOST_ADAPTER_UNSAFE_FILE');
73
+ }
74
+ bytes = Buffer.alloc(stat.size);
75
+ const { bytesRead } = await file.read(bytes, 0, bytes.length, 0);
76
+ if (bytesRead !== bytes.length) throw failure('HOST_ADAPTER_CHANGED');
77
+ } finally { await file.close(); }
78
+ } catch (error) {
79
+ if (error.code?.startsWith('HOST_ADAPTER_')) throw error;
80
+ throw failure('HOST_ADAPTER_INVALID_PATH');
81
+ }
82
+ if (createHash('sha256').update(bytes).digest('hex') !== sha256) throw failure('HOST_ADAPTER_PIN_MISMATCH');
83
+ let module;
84
+ try {
85
+ // The entry point and its transitive imports are trusted operator code. The
86
+ // digest is a cache identity as well as a pin; never accept tool-supplied code.
87
+ const url = pathToFileURL(path); url.searchParams.set('sha256', sha256);
88
+ module = await importVerified(url.href, bytes);
89
+ } catch { throw failure('HOST_ADAPTER_UNAVAILABLE'); }
90
+ if (!['submit', 'inspect', 'cleanup'].every(name => typeof module[name] === 'function')) {
91
+ throw failure('HOST_ADAPTER_INVALID_CONTRACT');
92
+ }
93
+ return Object.freeze(Object.fromEntries(['submit', 'inspect', 'cleanup'].map(name => [name, async input => {
94
+ try { return await module[name](input); }
95
+ catch { throw failure('HOST_ADAPTER_OPERATION_FAILED'); }
96
+ }])));
97
+ }