ruvnet-brain 4.5.4 → 4.5.6
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 +162 -27
- package/config/model-router/catalog.template.json +126 -52
- 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 +97 -75
- package/config/model-router/qualification-contract.json +124 -0
- package/config/model-router/routing-eval-cases.json +275 -0
- package/config/model-router/routing-policy.template.json +76 -0
- package/config/model-router/weekly-analyst-instruction.md +60 -0
- package/data/model-catalog.json +44 -49
- package/package.json +6 -3
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/mcp/managed-cli-interface.mjs +9 -4
- package/plugin/scripts/capability-claim-evidence.mjs +9 -2
- package/plugin/scripts/codex-hook-adapter.mjs +18 -9
- package/plugin/scripts/project-capture-queue.mjs +7 -1
- package/plugin/scripts/project-progression-hook.mjs +16 -7
- package/plugin/scripts/project-progression-outbox.mjs +96 -15
- package/plugin/scripts/project-progression-producer.mjs +12 -1
- package/plugin/scripts/project-progression-store.mjs +53 -1
- package/plugin/scripts/project-transition-hook.mjs +19 -8
- package/plugin/scripts/session-snapshot-hook.mjs +2 -1
- package/scripts/claude-terminal-mod.mjs +89 -0
- package/scripts/codex-hook-trust-reconcile.mjs +247 -0
- package/scripts/codex-routed.sh +3 -36
- package/scripts/goldie-weekly.sh +8 -64
- package/scripts/metaharness-router.mjs +7 -1
- package/scripts/model-analyst-sandbox.mjs +54 -0
- package/scripts/model-currency-evidence.mjs +139 -0
- package/scripts/model-currency.mjs +230 -0
- package/scripts/model-native-catalog.mjs +111 -0
- package/scripts/model-native-qualification.mjs +251 -0
- package/scripts/model-router-agent-hook.mjs +136 -0
- package/scripts/model-router-dispatch.mjs +161 -0
- package/scripts/model-router-engine.mjs +155 -104
- package/scripts/model-routing-eval.mjs +108 -0
- package/scripts/model-routing-gateway.mjs +453 -0
- package/scripts/model-routing-launchers.mjs +209 -0
- package/scripts/model-routing-policy-promotion.mjs +203 -0
- package/scripts/model-terminal-gateway.mjs +310 -0
- package/scripts/model-terminal-launchers.mjs +300 -0
- package/scripts/model-weekly-analyst.mjs +299 -0
- package/scripts/model-weekly-assessment.mjs +91 -0
- package/scripts/model-weekly-cycle.mjs +183 -0
- package/scripts/model-weekly-qualification.mjs +362 -0
- package/scripts/native-subscription-usage.mjs +57 -0
- package/scripts/release-qualification-contract.mjs +50 -1
- package/scripts/release-qualification.mjs +9 -6
- package/scripts/security-guidance-codex-compat.mjs +142 -0
- package/scripts/user-model-prompt-hook.mjs +69 -0
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// DISTINCT-FROM: scripts/model-router-dispatch.mjs — same-session native JSONL proxy, never a worker.
|
|
3
|
+
// Manual host adapter: Codex cliExecutable / Claude claudeProcessWrapper. No settings are installed.
|
|
4
|
+
// New turns fail closed. Active Codex additions require an exact approved pair; Claude packets defer.
|
|
5
|
+
// Approvals, tools, cancellation and shutdown retain native protocols.
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import os from 'node:os';
|
|
9
|
+
import crypto from 'node:crypto';
|
|
10
|
+
import { StringDecoder } from 'node:string_decoder';
|
|
11
|
+
import { spawn, execFileSync } from 'node:child_process';
|
|
12
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
13
|
+
import { subscriptionEnvironment, assertSubscriptionAuth, validateDispatchDecision, loadNativeCodexModels } from './model-router-dispatch.mjs';
|
|
14
|
+
|
|
15
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const REFUSED = 'Current reviewed native model/effort allocation unavailable; new turn was not forwarded.';
|
|
17
|
+
const EFFORTS = new Set(['low', 'medium', 'high', 'xhigh', 'max']);
|
|
18
|
+
// Native inference surfaces without qualified execution-time routing are denied.
|
|
19
|
+
const UNQUALIFIED = new Set(['review/start', 'thread/queue/add', 'thread/queue/update', 'thread/queue/start',
|
|
20
|
+
'thread/realtime/start', 'thread/realtime/appendAudio', 'thread/realtime/appendSpeech', 'thread/realtime/appendText',
|
|
21
|
+
'thread/goal/set', 'thread/goal/create', 'thread/goal/resume', 'thread/compact/start', 'turn/addUserMessage', 'thread/startAeon']);
|
|
22
|
+
|
|
23
|
+
function canonicalCodexConfig(config) {
|
|
24
|
+
if (!config || (config.model_provider ?? 'openai') !== 'openai') return false;
|
|
25
|
+
// Native 0.160 gives serviceTierForTurn precedence over daemon/thread Fast defaults.
|
|
26
|
+
// routeCodexTurn supplies both standard overrides; provider/auth checks remain mandatory.
|
|
27
|
+
// A provider with the built-in name can still be replaced by a custom endpoint.
|
|
28
|
+
if (config.model_providers && Object.hasOwn(config.model_providers, 'openai') && config.model_providers.openai != null) return false;
|
|
29
|
+
if (config.chatgpt_base_url != null && config.chatgpt_base_url !== 'https://chatgpt.com/backend-api/') return false;
|
|
30
|
+
// Native Config serializes openai_base_url:null when the override is unset.
|
|
31
|
+
return !Object.entries(config).some(([key, value]) => value != null && /^(model_providers\.openai|base_url|baseUrl|openai_base_url)(\.|$)/.test(key));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Fresh policy-only subprocess: prompt stays on stdin, never argv, receipts or diagnostics. */
|
|
35
|
+
export function decideNativeTurn(prompt, harness, { env = process.env, spawnEngine = spawn, timeoutMs = 4000, multimodal = false } = {}) {
|
|
36
|
+
return new Promise((resolve, reject) => {
|
|
37
|
+
const child = spawnEngine(process.execPath, [env.MODEL_ROUTER_ENGINE || path.join(HERE, 'model-router-engine.mjs'),
|
|
38
|
+
'--harness', harness, '--policy-only', '--json', ...(multimodal ? ['--request-json'] : [])], { env, shell: false, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
39
|
+
let stdout = '', done = false;
|
|
40
|
+
const finish = (error, decision) => {
|
|
41
|
+
if (done) return;
|
|
42
|
+
done = true; clearTimeout(timer);
|
|
43
|
+
if (error) child.kill();
|
|
44
|
+
error ? reject(error) : resolve(decision);
|
|
45
|
+
};
|
|
46
|
+
const timer = setTimeout(() => finish(new Error(REFUSED)), timeoutMs);
|
|
47
|
+
child.stdout.on('data', (chunk) => { stdout += chunk; if (stdout.length > 131072) finish(new Error(REFUSED)); });
|
|
48
|
+
child.stderr.on('data', () => {});
|
|
49
|
+
child.once('error', () => finish(new Error(REFUSED)));
|
|
50
|
+
child.stdin.on('error', () => finish(new Error(REFUSED)));
|
|
51
|
+
child.once('exit', (code) => {
|
|
52
|
+
try { if (code !== 0) throw new Error(); finish(null, JSON.parse(stdout)); }
|
|
53
|
+
catch { finish(new Error(REFUSED)); }
|
|
54
|
+
});
|
|
55
|
+
child.stdin.end(multimodal ? JSON.stringify({ prompt: harness === 'claude-code' ? 'Unresolved architecture uncertainty: unclassified multimodal input' : prompt || 'Unclassified multimodal input', taskFacts: { uncertainty: 'architecture' } }) : prompt);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function nativeGatewayLaunch({ harness, realBinary, args = [], env = process.env } = {}) {
|
|
60
|
+
if (!['codex', 'claude-code'].includes(harness) || !path.isAbsolute(realBinary || '')) {
|
|
61
|
+
throw new Error('Explicit absolute native host executable and supported harness required');
|
|
62
|
+
}
|
|
63
|
+
const binary = fs.realpathSync(realBinary);
|
|
64
|
+
if (binary === fs.realpathSync(fileURLToPath(import.meta.url)) || env.MODEL_ROUTER_GATEWAY_ACTIVE) {
|
|
65
|
+
throw new Error('Native routing gateway recursion refused');
|
|
66
|
+
}
|
|
67
|
+
const clean = subscriptionEnvironment(env);
|
|
68
|
+
clean.MODEL_ROUTER_GATEWAY_ACTIVE = '1';
|
|
69
|
+
const nativeArgs = [...args];
|
|
70
|
+
for (let index = 0; index < args.length; index++) {
|
|
71
|
+
const arg = args[index];
|
|
72
|
+
if (harness === 'codex' && (arg.startsWith('-c') || arg === '--config' || arg.startsWith('--config='))) {
|
|
73
|
+
const setting = arg.startsWith('--config=') ? arg.slice(9) : arg.startsWith('-c') && arg.length > 2 ? arg.slice(2).replace(/^=/, '') : args[++index];
|
|
74
|
+
const at = setting?.indexOf('=');
|
|
75
|
+
if (at == null || at < 0) throw new Error(REFUSED);
|
|
76
|
+
const key = setting.slice(0, at), raw = setting.slice(at + 1);
|
|
77
|
+
let value; try { value = JSON.parse(raw); } catch { value = raw; }
|
|
78
|
+
if (unsafeSettings({ [key]: value })) throw new Error(REFUSED);
|
|
79
|
+
}
|
|
80
|
+
if (harness === 'claude-code' && /^--settings(?:-file)?(?:=|$)/.test(arg)) {
|
|
81
|
+
const overlay = arg.includes('=') ? arg.slice(arg.indexOf('=') + 1) : args[++index];
|
|
82
|
+
let settings;
|
|
83
|
+
try { settings = JSON.parse(overlay.trim().startsWith('{') ? overlay : fs.readFileSync(overlay, 'utf8')); }
|
|
84
|
+
catch { throw new Error(REFUSED); }
|
|
85
|
+
if (unsafeSettings(settings)) throw new Error(REFUSED);
|
|
86
|
+
}
|
|
87
|
+
if (harness === 'codex' && /^(--profile(?:=|$)|-p)/.test(arg)) throw new Error(REFUSED);
|
|
88
|
+
if (/^--(?:betas|model-provider|api-key|base-url)(?:=|$)/.test(arg)) throw new Error(REFUSED);
|
|
89
|
+
}
|
|
90
|
+
if (harness === 'codex') {
|
|
91
|
+
if (!nativeArgs.includes('app-server')) throw new Error('Codex gateway requires native app-server mode');
|
|
92
|
+
nativeArgs.push('-c', 'model_provider="openai"', '-c', 'service_tier="default"', '-c', 'features.fast_mode=false');
|
|
93
|
+
} else if (!nativeArgs.includes('--input-format') || nativeArgs[nativeArgs.indexOf('--input-format') + 1] !== 'stream-json'
|
|
94
|
+
|| !nativeArgs.includes('--output-format') || nativeArgs[nativeArgs.indexOf('--output-format') + 1] !== 'stream-json') {
|
|
95
|
+
throw new Error('Claude gateway requires bidirectional native stream-json mode');
|
|
96
|
+
}
|
|
97
|
+
return { command: binary, args: nativeArgs, env: clean };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function appendGatewayReceipt(receipt, { env = process.env } = {}) {
|
|
101
|
+
const file = env.MODEL_ROUTER_GATEWAY_RECEIPTS || path.join(os.homedir(), '.claude', 'metaharness', 'native-turn-routing.jsonl');
|
|
102
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
103
|
+
const allowed = new Set(['ts', 'harness', 'status', 'model', 'effort', 'taskClass', 'modelObserved',
|
|
104
|
+
'serviceMode', 'allowanceVerified', 'reservation', 'evidence', 'classificationSource']);
|
|
105
|
+
const metadata = Object.fromEntries(Object.entries(receipt).filter(([key]) => allowed.has(key)));
|
|
106
|
+
fs.appendFileSync(file, JSON.stringify(metadata) + '\n', { mode: 0o600 });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function promptFor(message, harness) {
|
|
110
|
+
const content = harness === 'codex' ? message.params?.input : message.message?.content;
|
|
111
|
+
if (typeof content === 'string') return { prompt: content, multimodal: false };
|
|
112
|
+
if (!Array.isArray(content)) throw new Error(REFUSED);
|
|
113
|
+
const text = content.filter((part) => part?.type === 'text').map((part) => part.text);
|
|
114
|
+
if (text.some((part) => typeof part !== 'string')) throw new Error(REFUSED);
|
|
115
|
+
const multimodal = content.some((part) => ['image', 'localImage', 'image_url'].includes(part?.type));
|
|
116
|
+
if (!text.length && !multimodal) throw new Error(REFUSED);
|
|
117
|
+
return { prompt: text.join('\n'), multimodal };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Guard native provider/config overlays recursively, including flattened keys.
|
|
121
|
+
function unsafeSettings(value, prefix = '', routing = false) {
|
|
122
|
+
if (!value || typeof value !== 'object') return false;
|
|
123
|
+
return Object.entries(value).some(([key, entry]) => {
|
|
124
|
+
const name = prefix ? `${prefix}.${key}` : key;
|
|
125
|
+
if (/^(model_providers|apiKeyHelper|api_key|apiKey|base_url|baseUrl|openai_base_url|chatgpt_base_url|auth_token|authToken|customHeaders|env|modelSettings|alwaysThinkingEnabled|maxEffortLevel)(\.|$)/.test(name)) return true;
|
|
126
|
+
if (/^(model_provider|modelProvider)$/.test(name) && entry !== 'openai') return true;
|
|
127
|
+
if (/^(service_tier|serviceTier|service_tier_for_turn|serviceTierForTurn)$/.test(name) && entry != null && entry !== 'default') return true;
|
|
128
|
+
if (/^(features\.fast_mode|fastMode)$/.test(name) && entry !== false) return true;
|
|
129
|
+
if (routing && /^(model|effort|effortLevel|reasoning_effort|collaborationMode)(\.|$)/.test(name)) return true;
|
|
130
|
+
return unsafeSettings(entry, name, routing);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function verifyNativeVision(decision, models = loadNativeCodexModels()) {
|
|
135
|
+
const modalities = models.find((model) => model.slug === decision.model)?.input_modalities;
|
|
136
|
+
if (Array.isArray(modalities) && !modalities.includes('image')) throw new Error(REFUSED);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Pure native override: preserve every context, tool, approval and collaboration instruction. */
|
|
140
|
+
export function routeCodexTurn(message, decision) {
|
|
141
|
+
const params = { ...message.params, model: decision.model, effort: decision.effort, serviceTier: 'default', serviceTierForTurn: 'default' };
|
|
142
|
+
if (params.collaborationMode) params.collaborationMode = { ...params.collaborationMode,
|
|
143
|
+
settings: { ...params.collaborationMode.settings, model: decision.model, reasoning_effort: decision.effort } };
|
|
144
|
+
return { ...message, params };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Injectable protocol transport; no inference or model implementation lives in the gateway. */
|
|
148
|
+
export function connectNativeGateway({ harness, child, input, output, diagnostics = process.stderr,
|
|
149
|
+
decide = decideNativeTurn, verifyDecision = validateDispatchDecision, checkAuth = () => {},
|
|
150
|
+
receipt = appendGatewayReceipt, timeoutMs = 5000, maxDeferredBytes = 64 * 1024 * 1024, tempRoot = os.tmpdir(), now = () => new Date().toISOString() } = {}) {
|
|
151
|
+
const nonce = crypto.randomUUID();
|
|
152
|
+
const pending = new Map(), initIds = new Set(), ownIds = new Set(), held = new Set();
|
|
153
|
+
const accepted = new Map(), starts = new Map(), inactiveWaiters = new Set(), boundedWaiters = new Set();
|
|
154
|
+
let serial = Promise.resolve(), ready = harness === 'codex', active = false, closed = false, serialNumber = 0;
|
|
155
|
+
let readiness = null, resolveReady;
|
|
156
|
+
let queuedBytes = 0, spoolDirectory = null, spoolSequence = 0, declineUsers = false;
|
|
157
|
+
const spoolKey = crypto.randomBytes(32);
|
|
158
|
+
const waitInactive = () => !active ? Promise.resolve() : new Promise((resolve) => inactiveWaiters.add(resolve));
|
|
159
|
+
const packetFor = (token) => {
|
|
160
|
+
if (token.packet) return token.packet;
|
|
161
|
+
const bytes = fs.readFileSync(token.file);
|
|
162
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', spoolKey, bytes.subarray(0, 12));
|
|
163
|
+
decipher.setAuthTag(bytes.subarray(12, 28));
|
|
164
|
+
const packet = JSON.parse(Buffer.concat([decipher.update(bytes.subarray(28)), decipher.final()]).toString('utf8'));
|
|
165
|
+
fs.unlinkSync(token.file); token.file = null; token.packet = packet; return packet;
|
|
166
|
+
};
|
|
167
|
+
const retire = (token) => {
|
|
168
|
+
if (!held.delete(token)) return;
|
|
169
|
+
queuedBytes -= token.bytes;
|
|
170
|
+
if (token.file) { fs.rmSync(token.file, { force: true }); token.file = null; }
|
|
171
|
+
};
|
|
172
|
+
const retain = (line, message) => {
|
|
173
|
+
const token = { bytes: Buffer.byteLength(line), cancelled: false };
|
|
174
|
+
if (harness !== 'claude-code' || held.size < 32) token.packet = message;
|
|
175
|
+
else {
|
|
176
|
+
if (!spoolDirectory) { spoolDirectory = fs.mkdtempSync(path.join(tempRoot, 'native-routing-')); fs.chmodSync(spoolDirectory, 0o700); }
|
|
177
|
+
const nonce = crypto.randomBytes(12), cipher = crypto.createCipheriv('aes-256-gcm', spoolKey, nonce);
|
|
178
|
+
const encrypted = Buffer.concat([cipher.update(line, 'utf8'), cipher.final()]);
|
|
179
|
+
token.file = path.join(spoolDirectory, `${++spoolSequence}.bin`);
|
|
180
|
+
fs.writeFileSync(token.file, Buffer.concat([nonce, cipher.getAuthTag(), encrypted]), { mode: 0o600 });
|
|
181
|
+
}
|
|
182
|
+
held.add(token); queuedBytes += token.bytes; return token;
|
|
183
|
+
};
|
|
184
|
+
// Honor writable backpressure while preserving order and pausing its upstream.
|
|
185
|
+
const writer = (destination, source) => {
|
|
186
|
+
const queue = [], idleWaiters = []; let blocked = false, stopped = false, resuming = false;
|
|
187
|
+
const settle = () => { if (!queue.length && !blocked) idleWaiters.splice(0).forEach((resolve) => resolve()); };
|
|
188
|
+
const flush = () => {
|
|
189
|
+
while (!stopped && !blocked && !resuming && queue.length) {
|
|
190
|
+
const entry = queue.shift();
|
|
191
|
+
if (entry.token?.cancelled) { entry.resolve(false); continue; }
|
|
192
|
+
blocked = !destination.write(entry.line);
|
|
193
|
+
if (entry.token) { entry.token.dispatched = true; retire(entry.token); if (harness === 'claude-code') active = true; }
|
|
194
|
+
entry.resolve(true);
|
|
195
|
+
}
|
|
196
|
+
if (blocked) source.pause(); else if (!stopped) source.resume();
|
|
197
|
+
settle();
|
|
198
|
+
};
|
|
199
|
+
destination.on('drain', () => {
|
|
200
|
+
blocked = false; resuming = true; if (!stopped) source.resume();
|
|
201
|
+
setImmediate(() => { resuming = false; flush(); });
|
|
202
|
+
});
|
|
203
|
+
const write = (value, token) => new Promise((resolve) => {
|
|
204
|
+
if (stopped) { resolve(false); return; }
|
|
205
|
+
queue.push({ line: typeof value === 'string' ? value + '\n' : JSON.stringify(value) + '\n', token, resolve }); flush();
|
|
206
|
+
});
|
|
207
|
+
write.idle = () => !queue.length && !blocked ? Promise.resolve() : new Promise((resolve) => idleWaiters.push(resolve));
|
|
208
|
+
write.close = () => { stopped = true; queue.splice(0).forEach((entry) => entry.resolve(false)); blocked = false; settle(); };
|
|
209
|
+
return write;
|
|
210
|
+
};
|
|
211
|
+
const hostWriter = writer(child.stdin, input), writeClient = writer(output, child.stdout);
|
|
212
|
+
const writeHost = (value, token) => { if (closed) throw new Error(REFUSED); return hostWriter(value, token); };
|
|
213
|
+
const record = (status, decision, observed = false, extra = {}) => receipt({ ts: now(), harness, status,
|
|
214
|
+
...(decision ? { model: decision.model, effort: decision.effort, taskClass: decision.taskClass } : {}),
|
|
215
|
+
modelObserved: observed, ...(decision?.classificationSource ? { classificationSource: decision.classificationSource } : {}), ...extra });
|
|
216
|
+
const fail = (message, reason = REFUSED) => {
|
|
217
|
+
diagnostics.write(`[native-model-routing] ${reason}\n`);
|
|
218
|
+
if (harness === 'codex' && Object.hasOwn(message, 'id')) {
|
|
219
|
+
writeClient({ id: message.id, error: { code: -32001, message: reason } });
|
|
220
|
+
} else if (harness === 'claude-code' && message.type === 'control_request') {
|
|
221
|
+
writeClient({ type: 'control_response', response: { subtype: 'error', request_id: message.request_id, error: reason } });
|
|
222
|
+
} else if (harness === 'claude-code') {
|
|
223
|
+
writeClient({ type: 'result', subtype: 'error_during_execution', is_error: true,
|
|
224
|
+
errors: [reason], session_id: message.session_id || '', uuid: crypto.randomUUID(),
|
|
225
|
+
duration_ms: 0, duration_api_ms: 0, num_turns: 0, total_cost_usd: 0, usage: {}, modelUsage: {}, permission_denials: [] });
|
|
226
|
+
}
|
|
227
|
+
try { record('routing-refused'); } catch { /* diagnostics above are the fail-closed proof */ }
|
|
228
|
+
};
|
|
229
|
+
const waitReady = () => {
|
|
230
|
+
if (ready) return Promise.resolve();
|
|
231
|
+
if (!readiness) readiness = new Promise((resolve) => { resolveReady = resolve; });
|
|
232
|
+
return bounded(readiness);
|
|
233
|
+
};
|
|
234
|
+
function bounded(work) {
|
|
235
|
+
return new Promise((resolve, reject) => {
|
|
236
|
+
const finish = (error, value) => { clearTimeout(timer); boundedWaiters.delete(cancel); error ? reject(error) : resolve(value); };
|
|
237
|
+
const cancel = () => finish(new Error(REFUSED));
|
|
238
|
+
const timer = setTimeout(cancel, timeoutMs); boundedWaiters.add(cancel);
|
|
239
|
+
Promise.resolve(work).then((value) => finish(null, value), (error) => finish(error));
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
const control = (request) => new Promise((resolve, reject) => {
|
|
243
|
+
const id = `model-routing-gateway:${nonce}:${++serialNumber}`;
|
|
244
|
+
const timer = setTimeout(() => { pending.delete(id); reject(new Error(REFUSED)); }, timeoutMs);
|
|
245
|
+
ownIds.add(id);
|
|
246
|
+
pending.set(id, { resolve, reject, timer });
|
|
247
|
+
try {
|
|
248
|
+
writeHost(harness === 'codex' ? { id, ...request }
|
|
249
|
+
: { type: 'control_request', request_id: id, request });
|
|
250
|
+
} catch (error) { clearTimeout(timer); pending.delete(id); reject(error); }
|
|
251
|
+
});
|
|
252
|
+
async function route(token) {
|
|
253
|
+
let message;
|
|
254
|
+
const assertLive = () => { if (closed || token.cancelled) throw new Error('cancelled'); };
|
|
255
|
+
try {
|
|
256
|
+
message = packetFor(token);
|
|
257
|
+
await waitReady();
|
|
258
|
+
assertLive();
|
|
259
|
+
// Active Claude packets become unchanged next-turn input after the current result.
|
|
260
|
+
if (harness === 'claude-code' && active) {
|
|
261
|
+
try { record('active-input-deferred'); } catch { diagnostics.write('[native-model-routing] Deferred-input receipt unavailable; current work continues.\n'); }
|
|
262
|
+
await waitInactive(); assertLive();
|
|
263
|
+
}
|
|
264
|
+
const facts = promptFor(message, harness);
|
|
265
|
+
const decision = await bounded(decide(facts.prompt, harness, { multimodal: facts.multimodal }));
|
|
266
|
+
assertLive();
|
|
267
|
+
if (facts.multimodal && (decision.taskClass !== 'hard' || decision.effort !== 'high')) throw new Error(REFUSED);
|
|
268
|
+
if (facts.multimodal) decision.classificationSource = 'multimodal-uncertainty';
|
|
269
|
+
if (!decision?.subscriptionCovered || decision.harness !== harness || !EFFORTS.has(decision.effort)
|
|
270
|
+
|| !/^[a-zA-Z0-9][a-zA-Z0-9._-]+$/.test(decision.model || '')) throw new Error(REFUSED);
|
|
271
|
+
await bounded(verifyDecision(decision));
|
|
272
|
+
if (facts.multimodal && harness === 'codex' && verifyDecision === validateDispatchDecision) {
|
|
273
|
+
verifyNativeVision(decision);
|
|
274
|
+
}
|
|
275
|
+
assertLive();
|
|
276
|
+
await bounded(checkAuth(harness));
|
|
277
|
+
assertLive();
|
|
278
|
+
if (harness === 'codex') {
|
|
279
|
+
if (message.method !== 'turn/start') {
|
|
280
|
+
const pair = accepted.get(message.params?.threadId);
|
|
281
|
+
if (!pair || pair.turnId !== message.params?.expectedTurnId || pair.model !== decision.model || pair.effort !== decision.effort) {
|
|
282
|
+
fail(message, 'Input not submitted: requires a new turn with reviewed allocation; current work continues.'); return;
|
|
283
|
+
}
|
|
284
|
+
record('active-input-approved', decision, false, { evidence: 'native-turn-start.accepted-configured-pair' });
|
|
285
|
+
await writeHost(message, token); return;
|
|
286
|
+
}
|
|
287
|
+
const account = await control({ method: 'account/read', params: { refreshToken: false } });
|
|
288
|
+
assertLive();
|
|
289
|
+
if (account?.account?.type !== 'chatgpt') throw new Error(REFUSED);
|
|
290
|
+
const thread = await control({ method: 'thread/read', params: { threadId: message.params?.threadId, includeTurns: false } });
|
|
291
|
+
assertLive();
|
|
292
|
+
if (thread?.thread?.id !== message.params?.threadId || thread.thread.modelProvider !== 'openai') throw new Error(REFUSED);
|
|
293
|
+
const cwd = message.params?.cwd ?? thread.thread.cwd;
|
|
294
|
+
if (typeof cwd !== 'string' || !path.isAbsolute(cwd)) throw new Error(REFUSED);
|
|
295
|
+
const effective = await control({ method: 'config/read', params: { includeLayers: false, cwd } });
|
|
296
|
+
assertLive();
|
|
297
|
+
if (!canonicalCodexConfig(effective?.config)) throw new Error(REFUSED);
|
|
298
|
+
const allowance = await control({ method: 'account/rateLimits/read', params: { excludeResetCreditDetails: true, supportsLunaReserve: false } });
|
|
299
|
+
assertLive();
|
|
300
|
+
if (allowance?.ordinaryUsageAllowed !== true) throw new Error(REFUSED);
|
|
301
|
+
record('turn-ready', decision, false, { serviceMode: 'standard', allowanceVerified: true, reservation: false });
|
|
302
|
+
starts.set(message.id, { threadId: message.params?.threadId, model: decision.model, effort: decision.effort });
|
|
303
|
+
if (await writeHost(routeCodexTurn(message, decision), token)) {
|
|
304
|
+
record('turn-forwarded', decision, false, { serviceMode: 'standard', allowanceVerified: true, reservation: false });
|
|
305
|
+
}
|
|
306
|
+
} else {
|
|
307
|
+
if (!['low', 'medium', 'high', 'xhigh'].includes(decision.effort)) throw new Error(REFUSED);
|
|
308
|
+
await control({ subtype: 'apply_flag_settings', settings: { model: decision.model, effortLevel: decision.effort } });
|
|
309
|
+
assertLive();
|
|
310
|
+
const settings = await control({ subtype: 'get_settings' });
|
|
311
|
+
assertLive();
|
|
312
|
+
if (settings?.applied?.model !== decision.model || settings?.applied?.effort !== decision.effort) throw new Error(REFUSED);
|
|
313
|
+
record('turn-ready', decision, true, { evidence: 'native-get_settings.applied' });
|
|
314
|
+
if (await writeHost(message, token)) record('turn-forwarded', decision, true, { evidence: 'native-get_settings.applied' });
|
|
315
|
+
}
|
|
316
|
+
} catch {
|
|
317
|
+
if (!message && !closed && !token.cancelled) diagnostics.write('[native-model-routing] Deferred transport integrity unavailable; packet not submitted; current work continues.\n');
|
|
318
|
+
if (!closed && !token.cancelled && !token.dispatched && message && !(harness === 'claude-code' && active)) fail(message);
|
|
319
|
+
else if (token.dispatched) diagnostics.write('[native-model-routing] Post-dispatch receipt unavailable; native work continues.\n');
|
|
320
|
+
}
|
|
321
|
+
finally { retire(token); }
|
|
322
|
+
}
|
|
323
|
+
const listenLines = (stream, handler) => {
|
|
324
|
+
let buffer = ''; const decoder = new StringDecoder('utf8');
|
|
325
|
+
const pump = () => {
|
|
326
|
+
let split;
|
|
327
|
+
while ((split = buffer.indexOf('\n')) !== -1) {
|
|
328
|
+
const line = buffer.slice(0, split); buffer = buffer.slice(split + 1);
|
|
329
|
+
handler(line);
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
stream.on('data', (chunk) => { buffer += decoder.write(chunk); pump(); });
|
|
333
|
+
stream.on('resume', () => { if (buffer) setImmediate(pump); });
|
|
334
|
+
stream.on('end', () => { buffer += decoder.end(); if (buffer) handler(buffer); });
|
|
335
|
+
};
|
|
336
|
+
listenLines(input, (line) => {
|
|
337
|
+
if (closed) return;
|
|
338
|
+
let message;
|
|
339
|
+
try { message = JSON.parse(line); } catch { if (!closed) writeHost(line); return; }
|
|
340
|
+
if (harness === 'claude-code' && message.type === 'control_request' && message.request?.subtype === 'initialize') initIds.add(message.request_id);
|
|
341
|
+
if (harness === 'codex' && UNQUALIFIED.has(message.method)) {
|
|
342
|
+
fail(message, 'Native inference method is not qualified for gateway routing; request not forwarded.'); return;
|
|
343
|
+
}
|
|
344
|
+
const routable = harness === 'codex' ? ['turn/start', 'turn/steer'].includes(message.method) : message.type === 'user';
|
|
345
|
+
if (routable && harness === 'claude-code' && (declineUsers || queuedBytes + Buffer.byteLength(line) > maxDeferredBytes)) {
|
|
346
|
+
declineUsers = true;
|
|
347
|
+
const reason = 'Deferred input capacity (64 MiB) exceeded; new user packet not submitted; current work continues.';
|
|
348
|
+
diagnostics.write(`[native-model-routing] ${reason}\n`);
|
|
349
|
+
try { record('deferred-capacity-refused'); } catch { /* keep parsing permission/cancel controls */ }
|
|
350
|
+
if (!active) fail(message, reason);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
const cancel = harness === 'codex' ? message.method === 'turn/interrupt' : message.request?.subtype === 'interrupt';
|
|
354
|
+
const shutdown = message.method === 'shutdown';
|
|
355
|
+
if (shutdown || (cancel && (harness === 'codex' || !active))) {
|
|
356
|
+
for (const token of held) {
|
|
357
|
+
if (!token.cancelled && (shutdown || harness !== 'codex' || !message.params?.threadId || message.params.threadId === token.packet?.params?.threadId)) {
|
|
358
|
+
token.cancelled = true; fail(packetFor(token), 'Native turn cancelled before dispatch.');
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
let unsafe = false;
|
|
363
|
+
if (harness === 'codex') {
|
|
364
|
+
const params = message.params || {};
|
|
365
|
+
unsafe = unsafeSettings(params.config) || unsafeSettings({ modelProvider: params.modelProvider ?? 'openai',
|
|
366
|
+
serviceTier: params.serviceTier, service_tier: params.service_tier,
|
|
367
|
+
serviceTierForTurn: params.serviceTierForTurn, service_tier_for_turn: params.service_tier_for_turn });
|
|
368
|
+
if (message.method === 'account/login/start' && !['chatgpt', 'chatgptDeviceCode', 'chatgptAuthTokens'].includes(params.type)) unsafe = true;
|
|
369
|
+
if (['thread/settings/update', 'turn/settings/update'].includes(message.method)) unsafe ||= unsafeSettings(params, '', true);
|
|
370
|
+
const edits = message.method === 'config/value/write' ? [params] : message.method === 'config/batchWrite' ? params.edits || [] : [];
|
|
371
|
+
if (message.method === 'config/batchWrite' && params.reloadUserConfig === true) unsafe = true;
|
|
372
|
+
unsafe ||= edits.some((edit) => unsafeSettings({ [edit.keyPath]: edit.value }, '', true));
|
|
373
|
+
} else if (message.type === 'control_request') {
|
|
374
|
+
const request = message.request || {};
|
|
375
|
+
unsafe = ['set_model', 'set_max_thinking_tokens'].includes(request.subtype)
|
|
376
|
+
|| (['apply_flag_settings', 'update_settings'].includes(request.subtype) && unsafeSettings(request.settings || request, '', true));
|
|
377
|
+
}
|
|
378
|
+
if (unsafe) { fail(message, 'Native allocation/provider override refused; reviewed gateway routing owns model and effort.'); return; }
|
|
379
|
+
if (routable) {
|
|
380
|
+
try { const token = retain(line, message); serial = serial.then(() => route(token)); }
|
|
381
|
+
catch { diagnostics.write('[native-model-routing] Deferred transport storage unavailable; packet not submitted; current work continues.\n'); if (!active) fail(message); }
|
|
382
|
+
}
|
|
383
|
+
else {
|
|
384
|
+
if (!closed) writeHost(line);
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
listenLines(child.stdout, (line) => {
|
|
388
|
+
let message;
|
|
389
|
+
try { message = JSON.parse(line); } catch { writeClient(line); return; }
|
|
390
|
+
const id = harness === 'codex' ? message.id : message.type === 'control_response' ? message.response?.request_id : null;
|
|
391
|
+
if (pending.has(id)) {
|
|
392
|
+
const waiter = pending.get(id); pending.delete(id); clearTimeout(waiter.timer);
|
|
393
|
+
const failed = harness === 'codex' ? message.error : message.response?.subtype !== 'success';
|
|
394
|
+
failed ? waiter.reject(new Error(REFUSED)) : waiter.resolve(harness === 'codex' ? message.result : message.response.response);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
if (starts.has(id)) {
|
|
398
|
+
const pair = starts.get(id); starts.delete(id);
|
|
399
|
+
if (!message.error && typeof message.result?.turn?.id === 'string' && message.result.turn.id) accepted.set(pair.threadId, { ...pair, turnId: message.result.turn.id });
|
|
400
|
+
}
|
|
401
|
+
if (message.method === 'turn/completed' && accepted.get(message.params?.threadId)?.turnId === message.params?.turn?.id) accepted.delete(message.params.threadId);
|
|
402
|
+
if (ownIds.has(id)) return; // late own-control replies never escape into the host client
|
|
403
|
+
if (initIds.has(id) && message.response?.subtype === 'success') {
|
|
404
|
+
ready = true; active = message.response.response?.session_state && message.response.response.session_state !== 'idle'; resolveReady?.();
|
|
405
|
+
}
|
|
406
|
+
if (harness === 'claude-code' && ['assistant', 'stream_event'].includes(message.type)) active = true;
|
|
407
|
+
if (harness === 'claude-code' && message.type === 'result') { active = false; inactiveWaiters.forEach((resolve) => resolve()); inactiveWaiters.clear(); }
|
|
408
|
+
writeClient(line);
|
|
409
|
+
});
|
|
410
|
+
child.stderr?.on('data', (chunk) => diagnostics.write(chunk));
|
|
411
|
+
const close = () => {
|
|
412
|
+
if (closed) return;
|
|
413
|
+
closed = true; hostWriter.close(); inactiveWaiters.forEach((resolve) => resolve()); inactiveWaiters.clear();
|
|
414
|
+
boundedWaiters.forEach((cancel) => cancel()); boundedWaiters.clear(); accepted.clear(); starts.clear();
|
|
415
|
+
for (const token of [...held]) { token.cancelled = true; retire(token); }
|
|
416
|
+
if (spoolDirectory) fs.rmSync(spoolDirectory, { recursive: true, force: true });
|
|
417
|
+
spoolKey.fill(0);
|
|
418
|
+
for (const waiter of pending.values()) { clearTimeout(waiter.timer); waiter.reject(new Error(REFUSED)); }
|
|
419
|
+
pending.clear();
|
|
420
|
+
};
|
|
421
|
+
child.once('error', close); child.once('exit', close); child.stdin.on('error', close); child.stdout.on('error', close);
|
|
422
|
+
const disconnect = () => { close(); if (!child.stdin.destroyed) child.stdin.end(); };
|
|
423
|
+
input.on('end', disconnect); input.on('error', disconnect); input.on('close', disconnect);
|
|
424
|
+
return { idle: () => serial, close, spoolState: () => ({ directory: spoolDirectory, queuedBytes }) };
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
export function parseGatewayInvocation(argv, env = process.env) {
|
|
428
|
+
const args = [...argv];
|
|
429
|
+
let harness, realBinary;
|
|
430
|
+
if (args[0] === '--harness') { harness = args[1]; args.splice(0, 2); }
|
|
431
|
+
if (['--real-binary', '--executable'].includes(args[0])) { realBinary = args[1]; args.splice(0, 2); }
|
|
432
|
+
if (args[0] === '--') args.shift();
|
|
433
|
+
if (!harness) harness = args.includes('app-server') ? 'codex' : 'claude-code';
|
|
434
|
+
if (!realBinary) realBinary = harness === 'codex' ? env.MODEL_ROUTER_REAL_CODEX : args.shift();
|
|
435
|
+
return { harness, realBinary, args, env };
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
async function main(argv) {
|
|
439
|
+
const launch = nativeGatewayLaunch(parseGatewayInvocation(argv));
|
|
440
|
+
const harness = parseGatewayInvocation(argv).harness;
|
|
441
|
+
const auth = (host) => assertSubscriptionAuth(host, { env: launch.env,
|
|
442
|
+
probe: (_command, args, options) => execFileSync(launch.command, args, options) });
|
|
443
|
+
auth(harness);
|
|
444
|
+
const child = spawn(launch.command, launch.args, { env: launch.env, cwd: process.cwd(), shell: false, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
445
|
+
const gateway = connectNativeGateway({ harness, child, input: process.stdin, output: process.stdout,
|
|
446
|
+
decide: (prompt, host, metadata) => decideNativeTurn(prompt, host, { env: launch.env, ...metadata }), checkAuth: auth });
|
|
447
|
+
for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP']) process.on(signal, () => { gateway.close(); child.kill(signal); });
|
|
448
|
+
child.once('exit', (code, signal) => { process.stdin.pause(); process.stdin.unref?.(); process.exitCode = code ?? (signal ? 1 : 0); });
|
|
449
|
+
child.once('error', () => { process.stderr.write('[native-model-routing] Native host transport unavailable.\n'); process.exitCode = 1; });
|
|
450
|
+
}
|
|
451
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
452
|
+
main(process.argv.slice(2)).catch(() => { process.stderr.write('[native-model-routing] Explicit native executable or subscription authentication unavailable.\n'); process.exitCode = 1; });
|
|
453
|
+
}
|