@ruvector/ruvllm 3.0.0 → 4.0.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.
Files changed (151) hide show
  1. package/GPU-LOOP.md +229 -0
  2. package/LICENSE +201 -0
  3. package/NOTICE +9 -0
  4. package/README.md +159 -360
  5. package/examples/gpu-loop.example.json +26 -0
  6. package/package.json +50 -121
  7. package/src/adapters-cli.mjs +23 -0
  8. package/src/adapters.mjs +91 -0
  9. package/src/cli.mjs +30 -0
  10. package/src/engine.mjs +238 -0
  11. package/src/fixture.mjs +38 -0
  12. package/src/gpu-host.mjs +47 -0
  13. package/src/gpu-loop-cli.mjs +67 -0
  14. package/src/gpu-loop.mjs +184 -0
  15. package/src/gpu-spool-adapter.mjs +80 -0
  16. package/src/vast-host-adapter.mjs +274 -0
  17. package/bin/cli.js +0 -1040
  18. package/dist/cjs/benchmarks/embedding-benchmark.d.ts +0 -102
  19. package/dist/cjs/benchmarks/embedding-benchmark.d.ts.map +0 -1
  20. package/dist/cjs/benchmarks/embedding-benchmark.js +0 -436
  21. package/dist/cjs/benchmarks/index.d.ts +0 -34
  22. package/dist/cjs/benchmarks/index.d.ts.map +0 -1
  23. package/dist/cjs/benchmarks/index.js +0 -132
  24. package/dist/cjs/benchmarks/model-comparison.d.ts +0 -71
  25. package/dist/cjs/benchmarks/model-comparison.d.ts.map +0 -1
  26. package/dist/cjs/benchmarks/model-comparison.js +0 -476
  27. package/dist/cjs/benchmarks/routing-benchmark.d.ts +0 -70
  28. package/dist/cjs/benchmarks/routing-benchmark.d.ts.map +0 -1
  29. package/dist/cjs/benchmarks/routing-benchmark.js +0 -289
  30. package/dist/cjs/contrastive.d.ts +0 -229
  31. package/dist/cjs/contrastive.d.ts.map +0 -1
  32. package/dist/cjs/contrastive.js +0 -589
  33. package/dist/cjs/engine.d.ts +0 -160
  34. package/dist/cjs/engine.d.ts.map +0 -1
  35. package/dist/cjs/engine.js +0 -552
  36. package/dist/cjs/export.d.ts +0 -182
  37. package/dist/cjs/export.d.ts.map +0 -1
  38. package/dist/cjs/export.js +0 -422
  39. package/dist/cjs/federated.d.ts +0 -233
  40. package/dist/cjs/federated.d.ts.map +0 -1
  41. package/dist/cjs/federated.js +0 -525
  42. package/dist/cjs/index.d.ts +0 -68
  43. package/dist/cjs/index.d.ts.map +0 -1
  44. package/dist/cjs/index.js +0 -104
  45. package/dist/cjs/intelligence.d.ts +0 -136
  46. package/dist/cjs/intelligence.d.ts.map +0 -1
  47. package/dist/cjs/intelligence.js +0 -226
  48. package/dist/cjs/lora.d.ts +0 -251
  49. package/dist/cjs/lora.d.ts.map +0 -1
  50. package/dist/cjs/lora.js +0 -509
  51. package/dist/cjs/models.d.ts +0 -129
  52. package/dist/cjs/models.d.ts.map +0 -1
  53. package/dist/cjs/models.js +0 -323
  54. package/dist/cjs/native.d.ts +0 -140
  55. package/dist/cjs/native.d.ts.map +0 -1
  56. package/dist/cjs/native.js +0 -98
  57. package/dist/cjs/session.d.ts +0 -80
  58. package/dist/cjs/session.d.ts.map +0 -1
  59. package/dist/cjs/session.js +0 -203
  60. package/dist/cjs/simd.d.ts +0 -90
  61. package/dist/cjs/simd.d.ts.map +0 -1
  62. package/dist/cjs/simd.js +0 -209
  63. package/dist/cjs/sona.d.ts +0 -223
  64. package/dist/cjs/sona.d.ts.map +0 -1
  65. package/dist/cjs/sona.js +0 -562
  66. package/dist/cjs/streaming.d.ts +0 -55
  67. package/dist/cjs/streaming.d.ts.map +0 -1
  68. package/dist/cjs/streaming.js +0 -131
  69. package/dist/cjs/training.d.ts +0 -312
  70. package/dist/cjs/training.d.ts.map +0 -1
  71. package/dist/cjs/training.js +0 -610
  72. package/dist/cjs/types.d.ts +0 -696
  73. package/dist/cjs/types.d.ts.map +0 -1
  74. package/dist/cjs/types.js +0 -6
  75. package/dist/esm/benchmarks/embedding-benchmark.d.ts +0 -102
  76. package/dist/esm/benchmarks/embedding-benchmark.d.ts.map +0 -1
  77. package/dist/esm/benchmarks/embedding-benchmark.js +0 -427
  78. package/dist/esm/benchmarks/index.d.ts +0 -34
  79. package/dist/esm/benchmarks/index.d.ts.map +0 -1
  80. package/dist/esm/benchmarks/index.js +0 -110
  81. package/dist/esm/benchmarks/model-comparison.d.ts +0 -71
  82. package/dist/esm/benchmarks/model-comparison.d.ts.map +0 -1
  83. package/dist/esm/benchmarks/model-comparison.js +0 -434
  84. package/dist/esm/benchmarks/routing-benchmark.d.ts +0 -70
  85. package/dist/esm/benchmarks/routing-benchmark.d.ts.map +0 -1
  86. package/dist/esm/benchmarks/routing-benchmark.js +0 -283
  87. package/dist/esm/contrastive.d.ts +0 -229
  88. package/dist/esm/contrastive.d.ts.map +0 -1
  89. package/dist/esm/contrastive.js +0 -578
  90. package/dist/esm/engine.d.ts +0 -160
  91. package/dist/esm/engine.d.ts.map +0 -1
  92. package/dist/esm/engine.js +0 -548
  93. package/dist/esm/export.d.ts +0 -182
  94. package/dist/esm/export.d.ts.map +0 -1
  95. package/dist/esm/export.js +0 -414
  96. package/dist/esm/federated.d.ts +0 -233
  97. package/dist/esm/federated.d.ts.map +0 -1
  98. package/dist/esm/federated.js +0 -520
  99. package/dist/esm/index.d.ts +0 -68
  100. package/dist/esm/index.d.ts.map +0 -1
  101. package/dist/esm/index.js +0 -84
  102. package/dist/esm/intelligence.d.ts +0 -136
  103. package/dist/esm/intelligence.d.ts.map +0 -1
  104. package/dist/esm/intelligence.js +0 -188
  105. package/dist/esm/lora.d.ts +0 -251
  106. package/dist/esm/lora.d.ts.map +0 -1
  107. package/dist/esm/lora.js +0 -504
  108. package/dist/esm/models.d.ts +0 -129
  109. package/dist/esm/models.d.ts.map +0 -1
  110. package/dist/esm/models.js +0 -282
  111. package/dist/esm/native.d.ts +0 -140
  112. package/dist/esm/native.d.ts.map +0 -1
  113. package/dist/esm/native.js +0 -93
  114. package/dist/esm/session.d.ts +0 -80
  115. package/dist/esm/session.d.ts.map +0 -1
  116. package/dist/esm/session.js +0 -199
  117. package/dist/esm/simd.d.ts +0 -90
  118. package/dist/esm/simd.d.ts.map +0 -1
  119. package/dist/esm/simd.js +0 -205
  120. package/dist/esm/sona.d.ts +0 -223
  121. package/dist/esm/sona.d.ts.map +0 -1
  122. package/dist/esm/sona.js +0 -556
  123. package/dist/esm/streaming.d.ts +0 -55
  124. package/dist/esm/streaming.d.ts.map +0 -1
  125. package/dist/esm/streaming.js +0 -126
  126. package/dist/esm/training.d.ts +0 -312
  127. package/dist/esm/training.d.ts.map +0 -1
  128. package/dist/esm/training.js +0 -603
  129. package/dist/esm/types.d.ts +0 -696
  130. package/dist/esm/types.d.ts.map +0 -1
  131. package/dist/esm/types.js +0 -5
  132. package/scripts/ensemble-model-compare.js +0 -287
  133. package/scripts/fix-esm-extensions.mjs +0 -181
  134. package/scripts/huggingface/README.md +0 -537
  135. package/scripts/huggingface/publish.sh +0 -112
  136. package/scripts/hybrid-model-compare.js +0 -373
  137. package/scripts/improved-model-compare.js +0 -288
  138. package/scripts/optimized-model-compare.js +0 -364
  139. package/scripts/real-model-compare.js +0 -280
  140. package/scripts/smoke-esm.mjs +0 -82
  141. package/scripts/training/agentic-flow-capabilities.json +0 -329
  142. package/scripts/training/claude-code-synth.js +0 -505
  143. package/scripts/training/claude-flow-capabilities.json +0 -1322
  144. package/scripts/training/claude-hard-negatives.js +0 -222
  145. package/scripts/training/contrastive-finetune.js +0 -517
  146. package/scripts/training/ecosystem-triplets.jsonl +0 -1467
  147. package/scripts/training/generate-ecosystem-triplets.js +0 -1324
  148. package/scripts/training/routing-dataset.js +0 -634
  149. package/scripts/training/ruvector-capabilities.json +0 -798
  150. package/scripts/training/validate-ecosystem.js +0 -381
  151. package/scripts/training/validation-results.json +0 -23
