ruvnet-brain 4.5.7 → 4.5.8

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 (32) hide show
  1. package/README.md +1 -1
  2. package/bin/install.mjs +50 -1
  3. package/package.json +5 -3
  4. package/plugin/.claude-plugin/plugin.json +1 -1
  5. package/plugin/.codex-plugin/plugin.json +1 -1
  6. package/plugin/hooks/codex-hooks.json +31 -1
  7. package/plugin/hooks/hook-contracts.json +81 -1
  8. package/plugin/hooks/hooks.json +31 -1
  9. package/plugin/scripts/agentdb-recall.mjs +38 -10
  10. package/plugin/scripts/continuity-hook-policy.mjs +3 -0
  11. package/plugin/scripts/hook-shim.mjs +1 -1
  12. package/plugin/scripts/kb-copy-proof.mjs +80 -19
  13. package/plugin/scripts/learn-capture.mjs +44 -0
  14. package/plugin/scripts/learn-capture.sh +2 -241
  15. package/plugin/scripts/learn-flush.mjs +86 -191
  16. package/plugin/scripts/learning-observation.mjs +51 -0
  17. package/plugin/scripts/learning-queue.mjs +135 -0
  18. package/plugin/scripts/learning-store.mjs +105 -0
  19. package/plugin/scripts/learning-worker-supervisor.mjs +79 -0
  20. package/plugin/scripts/project-progression-contract.mjs +58 -4
  21. package/plugin/scripts/runtime-preferences.mjs +45 -4
  22. package/plugin/scripts/session-start-core.mjs +8 -0
  23. package/plugin/scripts/session-start-proof.mjs +49 -0
  24. package/scripts/codex-fresh-host-proof.mjs +179 -0
  25. package/scripts/codex-host-execution-proof.mjs +246 -0
  26. package/scripts/codex-host-proof-runtime.mjs +104 -0
  27. package/scripts/console-engine.mjs +29 -16
  28. package/scripts/health-repair.mjs +47 -68
  29. package/scripts/onboarding-console.mjs +25 -54
  30. package/scripts/qa/progression-validation-benchmark.mjs +66 -0
  31. package/scripts/release-qualification-contract.mjs +49 -3
  32. package/scripts/remedy-registry.mjs +9 -2
