@wrongstack/plugin-sdk 0.308.7

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.
@@ -0,0 +1,1004 @@
1
+ // src/runtime/index.ts
2
+ import { execFile } from "node:child_process";
3
+ import { existsSync, readdirSync, statSync } from "node:fs";
4
+ import { basename, extname as extname2, isAbsolute as isAbsolute3, relative as relative3, resolve as resolve3 } from "node:path";
5
+
6
+ // src/runtime/llm.ts
7
+ function stripOuterMarkdownFence(text) {
8
+ const trimmed = text.trim();
9
+ const match = trimmed.match(/^```(?:[a-z0-9_-]+)?\s*\r?\n([\s\S]*?)\r?\n```$/i);
10
+ return (match?.[1] ?? trimmed).trim();
11
+ }
12
+ function parseLlmJsonObject(text) {
13
+ const candidate = stripOuterMarkdownFence(text);
14
+ try {
15
+ const parsed = JSON.parse(candidate);
16
+ return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
17
+ } catch {
18
+ return null;
19
+ }
20
+ }
21
+ async function runOptionalPluginLlm(request) {
22
+ if (!request.requested) {
23
+ return { used: false, value: null, fallbackReason: "not-requested" };
24
+ }
25
+ if (!request.api.llm) {
26
+ return { used: false, value: null, fallbackReason: "unavailable" };
27
+ }
28
+ if (request.options?.signal?.aborted) {
29
+ return { used: false, value: null, fallbackReason: "cancelled" };
30
+ }
31
+ try {
32
+ const response = await request.api.llm.complete(request.prompt, request.options);
33
+ const parsed = request.parse(response.text);
34
+ if (parsed === null) {
35
+ request.api.log.warn(`${request.label}: ignored invalid LLM response`);
36
+ return { used: false, value: null, fallbackReason: "invalid-response" };
37
+ }
38
+ return { used: true, value: parsed, fallbackReason: null };
39
+ } catch (error) {
40
+ const cancelled = request.options?.signal?.aborted === true;
41
+ request.api.log.warn(`${request.label}: LLM enrichment failed; using deterministic fallback`, {
42
+ error: error instanceof Error ? error.message : String(error)
43
+ });
44
+ return {
45
+ used: false,
46
+ value: null,
47
+ fallbackReason: cancelled ? "cancelled" : "provider-error"
48
+ };
49
+ }
50
+ }
51
+ async function runOptionalPluginCouncil(request) {
52
+ if (!request.requested) {
53
+ return { used: false, value: null, fallbackReason: "not-requested" };
54
+ }
55
+ if (request.options?.signal?.aborted) {
56
+ return { used: false, value: null, fallbackReason: "cancelled" };
57
+ }
58
+ const council = request.api.llm?.council;
59
+ if (council) {
60
+ try {
61
+ const result = await council(request.prompt, {
62
+ ...request.context ? { context: request.context } : {},
63
+ ...request.profile ? { profile: request.profile } : {},
64
+ ...request.councilOptions ? { options: request.councilOptions } : {},
65
+ ...request.options?.signal ? { signal: request.options.signal } : {}
66
+ });
67
+ if (result.status === "cancelled") {
68
+ return { used: false, value: null, fallbackReason: "cancelled" };
69
+ }
70
+ const parsed = result.status === "decided" ? request.parse(result.answer ?? "") : null;
71
+ if (parsed !== null) return { used: true, value: parsed, fallbackReason: null };
72
+ request.api.log.warn(
73
+ `${request.label}: Council did not return a valid answer; trying One Shot`,
74
+ {
75
+ status: result.status,
76
+ resolution: result.resolution
77
+ }
78
+ );
79
+ } catch (error) {
80
+ if (request.options?.signal?.aborted) {
81
+ return { used: false, value: null, fallbackReason: "cancelled" };
82
+ }
83
+ request.api.log.warn(`${request.label}: Council failed; trying One Shot`, {
84
+ error: error instanceof Error ? error.message : String(error)
85
+ });
86
+ }
87
+ }
88
+ return runOptionalPluginLlm(request);
89
+ }
90
+
91
+ // src/runtime/local-bin.ts
92
+ import { createRequire } from "node:module";
93
+ import { delimiter, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
94
+ import { accessSync, constants, readFileSync } from "node:fs";
95
+ import { buildWin32CmdShimInvocation, resolveWin32Command } from "@wrongstack/tools/win32";
96
+ function resolveExecInvocation(command, args = []) {
97
+ const resolved = resolveWin32Command(command);
98
+ const normalizedResolved = resolved.toLowerCase();
99
+ const needsShell = process.platform === "win32" && (normalizedResolved.endsWith(".cmd") || normalizedResolved.endsWith(".bat"));
100
+ if (needsShell) {
101
+ const shim = buildWin32CmdShimInvocation(resolved, args);
102
+ return { cmd: shim.command, args: shim.args, windowsVerbatimArguments: true };
103
+ }
104
+ return { cmd: resolved, args: [...args], windowsVerbatimArguments: false };
105
+ }
106
+ function findOnPath(cmd) {
107
+ if (!cmd) return null;
108
+ const exists = (p) => {
109
+ try {
110
+ accessSync(p, constants.X_OK);
111
+ return true;
112
+ } catch {
113
+ return false;
114
+ }
115
+ };
116
+ if (cmd.includes("/") || cmd.includes("\\")) {
117
+ return exists(cmd) ? resolve(cmd) : null;
118
+ }
119
+ const suffixes = process.platform === "win32" && extname(cmd) === "" ? (process.env["PATHEXT"] ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean) : [""];
120
+ for (const dir of (process.env["PATH"] ?? "").split(delimiter)) {
121
+ if (!dir) continue;
122
+ const base = join(dir, cmd);
123
+ for (const suffix of suffixes) {
124
+ const candidate = `${base}${suffix}`;
125
+ if (exists(candidate)) return candidate;
126
+ }
127
+ }
128
+ return null;
129
+ }
130
+ function isInside(parent, candidate) {
131
+ const rel = relative(parent, candidate);
132
+ return rel === "" || !rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute(rel);
133
+ }
134
+ var binCache = /* @__PURE__ */ new Map();
135
+ var BIN_CACHE_MAX = 64;
136
+ var NEGATIVE_BIN_CACHE_TTL_MS = 5e3;
137
+ function cachePut(key, value) {
138
+ while (binCache.size >= BIN_CACHE_MAX) {
139
+ const oldest = binCache.keys().next().value;
140
+ if (oldest === void 0) break;
141
+ binCache.delete(oldest);
142
+ }
143
+ binCache.set(key, { value, cachedAt: Date.now() });
144
+ return value;
145
+ }
146
+ function clearLocalBinCache() {
147
+ binCache.clear();
148
+ }
149
+ function resolveNodeBin(packageName, binName, cwd, extraArgs = []) {
150
+ const key = `${packageName}|${binName}|${cwd}`;
151
+ const cached = binCache.get(key);
152
+ if (cached !== void 0) {
153
+ if (cached.value !== null || Date.now() - cached.cachedAt < NEGATIVE_BIN_CACHE_TTL_MS) {
154
+ return cached.value === null ? null : { ...cached.value, args: [cached.value.entry, ...extraArgs] };
155
+ }
156
+ binCache.delete(key);
157
+ }
158
+ let resolved = null;
159
+ try {
160
+ const requireFromProject = createRequire(resolve(cwd, "package.json"));
161
+ const packagePath = requireFromProject.resolve(`${packageName}/package.json`);
162
+ const packageJson = JSON.parse(readFileSync(packagePath, "utf-8"));
163
+ const relativeBin = typeof packageJson.bin === "string" ? packageJson.bin : packageJson.bin?.[binName] ?? Object.values(packageJson.bin ?? {})[0];
164
+ if (relativeBin && !isAbsolute(relativeBin)) {
165
+ const packageDir = dirname(packagePath);
166
+ const entry = resolve(packageDir, relativeBin);
167
+ if (isInside(packageDir, entry)) {
168
+ resolved = { cmd: process.execPath, args: [entry], entry };
169
+ }
170
+ }
171
+ } catch {
172
+ resolved = null;
173
+ }
174
+ cachePut(key, resolved);
175
+ return resolved === null ? null : { ...resolved, args: [resolved.entry, ...extraArgs] };
176
+ }
177
+ function resolveFirstNodeBin(candidates, cwd) {
178
+ for (const c of candidates) {
179
+ const hit = resolveNodeBin(c.packageName, c.binName, cwd, c.args ?? []);
180
+ if (hit) return { ...hit, packageName: c.packageName, binName: c.binName };
181
+ }
182
+ return null;
183
+ }
184
+
185
+ // src/runtime/bounded-map.ts
186
+ var BoundedMap = class {
187
+ map = /* @__PURE__ */ new Map();
188
+ max;
189
+ ttlMs;
190
+ now;
191
+ /** Entries dropped to stay under `max`. Surfaced by plugin health(). */
192
+ evictions = 0;
193
+ constructor(options) {
194
+ const normalizedMax = Math.floor(options.max);
195
+ this.max = Number.isSafeInteger(normalizedMax) ? Math.max(1, normalizedMax) : 1;
196
+ this.ttlMs = options.ttlMs;
197
+ this.now = options.now ?? Date.now;
198
+ }
199
+ expired(entry) {
200
+ return this.ttlMs !== void 0 && this.now() - entry.storedAt > this.ttlMs;
201
+ }
202
+ get(key) {
203
+ const entry = this.map.get(key);
204
+ if (entry === void 0) return void 0;
205
+ if (this.expired(entry)) {
206
+ this.map.delete(key);
207
+ return void 0;
208
+ }
209
+ this.map.delete(key);
210
+ this.map.set(key, entry);
211
+ return entry.value;
212
+ }
213
+ /**
214
+ * Read without promoting the key to most-recently-used. Use for
215
+ * diagnostics that must not perturb the eviction order.
216
+ */
217
+ peek(key) {
218
+ const entry = this.map.get(key);
219
+ if (entry === void 0 || this.expired(entry)) return void 0;
220
+ return entry.value;
221
+ }
222
+ has(key) {
223
+ const entry = this.map.get(key);
224
+ if (entry === void 0) return false;
225
+ if (this.expired(entry)) {
226
+ this.map.delete(key);
227
+ return false;
228
+ }
229
+ return true;
230
+ }
231
+ set(key, value) {
232
+ this.map.delete(key);
233
+ this.map.set(key, { value, storedAt: this.now() });
234
+ while (this.map.size > this.max) {
235
+ const coldest = this.map.keys().next().value;
236
+ if (coldest === void 0) break;
237
+ this.map.delete(coldest);
238
+ this.evictions += 1;
239
+ }
240
+ return this;
241
+ }
242
+ delete(key) {
243
+ return this.map.delete(key);
244
+ }
245
+ clear() {
246
+ this.map.clear();
247
+ this.evictions = 0;
248
+ }
249
+ get size() {
250
+ return this.map.size;
251
+ }
252
+ /** How many entries have been dropped to respect `max`, since the last clear. */
253
+ get evictionCount() {
254
+ return this.evictions;
255
+ }
256
+ /** Drop every expired entry. Cheap enough to call from a status tool. */
257
+ prune() {
258
+ if (this.ttlMs === void 0) return 0;
259
+ let removed = 0;
260
+ for (const [key, entry] of this.map) {
261
+ if (this.expired(entry)) {
262
+ this.map.delete(key);
263
+ removed += 1;
264
+ }
265
+ }
266
+ return removed;
267
+ }
268
+ /** Live (non-expired) entries, coldest first. */
269
+ *entries() {
270
+ for (const [key, entry] of this.map) {
271
+ if (!this.expired(entry)) yield [key, entry.value];
272
+ }
273
+ }
274
+ [Symbol.iterator]() {
275
+ return this.entries();
276
+ }
277
+ };
278
+ var BoundedSet = class {
279
+ inner;
280
+ constructor(options) {
281
+ this.inner = new BoundedMap(options);
282
+ }
283
+ has(value) {
284
+ return this.inner.has(value);
285
+ }
286
+ add(value) {
287
+ this.inner.set(value, true);
288
+ return this;
289
+ }
290
+ delete(value) {
291
+ return this.inner.delete(value);
292
+ }
293
+ clear() {
294
+ this.inner.clear();
295
+ }
296
+ get size() {
297
+ return this.inner.size;
298
+ }
299
+ /** How many entries have been dropped to respect `max`, since the last clear. */
300
+ get evictionCount() {
301
+ return this.inner.evictionCount;
302
+ }
303
+ *values() {
304
+ for (const [key] of this.inner) yield key;
305
+ }
306
+ [Symbol.iterator]() {
307
+ return this.values();
308
+ }
309
+ };
310
+
311
+ // src/runtime/credential-patterns.ts
312
+ var CREDENTIAL_PATTERNS = [
313
+ // LLM provider keys
314
+ {
315
+ type: "anthropic_key",
316
+ regex: /(?<![A-Za-z0-9])sk-ant-api\d+-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9])/g
317
+ },
318
+ { type: "openai_key", regex: /(?<![A-Za-z0-9])sk-(?!ant)(?:proj-)?[A-Za-z0-9_-]{20,}(?![A-Za-z0-9])/g },
319
+ // GitHub. `ghp_` is only the personal-access-token prefix — the OAuth
320
+ // (`gho_`), user-to-server (`ghu_`), server-to-server (`ghs_`) and
321
+ // refresh (`ghr_`) tokens grant the same or broader access and were
322
+ // previously not detected at all.
323
+ { type: "github_pat", regex: /(?<![A-Za-z0-9])ghp_[A-Za-z0-9]{36,}(?![A-Za-z0-9])/g },
324
+ {
325
+ type: "github_oauth_token",
326
+ regex: /(?<![A-Za-z0-9])gh[ousr]_[A-Za-z0-9]{36,}(?![A-Za-z0-9])/g
327
+ },
328
+ { type: "github_pat_v2", regex: /(?<![A-Za-z0-9])github_pat_[A-Za-z0-9_]{50,}(?![A-Za-z0-9])/g },
329
+ // GitLab
330
+ { type: "gitlab_pat", regex: /(?<![A-Za-z0-9])glpat-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g },
331
+ {
332
+ type: "gitlab_runner_token",
333
+ regex: /(?<![A-Za-z0-9])glrt-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g
334
+ },
335
+ // npm — a leaked publish token is a supply-chain compromise.
336
+ { type: "npm_token", regex: /(?<![A-Za-z0-9])npm_[A-Za-z0-9]{36}(?![A-Za-z0-9])/g },
337
+ // AWS
338
+ { type: "aws_access_key", regex: /(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}(?![A-Za-z0-9])/g },
339
+ // GCP
340
+ { type: "gcp_key", regex: /(?<![A-Za-z0-9])AIza[0-9A-Za-z_-]{35}(?![A-Za-z0-9])/g },
341
+ // Slack. `xoxe` (token-rotation) and `xapp` (app-level) were missing;
342
+ // both are as sensitive as the bot/user tokens already covered.
343
+ {
344
+ type: "slack_token",
345
+ regex: /(?<![A-Za-z0-9-])xox[abposer]-[A-Za-z0-9-]{10,}(?![A-Za-z0-9-])/g
346
+ },
347
+ { type: "slack_app_token", regex: /(?<![A-Za-z0-9-])xapp-\d-[A-Za-z0-9-]{10,}(?![A-Za-z0-9-])/g },
348
+ {
349
+ type: "slack_webhook",
350
+ regex: /https:\/\/hooks\.slack\.com\/services\/T[A-Za-z0-9_-]+\/B[A-Za-z0-9_-]+\/[A-Za-z0-9]{16,}/g
351
+ },
352
+ // Stripe
353
+ {
354
+ type: "stripe_key",
355
+ regex: /(?<![A-Za-z0-9])sk_(?:live|test)_[A-Za-z0-9]{24,}(?![A-Za-z0-9])/g
356
+ },
357
+ // Twilio
358
+ { type: "twilio_sid", regex: /(?<![A-Za-z0-9])AC[a-f0-9]{32}(?![A-Za-z0-9])/g },
359
+ // Telegram
360
+ {
361
+ type: "telegram_bot_token",
362
+ regex: /(?:(?<![A-Za-z0-9_])|(?<=(?:^|[^A-Za-z0-9_])bot))\d+:[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g
363
+ },
364
+ // JWT
365
+ {
366
+ type: "jwt",
367
+ regex: /(?<![A-Za-z0-9/+=])eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}(?![A-Za-z0-9/+=])/g
368
+ },
369
+ // Private keys
370
+ {
371
+ type: "private_key",
372
+ regex: /(?:^|\n)(?:-----BEGIN (?:RSA|EC|OPENSSH|DSA)? ?PRIVATE KEY-----[\s\S]*?-----END (?:RSA|EC|OPENSSH|DSA)? ?PRIVATE KEY-----|-----BEGIN PGP PRIVATE KEY BLOCK-----[\s\S]*?-----END PGP PRIVATE KEY BLOCK-----)(?!\S)/g
373
+ },
374
+ // AI/ML provider tokens
375
+ { type: "huggingface_token", regex: /(?<![A-Za-z0-9])hf_[A-Za-z0-9]{34}(?![A-Za-z0-9])/g },
376
+ { type: "replicate_token", regex: /(?<![A-Za-z0-9])r8_[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g },
377
+ { type: "perplexity_key", regex: /(?<![A-Za-z0-9])pplx-[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g },
378
+ { type: "groq_key", regex: /(?<![A-Za-z0-9])gsk_[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g },
379
+ // SaaS / infrastructure tokens — each grants API access on the user's
380
+ // account, and each was previously invisible to this gate.
381
+ { type: "sendgrid_key", regex: /(?<![A-Za-z0-9])SG\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g },
382
+ { type: "digitalocean_token", regex: /(?<![A-Za-z0-9])dop_v1_[a-f0-9]{64}(?![A-Za-z0-9])/g },
383
+ { type: "doppler_token", regex: /(?<![A-Za-z0-9])dp\.(?:pt|st|sa|scim|audit)\.[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g },
384
+ { type: "shopify_token", regex: /(?<![A-Za-z0-9])shp(?:at|ca|pa|ss)_[a-fA-F0-9]{32}(?![A-Za-z0-9])/g },
385
+ { type: "docker_pat", regex: /(?<![A-Za-z0-9])dckr_pat_[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g },
386
+ { type: "linear_key", regex: /(?<![A-Za-z0-9])lin_api_[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g },
387
+ { type: "atlassian_token", regex: /(?<![A-Za-z0-9])ATATT3[A-Za-z0-9_\-=]{40,}(?![A-Za-z0-9_\-=])/g },
388
+ { type: "square_token", regex: /(?<![A-Za-z0-9])(?:sq0(?:atp|csp)-|EAAA)[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g },
389
+ {
390
+ type: "azure_storage_key",
391
+ regex: /AccountKey=[A-Za-z0-9+/]{80,}={0,2}/g
392
+ },
393
+ {
394
+ type: "google_oauth_client_secret",
395
+ regex: /(?<![A-Za-z0-9_-])GOCSPX-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g
396
+ },
397
+ // Bearer tokens
398
+ {
399
+ type: "bearer_token",
400
+ regex: /(?<![A-Za-z0-9_.~+/-])Bearer\s+[A-Za-z0-9._~+/-]{12,512}=*(?![A-Za-z0-9._~+/-])/g
401
+ },
402
+ // Database URIs. Require password-bearing user-info; credential-free values stay scannable.
403
+ { type: "mongodb_uri", regex: /mongodb(?:\+srv)?:\/\/[^\s:/@"'`]*:[^\s/@"'`]+@[^\s"'`]+/g },
404
+ {
405
+ type: "postgres_uri",
406
+ // Query parsers decode percent-encoded parameter names. Recognize each
407
+ // encoded character in `password` so mixed forms such as `pass%77ord`
408
+ // cannot bypass detection while keeping the scan strictly bounded.
409
+ regex: /postgres(?:ql)?:\/\/(?:[^\s:/@"'`]*:[^\s/@"'`]+@[^\s"'`]+|[^&\s?"'`#]{1,2048}\?(?:(?!(?:p|%70)(?:a|%61)(?:s|%73)(?:s|%73)(?:w|%77)(?:o|%6[fF])(?:r|%72)(?:d|%64)=)[^&\s#"'`]{1,256}&){0,32}(?:p|%70)(?:a|%61)(?:s|%73)(?:s|%73)(?:w|%77)(?:o|%6[fF])(?:r|%72)(?:d|%64)=[^&\s#"'`]{1,4096})/g
410
+ },
411
+ { type: "mysql_uri", regex: /mysql:\/\/[^\s:/@"'`]*:[^\s/@"'`]+@[^\s"'`]+/g },
412
+ { type: "redis_uri", regex: /redis:\/\/[^\s:/@"'`]*:[^\s/@"'`]+@[^\s"'`]+/g },
413
+ {
414
+ // Credentials serialised as JSON, keyed rather than prefixed. Every other
415
+ // entry in this table recognises a credential by its SHAPE (`ghp_`, `sk-`,
416
+ // `eyJ`), which means a key with no distinctive prefix — Azure, a
417
+ // self-hosted gateway, an Anthropic/Codex OAuth token — was invisible to
418
+ // both surfaces. `prompt-firewall` guards the outgoing provider request, so
419
+ // this is what stops a JSON-shaped tool result carrying such a value to a
420
+ // third party.
421
+ //
422
+ // The key is matched in a LOOKBEHIND, so the reported match is the secret
423
+ // itself and the pattern keeps zero capturing groups — `secret-scanner`
424
+ // maps a combined-regex group index back to the pattern that fired, and an
425
+ // inner group would shift that mapping (see the style note above, enforced
426
+ // by credential-pattern-parity.test.ts).
427
+ //
428
+ // Mirrors `json_credential_key` in
429
+ // `@wrongstack/core` → `src/security/secret-scrubber.ts`. Keep the two key
430
+ // lists in step; the core side additionally preserves the key name when it
431
+ // rewrites, which is why it is written with capture groups instead.
432
+ type: "json_credential_key",
433
+ regex: /(?<="[A-Za-z0-9_]{0,64}(?:apiKey|api_key|token|secret|password|authorization|bearer|private_key|access_token|refresh_token|client_secret)"\s{0,8}:\s{0,8}")[^"\\]{8,512}(?=")/gi
434
+ }
435
+ ];
436
+ function cloneCredentialPatterns() {
437
+ return CREDENTIAL_PATTERNS.map((p) => ({
438
+ type: p.type,
439
+ regex: new RegExp(p.regex.source, p.regex.flags)
440
+ }));
441
+ }
442
+
443
+ // src/runtime/safe-json.ts
444
+ var UNSERIALIZABLE = "[unserializable]";
445
+ function safeJsonStringify(value, indent) {
446
+ try {
447
+ const stack = [];
448
+ const out = JSON.stringify(
449
+ value,
450
+ function replacer(_key, val) {
451
+ if (typeof val === "bigint") return `${val.toString()}n`;
452
+ if (val === null || typeof val !== "object") return val;
453
+ while (stack.length > 0 && stack[stack.length - 1] !== this) stack.pop();
454
+ if (stack.includes(val)) return "[circular]";
455
+ stack.push(val);
456
+ return val;
457
+ },
458
+ indent
459
+ );
460
+ return out ?? String(value);
461
+ } catch {
462
+ return UNSERIALIZABLE;
463
+ }
464
+ }
465
+
466
+ // src/runtime/handles.ts
467
+ function releaseHandle(off) {
468
+ if (off) {
469
+ try {
470
+ off();
471
+ } catch {
472
+ }
473
+ }
474
+ return null;
475
+ }
476
+ function releaseHandles(state, keys) {
477
+ for (const key of keys) {
478
+ state[key] = releaseHandle(state[key]);
479
+ }
480
+ }
481
+
482
+ // src/runtime/redos-guard.ts
483
+ import { Worker } from "node:worker_threads";
484
+ function withReDoSGuard(re, input, budgetMs = 50, options = {}) {
485
+ const opts = { budgetMs, ...options };
486
+ const start = Date.now();
487
+ const workerSource = buildWorkerSource(re.source, input, re.flags);
488
+ const worker = new Worker(workerSource, {
489
+ eval: true,
490
+ name: `redos-guard:${re.source.slice(0, 32)}`
491
+ });
492
+ return new Promise((resolve4) => {
493
+ let settled = false;
494
+ const onMessage = (msg) => {
495
+ if (settled) return;
496
+ settled = true;
497
+ clearTimeout(timer);
498
+ worker.terminate().catch(() => {
499
+ });
500
+ if (!msg.ok) {
501
+ resolve4({ timedOut: true, match: null });
502
+ return;
503
+ }
504
+ resolve4({ timedOut: false, match: msg.match });
505
+ };
506
+ const onError = () => {
507
+ if (settled) return;
508
+ settled = true;
509
+ clearTimeout(timer);
510
+ worker.terminate().catch(() => {
511
+ });
512
+ resolve4({ timedOut: true, match: null });
513
+ };
514
+ const timer = setTimeout(() => {
515
+ if (settled) return;
516
+ settled = true;
517
+ const elapsedMs = Date.now() - start;
518
+ worker.terminate().catch(() => {
519
+ });
520
+ try {
521
+ opts.onTimeout?.({
522
+ regex: re,
523
+ input,
524
+ budgetMs: opts.budgetMs,
525
+ elapsedMs
526
+ });
527
+ } catch {
528
+ }
529
+ resolve4({ timedOut: true, match: null });
530
+ }, opts.budgetMs);
531
+ timer.unref?.();
532
+ worker.on("message", onMessage);
533
+ worker.on("error", onError);
534
+ });
535
+ }
536
+ function buildWorkerSource(source, input, flags) {
537
+ const S = JSON.stringify(source);
538
+ const I = JSON.stringify(input);
539
+ const F = JSON.stringify(flags);
540
+ return `
541
+ const { parentPort } = require('node:worker_threads');
542
+ const source = ${S};
543
+ const input = ${I};
544
+ const flags = ${F};
545
+ try {
546
+ const re = new RegExp(source, flags);
547
+ const match = re.exec(input);
548
+ // parentPort.postMessage, NOT bare postMessage: with eval:true
549
+ // workers this Node version does not expose the bare postMessage
550
+ // global \u2014 the worker throws ReferenceError at startup and the
551
+ // host misreads it as a timeout (positive-path regression).
552
+ parentPort.postMessage({ ok: true, match });
553
+ } catch (err) {
554
+ parentPort.postMessage({ ok: false, error: err && err.message ? err.message : String(err) });
555
+ }
556
+ `;
557
+ }
558
+ function guardedMatcher(re, budgetMs = 50, onTimeout) {
559
+ return (input) => withReDoSGuard(re, input, budgetMs, onTimeout ? { onTimeout } : {});
560
+ }
561
+
562
+ // src/runtime/sandbox.ts
563
+ import { realpathSync } from "node:fs";
564
+ import { isAbsolute as isAbsolute2, relative as relative2, resolve as resolve2 } from "node:path";
565
+ var MAX_PATH_BYTES = 4096;
566
+ function safePath(input, options = {}) {
567
+ if (typeof input !== "string") return null;
568
+ if (input.length === 0 || input.length > MAX_PATH_BYTES) return null;
569
+ if (input.startsWith("-")) return null;
570
+ const projectRoot = resolve2(options.projectRoot ?? process.cwd());
571
+ const lexical = isAbsolute2(input) ? resolve2(input) : resolve2(projectRoot, input);
572
+ if (!withinLexical(projectRoot, lexical)) return null;
573
+ if (options.followSymlinks !== false) {
574
+ let real;
575
+ try {
576
+ real = realpathSync(lexical);
577
+ } catch {
578
+ return null;
579
+ }
580
+ if (!withinLexical(projectRoot, real)) return null;
581
+ return real;
582
+ }
583
+ return lexical;
584
+ }
585
+ function withinLexical(projectRoot, candidate) {
586
+ const rel = relative2(projectRoot, candidate);
587
+ if (rel === "" || rel === ".") return true;
588
+ if (rel.startsWith("..")) return false;
589
+ if (isAbsolute2(rel)) return false;
590
+ return true;
591
+ }
592
+ function isInsideProject(input, options = {}) {
593
+ return safePath(input, options) !== null;
594
+ }
595
+
596
+ // src/runtime/h1-state.ts
597
+ function createH1State(initial) {
598
+ const handles = /* @__PURE__ */ new Map();
599
+ const safeRelease = (unregister) => {
600
+ try {
601
+ unregister();
602
+ } catch {
603
+ }
604
+ };
605
+ return {
606
+ state: initial,
607
+ register(key, unregister) {
608
+ const prior = handles.get(key);
609
+ if (prior) {
610
+ safeRelease(prior);
611
+ handles.delete(key);
612
+ }
613
+ if (unregister) {
614
+ handles.set(key, unregister);
615
+ }
616
+ },
617
+ release(key) {
618
+ const prior = handles.get(key);
619
+ if (!prior) return;
620
+ handles.delete(key);
621
+ safeRelease(prior);
622
+ },
623
+ releaseAll() {
624
+ for (const unregister of handles.values()) {
625
+ safeRelease(unregister);
626
+ }
627
+ handles.clear();
628
+ },
629
+ size() {
630
+ return handles.size;
631
+ },
632
+ keys() {
633
+ return [...handles.keys()];
634
+ }
635
+ };
636
+ }
637
+
638
+ // src/runtime/index.ts
639
+ var META_CHARS = /["'`;&|<>\r\n]/;
640
+ var MAX_BUFFER_BYTES = 16 * 1024 * 1024;
641
+ function hasLeadingDash(arg) {
642
+ return arg.length > 0 && arg.startsWith("-");
643
+ }
644
+ function safeSplit(command) {
645
+ const trimmed = command.trim();
646
+ if (!trimmed || META_CHARS.test(trimmed)) return null;
647
+ return trimmed.split(/\s+/).filter(Boolean);
648
+ }
649
+ function withinProjectPath(projectRoot, candidate) {
650
+ if (candidate.length === 0 || candidate.length > 4096) return false;
651
+ if (hasLeadingDash(candidate)) return false;
652
+ const resolved = isAbsolute3(candidate) ? resolve3(candidate) : resolve3(projectRoot, candidate);
653
+ const rel = relative3(projectRoot, resolved);
654
+ return rel === "" || !rel.startsWith("..") && !isAbsolute3(rel);
655
+ }
656
+ function everyFlagAllowed(allowed, args) {
657
+ for (const arg of args) {
658
+ if (!hasLeadingDash(arg)) continue;
659
+ if (allowed === null) return false;
660
+ if (!allowed.has(arg)) return false;
661
+ }
662
+ return true;
663
+ }
664
+ function sanitizeRunnerPath(value, options = {}) {
665
+ if (!value || hasLeadingDash(value)) return null;
666
+ const projectRoot = resolve3(options.projectRoot ?? process.cwd());
667
+ if (!withinProjectPath(projectRoot, value)) return null;
668
+ return isAbsolute3(value) ? resolve3(value) : resolve3(projectRoot, value);
669
+ }
670
+ function resolveRunnerCommand(runtime, command, options = {}) {
671
+ const tokens = safeSplit(command);
672
+ if (!tokens || tokens.length === 0) return null;
673
+ const projectRoot = resolve3(options.projectRoot ?? process.cwd());
674
+ const launcher = runtime.packageManager;
675
+ const [head, second, ...rest] = tokens;
676
+ if (!head) return null;
677
+ const display = tokens.join(" ");
678
+ if (launcher !== "none" && tokens.length === 1 && head === launcher) {
679
+ return null;
680
+ }
681
+ if (head === runtime.executable) {
682
+ if (!everyFlagAllowed(runtime.allowedFlags, [second, ...rest].filter((v) => Boolean(v)))) {
683
+ return null;
684
+ }
685
+ return {
686
+ cmd: head,
687
+ args: [second, ...rest].filter((v) => Boolean(v)),
688
+ display
689
+ };
690
+ }
691
+ if (launcher !== "none" && head === launcher && runtime.subcommands.length === 0 && second === runtime.executable) {
692
+ if (!everyFlagAllowed(runtime.allowedFlags, rest)) return null;
693
+ return { cmd: head, args: [second, ...rest], display };
694
+ }
695
+ if (launcher !== "none" && head === launcher && runtime.subcommands.length > 0) {
696
+ const subcommand = runtime.subcommands[0];
697
+ const exe = rest[0];
698
+ if (second === subcommand && exe === runtime.executable) {
699
+ const tail = rest.slice(1);
700
+ if (!everyFlagAllowed(runtime.allowedFlags, tail)) return null;
701
+ return { cmd: head, args: [second, exe, ...tail], display };
702
+ }
703
+ }
704
+ if (isAbsolute3(head)) {
705
+ if (!withinProjectPath(projectRoot, head)) return null;
706
+ const base = basename(head);
707
+ if (base !== runtime.executable && base !== launcher) return null;
708
+ if (second !== runtime.executable) return null;
709
+ if (!everyFlagAllowed(runtime.allowedFlags, rest)) return null;
710
+ return { cmd: head, args: [second, ...rest], display };
711
+ }
712
+ return null;
713
+ }
714
+ function runRunnerCommand(argv, options) {
715
+ if (argv.length === 0) {
716
+ return Promise.resolve({
717
+ code: null,
718
+ stdout: "",
719
+ stderr: "runtime helper: empty argv",
720
+ timedOut: false,
721
+ spawnError: true
722
+ });
723
+ }
724
+ return new Promise((resolvePromise) => {
725
+ const projectRoot = resolve3(options.projectRoot ?? process.cwd());
726
+ const trimmedCwd = options.cwd.trim();
727
+ if (!withinProjectPath(projectRoot, trimmedCwd)) {
728
+ resolvePromise({
729
+ code: null,
730
+ stdout: "",
731
+ stderr: "runtime helper: cwd outside project",
732
+ timedOut: false,
733
+ spawnError: true
734
+ });
735
+ return;
736
+ }
737
+ let timedOut = false;
738
+ let spawnErrored = false;
739
+ const start = Date.now();
740
+ const stdoutChunks = [];
741
+ const stderrChunks = [];
742
+ let stdoutBytes = 0;
743
+ let stderrBytes = 0;
744
+ const onAbort = () => {
745
+ timedOut = true;
746
+ };
747
+ let invocation;
748
+ try {
749
+ invocation = resolveExecInvocation(argv[0], argv.slice(1));
750
+ } catch (err) {
751
+ resolvePromise({
752
+ code: null,
753
+ stdout: "",
754
+ stderr: `runtime helper: ${err instanceof Error ? err.message : String(err)}`,
755
+ timedOut: false,
756
+ spawnError: true
757
+ });
758
+ return;
759
+ }
760
+ options.signal?.addEventListener("abort", onAbort, { once: true });
761
+ const child = execFile(
762
+ invocation.cmd,
763
+ invocation.args,
764
+ {
765
+ cwd: trimmedCwd,
766
+ timeout: options.timeoutMs,
767
+ signal: options.signal,
768
+ maxBuffer: MAX_BUFFER_BYTES,
769
+ // execFile defaults `encoding` to 'utf8', which makes the
770
+ // stdout/stderr `data` events emit *strings*. The chunk arrays
771
+ // below are typed Buffer[] and every consumer runs them through
772
+ // Buffer.concat(...).toString('utf8'), which throws
773
+ // ERR_INVALID_ARG_TYPE on any non-empty string output. Pin the
774
+ // streams to buffers so the declared contract holds (regression:
775
+ // runRunnerCommand crashed on any child that actually wrote
776
+ // output; only the maxBuffer fixture exercised this path).
777
+ encoding: "buffer",
778
+ windowsHide: true,
779
+ shell: false,
780
+ ...invocation.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}
781
+ },
782
+ (err) => {
783
+ options.signal?.removeEventListener("abort", onAbort);
784
+ if (timedOut) {
785
+ resolvePromise({
786
+ code: null,
787
+ stdout: Buffer.concat(stdoutChunks).toString("utf8"),
788
+ stderr: Buffer.concat(stderrChunks).toString("utf8"),
789
+ timedOut: true,
790
+ spawnError: false
791
+ });
792
+ return;
793
+ }
794
+ if (spawnErrored) {
795
+ resolvePromise({
796
+ code: 127,
797
+ stdout: Buffer.concat(stdoutChunks).toString("utf8"),
798
+ stderr: Buffer.concat(stderrChunks).toString("utf8"),
799
+ timedOut: false,
800
+ spawnError: true
801
+ });
802
+ return;
803
+ }
804
+ if (err) {
805
+ const anyErr = err;
806
+ if ((anyErr.killed === true || anyErr.signal === "SIGTERM") && // maxBuffer overflow must NOT be misreported as a timeout
807
+ // — it's a real failure (the child wrote too much) and
808
+ // downstream callers (type-gate/index.ts:227) return
809
+ // null on timedOut=true, which would silently swallow
810
+ // maxBuffer overflow into a confusing empty-output
811
+ // result. Skip the timeout resolve when the err shape
812
+ // names maxBuffer explicitly.
813
+ !/maxBuffer length exceeded/i.test(anyErr.message ?? "") && // And only count it as a timeout if the wall clock has
814
+ // actually elapsed past the budget. External SIGTERMs and
815
+ // races against the exit handler don't satisfy this.
816
+ Date.now() - start >= options.timeoutMs) {
817
+ resolvePromise({
818
+ code: null,
819
+ stdout: Buffer.concat(stdoutChunks).toString("utf-8"),
820
+ stderr: Buffer.concat(stderrChunks).toString("utf-8"),
821
+ timedOut: true,
822
+ spawnError: false
823
+ });
824
+ return;
825
+ }
826
+ const code = typeof anyErr.code === "number" ? anyErr.code : 1;
827
+ resolvePromise({
828
+ code,
829
+ stdout: Buffer.concat(stdoutChunks).toString("utf-8"),
830
+ stderr: Buffer.concat(stderrChunks).toString("utf-8"),
831
+ timedOut: false,
832
+ spawnError: false
833
+ });
834
+ return;
835
+ }
836
+ resolvePromise({
837
+ code: 0,
838
+ stdout: Buffer.concat(stdoutChunks).toString("utf8"),
839
+ stderr: Buffer.concat(stderrChunks).toString("utf8"),
840
+ timedOut: false,
841
+ spawnError: false
842
+ });
843
+ }
844
+ );
845
+ child.on("exit", (_code, signal) => {
846
+ if (signal !== null && Date.now() - start >= options.timeoutMs) {
847
+ timedOut = true;
848
+ }
849
+ });
850
+ child.stdout?.on("data", (chunk) => {
851
+ stdoutBytes += chunk.length;
852
+ if (stdoutBytes <= MAX_BUFFER_BYTES) stdoutChunks.push(chunk);
853
+ });
854
+ child.stderr?.on("data", (chunk) => {
855
+ stderrBytes += chunk.length;
856
+ if (stderrBytes <= MAX_BUFFER_BYTES) stderrChunks.push(chunk);
857
+ });
858
+ child.on("error", (err) => {
859
+ if (err.code === "ENOENT" || err.code === "EPERM" || err.code === "EACCES") {
860
+ spawnErrored = true;
861
+ }
862
+ });
863
+ });
864
+ }
865
+ async function probeRunner(runtime, probeArg = "--version", options) {
866
+ const resolved = resolveRunnerCommand(runtime, `${runtime.executable} ${probeArg}`, options);
867
+ if (!resolved) return false;
868
+ const result = await runRunnerCommand([resolved.cmd, ...resolved.args], {
869
+ ...options,
870
+ timeoutMs: Math.min(options.timeoutMs, 5e3)
871
+ });
872
+ return result.code === 0;
873
+ }
874
+ function withinProject(p) {
875
+ const cwd = process.cwd();
876
+ return withinProjectPath(cwd, p) || relative3(cwd, p) === ".";
877
+ }
878
+ function locateRunnerEntry(runtime, projectRoot) {
879
+ const root = resolve3(projectRoot);
880
+ const candidates = [
881
+ resolve3(root, "node_modules", ".bin", runtime.executable),
882
+ resolve3(root, "node_modules", ".bin", `${runtime.executable}.cmd`),
883
+ resolve3(root, "node_modules", ".bin", `${runtime.executable}.ps1`)
884
+ ];
885
+ for (const c of candidates) {
886
+ if (existsSync(c)) return c;
887
+ }
888
+ return null;
889
+ }
890
+ var DEFAULT_EXCLUDE_DIRS = ["node_modules", "dist", ".git", "coverage"];
891
+ function collectSourceFiles(root, opts) {
892
+ const files = [];
893
+ if (!existsSync(root)) return files;
894
+ const s = statSync(root);
895
+ if (s.isFile()) {
896
+ if (matchesExtension(root, opts.extensions)) files.push(root);
897
+ return files;
898
+ }
899
+ if (!s.isDirectory()) return files;
900
+ const exclude = opts.excludeDirs ?? DEFAULT_EXCLUDE_DIRS;
901
+ const excludeSet = new Set(exclude);
902
+ function walk(dir, depth) {
903
+ if (opts.maxDepth !== void 0 && depth > opts.maxDepth) return;
904
+ let entries;
905
+ try {
906
+ entries = readdirSync(dir);
907
+ } catch {
908
+ return;
909
+ }
910
+ entries.sort();
911
+ for (const entry of entries) {
912
+ if (excludeSet.has(entry)) continue;
913
+ const full = resolve3(dir, entry);
914
+ let st;
915
+ try {
916
+ st = statSync(full);
917
+ } catch {
918
+ continue;
919
+ }
920
+ if (st.isDirectory()) {
921
+ walk(full, depth + 1);
922
+ } else if (st.isFile() && matchesExtension(full, opts.extensions)) {
923
+ files.push(full);
924
+ }
925
+ }
926
+ }
927
+ walk(root, 0);
928
+ return files;
929
+ }
930
+ async function collectSourceFilesAsync(root, opts) {
931
+ const { readdir, stat } = await import("node:fs/promises");
932
+ const files = [];
933
+ try {
934
+ const s = await stat(root);
935
+ if (s.isFile()) {
936
+ if (matchesExtension(root, opts.extensions)) files.push(root);
937
+ return files;
938
+ }
939
+ if (!s.isDirectory()) return files;
940
+ } catch {
941
+ return files;
942
+ }
943
+ const exclude = opts.excludeDirs ?? DEFAULT_EXCLUDE_DIRS;
944
+ const excludeSet = new Set(exclude);
945
+ async function walk(dir, depth) {
946
+ if (opts.maxDepth !== void 0 && depth > opts.maxDepth) return;
947
+ let entries;
948
+ try {
949
+ entries = await readdir(dir, { withFileTypes: true });
950
+ } catch {
951
+ return;
952
+ }
953
+ entries.sort((a, b) => a.name.localeCompare(b.name));
954
+ for (const entry of entries) {
955
+ if (excludeSet.has(entry.name)) continue;
956
+ const full = resolve3(dir, entry.name);
957
+ if (entry.isDirectory()) {
958
+ await walk(full, depth + 1);
959
+ } else if (entry.isFile() && matchesExtension(full, opts.extensions)) {
960
+ files.push(full);
961
+ }
962
+ }
963
+ }
964
+ await walk(root, 0);
965
+ return files;
966
+ }
967
+ function matchesExtension(p, exts) {
968
+ return exts.includes(extname2(p).toLowerCase());
969
+ }
970
+ export {
971
+ BoundedMap,
972
+ BoundedSet,
973
+ CREDENTIAL_PATTERNS,
974
+ UNSERIALIZABLE,
975
+ clearLocalBinCache,
976
+ cloneCredentialPatterns,
977
+ collectSourceFiles,
978
+ collectSourceFilesAsync,
979
+ createH1State,
980
+ findOnPath,
981
+ guardedMatcher,
982
+ isInsideProject,
983
+ locateRunnerEntry,
984
+ matchesExtension,
985
+ parseLlmJsonObject,
986
+ probeRunner,
987
+ releaseHandle,
988
+ releaseHandles,
989
+ resolveExecInvocation,
990
+ resolveFirstNodeBin,
991
+ resolveNodeBin,
992
+ resolveRunnerCommand,
993
+ resolveWin32Command,
994
+ runOptionalPluginCouncil,
995
+ runOptionalPluginLlm,
996
+ runRunnerCommand,
997
+ safeJsonStringify,
998
+ safePath,
999
+ sanitizeRunnerPath,
1000
+ stripOuterMarkdownFence,
1001
+ withReDoSGuard,
1002
+ withinProject
1003
+ };
1004
+ //# sourceMappingURL=runtime.js.map