ruvnet-brain 4.5.5 → 4.5.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/bin/install.mjs +24 -6
- package/config/model-router/claude-terminal-mod/.claude-plugin/plugin.json +1 -0
- package/config/model-router/claude-terminal-mod/README.md +22 -0
- package/config/model-router/claude-terminal-mod/hooks/hooks.json +1 -0
- package/config/model-router/claude-terminal-mod/hooks/policy.default.mjs +2 -0
- package/config/model-router/claude-terminal-mod/hooks/register.js +66 -0
- package/config/model-router/claude-terminal-mod/hooks/routing.js +55 -0
- package/config/model-router/claude-terminal-mod/hooks/runtime.js +2 -0
- package/config/model-router/claude-terminal-mod/tests/native.test.ts +86 -0
- package/config/model-router/policy.default.mjs +4 -1
- package/kb/forge-update.mjs +50 -9
- package/package.json +4 -3
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/commands/configure.md +13 -1
- package/plugin/hooks/hook-contracts.json +1 -1
- package/plugin/mcp/managed-cli-generation.mjs +91 -0
- package/plugin/mcp/managed-cli-interface.mjs +45 -16
- package/plugin/mcp/server.mjs +3 -2
- package/plugin/scripts/capability-claim-evidence.mjs +9 -2
- package/plugin/scripts/continuation-gate.mjs +3 -1
- package/plugin/scripts/continuity-journal.mjs +11 -11
- package/plugin/scripts/hook-shim.mjs +2 -1
- package/plugin/scripts/kb-copy-proof.mjs +4 -3
- package/plugin/scripts/nightly-scheduler.mjs +6 -2
- package/plugin/scripts/project-capture-queue.mjs +23 -8
- package/plugin/scripts/project-progression-hook.mjs +16 -7
- package/plugin/scripts/project-progression-outbox.mjs +116 -19
- package/plugin/scripts/project-progression-producer.mjs +12 -1
- package/plugin/scripts/project-progression-session-start.mjs +10 -0
- package/plugin/scripts/project-progression-store.mjs +53 -1
- package/plugin/scripts/project-progression-suspension.mjs +60 -0
- package/plugin/scripts/project-transition-hook.mjs +44 -16
- package/plugin/scripts/session-snapshot-hook.mjs +16 -8
- package/plugin/scripts/turn-outcome-capture.mjs +7 -3
- package/scripts/claude-terminal-mod.mjs +89 -0
- package/scripts/model-routing-gateway.mjs +47 -14
- package/scripts/model-routing-launchers.mjs +36 -1
- package/scripts/model-terminal-gateway.mjs +310 -0
- package/scripts/model-terminal-launchers.mjs +300 -0
- package/scripts/release-qualification-contract.mjs +37 -6
- package/scripts/release-qualification.mjs +9 -6
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Explicit per-user terminal integration. Native binaries and authentication remain user owned.
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import crypto from 'node:crypto';
|
|
7
|
+
import { spawn, execFileSync } from 'node:child_process';
|
|
8
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
9
|
+
import { classifyTerminalArguments, validateUpstreamSocket, terminalTempRoot } from './model-terminal-gateway.mjs';
|
|
10
|
+
import { nativeGatewayLaunch } from './model-routing-gateway.mjs';
|
|
11
|
+
import { subscriptionEnvironment, assertSubscriptionAuth } from './model-router-dispatch.mjs';
|
|
12
|
+
|
|
13
|
+
const SELF = fileURLToPath(import.meta.url);
|
|
14
|
+
const MARKER = '# Managed RuvNet Brain terminal launcher';
|
|
15
|
+
const START = '# >>> RuvNet Brain terminal routing >>>';
|
|
16
|
+
const END = '# <<< RuvNet Brain terminal routing <<<';
|
|
17
|
+
const quote = (v) => `'${String(v).replaceAll("'", "'\\''")}'`;
|
|
18
|
+
const digest = (v) => crypto.createHash('sha256').update(v).digest('hex');
|
|
19
|
+
|
|
20
|
+
function regular(file, { privateFile = false } = {}) {
|
|
21
|
+
if (!path.isAbsolute(file || '')) throw new Error('Absolute file path required');
|
|
22
|
+
const stat = fs.lstatSync(file);
|
|
23
|
+
const ownerAllowed = stat.uid === process.getuid?.() || (!privateFile && stat.uid === 0);
|
|
24
|
+
if (!stat.isFile() || stat.isSymbolicLink() || !ownerAllowed ||
|
|
25
|
+
(privateFile && (stat.mode & 0o077))) throw new Error(`Owned regular file required: ${file}`);
|
|
26
|
+
return fs.realpathSync(file);
|
|
27
|
+
}
|
|
28
|
+
function executable(file) {
|
|
29
|
+
const result = regular(fs.realpathSync(file)); fs.accessSync(result, fs.constants.X_OK); return result;
|
|
30
|
+
}
|
|
31
|
+
function atomic(file, content, mode = 0o600) {
|
|
32
|
+
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
33
|
+
if (fs.existsSync(file)) regular(file);
|
|
34
|
+
const temp = `${file}.${crypto.randomUUID()}.tmp`;
|
|
35
|
+
try {
|
|
36
|
+
const fd = fs.openSync(temp, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_NOFOLLOW, mode);
|
|
37
|
+
try { fs.writeFileSync(fd, content); fs.fchmodSync(fd, mode); fs.fsyncSync(fd); } finally { fs.closeSync(fd); }
|
|
38
|
+
fs.renameSync(temp, file);
|
|
39
|
+
} finally { fs.rmSync(temp, { force: true }); }
|
|
40
|
+
}
|
|
41
|
+
function backup(file) {
|
|
42
|
+
if (!fs.existsSync(file)) return null;
|
|
43
|
+
const bytes = fs.readFileSync(regular(file)); const target = `${file}.${digest(bytes)}.original`;
|
|
44
|
+
if (fs.existsSync(target)) {
|
|
45
|
+
if (digest(fs.readFileSync(regular(target))) !== digest(bytes)) throw new Error('Backup digest mismatch');
|
|
46
|
+
} else fs.writeFileSync(target, bytes, { flag: 'wx', mode: 0o400 });
|
|
47
|
+
return target;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Follow only the native daemon's known user-owned locator, then validate the actual private endpoint. */
|
|
51
|
+
export function resolveTerminalUpstream({ codexHome = path.join(os.homedir(), '.codex'), uid = process.getuid?.(),
|
|
52
|
+
daemonRoot = terminalTempRoot() } = {}) {
|
|
53
|
+
if (!path.isAbsolute(codexHome) || !path.isAbsolute(daemonRoot) || uid == null) throw new Error('Absolute owned socket locator required');
|
|
54
|
+
const home = fs.realpathSync(codexHome);
|
|
55
|
+
const directory = path.join(home, 'app-server-control');
|
|
56
|
+
const d = fs.lstatSync(directory);
|
|
57
|
+
if (!d.isDirectory() || d.isSymbolicLink() || d.uid !== uid || (d.mode & 0o022)) throw new Error('Native socket locator directory is not owned and protected');
|
|
58
|
+
const locator = path.join(directory, 'app-server-control.sock');
|
|
59
|
+
const s = fs.lstatSync(locator);
|
|
60
|
+
if (!s.isSymbolicLink()) return validateUpstreamSocket(locator, { uid });
|
|
61
|
+
if (s.uid !== uid) throw new Error('Native socket locator is foreign');
|
|
62
|
+
const actual = fs.realpathSync(locator);
|
|
63
|
+
const expected = path.join(fs.realpathSync(daemonRoot), `codex-daemon-${uid}`);
|
|
64
|
+
if (path.dirname(actual) !== expected || !/^[a-f0-9]{64}$/.test(path.basename(actual))) throw new Error('Native socket locator escaped the known daemon endpoint');
|
|
65
|
+
return validateUpstreamSocket(actual, { uid });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function unmanagedShellNames(text) {
|
|
69
|
+
return ['codex', 'claude'].filter((name) => new RegExp(`(?:^|\\n)\\s*(?:alias\\s+${name}\\s*=|(?:function\\s+)?${name}\\s*\\(\\s*\\)|function\\s+${name}\\s*(?:\\{|\\n))`).test(text));
|
|
70
|
+
}
|
|
71
|
+
export function terminalShellPlan(text, sourcePath, launchers) {
|
|
72
|
+
const starts = text.split(START).length - 1, ends = text.split(END).length - 1;
|
|
73
|
+
if (starts !== ends || starts > 1) throw new Error('Malformed managed terminal shell block');
|
|
74
|
+
const block = starts ? text.slice(text.indexOf(START), text.indexOf(END) + END.length) : null;
|
|
75
|
+
const outside = block ? text.replace(block, '') : text;
|
|
76
|
+
const conflicts = unmanagedShellNames(outside);
|
|
77
|
+
const lines = [MARKER];
|
|
78
|
+
for (const name of ['codex', 'claude']) {
|
|
79
|
+
if (launchers[name] && !conflicts.includes(name)) lines.push(`function ${name}() { ${quote(launchers[name])} "$@"; }`);
|
|
80
|
+
}
|
|
81
|
+
const managed = `${START}\nsource ${quote(sourcePath)}\n${END}`;
|
|
82
|
+
return { conflicts, source: lines.join('\n') + '\n', zshrc: block ? text.replace(block, managed) : `${text}${text && !text.endsWith('\n') ? '\n' : ''}${managed}\n` };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Runtime is supplied by the snapshot installer; this module does not rebuild or mutate it. */
|
|
86
|
+
export function installTerminalLaunchers({ home = os.homedir(), nodeBinary = process.execPath, runtimeRoot,
|
|
87
|
+
runtimeDigest, realCodex = path.join(home, '.local/bin/codex'), realClaude, apply = false, manageZsh = true } = {}) {
|
|
88
|
+
if (!['darwin', 'linux'].includes(process.platform)) throw new Error('Terminal launchers require macOS or Linux');
|
|
89
|
+
if (!path.isAbsolute(home) || !path.isAbsolute(runtimeRoot || '') || !/^[a-f0-9]{64}$/.test(runtimeDigest || '')) throw new Error('Absolute home and identified runtime required');
|
|
90
|
+
const runner = regular(path.join(runtimeRoot, 'scripts/model-terminal-launchers.mjs'));
|
|
91
|
+
const config = { schemaVersion: 1, managedBy: 'ruvnet-brain-terminal-launchers', runtimeDigest, runtimeRoot: fs.realpathSync(runtimeRoot), nodeBinary: executable(nodeBinary),
|
|
92
|
+
runner, realCodex: executable(realCodex), gatewayPath: regular(path.join(runtimeRoot, 'scripts/model-terminal-gateway.mjs')) };
|
|
93
|
+
if (realClaude) {
|
|
94
|
+
config.realClaude = executable(realClaude);
|
|
95
|
+
config.claudeHelperPath = regular(path.join(runtimeRoot, 'scripts/claude-terminal-mod.mjs'));
|
|
96
|
+
config.enginePath = regular(path.join(runtimeRoot, 'scripts/model-router-engine.mjs'));
|
|
97
|
+
}
|
|
98
|
+
const configPath = path.join(home, '.cache/ruvnet-brain/model-routing/terminal-launcher-config.json');
|
|
99
|
+
if (fs.existsSync(configPath) && JSON.parse(fs.readFileSync(regular(configPath), 'utf8')).managedBy !== config.managedBy) throw new Error('Preserving unmanaged terminal configuration');
|
|
100
|
+
const launchers = { codex: path.join(home, '.local/bin/ruvnet-brain-codex-terminal') };
|
|
101
|
+
if (realClaude) launchers.claude = path.join(home, '.local/bin/ruvnet-brain-claude-terminal');
|
|
102
|
+
const contents = Object.fromEntries(Object.entries(launchers).map(([host, file]) => [file,
|
|
103
|
+
`#!/bin/sh\n${MARKER}\nexec ${quote(config.nodeBinary)} ${quote(runner)} --launch ${quote(host)} --config ${quote(configPath)} -- "$@"\n`]));
|
|
104
|
+
const shellSource = path.join(home, '.config/ruvnet-brain/terminal-routing.zsh');
|
|
105
|
+
const zshrc = path.join(home, '.zshrc');
|
|
106
|
+
const shell = terminalShellPlan(fs.existsSync(zshrc) ? fs.readFileSync(regular(zshrc), 'utf8') : '', shellSource, launchers);
|
|
107
|
+
for (const file of [...Object.keys(contents), shellSource]) {
|
|
108
|
+
if (fs.existsSync(file) && !fs.readFileSync(regular(file), 'utf8').includes(MARKER)) throw new Error(`Preserving unmanaged terminal integration: ${file}`);
|
|
109
|
+
}
|
|
110
|
+
for (const binary of [config.realCodex, config.realClaude].filter(Boolean)) {
|
|
111
|
+
if ([runner, ...Object.keys(contents)].includes(binary)) throw new Error('Native terminal launcher recursion refused');
|
|
112
|
+
}
|
|
113
|
+
const receipt = { apply, launchers, configPath, config, shellSource, shellConflicts: shell.conflicts, backups: [],
|
|
114
|
+
claudeEnforcementScope: realClaude ? 'Startup hook activation guard; continuing native worker enforcement requires separate proof' : null };
|
|
115
|
+
if (!apply) return receipt;
|
|
116
|
+
for (const file of [configPath, ...Object.keys(contents), ...(manageZsh ? [shellSource, zshrc] : [])]) {
|
|
117
|
+
const original = backup(file); if (original) receipt.backups.push(original);
|
|
118
|
+
}
|
|
119
|
+
atomic(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
120
|
+
for (const [file, content] of Object.entries(contents)) atomic(file, content, 0o755);
|
|
121
|
+
if (manageZsh) { atomic(shellSource, shell.source); atomic(zshrc, shell.zshrc, fs.existsSync(zshrc) ? fs.statSync(zshrc).mode & 0o777 : 0o600); }
|
|
122
|
+
return receipt;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Supported native start is idempotent; never stop, restart, update or own the shared daemon. */
|
|
126
|
+
export function ensureTerminalDaemon(binary, env = process.env, { exec = execFileSync } = {}) {
|
|
127
|
+
try {
|
|
128
|
+
exec(binary, ['app-server', 'daemon', 'start'], { env: subscriptionEnvironment(env), timeout: 10000,
|
|
129
|
+
stdio: 'ignore', shell: false, maxBuffer: 65536 });
|
|
130
|
+
} catch { throw new Error('Native Codex daemon start unavailable; terminal launch blocked'); }
|
|
131
|
+
}
|
|
132
|
+
export function terminalInvocation({ host, args = [], config, env = process.env, daemonExec = execFileSync }) {
|
|
133
|
+
if (config.schemaVersion !== 1 || env.RNB_TERMINAL_LAUNCH_ACTIVE) throw new Error('Invalid or recursive terminal launch');
|
|
134
|
+
const binary = executable(host === 'codex' ? config.realCodex : config.realClaude);
|
|
135
|
+
if (host !== 'codex') throw new Error('Use guarded native Claude launch');
|
|
136
|
+
const mode = classifyTerminalArguments(args);
|
|
137
|
+
if (mode === 'admin') return { command: binary, args: [...args], routed: false };
|
|
138
|
+
// Validate provider/transport overrides before native daemon startup has any side effect.
|
|
139
|
+
nativeGatewayLaunch({ harness: 'codex', realBinary: binary, args: ['app-server', ...args], env: subscriptionEnvironment(env) });
|
|
140
|
+
ensureTerminalDaemon(binary, env, { exec: daemonExec });
|
|
141
|
+
const upstream = resolveTerminalUpstream({ codexHome: env.CODEX_HOME || path.join(os.homedir(), '.codex') });
|
|
142
|
+
return { command: executable(config.nodeBinary), args: [regular(config.gatewayPath), '--real-binary', binary, '--upstream-socket', upstream, '--', ...args], routed: true };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function verifyNativeWorkerAncestry(workerPid, nativePid, { uid = process.getuid?.() } = {}) {
|
|
146
|
+
if (![workerPid, nativePid, uid].every(Number.isSafeInteger) || workerPid <= 0 || nativePid <= 0) return false;
|
|
147
|
+
let current = workerPid;
|
|
148
|
+
for (let depth = 0; depth < 12; depth++) {
|
|
149
|
+
let result;
|
|
150
|
+
try { result = execFileSync('/bin/ps', ['-o', 'uid=,ppid=', '-p', String(current)], { encoding: 'utf8', timeout: 1000, stdio: ['ignore', 'pipe', 'ignore'] }).trim(); } catch { return false; }
|
|
151
|
+
const match = /^(\d+)\s+(\d+)$/.exec(result);
|
|
152
|
+
if (!match || Number(match[1]) !== uid) return false;
|
|
153
|
+
if (current === nativePid) return true;
|
|
154
|
+
const parent = Number(match[2]); if (parent <= 1 || parent === current) return false;
|
|
155
|
+
current = parent;
|
|
156
|
+
}
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
export function validateClaudeReadiness(file, { nonce, pluginDir, modDigest, pid, startedAt, now = Date.now() }) {
|
|
160
|
+
const receipt = JSON.parse(fs.readFileSync(regular(file, { privateFile: true }), 'utf8'));
|
|
161
|
+
const observed = Date.parse(receipt.observedAt);
|
|
162
|
+
if (receipt.schemaVersion !== 1 || receipt.nonce !== nonce || receipt.pluginRoot !== fs.realpathSync(pluginDir) ||
|
|
163
|
+
receipt.modDigest !== modDigest || !verifyNativeWorkerAncestry(receipt.pid, pid) || receipt.status !== 'ready' ||
|
|
164
|
+
!/^2\.1\.\d+$/.test(receipt.nativeVersion || '') || Number(receipt.nativeVersion.split('.')[2]) < 287 ||
|
|
165
|
+
typeof receipt.sessionId !== 'string' || !receipt.sessionId || !Number.isFinite(observed) ||
|
|
166
|
+
observed < startedAt || observed > now || now - observed > 10000) throw new Error('Native Claude hook activation receipt invalid');
|
|
167
|
+
return receipt;
|
|
168
|
+
}
|
|
169
|
+
function exitOf(child) {
|
|
170
|
+
return new Promise((resolve, reject) => { child.once('error', reject); child.once('exit', (code, signal) => resolve({ code, signal })); });
|
|
171
|
+
}
|
|
172
|
+
function forwardSignals(child, source) {
|
|
173
|
+
const handlers = new Map(['SIGINT', 'SIGTERM', 'SIGHUP'].map((signal) => [signal, () => child.kill(signal)]));
|
|
174
|
+
handlers.forEach((handler, signal) => source.on(signal, handler));
|
|
175
|
+
return () => handlers.forEach((handler, signal) => source.removeListener(signal, handler));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function validateClaudeTerminalSettings({ env = process.env, cwd = process.cwd(), home = os.homedir() } = {}) {
|
|
179
|
+
for (const key of ['CLAUDE_CODE_SAFE_MODE', 'CLAUDE_CODE_SIMPLE', 'CLAUDE_CODE_DISABLE_HOOKS', 'DISABLE_CLAUDE_CODE_MODS']) {
|
|
180
|
+
if (env[key] && !['0', 'false'].includes(env[key])) throw new Error('Claude environment disables native routing hooks');
|
|
181
|
+
}
|
|
182
|
+
const files = new Set([path.join(env.CLAUDE_CONFIG_DIR || path.join(home, '.claude'), 'settings.json'),
|
|
183
|
+
process.platform === 'darwin' ? '/Library/Application Support/ClaudeCode/managed-settings.json' : '/etc/claude-code/managed-settings.json']);
|
|
184
|
+
let directory = path.resolve(cwd);
|
|
185
|
+
while (true) {
|
|
186
|
+
for (const name of ['settings.json', 'settings.local.json']) files.add(path.join(directory, '.claude', name));
|
|
187
|
+
if (directory === path.dirname(directory)) break;
|
|
188
|
+
directory = path.dirname(directory);
|
|
189
|
+
}
|
|
190
|
+
const billing = /^(?:OPENAI_API_KEY|OPENAI_BASE_URL|ANTHROPIC_API_KEY|ANTHROPIC_AUTH_TOKEN|ANTHROPIC_BASE_URL|ANTHROPIC_CUSTOM_HEADERS|CLAUDE_CODE_USE_BEDROCK|CLAUDE_CODE_USE_VERTEX|CLAUDE_CODE_USE_FOUNDRY|CODEX_API_KEY|CLAUDE_CODE_SAFE_MODE|CLAUDE_CODE_SIMPLE|CLAUDE_CODE_DISABLE_HOOKS|DISABLE_CLAUDE_CODE_MODS)$/;
|
|
191
|
+
const unsafe = (value) => value && typeof value === 'object' && Object.entries(value).some(([key, entry]) => {
|
|
192
|
+
if (key === 'env') return !entry || typeof entry !== 'object' || Array.isArray(entry) || Object.keys(entry).some((name) => billing.test(name));
|
|
193
|
+
// The user's native modelSettings are ordinary model-keyed effort settings; the mod selects each turn.
|
|
194
|
+
if (key === 'modelSettings') return !entry || typeof entry !== 'object' || Array.isArray(entry) || Object.entries(entry).some(([model, settings]) =>
|
|
195
|
+
!/^claude-[a-z0-9][a-z0-9.-]*$/.test(model) || !settings || typeof settings !== 'object' || Array.isArray(settings) ||
|
|
196
|
+
Object.entries(settings).some(([name, level]) => name !== 'effortLevel' || !['low', 'medium', 'high', 'xhigh', 'max'].includes(level)));
|
|
197
|
+
return /^(?:apiKeyHelper|api_?key|base_?url|auth_?token|customHeaders|modelProvider|model_providers|alwaysThinkingEnabled|maxEffortLevel)$/i.test(key) ||
|
|
198
|
+
key === 'fastMode' && entry !== false || unsafe(entry);
|
|
199
|
+
});
|
|
200
|
+
for (const file of files) {
|
|
201
|
+
if (!fs.existsSync(file)) continue;
|
|
202
|
+
const stat = fs.lstatSync(file);
|
|
203
|
+
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error('Claude settings must be regular files');
|
|
204
|
+
let settings;
|
|
205
|
+
try { settings = JSON.parse(fs.readFileSync(file, 'utf8')); } catch { throw new Error('Claude settings unavailable or malformed'); }
|
|
206
|
+
if (!settings || typeof settings !== 'object' || Array.isArray(settings) || unsafe(settings)) throw new Error('Claude settings conflict with native subscription routing');
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
export function validateClaudeTerminalArguments(args) {
|
|
210
|
+
const flags = new Set(['--continue', '-c', '--resume', '-r', '--fork-session', '--no-session-persistence',
|
|
211
|
+
'--verbose', '--dangerously-skip-permissions', '--allow-dangerously-skip-permissions']);
|
|
212
|
+
const values = new Set(['--permission-mode', '--session-id', '--add-dir']);
|
|
213
|
+
const commands = new Set(['agents', 'remote-control', 'setup-token', 'serve', 'web', 'bridge', 'completion',
|
|
214
|
+
'attach', 'auto-mode', 'gateway', 'import', 'logs', 'purge', 'respawn', 'rm', 'stop', 'kill', 'ultrareview']);
|
|
215
|
+
const options = [], prompts = [];
|
|
216
|
+
for (let index = 0; index < args.length; index++) {
|
|
217
|
+
const arg = args[index];
|
|
218
|
+
if (arg === '--') { prompts.push(...args.slice(index + 1)); break; }
|
|
219
|
+
if (values.has(arg)) { if (!args[++index] || args[index].startsWith('-')) throw new Error('Claude terminal flag requires a value'); options.push(arg, args[index]); continue; }
|
|
220
|
+
if (arg.startsWith('-') && !flags.has(arg) || commands.has(arg)) throw new Error('Claude terminal arguments conflict with guarded native routing');
|
|
221
|
+
if (flags.has(arg)) {
|
|
222
|
+
options.push(arg);
|
|
223
|
+
if (['--resume', '-r'].includes(arg) && args[index + 1] && !args[index + 1].startsWith('-')) options.push(args[++index]);
|
|
224
|
+
} else prompts.push(arg);
|
|
225
|
+
}
|
|
226
|
+
return { options, prompts };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Native plugin startup only: an unloaded/crashed native worker may skip subsequent hooks. */
|
|
230
|
+
export async function runClaudeTerminal({ config, args = [], env = process.env, startupMs = 10000,
|
|
231
|
+
tempRoot = os.tmpdir(), cwd = process.cwd(), signalSource = process, diagnostics = process.stderr } = {}) {
|
|
232
|
+
if (env.RNB_TERMINAL_LAUNCH_ACTIVE) throw new Error('Native terminal launcher recursion refused');
|
|
233
|
+
if (!Number.isFinite(startupMs) || startupMs <= 0 || startupMs > 60000) throw new Error('Bounded native hook startup deadline required');
|
|
234
|
+
const binary = executable(config.realClaude), clean = subscriptionEnvironment(env);
|
|
235
|
+
const information = new Set(['--help', '-h', '--version', '-v']);
|
|
236
|
+
const ownerPermissionFlags = new Set(['--dangerously-skip-permissions', '--allow-dangerously-skip-permissions']);
|
|
237
|
+
const administrative = args.some((arg) => information.has(arg)) &&
|
|
238
|
+
args.every((arg) => information.has(arg) || ownerPermissionFlags.has(arg)) ||
|
|
239
|
+
['auth', 'mcp', 'plugin', 'plugins', 'update', 'upgrade', 'doctor', 'install'].includes(args[0]);
|
|
240
|
+
if (administrative) {
|
|
241
|
+
const child = spawn(binary, args, { env: clean, cwd, stdio: 'inherit', shell: false });
|
|
242
|
+
const unforward = forwardSignals(child, signalSource);
|
|
243
|
+
try { return await exitOf(child); } finally { unforward(); }
|
|
244
|
+
}
|
|
245
|
+
const parsed = validateClaudeTerminalArguments(args);
|
|
246
|
+
validateClaudeTerminalSettings({ env: clean, cwd });
|
|
247
|
+
assertSubscriptionAuth('claude-code', { env: clean, probe: (_name, probeArgs, options) => execFileSync(binary, probeArgs, { ...options, cwd }) });
|
|
248
|
+
const { prepareClaudeTerminalMod } = await import(pathToFileURL(regular(config.claudeHelperPath)).href);
|
|
249
|
+
const directory = fs.mkdtempSync(path.join(fs.realpathSync(tempRoot), 'rnb-claude-')); fs.chmodSync(directory, 0o700);
|
|
250
|
+
let child, unforward = () => {}, timer, killer;
|
|
251
|
+
try {
|
|
252
|
+
const prepared = prepareClaudeTerminalMod({ destination: path.join(directory, 'plugin'), nodePath: executable(config.nodeBinary),
|
|
253
|
+
helperPath: regular(config.claudeHelperPath), enginePath: regular(config.enginePath) });
|
|
254
|
+
const receiptPath = path.join(directory, 'ready.json'), nonce = crypto.randomBytes(32).toString('hex'), startedAt = Date.now();
|
|
255
|
+
Object.assign(clean, { RNB_TERMINAL_LAUNCH_ACTIVE: '1', RNB_CLAUDE_MOD_NONCE: nonce, RNB_CLAUDE_MOD_RECEIPT: receiptPath });
|
|
256
|
+
child = spawn(binary, [...parsed.options, '--plugin-dir', prepared.pluginDir, '--', ...parsed.prompts], { env: clean, cwd, stdio: 'inherit', shell: false });
|
|
257
|
+
const exited = exitOf(child); unforward = forwardSignals(child, signalSource);
|
|
258
|
+
let failure, readiness;
|
|
259
|
+
const guard = new Promise((resolve) => {
|
|
260
|
+
const check = () => {
|
|
261
|
+
try {
|
|
262
|
+
if (fs.existsSync(receiptPath)) { readiness = validateClaudeReadiness(receiptPath, { nonce, ...prepared, pid: child.pid, startedAt }); resolve(); return; }
|
|
263
|
+
if (Date.now() - startedAt >= startupMs) throw new Error('Native Claude hook activation was not acknowledged');
|
|
264
|
+
timer = setTimeout(check, 25);
|
|
265
|
+
} catch (error) { failure = error; resolve(); }
|
|
266
|
+
}; check();
|
|
267
|
+
});
|
|
268
|
+
const early = await Promise.race([guard.then(() => null), exited]);
|
|
269
|
+
if (early && !readiness) throw new Error('Native Claude exited before hook activation');
|
|
270
|
+
if (failure) {
|
|
271
|
+
child.kill('SIGTERM'); killer = setTimeout(() => child.kill('SIGKILL'), 1000);
|
|
272
|
+
await exited; throw failure;
|
|
273
|
+
}
|
|
274
|
+
diagnostics.write('[native-terminal-routing] Claude startup hooks acknowledged; continuing worker enforcement is not established by this receipt.\n');
|
|
275
|
+
return { ...(early || await exited), readinessReceipt: readiness };
|
|
276
|
+
} finally {
|
|
277
|
+
clearTimeout(timer); clearTimeout(killer); unforward();
|
|
278
|
+
if (child && child.exitCode == null && child.signalCode == null) child.kill('SIGTERM');
|
|
279
|
+
fs.rmSync(directory, { recursive: true, force: true });
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export async function runTerminalLauncher({ host, args, config, env = process.env, signalSource = process } = {}) {
|
|
284
|
+
if (host === 'claude') return runClaudeTerminal({ config, args, env, signalSource });
|
|
285
|
+
if (host !== 'codex') throw new Error('Unsupported terminal host');
|
|
286
|
+
const invocation = terminalInvocation({ host, args, config, env });
|
|
287
|
+
const child = spawn(invocation.command, invocation.args, { env: { ...subscriptionEnvironment(env), RNB_TERMINAL_LAUNCH_ACTIVE: '1' }, stdio: 'inherit', shell: false });
|
|
288
|
+
const unforward = forwardSignals(child, signalSource);
|
|
289
|
+
try { return await exitOf(child); } finally { unforward(); }
|
|
290
|
+
}
|
|
291
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === SELF) {
|
|
292
|
+
const argv = process.argv.slice(2);
|
|
293
|
+
Promise.resolve().then(() => {
|
|
294
|
+
if (argv[0] !== '--launch' || argv[2] !== '--config' || argv[4] !== '--') throw new Error('Explicit host/config terminal launcher required');
|
|
295
|
+
const config = JSON.parse(fs.readFileSync(regular(argv[3], { privateFile: true }), 'utf8'));
|
|
296
|
+
return runTerminalLauncher({ host: argv[1], args: argv.slice(5), config });
|
|
297
|
+
}).then(({ code, signal }) => {
|
|
298
|
+
if (signal) process.kill(process.pid, signal); else process.exitCode = code ?? 1;
|
|
299
|
+
}).catch((error) => { process.stderr.write(`[native-terminal-routing] ${error.message}\n`); process.exitCode = 1; });
|
|
300
|
+
}
|
|
@@ -44,7 +44,9 @@ export const RELEASE_REQUIREMENTS = Object.freeze({
|
|
|
44
44
|
"files": [
|
|
45
45
|
"tests/unit/automatic-hook-retirement.test.mjs",
|
|
46
46
|
"tests/unit/install-activation-rollback.test.mjs",
|
|
47
|
-
"tests/unit/forge-update-apply-rollback.test.mjs"
|
|
47
|
+
"tests/unit/forge-update-apply-rollback.test.mjs",
|
|
48
|
+
"tests/unit/forge-update-archive-digest.test.mjs",
|
|
49
|
+
"tests/unit/kb-copy-proof-legacy-sidecars.test.mjs"
|
|
48
50
|
]
|
|
49
51
|
},
|
|
50
52
|
{
|
|
@@ -62,8 +64,15 @@ export const RELEASE_REQUIREMENTS = Object.freeze({
|
|
|
62
64
|
"reason": "Codex and Claude native JSONL preserve context, UTF-8, control progress, cancellation and deferred FIFO while binding each new turn to an approved native route",
|
|
63
65
|
"files": [
|
|
64
66
|
"tests/unit/model-routing-gateway.test.mjs",
|
|
67
|
+
"tests/unit/model-routing-gateway-boundaries.test.mjs",
|
|
68
|
+
"tests/unit/claude-terminal-mod.test.mjs",
|
|
65
69
|
"tests/unit/model-routing-launchers.test.mjs"
|
|
66
|
-
]
|
|
70
|
+
],
|
|
71
|
+
"platformFiles": {
|
|
72
|
+
"linux": ["tests/unit/model-terminal-gateway.test.mjs", "tests/unit/model-terminal-launchers.test.mjs"],
|
|
73
|
+
"macos": ["tests/unit/model-terminal-gateway.test.mjs", "tests/unit/model-terminal-launchers.test.mjs"],
|
|
74
|
+
"windows": ["tests/unit/windows-terminal-boundary.test.mjs"]
|
|
75
|
+
}
|
|
67
76
|
},
|
|
68
77
|
{
|
|
69
78
|
"id": "weekly-routing-evidence",
|
|
@@ -86,7 +95,8 @@ export const RELEASE_REQUIREMENTS = Object.freeze({
|
|
|
86
95
|
"files": [
|
|
87
96
|
"tests/unit/nightly-scheduler.test.mjs",
|
|
88
97
|
"tests/unit/nightly-refresh-launcher.test.mjs",
|
|
89
|
-
"tests/unit/nightly-two-run-proof.test.mjs"
|
|
98
|
+
"tests/unit/nightly-two-run-proof.test.mjs",
|
|
99
|
+
"tests/unit/nightly-refresh-run-health.test.mjs"
|
|
90
100
|
]
|
|
91
101
|
},
|
|
92
102
|
{
|
|
@@ -144,12 +154,31 @@ export const RELEASE_REQUIREMENTS = Object.freeze({
|
|
|
144
154
|
"tests/mutation/citation-binding-mutation.test.mjs"
|
|
145
155
|
]
|
|
146
156
|
},
|
|
157
|
+
{
|
|
158
|
+
"id": "owner-capture-and-off-controls",
|
|
159
|
+
"reason": "Brain OFF suppresses capacity execution; promise opt-out suppresses only new capture and preserves existing closure and capability truth",
|
|
160
|
+
"files": [
|
|
161
|
+
"tests/unit/capacity-aware-parallel-work.test.mjs",
|
|
162
|
+
"tests/unit/continuation-gate-capability-truth.test.mjs",
|
|
163
|
+
"tests/unit/continuation-gate-completion-claims.test.mjs"
|
|
164
|
+
]
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
"id": "active-managed-generation",
|
|
168
|
+
"reason": "Active code selection binds help authorization and execution to one immutable leased generation across promotion",
|
|
169
|
+
"files": [
|
|
170
|
+
"tests/unit/managed-cli-generation.test.mjs"
|
|
171
|
+
]
|
|
172
|
+
},
|
|
147
173
|
{
|
|
148
174
|
"id": "pending-memory-durability",
|
|
149
175
|
"reason": "Complete outbox tails replay and accepted pending journal events survive capacity pressure without silent deletion",
|
|
150
176
|
"files": [
|
|
151
177
|
"tests/unit/project-progression-outbox.test.mjs",
|
|
152
|
-
"tests/unit/project-progression-durability.test.mjs"
|
|
178
|
+
"tests/unit/project-progression-durability.test.mjs",
|
|
179
|
+
"tests/unit/continuity-customer-regressions.test.mjs",
|
|
180
|
+
"tests/unit/progression-outbox-containment.test.mjs",
|
|
181
|
+
"tests/unit/transition-pending-notices.test.mjs"
|
|
153
182
|
]
|
|
154
183
|
}
|
|
155
184
|
],
|
|
@@ -158,7 +187,8 @@ export const RELEASE_REQUIREMENTS = Object.freeze({
|
|
|
158
187
|
"id": "native-explicit-interface",
|
|
159
188
|
"reason": "Real MCP subprocess readiness, command policy and literal argv safety",
|
|
160
189
|
"files": [
|
|
161
|
-
"tests/integration/managed-cli-mcp.test.mjs"
|
|
190
|
+
"tests/integration/managed-cli-mcp.test.mjs",
|
|
191
|
+
"tests/integration/managed-cli-server-boundary.test.mjs"
|
|
162
192
|
]
|
|
163
193
|
},
|
|
164
194
|
{
|
|
@@ -211,7 +241,8 @@ export const RELEASE_REQUIREMENTS = Object.freeze({
|
|
|
211
241
|
"tests/unit/turn-durable-transport.test.mjs",
|
|
212
242
|
"tests/integration/automatic-progression-continuation.test.mjs",
|
|
213
243
|
"tests/integration/capture-consent-boundary.test.mjs",
|
|
214
|
-
"tests/integration/session-start-turn-replay.test.mjs"
|
|
244
|
+
"tests/integration/session-start-turn-replay.test.mjs",
|
|
245
|
+
"tests/integration/progression-suspension.test.mjs"
|
|
215
246
|
]
|
|
216
247
|
},
|
|
217
248
|
{
|
|
@@ -11,13 +11,16 @@ import { RELEASE_REQUIREMENTS } from './release-qualification-contract.mjs';
|
|
|
11
11
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
12
12
|
const PLATFORM = { linux: 'linux', darwin: 'macos', win32: 'windows' }[process.platform];
|
|
13
13
|
|
|
14
|
-
export function qualificationPlan(suite = 'source') {
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
export function qualificationPlan(suite = 'source', platform = PLATFORM) {
|
|
15
|
+
if (!['linux', 'macos', 'windows'].includes(platform)) throw new Error('invalid qualification platform');
|
|
16
|
+
const inventory = RELEASE_REQUIREMENTS[suite];
|
|
17
|
+
if (!inventory?.length) throw new Error('unknown qualification suite');
|
|
18
|
+
const requirements = inventory.map(({ platformFiles, ...entry }) => ({ ...entry,
|
|
19
|
+
files: [...entry.files, ...(platformFiles?.[platform] || [])] }));
|
|
17
20
|
const files = requirements.flatMap((entry) => entry.files);
|
|
18
21
|
if (new Set(files).size !== files.length || files.some((file) => !file.startsWith('tests/')
|
|
19
22
|
|| !file.endsWith('.test.mjs') || file.includes('..'))) throw new Error('invalid qualification inventory');
|
|
20
|
-
return { requirements, files, contractSha256: digest(requirements) };
|
|
23
|
+
return { requirements, files, contractSha256: digest({ platform, requirements }) };
|
|
21
24
|
}
|
|
22
25
|
|
|
23
26
|
export function assessTestReport(report, files, root = ROOT, platform = PLATFORM) {
|
|
@@ -44,7 +47,7 @@ export function assessTestReport(report, files, root = ROOT, platform = PLATFORM
|
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
export function validateQualificationReceipt(report, { suite, platform, sourceSha }) {
|
|
47
|
-
const plan = qualificationPlan(suite);
|
|
50
|
+
const plan = qualificationPlan(suite, platform);
|
|
48
51
|
const { receiptSha256, ...payload } = report || {};
|
|
49
52
|
if (report?.schemaVersion !== 1 || report.kind !== 'ruvnet-brain-release-qualification'
|
|
50
53
|
|| report.suite !== suite || report.platform !== platform || report.status !== 'PASS'
|
|
@@ -70,7 +73,7 @@ export function validateQualificationReceipt(report, { suite, platform, sourceSh
|
|
|
70
73
|
export function runQualification({ suite = 'source', platform = PLATFORM, report: output, root = ROOT } = {}) {
|
|
71
74
|
if (platform !== PLATFORM) throw new Error(`cannot produce ${platform} evidence on ${PLATFORM}`);
|
|
72
75
|
if (!output || fs.existsSync(output)) throw new Error('a new --report path is required');
|
|
73
|
-
const plan = qualificationPlan(suite);
|
|
76
|
+
const plan = qualificationPlan(suite, platform);
|
|
74
77
|
for (const file of plan.files) if (!fs.statSync(path.resolve(root, file)).isFile()) throw new Error(`missing test ${file}`);
|
|
75
78
|
const source = sourceIdentity(root);
|
|
76
79
|
const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'release-qualification-'));
|