@@ -0,0 +1,246 @@
1
+ // Explicitly authorized owned startup, NOT a read-only doctor action. Never imported by doctor.
2
+ // packageSha256/workerSha256 must come from independently authenticated release receipts.
3
+ import fs from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import crypto from 'node:crypto';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { spawn, execFileSync } from 'node:child_process';
9
+ import { probeFreshCodexDeclarations, classifyFreshCodexDeclarations } from './codex-fresh-host-proof.mjs';
10
+ import { runtimeSnapshot } from './model-routing-launchers.mjs';
11
+ import { maintenanceStatus } from '../plugin/scripts/development-maintenance.mjs';
12
+ import { proofFile, proofDigest, verifyReleasedHostRuntime, assertHostRuntimeUnchanged } from './codex-host-proof-runtime.mjs';
13
+
14
+ const OWNER = 'ruvnet-brain@ruvnet-brain';
15
+ // The diagnostic verifies the canonical ordinary-work route, never selects a separate model.
16
+ const ordinaryRoute = JSON.parse(fs.readFileSync(new URL('../config/model-router/routing-policy.template.json', import.meta.url), 'utf8')).routes.codex.medium;
17
+ const failure = (reason) => ({ ok: false, state: 'fresh-execution-unproven', scope: 'one-owned-codex-session-start-and-brain-search', reason,
18
+ existingWindows: 'unproven', otherHooks: 'unknown', ongoingLiveness: 'unknown', startupHealth: 'unknown' });
19
+ export function startupDeclaration(listed, cwd) {
20
+ const groups = listed?.data;
21
+ if (!Array.isArray(groups) || groups.length !== 1 || groups[0]?.cwd !== cwd
22
+ || !Array.isArray(groups[0].hooks) || !Array.isArray(groups[0].warnings) || !Array.isArray(groups[0].errors)
23
+ || groups[0].warnings.length || groups[0].errors.length) throw new Error('Native registry errors, warnings, or scope prevent execution');
24
+ const starts = groups[0].hooks.filter((row) => row.eventName === 'sessionStart' && row.enabled !== false);
25
+ const triggered = new Set(['sessionStart', 'userPromptSubmit', 'stop', 'sessionEnd']);
26
+ if (groups[0].hooks.some((row) => triggered.has(row.eventName) && row.enabled !== false
27
+ && (row.pluginId !== OWNER || row.handlerType !== 'command' || !['trusted', 'managed'].includes(row.trustStatus)))) throw new Error('Foreign or non-command enabled turn/startup handler prevents owned execution');
28
+ // Independently bound published siblings (e.g. learn-flush) may share this event/source file.
29
+ const bodies = starts.filter((row) => /\s(?:\d+\s+)?session-start\s*$/.test(row.command || ''));
30
+ if (bodies.length !== 1 || !Number.isSafeInteger(bodies[0].displayOrder) || bodies[0].displayOrder < 0) throw new Error('Exact published SessionStart body identity is unavailable');
31
+ return bodies[0];
32
+ }
33
+ export function classifyOwnedExecution({ body, nonce, runtime, cwd, threadId, hookRuns, declaration, turnContext, search, mcp }) {
34
+ if (body?.schema !== 1 || body.state !== 'body-executed' || body.nonce !== nonce || body.cwd !== cwd
35
+ || body.version !== runtime.version || body.sourcePath !== path.join(runtime.codeRoot, 'scripts/session-start-core.mjs')
36
+ || runtime.bindings.find((row) => row.path === body.sourcePath)?.sha256 !== body.sourceSha256
37
+ || body.bodyFailed !== false || body.bannerFallback !== false || body.restore?.failed !== false
38
+ || !Array.isArray(body.stages) || !body.stages.length || body.stages.some((stage) => stage.skipped !== false)
39
+ || !body.stages.some((stage) => stage.name === 'banner')) throw new Error('Positive startup stage evidence is missing, failed, or skipped');
40
+ const starts = hookRuns.filter((row) => row.threadId === threadId && row.run?.eventName === 'sessionStart' && row.run.displayOrder === declaration.displayOrder);
41
+ if (starts.length !== 1 || starts[0].run.sourcePath !== declaration.sourcePath || starts[0].run.handlerType !== 'command'
42
+ || starts[0].run.source !== 'plugin' || starts[0].run.status !== 'completed') throw new Error('Native startup notification is missing or mismatched');
43
+ if (turnContext?.type !== 'turn_context' || turnContext.payload?.model !== ordinaryRoute.model
44
+ || turnContext.payload.effort !== ordinaryRoute.effort || turnContext.payload.cwd !== cwd
45
+ || ![null, undefined, 'default'].includes(turnContext.payload.service_tier)) throw new Error('Actual native ordinary-work Standard turn telemetry is unproven');
46
+ const text = search?.content?.filter((row) => row.type === 'text').map((row) => row.text).join('\n') || '';
47
+ const citation = /^path\s*:\s*(ruflo\/[^\r\n]+)$/m.exec(text)?.[1];
48
+ if (search?.isError !== false || !citation || !/#1\s+repo=ruflo\b/.test(text) || !text.includes('full document')) throw new Error('Same-thread Brain search lacks the expected non-error cited result');
49
+ if (!mcp?.owned || mcp.server !== 'ruvnet-brain' || mcp.readiness?.state !== 'ready' || mcp.readiness.workerPid !== mcp.workerPid
50
+ || mcp.readiness.pid !== mcp.pid || !String(mcp.readiness.generation).startsWith(`${runtime.generation}:`)
51
+ || mcp.shellSha256 !== runtime.bindings.find((row) => row.path === runtime.mcpShell)?.sha256
52
+ || mcp.workerSha256 !== runtime.bindings.find((row) => row.path === runtime.worker)?.sha256) throw new Error('Owned same-thread loaded MCP source and generation are unproven');
53
+ return { ok: true, state: 'fresh-execution-proven', scope: 'one-owned-codex-session-start-and-brain-search',
54
+ threadId, startupStages: body.stages, restore: body.restore, startupHealth: 'unknown', mcpReadiness: 'same-thread-cited-search-proven',
55
+ model: ordinaryRoute.model, effort: ordinaryRoute.effort, serviceTier: 'default', citation, searchSha256: proofDigest(JSON.stringify(search)),
56
+ existingWindows: 'unproven', otherHooks: 'unknown', ongoingLiveness: 'unknown' };
57
+ }
58
+
59
+ function processes(deadline) {
60
+ if (Date.now() >= deadline) throw new Error('Owned process inventory deadline exceeded');
61
+ const output = execFileSync('/bin/ps', ['-axo', 'uid=,pid=,ppid=,lstart=,command='], { encoding: 'utf8', timeout: Math.min(500, deadline - Date.now()), maxBuffer: 2 * 1024 * 1024 });
62
+ return output.split('\n').filter(Boolean).map((line) => {
63
+ const parts = line.trim().split(/\s+/);
64
+ if (parts.length < 9 || !parts.slice(0, 3).every((part) => /^\d+$/.test(part))) throw new Error('Owned process inventory is malformed');
65
+ return { uid: Number(parts[0]), pid: Number(parts[1]), ppid: Number(parts[2]), birth: parts.slice(3, 8).join(' '), command: parts.slice(8).join(' ') };
66
+ });
67
+ }
68
+
69
+ /** Explicit caller authorization permits ordinary production startup maintenance. No shared daemon. */
70
+ export async function produceOwnedCodexExecutionProof({ authorizeOwnedStartup = false, binary, codexHome, cwd, brainHome,
71
+ releasedPluginRoot, version, packageArchive, packageSha256, workerSha256, mcpShell, terminalConfig, expectedRoutingDigest,
72
+ timeoutMs = 90_000, env = process.env, dependencies = {} } = {}) {
73
+ const deadline = Date.now() + timeoutMs; const owned = new Map(); const children = []; let watcher; let child; let directory;
74
+ let trackingFailure; let outputBytes = 0; let inferenceRequests = 0; let closeWire = () => {}; let result = failure('Execution was not attempted');
75
+ const scan = dependencies.processes || processes;
76
+ const spawnHost = dependencies.spawn || spawn;
77
+ const proofEnv = { ...env };
78
+ for (const key of ['NODE_OPTIONS', 'NODE_PATH', 'BASH_ENV', 'ENV']) delete proofEnv[key];
79
+ // Reserve retirement time within the same absolute deadline, rather than adding another timeout.
80
+ const remaining = () => { const ms = deadline - 2000 - Date.now(); if (ms <= 0) throw new Error('Owned execution absolute deadline exceeded'); return ms; };
81
+ const observe = () => {
82
+ const rows = scan(deadline); let changed = true;
83
+ for (const process of children) {
84
+ const row = rows.find((item) => item.pid === process.pid);
85
+ if (row && process.exitCode === null && process.signalCode === null && !owned.has(row.pid) && row.uid === globalThis.process.getuid()) owned.set(row.pid, row);
86
+ if (process.pid && process.exitCode === null && process.signalCode === null && (!row || owned.get(row.pid)?.birth !== row.birth)) trackingFailure = true;
87
+ }
88
+ while (changed) { changed = false; for (const row of rows) {
89
+ const parent = owned.get(row.ppid); const liveParent = rows.find((item) => item.pid === row.ppid);
90
+ if (!owned.has(row.pid) && row.uid === globalThis.process.getuid() && parent && liveParent?.birth === parent.birth) { owned.set(row.pid, row); changed = true; }
91
+ } }
92
+ return rows;
93
+ };
94
+ const launch = (...args) => { const process = spawnHost(...args); children.push(process); observe(); return process; };
95
+ try {
96
+ if (!authorizeOwnedStartup || process.platform === 'win32' || !Number.isFinite(timeoutMs) || timeoutMs < 3000 || timeoutMs > 120_000
97
+ || !/^[a-f0-9]{64}$/.test(expectedRoutingDigest || '')
98
+ || ![binary, codexHome, cwd, brainHome, releasedPluginRoot, mcpShell, terminalConfig].every((file) => path.isAbsolute(file || ''))) throw new Error('Explicit owned Unix startup authorization, authenticated routing identity, and bounded absolute paths required');
99
+ cwd = fs.realpathSync(cwd); codexHome = fs.realpathSync(codexHome); brainHome = fs.realpathSync(brainHome);
100
+ if (maintenanceStatus(cwd).suspended) throw new Error('Development hooks are suspended; startup execution is unproven');
101
+ watcher = setInterval(() => { try { observe(); } catch { trackingFailure = true; } }, 100);
102
+ const declarations = await (dependencies.declarations || probeFreshCodexDeclarations)({ binary, codexHome, cwd, releasedPluginRoot,
103
+ expectedVersion: version, timeoutMs: Math.min(12_000, remaining()), env: proofEnv, spawnChild: launch });
104
+ if (!declarations.ok) throw new Error(declarations.reason);
105
+ if (declarations.nativeVersion !== 'codex-cli 0.160.0') throw new Error('Owned execution schema is verified only for native Codex 0.160.0');
106
+ const pluginRoot = declarations.pluginRoot;
107
+ const runtime = await (dependencies.verifyRuntime || verifyReleasedHostRuntime)({ version, packageArchive, packageSha256, workerSha256,
108
+ brainHome, pluginRoot, mcpShell, timeoutMs: remaining() });
109
+ const configBytes = proofFile(terminalConfig); const config = JSON.parse(configBytes);
110
+ const nodeBinary = fs.realpathSync(process.execPath); const nodeBytes = proofFile(nodeBinary, 256 * 1024 * 1024);
111
+ const magic = nodeBytes.subarray(0, 4).toString('hex');
112
+ if (!['cffaedfe', 'feedfacf', 'cefaedfe', 'feedface', 'cafebabe', 'bebafeca', '7f454c46'].includes(magic)) throw new Error('Actual native Node interpreter required');
113
+ const nodeSha256 = proofDigest(nodeBytes);
114
+ if (config.managedBy !== 'ruvnet-brain-terminal-launchers' || fs.realpathSync(config.realCodex) !== fs.realpathSync(binary)
115
+ || fs.realpathSync(config.nodeBinary) !== nodeBinary
116
+ || config.runtimeDigest !== expectedRoutingDigest || runtimeSnapshot(config.runtimeRoot).digest !== expectedRoutingDigest) throw new Error('Installed immutable Standard gateway closure is unproven');
117
+ const gateway = path.join(config.runtimeRoot, 'scripts/model-routing-gateway.mjs');
118
+ directory = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'rnb-owned-host-'))); fs.chmodSync(directory, 0o700);
119
+ const nonce = crypto.randomBytes(32).toString('hex'); const proofPath = path.join(directory, 'startup.json');
120
+ fs.writeFileSync(proofPath, JSON.stringify({ schema: 1, state: 'pending', nonce }), { mode: 0o600, flag: 'wx' });
121
+ fs.writeFileSync(path.join(directory, 'routing.jsonl'), '', { mode: 0o600, flag: 'wx' });
122
+ const clean = { ...proofEnv, CODEX_HOME: codexHome, RUVNET_BRAIN_HOME: brainHome, RUVNET_BRAIN_HOST_PROOF_PATH: proofPath,
123
+ RUVNET_BRAIN_HOST_PROOF_NONCE: nonce, MODEL_ROUTER_DECISIONS: path.join(directory, 'routing.jsonl') };
124
+ clean.PATH = `${path.dirname(nodeBinary)}${path.delimiter}${proofEnv.PATH || ''}`;
125
+ for (const key of ['OPENAI_API_KEY', 'OPENAI_BASE_URL', 'ANTHROPIC_API_KEY', 'CODEX_API_KEY', 'RUVNET_BRAIN_TEST', 'RUVNET_BRAIN_CHILD_MCP']) delete clean[key];
126
+ child = launch(nodeBinary, [gateway, '--harness', 'codex', '--real-binary', binary, 'app-server', '--listen', 'stdio://',
127
+ '-c', 'features.shell_tool=false', '-c', 'features.unified_exec=false', '-c', 'features.code_mode=false',
128
+ '-c', 'features.code_mode_host=false', '-c', 'features.multi_agent=false', '-c', 'web_search="disabled"'],
129
+ { cwd, env: clean, shell: false, stdio: ['pipe', 'pipe', 'pipe'] });
130
+ const pending = new Map(); const notices = []; let nextId = 1; let buffer = ''; let wireFailure;
131
+ const consume = (chunk) => { outputBytes += chunk.length; if (outputBytes > 8 * 1024 * 1024) throw new Error('Cumulative owned execution output exceeded bound'); };
132
+ const reject = (error) => { wireFailure = error; for (const item of pending.values()) { clearTimeout(item.timer); item.reject(error); } pending.clear(); };
133
+ closeWire = () => reject(new Error('Owned proof client closed'));
134
+ child.stdin.on('error', () => reject(new Error('Owned native input failed')));
135
+ child.on('error', () => reject(new Error('Owned native launch failed')));
136
+ child.on('exit', () => reject(new Error('Owned native process exited before proof')));
137
+ child.stderr.on('data', (chunk) => { try { consume(chunk); } catch (error) { reject(error); } });
138
+ child.stdout.on('data', (chunk) => { try {
139
+ consume(chunk); buffer += chunk;
140
+ while (buffer.includes('\n')) {
141
+ const end = buffer.indexOf('\n'); const row = JSON.parse(buffer.slice(0, end)); buffer = buffer.slice(end + 1);
142
+ if (row.method) {
143
+ if (row.id !== undefined) throw new Error('Native tool or approval request refused by proof client');
144
+ notices.push(row); if (notices.length > 20_000) throw new Error('Native notification count exceeds bound');
145
+ } else { const item = pending.get(row.id); if (item) { pending.delete(row.id); clearTimeout(item.timer);
146
+ row.error ? item.reject(new Error(`Native ${item.method} refused`)) : item.resolve(row.result); } }
147
+ }
148
+ } catch (error) { reject(error); } });
149
+ const rpc = (method, params) => new Promise((resolve, rejectRequest) => {
150
+ if (wireFailure) { rejectRequest(wireFailure); return; }
151
+ const id = nextId++; const timer = setTimeout(() => { pending.delete(id); rejectRequest(new Error(`Native ${method} deadline exceeded`)); }, remaining());
152
+ pending.set(id, { resolve, reject: rejectRequest, timer, method });
153
+ if (method === 'turn/start') inferenceRequests++;
154
+ child.stdin.write(`${JSON.stringify({ id, method, params })}\n`);
155
+ });
156
+ await rpc('initialize', { clientInfo: { name: 'rnb_owned_startup_proof', version: '1' }, capabilities: { experimentalApi: true } });
157
+ child.stdin.write('{"method":"initialized"}\n');
158
+ const before = await rpc('hooks/list', { cwds: [cwd] }); const declaration = startupDeclaration(before, cwd);
159
+ const target = { manifest: JSON.parse(proofFile(path.join(releasedPluginRoot, '.codex-plugin/plugin.json'))),
160
+ hooks: JSON.parse(proofFile(path.join(releasedPluginRoot, 'hooks/codex-hooks.json'))), hashes: declarations.sourceHashes };
161
+ const bound = classifyFreshCodexDeclarations({ plugin: { installed: true, enabled: true, version }, listed: before, target, expectedVersion: version, codexHome, cwd });
162
+ if (!bound.ok || trackingFailure) throw new Error(bound.reason || 'Owned descendant tracking is unproven');
163
+ const thread = (await rpc('thread/start', { cwd, ephemeral: false, serviceTier: 'default', sandbox: 'read-only', approvalPolicy: 'untrusted' })).thread;
164
+ if (!thread?.id || !path.isAbsolute(thread.path || '')) throw new Error('Owned persistent thread identity is unavailable');
165
+ await rpc('turn/start', { threadId: thread.id, input: [{ type: 'text', text: 'Without tools or file changes, explain 2 + 2 in one sentence.' }] });
166
+ while (!notices.some((row) => row.method === 'turn/completed' && row.params?.threadId === thread.id)) {
167
+ if (wireFailure) throw wireFailure; remaining(); await new Promise((resolve) => setTimeout(resolve, Math.min(25, remaining())));
168
+ }
169
+ const completed = notices.find((row) => row.method === 'turn/completed' && row.params?.threadId === thread.id);
170
+ if (completed.params.turn.status !== 'completed') throw new Error('Owned native turn did not complete');
171
+ if (notices.some((row) => ['item/started', 'item/completed'].includes(row.method) && row.params?.threadId === thread.id
172
+ && !['userMessage', 'agentMessage', 'reasoning', 'plan'].includes(row.params?.item?.type))) throw new Error('Unexpected model tool action invalidates the one-plaintext-turn proof');
173
+ const inventory = await rpc('mcpServerStatus/list', { threadId: thread.id, limit: 100, detail: 'toolsAndAuthOnly' });
174
+ const servers = inventory.data?.filter((row) => row.name === 'ruvnet-brain');
175
+ if (servers?.length !== 1 || servers[0].runtimeStatus !== 'connected') throw new Error('Same-thread Brain MCP is not connected');
176
+ const search = await rpc('mcpServer/tool/call', { threadId: thread.id, server: 'ruvnet-brain', tool: 'search_ruvnet', arguments: { query: 'Ruflo architecture', k: 1 } });
177
+ const after = await rpc('hooks/list', { cwds: [cwd] });
178
+ if (JSON.stringify(before) !== JSON.stringify(after)) throw new Error('Native registry changed during execution');
179
+ const live = observe(); const shells = live.filter((row) => owned.get(row.pid)?.birth === row.birth && row.command.endsWith(` ${mcpShell}`));
180
+ if (shells.length !== 1) throw new Error('Owned Brain MCP shell identity is ambiguous');
181
+ const shell = shells[0]; const readiness = JSON.parse(proofFile(path.join(brainHome, 'mcp-readiness.d', `${shell.pid}.json`)));
182
+ const worker = live.find((row) => row.pid === readiness.workerPid && row.ppid === shell.pid && row.command.endsWith(` ${runtime.worker}`));
183
+ if (!worker || owned.get(worker.pid)?.birth !== worker.birth) throw new Error('Loaded Brain worker ancestry is unproven');
184
+ const transcript = fs.realpathSync(thread.path);
185
+ if (!transcript.startsWith(`${fs.realpathSync(path.join(codexHome, 'sessions'))}${path.sep}`)) throw new Error('Native turn transcript escaped owned sessions');
186
+ const rows = proofFile(transcript, 8 * 1024 * 1024).toString().trim().split('\n').map((line) => JSON.parse(line));
187
+ if (rows.find((row) => row.type === 'session_meta')?.payload?.id !== thread.id) throw new Error('Native durable telemetry differs from owned thread');
188
+ const contexts = rows.filter((row) => row.type === 'turn_context'); if (contexts.length !== 1) throw new Error('Exact one-turn native telemetry is unproven');
189
+ const body = JSON.parse(proofFile(proofPath));
190
+ if (!owned.has(body.pid) || body.startedAt < deadline - timeoutMs || body.finishedAt > Date.now()) throw new Error('Startup body is not tied to owned process lifetime');
191
+ assertHostRuntimeUnchanged(runtime, brainHome);
192
+ if (trackingFailure || !proofFile(terminalConfig).equals(configBytes) || runtimeSnapshot(config.runtimeRoot).digest !== config.runtimeDigest
193
+ || proofDigest(proofFile(nodeBinary, 256 * 1024 * 1024)) !== nodeSha256
194
+ || proofDigest(proofFile(binary, 256 * 1024 * 1024)) !== declarations.nativeBinarySha256) throw new Error('Owned tracking, interpreter, native binary, or routing runtime changed during proof');
195
+ result = { ...classifyOwnedExecution({ body, nonce, runtime, cwd, threadId: thread.id, hookRuns: notices.filter((row) => row.method === 'hook/completed').map((row) => row.params),
196
+ declaration, turnContext: contexts[0], search, mcp: { owned: true, server: 'ruvnet-brain', pid: shell.pid, workerPid: worker.pid, readiness,
197
+ shellSha256: proofDigest(proofFile(mcpShell)), workerSha256: proofDigest(proofFile(runtime.worker, 32 * 1024 * 1024)) } }),
198
+ runtime, nativeBinary: declarations.nativeBinary, nativeBinarySha256: declarations.nativeBinarySha256, nativeVersion: declarations.nativeVersion,
199
+ nodeBinary, nodeSha256, codexHome, cwd, routingRuntimeDigest: config.runtimeDigest, transcriptSha256: proofDigest(proofFile(transcript, 8 * 1024 * 1024)),
200
+ optOuts: ['shell_tool', 'unified_exec', 'code_mode', 'code_mode_host', 'multi_agent', 'web_search'], ordinaryStartupMaintenance: 'permitted', nativeCacheRefresh: 'possible' };
201
+ await rpc('thread/unsubscribe', { threadId: thread.id });
202
+ // A ~40ms detach launcher can fork/reparent between ps samples. Retain the positively bound
203
+ // execution component, but never turn sampled ancestry into a full convergence/cleanup claim.
204
+ result = { ...result, ...failure('Exhaustive detached startup descendant retirement is unproven'), executionEvidence: result };
205
+ } catch (error) { result = failure(error.message); }
206
+ finally {
207
+ clearInterval(watcher); closeWire(); let unverified = false;
208
+ try { if (children.length) {
209
+ observe(); for (const signal of ['SIGTERM', 'SIGKILL']) {
210
+ const live = scan(deadline); const matches = live.filter((row) => owned.get(row.pid)?.birth === row.birth && row.uid === process.getuid());
211
+ for (const row of matches) { try { (dependencies.kill || process.kill)(row.pid, signal); } catch { /* verify actual disappearance below */ } }
212
+ await new Promise((resolve) => setTimeout(resolve, Math.max(0, Math.min(250, deadline - Date.now()))));
213
+ }
214
+ unverified = scan(deadline).some((row) => owned.get(row.pid)?.birth === row.birth);
215
+ } } catch { unverified = true; }
216
+ for (const process of children) { process.stdin?.destroy(); process.stdout?.destroy(); process.stderr?.destroy(); process.unref?.(); }
217
+ if (unverified || trackingFailure) result = failure('Owned descendant retirement or tracking is unverified');
218
+ result = { ...result, inferenceRequests, outputBytes, retirement: unverified || trackingFailure ? 'unverified' : 'discovered-owned-processes-retired',
219
+ cleanupScope: 'observed-owned-processes-only', exhaustiveDescendants: 'unproven', directory };
220
+ if (directory) fs.writeFileSync(path.join(directory, 'receipt.json'), `${JSON.stringify(result, null, 2)}\n`, { mode: 0o600, flag: 'wx' });
221
+ }
222
+ return result;
223
+ }
224
+
225
+ const CLI_FIELDS = new Set(['binary', 'codexHome', 'cwd', 'brainHome', 'releasedPluginRoot', 'version',
226
+ 'packageArchive', 'packageSha256', 'workerSha256', 'mcpShell', 'terminalConfig', 'expectedRoutingDigest', 'timeoutMs']);
227
+ export async function executionProofMain(args, { run = produceOwnedCodexExecutionProof, output = (text) => process.stdout.write(text), platform = process.platform } = {}) {
228
+ const usage = 'Usage: npm run host:codex:execution-proof -- --authorize-owned-startup --options /absolute/proof-inputs.json\n'
229
+ + 'Explicit opt-in: one native subscription turn, production startup maintenance, and one Brain search.\n'
230
+ + 'Requires independently authenticated release/routing inputs. Overall convergence remains UNPROVEN.\n';
231
+ if (args.length === 1 && args[0] === '--help') { output(usage); return 0; }
232
+ try {
233
+ if (args.length !== 3 || args[0] !== '--authorize-owned-startup' || args[1] !== '--options'
234
+ || !path.isAbsolute(args[2]) || platform === 'win32') throw new Error('Explicit Unix startup authorization and absolute options file required');
235
+ const options = JSON.parse(proofFile(args[2], 64 * 1024));
236
+ if (!options || typeof options !== 'object' || Array.isArray(options)
237
+ || Object.keys(options).some((key) => !CLI_FIELDS.has(key))
238
+ || Object.entries(options).some(([key, value]) => key === 'timeoutMs' ? typeof value !== 'number' : typeof value !== 'string')) throw new Error('Invalid proof options; environment, dependencies, and embedded authorization are forbidden');
239
+ const result = await run({ ...options, authorizeOwnedStartup: true });
240
+ output(`${JSON.stringify(result, null, 2)}\n`);
241
+ return result.ok ? 0 : 1;
242
+ } catch (error) { output(`${JSON.stringify(failure(error.message))}\n${usage}`); return 2; }
243
+ }
244
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
245
+ process.exitCode = await executionProofMain(process.argv.slice(2));
246
+ }
@@ -0,0 +1,104 @@
1
+ // Independently published npm bytes bind the execution closure. The caller must authenticate
2
+ // workerSha256 from its signed public bundle receipt; a local manifest is never a release anchor.
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import crypto from 'node:crypto';
6
+ import { gunzipSync } from 'node:zlib';
7
+
8
+ export const proofDigest = (bytes) => crypto.createHash('sha256').update(bytes).digest('hex');
9
+ export function proofFile(file, limit = 2 * 1024 * 1024) {
10
+ const stat = fs.lstatSync(file);
11
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > limit) throw new Error('Proof input is not a bounded regular file');
12
+ return fs.readFileSync(file);
13
+ }
14
+ export function publishedTarFiles(archive) {
15
+ const tar = gunzipSync(archive, { maxOutputLength: 128 * 1024 * 1024 }); const files = new Map();
16
+ let offset = 0; let paxPath;
17
+ while (offset + 512 <= tar.length) {
18
+ const header = tar.subarray(offset, offset + 512); offset += 512;
19
+ if (header.every((byte) => byte === 0)) break;
20
+ const field = (start, end) => header.subarray(start, end).toString().replace(/\0.*$/s, '');
21
+ const octal = (value) => /^[\s0-7]+\0?$/.test(value) ? parseInt(value.trim(), 8) : NaN;
22
+ const size = octal(field(124, 136)); const checksum = octal(field(148, 156));
23
+ const sum = [...header].reduce((total, byte, i) => total + (i >= 148 && i < 156 ? 32 : byte), 0);
24
+ if (!Number.isSafeInteger(size) || size < 0 || offset + size > tar.length || sum !== checksum) throw new Error('Published archive header is invalid');
25
+ const body = tar.subarray(offset, offset + size); offset += Math.ceil(size / 512) * 512;
26
+ const type = field(156, 157);
27
+ if (type === 'x') {
28
+ let cursor = 0;
29
+ while (cursor < body.length) {
30
+ const space = body.indexOf(32, cursor); const length = Number(body.subarray(cursor, space).toString());
31
+ if (space < cursor || !Number.isSafeInteger(length) || length < 3 || cursor + length > body.length) throw new Error('Published archive metadata is invalid');
32
+ const entry = body.subarray(space + 1, cursor + length - 1).toString();
33
+ if (entry.startsWith('path=')) paxPath = entry.slice(5);
34
+ cursor += length;
35
+ }
36
+ continue;
37
+ }
38
+ const name = paxPath || [field(345, 500), field(0, 100)].filter(Boolean).join('/'); paxPath = undefined;
39
+ if (type === '5') continue;
40
+ if (type !== '' && type !== '0') throw new Error('Published archive contains a non-regular member');
41
+ if (!name.startsWith('package/') || path.posix.normalize(name) !== name || name.split('/').includes('..') || files.has(name)) throw new Error('Published archive member is unsafe or duplicated');
42
+ files.set(name, body);
43
+ }
44
+ return files;
45
+ }
46
+
47
+ export async function verifyReleasedHostRuntime({ version, packageArchive, packageSha256, workerSha256,
48
+ brainHome, pluginRoot, mcpShell, timeoutMs, fetch: fetcher = globalThis.fetch }) {
49
+ if (!/^\d+\.\d+\.\d+$/.test(version || '') || ![packageSha256, workerSha256].every((hash) => /^[a-f0-9]{64}$/.test(hash || ''))
50
+ || ![packageArchive, brainHome, pluginRoot, mcpShell].every((file) => path.isAbsolute(file || ''))) throw new Error('Authenticated release inputs and absolute runtime paths required');
51
+ const archive = proofFile(packageArchive, 32 * 1024 * 1024);
52
+ if (proofDigest(archive) !== packageSha256) throw new Error('Public package digest mismatch');
53
+ const response = await fetcher(`https://registry.npmjs.org/ruvnet-brain/${version}`, { redirect: 'error', signal: AbortSignal.timeout(timeoutMs) });
54
+ if (!response.ok) throw new Error('Published package identity unavailable');
55
+ const reader = response.body.getReader(); const chunks = []; let length = 0;
56
+ try { for (;;) { const { done, value } = await reader.read(); if (done) break;
57
+ length += value.byteLength; if (length > 2 * 1024 * 1024) throw new Error('Published metadata exceeds bound'); chunks.push(Buffer.from(value));
58
+ } } finally { await reader.cancel().catch(() => {}); }
59
+ const bytes = Buffer.concat(chunks);
60
+ const metadata = JSON.parse(bytes);
61
+ if (metadata.name !== 'ruvnet-brain' || metadata.version !== version
62
+ || metadata.dist?.tarball !== `https://registry.npmjs.org/ruvnet-brain/-/ruvnet-brain-${version}.tgz`
63
+ || metadata.dist?.integrity !== `sha512-${crypto.createHash('sha512').update(archive).digest('base64')}`) throw new Error('Published package integrity mismatch');
64
+ const published = publishedTarFiles(archive);
65
+ const activeBytes = proofFile(path.join(brainHome, 'active.json')); const active = JSON.parse(activeBytes);
66
+ const codeRoot = fs.realpathSync(path.isAbsolute(active.codeRoot || '') ? active.codeRoot : path.join(brainHome, active.codeRoot || ''));
67
+ if (active.version !== version || !Number.isSafeInteger(active.generation) || active.generation < 1
68
+ || codeRoot !== fs.realpathSync(path.join(brainHome, 'versions', version))
69
+ || fs.existsSync(path.join(brainHome, 'dev.json')) || fs.existsSync(path.join(brainHome, '.kb.refresh-run.lock'))) throw new Error('Active released generation is unstable or unproven');
70
+ const bindings = [];
71
+ const bind = (file, member) => {
72
+ const expected = published.get(member); if (!expected || !proofFile(file).equals(expected)) throw new Error('Loaded host runtime differs from independent published bytes');
73
+ bindings.push({ path: file, sha256: proofDigest(expected) });
74
+ };
75
+ for (const [name] of published) {
76
+ if (/^package\/plugin\/(scripts|mcp)\/.*\.(mjs|sh)$/.test(name)) bind(path.join(codeRoot, name.slice('package/plugin/'.length)), name);
77
+ }
78
+ if (!bindings.some((row) => row.path === path.join(codeRoot, 'scripts/session-start-proof.mjs'))) throw new Error('Released startup diagnostic is unavailable');
79
+ bind(path.join(brainHome, 'codex-hook.mjs'), 'package/plugin/scripts/codex-hook-wrapper.mjs');
80
+ // The managed wrapper selects activeRoot/scripts/codex-hook-adapter.mjs; that adapter's shim
81
+ // is its immutable sibling, already bound above. There is no brainHome/scripts shim tree.
82
+ const optionalHelper = path.join(brainHome, 'development-maintenance.mjs'); const absent = [];
83
+ if (fs.existsSync(optionalHelper)) bind(optionalHelper, 'package/plugin/scripts/development-maintenance.mjs');
84
+ else absent.push(optionalHelper);
85
+ const visited = new Set(); const bindImports = (file, member) => {
86
+ if (visited.has(file)) return; visited.add(file); bind(file, member);
87
+ for (const match of published.get(member).toString().matchAll(/(?:from\s*|import\s*|import\(\s*)['"](\.[^'"]+)['"]/g)) {
88
+ bindImports(path.resolve(path.dirname(file), match[1]), path.posix.normalize(path.posix.join(path.posix.dirname(member), match[1])));
89
+ }
90
+ };
91
+ bindImports(mcpShell, 'package/plugin/mcp/server.mjs');
92
+ for (const member of ['.codex-plugin/plugin.json', 'hooks/codex-hooks.json']) bind(path.join(pluginRoot, member), `package/plugin/${member}`);
93
+ const worker = path.join(brainHome, 'kb/forge-mcp-all.mjs');
94
+ if (proofDigest(proofFile(worker, 32 * 1024 * 1024)) !== workerSha256) throw new Error('KB worker differs from authenticated public bundle');
95
+ bindings.push({ path: worker, sha256: workerSha256 });
96
+ return { version, generation: active.generation, codeRoot, activeSha256: proofDigest(activeBytes),
97
+ packageSha256, integrity: metadata.dist.integrity, worker, mcpShell, bindings, absent };
98
+ }
99
+
100
+ export function assertHostRuntimeUnchanged(runtime, brainHome) {
101
+ if (proofDigest(proofFile(path.join(brainHome, 'active.json'))) !== runtime.activeSha256
102
+ || runtime.bindings.some((row) => proofDigest(proofFile(row.path, 32 * 1024 * 1024)) !== row.sha256)
103
+ || runtime.absent?.some((file) => fs.existsSync(file))) throw new Error('Active generation or loaded source changed during proof');
104
+ }
@@ -100,44 +100,57 @@ export function buildHealthRecommendations({ memory = null, learning = null } =
100
100
  }
101
101
 
102
102
  // 2. A capture queue that fills but does not drain. The depth IS the evidence.
103
- const depth = Number(learning?.queueDepth);
104
- if (Number.isFinite(depth) && depth > 50) {
103
+ const depth = learning?.enabled === false || learning?.queueKnown === false ? 0 : Number(learning?.queueDepth);
104
+ if (Number.isFinite(depth) && (depth > 50 || depth > 0 && learning.capturePendingFailure || learning.workerRetirementUnconfirmed)) {
105
105
  recs.push(makeRecommendation({
106
106
  id: 'learning:flush',
107
- scope: 'user',
108
- title: 'Feed your captured work into the learner',
109
- rationale: 'Your AI captured this work, but none of it has reached the learner yet — so none of it has taught it anything.',
107
+ scope: learning?.scope === 'project' ? 'project' : 'user',
108
+ title: learning.workerRetirementUnconfirmed ? 'Learning recovery is paused' : 'Feed your captured work into the learner',
109
+ rationale: learning.workerRetirementUnconfirmed ? 'A previous learning worker could not be confirmed stopped. Its queue remains fenced and original observations are retained.' : 'Captured workflow observations remain pending until their canonical AgentDB rows are verified.',
110
110
  severity: 'IMPORTANT',
111
111
  touchesMachine: true,
112
112
  plainImpact:
113
- 'Sends events already captured on your own machine into the local learner so it improves from them. '
114
- + 'Nothing leaves your computer, and the queue is kept intact if the feed fails.',
115
- evidence: [{ observed: `${depth} captured events waiting, undelivered` }],
113
+ 'Stores already captured workflow observations in local project memory. '
114
+ + 'Canonical AgentDB readback verifies each observation; original queue bytes stay retained.',
115
+ evidence: [{ observed: `${depth} captured events waiting, undelivered${learning?.queueDir ? ` in ${learning.queueDir}` : ''}` }],
116
116
  cost: { time: 'under a minute', risk: 'low — local only; queue preserved on failure' },
117
117
  change: { human: 'drain the capture queue into the learner' },
118
118
  undo: { human: 'nothing to reverse — this only adds observations; learned state can be cleared separately' },
119
119
  }));
120
120
  }
121
121
 
122
+ if (learning?.enabled && learning.scope === 'project' && learning.legacyUserKnown && learning.legacyUserDepth > 0) {
123
+ recs.push(makeRecommendation({
124
+ id: 'learning:flush-legacy-user', scope: 'user', title: 'Process retained legacy user history',
125
+ rationale: 'Older captures belong to the home learner and are isolated from this project.',
126
+ severity: 'IMPORTANT', touchesMachine: true,
127
+ plainImpact: 'Minimizes old actions again and feeds only the original local home learner. Original queue bytes remain retained. Project learning is separate.',
128
+ evidence: [{ observed: `${learning.legacyUserDepth} legacy user events pending` }],
129
+ cost: { time: 'bounded rounds', risk: 'local observations; originals retained' },
130
+ change: { human: 'explicitly drain legacy user queue into home learner' },
131
+ undo: { human: 'original history retained; learned observations can be reset separately' },
132
+ }));
133
+ }
134
+
122
135
  // 3. A learner that has not trained in days. Installed-but-dormant is a DEFECT, not a neutral
123
136
  // state — the entire point of shipping a learning system is that it runs.
124
137
  const STALE_TRAIN_SECONDS = 60 * 60 * 24 * 2; // two days
125
- const age = Number(learning?.lastTrainSeconds);
126
- if (Number.isFinite(age) && age > STALE_TRAIN_SECONDS) {
138
+ const age = learning?.enabled === false ? 0 : Number(learning?.lastTrainSeconds);
139
+ if (learning?.statusKnown && ((Number.isFinite(age) && age > STALE_TRAIN_SECONDS) || (learning.observations > 0 && learning.lastTrainSeconds === null))) {
127
140
  recs.push(makeRecommendation({
128
141
  id: 'learning:train',
129
- scope: 'project',
142
+ scope: learning?.scope === 'user' ? 'user' : 'project',
130
143
  title: 'Your learner has gone quiet',
131
- rationale: 'It is installed and switched on, but it has not learned anything recently — so it is not getting smarter.',
144
+ rationale: 'Captured observations have no recent structural distillation receipt. Recorded observations and reusable patterns are separate.',
132
145
  severity: 'SUGGESTED',
133
146
  touchesMachine: true,
134
147
  plainImpact:
135
148
  'Runs one local training cycle so recent work becomes patterns it can reuse next time. '
136
- + 'Runs on your machine only and changes nothing about your projects.',
137
- evidence: [{ observed: `last trained ${(age / 86400).toFixed(1)} days ago (${learning?.trajectories ?? 0} trajectories recorded)` }],
138
- cost: { time: 'under a minute', risk: 'low — local, and the learned state is resettable' },
149
+ + 'Uses the same AgentDB, retains a checked local snapshot, and verifies pattern counts separately from recorded observations.',
150
+ evidence: [{ observed: `last distillation ${learning.lastTrainSeconds === null ? "not recorded" : (age / 86400).toFixed(1) + " days ago"} (${learning?.observations ?? learning?.trajectories ?? 0} observations recorded)${learning?.learningDb ? ` in ${learning.learningDb}` : ''}` }],
151
+ cost: { time: 'under a minute', risk: 'local structural patterns; snapshot retained; automatic exact restore unavailable' },
139
152
  change: { human: 'run one training cycle so captured work becomes reusable patterns' },
140
- undo: { human: 'the learned state can be reset, returning it to its pre-training condition' },
153
+ undo: { human: 'retain the WAL-safe snapshot for supervised offline recovery; automatic exact restore is unavailable' },
141
154
  }));
142
155
  }
143
156