cortad 0.3.10 → 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 +14 -7
- package/lib/auto-reach.mjs +6 -5
- package/lib/cli.mjs +4 -1
- package/lib/commits.mjs +202 -0
- package/lib/contact-text.mjs +3 -4
- package/lib/env-files.mjs +39 -0
- package/lib/env-secrets.mjs +12 -4
- package/lib/files.mjs +5 -0
- package/lib/fresh.mjs +1 -1
- package/lib/intel-text.mjs +47 -0
- package/lib/keys.mjs +10 -6
- package/lib/lock.mjs +7 -6
- package/lib/mcp.mjs +38 -5
- package/lib/proc.mjs +3 -1
- package/lib/proof.mjs +8 -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/lib/verbs.mjs +56 -1
- package/local.mjs +252 -520
- package/package.json +5 -15
- package/skill/SKILL.md +16 -9
- package/skill/references/results.md +5 -5
- package/lib/backing.mjs +0 -230
- package/lib/data.mjs +0 -137
- package/lib/hosted.mjs +0 -201
- package/lib/identities.mjs +0 -689
- package/lib/keeping.mjs +0 -174
- package/lib/ledger.mjs +0 -20
- package/lib/listing.mjs +0 -12
- package/lib/mint.mjs +0 -334
- package/lib/mongo.mjs +0 -74
- 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 -378
package/lib/reach.mjs
CHANGED
|
@@ -2,12 +2,12 @@ import { isIPv4 } from "node:net";
|
|
|
2
2
|
import { unreached } from "./contact-text.mjs";
|
|
3
3
|
import { plural, upper } from "./words.mjs";
|
|
4
4
|
|
|
5
|
-
// One request to each endpoint status lists as not reached, from this machine to the app on its port
|
|
6
|
-
// Each body is the one Cortad read that endpoint's client sending (the
|
|
7
|
-
// The connect command sends
|
|
8
|
-
// (sendOurs), signed in
|
|
9
|
-
// go from here to the app on this machine and nowhere else, and only their
|
|
10
|
-
// are real requests with real effects.
|
|
5
|
+
// One request to each endpoint status lists as not reached, from this machine to the app on its port,
|
|
6
|
+
// when the agent asks for it. Each body is the one Cortad read that endpoint's client sending (the
|
|
7
|
+
// server's `request`). The connect command sends them up to four at a time, each tagged x-cortad-turn:
|
|
8
|
+
// reach:agent:<n> (sendOurs), signed in as the person's own request was where one was seen. Headers
|
|
9
|
+
// are never the agent's: they go from here to the app on this machine and nowhere else, and only their
|
|
10
|
+
// names are printed. These are real requests with real effects.
|
|
11
11
|
|
|
12
12
|
// Under the 60 seconds Codex, Cursor CLI and Copilot CLI give an MCP call, with the status read first.
|
|
13
13
|
export const REACH_MS = 55_000;
|
|
@@ -82,7 +82,7 @@ export function reachText({ rows, sent, left, unknown }, answers, how) {
|
|
|
82
82
|
const AT_ONCE = 4;
|
|
83
83
|
export async function sendOurs(rows, { runner, origin, signIn = async () => null, next, stop = () => false, onAnswer = () => {}, fetchImpl = fetch, atOnce = AT_ONCE }) {
|
|
84
84
|
const app = appUrl(runner);
|
|
85
|
-
const base = () => ({ ...(origin ? { origin, referer: `${origin}/` } : {}), [RUN_TURN]: `reach:
|
|
85
|
+
const base = () => ({ ...(origin ? { origin, referer: `${origin}/` } : {}), [RUN_TURN]: `reach:agent:${next()}` });
|
|
86
86
|
const answers = [];
|
|
87
87
|
let at = 0, stopped = false;
|
|
88
88
|
const one = async (m) => {
|
package/lib/read-text.mjs
CHANGED
|
@@ -96,7 +96,6 @@ export function machineText(f) {
|
|
|
96
96
|
? `Cortad started your app in ${f.dir === "." ? "this folder" : f.dir} and stops it when this command stops. Your code and settings files are never edited.`
|
|
97
97
|
: `Cortad uses your app already running on port ${f.port} and leaves it running. Your code and settings files are never edited.`,
|
|
98
98
|
];
|
|
99
|
-
if (f.copies === "made") lines.push("Test conversations save into a copy of your database, made fresh at each connect and deleted when this command stops. Your own data is not changed.");
|
|
100
99
|
// Env values reach only a process the command started itself.
|
|
101
100
|
if (f.started) lines.push(`Your env values stay on this machine${f.changed?.length ? `, with ${listed(f.changed)} set for this session only` : ""}. None is sent to Cortad.`);
|
|
102
101
|
lines.push(f.hooked ? "Cortad sees each request that reaches your model and the model calls it makes. Request headers stay on this machine and are deleted when the command stops."
|
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) } : {});
|