ruvnet-brain 4.5.11 → 4.5.13
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 +2 -2
- package/bin/install.mjs +15 -10
- package/config/model-router/policy.default.mjs +3 -3
- package/package.json +4 -2
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/scripts/codex-console-alias.mjs +88 -0
- package/plugin/scripts/session-start-budget.mjs +1 -1
- package/plugin/scripts/session-start-core.mjs +10 -2
- package/scripts/architecture-review-lock.mjs +44 -0
- package/scripts/candidate-ci-receipt.mjs +60 -0
- package/scripts/claude-controlled-terminal.mjs +68 -23
- package/scripts/codex-managed-terminal.mjs +113 -0
- package/scripts/grok-subscription-host.mjs +348 -0
- package/scripts/managed-terminal-input.mjs +152 -0
- package/scripts/model-managed-prompt.mjs +289 -0
- package/scripts/model-managed-workflow-service.mjs +327 -0
- package/scripts/model-routing-controller.mjs +368 -0
- package/scripts/model-routing-defence.mjs +44 -0
- package/scripts/model-routing-execution-adapters.mjs +318 -0
- package/scripts/model-routing-launchers.mjs +31 -0
- package/scripts/model-terminal-launchers.mjs +58 -14
- package/scripts/model-weekly-analyst.mjs +64 -28
- package/scripts/model-weekly-qualification.mjs +6 -1
- package/scripts/prepublication-evidence.mjs +7 -2
- package/scripts/qe/agentic-qe-4.3.mjs +12 -2
- package/scripts/release-qualification-contract.mjs +32 -3
- package/scripts/release-transaction-provider.mjs +10 -1
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
const assertModelRoutingText = async text => (await import('./model-routing-defence.mjs')).assertModelRoutingText(text);
|
|
2
|
+
// Guarded native workers for Agentic Kit's existing execution runner.
|
|
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 } from 'node:child_process';
|
|
8
|
+
import { runControlledClaudeTurn } from './claude-controlled-terminal.mjs';
|
|
9
|
+
import { validateDispatchDecision, assertSubscriptionAuth, buildLaunch, subscriptionEnvironment } from './model-router-dispatch.mjs';
|
|
10
|
+
import { subscriptionOnlyEnv } from './subscription-hosts.mjs';
|
|
11
|
+
import { readCodexAllowance } from './native-subscription-usage.mjs';
|
|
12
|
+
|
|
13
|
+
const digest = bytes => crypto.createHash('sha256').update(bytes).digest('hex');
|
|
14
|
+
const blocked = reason => Object.assign(new Error(reason), { safetyBlocked: true });
|
|
15
|
+
|
|
16
|
+
export function nativeWorkflowBinaries(home = os.homedir()) {
|
|
17
|
+
const config = JSON.parse(fs.readFileSync(path.join(home, '.cache/ruvnet-brain/model-routing/terminal-launcher-config.json'), 'utf8'));
|
|
18
|
+
return { claude: config.realClaude, codex: config.realCodex };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function readCodexWorkerObservation(sessionId, { home = process.env.CODEX_HOME || path.join(os.homedir(), '.codex'), now = Date.now(), expectedPriorTurns, evidencePath, allowHistory = false } = {}) {
|
|
22
|
+
if (!/^[a-f0-9-]{36}$/.test(sessionId || '')) throw blocked('Native worker session identity missing');
|
|
23
|
+
const matches = [];
|
|
24
|
+
const directories = [];
|
|
25
|
+
const base = path.join(home, 'sessions');
|
|
26
|
+
if (evidencePath) directories.push(path.dirname(evidencePath));
|
|
27
|
+
else if ((expectedPriorTurns !== undefined || allowHistory) && fs.existsSync(base)) {
|
|
28
|
+
for (const year of fs.readdirSync(base)) if (/^\d{4}$/.test(year))
|
|
29
|
+
for (const month of fs.readdirSync(path.join(base, year))) if (/^\d{2}$/.test(month))
|
|
30
|
+
for (const day of fs.readdirSync(path.join(base, year, month))) if (/^\d{2}$/.test(day)) directories.push(path.join(base, year, month, day));
|
|
31
|
+
} else for (const offset of [0, 86400000]) {
|
|
32
|
+
const date = new Date(now - offset).toISOString().slice(0, 10).split('-');
|
|
33
|
+
directories.push(path.join(home, 'sessions', ...date));
|
|
34
|
+
}
|
|
35
|
+
for (const directory of new Set(directories)) {
|
|
36
|
+
if (!fs.existsSync(directory)) continue;
|
|
37
|
+
for (const name of fs.readdirSync(directory)) {
|
|
38
|
+
if (!name.endsWith(`-${sessionId}.jsonl`)) continue;
|
|
39
|
+
const file = path.join(directory, name), stat = fs.lstatSync(file);
|
|
40
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 16 * 1024 * 1024) throw blocked('Unsafe native worker evidence file');
|
|
41
|
+
matches.push({ file, bytes: fs.readFileSync(file) });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (matches.length !== 1) throw blocked('Exact native worker rollout unavailable or ambiguous');
|
|
45
|
+
const rows = matches[0].bytes.toString('utf8').trim().split('\n').map(line => JSON.parse(line));
|
|
46
|
+
const contexts = rows.filter(row => row.type === 'turn_context');
|
|
47
|
+
if (!contexts.length || !allowHistory && contexts.length !== (expectedPriorTurns === undefined ? 1 : expectedPriorTurns + 1)) throw blocked('Native worker has unexpected turn history');
|
|
48
|
+
const context = contexts.at(-1).payload;
|
|
49
|
+
return { model: context.model, effort: context.effort, cwd: context.cwd, sandbox: context.sandbox_policy, sessionId,
|
|
50
|
+
turnCount: contexts.length, evidence: { path: matches[0].file, sha256: digest(matches[0].bytes), type: 'native-turn-context' } };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Only the installed user-owned Brain search server is projected into isolated native workers. */
|
|
54
|
+
export function codexBrainSearchArguments(env = process.env) {
|
|
55
|
+
const server = path.join(env.HOME || os.homedir(), '.claude', 'ruvnet-brain', 'mcp', 'server.mjs');
|
|
56
|
+
if (!fs.existsSync(server)) return [];
|
|
57
|
+
const stat = fs.lstatSync(server);
|
|
58
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.uid !== process.getuid?.()) throw blocked('Installed Brain search server is not user-owned');
|
|
59
|
+
const table = `mcp_servers.ruvnet-brain={command=${JSON.stringify(process.execPath)},args=[${JSON.stringify(server)}],enabled_tools=["search_ruvnet"],startup_timeout_sec=30,tool_timeout_sec=90,env={RUVNET_HOOK_HOST="codex"}}`;
|
|
60
|
+
return ['-c', table];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const CLAUDE_WORKFLOW_SCHEMAS = {"planner":{"type":"object","properties":{"tasks":{"type":"array","minItems":1,"items":{"type":"object","additionalProperties":false,"properties":{"id":{"type":"string","pattern":"^[a-z][a-z0-9-]{0,79}$"},"instructions":{"type":"string","minLength":1},"dependsOn":{"type":"array","items":{"type":"string"}},"mode":{"type":"string","enum":["read","write"]},"worktree":{"type":"string"},"paths":{"type":"array","items":{"type":"string"}},"checkIds":{"type":"array","items":{"type":"string"}}},"required":["id","instructions","mode","checkIds"]}}},"required":["tasks"]},"worker":{"type":"object","properties":{"outcome":{"type":"string","minLength":1},"artifacts":{"type":"array","items":{}},"decisions":{"type":"array","items":{}},"risks":{"type":"array","items":{}}},"required":["outcome","artifacts","decisions","risks"]},"reviewer":{"type":"object","properties":{"passed":{"type":"boolean"},"artifactDigest":{"type":"string","pattern":"^[a-f0-9]{64}$"},"findings":{"type":"array","items":{}},"evidence":{"type":"array","items":{},"minItems":1}},"required":["passed","artifactDigest","findings","evidence"]}};
|
|
64
|
+
const plainObject = value => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
65
|
+
export function claudeWorkflowResponse(role) {
|
|
66
|
+
const kind = role === 'developer' ? 'worker' : role;
|
|
67
|
+
if (!Object.hasOwn(CLAUDE_WORKFLOW_SCHEMAS, kind)) throw blocked('Unknown native Claude workflow role');
|
|
68
|
+
const schema = CLAUDE_WORKFLOW_SCHEMAS[kind];
|
|
69
|
+
if (!schema) throw blocked('Unknown native Claude workflow role');
|
|
70
|
+
const nonblank = value => typeof value === 'string' && value.trim().length > 0;
|
|
71
|
+
const strings = value => Array.isArray(value) && value.every(item => typeof item === 'string');
|
|
72
|
+
const validate = value => {
|
|
73
|
+
if (!plainObject(value)) return false;
|
|
74
|
+
if (kind === 'planner') return Array.isArray(value.tasks) && value.tasks.length > 0 && value.tasks.every(task =>
|
|
75
|
+
plainObject(task) && Object.keys(task).every(key => ['id', 'instructions', 'dependsOn', 'mode', 'worktree', 'paths', 'checkIds'].includes(key))
|
|
76
|
+
&& /^[a-z][a-z0-9-]{0,79}$/.test(task.id || '') && nonblank(task.instructions) && ['read', 'write'].includes(task.mode)
|
|
77
|
+
&& strings(task.checkIds) && (task.dependsOn === undefined || strings(task.dependsOn))
|
|
78
|
+
&& (task.paths === undefined || strings(task.paths)) && (task.worktree === undefined || typeof task.worktree === 'string'));
|
|
79
|
+
if (kind === 'worker') return nonblank(value.outcome) && ['artifacts', 'decisions', 'risks'].every(key => Array.isArray(value[key]));
|
|
80
|
+
return typeof value.passed === 'boolean' && /^[a-f0-9]{64}$/.test(value.artifactDigest || '')
|
|
81
|
+
&& Array.isArray(value.findings) && Array.isArray(value.evidence) && value.evidence.length > 0;
|
|
82
|
+
};
|
|
83
|
+
return { responseSchema: structuredClone(schema), validateStructuredOutput: validate };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function executeCodexWorkflowWorker({ binary, decision, prompt, cwd, readOnly, signal, timeoutMs,
|
|
87
|
+
env = process.env, sessionId, launch = spawn, observe = readCodexWorkerObservation, allowance = readCodexAllowance }) {
|
|
88
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0 || signal?.aborted) throw blocked('Native worker cancelled or deadline unavailable');
|
|
89
|
+
const limit = performance.now() + timeoutMs;
|
|
90
|
+
await assertModelRoutingText(prompt);
|
|
91
|
+
const clean = { ...subscriptionOnlyEnv(subscriptionEnvironment(env)), RNB_TERMINAL_LAUNCH_ACTIVE: '1' };
|
|
92
|
+
assertSubscriptionAuth('codex', { env: clean });
|
|
93
|
+
const quota = await allowance({ env: clean,
|
|
94
|
+
spawnHost: (_command, args, options) => launch(binary, args, options) });
|
|
95
|
+
if (quota.ordinaryUsageAllowed !== true) throw blocked('Native Codex included allowance not available');
|
|
96
|
+
if (performance.now() >= limit || signal?.aborted) throw blocked('Native worker deadline expired during readiness');
|
|
97
|
+
const spec = buildLaunch(decision, { cwd });
|
|
98
|
+
const evidenceHome = clean.CODEX_HOME || path.join(os.homedir(), '.codex');
|
|
99
|
+
const prior = sessionId ? observe(sessionId, { home: evidenceHome, expectedPriorTurns: undefined, allowHistory: true }) : null;
|
|
100
|
+
const args = sessionId ? ['exec', 'resume', '--ignore-user-config', '--skip-git-repo-check', '--json', '--model', decision.model,
|
|
101
|
+
'-c', `model_reasoning_effort=\"${decision.effort}\"`, '-c', 'model_provider=\"openai\"',
|
|
102
|
+
'-c', 'service_tier=\"default\"', '-c', `sandbox_mode=\"${readOnly ? 'read-only' : 'workspace-write'}\"`,
|
|
103
|
+
'-c', 'features.fast_mode=false', '-c', 'features.multi_agent=false', '-c', 'features.multi_agent_v2=false', sessionId, '-']
|
|
104
|
+
: [...spec.args.slice(0, -1), '--skip-git-repo-check', '--json', '--sandbox', readOnly ? 'read-only' : 'workspace-write',
|
|
105
|
+
'-c', 'features.multi_agent=false', '-c', 'features.multi_agent_v2=false', '-'];
|
|
106
|
+
args.splice(args.length - 1, 0, ...codexBrainSearchArguments(clean));
|
|
107
|
+
const result = await new Promise((resolve, reject) => {
|
|
108
|
+
const child = launch(binary, args, { cwd, env: clean, shell: false, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
109
|
+
let stdout = '', stderr = '', finished = false, timer, killTimer, cancelled = false, cancelReason;
|
|
110
|
+
const finish = (error, value) => {
|
|
111
|
+
if (finished) return; finished = true; clearTimeout(timer); clearTimeout(killTimer);
|
|
112
|
+
signal?.removeEventListener('abort', cancel);
|
|
113
|
+
error ? reject(error) : resolve(value);
|
|
114
|
+
};
|
|
115
|
+
const safeKill = signalName => { try { child.kill(signalName); } catch { /* close remains required */ } };
|
|
116
|
+
const cancel = reason => {
|
|
117
|
+
if (finished || cancelled) return; cancelled = true; cancelReason = typeof reason === 'string' ? reason : 'Native worker cancelled';
|
|
118
|
+
safeKill('SIGTERM');
|
|
119
|
+
killTimer = setTimeout(() => { safeKill('SIGKILL');
|
|
120
|
+
child.stdin?.destroy(); child.stdout?.destroy(); child.stderr?.destroy(); child.unref?.();
|
|
121
|
+
finish(Object.assign(blocked('Native worker retirement not confirmed'), { retirementUnconfirmed: true })); }, 1000);
|
|
122
|
+
};
|
|
123
|
+
child.stdout.on('data', chunk => { if (cancelled) return; stdout += chunk; if (Buffer.byteLength(stdout) > 16 * 1024 * 1024) cancel('Native worker output exceeded bound'); });
|
|
124
|
+
child.stderr.on('data', chunk => { stderr = (stderr + chunk).slice(-8000); });
|
|
125
|
+
child.on('error', () => cancel('Native worker process error; retirement required'));
|
|
126
|
+
child.stdin.once('error', () => cancel('Native worker input delivery failed'));
|
|
127
|
+
child.once('close', (code, nativeSignal) => {
|
|
128
|
+
if (cancelled || signal?.aborted || nativeSignal) return finish(blocked('Native worker interrupted; effects require inspection'));
|
|
129
|
+
if (code !== 0) return finish(blocked(`Native worker failed (exit ${code}); inspect effects before retry`));
|
|
130
|
+
finish(null, { stdout, stderr });
|
|
131
|
+
});
|
|
132
|
+
signal?.addEventListener('abort', cancel, { once: true });
|
|
133
|
+
timer = setTimeout(() => cancel('Native worker deadline exceeded'), Math.max(1, limit - performance.now()));
|
|
134
|
+
if (signal?.aborted) cancel(); else child.stdin.end(prompt);
|
|
135
|
+
});
|
|
136
|
+
if (performance.now() >= limit || signal?.aborted) throw blocked('Native worker completed after its deadline');
|
|
137
|
+
const events = result.stdout.trim().split('\n').map(line => JSON.parse(line));
|
|
138
|
+
const thread = events.find(event => event.type === 'thread.started');
|
|
139
|
+
if (!events.some(event => event.type === 'turn.completed') || events.some(event => event.type === 'turn.failed' || event.type === 'error')) {
|
|
140
|
+
throw blocked('Native Codex turn did not complete successfully');
|
|
141
|
+
}
|
|
142
|
+
if (sessionId && thread?.thread_id !== sessionId) throw blocked('Native parent session changed');
|
|
143
|
+
const observation = observe(thread?.thread_id, sessionId ? { home: evidenceHome, expectedPriorTurns: prior.turnCount, evidencePath: prior.evidence.path } : { home: evidenceHome });
|
|
144
|
+
if (observation.model !== decision.model || observation.effort !== decision.effort || observation.cwd !== cwd
|
|
145
|
+
|| (readOnly && observation.sandbox?.type !== 'read-only')) throw blocked('Native worker model, effort, directory or sandbox mismatch');
|
|
146
|
+
const messages = events.filter(event => event.type === 'item.completed' && event.item?.type === 'agent_message');
|
|
147
|
+
const answer = messages.at(-1)?.item?.text;
|
|
148
|
+
if (typeof answer !== 'string') throw blocked('Native final answer unavailable');
|
|
149
|
+
await assertModelRoutingText(answer);
|
|
150
|
+
if (performance.now() >= limit || signal?.aborted) throw blocked('Native worker exceeded deadline during output inspection');
|
|
151
|
+
return { ...observation, answer, completed: true, modelObserved: true, effortSettingsObserved: true, effortEvidence: 'native-turn-context', usage: events.find(event => event.type === 'turn.completed')?.usage ?? null };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Native Codex stays read-only. Only exact, preconditioned owner-authorized files are published here. */
|
|
155
|
+
export function applyOwnedCodexEdits(worker, answer, { signal, deadline = Infinity } = {}) {
|
|
156
|
+
let report;
|
|
157
|
+
try { report = JSON.parse(answer); } catch { throw blocked('Codex write result must be bounded JSON edits'); }
|
|
158
|
+
if (!Array.isArray(report.edits) || !report.edits.length || report.edits.length > 32) throw blocked('Exact bounded edits required');
|
|
159
|
+
const root = fs.realpathSync(worker.ownership.worktree), targets = new Set(); let total = 0;
|
|
160
|
+
const live = () => { if (signal?.aborted || Date.now() >= deadline) throw blocked('Edit publication cancelled or expired'); };
|
|
161
|
+
const edits = report.edits.map(edit => {
|
|
162
|
+
live();
|
|
163
|
+
if (!edit || typeof edit.path !== 'string' || path.isAbsolute(edit.path) ||
|
|
164
|
+
edit.path.split(/[\\/]/).some(part => !part || part === '.' || part === '..') ||
|
|
165
|
+
!worker.ownership.paths.includes(edit.path) || typeof edit.content !== 'string') throw blocked('Edit escaped exact file ownership');
|
|
166
|
+
const file = path.join(root, edit.path); if (targets.has(file)) throw blocked('Duplicate edit'); targets.add(file);
|
|
167
|
+
total += Buffer.byteLength(edit.content); if (total > 4 * 1024 * 1024) throw blocked('Edit bytes exceeded bound');
|
|
168
|
+
for (let ancestor = file; ancestor !== root; ancestor = path.dirname(ancestor)) {
|
|
169
|
+
if (fs.existsSync(ancestor) && fs.lstatSync(ancestor).isSymbolicLink()) throw blocked('Symlink edit refused');
|
|
170
|
+
}
|
|
171
|
+
if (!fs.existsSync(path.dirname(file))) throw blocked('Edit parent must already exist');
|
|
172
|
+
const exists = fs.existsSync(file), stat = exists ? fs.lstatSync(file) : null;
|
|
173
|
+
if (stat && (!stat.isFile() || stat.nlink !== 1)) throw blocked('Nonregular or hardlinked edit refused');
|
|
174
|
+
if (exists ? edit.oldSha256 !== digest(fs.readFileSync(file)) : edit.oldSha256 !== null) throw blocked('Edit precondition changed');
|
|
175
|
+
return { ...edit, file, mode: stat?.mode & 0o777 || 0o600, exists };
|
|
176
|
+
});
|
|
177
|
+
const applied = [];
|
|
178
|
+
try {
|
|
179
|
+
for (const edit of edits) {
|
|
180
|
+
live();
|
|
181
|
+
if (edit.exists ? !fs.existsSync(edit.file) || digest(fs.readFileSync(edit.file)) !== edit.oldSha256 : fs.existsSync(edit.file)) throw blocked('Edit changed at publication');
|
|
182
|
+
const temporary = path.join(path.dirname(edit.file), `.rnb-edit-${crypto.randomUUID()}`);
|
|
183
|
+
try {
|
|
184
|
+
fs.writeFileSync(temporary, edit.content, { flag: 'wx', mode: edit.mode }); live();
|
|
185
|
+
if (edit.exists) {
|
|
186
|
+
if (fs.lstatSync(edit.file).isSymbolicLink() || digest(fs.readFileSync(edit.file)) !== edit.oldSha256) throw blocked('Edit changed before replacement');
|
|
187
|
+
// Single-writer ownership is required; external same-user writes are not an atomic CAS contract.
|
|
188
|
+
fs.renameSync(temporary, edit.file);
|
|
189
|
+
} else fs.linkSync(temporary, edit.file); // no-replace publication even if a destination races into existence
|
|
190
|
+
applied.push({ path: edit.file, digest: digest(fs.readFileSync(edit.file)) });
|
|
191
|
+
} finally { try { fs.unlinkSync(temporary); } catch (error) { if (error.code !== 'ENOENT') throw error; } }
|
|
192
|
+
}
|
|
193
|
+
} catch (error) {
|
|
194
|
+
throw Object.assign(blocked(`Edit publication blocked: ${error.message}; inspect actual applied files before retry`), { applied, uncertainEffects: applied.length > 0 });
|
|
195
|
+
}
|
|
196
|
+
return applied;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function ownedPermission(request, worker, nativeRequest) {
|
|
200
|
+
if (['Agent', 'Task'].includes(nativeRequest.tool_name)) return false;
|
|
201
|
+
if (['Read', 'Glob', 'Grep'].includes(nativeRequest.tool_name) || /__search_ruvnet$/.test(nativeRequest.tool_name)) return true;
|
|
202
|
+
if (worker.ownership?.mode !== 'write' || request.permissions?.write !== true) return false;
|
|
203
|
+
if (!['Write', 'Edit', 'MultiEdit'].includes(nativeRequest.tool_name)) return false;
|
|
204
|
+
const input = nativeRequest.input || {}, raw = input.file_path;
|
|
205
|
+
if (typeof raw !== 'string') return false;
|
|
206
|
+
const root = fs.realpathSync(worker.ownership.worktree), target = path.resolve(root, raw);
|
|
207
|
+
if (!target.startsWith(root + path.sep)) return false;
|
|
208
|
+
let ancestor = target;
|
|
209
|
+
while (!fs.existsSync(ancestor)) ancestor = path.dirname(ancestor);
|
|
210
|
+
if (fs.realpathSync(ancestor) !== ancestor) return false;
|
|
211
|
+
return worker.ownership.paths.some(relative => {
|
|
212
|
+
const allowed = path.resolve(root, relative);
|
|
213
|
+
return target === allowed || (relative.endsWith('/') && target.startsWith(allowed + path.sep));
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function createGuardedWorkflowAdapters({ request, budget, env = process.env,
|
|
218
|
+
binaries = nativeWorkflowBinaries(), executeNative, verifyDecision = validateDispatchDecision, captureObservation = () => {} }) {
|
|
219
|
+
const adapters = {};
|
|
220
|
+
for (const host of ['codex', 'claude']) {
|
|
221
|
+
adapters[host] = {
|
|
222
|
+
id: `brain-native-${host}`,
|
|
223
|
+
hostId: host,
|
|
224
|
+
readiness: async () => ({ ready: true }),
|
|
225
|
+
prepare: async ({ worker, signal, timeoutMs }) => {
|
|
226
|
+
const decision = worker.decision;
|
|
227
|
+
if (!decision || decision.harness !== (host === 'claude' ? 'claude-code' : host)
|
|
228
|
+
|| decision.model !== worker.configuredModel) throw blocked('Worker allocation missing or inconsistent');
|
|
229
|
+
verifyDecision(decision);
|
|
230
|
+
const binary = binaries[host];
|
|
231
|
+
if (!path.isAbsolute(binary || '') || !fs.statSync(binary).isFile()) throw blocked('Native executable unavailable');
|
|
232
|
+
const cwd = fs.realpathSync(worker.ownership?.worktree || request.projectRoot || request.cwd);
|
|
233
|
+
if (worker.ownership?.mode === 'write' && (request.permissions?.write !== true || !worker.ownership.paths?.length)) {
|
|
234
|
+
throw blocked('Worker write authority unavailable');
|
|
235
|
+
}
|
|
236
|
+
if (!request.originalPrompt || typeof worker.prompt !== 'string') throw blocked('Original request context unavailable');
|
|
237
|
+
let original;
|
|
238
|
+
try { original = JSON.parse(worker.prompt).originalPrompt; } catch { /* AK appends its handoff below the canonical JSON. */ }
|
|
239
|
+
if (original !== request.originalPrompt && !worker.prompt.includes(request.originalPrompt)
|
|
240
|
+
&& !worker.prompt.includes(JSON.stringify(request.originalPrompt))) throw blocked('Worker lost the original request');
|
|
241
|
+
for (const ref of request.contextRefs || []) {
|
|
242
|
+
if (digest(fs.readFileSync(ref.path)) !== ref.digest) throw blocked('Canonical context changed before launch');
|
|
243
|
+
}
|
|
244
|
+
const controller = new AbortController();
|
|
245
|
+
return { worker, decision, binary, cwd, controller, signal: signal ? AbortSignal.any([signal, controller.signal]) : controller.signal,
|
|
246
|
+
timeoutMs: Math.min(timeoutMs, budget.deadline - Date.now()), startedAt: new Date().toISOString(), finished: false };
|
|
247
|
+
},
|
|
248
|
+
launch: async state => {
|
|
249
|
+
try {
|
|
250
|
+
if (host === 'claude') state.timeoutMs = Math.floor(Math.min(state.timeoutMs, budget.deadline - Date.now()));
|
|
251
|
+
if (state.timeoutMs <= 0) throw blocked('Shared workflow deadline exhausted');
|
|
252
|
+
if (executeNative) state.observation = await executeNative({ ...state, prompt: state.worker.prompt + (state.worker.reviewContract ? '\n' + state.worker.reviewContract : ''), env });
|
|
253
|
+
else if (host === 'codex') {
|
|
254
|
+
const write = state.worker.ownership?.mode === 'write';
|
|
255
|
+
const contract = write ? '\nExecution contract: native execution is read-only. Return ONLY JSON with outcome, artifacts, decisions, risks, and edits:[{path,oldSha256,content}]. Read each existing file first and use its actual SHA256; new files use oldSha256:null. Paths must exactly match ownership.paths. Do not write files or run mutating commands.' : '';
|
|
256
|
+
state.observation = await executeCodexWorkflowWorker({ ...state, prompt: state.worker.prompt + contract + (state.worker.reviewContract ? '\n' + state.worker.reviewContract : ''), readOnly: true, env });
|
|
257
|
+
if (write) state.observation.appliedArtifacts = applyOwnedCodexEdits(state.worker, state.observation.answer, { signal: state.signal, deadline: budget.deadline });
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
const receipts = [];
|
|
261
|
+
const turn = await runControlledClaudeTurn({ ...state, ...claudeWorkflowResponse(state.worker.role), prompt: state.worker.prompt + (state.worker.reviewContract ? '\n' + state.worker.reviewContract : ''), env,
|
|
262
|
+
decide: async () => state.decision, approve: async permission => ownedPermission(request, state.worker, permission),
|
|
263
|
+
receipt: value => receipts.push(value) });
|
|
264
|
+
if (turn.structuredOutput !== true || typeof turn.finalAnswer !== 'string' || !turn.finalAnswer.trim()) throw blocked('Native final answer unavailable');
|
|
265
|
+
state.observation = { model: turn.decision?.model, effort: turn.decision?.effort,
|
|
266
|
+
sessionId: turn.sessionId, completed: turn.modelObserved === true && turn.effortSettingsObserved === true, answer: turn.finalAnswer,
|
|
267
|
+
evidence: receipts, responseFormat: 'json-schema', nativeSchemaRetries: 'not-observed', effortEvidence: 'Native settings observed before and after; per-request effort not exposed' };
|
|
268
|
+
}
|
|
269
|
+
if (state.observation?.completed && state.observation.model === state.decision.model && state.observation.effort === state.decision.effort) {
|
|
270
|
+
captureObservation(state.worker, state.observation);
|
|
271
|
+
}
|
|
272
|
+
} catch (error) { state.error = error; if (error.retirementUnconfirmed) state.retirementUnconfirmed = true; }
|
|
273
|
+
finally { state.finished = true; }
|
|
274
|
+
return state;
|
|
275
|
+
},
|
|
276
|
+
observe: async state => state.observation,
|
|
277
|
+
interpret: (state, observation) => {
|
|
278
|
+
const valid = !state.error && observation?.completed === true && observation.model === state.decision.model && observation.effort === state.decision.effort;
|
|
279
|
+
state.observation = observation;
|
|
280
|
+
const endedAt = new Date().toISOString();
|
|
281
|
+
return { workerId: state.worker.id, activity: state.worker.activity, role: state.worker.role, host,
|
|
282
|
+
status: valid ? 'succeeded' : 'blocked', exitCategory: valid ? 'success' : 'protocol_error',
|
|
283
|
+
startedAt: state.startedAt, endedAt, durationMs: Math.max(0, Date.parse(endedAt) - Date.parse(state.startedAt)),
|
|
284
|
+
provider: valid ? state.decision.provider : null, providerProvenance: valid ? 'configured' : 'unknown',
|
|
285
|
+
configuredModel: state.decision.model, observedModel: valid ? observation.model : null,
|
|
286
|
+
configuredEffort: state.decision.effort, observedEffort: valid ? observation.effort : null,
|
|
287
|
+
effortEvidence: valid ? observation.effortEvidence || 'native-turn-context' : null,
|
|
288
|
+
sessionId: observation?.sessionId || null, transcriptRefs: observation?.evidence?.path ? [observation.evidence.path] : [],
|
|
289
|
+
failure: valid ? null : { reason: state.error?.message || 'Native model/effort/completion unverified' }, usage: observation?.usage || null,
|
|
290
|
+
...(observation?.type === 'orphaned' || state.retirementUnconfirmed ? { status: 'blocked', exitCategory: 'orphaned' } : {}) };
|
|
291
|
+
},
|
|
292
|
+
summarize: state => {
|
|
293
|
+
try { return JSON.parse(state.observation.answer); } catch { throw blocked('Required native handoff is missing or malformed'); }
|
|
294
|
+
},
|
|
295
|
+
cancel: async state => { state?.controller.abort(); return { type: state?.finished ? 'cancelled' : 'orphaned' }; },
|
|
296
|
+
cleanup: async state => {
|
|
297
|
+
if (state && (!state.finished || state.retirementUnconfirmed)) throw blocked('Native worker cleanup is not confirmed');
|
|
298
|
+
return { cleaned: true };
|
|
299
|
+
},
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
return adapters;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export async function runObservedWorkflowWorker({ request, decision, prompt, ownership, role = 'worker',
|
|
306
|
+
id = `worker-${crypto.randomUUID()}`, env = process.env, signal, timeoutMs = 120000 }) {
|
|
307
|
+
const host = decision.harness === 'claude-code' ? 'claude' : decision.harness;
|
|
308
|
+
const adapters = createGuardedWorkflowAdapters({ request, env, budget: { deadline: request.deadline || Date.now() + timeoutMs } });
|
|
309
|
+
if (!adapters[host]) throw blocked('Native workflow host unavailable');
|
|
310
|
+
const worker = { id, host, activity: role === 'reviewer' ? 'review' : 'implementation', role, decision,
|
|
311
|
+
configuredModel: decision.model, configuredEffort: decision.effort, ownership, prompt };
|
|
312
|
+
const state = await adapters[host].prepare({ worker, signal, timeoutMs });
|
|
313
|
+
await adapters[host].launch(state);
|
|
314
|
+
const result = adapters[host].interpret(state, await adapters[host].observe(state));
|
|
315
|
+
await adapters[host].cleanup(state);
|
|
316
|
+
if (result.status !== 'succeeded') throw blocked(result.failure?.reason || 'Native worker completion unverified');
|
|
317
|
+
return { ...state.observation, result };
|
|
318
|
+
}
|
|
@@ -82,6 +82,7 @@ function backup(file) {
|
|
|
82
82
|
export function runtimeSnapshot(sourceRoot) {
|
|
83
83
|
const root = fs.realpathSync(sourceRoot); const files = new Map();
|
|
84
84
|
const visit = (relative) => {
|
|
85
|
+
relative = relative.split(path.sep).join('/');
|
|
85
86
|
if (files.has(relative)) return;
|
|
86
87
|
const file = regular(path.join(root, relative));
|
|
87
88
|
if (!file.startsWith(`${root}${path.sep}`)) throw new Error('Runtime dependency escaped source root');
|
|
@@ -112,6 +113,36 @@ export function runtimeSnapshot(sourceRoot) {
|
|
|
112
113
|
files.set(`node_modules/ws/${relative}`, fs.readFileSync(file));
|
|
113
114
|
}
|
|
114
115
|
}
|
|
116
|
+
// Freeze the installed managed runner with this runtime; no global-version drift or download.
|
|
117
|
+
if (files.has('scripts/model-routing-controller.mjs')) {
|
|
118
|
+
const require = createRequire(path.join(root, 'package.json'));
|
|
119
|
+
const kitRoot = path.dirname(regular(require.resolve('@pacphi/agentic-kit/package.json')));
|
|
120
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(kitRoot, 'package.json')));
|
|
121
|
+
if (manifest.name !== '@pacphi/agentic-kit') throw new Error('Unexpected managed runner identity');
|
|
122
|
+
const collect = relative => {
|
|
123
|
+
const absolute = path.join(kitRoot, relative), stat = fs.lstatSync(absolute);
|
|
124
|
+
if (stat.isSymbolicLink()) throw new Error('Managed runner dependency symlink refused');
|
|
125
|
+
if (stat.isDirectory()) {
|
|
126
|
+
for (const name of fs.readdirSync(absolute)) if (name !== 'node_modules') collect(path.join(relative, name));
|
|
127
|
+
} else if (stat.isFile()) files.set(`node_modules/@pacphi/agentic-kit/${relative.split(path.sep).join('/')}`, fs.readFileSync(absolute));
|
|
128
|
+
else throw new Error('Nonregular managed runner dependency');
|
|
129
|
+
};
|
|
130
|
+
collect('package.json'); collect('src');
|
|
131
|
+
}
|
|
132
|
+
if (files.has('scripts/model-routing-defence.mjs')) {
|
|
133
|
+
const defenceEntry = regular(fileURLToPath(import.meta.resolve('@claude-flow/aidefence')));
|
|
134
|
+
const defenceRoot = path.dirname(path.dirname(defenceEntry));
|
|
135
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(defenceRoot, 'package.json')));
|
|
136
|
+
if (manifest.name !== '@claude-flow/aidefence') throw new Error('Unexpected local defence identity');
|
|
137
|
+
const collect = relative => {
|
|
138
|
+
const absolute = path.join(defenceRoot, relative), stat = fs.lstatSync(absolute);
|
|
139
|
+
if (stat.isSymbolicLink()) throw new Error('Defence dependency symlink refused');
|
|
140
|
+
if (stat.isDirectory()) { for (const name of fs.readdirSync(absolute)) collect(path.join(relative, name)); }
|
|
141
|
+
else if (stat.isFile()) files.set(`node_modules/@claude-flow/aidefence/${relative.split(path.sep).join('/')}`, fs.readFileSync(absolute));
|
|
142
|
+
else throw new Error('Nonregular defence dependency');
|
|
143
|
+
};
|
|
144
|
+
collect('package.json'); collect('dist');
|
|
145
|
+
}
|
|
115
146
|
for (const name of fs.readdirSync(path.join(root, 'config/model-router'))) {
|
|
116
147
|
if (/\.(?:json|mjs)$/.test(name)) visit(`config/model-router/${name}`);
|
|
117
148
|
}
|
|
@@ -28,9 +28,17 @@ function regular(file, { privateFile = false } = {}) {
|
|
|
28
28
|
function executable(file) {
|
|
29
29
|
const result = regular(fs.realpathSync(file)); fs.accessSync(result, fs.constants.X_OK); return result;
|
|
30
30
|
}
|
|
31
|
-
function
|
|
31
|
+
function nativeExecutable(file) {
|
|
32
|
+
const result = executable(file), fd = fs.openSync(result, 'r');
|
|
33
|
+
const header = Buffer.alloc(512);
|
|
34
|
+
try {
|
|
35
|
+
if (header.subarray(0, fs.readSync(fd, header, 0, header.length, 0)).toString().includes(MARKER)) throw new Error('Native terminal launcher recursion refused');
|
|
36
|
+
} finally { fs.closeSync(fd); }
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
function atomic(file, content, mode = 0o600, replaceLink = false) {
|
|
32
40
|
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
33
|
-
if (fs.existsSync(file)) regular(file);
|
|
41
|
+
if (fs.existsSync(file) && !replaceLink) regular(file);
|
|
34
42
|
const temp = `${file}.${crypto.randomUUID()}.tmp`;
|
|
35
43
|
try {
|
|
36
44
|
const fd = fs.openSync(temp, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_NOFOLLOW, mode);
|
|
@@ -38,6 +46,27 @@ function atomic(file, content, mode = 0o600) {
|
|
|
38
46
|
fs.renameSync(temp, file);
|
|
39
47
|
} finally { fs.rmSync(temp, { force: true }); }
|
|
40
48
|
}
|
|
49
|
+
function canonicalEntry(file, binary, replace) {
|
|
50
|
+
let stat;
|
|
51
|
+
try { stat = fs.lstatSync(file); } catch (error) { if (error.code === 'ENOENT') return { file, symlink: false }; throw error; }
|
|
52
|
+
if (stat.uid !== process.getuid?.()) throw new Error(`Preserving foreign canonical terminal entry: ${file}`);
|
|
53
|
+
if (stat.isSymbolicLink()) {
|
|
54
|
+
if (fs.realpathSync(file) !== binary) throw new Error(`Preserving unrelated canonical terminal link: ${file}`);
|
|
55
|
+
return { file, symlink: true, link: fs.readlinkSync(file) };
|
|
56
|
+
}
|
|
57
|
+
regular(file);
|
|
58
|
+
if (fs.realpathSync(file) === binary) throw new Error('Separate native binary required before replacing canonical terminal entry');
|
|
59
|
+
if (!fs.readFileSync(file, 'utf8').includes(MARKER) && !replace) throw new Error(`Preserving unmanaged canonical terminal entry: ${file}`);
|
|
60
|
+
return { file, symlink: false };
|
|
61
|
+
}
|
|
62
|
+
function backupCanonical(entry) {
|
|
63
|
+
if (!entry.symlink) return backup(entry.file);
|
|
64
|
+
const target = `${entry.file}.${digest(entry.link)}.original`;
|
|
65
|
+
try { fs.symlinkSync(entry.link, target); } catch (error) {
|
|
66
|
+
if (error.code !== 'EEXIST' || !fs.lstatSync(target).isSymbolicLink() || fs.lstatSync(target).uid !== process.getuid?.() || fs.readlinkSync(target) !== entry.link) throw new Error('Canonical link backup mismatch');
|
|
67
|
+
}
|
|
68
|
+
return target;
|
|
69
|
+
}
|
|
41
70
|
function backup(file) {
|
|
42
71
|
if (!fs.existsSync(file)) return null;
|
|
43
72
|
const bytes = fs.readFileSync(regular(file)); const target = `${file}.${digest(bytes)}.original`;
|
|
@@ -84,40 +113,48 @@ export function terminalShellPlan(text, sourcePath, launchers) {
|
|
|
84
113
|
|
|
85
114
|
/** Runtime is supplied by the snapshot installer; this module does not rebuild or mutate it. */
|
|
86
115
|
export function installTerminalLaunchers({ home = os.homedir(), nodeBinary = process.execPath, runtimeRoot,
|
|
87
|
-
runtimeDigest, realCodex
|
|
116
|
+
runtimeDigest, realCodex, realClaude, apply = false, manageZsh = true,
|
|
117
|
+
replaceCanonicalEntries = [] } = {}) {
|
|
88
118
|
if (!['darwin', 'linux'].includes(process.platform)) throw new Error('Terminal launchers require macOS or Linux');
|
|
89
119
|
if (!path.isAbsolute(home) || !path.isAbsolute(runtimeRoot || '') || !/^[a-f0-9]{64}$/.test(runtimeDigest || '')) throw new Error('Absolute home and identified runtime required');
|
|
120
|
+
const configPath = path.join(home, '.cache/ruvnet-brain/model-routing/terminal-launcher-config.json');
|
|
121
|
+
const previous = fs.existsSync(configPath) ? JSON.parse(fs.readFileSync(regular(configPath), 'utf8')) : null;
|
|
122
|
+
if (previous && previous.managedBy !== 'ruvnet-brain-terminal-launchers') throw new Error('Preserving unmanaged terminal configuration');
|
|
123
|
+
realCodex ??= previous?.realCodex || path.join(home, '.local/bin/codex');
|
|
90
124
|
const runner = regular(path.join(runtimeRoot, 'scripts/model-terminal-launchers.mjs'));
|
|
91
125
|
const config = { schemaVersion: 1, managedBy: 'ruvnet-brain-terminal-launchers', runtimeDigest, runtimeRoot: fs.realpathSync(runtimeRoot), nodeBinary: executable(nodeBinary),
|
|
92
|
-
runner, realCodex:
|
|
126
|
+
runner, realCodex: nativeExecutable(realCodex), gatewayPath: regular(path.join(runtimeRoot, 'scripts/model-terminal-gateway.mjs')) };
|
|
93
127
|
if (realClaude) {
|
|
94
|
-
config.realClaude =
|
|
128
|
+
config.realClaude = nativeExecutable(realClaude);
|
|
95
129
|
config.claudeHelperPath = regular(path.join(runtimeRoot, 'scripts/claude-terminal-mod.mjs'));
|
|
96
130
|
config.enginePath = regular(path.join(runtimeRoot, 'scripts/model-router-engine.mjs'));
|
|
97
131
|
}
|
|
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
132
|
const launchers = { codex: path.join(home, '.local/bin/ruvnet-brain-codex-terminal') };
|
|
101
133
|
if (realClaude) launchers.claude = path.join(home, '.local/bin/ruvnet-brain-claude-terminal');
|
|
134
|
+
if (!Array.isArray(replaceCanonicalEntries) || replaceCanonicalEntries.some(host => !Object.hasOwn(launchers, host))) throw new Error('Explicit installed host names required for canonical entry migration');
|
|
135
|
+
const canonicalEntries = Object.fromEntries(Object.keys(launchers).map(host => [host, path.join(home, '.local/bin', host)]));
|
|
136
|
+
const canonical = Object.entries(canonicalEntries).map(([host, file]) => canonicalEntry(file, config[host === 'codex' ? 'realCodex' : 'realClaude'], replaceCanonicalEntries.includes(host)));
|
|
102
137
|
const contents = Object.fromEntries(Object.entries(launchers).map(([host, file]) => [file,
|
|
103
138
|
`#!/bin/sh\n${MARKER}\nexec ${quote(config.nodeBinary)} ${quote(runner)} --launch ${quote(host)} --config ${quote(configPath)} -- "$@"\n`]));
|
|
139
|
+
for (const [host, file] of Object.entries(canonicalEntries)) contents[file] = contents[launchers[host]];
|
|
104
140
|
const shellSource = path.join(home, '.config/ruvnet-brain/terminal-routing.zsh');
|
|
105
141
|
const zshrc = path.join(home, '.zshrc');
|
|
106
142
|
const shell = terminalShellPlan(fs.existsSync(zshrc) ? fs.readFileSync(regular(zshrc), 'utf8') : '', shellSource, launchers);
|
|
107
|
-
for (const file of [...Object.
|
|
143
|
+
for (const file of [...Object.values(launchers), shellSource]) {
|
|
108
144
|
if (fs.existsSync(file) && !fs.readFileSync(regular(file), 'utf8').includes(MARKER)) throw new Error(`Preserving unmanaged terminal integration: ${file}`);
|
|
109
145
|
}
|
|
110
146
|
for (const binary of [config.realCodex, config.realClaude].filter(Boolean)) {
|
|
111
147
|
if ([runner, ...Object.keys(contents)].includes(binary)) throw new Error('Native terminal launcher recursion refused');
|
|
112
148
|
}
|
|
113
|
-
const receipt = { apply, launchers, configPath, config, shellSource, shellConflicts: shell.conflicts, backups: [],
|
|
149
|
+
const receipt = { apply, launchers, canonicalEntries, configPath, config, shellSource, shellConflicts: shell.conflicts, backups: [],
|
|
114
150
|
claudeEnforcementScope: realClaude ? 'Controlled native prompt boundary; model observations and effective effort settings checked for each completed turn' : null };
|
|
115
151
|
if (!apply) return receipt;
|
|
116
|
-
for (const file of [configPath, ...Object.
|
|
152
|
+
for (const file of [configPath, ...Object.values(launchers), ...(manageZsh ? [shellSource, zshrc] : [])]) {
|
|
117
153
|
const original = backup(file); if (original) receipt.backups.push(original);
|
|
118
154
|
}
|
|
155
|
+
for (const entry of canonical) { const original = backupCanonical(entry); if (original) receipt.backups.push(original); }
|
|
119
156
|
atomic(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
120
|
-
for (const [file, content] of Object.entries(contents)) atomic(file, content, 0o755);
|
|
157
|
+
for (const [file, content] of Object.entries(contents)) atomic(file, content, 0o755, canonical.some(entry => entry.file === file && entry.symlink));
|
|
121
158
|
if (manageZsh) { atomic(shellSource, shell.source); atomic(zshrc, shell.zshrc, fs.existsSync(zshrc) ? fs.statSync(zshrc).mode & 0o777 : 0o600); }
|
|
122
159
|
return receipt;
|
|
123
160
|
}
|
|
@@ -131,7 +168,7 @@ export function ensureTerminalDaemon(binary, env = process.env, { exec = execFil
|
|
|
131
168
|
}
|
|
132
169
|
export function terminalInvocation({ host, args = [], config, env = process.env, daemonExec = execFileSync }) {
|
|
133
170
|
if (config.schemaVersion !== 1 || env.RNB_TERMINAL_LAUNCH_ACTIVE) throw new Error('Invalid or recursive terminal launch');
|
|
134
|
-
const binary =
|
|
171
|
+
const binary = nativeExecutable(host === 'codex' ? config.realCodex : config.realClaude);
|
|
135
172
|
if (host !== 'codex') throw new Error('Use guarded native Claude launch');
|
|
136
173
|
const mode = classifyTerminalArguments(args);
|
|
137
174
|
if (mode === 'admin') return { command: binary, args: [...args], routed: false };
|
|
@@ -240,7 +277,7 @@ export async function runClaudeTerminal({ config, args = [], env = process.env,
|
|
|
240
277
|
tempRoot = os.tmpdir(), cwd = process.cwd(), signalSource = process, diagnostics = process.stderr } = {}) {
|
|
241
278
|
if (env.RNB_TERMINAL_LAUNCH_ACTIVE) throw new Error('Native terminal launcher recursion refused');
|
|
242
279
|
if (!Number.isFinite(startupMs) || startupMs <= 0 || startupMs > 60000) throw new Error('Bounded native hook startup deadline required');
|
|
243
|
-
const binary =
|
|
280
|
+
const binary = nativeExecutable(config.realClaude), clean = subscriptionEnvironment(env);
|
|
244
281
|
if (claudeAdministrativeArguments(args)) {
|
|
245
282
|
const child = spawn(binary, args, { env: clean, cwd, stdio: 'inherit', shell: false });
|
|
246
283
|
const unforward = forwardSignals(child, signalSource);
|
|
@@ -289,10 +326,17 @@ export async function runTerminalLauncher({ host, args = [], config, env = proce
|
|
|
289
326
|
if (claudeAdministrativeArguments(args)) return runClaudeTerminal({ config, args, env, signalSource });
|
|
290
327
|
if (env.RNB_TERMINAL_LAUNCH_ACTIVE) throw new Error('Native terminal launcher recursion refused');
|
|
291
328
|
const { launchControlledClaudeTerminal } = await import('./claude-controlled-terminal.mjs');
|
|
292
|
-
await launchControlledClaudeTerminal({ binary:
|
|
329
|
+
await launchControlledClaudeTerminal({ binary: nativeExecutable(config.realClaude), args, env, cwd: process.cwd() });
|
|
293
330
|
return { code: 0, signal: null };
|
|
294
331
|
}
|
|
295
332
|
if (host !== 'codex') throw new Error('Unsupported terminal host');
|
|
333
|
+
const classification = classifyTerminalArguments(args);
|
|
334
|
+
if (classification === 'interactive') {
|
|
335
|
+
if (env.RNB_TERMINAL_LAUNCH_ACTIVE) throw new Error('Native terminal launcher recursion refused');
|
|
336
|
+
const { launchManagedCodexTerminal } = await import('./codex-managed-terminal.mjs');
|
|
337
|
+
await launchManagedCodexTerminal({ binary: nativeExecutable(config.realCodex), args, env, cwd: process.cwd() });
|
|
338
|
+
return { code: 0, signal: null };
|
|
339
|
+
}
|
|
296
340
|
const invocation = terminalInvocation({ host, args, config, env });
|
|
297
341
|
const child = spawn(invocation.command, invocation.args, { env: { ...subscriptionEnvironment(env), RNB_TERMINAL_LAUNCH_ACTIVE: '1' }, stdio: 'inherit', shell: false });
|
|
298
342
|
const unforward = forwardSignals(child, signalSource);
|