openzoo 0.50.19 → 0.50.21

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.
@@ -31,8 +31,12 @@ 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 { execFileSync } from 'node:child_process';
35
- import { encodeAvailableModels, encodeForMethod } from './cursorapi.js';
34
+ import zlib from 'node:zlib';
35
+ import { execFileSync, execFile } from 'node:child_process';
36
+ import { promisify } from 'node:util';
37
+ import tls from 'node:tls';
38
+ import { randomUUID } from 'node:crypto';
39
+ import { encodeAvailableModels, encodeForMethod, encodeEnsureSandBox, encodeGetGrokBotSendStatus, decodeProtoFields, unwrapConnect } from './cursorapi.js';
36
40
 
37
41
  const TLS_DIR = path.join(os.homedir(), '.openzoo', 'cursor-tls');
38
42
  const CURSOR_HOSTS = ['api2.cursor.sh', 'api3.cursor.sh', 'api4.cursor.sh', 'repo42.cursor.sh'];
@@ -284,70 +288,1073 @@ const RESP_DROP = new Set([
284
288
  'transfer-encoding', 'te', 'trailer', 'content-length',
285
289
  ]);
286
290
 
291
+ const SNIFF_FILE = path.join(os.homedir(), '.openzoo', 'grokbot-sniff.jsonl');
292
+ let realPod = null; // { agent, vnc, token, p1340, p6081, region, accountId, podId }
293
+
294
+ function sniffOn() { return process.env.OPENZOO_SNIFF === '1'; }
295
+ function sniffSelf() { return process.env.OZ_SNIFF_SELF || 'https://127.0.0.1:8443'; }
296
+ /** CURSOR_API_BASE_URL=https://127.0.0.1:8443 makes Host 127.0.0.1 — public DNS
297
+ * of that name is ENOTFOUND (measured sniff #3 /events, #4 listAgents). */
298
+ function cursorUpstream(host) {
299
+ const h = String(host || '').replace(/:\d+$/, '');
300
+ if (/cursor\.sh$/.test(h) || /anthropic\.com$/.test(h) || /cursorvm\.com$/.test(h)) return h;
301
+ return 'api2.cursor.sh';
302
+ }
303
+ function sniffDump(rec) {
304
+ if (!sniffOn()) return;
305
+ try {
306
+ fs.mkdirSync(path.dirname(SNIFF_FILE), { recursive: true, mode: 0o700 });
307
+ fs.appendFileSync(SNIFF_FILE, JSON.stringify({ at: new Date().toISOString(), ...rec }) + '\n');
308
+ } catch { /* dump must never break the proxy */ }
309
+ }
310
+ function jsonish(buf, limit = 12000) {
311
+ const s = Buffer.isBuffer(buf) ? buf.toString('utf8') : String(buf || '');
312
+ try { return JSON.parse(s); } catch { return s.slice(0, limit); }
313
+ }
314
+ function copyReqHeaders(req, host) {
315
+ const headers = {};
316
+ for (const [k, v] of Object.entries(req.headers)) {
317
+ if (k.startsWith(':') || HOP_BY_HOP.has(k)) continue;
318
+ headers[k] = Array.isArray(v) ? v.join(', ') : String(v);
319
+ }
320
+ if (host) headers.host = host;
321
+ delete headers['content-length'];
322
+ return headers;
323
+ }
324
+ function lookupPinned(ip) {
325
+ return (h, o, cb) => (o && o.all ? cb(null, [{ address: ip, family: 4 }]) : cb(null, ip, 4));
326
+ }
327
+ function inflateBody(buf, headers) {
328
+ const enc = String(headers?.['content-encoding'] || '').toLowerCase();
329
+ try {
330
+ if (enc.includes('gzip')) return zlib.gunzipSync(buf);
331
+ if (enc.includes('deflate')) return zlib.inflateSync(buf);
332
+ if (enc.includes('br')) return zlib.brotliDecompressSync(buf);
333
+ } catch { /* fall through */ }
334
+ if (buf.length >= 2 && buf[0] === 0x1f && buf[1] === 0x8b) {
335
+ try { return zlib.gunzipSync(buf); } catch { /* */ }
336
+ }
337
+ return buf;
338
+ }
339
+ function writeCaptured(res, status, respHeaders, buf) {
340
+ const h = {};
341
+ for (const [k, v] of Object.entries(respHeaders || {})) {
342
+ if (RESP_DROP.has(k.toLowerCase())) continue;
343
+ h[k] = v;
344
+ }
345
+ res.writeHead(status, h);
346
+ res.end(buf);
347
+ }
348
+
349
+ async function upstreamUnary({ host, path: pth, method, headers, body, timeoutMs = 20000 }) {
350
+ const ip = await resolveRealAnthropic(host);
351
+ return new Promise((resolve, reject) => {
352
+ const up = https.request({
353
+ hostname: host, servername: host, port: 443, path: pth,
354
+ method, headers, lookup: lookupPinned(ip),
355
+ }, (r) => {
356
+ const chunks = [];
357
+ r.on('data', (d) => chunks.push(d));
358
+ r.on('end', () => resolve({
359
+ status: r.statusCode, respHeaders: r.headers, buf: Buffer.concat(chunks),
360
+ }));
361
+ r.on('error', reject);
362
+ });
363
+ up.on('error', reject);
364
+ if (timeoutMs) {
365
+ up.setTimeout(timeoutMs, () => {
366
+ reject(new Error('upstream timeout'));
367
+ try { up.destroy(); } catch { /* */ }
368
+ });
369
+ }
370
+ if (method !== 'GET' && method !== 'HEAD' && body && body.length) up.write(body);
371
+ up.end();
372
+ });
373
+ }
374
+
287
375
  async function passthroughToRealAnthropic(req, res, body, host, full, log) {
288
376
  try {
289
- const ip = await resolveRealAnthropic(host);
290
- const headers = {};
291
- for (const [k, v] of Object.entries(req.headers)) {
292
- if (k.startsWith(':') || HOP_BY_HOP.has(k)) continue;
293
- headers[k] = Array.isArray(v) ? v.join(', ') : String(v);
377
+ host = cursorUpstream(host);
378
+ const streaming = /Watch|Stream|Subscribe/i.test(full);
379
+ if (streaming) {
380
+ await passthroughPipe(req, res, body, host, full, log);
381
+ return;
382
+ }
383
+ const headers = copyReqHeaders(req, host);
384
+ const cap = await upstreamUnary({
385
+ host, path: full, method: req.method, headers, body, timeoutMs: 20000,
386
+ });
387
+ if (process.env.OPENZOO_DUMP === '1' && cap.buf.length) {
388
+ try {
389
+ const dir = '/tmp/openzoo-sniff';
390
+ fs.mkdirSync(dir, { recursive: true });
391
+ const safe = full.replace(/[^a-zA-Z0-9]+/g, '_').slice(0, 80);
392
+ fs.writeFileSync(`${dir}/${Date.now()}_${safe}.bin`, cap.buf);
393
+ } catch { /* dumping must never break the proxy */ }
294
394
  }
295
- headers.host = host;
296
-
297
- // DIAL THE ADDRESS, BUT SPEAK THE NAME. fetch(`https://${ip}/...`) cannot do
298
- // this: undici derives SNI from the URL, so the server saw a bare IP, had no
299
- // certificate to offer for it, and killed the handshake
300
- // (ERR_SSL_SSL/TLS_ALERT_HANDSHAKE_FAILURE). node:https lets the two be set
301
- // independently — a custom `lookup` pins the connection to the public-DNS
302
- // address (bypassing our own /etc/hosts entry, which would otherwise loop
303
- // this request straight back into this very server), while `servername` and
304
- // the Host header keep presenting the real hostname so the cert validates.
305
- const lookup = (h, o, cb) => (o && o.all ? cb(null, [{ address: ip, family: 4 }]) : cb(null, ip, 4));
306
- const { status, respHeaders, buf } = await new Promise((resolve, reject) => {
395
+ writeCaptured(res, cap.status, cap.respHeaders, cap.buf);
396
+ log(`cursor-backend: ${req.method} ${full} -> REAL ${host} (${cap.status}, ${cap.buf.length}b)`);
397
+ } catch (e) {
398
+ res.writeHead(502, { 'content-type': 'application/json' });
399
+ res.end(JSON.stringify({ type: 'error', error: { type: 'api_error', message: `passthrough failed: ${e.message}` } }));
400
+ log(`cursor-backend: passthrough ${full} FAILED: ${e.message}`);
401
+ }
402
+ }
403
+
404
+ async function passthroughPipe(req, res, body, host, full, log) {
405
+ host = cursorUpstream(host);
406
+ const ip = await resolveRealAnthropic(host);
407
+ const headers = copyReqHeaders(req, host);
408
+ await new Promise((resolve, reject) => {
409
+ const up = https.request({
410
+ hostname: host, servername: host, port: 443, path: full,
411
+ method: req.method, headers, lookup: lookupPinned(ip),
412
+ }, (r) => {
413
+ const h = {};
414
+ for (const [k, v] of Object.entries(r.headers || {})) {
415
+ if (RESP_DROP.has(k.toLowerCase())) continue;
416
+ h[k] = v;
417
+ }
418
+ res.writeHead(r.statusCode, h);
419
+ r.pipe(res);
420
+ r.on('end', resolve);
421
+ r.on('error', reject);
422
+ });
423
+ up.on('error', reject);
424
+ req.on('close', () => { try { up.destroy(); } catch { /* */ } });
425
+ if (req.method !== 'GET' && req.method !== 'HEAD' && body && body.length) up.write(body);
426
+ up.end();
427
+ });
428
+ log(`cursor-backend: -> PIPE ${full} -> REAL ${host}`);
429
+ }
430
+
431
+ function rememberPod(fields, log) {
432
+ if (!fields || !fields[6]) return null;
433
+ const execDaemon = String(fields[6] || '');
434
+ const gateway = String(fields[10] || fields[6] || '');
435
+ realPod = {
436
+ execDaemon,
437
+ agent: gateway, // /api/sendPrompt lives on gateway_url (1340), not exec_daemon (1337)
438
+ vnc: fields[7] ? String(fields[7]).split('/vnc.html')[0] : gateway,
439
+ vncPath: fields[7] ? String(fields[7]) : undefined,
440
+ token: String(fields[4] || ''),
441
+ accessToken: String(fields[11] || ''),
442
+ p1340: gateway,
443
+ p6081: fields[12] ? String(fields[12]) : undefined,
444
+ region: String(fields[1] || 'us1'),
445
+ accountId: String(fields[2] || ''),
446
+ podId: String(fields[3] || ''),
447
+ };
448
+ sniffDump({ kind: 'pod', fields, realPod });
449
+ log(`cursor-backend: SNIFF real pod ${realPod.agent}`);
450
+ return realPod;
451
+ }
452
+
453
+ function rewrittenBox() {
454
+ const self = sniffSelf();
455
+ const p = realPod || {};
456
+ return encodeEnsureSandBox({
457
+ region: p.region || 'us1',
458
+ accountId: p.accountId || 'openzoo',
459
+ podId: p.podId || 'openzoo-sniff',
460
+ token: p.token || 'openzoo',
461
+ accessToken: p.accessToken || p.token || 'openzoo',
462
+ agent: self,
463
+ vnc: self,
464
+ p1340: self,
465
+ p6081: self,
466
+ });
467
+ }
468
+
469
+ async function sniffEnsureSandBox(req, res, body, host, full, log) {
470
+ const upstream = cursorUpstream(host);
471
+ const headers = copyReqHeaders(req, upstream);
472
+ const cap = await upstreamUnary({
473
+ host: upstream, path: full, method: req.method, headers, body, timeoutMs: 30000,
474
+ });
475
+ const raw = inflateBody(cap.buf, cap.respHeaders);
476
+ const proto = unwrapConnect(raw);
477
+ const fields = decodeProtoFields(proto);
478
+ rememberPod(fields, log);
479
+ sniffDump({
480
+ kind: 'EnsureSandBox',
481
+ status: cap.status,
482
+ bytes: cap.buf.length,
483
+ inflated: raw.length,
484
+ encoding: cap.respHeaders?.['content-encoding'] || '',
485
+ head: Buffer.from(raw.subarray(0, 24)).toString('hex'),
486
+ fields,
487
+ rewrittenTo: sniffSelf(),
488
+ });
489
+ const payload = realPod ? rewrittenBox() : proto;
490
+ const ct = String(req.headers['content-type'] || cap.respHeaders?.['content-type'] || '');
491
+ if (/WatchSandBoxMigration/.test(full) || ct.includes('connect+proto')) {
492
+ res.writeHead(200, {
493
+ 'content-type': 'application/connect+proto',
494
+ 'grpc-status': '0',
495
+ ...CORS,
496
+ });
497
+ const end = Buffer.from('{}');
498
+ const h = Buffer.alloc(5); h.writeUInt8(0x02, 0); h.writeUInt32BE(end.length, 1);
499
+ res.end(Buffer.concat([envelope(payload), h, end]));
500
+ } else if (ct.includes('grpc-web')) {
501
+ res.writeHead(200, {
502
+ 'content-type': ct.includes('text') ? 'application/grpc-web-text+proto' : 'application/grpc-web+proto',
503
+ 'grpc-status': '0', ...CORS,
504
+ });
505
+ res.end(Buffer.concat([envelope(payload), grpcWebTrailer()]));
506
+ } else {
507
+ res.writeHead(200, { 'content-type': 'application/proto', ...CORS });
508
+ res.end(payload);
509
+ }
510
+ log(`cursor-backend: SNIFF EnsureSandBox -> rewrite ${sniffSelf()} (${cap.status}, real ${cap.buf.length}b)`);
511
+ }
512
+
513
+ async function proxyPodHttp(req, res, full, body, log) {
514
+ if (!realPod?.agent) return false;
515
+ const agent = new URL(realPod.agent);
516
+ const headers = copyReqHeaders(req, agent.host);
517
+ if (realPod.accessToken && !headers.authorization && !headers.Authorization) {
518
+ headers.authorization = `Bearer ${realPod.accessToken}`;
519
+ }
520
+ if (realPod.token && !headers['x-anyrun-network-token']) {
521
+ headers['x-anyrun-network-token'] = realPod.token;
522
+ }
523
+ if (!headers['x-sand-slim-avatars']) headers['x-sand-slim-avatars'] = '1';
524
+ const path0 = (full || '').split('?')[0];
525
+ const interesting = /sendPrompt|Transcript|listAgents|promptAcceptance|openAgentTail|createAgent/i.test(path0);
526
+ if (path0 === '/events') {
527
+ const ip = await resolveRealAnthropic(agent.hostname);
528
+ await new Promise((resolve, reject) => {
307
529
  const up = https.request({
308
- hostname: host, servername: host, port: 443, path: full,
309
- method: req.method, headers, lookup,
530
+ hostname: agent.hostname, servername: agent.hostname, port: 443, path: full,
531
+ method: req.method, headers, lookup: lookupPinned(ip),
310
532
  }, (r) => {
533
+ const h = { ...CORS };
534
+ for (const [k, v] of Object.entries(r.headers || {})) {
535
+ if (RESP_DROP.has(k.toLowerCase())) continue;
536
+ h[k] = v;
537
+ }
538
+ res.writeHead(r.statusCode, h);
311
539
  const chunks = [];
312
- r.on('data', (d) => chunks.push(d));
313
- r.on('end', () => resolve({
314
- status: r.statusCode, respHeaders: r.headers, buf: Buffer.concat(chunks),
315
- }));
540
+ r.on('data', (d) => {
541
+ res.write(d);
542
+ if (chunks.length < 40) chunks.push(d);
543
+ });
544
+ r.on('end', () => {
545
+ sniffDump({ kind: 'sse', path: full, sample: Buffer.concat(chunks).toString('utf8').slice(0, 8000) });
546
+ try { res.end(); } catch { /* */ }
547
+ resolve();
548
+ });
316
549
  r.on('error', reject);
317
550
  });
318
551
  up.on('error', reject);
319
- // WatchSandBoxMigration is a LONG POLL a 20s cap aborts it every time and
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);
552
+ req.on('close', () => { try { up.destroy(); } catch { /* */ } });
324
553
  up.end();
325
554
  });
555
+ log('cursor-backend: SNIFF /events -> real pod (piped)');
556
+ return true;
557
+ }
558
+ try {
559
+ const cap = await upstreamUnary({
560
+ host: agent.hostname,
561
+ path: full,
562
+ method: req.method,
563
+ headers,
564
+ body,
565
+ timeoutMs: path0 === '/health' ? 8000 : 120000,
566
+ });
567
+ writeCaptured(res, cap.status, cap.respHeaders, cap.buf);
568
+ const rec = {
569
+ kind: 'pod-http',
570
+ method: req.method,
571
+ path: path0,
572
+ status: cap.status,
573
+ reqBytes: body?.length || 0,
574
+ resBytes: cap.buf.length,
575
+ req: jsonish(body, 4000),
576
+ res: jsonish(inflateBody(cap.buf, cap.respHeaders), 16000),
577
+ };
578
+ sniffDump(rec);
579
+ if (interesting) {
580
+ log(`cursor-backend: SNIFF ${path0} ${cap.status} req=${JSON.stringify(rec.req).slice(0, 220)} res=${JSON.stringify(rec.res).slice(0, 500)}`);
581
+ } else {
582
+ log(`cursor-backend: SNIFF ${path0} -> real pod (${cap.status}, ${cap.buf.length}b)`);
583
+ }
584
+ return true;
585
+ } catch (e) {
586
+ log(`cursor-backend: SNIFF ${path0} FAILED ${e.message}`);
587
+ return false;
588
+ }
589
+ }
590
+
591
+ /** JSON for the in-pod agent HTTP API. Hijack points field-6 at THIS
592
+ * server, so GET /health and POST /api/* land here — empty protobuf on
593
+ * those is "unhealthy" and Grok Bot retries EnsureSandBox forever
594
+ * (measured: /health empty-ok then EnsureSandBox #1945+). */
595
+ function jsonSend(res, obj) {
596
+ res.writeHead(200, { 'content-type': 'application/json', ...CORS });
597
+ res.end(JSON.stringify(obj));
598
+ }
599
+ /** Box HTTP API envelope. Client parse (CVr) returns null unless both
600
+ * status==="ok" AND "value" in n — our unwrapped {ok:true}/{entries} was
601
+ * dropped, so Grok Bot never painted the zoo reply (measured: << zoo 200 91c
602
+ * then tail polls with Failed to send). */
603
+ function jsonApi(res, value) {
604
+ jsonSend(res, { status: 'ok', value });
605
+ }
606
+
607
+ const sseClients = new Set();
608
+ /** Gateway SSE parser (asar dispatchEventBlock) only reads `data:` lines and
609
+ * requires `{channel, payload}`. `event:` names are ignored. */
610
+ function ssePush(channel, payload) {
611
+ const chunk = `data: ${JSON.stringify({ channel, payload })}\n\n`;
612
+ for (const c of sseClients) {
613
+ try { c.write(chunk); } catch { sseClients.delete(c); }
614
+ }
615
+ }
326
616
 
327
- const h = {};
328
- for (const [k, v] of Object.entries(respHeaders)) {
329
- if (RESP_DROP.has(k.toLowerCase())) continue;
330
- h[k] = v;
617
+ /** Grok Bot Helper daemon: GET /local-exec/requests is SSE, POST /local-exec/responses
618
+ * is `{providerId, frames}`. We used to JSON-[] the GET which is "disconnected"
619
+ * (measured: Grok Bot "can't see files on your local computer"). */
620
+ const localExecSse = new Set();
621
+ const localExecWaiters = new Map();
622
+ let localExecHello = null;
623
+ function localExecPush(frame) {
624
+ const chunk = `data: ${JSON.stringify(frame)}\n\n`;
625
+ for (const c of localExecSse) {
626
+ try { c.write(chunk); } catch { localExecSse.delete(c); }
627
+ }
628
+ }
629
+ function handleLocalExecFrames(frames, log) {
630
+ for (const f of frames || []) {
631
+ if (!f || typeof f !== 'object') continue;
632
+ if (f.kind === 'hello') {
633
+ localExecHello = f;
634
+ log(`cursor-backend: local-exec hello computer=${f.computerId || f.label || '?'} root=${f.localRoot || '?'}`);
331
635
  }
332
- res.writeHead(status, h);
333
- res.end(buf);
334
- // DUMP THE INTERESTING ONES. EnsureSandBox's response is what names the
335
- // sandbox, and the sandbox is the candidate inference path — you cannot
336
- // read that from a byte count. Written raw for offline decoding.
337
- if (process.env.OPENZOO_DUMP === '1' && buf.length) {
338
- try {
339
- const dir = '/tmp/openzoo-sniff';
340
- fs.mkdirSync(dir, { recursive: true });
341
- const safe = full.replace(/[^a-zA-Z0-9]+/g, '_').slice(0, 80);
342
- fs.writeFileSync(`${dir}/${Date.now()}_${safe}.bin`, buf);
343
- } catch { /* dumping must never break the proxy */ }
636
+ const w = f.requestId && localExecWaiters.get(f.requestId);
637
+ if (!w) continue;
638
+ if (f.kind === 'file') {
639
+ const buf = Buffer.from(f.bytesBase64 || '', 'base64');
640
+ w.resolve({ kind: 'file', bytes: buf, text: buf.toString('utf8') });
641
+ localExecWaiters.delete(f.requestId);
642
+ } else if (f.kind === 'file-error' || f.kind === 'messages-error') {
643
+ w.reject(new Error(f.error || 'local-exec error'));
644
+ localExecWaiters.delete(f.requestId);
645
+ } else if (f.kind === 'client' || f.kind === 'control' || f.kind === 'result' || f.kind === 'exec-result') {
646
+ w.resolve({
647
+ kind: f.kind,
648
+ message: f.message || f.text || f.stdout || JSON.stringify(f),
649
+ stdout: f.stdout,
650
+ stderr: f.stderr,
651
+ exitCode: f.exitCode,
652
+ });
653
+ localExecWaiters.delete(f.requestId);
654
+ }
655
+ }
656
+ }
657
+ function localExecAsk(frame, timeoutMs = 45000) {
658
+ const requestId = frame.requestId || randomUUID();
659
+ const job = { ...frame, requestId };
660
+ return new Promise((resolve, reject) => {
661
+ const t = setTimeout(() => {
662
+ localExecWaiters.delete(requestId);
663
+ reject(new Error('local-exec timeout — is Grok Bot Helper connected?'));
664
+ }, timeoutMs);
665
+ localExecWaiters.set(requestId, {
666
+ resolve: (v) => { clearTimeout(t); resolve(v); },
667
+ reject: (e) => { clearTimeout(t); reject(e); },
668
+ });
669
+ localExecPush(job);
670
+ });
671
+ }
672
+ async function handleLocalExecHttp(req, res, path0, body, log) {
673
+ if (!/^\/local-exec\//.test(path0)) return false;
674
+ if (req.method === 'GET' && /\/requests$/.test(path0)) {
675
+ res.writeHead(200, {
676
+ 'content-type': 'text/event-stream',
677
+ 'cache-control': 'no-cache',
678
+ ...CORS,
679
+ });
680
+ localExecSse.add(res);
681
+ // Daemon parser (asar CNt) ignores unknown welcome; a comment ping is enough
682
+ // to keep the stream alive until it POSTs hello to /responses.
683
+ res.write(': openzoo local-exec\n\n');
684
+ const iv = setInterval(() => {
685
+ try { res.write(': ping\n\n'); } catch { clearInterval(iv); localExecSse.delete(res); }
686
+ }, 10000);
687
+ req.on('close', () => { clearInterval(iv); localExecSse.delete(res); log('cursor-backend: local-exec sse closed'); });
688
+ log(`cursor-backend: -> local-exec /requests sse n=${localExecSse.size}`);
689
+ return true;
690
+ }
691
+ if (/\/responses$/.test(path0)) {
692
+ let parsed = {};
693
+ try { parsed = JSON.parse(String(body || '{}')); } catch { parsed = {}; }
694
+ handleLocalExecFrames(parsed.frames, log);
695
+ jsonSend(res, { ok: true });
696
+ log(`cursor-backend: -> local-exec /responses n=${(parsed.frames || []).length} kinds=${(parsed.frames || []).map((f) => f?.kind).join(',')}`);
697
+ return true;
698
+ }
699
+ jsonSend(res, { ok: true });
700
+ return true;
701
+ }
702
+ function extractLocalPaths(text) {
703
+ const out = [];
704
+ const re = /(?:~\/|\/Users\/)[^\s,;:!?()[\]{}"'`]+/g;
705
+ let m;
706
+ while ((m = re.exec(String(text || '')))) {
707
+ out.push(m[0].replace(/[.,;:]+$/, ''));
708
+ }
709
+ return [...new Set(out)];
710
+ }
711
+ function expandUserPath(p) {
712
+ if (p.startsWith('~/')) return path.join(os.homedir(), p.slice(2));
713
+ return p;
714
+ }
715
+
716
+ /** Per-agent transcript. sendPrompt is async: accept immediately, zooComplete
717
+ * fills this, getAgentTranscriptTail is what the UI actually polls.
718
+ *
719
+ * Gateway `isValidTranscriptEntry` (asar Fgi/RC) DROPS anything that is not
720
+ * `{id, kind:"message", content}` / `{id, kind:"send-message", message}` —
721
+ * proto wrappers with entryKind/body were counted n=2/2 here and painted
722
+ * as zero on the client, so overlay timed out → "Failed to send". */
723
+ const transcripts = new Map();
724
+ const tailedAgents = new Set();
725
+ let lastSendEchoId = `oz-${Date.now()}`;
726
+ function agentTranscript(id) {
727
+ let t = transcripts.get(id);
728
+ if (!t) { t = { seq: 0, entries: [] }; transcripts.set(id, t); }
729
+ return t;
730
+ }
731
+ function appendLine(agentId, role, text, extra = {}) {
732
+ const t = agentTranscript(agentId);
733
+ t.seq += 1;
734
+ const nonce = extra.clientNonce ? String(extra.clientNonce) : undefined;
735
+ const ts = Date.now();
736
+ const requestId = extra.requestId || nonce || `oz-req-${t.seq}`;
737
+ let e;
738
+ if (role === 'user') {
739
+ // Live cursorvm getAgentTranscriptTail (2026-08-29): user echo is
740
+ // kind:"message" + role:"user" + clientNonce. Assistant is kind:"send-message".
741
+ e = {
742
+ seq: t.seq,
743
+ kind: 'message',
744
+ id: extra.id || `t${t.seq}u`,
745
+ role: 'user',
746
+ content: String(text || ''),
747
+ richText: extra.richText || JSON.stringify({ type: 'doc', content: [{ type: 'paragraph', content: [{ type: 'text', text: String(text || '') }] }] }),
748
+ isStreaming: false,
749
+ timestampMs: ts,
750
+ ...(nonce ? { clientNonce: nonce } : {}),
751
+ requestId,
752
+ };
753
+ } else {
754
+ e = {
755
+ seq: t.seq,
756
+ kind: 'send-message',
757
+ id: extra.id || `t${t.seq}s0`,
758
+ message: { type: 'text', content: String(text || '') },
759
+ timestampMs: ts,
760
+ requestId,
761
+ };
762
+ }
763
+ t.entries.push(e);
764
+ return e;
765
+ }
766
+ function fanoutLine(primaryId, role, text, extra = {}) {
767
+ const ids = new Set([primaryId, ...tailedAgents]);
768
+ let last = null;
769
+ for (const id of ids) last = appendLine(id, role, text, extra);
770
+ return last;
771
+ }
772
+ function gatewayEntry(e) {
773
+ const { seq, ...rest } = e;
774
+ return rest;
775
+ }
776
+
777
+ function promptFromSendBody(raw) {
778
+ let obj = raw;
779
+ if (Buffer.isBuffer(raw) || typeof raw === 'string') {
780
+ try { obj = JSON.parse(String(raw)); } catch { return String(raw || ''); }
781
+ }
782
+ if (!obj || typeof obj !== 'object') return '';
783
+ const pick = (v) => (typeof v === 'string' && v.trim() ? v : '');
784
+ let fromMsgs = '';
785
+ if (Array.isArray(obj.messages)) {
786
+ for (let i = obj.messages.length - 1; i >= 0; i--) {
787
+ const m = obj.messages[i];
788
+ fromMsgs = pick(m?.content) || pick(m?.text) || pick(m?.prompt);
789
+ if (fromMsgs) break;
790
+ }
791
+ }
792
+ return pick(obj.prompt) || pick(obj.text) || pick(obj.message)
793
+ || pick(obj.content) || pick(obj.input)
794
+ || pick(obj.message?.content) || pick(obj.message?.text)
795
+ || fromMsgs
796
+ || '';
797
+ }
798
+
799
+ let walletUsdCache = { usd: null, at: 0 };
800
+ async function walletUsdCached() {
801
+ if (walletUsdCache.usd != null && Date.now() - walletUsdCache.at < 60_000) return walletUsdCache.usd;
802
+ try {
803
+ const { affordableUsd } = await import('./info.js');
804
+ const n = await Promise.race([
805
+ affordableUsd(),
806
+ new Promise((_, rej) => setTimeout(() => rej(new Error('balance timeout')), 4000)),
807
+ ]);
808
+ if (Number.isFinite(n)) {
809
+ walletUsdCache = { usd: Number(n), at: Date.now() };
810
+ return walletUsdCache.usd;
344
811
  }
345
- log(`cursor-backend: ${req.method} ${full} -> REAL ${host} (${status}, ${buf.length}b)`);
812
+ } catch { /* keep last */ }
813
+ return walletUsdCache.usd;
814
+ }
815
+
816
+ async function zooSpendOverlay(data) {
817
+ const x = data?.x402 || {};
818
+ let info = {};
819
+ try {
820
+ const r = await fetch('http://127.0.0.1:8402/v1/info', { signal: AbortSignal.timeout(1500) });
821
+ if (r.ok) info = await r.json();
822
+ } catch { /* */ }
823
+ let session = {};
824
+ try {
825
+ session = JSON.parse(fs.readFileSync(path.join(os.homedir(), '.openzoo', 'session.json'), 'utf8'));
826
+ } catch { /* */ }
827
+ const spent = Number(info.spendUsd ?? session.spentUsd ?? x.billedUsd ?? 0);
828
+ const would = Number(info.directUsd ?? session.directUsd ?? x.directUsd ?? 0);
829
+ const saved = Number(info.savedUsd ?? Math.max(0, would - spent));
830
+ const pct = would > 0 ? (100 * saved / would) : 0;
831
+ const credit = Number(info.creditUsd);
832
+ const wallet = await walletUsdCached();
833
+ const bal = Number.isFinite(wallet) && wallet > 0.004
834
+ ? wallet
835
+ : (Number.isFinite(credit) && credit > 0.004 ? credit : null);
836
+ const lines = ['', ''];
837
+ if (x.billedUsd != null) {
838
+ lines.push(`this call $${Number(x.billedUsd).toFixed(6)} · OpenRouter $${Number(x.directUsd || 0).toFixed(6)}`);
839
+ }
840
+ const balTxt = bal != null ? ` · balance $${bal.toFixed(2)}` : '';
841
+ lines.push(`spent $${spent.toFixed(4)}${balTxt} · OpenRouter would $${would.toFixed(4)} · saved $${saved.toFixed(4)} (${pct.toFixed(0)}%)`);
842
+ return lines.join('\n');
843
+ }
844
+
845
+ const MODELS_PATH = path.join(os.homedir(), '.openzoo', 'grokbot-models.json');
846
+ function loadAgentModels() {
847
+ try {
848
+ return new Map(Object.entries(JSON.parse(fs.readFileSync(MODELS_PATH, 'utf8'))));
849
+ } catch { return new Map(); }
850
+ }
851
+ function saveAgentModels() {
852
+ try {
853
+ fs.mkdirSync(path.dirname(MODELS_PATH), { recursive: true });
854
+ fs.writeFileSync(MODELS_PATH, JSON.stringify(Object.fromEntries(agentModels)));
855
+ } catch { /* */ }
856
+ }
857
+ const agentModels = loadAgentModels();
858
+ const MODEL_ALIASES = {
859
+ fable: 'anthropic/claude-fable-5',
860
+ 'fable-5': 'anthropic/claude-fable-5',
861
+ 'claude-fable-5': 'anthropic/claude-fable-5',
862
+ opus: 'anthropic/claude-opus-5',
863
+ 'opus-5': 'anthropic/claude-opus-5',
864
+ sonnet: 'anthropic/claude-sonnet-5',
865
+ grok: 'x-ai/grok-4.6',
866
+ 'grok-4': 'x-ai/grok-4.6',
867
+ 'grok-4.6': 'x-ai/grok-4.6',
868
+ };
869
+ function resolveModelId(raw) {
870
+ const s = String(raw || '').trim();
871
+ if (!s) return null;
872
+ const lower = s.toLowerCase().replace(/^\/+/, '');
873
+ if (MODEL_ALIASES[lower]) return MODEL_ALIASES[lower];
874
+ if (s.includes('/')) return s;
875
+ return null;
876
+ }
877
+ function currentModel(agentId) {
878
+ return agentModels.get(agentId)
879
+ || process.env.OPENZOO_DEFAULT_MODEL
880
+ || 'x-ai/grok-4.6';
881
+ }
882
+
883
+ const execFileAsync = promisify(execFile);
884
+ const IMAGE_EXT = /\.(png|jpe?g|gif|webp|bmp|heic|svg)$/i;
885
+ const IMAGE_MAGIC = [
886
+ [Buffer.from([0x89, 0x50, 0x4e, 0x47]), 'image/png'],
887
+ [Buffer.from([0xff, 0xd8, 0xff]), 'image/jpeg'],
888
+ [Buffer.from('GIF8'), 'image/gif'],
889
+ [Buffer.from('RIFF'), 'image/webp'],
890
+ ];
891
+ function mimeFromBytes(buf, p = '') {
892
+ if (IMAGE_EXT.test(p)) {
893
+ const ext = path.extname(p).toLowerCase();
894
+ return { 'png': 'image/png', '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.webp': 'image/webp', '.bmp': 'image/bmp', '.heic': 'image/heic', '.svg': 'image/svg+xml' }[ext] || 'image/png';
895
+ }
896
+ for (const [magic, mime] of IMAGE_MAGIC) {
897
+ if (buf.length >= magic.length && buf.subarray(0, magic.length).equals(magic)) return mime;
898
+ }
899
+ return null;
900
+ }
901
+ function dataUrlsFromRichText(rt) {
902
+ const s = String(rt || '');
903
+ const out = [];
904
+ const re = /data:image\/[a-zA-Z0-9.+-]+;base64,[A-Za-z0-9+/]+=*/g;
905
+ let m;
906
+ while ((m = re.exec(s))) out.push(m[0]);
907
+ return out;
908
+ }
909
+ function attachmentList(parsed, prompt) {
910
+ const out = [];
911
+ const seen = new Set();
912
+ const add = (raw, name) => {
913
+ if (!raw || seen.has(raw)) return;
914
+ seen.add(raw);
915
+ out.push({ raw, name });
916
+ };
917
+ const paths = parsed?.attachmentPaths;
918
+ const names = parsed?.attachmentNames;
919
+ if (Array.isArray(paths)) {
920
+ for (let i = 0; i < paths.length; i++) add(String(paths[i]), names?.[i]);
921
+ }
922
+ for (const p of extractLocalPaths(prompt)) {
923
+ if (/[*?]/.test(p)) continue;
924
+ add(p);
925
+ }
926
+ const rt = String(parsed?.richText || '');
927
+ const srcRe = /(?:src|path|filePath|url)"?\s*[:=]\s*"((?:file:\/\/|\/(?:var|tmp|private|Users)|~\/)[^"]+\.(?:png|jpe?g|gif|webp|bmp|heic))"/gi;
928
+ let sm;
929
+ while ((sm = srcRe.exec(rt))) {
930
+ add(sm[1].replace(/^file:\/\//, ''));
931
+ }
932
+ return out;
933
+ }
934
+
935
+ async function readLocalBytes(abs, log) {
936
+ if (localExecSse.size > 0) {
937
+ log(`cursor-backend: local-exec download ${abs}`);
938
+ const got = await localExecAsk({ kind: 'download', path: abs });
939
+ return got.bytes || Buffer.from(got.text || '', 'utf8');
940
+ }
941
+ return fs.readFileSync(abs);
942
+ }
943
+ async function writeLocalBytes(abs, bytes, log) {
944
+ const buf = Buffer.isBuffer(bytes) ? bytes : Buffer.from(String(bytes ?? ''), 'utf8');
945
+ if (localExecSse.size > 0) {
946
+ log(`cursor-backend: local-exec upload ${abs} ${buf.length}b`);
947
+ await localExecAsk({
948
+ kind: 'upload',
949
+ path: abs,
950
+ bytesBase64: buf.toString('base64'),
951
+ });
952
+ return `wrote ${abs} (${buf.length} bytes) via local-exec`;
953
+ }
954
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
955
+ fs.writeFileSync(abs, buf);
956
+ return `wrote ${abs} (${buf.length} bytes)`;
957
+ }
958
+ async function execLocal(command, cwd, log) {
959
+ const dir = cwd ? expandUserPath(cwd) : os.homedir();
960
+ if (localExecSse.size > 0) {
961
+ log(`cursor-backend: local-exec exec ${JSON.stringify(command).slice(0, 80)}`);
962
+ const got = await localExecAsk({
963
+ kind: 'exec',
964
+ serverMessage: { command, cwd: dir, workingDirectory: dir },
965
+ }, 60000);
966
+ const out = got.stdout || got.message || '';
967
+ const err = got.stderr ? `\nstderr:\n${got.stderr}` : '';
968
+ return `${out}${err}`.trim() || `(exit ${got.exitCode ?? 0})`;
969
+ }
970
+ const { stdout, stderr } = await execFileAsync('/bin/zsh', ['-lc', command], {
971
+ cwd: dir,
972
+ timeout: 30000,
973
+ maxBuffer: 2 * 1024 * 1024,
974
+ env: process.env,
975
+ });
976
+ return `${stdout || ''}${stderr ? `\nstderr:\n${stderr}` : ''}`.trim() || '(no output)';
977
+ }
978
+
979
+ const LOCAL_TOOLS = [
980
+ {
981
+ type: 'function',
982
+ function: {
983
+ name: 'read_file',
984
+ description: 'Read a file on the user\'s Mac. Use this for any local path.',
985
+ parameters: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] },
986
+ },
987
+ },
988
+ {
989
+ type: 'function',
990
+ function: {
991
+ name: 'write_file',
992
+ description: 'Write a file on the user\'s Mac. Put HTML/games/code on disk — do not dump huge files in chat.',
993
+ parameters: {
994
+ type: 'object',
995
+ properties: { path: { type: 'string' }, content: { type: 'string' } },
996
+ required: ['path', 'content'],
997
+ },
998
+ },
999
+ },
1000
+ {
1001
+ type: 'function',
1002
+ function: {
1003
+ name: 'exec',
1004
+ description: 'Run a shell command on the user\'s Mac (zsh -lc). Use for npm, curl, ls, git, installing MCP, etc.',
1005
+ parameters: {
1006
+ type: 'object',
1007
+ properties: { command: { type: 'string' }, cwd: { type: 'string' } },
1008
+ required: ['command'],
1009
+ },
1010
+ },
1011
+ },
1012
+ {
1013
+ type: 'function',
1014
+ function: {
1015
+ name: 'list_dir',
1016
+ description: 'List a directory on the user\'s Mac.',
1017
+ parameters: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] },
1018
+ },
1019
+ },
1020
+ ];
1021
+
1022
+ async function runLocalTool(name, args, log) {
1023
+ try {
1024
+ if (name === 'read_file') {
1025
+ const buf = await readLocalBytes(expandUserPath(args.path), log);
1026
+ if (buf.length > 180000) return `file ${args.path} is ${buf.length} bytes; first 180000:\n${buf.subarray(0, 180000).toString('utf8')}`;
1027
+ return buf.toString('utf8');
1028
+ }
1029
+ if (name === 'write_file') {
1030
+ return await writeLocalBytes(expandUserPath(args.path), args.content ?? '', log);
1031
+ }
1032
+ if (name === 'list_dir') {
1033
+ const abs = expandUserPath(args.path || os.homedir());
1034
+ if (localExecSse.size > 0) {
1035
+ const got = await localExecAsk({ kind: 'exec', serverMessage: { command: `ls -la ${JSON.stringify(abs)}`, cwd: os.homedir() } });
1036
+ return got.stdout || got.message || '';
1037
+ }
1038
+ return fs.readdirSync(abs, { withFileTypes: true })
1039
+ .map((e) => `${e.isDirectory() ? 'd' : '-'} ${e.name}`)
1040
+ .join('\n');
1041
+ }
1042
+ if (name === 'exec') {
1043
+ return await execLocal(String(args.command || ''), args.cwd, log);
1044
+ }
1045
+ return `unknown tool ${name}`;
346
1046
  } catch (e) {
347
- res.writeHead(502, { 'content-type': 'application/json' });
348
- res.end(JSON.stringify({ type: 'error', error: { type: 'api_error', message: `passthrough failed: ${e.message}` } }));
349
- log(`cursor-backend: passthrough ${full} FAILED: ${e.message}`);
1047
+ return `ERROR ${e.message}`;
1048
+ }
1049
+ }
1050
+
1051
+ function zooTextFromMessage(msg, data) {
1052
+ let c = msg?.content;
1053
+ if (Array.isArray(c)) {
1054
+ c = c.map((p) => (typeof p === 'string' ? p : (p?.text || p?.content || ''))).join('');
350
1055
  }
1056
+ if (typeof c === 'string' && c.trim()) return c;
1057
+ if (typeof data?.error?.message === 'string' && data.error.message) return data.error.message;
1058
+ return '';
1059
+ }
1060
+
1061
+ async function zooComplete(prompt, log, agentId, parsed = {}) {
1062
+ const model = currentModel(agentId);
1063
+ const helper = localExecSse.size > 0;
1064
+ log(`cursor-backend: zoo POST :8402 model=${model} helper=${helper ? localExecSse.size : 0} ${JSON.stringify((prompt || '').slice(0, 60))}`);
1065
+
1066
+ const images = [];
1067
+ const textFiles = [];
1068
+ for (const { raw, name } of attachmentList(parsed, prompt)) {
1069
+ const abs = expandUserPath(raw);
1070
+ try {
1071
+ const buf = await readLocalBytes(abs, log);
1072
+ const mime = mimeFromBytes(buf, name || raw);
1073
+ if (mime) {
1074
+ images.push({ path: raw, mime, dataUrl: `data:${mime};base64,${buf.toString('base64')}` });
1075
+ log(`cursor-backend: attached image ${raw} ${mime} ${buf.length}b`);
1076
+ } else {
1077
+ textFiles.push({ path: raw, abs, text: buf.toString('utf8') });
1078
+ }
1079
+ } catch (e) {
1080
+ textFiles.push({ path: raw, abs, error: e.message });
1081
+ }
1082
+ }
1083
+ for (const url of dataUrlsFromRichText(parsed.richText)) {
1084
+ images.push({ path: '(richText)', mime: 'image', dataUrl: url });
1085
+ }
1086
+
1087
+ const via = helper ? 'Grok Bot Helper local-exec SSE' : 'this Mac (hijack process; Helper SSE not connected yet)';
1088
+ const messages = [
1089
+ {
1090
+ role: 'system',
1091
+ content: [
1092
+ `You are ${model} served through openzoo inside Grok Bot.`,
1093
+ `You HAVE local tools on the user's computer via ${via}.`,
1094
+ 'Tools: read_file, write_file, exec, list_dir. USE THEM. Write files to disk instead of pasting giant HTML into chat.',
1095
+ 'Pasted images arrive as attachments — you can see them when present. Do not claim you cannot see images if they are in this turn.',
1096
+ 'Never claim you lack filesystem access or local-exec. If a tool errors, report the error.',
1097
+ 'A spend footer is appended after your reply by the host — ignore it.',
1098
+ ].join(' '),
1099
+ },
1100
+ ];
1101
+ if (textFiles.length) {
1102
+ const bits = textFiles.map((a) => (
1103
+ a.error
1104
+ ? `FILE ${a.path} ERROR: ${a.error}`
1105
+ : `FILE ${a.path} (${a.abs})\n${String(a.text).slice(0, 180000)}`
1106
+ ));
1107
+ messages.push({ role: 'user', content: bits.join('\n\n') });
1108
+ }
1109
+
1110
+ const userContent = [];
1111
+ for (const img of images) {
1112
+ userContent.push({ type: 'image_url', image_url: { url: img.dataUrl } });
1113
+ }
1114
+ userContent.push({ type: 'text', text: prompt || (images.length ? '(see attached image)' : 'hello') });
1115
+ messages.push({
1116
+ role: 'user',
1117
+ content: userContent.length === 1 && userContent[0].type === 'text'
1118
+ ? userContent[0].text
1119
+ : userContent,
1120
+ });
1121
+
1122
+ const maxTok = Number(process.env.OPENZOO_ASK_MAX_TOKENS || 4096);
1123
+ let lastData = {};
1124
+ let text = '';
1125
+ for (let step = 0; step < 8; step++) {
1126
+ const payload = {
1127
+ model,
1128
+ messages,
1129
+ tools: LOCAL_TOOLS,
1130
+ tool_choice: 'auto',
1131
+ max_tokens: maxTok,
1132
+ };
1133
+ const post = () => fetch('http://127.0.0.1:8402/v1/chat/completions', {
1134
+ method: 'POST',
1135
+ headers: { 'content-type': 'application/json', authorization: 'Bearer sk-openzoo' },
1136
+ body: JSON.stringify(payload),
1137
+ signal: AbortSignal.timeout(120000),
1138
+ });
1139
+ let r = await post();
1140
+ if (r.status === 402) {
1141
+ log('cursor-backend: x402 402 — dwell/retry');
1142
+ await new Promise((ok) => setTimeout(ok, 2500));
1143
+ r = await post();
1144
+ }
1145
+ const data = await r.json();
1146
+ lastData = data;
1147
+ const msg = data.choices?.[0]?.message || {};
1148
+ const calls = Array.isArray(msg.tool_calls) ? msg.tool_calls : [];
1149
+ if (calls.length) {
1150
+ log(`cursor-backend: zoo tools step=${step} n=${calls.length} ${calls.map((c) => c.function?.name || c.name).join(',')}`);
1151
+ messages.push(msg);
1152
+ for (const c of calls) {
1153
+ let args = {};
1154
+ try { args = JSON.parse(c.function?.arguments || c.arguments || '{}'); } catch { args = {}; }
1155
+ const name = c.function?.name || c.name || '';
1156
+ const result = await runLocalTool(name, args, log);
1157
+ messages.push({
1158
+ role: 'tool',
1159
+ tool_call_id: c.id,
1160
+ content: String(result).slice(0, 120000),
1161
+ });
1162
+ }
1163
+ continue;
1164
+ }
1165
+ text = zooTextFromMessage(msg, data) || (step ? '(tool loop ended with empty content)' : '(empty zoo reply)');
1166
+ log(`cursor-backend: << zoo ${r.status} ${text.length}c model=${data.model || model} finish=${data.choices?.[0]?.finish_reason || '?'}`);
1167
+ break;
1168
+ }
1169
+ try { text += await zooSpendOverlay(lastData); } catch { /* overlay must never eat the reply */ }
1170
+ return { text, data: lastData };
1171
+ }
1172
+
1173
+ async function handleHijackedPodHttp(req, res, full, body, log) {
1174
+ if (sniffOn()) {
1175
+ const waiting = (full || '').split('?')[0];
1176
+ const podPath = waiting === '/health' || waiting === '/healthz' || waiting === '/events'
1177
+ || waiting.startsWith('/api/') || waiting.startsWith('/webauthn/')
1178
+ || waiting.startsWith('/cookie-origin-approval/');
1179
+ if (waiting.startsWith('/local-exec/')) return handleLocalExecHttp(req, res, waiting, body, log);
1180
+ if (realPod?.agent && podPath) return proxyPodHttp(req, res, full, body, log);
1181
+ if (!podPath) return false;
1182
+ if (waiting === '/health' || waiting === '/healthz') {
1183
+ jsonSend(res, { ok: true, status: 'ok', ready: true });
1184
+ return true;
1185
+ }
1186
+ return false;
1187
+ }
1188
+ const path0 = (full || '').split('?')[0];
1189
+ if (path0 === '/health' || path0 === '/healthz') {
1190
+ jsonSend(res, { ok: true, status: 'ok', ready: true });
1191
+ log('cursor-backend: -> pod /health ok');
1192
+ return true;
1193
+ }
1194
+ if (path0 === '/events') {
1195
+ res.writeHead(200, {
1196
+ 'content-type': 'text/event-stream',
1197
+ 'cache-control': 'no-cache',
1198
+ ...CORS,
1199
+ });
1200
+ res.write('data: {"channel":"ping","payload":{}}\n\n');
1201
+ sseClients.add(res);
1202
+ const iv = setInterval(() => {
1203
+ try { res.write('data: {"channel":"ping","payload":{}}\n\n'); } catch { clearInterval(iv); sseClients.delete(res); }
1204
+ }, 15000);
1205
+ req.on('close', () => { clearInterval(iv); sseClients.delete(res); });
1206
+ log('cursor-backend: -> pod /events sse');
1207
+ return true;
1208
+ }
1209
+ if (path0.startsWith('/webauthn/') || path0.startsWith('/cookie-origin-approval/')) {
1210
+ if (req.method === 'GET') jsonSend(res, []);
1211
+ else jsonSend(res, { ok: true });
1212
+ log(`cursor-backend: -> pod ${path0}`);
1213
+ return true;
1214
+ }
1215
+ if (!path0.startsWith('/api/')) return false;
1216
+ const name = path0.slice('/api/'.length);
1217
+ // Roster/settings come from the REAL 1340 gateway (names, avatars, trays).
1218
+ // Chat stays local so inference is zoo. Discovered on EnsureSandBox rewrite.
1219
+ const roster = new Set([
1220
+ 'listAgents', 'countAgents', 'searchAgents', 'getTrays', 'getHostSettings',
1221
+ 'setHostSettings', 'getAgentChannels', 'getAgentWorkflows', 'skillsCatalog',
1222
+ 'getSubagents', 'getAsyncTasks', 'getForeverBoxStatus', 'getSharingState',
1223
+ 'getBotTemplateExportPolicy', 'getTeachRecordingStatus', 'isGlobalSearchEnabled',
1224
+ 'isEgressTunnelAvailable', 'listBoxMcpServers', 'getHostStatus',
1225
+ 'setWindowFocused', 'getAgentAutomations',
1226
+ 'createAgent', 'createAgentFromTemplate', 'createGroup', 'setGroupMembers',
1227
+ 'updateAgent', 'deleteAgents', 'duplicateAgent', 'kickstartAgent',
1228
+ 'interruptAgentRun', 'requestDiskSaverAudit', 'broadcastToAgents',
1229
+ 'setAgentUnread', 'setAgentHiddenFromSidebar', 'setAgentNotificationsEnabled',
1230
+ 'setAgentNotifyOnUpdates', 'setAgentAvatarBytes', 'getAgentAvatar',
1231
+ ]);
1232
+ if (!sniffOn() && realPod?.agent && roster.has(name)) {
1233
+ return proxyPodHttp(req, res, full, body, log);
1234
+ }
1235
+
1236
+ // THE ACTUAL CHAT PATH. Grok Bot does not send StreamUnifiedChat on this
1237
+ // surface — measured: POST /api/sendPrompt 374b/462b after EnsureSandBox
1238
+ // hijack. Stubbing {ok:true} ate the prompt. Forward to the paying proxy.
1239
+ if (name === 'sendPrompt') {
1240
+ let parsed = {};
1241
+ try { parsed = JSON.parse(String(body || '{}')); } catch { parsed = {}; }
1242
+ const prompt = promptFromSendBody(parsed);
1243
+ const agentId = String(parsed.agentId || parsed.id || 'openzoo');
1244
+ const nonce = parsed.clientNonce || `oz-${Date.now()}`;
1245
+ const attN = Array.isArray(parsed.attachmentPaths) ? parsed.attachmentPaths.length : 0;
1246
+ log(`cursor-backend: >> sendPrompt agent=${agentId} keys=${Object.keys(parsed).join(',')} attachments=${attN} prompt=${JSON.stringify((prompt || '').slice(0, 80))}`);
1247
+ lastSendEchoId = String(nonce);
1248
+ const userLine = fanoutLine(agentId, 'user', prompt, {
1249
+ clientNonce: nonce,
1250
+ requestId: nonce,
1251
+ richText: parsed.richText,
1252
+ });
1253
+ ssePush('transcript', { ...gatewayEntry(userLine), agentId });
1254
+ jsonSend(res, { accepted: true });
1255
+ const modelCmd = /^\s*\/model(?:\s+(\S+))?\s*$/i.exec(prompt || '');
1256
+ setImmediate(async () => {
1257
+ let text = '';
1258
+ try {
1259
+ if (modelCmd) {
1260
+ const want = modelCmd[1];
1261
+ if (!want) {
1262
+ const cur = currentModel(agentId);
1263
+ text = `current model: ${cur}\nset with /model fable | opus | sonnet | grok | provider/id`;
1264
+ } else {
1265
+ const id = resolveModelId(want) || (want.includes('/') ? want : null);
1266
+ if (!id) {
1267
+ text = `unknown model "${want}". try /model fable | opus | sonnet | grok or a full id like anthropic/claude-fable-5`;
1268
+ } else {
1269
+ agentModels.set(agentId, id);
1270
+ saveAgentModels();
1271
+ text = `model set to ${id}`;
1272
+ }
1273
+ }
1274
+ try { text += await zooSpendOverlay({}); } catch { /* */ }
1275
+ } else {
1276
+ const z = await zooComplete(prompt, log, agentId, parsed);
1277
+ text = z.text;
1278
+ }
1279
+ } catch (e) {
1280
+ text = `openzoo error: ${e.message}`;
1281
+ log(`cursor-backend: sendPrompt zoo failed: ${e.message}`);
1282
+ }
1283
+ const line = fanoutLine(agentId, 'assistant', text, { clientNonce: nonce, requestId: nonce });
1284
+ ssePush('transcript', { ...gatewayEntry(line), agentId });
1285
+ log(`cursor-backend: sendPrompt done agent=${agentId} seq=${line.seq} text=${JSON.stringify(text.slice(0, 80))}`);
1286
+ });
1287
+ return true;
1288
+ }
1289
+ if (name === 'getAgentTranscriptTail' || name === 'getAgentTranscriptWindow' || name === 'openAgentTail') {
1290
+ let parsed = {};
1291
+ try { parsed = JSON.parse(String(body || '{}')); } catch { parsed = {}; }
1292
+ let id = String(parsed.id || parsed.agentId || 'openzoo');
1293
+ tailedAgents.add(id);
1294
+ let t = agentTranscript(id);
1295
+ if (!t.entries.length) {
1296
+ for (const [other, ot] of transcripts) {
1297
+ if (ot.entries.length) { id = other; t = ot; break; }
1298
+ }
1299
+ }
1300
+ const limit = Math.min(Number(parsed.limit) || 50, 200);
1301
+ const before = parsed.beforeSeq != null ? Number(parsed.beforeSeq) : Infinity;
1302
+ const sliced = t.entries.filter((e) => e.seq < before).slice(-limit);
1303
+ const page = { entries: sliced.map(gatewayEntry) };
1304
+ if (t.entries.length > sliced.length && sliced.length) page.nextBeforeSeq = sliced[0].seq;
1305
+ // Live pod returns RAW {entries, nextBeforeSeq} — no CVr envelope
1306
+ // (measured 1340 getAgentTranscriptTail 2026-08-29). Wrapping {status,value}
1307
+ // made transcript-page validation fail and the canvas stayed empty.
1308
+ jsonSend(res, page);
1309
+ log(`cursor-backend: -> transcript ${id} n=${sliced.length}/${t.seq}`);
1310
+ return true;
1311
+ }
1312
+ if (name === 'promptAcceptanceStatus') {
1313
+ let parsed = {};
1314
+ try { parsed = JSON.parse(String(body || '{}')); } catch { parsed = {}; }
1315
+ const nonce = String(parsed.clientNonce || lastSendEchoId);
1316
+ const agentId = String(parsed.agentId || '');
1317
+ jsonSend(res, {
1318
+ outcome: 'found',
1319
+ record: {
1320
+ status: 'accepted',
1321
+ acceptedAtMs: Date.now(),
1322
+ echoEntryId: nonce,
1323
+ clientNonce: nonce,
1324
+ agentId,
1325
+ inputDigest: '',
1326
+ },
1327
+ });
1328
+ log(`cursor-backend: -> promptAcceptanceStatus found echo=${nonce}`);
1329
+ return true;
1330
+ }
1331
+
1332
+ const stubs = {
1333
+ getHostStatus: { status: 'ready', ready: true, state: 'ready', hostStatus: 'ready' },
1334
+ getHostSettings: { settings: {} },
1335
+ setHostSettings: { ok: true },
1336
+ setBoxSecrets: { ok: true },
1337
+ listAgents: [...new Set([...transcripts.keys(), ...tailedAgents])].map((id) => ({ id, name: id, status: 'ready' })),
1338
+ getAgentTranscriptTail: { tail: '', lines: [], dropped: false, ok: true },
1339
+ getTeachRecordingStatus: { recording: false },
1340
+ getTrays: { trays: [] },
1341
+ isGlobalSearchEnabled: { enabled: false },
1342
+ isEgressTunnelAvailable: { available: false },
1343
+ getSharingState: { sharing: false },
1344
+ getBotTemplateExportPolicy: { allowed: true },
1345
+ getAgentAutomations: { automations: [] },
1346
+ getForeverBoxStatus: { enabled: false },
1347
+ getSubagents: { subagents: [] },
1348
+ getAsyncTasks: { tasks: [] },
1349
+ getAgentWorkflows: { workflows: [] },
1350
+ setWindowFocused: { ok: true },
1351
+ };
1352
+ const payload = stubs[name] !== undefined ? stubs[name] : { ok: true };
1353
+ // Live 1340 listAgents is a RAW array, not CVr (measured 104674b 2026-08-29).
1354
+ if (name === 'listAgents') jsonSend(res, payload);
1355
+ else jsonApi(res, payload);
1356
+ log(`cursor-backend: -> pod /api/${name}`);
1357
+ return true;
351
1358
  }
352
1359
 
353
1360
  /**
@@ -408,6 +1415,9 @@ function respond(req, res, method, models) {
408
1415
  */
409
1416
  export function startCursorBackend({ port = 8443, models, log = () => {} } = {}) {
410
1417
  const { cert, key } = ensureCert(log);
1418
+ const certPem = fs.readFileSync(cert);
1419
+ const keyPem = fs.readFileSync(key);
1420
+ const secureContext = tls.createSecureContext({ cert: certPem, key: keyPem });
411
1421
  let conns = 0;
412
1422
  // SERVE BOTH h2 AND h1. An earlier build forced h1-only after concluding the
413
1423
  // editor's h2 connections reset — but that log was the STALE 8443 backend the
@@ -416,13 +1426,20 @@ export function startCursorBackend({ port = 8443, models, log = () => {} } = {})
416
1426
  // (ERR_SSL_NO_APPLICATION_PROTOCOL when we advertise just h1), so we must
417
1427
  // negotiate both. allowHTTP1 keeps the h1 fetch() calls (stripe/updates)
418
1428
  // working; ALPN h2 first satisfies the Connect gRPC client.
1429
+ // SNICallback MUST pass the SecureContext — cb(null) with no ctx is why the
1430
+ // Helper daemon's Node fetch never completed GET /local-exec/requests (TLS
1431
+ // ECONNRESET, no request log). Chromium --ignore-certificate-errors hid this
1432
+ // for the UI process.
419
1433
  const server = http2.createSecureServer(
420
1434
  {
421
- cert: fs.readFileSync(cert),
422
- key: fs.readFileSync(key),
1435
+ cert: certPem,
1436
+ key: keyPem,
423
1437
  allowHTTP1: true,
424
1438
  ALPNProtocols: ['h2', 'http/1.1'],
425
- SNICallback: (servername, cb) => { log(`cursor-tls: <- ClientHello SNI=${servername}`); cb(null); },
1439
+ SNICallback: (servername, cb) => {
1440
+ log(`cursor-tls: <- ClientHello SNI=${servername || '?'}`);
1441
+ cb(null, secureContext);
1442
+ },
426
1443
  },
427
1444
  async (req, res) => {
428
1445
  conns += 1;
@@ -494,13 +1511,38 @@ export function startCursorBackend({ port = 8443, models, log = () => {} } = {})
494
1511
  {
495
1512
  const path0 = full.split('?')[0];
496
1513
  const oauth = /^\/oauth(\/|$)/.test(path0);
497
- const needRealPod = /GrokBotService\/(EnsureSandBox|WatchSandBoxMigration)/.test(full)
498
- && !process.env.OZ_HIJACK_POD;
499
- if (oauth || needRealPod) {
1514
+ // Coordinator talks to /sand-box/local-exec-daemon-credential. empty-ok
1515
+ // here is ControlPortCallError: main-execution-failure: fetch failed.
1516
+ const sandBox = /^\/sand-box(\/|$)/.test(path0);
1517
+ const grokCred = /IssueGrokBotUserComputerCredential/.test(full);
1518
+ const sniffBox = sniffOn() && /GrokBotService\/(EnsureSandBox|WatchSandBoxMigration)/.test(full);
1519
+ const needRealPod = /GrokBotService\/EnsureSandBox/.test(full)
1520
+ && !process.env.OZ_HIJACK_POD
1521
+ && !sniffBox;
1522
+ if (sniffBox) {
1523
+ if (/WatchSandBoxMigration/.test(full) && realPod) {
1524
+ const payload = rewrittenBox();
1525
+ res.writeHead(200, {
1526
+ 'content-type': 'application/connect+proto',
1527
+ 'grpc-status': '0',
1528
+ ...CORS,
1529
+ });
1530
+ const end = Buffer.from('{}');
1531
+ const h = Buffer.alloc(5); h.writeUInt8(0x02, 0); h.writeUInt32BE(end.length, 1);
1532
+ res.end(Buffer.concat([envelope(payload), h, end]));
1533
+ log('cursor-backend: SNIFF WatchSandBoxMigration ready (rewritten)');
1534
+ return;
1535
+ }
1536
+ await sniffEnsureSandBox(req, res, body, host, full, log);
1537
+ return;
1538
+ }
1539
+ if (oauth || sandBox || grokCred || needRealPod) {
500
1540
  const upstream = /cursor\.sh$/.test(host) ? host : 'api2.cursor.sh';
501
1541
  await passthroughToRealAnthropic(req, res, body, upstream, full, log);
502
1542
  return;
503
1543
  }
1544
+ if (await handleLocalExecHttp(req, res, path0, body, log)) return;
1545
+ if (await handleHijackedPodHttp(req, res, full, body, log)) return;
504
1546
  }
505
1547
  // HIJACK EnsureSandBox FIRST — before passthrough, or passthrough eats it.
506
1548
  // MEASURED: with OPENZOO_PASSTHRU=1 set, EnsureSandBox went `-> REAL
@@ -508,8 +1550,44 @@ export function startCursorBackend({ port = 8443, models, log = () => {} } = {})
508
1550
  // never our box. This method (and ONLY this one) must be answered locally
509
1551
  // with OUR box so Grok Bot's UI wires to our sandbox; everything else
510
1552
  // still passes through so the app loads normally.
511
- if (/GrokBotService\/EnsureSandBox/.test(full) && process.env.OZ_HIJACK_POD) {
512
- respond(req, res, method, models); // encodeForMethod('EnsureSandBox') reads OZ_HIJACK_POD
1553
+ if (/GrokBotService\/(EnsureSandBox|WatchSandBoxMigration)/.test(full) && process.env.OZ_HIJACK_POD) {
1554
+ if (/WatchSandBoxMigration/.test(full) && realPod) {
1555
+ const payload = rewrittenBox();
1556
+ res.writeHead(200, {
1557
+ 'content-type': 'application/connect+proto',
1558
+ 'grpc-status': '0',
1559
+ ...CORS,
1560
+ });
1561
+ const end = Buffer.from('{}');
1562
+ const h = Buffer.alloc(5); h.writeUInt8(0x02, 0); h.writeUInt32BE(end.length, 1);
1563
+ res.end(Buffer.concat([envelope(payload), h, end]));
1564
+ log('cursor-backend: -> WatchSandBoxMigration ready (hijack, real roster)');
1565
+ return;
1566
+ }
1567
+ try {
1568
+ process.env.OZ_SNIFF_SELF = process.env.OZ_SNIFF_SELF || 'https://127.0.0.1:8443';
1569
+ await sniffEnsureSandBox(req, res, body, host, full, log);
1570
+ log('cursor-backend: -> HIJACKED EnsureSandBox -> our box (roster from real 1340)');
1571
+ return;
1572
+ } catch (e) {
1573
+ log(`cursor-backend: EnsureSandBox discover failed (${e.message}) — env box`);
1574
+ }
1575
+ let pod;
1576
+ try { pod = JSON.parse(process.env.OZ_HIJACK_POD); } catch { pod = null; }
1577
+ if (pod && /WatchSandBoxMigration/.test(full)) {
1578
+ const payload = encodeEnsureSandBox(pod);
1579
+ res.writeHead(200, {
1580
+ 'content-type': 'application/connect+proto',
1581
+ 'grpc-status': '0',
1582
+ ...CORS,
1583
+ });
1584
+ const end = Buffer.from('{}');
1585
+ const h = Buffer.alloc(5); h.writeUInt8(0x02, 0); h.writeUInt32BE(end.length, 1);
1586
+ res.end(Buffer.concat([envelope(payload), h, end]));
1587
+ log('cursor-backend: -> WatchSandBoxMigration ready (hijack)');
1588
+ return;
1589
+ }
1590
+ respond(req, res, 'EnsureSandBox', models);
513
1591
  log(`cursor-backend: -> HIJACKED EnsureSandBox -> our box`);
514
1592
  return;
515
1593
  }
@@ -519,6 +1597,50 @@ export function startCursorBackend({ port = 8443, models, log = () => {} } = {})
519
1597
  await handleStreamChat(req, res, body, log);
520
1598
  return;
521
1599
  }
1600
+ // Telegram/"Failed to send": GetGrokBotSendStatus empty proto is
1601
+ // UNSPECIFIED. ACCEPTED=2. WatchGrokBotUserComputerRequests is a
1602
+ // long-lived stream — empty-ok closes it and the channel looks offline.
1603
+ if (/GetGrokBotSendStatus/.test(full)) {
1604
+ if (sniffOn()) {
1605
+ const fields = decodeProtoFields(body);
1606
+ sniffDump({ kind: 'GetGrokBotSendStatus.req', fields });
1607
+ const upstream = /cursor\.sh$/.test(host) ? host : 'api2.cursor.sh';
1608
+ await passthroughToRealAnthropic(req, res, body, upstream, full, log);
1609
+ return;
1610
+ }
1611
+ const fields = decodeProtoFields(body);
1612
+ const echoId = String(fields[2] || lastSendEchoId);
1613
+ lastSendEchoId = echoId;
1614
+ const payload = encodeGetGrokBotSendStatus(echoId);
1615
+ const reqCt = String(req.headers['content-type'] || '');
1616
+ if (reqCt.includes('grpc-web')) {
1617
+ res.writeHead(200, {
1618
+ 'content-type': reqCt.includes('text') ? 'application/grpc-web-text+proto' : 'application/grpc-web+proto',
1619
+ 'grpc-status': '0', ...CORS,
1620
+ });
1621
+ res.end(Buffer.concat([envelope(payload), grpcWebTrailer()]));
1622
+ } else {
1623
+ res.writeHead(200, { 'content-type': 'application/proto', ...CORS });
1624
+ res.end(payload);
1625
+ }
1626
+ log(`cursor-backend: -> GetGrokBotSendStatus ACCEPTED echo=${echoId}`);
1627
+ return;
1628
+ }
1629
+ if (/WatchGrokBotUserComputerRequests/.test(full)) {
1630
+ if (sniffOn()) {
1631
+ const upstream = /cursor\.sh$/.test(host) ? host : 'api2.cursor.sh';
1632
+ await passthroughPipe(req, res, body, upstream, full, log);
1633
+ return;
1634
+ }
1635
+ res.writeHead(200, {
1636
+ 'content-type': 'application/connect+proto',
1637
+ 'grpc-status': '0',
1638
+ ...CORS,
1639
+ });
1640
+ req.on('close', () => { try { res.end(); } catch { /* */ } });
1641
+ log('cursor-backend: -> WatchGrokBotUserComputerRequests held');
1642
+ return;
1643
+ }
522
1644
  // FULL PASSTHROUGH MODE — observe, do not stub.
523
1645
  //
524
1646
  // Stubbing unknown methods with empty protobufs BREAKS Grok Bot: it never