@hraness/xcb 0.9.1

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 (61) hide show
  1. package/LICENSE +21 -0
  2. package/MANAGED-CODEX.md +213 -0
  3. package/README.md +555 -0
  4. package/dist/accounts.d.ts +46 -0
  5. package/dist/broker-descriptors.d.ts +5 -0
  6. package/dist/broker.d.ts +62 -0
  7. package/dist/browser-session.d.ts +161 -0
  8. package/dist/canonical-json.d.ts +2 -0
  9. package/dist/capabilities.d.ts +72 -0
  10. package/dist/claude-api-models.d.ts +24 -0
  11. package/dist/claude-api-transport.d.ts +5 -0
  12. package/dist/claude-api.d.ts +23 -0
  13. package/dist/claude-credentials.d.ts +13 -0
  14. package/dist/claude-options.d.ts +10 -0
  15. package/dist/claude-sdk.d.ts +48 -0
  16. package/dist/claude-task-adapter.d.ts +65 -0
  17. package/dist/cli.js +4190 -0
  18. package/dist/codex-account-process.d.ts +149 -0
  19. package/dist/codex-account-transport.d.ts +39 -0
  20. package/dist/codex-account.d.ts +126 -0
  21. package/dist/codex-config.d.ts +53 -0
  22. package/dist/codex-host.d.ts +40 -0
  23. package/dist/codex-managed-baseline.d.ts +5 -0
  24. package/dist/codex-managed-catalog.d.ts +32 -0
  25. package/dist/codex-managed-config.d.ts +93 -0
  26. package/dist/codex-managed-ledger.d.ts +37 -0
  27. package/dist/codex-managed-session.d.ts +62 -0
  28. package/dist/codex-managed-task-adapter.d.ts +21 -0
  29. package/dist/codex-process.d.ts +67 -0
  30. package/dist/codex-protocol-manifest.d.ts +27 -0
  31. package/dist/codex-relay.d.ts +80 -0
  32. package/dist/codex-scratch.d.ts +36 -0
  33. package/dist/codex-session.d.ts +44 -0
  34. package/dist/codex-task-adapter.d.ts +24 -0
  35. package/dist/codex-task-process.d.ts +21 -0
  36. package/dist/devin-acp.d.ts +105 -0
  37. package/dist/devin-adapter.d.ts +44 -0
  38. package/dist/devin-client.d.ts +36 -0
  39. package/dist/devin-mcp.d.ts +28 -0
  40. package/dist/egress-bridge.d.ts +35 -0
  41. package/dist/egress-client.d.ts +66 -0
  42. package/dist/index-kg2gx694.js +7217 -0
  43. package/dist/index.d.ts +58 -0
  44. package/dist/index.js +3455 -0
  45. package/dist/judge.d.ts +121 -0
  46. package/dist/loopback-server.d.ts +17 -0
  47. package/dist/managed-account.d.ts +176 -0
  48. package/dist/models.d.ts +23 -0
  49. package/dist/os-sandbox.d.ts +168 -0
  50. package/dist/private-file.d.ts +215 -0
  51. package/dist/process-port.d.ts +44 -0
  52. package/dist/process-write.d.ts +13 -0
  53. package/dist/provider-process.d.ts +26 -0
  54. package/dist/public-web.d.ts +21 -0
  55. package/dist/router.d.ts +43 -0
  56. package/dist/runtime.d.ts +90 -0
  57. package/dist/sqlite-port.d.ts +29 -0
  58. package/dist/task-runtime.d.ts +150 -0
  59. package/dist/validation.d.ts +6 -0
  60. package/package.json +70 -0
  61. package/sandbox/loopback-forwarder.cjs +172 -0
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "@hraness/xcb",
3
+ "version": "0.9.1",
4
+ "description": "TypeScript SDK and compatibility CLI for xcb, which routes coding tasks across the Claude, Codex, and Devin subscriptions you already pay for.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "packageManager": "bun@1.3.14",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/hraness/xcb.git"
11
+ },
12
+ "homepage": "https://xcb.sh",
13
+ "bugs": {
14
+ "url": "https://github.com/hraness/xcb/issues"
15
+ },
16
+ "publishConfig": {
17
+ "access": "public",
18
+ "provenance": true,
19
+ "registry": "https://registry.npmjs.org"
20
+ },
21
+ "engines": {
22
+ "bun": ">=1.3.14",
23
+ "node": ">=22.13"
24
+ },
25
+ "exports": {
26
+ ".": {
27
+ "types": "./dist/index.d.ts",
28
+ "import": "./dist/index.js",
29
+ "default": "./dist/index.js"
30
+ }
31
+ },
32
+ "bin": {
33
+ "xcb-compat": "dist/cli.js"
34
+ },
35
+ "files": [
36
+ "dist",
37
+ "MANAGED-CODEX.md",
38
+ "sandbox"
39
+ ],
40
+ "scripts": {
41
+ "build:dist": "bun scripts/build-dist.ts",
42
+ "check:package": "bun scripts/package-smoke.ts",
43
+ "test": "bun test ./src ./test ./scripts",
44
+ "gobstopper-editor": "bun src/gobstopper-editor.ts",
45
+ "typecheck": "tsc --noEmit -p tsconfig.json",
46
+ "check:versions": "bun scripts/release-version-contract.ts",
47
+ "check": "bun run check:versions && bun run typecheck && bun run check:convex && bun run test && bun run build:dist && bun run check:package",
48
+ "prepack": "bun run check",
49
+ "convex:init": "bun scripts/convex-local.ts init",
50
+ "convex:dev": "bun scripts/convex-local.ts dev --tail-logs disable",
51
+ "convex:codegen": "bun scripts/convex-local.ts dev --once --tail-logs disable",
52
+ "check:convex": "tsc -p convex/tsconfig.json && bun test ./convex"
53
+ },
54
+ "dependencies": {
55
+ "@anthropic-ai/claude-agent-sdk": "0.3.278",
56
+ "@anthropic-ai/sdk": "0.127.0",
57
+ "@modelcontextprotocol/sdk": "1.30.0",
58
+ "zod": "4.6.5"
59
+ },
60
+ "devDependencies": {
61
+ "@convex-dev/auth": "0.0.95",
62
+ "@hraness/relay": "github:hraness/relay#d8251ff4b2f972c7b5694f7c583fc1a08049395a",
63
+ "@types/bun": "1.4.2",
64
+ "@types/node": "26.6.2",
65
+ "convex": "1.45.0",
66
+ "convex-test": "0.0.59",
67
+ "sigstore": "5.0.0",
68
+ "typescript": "7.0.2"
69
+ }
70
+ }
@@ -0,0 +1,172 @@
1
+ "use strict";
2
+ /** In-namespace loopback CONNECT forwarder — the stock-binary consumption
3
+ * half of `provider-tcp443-dns`. Runs inside the bwrap network namespace on a
4
+ * mounted JS runtime (node or bun); it is not a launcher and holds no
5
+ * credentials.
6
+ *
7
+ * <runtime> loopback-forwarder.cjs <bridge-socket> <port> <lo-up-cmd|-> <env-file|-> [<service|->] -- <child argv...>
8
+ *
9
+ * The optional `service` positional is `<host-unix-socket>:<port>`: the
10
+ * forwarder also binds 127.0.0.1:<port> inside the namespace and pipes each
11
+ * accepted connection verbatim to that mounted host socket — the in-namespace
12
+ * consumption path for host services (the tool relay) that are not CONNECT
13
+ * egress. `-` (or omitting the argument) disables it.
14
+ *
15
+ * It listens on 127.0.0.1:<port>, accepts only `CONNECT host:443`, forwards
16
+ * each request verbatim onto the mounted host bridge unix socket, relays the
17
+ * bridge's response head, then pipes both directions. The child is spawned
18
+ * with standard proxy variables pointing at the forwarder, so a stock binary
19
+ * (curl, a provider CLI) reaches providers through the bridge with no DNS or
20
+ * direct TCP inside the namespace. `lo-up-cmd` is a diagnostic escape hatch:
21
+ * an absolute path to `ip` runs `ip link set lo up` first; `-` skips (bwrap
22
+ * raises loopback itself on --unshare-net).
23
+ *
24
+ * `env-file` is the secret channel: bwrap `--setenv` values are visible in
25
+ * the wrapper's own argv, so the host writes `KEY=VALUE` pairs into a
26
+ * private file inside the writable scratch instead. The forwarder reads,
27
+ * validates, and deletes it before spawning, injecting the pairs only into
28
+ * the child's environment; proxy variables are forwarder-owned and always
29
+ * win over file entries.
30
+ *
31
+ * Diagnostics go to stderr as `FWD key=json` lines; the child's stdio is
32
+ * inherited verbatim. Exit status is the child's. */
33
+ const net = require("node:net");
34
+ const fs = require("node:fs");
35
+ const { spawn, spawnSync } = require("node:child_process");
36
+
37
+ // Positional layout: <socket> <port> <lo-up|-> <env-file|-> [<service|->] -- <child...>
38
+ // The service slot is optional — when the argument before `--` is absent the
39
+ // arity is the legacy five, so both forms parse.
40
+ const raw = process.argv.slice(2);
41
+ const separatorIndex = raw.indexOf("--");
42
+ const positionals = separatorIndex === -1 ? raw : raw.slice(0, separatorIndex);
43
+ const [socketPath, portText, loUpPath, envFilePath, serviceText] = positionals;
44
+ const childArgv = separatorIndex === -1 ? [] : raw.slice(separatorIndex + 1);
45
+ if (typeof socketPath !== "string" || !socketPath.startsWith("/")
46
+ || !/^[0-9]+$/.test(portText ?? "") || positionals.length < 4 || positionals.length > 5
47
+ || separatorIndex === -1 || childArgv.length === 0) {
48
+ fs.writeSync(2, "FWD usage=<bridge-socket> <port> <lo-up-cmd|-> <env-file|-> [<service|->] -- <child argv...>\n");
49
+ process.exit(2);
50
+ }
51
+ const report = (key, value) => { try { fs.writeSync(2, "FWD " + key + "=" + JSON.stringify(value) + "\n"); } catch {} };
52
+ const status = (() => { try { return fs.readFileSync("/proc/self/status", "utf8"); } catch { return ""; } })();
53
+ report("capEff", (/CapEff:\s*([0-9a-f]+)/i.exec(status) || [])[1] ?? null);
54
+ report("loOperstate", (() => { try { return fs.readFileSync("/sys/class/net/lo/operstate", "utf8").trim(); } catch { return null; } })());
55
+ report("loFlags", (() => { try { return fs.readFileSync("/sys/class/net/lo/flags", "utf8").trim(); } catch { return null; } })());
56
+ if (loUpPath !== "-") {
57
+ const raised = spawnSync(loUpPath, ["link", "set", "lo", "up"], { stdio: ["ignore", "ignore", "pipe"], timeout: 5000 });
58
+ report("loUpStatus", raised.status);
59
+ report("loUpError", raised.error ? String(raised.error.code || raised.error) : null);
60
+ report("loUpStderr", (raised.stderr || "").toString().slice(0, 200));
61
+ }
62
+
63
+ // Secret channel: bounded KEY=VALUE lines the host left inside the writable
64
+ // scratch. The file is deleted before the child spawns whether parsing
65
+ // succeeds or not — never log its contents.
66
+ const PROXY_KEYS = new Set(["http_proxy", "HTTP_PROXY", "https_proxy", "HTTPS_PROXY", "all_proxy", "ALL_PROXY", "no_proxy", "NO_PROXY"]);
67
+ const fileEnv = Object.create(null);
68
+ if (envFilePath !== "-") {
69
+ let text;
70
+ try { text = fs.readFileSync(envFilePath, "utf8"); }
71
+ catch (error) { report("envFileError", String(error && error.code || error)); process.exit(1); }
72
+ try { fs.unlinkSync(envFilePath); } catch {}
73
+ if (Buffer.byteLength(text) > 64 * 1024) { report("envFileError", "limit"); process.exit(1); }
74
+ for (const line of text.split("\n")) {
75
+ if (line === "") continue;
76
+ const eq = line.indexOf("=");
77
+ const key = eq === -1 ? "" : line.slice(0, eq);
78
+ const value = eq === -1 ? "" : line.slice(eq + 1);
79
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key) || PROXY_KEYS.has(key) || value.includes("\0")) {
80
+ report("envFileError", "invalid"); process.exit(1);
81
+ }
82
+ fileEnv[key] = value;
83
+ }
84
+ }
85
+
86
+ // Optional host-service forward: "<socket>:<port>" pipes 127.0.0.1:<port> in
87
+ // this namespace straight to the mounted unix socket — no CONNECT protocol,
88
+ // the host service owns its own admission.
89
+ let service = null;
90
+ if (serviceText !== undefined && serviceText !== "-") {
91
+ const split = serviceText.lastIndexOf(":");
92
+ const svcSocket = split === -1 ? "" : serviceText.slice(0, split);
93
+ const svcPort = split === -1 ? NaN : Number(serviceText.slice(split + 1));
94
+ if (!svcSocket.startsWith("/") || !Number.isInteger(svcPort) || svcPort < 1 || svcPort > 65535) {
95
+ report("serviceError", "invalid"); process.exit(2);
96
+ }
97
+ service = { socket: svcSocket, port: svcPort };
98
+ }
99
+ if (service !== null) {
100
+ const serviceServer = net.createServer((inbound) => {
101
+ inbound.on("error", () => {});
102
+ const upstream = net.createConnection(service.socket);
103
+ upstream.on("error", () => {});
104
+ // Pipe immediately: inbound data must not be dropped while the upstream
105
+ // unix connection is still establishing, and upstream cannot produce data
106
+ // before it connects.
107
+ inbound.pipe(upstream);
108
+ upstream.pipe(inbound);
109
+ upstream.once("error", () => inbound.destroy());
110
+ inbound.once("close", () => upstream.destroy());
111
+ });
112
+ serviceServer.on("error", (error) => { report("serviceListenError", String(error && error.code || error)); process.exit(1); });
113
+ serviceServer.listen(service.port, "127.0.0.1", () => report("serviceListening", serviceServer.address()));
114
+ }
115
+
116
+ const port = Number(portText);
117
+ const server = net.createServer((inbound) => {
118
+ // A raced write into a closing peer must never take the supervisor down —
119
+ // both sockets carry their own error path.
120
+ inbound.on("error", () => {});
121
+ let head = Buffer.alloc(0);
122
+ const onData = (chunk) => {
123
+ head = Buffer.concat([head, chunk]);
124
+ const end = head.indexOf("\r\n\r\n");
125
+ if (end === -1) { if (head.length > 4096) inbound.destroy(); return; }
126
+ // Consume exactly one head: leaving this listener attached would re-run
127
+ // CONNECT handling for every tunneled byte (a ClientHello would spawn a
128
+ // second bridge connection and inject a second "200" into the TLS stream).
129
+ inbound.removeListener("data", onData);
130
+ const request = head.subarray(0, end).toString("latin1").split("\r\n")[0];
131
+ const match = /^CONNECT ([A-Za-z0-9._-]+):443 HTTP\/1\.[01]$/.exec(request);
132
+ inbound.pause();
133
+ if (match === null) { report("refused", request.slice(0, 120)); inbound.destroy(); return; }
134
+ const upstream = net.createConnection(socketPath, () => {
135
+ upstream.on("error", () => {});
136
+ upstream.write(head.subarray(0, end + 4));
137
+ const clientExtra = head.subarray(end + 4);
138
+ let replyHead = Buffer.alloc(0);
139
+ const onReply = (replyChunk) => {
140
+ replyHead = Buffer.concat([replyHead, replyChunk]);
141
+ const replyEnd = replyHead.indexOf("\r\n\r\n");
142
+ if (replyEnd === -1) { if (replyHead.length > 4096) { upstream.destroy(); inbound.destroy(); } return; }
143
+ upstream.removeListener("data", onReply);
144
+ inbound.write(replyHead.subarray(0, replyEnd + 4));
145
+ // Reply-head tail belongs to the client; the client's own post-head
146
+ // bytes belong upstream. Keep the two directions separate.
147
+ const bridgeExtra = replyHead.subarray(replyEnd + 4);
148
+ if (bridgeExtra.length) inbound.write(bridgeExtra);
149
+ if (clientExtra.length) upstream.write(clientExtra);
150
+ upstream.pipe(inbound);
151
+ inbound.pipe(upstream);
152
+ inbound.resume();
153
+ };
154
+ upstream.on("data", onReply);
155
+ });
156
+ upstream.once("error", () => inbound.destroy());
157
+ inbound.once("close", () => upstream.destroy());
158
+ };
159
+ inbound.on("data", onData);
160
+ });
161
+ server.listen(port, "127.0.0.1", () => {
162
+ report("listening", server.address());
163
+ const proxy = "http://127.0.0.1:" + port;
164
+ const env = { ...process.env, ...fileEnv,
165
+ http_proxy: proxy, HTTP_PROXY: proxy, https_proxy: proxy, HTTPS_PROXY: proxy,
166
+ all_proxy: proxy, ALL_PROXY: proxy, no_proxy: "", NO_PROXY: "" };
167
+ const child = spawn(childArgv[0], childArgv.slice(1), { stdio: "inherit", env });
168
+ for (const signal of ["SIGTERM", "SIGINT"]) process.on(signal, () => child.kill(signal));
169
+ child.on("exit", (code, signal) => { report("childExit", { code, signal }); process.exit(code ?? 1); });
170
+ child.on("error", (error) => { report("childError", String(error)); process.exit(1); });
171
+ });
172
+ server.on("error", (error) => { report("listenError", String(error && error.code || error)); process.exit(1); });