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 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
@@ -175,11 +175,14 @@ function encodeChatResponse(text) {
175
175
  }
176
176
 
177
177
  /**
178
- * ROUTE AUTO THROUGH THE ZOO. Cursor's chat inference is ChatService/
179
- * StreamUnifiedChat to the agent host (now impersonated). Parse the prompt, pay
180
- * the zoo via the local proxy, stream the answer back as StreamUnifiedChatResponse
181
- * frames. Legitimate: Auto is free-allowed; we redirect the operator's OWN
182
- * inference to their OWN paid proxy.
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
- const zoo = await fetch('http://127.0.0.1:8402/v1/chat/completions', {
192
- method: 'POST',
193
- headers: { 'content-type': 'application/json', authorization: 'Bearer sk-openzoo' },
194
- body: JSON.stringify({
195
- model: process.env.OPENZOO_DEFAULT_MODEL || 'anthropic/claude-opus-5',
196
- messages: [{ role: 'user', content: prompt }],
197
- max_tokens: Number(process.env.OPENZOO_ASK_MAX_TOKENS || 2048),
198
- }),
199
- });
200
- const data = await zoo.json();
201
- text = data.choices?.[0]?.message?.content || '(no content)';
202
- log(`cursor-backend: << zoo replied ${text.length} chars (paid x402)`);
203
- } catch (e) { text = `openzoo error: ${e.message}`; log(`cursor-backend: zoo call failed: ${e.message}`); }
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 drive a Linux/macOS shell to accomplish a task. Reply with EXACTLY one line:
74
- either RUN: <a single shell command>
75
- or DONE: <one-line summary> when the task is complete.
76
- No prose, no fences. You are given each command's output before your next line.`;
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
- if (!run) { record({ ev: 'task-done', step, line }); return; }
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; when it connects, if a
120
- // task is queued (OZ_TASK) run the agent loop against it.
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
- record({ port, method: 'GET', path: req.url, note: 'SSE opened', bodyBytes: 0 });
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, then drive the task
159
+ // announce ourselves; then wait for the UI to drive us
129
160
  sseSend(res, { kind: 'welcome', providerId: 'openzoo' });
130
- req.on('close', () => ctxByToken.delete(token));
131
- if (process.env.OZ_TASK) {
132
- setTimeout(() => runTask(process.env.OZ_TASK, res, ctx).catch((e) => record({ ev: 'task-error', err: String(e) })), 1200);
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.41.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",