openzoo 0.29.1 → 0.29.3
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/bin/cursor-backend.js +33 -0
- package/bin/openzoo.js +2 -1
- package/lib/hosts.js +31 -0
- package/lib/setup.js +46 -9
- package/package.json +1 -1
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Standalone impersonation backend, meant to be spawned WITH PRIVILEGE so it can
|
|
4
|
+
* bind 127.0.0.1:443 directly — the port the editor dials for api2.cursor.sh.
|
|
5
|
+
*
|
|
6
|
+
* WHY A SEPARATE ROOT PROCESS. Binding 443 needs root on unix, but the paying
|
|
7
|
+
* proxy must NOT run as root (it holds the wallet). This process is static — it
|
|
8
|
+
* serves a model catalog and empty stubs, touches no keys, no money — so running
|
|
9
|
+
* only THIS as root is the safe split. The pfctl 443->8443 redirect that would
|
|
10
|
+
* have avoided root does not deliver loopback-to-loopback on macOS (measured:
|
|
11
|
+
* self-test FAIL), so a direct bind is the reliable path.
|
|
12
|
+
*
|
|
13
|
+
* argv: <port> <models.json> <log-file>
|
|
14
|
+
*/
|
|
15
|
+
import fs from 'node:fs';
|
|
16
|
+
import { startCursorBackend } from '../lib/cursorbackend.js';
|
|
17
|
+
|
|
18
|
+
const port = Number(process.argv[2] || 443);
|
|
19
|
+
const modelsPath = process.argv[3];
|
|
20
|
+
const logPath = process.argv[4];
|
|
21
|
+
|
|
22
|
+
const log = (s) => {
|
|
23
|
+
const line = `${s}\n`;
|
|
24
|
+
try { if (logPath) fs.appendFileSync(logPath, line); } catch { /* ignore */ }
|
|
25
|
+
process.stdout.write(line);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
let models = [];
|
|
29
|
+
try { models = JSON.parse(fs.readFileSync(modelsPath, 'utf8')); }
|
|
30
|
+
catch (e) { log(`cursor-backend: could not read models (${e.message})`); process.exit(1); }
|
|
31
|
+
|
|
32
|
+
startCursorBackend({ port, models, log });
|
|
33
|
+
log(`cursor-backend: standalone up on :${port} (${models.length} models)`);
|
package/bin/openzoo.js
CHANGED
|
@@ -78,7 +78,8 @@ async function main() {
|
|
|
78
78
|
}
|
|
79
79
|
case 'unblock': {
|
|
80
80
|
const { unblockBackend, unredirect443, isBlocked } = await import('../lib/hosts.js');
|
|
81
|
-
try { unredirect443(); } catch { /* no redirect
|
|
81
|
+
try { unredirect443(); } catch { /* no redirect */ }
|
|
82
|
+
try { (await import('../lib/hosts.js')).unbindBackend443(); } catch { /* no backend */ }
|
|
82
83
|
const r = unblockBackend();
|
|
83
84
|
console.log(r.already ? 'not blocked — nothing to undo' : (isBlocked() ? 'still blocked (sudo declined?)' : 'restored: the editor can reach its own backend again'));
|
|
84
85
|
break;
|
package/lib/hosts.js
CHANGED
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
* `npx openzoo unblock` restores it. `--no-block` skips it entirely.
|
|
18
18
|
*/
|
|
19
19
|
import fs from 'node:fs';
|
|
20
|
+
import path from 'node:path';
|
|
21
|
+
import { fileURLToPath } from 'node:url';
|
|
20
22
|
import { execFileSync, spawnSync } from 'node:child_process';
|
|
21
23
|
|
|
22
24
|
/**
|
|
@@ -137,3 +139,32 @@ export function unredirect443() {
|
|
|
137
139
|
if (process.platform === 'linux') return { ok: privileged('iptables -t nat -D OUTPUT -p tcp -o lo --dport 443 -j REDIRECT --to-ports 8443 2>/dev/null; true') };
|
|
138
140
|
return { ok: false, manual: true };
|
|
139
141
|
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Bind the impersonation backend on 127.0.0.1:443 as ROOT — the reliable path
|
|
145
|
+
* after pfctl loopback redirect proved not to deliver on macOS. Spawned detached
|
|
146
|
+
* inside the SAME sudo prompt used for the hosts entry; only this static server
|
|
147
|
+
* runs privileged, never the wallet-holding proxy. unbindBackend443 kills it.
|
|
148
|
+
*/
|
|
149
|
+
export function bindBackend443(modelsPath, logPath, log = console.log) {
|
|
150
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
151
|
+
const script = path.join(here, '..', 'bin', 'cursor-backend.js');
|
|
152
|
+
const node = process.execPath;
|
|
153
|
+
if (WIN) {
|
|
154
|
+
log(' windows: run in an ADMINISTRATOR shell:');
|
|
155
|
+
log(` "${node}" "${script}" 443 "${modelsPath}" "${logPath}"`);
|
|
156
|
+
return { ok: false, manual: true };
|
|
157
|
+
}
|
|
158
|
+
// nohup + & so sudo returns immediately; the root process keeps 443 bound.
|
|
159
|
+
const ok = privileged(
|
|
160
|
+
`pkill -f 'cursor-backend.js' 2>/dev/null; `
|
|
161
|
+
+ `nohup '${node}' '${script}' 443 '${modelsPath}' '${logPath}' >/dev/null 2>&1 & `
|
|
162
|
+
+ 'sleep 1; true',
|
|
163
|
+
);
|
|
164
|
+
return { ok };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function unbindBackend443() {
|
|
168
|
+
if (WIN) return { ok: false, manual: true };
|
|
169
|
+
return { ok: privileged("pkill -f 'cursor-backend.js' 2>/dev/null; true") };
|
|
170
|
+
}
|
package/lib/setup.js
CHANGED
|
@@ -448,16 +448,53 @@ export async function setupEditor(which, target) {
|
|
|
448
448
|
// ourselves: catalog with every gate open, empty-valid for the rest. No CA
|
|
449
449
|
// install — the editor is launched with --ignore-certificate-errors below,
|
|
450
450
|
// and it does not pin certs (verified), so a self-signed cert is accepted.
|
|
451
|
-
|
|
451
|
+
// DEFAULT ON. Takeover is what makes a plan-less account route, and it is
|
|
452
|
+
// harmless on an entitled one (the editor just reads our catalog instead of
|
|
453
|
+
// theirs), so it runs unless explicitly disabled with --no-takeover.
|
|
454
|
+
const doTakeover = target0 === 'cursor' && !process.argv.includes('--no-takeover');
|
|
455
|
+
if (doTakeover) {
|
|
452
456
|
try {
|
|
453
|
-
const {
|
|
454
|
-
const {
|
|
457
|
+
const { ensureCert } = await import('./cursorbackend.js');
|
|
458
|
+
const { bindBackend443 } = await import('./hosts.js');
|
|
455
459
|
const backModels = models.map((m) => ({ name: m, label: m }));
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
console.log
|
|
459
|
-
|
|
460
|
-
|
|
460
|
+
// Cert is minted as the USER (root can still read it); models handed to the
|
|
461
|
+
// privileged listener via a temp file.
|
|
462
|
+
ensureCert(console.log);
|
|
463
|
+
const modelsFile = path.join(os.tmpdir(), 'openzoo-cursor-models.json');
|
|
464
|
+
fs.writeFileSync(modelsFile, JSON.stringify(backModels));
|
|
465
|
+
const backendLog = path.join(os.homedir(), '.openzoo', 'cursor-backend.log');
|
|
466
|
+
try { fs.writeFileSync(backendLog, ''); } catch { /* ignore */ }
|
|
467
|
+
// DIRECT 443 BIND (root) — pfctl loopback redirect measured FAIL, this is
|
|
468
|
+
// the reliable path. Only this static server runs privileged.
|
|
469
|
+
const rr = bindBackend443(modelsFile, backendLog, console.log);
|
|
470
|
+
console.log(rr.ok ? `takeover: backend bound on 127.0.0.1:443 (root); log ${backendLog}`
|
|
471
|
+
: rr.manual ? 'takeover: run the printed command in an elevated shell, then relaunch'
|
|
472
|
+
: 'takeover: could NOT bind 443 — impersonation will not receive traffic');
|
|
473
|
+
|
|
474
|
+
// SELF-TEST — prove the whole path (hosts -> redirect -> our server) works
|
|
475
|
+
// NOW, at startup, so a failure is a loud line here instead of a silent
|
|
476
|
+
// "still gated" in the editor. We dial api2.cursor.sh exactly as the editor
|
|
477
|
+
// will; if our backend logs the ClientHello and answers, it is wired.
|
|
478
|
+
await new Promise((r) => setTimeout(r, 400));
|
|
479
|
+
try {
|
|
480
|
+
const https = await import('node:https');
|
|
481
|
+
const ok = await new Promise((resolve) => {
|
|
482
|
+
const rq = https.request({
|
|
483
|
+
host: 'api2.cursor.sh', port: 443, method: 'POST',
|
|
484
|
+
path: '/aiserver.v1.AiService/AvailableModels',
|
|
485
|
+
headers: { 'content-type': 'application/grpc-web+proto' },
|
|
486
|
+
rejectUnauthorized: false, timeout: 4000,
|
|
487
|
+
}, (rp) => { rp.on('data', () => {}); rp.on('end', () => resolve(rp.statusCode === 200)); });
|
|
488
|
+
rq.on('error', () => resolve(false));
|
|
489
|
+
rq.on('timeout', () => { rq.destroy(); resolve(false); });
|
|
490
|
+
rq.end();
|
|
491
|
+
});
|
|
492
|
+
console.log(ok
|
|
493
|
+
? 'takeover: SELF-TEST PASS — api2.cursor.sh:443 reaches our backend. The editor will too.'
|
|
494
|
+
: 'takeover: SELF-TEST FAIL — api2.cursor.sh:443 did NOT reach our backend.\n'
|
|
495
|
+
+ ' The 443->8443 redirect is not delivering (pfctl/loopback). Tell me and I switch to a root-bound 443.');
|
|
496
|
+
} catch (e) { console.log(`takeover: self-test error (${e.message})`); }
|
|
497
|
+
|
|
461
498
|
console.log('takeover: launching with --ignore-certificate-errors so the self-signed cert is trusted (no CA install)');
|
|
462
499
|
} catch (e) {
|
|
463
500
|
console.log(`takeover: failed to start (${e.message}) — falling back to plain routing`);
|
|
@@ -497,7 +534,7 @@ export async function setupEditor(which, target) {
|
|
|
497
534
|
const args = [cwd];
|
|
498
535
|
// Trust our self-signed impersonation cert without any CA install — this flag
|
|
499
536
|
// is the entire reason no trust prompt is needed. Only added under --takeover.
|
|
500
|
-
if (target0 === 'cursor' && process.argv.includes('--takeover')) args.unshift('--ignore-certificate-errors');
|
|
537
|
+
if (target0 === 'cursor' && !process.argv.includes('--no-takeover')) args.unshift('--ignore-certificate-errors');
|
|
501
538
|
if (useProfile) {
|
|
502
539
|
fs.mkdirSync(PROFILE_DIR, { recursive: true });
|
|
503
540
|
args.unshift(`--user-data-dir=${PROFILE_DIR}`, `--extensions-dir=${path.join(PROFILE_DIR, 'extensions')}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openzoo",
|
|
3
|
-
"version": "0.29.
|
|
3
|
+
"version": "0.29.3",
|
|
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",
|