openzoo 0.41.1 → 0.43.0
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 +35 -18
- package/lib/grokbot.js +22 -12
- package/lib/podagent.mjs +177 -15
- package/package.json +1 -1
package/lib/cursorbackend.js
CHANGED
|
@@ -175,11 +175,14 @@ function encodeChatResponse(text) {
|
|
|
175
175
|
}
|
|
176
176
|
|
|
177
177
|
/**
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
178
|
+
* THE TRIGGER. Cursor's chat inference is ChatService/StreamUnifiedChat to the
|
|
179
|
+
* agent host (now impersonated). Whatever the user typed into Grok Bot's UI is
|
|
180
|
+
* in this request — pull it out and hand it to OUR box (OZ_BOX_AGENT, set by
|
|
181
|
+
* grokbot.js to the box's :1337 podagent). The box's own openzoo brain drives
|
|
182
|
+
* the local-exec loop against the daemon running on this Mac (approval-gated),
|
|
183
|
+
* and its DONE: answer is what streams back into the UI. If no box is wired
|
|
184
|
+
* (OZ_BOX_AGENT unset — e.g. running the backend standalone), fall back to a
|
|
185
|
+
* plain one-shot zoo call so the app still gets a reply.
|
|
183
186
|
*/
|
|
184
187
|
async function handleStreamChat(req, res, body, log) {
|
|
185
188
|
const ct = String(req.headers['content-type'] || '');
|
|
@@ -187,20 +190,34 @@ async function handleStreamChat(req, res, body, log) {
|
|
|
187
190
|
const prompt = extractPromptText(body) || 'hello';
|
|
188
191
|
log(`cursor-backend: >> StreamUnifiedChat prompt: ${JSON.stringify(prompt.slice(0, 80))}`);
|
|
189
192
|
let text = '';
|
|
193
|
+
const boxAgent = process.env.OZ_BOX_AGENT;
|
|
190
194
|
try {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
})
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
195
|
+
if (boxAgent) {
|
|
196
|
+
const drive = await fetch(`${boxAgent}/drive`, {
|
|
197
|
+
method: 'POST',
|
|
198
|
+
headers: { 'content-type': 'application/json' },
|
|
199
|
+
body: JSON.stringify({ task: prompt }),
|
|
200
|
+
// worst case: MAX_STEPS(10) x 60s exec waits + brain latency headroom
|
|
201
|
+
signal: AbortSignal.timeout(11 * 60_000),
|
|
202
|
+
});
|
|
203
|
+
const data = await drive.json();
|
|
204
|
+
text = data.text || '(box returned nothing)';
|
|
205
|
+
log(`cursor-backend: << box drove task -> ${text.length} chars`);
|
|
206
|
+
} else {
|
|
207
|
+
const zoo = await fetch('http://127.0.0.1:8402/v1/chat/completions', {
|
|
208
|
+
method: 'POST',
|
|
209
|
+
headers: { 'content-type': 'application/json', authorization: 'Bearer sk-openzoo' },
|
|
210
|
+
body: JSON.stringify({
|
|
211
|
+
model: process.env.OPENZOO_DEFAULT_MODEL || 'anthropic/claude-opus-5',
|
|
212
|
+
messages: [{ role: 'user', content: prompt }],
|
|
213
|
+
max_tokens: Number(process.env.OPENZOO_ASK_MAX_TOKENS || 2048),
|
|
214
|
+
}),
|
|
215
|
+
});
|
|
216
|
+
const data = await zoo.json();
|
|
217
|
+
text = data.choices?.[0]?.message?.content || '(no content)';
|
|
218
|
+
log(`cursor-backend: << zoo replied ${text.length} chars (paid x402, no box wired)`);
|
|
219
|
+
}
|
|
220
|
+
} catch (e) { text = `openzoo error: ${e.message}`; log(`cursor-backend: drive call failed: ${e.message}`); }
|
|
204
221
|
|
|
205
222
|
res.writeHead(200, {
|
|
206
223
|
'content-type': isGrpcWeb ? 'application/grpc-web+proto' : 'application/connect+proto',
|
package/lib/grokbot.js
CHANGED
|
@@ -20,10 +20,10 @@ import { existsSync } from 'node:fs';
|
|
|
20
20
|
import { homedir } from 'node:os';
|
|
21
21
|
import { join } from 'node:path';
|
|
22
22
|
|
|
23
|
-
import { config } from './config.js';
|
|
23
|
+
import { config, USDC_MINT, TOKEN_MINT } from './config.js';
|
|
24
24
|
import { PayClient } from './pay.js';
|
|
25
25
|
import {
|
|
26
|
-
podEndpoints, readyBox, runpodConfigured, spawnBox, listBoxes,
|
|
26
|
+
podEndpoints, readyBox, runpodConfigured, spawnBox, listBoxes, execInBox,
|
|
27
27
|
} from './boxes.js';
|
|
28
28
|
|
|
29
29
|
const APP = '/Applications/Grok Bot.app';
|
|
@@ -46,19 +46,15 @@ export async function runGrokBot(argv = []) {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
const days = Number((argv.find((a) => /^--days=/.test(a)) || '').split('=')[1]) || 1;
|
|
49
|
-
const shareWallet = argv.includes('--share-wallet');
|
|
50
|
-
const client = new PayClient();
|
|
51
49
|
|
|
52
50
|
// 1) a box, ready, BEFORE we touch the app — so the first EnsureSandBox we
|
|
53
|
-
// hijack already has a live pod to point at.
|
|
51
|
+
// hijack already has a live pod to point at. The box mints its OWN wallet
|
|
52
|
+
// (never the operator's secret key — that never leaves this Mac). There is
|
|
53
|
+
// NO --task and NO --share-wallet: the task is whatever the user types
|
|
54
|
+
// into the Grok Bot UI, captured from the MITM'd chat request and driven
|
|
55
|
+
// through to the box over POST /drive.
|
|
54
56
|
console.error('openzoo grokbot: spawning your box (CPU, x402-funded)…');
|
|
55
|
-
const
|
|
56
|
-
|| (argv.includes('--task') ? argv[argv.indexOf('--task') + 1] : '');
|
|
57
|
-
const spawned = await spawnBox({
|
|
58
|
-
name: 'grokbot', days,
|
|
59
|
-
...(shareWallet ? { walletJson: JSON.stringify([...client.keypair.secretKey]) } : {}),
|
|
60
|
-
...(taskArg ? { task: taskArg } : {}),
|
|
61
|
-
});
|
|
57
|
+
const spawned = await spawnBox({ name: 'grokbot', days });
|
|
62
58
|
if (!spawned.ok) { console.error(` spawn failed: ${spawned.error}`); process.exit(1); }
|
|
63
59
|
const box = spawned.box;
|
|
64
60
|
console.error(` ${box.name} ${box.id} expires ${spawned.expiresAt.slice(0, 16)}`);
|
|
@@ -66,6 +62,18 @@ export async function runGrokBot(argv = []) {
|
|
|
66
62
|
if (!ready.ok) console.error(` ${ready.error} — continuing; the capture agent may still bind`);
|
|
67
63
|
else console.error(` box ready in ${ready.seconds}s`);
|
|
68
64
|
|
|
65
|
+
// the box minted its OWN wallet on boot — print its funding address so it
|
|
66
|
+
// can be topped up directly. Its brain pays per call from THIS wallet, not
|
|
67
|
+
// the operator's; a fresh box starts at $0 until funded.
|
|
68
|
+
if (box.ssh) {
|
|
69
|
+
const addr = await execInBox(box, 'npx --yes openzoo address 2>/dev/null');
|
|
70
|
+
if (addr.ok && addr.stdout.trim()) {
|
|
71
|
+
console.error(' box wallet (top up to give it inference budget):');
|
|
72
|
+
console.error(addr.stdout.trim().split('\n').map((l) => ` ${l}`).join('\n'));
|
|
73
|
+
}
|
|
74
|
+
console.error(` mints: USDC ${USDC_MINT} · TOKEN ${TOKEN_MINT} (Solana)`);
|
|
75
|
+
}
|
|
76
|
+
|
|
69
77
|
const ep = podEndpoints(box.id);
|
|
70
78
|
process.env.OZ_HIJACK_POD = JSON.stringify({
|
|
71
79
|
region: 'us1',
|
|
@@ -74,6 +82,8 @@ export async function runGrokBot(argv = []) {
|
|
|
74
82
|
token: `nto-${Math.random().toString(36).slice(2, 12)}`,
|
|
75
83
|
agent: ep.agent, vnc: ep.vnc, p1340: ep.p1340, p6081: ep.p6081,
|
|
76
84
|
});
|
|
85
|
+
// the backend (handleStreamChat) forwards each UI prompt here to be driven
|
|
86
|
+
process.env.OZ_BOX_AGENT = ep.agent;
|
|
77
87
|
// other methods must still reach the REAL backend, or the app can't load
|
|
78
88
|
process.env.OPENZOO_PASSTHRU = '1';
|
|
79
89
|
process.env.OPENZOO_FORCE_BLOCK = '1';
|
package/lib/podagent.mjs
CHANGED
|
@@ -30,6 +30,109 @@ const PROXY = process.env.OZ_PROXY || 'http://127.0.0.1:8402/v1';
|
|
|
30
30
|
const MODEL = process.env.OZ_BRAIN_MODEL || 'x-ai/grok-4.6';
|
|
31
31
|
const MAX_STEPS = Number(process.env.OZ_MAX_STEPS || 10);
|
|
32
32
|
|
|
33
|
+
// Matches the Grok Bot chat surface itself (dark canvas, right-aligned grey
|
|
34
|
+
// user pills, pink-avatar left-aligned replies, pill input bar with + / mic) —
|
|
35
|
+
// this renders INSIDE Grok Bot's own sandbox panel (its sidebar/titlebar are
|
|
36
|
+
// the app's own chrome, not ours), so only the message canvas needs to match.
|
|
37
|
+
const VNC_CHAT_HTML = `<!doctype html>
|
|
38
|
+
<html><head><meta charset="utf-8"><title>openzoo box</title>
|
|
39
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
40
|
+
<style>
|
|
41
|
+
:root { color-scheme: dark; }
|
|
42
|
+
* { box-sizing: border-box; }
|
|
43
|
+
html, body { margin: 0; height: 100%; background: #000; }
|
|
44
|
+
body { color: #ececec; font: 15px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
|
45
|
+
display: flex; flex-direction: column; }
|
|
46
|
+
#log { flex: 1; overflow-y: auto; padding: 28px 24px 12px; display: flex; flex-direction: column; gap: 18px; }
|
|
47
|
+
.row { display: flex; align-items: flex-start; gap: 10px; max-width: 78%; }
|
|
48
|
+
.row.user { align-self: flex-end; flex-direction: row-reverse; }
|
|
49
|
+
.row.bot { align-self: flex-start; }
|
|
50
|
+
.avatar { width: 30px; height: 30px; border-radius: 8px; flex: 0 0 30px; background: #e91e8c;
|
|
51
|
+
display: flex; align-items: center; justify-content: center; }
|
|
52
|
+
.avatar svg { width: 16px; height: 16px; }
|
|
53
|
+
.bubble { padding: 11px 15px; border-radius: 18px; white-space: pre-wrap; word-break: break-word; }
|
|
54
|
+
.row.user .bubble { background: #3a3a3c; border-bottom-right-radius: 5px; }
|
|
55
|
+
.row.bot .bubble { background: transparent; padding: 6px 0; color: #ececec; }
|
|
56
|
+
.row.bot.pending .bubble { color: #8e8e93; }
|
|
57
|
+
.dots span { display: inline-block; width: 5px; height: 5px; margin-right: 3px; border-radius: 50%;
|
|
58
|
+
background: #8e8e93; animation: blink 1.2s infinite ease-in-out; }
|
|
59
|
+
.dots span:nth-child(2) { animation-delay: .2s; } .dots span:nth-child(3) { animation-delay: .4s; }
|
|
60
|
+
@keyframes blink { 0%, 80%, 100% { opacity: .25; } 40% { opacity: 1; } }
|
|
61
|
+
#bar { padding: 10px 16px 18px; }
|
|
62
|
+
#pill { display: flex; align-items: center; gap: 10px; background: #2c2c2e; border-radius: 26px;
|
|
63
|
+
padding: 8px 10px 8px 14px; }
|
|
64
|
+
.icon-btn { width: 32px; height: 32px; border-radius: 50%; border: none; background: transparent;
|
|
65
|
+
color: #ececec; display: flex; align-items: center; justify-content: center; cursor: pointer;
|
|
66
|
+
flex: 0 0 32px; }
|
|
67
|
+
.icon-btn:hover { background: #3a3a3c; }
|
|
68
|
+
.icon-btn svg { width: 18px; height: 18px; }
|
|
69
|
+
#inp { flex: 1; background: transparent; border: none; color: #ececec; font: inherit;
|
|
70
|
+
padding: 6px 0; min-width: 0; }
|
|
71
|
+
#inp::placeholder { color: #8e8e93; }
|
|
72
|
+
#inp:focus { outline: none; }
|
|
73
|
+
</style></head>
|
|
74
|
+
<body>
|
|
75
|
+
<div id="log"></div>
|
|
76
|
+
<div id="bar">
|
|
77
|
+
<div id="pill">
|
|
78
|
+
<button class="icon-btn" tabindex="-1">
|
|
79
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
|
80
|
+
</button>
|
|
81
|
+
<input id="inp" placeholder="Message pods" autofocus>
|
|
82
|
+
<button class="icon-btn" id="send">
|
|
83
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19V5"/><path d="M6 11l6-6 6 6"/></svg>
|
|
84
|
+
</button>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
<script>
|
|
88
|
+
const log = document.getElementById('log');
|
|
89
|
+
const inp = document.getElementById('inp');
|
|
90
|
+
const send = document.getElementById('send');
|
|
91
|
+
const AVATAR = '<svg viewBox="0 0 24 24" fill="none"><path d="M6 10c0-1 1-2 2-1l2 2 2-2c1-1 2 0 2 1v2c0 2-2 3-4 3s-4-1-4-3v-2z" fill="#fff"/></svg>';
|
|
92
|
+
function addRow(who, text) {
|
|
93
|
+
const row = document.createElement('div');
|
|
94
|
+
row.className = 'row ' + who + (text === null ? ' pending' : '');
|
|
95
|
+
if (who === 'bot') {
|
|
96
|
+
const av = document.createElement('div');
|
|
97
|
+
av.className = 'avatar';
|
|
98
|
+
av.innerHTML = AVATAR;
|
|
99
|
+
row.appendChild(av);
|
|
100
|
+
}
|
|
101
|
+
const bubble = document.createElement('div');
|
|
102
|
+
bubble.className = 'bubble';
|
|
103
|
+
bubble.innerHTML = text === null ? '<span class="dots"><span></span><span></span><span></span></span>' : '';
|
|
104
|
+
if (text !== null) bubble.textContent = text;
|
|
105
|
+
row.appendChild(bubble);
|
|
106
|
+
log.appendChild(row);
|
|
107
|
+
log.scrollTop = log.scrollHeight;
|
|
108
|
+
return { row, bubble };
|
|
109
|
+
}
|
|
110
|
+
async function submit() {
|
|
111
|
+
const task = inp.value.trim();
|
|
112
|
+
if (!task) return;
|
|
113
|
+
inp.value = '';
|
|
114
|
+
inp.disabled = true; send.disabled = true;
|
|
115
|
+
addRow('user', task);
|
|
116
|
+
const pending = addRow('bot', null);
|
|
117
|
+
try {
|
|
118
|
+
const r = await fetch('/drive', {
|
|
119
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
120
|
+
body: JSON.stringify({ task }),
|
|
121
|
+
});
|
|
122
|
+
const j = await r.json();
|
|
123
|
+
pending.row.classList.remove('pending');
|
|
124
|
+
pending.bubble.textContent = j.text || '(no response)';
|
|
125
|
+
} catch (e) {
|
|
126
|
+
pending.row.classList.remove('pending');
|
|
127
|
+
pending.bubble.textContent = 'error: ' + e.message;
|
|
128
|
+
}
|
|
129
|
+
inp.disabled = false; send.disabled = false; inp.focus();
|
|
130
|
+
}
|
|
131
|
+
send.addEventListener('click', submit);
|
|
132
|
+
inp.addEventListener('keydown', (e) => { if (e.key === 'Enter') submit(); });
|
|
133
|
+
</script>
|
|
134
|
+
</body></html>`;
|
|
135
|
+
|
|
33
136
|
function record(entry) {
|
|
34
137
|
try { appendFileSync(LOG, JSON.stringify(entry) + '\n'); } catch { /* best effort */ }
|
|
35
138
|
console.log(`[agent:${entry.port ?? '-'}] ${entry.method || entry.ev} ${entry.path || ''} ${entry.bodyBytes ?? ''}${entry.frames ? ' frames=' + entry.frames : ''}`);
|
|
@@ -70,25 +173,42 @@ async function brain(messages) {
|
|
|
70
173
|
return j?.choices?.[0]?.message?.content ?? '';
|
|
71
174
|
}
|
|
72
175
|
|
|
73
|
-
const SYSTEM = `You
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
176
|
+
const SYSTEM = `You are the brain of a Grok-Bot-style coding/ops agent. The polished chat UI
|
|
177
|
+
the user sees is Grok Bot (Anysphere's app); its "sandbox" has been pointed at THIS box, and
|
|
178
|
+
your reasoning is served by openzoo (pay-per-call access to ~435 models over x402 — no API key,
|
|
179
|
+
no account, each call paid from a burner wallet). You are the substitute agent server.
|
|
180
|
+
|
|
181
|
+
WHERE YOUR COMMANDS RUN: each shell command you emit is pushed to a local-exec daemon running on
|
|
182
|
+
the USER'S OWN Mac (a supervised "sand" sandbox rooted at their home dir). Every command is
|
|
183
|
+
APPROVAL-GATED — the user sees and approves it before it runs. So: act on the user's real machine,
|
|
184
|
+
be careful, never destructive, prefer read-before-write, and explain nothing to the shell.
|
|
185
|
+
|
|
186
|
+
PROTOCOL — reply with EXACTLY one line, no prose, no code fences:
|
|
187
|
+
RUN: <a single shell command> to execute a step
|
|
188
|
+
DONE: <a short natural-language answer> when the task is complete (this text is shown in the UI)
|
|
189
|
+
You are given each command's output before your next line. If the task needs no shell (a question,
|
|
190
|
+
an explanation), answer it directly with a single DONE: line. Keep DONE summaries human and useful —
|
|
191
|
+
they are the assistant's reply to the user, not a log.`;
|
|
77
192
|
|
|
78
193
|
/** The agent loop for one task, executed through the connected daemon. Each
|
|
79
194
|
* RUN is pushed as an exec frame; the daemon's result frames (captured in
|
|
80
195
|
* `pendingResults`) feed the next turn. */
|
|
81
196
|
async function runTask(task, stream, ctx) {
|
|
82
197
|
const messages = [{ role: 'system', content: SYSTEM }, { role: 'user', content: task }];
|
|
198
|
+
let answer = '';
|
|
83
199
|
for (let step = 0; step < MAX_STEPS; step++) {
|
|
84
200
|
const line = (await brain(messages)).trim();
|
|
85
201
|
record({ ev: 'brain', step, line });
|
|
202
|
+
const done = /^DONE:\s*([\s\S]+)/.exec(line);
|
|
203
|
+
if (done) { answer = done[1].trim(); record({ ev: 'task-done', step, answer }); return answer; }
|
|
86
204
|
const run = /^RUN:\s*([\s\S]+)/.exec(line);
|
|
87
|
-
|
|
205
|
+
// no RUN and no DONE — treat the whole line as a direct answer to the user
|
|
206
|
+
if (!run) { answer = line.replace(/^DONE:\s*/i, ''); record({ ev: 'task-freeform', step, answer }); return answer; }
|
|
88
207
|
const cmd = run[1].trim();
|
|
89
208
|
const frame = execFrame(cmd);
|
|
90
209
|
ctx.awaiting = frame.requestId;
|
|
91
210
|
ctx.output = '';
|
|
211
|
+
ctx.done = false;
|
|
92
212
|
sseSend(stream, frame);
|
|
93
213
|
record({ ev: 'exec-push', step, requestId: frame.requestId, cmd });
|
|
94
214
|
// wait for the daemon's result frames for this requestId (or timeout)
|
|
@@ -96,6 +216,7 @@ async function runTask(task, stream, ctx) {
|
|
|
96
216
|
messages.push({ role: 'assistant', content: line });
|
|
97
217
|
messages.push({ role: 'user', content: `output:\n${out || '(none)'}` });
|
|
98
218
|
}
|
|
219
|
+
return answer || '(reached step limit without finishing)';
|
|
99
220
|
}
|
|
100
221
|
|
|
101
222
|
function waitResult(ctx, timeoutMs) {
|
|
@@ -113,36 +234,70 @@ function waitResult(ctx, timeoutMs) {
|
|
|
113
234
|
|
|
114
235
|
// per-connection context so a result frame can be matched to its exec
|
|
115
236
|
const ctxByToken = new Map();
|
|
237
|
+
// the single live daemon (Grok Bot's local-exec on the user's Mac). Set when it
|
|
238
|
+
// opens its SSE; the /drive endpoint pushes exec frames down THIS stream.
|
|
239
|
+
let activeDaemon = null;
|
|
240
|
+
// one exec loop at a time against the shared daemon ctx — two concurrent
|
|
241
|
+
// runTask calls would interleave result frames onto the same ctx.output.
|
|
242
|
+
let driveQueue = Promise.resolve();
|
|
243
|
+
function queueDrive(fn) {
|
|
244
|
+
const next = driveQueue.then(fn, fn);
|
|
245
|
+
driveQueue = next.catch(() => {});
|
|
246
|
+
return next;
|
|
247
|
+
}
|
|
116
248
|
|
|
117
249
|
for (const port of PORTS) {
|
|
118
250
|
const server = http.createServer((req, res) => {
|
|
119
|
-
// The daemon's receive-channel: hold the SSE open
|
|
120
|
-
//
|
|
251
|
+
// The daemon's receive-channel: hold the SSE open. The TASK does NOT come
|
|
252
|
+
// from here or from any env var — it arrives via POST /drive, forwarded by
|
|
253
|
+
// the backend from whatever the user typed into the Grok Bot UI.
|
|
121
254
|
if (req.method === 'GET' && req.url && req.url.startsWith('/local-exec/requests')) {
|
|
122
255
|
const token = (req.headers['x-anyrun-network-token'] || 'default');
|
|
123
256
|
const ctx = { awaiting: null, output: '', done: false };
|
|
124
257
|
ctxByToken.set(token, ctx);
|
|
125
|
-
|
|
258
|
+
activeDaemon = { stream: res, ctx };
|
|
259
|
+
record({ port, method: 'GET', path: req.url, note: 'SSE opened — daemon connected', bodyBytes: 0 });
|
|
126
260
|
res.writeHead(200, { 'content-type': 'text/event-stream', 'cache-control': 'no-cache', connection: 'keep-alive' });
|
|
127
261
|
res.write(':ok\n\n');
|
|
128
|
-
// announce ourselves
|
|
262
|
+
// announce ourselves; then wait for the UI to drive us
|
|
129
263
|
sseSend(res, { kind: 'welcome', providerId: 'openzoo' });
|
|
130
|
-
req.on('close', () =>
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}
|
|
264
|
+
req.on('close', () => {
|
|
265
|
+
ctxByToken.delete(token);
|
|
266
|
+
if (activeDaemon && activeDaemon.stream === res) activeDaemon = null;
|
|
267
|
+
});
|
|
134
268
|
return;
|
|
135
269
|
}
|
|
136
270
|
|
|
137
271
|
const chunks = [];
|
|
138
272
|
req.on('data', (d) => chunks.push(d));
|
|
139
|
-
req.on('end', () => {
|
|
273
|
+
req.on('end', async () => {
|
|
140
274
|
const body = Buffer.concat(chunks);
|
|
141
275
|
let parsed, kinds;
|
|
142
276
|
try { parsed = JSON.parse(body.toString('utf8')); kinds = parsed.frames?.map((f) => f.kind).join(','); } catch { /* not json */ }
|
|
143
277
|
record({ port, method: req.method, path: req.url, bodyBytes: body.length, frames: kinds,
|
|
144
278
|
bodyUtf8: body.slice(0, 1024).toString('utf8').replace(/[^\x20-\x7e]/g, '.') });
|
|
145
279
|
|
|
280
|
+
// THE TRIGGER: the backend forwards the user's Grok Bot prompt here. Drive
|
|
281
|
+
// the agent loop against the connected daemon and return the answer text.
|
|
282
|
+
if (req.method === 'POST' && req.url && req.url.startsWith('/drive')) {
|
|
283
|
+
let task = '';
|
|
284
|
+
try { task = (JSON.parse(body.toString('utf8')).task || '').toString(); }
|
|
285
|
+
catch { task = body.toString('utf8'); }
|
|
286
|
+
record({ ev: 'drive', task: task.slice(0, 160) });
|
|
287
|
+
if (!activeDaemon) {
|
|
288
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
289
|
+
res.end(JSON.stringify({ ok: false, text: '(sandbox not connected yet — give the app a moment and retry)' }));
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
let text;
|
|
293
|
+
try {
|
|
294
|
+
text = await queueDrive(() => runTask(task, activeDaemon.stream, activeDaemon.ctx));
|
|
295
|
+
} catch (e) { text = `agent error: ${e.message}`; record({ ev: 'drive-error', err: String(e) }); }
|
|
296
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
297
|
+
res.end(JSON.stringify({ ok: true, text }));
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
146
301
|
// the daemon posts result frames here — accumulate output for the loop
|
|
147
302
|
if (parsed?.frames && req.url?.includes('/local-exec/responses')) {
|
|
148
303
|
const token = (req.headers['x-anyrun-network-token'] || 'default');
|
|
@@ -153,9 +308,16 @@ for (const port of PORTS) {
|
|
|
153
308
|
}
|
|
154
309
|
}
|
|
155
310
|
|
|
311
|
+
// THE REAL TRIGGER. Grok Bot's own chat (StreamUnifiedChat) never reaches
|
|
312
|
+
// us — inference happens server-side inside whatever pod EnsureSandBox
|
|
313
|
+
// named, and since we hijacked that to a pod that doesn't exist on
|
|
314
|
+
// Cursor's side, the UI's chat box just sits silent forever. But the app
|
|
315
|
+
// ALSO opens this vnc.html URL — OUR box — inside its own sandbox panel.
|
|
316
|
+
// Serve a real chat page here instead of a stub: the user types inside
|
|
317
|
+
// Grok Bot's own window, it POSTs straight to /drive on this box.
|
|
156
318
|
if (req.url && req.url.includes('/vnc')) {
|
|
157
319
|
res.writeHead(200, { 'content-type': 'text/html' });
|
|
158
|
-
res.end(
|
|
320
|
+
res.end(VNC_CHAT_HTML);
|
|
159
321
|
} else {
|
|
160
322
|
res.writeHead(200, { 'content-type': 'application/json' });
|
|
161
323
|
res.end('{"ok":true}');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openzoo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.43.0",
|
|
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",
|