openzoo 0.40.0 → 0.41.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.
Files changed (2) hide show
  1. package/lib/podagent.mjs +133 -71
  2. package/package.json +1 -1
package/lib/podagent.mjs CHANGED
@@ -1,77 +1,136 @@
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).
1
+ // Pod agent — runs INSIDE our box. It IS the Grok Bot sandbox brain, on the
2
+ // ports the app expects (1337 agent, 6080 vnc, 1340 local-exec), but the brain
3
+ // is openzoo (x402-paid) and the sandbox is ours.
3
4
  //
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-…
5
+ // PROTOCOL, REVERSE-ENGINEERED FROM local-exec-daemon/main.cjs (2026-08-16):
6
+ // GET /local-exec/requests the daemon opens an SSE stream to RECEIVE frames
7
+ // POST /local-exec/responses the daemon SENDS frames: hello, ping, output,
8
+ // stdout, result, exit
9
+ // frames the daemon RECEIVES (we push down the SSE):
10
+ // {kind:"welcome", providerId}
11
+ // {kind:"exec", requestId, approvalId, serverMessage:{shellArgs:{command,
12
+ // workingDirectory, timeout}}} (serverMessage = agent.v1.ExecServerMessage,
13
+ // shellArgs = agent.v1.ShellArgs; the daemon assigns id itself)
14
+ // {kind:"upload"|"download"|"cancel", ...}
15
+ // exec is APPROVAL-GATED on the user's Mac (isLocalUseBlocked / supervised).
12
16
  //
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.
17
+ // So: the daemon runs commands on the USER's machine (variant "sand",
18
+ // localRoot=/Users/…), and WE from the box decide what to run, paying for
19
+ // the reasoning via openzoo. That is the whole product: a Grok-Bot-shaped agent
20
+ // whose brain is the zoo.
21
21
 
22
22
  import http from 'node:http';
23
23
  import { appendFileSync } from 'node:fs';
24
+ import { randomUUID } from 'node:crypto';
24
25
 
25
26
  const PORTS = (process.env.OZ_AGENT_PORTS || '1337,6080,1340,6081')
26
27
  .split(',').map((s) => Number(s.trim())).filter(Boolean);
27
28
  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';
29
+ const PROXY = process.env.OZ_PROXY || 'http://127.0.0.1:8402/v1';
30
+ const MODEL = process.env.OZ_BRAIN_MODEL || 'x-ai/grok-4.6';
31
+ const MAX_STEPS = Number(process.env.OZ_MAX_STEPS || 10);
31
32
 
