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,251 @@
|
|
|
1
|
+
// Native subscription qualification: fixed plaintext tasks, never candidate code execution.
|
|
2
|
+
// Protocol sequence derives from the source-bound native gateway acceptance probe.
|
|
3
|
+
// Configuration identity is observable; provider/backend identity is never proved here.
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import crypto from 'node:crypto';
|
|
8
|
+
import { spawn, execFileSync } from 'node:child_process';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import { StringDecoder } from 'node:string_decoder';
|
|
11
|
+
import { assertSubscriptionAuth, subscriptionEnvironment } from './model-router-dispatch.mjs';
|
|
12
|
+
import { discoverNativeCodex } from './model-routing-launchers.mjs';
|
|
13
|
+
|
|
14
|
+
const SELF = fileURLToPath(import.meta.url);
|
|
15
|
+
const digest = (bytes) => crypto.createHash('sha256').update(bytes).digest('hex');
|
|
16
|
+
const EFFORTS = new Set(['low', 'medium', 'high', 'xhigh', 'max']);
|
|
17
|
+
const ID = /^[a-z][a-z0-9]*(?:[-.][a-z0-9]+)*$/;
|
|
18
|
+
const redact = (key, value) => /^(?:email|account_id|access_token|refresh_token|id_token|api_key|authorization|credentials)$/i.test(key) ? '[redacted]' : value;
|
|
19
|
+
|
|
20
|
+
function binaryFor(host) {
|
|
21
|
+
const extensions = path.join(os.homedir(), '.vscode-server/extensions');
|
|
22
|
+
if (host === 'codex') return discoverNativeCodex(extensions);
|
|
23
|
+
const entries = JSON.parse(fs.readFileSync(path.join(extensions, 'extensions.json')));
|
|
24
|
+
const entry = entries.find((e) => e.identifier?.id === 'anthropic.claude-code');
|
|
25
|
+
if (!entry) throw new Error('Native Claude extension unavailable');
|
|
26
|
+
return path.join(entry.location.path, 'resources/native-binary', process.platform === 'win32' ? 'claude.exe' : 'claude');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function verifyClaudeAllowance(usage, model) {
|
|
30
|
+
// Native 2.1.289 get_usage is explicitly experimental. Unknown shapes fail closed.
|
|
31
|
+
if (usage?.rate_limits_available !== true || usage.rate_limits?.extra_usage?.is_enabled !== false) throw new Error('Native Claude allowance unknown or extra usage enabled');
|
|
32
|
+
const limits = usage.rate_limits;
|
|
33
|
+
for (const key of ['five_hour', 'seven_day']) {
|
|
34
|
+
const window = limits[key];
|
|
35
|
+
if (!window || !Number.isFinite(window.utilization) || window.utilization < 0 || window.utilization >= 100 || window.locked_reason !== null) throw new Error('Native Claude ordinary allowance unavailable');
|
|
36
|
+
}
|
|
37
|
+
for (const scoped of limits.model_scoped || []) {
|
|
38
|
+
if (!Number.isFinite(scoped.utilization) || scoped.utilization < 0 || scoped.utilization >= 100 || scoped.locked_reason != null) throw new Error('Native Claude scoped allowance unavailable or unmapped');
|
|
39
|
+
}
|
|
40
|
+
for (const limit of limits.limits || []) {
|
|
41
|
+
if (!Number.isFinite(limit.percent) || limit.percent < 0 || limit.percent >= 100 || !['normal', 'warning'].includes(limit.severity)) throw new Error('Native Claude allowance unavailable or unknown');
|
|
42
|
+
}
|
|
43
|
+
return { verified: true, basis: 'native-get_usage-2.1.289', checkedAt: new Date().toISOString(), reservation: false, raceSafe: false, model };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
class Transport {
|
|
47
|
+
constructor(child, host, fail, transcript, signal) {
|
|
48
|
+
this.child = child; this.host = host; this.fail = fail; this.transcript = transcript;
|
|
49
|
+
this.pending = new Map(); this.events = []; this.waiters = []; this.serial = 0; this.buffer = ''; this.bytes = 0; this.decoder = new StringDecoder('utf8');
|
|
50
|
+
child.stderr.on('data', () => {}); child.stdin.on('error', () => fail(new Error('Native input transport failed')));
|
|
51
|
+
child.once('error', () => fail(new Error('Native executable unavailable')));
|
|
52
|
+
child.once('exit', () => { if (!signal.aborted) fail(new Error('Native host exited before qualification')); });
|
|
53
|
+
child.stdout.on('data', (chunk) => {
|
|
54
|
+
this.bytes += chunk.length; if (this.bytes > 512 * 1024) return fail(new Error('Native transcript exceeded bound'));
|
|
55
|
+
this.buffer += this.decoder.write(chunk);
|
|
56
|
+
let at;
|
|
57
|
+
while ((at = this.buffer.indexOf('\n')) >= 0) {
|
|
58
|
+
const line = this.buffer.slice(0, at); this.buffer = this.buffer.slice(at + 1);
|
|
59
|
+
let message; try { message = JSON.parse(line); } catch { return fail(new Error('Malformed native protocol output')); }
|
|
60
|
+
transcript.push({ direction: 'native', packet: message });
|
|
61
|
+
const id = host === 'codex' ? message.id : message.type === 'control_response' ? message.response?.request_id : null;
|
|
62
|
+
const pending = this.pending.get(id);
|
|
63
|
+
if (pending) {
|
|
64
|
+
this.pending.delete(id);
|
|
65
|
+
if (message.error || (host === 'claude-code' && message.response.subtype !== 'success')) pending.reject(new Error('Native request refused'));
|
|
66
|
+
else pending.resolve(host === 'codex' ? message.result : message.response.response);
|
|
67
|
+
} else {
|
|
68
|
+
// No native tool/approval request is ever serviced by this adapter.
|
|
69
|
+
const tool = host === 'codex' ? Object.hasOwn(message, 'id') && Boolean(message.method)
|
|
70
|
+
: message.type === 'control_request' || message.message?.content?.some((p) => p.type === 'tool_use');
|
|
71
|
+
const item = message.params?.item;
|
|
72
|
+
if (tool || (item?.type && !['agentMessage', 'reasoning', 'userMessage'].includes(item.type))) return fail(new Error('Native tool inference refused'));
|
|
73
|
+
this.events.push(message);
|
|
74
|
+
for (const waiter of [...this.waiters]) if (waiter.predicate(message)) { this.waiters.splice(this.waiters.indexOf(waiter), 1); waiter.resolve(message); }
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
send(packet) { this.transcript.push({ direction: 'client', packet }); this.child.stdin.write(`${JSON.stringify(packet)}\n`); }
|
|
80
|
+
request(method, params = {}) {
|
|
81
|
+
const id = `qualification-${++this.serial}`;
|
|
82
|
+
const promise = new Promise((resolve, reject) => this.pending.set(id, { resolve, reject }));
|
|
83
|
+
this.send(this.host === 'codex' ? { id, method, params } : { type: 'control_request', request_id: id, request: { subtype: method, ...params } });
|
|
84
|
+
return promise;
|
|
85
|
+
}
|
|
86
|
+
wait(predicate, start = 0) {
|
|
87
|
+
const existing = this.events.slice(start).find(predicate); if (existing) return Promise.resolve(existing);
|
|
88
|
+
return new Promise((resolve) => this.waiters.push({ predicate, resolve }));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function run(options, inspectOnly) {
|
|
93
|
+
const { host, model, effort, prompt, cwd, env = process.env, deadline = Date.now() + 60000,
|
|
94
|
+
spawnHost = spawn, versionProbe = execFileSync, authCheck = assertSubscriptionAuth, now = Date.now } = options;
|
|
95
|
+
const started = now(); const transcript = []; let child, transport;
|
|
96
|
+
const result = { completed: false, output: '', nativeModel: null, nativeEffort: null, identityBasis: null,
|
|
97
|
+
backendIdentityProved: false, nativeSubscription: false, available: false, supported: false, models: [],
|
|
98
|
+
harnessVersion: null, elapsedMs: 0, nativeSessionId: null, nativeTurnId: null, sourceReceipt: {
|
|
99
|
+
schemaVersion: 1, kind: 'native-model-qualification', host, backendIdentityProved: false,
|
|
100
|
+
adapterSha256: digest(fs.readFileSync(SELF)), nativeSettings: {}, nativeTurn: null,
|
|
101
|
+
request: inspectOnly ? null : { model, effort, promptSha256: digest(String(prompt)) }, transcriptRedactions: ['account/auth identifying fields'],
|
|
102
|
+
} };
|
|
103
|
+
const controller = new AbortController(); let abortError;
|
|
104
|
+
const fail = (error) => { if (!controller.signal.aborted) { abortError = error; controller.abort(); } };
|
|
105
|
+
const remaining = () => { const ms = deadline - now(); if (ms <= 0) throw new Error('Shared native qualification deadline exhausted'); return ms; };
|
|
106
|
+
const bounded = (promise) => new Promise((resolve, reject) => {
|
|
107
|
+
if (controller.signal.aborted) return reject(abortError);
|
|
108
|
+
const stop = () => reject(abortError); controller.signal.addEventListener('abort', stop, { once: true });
|
|
109
|
+
Promise.resolve(promise).then(resolve, reject).finally(() => controller.signal.removeEventListener('abort', stop));
|
|
110
|
+
});
|
|
111
|
+
let timer;
|
|
112
|
+
try {
|
|
113
|
+
if (!['codex', 'claude-code'].includes(host) || !Number.isFinite(deadline)) throw new Error('Supported native host and absolute deadline required');
|
|
114
|
+
remaining(); timer = setTimeout(() => fail(new Error('Shared native qualification deadline exhausted')), remaining());
|
|
115
|
+
if (!inspectOnly && (!ID.test(model || '') || !EFFORTS.has(effort) || typeof prompt !== 'string' || !prompt.trim() || Buffer.byteLength(prompt) > 16384 || !path.isAbsolute(cwd || ''))) throw new Error('Static model/effort, bounded plaintext prompt and absolute sandbox cwd required');
|
|
116
|
+
const binary = fs.realpathSync(options.binary || binaryFor(host));
|
|
117
|
+
result.sourceReceipt.binary = binary;
|
|
118
|
+
// Exact native executable bytes, without retaining executable or credentials in the receipt.
|
|
119
|
+
const hash = crypto.createHash('sha256'); const stream = fs.createReadStream(binary);
|
|
120
|
+
const stopHash = () => stream.destroy(abortError);
|
|
121
|
+
controller.signal.addEventListener('abort', stopHash, { once: true });
|
|
122
|
+
try { await bounded(new Promise((resolve, reject) => { stream.on('data', (bytes) => { try { remaining(); hash.update(bytes); } catch (e) { stream.destroy(e); } }); stream.once('error', reject); stream.once('end', resolve); })); }
|
|
123
|
+
finally { controller.signal.removeEventListener('abort', stopHash); }
|
|
124
|
+
result.sourceReceipt.nativeBinarySha256 = hash.digest('hex');
|
|
125
|
+
const clean = subscriptionEnvironment(env);
|
|
126
|
+
result.harnessVersion = String(versionProbe(binary, ['--version'], { env: clean, encoding: 'utf8', timeout: remaining(), stdio: ['ignore', 'pipe', 'pipe'] })).trim().slice(0, 128);
|
|
127
|
+
await bounded(authCheck(host, { env: clean, probe: (_command, args, opts) => versionProbe(binary, args, { ...opts, timeout: remaining() }) }));
|
|
128
|
+
result.nativeSubscription = true;
|
|
129
|
+
const args = host === 'codex' ? ['app-server', '--strict-config', '-c', 'model_provider="openai"', '-c', 'service_tier="default"', '-c', 'features.fast_mode=false',
|
|
130
|
+
'-c', 'features.shell_tool=false', '-c', 'features.unified_exec=false', '-c', 'features.code_mode=false', '-c', 'features.code_mode_host=false', '-c', 'features.multi_agent=false', '-c', 'features.plugins=false', '-c', 'web_search="disabled"', '-c', 'mcp_servers={}']
|
|
131
|
+
: ['--print', '--verbose', '--input-format', 'stream-json', '--output-format', 'stream-json', '--include-partial-messages', '--tools', '', '--mcp-config', '{"mcpServers":{}}', '--strict-mcp-config', '--no-session-persistence'];
|
|
132
|
+
remaining(); child = spawnHost(binary, args, { env: clean, cwd: cwd || os.tmpdir(), shell: false, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
133
|
+
transport = new Transport(child, host, fail, transcript, controller.signal);
|
|
134
|
+
const metadata = (promise) => {
|
|
135
|
+
const timeout = setTimeout(() => fail(new Error(`Native metadata timeout: ${result.sourceReceipt.stage}`)), Math.min(10000, remaining()));
|
|
136
|
+
return bounded(promise).finally(() => clearTimeout(timeout));
|
|
137
|
+
};
|
|
138
|
+
const request = (...params) => { result.sourceReceipt.stage = params[0]; return metadata(transport.request(...params)); };
|
|
139
|
+
if (host === 'codex') {
|
|
140
|
+
await request('initialize', { clientInfo: { name: 'native_model_qualification', version: '1' }, capabilities: { experimentalApi: true } }); transport.send({ method: 'initialized' });
|
|
141
|
+
const account = await request('account/read', { refreshToken: false });
|
|
142
|
+
if (account?.account?.type !== 'chatgpt') throw new Error('Native same-process subscription account unavailable');
|
|
143
|
+
const allowance = await request('account/rateLimits/read', { excludeResetCreditDetails: true, supportsLunaReserve: false });
|
|
144
|
+
if (allowance?.ordinaryUsageAllowed !== true) throw new Error('Native ordinary subscription allowance unavailable');
|
|
145
|
+
result.sourceReceipt.allowance = { verified: true, basis: 'native-account/rateLimits/read', reservation: false, raceSafe: false };
|
|
146
|
+
let cursor;
|
|
147
|
+
do {
|
|
148
|
+
const catalog = await request('model/list', { ...(cursor ? { cursor } : {}), includeHidden: true });
|
|
149
|
+
if (!Array.isArray(catalog?.data)) throw new Error('Native model catalog unavailable');
|
|
150
|
+
result.models.push(...catalog.data.map((m) => ({ id: m.model, efforts: (m.supportedReasoningEfforts || []).map((e) => e.reasoningEffort), inputModalities: m.inputModalities })));
|
|
151
|
+
cursor = catalog.nextCursor;
|
|
152
|
+
if (result.models.length > 1000) throw new Error('Native catalog exceeded bound');
|
|
153
|
+
} while (cursor);
|
|
154
|
+
result.available = true; result.catalogStatus = 'verified';
|
|
155
|
+
if (!inspectOnly) {
|
|
156
|
+
if (!result.models.some((m) => m.id === model && m.efforts.includes(effort))) throw new Error('Native model/effort unsupported');
|
|
157
|
+
result.supported = true;
|
|
158
|
+
const thread = await request('thread/start', { cwd, model, modelProvider: 'openai', serviceTier: 'default', sandbox: 'read-only', approvalPolicy: 'untrusted', ephemeral: true, environments: [], dynamicTools: [],
|
|
159
|
+
developerInstructions: 'Qualification: plaintext answer only. Never use tools, read files, execute code, or delegate.' });
|
|
160
|
+
const threadId = thread?.thread?.id; if (!threadId) throw new Error('Native thread identity missing');
|
|
161
|
+
result.nativeSessionId = threadId;
|
|
162
|
+
const settings = async (params) => {
|
|
163
|
+
const start = transport.events.length;
|
|
164
|
+
await request('thread/settings/update', { threadId, ...params });
|
|
165
|
+
result.sourceReceipt.stage = 'thread/settings/updated';
|
|
166
|
+
const event = await metadata(transport.wait((m) => m.method === 'thread/settings/updated' && m.params?.threadId === threadId, start));
|
|
167
|
+
const s = event.params.threadSettings;
|
|
168
|
+
if (s?.model !== model || s.effort !== effort || s.modelProvider !== 'openai' || s.serviceTier !== 'default') throw new Error('Native configured model/effort/provider mismatch');
|
|
169
|
+
return { model: s.model, effort: s.effort, provider: s.modelProvider, serviceTier: s.serviceTier, threadId };
|
|
170
|
+
};
|
|
171
|
+
result.sourceReceipt.nativeSettings.before = await settings({ model, effort, serviceTier: 'default' });
|
|
172
|
+
const accepted = await request('turn/start', { threadId, model, effort, serviceTier: 'default', input: [{ type: 'text', text: prompt }] });
|
|
173
|
+
const turnId = accepted?.turn?.id; if (!turnId) throw new Error('Native turn identity missing');
|
|
174
|
+
result.sourceReceipt.stage = 'turn/completed';
|
|
175
|
+
const end = await bounded(transport.wait((m) => m.method === 'turn/completed' && m.params?.threadId === threadId && m.params?.turn?.id === turnId));
|
|
176
|
+
if (end.params.turn.status !== 'completed') throw new Error('Native turn did not complete');
|
|
177
|
+
const items = transport.events.filter((m) => m.method === 'item/completed' && m.params?.threadId === threadId && m.params?.turnId === turnId);
|
|
178
|
+
result.output = items.filter((m) => m.params.item?.type === 'agentMessage').map((m) => m.params.item.text || '').join('\n');
|
|
179
|
+
result.nativeTurnId = turnId;
|
|
180
|
+
// Native same-value updates emit no notification. Read actual live thread
|
|
181
|
+
// metadata after completion; never infer settings from a no-op acknowledgment.
|
|
182
|
+
const observed = (await request('thread/read', { threadId, includeTurns: false }))?.thread;
|
|
183
|
+
if (observed?.id !== threadId || observed.model !== model || observed.reasoningEffort !== effort || observed.modelProvider !== 'openai') throw new Error('Native completed thread model/effort/provider mismatch');
|
|
184
|
+
result.sourceReceipt.nativeSettings.after = { model: observed.model, effort: observed.reasoningEffort, provider: observed.modelProvider, threadId, basis: 'native-thread/read' };
|
|
185
|
+
if (Object.hasOwn(observed, 'serviceTier')) {
|
|
186
|
+
if (observed.serviceTier !== 'default') throw new Error('Native completed thread service tier mismatch');
|
|
187
|
+
result.sourceReceipt.nativeSettings.after.serviceTier = observed.serviceTier;
|
|
188
|
+
result.sourceReceipt.nativeSettings.after.serviceTierBasis = 'native-thread/read';
|
|
189
|
+
} else result.sourceReceipt.nativeSettings.after.serviceTierBasis = 'pre-turn-native-settings-and-fixed-host-configuration';
|
|
190
|
+
}
|
|
191
|
+
} else {
|
|
192
|
+
if (!/^2\.1\.289 \(Claude Code\)$/.test(result.harnessVersion)) throw new Error('Experimental Claude allowance contract version unsupported');
|
|
193
|
+
const initialized = await request('initialize');
|
|
194
|
+
if (initialized?.session_state !== 'idle') throw new Error('Native Claude session not idle');
|
|
195
|
+
result.sourceReceipt.allowance = verifyClaudeAllowance(await request('get_usage', { skip_behaviors: true }), model);
|
|
196
|
+
// Native initialize.models is the extension's supportedModels() source.
|
|
197
|
+
result.catalogStatus = Array.isArray(initialized.models) ? 'verified' : 'unknown';
|
|
198
|
+
for (const m of initialized.models || []) {
|
|
199
|
+
if (typeof m.resolvedModel !== 'string') throw new Error('Native Claude model catalog malformed');
|
|
200
|
+
if (!result.models.some((entry) => entry.id === m.resolvedModel)) result.models.push({ id: m.resolvedModel,
|
|
201
|
+
efforts: m.supportsEffort === true && Array.isArray(m.supportedEffortLevels) ? m.supportedEffortLevels.filter((e) => EFFORTS.has(e)) : [] });
|
|
202
|
+
}
|
|
203
|
+
result.available = true;
|
|
204
|
+
if (!inspectOnly) {
|
|
205
|
+
if (!result.models.some((m) => m.id === model && m.efforts.includes(effort))) throw new Error('Native Claude model/effort unsupported or unknown');
|
|
206
|
+
await request('apply_flag_settings', { settings: { model, effortLevel: effort } });
|
|
207
|
+
const settings = async () => {
|
|
208
|
+
const observed = (await request('get_settings'))?.applied;
|
|
209
|
+
if (observed?.model !== model || observed.effort !== effort) throw new Error('Native Claude configured model/effort mismatch');
|
|
210
|
+
return { model: observed.model, effort: observed.effort };
|
|
211
|
+
};
|
|
212
|
+
result.sourceReceipt.nativeSettings.before = await settings(); result.supported = true;
|
|
213
|
+
transport.send({ type: 'user', uuid: crypto.randomUUID(), session_id: '', message: { role: 'user', content: [{ type: 'text', text: prompt }] }, parent_tool_use_id: null });
|
|
214
|
+
result.sourceReceipt.stage = 'result';
|
|
215
|
+
const completion = await bounded(transport.wait((m) => m.type === 'result'));
|
|
216
|
+
if (completion.is_error || completion.subtype !== 'success' || !completion.uuid || !completion.session_id || !completion.usage || !(completion.duration_api_ms > 0) || !(completion.num_turns > 0)) throw new Error('Native Claude completion identity/usage unavailable');
|
|
217
|
+
result.output = completion.result || ''; result.nativeSessionId = completion.session_id; result.nativeTurnId = completion.uuid;
|
|
218
|
+
result.sourceReceipt.nativeSettings.after = await settings();
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (!inspectOnly) {
|
|
222
|
+
if (typeof result.output !== 'string' || !result.output.trim() || Buffer.byteLength(result.output) > 32768) throw new Error('Native plaintext output missing or exceeded bound');
|
|
223
|
+
result.completed = true; result.nativeModel = result.sourceReceipt.nativeSettings.after.model; result.nativeEffort = result.sourceReceipt.nativeSettings.after.effort; result.identityBasis = 'native-host-confirmed-configuration';
|
|
224
|
+
result.sourceReceipt.nativeTurn = { threadId: result.nativeSessionId, turnId: result.nativeTurnId, status: 'completed', toolEvents: [],
|
|
225
|
+
turnIdBasis: host === 'codex' ? 'native-turn.id' : 'native-result.uuid' };
|
|
226
|
+
result.sourceReceipt.stage = 'completed';
|
|
227
|
+
}
|
|
228
|
+
} catch (error) { result.failure = error.message; result.completed = false; result.output = ''; }
|
|
229
|
+
finally {
|
|
230
|
+
clearTimeout(timer); controller.abort();
|
|
231
|
+
if (child) {
|
|
232
|
+
await new Promise((resolve) => {
|
|
233
|
+
const grace = setTimeout(() => { child.kill('SIGKILL'); resolve(); }, Math.max(0, Math.min(100, deadline - now())));
|
|
234
|
+
child.once('close', () => { clearTimeout(grace); resolve(); });
|
|
235
|
+
child.stdin.end(); child.kill('SIGTERM');
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
result.elapsedMs = Math.max(0, now() - started); result.checkedAt = new Date().toISOString();
|
|
239
|
+
result.transcript = transcript.map((frame) => JSON.stringify(frame, redact)).join('\n');
|
|
240
|
+
if (Buffer.byteLength(result.transcript) > 1024 * 1024) {
|
|
241
|
+
result.completed = false; result.output = ''; result.failure = 'Archived native transcript exceeded bound';
|
|
242
|
+
result.transcript = ''; result.sourceReceipt.nativeTurn = null;
|
|
243
|
+
}
|
|
244
|
+
result.transcriptSha256 = digest(result.transcript); result.sourceReceipt.transcriptSha256 = result.transcriptSha256;
|
|
245
|
+
result.sourceReceipt.harnessVersion = result.harnessVersion;
|
|
246
|
+
}
|
|
247
|
+
return result;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export const runNativeQualification = (options) => run(options, false);
|
|
251
|
+
export const inspectNativeQualificationHost = (options) => run(options, true);
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Per-user PreToolUse boundary for native agent launches. Root owns host registration.
|
|
3
|
+
// Codex v2 spawn_agent model/reasoning_effort/fork_turns fields checked in official source and
|
|
4
|
+
// installed CLI. Claude's installed Agent schema lacks per-call effort and permits model aliases
|
|
5
|
+
// only; do not invent unsupported fields. Actual rewritten dispatch requires host acceptance proof.
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import os from 'node:os';
|
|
9
|
+
import { execFileSync } from 'node:child_process';
|
|
10
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
11
|
+
|
|
12
|
+
import { readCodexAllowance } from './native-subscription-usage.mjs';
|
|
13
|
+
|
|
14
|
+
const DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
const TASK_CLASSES = new Set(['fast', 'medium', 'substantial', 'hard', 'exceptional']);
|
|
16
|
+
const EFFORTS = new Set(['low', 'medium', 'high', 'xhigh', 'max']);
|
|
17
|
+
const CODEX_AGENT = /^(?:(?:functions|collaboration|multi_agent_v2)\.)?spawn_agent$/;
|
|
18
|
+
const CLAUDE_AGENT = /^(?:Agent|Task)$/;
|
|
19
|
+
|
|
20
|
+
function deny(reason) {
|
|
21
|
+
return { hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: reason } };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function advisory(reason) {
|
|
25
|
+
return { hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: reason } };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function loadScope(harness, { routerDir = process.env.MODEL_ROUTER_CONFIG_DIR || path.join(os.homedir(), '.claude', 'model-router') } = {}) {
|
|
29
|
+
try {
|
|
30
|
+
const profile = JSON.parse(fs.readFileSync(process.env.MODEL_ROUTER_PROFILE || path.join(routerDir, 'profile.json'), 'utf8'));
|
|
31
|
+
const selection = JSON.parse(fs.readFileSync(process.env.MODEL_ROUTER_SELECTION || path.join(routerDir, 'routing-policy.json'), 'utf8'));
|
|
32
|
+
const user = profile.nativeAgentRouting?.[harness];
|
|
33
|
+
const reviewed = selection.nativeAgentRouting?.[harness];
|
|
34
|
+
return { enabled: user?.enabled === true && reviewed?.enabled === true,
|
|
35
|
+
strict: user?.strict === true && reviewed?.strict === true };
|
|
36
|
+
} catch { return { enabled: false, strict: false }; }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function decideViaEngine(prompt, harness, { engine = process.env.MODEL_ROUTER_ENGINE || path.join(DIR, 'model-router-engine.mjs'),
|
|
40
|
+
run = execFileSync } = {}) {
|
|
41
|
+
return JSON.parse(run(process.execPath, [engine, '--harness', harness, '--policy-only', '--json'], {
|
|
42
|
+
input: prompt, encoding: 'utf8', timeout: 2000, maxBuffer: 131072,
|
|
43
|
+
// No policy errors or raw task text are emitted by this hook on child failure.
|
|
44
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
45
|
+
}));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function loadNativeModels(file = process.env.MODEL_ROUTER_NATIVE_MODELS ||
|
|
49
|
+
path.join(process.env.CODEX_HOME || path.join(os.homedir(), '.codex'), 'models_cache.json')) {
|
|
50
|
+
const cache = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
51
|
+
if (!Array.isArray(cache.models)) throw new Error('Native model support cache missing');
|
|
52
|
+
return cache.models;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function routeAgentLaunch(event, { harness, decide = decideViaEngine, nativeModels = loadNativeModels, scope = loadScope, parentState = null } = {}) {
|
|
56
|
+
const tool = event?.tool_name || event?.toolName;
|
|
57
|
+
const isAgent = harness === 'codex' ? CODEX_AGENT.test(tool || '') :
|
|
58
|
+
harness === 'claude-code' ? CLAUDE_AGENT.test(tool || '') : false;
|
|
59
|
+
if (!isAgent || (event.hook_event_name && event.hook_event_name !== 'PreToolUse')) return {};
|
|
60
|
+
const routingScope = scope(harness);
|
|
61
|
+
if (!routingScope.enabled) return {};
|
|
62
|
+
const refuse = (reason) => harness === 'codex' && routingScope.strict ? deny(reason) : advisory(reason);
|
|
63
|
+
const input = event.tool_input || event.toolInput;
|
|
64
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) return refuse('Agent launch arguments unavailable; use a classified managed worker.');
|
|
65
|
+
const prompt = harness === 'codex' ? input.message : input.prompt;
|
|
66
|
+
if (typeof prompt !== 'string' || !prompt.trim()) return refuse('Agent task unavailable for classification; use a classified managed worker.');
|
|
67
|
+
let decision;
|
|
68
|
+
try { decision = decide(prompt, harness); }
|
|
69
|
+
catch { return refuse('Current reviewed model/effort allocation unavailable; review routing policy before agent dispatch.'); }
|
|
70
|
+
if (!decision?.subscriptionCovered || decision.harness !== harness || !TASK_CLASSES.has(decision.taskClass) ||
|
|
71
|
+
!EFFORTS.has(decision.effort) || !/^[a-zA-Z0-9][a-zA-Z0-9._-]+$/.test(decision.model || '')) {
|
|
72
|
+
return refuse('Agent routing did not produce a qualified native subscription model and effort.');
|
|
73
|
+
}
|
|
74
|
+
if (harness === 'codex' && ['xhigh', 'max'].includes(decision.effort) &&
|
|
75
|
+
(decision.taskClass !== 'exceptional' || decision.effort !== 'xhigh' || !/^[a-z][a-z0-9-]{2,79}$/.test(decision.exceptionalReason || ''))) {
|
|
76
|
+
return refuse('Exceptional native agent effort requires an explicit named reason and qualified xhigh route.');
|
|
77
|
+
}
|
|
78
|
+
if (harness === 'claude-code') {
|
|
79
|
+
return advisory(`Native Claude Agent cannot enforce this reviewed ${decision.taskClass} model and ${decision.effort} effort through its current per-call schema. Use model-router-dispatch.mjs; do not add unsupported effort fields.`);
|
|
80
|
+
}
|
|
81
|
+
// Never convert all-history forks into context-free tasks to make overrides fit.
|
|
82
|
+
if ('fork_context' in input) return refuse('Legacy fork_context dispatch is unsupported by this v2 routing hook; preserve context through a supported launch.');
|
|
83
|
+
const fork = typeof input.fork_turns === 'string' ? input.fork_turns.trim().toLowerCase() : 'all';
|
|
84
|
+
if (fork === 'all' || !fork) {
|
|
85
|
+
// Parent evidence must come from a verified host adapter, never the requested tool arguments.
|
|
86
|
+
// Current native hook has no proven effective-effort field, so main supplies no parent evidence.
|
|
87
|
+
if (parentState?.observed === true && parentState.model === decision.model && parentState.effort === decision.effort) {
|
|
88
|
+
return advisory('Full-history fork already matches the reviewed model and effort; input and inherited context preserved.');
|
|
89
|
+
}
|
|
90
|
+
return refuse('Full-history agent forks inherit parent model and effort; a matching effective parent allocation is not verified. Routing will not discard context; use an explicitly authorized bounded fork or managed worker.');
|
|
91
|
+
}
|
|
92
|
+
if (fork !== 'none' && !/^[1-9]\d*$/.test(fork)) return refuse('Invalid fork_turns; routing will not alter the context boundary.');
|
|
93
|
+
let model;
|
|
94
|
+
try { model = nativeModels().find((m) => m.slug === decision.model); }
|
|
95
|
+
catch { return refuse('Native agent model support unavailable; refresh the host model cache before dispatch.'); }
|
|
96
|
+
if (!model || model.multi_agent_version !== 'v2' ||
|
|
97
|
+
!(model.supported_reasoning_levels || []).some((entry) => entry.effort === decision.effort)) {
|
|
98
|
+
return refuse('Reviewed model/effort is not supported by this native v2 agent host.');
|
|
99
|
+
}
|
|
100
|
+
// updatedInput replaces the COMPLETE object. No context, capabilities, auth, or task fields dropped.
|
|
101
|
+
return { hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'allow',
|
|
102
|
+
updatedInput: { ...input, model: decision.model, reasoning_effort: decision.effort },
|
|
103
|
+
...(decision.exceptionalReason ? { additionalContext: `Qualified exceptional reason: ${decision.exceptionalReason}` } : {}) } };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function main(argv) {
|
|
107
|
+
const at = argv.indexOf('--harness');
|
|
108
|
+
const harness = at < 0 ? null : argv[at + 1];
|
|
109
|
+
if (!['codex', 'claude-code'].includes(harness)) throw new Error('Explicit supported --harness required');
|
|
110
|
+
let event;
|
|
111
|
+
try { event = JSON.parse(fs.readFileSync(0, 'utf8')); }
|
|
112
|
+
catch {
|
|
113
|
+
const scope = loadScope(harness);
|
|
114
|
+
const output = !scope.enabled ? {} : harness === 'codex' && scope.strict
|
|
115
|
+
? deny('Malformed agent hook input; launch was not classified.') : advisory('Malformed agent hook input; launch was not classified.');
|
|
116
|
+
process.stdout.write(JSON.stringify(output) + '\n'); return;
|
|
117
|
+
}
|
|
118
|
+
let output = routeAgentLaunch(event, { harness });
|
|
119
|
+
if (harness === 'codex' && output.hookSpecificOutput?.permissionDecision === 'allow') {
|
|
120
|
+
try { await readCodexAllowance(); }
|
|
121
|
+
catch {
|
|
122
|
+
output = loadScope(harness).strict ? deny('Current native ordinary subscription allowance unavailable; no credit fallback authorized.')
|
|
123
|
+
: advisory('Current native ordinary subscription allowance unavailable; managed dispatch cannot proceed without fresh allowance.');
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// The host's normal transcript records its tool input. This hook persists no task text.
|
|
127
|
+
process.stdout.write(JSON.stringify(output) + '\n');
|
|
128
|
+
}
|
|
129
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
130
|
+
main(process.argv.slice(2)).catch(() => {
|
|
131
|
+
const args = process.argv.slice(2); const harness = args[args.indexOf('--harness') + 1];
|
|
132
|
+
const result = harness === 'codex' && loadScope(harness).strict ? deny('Agent routing hook failed; managed dispatch required.')
|
|
133
|
+
: advisory('Agent routing hook failed; managed dispatch remains the supported enforcement path.');
|
|
134
|
+
process.stdout.write(JSON.stringify(result) + '\n');
|
|
135
|
+
});
|
|
136
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Managed native worker launcher. A prompt hook advises; this boundary enforces launch arguments.
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import { spawn, execFileSync } from 'node:child_process';
|
|
7
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
8
|
+
|
|
9
|
+
import { assertCurrentSelection, selectionEvidenceStatus, loadSelection, loadCatalog, loadProfile, applyProfile, eligibleCandidates, TASK_CLASSES } from './model-router-engine.mjs';
|
|
10
|
+
|
|
11
|
+
import { readCodexAllowance } from './native-subscription-usage.mjs';
|
|
12
|
+
|
|
13
|
+
const DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const BILLING_ENV = /^(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)$/;
|
|
15
|
+
|
|
16
|
+
export function subscriptionEnvironment(env = process.env) {
|
|
17
|
+
return Object.fromEntries(Object.entries(env).filter(([key]) => !BILLING_ENV.test(key)));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function assertSubscriptionAuth(harness, { env = subscriptionEnvironment(), read = fs.readFileSync,
|
|
21
|
+
probe = execFileSync } = {}) {
|
|
22
|
+
if (harness === 'codex') {
|
|
23
|
+
let auth;
|
|
24
|
+
try { auth = JSON.parse(read(path.join(env.CODEX_HOME || path.join(os.homedir(), '.codex'), 'auth.json'), 'utf8')); }
|
|
25
|
+
catch { throw new Error('Native Codex subscription authentication unreadable; dispatch blocked'); }
|
|
26
|
+
if (!auth.tokens || auth.OPENAI_API_KEY || (auth.auth_mode && auth.auth_mode !== 'chatgpt')) {
|
|
27
|
+
throw new Error('Codex subscription OAuth authentication required; no API-key fallback');
|
|
28
|
+
}
|
|
29
|
+
} else if (harness === 'claude-code') {
|
|
30
|
+
let status;
|
|
31
|
+
try { status = JSON.parse(probe('claude', ['auth', 'status', '--json'], {
|
|
32
|
+
env, encoding: 'utf8', timeout: 10000, stdio: ['ignore', 'pipe', 'pipe'],
|
|
33
|
+
})); } catch { throw new Error('Native Claude subscription authentication unavailable; dispatch blocked'); }
|
|
34
|
+
if (!status.loggedIn || status.authMethod !== 'claude.ai' || status.apiProvider !== 'firstParty' || !status.subscriptionType) {
|
|
35
|
+
throw new Error('Claude native subscription authentication required; no API fallback');
|
|
36
|
+
}
|
|
37
|
+
} else throw new Error(`Unsupported harness: ${harness}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function loadNativeCodexModels() {
|
|
41
|
+
const file = process.env.MODEL_ROUTER_NATIVE_MODELS || path.join(process.env.CODEX_HOME || path.join(os.homedir(), '.codex'), 'models_cache.json');
|
|
42
|
+
try {
|
|
43
|
+
const cache = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
44
|
+
if (Array.isArray(cache.models)) return cache.models;
|
|
45
|
+
} catch { /* no inferred capability from API or catalog pricing */ }
|
|
46
|
+
throw new Error('Native Codex model support cache unavailable; refresh host metadata before dispatch');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function validateDispatchDecision(decision, { selection = loadSelection(), profile = loadProfile(),
|
|
50
|
+
candidates = applyProfile(loadCatalog(), profile), nativeModels } = {}) {
|
|
51
|
+
const evidence = selectionEvidenceStatus(selection);
|
|
52
|
+
if (selection.reviewedAt !== decision.selectionReviewedAt || !evidence.routeDigest || evidence.routeDigest !== decision.selectionRouteDigest) throw new Error('Allocation changed since selection; reclassify prompt');
|
|
53
|
+
const approved = selection.routes?.[decision.harness]?.[decision.taskClass];
|
|
54
|
+
const efforts = [approved?.effort];
|
|
55
|
+
if (decision.harness === 'claude-code' && decision.taskClass === 'medium') {
|
|
56
|
+
efforts.push(selection.routes?.[decision.harness]?.codingEffort);
|
|
57
|
+
}
|
|
58
|
+
if (decision.harness === 'codex' && ['xhigh', 'max'].includes(decision.effort) &&
|
|
59
|
+
(decision.taskClass !== 'exceptional' || decision.effort !== 'xhigh' || !approved?.requiresNamedReason ||
|
|
60
|
+
!/^[a-z][a-z0-9-]{2,79}$/.test(decision.exceptionalReason || ''))) {
|
|
61
|
+
throw new Error('Exceptional xhigh dispatch requires explicit qualified route and named reason');
|
|
62
|
+
}
|
|
63
|
+
if (approved?.model !== decision.model || !efforts.includes(decision.effort) ||
|
|
64
|
+
!eligibleCandidates(candidates, profile, decision.harness).some((m) => m.id === decision.model)) {
|
|
65
|
+
throw new Error('Launch no longer satisfies reviewed native subscription allocation');
|
|
66
|
+
}
|
|
67
|
+
if (decision.harness === 'codex') {
|
|
68
|
+
const native = (nativeModels || loadNativeCodexModels()).find((m) => m.slug === decision.model);
|
|
69
|
+
if (!native || !(native.supported_reasoning_levels || []).some((m) => m.effort === decision.effort)) {
|
|
70
|
+
throw new Error('Native Codex host does not support the reviewed model/effort dispatch');
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return evidence;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function buildLaunch(decision, { cwd = process.cwd(), interactive = false } = {}) {
|
|
77
|
+
if (interactive) throw new Error('Interactive host cannot keep managed-worker prompts on stdin; use managed worker launch');
|
|
78
|
+
assertCurrentSelection({ schemaVersion: 1, reviewedAt: decision.selectionReviewedAt, maxAgeMs: decision.selectionMaxAgeMs });
|
|
79
|
+
if (!decision.subscriptionCovered || !TASK_CLASSES.includes(decision.taskClass) ||
|
|
80
|
+
!['low', 'medium', 'high', 'xhigh', 'max'].includes(decision.effort) || !/^[a-zA-Z0-9][a-zA-Z0-9._-]+$/.test(decision.model || '')) {
|
|
81
|
+
throw new Error('Invalid or unauthorized routing decision');
|
|
82
|
+
}
|
|
83
|
+
if (decision.harness === 'codex' && decision.provider === 'openai') {
|
|
84
|
+
return { command: 'codex', args: ['exec', '--ignore-user-config',
|
|
85
|
+
'--model', decision.model, '-c', `model_reasoning_effort="${decision.effort}"`,
|
|
86
|
+
'-c', 'model_provider="openai"', '-c', 'service_tier="default"', '-c', 'features.fast_mode=false', '--cd', cwd, '-'] };
|
|
87
|
+
}
|
|
88
|
+
if (decision.harness === 'claude-code' && decision.provider === 'anthropic') {
|
|
89
|
+
// Ignore user/project API-key helpers and provider redirects. Native OAuth remains readable.
|
|
90
|
+
return { command: 'claude', args: ['--print', '--model', decision.model,
|
|
91
|
+
'--effort', decision.effort, '--setting-sources', '', '--settings', '{"apiKeyHelper":""}'] };
|
|
92
|
+
}
|
|
93
|
+
throw new Error('Decision does not target a supported native subscription host');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function dispatch(decision, prompt, { spawnWorker = spawn, checkAuth = assertSubscriptionAuth, verifyDecision = validateDispatchDecision,
|
|
97
|
+
env = process.env, cwd = process.cwd(), interactive = false, checkAllowance = readCodexAllowance,
|
|
98
|
+
receiptFile = process.env.MODEL_ROUTER_DISPATCH_RECEIPTS || path.join(os.homedir(), '.claude', 'metaharness', 'dispatch-decisions.jsonl') } = {}) {
|
|
99
|
+
const evidence = verifyDecision(decision) || selectionEvidenceStatus({ schemaVersion: 1,
|
|
100
|
+
reviewedAt: decision.selectionReviewedAt, maxAgeMs: decision.selectionMaxAgeMs });
|
|
101
|
+
const launch = buildLaunch(decision, { cwd, interactive });
|
|
102
|
+
const cleanEnv = subscriptionEnvironment(env);
|
|
103
|
+
checkAuth(decision.harness, { env: cleanEnv });
|
|
104
|
+
const allowance = decision.harness === 'codex' ? await checkAllowance({ env: cleanEnv })
|
|
105
|
+
: { checkedAt: null, ordinaryUsageAllowed: null, status: 'native-claude-allowance-not-verified' };
|
|
106
|
+
const receipt = { ts: new Date().toISOString(), harness: decision.harness, model: decision.model,
|
|
107
|
+
effort: decision.effort, taskClass: decision.taskClass, exceptionalReason: decision.exceptionalReason, subscriptionCovered: true,
|
|
108
|
+
selectionReviewedAt: evidence.reviewedAt, selectionRouteDigest: evidence.routeDigest || decision.selectionRouteDigest,
|
|
109
|
+
selectionEvidenceStale: evidence.stale,
|
|
110
|
+
status: 'launch-requested', modelObserved: false, serviceMode: decision.harness === 'codex' ? 'standard' : 'existing-claude-policy',
|
|
111
|
+
allowance: { checkedAt: allowance.checkedAt, ordinaryUsageAllowed: allowance.ordinaryUsageAllowed, reservation: false, raceSafe: false } };
|
|
112
|
+
// Durable receipt is required. It never contains the prompt or policy-supplied reason.
|
|
113
|
+
fs.mkdirSync(path.dirname(receiptFile), { recursive: true });
|
|
114
|
+
fs.appendFileSync(receiptFile, JSON.stringify(receipt) + '\n', { mode: 0o600 });
|
|
115
|
+
const child = spawnWorker(launch.command, launch.args, {
|
|
116
|
+
cwd, env: cleanEnv, shell: false, stdio: ['pipe', 'inherit', 'inherit'],
|
|
117
|
+
});
|
|
118
|
+
child.stdin.end(prompt);
|
|
119
|
+
return await new Promise((resolve, reject) => {
|
|
120
|
+
child.once('error', reject);
|
|
121
|
+
child.once('exit', (code, signal) => {
|
|
122
|
+
fs.appendFileSync(receiptFile, JSON.stringify({ ...receipt, ts: new Date().toISOString(),
|
|
123
|
+
status: code === 0 ? 'process-completed' : 'process-failed', exitCode: code, signal }) + '\n');
|
|
124
|
+
resolve(code ?? 1);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function main(argv) {
|
|
130
|
+
let harness = 'codex', policy, interactive = false, dryRun = false, requestJson = false;
|
|
131
|
+
const promptParts = [];
|
|
132
|
+
for (let i = 0; i < argv.length; i++) {
|
|
133
|
+
if (argv[i] === '--harness') harness = argv[++i];
|
|
134
|
+
else if (argv[i] === '--policy') policy = argv[++i];
|
|
135
|
+
else if (argv[i] === '--request-json') requestJson = true;
|
|
136
|
+
else if (argv[i] === '--interactive') interactive = true;
|
|
137
|
+
else if (argv[i] === '--dry-run') dryRun = true;
|
|
138
|
+
else if (argv[i] === '--') { promptParts.push(...argv.slice(i + 1)); break; }
|
|
139
|
+
else if (argv[i].startsWith('-')) throw new Error(`Unsupported launch option: ${argv[i]}`);
|
|
140
|
+
else promptParts.push(argv[i]);
|
|
141
|
+
}
|
|
142
|
+
const raw = promptParts.length ? promptParts.join(' ') : fs.readFileSync(0, 'utf8');
|
|
143
|
+
const request = requestJson ? JSON.parse(raw) : { prompt: raw };
|
|
144
|
+
const prompt = request.prompt;
|
|
145
|
+
if (typeof prompt !== 'string') throw new Error('Request prompt must be a string');
|
|
146
|
+
const args = [path.join(DIR, 'model-router-engine.mjs'), '--harness', harness, '--json'];
|
|
147
|
+
if (policy) args.push('--policy', policy);
|
|
148
|
+
if (requestJson) args.push('--request-json');
|
|
149
|
+
const decision = JSON.parse(execFileSync(process.execPath, args, {
|
|
150
|
+
input: requestJson ? JSON.stringify(request) : prompt, encoding: 'utf8', timeout: 30000, maxBuffer: 1024 * 1024,
|
|
151
|
+
}));
|
|
152
|
+
if (dryRun) {
|
|
153
|
+
validateDispatchDecision(decision);
|
|
154
|
+
const launch = buildLaunch(decision, { interactive });
|
|
155
|
+
process.stdout.write(JSON.stringify({ ...launch, model: decision.model, effort: decision.effort,
|
|
156
|
+
taskClass: decision.taskClass, executed: false }) + '\n');
|
|
157
|
+
} else process.exitCode = await dispatch(decision, prompt, { interactive });
|
|
158
|
+
}
|
|
159
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
160
|
+
main(process.argv.slice(2)).catch((e) => { process.stderr.write(`model-router-dispatch: ${e.message}\n`); process.exitCode = 1; });
|
|
161
|
+
}
|