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,348 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
6
|
+
import { performance } from 'node:perf_hooks';
|
|
7
|
+
import { setImmediate as yieldImmediate } from 'node:timers/promises';
|
|
8
|
+
import { subscriptionOnlyEnv } from './subscription-hosts.mjs';
|
|
9
|
+
|
|
10
|
+
export function grokSubscriptionEnv(parent = process.env) {
|
|
11
|
+
const env = subscriptionOnlyEnv(parent);
|
|
12
|
+
for (const key of ['GROK_API_KEY', 'GROK_AUTH_PROVIDER_COMMAND', 'GROK_OIDC_ISSUER',
|
|
13
|
+
'GROK_OIDC_CLIENT_ID', 'GROK_MODELS_BASE_URL', 'GROK_MODELS_LIST_URL',
|
|
14
|
+
'GROK_CLI_CHAT_PROXY_BASE_URL', 'GROK_XAI_API_BASE_URL', 'GROK_WS_URL', 'GROK_WS_ORIGIN']) delete env[key];
|
|
15
|
+
env.GROK_DISABLE_API_KEY_AUTH = '1';
|
|
16
|
+
// Background ACP workers have no dictation UI. The installed native hook
|
|
17
|
+
// wrapper explicitly supports this override and skips nonexecutable paths.
|
|
18
|
+
// Without it, superwhisper's Stop agent-hook blocks native prompt completion.
|
|
19
|
+
// This child-only setting leaves the interactive app and hook file untouched.
|
|
20
|
+
env.SUPERWHISPER_GROK_HOOK = '/dev/null';
|
|
21
|
+
return env;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// These provider fields are admission evidence, not an atomic credit reservation.
|
|
25
|
+
export function grokBillingReceipt(auth, billing, topup) {
|
|
26
|
+
const meta = auth?._meta ?? auth?.meta;
|
|
27
|
+
const config = billing?.config;
|
|
28
|
+
const subscription = typeof meta?.subscription_tier === 'string' && meta.subscription_tier.length > 0;
|
|
29
|
+
const included = meta?.auth_mode === 'Oidc' && meta?.backend_billed === false
|
|
30
|
+
&& meta?.gate === null && subscription && auth?.authenticated !== false;
|
|
31
|
+
const zero = config?.onDemandCap?.val === 0 && config?.onDemandUsed?.val === 0
|
|
32
|
+
&& config?.prepaidBalance?.val === 0;
|
|
33
|
+
// Official pager effects/helpers.rs: a successful empty response means no
|
|
34
|
+
// rule and maps to AutoTopupInfo::disabled; proto3 also omits enabled=false.
|
|
35
|
+
// Never normalize failed requests, raw bodies or malformed shapes to false.
|
|
36
|
+
const validTopup = topup && typeof topup === 'object' && !Array.isArray(topup)
|
|
37
|
+
&& Object.keys(topup).every((key) => key === 'rule');
|
|
38
|
+
const disabledTopup = validTopup && (topup.rule == null || (
|
|
39
|
+
typeof topup.rule === 'object' && !Array.isArray(topup.rule)
|
|
40
|
+
&& (topup.rule.enabled === false || !Object.hasOwn(topup.rule, 'enabled'))));
|
|
41
|
+
return {
|
|
42
|
+
eligible: Boolean(included && zero && disabledTopup && billing?.subscription_tier === meta.subscription_tier),
|
|
43
|
+
auth: included ? 'grok-oidc-subscription' : 'unknown-or-metered',
|
|
44
|
+
plan: meta?.subscription_tier ?? null,
|
|
45
|
+
backendBilled: meta?.backend_billed ?? null,
|
|
46
|
+
onDemandCap: config?.onDemandCap?.val ?? null,
|
|
47
|
+
onDemandUsed: config?.onDemandUsed?.val ?? null,
|
|
48
|
+
prepaidBalance: config?.prepaidBalance?.val ?? null,
|
|
49
|
+
autoTopupDisabled: Boolean(disabledTopup),
|
|
50
|
+
period: config?.currentPeriod ?? null,
|
|
51
|
+
reason: included && zero && disabledTopup ? 'Native subscription, zero paid-credit sources and disabled auto top-up verified'
|
|
52
|
+
: 'Included subscription, zero paid-credit sources or disabled auto top-up not verified',
|
|
53
|
+
atomicReservation: false,
|
|
54
|
+
concurrentAccountChangesSupported: false,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function identity(auth) {
|
|
59
|
+
const meta = auth?._meta ?? auth?.meta;
|
|
60
|
+
if (typeof meta?.email !== 'string' || !meta.email) throw new Error('Native Grok account identity unavailable');
|
|
61
|
+
return createHash('sha256').update(JSON.stringify([meta.email, meta.team_id, meta.is_team_principal])).digest('hex');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function grokModelReceipt(state) {
|
|
65
|
+
return (state?.availableModels ?? []).map((m) => ({
|
|
66
|
+
model: m.modelId, name: m.name, contextWindow: m._meta?.totalContextTokens ?? null,
|
|
67
|
+
contextWindows: m._meta?.contextWindows ?? [],
|
|
68
|
+
efforts: (m._meta?.reasoningEfforts ?? []).map((e) => e.value ?? e.id),
|
|
69
|
+
effort: m._meta?.reasoningEffort ?? null,
|
|
70
|
+
}));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function runBudget({ timeoutMs = 30_000, signal } = {}) {
|
|
74
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) throw new Error('Positive Grok deadline required');
|
|
75
|
+
const deadline = performance.now() + timeoutMs;
|
|
76
|
+
const check = () => {
|
|
77
|
+
if (signal?.aborted) throw new Error('Grok execution aborted');
|
|
78
|
+
if (performance.now() >= deadline) throw new Error('Grok native deadline exceeded');
|
|
79
|
+
};
|
|
80
|
+
return { signal, check, remaining: () => { check(); return Math.max(1, Math.ceil(deadline - performance.now())); } };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Python's isolated stdlib parser handles quoted, dotted, inline and escaped
|
|
84
|
+
// TOML keys. Missing Python 3.11+ or malformed TOML denies execution; no config
|
|
85
|
+
// values or parser errors (which can contain credentials) leave this process.
|
|
86
|
+
const TOML_POLICY = `import json,sys,tomllib
|
|
87
|
+
blocked=set("api_key env_key auth_provider auth_provider_command models_base_url models_list_url cli_chat_proxy_base_url xai_api_base_url grok_ws_url grok_ws_origin base_url preferred_method".split())
|
|
88
|
+
def unsafe(value):
|
|
89
|
+
if isinstance(value,dict): return any(key in blocked or unsafe(item) for key,item in value.items())
|
|
90
|
+
if isinstance(value,list): return any(unsafe(item) for item in value)
|
|
91
|
+
return False
|
|
92
|
+
try:
|
|
93
|
+
parsed=tomllib.loads(sys.stdin.read())
|
|
94
|
+
print(json.dumps({"unsafe":unsafe(parsed)}))
|
|
95
|
+
except Exception:
|
|
96
|
+
sys.exit(1)
|
|
97
|
+
`;
|
|
98
|
+
|
|
99
|
+
export function verifyGrokConfig({ binary, cwd, env, run = spawnSync, parseRun = spawnSync,
|
|
100
|
+
read = fs.readFileSync, budget = runBudget() }) {
|
|
101
|
+
budget.check();
|
|
102
|
+
const result = run(binary, ['inspect', '--json'], { cwd, env, encoding: 'utf8',
|
|
103
|
+
timeout: Math.min(15_000, budget.remaining()), killSignal: 'SIGKILL' });
|
|
104
|
+
budget.check();
|
|
105
|
+
if (result.status !== 0) throw new Error('Grok configuration inspection failed');
|
|
106
|
+
let inspection;
|
|
107
|
+
try { inspection = JSON.parse(result.stdout); } catch { throw new Error('Grok configuration inspection is invalid'); }
|
|
108
|
+
if (inspection.loginPolicy?.apiKeyAuthDisabled !== true) throw new Error('Grok API-key prohibition not active');
|
|
109
|
+
const layers = inspection.configSources?.layers;
|
|
110
|
+
if (!Array.isArray(layers)) throw new Error('Grok configuration sources not verified');
|
|
111
|
+
for (const layer of layers) {
|
|
112
|
+
budget.check();
|
|
113
|
+
if (!path.isAbsolute(layer.path ?? '')) throw new Error('Grok config source is not inspectable');
|
|
114
|
+
const input = read(layer.path, 'utf8');
|
|
115
|
+
const parsed = parseRun('python3', ['-I', '-c', TOML_POLICY], { env, input,
|
|
116
|
+
encoding: 'utf8', timeout: Math.min(5_000, budget.remaining()), killSignal: 'SIGKILL', maxBuffer: 4096 });
|
|
117
|
+
budget.check();
|
|
118
|
+
if (parsed.status !== 0) throw new Error('Trusted Grok TOML inspection unavailable or invalid');
|
|
119
|
+
let policy;
|
|
120
|
+
try { policy = JSON.parse(parsed.stdout); } catch { throw new Error('Trusted Grok TOML inspection invalid'); }
|
|
121
|
+
if (policy?.unsafe !== false) throw new Error('Custom Grok credential or endpoint configuration is unsupported for subscription routing');
|
|
122
|
+
}
|
|
123
|
+
return { apiKeyAuthDisabled: true, configSourcesChecked: layers.length };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
class GrokAcp {
|
|
127
|
+
constructor({ binary, cwd, env, budget, retirementMs = 2_000, launch = spawn }) {
|
|
128
|
+
budget.check();
|
|
129
|
+
if (!Number.isFinite(retirementMs) || retirementMs <= 0 || retirementMs > 2_000) {
|
|
130
|
+
throw new Error('Grok retirement bound must be within 2000ms');
|
|
131
|
+
}
|
|
132
|
+
this.budget = budget; this.retirementMs = retirementMs;
|
|
133
|
+
this.pending = new Map(); this.id = 0; this.buffer = ''; this.closed = false;
|
|
134
|
+
this.child = launch(binary, ['agent', '--no-leader', 'stdio'], {
|
|
135
|
+
cwd, env, detached: process.platform !== 'win32', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
136
|
+
this.retired = new Promise((resolve) => { this.confirmRetired = resolve; });
|
|
137
|
+
this.child.stdout.on('data', (data) => this.receive(data.toString()));
|
|
138
|
+
// Native stderr may include account identity, config and prompts; never forward it.
|
|
139
|
+
this.child.stderr.on('data', () => {});
|
|
140
|
+
this.child.stdin.on?.('error', () => this.fail(new Error('Grok native request delivery failed')));
|
|
141
|
+
this.child.on('error', () => {
|
|
142
|
+
if (!this.child.pid) this.confirmRetired();
|
|
143
|
+
this.fail(new Error('Grok native process could not start'));
|
|
144
|
+
});
|
|
145
|
+
this.child.once('close', () => this.confirmRetired());
|
|
146
|
+
this.child.on('exit', () => this.fail(new Error('Grok native process exited')));
|
|
147
|
+
this.abort = () => this.fail(new Error('Grok execution aborted'));
|
|
148
|
+
budget.signal?.addEventListener('abort', this.abort, { once: true });
|
|
149
|
+
try { this.timer = setTimeout(() => this.fail(new Error('Grok native deadline exceeded')), budget.remaining()); }
|
|
150
|
+
catch (error) { this.fail(error); }
|
|
151
|
+
if (budget.signal?.aborted) this.abort();
|
|
152
|
+
}
|
|
153
|
+
fail(error) {
|
|
154
|
+
this.failure ??= error;
|
|
155
|
+
for (const p of this.pending.values()) p.reject(this.failure);
|
|
156
|
+
this.pending.clear();
|
|
157
|
+
void this.close().catch(() => {});
|
|
158
|
+
}
|
|
159
|
+
receive(data) {
|
|
160
|
+
if (this.closed) return;
|
|
161
|
+
try { this.budget.check(); } catch (error) { return this.fail(error); }
|
|
162
|
+
this.buffer += data;
|
|
163
|
+
if (this.buffer.length > 16 * 1024 * 1024) return this.fail(new Error('Grok protocol line exceeds bound'));
|
|
164
|
+
let end;
|
|
165
|
+
while (!this.closed && (end = this.buffer.indexOf('\n')) >= 0) {
|
|
166
|
+
const line = this.buffer.slice(0, end); this.buffer = this.buffer.slice(end + 1);
|
|
167
|
+
let message;
|
|
168
|
+
try { this.budget.check(); message = JSON.parse(line); } catch (error) {
|
|
169
|
+
return this.fail(error.message.includes('Grok') ? error : new Error('Grok native protocol is invalid'));
|
|
170
|
+
}
|
|
171
|
+
if (message.method && message.id !== undefined) {
|
|
172
|
+
const reply = message.method === 'session/request_permission'
|
|
173
|
+
? { result: { outcome: { outcome: 'cancelled' } } }
|
|
174
|
+
: { error: { code: -32601, message: 'Client capability unavailable' } };
|
|
175
|
+
this.child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id: message.id, ...reply })}\n`);
|
|
176
|
+
} else if (this.pending.has(message.id)) {
|
|
177
|
+
const p = this.pending.get(message.id); this.pending.delete(message.id);
|
|
178
|
+
message.error ? p.reject(new Error(`Grok rejected ${p.method}`)) : p.resolve(message.result);
|
|
179
|
+
} else this.onNotification?.(message);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
request(method, params) {
|
|
183
|
+
try { this.budget.check(); } catch (error) { this.fail(error); return Promise.reject(error); }
|
|
184
|
+
if (this.closed) return Promise.reject(this.failure ?? new Error('Grok native connection is closed'));
|
|
185
|
+
const id = ++this.id;
|
|
186
|
+
return new Promise((resolve, reject) => {
|
|
187
|
+
this.pending.set(id, { method, resolve, reject });
|
|
188
|
+
try { this.child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id, method, params })}\n`); }
|
|
189
|
+
catch { this.fail(new Error('Grok native request delivery failed')); }
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
close() {
|
|
193
|
+
if (this.retirement) return this.retirement;
|
|
194
|
+
let cleanupFailed = false;
|
|
195
|
+
const safely = (action) => { try { action(); } catch { cleanupFailed = true; } };
|
|
196
|
+
this.closed = true;
|
|
197
|
+
safely(() => clearTimeout(this.timer));
|
|
198
|
+
safely(() => this.budget.signal?.removeEventListener('abort', this.abort));
|
|
199
|
+
for (const p of this.pending.values()) safely(() => p.reject(
|
|
200
|
+
this.failure ?? new Error('Grok native connection is closed')));
|
|
201
|
+
this.pending.clear();
|
|
202
|
+
this.retirement = (async () => {
|
|
203
|
+
let timer;
|
|
204
|
+
const bounded = new Promise((_, reject) => { timer = setTimeout(() => {
|
|
205
|
+
const error = new Error('Grok native process retirement not confirmed');
|
|
206
|
+
error.retirementUnconfirmed = true;
|
|
207
|
+
reject(error);
|
|
208
|
+
}, this.retirementMs); });
|
|
209
|
+
try {
|
|
210
|
+
await Promise.race([this.retired, bounded]);
|
|
211
|
+
if (cleanupFailed) throw new Error('Grok native cleanup failed');
|
|
212
|
+
} catch (error) {
|
|
213
|
+
// An unconfirmed process must not retain the controller's event loop.
|
|
214
|
+
// Unref does not claim the process exited or turn failure into success.
|
|
215
|
+
safely(() => this.child.unref?.());
|
|
216
|
+
for (const pipe of [this.child.stdin, this.child.stdout, this.child.stderr]) safely(() => pipe.unref?.());
|
|
217
|
+
throw error;
|
|
218
|
+
} finally { safely(() => clearTimeout(timer)); }
|
|
219
|
+
})();
|
|
220
|
+
// Observe immediately even if cleanup hooks throw or close is called from
|
|
221
|
+
// an event handler. Every caller still awaits this same rejecting promise.
|
|
222
|
+
void this.retirement.catch(() => {});
|
|
223
|
+
// --no-leader creates an owned process group, never a shared user leader.
|
|
224
|
+
let signalled = false;
|
|
225
|
+
if (process.platform !== 'win32' && Number.isInteger(this.child.pid)) {
|
|
226
|
+
try { process.kill(-this.child.pid, 'SIGKILL'); signalled = true; } catch { /* Fall back to owned child. */ }
|
|
227
|
+
}
|
|
228
|
+
if (!signalled) safely(() => this.child.kill('SIGKILL'));
|
|
229
|
+
for (const pipe of [this.child.stdin, this.child.stdout, this.child.stderr]) safely(() => pipe.destroy?.());
|
|
230
|
+
return this.retirement;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function connect(options, budget) {
|
|
235
|
+
budget.check();
|
|
236
|
+
const cwd = options.cwd ?? process.cwd();
|
|
237
|
+
if (!path.isAbsolute(cwd)) throw new Error('Absolute Grok working directory required');
|
|
238
|
+
const binary = options.binary ?? path.join(os.homedir(), '.grok/bin/grok');
|
|
239
|
+
const env = grokSubscriptionEnv(options.env ?? process.env);
|
|
240
|
+
const config = verifyGrokConfig({ binary, cwd, env, run: options.run, parseRun: options.parseRun, read: options.read, budget });
|
|
241
|
+
await yieldImmediate(); // Deliver cancellation queued while synchronous inspection ran.
|
|
242
|
+
budget.check();
|
|
243
|
+
const client = new GrokAcp({ ...options, binary, cwd, env, budget });
|
|
244
|
+
try {
|
|
245
|
+
const init = await client.request('initialize', { protocolVersion: 1, clientCapabilities: {},
|
|
246
|
+
clientInfo: { name: 'ruvnet-subscription-router', version: '1' } });
|
|
247
|
+
if (!init.authMethods?.some((m) => m.id === 'cached_token')) throw new Error('Native Grok cached OAuth login unavailable');
|
|
248
|
+
const auth = await client.request('authenticate', { methodId: 'cached_token' });
|
|
249
|
+
const billing = await client.request('_x.ai/billing', {});
|
|
250
|
+
const topup = await client.request('_x.ai/auto-topup-rule', {});
|
|
251
|
+
const receipt = grokBillingReceipt(auth, billing, topup);
|
|
252
|
+
return { client, init, receipt, config, cwd, accountIdentity: identity(auth) };
|
|
253
|
+
} catch (error) { await client.close(); throw error; }
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export async function probeGrokSubscriptionNative(options = {}) {
|
|
257
|
+
let client;
|
|
258
|
+
try {
|
|
259
|
+
const budget = runBudget(options);
|
|
260
|
+
const connected = await connect(options, budget); client = connected.client;
|
|
261
|
+
await client.close(); budget.check();
|
|
262
|
+
return { host: 'grok', ...connected.receipt, eligibilityScope: 'subscription-billing-preflight',
|
|
263
|
+
nativeVersion: connected.init._meta?.agentVersion ?? null,
|
|
264
|
+
config: connected.config, models: grokModelReceipt(connected.init._meta?.modelState),
|
|
265
|
+
checkedAt: new Date().toISOString(), inferenceTested: false };
|
|
266
|
+
} catch (error) {
|
|
267
|
+
return { host: 'grok', eligible: false, auth: 'unknown', reason: error.message, inferenceTested: false };
|
|
268
|
+
} finally { await client?.close().catch(() => {}); }
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export async function executeGrokSubscription(options = {}) {
|
|
272
|
+
if (typeof options.prompt !== 'string' || !options.prompt.trim()) throw new Error('Grok prompt required');
|
|
273
|
+
if (!options.model || !options.effort) throw new Error('Explicit Grok model and effort required');
|
|
274
|
+
if (options.workerThreads !== undefined && options.workerThreads !== 1) throw new Error('Grok worker-thread limits unsupported');
|
|
275
|
+
const budget = runBudget(options);
|
|
276
|
+
budget.check();
|
|
277
|
+
const { client, init, receipt, config, cwd, accountIdentity } = await connect(options, budget);
|
|
278
|
+
try {
|
|
279
|
+
if (!receipt.eligible) throw new Error(receipt.reason);
|
|
280
|
+
let models = grokModelReceipt(init._meta?.modelState);
|
|
281
|
+
const text = []; let notificationFailure; let collectAssistant = false;
|
|
282
|
+
client.onNotification = (message) => {
|
|
283
|
+
if (message.method === '_x.ai/models/update') models = grokModelReceipt(message.params);
|
|
284
|
+
if (message.method === '_x.ai/settings/update' && message.params?.allow_access === false) {
|
|
285
|
+
notificationFailure = new Error('Grok subscription access withdrawn'); client.fail(notificationFailure);
|
|
286
|
+
}
|
|
287
|
+
if (/^_x\.ai\/billing(?:\/|$)/.test(message.method ?? '') && (
|
|
288
|
+
message.params?.config?.onDemandCap?.val !== 0 || message.params?.config?.onDemandUsed?.val !== 0
|
|
289
|
+
|| message.params?.config?.prepaidBalance?.val !== 0)) {
|
|
290
|
+
notificationFailure = new Error('Grok paid-credit state changed'); client.fail(notificationFailure);
|
|
291
|
+
}
|
|
292
|
+
if (message.method === '_x.ai/auto-topup-rule' && message.params?.rule?.enabled !== false) {
|
|
293
|
+
notificationFailure = new Error('Grok auto top-up state changed'); client.fail(notificationFailure);
|
|
294
|
+
}
|
|
295
|
+
if (collectAssistant && message.method === 'session/update' && message.params?.update?.sessionUpdate === 'agent_message_chunk') {
|
|
296
|
+
const content = message.params.update.content;
|
|
297
|
+
if (content?.type === 'text') text.push(content.text);
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
let fork;
|
|
301
|
+
let session;
|
|
302
|
+
if (options.parentSessionId) {
|
|
303
|
+
if (!path.isAbsolute(options.parentCwd ?? '')) throw new Error('Native Grok fork requires absolute parentCwd');
|
|
304
|
+
fork = await client.request('_x.ai/session/fork', { sourceSessionId: options.parentSessionId,
|
|
305
|
+
sourceCwd: options.parentCwd, newCwd: cwd });
|
|
306
|
+
if (fork.parentSessionId !== options.parentSessionId || !fork.newSessionId) throw new Error('Native Grok fork lineage not verified');
|
|
307
|
+
session = { ...await client.request('session/load', { sessionId: fork.newSessionId, cwd, mcpServers: [] }),
|
|
308
|
+
sessionId: fork.newSessionId };
|
|
309
|
+
} else session = await client.request('session/new', { cwd, mcpServers: [] });
|
|
310
|
+
models = grokModelReceipt(session.models ?? init._meta?.modelState);
|
|
311
|
+
const selected = models.find((m) => m.model === options.model);
|
|
312
|
+
if (!selected || !selected.efforts.includes(options.effort)) throw new Error('Requested Grok model or effort absent from native catalog');
|
|
313
|
+
if (options.contextWindow !== undefined && options.contextWindow !== selected.contextWindow) {
|
|
314
|
+
throw new Error('Native Grok ACP cannot set requested context window; refusing substitution');
|
|
315
|
+
}
|
|
316
|
+
let controls = await client.request('session/set_config_option', {
|
|
317
|
+
sessionId: session.sessionId, configId: 'model', value: options.model });
|
|
318
|
+
controls = await client.request('session/set_config_option', {
|
|
319
|
+
sessionId: session.sessionId, configId: 'reasoning_effort', value: options.effort });
|
|
320
|
+
const actual = Object.fromEntries((controls.configOptions ?? []).map((c) => [c.id, c.currentValue]));
|
|
321
|
+
if (actual.model !== options.model || actual.reasoning_effort !== options.effort) throw new Error('Native Grok model/effort receipt mismatch');
|
|
322
|
+
const beforeAuth = await client.request('_x.ai/auth/check_subscription', {});
|
|
323
|
+
if (identity(beforeAuth) !== accountIdentity) throw new Error('Native Grok account changed before execution');
|
|
324
|
+
const before = grokBillingReceipt(beforeAuth, await client.request('_x.ai/billing', {}),
|
|
325
|
+
await client.request('_x.ai/auto-topup-rule', {}));
|
|
326
|
+
if (!before.eligible) throw new Error(before.reason);
|
|
327
|
+
// session/load replays the parent's history; only the dispatched turn is
|
|
328
|
+
// execution output. Never return old assistant messages as a new answer.
|
|
329
|
+
collectAssistant = true;
|
|
330
|
+
const result = await client.request('session/prompt', { sessionId: session.sessionId,
|
|
331
|
+
prompt: [{ type: 'text', text: options.prompt }] });
|
|
332
|
+
collectAssistant = false;
|
|
333
|
+
const afterAuth = await client.request('_x.ai/auth/check_subscription', {});
|
|
334
|
+
if (identity(afterAuth) !== accountIdentity) throw new Error('Native Grok account changed during execution');
|
|
335
|
+
const after = grokBillingReceipt(afterAuth, await client.request('_x.ai/billing', {}),
|
|
336
|
+
await client.request('_x.ai/auto-topup-rule', {}));
|
|
337
|
+
if (notificationFailure || !after.eligible) throw new Error('Grok billing/access state changed during execution');
|
|
338
|
+
return { host: 'grok', sessionId: session.sessionId, model: actual.model,
|
|
339
|
+
effort: actual.reasoning_effort, contextWindow: selected.contextWindow,
|
|
340
|
+
fork: fork ? { parentSessionId: fork.parentSessionId, newSessionId: fork.newSessionId,
|
|
341
|
+
chatMessagesCopied: fork.chatMessagesCopied } : null,
|
|
342
|
+
output: text.join(''), stopReason: result.stopReason, completed: result.stopReason === 'end_turn', config,
|
|
343
|
+
subscription: { before, after }, nativeVersion: init._meta?.agentVersion ?? null,
|
|
344
|
+
workerThreadLimitEnforced: false, permissionRequests: 'deny',
|
|
345
|
+
desktopDictationHook: 'suppressed-in-background-worker',
|
|
346
|
+
inferenceTested: true, checkedAt: new Date().toISOString() };
|
|
347
|
+
} finally { await client.close(); budget.check(); }
|
|
348
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// Editable single-line input plus exact, atomically framed bracketed paste.
|
|
2
|
+
import readline from 'node:readline';
|
|
3
|
+
import { PassThrough } from 'node:stream';
|
|
4
|
+
import { TextDecoder } from 'node:util';
|
|
5
|
+
import { EventEmitter } from 'node:events';
|
|
6
|
+
|
|
7
|
+
const START = '\x1b[200~', END = '\x1b[201~';
|
|
8
|
+
const bytes = value => Buffer.byteLength(value, 'utf8');
|
|
9
|
+
|
|
10
|
+
export function createManagedTerminal({ input, output, maxPromptBytes = 64 * 1024,
|
|
11
|
+
maxQueuedPrompts = 32, maxQueueBytes = 512 * 1024 } = {}) {
|
|
12
|
+
if (!input?.on || !output?.write || ![maxPromptBytes, maxQueuedPrompts, maxQueueBytes].every(n => Number.isSafeInteger(n) && n > 0)) {
|
|
13
|
+
throw new Error('Managed terminal requires streams and positive input bounds');
|
|
14
|
+
}
|
|
15
|
+
const priorFlowing = input.readableFlowing, priorRaw = input.isRaw;
|
|
16
|
+
const terminal = Boolean(input.isTTY && output.isTTY), dumbEditor = process.env.TERM === 'dumb', editorInput = new PassThrough();
|
|
17
|
+
editorInput.isTTY = terminal;
|
|
18
|
+
editorInput.setRawMode = mode => input.setRawMode?.(mode);
|
|
19
|
+
const editor = readline.createInterface({ input: editorInput, output, terminal });
|
|
20
|
+
const events = new EventEmitter(), decoder = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true });
|
|
21
|
+
const queue = [];
|
|
22
|
+
let queuedBytes = 0, ordinary, approval, owner, pendingPaste = '', hasPaste = false;
|
|
23
|
+
let frame = false, frameBytes = '', parsing = '', closed = false, failure;
|
|
24
|
+
let nonterminalLine = '', cleanEOF = false;
|
|
25
|
+
if (terminal) output.write('\x1b[?2004h');
|
|
26
|
+
|
|
27
|
+
function settle(waiter, error, value) {
|
|
28
|
+
if (!waiter) return;
|
|
29
|
+
waiter.signal?.removeEventListener('abort', waiter.abort);
|
|
30
|
+
error ? waiter.reject(error) : waiter.resolve(value);
|
|
31
|
+
}
|
|
32
|
+
function retire(error) {
|
|
33
|
+
if (closed) return;
|
|
34
|
+
closed = true; failure = error;
|
|
35
|
+
input.removeListener('data', receive); input.removeListener('end', ended); input.removeListener('error', inputError);
|
|
36
|
+
editor.removeListener('line', line); editor.removeListener('SIGINT', interrupted); editor.removeListener('close', ended);
|
|
37
|
+
editor.close(); editorInput.destroy();
|
|
38
|
+
if (priorFlowing !== true) input.pause?.();
|
|
39
|
+
if (terminal && typeof priorRaw === 'boolean') input.setRawMode?.(priorRaw);
|
|
40
|
+
if (terminal) output.write('\x1b[?2004l');
|
|
41
|
+
settle(ordinary, error || new Error('Terminal input closed')); settle(approval, error || new Error('Terminal input closed'));
|
|
42
|
+
ordinary = approval = undefined;
|
|
43
|
+
}
|
|
44
|
+
function fail(reason) { retire(new Error(`Managed terminal input refused: ${reason}`)); }
|
|
45
|
+
function line(value) {
|
|
46
|
+
if (closed) return;
|
|
47
|
+
const text = pendingPaste + value, kind = owner === 'approval' && approval ? 'approval' : 'ordinary', pasted = hasPaste;
|
|
48
|
+
owner = undefined; pendingPaste = ''; hasPaste = false; nonterminalLine = '';
|
|
49
|
+
if (bytes(text) > maxPromptBytes) return fail('prompt byte limit exceeded');
|
|
50
|
+
if (kind === 'approval' && !pasted) {
|
|
51
|
+
const waiter = approval; approval = undefined; settle(waiter, undefined, text); return;
|
|
52
|
+
}
|
|
53
|
+
if (ordinary) { const waiter = ordinary; ordinary = undefined; settle(waiter, undefined, text); return; }
|
|
54
|
+
if (queue.length >= maxQueuedPrompts || queuedBytes + bytes(text) > maxQueueBytes) return fail('prompt FIFO limit exceeded');
|
|
55
|
+
queue.push(text); queuedBytes += bytes(text);
|
|
56
|
+
}
|
|
57
|
+
editor.on('line', line);
|
|
58
|
+
function interrupted() {
|
|
59
|
+
events.emit('SIGINT');
|
|
60
|
+
retire(new Error('Terminal input interrupted'));
|
|
61
|
+
}
|
|
62
|
+
editor.on('SIGINT', interrupted);
|
|
63
|
+
function currentLine() { return terminal ? editor.line : nonterminalLine; }
|
|
64
|
+
function bounded() {
|
|
65
|
+
if (bytes(pendingPaste) + bytes(frameBytes) + bytes(currentLine()) > maxPromptBytes) fail('prompt byte limit exceeded');
|
|
66
|
+
}
|
|
67
|
+
function plain(value) {
|
|
68
|
+
if (!value || closed) return;
|
|
69
|
+
// Feed ordinary bytes individually so ownership starts before each editor line.
|
|
70
|
+
for (const character of value) {
|
|
71
|
+
if (closed) break;
|
|
72
|
+
if (character === '\n' && plain.lastReturn) { plain.lastReturn = false; continue; }
|
|
73
|
+
plain.lastReturn = character === '\r';
|
|
74
|
+
owner ||= approval ? 'approval' : 'ordinary';
|
|
75
|
+
if (!terminal) {
|
|
76
|
+
if (character === '\n' || character === '\r') {
|
|
77
|
+
const text = nonterminalLine; line(text);
|
|
78
|
+
} else nonterminalLine += character;
|
|
79
|
+
} else editorInput.write(character === '\n' ? '\r' : character);
|
|
80
|
+
bounded();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function beginPaste() {
|
|
84
|
+
plain.lastReturn = false;
|
|
85
|
+
const prior = currentLine();
|
|
86
|
+
if (terminal && dumbEditor && prior) return fail('paste after a prefix is unsupported with the dumb editor');
|
|
87
|
+
if (terminal && editor.cursor !== prior.length) return fail('paste at a midline cursor is unsupported');
|
|
88
|
+
// Paste never supplies approval, including a frame starting after the question.
|
|
89
|
+
owner = 'ordinary'; hasPaste = true; pendingPaste += prior;
|
|
90
|
+
if (terminal && prior) editor.write(null, { ctrl: true, name: 'u' });
|
|
91
|
+
nonterminalLine = ''; frame = true; frameBytes = ''; bounded();
|
|
92
|
+
}
|
|
93
|
+
function paste(value) {
|
|
94
|
+
frameBytes += value;
|
|
95
|
+
if (frameBytes.includes(START)) return fail('nested bracketed paste frame');
|
|
96
|
+
bounded();
|
|
97
|
+
}
|
|
98
|
+
function finishPaste() {
|
|
99
|
+
pendingPaste += frameBytes; frameBytes = ''; frame = false; bounded();
|
|
100
|
+
if (terminal && !closed) output.write(`[paste captured: ${bytes(pendingPaste)} bytes; Enter submits]\n`);
|
|
101
|
+
}
|
|
102
|
+
function holdSuffix(text, markers) {
|
|
103
|
+
let n = 0;
|
|
104
|
+
for (const marker of markers) for (let i = 1; i < marker.length; i++) if (text.endsWith(marker.slice(0, i))) n = Math.max(n, i);
|
|
105
|
+
return n;
|
|
106
|
+
}
|
|
107
|
+
function parse() {
|
|
108
|
+
while (parsing && !closed) {
|
|
109
|
+
if (frame) {
|
|
110
|
+
const end = parsing.indexOf(END);
|
|
111
|
+
if (end >= 0) { paste(parsing.slice(0, end)); parsing = parsing.slice(end + END.length); if (!closed) finishPaste(); }
|
|
112
|
+
else { const held = holdSuffix(parsing, [END]); paste(parsing.slice(0, parsing.length - held)); parsing = parsing.slice(parsing.length - held); break; }
|
|
113
|
+
} else {
|
|
114
|
+
const start = parsing.indexOf(START), end = parsing.indexOf(END);
|
|
115
|
+
if (end >= 0 && (start < 0 || end < start)) return fail('unexpected bracketed paste end');
|
|
116
|
+
if (start >= 0) { plain(parsing.slice(0, start)); parsing = parsing.slice(start + START.length); if (!closed) beginPaste(); }
|
|
117
|
+
else { const held = holdSuffix(parsing, [START, END]); plain(parsing.slice(0, parsing.length - held)); parsing = parsing.slice(parsing.length - held); break; }
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function receive(chunk) {
|
|
122
|
+
if (closed) return;
|
|
123
|
+
try { parsing += decoder.decode(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk), { stream: true }); parse(); }
|
|
124
|
+
catch { fail('invalid UTF-8 input'); }
|
|
125
|
+
}
|
|
126
|
+
function ended() {
|
|
127
|
+
if (closed) return;
|
|
128
|
+
try { parsing += decoder.decode(); parse(); } catch { return fail('incomplete UTF-8 input at EOF'); }
|
|
129
|
+
if (frame || parsing || hasPaste || currentLine()) return fail('incomplete input at EOF');
|
|
130
|
+
cleanEOF = true; retire(new Error('Terminal input ended'));
|
|
131
|
+
}
|
|
132
|
+
function inputError(error) { retire(error); }
|
|
133
|
+
editor.on('close', ended);
|
|
134
|
+
input.on('data', receive); input.on('end', ended); input.on('error', inputError);
|
|
135
|
+
input.resume?.();
|
|
136
|
+
function question(label, { signal, approval: approvalMode = false } = {}) {
|
|
137
|
+
if (signal?.aborted) { const error = signal.reason || new Error('Terminal input aborted'); retire(error); return Promise.reject(error); }
|
|
138
|
+
if (!approvalMode && cleanEOF && queue.length) { const text = queue.shift(); queuedBytes -= bytes(text); return Promise.resolve(text); }
|
|
139
|
+
if (failure || closed) return Promise.reject(failure || new Error('Terminal input closed'));
|
|
140
|
+
if (!approvalMode && queue.length) { const text = queue.shift(); queuedBytes -= bytes(text); return Promise.resolve(text); }
|
|
141
|
+
if ((approvalMode && approval) || (!approvalMode && ordinary)) return Promise.reject(new Error('Concurrent terminal question unsupported'));
|
|
142
|
+
return new Promise((resolve, reject) => {
|
|
143
|
+
const waiter = { resolve, reject, signal };
|
|
144
|
+
waiter.abort = () => retire(signal.reason || new Error('Terminal input aborted'));
|
|
145
|
+
if (approvalMode) approval = waiter; else ordinary = waiter;
|
|
146
|
+
signal?.addEventListener('abort', waiter.abort, { once: true });
|
|
147
|
+
if (terminal) { editor.setPrompt(label); editor.prompt(true); } else output.write(label);
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
return { question, on(name, handler) { events.on(name, handler); return this; },
|
|
151
|
+
close() { retire(); } };
|
|
152
|
+
}
|