32
33
  function record(entry) {
33
34
  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}`);
35
+ console.log(`[agent:${entry.port ?? '-'}] ${entry.method || entry.ev} ${entry.path || ''} ${entry.bodyBytes ?? ''}${entry.frames ? ' frames=' + entry.frames : ''}`);
36
+ }
37
+
38
+ // ------------------------------------------------------------------ frames --
39
+
40
+ /** Send frames to the daemon down its SSE. The daemon reads the stream with a
41
+ * TextDecoder and parses per-line JSON, so one frame per `data:` line. We wrap
42
+ * as {frames:[…]} to mirror the response side; a bare frame is the fallback. */
43
+ function sseSend(res, frame) {
44
+ const line = `data: ${JSON.stringify({ frames: [frame] })}\n\n`;
45
+ try { res.write(line); return true; } catch { return false; }
46
+ }
47
+
48
+ function execFrame(command, cwd = '/tmp') {
49
+ return {
50
+ kind: 'exec',
51
+ requestId: randomUUID(),
52
+ approvalId: randomUUID(),
53
+ // agent.v1.ExecServerMessage as protobuf-es JSON (camelCase). The daemon
54
+ // assigns `id`; we only supply the shell variant.
55
+ serverMessage: { shellArgs: { command, workingDirectory: cwd, timeout: 120 } },
56
+ };
57
+ }
58
+
59
+ // ------------------------------------------------------------------- brain --
60
+
61
+ /** One openzoo chat turn. Paid per call by the box's own wallet via the local
62
+ * proxy — no key, no account. */
63
+ async function brain(messages) {
64
+ const r = await fetch(`${PROXY}/chat/completions`, {
65
+ method: 'POST',
66
+ headers: { 'content-type': 'application/json', authorization: 'Bearer sk-openzoo' },
67
+ body: JSON.stringify({ model: MODEL, max_tokens: 900, messages }),
68
+ });
69
+ const j = await r.json().catch(() => ({}));
70
+ return j?.choices?.[0]?.message?.content ?? '';
71
+ }
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.`;
77
+
78
+ /** The agent loop for one task, executed through the connected daemon. Each
79
+ * RUN is pushed as an exec frame; the daemon's result frames (captured in
80
+ * `pendingResults`) feed the next turn. */
81
+ async function runTask(task, stream, ctx) {
82
+ const messages = [{ role: 'system', content: SYSTEM }, { role: 'user', content: task }];
83
+ for (let step = 0; step < MAX_STEPS; step++) {
84
+ const line = (await brain(messages)).trim();
85
+ record({ ev: 'brain', step, line });
86
+ const run = /^RUN:\s*([\s\S]+)/.exec(line);
87
+ if (!run) { record({ ev: 'task-done', step, line }); return; }
88
+ const cmd = run[1].trim();
89
+ const frame = execFrame(cmd);
90
+ ctx.awaiting = frame.requestId;
91
+ ctx.output = '';
92
+ sseSend(stream, frame);
93
+ record({ ev: 'exec-push', step, requestId: frame.requestId, cmd });
94
+ // wait for the daemon's result frames for this requestId (or timeout)
95
+ const out = await waitResult(ctx, 60_000);
96
+ messages.push({ role: 'assistant', content: line });
97
+ messages.push({ role: 'user', content: `output:\n${out || '(none)'}` });
98
+ }
36
99
  }
37
100
 
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
- // THE COMMAND FRAME IS kind:"shell", NOT "exec". Decoded from the app bundle:
48
- // the daemon's receive-union has kind "shell" (run), "sequence", "abort"; the
49
- // proto is {command: scalar, args, cwd, env}. Our first probe used "exec"
50
- // (which appears once in the bundle) and the daemon silently ignored it,
51
- // sending only pings. "shell" is what it actually runs.
52
- const frame = { frames: [{ kind: 'shell', id, command: cmd, cwd: '/tmp', supervised: true }] };
53
- const payload = `data: ${JSON.stringify(frame)}\n\n`;
54
- for (const res of execStreams) { try { res.write(payload); } catch { /* dead stream */ } }
55
- record({ t: new Date().toISOString(), port: 1340, method: 'SSE-PUSH', path: '/local-exec/requests',
56
- bodyBytes: payload.length, pushed: frame });
101
+ function waitResult(ctx, timeoutMs) {
102
+ return new Promise((resolve) => {
103
+ const started = Date.now();
104
+ const iv = setInterval(() => {
105
+ if (ctx.done || Date.now() - started > timeoutMs) {
106
+ clearInterval(iv); ctx.done = false; resolve(ctx.output || '');
107
+ }
108
+ }, 250);
109
+ });
57
110
  }
58
111
 
