openzoo 0.41.0 → 0.42.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/boxes.js +2 -1
- package/lib/cursorbackend.js +35 -18
- package/lib/grokbot.js +22 -9
- package/lib/podagent.mjs +66 -14
- package/package.json +1 -1
package/lib/boxes.js
CHANGED
|
@@ -204,7 +204,7 @@ export function quoteTiers(days = 1) {
|
|
|
204
204
|
}));
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
-
export async function spawnBox({ name = 'bot', days = 1, tier = DEFAULT_TIER, walletJson, pubkey } = {}) {
|
|
207
|
+
export async function spawnBox({ name = 'bot', days = 1, tier = DEFAULT_TIER, walletJson, pubkey, task } = {}) {
|
|
208
208
|
const spec = BOX_TIERS[tier] || BOX_TIERS[DEFAULT_TIER];
|
|
209
209
|
const expiresAt = new Date(Date.now() + ttlHours(days) * 3600 * 1000);
|
|
210
210
|
const unix = Math.floor(expiresAt.getTime() / 1000);
|
|
@@ -234,6 +234,7 @@ export async function spawnBox({ name = 'bot', days = 1, tier = DEFAULT_TIER, wa
|
|
|
234
234
|
...(walletJson ? { OPENZOO_WALLET_JSON: walletJson } : {}),
|
|
235
235
|
...(pubkey ? { OPENZOO_WALLET_PUBKEY: pubkey } : {}),
|
|
236
236
|
OZ_PODAGENT_B64: podAgentB64(),
|
|
237
|
+
...(task ? { OZ_TASK: task } : {}),
|
|
237
238
|
},
|
|
238
239
|
};
|
|
239
240
|
// GPU ONLY ON A GPU TIER. A cpu tier must never carry GPU fields — that would
|
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,16 +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 spawned = await spawnBox({
|
|
56
|
-
name: 'grokbot', days,
|
|
57
|
-
...(shareWallet ? { walletJson: JSON.stringify([...client.keypair.secretKey]) } : {}),
|
|
58
|
-
});
|
|
57
|
+
const spawned = await spawnBox({ name: 'grokbot', days });
|
|
59
58
|
if (!spawned.ok) { console.error(` spawn failed: ${spawned.error}`); process.exit(1); }
|
|
60
59
|
const box = spawned.box;
|
|
61
60
|
console.error(` ${box.name} ${box.id} expires ${spawned.expiresAt.slice(0, 16)}`);
|
|
@@ -63,6 +62,18 @@ export async function runGrokBot(argv = []) {
|
|
|
63
62
|
if (!ready.ok) console.error(` ${ready.error} — continuing; the capture agent may still bind`);
|
|
64
63
|
else console.error(` box ready in ${ready.seconds}s`);
|
|
65
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
|
+
|
|
66
77
|
const ep = podEndpoints(box.id);
|
|
67
78
|
process.env.OZ_HIJACK_POD = JSON.stringify({
|
|
68
79
|
region: 'us1',
|
|
@@ -71,6 +82,8 @@ export async function runGrokBot(argv = []) {
|
|
|
71
82
|
token: `nto-${Math.random().toString(36).slice(2, 12)}`,
|
|
72
83
|
agent: ep.agent, vnc: ep.vnc, p1340: ep.p1340, p6081: ep.p6081,
|
|
73
84
|
});
|
|
85
|
+
// the backend (handleStreamChat) forwards each UI prompt here to be driven
|
|
86
|
+
process.env.OZ_BOX_AGENT = ep.agent;
|
|
74
87
|
// other methods must still reach the REAL backend, or the app can't load
|
|
75
88
|
process.env.OPENZOO_PASSTHRU = '1';
|
|
76
89
|
process.env.OPENZOO_FORCE_BLOCK = '1';
|
package/lib/podagent.mjs
CHANGED
|
@@ -70,25 +70,42 @@ async function brain(messages) {
|
|
|
70
70
|
return j?.choices?.[0]?.message?.content ?? '';
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
const SYSTEM = `You
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
73
|
+
const SYSTEM = `You are the brain of a Grok-Bot-style coding/ops agent. The polished chat UI
|
|
74
|
+
the user sees is Grok Bot (Anysphere's app); its "sandbox" has been pointed at THIS box, and
|
|
75
|
+
your reasoning is served by openzoo (pay-per-call access to ~435 models over x402 — no API key,
|
|
76
|
+
no account, each call paid from a burner wallet). You are the substitute agent server.
|
|
77
|
+
|
|
78
|
+
WHERE YOUR COMMANDS RUN: each shell command you emit is pushed to a local-exec daemon running on
|
|
79
|
+
the USER'S OWN Mac (a supervised "sand" sandbox rooted at their home dir). Every command is
|
|
80
|
+
APPROVAL-GATED — the user sees and approves it before it runs. So: act on the user's real machine,
|
|
81
|
+
be careful, never destructive, prefer read-before-write, and explain nothing to the shell.
|
|
82
|
+
|
|
83
|
+
PROTOCOL — reply with EXACTLY one line, no prose, no code fences:
|
|
84
|
+
RUN: <a single shell command> to execute a step
|
|
85
|
+
DONE: <a short natural-language answer> when the task is complete (this text is shown in the UI)
|
|
86
|
+
You are given each command's output before your next line. If the task needs no shell (a question,
|
|
87
|
+
an explanation), answer it directly with a single DONE: line. Keep DONE summaries human and useful —
|
|
88
|
+
they are the assistant's reply to the user, not a log.`;
|
|
77
89
|
|
|
78
90
|
/** The agent loop for one task, executed through the connected daemon. Each
|
|
79
91
|
* RUN is pushed as an exec frame; the daemon's result frames (captured in
|
|
80
92
|
* `pendingResults`) feed the next turn. */
|
|
81
93
|
async function runTask(task, stream, ctx) {
|
|
82
94
|
const messages = [{ role: 'system', content: SYSTEM }, { role: 'user', content: task }];
|
|
95
|
+
let answer = '';
|
|
83
96
|
for (let step = 0; step < MAX_STEPS; step++) {
|
|
84
97
|
const line = (await brain(messages)).trim();
|
|
85
98
|
record({ ev: 'brain', step, line });
|
|
99
|
+
const done = /^DONE:\s*([\s\S]+)/.exec(line);
|
|
100
|
+
if (done) { answer = done[1].trim(); record({ ev: 'task-done', step, answer }); return answer; }
|
|
86
101
|
const run = /^RUN:\s*([\s\S]+)/.exec(line);
|
|
87
|
-
|
|
102
|
+
// no RUN and no DONE — treat the whole line as a direct answer to the user
|
|
103
|
+
if (!run) { answer = line.replace(/^DONE:\s*/i, ''); record({ ev: 'task-freeform', step, answer }); return answer; }
|
|
88
104
|
const cmd = run[1].trim();
|
|
89
105
|
const frame = execFrame(cmd);
|
|
90
106
|
ctx.awaiting = frame.requestId;
|
|
91
107
|
ctx.output = '';
|
|
108
|
+
ctx.done = false;
|
|
92
109
|
sseSend(stream, frame);
|
|
93
110
|
record({ ev: 'exec-push', step, requestId: frame.requestId, cmd });
|
|
94
111
|
// wait for the daemon's result frames for this requestId (or timeout)
|
|
@@ -96,6 +113,7 @@ async function runTask(task, stream, ctx) {
|
|
|
96
113
|
messages.push({ role: 'assistant', content: line });
|
|
97
114
|
messages.push({ role: 'user', content: `output:\n${out || '(none)'}` });
|
|
98
115
|
}
|
|
116
|
+
return answer || '(reached step limit without finishing)';
|
|
99
117
|
}
|
|
100
118
|
|
|
101
119
|
function waitResult(ctx, timeoutMs) {
|
|
@@ -113,36 +131,70 @@ function waitResult(ctx, timeoutMs) {
|
|
|
113
131
|
|
|
114
132
|
// per-connection context so a result frame can be matched to its exec
|
|
115
133
|
const ctxByToken = new Map();
|
|
134
|
+
// the single live daemon (Grok Bot's local-exec on the user's Mac). Set when it
|
|
135
|
+
// opens its SSE; the /drive endpoint pushes exec frames down THIS stream.
|
|
136
|
+
let activeDaemon = null;
|
|
137
|
+
// one exec loop at a time against the shared daemon ctx — two concurrent
|
|
138
|
+
// runTask calls would interleave result frames onto the same ctx.output.
|
|
139
|
+
let driveQueue = Promise.resolve();
|
|
140
|
+
function queueDrive(fn) {
|
|
141
|
+
const next = driveQueue.then(fn, fn);
|
|
142
|
+
driveQueue = next.catch(() => {});
|
|
143
|
+
return next;
|
|
144
|
+
}
|
|
116
145
|
|
|
117
146
|
for (const port of PORTS) {
|
|
118
147
|
const server = http.createServer((req, res) => {
|
|
119
|
-
// The daemon's receive-channel: hold the SSE open
|
|
120
|
-
//
|
|
148
|
+
// The daemon's receive-channel: hold the SSE open. The TASK does NOT come
|
|
149
|
+
// from here or from any env var — it arrives via POST /drive, forwarded by
|
|
150
|
+
// the backend from whatever the user typed into the Grok Bot UI.
|
|
121
151
|
if (req.method === 'GET' && req.url && req.url.startsWith('/local-exec/requests')) {
|
|
122
152
|
const token = (req.headers['x-anyrun-network-token'] || 'default');
|
|
123
153
|
const ctx = { awaiting: null, output: '', done: false };
|
|
124
154
|
ctxByToken.set(token, ctx);
|
|
125
|
-
|
|
155
|
+
activeDaemon = { stream: res, ctx };
|
|
156
|
+
record({ port, method: 'GET', path: req.url, note: 'SSE opened — daemon connected', bodyBytes: 0 });
|
|
126
157
|
res.writeHead(200, { 'content-type': 'text/event-stream', 'cache-control': 'no-cache', connection: 'keep-alive' });
|
|
127
158
|
res.write(':ok\n\n');
|
|
128
|
-
// announce ourselves
|
|
159
|
+
// announce ourselves; then wait for the UI to drive us
|
|
129
160
|
sseSend(res, { kind: 'welcome', providerId: 'openzoo' });
|
|
130
|
-
req.on('close', () =>
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}
|
|
161
|
+
req.on('close', () => {
|
|
162
|
+
ctxByToken.delete(token);
|
|
163
|
+
if (activeDaemon && activeDaemon.stream === res) activeDaemon = null;
|
|
164
|
+
});
|
|
134
165
|
return;
|
|
135
166
|
}
|
|
136
167
|
|
|
137
168
|
const chunks = [];
|
|
138
169
|
req.on('data', (d) => chunks.push(d));
|
|
139
|
-
req.on('end', () => {
|
|
170
|
+
req.on('end', async () => {
|
|
140
171
|
const body = Buffer.concat(chunks);
|
|
141
172
|
let parsed, kinds;
|
|
142
173
|
try { parsed = JSON.parse(body.toString('utf8')); kinds = parsed.frames?.map((f) => f.kind).join(','); } catch { /* not json */ }
|
|
143
174
|
record({ port, method: req.method, path: req.url, bodyBytes: body.length, frames: kinds,
|
|
144
175
|
bodyUtf8: body.slice(0, 1024).toString('utf8').replace(/[^\x20-\x7e]/g, '.') });
|
|
145
176
|
|
|
177
|
+
// THE TRIGGER: the backend forwards the user's Grok Bot prompt here. Drive
|
|
178
|
+
// the agent loop against the connected daemon and return the answer text.
|
|
179
|
+
if (req.method === 'POST' && req.url && req.url.startsWith('/drive')) {
|
|
180
|
+
let task = '';
|
|
181
|
+
try { task = (JSON.parse(body.toString('utf8')).task || '').toString(); }
|
|
182
|
+
catch { task = body.toString('utf8'); }
|
|
183
|
+
record({ ev: 'drive', task: task.slice(0, 160) });
|
|
184
|
+
if (!activeDaemon) {
|
|
185
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
186
|
+
res.end(JSON.stringify({ ok: false, text: '(sandbox not connected yet — give the app a moment and retry)' }));
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
let text;
|
|
190
|
+
try {
|
|
191
|
+
text = await queueDrive(() => runTask(task, activeDaemon.stream, activeDaemon.ctx));
|
|
192
|
+
} catch (e) { text = `agent error: ${e.message}`; record({ ev: 'drive-error', err: String(e) }); }
|
|
193
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
194
|
+
res.end(JSON.stringify({ ok: true, text }));
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
146
198
|
// the daemon posts result frames here — accumulate output for the loop
|
|
147
199
|
if (parsed?.frames && req.url?.includes('/local-exec/responses')) {
|
|
148
200
|
const token = (req.headers['x-anyrun-network-token'] || 'default');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openzoo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.42.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",
|