openzoo 0.49.17 → 0.50.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 DELETED
@@ -1,299 +0,0 @@
1
- // RunPod CPU boxes — the sandbox fleet `openzoo grokbot` runs its agents in.
2
- //
3
- // SAME SHAPE AS GROK BOT, DIFFERENT ECONOMICS. Grok Bot provisions a
4
- // cursorvm.com pod per bot and runs the agent inside Cursor's cloud, billing
5
- // through them (MEASURED: EnsureSandBox returns
6
- // `<id>-pod-<id>-1337.us9.cursorvm.com` + a VNC port + /workspace/terminals).
7
- // This does the same thing on RunPod, with inference paid per-call by x402
8
- // from a wallet the box holds — no account, no provider key.
9
- //
10
- // CPU ONLY, ALWAYS. A GPU flavour here would be a silent 100x on the bill for
11
- // a box that only ever runs a shell and a node process. `spawnBox` builds its
12
- // request body without GPU fields AND asserts on the serialized JSON, because
13
- // an `in` check against a literal you wrote three lines earlier can never fail
14
- // — that guard is decoration, not enforcement.
15
-
16
- import { execFile } from 'node:child_process';
17
- import { promisify } from 'node:util';
18
-
19
- const pexec = promisify(execFile);
20
-
21
- import { readFileSync } from 'node:fs';
22
- import { fileURLToPath } from 'node:url';
23
- import { dirname, join } from 'node:path';
24
- let _agentB64 = null;
25
- function podAgentB64() {
26
- if (_agentB64) return _agentB64;
27
- const here = dirname(fileURLToPath(import.meta.url));
28
- _agentB64 = readFileSync(join(here, 'podagent.mjs')).toString('base64');
29
- return _agentB64;
30
- }
31
-
32
- const REST = 'https://rest.runpod.io/v1';
33
- export const BOX_PREFIX = 'openzoo-box-';
34
- const CPU_FLAVORS = ['cpu3c', 'cpu3g', 'cpu5c'];
35
- // GLIBC, NOT ALPINE. `npx openzoo` pulls native deps (ed25519 / ws helpers);
36
- // on musl they fail to build because the entrypoint has no toolchain, which is
37
- // why alpine boxes sat at 404 forever. Debian-slim runs the shim as published,
38
- // and its apt has a real sshd.
39
- // Everything the box runs is ALREADY IN THE IMAGE: the shim, its node_modules,
40
- // grokui, and sshd. Boxes used to apt-get and `npx openzoo` at boot, which put
41
- // a package fetch on the critical path from RunPod egress IPs — and those get
42
- // rate limited. A 429 from raw.githubusercontent arrives as a FILE whose
43
- // contents are the text "429: Too Many Requests", so node ran it and failed in
44
- // a way that read as an app bug rather than a download bug.
45
- // Still glibc, never musl: @solana's native deps don't build on the box.
46
- const BOX_IMAGE = process.env.OPENZOO_BOX_IMAGE || 'jrsdunn123/openzoo-box:latest';
47
- const BOX_PORTS = ['8402/http', '4173/http', '1337/http', '6080/http', '1340/http', '6081/http', '22/tcp'];
48
- const TTL_MAX_HOURS = 90 * 24;
49
-
50
- /**
51
- * Boot: write the wallet, START SSHD, start the x402 proxy + capture agent.
52
- *
53
- * SSHD IS NOT OPTIONAL and was the second bug: our dockerStartCmd replaces the
54
- * image's own init, so nothing starts sshd and every `execInBox` got
55
- * "connection refused" — the agent had no hands. RunPod injects the account
56
- * key at $PUBLIC_KEY / $SSH_PUBLIC_KEY; we write it to authorized_keys, generate
57
- * host keys, and run sshd in the foreground of the background. Passwordless
58
- * root login is NOT enabled — key-only.
59
- *
60
- * NOTHING IS FETCHED AT BOOT. sshd, the shim and grokui are baked into
61
- * BOX_IMAGE by box.Dockerfile; this only copies /opt into the volume RunPod
62
- * mounts over /workspace and starts things. readyBox() still polls /v1/models
63
- * rather than assuming a timing.
64
- */
65
- const ENTRYPOINT = [
66
- 'bash', '-lc',
67
- 'set +e; mkdir -p /workspace /root/.openzoo /var/log/openzoo /run/sshd /root/.ssh; '
68
- + 'if [ -n "$OPENZOO_WALLET_JSON" ]; then printf %s "$OPENZOO_WALLET_JSON" > /root/.openzoo/wallet.json; chmod 600 /root/.openzoo/wallet.json; fi; '
69
- // RunPod hands the operator pubkey in one of these — accept whichever is set
70
- + 'for K in "$PUBLIC_KEY" "$SSH_PUBLIC_KEY"; do [ -n "$K" ] && echo "$K" >> /root/.ssh/authorized_keys; done; chmod 600 /root/.ssh/authorized_keys 2>/dev/null; '
71
- + 'ssh-keygen -A >/dev/null 2>&1; sed -i "s/^#\\?PermitRootLogin.*/PermitRootLogin prohibit-password/" /etc/ssh/sshd_config 2>/dev/null; /usr/sbin/sshd -e >/var/log/openzoo/sshd.log 2>&1 & '
72
- // RUN FROM /opt, never from /workspace. Staging the app onto the volume and
73
- // gating on `[ ! -f .../bin/openzoo.js ]` is a trap: one interrupted copy
74
- // leaves that file behind, every later boot skips the copy, and the box
75
- // crash-loops on a half-copied node_modules forever (seen in production as
76
- // "Cannot find module .../viem/accounts" every 15s, which also cost the pod
77
- // its HTTP port mappings). The app is read-only at runtime; mutable state
78
- // lives in /root/.openzoo.
79
- + 'until OPENZOO_BIND=0.0.0.0 node /opt/openzoo/bin/openzoo.js >> /var/log/openzoo/proxy.log 2>&1; do echo "proxy exited, restarting" >> /var/log/openzoo/proxy.log; sleep 2; done & '
80
- + 'until OZ_GROKUI_BIND=0.0.0.0 OZ_GROKUI_PORT=4173 node /opt/openzoo/lib/grokui.mjs >> /var/log/openzoo/grokui.log 2>&1; do echo "grokui exited, restarting" >> /var/log/openzoo/grokui.log; sleep 2; done & '
81
- // the capture agent answers the ports Grok Bot expects a Cursor sandbox on,
82
- // logging the protocol we do not yet speak (see lib/podagent.mjs)
83
- + 'if [ -n "$OZ_PODAGENT_B64" ]; then printf %s "$OZ_PODAGENT_B64" | base64 -d > /opt/podagent.mjs; node /opt/podagent.mjs > /var/log/openzoo/agent.log 2>&1 & fi; '
84
- + 'sleep infinity',
85
- ];
86
-
87
- function key() {
88
- const k = process.env.RUNPOD_API_KEY || '';
89
- return typeof k === 'string' ? k.trim() : '';
90
- }
91
-
92
- export function runpodConfigured() {
93
- // guard the access, not just the prefix — an unset key must not throw here
94
- return key().startsWith('rpa_');
95
- }
96
-
97
- async function rp(path, init) {
98
- const k = key();
99
- if (!k) return { ok: false, status: 503, data: { error: 'RUNPOD_API_KEY not set' } };
100
- const res = await fetch(`${REST}${path}`, {
101
- ...init,
102
- headers: { authorization: `Bearer ${k}`, 'content-type': 'application/json', ...(init?.headers || {}) },
103
- });
104
- const data = await res.json().catch(() => ({}));
105
- return { ok: res.ok, status: res.status, data };
106
- }
107
-
108
- export function ttlHours(days = 30) {
109
- return Math.min(Math.max(Number(days) || 30, 1) * 24, TTL_MAX_HOURS);
110
- }
111
-
112
- /**
113
- * Expiry is encoded in the name so a box is reapable even if its env is lost.
114
- *
115
- * PARSED FROM A FIXED POSITION, NOT `.pop()`. The name ends
116
- * `-<unix>-<rand4>`, so popping the last segment yields the RANDOM suffix,
117
- * Number() gives NaN, and every box reports "no expiry" — which is exactly the
118
- * bug the site's copy of this had.
119
- */
120
- export function expiresFromName(name) {
121
- if (!name?.startsWith(BOX_PREFIX)) return null;
122
- const parts = name.split('-');
123
- const n = Number(parts[parts.length - 2]);
124
- return Number.isFinite(n) && n > 1_700_000_000 ? n : null;
125
- }
126
-
127
- export function proxyUrl(id, port = 8402) {
128
- return id ? `https://${id}-${port}.proxy.runpod.net` : null;
129
- }
130
-
131
- /** The URLs Grok Bot's EnsureSandBox response needs, all fronted by RunPod. */
132
- export function podEndpoints(id) {
133
- return id ? {
134
- agent: proxyUrl(id, 1337),
135
- vnc: proxyUrl(id, 6080),
136
- p1340: proxyUrl(id, 1340),
137
- p6081: proxyUrl(id, 6081),
138
- proxy: proxyUrl(id, 8402),
139
- } : null;
140
- }
141
-
142
- /** What a caller may see. NEVER the raw pod: `env` carries OPENZOO_WALLET_JSON,
143
- * which is a funded private key. Raw pods must not escape this module. */
144
- export function serializeBox(p) {
145
- const exp = expiresFromName(p.name) || Number(p.env?.OPENZOO_EXPIRES_UNIX || 0) || null;
146
- const sshPort = p.portMappings?.['22'];
147
- return {
148
- id: p.id,
149
- name: p.name,
150
- status: p.desiredStatus,
151
- costPerHr: p.costPerHr,
152
- proxy: proxyUrl(p.id),
153
- ssh: p.publicIp && sshPort ? `ssh -o StrictHostKeyChecking=no root@${p.publicIp} -p ${sshPort}` : null,
154
- host: p.publicIp || null,
155
- sshPort: sshPort || null,
156
- console: p.id ? `https://www.runpod.io/console/pods/${p.id}` : null,
157
- expiresAt: exp ? new Date(exp * 1000).toISOString() : null,
158
- };
159
- }
160
-
161
- /** Our boxes only, serialized. */
162
- export async function listBoxes() {
163
- const r = await rp('/pods');
164
- if (!Array.isArray(r.data)) return { ok: false, error: r.data?.error || `pods ${r.status}`, boxes: [] };
165
- return {
166
- ok: true,
167
- boxes: r.data.filter((p) => !p.gpuCount && p.name?.startsWith(BOX_PREFIX)).map(serializeBox),
168
- };
169
- }
170
-
171
- export async function killBox(id) {
172
- return rp(`/pods/${id}`, { method: 'DELETE' });
173
- }
174
-
175
- /** Delete our boxes past their deadline. Name-prefix scoped AND gpu-guarded. */
176
- export async function reapExpired() {
177
- const r = await rp('/pods');
178
- if (!Array.isArray(r.data)) return { reaped: [] };
179
- const now = Math.floor(Date.now() / 1000);
180
- const reaped = [];
181
- for (const p of r.data) {
182
- if (!p.id || p.gpuCount || !p.name?.startsWith(BOX_PREFIX)) continue;
183
- const deadline = expiresFromName(p.name) || Number(p.env?.OPENZOO_EXPIRES_UNIX || 0);
184
- if (deadline && now >= deadline) {
185
- await killBox(p.id);
186
- reaped.push(p.id);
187
- }
188
- }
189
- return { reaped };
190
- }
191
-
192
- /**
193
- * MACHINE TIERS — a menu, not one size.
194
- *
195
- * A box that only runs a shell is fine on 2 vCPU, but a grokbot agent that
196
- * builds a site, runs a dev server, or compiles wants real muscle — and some
197
- * tasks want a GPU. So the orchestrator quotes SEVERAL tiers per timeframe and
198
- * the user picks. GPU is a DELIBERATE tier here, never an accident: the guard
199
- * below refuses GPU fields on a CPU tier, so a cpu tier can't silently 100x the
200
- * bill, while a gpu tier carries them on purpose.
201
- *
202
- * $/hr are approximate RunPod secure-cloud rates for sizing the quote; the real
203
- * charge is whatever RunPod bills. estUsdHr is labelled approximate for that.
204
- */
205
- export const BOX_TIERS = {
206
- 'cpu-sm': { label: 'CPU · 2 vCPU', compute: 'CPU', vcpu: 2, disk: 20, estUsdHr: 0.06 },
207
- 'cpu-md': { label: 'CPU · 4 vCPU', compute: 'CPU', vcpu: 4, disk: 40, estUsdHr: 0.12 },
208
- 'cpu-lg': { label: 'CPU · 8 vCPU', compute: 'CPU', vcpu: 8, disk: 60, estUsdHr: 0.24 },
209
- 'gpu-4090':{ label: 'GPU · 1× RTX 4090', compute: 'GPU', gpuTypeIds: ['NVIDIA GeForce RTX 4090'], gpuCount: 1, disk: 60, estUsdHr: 0.44 },
210
- 'gpu-a100':{ label: 'GPU · 1× A100 80GB', compute: 'GPU', gpuTypeIds: ['NVIDIA A100 80GB PCIe'], gpuCount: 1, disk: 80, estUsdHr: 1.64 },
211
- };
212
- export const DEFAULT_TIER = 'cpu-sm';
213
-
214
- /** The quote menu the orchestrator shows: every tier priced for `days`. */
215
- export function quoteTiers(days = 1) {
216
- const hrs = ttlHours(days);
217
- return Object.entries(BOX_TIERS).map(([key, t]) => ({
218
- tier: key, label: t.label, days: Math.round(hrs / 24 * 10) / 10,
219
- estUsd: Math.round(t.estUsdHr * hrs * 100) / 100, estUsdHr: t.estUsdHr, gpu: t.compute === 'GPU',
220
- }));
221
- }
222
-
223
- export async function spawnBox({ name = 'bot', days = 1, tier = DEFAULT_TIER, walletJson, pubkey, task } = {}) {
224
- const spec = BOX_TIERS[tier] || BOX_TIERS[DEFAULT_TIER];
225
- const expiresAt = new Date(Date.now() + ttlHours(days) * 3600 * 1000);
226
- const unix = Math.floor(expiresAt.getTime() / 1000);
227
- const rand = Math.random().toString(36).slice(2, 6);
228
- const full = `${BOX_PREFIX}${name}-${unix}-${rand}`;
229
-
230
- const body = {
231
- name: full,
232
- computeType: spec.compute,
233
- cloudType: 'SECURE',
234
- imageName: BOX_IMAGE,
235
- containerDiskInGb: spec.disk,
236
- volumeInGb: 10,
237
- volumeMountPath: '/workspace',
238
- ports: BOX_PORTS,
239
- dockerStartCmd: ENTRYPOINT,
240
- ...(spec.compute === 'CPU'
241
- ? { vcpuCount: spec.vcpu, cpuFlavorIds: CPU_FLAVORS, cpuFlavorPriority: 'availability' }
242
- : { gpuCount: spec.gpuCount, gpuTypeIds: spec.gpuTypeIds }),
243
- env: {
244
- OPENZOO_BOX: '1',
245
- OPENZOO_TIER: tier,
246
- OPENZOO_EXPIRES_UNIX: String(unix),
247
- OPENZOO_NO_TUNNEL: '1', // the runpod proxy already fronts it
248
- OPENZOO_BIND: '0.0.0.0', // else the runpod proxy can't reach :8402
249
- OPENZOO_TUNNEL_TOKEN: `oz-${Math.random().toString(36).slice(2)}${Math.random().toString(36).slice(2)}`,
250
- ...(walletJson ? { OPENZOO_WALLET_JSON: walletJson } : {}),
251
- ...(pubkey ? { OPENZOO_WALLET_PUBKEY: pubkey } : {}),
252
- OZ_PODAGENT_B64: podAgentB64(),
253
- ...(task ? { OZ_TASK: task } : {}),
254
- },
255
- };
256
- // GPU ONLY ON A GPU TIER. A cpu tier must never carry GPU fields — that would
257
- // be a silent 100x. Enforced on the serialized payload, not a literal.
258
- const wire = JSON.stringify(body);
259
- if (spec.compute === 'CPU' && /"gpu(Count|TypeIds)"/.test(wire)) {
260
- throw new Error('refusing to spawn: GPU fields on a CPU tier');
261
- }
262
-
263
- const created = await rp('/pods', { method: 'POST', body: wire });
264
- if (!created.ok) return { ok: false, error: created.data?.error || `spawn ${created.status}` };
265
- return { ok: true, tier, box: serializeBox(created.data), expiresAt: expiresAt.toISOString() };
266
- }
267
-
268
- /** Poll until the box answers on its proxy — npx has to fetch the shim first. */
269
- export async function readyBox(id, { timeoutMs = 240_000, log = () => {} } = {}) {
270
- const url = `${proxyUrl(id)}/v1/models`;
271
- const started = Date.now();
272
- let last = '';
273
- while (Date.now() - started < timeoutMs) {
274
- try {
275
- const r = await fetch(url, { signal: AbortSignal.timeout(6000) });
276
- if (r.ok) return { ok: true, seconds: Math.round((Date.now() - started) / 1000) };
277
- last = `http ${r.status}`;
278
- } catch (e) { last = e.name === 'TimeoutError' ? 'timeout' : e.message; }
279
- log(` waiting for box proxy… (${Math.round((Date.now() - started) / 1000)}s, ${last})`);
280
- await new Promise((r) => setTimeout(r, 6000));
281
- }
282
- return { ok: false, error: `box proxy never came up (${last})` };
283
- }
284
-
285
- /** Run a command INSIDE the box. This is the sandbox half of the product:
286
- * the model runs wherever openzoo routes it, the side effects land here. */
287
- export async function execInBox(box, cmd, { timeoutMs = 120_000 } = {}) {
288
- if (!box.host || !box.sshPort) return { ok: false, stdout: '', stderr: 'box has no ssh yet' };
289
- try {
290
- const { stdout, stderr } = await pexec('ssh', [
291
- '-o', 'StrictHostKeyChecking=no', '-o', 'UserKnownHostsFile=/dev/null',
292
- '-o', 'LogLevel=ERROR', '-o', 'ConnectTimeout=15',
293
- '-p', String(box.sshPort), `root@${box.host}`, cmd,
294
- ], { timeout: timeoutMs, maxBuffer: 8 * 1024 * 1024 });
295
- return { ok: true, stdout, stderr };
296
- } catch (e) {
297
- return { ok: false, stdout: e.stdout || '', stderr: e.stderr || e.message };
298
- }
299
- }
package/lib/brief.js DELETED
@@ -1,76 +0,0 @@
1
- /**
2
- * The capability briefing the proxy injects into every conversation.
3
- *
4
- * WHY THIS EXISTS: an agent on the other side of this proxy has no idea what
5
- * it is connected to. It assumes a stock OpenAI endpoint, so it chunks corpora
6
- * it could bind whole, hunts for metadata by shelling around the operator's
7
- * machine, and re-uploads the same megabytes every turn. Documentation the
8
- * agent never reads does not help it. Telling it IN BAND does.
9
- *
10
- * Kept short on purpose — it rides every request, so it earns its tokens.
11
- * OPENZOO_NO_BRIEF=1 turns it off.
12
- */
13
-
14
- /**
15
- * `selfUrl` is the base URL THIS request arrived on — the tunnel URL for a
16
- * remote harness, localhost otherwise. Without it an agent writing a helper
17
- * script has to guess its own endpoint, and it guesses wrong: observed in the
18
- * wild, a Cursor agent hardcoded `https://openzoo.fun/v1` (a marketing site,
19
- * not an API) because nothing in the conversation named the real one.
20
- */
21
- export const briefFor = (selfUrl) => [
22
- // SMALL ON PURPOSE. This used to be ~2.2KB of prose on EVERY request —
23
- // corpus binding, request-size limits, payment rails, model-id matching —
24
- // most of which a given call never needs, all of which the caller pays for in
25
- // tokens and latency. An agent that needs the detail can ask; what it cannot
26
- // work out for itself is the base URL and the one behaviour that changes how
27
- // it should send a big body. Everything else was documentation shipped as
28
- // overhead.
29
- ...(selfUrl ? [`Endpoint: ${selfUrl} (already ends in /v1). Routes: /chat/completions, /hrr/bind, /models.`] : []),
30
- 'Bodies over ~16KB are bound to holographic memory and answered by retrieval, so a large corpus can be sent whole rather than summarised or chunked. A corpus sent once is not re-uploaded.',
31
- 'Calls are paid per request from the operator\'s wallet; there is no key to supply. Unknown model ids match the nearest served model.',
32
- ].join('\n');
33
-
34
- /** Stable substring used to detect an already-injected brief. Must appear in
35
- * briefFor() output verbatim — see injectBrief(). */
36
- export const BRIEF_MARK = 'bound to holographic memory and answered by retrieval';
37
-
38
- /** Back-compat: the briefing with no endpoint line. */
39
- export const BRIEF = briefFor(null);
40
-
41
- /**
42
- * Inject the briefing as a system message. Idempotent (never doubles up if a
43
- * conversation already carries it), non-destructive (an existing system
44
- * message keeps its position and content — ours is appended after it, since
45
- * harnesses often pin behaviour in the first system turn).
46
- * Returns null when nothing should change.
47
- */
48
- export function injectBrief(body, selfUrl = null) {
49
- if (process.env.OPENZOO_NO_BRIEF === '1') return null;
50
- const msgs = body?.messages;
51
- if (!Array.isArray(msgs) || !msgs.length) return null;
52
- // THE SENTINEL MUST BE TEXT THE BRIEF ACTUALLY CONTAINS. This checked for
53
- // 'connected through an openzoo proxy' — the old opening line — so shrinking
54
- // the brief would have silently broken idempotency and stacked a fresh copy
55
- // onto every single turn, growing the system block without bound.
56
- if (msgs.some((m) => typeof m?.content === 'string' && m.content.includes(BRIEF_MARK))) return null;
57
-
58
- const brief = { role: 'system', content: briefFor(selfUrl) };
59
- // THE LEADING SYSTEM RUN ONLY — NOT THE LAST SYSTEM ANYWHERE.
60
- //
61
- // This used to reduce over the WHOLE array for the last `role === 'system'`,
62
- // which is the same thing only while every system message sits at the front.
63
- // The moment anything injects one later, the brief is spliced in AFTER the
64
- // user's turn, and the conversation the model receives ends with operator
65
- // notes instead of a question.
66
- //
67
- // OBSERVED live: forwarded tail `s a t a t a u s a u s s` — two system
68
- // messages after the last user message. The agent replied "I don't see an
69
- // explicit question or task for this turn", answered the PREVIOUS turn, and
70
- // read as one message behind for an entire session.
71
- let lead = -1;
72
- while (lead + 1 < msgs.length && msgs[lead + 1]?.role === 'system') lead += 1;
73
- const out = [...msgs];
74
- out.splice(lead + 1, 0, brief); // after the leading system block, before any turn
75
- return { ...body, messages: out };
76
- }
package/lib/grokbot.js DELETED
@@ -1,120 +0,0 @@
1
- // `npx openzoo grokbot` — keep Grok Bot's UI, serve OUR box underneath.
2
- //
3
- // Grok Bot is a thin client over Cursor's aiserver.v1 API. Each bot gets a
4
- // pod from `GrokBotService/EnsureSandBox` — a cursorvm.com VM the agent runs
5
- // in. We MITM api2.cursor.sh (proven: /etc/hosts pin + Node TLS override lets
6
- // us answer 88 methods and passthrough the rest with real bodies), and for
7
- // that ONE method we return OUR RunPod box in Cursor's exact 12-field wire
8
- // shape. The polished UI is theirs; the sandbox is ours.
9
- //
10
- // THE WALL, STATED HONESTLY: once the app has our pod, it speaks Cursor's
11
- // in-pod agent protocol to port 1337 and expects a VNC/websockify desktop on
12
- // 6080. We do not yet speak either. The box therefore runs lib/podagent.mjs,
13
- // which LOGS every request on those ports to /var/log/openzoo/agent.jsonl. So
14
- // this ships as a CAPTURE hijack: the UI connects to our box, and we read what
15
- // it sends to learn the protocol we then have to implement. That is the honest
16
- // state — a working hijack needs that protocol, and this is how we get it.
17
-
18
- import { spawn, execSync } from 'node:child_process';
19
- import { existsSync } from 'node:fs';
20
- import { homedir } from 'node:os';
21
- import { join } from 'node:path';
22
-
23
- import { config, USDC_MINT, TOKEN_MINT } from './config.js';
24
- import { PayClient } from './pay.js';
25
- import {
26
- podEndpoints, readyBox, runpodConfigured, spawnBox, listBoxes, execInBox,
27
- } from './boxes.js';
28
-
29
- const APP = '/Applications/Grok Bot.app';
30
- const GROK_HOME = process.env.GROK_HOME || join(homedir(), '.grok');
31
-
32
- export async function runGrokBot(argv = []) {
33
- if (process.platform !== 'darwin') {
34
- console.error('openzoo grokbot: the app hijack is macOS-only (needs Grok Bot.app + /etc/hosts).');
35
- process.exit(1);
36
- }
37
- if (!existsSync(APP)) {
38
- console.error('openzoo grokbot: /Applications/Grok Bot.app not found.');
39
- process.exit(1);
40
- }
41
- if (!runpodConfigured()) {
42
- console.error('openzoo grokbot: RUNPOD_API_KEY not set (needs an rpa_… key).');
43
- console.error(' export RUNPOD_API_KEY=rpa_… then re-run. Boxes bill to that account;');
44
- console.error(' inference bills per-call to the box\'s own openzoo wallet.');
45
- process.exit(1);
46
- }
47
-
48
- const days = Number((argv.find((a) => /^--days=/.test(a)) || '').split('=')[1]) || 1;
49
-
50
- // 1) a box, ready, BEFORE we touch the app — so the first EnsureSandBox we
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.
56
- console.error('openzoo grokbot: spawning your box (CPU, x402-funded)…');
57
- const spawned = await spawnBox({ name: 'grokbot', days });
58
- if (!spawned.ok) { console.error(` spawn failed: ${spawned.error}`); process.exit(1); }
59
- const box = spawned.box;
60
- console.error(` ${box.name} ${box.id} expires ${spawned.expiresAt.slice(0, 16)}`);
61
- const ready = await readyBox(box.id, { log: (m) => console.error(m) });
62
- if (!ready.ok) console.error(` ${ready.error} — continuing; the capture agent may still bind`);
63
- else console.error(` box ready in ${ready.seconds}s`);
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
-
77
- const ep = podEndpoints(box.id);
78
- process.env.OZ_HIJACK_POD = JSON.stringify({
79
- region: 'us1',
80
- accountId: box.id.replace(/[^a-z0-9]/gi, '').slice(0, 20),
81
- podId: `pod-${box.id}`,
82
- token: `nto-${Math.random().toString(36).slice(2, 12)}`,
83
- agent: ep.agent, vnc: ep.vnc, p1340: ep.p1340, p6081: ep.p6081,
84
- });
85
- // the backend (handleStreamChat) forwards each UI prompt here to be driven
86
- process.env.OZ_BOX_AGENT = ep.agent;
87
- // other methods must still reach the REAL backend, or the app can't load
88
- process.env.OPENZOO_PASSTHRU = '1';
89
- process.env.OPENZOO_FORCE_BLOCK = '1';
90
-
91
- // 2) pin + serve api2.cursor.sh locally (sudo). Chromium flags don't reach
92
- // the Electron main process; only /etc/hosts + Node TLS override do.
93
- const { blockBackend, unblockBackend, isBlocked } = await import('./hosts.js');
94
- const { startCursorBackend } = await import('./cursorbackend.js');
95
- console.error('openzoo grokbot: pinning api2.cursor.sh (sudo)…');
96
- if (!isBlocked()) blockBackend();
97
- startCursorBackend({ port: 443, log: (m) => console.error(` backend: ${m}`) });
98
-
99
- const restore = () => { try { unblockBackend(); console.error('openzoo: /etc/hosts restored.'); } catch { /* */ } };
100
- process.on('SIGINT', () => { restore(); process.exit(0); });
101
- process.on('exit', restore);
102
-
103
- // 3) launch the app directly (open -a can't pass env) with Node TLS override
104
- console.error('openzoo grokbot: launching Grok Bot pointed at your box…');
105
- console.error(` box proxy : ${ep.proxy}`);
106
- console.error(` agent (1337): ${ep.agent}`);
107
- console.error(` ssh : ${box.ssh || '(pending)'}`);
108
- console.error(' WATCH the box agent log for what the app sends:');
109
- console.error(` ${box.ssh ? box.ssh + " 'tail -f /var/log/openzoo/agent.log'" : '(ssh not ready yet)'}`);
110
- try { execSync('osascript -e \'tell application "Grok Bot" to quit\'', { stdio: 'ignore' }); } catch { /* */ }
111
- await new Promise((r) => setTimeout(r, 2500));
112
- spawn(`${APP}/Contents/MacOS/Grok Bot`, ['--ignore-certificate-errors'], {
113
- stdio: 'ignore', detached: true,
114
- env: { ...process.env, NODE_TLS_REJECT_UNAUTHORIZED: '0' },
115
- }).unref();
116
-
117
- console.error('openzoo grokbot: ctrl-c to stop, restore /etc/hosts, and leave the box running.');
118
- console.error(` the box keeps running until its TTL — manage with: RUNPOD_API_KEY=… openzoo grokbot (again) or the runpod console`);
119
- await new Promise(() => {});
120
- }