openzoo 0.50.19 → 0.50.20
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/bin/openzoo.js +7 -0
- package/lib/cursorapi.js +78 -3
- package/lib/cursorbackend.js +906 -54
- package/lib/grokcli.js +100 -0
- package/package.json +1 -1
package/bin/openzoo.js
CHANGED
|
@@ -89,6 +89,10 @@ const HELP = `openzoo — local x402-paying proxy + MCP server for openzoo.fun
|
|
|
89
89
|
usage:
|
|
90
90
|
npx openzoo start the proxy: http://localhost:8402/v1 (keyless) PLUS a
|
|
91
91
|
public HTTPS url for cloud IDEs (key required, printed at start)
|
|
92
|
+
npx openzoo bot Grok Bot.app on the zoo. Starts :8402 + aiserver on
|
|
93
|
+
:8443, then launches the app with
|
|
94
|
+
CURSOR_API_BASE_URL=https://127.0.0.1:8443
|
|
95
|
+
(no sudo, no /etc/hosts). Leave it running.
|
|
92
96
|
npx openzoo cursor [dir] start proxy+tunnel, write MCP config + every model
|
|
93
97
|
into the picker, and LAUNCH Cursor on [dir]
|
|
94
98
|
(defaults to the current directory; ~ works;
|
|
@@ -164,6 +168,9 @@ async function main() {
|
|
|
164
168
|
case 'start':
|
|
165
169
|
await (await import('../lib/proxy.js')).startProxy({ autoTunnel: true });
|
|
166
170
|
break;
|
|
171
|
+
case 'bot':
|
|
172
|
+
await (await import('../lib/grokcli.js')).runBot(process.argv.slice(3));
|
|
173
|
+
break;
|
|
167
174
|
case 'editor':
|
|
168
175
|
case 'cursor':
|
|
169
176
|
case 'vscode':
|
package/lib/cursorapi.js
CHANGED
|
@@ -177,6 +177,11 @@ export function encodeForMethod(method, models) {
|
|
|
177
177
|
return encodeEnsureSandBox(pod);
|
|
178
178
|
} catch { return null; }
|
|
179
179
|
}
|
|
180
|
+
case 'GetGrokBotSendStatus':
|
|
181
|
+
// Empty body decodes status=UNSPECIFIED → UI "Failed to send".
|
|
182
|
+
// ACCEPTED=2 (aiserver.v1.GrokBotSendStatus). echo_entry_id (field 2)
|
|
183
|
+
// must echo the request's message_id or the overlay never reconciles.
|
|
184
|
+
return encodeGetGrokBotSendStatus();
|
|
180
185
|
case 'GetPlanInfo': return encodeGetPlanInfo();
|
|
181
186
|
case 'GetMe': return encodeGetMe();
|
|
182
187
|
case 'GetDefaultModel': return encodeGetDefaultModel(models);
|
|
@@ -204,10 +209,80 @@ export function encodeForMethod(method, models) {
|
|
|
204
209
|
* those. This encoder is the redirect; whether the box satisfies the protocol
|
|
205
210
|
* is a separate, unfinished problem — point it at a logging box first.
|
|
206
211
|
*/
|
|
207
|
-
|
|
212
|
+
/** GetGrokBotSendStatusResponse { status=ACCEPTED, echo_entry_id, accepted_at_ms } */
|
|
213
|
+
export function encodeGetGrokBotSendStatus(echoId = `oz-${Date.now()}`) {
|
|
214
|
+
return new Buf()
|
|
215
|
+
.int(1, 2) // GROK_BOT_SEND_STATUS_ACCEPTED
|
|
216
|
+
.str(2, echoId)
|
|
217
|
+
.int(4, Date.now())
|
|
218
|
+
.done();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Strip Connect-RPC 5-byte envelope if present (flags=0, big-endian length). */
|
|
222
|
+
export function unwrapConnect(buf) {
|
|
223
|
+
const b = Buffer.isBuffer(buf) ? buf : Buffer.from(buf || []);
|
|
224
|
+
if (b.length >= 5 && (b[0] === 0 || b[0] === 2)) {
|
|
225
|
+
const n = b.readUInt32BE(1);
|
|
226
|
+
if (n > 0 && 5 + n <= b.length) return b.subarray(5, 5 + n);
|
|
227
|
+
}
|
|
228
|
+
return b;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Bare protobuf → {fieldNumber: string|number}. Enough for GetGrokBotSendStatusRequest
|
|
232
|
+
* {1 agent_id, 2 message_id} (measured body=76b = two uuid strings). */
|
|
233
|
+
export function decodeProtoFields(buf) {
|
|
234
|
+
const b = unwrapConnect(buf);
|
|
235
|
+
const out = {};
|
|
236
|
+
let i = 0;
|
|
237
|
+
while (i < b.length) {
|
|
238
|
+
let key = 0;
|
|
239
|
+
let shift = 0;
|
|
240
|
+
while (i < b.length) {
|
|
241
|
+
const v = b[i++];
|
|
242
|
+
key |= (v & 0x7f) << shift;
|
|
243
|
+
if (!(v & 0x80)) break;
|
|
244
|
+
shift += 7;
|
|
245
|
+
if (shift > 28) return out;
|
|
246
|
+
}
|
|
247
|
+
const field = key >>> 3;
|
|
248
|
+
const wire = key & 7;
|
|
249
|
+
if (wire === 0) {
|
|
250
|
+
let n = 0;
|
|
251
|
+
shift = 0;
|
|
252
|
+
while (i < b.length) {
|
|
253
|
+
const v = b[i++];
|
|
254
|
+
n |= (v & 0x7f) << shift;
|
|
255
|
+
if (!(v & 0x80)) break;
|
|
256
|
+
shift += 7;
|
|
257
|
+
}
|
|
258
|
+
out[field] = n;
|
|
259
|
+
} else if (wire === 2) {
|
|
260
|
+
let len = 0;
|
|
261
|
+
shift = 0;
|
|
262
|
+
while (i < b.length) {
|
|
263
|
+
const v = b[i++];
|
|
264
|
+
len |= (v & 0x7f) << shift;
|
|
265
|
+
if (!(v & 0x80)) break;
|
|
266
|
+
shift += 7;
|
|
267
|
+
}
|
|
268
|
+
if (i + len > b.length) break;
|
|
269
|
+
out[field] = b.subarray(i, i + len).toString('utf8');
|
|
270
|
+
i += len;
|
|
271
|
+
} else {
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return out;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export function encodeEnsureSandBox({ region = 'us1', accountId, podId, token, accessToken, agent, vnc, vncPath, p1340, p6081 }) {
|
|
208
279
|
// per-port URLs — field 6 is the agent (1337), field 7 the VNC desktop
|
|
209
280
|
// (6080), matching the live cursorvm response. RunPod fronts each port at
|
|
210
281
|
// https://<id>-<port>.proxy.runpod.net, so agent !== vnc.
|
|
282
|
+
// Field 4 = network_token (nto-…); field 11 = agent HTTP bearer. Mixing
|
|
283
|
+
// them 401s /api/* on the real pod (measured sniff 2026-08-29).
|
|
284
|
+
const auth = accessToken || token;
|
|
285
|
+
const vncUrl = vncPath || `${vnc}/vnc.html?network_token=${token}&resume_lower_s=900&resume_upper_s=18000&path=websockify%3Fnetwork_token%3D${token}`;
|
|
211
286
|
return new Buf()
|
|
212
287
|
.str(1, region)
|
|
213
288
|
.str(2, accountId)
|
|
@@ -215,11 +290,11 @@ export function encodeEnsureSandBox({ region = 'us1', accountId, podId, token, a
|
|
|
215
290
|
.str(4, token)
|
|
216
291
|
.str(5, 'local')
|
|
217
292
|
.str(6, agent)
|
|
218
|
-
.str(7,
|
|
293
|
+
.str(7, vncUrl)
|
|
219
294
|
.str(8, '/workspace/terminals')
|
|
220
295
|
.int(9, 1)
|
|
221
296
|
.str(10, p1340 || agent)
|
|
222
|
-
.str(11,
|
|
297
|
+
.str(11, auth)
|
|
223
298
|
.str(12, p6081 || vnc)
|
|
224
299
|
.done();
|
|
225
300
|
}
|
package/lib/cursorbackend.js
CHANGED
|
@@ -31,8 +31,10 @@ import https from 'node:https';
|
|
|
31
31
|
import fs from 'node:fs';
|
|
32
32
|
import os from 'node:os';
|
|
33
33
|
import path from 'node:path';
|
|
34
|
+
import zlib from 'node:zlib';
|
|
34
35
|
import { execFileSync } from 'node:child_process';
|
|
35
|
-
import {
|
|
36
|
+
import { randomUUID } from 'node:crypto';
|
|
37
|
+
import { encodeAvailableModels, encodeForMethod, encodeEnsureSandBox, encodeGetGrokBotSendStatus, decodeProtoFields, unwrapConnect } from './cursorapi.js';
|
|
36
38
|
|
|
37
39
|
const TLS_DIR = path.join(os.homedir(), '.openzoo', 'cursor-tls');
|
|
38
40
|
const CURSOR_HOSTS = ['api2.cursor.sh', 'api3.cursor.sh', 'api4.cursor.sh', 'repo42.cursor.sh'];
|
|
@@ -284,70 +286,815 @@ const RESP_DROP = new Set([
|
|
|
284
286
|
'transfer-encoding', 'te', 'trailer', 'content-length',
|
|
285
287
|
]);
|
|
286
288
|
|
|
289
|
+
const SNIFF_FILE = path.join(os.homedir(), '.openzoo', 'grokbot-sniff.jsonl');
|
|
290
|
+
let realPod = null; // { agent, vnc, token, p1340, p6081, region, accountId, podId }
|
|
291
|
+
|
|
292
|
+
function sniffOn() { return process.env.OPENZOO_SNIFF === '1'; }
|
|
293
|
+
function sniffSelf() { return process.env.OZ_SNIFF_SELF || 'https://127.0.0.1:8443'; }
|
|
294
|
+
/** CURSOR_API_BASE_URL=https://127.0.0.1:8443 makes Host 127.0.0.1 — public DNS
|
|
295
|
+
* of that name is ENOTFOUND (measured sniff #3 /events, #4 listAgents). */
|
|
296
|
+
function cursorUpstream(host) {
|
|
297
|
+
const h = String(host || '').replace(/:\d+$/, '');
|
|
298
|
+
if (/cursor\.sh$/.test(h) || /anthropic\.com$/.test(h) || /cursorvm\.com$/.test(h)) return h;
|
|
299
|
+
return 'api2.cursor.sh';
|
|
300
|
+
}
|
|
301
|
+
function sniffDump(rec) {
|
|
302
|
+
if (!sniffOn()) return;
|
|
303
|
+
try {
|
|
304
|
+
fs.mkdirSync(path.dirname(SNIFF_FILE), { recursive: true, mode: 0o700 });
|
|
305
|
+
fs.appendFileSync(SNIFF_FILE, JSON.stringify({ at: new Date().toISOString(), ...rec }) + '\n');
|
|
306
|
+
} catch { /* dump must never break the proxy */ }
|
|
307
|
+
}
|
|
308
|
+
function jsonish(buf, limit = 12000) {
|
|
309
|
+
const s = Buffer.isBuffer(buf) ? buf.toString('utf8') : String(buf || '');
|
|
310
|
+
try { return JSON.parse(s); } catch { return s.slice(0, limit); }
|
|
311
|
+
}
|
|
312
|
+
function copyReqHeaders(req, host) {
|
|
313
|
+
const headers = {};
|
|
314
|
+
for (const [k, v] of Object.entries(req.headers)) {
|
|
315
|
+
if (k.startsWith(':') || HOP_BY_HOP.has(k)) continue;
|
|
316
|
+
headers[k] = Array.isArray(v) ? v.join(', ') : String(v);
|
|
317
|
+
}
|
|
318
|
+
if (host) headers.host = host;
|
|
319
|
+
delete headers['content-length'];
|
|
320
|
+
return headers;
|
|
321
|
+
}
|
|
322
|
+
function lookupPinned(ip) {
|
|
323
|
+
return (h, o, cb) => (o && o.all ? cb(null, [{ address: ip, family: 4 }]) : cb(null, ip, 4));
|
|
324
|
+
}
|
|
325
|
+
function inflateBody(buf, headers) {
|
|
326
|
+
const enc = String(headers?.['content-encoding'] || '').toLowerCase();
|
|
327
|
+
try {
|
|
328
|
+
if (enc.includes('gzip')) return zlib.gunzipSync(buf);
|
|
329
|
+
if (enc.includes('deflate')) return zlib.inflateSync(buf);
|
|
330
|
+
if (enc.includes('br')) return zlib.brotliDecompressSync(buf);
|
|
331
|
+
} catch { /* fall through */ }
|
|
332
|
+
if (buf.length >= 2 && buf[0] === 0x1f && buf[1] === 0x8b) {
|
|
333
|
+
try { return zlib.gunzipSync(buf); } catch { /* */ }
|
|
334
|
+
}
|
|
335
|
+
return buf;
|
|
336
|
+
}
|
|
337
|
+
function writeCaptured(res, status, respHeaders, buf) {
|
|
338
|
+
const h = {};
|
|
339
|
+
for (const [k, v] of Object.entries(respHeaders || {})) {
|
|
340
|
+
if (RESP_DROP.has(k.toLowerCase())) continue;
|
|
341
|
+
h[k] = v;
|
|
342
|
+
}
|
|
343
|
+
res.writeHead(status, h);
|
|
344
|
+
res.end(buf);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
async function upstreamUnary({ host, path: pth, method, headers, body, timeoutMs = 20000 }) {
|
|
348
|
+
const ip = await resolveRealAnthropic(host);
|
|
349
|
+
return new Promise((resolve, reject) => {
|
|
350
|
+
const up = https.request({
|
|
351
|
+
hostname: host, servername: host, port: 443, path: pth,
|
|
352
|
+
method, headers, lookup: lookupPinned(ip),
|
|
353
|
+
}, (r) => {
|
|
354
|
+
const chunks = [];
|
|
355
|
+
r.on('data', (d) => chunks.push(d));
|
|
356
|
+
r.on('end', () => resolve({
|
|
357
|
+
status: r.statusCode, respHeaders: r.headers, buf: Buffer.concat(chunks),
|
|
358
|
+
}));
|
|
359
|
+
r.on('error', reject);
|
|
360
|
+
});
|
|
361
|
+
up.on('error', reject);
|
|
362
|
+
if (timeoutMs) {
|
|
363
|
+
up.setTimeout(timeoutMs, () => {
|
|
364
|
+
reject(new Error('upstream timeout'));
|
|
365
|
+
try { up.destroy(); } catch { /* */ }
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
if (method !== 'GET' && method !== 'HEAD' && body && body.length) up.write(body);
|
|
369
|
+
up.end();
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
287
373
|
async function passthroughToRealAnthropic(req, res, body, host, full, log) {
|
|
288
374
|
try {
|
|
289
|
-
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
375
|
+
host = cursorUpstream(host);
|
|
376
|
+
const streaming = /Watch|Stream|Subscribe/i.test(full);
|
|
377
|
+
if (streaming) {
|
|
378
|
+
await passthroughPipe(req, res, body, host, full, log);
|
|
379
|
+
return;
|
|
294
380
|
}
|
|
295
|
-
headers
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
381
|
+
const headers = copyReqHeaders(req, host);
|
|
382
|
+
const cap = await upstreamUnary({
|
|
383
|
+
host, path: full, method: req.method, headers, body, timeoutMs: 20000,
|
|
384
|
+
});
|
|
385
|
+
if (process.env.OPENZOO_DUMP === '1' && cap.buf.length) {
|
|
386
|
+
try {
|
|
387
|
+
const dir = '/tmp/openzoo-sniff';
|
|
388
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
389
|
+
const safe = full.replace(/[^a-zA-Z0-9]+/g, '_').slice(0, 80);
|
|
390
|
+
fs.writeFileSync(`${dir}/${Date.now()}_${safe}.bin`, cap.buf);
|
|
391
|
+
} catch { /* dumping must never break the proxy */ }
|
|
392
|
+
}
|
|
393
|
+
writeCaptured(res, cap.status, cap.respHeaders, cap.buf);
|
|
394
|
+
log(`cursor-backend: ${req.method} ${full} -> REAL ${host} (${cap.status}, ${cap.buf.length}b)`);
|
|
395
|
+
} catch (e) {
|
|
396
|
+
res.writeHead(502, { 'content-type': 'application/json' });
|
|
397
|
+
res.end(JSON.stringify({ type: 'error', error: { type: 'api_error', message: `passthrough failed: ${e.message}` } }));
|
|
398
|
+
log(`cursor-backend: passthrough ${full} FAILED: ${e.message}`);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
async function passthroughPipe(req, res, body, host, full, log) {
|
|
403
|
+
host = cursorUpstream(host);
|
|
404
|
+
const ip = await resolveRealAnthropic(host);
|
|
405
|
+
const headers = copyReqHeaders(req, host);
|
|
406
|
+
await new Promise((resolve, reject) => {
|
|
407
|
+
const up = https.request({
|
|
408
|
+
hostname: host, servername: host, port: 443, path: full,
|
|
409
|
+
method: req.method, headers, lookup: lookupPinned(ip),
|
|
410
|
+
}, (r) => {
|
|
411
|
+
const h = {};
|
|
412
|
+
for (const [k, v] of Object.entries(r.headers || {})) {
|
|
413
|
+
if (RESP_DROP.has(k.toLowerCase())) continue;
|
|
414
|
+
h[k] = v;
|
|
415
|
+
}
|
|
416
|
+
res.writeHead(r.statusCode, h);
|
|
417
|
+
r.pipe(res);
|
|
418
|
+
r.on('end', resolve);
|
|
419
|
+
r.on('error', reject);
|
|
420
|
+
});
|
|
421
|
+
up.on('error', reject);
|
|
422
|
+
req.on('close', () => { try { up.destroy(); } catch { /* */ } });
|
|
423
|
+
if (req.method !== 'GET' && req.method !== 'HEAD' && body && body.length) up.write(body);
|
|
424
|
+
up.end();
|
|
425
|
+
});
|
|
426
|
+
log(`cursor-backend: -> PIPE ${full} -> REAL ${host}`);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function rememberPod(fields, log) {
|
|
430
|
+
if (!fields || !fields[6]) return null;
|
|
431
|
+
const execDaemon = String(fields[6] || '');
|
|
432
|
+
const gateway = String(fields[10] || fields[6] || '');
|
|
433
|
+
realPod = {
|
|
434
|
+
execDaemon,
|
|
435
|
+
agent: gateway, // /api/sendPrompt lives on gateway_url (1340), not exec_daemon (1337)
|
|
436
|
+
vnc: fields[7] ? String(fields[7]).split('/vnc.html')[0] : gateway,
|
|
437
|
+
vncPath: fields[7] ? String(fields[7]) : undefined,
|
|
438
|
+
token: String(fields[4] || ''),
|
|
439
|
+
accessToken: String(fields[11] || ''),
|
|
440
|
+
p1340: gateway,
|
|
441
|
+
p6081: fields[12] ? String(fields[12]) : undefined,
|
|
442
|
+
region: String(fields[1] || 'us1'),
|
|
443
|
+
accountId: String(fields[2] || ''),
|
|
444
|
+
podId: String(fields[3] || ''),
|
|
445
|
+
};
|
|
446
|
+
sniffDump({ kind: 'pod', fields, realPod });
|
|
447
|
+
log(`cursor-backend: SNIFF real pod ${realPod.agent}`);
|
|
448
|
+
return realPod;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function rewrittenBox() {
|
|
452
|
+
const self = sniffSelf();
|
|
453
|
+
const p = realPod || {};
|
|
454
|
+
return encodeEnsureSandBox({
|
|
455
|
+
region: p.region || 'us1',
|
|
456
|
+
accountId: p.accountId || 'openzoo',
|
|
457
|
+
podId: p.podId || 'openzoo-sniff',
|
|
458
|
+
token: p.token || 'openzoo',
|
|
459
|
+
accessToken: p.accessToken || p.token || 'openzoo',
|
|
460
|
+
agent: self,
|
|
461
|
+
vnc: self,
|
|
462
|
+
p1340: self,
|
|
463
|
+
p6081: self,
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
async function sniffEnsureSandBox(req, res, body, host, full, log) {
|
|
468
|
+
const upstream = cursorUpstream(host);
|
|
469
|
+
const headers = copyReqHeaders(req, upstream);
|
|
470
|
+
const cap = await upstreamUnary({
|
|
471
|
+
host: upstream, path: full, method: req.method, headers, body, timeoutMs: 30000,
|
|
472
|
+
});
|
|
473
|
+
const raw = inflateBody(cap.buf, cap.respHeaders);
|
|
474
|
+
const proto = unwrapConnect(raw);
|
|
475
|
+
const fields = decodeProtoFields(proto);
|
|
476
|
+
rememberPod(fields, log);
|
|
477
|
+
sniffDump({
|
|
478
|
+
kind: 'EnsureSandBox',
|
|
479
|
+
status: cap.status,
|
|
480
|
+
bytes: cap.buf.length,
|
|
481
|
+
inflated: raw.length,
|
|
482
|
+
encoding: cap.respHeaders?.['content-encoding'] || '',
|
|
483
|
+
head: Buffer.from(raw.subarray(0, 24)).toString('hex'),
|
|
484
|
+
fields,
|
|
485
|
+
rewrittenTo: sniffSelf(),
|
|
486
|
+
});
|
|
487
|
+
const payload = realPod ? rewrittenBox() : proto;
|
|
488
|
+
const ct = String(req.headers['content-type'] || cap.respHeaders?.['content-type'] || '');
|
|
489
|
+
if (/WatchSandBoxMigration/.test(full) || ct.includes('connect+proto')) {
|
|
490
|
+
res.writeHead(200, {
|
|
491
|
+
'content-type': 'application/connect+proto',
|
|
492
|
+
'grpc-status': '0',
|
|
493
|
+
...CORS,
|
|
494
|
+
});
|
|
495
|
+
const end = Buffer.from('{}');
|
|
496
|
+
const h = Buffer.alloc(5); h.writeUInt8(0x02, 0); h.writeUInt32BE(end.length, 1);
|
|
497
|
+
res.end(Buffer.concat([envelope(payload), h, end]));
|
|
498
|
+
} else if (ct.includes('grpc-web')) {
|
|
499
|
+
res.writeHead(200, {
|
|
500
|
+
'content-type': ct.includes('text') ? 'application/grpc-web-text+proto' : 'application/grpc-web+proto',
|
|
501
|
+
'grpc-status': '0', ...CORS,
|
|
502
|
+
});
|
|
503
|
+
res.end(Buffer.concat([envelope(payload), grpcWebTrailer()]));
|
|
504
|
+
} else {
|
|
505
|
+
res.writeHead(200, { 'content-type': 'application/proto', ...CORS });
|
|
506
|
+
res.end(payload);
|
|
507
|
+
}
|
|
508
|
+
log(`cursor-backend: SNIFF EnsureSandBox -> rewrite ${sniffSelf()} (${cap.status}, real ${cap.buf.length}b)`);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
async function proxyPodHttp(req, res, full, body, log) {
|
|
512
|
+
if (!realPod?.agent) return false;
|
|
513
|
+
const agent = new URL(realPod.agent);
|
|
514
|
+
const headers = copyReqHeaders(req, agent.host);
|
|
515
|
+
if (realPod.accessToken && !headers.authorization && !headers.Authorization) {
|
|
516
|
+
headers.authorization = `Bearer ${realPod.accessToken}`;
|
|
517
|
+
}
|
|
518
|
+
if (realPod.token && !headers['x-anyrun-network-token']) {
|
|
519
|
+
headers['x-anyrun-network-token'] = realPod.token;
|
|
520
|
+
}
|
|
521
|
+
if (!headers['x-sand-slim-avatars']) headers['x-sand-slim-avatars'] = '1';
|
|
522
|
+
const path0 = (full || '').split('?')[0];
|
|
523
|
+
const interesting = /sendPrompt|Transcript|listAgents|promptAcceptance|openAgentTail|createAgent/i.test(path0);
|
|
524
|
+
if (path0 === '/events') {
|
|
525
|
+
const ip = await resolveRealAnthropic(agent.hostname);
|
|
526
|
+
await new Promise((resolve, reject) => {
|
|
307
527
|
const up = https.request({
|
|
308
|
-
hostname:
|
|
309
|
-
method: req.method, headers, lookup,
|
|
528
|
+
hostname: agent.hostname, servername: agent.hostname, port: 443, path: full,
|
|
529
|
+
method: req.method, headers, lookup: lookupPinned(ip),
|
|
310
530
|
}, (r) => {
|
|
531
|
+
const h = { ...CORS };
|
|
532
|
+
for (const [k, v] of Object.entries(r.headers || {})) {
|
|
533
|
+
if (RESP_DROP.has(k.toLowerCase())) continue;
|
|
534
|
+
h[k] = v;
|
|
535
|
+
}
|
|
536
|
+
res.writeHead(r.statusCode, h);
|
|
311
537
|
const chunks = [];
|
|
312
|
-
r.on('data', (d) =>
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
})
|
|
538
|
+
r.on('data', (d) => {
|
|
539
|
+
res.write(d);
|
|
540
|
+
if (chunks.length < 40) chunks.push(d);
|
|
541
|
+
});
|
|
542
|
+
r.on('end', () => {
|
|
543
|
+
sniffDump({ kind: 'sse', path: full, sample: Buffer.concat(chunks).toString('utf8').slice(0, 8000) });
|
|
544
|
+
try { res.end(); } catch { /* */ }
|
|
545
|
+
resolve();
|
|
546
|
+
});
|
|
316
547
|
r.on('error', reject);
|
|
317
548
|
});
|
|
318
549
|
up.on('error', reject);
|
|
319
|
-
|
|
320
|
-
// the app never learns its sandbox is ready. Streams get no deadline.
|
|
321
|
-
const streaming = /Watch|Stream|Subscribe/i.test(full);
|
|
322
|
-
if (!streaming) up.setTimeout(20000, () => up.destroy(new Error('upstream timeout')));
|
|
323
|
-
if (req.method !== 'GET' && req.method !== 'HEAD' && body && body.length) up.write(body);
|
|
550
|
+
req.on('close', () => { try { up.destroy(); } catch { /* */ } });
|
|
324
551
|
up.end();
|
|
325
552
|
});
|
|
553
|
+
log('cursor-backend: SNIFF /events -> real pod (piped)');
|
|
554
|
+
return true;
|
|
555
|
+
}
|
|
556
|
+
try {
|
|
557
|
+
const cap = await upstreamUnary({
|
|
558
|
+
host: agent.hostname,
|
|
559
|
+
path: full,
|
|
560
|
+
method: req.method,
|
|
561
|
+
headers,
|
|
562
|
+
body,
|
|
563
|
+
timeoutMs: path0 === '/health' ? 8000 : 120000,
|
|
564
|
+
});
|
|
565
|
+
writeCaptured(res, cap.status, cap.respHeaders, cap.buf);
|
|
566
|
+
const rec = {
|
|
567
|
+
kind: 'pod-http',
|
|
568
|
+
method: req.method,
|
|
569
|
+
path: path0,
|
|
570
|
+
status: cap.status,
|
|
571
|
+
reqBytes: body?.length || 0,
|
|
572
|
+
resBytes: cap.buf.length,
|
|
573
|
+
req: jsonish(body, 4000),
|
|
574
|
+
res: jsonish(inflateBody(cap.buf, cap.respHeaders), 16000),
|
|
575
|
+
};
|
|
576
|
+
sniffDump(rec);
|
|
577
|
+
if (interesting) {
|
|
578
|
+
log(`cursor-backend: SNIFF ${path0} ${cap.status} req=${JSON.stringify(rec.req).slice(0, 220)} res=${JSON.stringify(rec.res).slice(0, 500)}`);
|
|
579
|
+
} else {
|
|
580
|
+
log(`cursor-backend: SNIFF ${path0} -> real pod (${cap.status}, ${cap.buf.length}b)`);
|
|
581
|
+
}
|
|
582
|
+
return true;
|
|
583
|
+
} catch (e) {
|
|
584
|
+
log(`cursor-backend: SNIFF ${path0} FAILED ${e.message}`);
|
|
585
|
+
return false;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
326
588
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
589
|
+
/** JSON for the in-pod agent HTTP API. Hijack points field-6 at THIS
|
|
590
|
+
* server, so GET /health and POST /api/* land here — empty protobuf on
|
|
591
|
+
* those is "unhealthy" and Grok Bot retries EnsureSandBox forever
|
|
592
|
+
* (measured: /health empty-ok then EnsureSandBox #1945+). */
|
|
593
|
+
function jsonSend(res, obj) {
|
|
594
|
+
res.writeHead(200, { 'content-type': 'application/json', ...CORS });
|
|
595
|
+
res.end(JSON.stringify(obj));
|
|
596
|
+
}
|
|
597
|
+
/** Box HTTP API envelope. Client parse (CVr) returns null unless both
|
|
598
|
+
* status==="ok" AND "value" in n — our unwrapped {ok:true}/{entries} was
|
|
599
|
+
* dropped, so Grok Bot never painted the zoo reply (measured: << zoo 200 91c
|
|
600
|
+
* then tail polls with Failed to send). */
|
|
601
|
+
function jsonApi(res, value) {
|
|
602
|
+
jsonSend(res, { status: 'ok', value });
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const sseClients = new Set();
|
|
606
|
+
/** Gateway SSE parser (asar dispatchEventBlock) only reads `data:` lines and
|
|
607
|
+
* requires `{channel, payload}`. `event:` names are ignored. */
|
|
608
|
+
function ssePush(channel, payload) {
|
|
609
|
+
const chunk = `data: ${JSON.stringify({ channel, payload })}\n\n`;
|
|
610
|
+
for (const c of sseClients) {
|
|
611
|
+
try { c.write(chunk); } catch { sseClients.delete(c); }
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/** Grok Bot Helper daemon: GET /local-exec/requests is SSE, POST /local-exec/responses
|
|
616
|
+
* is `{providerId, frames}`. We used to JSON-[] the GET which is "disconnected"
|
|
617
|
+
* (measured: Grok Bot "can't see files on your local computer"). */
|
|
618
|
+
const localExecSse = new Set();
|
|
619
|
+
const localExecWaiters = new Map();
|
|
620
|
+
let localExecHello = null;
|
|
621
|
+
function localExecPush(frame) {
|
|
622
|
+
const chunk = `data: ${JSON.stringify(frame)}\n\n`;
|
|
623
|
+
for (const c of localExecSse) {
|
|
624
|
+
try { c.write(chunk); } catch { localExecSse.delete(c); }
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
function handleLocalExecFrames(frames, log) {
|
|
628
|
+
for (const f of frames || []) {
|
|
629
|
+
if (!f || typeof f !== 'object') continue;
|
|
630
|
+
if (f.kind === 'hello') {
|
|
631
|
+
localExecHello = f;
|
|
632
|
+
log(`cursor-backend: local-exec hello computer=${f.computerId || f.label || '?'} root=${f.localRoot || '?'}`);
|
|
633
|
+
}
|
|
634
|
+
const w = f.requestId && localExecWaiters.get(f.requestId);
|
|
635
|
+
if (!w) continue;
|
|
636
|
+
if (f.kind === 'file') {
|
|
637
|
+
const buf = Buffer.from(f.bytesBase64 || '', 'base64');
|
|
638
|
+
w.resolve({ kind: 'file', bytes: buf, text: buf.toString('utf8') });
|
|
639
|
+
localExecWaiters.delete(f.requestId);
|
|
640
|
+
} else if (f.kind === 'file-error' || f.kind === 'messages-error') {
|
|
641
|
+
w.reject(new Error(f.error || 'local-exec error'));
|
|
642
|
+
localExecWaiters.delete(f.requestId);
|
|
643
|
+
} else if (f.kind === 'client' || f.kind === 'control') {
|
|
644
|
+
w.resolve({ kind: f.kind, message: f.message });
|
|
645
|
+
localExecWaiters.delete(f.requestId);
|
|
331
646
|
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
function localExecAsk(frame, timeoutMs = 45000) {
|
|
650
|
+
const requestId = frame.requestId || randomUUID();
|
|
651
|
+
const job = { ...frame, requestId };
|
|
652
|
+
return new Promise((resolve, reject) => {
|
|
653
|
+
const t = setTimeout(() => {
|
|
654
|
+
localExecWaiters.delete(requestId);
|
|
655
|
+
reject(new Error('local-exec timeout — is Grok Bot Helper connected?'));
|
|
656
|
+
}, timeoutMs);
|
|
657
|
+
localExecWaiters.set(requestId, {
|
|
658
|
+
resolve: (v) => { clearTimeout(t); resolve(v); },
|
|
659
|
+
reject: (e) => { clearTimeout(t); reject(e); },
|
|
660
|
+
});
|
|
661
|
+
localExecPush(job);
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
async function handleLocalExecHttp(req, res, path0, body, log) {
|
|
665
|
+
if (!/^\/local-exec\//.test(path0)) return false;
|
|
666
|
+
if (req.method === 'GET' && /\/requests$/.test(path0)) {
|
|
667
|
+
res.writeHead(200, {
|
|
668
|
+
'content-type': 'text/event-stream',
|
|
669
|
+
'cache-control': 'no-cache',
|
|
670
|
+
...CORS,
|
|
671
|
+
});
|
|
672
|
+
localExecSse.add(res);
|
|
673
|
+
res.write(`data: ${JSON.stringify({ kind: 'welcome', providerId: 'openzoo' })}\n\n`);
|
|
674
|
+
const iv = setInterval(() => {
|
|
675
|
+
try { res.write(': ping\n\n'); } catch { clearInterval(iv); localExecSse.delete(res); }
|
|
676
|
+
}, 15000);
|
|
677
|
+
req.on('close', () => { clearInterval(iv); localExecSse.delete(res); });
|
|
678
|
+
log('cursor-backend: -> local-exec /requests sse');
|
|
679
|
+
return true;
|
|
680
|
+
}
|
|
681
|
+
if (/\/responses$/.test(path0)) {
|
|
682
|
+
let parsed = {};
|
|
683
|
+
try { parsed = JSON.parse(String(body || '{}')); } catch { parsed = {}; }
|
|
684
|
+
handleLocalExecFrames(parsed.frames, log);
|
|
685
|
+
jsonSend(res, { ok: true });
|
|
686
|
+
log(`cursor-backend: -> local-exec /responses n=${(parsed.frames || []).length}`);
|
|
687
|
+
return true;
|
|
688
|
+
}
|
|
689
|
+
jsonSend(res, { ok: true });
|
|
690
|
+
return true;
|
|
691
|
+
}
|
|
692
|
+
function extractLocalPaths(text) {
|
|
693
|
+
const out = [];
|
|
694
|
+
const re = /(?:~\/|\/Users\/)[^\s,;:!?()[\]{}"'`]+/g;
|
|
695
|
+
let m;
|
|
696
|
+
while ((m = re.exec(String(text || '')))) {
|
|
697
|
+
out.push(m[0].replace(/[.,;:]+$/, ''));
|
|
698
|
+
}
|
|
699
|
+
return [...new Set(out)];
|
|
700
|
+
}
|
|
701
|
+
function expandUserPath(p) {
|
|
702
|
+
if (p.startsWith('~/')) return path.join(os.homedir(), p.slice(2));
|
|
703
|
+
return p;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/** Per-agent transcript. sendPrompt is async: accept immediately, zooComplete
|
|
707
|
+
* fills this, getAgentTranscriptTail is what the UI actually polls.
|
|
708
|
+
*
|
|
709
|
+
* Gateway `isValidTranscriptEntry` (asar Fgi/RC) DROPS anything that is not
|
|
710
|
+
* `{id, kind:"message", content}` / `{id, kind:"send-message", message}` —
|
|
711
|
+
* proto wrappers with entryKind/body were counted n=2/2 here and painted
|
|
712
|
+
* as zero on the client, so overlay timed out → "Failed to send". */
|
|
713
|
+
const transcripts = new Map();
|
|
714
|
+
const tailedAgents = new Set();
|
|
715
|
+
let lastSendEchoId = `oz-${Date.now()}`;
|
|
716
|
+
function agentTranscript(id) {
|
|
717
|
+
let t = transcripts.get(id);
|
|
718
|
+
if (!t) { t = { seq: 0, entries: [] }; transcripts.set(id, t); }
|
|
719
|
+
return t;
|
|
720
|
+
}
|
|
721
|
+
function appendLine(agentId, role, text, extra = {}) {
|
|
722
|
+
const t = agentTranscript(agentId);
|
|
723
|
+
t.seq += 1;
|
|
724
|
+
const nonce = extra.clientNonce ? String(extra.clientNonce) : undefined;
|
|
725
|
+
const ts = Date.now();
|
|
726
|
+
const requestId = extra.requestId || nonce || `oz-req-${t.seq}`;
|
|
727
|
+
let e;
|
|
728
|
+
if (role === 'user') {
|
|
729
|
+
// Live cursorvm getAgentTranscriptTail (2026-08-29): user echo is
|
|
730
|
+
// kind:"message" + role:"user" + clientNonce. Assistant is kind:"send-message".
|
|
731
|
+
e = {
|
|
732
|
+
seq: t.seq,
|
|
733
|
+
kind: 'message',
|
|
734
|
+
id: extra.id || `t${t.seq}u`,
|
|
735
|
+
role: 'user',
|
|
736
|
+
content: String(text || ''),
|
|
737
|
+
richText: extra.richText || JSON.stringify({ type: 'doc', content: [{ type: 'paragraph', content: [{ type: 'text', text: String(text || '') }] }] }),
|
|
738
|
+
isStreaming: false,
|
|
739
|
+
timestampMs: ts,
|
|
740
|
+
...(nonce ? { clientNonce: nonce } : {}),
|
|
741
|
+
requestId,
|
|
742
|
+
};
|
|
743
|
+
} else {
|
|
744
|
+
e = {
|
|
745
|
+
seq: t.seq,
|
|
746
|
+
kind: 'send-message',
|
|
747
|
+
id: extra.id || `t${t.seq}s0`,
|
|
748
|
+
message: { type: 'text', content: String(text || '') },
|
|
749
|
+
timestampMs: ts,
|
|
750
|
+
requestId,
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
t.entries.push(e);
|
|
754
|
+
return e;
|
|
755
|
+
}
|
|
756
|
+
function fanoutLine(primaryId, role, text, extra = {}) {
|
|
757
|
+
const ids = new Set([primaryId, ...tailedAgents]);
|
|
758
|
+
let last = null;
|
|
759
|
+
for (const id of ids) last = appendLine(id, role, text, extra);
|
|
760
|
+
return last;
|
|
761
|
+
}
|
|
762
|
+
function gatewayEntry(e) {
|
|
763
|
+
const { seq, ...rest } = e;
|
|
764
|
+
return rest;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function promptFromSendBody(raw) {
|
|
768
|
+
let obj = raw;
|
|
769
|
+
if (Buffer.isBuffer(raw) || typeof raw === 'string') {
|
|
770
|
+
try { obj = JSON.parse(String(raw)); } catch { return String(raw || ''); }
|
|
771
|
+
}
|
|
772
|
+
if (!obj || typeof obj !== 'object') return '';
|
|
773
|
+
const pick = (v) => (typeof v === 'string' && v.trim() ? v : '');
|
|
774
|
+
let fromMsgs = '';
|
|
775
|
+
if (Array.isArray(obj.messages)) {
|
|
776
|
+
for (let i = obj.messages.length - 1; i >= 0; i--) {
|
|
777
|
+
const m = obj.messages[i];
|
|
778
|
+
fromMsgs = pick(m?.content) || pick(m?.text) || pick(m?.prompt);
|
|
779
|
+
if (fromMsgs) break;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
return pick(obj.prompt) || pick(obj.text) || pick(obj.message)
|
|
783
|
+
|| pick(obj.content) || pick(obj.input)
|
|
784
|
+
|| pick(obj.message?.content) || pick(obj.message?.text)
|
|
785
|
+
|| fromMsgs
|
|
786
|
+
|| '';
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
let walletUsdCache = { usd: null, at: 0 };
|
|
790
|
+
async function walletUsdCached() {
|
|
791
|
+
if (walletUsdCache.usd != null && Date.now() - walletUsdCache.at < 60_000) return walletUsdCache.usd;
|
|
792
|
+
try {
|
|
793
|
+
const { affordableUsd } = await import('./info.js');
|
|
794
|
+
const n = await Promise.race([
|
|
795
|
+
affordableUsd(),
|
|
796
|
+
new Promise((_, rej) => setTimeout(() => rej(new Error('balance timeout')), 4000)),
|
|
797
|
+
]);
|
|
798
|
+
if (Number.isFinite(n)) {
|
|
799
|
+
walletUsdCache = { usd: Number(n), at: Date.now() };
|
|
800
|
+
return walletUsdCache.usd;
|
|
801
|
+
}
|
|
802
|
+
} catch { /* keep last */ }
|
|
803
|
+
return walletUsdCache.usd;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
async function zooSpendOverlay(data) {
|
|
807
|
+
const x = data?.x402 || {};
|
|
808
|
+
let info = {};
|
|
809
|
+
try {
|
|
810
|
+
const r = await fetch('http://127.0.0.1:8402/v1/info', { signal: AbortSignal.timeout(1500) });
|
|
811
|
+
if (r.ok) info = await r.json();
|
|
812
|
+
} catch { /* */ }
|
|
813
|
+
let session = {};
|
|
814
|
+
try {
|
|
815
|
+
session = JSON.parse(fs.readFileSync(path.join(os.homedir(), '.openzoo', 'session.json'), 'utf8'));
|
|
816
|
+
} catch { /* */ }
|
|
817
|
+
const spent = Number(info.spendUsd ?? session.spentUsd ?? x.billedUsd ?? 0);
|
|
818
|
+
const would = Number(info.directUsd ?? session.directUsd ?? x.directUsd ?? 0);
|
|
819
|
+
const saved = Number(info.savedUsd ?? Math.max(0, would - spent));
|
|
820
|
+
const pct = would > 0 ? (100 * saved / would) : 0;
|
|
821
|
+
const credit = Number(info.creditUsd);
|
|
822
|
+
const wallet = await walletUsdCached();
|
|
823
|
+
const bal = Number.isFinite(wallet) && wallet > 0.004
|
|
824
|
+
? wallet
|
|
825
|
+
: (Number.isFinite(credit) && credit > 0.004 ? credit : null);
|
|
826
|
+
const lines = ['', ''];
|
|
827
|
+
if (x.billedUsd != null) {
|
|
828
|
+
lines.push(`this call $${Number(x.billedUsd).toFixed(6)} · OpenRouter $${Number(x.directUsd || 0).toFixed(6)}`);
|
|
829
|
+
}
|
|
830
|
+
const balTxt = bal != null ? ` · balance $${bal.toFixed(2)}` : '';
|
|
831
|
+
lines.push(`spent $${spent.toFixed(4)}${balTxt} · OpenRouter would $${would.toFixed(4)} · saved $${saved.toFixed(4)} (${pct.toFixed(0)}%)`);
|
|
832
|
+
return lines.join('\n');
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
const agentModels = new Map();
|
|
836
|
+
const MODEL_ALIASES = {
|
|
837
|
+
fable: 'anthropic/claude-fable-5',
|
|
838
|
+
'fable-5': 'anthropic/claude-fable-5',
|
|
839
|
+
'claude-fable-5': 'anthropic/claude-fable-5',
|
|
840
|
+
opus: 'anthropic/claude-opus-5',
|
|
841
|
+
'opus-5': 'anthropic/claude-opus-5',
|
|
842
|
+
sonnet: 'anthropic/claude-sonnet-5',
|
|
843
|
+
grok: 'x-ai/grok-4.6',
|
|
844
|
+
'grok-4': 'x-ai/grok-4.6',
|
|
845
|
+
'grok-4.6': 'x-ai/grok-4.6',
|
|
846
|
+
};
|
|
847
|
+
function resolveModelId(raw) {
|
|
848
|
+
const s = String(raw || '').trim();
|
|
849
|
+
if (!s) return null;
|
|
850
|
+
const lower = s.toLowerCase().replace(/^\/+/, '');
|
|
851
|
+
if (MODEL_ALIASES[lower]) return MODEL_ALIASES[lower];
|
|
852
|
+
if (s.includes('/')) return s;
|
|
853
|
+
return null;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
async function zooComplete(prompt, log, agentId) {
|
|
857
|
+
log(`cursor-backend: zoo POST :8402 ${JSON.stringify((prompt || '').slice(0, 60))}`);
|
|
858
|
+
const model = agentModels.get(agentId)
|
|
859
|
+
|| process.env.OPENZOO_DEFAULT_MODEL
|
|
860
|
+
|| 'anthropic/claude-opus-5';
|
|
861
|
+
const attachments = [];
|
|
862
|
+
for (const raw of extractLocalPaths(prompt)) {
|
|
863
|
+
const abs = expandUserPath(raw);
|
|
864
|
+
try {
|
|
865
|
+
if (localExecSse.size > 0) {
|
|
866
|
+
log(`cursor-backend: local-exec download ${abs}`);
|
|
867
|
+
const got = await localExecAsk({ kind: 'download', path: abs });
|
|
868
|
+
attachments.push({ path: raw, abs, text: got.text || '' });
|
|
869
|
+
} else {
|
|
870
|
+
const text = fs.readFileSync(abs, 'utf8');
|
|
871
|
+
attachments.push({ path: raw, abs, text });
|
|
872
|
+
}
|
|
873
|
+
} catch (e) {
|
|
874
|
+
attachments.push({ path: raw, abs, error: e.message });
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
const messages = [];
|
|
878
|
+
if (attachments.length) {
|
|
879
|
+
const bits = attachments.map((a) => (
|
|
880
|
+
a.error
|
|
881
|
+
? `FILE ${a.path} ERROR: ${a.error}`
|
|
882
|
+
: `FILE ${a.path} (${a.abs})\n${String(a.text).slice(0, 180000)}`
|
|
883
|
+
));
|
|
884
|
+
messages.push({
|
|
885
|
+
role: 'system',
|
|
886
|
+
content: 'Local files below were fetched via Grok Bot local-exec (the user\'s computer). You CAN review them. Do not claim you lack filesystem access.',
|
|
887
|
+
});
|
|
888
|
+
messages.push({ role: 'user', content: bits.join('\n\n') });
|
|
889
|
+
}
|
|
890
|
+
messages.push({ role: 'user', content: prompt || 'hello' });
|
|
891
|
+
const payload = {
|
|
892
|
+
model,
|
|
893
|
+
messages,
|
|
894
|
+
max_tokens: Number(process.env.OPENZOO_ASK_MAX_TOKENS || 2048),
|
|
895
|
+
};
|
|
896
|
+
const post = () => fetch('http://127.0.0.1:8402/v1/chat/completions', {
|
|
897
|
+
method: 'POST',
|
|
898
|
+
headers: { 'content-type': 'application/json', authorization: 'Bearer sk-openzoo' },
|
|
899
|
+
body: JSON.stringify(payload),
|
|
900
|
+
signal: AbortSignal.timeout(120000),
|
|
901
|
+
});
|
|
902
|
+
let r = await post();
|
|
903
|
+
// x402 dwell: proxy usually pays internally; a leftover 402 is retryable.
|
|
904
|
+
if (r.status === 402) {
|
|
905
|
+
log('cursor-backend: x402 402 — dwell/retry');
|
|
906
|
+
await new Promise((ok) => setTimeout(ok, 2500));
|
|
907
|
+
r = await post();
|
|
908
|
+
}
|
|
909
|
+
const data = await r.json();
|
|
910
|
+
let text = data.choices?.[0]?.message?.content || data.error?.message || '(empty zoo reply)';
|
|
911
|
+
try { text += await zooSpendOverlay(data); } catch { /* overlay must never eat the reply */ }
|
|
912
|
+
log(`cursor-backend: << zoo ${r.status} ${text.length}c`);
|
|
913
|
+
return { text, data };
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
async function handleHijackedPodHttp(req, res, full, body, log) {
|
|
917
|
+
if (sniffOn()) {
|
|
918
|
+
const waiting = (full || '').split('?')[0];
|
|
919
|
+
const podPath = waiting === '/health' || waiting === '/healthz' || waiting === '/events'
|
|
920
|
+
|| waiting.startsWith('/api/') || waiting.startsWith('/webauthn/')
|
|
921
|
+
|| waiting.startsWith('/cookie-origin-approval/') || waiting.startsWith('/local-exec/');
|
|
922
|
+
if (realPod?.agent && podPath) return proxyPodHttp(req, res, full, body, log);
|
|
923
|
+
if (!podPath) return false;
|
|
924
|
+
if (waiting === '/health' || waiting === '/healthz') {
|
|
925
|
+
jsonSend(res, { ok: true, status: 'ok', ready: true });
|
|
926
|
+
return true;
|
|
927
|
+
}
|
|
928
|
+
return false;
|
|
929
|
+
}
|
|
930
|
+
const path0 = (full || '').split('?')[0];
|
|
931
|
+
if (path0 === '/health' || path0 === '/healthz') {
|
|
932
|
+
jsonSend(res, { ok: true, status: 'ok', ready: true });
|
|
933
|
+
log('cursor-backend: -> pod /health ok');
|
|
934
|
+
return true;
|
|
935
|
+
}
|
|
936
|
+
if (path0 === '/events') {
|
|
937
|
+
res.writeHead(200, {
|
|
938
|
+
'content-type': 'text/event-stream',
|
|
939
|
+
'cache-control': 'no-cache',
|
|
940
|
+
...CORS,
|
|
941
|
+
});
|
|
942
|
+
res.write('data: {"channel":"ping","payload":{}}\n\n');
|
|
943
|
+
sseClients.add(res);
|
|
944
|
+
const iv = setInterval(() => {
|
|
945
|
+
try { res.write('data: {"channel":"ping","payload":{}}\n\n'); } catch { clearInterval(iv); sseClients.delete(res); }
|
|
946
|
+
}, 15000);
|
|
947
|
+
req.on('close', () => { clearInterval(iv); sseClients.delete(res); });
|
|
948
|
+
log('cursor-backend: -> pod /events sse');
|
|
949
|
+
return true;
|
|
950
|
+
}
|
|
951
|
+
if (path0.startsWith('/webauthn/') || path0.startsWith('/cookie-origin-approval/')) {
|
|
952
|
+
if (req.method === 'GET') jsonSend(res, []);
|
|
953
|
+
else jsonSend(res, { ok: true });
|
|
954
|
+
log(`cursor-backend: -> pod ${path0}`);
|
|
955
|
+
return true;
|
|
956
|
+
}
|
|
957
|
+
if (!path0.startsWith('/api/')) return false;
|
|
958
|
+
const name = path0.slice('/api/'.length);
|
|
959
|
+
// Roster/settings come from the REAL 1340 gateway (names, avatars, trays).
|
|
960
|
+
// Chat stays local so inference is zoo. Discovered on EnsureSandBox rewrite.
|
|
961
|
+
const roster = new Set([
|
|
962
|
+
'listAgents', 'countAgents', 'searchAgents', 'getTrays', 'getHostSettings',
|
|
963
|
+
'setHostSettings', 'getAgentChannels', 'getAgentWorkflows', 'skillsCatalog',
|
|
964
|
+
'getSubagents', 'getAsyncTasks', 'getForeverBoxStatus', 'getSharingState',
|
|
965
|
+
'getBotTemplateExportPolicy', 'getTeachRecordingStatus', 'isGlobalSearchEnabled',
|
|
966
|
+
'isEgressTunnelAvailable', 'listBoxMcpServers', 'getHostStatus',
|
|
967
|
+
'setWindowFocused', 'getAgentAutomations',
|
|
968
|
+
'createAgent', 'createAgentFromTemplate', 'createGroup', 'setGroupMembers',
|
|
969
|
+
'updateAgent', 'deleteAgents', 'duplicateAgent', 'kickstartAgent',
|
|
970
|
+
'interruptAgentRun', 'requestDiskSaverAudit', 'broadcastToAgents',
|
|
971
|
+
'setAgentUnread', 'setAgentHiddenFromSidebar', 'setAgentNotificationsEnabled',
|
|
972
|
+
'setAgentNotifyOnUpdates', 'setAgentAvatarBytes', 'getAgentAvatar',
|
|
973
|
+
]);
|
|
974
|
+
if (!sniffOn() && realPod?.agent && roster.has(name)) {
|
|
975
|
+
return proxyPodHttp(req, res, full, body, log);
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
// THE ACTUAL CHAT PATH. Grok Bot does not send StreamUnifiedChat on this
|
|
979
|
+
// surface — measured: POST /api/sendPrompt 374b/462b after EnsureSandBox
|
|
980
|
+
// hijack. Stubbing {ok:true} ate the prompt. Forward to the paying proxy.
|
|
981
|
+
if (name === 'sendPrompt') {
|
|
982
|
+
let parsed = {};
|
|
983
|
+
try { parsed = JSON.parse(String(body || '{}')); } catch { parsed = {}; }
|
|
984
|
+
const prompt = promptFromSendBody(parsed);
|
|
985
|
+
const agentId = String(parsed.agentId || parsed.id || 'openzoo');
|
|
986
|
+
const nonce = parsed.clientNonce || `oz-${Date.now()}`;
|
|
987
|
+
log(`cursor-backend: >> sendPrompt agent=${agentId} keys=${Object.keys(parsed).join(',')} prompt=${JSON.stringify((prompt || '').slice(0, 80))}`);
|
|
988
|
+
lastSendEchoId = String(nonce);
|
|
989
|
+
const userLine = fanoutLine(agentId, 'user', prompt, {
|
|
990
|
+
clientNonce: nonce,
|
|
991
|
+
requestId: nonce,
|
|
992
|
+
richText: parsed.richText,
|
|
993
|
+
});
|
|
994
|
+
ssePush('transcript', { ...gatewayEntry(userLine), agentId });
|
|
995
|
+
jsonSend(res, { accepted: true });
|
|
996
|
+
const modelCmd = /^\s*\/model(?:\s+(\S+))?\s*$/i.exec(prompt || '');
|
|
997
|
+
setImmediate(async () => {
|
|
998
|
+
let text = '';
|
|
338
999
|
try {
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
1000
|
+
if (modelCmd) {
|
|
1001
|
+
const want = modelCmd[1];
|
|
1002
|
+
if (!want) {
|
|
1003
|
+
const cur = agentModels.get(agentId) || process.env.OPENZOO_DEFAULT_MODEL || 'anthropic/claude-opus-5';
|
|
1004
|
+
text = `current model: ${cur}\nset with /model fable | opus | sonnet | grok | provider/id`;
|
|
1005
|
+
} else {
|
|
1006
|
+
const id = resolveModelId(want) || (want.includes('/') ? want : null);
|
|
1007
|
+
if (!id) {
|
|
1008
|
+
text = `unknown model "${want}". try /model fable | opus | sonnet | grok or a full id like anthropic/claude-fable-5`;
|
|
1009
|
+
} else {
|
|
1010
|
+
agentModels.set(agentId, id);
|
|
1011
|
+
text = `model set to ${id}`;
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
try { text += await zooSpendOverlay({}); } catch { /* */ }
|
|
1015
|
+
} else {
|
|
1016
|
+
const z = await zooComplete(prompt, log, agentId);
|
|
1017
|
+
text = z.text;
|
|
1018
|
+
}
|
|
1019
|
+
} catch (e) {
|
|
1020
|
+
text = `openzoo error: ${e.message}`;
|
|
1021
|
+
log(`cursor-backend: sendPrompt zoo failed: ${e.message}`);
|
|
1022
|
+
}
|
|
1023
|
+
const line = fanoutLine(agentId, 'assistant', text, { clientNonce: nonce, requestId: nonce });
|
|
1024
|
+
ssePush('transcript', { ...gatewayEntry(line), agentId });
|
|
1025
|
+
log(`cursor-backend: sendPrompt done agent=${agentId} seq=${line.seq} text=${JSON.stringify(text.slice(0, 80))}`);
|
|
1026
|
+
});
|
|
1027
|
+
return true;
|
|
1028
|
+
}
|
|
1029
|
+
if (name === 'getAgentTranscriptTail' || name === 'getAgentTranscriptWindow' || name === 'openAgentTail') {
|
|
1030
|
+
let parsed = {};
|
|
1031
|
+
try { parsed = JSON.parse(String(body || '{}')); } catch { parsed = {}; }
|
|
1032
|
+
let id = String(parsed.id || parsed.agentId || 'openzoo');
|
|
1033
|
+
tailedAgents.add(id);
|
|
1034
|
+
let t = agentTranscript(id);
|
|
1035
|
+
if (!t.entries.length) {
|
|
1036
|
+
for (const [other, ot] of transcripts) {
|
|
1037
|
+
if (ot.entries.length) { id = other; t = ot; break; }
|
|
1038
|
+
}
|
|
344
1039
|
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
1040
|
+
const limit = Math.min(Number(parsed.limit) || 50, 200);
|
|
1041
|
+
const before = parsed.beforeSeq != null ? Number(parsed.beforeSeq) : Infinity;
|
|
1042
|
+
const sliced = t.entries.filter((e) => e.seq < before).slice(-limit);
|
|
1043
|
+
const page = { entries: sliced.map(gatewayEntry) };
|
|
1044
|
+
if (t.entries.length > sliced.length && sliced.length) page.nextBeforeSeq = sliced[0].seq;
|
|
1045
|
+
// Live pod returns RAW {entries, nextBeforeSeq} — no CVr envelope
|
|
1046
|
+
// (measured 1340 getAgentTranscriptTail 2026-08-29). Wrapping {status,value}
|
|
1047
|
+
// made transcript-page validation fail and the canvas stayed empty.
|
|
1048
|
+
jsonSend(res, page);
|
|
1049
|
+
log(`cursor-backend: -> transcript ${id} n=${sliced.length}/${t.seq}`);
|
|
1050
|
+
return true;
|
|
1051
|
+
}
|
|
1052
|
+
if (name === 'promptAcceptanceStatus') {
|
|
1053
|
+
let parsed = {};
|
|
1054
|
+
try { parsed = JSON.parse(String(body || '{}')); } catch { parsed = {}; }
|
|
1055
|
+
const nonce = String(parsed.clientNonce || lastSendEchoId);
|
|
1056
|
+
const agentId = String(parsed.agentId || '');
|
|
1057
|
+
jsonSend(res, {
|
|
1058
|
+
outcome: 'found',
|
|
1059
|
+
record: {
|
|
1060
|
+
status: 'accepted',
|
|
1061
|
+
acceptedAtMs: Date.now(),
|
|
1062
|
+
echoEntryId: nonce,
|
|
1063
|
+
clientNonce: nonce,
|
|
1064
|
+
agentId,
|
|
1065
|
+
inputDigest: '',
|
|
1066
|
+
},
|
|
1067
|
+
});
|
|
1068
|
+
log(`cursor-backend: -> promptAcceptanceStatus found echo=${nonce}`);
|
|
1069
|
+
return true;
|
|
350
1070
|
}
|
|
1071
|
+
|
|
1072
|
+
const stubs = {
|
|
1073
|
+
getHostStatus: { status: 'ready', ready: true, state: 'ready', hostStatus: 'ready' },
|
|
1074
|
+
getHostSettings: { settings: {} },
|
|
1075
|
+
setHostSettings: { ok: true },
|
|
1076
|
+
setBoxSecrets: { ok: true },
|
|
1077
|
+
listAgents: [...new Set([...transcripts.keys(), ...tailedAgents])].map((id) => ({ id, name: id, status: 'ready' })),
|
|
1078
|
+
getAgentTranscriptTail: { tail: '', lines: [], dropped: false, ok: true },
|
|
1079
|
+
getTeachRecordingStatus: { recording: false },
|
|
1080
|
+
getTrays: { trays: [] },
|
|
1081
|
+
isGlobalSearchEnabled: { enabled: false },
|
|
1082
|
+
isEgressTunnelAvailable: { available: false },
|
|
1083
|
+
getSharingState: { sharing: false },
|
|
1084
|
+
getBotTemplateExportPolicy: { allowed: true },
|
|
1085
|
+
getAgentAutomations: { automations: [] },
|
|
1086
|
+
getForeverBoxStatus: { enabled: false },
|
|
1087
|
+
getSubagents: { subagents: [] },
|
|
1088
|
+
getAsyncTasks: { tasks: [] },
|
|
1089
|
+
getAgentWorkflows: { workflows: [] },
|
|
1090
|
+
setWindowFocused: { ok: true },
|
|
1091
|
+
};
|
|
1092
|
+
const payload = stubs[name] !== undefined ? stubs[name] : { ok: true };
|
|
1093
|
+
// Live 1340 listAgents is a RAW array, not CVr (measured 104674b 2026-08-29).
|
|
1094
|
+
if (name === 'listAgents') jsonSend(res, payload);
|
|
1095
|
+
else jsonApi(res, payload);
|
|
1096
|
+
log(`cursor-backend: -> pod /api/${name}`);
|
|
1097
|
+
return true;
|
|
351
1098
|
}
|
|
352
1099
|
|
|
353
1100
|
/**
|
|
@@ -494,13 +1241,38 @@ export function startCursorBackend({ port = 8443, models, log = () => {} } = {})
|
|
|
494
1241
|
{
|
|
495
1242
|
const path0 = full.split('?')[0];
|
|
496
1243
|
const oauth = /^\/oauth(\/|$)/.test(path0);
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
1244
|
+
// Coordinator talks to /sand-box/local-exec-daemon-credential. empty-ok
|
|
1245
|
+
// here is ControlPortCallError: main-execution-failure: fetch failed.
|
|
1246
|
+
const sandBox = /^\/sand-box(\/|$)/.test(path0);
|
|
1247
|
+
const grokCred = /IssueGrokBotUserComputerCredential/.test(full);
|
|
1248
|
+
const sniffBox = sniffOn() && /GrokBotService\/(EnsureSandBox|WatchSandBoxMigration)/.test(full);
|
|
1249
|
+
const needRealPod = /GrokBotService\/EnsureSandBox/.test(full)
|
|
1250
|
+
&& !process.env.OZ_HIJACK_POD
|
|
1251
|
+
&& !sniffBox;
|
|
1252
|
+
if (sniffBox) {
|
|
1253
|
+
if (/WatchSandBoxMigration/.test(full) && realPod) {
|
|
1254
|
+
const payload = rewrittenBox();
|
|
1255
|
+
res.writeHead(200, {
|
|
1256
|
+
'content-type': 'application/connect+proto',
|
|
1257
|
+
'grpc-status': '0',
|
|
1258
|
+
...CORS,
|
|
1259
|
+
});
|
|
1260
|
+
const end = Buffer.from('{}');
|
|
1261
|
+
const h = Buffer.alloc(5); h.writeUInt8(0x02, 0); h.writeUInt32BE(end.length, 1);
|
|
1262
|
+
res.end(Buffer.concat([envelope(payload), h, end]));
|
|
1263
|
+
log('cursor-backend: SNIFF WatchSandBoxMigration ready (rewritten)');
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1266
|
+
await sniffEnsureSandBox(req, res, body, host, full, log);
|
|
1267
|
+
return;
|
|
1268
|
+
}
|
|
1269
|
+
if (oauth || sandBox || grokCred || needRealPod) {
|
|
500
1270
|
const upstream = /cursor\.sh$/.test(host) ? host : 'api2.cursor.sh';
|
|
501
1271
|
await passthroughToRealAnthropic(req, res, body, upstream, full, log);
|
|
502
1272
|
return;
|
|
503
1273
|
}
|
|
1274
|
+
if (await handleLocalExecHttp(req, res, path0, body, log)) return;
|
|
1275
|
+
if (await handleHijackedPodHttp(req, res, full, body, log)) return;
|
|
504
1276
|
}
|
|
505
1277
|
// HIJACK EnsureSandBox FIRST — before passthrough, or passthrough eats it.
|
|
506
1278
|
// MEASURED: with OPENZOO_PASSTHRU=1 set, EnsureSandBox went `-> REAL
|
|
@@ -508,8 +1280,44 @@ export function startCursorBackend({ port = 8443, models, log = () => {} } = {})
|
|
|
508
1280
|
// never our box. This method (and ONLY this one) must be answered locally
|
|
509
1281
|
// with OUR box so Grok Bot's UI wires to our sandbox; everything else
|
|
510
1282
|
// still passes through so the app loads normally.
|
|
511
|
-
if (/GrokBotService\/EnsureSandBox/.test(full) && process.env.OZ_HIJACK_POD) {
|
|
512
|
-
|
|
1283
|
+
if (/GrokBotService\/(EnsureSandBox|WatchSandBoxMigration)/.test(full) && process.env.OZ_HIJACK_POD) {
|
|
1284
|
+
if (/WatchSandBoxMigration/.test(full) && realPod) {
|
|
1285
|
+
const payload = rewrittenBox();
|
|
1286
|
+
res.writeHead(200, {
|
|
1287
|
+
'content-type': 'application/connect+proto',
|
|
1288
|
+
'grpc-status': '0',
|
|
1289
|
+
...CORS,
|
|
1290
|
+
});
|
|
1291
|
+
const end = Buffer.from('{}');
|
|
1292
|
+
const h = Buffer.alloc(5); h.writeUInt8(0x02, 0); h.writeUInt32BE(end.length, 1);
|
|
1293
|
+
res.end(Buffer.concat([envelope(payload), h, end]));
|
|
1294
|
+
log('cursor-backend: -> WatchSandBoxMigration ready (hijack, real roster)');
|
|
1295
|
+
return;
|
|
1296
|
+
}
|
|
1297
|
+
try {
|
|
1298
|
+
process.env.OZ_SNIFF_SELF = process.env.OZ_SNIFF_SELF || 'https://127.0.0.1:8443';
|
|
1299
|
+
await sniffEnsureSandBox(req, res, body, host, full, log);
|
|
1300
|
+
log('cursor-backend: -> HIJACKED EnsureSandBox -> our box (roster from real 1340)');
|
|
1301
|
+
return;
|
|
1302
|
+
} catch (e) {
|
|
1303
|
+
log(`cursor-backend: EnsureSandBox discover failed (${e.message}) — env box`);
|
|
1304
|
+
}
|
|
1305
|
+
let pod;
|
|
1306
|
+
try { pod = JSON.parse(process.env.OZ_HIJACK_POD); } catch { pod = null; }
|
|
1307
|
+
if (pod && /WatchSandBoxMigration/.test(full)) {
|
|
1308
|
+
const payload = encodeEnsureSandBox(pod);
|
|
1309
|
+
res.writeHead(200, {
|
|
1310
|
+
'content-type': 'application/connect+proto',
|
|
1311
|
+
'grpc-status': '0',
|
|
1312
|
+
...CORS,
|
|
1313
|
+
});
|
|
1314
|
+
const end = Buffer.from('{}');
|
|
1315
|
+
const h = Buffer.alloc(5); h.writeUInt8(0x02, 0); h.writeUInt32BE(end.length, 1);
|
|
1316
|
+
res.end(Buffer.concat([envelope(payload), h, end]));
|
|
1317
|
+
log('cursor-backend: -> WatchSandBoxMigration ready (hijack)');
|
|
1318
|
+
return;
|
|
1319
|
+
}
|
|
1320
|
+
respond(req, res, 'EnsureSandBox', models);
|
|
513
1321
|
log(`cursor-backend: -> HIJACKED EnsureSandBox -> our box`);
|
|
514
1322
|
return;
|
|
515
1323
|
}
|
|
@@ -519,6 +1327,50 @@ export function startCursorBackend({ port = 8443, models, log = () => {} } = {})
|
|
|
519
1327
|
await handleStreamChat(req, res, body, log);
|
|
520
1328
|
return;
|
|
521
1329
|
}
|
|
1330
|
+
// Telegram/"Failed to send": GetGrokBotSendStatus empty proto is
|
|
1331
|
+
// UNSPECIFIED. ACCEPTED=2. WatchGrokBotUserComputerRequests is a
|
|
1332
|
+
// long-lived stream — empty-ok closes it and the channel looks offline.
|
|
1333
|
+
if (/GetGrokBotSendStatus/.test(full)) {
|
|
1334
|
+
if (sniffOn()) {
|
|
1335
|
+
const fields = decodeProtoFields(body);
|
|
1336
|
+
sniffDump({ kind: 'GetGrokBotSendStatus.req', fields });
|
|
1337
|
+
const upstream = /cursor\.sh$/.test(host) ? host : 'api2.cursor.sh';
|
|
1338
|
+
await passthroughToRealAnthropic(req, res, body, upstream, full, log);
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1341
|
+
const fields = decodeProtoFields(body);
|
|
1342
|
+
const echoId = String(fields[2] || lastSendEchoId);
|
|
1343
|
+
lastSendEchoId = echoId;
|
|
1344
|
+
const payload = encodeGetGrokBotSendStatus(echoId);
|
|
1345
|
+
const reqCt = String(req.headers['content-type'] || '');
|
|
1346
|
+
if (reqCt.includes('grpc-web')) {
|
|
1347
|
+
res.writeHead(200, {
|
|
1348
|
+
'content-type': reqCt.includes('text') ? 'application/grpc-web-text+proto' : 'application/grpc-web+proto',
|
|
1349
|
+
'grpc-status': '0', ...CORS,
|
|
1350
|
+
});
|
|
1351
|
+
res.end(Buffer.concat([envelope(payload), grpcWebTrailer()]));
|
|
1352
|
+
} else {
|
|
1353
|
+
res.writeHead(200, { 'content-type': 'application/proto', ...CORS });
|
|
1354
|
+
res.end(payload);
|
|
1355
|
+
}
|
|
1356
|
+
log(`cursor-backend: -> GetGrokBotSendStatus ACCEPTED echo=${echoId}`);
|
|
1357
|
+
return;
|
|
1358
|
+
}
|
|
1359
|
+
if (/WatchGrokBotUserComputerRequests/.test(full)) {
|
|
1360
|
+
if (sniffOn()) {
|
|
1361
|
+
const upstream = /cursor\.sh$/.test(host) ? host : 'api2.cursor.sh';
|
|
1362
|
+
await passthroughPipe(req, res, body, upstream, full, log);
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1365
|
+
res.writeHead(200, {
|
|
1366
|
+
'content-type': 'application/connect+proto',
|
|
1367
|
+
'grpc-status': '0',
|
|
1368
|
+
...CORS,
|
|
1369
|
+
});
|
|
1370
|
+
req.on('close', () => { try { res.end(); } catch { /* */ } });
|
|
1371
|
+
log('cursor-backend: -> WatchGrokBotUserComputerRequests held');
|
|
1372
|
+
return;
|
|
1373
|
+
}
|
|
522
1374
|
// FULL PASSTHROUGH MODE — observe, do not stub.
|
|
523
1375
|
//
|
|
524
1376
|
// Stubbing unknown methods with empty protobufs BREAKS Grok Bot: it never
|
package/lib/grokcli.js
CHANGED
|
@@ -98,6 +98,106 @@ ${body}
|
|
|
98
98
|
`;
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
/**
|
|
102
|
+
* `openzoo bot` — Grok Bot.app on the zoo, no sudo, no /etc/hosts.
|
|
103
|
+
*
|
|
104
|
+
* CURSOR_API_BASE_URL (and SAND_BACKEND_URL) are what the Electron MAIN
|
|
105
|
+
* process actually reads. Chromium --host-resolver-rules do not. Measured:
|
|
106
|
+
* a bare `127.0.0.1:443` is HTTP and logs ERR_SSL_HTTP_REQUEST; the URL
|
|
107
|
+
* must be https://127.0.0.1:8443. The TLS warning on launch is Node
|
|
108
|
+
* complaining about NODE_TLS_REJECT_UNAUTHORIZED=0 — expected, the cert
|
|
109
|
+
* is ours and self-signed.
|
|
110
|
+
*
|
|
111
|
+
* We bind 8443 ourselves (unprivileged). :443 is the old root takeover and
|
|
112
|
+
* stubs oauth if it is the unpatched npx copy — do not point the app there.
|
|
113
|
+
*/
|
|
114
|
+
export async function runBot(argv = []) {
|
|
115
|
+
const port = 8443;
|
|
116
|
+
const url = `https://127.0.0.1:${port}`;
|
|
117
|
+
const bin = `${APP}/Contents/MacOS/Grok Bot`;
|
|
118
|
+
if (!existsSync(bin)) {
|
|
119
|
+
console.error('openzoo: Grok Bot.app not found at', APP);
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const proxyBase = `http://localhost:${config.port}/v1`;
|
|
124
|
+
let up = false;
|
|
125
|
+
try { up = (await fetch(`${proxyBase}/models`, { signal: AbortSignal.timeout(2000) })).ok; } catch { up = false; }
|
|
126
|
+
if (!up) {
|
|
127
|
+
console.error('openzoo: starting proxy on', proxyBase);
|
|
128
|
+
const { startProxy } = await import('./proxy.js');
|
|
129
|
+
await startProxy({ silent: true, autoTunnel: process.env.OPENZOO_NO_TUNNEL === '1' ? false : true });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
process.env.OPENZOO_BYOK = '1';
|
|
133
|
+
const sniff = argv.includes('--sniff') || argv.includes('--passthru');
|
|
134
|
+
if (sniff) {
|
|
135
|
+
// REAL pod, we only sit in the middle. EnsureSandBox is rewritten so
|
|
136
|
+
// /api/sendPrompt and getAgentTranscriptTail land here, then we proxy
|
|
137
|
+
// them to cursorvm and dump the live JSON. That is the comparison
|
|
138
|
+
// surface — hijack guesses have been wrong twice.
|
|
139
|
+
delete process.env.OZ_HIJACK_POD;
|
|
140
|
+
process.env.OPENZOO_PASSTHRU = '1';
|
|
141
|
+
process.env.OPENZOO_SNIFF = '1';
|
|
142
|
+
process.env.OZ_SNIFF_SELF = url;
|
|
143
|
+
} else {
|
|
144
|
+
// Do NOT hand Grok Bot a real cursorvm pod — inference then never leaves
|
|
145
|
+
// Anysphere (measured: EnsureSandBox -> REAL api2, then StreamUnifiedChat
|
|
146
|
+
// never arrives). Point the sandbox URLs at THIS aiserver; StreamUnifiedChat
|
|
147
|
+
// is answered from :8402 (x402).
|
|
148
|
+
process.env.OZ_HIJACK_POD = JSON.stringify({
|
|
149
|
+
region: 'local',
|
|
150
|
+
accountId: 'openzoo',
|
|
151
|
+
podId: 'openzoo-local',
|
|
152
|
+
token: 'openzoo',
|
|
153
|
+
agent: url,
|
|
154
|
+
vnc: url,
|
|
155
|
+
p1340: url,
|
|
156
|
+
p6081: url,
|
|
157
|
+
});
|
|
158
|
+
process.env.OZ_SNIFF_SELF = url;
|
|
159
|
+
}
|
|
160
|
+
const { startCursorBackend } = await import('./cursorbackend.js');
|
|
161
|
+
const models = [
|
|
162
|
+
'gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', 'gpt-4.1', 'gpt-4.1-mini', 'gpt-3.5-turbo',
|
|
163
|
+
].map((n) => ({ name: n, label: n }));
|
|
164
|
+
const log = (m) => console.error(' backend:', m);
|
|
165
|
+
try {
|
|
166
|
+
startCursorBackend({ port, models, log });
|
|
167
|
+
} catch (e) {
|
|
168
|
+
console.error('openzoo: 8443 already bound — reusing it (', e.message, ')');
|
|
169
|
+
}
|
|
170
|
+
await new Promise((r) => setTimeout(r, 400));
|
|
171
|
+
console.error(`openzoo: aiserver on ${url}`);
|
|
172
|
+
console.error(' oauth + /sand-box creds -> real api2');
|
|
173
|
+
if (sniff) {
|
|
174
|
+
console.error(' SNIFF: real EnsureSandBox, /api/* proxied, dump ~/.openzoo/grokbot-sniff.jsonl');
|
|
175
|
+
console.error(' send a message in Grok Bot — it should paint for real');
|
|
176
|
+
} else {
|
|
177
|
+
console.error(' EnsureSandBox HIJACKED here; StreamUnifiedChat -> :8402 (x402)');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
try { execSync('osascript -e \'tell application "Grok Bot" to quit\'', { stdio: 'ignore' }); } catch { /* ok */ }
|
|
181
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
182
|
+
|
|
183
|
+
console.error('openzoo: launching Grok Bot');
|
|
184
|
+
console.error(` CURSOR_API_BASE_URL=${url}`);
|
|
185
|
+
spawn(bin, ['--ignore-certificate-errors'], {
|
|
186
|
+
stdio: 'ignore',
|
|
187
|
+
detached: true,
|
|
188
|
+
env: {
|
|
189
|
+
...process.env,
|
|
190
|
+
NODE_TLS_REJECT_UNAUTHORIZED: '0',
|
|
191
|
+
CURSOR_API_BASE_URL: url,
|
|
192
|
+
SAND_BACKEND_URL: url,
|
|
193
|
+
},
|
|
194
|
+
}).unref();
|
|
195
|
+
|
|
196
|
+
console.error('openzoo: leave this running. ctrl-c stops the backend.');
|
|
197
|
+
if (argv.includes('--once')) return;
|
|
198
|
+
await new Promise(() => {});
|
|
199
|
+
}
|
|
200
|
+
|
|
101
201
|
export async function setupGrokBot(argv = []) {
|
|
102
202
|
const base = `http://localhost:${config.port}/v1`;
|
|
103
203
|
const launch = !argv.includes('--no-launch');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openzoo",
|
|
3
|
-
"version": "0.50.
|
|
3
|
+
"version": "0.50.20",
|
|
4
4
|
"description": "Local x402-paying proxy + MCP server for openzoo.fun — point any OpenAI-compatible harness (Cursor, Claude Code, aider, SDKs) at localhost and it pays per call from a local burner wallet. Solana and Base rails live; Robinhood experimental.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|