openzoo 0.39.2 → 0.39.4
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/lib/cursorbackend.js +11 -0
- package/lib/podagent.mjs +73 -34
- package/package.json +1 -1
package/lib/cursorbackend.js
CHANGED
|
@@ -469,6 +469,17 @@ export function startCursorBackend({ port = 8443, models, log = () => {} } = {})
|
|
|
469
469
|
return;
|
|
470
470
|
}
|
|
471
471
|
log(`cursor-backend: #${conns} ${req.method} ${full} ct=${ct} body=${body.length}b`);
|
|
472
|
+
// HIJACK EnsureSandBox FIRST — before passthrough, or passthrough eats it.
|
|
473
|
+
// MEASURED: with OPENZOO_PASSTHRU=1 set, EnsureSandBox went `-> REAL
|
|
474
|
+
// anthropic (200, 707b)` every time and the app got a real cursorvm pod,
|
|
475
|
+
// never our box. This method (and ONLY this one) must be answered locally
|
|
476
|
+
// with OUR box so Grok Bot's UI wires to our sandbox; everything else
|
|
477
|
+
// still passes through so the app loads normally.
|
|
478
|
+
if (/GrokBotService\/EnsureSandBox/.test(full) && process.env.OZ_HIJACK_POD) {
|
|
479
|
+
respond(req, res, method, models); // encodeForMethod('EnsureSandBox') reads OZ_HIJACK_POD
|
|
480
|
+
log(`cursor-backend: -> HIJACKED EnsureSandBox -> our box`);
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
472
483
|
// CAPTURE the chat inference request so its schema can be decoded from
|
|
473
484
|
// REAL bytes (Auto's StreamUnifiedChat). Written once; inspect then build.
|
|
474
485
|
if (/StreamUnifiedChat/.test(full)) {
|
package/lib/podagent.mjs
CHANGED
|
@@ -1,14 +1,23 @@
|
|
|
1
|
-
// Pod
|
|
2
|
-
//
|
|
1
|
+
// Pod agent — runs INSIDE our box, on the ports Grok Bot expects a Cursor
|
|
2
|
+
// sandbox to answer (1337 agent, 6080 vnc, 1340 local-exec, 6081).
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
4
|
+
// THE PROTOCOL, DECODED FROM A LIVE CAPTURE (2026-08-16). Grok Bot's sandbox
|
|
5
|
+
// speaks plain HTTP JSON, not a proprietary binary:
|
|
6
|
+
// GET /local-exec/requests <- the local-exec-daemon opens an SSE stream to
|
|
7
|
+
// RECEIVE exec requests from the pod.
|
|
8
|
+
// POST /local-exec/responses <- the daemon SENDS frames back:
|
|
9
|
+
// {"frames":[{"kind":"hello","localRoot":"/Users/…","variant":"sand",…}]}
|
|
10
|
+
// {"frames":[{"kind":"ping","supervised":true}]}
|
|
11
|
+
// auth: Bearer <network_token> / x-anyrun-network-token: nto-…
|
|
12
|
+
//
|
|
13
|
+
// So the box is the ORCHESTRATOR: it holds the SSE open and pushes exec frames
|
|
14
|
+
// down it; the daemon runs them and posts results to /local-exec/responses.
|
|
15
|
+
// This file (a) answers the handshake so the daemon stays connected, (b) can
|
|
16
|
+
// PUSH a probe command down the SSE to capture what an exec frame + its output
|
|
17
|
+
// frame look like (OZ_PROBE_CMD), and (c) logs every frame to agent.jsonl.
|
|
18
|
+
//
|
|
19
|
+
// STILL A CAPTURE until we've seen one real exec/output frame — then the
|
|
20
|
+
// openzoo agent brain can drive this for real. Honest about which.
|
|
12
21
|
|
|
13
22
|
import http from 'node:http';
|
|
14
23
|
import { appendFileSync } from 'node:fs';
|
|
@@ -16,53 +25,83 @@ import { appendFileSync } from 'node:fs';
|
|
|
16
25
|
const PORTS = (process.env.OZ_AGENT_PORTS || '1337,6080,1340,6081')
|
|
17
26
|
.split(',').map((s) => Number(s.trim())).filter(Boolean);
|
|
18
27
|
const LOG = process.env.OZ_AGENT_LOG || '/var/log/openzoo/agent.jsonl';
|
|
28
|
+
// A shell command to push down the SSE the first time a daemon connects, to
|
|
29
|
+
// capture the exec-request AND its result frame. Default probes harmlessly.
|
|
30
|
+
const PROBE = process.env.OZ_PROBE_CMD || 'echo openzoo-probe && uname -a';
|
|
19
31
|
|
|
20
32
|
function record(entry) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
console.log(`[agent:${entry.port}] ${entry.method} ${entry.path} ${entry.bodyBytes}b`
|
|
24
|
-
|
|
33
|
+
try { appendFileSync(LOG, JSON.stringify(entry) + '\n'); } catch { /* best effort */ }
|
|
34
|
+
const tag = entry.frames ? ` frames=${entry.frames}` : '';
|
|
35
|
+
console.log(`[agent:${entry.port}] ${entry.method} ${entry.path} ${entry.bodyBytes}b${tag}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// SSE clients currently holding /local-exec/requests open, so we can push.
|
|
39
|
+
const execStreams = new Set();
|
|
40
|
+
let probedOnce = false;
|
|
41
|
+
|
|
42
|
+
/** Push one exec frame down every open request stream. Frame shape is our best
|
|
43
|
+
* guess from the response frames; the daemon's reply (accept or error) TEACHES
|
|
44
|
+
* us the real schema, which is the whole point. */
|
|
45
|
+
function pushExec(cmd) {
|
|
46
|
+
const id = `oz-${Date.now().toString(36)}`;
|
|
47
|
+
// Try the most likely shapes the daemon will accept; it will 400/echo the
|
|
48
|
+
// one it understands, which the capture records.
|
|
49
|
+
const frame = { frames: [{ kind: 'exec', id, command: cmd, cwd: '/tmp', supervised: true }] };
|
|
50
|
+
const payload = `data: ${JSON.stringify(frame)}\n\n`;
|
|
51
|
+
for (const res of execStreams) { try { res.write(payload); } catch { /* dead stream */ } }
|
|
52
|
+
record({ t: new Date().toISOString(), port: 1340, method: 'SSE-PUSH', path: '/local-exec/requests',
|
|
53
|
+
bodyBytes: payload.length, pushed: frame });
|
|
25
54
|
}
|
|
26
55
|
|
|
27
56
|
for (const port of PORTS) {
|
|
28
57
|
const server = http.createServer((req, res) => {
|
|
58
|
+
// SSE receive-channel: hold it open, let us push exec frames.
|
|
59
|
+
if (req.method === 'GET' && req.url && req.url.startsWith('/local-exec/requests')) {
|
|
60
|
+
record({ t: new Date().toISOString(), port, method: 'GET', path: req.url,
|
|
61
|
+
headers: req.headers, bodyBytes: 0, note: 'SSE stream opened' });
|
|
62
|
+
res.writeHead(200, {
|
|
63
|
+
'content-type': 'text/event-stream',
|
|
64
|
+
'cache-control': 'no-cache',
|
|
65
|
+
connection: 'keep-alive',
|
|
66
|
+
});
|
|
67
|
+
res.write(':ok\n\n');
|
|
68
|
+
execStreams.add(res);
|
|
69
|
+
req.on('close', () => execStreams.delete(res));
|
|
70
|
+
// once a stream is open, push the probe command to capture the exec loop
|
|
71
|
+
if (!probedOnce) { probedOnce = true; setTimeout(() => pushExec(PROBE), 1500); }
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
29
75
|
const chunks = [];
|
|
30
76
|
req.on('data', (d) => chunks.push(d));
|
|
31
77
|
req.on('end', () => {
|
|
32
78
|
const body = Buffer.concat(chunks);
|
|
79
|
+
let frames;
|
|
80
|
+
try { frames = JSON.parse(body.toString('utf8')).frames?.map((f) => f.kind).join(','); } catch { /* not json */ }
|
|
33
81
|
record({
|
|
34
82
|
t: new Date().toISOString(), port,
|
|
35
|
-
method: req.method, path: req.url,
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
bodyUtf8: body.slice(0, 512).toString('utf8').replace(/[^\x20-\x7e]/g, '.'),
|
|
83
|
+
method: req.method, path: req.url, headers: req.headers,
|
|
84
|
+
bodyBytes: body.length, frames,
|
|
85
|
+
bodyHex: body.slice(0, 256).toString('hex'),
|
|
86
|
+
bodyUtf8: body.slice(0, 1024).toString('utf8').replace(/[^\x20-\x7e]/g, '.'),
|
|
40
87
|
});
|
|
41
|
-
// Benign answers so the client does not give up immediately.
|
|
42
88
|
if (req.url && req.url.includes('/vnc')) {
|
|
43
89
|
res.writeHead(200, { 'content-type': 'text/html' });
|
|
44
90
|
res.end('<!doctype html><title>openzoo box</title><body>capture</body>');
|
|
45
91
|
} else {
|
|
92
|
+
// /local-exec/responses and everything else: accept it so the daemon
|
|
93
|
+
// keeps its session and keeps sending frames.
|
|
46
94
|
res.writeHead(200, { 'content-type': 'application/json' });
|
|
47
|
-
res.end('{}');
|
|
95
|
+
res.end('{"ok":true}');
|
|
48
96
|
}
|
|
49
97
|
});
|
|
50
98
|
});
|
|
51
|
-
// Capture websocket upgrades too — VNC/websockify is a WS, and the agent
|
|
52
|
-
// channel may be one; log the handshake even though we don't speak it yet.
|
|
53
99
|
server.on('upgrade', (req, socket) => {
|
|
54
|
-
record({
|
|
55
|
-
|
|
56
|
-
method: req.method, path: req.url, upgrade: req.headers.upgrade,
|
|
57
|
-
headers: req.headers, bodyBytes: 0,
|
|
58
|
-
});
|
|
59
|
-
// 101 with no real protocol; the client will close, which is fine — we
|
|
60
|
-
// only needed the handshake headers.
|
|
100
|
+
record({ t: new Date().toISOString(), port, method: 'UPGRADE', path: req.url,
|
|
101
|
+
upgrade: req.headers.upgrade, headers: req.headers, bodyBytes: 0 });
|
|
61
102
|
socket.write('HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n\r\n');
|
|
62
|
-
socket.on('data', (d) => record({
|
|
63
|
-
|
|
64
|
-
bodyBytes: d.length, bodyHex: d.slice(0, 256).toString('hex'),
|
|
65
|
-
}));
|
|
103
|
+
socket.on('data', (d) => record({ t: new Date().toISOString(), port, method: 'WS-DATA', path: req.url,
|
|
104
|
+
bodyBytes: d.length, bodyHex: d.slice(0, 256).toString('hex') }));
|
|
66
105
|
});
|
|
67
106
|
server.listen(port, '0.0.0.0', () => console.log(`[agent] capturing on :${port}`));
|
|
68
107
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openzoo",
|
|
3
|
-
"version": "0.39.
|
|
3
|
+
"version": "0.39.4",
|
|
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",
|