@@ -0,0 +1,274 @@
1
+ // Vast.ai host adapter for the scheduled GPU loop (ADR-015, ADR-017). Trusted
2
+ // operator code: it resolves the job's secretRef from an owner-only host config,
3
+ // rents exactly the configured offer, never reuses a shared instance, and only
4
+ // reports a verified completion after re-scoring the retrieved raw outputs here.
5
+ // No effect happens at import; `node vast-host-adapter.mjs guard ID REF` is the
6
+ // independent destroy timer armed for every rental.
7
+ import { spawn } from 'node:child_process';
8
+ import { createHash } from 'node:crypto';
9
+ import { constants, createWriteStream } from 'node:fs';
10
+ import { mkdir, open, readFile, rm, stat, writeFile } from 'node:fs/promises';
11
+ import { isDeepStrictEqual } from 'node:util';
12
+ import { isAbsolute, join, resolve } from 'node:path';
13
+ import { fileURLToPath } from 'node:url';
14
+
15
+ export const HOST_CONFIG_ENV = 'RUVLLM_VAST_HOST_CONFIG';
16
+ const SELF = fileURLToPath(import.meta.url);
17
+ const INSPECT_BUDGET_MS = 24000;
18
+ const fail = code => { const error = new Error(code); error.code = code; throw error; };
19
+ const need = (ok, code) => { if (!ok) fail(code); };
20
+ const abs = value => typeof value === 'string' && isAbsolute(value) && resolve(value) === value;
21
+ const ceilMicro = usd => Math.ceil(usd * 1e6) / 1e6;
22
+
23
+ async function privateJson(path) {
24
+ need(abs(path), 'VAST_HOST_CONFIG_PATH');
25
+ const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
26
+ try {
27
+ const info = await handle.stat();
28
+ need(info.isFile() && (info.mode & 0o077) === 0 && info.uid === process.getuid() && info.size <= 65536, 'VAST_HOST_CONFIG_UNSAFE');
29
+ const bytes = await handle.readFile();
30
+ return { value: JSON.parse(bytes), sha256: createHash('sha256').update(bytes).digest('hex') };
31
+ } finally { await handle.close(); }
32
+ }
33
+
34
+ export async function readHostConfig(path = process.env[HOST_CONFIG_ENV]) {
35
+ const { value: config, sha256 } = await privateJson(path);
36
+ need(config?.schema === 'ruvllm-vast-host/v1', 'VAST_HOST_CONFIG_SCHEMA');
37
+ need(/^https:\/\/[a-z0-9.-]+$/.test(config.apiBase), 'VAST_HOST_CONFIG_API');
38
+ need(/^[a-z0-9./_-]+@sha256:[a-f0-9]{64}$/.test(config.image), 'VAST_HOST_CONFIG_IMAGE');
39
+ need(/^https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\.git$/.test(config.repoUrl) && /^[a-f0-9]{40}$/.test(config.commit), 'VAST_HOST_CONFIG_SOURCE');
40
+ need(/^v[0-9]+$/.test(config.bundle), 'VAST_HOST_CONFIG_BUNDLE');
41
+ need(Number.isInteger(config.diskGb) && config.diskGb >= 20 && config.diskGb <= 200, 'VAST_HOST_CONFIG_DISK');
42
+ need(Number.isInteger(config.armDeadlineSeconds) && config.armDeadlineSeconds >= 60 && config.armDeadlineSeconds <= 43200, 'VAST_HOST_CONFIG_DEADLINE');
43
+ need(Number.isInteger(config.guardGraceSeconds) && config.guardGraceSeconds >= 60 && config.guardGraceSeconds <= 3600, 'VAST_HOST_CONFIG_GUARD');
44
+ need(['resultsRoot', 'sshKey', 'repoRoot', 'python', 'node'].every(key => abs(config[key])), 'VAST_HOST_CONFIG_PATHS');
45
+ need(config.adapterDir === undefined || (/^[a-z0-9][a-z0-9._-]{0,63}$/.test(config.adapterDir) && !config.adapterDir.includes('..')), 'VAST_HOST_CONFIG_ADAPTER');
46
+ need(config.trainPayload === undefined || /^experiments\/model-improvement\/training\/[a-z0-9][a-z0-9._-]{0,63}$/.test(config.trainPayload), 'VAST_HOST_CONFIG_PAYLOAD');
47
+ need(config.secrets && typeof config.secrets === 'object' && Object.values(config.secrets).every(entry => abs(entry?.file)), 'VAST_HOST_CONFIG_SECRETS');
48
+ return Object.freeze({ ...config, sha256 });
49
+ }
50
+
51
+ async function apiKey(config, ref) {
52
+ const entry = Object.hasOwn(config.secrets, ref) ? config.secrets[ref] : null;
53
+ need(entry, 'VAST_SECRET_REF_UNKNOWN');
54
+ const { mode, uid } = await stat(entry.file);
55
+ need((mode & 0o077) === 0 && uid === process.getuid(), 'VAST_SECRET_FILE_UNSAFE');
56
+ const key = (await readFile(entry.file, 'utf8')).trim();
57
+ need(/^[A-Za-z0-9_-]{16,256}$/.test(key), 'VAST_SECRET_INVALID');
58
+ return key;
59
+ }
60
+
61
+ // Bounded child process without a shell. Output is collected, never echoed.
62
+ function defaultRun(command, args, { timeoutMs, cwd, stdoutFile, input } = {}) {
63
+ return new Promise(resolvePromise => {
64
+ const child = spawn(command, args, { cwd, stdio: ['pipe', 'pipe', 'pipe'] });
65
+ const chunks = [], errors = [];
66
+ let size = 0;
67
+ const sink = stdoutFile ? createWriteStream(stdoutFile, { flags: 'wx', mode: 0o600 }) : null;
68
+ const flushed = sink ? new Promise(done => { sink.on('close', done); sink.on('error', done); }) : Promise.resolve();
69
+ const timer = setTimeout(() => child.kill('SIGKILL'), Math.max(1, timeoutMs ?? 10000));
70
+ if (sink) child.stdout.pipe(sink);
71
+ else child.stdout.on('data', chunk => { size += chunk.length; if (size <= 8 << 20) chunks.push(chunk); });
72
+ child.stderr.on('data', chunk => { if (errors.length < 64) errors.push(chunk); });
73
+ child.on('error', () => {});
74
+ child.on('close', async code => {
75
+ clearTimeout(timer);
76
+ await flushed;
77
+ resolvePromise({ code, stdout: Buffer.concat(chunks).toString('utf8'), stderr: Buffer.concat(errors).toString('utf8') });
78
+ });
79
+ child.stdin.end(input ?? '');
80
+ });
81
+ }
82
+
83
+ async function writePrivate(path, value) {
84
+ await writeFile(path, typeof value === 'string' ? value : JSON.stringify(value, null, 2) + '\n', { mode: 0o600 });
85
+ }
86
+
87
+ export function onstartScript(config, task = 'evaluate') {
88
+ const scripts = '/workspace/ruvllm/experiments/model-improvement/vast';
89
+ if (task === 'train') need(config.trainPayload, 'VAST_HOST_CONFIG_PAYLOAD');
90
+ const run = task === 'train'
91
+ ? `${scripts}/remote-train.sh ${config.commit} ${config.armDeadlineSeconds} ${config.trainPayload}`
92
+ : `${scripts}/remote-run.sh ${config.commit} ${config.armDeadlineSeconds} ${config.bundle} ${config.adapterDir ?? 'coding-4b'}`;
93
+ const failed = `grep -q '"failed"' /workspace/run/status.json 2>/dev/null || printf '{"schema":"ruvllm-vast-remote-status/v1","state":"failed","step":"bootstrap"}\\n' > /workspace/run/status.json`;
94
+ return ['#!/bin/bash', 'mkdir -p /workspace/run',
95
+ `nohup bash -c '(export DEBIAN_FRONTEND=noninteractive; (command -v git >/dev/null || (apt-get update -qq && apt-get install -y -qq git >/dev/null)) && git clone --quiet ${config.repoUrl} /workspace/ruvllm && git -C /workspace/ruvllm checkout --quiet ${config.commit} && bash ${run}) >/workspace/run/remote.log 2>&1 || { ${failed.replaceAll("'", "'\\''")}; }' >/dev/null 2>&1 &`, ''].join('\n');
96
+ }
97
+
98
+ export function createVastAdapter({ fetchImpl = globalThis.fetch, run = defaultRun, now = () => Date.now(), readConfig = readHostConfig } = {}) {
99
+ async function api(config, key, method, path, { body, query, signal } = {}) {
100
+ const url = new URL(config.apiBase + path);
101
+ for (const [name, value] of Object.entries(query ?? {})) url.searchParams.set(name, JSON.stringify(value));
102
+ const response = await fetchImpl(url, { method, signal, headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
103
+ ...(body === undefined ? {} : { body: JSON.stringify(body) }) });
104
+ if (response.status === 404) return null;
105
+ need(response.ok, `VAST_HTTP_${response.status}`);
106
+ return response.json();
107
+ }
108
+ async function instances(config, key, signal) {
109
+ const rows = [];
110
+ let after;
111
+ for (let page = 0; page < 40; page++) {
112
+ const data = await api(config, key, 'GET', '/api/v1/instances/', { signal,
113
+ query: { select_filters: {}, order_by: [{ col: 'id', dir: 'asc' }], limit: 25, ...(after ? { after_token: after } : {}) } });
114
+ rows.push(...(data?.instances ?? []));
115
+ if (!data?.next_token) return rows;
116
+ after = data.next_token;
117
+ }
118
+ fail('VAST_LIST_UNBOUNDED');
119
+ }
120
+ const attemptDir = (config, key) => join(config.resultsRoot, key);
121
+ const ssh = (config, instance, remote) => ['-i', config.sshKey, '-p', String(instance.ssh_port), '-o', 'BatchMode=yes', '-o', 'ConnectTimeout=8',
122
+ '-o', 'StrictHostKeyChecking=accept-new', '-o', `UserKnownHostsFile=${join(config.resultsRoot, `known_hosts-${instance.id}`)}`, '-o', 'IdentitiesOnly=yes',
123
+ `root@${instance.ssh_host}`, remote];
124
+
125
+ async function submit({ job, idempotencyKey, signal }) {
126
+ const config = await readConfig(), key = await apiKey(config, job.secretRef);
127
+ need(typeof job.resource.offerId === 'string' && /^[0-9]{1,12}$/.test(job.resource.offerId), 'VAST_OFFER_REQUIRED');
128
+ const label = `ruvllm-${idempotencyKey}`;
129
+ const existing = (await instances(config, key, signal)).find(row => row.label === label);
130
+ if (existing) return { runId: String(existing.id) };
131
+ // The bundles search ignores `id`; `ask_contract_id` is the offer identity filter.
132
+ const offers = (await api(config, key, 'POST', '/api/v0/bundles/', { signal, body: { ask_contract_id: { eq: Number(job.resource.offerId) },
133
+ rentable: { eq: true }, type: 'on-demand', allocated_storage: config.diskGb, limit: 2 } }))?.offers ?? [];
134
+ need(offers.length === 1 && String(offers[0].id) === job.resource.offerId
135
+ && Number.isFinite(offers[0].dph_total) && offers[0].dph_total > 0, 'VAST_OFFER_UNAVAILABLE');
136
+ const offer = offers[0];
137
+ // Spending ceiling: the worst case (full timeout at the offer's total hourly
138
+ // price, storage included) must fit the job's reserved ceiling before renting.
139
+ const worstCaseUsd = ceilMicro(offer.dph_total * job.timeoutSeconds / 3600);
140
+ need(worstCaseUsd <= job.maxCostUsd, 'VAST_COST_CEILING');
141
+ await mkdir(attemptDir(config, idempotencyKey), { mode: 0o700 });
142
+ const created = await api(config, key, 'PUT', `/api/v0/asks/${offer.id}/`, { signal, body: { client_id: 'me', image: config.image,
143
+ env: {}, price: null, disk: config.diskGb, label, onstart: onstartScript(config, job.task), runtype: 'ssh', force: false, cancel_unavail: true } });
144
+ need(created?.success === true && Number.isSafeInteger(created.new_contract), 'VAST_CREATE_FAILED');
145
+ const instanceId = created.new_contract;
146
+ // Independent of this scheduler: a user timer destroys the rental after
147
+ // timeout + grace even if no further tick ever runs.
148
+ const guard = await run('systemd-run', ['--user', '--quiet', `--unit=ruvllm-vast-guard-${instanceId}`,
149
+ `--on-active=${job.timeoutSeconds + config.guardGraceSeconds}s`, `--setenv=${HOST_CONFIG_ENV}=${process.env[HOST_CONFIG_ENV] ?? ''}`,
150
+ config.node, SELF, 'guard', String(instanceId), job.secretRef], { timeoutMs: 8000 });
151
+ await writePrivate(join(attemptDir(config, idempotencyKey), 'submission.json'), { schema: 'ruvllm-vast-submission/v1',
152
+ jobId: job.id, idempotencyKey, label, instanceId, offerId: offer.id, gpuName: offer.gpu_name, dphTotal: offer.dph_total,
153
+ worstCaseUsd, maxCostUsd: job.maxCostUsd, timeoutSeconds: job.timeoutSeconds, image: config.image, commit: config.commit,
154
+ task: job.task, bundle: config.bundle, adapterDir: config.adapterDir ?? 'coding-4b', trainPayload: config.trainPayload ?? null, hostConfigSha256: config.sha256, guardArmed: guard.code === 0, submittedAt: new Date(now()).toISOString() });
155
+ if (guard.code !== 0) {
156
+ await api(config, key, 'DELETE', `/api/v0/instances/${instanceId}/`, { body: {} });
157
+ fail('VAST_GUARD_UNAVAILABLE');
158
+ }
159
+ return { runId: String(instanceId) };
160
+ }
161
+
162
+ async function retrieve(config, instance, dir, started) {
163
+ const left = () => INSPECT_BUDGET_MS - (now() - started);
164
+ const archive = join(dir, 'evidence.tar');
165
+ await rm(archive, { force: true }); await rm(join(dir, 'evidence'), { recursive: true, force: true });
166
+ if (left() < 4000) return false;
167
+ const pulled = await run('ssh', ssh(config, instance, 'tar -C /workspace -cf - out run'), { timeoutMs: left() - 3000, stdoutFile: archive });
168
+ if (pulled.code !== 0) return false;
169
+ await mkdir(join(dir, 'evidence'), { mode: 0o700 });
170
+ return (await run('tar', ['-xf', archive, '-C', join(dir, 'evidence'), '--no-same-owner', '--no-same-permissions'], { timeoutMs: 5000 })).code === 0;
171
+ }
172
+
173
+ async function verify(config, job, dir) {
174
+ const evidence = join(dir, 'evidence', 'out'), scored = join(dir, 'local-scored');
175
+ await rm(scored, { recursive: true, force: true });
176
+ const bundle = join(config.repoRoot, 'experiments/model-improvement/frozen', config.bundle);
177
+ const result = await run(config.python, [join(config.repoRoot, 'experiments/model-improvement/measurement.py'), '--bundle', bundle,
178
+ '--base-run', join(evidence, 'base'), '--adapter-run', join(evidence, 'adapter'), '--out', scored], { timeoutMs: 15000, cwd: config.repoRoot });
179
+ if (result.code !== 0) return { ok: false, reason: 'local_rescore_failed' };
180
+ const local = JSON.parse(await readFile(join(scored, 'report.json'), 'utf8'));
181
+ const remote = JSON.parse(await readFile(join(evidence, 'scored', 'report.json'), 'utf8'));
182
+ const ok = isDeepStrictEqual(local, remote) && local.evidence_mode === 'gpu-inference';
183
+ await writePrivate(join(dir, 'verification.json'), { schema: 'ruvllm-vast-verification/v1', jobId: job.id,
184
+ localRescoreMatchesRemote: isDeepStrictEqual(local, remote), evidenceMode: local.evidence_mode,
185
+ researchImprovementConfirmed: local.research_improvement_confirmed, reasons: local.reasons });
186
+ return { ok, reason: ok ? 'verified' : 'report_mismatch' };
187
+ }
188
+
189
+ // Training evidence: the trainer's own manifest must hash every retrieved file,
190
+ // bind the committed payload, and match the completed event in the event log.
191
+ async function verifyTraining(config, job, dir) {
192
+ const out = join(dir, 'evidence', 'out', 'training-v1');
193
+ const sha = async path => createHash('sha256').update(await readFile(path)).digest('hex');
194
+ const checks = {};
195
+ try {
196
+ const bridge = JSON.parse(await readFile(join(config.repoRoot, config.trainPayload, 'bridge.json'), 'utf8'));
197
+ const manifestBytes = await readFile(join(out, 'manifest.json'));
198
+ const manifest = JSON.parse(manifestBytes);
199
+ const events = (await readFile(join(dir, 'evidence', 'run', 'train.log'), 'utf8')).split('\n')
200
+ .filter(line => line.startsWith('RUFLO_TRAIN ')).map(line => JSON.parse(line.slice(12)));
201
+ const completed = events.find(event => event.kind === 'completed');
202
+ checks.payloadBound = manifest.training_sha256 === bridge.training_sha256;
203
+ checks.manifestEvent = completed?.data?.manifest_sha256 === createHash('sha256').update(manifestBytes).digest('hex');
204
+ checks.steps = manifest.steps === 96 && manifest.revision === job.model.revision && manifest.model === job.model.repo;
205
+ const files = Object.entries(manifest.files ?? {});
206
+ checks.files = files.length >= 6 && (await Promise.all(files.map(async ([name, digest]) =>
207
+ !name.split('/').includes('..') && await sha(join(out, name)) === digest))).every(Boolean);
208
+ checks.validation = events.filter(event => event.kind === 'validation').map(event => event.data);
209
+ } catch (error) { checks.error = error.code ?? error.name; }
210
+ const ok = Boolean(checks.payloadBound && checks.manifestEvent && checks.steps && checks.files && !checks.error);
211
+ await writePrivate(join(dir, 'verification.json'), { schema: 'ruvllm-vast-training-verification/v1', jobId: job.id, ...checks, ok });
212
+ return { ok };
213
+ }
214
+
215
+ async function inspect({ job, runId, idempotencyKey, signal }) {
216
+ const started = now(), config = await readConfig(), key = await apiKey(config, job.secretRef);
217
+ const instance = (await instances(config, key, signal)).find(row => String(row.id) === runId);
218
+ if (!instance) return { status: 'failed' };
219
+ const costUsd = ceilMicro(instance.dph_total * Math.max(0, now() / 1000 - (instance.start_date ?? now() / 1000)) / 3600);
220
+ if (costUsd > job.maxCostUsd) return { status: 'failed', costUsd };
221
+ // A host whose container runtime cannot start the image never recovers; fail now instead of at the deadline.
222
+ if (instance.actual_status !== 'running' && /error response from daemon|oci runtime|invalid runtime/i.test(instance.status_msg ?? '')) return { status: 'failed', costUsd };
223
+ if (instance.actual_status !== 'running' || !instance.ssh_host || !instance.ssh_port) return { status: 'running', costUsd };
224
+ const dir = attemptDir(config, idempotencyKey);
225
+ const probe = await run('ssh', ssh(config, instance, `cat /workspace/run/status.json && echo && { tail -c 400 /workspace/run/${job.task === 'train' ? 'train' : 'run-pair'}.log 2>/dev/null || true; }`), { timeoutMs: 10000 });
226
+ if (probe.code !== 0) return { status: 'running', costUsd };
227
+ let remote;
228
+ try { remote = JSON.parse(probe.stdout.split('\n')[0]); } catch { return { status: 'running', costUsd }; }
229
+ await writePrivate(join(dir, 'progress.json'), { at: new Date(now()).toISOString(), costUsd, remote, tail: probe.stdout.split('\n').slice(1).join('\n').slice(-400) });
230
+ if (remote.state === 'running') return { status: 'running', costUsd };
231
+ if (remote.state === 'failed') { await retrieve(config, instance, dir, started); return { status: 'failed', costUsd }; }
232
+ if (remote.state !== 'done') return { status: 'running', costUsd };
233
+ // Retain raw outputs locally before any terminal report; retry next tick on a slow pull.
234
+ if (!await retrieve(config, instance, dir, started)) return { status: 'running', costUsd };
235
+ const checked = job.task === 'train' ? await verifyTraining(config, job, dir) : await verify(config, job, dir);
236
+ return { status: 'completed', costUsd, completion: { completionVersion: 1, completionVerified: checked.ok, status: checked.ok ? 'ok' : 'error',
237
+ exitCode: checked.ok ? 0 : 1, assertionsVerified: checked.ok, evidenceComplete: checked.ok, truncated: false, jobId: job.id,
238
+ modelRepo: job.model.repo, modelRevision: job.model.revision, runId, idempotencyKey } };
239
+ }
240
+
241
+ async function destroyConfirmed(config, key, instanceId, signal) {
242
+ await api(config, key, 'DELETE', `/api/v0/instances/${instanceId}/`, { body: {}, signal });
243
+ for (let check = 0; check < 6; check++) {
244
+ if (!(await instances(config, key, signal)).some(row => String(row.id) === String(instanceId))) return true;
245
+ await new Promise(done => setTimeout(done, 2500));
246
+ }
247
+ return false;
248
+ }
249
+
250
+ async function cleanup({ job, runId, signal }) {
251
+ const config = await readConfig(), key = await apiKey(config, job.secretRef);
252
+ const released = await destroyConfirmed(config, key, runId, signal);
253
+ if (released) await run('systemctl', ['--user', 'stop', `ruvllm-vast-guard-${runId}.timer`], { timeoutMs: 5000 });
254
+ await writePrivate(join(config.resultsRoot, `cleanup-${runId}.json`), { schema: 'ruvllm-vast-cleanup/v1', jobId: job.id,
255
+ instanceId: runId, confirmedAbsent: released, at: new Date(now()).toISOString() });
256
+ return { released };
257
+ }
258
+
259
+ return { submit, inspect, cleanup, destroyConfirmed, instances };
260
+ }
261
+
262
+ const defaults = createVastAdapter();
263
+ export const submit = input => defaults.submit(input);
264
+ export const inspect = input => defaults.inspect(input);
265
+ export const cleanup = input => defaults.cleanup(input);
266
+
267
+ if (process.argv[1] === SELF && process.argv[2] === 'guard') {
268
+ const [instanceId, ref] = process.argv.slice(3);
269
+ const config = await readHostConfig();
270
+ const released = await defaults.destroyConfirmed(config, await apiKey(config, ref), instanceId);
271
+ await writePrivate(join(config.resultsRoot, `guard-${instanceId}.json`), { schema: 'ruvllm-vast-guard/v1', instanceId,
272
+ confirmedAbsent: released, at: new Date().toISOString() });
273
+ process.exitCode = released ? 0 : 1;
274
+ }