cortad 0.3.11 → 0.4.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/README.md +13 -9
- package/lib/auto-reach.mjs +6 -5
- package/lib/contact-text.mjs +3 -4
- package/lib/env-files.mjs +0 -85
- package/lib/files.mjs +5 -0
- package/lib/fresh.mjs +1 -1
- package/lib/mcp.mjs +3 -3
- package/lib/proc.mjs +3 -1
- package/lib/proxy.mjs +14 -3
- package/lib/pyhook/sitecustomize.py +1 -500
- package/lib/reach.mjs +7 -7
- package/lib/read-text.mjs +0 -1
- package/lib/replay.mjs +6 -11
- package/lib/start.mjs +13 -34
- package/lib/switches.mjs +9 -69
- package/lib/trace.cjs +2 -320
- package/local.mjs +167 -517
- package/package.json +2 -17
- package/skill/SKILL.md +12 -10
- package/skill/references/results.md +5 -5
- package/lib/backing.mjs +0 -230
- package/lib/container.mjs +0 -60
- package/lib/data.mjs +0 -143
- package/lib/elastic.mjs +0 -88
- package/lib/hosted.mjs +0 -201
- package/lib/identities.mjs +0 -689
- package/lib/keeping.mjs +0 -181
- package/lib/ledger.mjs +0 -20
- package/lib/listing.mjs +0 -12
- package/lib/mint.mjs +0 -350
- package/lib/mongo.mjs +0 -135
- package/lib/pg.mjs +0 -118
- package/lib/qdrant.mjs +0 -48
- package/lib/redis.mjs +0 -130
- package/lib/service.mjs +0 -96
- package/lib/stores.mjs +0 -433
package/lib/replay.mjs
CHANGED
|
@@ -7,13 +7,16 @@ import { SourceMap } from "node:module";
|
|
|
7
7
|
import { tmpdir } from "node:os";
|
|
8
8
|
import { dirname, isAbsolute, join, relative } from "node:path";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
|
-
import { CODE, TEST } from "./
|
|
10
|
+
import { CODE, TEST } from "./files.mjs";
|
|
11
11
|
import { canaryOf, doorOf, doorsOf, exchangesOf, isCanary, proofOf, provingOf, sitesOf } from "./proof.mjs";
|
|
12
12
|
|
|
13
13
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
14
14
|
const HOOK = join(HERE, "trace.cjs");
|
|
15
15
|
const PYHOOK = join(HERE, "pyhook");
|
|
16
16
|
export const CAPTURED = "captured";
|
|
17
|
+
// The header a run's request carries to speak as one of the app's own callers: the role, never the
|
|
18
|
+
// token, which this machine holds and puts in (local.mjs).
|
|
19
|
+
export const AS_HEADER = "x-cortad-as";
|
|
17
20
|
// Never replayed: they describe one connection, not the caller.
|
|
18
21
|
const SECRET_HEADER = /authorization|cookie|token|secret|session|csrf|api-?key/i;
|
|
19
22
|
const HOP = /^(?:host|content-length|connection|keep-alive|transfer-encoding|upgrade|expect|te|trailer|accept-encoding|x-cortad-as|x-cortad-turn)$/i;
|
|
@@ -37,8 +40,6 @@ export function makeCapture({ work, keepSecret, onProof = () => {}, writes = nul
|
|
|
37
40
|
let newestTable = null;
|
|
38
41
|
// Each server a hooked process opened a connection to, by host and port.
|
|
39
42
|
const conns = new Map();
|
|
40
|
-
// The Qdrant collections the hook copied for a run's writes, by server and name (lib/stores.mjs drops them).
|
|
41
|
-
const copies = new Map();
|
|
42
43
|
// The model's last words and status for each of a run's turns on an endpoint the app answers
|
|
43
44
|
// later, by turn (lib/wire.cjs answeredLater).
|
|
44
45
|
const answers = new Map();
|
|
@@ -50,7 +51,7 @@ export function makeCapture({ work, keepSecret, onProof = () => {}, writes = nul
|
|
|
50
51
|
// requires, to one without.
|
|
51
52
|
const bunHook = /\s/.test(HOOK) ? (() => {
|
|
52
53
|
const dir = mkdtempSync(join(tmpdir(), "cortad-"));
|
|
53
|
-
for (const name of ["trace.cjs", "wire.cjs", "tied.cjs"
|
|
54
|
+
for (const name of ["trace.cjs", "wire.cjs", "tied.cjs"]) copyFileSync(join(HERE, name), join(dir, name));
|
|
54
55
|
return join(dir, "trace.cjs");
|
|
55
56
|
})() : HOOK;
|
|
56
57
|
// The hooks are handed to whatever is started: a Node app loads the first, a Bun app the same file
|
|
@@ -64,9 +65,6 @@ export function makeCapture({ work, keepSecret, onProof = () => {}, writes = nul
|
|
|
64
65
|
// The read's tools by name, file and function, written by the run the same way: the hook wraps
|
|
65
66
|
// those functions so a tool your own code runs is seen even when no model named it.
|
|
66
67
|
CORTAD_TOOLS_FILE: join(work, "tools.json"),
|
|
67
|
-
// The hosts a trial's outbound writes may reach; every other write made inside a trial's turn
|
|
68
|
-
// is held in the app and answered there (the person's yes, written by the command).
|
|
69
|
-
CORTAD_OUTBOUND_FILE: join(work, "outbound.json"),
|
|
70
68
|
// While a run is on, the hook records each file the app writes under its own folder, with a
|
|
71
69
|
// copy from before the first write, and the run's end puts them back (lib/writes.mjs).
|
|
72
70
|
...(writes && root ? { CORTAD_WRITES_DIR: writes, CORTAD_APP_ROOT: root } : {}),
|
|
@@ -98,7 +96,6 @@ export function makeCapture({ work, keepSecret, onProof = () => {}, writes = nul
|
|
|
98
96
|
if (Number.isInteger(row.listen)) { ports.add(row.listen); continue; }
|
|
99
97
|
if (row.routes) { const t = routeTable(row.routes); if (t) { tables.set(t.port, t); newestTable = t; } continue; }
|
|
100
98
|
if (row.conn) { const { host, port } = row.conn; if (typeof host === "string" && Number.isInteger(port)) conns.set(`${host}:${port}`, { host, port }); continue; }
|
|
101
|
-
if (row.copy) { const { base, collection, clone } = row.copy; if ([base, collection, clone].every((v) => typeof v === "string" && v)) copies.set(`${base}|${collection}`, { base, collection, clone }); continue; }
|
|
102
99
|
if (row.answer) {
|
|
103
100
|
const { turn, status, reply } = row.answer;
|
|
104
101
|
if (TURN.test(turn ?? "")) answers.set(turn, { status: Number.isInteger(status) ? status : 0, reply: typeof reply === "string" ? reply : "" });
|
|
@@ -159,10 +156,8 @@ export function makeCapture({ work, keepSecret, onProof = () => {}, writes = nul
|
|
|
159
156
|
// The app's routes as its framework holds them: the table of the process on `port`, else the
|
|
160
157
|
// newest any hooked process wrote. Empty when no hook read one.
|
|
161
158
|
registry: (port) => { poll(); return tables.get(port) ?? newestTable ?? {}; },
|
|
162
|
-
// The servers the app has connected to so far
|
|
159
|
+
// The servers the app has connected to so far: where test conversations write, said on the card.
|
|
163
160
|
connections: () => { poll(); return [...conns.values()]; },
|
|
164
|
-
// The collections the hook copied on a Qdrant server for a run's writes, to be deleted at the end.
|
|
165
|
-
copies: () => { poll(); return [...copies.values()]; },
|
|
166
161
|
// The model's last words for a run's turn on an endpoint the app answers later, once the hook
|
|
167
162
|
// has written them; null at `until`, or once `signal` aborts. `forget` drops a turn sent again.
|
|
168
163
|
answered: async (turn, until, signal) => {
|
package/lib/start.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
// How their app
|
|
2
|
-
//
|
|
1
|
+
// How their app most likely starts, and from which folder, read off the repository: printed as the
|
|
2
|
+
// one suggestion when the person gave no start command (local.mjs), never run on its own. A
|
|
3
|
+
// monorepo root has no app of its own: the app is the workspace that serves the AI.
|
|
3
4
|
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
4
5
|
import { dirname, join, relative } from "node:path";
|
|
5
6
|
|
|
@@ -120,7 +121,16 @@ const BOUND_APP = /^(app|application)\s*=\s*[\w.]+\(/m;
|
|
|
120
121
|
const IMPORTED_APP = /^from\s+[\w.]+\s+import\s+(?:\(\s*[^)]*?\b(app|application)\b[^)]*\)|[^\n(]*\b(app|application)\b)/m;
|
|
121
122
|
const KNOWN = ["package.json", "pyproject.toml", "requirements.txt", "manage.py", "Pipfile", "uv.lock"];
|
|
122
123
|
// A folder whose app the hooks can load into: Node, Bun or Python. Any other is seen through lib/proxy.mjs.
|
|
123
|
-
|
|
124
|
+
// Whether the hook can ride into the process the command starts: a Node, Bun or Python process takes it
|
|
125
|
+
// from its environment whatever folder it runs in; a container, a Go or Java binary does not, and is
|
|
126
|
+
// watched from outside (lib/proxy.mjs). Judged by the command when there is one, else by the folder.
|
|
127
|
+
const HOOKED = /^(?:python3?(?:\.\d+)?|node|nodemon|tsx|ts-node|bun|bunx|deno|npm|npx|pnpm|yarn|uvicorn|gunicorn|hypercorn|flask|fastapi|poetry|uv|pipenv)$/;
|
|
128
|
+
// The program a command runs, past any PORT=3000 in front of it and the quotes or folder around it.
|
|
129
|
+
const runtimeOf = (cmd) => {
|
|
130
|
+
const words = cmd.trim().split(/\s+/).filter((w) => !/^[A-Za-z_][A-Za-z0-9_]*=/.test(w));
|
|
131
|
+
return (words[0] ?? "").replace(/^["']|["']$/g, "").split("/").pop() ?? "";
|
|
132
|
+
};
|
|
133
|
+
export const hookable = (dir, cmd = "") => (cmd ? HOOKED.test(runtimeOf(cmd)) : KNOWN.some((f) => existsSync(join(dir, f))));
|
|
124
134
|
|
|
125
135
|
const startOf = (dir, onPath) => nodeStart(dir, onPath) ?? pythonStart(dir, onPath);
|
|
126
136
|
|
|
@@ -209,37 +219,6 @@ export function startPlan({ root, typed, onPath }) {
|
|
|
209
219
|
return serving ? { ...at(serving), unsure: true } : { cmd: null, cwd: root, noServer: true };
|
|
210
220
|
}
|
|
211
221
|
|
|
212
|
-
// What an app says when its dependencies are not installed on this machine, in every runtime we
|
|
213
|
-
// start, and the name of the one it named first.
|
|
214
|
-
const MISSING = /Cannot find module ['"]?([@\w./-]+)|Cannot find package ['"]?([@\w./-]+)|ModuleNotFoundError: No module named ['"]?([\w.]+)|ImportError: No module named ['"]?([\w.]+)|(?:^|[:/ ])([\w.-]+): (?:command )?not found|command not found: ?([\w.-]+)/m;
|
|
215
|
-
export const missingDependency = (said) => MISSING.exec(String(said ?? ""))?.slice(1).find(Boolean) ?? "";
|
|
216
|
-
|
|
217
|
-
// One install, with the manager the project locked. Never a global one: a Python project without an
|
|
218
|
-
// interpreter of its own gets a virtual environment beside its code, so nothing installed here
|
|
219
|
-
// reaches the rest of the machine.
|
|
220
|
-
export function installPlan(dir, onPath, root = dir) {
|
|
221
|
-
if (json(join(dir, "package.json"))) {
|
|
222
|
-
// A workspace member holds no lockfile of its own: the manager, and the install, belong to the
|
|
223
|
-
// repository that owns it. novel's apps/web installed with npm here, and npm cannot read the
|
|
224
|
-
// `workspace:^` ranges its own packages are pinned with: "Unsupported URL Type".
|
|
225
|
-
const at = lockRoot(dir, root);
|
|
226
|
-
// The manager the repository locked, fetched for the job when this machine has not got it: npm
|
|
227
|
-
// refuses vercel's ai-chatbot outright over a peer range pnpm resolves without a word.
|
|
228
|
-
const locked = manager(at);
|
|
229
|
-
const where = at === dir ? "" : `cd ${JSON.stringify(at)} && `;
|
|
230
|
-
if (locked !== "npm") return `${where}${onPath(locked) ? `${locked} install` : `npx --yes ${locked} install`}`;
|
|
231
|
-
return `${where}${existsSync(join(at, "package-lock.json")) ? "npm ci || npm install --legacy-peer-deps" : "npm install || npm install --legacy-peer-deps"}`;
|
|
232
|
-
}
|
|
233
|
-
if (existsSync(join(dir, "uv.lock")) && onPath("uv")) return "uv sync";
|
|
234
|
-
if (existsSync(join(dir, "poetry.lock")) && onPath("poetry")) return "poetry install";
|
|
235
|
-
const venv = [".venv/bin/python", "venv/bin/python", "env/bin/python"].map((p) => join(dir, p)).find(existsSync);
|
|
236
|
-
const py = venv ? JSON.stringify(venv) : null;
|
|
237
|
-
const into = (what) => (py ? `${py} -m pip install ${what}` : `python3 -m venv .venv && .venv/bin/python -m pip install --upgrade pip && .venv/bin/python -m pip install ${what}`);
|
|
238
|
-
if (existsSync(join(dir, "requirements.txt"))) return into("-r requirements.txt");
|
|
239
|
-
if (existsSync(join(dir, "pyproject.toml"))) return into("-e .");
|
|
240
|
-
return null;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
222
|
// The start command as it leaves this machine: a value typed into it (DATABASE_URL=...) stays here
|
|
244
223
|
// unless it is a plain word, and it is cut to the length the server keeps. A command past that
|
|
245
224
|
// length was refused whole, as "port is required", with a valid port beside it.
|
package/lib/switches.mjs
CHANGED
|
@@ -1,83 +1,23 @@
|
|
|
1
|
-
//
|
|
2
|
-
// nothing else
|
|
3
|
-
//
|
|
1
|
+
// Your app's own request limits, raised for the session of the app this command started, and for
|
|
2
|
+
// nothing else: a run asks in twenty minutes what a person asks in a month, and a limiter that fires
|
|
3
|
+
// answers instead of your AI. Only a limit the code reads, by its name; the customer's files are never
|
|
4
|
+
// written, and every name is printed before the app starts. A sign-in switch, a lockout, an attempt
|
|
5
|
+
// counter and a password rule are never touched: who the app lets in is the app's own business.
|
|
4
6
|
import { readFileSync, statSync } from "node:fs";
|
|
5
7
|
|
|
6
|
-
const GUARDED = /(AUTH|LOGIN|PASSWORD|BREAKER|LOCKOUT|ATTEMPT|FAIL|BAN|BLOCK)/;
|
|
7
8
|
const ENV_READ = /(?:process\.env(?:\.|\[\s*['"])|os\.(?:environ\.get|getenv)\(\s*['"]|os\.environ\[\s*['"]|\benv\(\s*['"]|Deno\.env\.get\(\s*['"])([A-Z][A-Z0-9_]*)/g;
|
|
8
|
-
|
|
9
|
-
// A switch their own app reads to decide whether a caller has to be signed in at all. morphic's
|
|
10
|
-
// chat answers 401 to everyone until ENABLE_GUEST_CHAT is true; open-webui, librechat and a dozen
|
|
11
|
-
// others carry the same idea under their own name. Set for the app this command started and for
|
|
12
|
-
// nothing else: their files are not touched, and the name is printed before anything starts.
|
|
13
|
-
// A lockout, an attempt counter and a password rule carry the same words and are never touched.
|
|
14
|
-
const OPENS_AUTH = /^(?:ENABLE|REQUIRE)_AUTH(?:ENTICATION)?$|^AUTH(?:ENTICATION)?_(?:ENABLED|REQUIRED)$|^(?:REQUIRE|ENABLE)_(?:LOGIN|SIGN_?IN)$/;
|
|
15
|
-
const CLOSES_AUTH = /^(?:DISABLE|SKIP|NO)_AUTH(?:ENTICATION)?$|^AUTH(?:ENTICATION)?_DISABLED$/;
|
|
16
|
-
const OPENS_GUEST = /^(?:ENABLE|ALLOW)_(?:GUEST|ANONYMOUS|PUBLIC)(?:_[A-Z0-9_]+)?$|^(?:GUEST|ANONYMOUS|PUBLIC)_(?:MODE|ACCESS|CHAT|ENABLED|LOGIN)$/;
|
|
17
|
-
const CLOSES_GUEST = /^(?:DISABLE|BLOCK)_(?:GUEST|ANONYMOUS|PUBLIC)(?:_[A-Z0-9_]+)?$|^(?:GUEST|ANONYMOUS|PUBLIC)_(?:MODE|ACCESS|CHAT)_DISABLED$/;
|
|
18
|
-
const BOOLEAN = /^(?:true|false|1|0|yes|no|on|off)$/i;
|
|
19
|
-
export function openSwitches(envFiles, sources = []) {
|
|
20
|
-
const out = {};
|
|
21
|
-
const open = (name) => {
|
|
22
|
-
if (GUARDED.test(name) && !OPENS_AUTH.test(name) && !CLOSES_AUTH.test(name)) return null;
|
|
23
|
-
if (OPENS_AUTH.test(name) || CLOSES_GUEST.test(name)) return "false";
|
|
24
|
-
if (CLOSES_AUTH.test(name) || OPENS_GUEST.test(name)) return "true";
|
|
25
|
-
return null;
|
|
26
|
-
};
|
|
27
|
-
for (const file of envFiles) {
|
|
28
|
-
let text = "";
|
|
29
|
-
try { text = readFileSync(file, "utf8"); } catch { continue; }
|
|
30
|
-
for (const line of text.split("\n")) {
|
|
31
|
-
const m = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/.exec(line);
|
|
32
|
-
if (!m) continue;
|
|
33
|
-
const value = m[2].trim().replace(/^(['"])(.*)\1$/, "$2");
|
|
34
|
-
const to = open(m[1]);
|
|
35
|
-
// Only a switch: a name of this shape holding a URL or a key is something else entirely.
|
|
36
|
-
if (to && (value === "" || BOOLEAN.test(value))) out[m[1]] = to;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
for (const file of sources) {
|
|
40
|
-
if (!/\.(?:[cm]?[jt]sx?|py|go|rb|php|rs)$/.test(file)) continue;
|
|
41
|
-
let text = "";
|
|
42
|
-
try { if (statSync(file).size > 512_000) continue; text = readFileSync(file, "utf8"); } catch { continue; }
|
|
43
|
-
for (const m of text.matchAll(ENV_READ)) { const to = open(m[1]); if (to && !(m[1] in out)) out[m[1]] = to; }
|
|
44
|
-
}
|
|
45
|
-
return out;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Your app's own request limits, raised for this session only, the way the sandbox raises them:
|
|
49
|
-
// a run asks in twenty minutes what a person asks in a month, and a limiter that fires answers
|
|
50
|
-
// instead of your AI. Numeric values under a limit-shaped name; switches and guards are left alone.
|
|
51
9
|
const THROUGHPUT = /(RATE_?LIMIT|DAILY_LIMIT|HOURLY_LIMIT|MINUTE_LIMIT|REQUESTS_PER|TOKEN_BUDGET|MAX_SSE|MAX_CONCURRENT|THROTTLE|_RPM$|_RPS$|_QPS$)/;
|
|
10
|
+
const GUARDED = /(AUTH|LOGIN|PASSWORD|BREAKER|LOCKOUT|ATTEMPT|FAIL|BAN|BLOCK)/;
|
|
52
11
|
const SWITCH = /_(?:ENABLED|DISABLED|ENABLE|DISABLE)$|^(?:ENABLE|DISABLE)_/;
|
|
53
12
|
const lifted = (name) => (/(TOKEN_BUDGET|TOKENS)/.test(name) ? "1000000000" : "1000000");
|
|
54
13
|
const liftable = (name) => THROUGHPUT.test(name) && !GUARDED.test(name) && !SWITCH.test(name);
|
|
55
|
-
export function liftedLimits(
|
|
56
|
-
|
|
57
|
-
// naming it as one raised was the tell an engineer caught first.
|
|
58
|
-
const read = new Set();
|
|
14
|
+
export function liftedLimits(sources = []) {
|
|
15
|
+
const out = {};
|
|
59
16
|
for (const file of sources) {
|
|
60
17
|
if (!/\.(?:[cm]?[jt]sx?|py|go|rb|php|rs)$/.test(file)) continue;
|
|
61
18
|
let text = "";
|
|
62
19
|
try { if (statSync(file).size > 512_000) continue; text = readFileSync(file, "utf8"); } catch { continue; }
|
|
63
|
-
for (const m of text.matchAll(ENV_READ)) if (liftable(m[1]))
|
|
20
|
+
for (const m of text.matchAll(ENV_READ)) if (liftable(m[1])) out[m[1]] = lifted(m[1]);
|
|
64
21
|
}
|
|
65
|
-
const out = {};
|
|
66
|
-
for (const file of envFiles) {
|
|
67
|
-
let text = "";
|
|
68
|
-
try { text = readFileSync(file, "utf8"); } catch { continue; }
|
|
69
|
-
for (const line of text.split("\n")) {
|
|
70
|
-
const m = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/.exec(line);
|
|
71
|
-
if (!m) continue;
|
|
72
|
-
const [, name, raw] = m;
|
|
73
|
-
const value = raw.trim().replace(/^(['"])(.*)\1$/, "$2");
|
|
74
|
-
if (liftable(name) && /^\d+$/.test(value) && (!sources.length || read.has(name))) out[name] = lifted(name);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
for (const name of read) if (!(name in out)) out[name] = lifted(name);
|
|
78
22
|
return out;
|
|
79
23
|
}
|
|
80
|
-
|
|
81
|
-
// What the app is started with beyond its own env: nothing until the person said yes, in this
|
|
82
|
-
// terminal or in the browser (setup.consent.limits). A guard of theirs stays on by default.
|
|
83
|
-
export const sessionLifts = (yes, envFiles, sources = []) => (yes ? { ...liftedLimits(envFiles, sources), ...openSwitches(envFiles, sources) } : {});
|
package/lib/trace.cjs
CHANGED
|
@@ -17,7 +17,7 @@ if (FILE) {
|
|
|
17
17
|
const http = require("node:http");
|
|
18
18
|
const https = require("node:https");
|
|
19
19
|
const als = new AsyncLocalStorage();
|
|
20
|
-
const { MAX, REPLY_MAX, EMBEDDING, TOOL_TEXT, toolText, scrubbed, list, clipped, argsText, turnOf, isModelCall, isRetrieval,
|
|
20
|
+
const { MAX, REPLY_MAX, EMBEDDING, TOOL_TEXT, toolText, scrubbed, list, clipped, argsText, turnOf, isModelCall, isRetrieval, passagesFrom, decoded, cookieNames, makeRecorder, asChat, GATEWAY_MODEL } = require("./wire.cjs");
|
|
21
21
|
const row = (value) => { try { fs.appendFileSync(FILE, JSON.stringify(scrubbed(value)) + "\n", { mode: 0o600 }); } catch { /* the command is gone */ } };
|
|
22
22
|
// What the app writes into its own folder while a run is on: each file copied once before its
|
|
23
23
|
// first write, so the command puts every one back when the run ends (lib/writes.mjs). Trials
|
|
@@ -313,13 +313,10 @@ if (FILE) {
|
|
|
313
313
|
recheck = false;
|
|
314
314
|
setImmediate(() => { try { if (table().routes.length !== routesSaid) sayRoutes(lastPort); } catch { /* as above */ } });
|
|
315
315
|
};
|
|
316
|
-
// When the run last sent a request: a write made outside every request (a queue's worker, a
|
|
317
|
-
// timer) is held while a run is live, since it may be carrying out a trial's ask.
|
|
318
|
-
let lastTurnAt = -Infinity;
|
|
319
316
|
const { tied, requestCtx, note, ended, meter } = makeRecorder({
|
|
320
317
|
row,
|
|
321
318
|
rulesFile: process.env.CORTAD_RULES_FILE,
|
|
322
|
-
onTurn: () => {
|
|
319
|
+
onTurn: () => {},
|
|
323
320
|
onEnded: (ctx, steps) => writtenAt(ctx, steps),
|
|
324
321
|
});
|
|
325
322
|
// Where in the app's own code each part of a reply was written: the app's line on the stack of the
|
|
@@ -419,7 +416,6 @@ if (FILE) {
|
|
|
419
416
|
// context a model call is pinned by: it can only be the step an answer comes on.
|
|
420
417
|
const emit = http.Server.prototype.emit;
|
|
421
418
|
http.Server.prototype.emit = function (type, req, ...rest) {
|
|
422
|
-
if (this[HELD]) return emit.call(this, type, req, ...rest);
|
|
423
419
|
if (type === "listening") { try { listening(this.address()?.port); } catch { /* not a TCP server */ } }
|
|
424
420
|
if (type === "request") { recheckRoutes(); sweepTools(); }
|
|
425
421
|
if (type !== "request" || !req || !req.method || /^(?:HEAD|OPTIONS)$/.test(req.method)) return emit.call(this, type, req, ...rest);
|
|
@@ -735,327 +731,16 @@ if (FILE) {
|
|
|
735
731
|
return socketConnect.apply(this, args);
|
|
736
732
|
};
|
|
737
733
|
|
|
738
|
-
// Outbound writes a trial makes are held in the app, never sent: a trial that books, charges,
|
|
739
|
-
// emails or deletes would otherwise do it for real. Held: a call made inside a request the run
|
|
740
|
-
// tagged, with a writing method, to a host that is not a model, a retrieval, this machine, a copy
|
|
741
|
-
// of ours or a host the person said yes to. A call carrying a provider's test key goes to that
|
|
742
|
-
// provider's own sandbox as sent. The app is answered with a success shaped like what it sent,
|
|
743
|
-
// and the run is told by a dep row with `held` and the clipped call.
|
|
744
|
-
const OUTBOUND = process.env.CORTAD_OUTBOUND_FILE;
|
|
745
|
-
let passed = [], passedAt = -1;
|
|
746
|
-
const passNow = () => {
|
|
747
|
-
try {
|
|
748
|
-
const at = fs.statSync(OUTBOUND).mtimeMs;
|
|
749
|
-
if (at !== passedAt) { passedAt = at; const got = JSON.parse(fs.readFileSync(OUTBOUND, "utf8")); passed = Array.isArray(got.pass) ? got.pass.filter((h) => typeof h === "string" && h) : []; }
|
|
750
|
-
} catch { /* not written yet: nothing passes */ }
|
|
751
|
-
return passed;
|
|
752
|
-
};
|
|
753
|
-
const WRITES = /^(?:POST|PUT|PATCH|DELETE)$/;
|
|
754
|
-
const THIS_MACHINE = /^(?:localhost|127(?:\.\d+){3}|\[?::1\]?|0\.0\.0\.0)$/i;
|
|
755
|
-
// A provider's own sandbox, named so in the host: api.sandbox.paypal.com, sandbox-api.polar.sh.
|
|
756
|
-
const SANDBOX_HOST = /(?:^|[.-])(?:sandbox|sbox)[.-]/i;
|
|
757
|
-
// ponytail: a sign-in's token refresh is a POST that changes nothing, told apart by its path only.
|
|
758
|
-
const TOKEN_PATH = /\/(?:oauth2?\/)?token(?:[/?]|$)/i;
|
|
759
|
-
// An API where every call is a POST names what a call does in its last segment, as
|
|
760
|
-
// resource.verb_words (customers.get, billing.preview_attach) or as a gRPC or Connect method
|
|
761
|
-
// (acme.v1.UserService/GetUser). A POST so named that starts with a reading word, and holds no word
|
|
762
|
-
// that makes the read a write (get_or_create, check_in, GetLease), goes out.
|
|
763
|
-
// A preview commits nothing whatever it previews; any other reading word reads only while no word
|
|
764
|
-
// after it writes: credits.check_deduct and usage.count_increment went out as reads.
|
|
765
|
-
// ponytail: decided on the name alone, so a POST named as a plain read whose body writes goes out.
|
|
766
|
-
// A rule on the body is the upgrade if one is ever seen.
|
|
767
|
-
const WRITE_WORD = "create|update|delete|remove|set|add|put|insert|upsert|write|save|store|record|track|log|send|deduct|increment|decrement|consume|use|spend|charge|pay|refund|cancel|reserve|lock|unlock|commit|apply|attach|detach|start|stop|run|trigger|reset|rotate|revoke|grant|issue|mint|transfer|move|merge|sync|import|export|upload|publish|enqueue|push|pop|ack|touch|mark|close|approve|reject|submit|confirm|redeem|claim";
|
|
768
|
-
const DOT_CALL = /^[A-Za-z][\w-]*\.[a-z][a-z_]*$/, DOT_READ = /^(?:get|list|check|search|query|find|lookup|retrieve|describe|count|preview)$/, DOT_NOT = new RegExp(`^(?:or|and|in|out|next|${WRITE_WORD})$`);
|
|
769
|
-
const RPC_CALL = /^(?:[A-Z][a-z0-9]*)+$/, RPC_READ = /^(?:Get|List|Search|Query|Find|Lookup|Describe)$/, RPC_NOT = new RegExp(`^(?:or|and|next|lease|${WRITE_WORD})$`, "i");
|
|
770
|
-
const readWords = (words, read, not) => read.test(words[0]) && (/^preview$/i.test(words[0]) || !words.slice(1).some((w) => not.test(w)));
|
|
771
|
-
const readsByName = (method, pathname) => {
|
|
772
|
-
if (method !== "POST") return false;
|
|
773
|
-
const parts = pathname.split("/").filter(Boolean);
|
|
774
|
-
const last = parts[parts.length - 1] || "", before = parts[parts.length - 2] || "";
|
|
775
|
-
if (DOT_CALL.test(last)) return readWords(last.split(".")[1].split("_"), DOT_READ, DOT_NOT);
|
|
776
|
-
return /\.[A-Z]\w*$/.test(before) && RPC_CALL.test(last) && readWords(last.split(/(?=[A-Z])/), RPC_READ, RPC_NOT);
|
|
777
|
-
};
|
|
778
|
-
// A provider's test-mode key says so in the prefix of the key itself, set off by underscores
|
|
779
|
-
// (Stripe's sk_test_, am_sk_test_, pdl_sdbx_), or it is a value of a setting the app keeps as a
|
|
780
|
-
// test one. The word elsewhere is not it: a Basic user named test-admin and a live key holding
|
|
781
|
-
// "-Test-" both went out as test keys.
|
|
782
|
-
const TEST_KEY = /^(?:[a-z0-9]{2,8}_){0,3}(?:test|sandbox|sbox|sdbx)_[\w-]{4,}$/i;
|
|
783
|
-
const testKey = (auth) => Boolean(auth) && (auth.split(/[\s:]+/).some((piece) => TEST_KEY.test(piece))
|
|
784
|
-
|| Object.entries(process.env).some(([k, v]) => /(?:^|_)TEST(?:_|$)/i.test(k) && v && v.length >= 8 && auth.includes(v)));
|
|
785
|
-
const authText = (value) => { const a = String(value || ""); const m = /^Basic\s+(\S+)$/i.exec(a); return m ? Buffer.from(m[1], "base64").toString("utf8") : a; };
|
|
786
|
-
// The headers a key travels in.
|
|
787
|
-
const KEY_HEADERS = ["authorization", "x-api-key", "api-key", "apikey", "x-auth-token"];
|
|
788
|
-
// Neon's HTTP driver sends every query, reads too, to its region's API host and names the
|
|
789
|
-
// database in a header: the call is decided on that database's host. No header: the API host.
|
|
790
|
-
const NEON_SQL = /^api(?:auth)?\.[a-z0-9.-]+\.neon\.tech$/;
|
|
791
|
-
const targetOf = (u, headerOf) => {
|
|
792
|
-
const host = u.hostname.toLowerCase();
|
|
793
|
-
if (!NEON_SQL.test(host) || u.pathname !== "/sql") return host;
|
|
794
|
-
try { return new URL(headerOf("neon-connection-string")).hostname.toLowerCase() || host; } catch { return host; }
|
|
795
|
-
};
|
|
796
|
-
// `headerOf(name)`: one of the call's headers, read only for a call that is otherwise held. Once a
|
|
797
|
-
// tagged write is being decided, a failure to decide holds it.
|
|
798
|
-
// ponytail: while a run is live, a person's own queued write is held too; per-ask pinning of
|
|
799
|
-
// background writes is the upgrade when that matters.
|
|
800
|
-
const RUN_LIVE_MS = 10 * 60_000;
|
|
801
|
-
// The copies this hook makes of Qdrant collections carry this suffix (copyOf below).
|
|
802
|
-
const OUR_COPY = /_cortad_\d+$/;
|
|
803
|
-
const heldHere = (u, method, headerOf) => {
|
|
804
|
-
const ctx = als.getStore();
|
|
805
|
-
if (!WRITES.test(method) || (ctx ? !ctx.turn : Date.now() - lastTurnAt > RUN_LIVE_MS)) return false;
|
|
806
|
-
// A read of a Qdrant collection, however its server is named (a bare address holds no word
|
|
807
|
-
// like qdrant), and any call to a copy this hook made, go out.
|
|
808
|
-
const q = qdrantCall(u, method);
|
|
809
|
-
if (q && (!q.write || (q.collection && OUR_COPY.test(q.collection)))) return false;
|
|
810
|
-
try {
|
|
811
|
-
const host = targetOf(u, headerOf);
|
|
812
|
-
if (THIS_MACHINE.test(host) || SANDBOX_HOST.test(host) || isModelCall(u.host, u.pathname) || isRetrieval(u) || TOKEN_PATH.test(u.pathname) || readsByName(method, u.pathname) || KEY_HEADERS.some((name) => testKey(authText(headerOf(name))))) return false;
|
|
813
|
-
return !passNow().some((p) => host === p || host.endsWith("." + p));
|
|
814
|
-
} catch { return true; }
|
|
815
|
-
};
|
|
816
|
-
// A write to a Qdrant collection made inside a run goes to a copy of that collection on the same
|
|
817
|
-
// server, made at the first such write: the collection is read page by page and written under
|
|
818
|
-
// `<name>_cortad_<pid>`, and from then on every call to that collection, reads too, goes to the
|
|
819
|
-
// copy, so the app finds what it wrote. The original is never written. A copy that cannot be
|
|
820
|
-
// made (a collection over the size a run copies, a server that refused) leaves the write held
|
|
821
|
-
// like any other. Each copy is reported as a `copy` row, and the command deletes them when it
|
|
822
|
-
// ends. A copy of ours is never copied again. ulaim's hosted Qdrant held Run for the person's
|
|
823
|
-
// yes for seventeen minutes; this is what the yes stood in for.
|
|
824
|
-
const copies = new Map();
|
|
825
|
-
// What one process copies at most: collections, and points across them. Past it a write is held
|
|
826
|
-
// like any other, and the app's output says so once. A run is a few conversations' writes, never
|
|
827
|
-
// a cluster's.
|
|
828
|
-
const COPIES_MAX = 8;
|
|
829
|
-
const POINTS_MAX = 300_000;
|
|
830
|
-
let copied = 0, pointsCopied = 0;
|
|
831
|
-
const nativeFetch = typeof globalThis.fetch === "function" ? globalThis.fetch : null;
|
|
832
|
-
let qdrantLib = null;
|
|
833
|
-
const qdrantMod = () => (qdrantLib ??= import(require("node:url").pathToFileURL(require("node:path").join(__dirname, "qdrant.mjs")).href));
|
|
834
|
-
const inRun = () => { const ctx = als.getStore(); return ctx ? Boolean(ctx.turn) : Date.now() - lastTurnAt <= RUN_LIVE_MS; };
|
|
835
|
-
// What to do with a call: null to leave it alone; `hold` for a write of the server itself
|
|
836
|
-
// (/collections/aliases); else the collection's copy when there is one, or none yet for a write
|
|
837
|
-
// in a run, which makes it.
|
|
838
|
-
const guarded = (u, method) => {
|
|
839
|
-
if (!/^https?:$/.test(u.protocol)) return null;
|
|
840
|
-
const call = qdrantCall(u, method);
|
|
841
|
-
if (!call) return null;
|
|
842
|
-
if (!call.collection) return call.write && inRun() ? { hold: true } : null;
|
|
843
|
-
// The person's own calls, outside a run, go where they were sent: their writes are theirs to keep.
|
|
844
|
-
if (OUR_COPY.test(call.collection) || !inRun()) return null;
|
|
845
|
-
const key = `${u.host}|${call.collection}`;
|
|
846
|
-
const copy = copies.get(key);
|
|
847
|
-
if (copy) return { key, call, copy };
|
|
848
|
-
if (!call.write) return null;
|
|
849
|
-
// A change to the collection's shape, or a write past this process's copy budget, is held.
|
|
850
|
-
if (call.kind === "schema") return { hold: true };
|
|
851
|
-
if (copied >= COPIES_MAX || pointsCopied >= POINTS_MAX) { copySay(u.host, call.collection, `this run already copied ${copied} collections, ${pointsCopied} points`); return { hold: true }; }
|
|
852
|
-
return { key, call, copy: null };
|
|
853
|
-
};
|
|
854
|
-
// Said once per collection in the app's own output when no copy could be made and the write was
|
|
855
|
-
// kept on this machine instead, with the reason the server gave.
|
|
856
|
-
const copySaid = new Set();
|
|
857
|
-
const copySay = (host, collection, why) => {
|
|
858
|
-
if (copySaid.has(`${host}|${collection}`)) return;
|
|
859
|
-
copySaid.add(`${host}|${collection}`);
|
|
860
|
-
try { process.stderr.write(`cortad: a request Cortad sent made this app write into the Qdrant collection ${collection} at ${host}. Cortad could not copy that collection on its server (${why}), so the write was kept on this machine and answered with a stand-in success.\n`); } catch { /* no stderr */ }
|
|
861
|
-
};
|
|
862
|
-
const toCopy = (u, clone, call) => { const t = new URL(u.href); t.pathname = `/collections/${encodeURIComponent(clone)}${call.rest}`; return t; };
|
|
863
|
-
// The copy's name, made now when none exists; null when none could be made. The key the app sent
|
|
864
|
-
// is the key the copy is made with, and never leaves this process.
|
|
865
|
-
const copyOf = (u, headerOf, g) => {
|
|
866
|
-
const have = copies.get(g.key);
|
|
867
|
-
if (have) return Promise.resolve(have);
|
|
868
|
-
if (!nativeFetch) return Promise.resolve(null);
|
|
869
|
-
const clone = `${g.call.collection.slice(0, 40)}_cortad_${process.pid}`;
|
|
870
|
-
const bearer = /^Bearer\s+(\S+)/i.exec(headerOf("authorization") || "");
|
|
871
|
-
const conn = { base: `${u.protocol}//${u.host}`, apiKey: headerOf("api-key") || (bearer ? bearer[1] : ""), fetch: nativeFetch };
|
|
872
|
-
const made = qdrantMod().then(({ cloneQdrant }) => cloneQdrant(conn, g.call.collection, clone)).then((got) => {
|
|
873
|
-
if (!got || got.why) { copies.delete(g.key); copySay(u.host, g.call.collection, got ? got.why : "nothing"); return null; }
|
|
874
|
-
copied += 1; pointsCopied += got.points ?? 0;
|
|
875
|
-
copies.set(g.key, clone);
|
|
876
|
-
row({ copy: { host: u.hostname, base: conn.base, collection: g.call.collection, clone } });
|
|
877
|
-
return clone;
|
|
878
|
-
}, (e) => { copies.delete(g.key); copySay(u.host, g.call.collection, e && e.message ? e.message : "error"); return null; });
|
|
879
|
-
copies.set(g.key, made);
|
|
880
|
-
return made;
|
|
881
|
-
};
|
|
882
|
-
// A GraphQL query reads; a mutation writes. A `query` in JSON that is not GraphQL (Neon's SQL, where
|
|
883
|
-
// a SELECT can write through a function) is held.
|
|
884
|
-
// ClickHouse, Trino and Presto take every query as a POST of its text, a SELECT too, and a SELECT
|
|
885
|
-
// there cannot write: one statement that starts as a read and holds no word that writes, sent to
|
|
886
|
-
// what its address or its headers show is one of them, goes out. Anything this cannot read as that
|
|
887
|
-
// stays held. Databuddy's agent had 45 replies come back empty while its own ClickHouse queries
|
|
888
|
-
// were answered here with a stand-in.
|
|
889
|
-
const SQL_QUOTED = /'(?:[^'\\]|\\.|'')*'|"(?:[^"\\]|\\.|"")*"|`[^`]*`|--[^\n]*|\/\*[\s\S]*?\*\//g;
|
|
890
|
-
// SHOW, DESCRIBE and EXISTS read whatever follows them. A SELECT, a WITH or an EXPLAIN reads
|
|
891
|
-
// unless it holds a word that writes inside it (a CTE that deletes, SELECT INTO, EXPLAIN ANALYZE
|
|
892
|
-
// of an insert) or hands its text on to another database to run.
|
|
893
|
-
const SQL_SHOWS = /^(?:show|describe|desc|exists)\b/i, SQL_SELECTS = /^(?:select|with|explain)\b/i;
|
|
894
|
-
const SQL_WRITES = /(?<![.\w])(?:insert|update|delete|merge|into|create|alter|drop|truncate|refresh|call|execute)(?![.\w])/i;
|
|
895
|
-
const SQL_HANDS_ON = /\b(?:table|query|jdbc|odbc|executable)\s*\(/i;
|
|
896
|
-
const statementOf = (sql) => String(sql || "").replace(SQL_QUOTED, (m) => (m[0] === "'" ? "''" : /^["`]/.test(m) ? "x" : " ")).trim().replace(/;\s*$/, "");
|
|
897
|
-
// A quote or a comment left open, a second statement or a dollar-quoted body is not read as a read.
|
|
898
|
-
const whole = (bare) => !/[;'"`]|\/\*|\$\$/.test(bare.replace(/''/g, ""));
|
|
899
|
-
const selects = (bare) => whole(bare) && (SQL_SHOWS.test(bare) || (SQL_SELECTS.test(bare) && !SQL_WRITES.test(bare) && !SQL_HANDS_ON.test(bare)));
|
|
900
|
-
const ANALYTICS_NAME = /clickhouse|trino|presto/i;
|
|
901
|
-
const ANALYTICS_HEADERS = ["x-clickhouse-user", "x-clickhouse-key", "x-clickhouse-database", "x-clickhouse-format", "x-trino-user", "x-presto-user"];
|
|
902
|
-
const analytics = (u, headerOf, port) => /^(?:8123|8443)$/.test(String(port || u.port)) || ANALYTICS_NAME.test(u.hostname) || ANALYTICS_NAME.test(headerOf("user-agent") || "")
|
|
903
|
-
|| ANALYTICS_HEADERS.some((name) => headerOf(name)) || u.pathname === "/v1/statement";
|
|
904
|
-
const readsOnly = (text, u, headerOf = () => "", port) => {
|
|
905
|
-
const sent = String(text || "").trim();
|
|
906
|
-
try { const q = JSON.parse(sent)?.query; return typeof q === "string" && /^\s*(?:\{|query\b|fragment\b)/.test(q) && !/\bmutation\b/.test(q); } catch { /* not JSON: a statement sent as text */ }
|
|
907
|
-
if (!u || !analytics(u, headerOf, port)) return false;
|
|
908
|
-
// ClickHouse joins a `query` in the address to the body: both are read as the one statement.
|
|
909
|
-
const asked = `${u.searchParams.get("query") || ""} ${sent}`;
|
|
910
|
-
// ClickHouse reads a backslash before a quote as part of the string and Trino as its end, so
|
|
911
|
-
// where a string stops is not known here: held.
|
|
912
|
-
// ponytail: a ClickHouse read with an escaped quote in a string is held too; told apart by which
|
|
913
|
-
// database answered if that is ever seen.
|
|
914
|
-
return !/\\['"`]/.test(asked) && selects(statementOf(asked));
|
|
915
|
-
};
|
|
916
|
-
// A call passed on from this hook's own server, after its body was read: never held again.
|
|
917
|
-
const PASSED = Symbol("cortad.passed");
|
|
918
|
-
const passedOn = (args) => { const o = typeof args[0] === "string" || args[0] instanceof URL ? args[1] : args[0]; return Boolean(o && o[PASSED]); };
|
|
919
|
-
let heldCount = 0;
|
|
920
|
-
const fieldsOf = (text, type) => {
|
|
921
|
-
try { const b = /form/i.test(type || "") ? Object.fromEntries(new URLSearchParams(text)) : JSON.parse(text); return b && typeof b === "object" && !Array.isArray(b) ? b : {}; } catch { return {}; }
|
|
922
|
-
};
|
|
923
|
-
const heldReply = (text, type) => JSON.stringify({ ...fieldsOf(text, type), id: `cortad-held-${++heldCount}`, status: "ok" });
|
|
924
|
-
// Said once per method and host in the app's own output, where the error that follows is read:
|
|
925
|
-
// a stand-in an SDK cannot parse otherwise reads as a bug in the app. It holds no word that marks
|
|
926
|
-
// an error, so it is never quoted back as the app's own.
|
|
927
|
-
const heldSaid = new Set();
|
|
928
|
-
const heldSay = (method, host) => {
|
|
929
|
-
if (heldSaid.has(`${method} ${host}`)) return;
|
|
930
|
-
heldSaid.add(`${method} ${host}`);
|
|
931
|
-
try { process.stderr.write(`cortad: a request Cortad sent made this app send ${method} to ${host}. Cortad kept that call on this machine and answered it with a stand-in success, since it could change something real. A failure right after this line may come from that stand-in rather than from your code. With that service's test key the call goes out as sent.\n`); } catch { /* no stderr */ }
|
|
932
|
-
};
|
|
933
|
-
// Named by method and host only: a webhook's path is its credential.
|
|
934
|
-
const heldRow = (u, method, text, type, ctx) => { heldSay(method, u.hostname); row({ dep: { at: Date.now(), host: u.hostname, status: 200, held: true, ...(ctx ? { ex: ctx.id, turn: ctx.turn } : {}), called: [{ name: `${method} ${u.hostname}`.slice(0, 80), arguments: argsText(fieldsOf(text, type)) }] } }); };
|
|
935
|
-
// http.request hands back a request before its body is written, so a held one is sent to a server
|
|
936
|
-
// of this hook's own on this machine, which answers it. Started at the first tagged request.
|
|
937
|
-
const HELD = Symbol("cortad.held");
|
|
938
|
-
const heldWaiting = new Map();
|
|
939
|
-
let heldPort = null, heldStarted = false, heldSent = 0;
|
|
940
|
-
function heldWarm() {
|
|
941
|
-
if (heldStarted) return;
|
|
942
|
-
heldStarted = true;
|
|
943
|
-
try {
|
|
944
|
-
const server = http.createServer((req, res) => {
|
|
945
|
-
const parts = [];
|
|
946
|
-
req.on("data", (c) => { if (parts.length < 256) parts.push(c); });
|
|
947
|
-
req.on("end", () => {
|
|
948
|
-
const id = String(req.headers["x-cortad-held"] || "");
|
|
949
|
-
const w = heldWaiting.get(id);
|
|
950
|
-
heldWaiting.delete(id);
|
|
951
|
-
const sent = Buffer.concat(parts);
|
|
952
|
-
const text = sent.toString("utf8").slice(0, MAX);
|
|
953
|
-
const headerOf = (name) => String(req.headers[name] || "");
|
|
954
|
-
// The call goes on to where it was sent, as it was sent, or to `to` instead, and its
|
|
955
|
-
// answer comes back through this server.
|
|
956
|
-
const forward = (to) => {
|
|
957
|
-
const { "x-cortad-held": _held, host: _host, connection: _connection, ...headers } = req.headers;
|
|
958
|
-
const given = typeof w.args[0] === "string" || w.args[0] instanceof URL;
|
|
959
|
-
const options = { ...((given ? w.args[1] : w.args[0]) || {}), headers, signal: undefined, [PASSED]: true, ...(to && !given ? { path: to.pathname + to.search } : {}) };
|
|
960
|
-
// Sent inside the request that asked, so the call is recorded behind that reply.
|
|
961
|
-
const send = () => (w.secure ? https : http).request(...(given ? [to ? to.href : w.args[0], options] : [options]));
|
|
962
|
-
const up = w.ctx ? als.run(w.ctx, send) : send();
|
|
963
|
-
up.on("response", (got) => { res.writeHead(got.statusCode || 502, got.headers); got.pipe(res); });
|
|
964
|
-
up.on("error", () => { if (!res.headersSent) res.writeHead(502); res.end(); });
|
|
965
|
-
res.on("close", () => up.destroy());
|
|
966
|
-
return up.end(sent);
|
|
967
|
-
};
|
|
968
|
-
const answer = () => {
|
|
969
|
-
if (w) heldRow(w.u, w.method, text, req.headers["content-type"], w.ctx);
|
|
970
|
-
res.writeHead(200, { "content-type": "application/json" });
|
|
971
|
-
res.end(heldReply(text, req.headers["content-type"]));
|
|
972
|
-
};
|
|
973
|
-
// A body that only reads, known now that it is here, goes on as it was sent.
|
|
974
|
-
if (w && parts.length < 256 && readsOnly(text, w.u, headerOf, w.port)) return forward(null);
|
|
975
|
-
// A call to a Qdrant collection goes to the run's copy of it, made now when this is its first write.
|
|
976
|
-
if (w && w.copy) return copyOf(w.u, headerOf, w.copy).then((clone) => (clone ? forward(toCopy(w.u, clone, w.copy.call)) : answer()), answer);
|
|
977
|
-
answer();
|
|
978
|
-
});
|
|
979
|
-
});
|
|
980
|
-
server[HELD] = true;
|
|
981
|
-
server.listen(0, "127.0.0.1", () => { heldPort = server.address().port; });
|
|
982
|
-
server.unref();
|
|
983
|
-
} catch { /* no server: a held write fails to connect instead of leaving */ }
|
|
984
|
-
}
|
|
985
|
-
const headerIn = (headers, name) => {
|
|
986
|
-
if (!headers || typeof headers !== "object" || Array.isArray(headers)) return "";
|
|
987
|
-
const k = Object.keys(headers).find((n) => n.toLowerCase() === name);
|
|
988
|
-
return k ? String(headers[k]) : "";
|
|
989
|
-
};
|
|
990
|
-
// The call http.request was asked for, when it is one to hold.
|
|
991
|
-
const heldCall = (args, secure) => {
|
|
992
|
-
const first = args[0];
|
|
993
|
-
const given = typeof first === "string" || first instanceof URL ? new URL(String(first)) : null;
|
|
994
|
-
const o = (given ? (args[1] && typeof args[1] === "object" ? args[1] : {}) : first) || {};
|
|
995
|
-
const host = String(o.hostname || (o.host && String(o.host).replace(/:\d+$/, "")) || (given && given.hostname) || "localhost");
|
|
996
|
-
const path = String(o.path || (given ? given.pathname + given.search : "/"));
|
|
997
|
-
const method = String(o.method || "GET").toUpperCase();
|
|
998
|
-
const u = new URL(`${secure ? "https" : "http"}://${host.includes(":") && !host.startsWith("[") ? `[${host}]` : host}${path.startsWith("/") ? path : "/" + path}`);
|
|
999
|
-
const headerOf = (name) => headerIn(o.headers, name) || (name === "authorization" && o.auth ? `Basic ${Buffer.from(String(o.auth)).toString("base64")}` : "");
|
|
1000
|
-
const port = o.port || (given && given.port) || "";
|
|
1001
|
-
// The server's address whole, port included, names a collection's copy; the hold is decided on
|
|
1002
|
-
// the host alone, as before.
|
|
1003
|
-
const at = new URL(u.href);
|
|
1004
|
-
if (port) at.port = String(port);
|
|
1005
|
-
const g = guarded(at, method);
|
|
1006
|
-
if (!g && !heldHere(u, method, headerOf)) return null;
|
|
1007
|
-
return { u: at, method, o, args: args.filter((a) => typeof a !== "function"), port, cb: args.find((a) => typeof a === "function"), ...(g && !g.hold ? { copy: g } : {}) };
|
|
1008
|
-
};
|
|
1009
|
-
const httpRequest = http.request;
|
|
1010
|
-
const sendHeld = ({ u, method, o, cb, args, port, copy }, secure) => {
|
|
1011
|
-
const id = String(++heldSent);
|
|
1012
|
-
heldWaiting.set(id, { u, method, ctx: als.getStore(), args, port, secure, copy });
|
|
1013
|
-
const headers = { ...(o.headers && typeof o.headers === "object" && !Array.isArray(o.headers) ? o.headers : {}), "x-cortad-held": id };
|
|
1014
|
-
// Port 1 answers nothing: a write held before the server listens fails instead of leaving.
|
|
1015
|
-
const req = httpRequest({ host: "127.0.0.1", port: heldPort || 1, method, path: u.pathname + u.search, headers, ...(o.timeout ? { timeout: o.timeout } : {}), ...(o.signal ? { signal: o.signal } : {}) }, cb);
|
|
1016
|
-
// A TLS client waits for the handshake before it writes (Stripe's does).
|
|
1017
|
-
if (secure) req.once("socket", (socket) => socket.once("connect", () => socket.emit("secureConnect")));
|
|
1018
|
-
return req;
|
|
1019
|
-
};
|
|
1020
|
-
const heldFetch = async (realFetch, input, init) => {
|
|
1021
|
-
const req = new Request(input, init);
|
|
1022
|
-
const text = await req.clone().text().catch(() => "");
|
|
1023
|
-
if (readsOnly(text, new URL(req.url), (name) => req.headers.get(name) || "")) return realFetch(req);
|
|
1024
|
-
const u = new URL(req.url);
|
|
1025
|
-
heldRow(u, req.method, text, req.headers.get("content-type"), als.getStore());
|
|
1026
|
-
return new Response(heldReply(text, req.headers.get("content-type")), { status: 200, headers: { "content-type": "application/json" } });
|
|
1027
|
-
};
|
|
1028
|
-
|
|
1029
734
|
// Outbound: the model call your app makes while it handles that request.
|
|
1030
735
|
const bodyText = (body) => (typeof body === "string" ? body : Buffer.isBuffer(body) ? body.toString("utf8") : body instanceof Uint8Array ? Buffer.from(body).toString("utf8") : "");
|
|
1031
736
|
if (typeof globalThis.fetch === "function") {
|
|
1032
737
|
const realFetch = globalThis.fetch;
|
|
1033
|
-
// A call to a Qdrant collection, sent again through this same wrapper at the copy's address, so
|
|
1034
|
-
// it is recorded like any retrieval; held when no copy could be made.
|
|
1035
|
-
const guardedFetch = async (self, wrapped, input, init, u, method, g) => {
|
|
1036
|
-
if (g.hold) return heldFetch(realFetch, input, init);
|
|
1037
|
-
const req = new Request(input, init);
|
|
1038
|
-
const clone = await (typeof g.copy === "string" ? g.copy : copyOf(u, (name) => req.headers.get(name) || "", g));
|
|
1039
|
-
if (!clone) return heldFetch(realFetch, input, init);
|
|
1040
|
-
const body = method === "GET" || method === "HEAD" ? undefined : await req.arrayBuffer();
|
|
1041
|
-
return wrapped.call(self, toCopy(u, clone, g.call).href, { method, headers: req.headers, body, signal: req.signal });
|
|
1042
|
-
};
|
|
1043
738
|
globalThis.fetch = function (input, init) {
|
|
1044
739
|
let url = null;
|
|
1045
|
-
let hold = false;
|
|
1046
|
-
let guard = null;
|
|
1047
|
-
let at = null;
|
|
1048
|
-
let method = "GET";
|
|
1049
740
|
try {
|
|
1050
741
|
const u = new URL(typeof input === "string" ? input : input && input.url ? input.url : String(input));
|
|
1051
|
-
at = u;
|
|
1052
742
|
if (isModelCall(u.host, u.pathname)) url = u;
|
|
1053
|
-
method = String((init && init.method) || (input && input.method) || "GET").toUpperCase();
|
|
1054
|
-
guard = !url ? guarded(u, method) : null;
|
|
1055
|
-
hold = !url && !guard && heldHere(u, method, (name) => new Headers((init && init.headers) || (input && input.headers) || undefined).get(name));
|
|
1056
743
|
} catch { /* not a URL we can read */ }
|
|
1057
|
-
if (guard) return guardedFetch(this, globalThis.fetch, input, init, at, method, guard);
|
|
1058
|
-
if (hold) return heldFetch(realFetch, input, init);
|
|
1059
744
|
if (!url) {
|
|
1060
745
|
// Every outbound host, named by the setting that points at it when one does, and by the
|
|
1061
746
|
// host alone otherwise: the search provider behind TAVILY_API_KEY has no URL setting.
|
|
@@ -1097,9 +782,6 @@ if (FILE) {
|
|
|
1097
782
|
for (const mod of [http, https]) {
|
|
1098
783
|
const request = mod.request;
|
|
1099
784
|
mod.request = function (...args) {
|
|
1100
|
-
let held = null;
|
|
1101
|
-
try { held = passedOn(args) ? null : heldCall(args, mod === https); } catch { /* not a call we can read: sent as asked */ }
|
|
1102
|
-
if (held) return sendHeld(held, mod === https);
|
|
1103
785
|
const req = request.apply(this, args);
|
|
1104
786
|
try {
|
|
1105
787
|
const o = typeof args[0] === "string" || args[0] instanceof URL ? new URL(String(args[0])) : args[0] || {};
|