112
+ // ----------------------------------------------------------------- servers --
113
+
114
+ // per-connection context so a result frame can be matched to its exec
115
+ const ctxByToken = new Map();
116
+
59
117
  for (const port of PORTS) {
60
118
  const server = http.createServer((req, res) => {
61
- // SSE receive-channel: hold it open, let us push exec frames.
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.
62
121
  if (req.method === 'GET' && req.url && req.url.startsWith('/local-exec/requests')) {
63
- record({ t: new Date().toISOString(), port, method: 'GET', path: req.url,
64
- headers: req.headers, bodyBytes: 0, note: 'SSE stream opened' });
65
- res.writeHead(200, {
66
- 'content-type': 'text/event-stream',
67
- 'cache-control': 'no-cache',
68
- connection: 'keep-alive',
69
- });
122
+ const token = (req.headers['x-anyrun-network-token'] || 'default');
123
+ const ctx = { awaiting: null, output: '', done: false };
124
+ ctxByToken.set(token, ctx);
125
+ record({ port, method: 'GET', path: req.url, note: 'SSE opened', bodyBytes: 0 });
126
+ res.writeHead(200, { 'content-type': 'text/event-stream', 'cache-control': 'no-cache', connection: 'keep-alive' });
70
127
  res.write(':ok\n\n');
71
- execStreams.add(res);
72
- req.on('close', () => execStreams.delete(res));
73
- // once a stream is open, push the probe command to capture the exec loop
74
- if (!probedOnce) { probedOnce = true; setTimeout(() => pushExec(PROBE), 1500); }
128
+ // announce ourselves, then drive the task
129
+ 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
+ }
75
134
  return;
76
135
  }
77
136
 
@@ -79,32 +138,35 @@ for (const port of PORTS) {
79
138
  req.on('data', (d) => chunks.push(d));
80
139
  req.on('end', () => {
81
140
  const body = Buffer.concat(chunks);
82
- let frames;
83
- try { frames = JSON.parse(body.toString('utf8')).frames?.map((f) => f.kind).join(','); } catch { /* not json */ }
84
- record({
85
- t: new Date().toISOString(), port,
86
- method: req.method, path: req.url, headers: req.headers,
87
- bodyBytes: body.length, frames,
88
- bodyHex: body.slice(0, 256).toString('hex'),
89
- bodyUtf8: body.slice(0, 1024).toString('utf8').replace(/[^\x20-\x7e]/g, '.'),
90
- });
141
+ let parsed, kinds;
142
+ try { parsed = JSON.parse(body.toString('utf8')); kinds = parsed.frames?.map((f) => f.kind).join(','); } catch { /* not json */ }
143
+ record({ port, method: req.method, path: req.url, bodyBytes: body.length, frames: kinds,
144
+ bodyUtf8: body.slice(0, 1024).toString('utf8').replace(/[^\x20-\x7e]/g, '.') });
145
+
146
+ // the daemon posts result frames here — accumulate output for the loop
147
+ if (parsed?.frames && req.url?.includes('/local-exec/responses')) {
148
+ const token = (req.headers['x-anyrun-network-token'] || 'default');
149
+ const ctx = ctxByToken.get(token);
150
+ for (const f of parsed.frames) {
151
+ if (ctx && (f.kind === 'output' || f.kind === 'stdout')) ctx.output += (f.data || f.text || f.chunk || '');
152
+ if (ctx && (f.kind === 'result' || f.kind === 'exit')) ctx.done = true;
153
+ }
154
+ }
155
+
91
156
  if (req.url && req.url.includes('/vnc')) {
92
157
  res.writeHead(200, { 'content-type': 'text/html' });
93
- res.end('<!doctype html><title>openzoo box</title><body>capture</body>');
158
+ res.end('<!doctype html><title>openzoo box</title>');
94
159
  } else {
95
- // /local-exec/responses and everything else: accept it so the daemon
96
- // keeps its session and keeps sending frames.
97
160
  res.writeHead(200, { 'content-type': 'application/json' });
98
161
  res.end('{"ok":true}');
99
162
  }
100
163
  });
101
164
  });
102
165
  server.on('upgrade', (req, socket) => {
103
- record({ t: new Date().toISOString(), port, method: 'UPGRADE', path: req.url,
104
- upgrade: req.headers.upgrade, headers: req.headers, bodyBytes: 0 });
166
+ record({ port, method: 'UPGRADE', path: req.url, bodyBytes: 0 });
105
167
  socket.write('HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n\r\n');
106
- socket.on('data', (d) => record({ t: new Date().toISOString(), port, method: 'WS-DATA', path: req.url,
107
- bodyBytes: d.length, bodyHex: d.slice(0, 256).toString('hex') }));
108
168
  });
109
- server.listen(port, '0.0.0.0', () => console.log(`[agent] capturing on :${port}`));
169
+ server.listen(port, '0.0.0.0', () => console.log(`[agent] on :${port}`));
110
170
  }
171
+
172
+ console.log(`[agent] brain=${MODEL} via ${PROXY} task=${process.env.OZ_TASK ? JSON.stringify(process.env.OZ_TASK).slice(0, 60) : '(none — waiting)'}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openzoo",
3
- "version": "0.40.0",
3
+ "version": "0.41.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",