@maximtop/opencode-debug-mode 0.1.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.
Files changed (71) hide show
  1. package/ATTRIBUTION.md +5 -0
  2. package/CONTRIBUTING.md +48 -0
  3. package/LICENSE +21 -0
  4. package/README.md +76 -0
  5. package/SECURITY.md +17 -0
  6. package/assets/debug-agent.md +71 -0
  7. package/dist/chunk-2PCBVVWX.js +20 -0
  8. package/dist/chunk-2PCBVVWX.js.map +1 -0
  9. package/dist/cleanup/export.d.ts +23 -0
  10. package/dist/cleanup/service.d.ts +33 -0
  11. package/dist/cleanup/types.d.ts +235 -0
  12. package/dist/collector/auth.d.ts +3 -0
  13. package/dist/collector/body.d.ts +7 -0
  14. package/dist/collector/ingest.d.ts +8 -0
  15. package/dist/collector/router.d.ts +8 -0
  16. package/dist/collector/server.d.ts +25 -0
  17. package/dist/core/clock.d.ts +5 -0
  18. package/dist/core/constants.d.ts +20 -0
  19. package/dist/core/errors.d.ts +12 -0
  20. package/dist/core/result.d.ts +24 -0
  21. package/dist/core/schemas.d.ts +9 -0
  22. package/dist/errors-IQTPGK5R.js +7 -0
  23. package/dist/errors-IQTPGK5R.js.map +1 -0
  24. package/dist/evidence/read.d.ts +8 -0
  25. package/dist/evidence/sanitize.d.ts +9 -0
  26. package/dist/evidence/store.d.ts +65 -0
  27. package/dist/evidence/types.d.ts +83 -0
  28. package/dist/index.d.ts +1 -0
  29. package/dist/index.js +3652 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/investigation/schema.d.ts +184 -0
  32. package/dist/investigation/store.d.ts +30 -0
  33. package/dist/plugin.d.ts +8 -0
  34. package/dist/probes/extension-permissions.d.ts +8 -0
  35. package/dist/probes/helper.d.ts +22 -0
  36. package/dist/probes/registry.d.ts +18 -0
  37. package/dist/probes/remove.d.ts +16 -0
  38. package/dist/probes/template.d.ts +21 -0
  39. package/dist/probes/types.d.ts +24 -0
  40. package/dist/process/line-decoder.d.ts +26 -0
  41. package/dist/process/protocol.d.ts +40 -0
  42. package/dist/process/service.d.ts +46 -0
  43. package/dist/process/tree.d.ts +16 -0
  44. package/dist/process-supervisor.js +276 -0
  45. package/dist/process-supervisor.js.map +1 -0
  46. package/dist/run/service.d.ts +21 -0
  47. package/dist/session/atomic-json.d.ts +1 -0
  48. package/dist/session/manifest-store.d.ts +22 -0
  49. package/dist/session/orphan-recovery.d.ts +16 -0
  50. package/dist/session/paths.d.ts +11 -0
  51. package/dist/session/registry.d.ts +48 -0
  52. package/dist/session/secret-store.d.ts +7 -0
  53. package/dist/session/types.d.ts +296 -0
  54. package/dist/tools/cleanup-tool.d.ts +4 -0
  55. package/dist/tools/collector-tools.d.ts +17 -0
  56. package/dist/tools/common.d.ts +4 -0
  57. package/dist/tools/evidence-tools.d.ts +4 -0
  58. package/dist/tools/index.d.ts +30 -0
  59. package/dist/tools/probe-tools.d.ts +5 -0
  60. package/dist/tools/run-tools.d.ts +12 -0
  61. package/dist/tools/session-tools.d.ts +4 -0
  62. package/dist/tools/state-tools.d.ts +4 -0
  63. package/docs/architecture.md +15 -0
  64. package/docs/lifecycle.md +11 -0
  65. package/examples/chrome-extension/background.js +5 -0
  66. package/examples/chrome-extension/content.js +3 -0
  67. package/examples/cli/run.mjs +3 -0
  68. package/examples/firefox-extension/background.js +4 -0
  69. package/examples/firefox-extension/content.js +3 -0
  70. package/examples/web/app.js +5 -0
  71. package/package.json +80 -0
package/dist/index.js ADDED
@@ -0,0 +1,3652 @@
1
+ import {
2
+ DebugModeError
3
+ } from "./chunk-2PCBVVWX.js";
4
+
5
+ // src/plugin.ts
6
+ import { readFile as readFile9 } from "fs/promises";
7
+ import { tmpdir } from "os";
8
+ import path13 from "path";
9
+
10
+ // src/cleanup/service.ts
11
+ import { spawn as spawn2 } from "child_process";
12
+ import { createHash as createHash3 } from "crypto";
13
+ import { readFile as readFile4, rm as rm2 } from "fs/promises";
14
+ import { performance as performance2 } from "perf_hooks";
15
+
16
+ // src/probes/extension-permissions.ts
17
+ import { readFile, writeFile } from "fs/promises";
18
+ import { applyEdits, modify, parse } from "jsonc-parser";
19
+ var LOOPBACK_MATCH = /^http:\/\/(?:127\.0\.0\.1:\d{1,5}|\[::1\]:\d{1,5})\/\*$/u;
20
+ var formatting = { tabSize: 2, insertSpaces: true, eol: "\n" };
21
+ function readManifest(text) {
22
+ const errors = [];
23
+ const value = parse(text, errors, { allowTrailingComma: true, disallowComments: false });
24
+ if (errors.length > 0 || typeof value !== "object" || value === null || Array.isArray(value)) {
25
+ throw new DebugModeError("PERMISSION_MISMATCH", "Extension manifest is invalid");
26
+ }
27
+ return value;
28
+ }
29
+ function permissionProperty(manifest) {
30
+ if (manifest.manifest_version === 2) return "permissions";
31
+ if (manifest.manifest_version === 3) return "host_permissions";
32
+ throw new DebugModeError("PERMISSION_MISMATCH", "Extension manifest version must be 2 or 3");
33
+ }
34
+ async function addLoopbackPermission(manifestPath, matchPattern) {
35
+ if (!LOOPBACK_MATCH.test(matchPattern)) {
36
+ throw new DebugModeError("PERMISSION_MISMATCH", "Only an exact active loopback match pattern is allowed");
37
+ }
38
+ const text = await readFile(manifestPath, "utf8");
39
+ const manifest = readManifest(text);
40
+ const property = permissionProperty(manifest);
41
+ const current = manifest[property];
42
+ if (current !== void 0 && (!Array.isArray(current) || current.some((entry) => typeof entry !== "string"))) {
43
+ throw new DebugModeError("PERMISSION_MISMATCH", `Extension ${property} must be a string array`);
44
+ }
45
+ const permissions = current ?? [];
46
+ if (permissions.includes(matchPattern)) {
47
+ return { manifestPath, property, matchPattern, addedBySession: false };
48
+ }
49
+ const edits = Array.isArray(current) ? modify(text, [property, permissions.length], matchPattern, {
50
+ formattingOptions: formatting,
51
+ isArrayInsertion: true
52
+ }) : modify(text, [property], [matchPattern], { formattingOptions: formatting });
53
+ await writeFile(manifestPath, applyEdits(text, edits), "utf8");
54
+ return { manifestPath, property, matchPattern, addedBySession: true };
55
+ }
56
+ async function removeLoopbackPermission(manifestPath, change) {
57
+ if (!change.addedBySession) return { status: "already-clean" };
58
+ let text;
59
+ try {
60
+ text = await readFile(manifestPath, "utf8");
61
+ } catch (error2) {
62
+ if (error2.code === "ENOENT") return { status: "already-clean" };
63
+ return { status: "failed", reason: "manifest-read-failed" };
64
+ }
65
+ let manifest;
66
+ try {
67
+ manifest = readManifest(text);
68
+ } catch {
69
+ return { status: "failed", reason: "manifest-invalid" };
70
+ }
71
+ if (permissionProperty(manifest) !== change.property) return { status: "failed", reason: "manifest-version-changed" };
72
+ const current = manifest[change.property];
73
+ if (current === void 0) return { status: "already-clean" };
74
+ if (!Array.isArray(current) || current.some((entry) => typeof entry !== "string")) {
75
+ return { status: "failed", reason: "permission-structure-changed" };
76
+ }
77
+ const matches2 = current.flatMap((entry, index2) => entry === change.matchPattern ? [index2] : []);
78
+ if (matches2.length === 0) return { status: "already-clean" };
79
+ if (matches2.length !== 1) return { status: "failed", reason: "permission-ambiguous" };
80
+ const index = matches2[0];
81
+ if (index === void 0) return { status: "already-clean" };
82
+ const edits = modify(text, [change.property, index], void 0, { formattingOptions: formatting });
83
+ await writeFile(manifestPath, applyEdits(text, edits), "utf8");
84
+ return { status: "success" };
85
+ }
86
+
87
+ // src/probes/remove.ts
88
+ import { createHash } from "crypto";
89
+ import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
90
+ function occurrences(value, needle) {
91
+ if (needle.length === 0) return 0;
92
+ return value.split(needle).length - 1;
93
+ }
94
+ function lineAt(value, index) {
95
+ return value.slice(0, index).split(/\r?\n/u).length;
96
+ }
97
+ async function removeOwnedProbe(probe) {
98
+ for (let attempt = 0; attempt < 2; attempt += 1) {
99
+ let source;
100
+ try {
101
+ source = await readFile2(probe.sourceFile, "utf8");
102
+ } catch (error2) {
103
+ if (error2.code === "ENOENT") return { status: "already-clean", file: probe.sourceFile };
104
+ return { status: "failed", file: probe.sourceFile, reason: "source-read-failed" };
105
+ }
106
+ const starts = occurrences(source, probe.markerStart);
107
+ const ends = occurrences(source, probe.markerEnd);
108
+ if (starts === 0 && ends === 0) return { status: "already-clean", file: probe.sourceFile };
109
+ const startIndex = source.indexOf(probe.markerStart);
110
+ if (starts !== 1 || ends !== 1 || startIndex < 0) {
111
+ return {
112
+ status: "failed",
113
+ file: probe.sourceFile,
114
+ reason: "marker-ambiguous",
115
+ ...startIndex < 0 ? {} : { line: lineAt(source, startIndex) }
116
+ };
117
+ }
118
+ if (probe.expectedBlock === void 0 || probe.expectedHash === void 0) {
119
+ return {
120
+ status: "failed",
121
+ file: probe.sourceFile,
122
+ reason: "marker-ownership-incomplete",
123
+ line: lineAt(source, startIndex)
124
+ };
125
+ }
126
+ if (occurrences(source, probe.expectedBlock) !== 1 || createHash("sha256").update(probe.expectedBlock).digest("hex") !== probe.expectedHash) {
127
+ return {
128
+ status: "failed",
129
+ file: probe.sourceFile,
130
+ reason: "marker-content-mismatch",
131
+ line: lineAt(source, startIndex)
132
+ };
133
+ }
134
+ const current = await readFile2(probe.sourceFile, "utf8");
135
+ if (current !== source) continue;
136
+ const blockIndex = source.indexOf(probe.expectedBlock);
137
+ const next = source.slice(0, blockIndex) + source.slice(blockIndex + probe.expectedBlock.length);
138
+ await writeFile2(probe.sourceFile, next, "utf8");
139
+ return { status: "success", file: probe.sourceFile };
140
+ }
141
+ return { status: "failed", file: probe.sourceFile, reason: "concurrent-source-change" };
142
+ }
143
+
144
+ // src/process/tree.ts
145
+ import { spawn } from "child_process";
146
+ import { performance } from "perf_hooks";
147
+
148
+ // src/core/constants.ts
149
+ var PACKAGE_ID = "opencode-debug-mode";
150
+ var MANIFEST_SCHEMA_VERSION = 1;
151
+ var STATE_SCHEMA_VERSION = 1;
152
+ var EVENT_SCHEMA_VERSION = 1;
153
+ var TEMP_BASE_NAME = "opencode-debug-mode-v1";
154
+ var PROCESS_EVENT_PREFIX = "__OPENCODE_DEBUG_EVENT_V1__";
155
+ var LIMITS = Object.freeze({
156
+ requestBytes: 64 * 1024,
157
+ scalarBytes: 8 * 1024,
158
+ events: 25e3,
159
+ evidenceBytes: 25 * 1024 * 1024,
160
+ checkpointBytes: 256 * 1024,
161
+ eventsPerBatch: 100,
162
+ idleMs: 30 * 60 * 1e3,
163
+ collectorReadyMs: 2e3,
164
+ cleanupMs: 5e3,
165
+ gracefulKillMs: 750,
166
+ forcedKillMs: 1500,
167
+ noSignalIterations: 3
168
+ });
169
+
170
+ // src/process/tree.ts
171
+ var defaultExecute = (executable, args) => new Promise((resolve, reject) => {
172
+ const child = spawn(executable, args, { shell: false, windowsHide: true, stdio: "ignore" });
173
+ child.once("error", reject);
174
+ child.once("exit", (exitCode) => resolve({ exitCode }));
175
+ });
176
+ function isAlive(pid) {
177
+ try {
178
+ process.kill(pid, 0);
179
+ return true;
180
+ } catch (error2) {
181
+ return error2.code !== "ESRCH";
182
+ }
183
+ }
184
+ async function waitForExit(pid, milliseconds) {
185
+ const deadline = performance.now() + milliseconds;
186
+ while (performance.now() < deadline) {
187
+ if (!isAlive(pid)) return true;
188
+ await new Promise((resolve) => setTimeout(resolve, 20));
189
+ }
190
+ return !isAlive(pid);
191
+ }
192
+ function safeError(error2) {
193
+ const code = error2.code;
194
+ return typeof code === "string" ? code.slice(0, 64) : "termination-failed";
195
+ }
196
+ async function terminateTree(targetPid, options = {}) {
197
+ const started = performance.now();
198
+ const errors = [];
199
+ let graceful = false;
200
+ let forced = false;
201
+ const platform = options.platform ?? process.platform;
202
+ const gracefulMs = options.gracefulMs ?? LIMITS.gracefulKillMs;
203
+ const forceMs = options.forceMs ?? LIMITS.forcedKillMs;
204
+ if (!Number.isInteger(targetPid) || targetPid <= 0) {
205
+ return {
206
+ graceful: false,
207
+ forced: false,
208
+ remaining: false,
209
+ durationMs: performance.now() - started,
210
+ errors: ["invalid-pid"]
211
+ };
212
+ }
213
+ if (platform === "win32") {
214
+ const execute = options.execute ?? defaultExecute;
215
+ try {
216
+ const result = await execute("taskkill", ["/PID", String(targetPid), "/T"]);
217
+ graceful = result.exitCode === 0;
218
+ } catch (error2) {
219
+ errors.push(safeError(error2));
220
+ }
221
+ await waitForExit(targetPid, gracefulMs);
222
+ try {
223
+ const result = await execute("taskkill", ["/PID", String(targetPid), "/T", "/F"]);
224
+ forced = result.exitCode === 0;
225
+ } catch (error2) {
226
+ errors.push(safeError(error2));
227
+ }
228
+ await waitForExit(targetPid, forceMs);
229
+ } else {
230
+ try {
231
+ process.kill(-targetPid, "SIGTERM");
232
+ graceful = true;
233
+ } catch (error2) {
234
+ if (error2.code === "ESRCH") {
235
+ return { graceful: false, forced: false, remaining: false, durationMs: performance.now() - started, errors };
236
+ }
237
+ errors.push(safeError(error2));
238
+ }
239
+ if (!await waitForExit(targetPid, gracefulMs)) {
240
+ try {
241
+ process.kill(-targetPid, "SIGKILL");
242
+ forced = true;
243
+ } catch (error2) {
244
+ if (error2.code !== "ESRCH") errors.push(safeError(error2));
245
+ }
246
+ await waitForExit(targetPid, forceMs);
247
+ }
248
+ }
249
+ return {
250
+ graceful,
251
+ forced,
252
+ remaining: isAlive(targetPid),
253
+ durationMs: performance.now() - started,
254
+ errors
255
+ };
256
+ }
257
+
258
+ // src/cleanup/export.ts
259
+ import { createHash as createHash2, randomBytes } from "crypto";
260
+ import { createReadStream } from "fs";
261
+ import { appendFile, mkdir as mkdir2, readdir, readFile as readFile3, realpath as realpath2, rename, rm, writeFile as writeFile3 } from "fs/promises";
262
+ import path2 from "path";
263
+ import { createInterface } from "readline";
264
+
265
+ // src/evidence/sanitize.ts
266
+ var MAX_DEPTH = 6;
267
+ var MAX_KEYS = 50;
268
+ var MAX_ARRAY = 100;
269
+ var SECRET_KEYS = /* @__PURE__ */ new Set([
270
+ "authorization",
271
+ "cookie",
272
+ "set-cookie",
273
+ "password",
274
+ "passwd",
275
+ "secret",
276
+ "token",
277
+ "access-token",
278
+ "refresh-token",
279
+ "api-key",
280
+ "apikey",
281
+ "private-key",
282
+ "client-secret"
283
+ ]);
284
+ function normalizeKey(key) {
285
+ return key.toLowerCase().replace(/[_\s]+/g, "-");
286
+ }
287
+ function truncateUtf8(value, maximumBytes) {
288
+ if (Buffer.byteLength(value) <= maximumBytes) return value;
289
+ return Buffer.from(value).subarray(0, maximumBytes).toString("utf8").replace(/\uFFFD$/u, "");
290
+ }
291
+ function estimateOriginalBytes(value) {
292
+ try {
293
+ const serialized = JSON.stringify(value);
294
+ return serialized === void 0 ? void 0 : Buffer.byteLength(serialized);
295
+ } catch {
296
+ return void 0;
297
+ }
298
+ }
299
+ function sanitizeEvidenceData(input) {
300
+ const flags = /* @__PURE__ */ new Set();
301
+ const seen = /* @__PURE__ */ new WeakSet();
302
+ let droppedKeys = 0;
303
+ const visit = (value2, depth) => {
304
+ if (depth > MAX_DEPTH) {
305
+ flags.add("truncated");
306
+ return "[TRUNCATED: depth]";
307
+ }
308
+ if (value2 === null) return null;
309
+ if (typeof value2 === "string") {
310
+ const truncated = truncateUtf8(value2, LIMITS.scalarBytes);
311
+ if (truncated !== value2) flags.add("truncated");
312
+ return truncated;
313
+ }
314
+ if (typeof value2 === "boolean") return value2;
315
+ if (typeof value2 === "number") {
316
+ if (Number.isFinite(value2)) return value2;
317
+ flags.add("unsupported");
318
+ return `[${String(value2)}]`;
319
+ }
320
+ if (typeof value2 === "bigint") {
321
+ flags.add("unsupported");
322
+ return `[BigInt ${truncateUtf8(value2.toString(), 128)}]`;
323
+ }
324
+ if (typeof value2 === "undefined") {
325
+ flags.add("unsupported");
326
+ return "[undefined]";
327
+ }
328
+ if (typeof value2 === "function" || typeof value2 === "symbol") {
329
+ flags.add("unsupported");
330
+ return `[${typeof value2}]`;
331
+ }
332
+ if (Buffer.isBuffer(value2) || ArrayBuffer.isView(value2) || value2 instanceof ArrayBuffer) {
333
+ flags.add("binary");
334
+ const length = Buffer.isBuffer(value2) ? value2.byteLength : value2 instanceof ArrayBuffer ? value2.byteLength : value2.byteLength;
335
+ return `[Binary ${length} bytes]`;
336
+ }
337
+ if (value2 instanceof Date) return Number.isNaN(value2.getTime()) ? "[Invalid Date]" : value2.toISOString();
338
+ if (value2 instanceof RegExp) {
339
+ flags.add("unsupported");
340
+ return truncateUtf8(value2.toString(), 256);
341
+ }
342
+ if (value2 instanceof Error) {
343
+ flags.add("unsupported");
344
+ return { name: truncateUtf8(value2.name, 128), message: truncateUtf8(value2.message, LIMITS.scalarBytes) };
345
+ }
346
+ if (seen.has(value2)) {
347
+ flags.add("cycle");
348
+ return "[CYCLE]";
349
+ }
350
+ seen.add(value2);
351
+ if (Array.isArray(value2)) {
352
+ if (value2.length > MAX_ARRAY) {
353
+ flags.add("truncated");
354
+ droppedKeys += value2.length - MAX_ARRAY;
355
+ }
356
+ return value2.slice(0, MAX_ARRAY).map((entry) => visit(entry, depth + 1));
357
+ }
358
+ const record = value2;
359
+ try {
360
+ if (typeof record.nodeType === "number" && typeof record.nodeName === "string") {
361
+ flags.add("unsupported");
362
+ return `[DOM ${truncateUtf8(record.nodeName, 128)}]`;
363
+ }
364
+ } catch {
365
+ flags.add("unsupported");
366
+ }
367
+ const prototype = Object.getPrototypeOf(value2);
368
+ if (prototype !== Object.prototype && prototype !== null) {
369
+ flags.add("unsupported");
370
+ return `[Unsupported ${truncateUtf8(value2.constructor?.name ?? "object", 128)}]`;
371
+ }
372
+ let keys;
373
+ try {
374
+ keys = Object.keys(record).sort();
375
+ } catch {
376
+ flags.add("unsupported");
377
+ return "[Unreadable object]";
378
+ }
379
+ if (keys.length > MAX_KEYS) {
380
+ flags.add("truncated");
381
+ droppedKeys += keys.length - MAX_KEYS;
382
+ keys = keys.slice(0, MAX_KEYS);
383
+ }
384
+ const result = {};
385
+ for (const key of keys) {
386
+ if (SECRET_KEYS.has(normalizeKey(key))) {
387
+ result[key] = "[REDACTED]";
388
+ flags.add("redacted");
389
+ continue;
390
+ }
391
+ try {
392
+ result[key] = visit(record[key], depth + 1);
393
+ } catch {
394
+ result[key] = "[Unreadable property]";
395
+ flags.add("unsupported");
396
+ }
397
+ }
398
+ return result;
399
+ };
400
+ const value = visit(input, 0);
401
+ const storedBytes = Buffer.byteLength(JSON.stringify(value));
402
+ const originalBytes = estimateOriginalBytes(input);
403
+ return {
404
+ value,
405
+ flags: [...flags].sort(),
406
+ droppedKeys,
407
+ ...originalBytes === void 0 ? {} : { originalBytes },
408
+ storedBytes
409
+ };
410
+ }
411
+
412
+ // src/evidence/types.ts
413
+ import { z as z2 } from "zod";
414
+
415
+ // src/core/schemas.ts
416
+ import { z } from "zod";
417
+ var OpaqueIdSchema = z.string().min(1).max(64).regex(/^[A-Za-z0-9_-]+$/);
418
+ var IsoTimestampSchema = z.string().datetime({ offset: true });
419
+ var HexSha256Schema = z.string().regex(/^[a-f0-9]{64}$/);
420
+ var RunLabelSchema = z.enum(["pre-fix", "post-fix"]);
421
+
422
+ // src/evidence/types.ts
423
+ var SourceLocationSchema = z2.object({
424
+ file: z2.string().min(1).max(LIMITS.scalarBytes),
425
+ line: z2.number().int().positive(),
426
+ column: z2.number().int().positive().optional()
427
+ }).strict();
428
+ var EventInputSchema = z2.object({
429
+ schemaVersion: z2.literal(EVENT_SCHEMA_VERSION),
430
+ sessionId: OpaqueIdSchema,
431
+ runId: OpaqueIdSchema,
432
+ runLabel: RunLabelSchema,
433
+ hypothesisId: OpaqueIdSchema,
434
+ probeId: OpaqueIdSchema,
435
+ timestamp: IsoTimestampSchema,
436
+ message: z2.string().min(1).max(LIMITS.scalarBytes),
437
+ source: SourceLocationSchema,
438
+ data: z2.unknown().optional()
439
+ }).strict();
440
+ var SanitizationFlagSchema = z2.enum(["redacted", "truncated", "cycle", "binary", "unsupported"]);
441
+ var EvidenceEventSchema = z2.object({
442
+ schemaVersion: z2.literal(EVENT_SCHEMA_VERSION),
443
+ eventId: OpaqueIdSchema,
444
+ receivedAt: IsoTimestampSchema,
445
+ timestamp: IsoTimestampSchema,
446
+ sessionId: OpaqueIdSchema,
447
+ runId: OpaqueIdSchema,
448
+ runLabel: RunLabelSchema,
449
+ hypothesisId: OpaqueIdSchema,
450
+ probeId: OpaqueIdSchema,
451
+ kind: z2.string().min(1).max(128),
452
+ message: z2.string().min(1).max(LIMITS.scalarBytes),
453
+ data: z2.unknown(),
454
+ source: SourceLocationSchema,
455
+ sanitization: z2.object({
456
+ flags: z2.array(SanitizationFlagSchema),
457
+ droppedKeys: z2.number().int().nonnegative(),
458
+ originalBytes: z2.number().int().nonnegative().optional(),
459
+ storedBytes: z2.number().int().nonnegative()
460
+ }).strict()
461
+ }).strict();
462
+
463
+ // src/investigation/schema.ts
464
+ import { z as z3 } from "zod";
465
+ var Text = z3.string().max(LIMITS.scalarBytes);
466
+ var TextList = (maximum) => z3.array(Text).max(maximum);
467
+ var EvidenceIds = z3.array(OpaqueIdSchema).max(500);
468
+ var HypothesisSchema = z3.object({
469
+ id: OpaqueIdSchema,
470
+ rank: z3.number().int().min(1).max(4),
471
+ statement: Text,
472
+ confirmationSignals: TextList(20),
473
+ eliminationSignals: TextList(20),
474
+ status: z3.enum(["open", "confirmed", "eliminated"]),
475
+ evidenceRefs: EvidenceIds,
476
+ invalidatedBy: Text.optional()
477
+ }).strict();
478
+ var CompletedCheckSchema = z3.object({
479
+ id: OpaqueIdSchema,
480
+ summary: Text,
481
+ interpretation: Text,
482
+ conclusive: z3.boolean(),
483
+ evidenceRefs: EvidenceIds,
484
+ completedAt: IsoTimestampSchema,
485
+ invalidatedBy: Text.optional()
486
+ }).strict();
487
+ var RunReferenceSchema = z3.object({
488
+ id: OpaqueIdSchema,
489
+ label: RunLabelSchema,
490
+ status: z3.enum(["planned", "running", "waiting", "completed", "failed", "timed_out", "cancelled"]),
491
+ evidenceRefs: EvidenceIds
492
+ }).strict();
493
+ var ProbeReferenceSchema = z3.object({
494
+ id: OpaqueIdSchema,
495
+ runId: OpaqueIdSchema,
496
+ hypothesisId: OpaqueIdSchema,
497
+ sourceFile: Text,
498
+ status: z3.enum(["planned", "registered", "validated", "active", "removed", "ambiguous"])
499
+ }).strict();
500
+ var DeveloperConfirmationSchema = z3.object({ id: OpaqueIdSchema, statement: Text, confirmedAt: IsoTimestampSchema }).strict();
501
+ var DecisionSchema = z3.object({ id: OpaqueIdSchema, summary: Text, evidenceRefs: EvidenceIds, decidedAt: IsoTimestampSchema }).strict();
502
+ var InvestigationStateSchema = z3.object({
503
+ schemaVersion: z3.literal(STATE_SCHEMA_VERSION),
504
+ revision: z3.number().int().nonnegative(),
505
+ updatedAt: IsoTimestampSchema,
506
+ problemSummary: Text,
507
+ expectedBehavior: Text,
508
+ actualBehavior: Text,
509
+ runtimeContext: z3.object({ kind: z3.enum(["cli", "web", "extension", "other"]), target: Text }).strict(),
510
+ reproduction: z3.object({ method: Text, requiresUser: z3.boolean(), confirmed: z3.boolean().nullable() }).strict(),
511
+ successCriteria: TextList(50),
512
+ phase: z3.enum([
513
+ "intake",
514
+ "hypotheses",
515
+ "baseline",
516
+ "instrumenting",
517
+ "waiting_for_reproduction",
518
+ "analyzing",
519
+ "fixing",
520
+ "verifying",
521
+ "cleaning",
522
+ "completed",
523
+ "abandoned",
524
+ "escalated"
525
+ ]),
526
+ loopIteration: z3.number().int().min(0).max(3),
527
+ singleCauseEvidenceRef: OpaqueIdSchema.nullable(),
528
+ hypotheses: z3.array(HypothesisSchema).max(4),
529
+ completedChecks: z3.array(CompletedCheckSchema).max(100),
530
+ runs: z3.array(RunReferenceSchema).max(20),
531
+ probeRefs: z3.array(ProbeReferenceSchema).max(100),
532
+ decidingEvidenceIds: EvidenceIds,
533
+ developerConfirmations: z3.array(DeveloperConfirmationSchema).max(100),
534
+ decisions: z3.array(DecisionSchema).max(100),
535
+ nextAction: Text,
536
+ instrumentedFiles: TextList(200),
537
+ fixedFiles: TextList(200),
538
+ cleanup: z3.object({
539
+ status: z3.enum(["not_started", "running", "complete", "partial"]),
540
+ completedResources: TextList(1e3)
541
+ }).strict()
542
+ }).strict();
543
+
544
+ // src/session/paths.ts
545
+ import { lstat, mkdir, mkdtemp, realpath } from "fs/promises";
546
+ import path from "path";
547
+ function isContained(parent, child) {
548
+ const relative = path.relative(parent, child);
549
+ return relative !== "" && !relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative);
550
+ }
551
+ async function createSessionPaths(tempBase, projectRoot) {
552
+ const absoluteBase = path.resolve(tempBase);
553
+ try {
554
+ const existing = await lstat(absoluteBase);
555
+ if (existing.isSymbolicLink()) throw new Error("Temporary base must not be a symbolic link");
556
+ if (!existing.isDirectory()) throw new Error("Temporary base must be a directory");
557
+ } catch (error2) {
558
+ if (error2.code !== "ENOENT") throw error2;
559
+ await mkdir(absoluteBase, { recursive: true, mode: 448 });
560
+ }
561
+ await realpath(absoluteBase);
562
+ const canonicalProject = await realpath(projectRoot);
563
+ const sessionDir = await mkdtemp(path.join(absoluteBase, "session-"));
564
+ if (!isContained(absoluteBase, sessionDir)) throw new Error("Created session directory escaped the temporary base");
565
+ return Object.freeze({
566
+ baseDir: absoluteBase,
567
+ sessionDir,
568
+ projectRoot: canonicalProject,
569
+ manifestFile: path.join(sessionDir, "manifest.json"),
570
+ secretFile: path.join(sessionDir, "secret.bin"),
571
+ stateFile: path.join(sessionDir, "investigation-state.json"),
572
+ evidenceFile: path.join(sessionDir, "evidence.ndjson")
573
+ });
574
+ }
575
+
576
+ // src/cleanup/types.ts
577
+ import { z as z4 } from "zod";
578
+ var ResourceCleanupResultSchema = z4.object({
579
+ status: z4.enum(["success", "already-clean", "skipped", "failed"]),
580
+ reason: z4.string().max(8192).optional(),
581
+ location: z4.string().max(8192).optional()
582
+ }).strict();
583
+ var CleanupResultSchema = z4.object({
584
+ status: z4.enum(["complete", "partial"]),
585
+ reason: z4.string().max(256),
586
+ resources: z4.object({
587
+ collector: ResourceCleanupResultSchema,
588
+ processes: z4.array(ResourceCleanupResultSchema),
589
+ probes: z4.array(ResourceCleanupResultSchema),
590
+ permissions: z4.array(ResourceCleanupResultSchema),
591
+ files: z4.array(ResourceCleanupResultSchema),
592
+ secret: ResourceCleanupResultSchema,
593
+ sessionDirectory: ResourceCleanupResultSchema
594
+ }).strict(),
595
+ remainingArtifacts: z4.array(z4.string().max(8192)),
596
+ durationMs: z4.number().nonnegative(),
597
+ cleanCheck: z4.object({
598
+ command: z4.string().max(8192),
599
+ exitCode: z4.number().int().nullable(),
600
+ timedOut: z4.boolean(),
601
+ durationMs: z4.number().nonnegative()
602
+ }).strict().optional(),
603
+ retainedArtifactLocation: z4.string().max(8192).optional()
604
+ }).strict();
605
+ var FinalReportInputSchema = z4.object({
606
+ outcome: z4.enum(["completed", "unresolved", "abandoned", "escalated"]),
607
+ rootCause: z4.string().max(8192),
608
+ decidingEvidence: z4.array(z4.string().max(8192)).max(100),
609
+ hypotheses: z4.array(
610
+ z4.object({
611
+ id: z4.string().max(64),
612
+ status: z4.enum(["open", "confirmed", "eliminated"]),
613
+ statement: z4.string().max(8192)
614
+ }).strict()
615
+ ).max(4),
616
+ fix: z4.string().max(8192),
617
+ changedFiles: z4.array(z4.string().max(8192)).max(200),
618
+ verification: z4.array(z4.string().max(8192)).max(100)
619
+ }).strict();
620
+ var FinalReportSchema = FinalReportInputSchema.extend({
621
+ cleanup: CleanupResultSchema,
622
+ retainedArtifactLocation: z4.string().max(8192).optional()
623
+ }).strict();
624
+
625
+ // src/cleanup/export.ts
626
+ function sha256(value) {
627
+ return createHash2("sha256").update(value).digest("hex");
628
+ }
629
+ function redactKnownSecrets(value, secrets) {
630
+ if (typeof value === "string") {
631
+ return secrets.reduce((text, secret) => secret.length === 0 ? text : text.replaceAll(secret, "[REDACTED]"), value);
632
+ }
633
+ if (Array.isArray(value)) return value.map((entry) => redactKnownSecrets(entry, secrets));
634
+ if (typeof value === "object" && value !== null) {
635
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, redactKnownSecrets(entry, secrets)]));
636
+ }
637
+ return value;
638
+ }
639
+ async function sanitizedEvidence(source, destination) {
640
+ let count = 0;
641
+ await writeFile3(destination, "", { mode: 384 });
642
+ const lines = createInterface({ input: createReadStream(source), crlfDelay: Number.POSITIVE_INFINITY });
643
+ for await (const line of lines) {
644
+ if (line.length === 0) continue;
645
+ const event = EvidenceEventSchema.parse(JSON.parse(line));
646
+ const sanitized = sanitizeEvidenceData(event.data);
647
+ await appendFile(
648
+ destination,
649
+ `${JSON.stringify({
650
+ ...event,
651
+ data: sanitized.value,
652
+ sanitization: {
653
+ flags: [.../* @__PURE__ */ new Set([...event.sanitization.flags, ...sanitized.flags])].sort(),
654
+ droppedKeys: event.sanitization.droppedKeys + sanitized.droppedKeys,
655
+ storedBytes: sanitized.storedBytes,
656
+ ...sanitized.originalBytes === void 0 ? {} : { originalBytes: sanitized.originalBytes }
657
+ }
658
+ })}
659
+ `,
660
+ { mode: 384 }
661
+ );
662
+ count += 1;
663
+ }
664
+ return count;
665
+ }
666
+ function renderReport(report, cleanup, retainedPath) {
667
+ const hypotheses = report.hypotheses.map((value) => `- ${value.id}: ${value.status} \u2014 ${value.statement}`).join("\n");
668
+ return `# Debug investigation report
669
+
670
+ Outcome: ${report.outcome}
671
+
672
+ ## Root cause
673
+
674
+ ${report.rootCause}
675
+
676
+ ## Deciding evidence
677
+
678
+ ${report.decidingEvidence.map((value) => `- ${value}`).join("\n")}
679
+
680
+ ## Hypotheses
681
+
682
+ ${hypotheses}
683
+
684
+ ## Fix
685
+
686
+ ${report.fix}
687
+
688
+ Changed files: ${report.changedFiles.join(", ")}
689
+
690
+ ## Verification
691
+
692
+ ${report.verification.map((value) => `- ${value}`).join("\n")}
693
+
694
+ ## Cleanup
695
+
696
+ Status: ${cleanup.status}
697
+
698
+ Retained artifact: ${retainedPath}
699
+ `;
700
+ }
701
+ async function stageRetainedBundle(input) {
702
+ if (!input.keepArtifacts || input.destination === void 0) {
703
+ throw new DebugModeError("DESTINATION_REQUIRED", "Explicit retention is not enabled");
704
+ }
705
+ let partialPath;
706
+ try {
707
+ const destination = await realpath2(input.destination);
708
+ const sessionDir = await realpath2(input.sessionDir);
709
+ if (destination === sessionDir || isContained(sessionDir, destination)) {
710
+ throw new DebugModeError("EXPORT_FAILED", "Retention destination cannot be inside the ephemeral session");
711
+ }
712
+ const suffix = randomBytes(8).toString("hex");
713
+ partialPath = path2.join(destination, `.partial-${PACKAGE_ID}-${suffix}`);
714
+ const finalPath = path2.join(
715
+ destination,
716
+ `${PACKAGE_ID}-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/gu, "-")}-${suffix}`
717
+ );
718
+ await mkdir2(partialPath, { mode: 448 });
719
+ const state = InvestigationStateSchema.parse(JSON.parse(await readFile3(input.stateFile, "utf8")));
720
+ const sanitizedState = InvestigationStateSchema.parse(
721
+ sanitizeEvidenceData(redactKnownSecrets(state, [input.token, ...input.securityValues ?? []])).value
722
+ );
723
+ await writeFile3(
724
+ path2.join(partialPath, "investigation-state.json"),
725
+ `${JSON.stringify(sanitizedState, null, 2)}
726
+ `,
727
+ {
728
+ mode: 384
729
+ }
730
+ );
731
+ const eventCount = await sanitizedEvidence(input.evidenceFile, path2.join(partialPath, "evidence.ndjson"));
732
+ return {
733
+ partialPath,
734
+ finalPath,
735
+ token: input.token,
736
+ securityValues: input.securityValues ?? [],
737
+ report: FinalReportInputSchema.parse(input.finalReport),
738
+ eventCount
739
+ };
740
+ } catch (error2) {
741
+ if (partialPath !== void 0) await rm(partialPath, { recursive: true, force: true }).catch(() => void 0);
742
+ if (error2 instanceof DebugModeError) throw error2;
743
+ throw new DebugModeError("EXPORT_FAILED", "Retained bundle could not be staged");
744
+ }
745
+ }
746
+ async function finalizeRetainedBundle(staged, cleanupInput) {
747
+ try {
748
+ const cleanup = CleanupResultSchema.parse(cleanupInput);
749
+ const report = renderReport(staged.report, cleanup, staged.finalPath);
750
+ await writeFile3(path2.join(staged.partialPath, "report.md"), report, { mode: 384 });
751
+ const publicFiles = ["evidence.ndjson", "investigation-state.json", "report.md"];
752
+ const files = {};
753
+ for (const name of publicFiles) {
754
+ const value = await readFile3(path2.join(staged.partialPath, name));
755
+ files[name] = { bytes: value.byteLength, sha256: sha256(value) };
756
+ }
757
+ const manifest = {
758
+ package: PACKAGE_ID,
759
+ schemaVersion: 1,
760
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
761
+ eventCount: staged.eventCount,
762
+ files
763
+ };
764
+ await writeFile3(path2.join(staged.partialPath, "bundle-manifest.json"), `${JSON.stringify(manifest, null, 2)}
765
+ `, {
766
+ mode: 384
767
+ });
768
+ for (const name of await readdir(staged.partialPath)) {
769
+ const text = await readFile3(path2.join(staged.partialPath, name), "utf8");
770
+ if ([staged.token, ...staged.securityValues].some((secret) => secret.length > 0 && text.includes(secret))) {
771
+ throw new DebugModeError("EXPORT_FAILED", "Retained bundle failed its secret scan");
772
+ }
773
+ }
774
+ await rename(staged.partialPath, staged.finalPath);
775
+ return { path: staged.finalPath };
776
+ } catch (error2) {
777
+ await rm(staged.partialPath, { recursive: true, force: true }).catch(() => void 0);
778
+ if (error2 instanceof DebugModeError) throw error2;
779
+ throw new DebugModeError("EXPORT_FAILED", "Retained bundle could not be finalized");
780
+ }
781
+ }
782
+
783
+ // src/cleanup/service.ts
784
+ var CleanupService = class {
785
+ constructor(session, dependencies = {}) {
786
+ this.session = session;
787
+ this.dependencies = dependencies;
788
+ }
789
+ session;
790
+ dependencies;
791
+ running;
792
+ completed;
793
+ async run(input) {
794
+ if (this.completed !== void 0) return this.completed;
795
+ this.running ??= this.execute(input);
796
+ this.completed = await this.running;
797
+ return this.completed;
798
+ }
799
+ async execute(input) {
800
+ const started = performance2.now();
801
+ const finalReport = FinalReportInputSchema.parse(input.finalReport);
802
+ const manifest = await this.session.manifestStore.modify((value) => ({
803
+ ...value,
804
+ status: "cleaning",
805
+ cleanup: { status: "running", completedResources: [] }
806
+ })).catch(() => this.session.manifestStore.read());
807
+ const state = await this.session.investigationStore.read().catch(() => void 0);
808
+ if (state !== void 0) {
809
+ await this.session.investigationStore.checkpoint(state.revision, {
810
+ ...state,
811
+ phase: "cleaning",
812
+ cleanup: { status: "running", completedResources: state.cleanup.completedResources }
813
+ }).catch(() => void 0);
814
+ }
815
+ let collector = { status: "skipped", reason: "not-running" };
816
+ if (manifest.collector !== null) {
817
+ if (this.dependencies.collector === void 0)
818
+ collector = { status: "failed", reason: "collector-runtime-unavailable" };
819
+ else {
820
+ try {
821
+ await this.dependencies.collector.close();
822
+ collector = { status: "success" };
823
+ } catch {
824
+ collector = { status: "failed", reason: "collector-close-failed" };
825
+ }
826
+ }
827
+ }
828
+ const processes = [];
829
+ for (const owned of manifest.processes) {
830
+ try {
831
+ if (this.dependencies.terminateProcess !== void 0)
832
+ processes.push(await this.dependencies.terminateProcess(owned));
833
+ else if (owned.targetPid !== void 0) {
834
+ const result2 = await terminateTree(owned.targetPid);
835
+ processes.push(result2.remaining ? { status: "failed", reason: "process-remains" } : { status: "success" });
836
+ } else processes.push({ status: "already-clean" });
837
+ } catch {
838
+ processes.push({ status: "failed", reason: "process-termination-failed" });
839
+ }
840
+ }
841
+ const probes = [];
842
+ for (const probe of manifest.probes) {
843
+ const result2 = await removeOwnedProbe(probe);
844
+ probes.push({
845
+ status: result2.status === "already-clean" ? "already-clean" : result2.status,
846
+ ...result2.reason === void 0 ? {} : { reason: result2.reason },
847
+ location: probe.sourceFile
848
+ });
849
+ }
850
+ const permissions = [];
851
+ for (const change of manifest.permissionChanges) {
852
+ const result2 = await removeLoopbackPermission(change.manifestPath, change);
853
+ permissions.push({
854
+ status: result2.status,
855
+ ...result2.reason === void 0 ? {} : { reason: result2.reason },
856
+ location: change.manifestPath
857
+ });
858
+ }
859
+ const files = [];
860
+ for (const owned of manifest.ownedFiles) files.push(await this.removeOwnedFile(owned));
861
+ const cleanCheck = input.cleanCheck === void 0 ? void 0 : await this.runCleanCheck(input.cleanCheck);
862
+ let staged;
863
+ if (manifest.keepArtifacts && manifest.retentionDestination !== void 0) {
864
+ try {
865
+ staged = await stageRetainedBundle({
866
+ keepArtifacts: true,
867
+ destination: manifest.retentionDestination,
868
+ sessionDir: manifest.sessionDir,
869
+ evidenceFile: this.session.paths.evidenceFile,
870
+ stateFile: this.session.paths.stateFile,
871
+ token: this.session.secret,
872
+ securityValues: this.dependencies.securityValues ?? [],
873
+ finalReport
874
+ });
875
+ } catch {
876
+ files.push({ status: "failed", reason: "retention-export-failed" });
877
+ }
878
+ }
879
+ let secret;
880
+ try {
881
+ const status = await (this.dependencies.removeSecret?.() ?? this.session.secretStore.remove());
882
+ secret = { status };
883
+ } catch {
884
+ secret = { status: "failed", reason: "secret-removal-failed" };
885
+ }
886
+ let sessionDirectory;
887
+ try {
888
+ await rm2(this.session.paths.sessionDir, { recursive: true, force: true });
889
+ sessionDirectory = { status: "success" };
890
+ } catch {
891
+ sessionDirectory = { status: "failed", reason: "session-directory-removal-failed" };
892
+ }
893
+ const resources = { collector, processes, probes, permissions, files, secret, sessionDirectory };
894
+ const failures = [collector, ...processes, ...probes, ...permissions, ...files, secret, sessionDirectory].filter(
895
+ (result2) => result2.status === "failed"
896
+ );
897
+ const remainingArtifacts = failures.flatMap((result2) => result2.location === void 0 ? [] : [result2.location]);
898
+ const result = {
899
+ status: failures.length === 0 ? "complete" : "partial",
900
+ reason: input.reason.slice(0, 256),
901
+ resources,
902
+ remainingArtifacts,
903
+ durationMs: performance2.now() - started,
904
+ ...cleanCheck === void 0 ? {} : { cleanCheck }
905
+ };
906
+ if (staged !== void 0) {
907
+ try {
908
+ const retained = await finalizeRetainedBundle(staged, result);
909
+ result.retainedArtifactLocation = retained.path;
910
+ } catch (error2) {
911
+ result.status = "partial";
912
+ result.resources.files.push({
913
+ status: "failed",
914
+ reason: error2 instanceof DebugModeError ? `retention-finalize-failed:${error2.code}:${error2.message}`.slice(0, 8192) : "retention-finalize-failed"
915
+ });
916
+ }
917
+ }
918
+ return result;
919
+ }
920
+ async removeOwnedFile(owned) {
921
+ try {
922
+ const content = await readFile4(owned.path);
923
+ if (createHash3("sha256").update(content).digest("hex") !== owned.sha256 || content.byteLength !== owned.bytes) {
924
+ return { status: "failed", reason: "owned-file-hash-mismatch", location: owned.path };
925
+ }
926
+ await rm2(owned.path);
927
+ return { status: "success", location: owned.path };
928
+ } catch (error2) {
929
+ if (error2.code === "ENOENT") return { status: "already-clean", location: owned.path };
930
+ return { status: "failed", reason: "owned-file-removal-failed", location: owned.path };
931
+ }
932
+ }
933
+ runCleanCheck(input) {
934
+ const started = performance2.now();
935
+ return new Promise((resolve) => {
936
+ const child = spawn2(input.executable, input.args, {
937
+ cwd: input.cwd,
938
+ shell: false,
939
+ stdio: "ignore",
940
+ windowsHide: true
941
+ });
942
+ let timedOut = false;
943
+ const timeout = setTimeout(() => {
944
+ timedOut = true;
945
+ child.kill("SIGKILL");
946
+ }, input.timeoutMs);
947
+ child.once("exit", (exitCode) => {
948
+ clearTimeout(timeout);
949
+ resolve({
950
+ command: [input.executable, ...input.args].join(" ").slice(0, 8192),
951
+ exitCode,
952
+ timedOut,
953
+ durationMs: performance2.now() - started
954
+ });
955
+ });
956
+ child.once("error", () => {
957
+ clearTimeout(timeout);
958
+ resolve({
959
+ command: input.executable.slice(0, 8192),
960
+ exitCode: null,
961
+ timedOut,
962
+ durationMs: performance2.now() - started
963
+ });
964
+ });
965
+ });
966
+ }
967
+ };
968
+
969
+ // src/collector/ingest.ts
970
+ import { randomBytes as randomBytes2 } from "crypto";
971
+ import { z as z5 } from "zod";
972
+
973
+ // src/collector/body.ts
974
+ var CollectorBodyError = class extends Error {
975
+ constructor(status, code) {
976
+ super(code);
977
+ this.status = status;
978
+ this.code = code;
979
+ }
980
+ status;
981
+ code;
982
+ };
983
+ async function readBoundedJsonBody(request) {
984
+ const contentType = request.headers["content-type"]?.split(";", 1)[0]?.trim().toLowerCase();
985
+ if (contentType !== "application/json") throw new CollectorBodyError(415, "UNSUPPORTED_MEDIA_TYPE");
986
+ const declared = request.headers["content-length"];
987
+ if (declared !== void 0) {
988
+ if (!/^\d+$/u.test(declared)) throw new CollectorBodyError(400, "INVALID_REQUEST");
989
+ if (Number(declared) > LIMITS.requestBytes) {
990
+ request.resume();
991
+ throw new CollectorBodyError(413, "LIMIT_EXCEEDED");
992
+ }
993
+ }
994
+ const chunks = [];
995
+ let bytes = 0;
996
+ for await (const chunk of request) {
997
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
998
+ bytes += buffer.byteLength;
999
+ if (bytes > LIMITS.requestBytes) {
1000
+ request.resume();
1001
+ throw new CollectorBodyError(413, "LIMIT_EXCEEDED");
1002
+ }
1003
+ chunks.push(buffer);
1004
+ }
1005
+ try {
1006
+ return JSON.parse(Buffer.concat(chunks, bytes).toString("utf8"));
1007
+ } catch {
1008
+ throw new CollectorBodyError(400, "INVALID_REQUEST");
1009
+ }
1010
+ }
1011
+
1012
+ // src/collector/auth.ts
1013
+ import { createHash as createHash4, timingSafeEqual } from "crypto";
1014
+ var TOKEN = /^[A-Za-z0-9_-]{43}$/;
1015
+ function decode(value) {
1016
+ if (!TOKEN.test(value)) return void 0;
1017
+ const decoded = Buffer.from(value, "base64url");
1018
+ if (decoded.byteLength !== 32 || decoded.toString("base64url") !== value) return void 0;
1019
+ return decoded;
1020
+ }
1021
+ function authenticateBearer(header, expectedToken) {
1022
+ if (header === void 0 || !header.startsWith("Bearer ") || header.indexOf(" ", 7) !== -1) return false;
1023
+ const provided = decode(header.slice(7));
1024
+ const expected = decode(expectedToken);
1025
+ if (provided === void 0 || expected === void 0) return false;
1026
+ const providedDigest = createHash4("sha256").update(provided).digest();
1027
+ const expectedDigest = createHash4("sha256").update(expected).digest();
1028
+ return timingSafeEqual(providedDigest, expectedDigest);
1029
+ }
1030
+
1031
+ // src/collector/router.ts
1032
+ var ERROR_MESSAGES = {
1033
+ INVALID_REQUEST: "Invalid request",
1034
+ UNAUTHORIZED: "Unauthorized",
1035
+ NOT_FOUND: "Not found",
1036
+ METHOD_NOT_ALLOWED: "Method not allowed",
1037
+ COLLECTOR_DRAINING: "Collector is draining"
1038
+ };
1039
+ function baseHeaders() {
1040
+ return {
1041
+ "Cache-Control": "no-store",
1042
+ "X-Content-Type-Options": "nosniff",
1043
+ "Content-Type": "application/json; charset=utf-8"
1044
+ };
1045
+ }
1046
+ function json(response, status, value, headers = {}) {
1047
+ response.writeHead(status, { ...baseHeaders(), ...headers });
1048
+ response.end(JSON.stringify(value));
1049
+ }
1050
+ function error(response, status, code, retryable = false, headers = {}) {
1051
+ json(response, status, { ok: false, error: { code, message: ERROR_MESSAGES[code], retryable } }, headers);
1052
+ }
1053
+ function validOrigin(value) {
1054
+ if (value.length > 2048 || /[\s\r\n]/u.test(value)) return false;
1055
+ return /^[A-Za-z][A-Za-z0-9+.-]*:\/\/[^/]+$/u.test(value);
1056
+ }
1057
+ function preflight(request, response) {
1058
+ const origin = request.headers.origin;
1059
+ const requestedMethod = request.headers["access-control-request-method"];
1060
+ const rawHeaders = request.headers["access-control-request-headers"] ?? "";
1061
+ if (origin === void 0 || !validOrigin(origin) || requestedMethod !== "POST" || typeof rawHeaders !== "string" || rawHeaders.length > 256) {
1062
+ error(response, 400, "INVALID_REQUEST");
1063
+ return;
1064
+ }
1065
+ const requestedHeaders = rawHeaders.split(",").map((value) => value.trim().toLowerCase()).filter(Boolean);
1066
+ if (requestedHeaders.some((value) => value !== "authorization" && value !== "content-type")) {
1067
+ error(response, 400, "INVALID_REQUEST");
1068
+ return;
1069
+ }
1070
+ response.writeHead(204, {
1071
+ ...baseHeaders(),
1072
+ "Access-Control-Allow-Origin": origin,
1073
+ "Access-Control-Allow-Methods": "POST",
1074
+ "Access-Control-Allow-Headers": "Authorization, Content-Type",
1075
+ "Access-Control-Max-Age": "600",
1076
+ Vary: "Origin"
1077
+ });
1078
+ response.end();
1079
+ }
1080
+ function createCollectorRouter(options) {
1081
+ return async (request, response, status) => {
1082
+ const rawUrl = request.url ?? "";
1083
+ let url;
1084
+ try {
1085
+ url = new URL(rawUrl, "http://loopback.invalid");
1086
+ } catch {
1087
+ error(response, 404, "NOT_FOUND");
1088
+ return;
1089
+ }
1090
+ if (url.search !== "") {
1091
+ error(response, 404, "NOT_FOUND");
1092
+ return;
1093
+ }
1094
+ const pathname = url.pathname;
1095
+ if (request.method === "OPTIONS") {
1096
+ if (pathname === "/v1/events") preflight(request, response);
1097
+ else error(response, 404, "NOT_FOUND");
1098
+ return;
1099
+ }
1100
+ const authorization = Array.isArray(request.headers.authorization) ? void 0 : request.headers.authorization;
1101
+ if (!authenticateBearer(authorization, options.token)) {
1102
+ error(response, 401, "UNAUTHORIZED");
1103
+ return;
1104
+ }
1105
+ await options.onAuthenticated?.();
1106
+ if (pathname === "/v1/health") {
1107
+ if (request.method !== "GET") {
1108
+ error(response, 405, "METHOD_NOT_ALLOWED", false, { Allow: "GET" });
1109
+ return;
1110
+ }
1111
+ json(response, 200, { ok: true, status: status() });
1112
+ return;
1113
+ }
1114
+ if (pathname === "/v1/events") {
1115
+ if (request.method !== "POST") {
1116
+ error(response, 405, "METHOD_NOT_ALLOWED", false, { Allow: "OPTIONS, POST" });
1117
+ return;
1118
+ }
1119
+ if (status() === "draining") {
1120
+ error(response, 429, "COLLECTOR_DRAINING", true);
1121
+ return;
1122
+ }
1123
+ if (options.ingest === void 0) {
1124
+ error(response, 400, "INVALID_REQUEST");
1125
+ return;
1126
+ }
1127
+ const origin = request.headers.origin;
1128
+ await options.ingest(request, response, typeof origin === "string" && validOrigin(origin) ? origin : void 0);
1129
+ return;
1130
+ }
1131
+ error(response, 404, "NOT_FOUND");
1132
+ };
1133
+ }
1134
+ function writeCollectorJson(response, status, value, origin) {
1135
+ json(response, status, value, origin === void 0 ? {} : { "Access-Control-Allow-Origin": origin, Vary: "Origin" });
1136
+ }
1137
+
1138
+ // src/collector/ingest.ts
1139
+ var EventBatchSchema = z5.object({ events: z5.array(EventInputSchema).min(1).max(LIMITS.eventsPerBatch) }).strict();
1140
+ function errorBody(code, message, retryable = false) {
1141
+ return { ok: false, error: { code, message, retryable } };
1142
+ }
1143
+ function createIngestHandler(options) {
1144
+ return async (request, response, origin) => {
1145
+ await options.evidence.countRequest();
1146
+ let body;
1147
+ try {
1148
+ body = await readBoundedJsonBody(request);
1149
+ } catch (error2) {
1150
+ if (error2 instanceof CollectorBodyError) {
1151
+ await options.evidence.recordRejected();
1152
+ writeCollectorJson(response, error2.status, errorBody(error2.code, error2.message), origin);
1153
+ return;
1154
+ }
1155
+ throw error2;
1156
+ }
1157
+ const parsed = EventBatchSchema.safeParse(body);
1158
+ if (!parsed.success) {
1159
+ await options.evidence.recordRejected();
1160
+ writeCollectorJson(response, 400, errorBody("INVALID_REQUEST", "Invalid event batch"), origin);
1161
+ return;
1162
+ }
1163
+ const validated = [];
1164
+ try {
1165
+ for (const event of parsed.data.events) validated.push(await options.validateEvent(event));
1166
+ } catch {
1167
+ await options.evidence.recordRejected(parsed.data.events.length);
1168
+ writeCollectorJson(response, 400, errorBody("INVALID_REQUEST", "Event ownership is invalid"), origin);
1169
+ return;
1170
+ }
1171
+ let accepted = 0;
1172
+ let sampled = 0;
1173
+ let dropped = 0;
1174
+ for (const event of validated) {
1175
+ const result = await options.evidence.append(
1176
+ {
1177
+ ...event,
1178
+ eventId: `event_${randomBytes2(16).toString("base64url")}`,
1179
+ kind: "probe"
1180
+ },
1181
+ { sampled: await options.sample?.(event) ?? false }
1182
+ );
1183
+ if (result.status === "accepted") accepted += 1;
1184
+ else if (result.status === "sampled") sampled += 1;
1185
+ else if (result.status === "dropped") dropped += 1;
1186
+ }
1187
+ writeCollectorJson(response, 202, { ok: true, accepted, sampled, dropped }, origin);
1188
+ };
1189
+ }
1190
+
1191
+ // src/collector/server.ts
1192
+ import { randomBytes as randomBytes3 } from "crypto";
1193
+ import { createServer } from "http";
1194
+ var CollectorServer = class {
1195
+ constructor(handler = (_request, response) => {
1196
+ response.writeHead(404, { "Content-Type": "application/json", "Cache-Control": "no-store" });
1197
+ response.end('{"ok":false,"error":{"code":"NOT_FOUND","message":"Not found","retryable":false}}');
1198
+ }, onFailure) {
1199
+ this.handler = handler;
1200
+ this.onFailure = onFailure;
1201
+ }
1202
+ handler;
1203
+ onFailure;
1204
+ server;
1205
+ sockets = /* @__PURE__ */ new Set();
1206
+ state = "stopped";
1207
+ handle;
1208
+ failureReported = false;
1209
+ async start() {
1210
+ if (this.handle !== void 0 && this.state === "ready") return this.handle;
1211
+ if (this.state !== "stopped") throw new DebugModeError("COLLECTOR_EXISTS", "A collector already exists");
1212
+ this.state = "starting";
1213
+ try {
1214
+ this.handle = await this.bind("127.0.0.1");
1215
+ } catch (error2) {
1216
+ const code = error2.code;
1217
+ if (code !== "EAFNOSUPPORT" && code !== "EADDRNOTAVAIL") {
1218
+ this.state = "failed";
1219
+ throw new DebugModeError("LOOPBACK_BIND_FAILED", "The IPv4 loopback collector could not bind");
1220
+ }
1221
+ try {
1222
+ this.handle = await this.bind("::1");
1223
+ } catch {
1224
+ this.state = "failed";
1225
+ throw new DebugModeError("LOOPBACK_BIND_FAILED", "Neither loopback address could bind");
1226
+ }
1227
+ }
1228
+ this.state = "ready";
1229
+ return this.handle;
1230
+ }
1231
+ get status() {
1232
+ return this.state;
1233
+ }
1234
+ async close() {
1235
+ if (this.state === "stopped") return;
1236
+ this.state = "draining";
1237
+ const server = this.server;
1238
+ if (server === void 0) {
1239
+ this.state = "stopped";
1240
+ return;
1241
+ }
1242
+ await Promise.race([
1243
+ new Promise((resolve) => server.close(() => resolve())),
1244
+ new Promise(
1245
+ (resolve) => setTimeout(() => {
1246
+ for (const socket of this.sockets) socket.destroy();
1247
+ server.closeAllConnections?.();
1248
+ resolve();
1249
+ }, 1e3)
1250
+ )
1251
+ ]);
1252
+ for (const socket of this.sockets) socket.destroy();
1253
+ this.sockets.clear();
1254
+ this.server = void 0;
1255
+ this.handle = void 0;
1256
+ this.state = "stopped";
1257
+ }
1258
+ bind(host) {
1259
+ return new Promise((resolve, reject) => {
1260
+ const server = createServer(
1261
+ {
1262
+ requestTimeout: 5e3,
1263
+ headersTimeout: 5e3,
1264
+ keepAliveTimeout: 1e3,
1265
+ maxHeaderSize: 16384,
1266
+ connectionsCheckingInterval: 1e3
1267
+ },
1268
+ (request, response) => {
1269
+ void Promise.resolve(
1270
+ this.handler(request, response, () => this.state === "draining" ? "draining" : "ready")
1271
+ ).catch(() => {
1272
+ if (!response.headersSent) response.writeHead(500, { "Content-Type": "application/json" });
1273
+ response.end('{"ok":false,"error":{"code":"INTERNAL_ERROR","message":"Request failed","retryable":false}}');
1274
+ });
1275
+ }
1276
+ );
1277
+ server.maxHeadersCount = 32;
1278
+ this.server = server;
1279
+ server.on("connection", (socket) => {
1280
+ this.sockets.add(socket);
1281
+ socket.once("close", () => this.sockets.delete(socket));
1282
+ });
1283
+ const startupTimeout = setTimeout(() => {
1284
+ cleanupStartup();
1285
+ server.close();
1286
+ const error2 = new Error("Collector startup timed out");
1287
+ error2.code = "ETIMEDOUT";
1288
+ reject(error2);
1289
+ }, LIMITS.collectorReadyMs);
1290
+ const startupError = (error2) => {
1291
+ cleanupStartup();
1292
+ server.close();
1293
+ reject(error2);
1294
+ };
1295
+ const listening = () => {
1296
+ cleanupStartup();
1297
+ const address = server.address();
1298
+ if (address === null || typeof address === "string") {
1299
+ reject(new Error("Collector returned no TCP address"));
1300
+ return;
1301
+ }
1302
+ const handle = Object.freeze({
1303
+ id: `collector_${randomBytes3(16).toString("base64url")}`,
1304
+ host,
1305
+ port: address.port,
1306
+ status: "ready",
1307
+ close: () => this.close()
1308
+ });
1309
+ server.on("error", () => void this.reportFailure("listener-error"));
1310
+ server.on("close", () => {
1311
+ if (this.state === "ready") void this.reportFailure("unexpected-close");
1312
+ });
1313
+ resolve(handle);
1314
+ };
1315
+ const cleanupStartup = () => {
1316
+ clearTimeout(startupTimeout);
1317
+ server.off("error", startupError);
1318
+ server.off("listening", listening);
1319
+ };
1320
+ server.once("error", startupError);
1321
+ server.once("listening", listening);
1322
+ server.listen({ host, port: 0, exclusive: true });
1323
+ });
1324
+ }
1325
+ async reportFailure(reason) {
1326
+ if (this.failureReported) return;
1327
+ this.failureReported = true;
1328
+ this.state = "failed";
1329
+ await this.onFailure?.(reason);
1330
+ }
1331
+ };
1332
+
1333
+ // src/core/clock.ts
1334
+ import { performance as performance3 } from "perf_hooks";
1335
+ var systemClock = Object.freeze({
1336
+ now: () => /* @__PURE__ */ new Date(),
1337
+ monotonicMs: () => performance3.now()
1338
+ });
1339
+
1340
+ // src/evidence/store.ts
1341
+ import { appendFile as appendFile2, stat } from "fs/promises";
1342
+ import { z as z7 } from "zod";
1343
+
1344
+ // src/session/types.ts
1345
+ import { z as z6 } from "zod";
1346
+ var EvidenceCountersSchema = z6.object({
1347
+ accepted: z6.number().int().nonnegative(),
1348
+ rejected: z6.number().int().nonnegative(),
1349
+ sampled: z6.number().int().nonnegative(),
1350
+ truncated: z6.number().int().nonnegative(),
1351
+ dropped: z6.number().int().nonnegative(),
1352
+ requests: z6.number().int().nonnegative()
1353
+ }).strict();
1354
+ var CollectorManifestSchema = z6.object({
1355
+ id: OpaqueIdSchema,
1356
+ host: z6.enum(["127.0.0.1", "::1"]),
1357
+ port: z6.number().int().min(1).max(65535),
1358
+ status: z6.enum(["starting", "ready", "draining", "stopped", "failed"]),
1359
+ startedAt: IsoTimestampSchema,
1360
+ stoppedAt: IsoTimestampSchema.optional()
1361
+ }).strict();
1362
+ var RunManifestSchema = z6.object({
1363
+ id: OpaqueIdSchema,
1364
+ label: RunLabelSchema,
1365
+ reproduction: z6.string().max(LIMITS.scalarBytes),
1366
+ status: z6.enum(["planned", "running", "waiting", "completed", "failed", "timed_out", "cancelled"]),
1367
+ createdAt: IsoTimestampSchema,
1368
+ completedAt: IsoTimestampSchema.optional()
1369
+ }).strict();
1370
+ var ProcessManifestSchema = z6.object({
1371
+ id: OpaqueIdSchema,
1372
+ runId: OpaqueIdSchema,
1373
+ commandSummary: z6.string().max(LIMITS.scalarBytes),
1374
+ supervisorPid: z6.number().int().positive().optional(),
1375
+ targetPid: z6.number().int().positive().optional(),
1376
+ ownerNonceHash: HexSha256Schema,
1377
+ status: z6.enum(["starting", "running", "exited", "timed_out", "cancelled", "terminated", "failed"]),
1378
+ startedAt: IsoTimestampSchema,
1379
+ completedAt: IsoTimestampSchema.optional(),
1380
+ exitCode: z6.number().int().nullable().optional(),
1381
+ signal: z6.string().max(64).nullable().optional()
1382
+ }).strict();
1383
+ var ProbeManifestSchema = z6.object({
1384
+ id: OpaqueIdSchema,
1385
+ runId: OpaqueIdSchema,
1386
+ hypothesisId: OpaqueIdSchema,
1387
+ sourceFile: z6.string().min(1),
1388
+ sourceLine: z6.number().int().positive(),
1389
+ sourceColumn: z6.number().int().positive().optional(),
1390
+ message: z6.string().min(1).max(LIMITS.scalarBytes),
1391
+ transport: z6.enum(["process", "http-web", "extension-background", "extension-content"]),
1392
+ captures: z6.array(z6.object({ label: z6.string().min(1).max(128), path: z6.string().min(1).max(512) }).strict()).max(20),
1393
+ sampling: z6.discriminatedUnion("mode", [
1394
+ z6.object({ mode: z6.literal("every"), n: z6.number().int().min(1).max(1e4) }).strict(),
1395
+ z6.object({ mode: z6.literal("aggregate"), windowMs: z6.number().int().min(100).max(6e4) }).strict()
1396
+ ]),
1397
+ status: z6.enum(["planned", "registered", "validated", "active", "removed", "ambiguous"]),
1398
+ validationStatus: z6.enum(["pending", "validated", "failed"]),
1399
+ markerStart: z6.string().min(1),
1400
+ markerEnd: z6.string().min(1),
1401
+ expectedBlock: z6.string().optional(),
1402
+ expectedHash: HexSha256Schema.optional()
1403
+ }).strict();
1404
+ var OwnedFileManifestSchema = z6.object({
1405
+ path: z6.string().min(1),
1406
+ sha256: HexSha256Schema,
1407
+ bytes: z6.number().int().nonnegative(),
1408
+ kind: z6.enum(["transport-helper", "temporary"])
1409
+ }).strict();
1410
+ var PermissionChangeSchema = z6.object({
1411
+ manifestPath: z6.string().min(1),
1412
+ property: z6.enum(["permissions", "host_permissions"]),
1413
+ matchPattern: z6.string().min(1),
1414
+ addedBySession: z6.boolean()
1415
+ }).strict();
1416
+ var CleanupProgressSchema = z6.object({
1417
+ status: z6.enum(["not_started", "running", "complete", "partial"]),
1418
+ completedResources: z6.array(z6.string().max(256)).max(1e4)
1419
+ }).strict();
1420
+ var ManifestSchema = z6.object({
1421
+ package: z6.literal(PACKAGE_ID),
1422
+ schemaVersion: z6.literal(MANIFEST_SCHEMA_VERSION),
1423
+ revision: z6.number().int().nonnegative(),
1424
+ sessionId: OpaqueIdSchema,
1425
+ trustedSessionHash: HexSha256Schema,
1426
+ projectRoot: z6.string().min(1),
1427
+ sessionDir: z6.string().min(1),
1428
+ status: z6.enum(["active", "cleaning", "cleaned", "partial"]),
1429
+ createdAt: IsoTimestampSchema,
1430
+ lastActivityAt: IsoTimestampSchema,
1431
+ expiresAt: IsoTimestampSchema,
1432
+ waitingForReproduction: z6.boolean(),
1433
+ keepArtifacts: z6.boolean(),
1434
+ retentionDestination: z6.string().min(1).optional(),
1435
+ collector: CollectorManifestSchema.nullable(),
1436
+ runs: z6.array(RunManifestSchema),
1437
+ processes: z6.array(ProcessManifestSchema),
1438
+ probes: z6.array(ProbeManifestSchema),
1439
+ ownedFiles: z6.array(OwnedFileManifestSchema),
1440
+ permissionChanges: z6.array(PermissionChangeSchema),
1441
+ counters: EvidenceCountersSchema,
1442
+ cleanup: CleanupProgressSchema
1443
+ }).strict();
1444
+
1445
+ // src/evidence/read.ts
1446
+ import { createReadStream as createReadStream2 } from "fs";
1447
+ function matches(event, filter) {
1448
+ if (filter.sessionId !== void 0 && event.sessionId !== filter.sessionId) return false;
1449
+ if (filter.runId !== void 0 && event.runId !== filter.runId) return false;
1450
+ if (filter.hypothesisId !== void 0 && event.hypothesisId !== filter.hypothesisId) return false;
1451
+ if (filter.probeId !== void 0 && event.probeId !== filter.probeId) return false;
1452
+ if (filter.from !== void 0 && event.timestamp < filter.from) return false;
1453
+ if (filter.to !== void 0 && event.timestamp > filter.to) return false;
1454
+ if (filter.keyword !== void 0 && !JSON.stringify(event).toLowerCase().includes(filter.keyword.toLowerCase()))
1455
+ return false;
1456
+ return true;
1457
+ }
1458
+ async function readEvidence(filename, filter = {}) {
1459
+ const limit = Math.min(Math.max(filter.limit ?? 100, 1), 100);
1460
+ const start = filter.cursor === void 0 ? 0 : Number(filter.cursor);
1461
+ if (!Number.isSafeInteger(start) || start < 0) throw new Error("Evidence cursor is invalid");
1462
+ const events = [];
1463
+ let invalidLines = 0;
1464
+ let trailingPartialLine = false;
1465
+ let buffer = Buffer.alloc(0);
1466
+ let offset = start;
1467
+ let nextCursor = null;
1468
+ const stream = createReadStream2(filename, { start });
1469
+ try {
1470
+ for await (const chunk of stream) {
1471
+ buffer = Buffer.concat([buffer, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)]);
1472
+ while (true) {
1473
+ const newline = buffer.indexOf(10);
1474
+ if (newline < 0) break;
1475
+ const line = buffer.subarray(0, newline);
1476
+ buffer = buffer.subarray(newline + 1);
1477
+ offset += newline + 1;
1478
+ if (line.byteLength === 0) continue;
1479
+ try {
1480
+ const event = EvidenceEventSchema.parse(JSON.parse(line.toString("utf8")));
1481
+ if (matches(event, filter)) {
1482
+ events.push(event);
1483
+ if (events.length >= limit) {
1484
+ nextCursor = String(offset);
1485
+ stream.destroy();
1486
+ return { events, nextCursor, trailingPartialLine: false, invalidLines };
1487
+ }
1488
+ }
1489
+ } catch {
1490
+ invalidLines += 1;
1491
+ }
1492
+ }
1493
+ }
1494
+ trailingPartialLine = buffer.byteLength > 0;
1495
+ } catch (error2) {
1496
+ if (error2.code !== "ENOENT") throw error2;
1497
+ } finally {
1498
+ stream.destroy();
1499
+ }
1500
+ return { events, nextCursor, trailingPartialLine, invalidLines };
1501
+ }
1502
+
1503
+ // src/evidence/store.ts
1504
+ var AppendEventSchema = EvidenceEventSchema.omit({ receivedAt: true, sanitization: true, data: true }).extend({
1505
+ schemaVersion: z7.literal(EVENT_SCHEMA_VERSION),
1506
+ data: z7.unknown().optional()
1507
+ });
1508
+ var EvidenceStore = class {
1509
+ constructor(filename, onCounters, clock = systemClock, loadCounters) {
1510
+ this.filename = filename;
1511
+ this.onCounters = onCounters;
1512
+ this.clock = clock;
1513
+ this.loadCounters = loadCounters;
1514
+ }
1515
+ filename;
1516
+ onCounters;
1517
+ clock;
1518
+ loadCounters;
1519
+ tail = Promise.resolve();
1520
+ currentBytes = 0;
1521
+ initialized = false;
1522
+ counters = {
1523
+ accepted: 0,
1524
+ rejected: 0,
1525
+ sampled: 0,
1526
+ truncated: 0,
1527
+ dropped: 0,
1528
+ requests: 0
1529
+ };
1530
+ async append(input, options = {}) {
1531
+ return this.exclusive(async () => {
1532
+ await this.initialize();
1533
+ if (options.sampled === true) {
1534
+ await this.increment("sampled");
1535
+ return { status: "sampled" };
1536
+ }
1537
+ const parsed = AppendEventSchema.safeParse(input);
1538
+ if (!parsed.success) {
1539
+ await this.increment("rejected");
1540
+ return { status: "rejected" };
1541
+ }
1542
+ if (this.counters.accepted >= LIMITS.events) {
1543
+ await this.increment("dropped");
1544
+ return { status: "dropped" };
1545
+ }
1546
+ const sanitized = sanitizeEvidenceData(parsed.data.data);
1547
+ const event = EvidenceEventSchema.parse({
1548
+ ...parsed.data,
1549
+ data: sanitized.value,
1550
+ receivedAt: this.clock.now().toISOString(),
1551
+ sanitization: {
1552
+ flags: sanitized.flags,
1553
+ droppedKeys: sanitized.droppedKeys,
1554
+ storedBytes: sanitized.storedBytes,
1555
+ ...sanitized.originalBytes === void 0 ? {} : { originalBytes: sanitized.originalBytes }
1556
+ }
1557
+ });
1558
+ const line = `${JSON.stringify(event)}
1559
+ `;
1560
+ const bytes = Buffer.byteLength(line);
1561
+ if (this.currentBytes + bytes > LIMITS.evidenceBytes) {
1562
+ await this.increment("dropped");
1563
+ return { status: "dropped" };
1564
+ }
1565
+ try {
1566
+ await appendFile2(this.filename, line, { encoding: "utf8", mode: 384 });
1567
+ } catch (error2) {
1568
+ await this.increment("rejected");
1569
+ throw error2;
1570
+ }
1571
+ this.currentBytes += bytes;
1572
+ await this.increment("accepted");
1573
+ if (sanitized.flags.includes("truncated")) await this.increment("truncated");
1574
+ return { status: "accepted", event };
1575
+ });
1576
+ }
1577
+ async read(filter = {}) {
1578
+ await this.initialize();
1579
+ const page = await readEvidence(this.filename, filter);
1580
+ return { ...page, counters: EvidenceCountersSchema.parse(this.counters) };
1581
+ }
1582
+ snapshotCounters() {
1583
+ return EvidenceCountersSchema.parse(this.counters);
1584
+ }
1585
+ async countRequest() {
1586
+ await this.exclusive(async () => {
1587
+ await this.initialize();
1588
+ await this.increment("requests");
1589
+ });
1590
+ }
1591
+ async recordRejected(count = 1) {
1592
+ await this.exclusive(async () => {
1593
+ await this.initialize();
1594
+ for (let index = 0; index < count; index += 1) await this.increment("rejected");
1595
+ });
1596
+ }
1597
+ async initialize() {
1598
+ if (this.initialized) return;
1599
+ if (this.loadCounters !== void 0) {
1600
+ Object.assign(this.counters, EvidenceCountersSchema.parse(await this.loadCounters()));
1601
+ }
1602
+ try {
1603
+ this.currentBytes = (await stat(this.filename)).size;
1604
+ } catch (error2) {
1605
+ if (error2.code !== "ENOENT") throw error2;
1606
+ }
1607
+ this.initialized = true;
1608
+ }
1609
+ async increment(field) {
1610
+ this.counters[field] += 1;
1611
+ await this.onCounters?.(EvidenceCountersSchema.parse(this.counters));
1612
+ }
1613
+ async exclusive(operation) {
1614
+ const previous = this.tail;
1615
+ let release;
1616
+ this.tail = new Promise((resolve) => {
1617
+ release = resolve;
1618
+ });
1619
+ await previous;
1620
+ try {
1621
+ return await operation();
1622
+ } finally {
1623
+ release();
1624
+ }
1625
+ }
1626
+ };
1627
+
1628
+ // src/probes/helper.ts
1629
+ import { createHash as createHash5 } from "crypto";
1630
+ import { mkdir as mkdir3, realpath as realpath3, writeFile as writeFile4 } from "fs/promises";
1631
+ import path3 from "path";
1632
+ function helperSource(options) {
1633
+ const listener = options.extensionBackground ? `
1634
+ const runtime = globalThis.browser?.runtime ?? globalThis.chrome?.runtime
1635
+ runtime?.onMessage?.addListener((message) => {
1636
+ if (message?.type === "opencode-debug-event") __opencodeDebugEmit(message.event)
1637
+ })
1638
+ ` : "";
1639
+ return `const endpoint = ${JSON.stringify(options.endpoint)}
1640
+ const authorization = ${JSON.stringify(`Bearer ${options.token}`)}
1641
+ const queue = []
1642
+ let sending = false
1643
+ let dropped = 0
1644
+
1645
+ function bound(value, depth = 0) {
1646
+ if (depth > 6) return "[TRUNCATED]"
1647
+ if (typeof value === "string") return value.slice(0, 8192)
1648
+ if (Array.isArray(value)) return value.slice(0, 100).map((item) => bound(item, depth + 1))
1649
+ if (value && typeof value === "object") {
1650
+ return Object.fromEntries(Object.keys(value).sort().slice(0, 50).map((key) => [key, bound(value[key], depth + 1)]))
1651
+ }
1652
+ return value
1653
+ }
1654
+
1655
+ export function __opencodeDebugEmit(event) {
1656
+ if (queue.length >= 100) { dropped += 1; return }
1657
+ queue.push(bound(event))
1658
+ void flush()
1659
+ }
1660
+
1661
+ async function flush() {
1662
+ if (sending) return
1663
+ sending = true
1664
+ try {
1665
+ while (queue.length > 0) {
1666
+ const events = queue.splice(0, 100)
1667
+ if (dropped > 0) { events.push({ ...events[0], message: "dropped events", data: { dropped } }); dropped = 0 }
1668
+ const response = await fetch(endpoint, {
1669
+ method: "POST",
1670
+ credentials: "omit",
1671
+ headers: { Authorization: authorization, "Content-Type": "application/json" },
1672
+ body: JSON.stringify({ events }),
1673
+ })
1674
+ if (!response.ok) break
1675
+ }
1676
+ } catch {
1677
+ // Runtime evidence transport is best effort and never affects target behavior.
1678
+ } finally {
1679
+ sending = false
1680
+ }
1681
+ }
1682
+ ${listener}`;
1683
+ }
1684
+ var TransportHelper = class {
1685
+ constructor(projectRoot, recordOwnedFile) {
1686
+ this.projectRoot = projectRoot;
1687
+ this.recordOwnedFile = recordOwnedFile;
1688
+ }
1689
+ projectRoot;
1690
+ recordOwnedFile;
1691
+ async create(options) {
1692
+ const canonicalRoot = await realpath3(this.projectRoot);
1693
+ const absoluteTarget = path3.resolve(canonicalRoot, options.targetPath);
1694
+ if (!isContained(canonicalRoot, absoluteTarget)) {
1695
+ throw new DebugModeError("HELPER_PATH_UNSAFE", "Transport helper must remain inside the project");
1696
+ }
1697
+ const parent = path3.dirname(absoluteTarget);
1698
+ await mkdir3(parent, { recursive: true });
1699
+ if (await realpath3(parent) !== parent) {
1700
+ throw new DebugModeError("HELPER_PATH_UNSAFE", "Transport helper parent is not canonical");
1701
+ }
1702
+ const endpointHost = options.host === "::1" ? "[::1]" : options.host;
1703
+ const source = helperSource({
1704
+ endpoint: `http://${endpointHost}:${options.port}/v1/events`,
1705
+ token: options.token,
1706
+ extensionBackground: options.runtime === "extension-background"
1707
+ });
1708
+ await writeFile4(absoluteTarget, source, { flag: "wx", mode: 384 });
1709
+ const bytes = Buffer.byteLength(source);
1710
+ const sha2563 = createHash5("sha256").update(source).digest("hex");
1711
+ await this.recordOwnedFile?.({ path: absoluteTarget, sha256: sha2563, bytes });
1712
+ const relativePath = `./${path3.relative(canonicalRoot, absoluteTarget).split(path3.sep).join("/")}`;
1713
+ return {
1714
+ relativePath,
1715
+ requiredImport: `import { __opencodeDebugEmit } from ${JSON.stringify(relativePath)}`,
1716
+ sha256: sha2563,
1717
+ bytes
1718
+ };
1719
+ }
1720
+ };
1721
+
1722
+ // src/probes/registry.ts
1723
+ import { createHash as createHash6, randomBytes as randomBytes4 } from "crypto";
1724
+ import { readFile as readFile5, realpath as realpath4 } from "fs/promises";
1725
+ import path4 from "path";
1726
+
1727
+ // src/probes/types.ts
1728
+ var SAFE_CAPTURE = /^[A-Za-z_$][\w$]*(?:(?:\.|\?\.)[A-Za-z_$][\w$]*)*$/;
1729
+
1730
+ // src/probes/template.ts
1731
+ function createProbeTemplate(input) {
1732
+ if (input.captures.some((capture) => !SAFE_CAPTURE.test(capture.path))) {
1733
+ throw new DebugModeError("UNSAFE_CAPTURE", "Probe capture path is unsafe");
1734
+ }
1735
+ const ownership = `opencode-debug-mode session=${input.sessionId} run=${input.runId} hypothesis=${input.hypothesisId} probe=${input.probeId}`;
1736
+ const data = input.captures.map((capture) => `${JSON.stringify(capture.label)}: ${capture.path}`).join(", ");
1737
+ const event = `{
1738
+ schemaVersion: 1,
1739
+ sessionId: ${JSON.stringify(input.sessionId)},
1740
+ runId: ${JSON.stringify(input.runId)},
1741
+ runLabel: ${JSON.stringify(input.runLabel)},
1742
+ hypothesisId: ${JSON.stringify(input.hypothesisId)},
1743
+ probeId: ${JSON.stringify(input.probeId)},
1744
+ timestamp: new Date().toISOString(),
1745
+ message: ${JSON.stringify(input.message)},
1746
+ source: { file: ${JSON.stringify(input.sourceFile)}, line: ${input.sourceLine} },
1747
+ data: { ${data} },
1748
+ }`;
1749
+ let emission;
1750
+ if (input.transport === "process") {
1751
+ emission = `void ((event) => process.stderr.write(${JSON.stringify(PROCESS_EVENT_PREFIX)} + JSON.stringify(event) + "\\n"))(${event})`;
1752
+ } else if (input.transport === "extension-content") {
1753
+ const adapter = input.contentAdapter ?? "chrome.runtime.sendMessage";
1754
+ emission = `void ${adapter}({ type: "opencode-debug-event", event: ${event} })`;
1755
+ } else {
1756
+ emission = `void __opencodeDebugEmit(${event})`;
1757
+ }
1758
+ return {
1759
+ markerBlock: `/* DEBUG-START ${ownership} */
1760
+ ${emission}
1761
+ /* DEBUG-END ${ownership} */`
1762
+ };
1763
+ }
1764
+
1765
+ // src/probes/registry.ts
1766
+ var SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"]);
1767
+ function probeId() {
1768
+ return `probe_${randomBytes4(16).toString("base64url")}`;
1769
+ }
1770
+ function sha2562(value) {
1771
+ return createHash6("sha256").update(value).digest("hex");
1772
+ }
1773
+ function markerLines(sessionId, input, id) {
1774
+ const ownership = `opencode-debug-mode session=${sessionId} run=${input.runId} hypothesis=${input.hypothesisId} probe=${id}`;
1775
+ return { start: `/* DEBUG-START ${ownership} */`, end: `/* DEBUG-END ${ownership} */` };
1776
+ }
1777
+ var ProbeRegistry = class {
1778
+ constructor(store, projectRoot, hasHypothesis) {
1779
+ this.store = store;
1780
+ this.projectRoot = projectRoot;
1781
+ this.hasHypothesis = hasHypothesis;
1782
+ }
1783
+ store;
1784
+ projectRoot;
1785
+ hasHypothesis;
1786
+ async plan(input) {
1787
+ const manifest = await this.store.read();
1788
+ if (!manifest.runs.some((run2) => run2.id === input.runId))
1789
+ throw new DebugModeError("RUN_NOT_FOUND", "Run was not found");
1790
+ if (!await this.hasHypothesis(input.hypothesisId)) {
1791
+ throw new DebugModeError("STATE_INVALID", "Hypothesis was not found in the checkpoint");
1792
+ }
1793
+ if (!Number.isInteger(input.sourceLine) || input.sourceLine < 1) {
1794
+ throw new DebugModeError("MARKER_MISMATCH", "Source line must be one-based");
1795
+ }
1796
+ if (input.message.length < 1 || Buffer.byteLength(input.message) > 8192) {
1797
+ throw new DebugModeError("MARKER_MISMATCH", "Probe message is invalid");
1798
+ }
1799
+ if (input.captures.length > 20 || input.captures.some((capture) => !SAFE_CAPTURE.test(capture.path))) {
1800
+ throw new DebugModeError("UNSAFE_CAPTURE", "Probe capture path is unsafe");
1801
+ }
1802
+ const absoluteSource = path4.resolve(this.projectRoot, input.sourceFile);
1803
+ if (!isContained(this.projectRoot, absoluteSource)) {
1804
+ throw new DebugModeError("HELPER_PATH_UNSAFE", "Probe source is outside the project");
1805
+ }
1806
+ if (!SUPPORTED_EXTENSIONS.has(path4.extname(absoluteSource).toLowerCase())) {
1807
+ throw new DebugModeError("UNSUPPORTED_LANGUAGE", "Only JavaScript and TypeScript probes are supported");
1808
+ }
1809
+ const canonicalSource = await realpath4(absoluteSource);
1810
+ if (!isContained(this.projectRoot, canonicalSource)) {
1811
+ throw new DebugModeError("HELPER_PATH_UNSAFE", "Probe source resolves outside the project");
1812
+ }
1813
+ const id = probeId();
1814
+ const markers = markerLines(manifest.sessionId, input, id);
1815
+ const run = manifest.runs.find((candidate) => candidate.id === input.runId);
1816
+ if (run === void 0) throw new DebugModeError("RUN_NOT_FOUND", "Run was not found");
1817
+ const markerBlock = input.markerBlock ?? createProbeTemplate({
1818
+ sessionId: manifest.sessionId,
1819
+ runId: input.runId,
1820
+ runLabel: run.label,
1821
+ hypothesisId: input.hypothesisId,
1822
+ probeId: id,
1823
+ sourceFile: path4.relative(this.projectRoot, canonicalSource),
1824
+ sourceLine: input.sourceLine,
1825
+ message: input.message,
1826
+ captures: input.captures,
1827
+ transport: input.transport,
1828
+ sampling: input.sampling
1829
+ }).markerBlock;
1830
+ const probe = {
1831
+ id,
1832
+ runId: input.runId,
1833
+ hypothesisId: input.hypothesisId,
1834
+ sourceFile: canonicalSource,
1835
+ sourceLine: input.sourceLine,
1836
+ ...input.sourceColumn === void 0 ? {} : { sourceColumn: input.sourceColumn },
1837
+ message: input.message,
1838
+ transport: input.transport,
1839
+ captures: input.captures,
1840
+ sampling: input.sampling,
1841
+ status: "planned",
1842
+ validationStatus: "pending",
1843
+ markerStart: markers.start,
1844
+ markerEnd: markers.end,
1845
+ expectedBlock: markerBlock
1846
+ };
1847
+ await this.update(manifest, (value) => ({ ...value, probes: [...value.probes, probe] }));
1848
+ return { ...probe, markerBlock };
1849
+ }
1850
+ async register(id) {
1851
+ const manifest = await this.store.read();
1852
+ const probe = manifest.probes.find((candidate) => candidate.id === id);
1853
+ if (probe === void 0) throw new DebugModeError("MARKER_MISSING", "Probe was not planned");
1854
+ if (probe.expectedBlock === void 0)
1855
+ throw new DebugModeError("MARKER_MISMATCH", "Probe marker content is unavailable");
1856
+ const source = await readFile5(probe.sourceFile, "utf8");
1857
+ const occurrences2 = source.split(probe.expectedBlock).length - 1;
1858
+ if (occurrences2 === 0) throw new DebugModeError("MARKER_MISSING", "Owned probe marker is missing");
1859
+ if (occurrences2 !== 1) throw new DebugModeError("MARKER_MISMATCH", "Owned probe marker is not unique");
1860
+ const updated = {
1861
+ ...probe,
1862
+ expectedHash: sha2562(probe.expectedBlock),
1863
+ status: "registered",
1864
+ validationStatus: "pending"
1865
+ };
1866
+ await this.update(manifest, (value) => ({
1867
+ ...value,
1868
+ probes: value.probes.map((candidate) => candidate.id === id ? updated : candidate)
1869
+ }));
1870
+ return updated;
1871
+ }
1872
+ async validate(probeIds) {
1873
+ const manifest = await this.store.read();
1874
+ if (probeIds.some((id) => !manifest.probes.some((probe) => probe.id === id && probe.status === "registered"))) {
1875
+ throw new DebugModeError("MARKER_MISMATCH", "Only registered probes can be validated");
1876
+ }
1877
+ await this.update(manifest, (value) => ({
1878
+ ...value,
1879
+ probes: value.probes.map(
1880
+ (probe) => probeIds.includes(probe.id) ? { ...probe, status: "validated", validationStatus: "validated" } : probe
1881
+ )
1882
+ }));
1883
+ }
1884
+ async requireValidatedForRun(runId) {
1885
+ const manifest = await this.store.read();
1886
+ if (manifest.probes.some((probe) => probe.runId === runId && probe.validationStatus !== "validated")) {
1887
+ throw new DebugModeError("PROBE_NOT_VALIDATED", "Every active probe must pass an instrumentation check");
1888
+ }
1889
+ }
1890
+ async validateEvent(input) {
1891
+ const manifest = await this.store.read();
1892
+ const probe = manifest.probes.find((candidate) => candidate.id === input.probeId);
1893
+ const run = manifest.runs.find((candidate) => candidate.id === input.runId);
1894
+ if (probe === void 0 || run === void 0 || input.sessionId !== manifest.sessionId || probe.runId !== input.runId || probe.hypothesisId !== input.hypothesisId || run.label !== input.runLabel) {
1895
+ throw new DebugModeError("MARKER_MISMATCH", "Runtime event ownership does not match the registered probe");
1896
+ }
1897
+ return {
1898
+ ...input,
1899
+ source: {
1900
+ file: path4.relative(this.projectRoot, probe.sourceFile),
1901
+ line: probe.sourceLine,
1902
+ ...probe.sourceColumn === void 0 ? {} : { column: probe.sourceColumn }
1903
+ }
1904
+ };
1905
+ }
1906
+ async update(_manifest, mutate) {
1907
+ return this.store.modify(mutate);
1908
+ }
1909
+ };
1910
+
1911
+ // src/process/service.ts
1912
+ import { fork } from "child_process";
1913
+ import { createHash as createHash7, randomBytes as randomBytes5 } from "crypto";
1914
+ import { existsSync } from "fs";
1915
+ import path5 from "path";
1916
+ import { fileURLToPath } from "url";
1917
+
1918
+ // src/process/line-decoder.ts
1919
+ import { StringDecoder } from "string_decoder";
1920
+ var ProcessLineDecoder = class {
1921
+ constructor(options) {
1922
+ this.options = options;
1923
+ }
1924
+ options;
1925
+ streams = {
1926
+ stdout: { decoder: new StringDecoder("utf8"), pending: "", discarding: false },
1927
+ stderr: { decoder: new StringDecoder("utf8"), pending: "", discarding: false }
1928
+ };
1929
+ push(stream, chunk) {
1930
+ return this.consume(stream, this.streams[stream].decoder.write(chunk));
1931
+ }
1932
+ flush(stream) {
1933
+ const state = this.streams[stream];
1934
+ const records = this.consume(stream, state.decoder.end());
1935
+ if (!state.discarding && state.pending.length > 0) {
1936
+ records.push(this.decodeLine(stream, state.pending.replace(/\r$/u, "")));
1937
+ }
1938
+ state.pending = "";
1939
+ state.discarding = false;
1940
+ return records;
1941
+ }
1942
+ consume(stream, text) {
1943
+ const state = this.streams[stream];
1944
+ const records = [];
1945
+ let remainder = text;
1946
+ if (state.discarding) {
1947
+ const newline = remainder.indexOf("\n");
1948
+ if (newline < 0) return records;
1949
+ state.discarding = false;
1950
+ remainder = remainder.slice(newline + 1);
1951
+ }
1952
+ state.pending += remainder;
1953
+ while (true) {
1954
+ const newline = state.pending.indexOf("\n");
1955
+ if (newline < 0) break;
1956
+ const line = state.pending.slice(0, newline).replace(/\r$/u, "");
1957
+ state.pending = state.pending.slice(newline + 1);
1958
+ if (Buffer.byteLength(line) > this.options.maxLineBytes) {
1959
+ records.push({ kind: "truncated", stream, maximumBytes: this.options.maxLineBytes });
1960
+ } else {
1961
+ records.push(this.decodeLine(stream, line));
1962
+ }
1963
+ }
1964
+ if (Buffer.byteLength(state.pending) > this.options.maxLineBytes) {
1965
+ state.pending = "";
1966
+ state.discarding = true;
1967
+ records.push({ kind: "truncated", stream, maximumBytes: this.options.maxLineBytes });
1968
+ }
1969
+ return records;
1970
+ }
1971
+ decodeLine(stream, text) {
1972
+ if (!text.startsWith(PROCESS_EVENT_PREFIX)) return { kind: "output", stream, text };
1973
+ try {
1974
+ return { kind: "probe-candidate", stream, value: JSON.parse(text.slice(PROCESS_EVENT_PREFIX.length)) };
1975
+ } catch {
1976
+ return { kind: "output", stream, text, rejectedProbe: true };
1977
+ }
1978
+ }
1979
+ };
1980
+
1981
+ // src/process/protocol.ts
1982
+ import { z as z8 } from "zod";
1983
+ var MAX_MESSAGE_BYTES = 64 * 1024;
1984
+ var BoundedString = z8.string().max(8192);
1985
+ var StartMessageSchema = z8.object({
1986
+ type: z8.literal("start"),
1987
+ executable: BoundedString.min(1),
1988
+ args: z8.array(BoundedString).max(256),
1989
+ cwd: BoundedString.min(1),
1990
+ env: z8.record(z8.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/), BoundedString).refine((value) => Object.keys(value).length <= 256),
1991
+ timeoutMs: z8.number().int().min(1).max(3e5),
1992
+ ownerNonce: z8.string().min(32).max(128).regex(/^[A-Za-z0-9_-]+$/)
1993
+ }).strict();
1994
+ var TerminateMessageSchema = z8.object({ type: z8.literal("terminate"), reason: BoundedString }).strict();
1995
+ var ParentMessageSchema = z8.discriminatedUnion("type", [StartMessageSchema, TerminateMessageSchema]);
1996
+ var ChildMessageSchema = z8.discriminatedUnion("type", [
1997
+ z8.object({ type: z8.literal("ready") }).strict(),
1998
+ z8.object({ type: z8.literal("started"), targetPid: z8.number().int().positive() }).strict(),
1999
+ z8.object({
2000
+ type: z8.literal("result"),
2001
+ targetPid: z8.number().int().positive(),
2002
+ exitCode: z8.number().int().nullable(),
2003
+ signal: z8.string().max(64).nullable(),
2004
+ timedOut: z8.boolean(),
2005
+ termination: z8.object({
2006
+ graceful: z8.boolean(),
2007
+ forced: z8.boolean(),
2008
+ remaining: z8.boolean(),
2009
+ durationMs: z8.number().nonnegative(),
2010
+ errors: z8.array(z8.string().max(256)).max(20)
2011
+ }).strict().optional()
2012
+ }).strict(),
2013
+ z8.object({ type: z8.literal("failure"), code: z8.string().max(128), message: z8.string().max(512) }).strict()
2014
+ ]);
2015
+ function assertSize(value) {
2016
+ let serialized;
2017
+ try {
2018
+ serialized = JSON.stringify(value);
2019
+ } catch {
2020
+ throw new Error("IPC message is not serializable");
2021
+ }
2022
+ if (Buffer.byteLength(serialized) > MAX_MESSAGE_BYTES) throw new Error("IPC message exceeds 64 KiB");
2023
+ }
2024
+ function parseChildMessage(value) {
2025
+ assertSize(value);
2026
+ return ChildMessageSchema.parse(value);
2027
+ }
2028
+
2029
+ // src/process/service.ts
2030
+ function generatedId(prefix) {
2031
+ return `${prefix}${randomBytes5(16).toString("base64url")}`;
2032
+ }
2033
+ function defaultSupervisorPath() {
2034
+ const directory = path5.dirname(fileURLToPath(import.meta.url));
2035
+ const bundled = path5.join(directory, "process-supervisor.js");
2036
+ if (existsSync(bundled)) return bundled;
2037
+ return path5.resolve(directory, "../../dist/process-supervisor.js");
2038
+ }
2039
+ var ProcessService = class {
2040
+ constructor(dependencies) {
2041
+ this.dependencies = dependencies;
2042
+ }
2043
+ dependencies;
2044
+ async capture(input) {
2045
+ const run = await this.dependencies.runs.require(input.runId);
2046
+ const release = await this.dependencies.acquireLease();
2047
+ const clock = this.dependencies.clock ?? systemClock;
2048
+ const startedAt = clock.monotonicMs();
2049
+ const decoder = new ProcessLineDecoder({ maxLineBytes: 8192 });
2050
+ let stdoutEvents = 0;
2051
+ let stderrEvents = 0;
2052
+ let probeEvents = 0;
2053
+ let writes = Promise.resolve();
2054
+ let startedUpdate = Promise.resolve();
2055
+ let child;
2056
+ const processId = generatedId("process_");
2057
+ const ownerNonce = randomBytes5(32).toString("base64url");
2058
+ try {
2059
+ child = fork(this.dependencies.supervisorPath ?? defaultSupervisorPath(), [], {
2060
+ stdio: ["ignore", "pipe", "pipe", "ipc"]
2061
+ });
2062
+ if (child.pid === void 0) throw new DebugModeError("PROCESS_START_FAILED", "Supervisor did not receive a PID");
2063
+ await this.waitForMessage(child, "ready", 2e3);
2064
+ await this.addOwnedProcess({
2065
+ id: processId,
2066
+ runId: input.runId,
2067
+ commandSummary: [path5.basename(input.executable), ...input.args.map((value) => path5.basename(value))].join(" ").slice(0, 8192),
2068
+ supervisorPid: child.pid,
2069
+ ownerNonceHash: createHash7("sha256").update(ownerNonce).digest("hex"),
2070
+ status: "starting",
2071
+ startedAt: clock.now().toISOString()
2072
+ });
2073
+ const consume = (stream, chunk) => {
2074
+ for (const record of decoder.push(stream, chunk)) {
2075
+ writes = writes.then(() => this.persistRecord(record, input, run.label)).then((kind) => {
2076
+ if (kind === "probe") probeEvents += 1;
2077
+ else if (kind === "stdout") stdoutEvents += 1;
2078
+ else if (kind === "stderr") stderrEvents += 1;
2079
+ });
2080
+ }
2081
+ };
2082
+ child.stdout?.on("data", (chunk) => consume("stdout", chunk));
2083
+ child.stderr?.on("data", (chunk) => consume("stderr", chunk));
2084
+ const resultPromise = new Promise((resolve, reject) => {
2085
+ child?.on("message", (value) => {
2086
+ try {
2087
+ const message = parseChildMessage(value);
2088
+ if (message.type === "started") startedUpdate = this.markStarted(processId, message.targetPid);
2089
+ if (message.type === "failure") reject(new DebugModeError("PROCESS_START_FAILED", message.message));
2090
+ if (message.type === "result") resolve(message);
2091
+ } catch {
2092
+ reject(new DebugModeError("PROCESS_START_FAILED", "Supervisor returned an invalid message"));
2093
+ }
2094
+ });
2095
+ child?.once("error", () => reject(new DebugModeError("PROCESS_START_FAILED", "Supervisor failed to start")));
2096
+ child?.once("exit", (code) => {
2097
+ if (code !== 0) reject(new DebugModeError("PROCESS_START_FAILED", "Supervisor exited unexpectedly"));
2098
+ });
2099
+ });
2100
+ child.send({
2101
+ type: "start",
2102
+ executable: input.executable,
2103
+ args: input.args,
2104
+ cwd: input.cwd,
2105
+ env: input.env,
2106
+ timeoutMs: input.timeoutMs,
2107
+ ownerNonce
2108
+ });
2109
+ const abort = () => {
2110
+ if (child?.connected) child.send({ type: "terminate", reason: "abort" });
2111
+ };
2112
+ input.signal?.addEventListener("abort", abort, { once: true });
2113
+ if (input.signal?.aborted === true) abort();
2114
+ let result;
2115
+ try {
2116
+ result = await resultPromise;
2117
+ } finally {
2118
+ input.signal?.removeEventListener("abort", abort);
2119
+ }
2120
+ if (result.type !== "result") throw new DebugModeError("PROCESS_START_FAILED", "Supervisor returned no result");
2121
+ for (const stream of ["stdout", "stderr"]) {
2122
+ for (const record of decoder.flush(stream)) {
2123
+ writes = writes.then(() => this.persistRecord(record, input, run.label)).then((kind) => {
2124
+ if (kind === "probe") probeEvents += 1;
2125
+ else if (kind === "stdout") stdoutEvents += 1;
2126
+ else if (kind === "stderr") stderrEvents += 1;
2127
+ });
2128
+ }
2129
+ }
2130
+ await writes;
2131
+ await startedUpdate;
2132
+ await this.markCompleted(processId, result);
2133
+ return {
2134
+ processId,
2135
+ runId: input.runId,
2136
+ exitCode: result.exitCode,
2137
+ signal: result.signal,
2138
+ timedOut: result.timedOut,
2139
+ durationMs: clock.monotonicMs() - startedAt,
2140
+ stdoutEvents,
2141
+ stderrEvents,
2142
+ probeEvents
2143
+ };
2144
+ } finally {
2145
+ if (child?.connected) child.disconnect();
2146
+ release();
2147
+ }
2148
+ }
2149
+ async persistRecord(record, input, runLabel) {
2150
+ if (record.kind === "truncated") {
2151
+ const result2 = await this.dependencies.evidence.append({
2152
+ schemaVersion: EVENT_SCHEMA_VERSION,
2153
+ eventId: generatedId("event_"),
2154
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2155
+ sessionId: this.dependencies.session.publicId,
2156
+ runId: input.runId,
2157
+ runLabel,
2158
+ hypothesisId: "hyp_process",
2159
+ probeId: "probe_process",
2160
+ kind: `process.${record.stream}`,
2161
+ message: "[TRUNCATED OUTPUT LINE]",
2162
+ data: { maximumBytes: record.maximumBytes },
2163
+ source: { file: "process", line: 1 }
2164
+ });
2165
+ return result2.status === "accepted" ? record.stream : "ignored";
2166
+ }
2167
+ if (record.kind === "output") {
2168
+ if (record.text.length === 0) return "ignored";
2169
+ const result2 = await this.dependencies.evidence.append({
2170
+ schemaVersion: EVENT_SCHEMA_VERSION,
2171
+ eventId: generatedId("event_"),
2172
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2173
+ sessionId: this.dependencies.session.publicId,
2174
+ runId: input.runId,
2175
+ runLabel,
2176
+ hypothesisId: "hyp_process",
2177
+ probeId: "probe_process",
2178
+ kind: `process.${record.stream}`,
2179
+ message: record.text,
2180
+ data: null,
2181
+ source: { file: "process", line: 1 }
2182
+ });
2183
+ return result2.status === "accepted" ? record.stream : "ignored";
2184
+ }
2185
+ const parsed = EventInputSchema.safeParse(record.value);
2186
+ if (!parsed.success) return "ignored";
2187
+ const validated = this.dependencies.probes === void 0 ? parsed.data : await this.dependencies.probes.validateEvent(parsed.data);
2188
+ const result = await this.dependencies.evidence.append({
2189
+ ...validated,
2190
+ eventId: generatedId("event_"),
2191
+ kind: "probe"
2192
+ });
2193
+ return result.status === "accepted" ? "probe" : "ignored";
2194
+ }
2195
+ async addOwnedProcess(owned) {
2196
+ await this.updateManifest((manifest) => ({ ...manifest, processes: [...manifest.processes, owned] }));
2197
+ }
2198
+ async markStarted(id, targetPid) {
2199
+ await this.updateManifest((manifest) => ({
2200
+ ...manifest,
2201
+ processes: manifest.processes.map(
2202
+ (entry) => entry.id === id ? { ...entry, targetPid, status: "running" } : entry
2203
+ )
2204
+ }));
2205
+ }
2206
+ async markCompleted(id, result) {
2207
+ await this.updateManifest((manifest) => ({
2208
+ ...manifest,
2209
+ processes: manifest.processes.map(
2210
+ (entry) => entry.id === id ? {
2211
+ ...entry,
2212
+ status: result.timedOut ? "timed_out" : "exited",
2213
+ completedAt: (/* @__PURE__ */ new Date()).toISOString(),
2214
+ exitCode: result.exitCode,
2215
+ signal: result.signal
2216
+ } : entry
2217
+ )
2218
+ }));
2219
+ }
2220
+ async updateManifest(mutate) {
2221
+ await this.dependencies.session.manifestStore.modify(mutate);
2222
+ }
2223
+ waitForMessage(child, type, timeoutMs) {
2224
+ return new Promise((resolve, reject) => {
2225
+ const timeout = setTimeout(() => {
2226
+ cleanup();
2227
+ reject(new DebugModeError("PROCESS_START_FAILED", `Supervisor did not become ${type}`));
2228
+ }, timeoutMs);
2229
+ const message = (value) => {
2230
+ try {
2231
+ if (parseChildMessage(value).type === type) {
2232
+ cleanup();
2233
+ resolve();
2234
+ }
2235
+ } catch {
2236
+ cleanup();
2237
+ reject(new DebugModeError("PROCESS_START_FAILED", "Supervisor returned an invalid message"));
2238
+ }
2239
+ };
2240
+ const exit = () => {
2241
+ cleanup();
2242
+ reject(new DebugModeError("PROCESS_START_FAILED", "Supervisor exited before becoming ready"));
2243
+ };
2244
+ const cleanup = () => {
2245
+ clearTimeout(timeout);
2246
+ child.off("message", message);
2247
+ child.off("exit", exit);
2248
+ };
2249
+ child.on("message", message);
2250
+ child.once("exit", exit);
2251
+ });
2252
+ }
2253
+ };
2254
+
2255
+ // src/run/service.ts
2256
+ import { randomBytes as randomBytes6 } from "crypto";
2257
+ function createId(prefix) {
2258
+ return `${prefix}${randomBytes6(16).toString("base64url")}`;
2259
+ }
2260
+ var RunService = class {
2261
+ constructor(persistence, clock = systemClock, acquireLease) {
2262
+ this.persistence = persistence;
2263
+ this.clock = clock;
2264
+ this.acquireLease = acquireLease;
2265
+ }
2266
+ persistence;
2267
+ clock;
2268
+ acquireLease;
2269
+ waitingReleases = /* @__PURE__ */ new Map();
2270
+ async start(input) {
2271
+ const label = RunLabelSchema.parse(input.label);
2272
+ const current = await this.persistence.read();
2273
+ if (current.runs.length >= 20) throw new DebugModeError("RUN_LIMIT", "The run limit has been reached");
2274
+ const now = this.clock.now().toISOString();
2275
+ const run = {
2276
+ id: createId("run_"),
2277
+ label,
2278
+ reproduction: input.reproduction.slice(0, 8192),
2279
+ status: input.waitingForUser ? "waiting" : "running",
2280
+ createdAt: now
2281
+ };
2282
+ await this.persistence.update(current.revision, (manifest) => ({
2283
+ ...manifest,
2284
+ waitingForReproduction: input.waitingForUser || manifest.waitingForReproduction,
2285
+ runs: [...manifest.runs, run]
2286
+ }));
2287
+ if (input.waitingForUser && this.acquireLease !== void 0) {
2288
+ this.waitingReleases.set(run.id, await this.acquireLease("waiting"));
2289
+ }
2290
+ return run;
2291
+ }
2292
+ async require(runId) {
2293
+ const manifest = await this.persistence.read();
2294
+ const run = manifest.runs.find((candidate) => candidate.id === runId);
2295
+ if (run === void 0) throw new DebugModeError("RUN_NOT_FOUND", "Run was not found");
2296
+ return run;
2297
+ }
2298
+ async complete(runId, status) {
2299
+ const current = await this.persistence.read();
2300
+ const index = current.runs.findIndex((candidate) => candidate.id === runId);
2301
+ if (index < 0) throw new DebugModeError("RUN_NOT_FOUND", "Run was not found");
2302
+ const existing = current.runs[index];
2303
+ if (existing === void 0) throw new DebugModeError("RUN_NOT_FOUND", "Run was not found");
2304
+ const updated = { ...existing, status, completedAt: this.clock.now().toISOString() };
2305
+ await this.persistence.update(current.revision, (manifest) => ({
2306
+ ...manifest,
2307
+ waitingForReproduction: false,
2308
+ runs: manifest.runs.map((candidate) => candidate.id === runId ? updated : candidate)
2309
+ }));
2310
+ this.waitingReleases.get(runId)?.();
2311
+ this.waitingReleases.delete(runId);
2312
+ return updated;
2313
+ }
2314
+ };
2315
+
2316
+ // src/session/orphan-recovery.ts
2317
+ import { lstat as lstat2, readdir as readdir2, realpath as realpath6 } from "fs/promises";
2318
+ import path8 from "path";
2319
+
2320
+ // src/investigation/store.ts
2321
+ import { readFile as readFile6, stat as stat2 } from "fs/promises";
2322
+
2323
+ // src/session/atomic-json.ts
2324
+ import { randomBytes as randomBytes7 } from "crypto";
2325
+ import { open, rename as rename2, rm as rm3 } from "fs/promises";
2326
+ import path6 from "path";
2327
+ async function atomicWriteJson(filename, value, maximumBytes) {
2328
+ const serialized = `${JSON.stringify(value)}
2329
+ `;
2330
+ const bytes = Buffer.byteLength(serialized);
2331
+ if (bytes > maximumBytes) throw new RangeError(`Serialized JSON exceeds ${maximumBytes} bytes`);
2332
+ const temporary = path6.join(
2333
+ path6.dirname(filename),
2334
+ `${path6.basename(filename)}.next-${randomBytes7(8).toString("hex")}`
2335
+ );
2336
+ let handle;
2337
+ try {
2338
+ handle = await open(temporary, "wx", 384);
2339
+ await handle.writeFile(serialized, "utf8");
2340
+ await handle.sync();
2341
+ await handle.close();
2342
+ handle = void 0;
2343
+ await rename2(temporary, filename);
2344
+ return bytes;
2345
+ } catch (error2) {
2346
+ await handle?.close().catch(() => void 0);
2347
+ await rm3(temporary, { force: true }).catch(() => void 0);
2348
+ throw error2;
2349
+ }
2350
+ }
2351
+
2352
+ // src/investigation/store.ts
2353
+ function initialInvestigationState(now) {
2354
+ return InvestigationStateSchema.parse({
2355
+ schemaVersion: STATE_SCHEMA_VERSION,
2356
+ revision: 0,
2357
+ updatedAt: now,
2358
+ problemSummary: "",
2359
+ expectedBehavior: "",
2360
+ actualBehavior: "",
2361
+ runtimeContext: { kind: "other", target: "" },
2362
+ reproduction: { method: "", requiresUser: false, confirmed: null },
2363
+ successCriteria: [],
2364
+ phase: "intake",
2365
+ loopIteration: 0,
2366
+ singleCauseEvidenceRef: null,
2367
+ hypotheses: [],
2368
+ completedChecks: [],
2369
+ runs: [],
2370
+ probeRefs: [],
2371
+ decidingEvidenceIds: [],
2372
+ developerConfirmations: [],
2373
+ decisions: [],
2374
+ nextAction: "",
2375
+ instrumentedFiles: [],
2376
+ fixedFiles: [],
2377
+ cleanup: { status: "not_started", completedResources: [] }
2378
+ });
2379
+ }
2380
+ var InvestigationStore = class {
2381
+ constructor(filename, clock = systemClock) {
2382
+ this.filename = filename;
2383
+ this.clock = clock;
2384
+ }
2385
+ filename;
2386
+ clock;
2387
+ tail = Promise.resolve();
2388
+ async create(state) {
2389
+ const parsed = this.validate(state);
2390
+ try {
2391
+ await stat2(this.filename);
2392
+ throw new DebugModeError("STATE_INVALID", "Investigation checkpoint already exists");
2393
+ } catch (error2) {
2394
+ if (error2.code !== "ENOENT") throw error2;
2395
+ }
2396
+ return this.write(parsed);
2397
+ }
2398
+ async read() {
2399
+ const recovery = await this.readRecovery();
2400
+ if (!recovery.ok) throw new DebugModeError(recovery.error.code, recovery.error.message);
2401
+ return recovery.state;
2402
+ }
2403
+ async readRecovery() {
2404
+ let raw;
2405
+ try {
2406
+ const info = await stat2(this.filename);
2407
+ if (info.size > LIMITS.checkpointBytes) {
2408
+ return { ok: false, error: { code: "STATE_INVALID", message: "Checkpoint exceeds its byte limit" } };
2409
+ }
2410
+ raw = await readFile6(this.filename, "utf8");
2411
+ } catch (error2) {
2412
+ if (error2.code === "ENOENT") {
2413
+ return { ok: false, error: { code: "STATE_MISSING", message: "Investigation checkpoint is missing" } };
2414
+ }
2415
+ return { ok: false, error: { code: "STATE_INVALID", message: "Investigation checkpoint could not be read" } };
2416
+ }
2417
+ try {
2418
+ const value = JSON.parse(raw);
2419
+ if (typeof value === "object" && value !== null && "schemaVersion" in value && value.schemaVersion !== STATE_SCHEMA_VERSION) {
2420
+ return {
2421
+ ok: false,
2422
+ error: { code: "STATE_VERSION_UNSUPPORTED", message: "Investigation checkpoint version is unsupported" }
2423
+ };
2424
+ }
2425
+ return { ok: true, state: InvestigationStateSchema.parse(value), warnings: [] };
2426
+ } catch {
2427
+ return { ok: false, error: { code: "STATE_INVALID", message: "Investigation checkpoint is invalid" } };
2428
+ }
2429
+ }
2430
+ async checkpoint(expectedRevision, state) {
2431
+ return this.exclusive(async () => {
2432
+ const current = await this.read();
2433
+ if (current.revision !== expectedRevision) {
2434
+ throw new DebugModeError("STALE_REVISION", `Expected revision ${expectedRevision}; found ${current.revision}`);
2435
+ }
2436
+ const candidate = this.validate({
2437
+ ...state,
2438
+ revision: expectedRevision + 1,
2439
+ updatedAt: this.clock.now().toISOString()
2440
+ });
2441
+ const bytes = await this.write(candidate);
2442
+ return { state: candidate, bytes };
2443
+ });
2444
+ }
2445
+ validate(value) {
2446
+ const result = InvestigationStateSchema.safeParse(value);
2447
+ if (!result.success) throw new DebugModeError("STATE_INVALID", "Investigation checkpoint failed schema validation");
2448
+ const bytes = Buffer.byteLength(`${JSON.stringify(result.data)}
2449
+ `);
2450
+ if (bytes > LIMITS.checkpointBytes)
2451
+ throw new DebugModeError("STATE_TOO_LARGE", "Investigation checkpoint is too large");
2452
+ return result.data;
2453
+ }
2454
+ async write(state) {
2455
+ try {
2456
+ return await atomicWriteJson(this.filename, state, LIMITS.checkpointBytes);
2457
+ } catch (error2) {
2458
+ if (error2 instanceof RangeError)
2459
+ throw new DebugModeError("STATE_TOO_LARGE", "Investigation checkpoint is too large");
2460
+ throw error2;
2461
+ }
2462
+ }
2463
+ async exclusive(operation) {
2464
+ const previous = this.tail;
2465
+ let release;
2466
+ this.tail = new Promise((resolve) => {
2467
+ release = resolve;
2468
+ });
2469
+ await previous;
2470
+ try {
2471
+ return await operation();
2472
+ } finally {
2473
+ release();
2474
+ }
2475
+ }
2476
+ };
2477
+
2478
+ // src/session/manifest-store.ts
2479
+ import { readFile as readFile7, realpath as realpath5, stat as stat3 } from "fs/promises";
2480
+ import path7 from "path";
2481
+ var MANIFEST_MAX_BYTES = 1024 * 1024;
2482
+ function createInitialManifest(input) {
2483
+ const expiresAt = new Date(new Date(input.now).getTime() + LIMITS.idleMs).toISOString();
2484
+ const candidate = {
2485
+ package: PACKAGE_ID,
2486
+ schemaVersion: MANIFEST_SCHEMA_VERSION,
2487
+ revision: 0,
2488
+ sessionId: input.sessionId,
2489
+ trustedSessionHash: input.trustedSessionHash,
2490
+ projectRoot: input.projectRoot,
2491
+ sessionDir: input.sessionDir,
2492
+ status: "active",
2493
+ createdAt: input.now,
2494
+ lastActivityAt: input.now,
2495
+ expiresAt,
2496
+ waitingForReproduction: false,
2497
+ keepArtifacts: input.keepArtifacts ?? false,
2498
+ collector: null,
2499
+ runs: [],
2500
+ processes: [],
2501
+ probes: [],
2502
+ ownedFiles: [],
2503
+ permissionChanges: [],
2504
+ counters: { accepted: 0, rejected: 0, sampled: 0, truncated: 0, dropped: 0, requests: 0 },
2505
+ cleanup: { status: "not_started", completedResources: [] },
2506
+ ...input.retentionDestination === void 0 ? {} : { retentionDestination: input.retentionDestination }
2507
+ };
2508
+ return ManifestSchema.parse(candidate);
2509
+ }
2510
+ var ManifestStore = class {
2511
+ constructor(filename) {
2512
+ this.filename = filename;
2513
+ }
2514
+ filename;
2515
+ tail = Promise.resolve();
2516
+ async create(value) {
2517
+ return this.exclusive(async () => {
2518
+ const parsed = ManifestSchema.parse(value);
2519
+ await this.verifyPaths(parsed);
2520
+ try {
2521
+ await stat3(this.filename);
2522
+ throw new DebugModeError("SESSION_EXISTS", "Session manifest already exists");
2523
+ } catch (error2) {
2524
+ if (error2.code !== "ENOENT") throw error2;
2525
+ }
2526
+ await atomicWriteJson(this.filename, parsed, MANIFEST_MAX_BYTES);
2527
+ return parsed;
2528
+ });
2529
+ }
2530
+ async read() {
2531
+ const info = await stat3(this.filename);
2532
+ if (info.size > MANIFEST_MAX_BYTES) throw new Error("Manifest exceeds its byte limit");
2533
+ const parsed = ManifestSchema.parse(JSON.parse(await readFile7(this.filename, "utf8")));
2534
+ await this.verifyPaths(parsed);
2535
+ return parsed;
2536
+ }
2537
+ async update(expectedRevision, mutate) {
2538
+ return this.exclusive(async () => {
2539
+ const current = await this.read();
2540
+ if (current.revision !== expectedRevision) {
2541
+ throw new DebugModeError("STALE_REVISION", `Expected revision ${expectedRevision}; found ${current.revision}`);
2542
+ }
2543
+ return this.writeNext(current, mutate);
2544
+ });
2545
+ }
2546
+ async modify(mutate) {
2547
+ return this.exclusive(async () => this.writeNext(await this.read(), mutate));
2548
+ }
2549
+ async writeNext(current, mutate) {
2550
+ const next = ManifestSchema.parse({ ...mutate(structuredClone(current)), revision: current.revision + 1 });
2551
+ await this.verifyPaths(next);
2552
+ await atomicWriteJson(this.filename, next, MANIFEST_MAX_BYTES);
2553
+ return next;
2554
+ }
2555
+ async verifyPaths(value) {
2556
+ const actualSessionDir = await realpath5(path7.dirname(this.filename));
2557
+ const declaredSessionDir = await realpath5(value.sessionDir);
2558
+ if (actualSessionDir !== declaredSessionDir)
2559
+ throw new Error("Manifest session directory does not match its location");
2560
+ const packageBase = await realpath5(path7.dirname(actualSessionDir));
2561
+ if (!isContained(packageBase, actualSessionDir)) throw new Error("Manifest is outside the package temporary base");
2562
+ if (await realpath5(value.projectRoot) !== value.projectRoot) {
2563
+ throw new Error("Manifest project root is not canonical");
2564
+ }
2565
+ }
2566
+ async exclusive(operation) {
2567
+ const previous = this.tail;
2568
+ let release;
2569
+ this.tail = new Promise((resolve) => {
2570
+ release = resolve;
2571
+ });
2572
+ await previous;
2573
+ try {
2574
+ return await operation();
2575
+ } finally {
2576
+ release();
2577
+ }
2578
+ }
2579
+ };
2580
+
2581
+ // src/session/secret-store.ts
2582
+ import { randomBytes as randomBytes8 } from "crypto";
2583
+ import { readFile as readFile8, unlink, writeFile as writeFile5 } from "fs/promises";
2584
+ var TOKEN_PATTERN = /^[A-Za-z0-9_-]{43}$/;
2585
+ var SecretStore = class {
2586
+ constructor(filename) {
2587
+ this.filename = filename;
2588
+ }
2589
+ filename;
2590
+ async create() {
2591
+ const value = randomBytes8(32).toString("base64url");
2592
+ await writeFile5(this.filename, value, { flag: "wx", mode: 384 });
2593
+ return value;
2594
+ }
2595
+ async read() {
2596
+ const value = await readFile8(this.filename, "utf8");
2597
+ if (!TOKEN_PATTERN.test(value) || Buffer.from(value, "base64url").byteLength !== 32) {
2598
+ throw new Error("Stored collector credential is invalid");
2599
+ }
2600
+ return value;
2601
+ }
2602
+ async remove() {
2603
+ let length;
2604
+ try {
2605
+ length = (await readFile8(this.filename)).byteLength;
2606
+ } catch (error2) {
2607
+ if (error2.code === "ENOENT") return "already-clean";
2608
+ throw error2;
2609
+ }
2610
+ try {
2611
+ await writeFile5(this.filename, randomBytes8(length), { flag: "r+" });
2612
+ } catch {
2613
+ }
2614
+ try {
2615
+ await unlink(this.filename);
2616
+ return "success";
2617
+ } catch (error2) {
2618
+ if (error2.code === "ENOENT") return "already-clean";
2619
+ throw error2;
2620
+ }
2621
+ }
2622
+ };
2623
+
2624
+ // src/session/orphan-recovery.ts
2625
+ async function assertContainedReference(target, roots) {
2626
+ const absolute = path8.resolve(target);
2627
+ if (!roots.some((root) => absolute === root || isContained(root, absolute))) {
2628
+ throw new Error("Manifest path escapes its owned roots");
2629
+ }
2630
+ try {
2631
+ const canonical = await realpath6(absolute);
2632
+ if (!roots.some((root) => canonical === root || isContained(root, canonical))) {
2633
+ throw new Error("Manifest path resolves outside its owned roots");
2634
+ }
2635
+ } catch (error2) {
2636
+ if (error2.code !== "ENOENT") throw error2;
2637
+ }
2638
+ }
2639
+ async function recoverOrphans(options) {
2640
+ const cleaned = [];
2641
+ const ignored = [];
2642
+ const errors = [];
2643
+ let entries;
2644
+ try {
2645
+ entries = await readdir2(options.tempBase, { withFileTypes: true });
2646
+ } catch (error2) {
2647
+ if (error2.code === "ENOENT") return { cleaned, ignored, errors };
2648
+ throw error2;
2649
+ }
2650
+ const now = (options.now ?? /* @__PURE__ */ new Date()).getTime();
2651
+ for (const entry of entries) {
2652
+ if (!entry.isDirectory() || !entry.name.startsWith("session-")) {
2653
+ ignored.push(entry.name);
2654
+ continue;
2655
+ }
2656
+ const sessionDir = path8.join(options.tempBase, entry.name);
2657
+ try {
2658
+ if ((await lstat2(sessionDir)).isSymbolicLink()) {
2659
+ ignored.push(entry.name);
2660
+ continue;
2661
+ }
2662
+ const canonicalSessionDir = await realpath6(sessionDir);
2663
+ if (options.activeSessionDirs?.has(canonicalSessionDir) === true) {
2664
+ ignored.push(entry.name);
2665
+ continue;
2666
+ }
2667
+ const manifestStore = new ManifestStore(path8.join(sessionDir, "manifest.json"));
2668
+ const manifest = await manifestStore.read();
2669
+ const canonicalProjectRoot = await realpath6(manifest.projectRoot);
2670
+ if (await realpath6(manifest.sessionDir) !== canonicalSessionDir || new Date(manifest.expiresAt).getTime() >= now) {
2671
+ ignored.push(entry.name);
2672
+ continue;
2673
+ }
2674
+ for (const probe of manifest.probes) {
2675
+ await assertContainedReference(probe.sourceFile, [canonicalProjectRoot]);
2676
+ }
2677
+ for (const permission of manifest.permissionChanges) {
2678
+ await assertContainedReference(permission.manifestPath, [canonicalProjectRoot]);
2679
+ }
2680
+ for (const owned of manifest.ownedFiles) {
2681
+ await assertContainedReference(owned.path, [canonicalProjectRoot, canonicalSessionDir]);
2682
+ }
2683
+ const paths = Object.freeze({
2684
+ baseDir: options.tempBase,
2685
+ sessionDir: manifest.sessionDir,
2686
+ projectRoot: canonicalProjectRoot,
2687
+ manifestFile: path8.join(manifest.sessionDir, "manifest.json"),
2688
+ secretFile: path8.join(manifest.sessionDir, "secret.bin"),
2689
+ stateFile: path8.join(manifest.sessionDir, "investigation-state.json"),
2690
+ evidenceFile: path8.join(manifest.sessionDir, "evidence.ndjson")
2691
+ });
2692
+ const secretStore = new SecretStore(paths.secretFile);
2693
+ const secret = await secretStore.read().catch(() => "");
2694
+ const session = {
2695
+ publicId: manifest.sessionId,
2696
+ trustedHash: manifest.trustedSessionHash,
2697
+ projectRoot: canonicalProjectRoot,
2698
+ directory: canonicalProjectRoot,
2699
+ paths,
2700
+ manifestStore,
2701
+ investigationStore: new InvestigationStore(paths.stateFile),
2702
+ secretStore,
2703
+ secret
2704
+ };
2705
+ if (options.cleanup !== void 0) await options.cleanup(session, manifest);
2706
+ else {
2707
+ const report = {
2708
+ outcome: "abandoned",
2709
+ rootCause: "Investigation ended after its orphaned session expired",
2710
+ decidingEvidence: [],
2711
+ hypotheses: [],
2712
+ fix: "No recovery-time fix was applied",
2713
+ changedFiles: [],
2714
+ verification: ["Verified package-owned cleanup during startup recovery"]
2715
+ };
2716
+ await new CleanupService(session).run({ reason: "orphan-recovery", finalReport: report });
2717
+ }
2718
+ cleaned.push(manifest.sessionId);
2719
+ } catch (error2) {
2720
+ errors.push({
2721
+ directory: entry.name,
2722
+ reason: error2 instanceof Error ? error2.name.slice(0, 128) : "recovery-failed"
2723
+ });
2724
+ }
2725
+ }
2726
+ return { cleaned, ignored, errors };
2727
+ }
2728
+
2729
+ // src/session/registry.ts
2730
+ import { createHash as createHash8, randomBytes as randomBytes9 } from "crypto";
2731
+ import { lstat as lstat3, readdir as readdir3, realpath as realpath7, rm as rm4 } from "fs/promises";
2732
+ import path9 from "path";
2733
+ function trustedSessionHash(openCodeSessionId) {
2734
+ return createHash8("sha256").update("opencode-debug-mode:v1:", "utf8").update(openCodeSessionId, "utf8").digest("hex");
2735
+ }
2736
+ var SessionRegistry = class {
2737
+ constructor(tempBase, clock = systemClock, cleanup) {
2738
+ this.tempBase = tempBase;
2739
+ this.clock = clock;
2740
+ this.cleanup = cleanup;
2741
+ this.timer = setInterval(() => void this.sweep(), 3e4);
2742
+ this.timer.unref();
2743
+ }
2744
+ tempBase;
2745
+ clock;
2746
+ cleanup;
2747
+ sessions = /* @__PURE__ */ new Map();
2748
+ timer;
2749
+ closed = false;
2750
+ async start(trustedId, context, options = {}) {
2751
+ this.assertOpen();
2752
+ if (Number(process.versions.node.split(".")[0]) < 20) {
2753
+ throw new DebugModeError("NODE_UNSUPPORTED", "Node.js 20 or newer is required");
2754
+ }
2755
+ if (options.keepArtifacts === true && options.retentionDestination === void 0) {
2756
+ throw new DebugModeError("DESTINATION_REQUIRED", "An explicit retention destination is required");
2757
+ }
2758
+ const hash = trustedSessionHash(trustedId);
2759
+ if (this.sessions.has(hash) || await this.findPersisted(hash) !== void 0) {
2760
+ throw new DebugModeError("SESSION_EXISTS", "A debug session already exists for this OpenCode session");
2761
+ }
2762
+ const projectRoot = await realpath7(context.worktree);
2763
+ const directory = await realpath7(context.directory);
2764
+ if (directory !== projectRoot && !isContained(projectRoot, directory)) {
2765
+ throw new DebugModeError("STORAGE_UNAVAILABLE", "The active directory is outside the worktree");
2766
+ }
2767
+ let paths;
2768
+ try {
2769
+ paths = await createSessionPaths(this.tempBase, projectRoot);
2770
+ const secretStore = new SecretStore(paths.secretFile);
2771
+ const secret = await secretStore.create();
2772
+ const publicId = `session_${randomBytes9(16).toString("base64url")}`;
2773
+ const manifestStore = new ManifestStore(paths.manifestFile);
2774
+ const investigationStore = new InvestigationStore(paths.stateFile, this.clock);
2775
+ const now = this.clock.now().toISOString();
2776
+ await manifestStore.create(
2777
+ createInitialManifest({
2778
+ sessionId: publicId,
2779
+ trustedSessionHash: hash,
2780
+ projectRoot,
2781
+ sessionDir: paths.sessionDir,
2782
+ now,
2783
+ keepArtifacts: options.keepArtifacts ?? false,
2784
+ ...options.retentionDestination === void 0 ? {} : { retentionDestination: path9.resolve(options.retentionDestination) }
2785
+ })
2786
+ );
2787
+ await investigationStore.create(initialInvestigationState(now));
2788
+ const session = {
2789
+ publicId,
2790
+ trustedHash: hash,
2791
+ projectRoot,
2792
+ directory,
2793
+ paths,
2794
+ manifestStore,
2795
+ investigationStore,
2796
+ secretStore,
2797
+ secret,
2798
+ leases: /* @__PURE__ */ new Map()
2799
+ };
2800
+ this.sessions.set(hash, session);
2801
+ return session;
2802
+ } catch (error2) {
2803
+ if (paths !== void 0) await rm4(paths.sessionDir, { recursive: true, force: true }).catch(() => void 0);
2804
+ if (error2 instanceof DebugModeError) throw error2;
2805
+ throw new DebugModeError("STORAGE_UNAVAILABLE", "The debug session could not be initialized");
2806
+ }
2807
+ }
2808
+ async requireOwned(trustedId) {
2809
+ this.assertOpen();
2810
+ const hash = trustedSessionHash(trustedId);
2811
+ const inMemory = this.sessions.get(hash);
2812
+ if (inMemory !== void 0) return inMemory;
2813
+ const persisted = await this.findPersisted(hash);
2814
+ if (persisted === void 0) throw new DebugModeError("NO_ACTIVE_SESSION", "No active debug session exists");
2815
+ this.sessions.set(hash, persisted);
2816
+ return persisted;
2817
+ }
2818
+ async hasTrusted(trustedId) {
2819
+ try {
2820
+ await this.requireOwned(trustedId);
2821
+ return true;
2822
+ } catch (error2) {
2823
+ if (error2 instanceof DebugModeError && error2.code === "NO_ACTIVE_SESSION") return false;
2824
+ throw error2;
2825
+ }
2826
+ }
2827
+ async touch(trustedId) {
2828
+ const session = await this.requireOwned(trustedId);
2829
+ await this.touchSession(session);
2830
+ }
2831
+ async touchSession(sessionValue) {
2832
+ const session = this.sessions.get(sessionValue.trustedHash);
2833
+ if (session === void 0 || session.publicId !== sessionValue.publicId) {
2834
+ throw new DebugModeError("SESSION_OWNERSHIP_MISMATCH", "Session is not owned by this registry");
2835
+ }
2836
+ const now = this.clock.now();
2837
+ await session.manifestStore.modify((manifest) => ({
2838
+ ...manifest,
2839
+ lastActivityAt: now.toISOString(),
2840
+ expiresAt: new Date(now.getTime() + LIMITS.idleMs).toISOString()
2841
+ }));
2842
+ }
2843
+ async acquireLease(trustedId, kind) {
2844
+ const session = await this.requireOwned(trustedId);
2845
+ return this.acquireLeaseForSession(session, kind);
2846
+ }
2847
+ acquireLeaseForSession(sessionValue, kind) {
2848
+ const session = this.sessions.get(sessionValue.trustedHash);
2849
+ if (session === void 0 || session.publicId !== sessionValue.publicId) {
2850
+ throw new DebugModeError("SESSION_OWNERSHIP_MISMATCH", "Session is not owned by this registry");
2851
+ }
2852
+ session.leases.set(kind, (session.leases.get(kind) ?? 0) + 1);
2853
+ let released = false;
2854
+ return () => {
2855
+ if (released) return;
2856
+ released = true;
2857
+ const next = Math.max(0, (session.leases.get(kind) ?? 1) - 1);
2858
+ if (next === 0) session.leases.delete(kind);
2859
+ else session.leases.set(kind, next);
2860
+ };
2861
+ }
2862
+ async sweep() {
2863
+ if (this.closed) return;
2864
+ const now = this.clock.now().getTime();
2865
+ for (const [hash, session] of this.sessions) {
2866
+ const manifest = await session.manifestStore.read().catch(() => void 0);
2867
+ if (manifest === void 0 || manifest.waitingForReproduction || session.leases.size > 0 || new Date(manifest.lastActivityAt).getTime() + LIMITS.idleMs > now) {
2868
+ continue;
2869
+ }
2870
+ if (this.cleanup !== void 0) await this.cleanup(session, "idle-expired");
2871
+ this.sessions.delete(hash);
2872
+ }
2873
+ }
2874
+ listActive() {
2875
+ return [...this.sessions.values()];
2876
+ }
2877
+ forgetTrusted(trustedId) {
2878
+ this.sessions.delete(trustedSessionHash(trustedId));
2879
+ }
2880
+ async closeAll() {
2881
+ if (this.closed) return;
2882
+ this.closed = true;
2883
+ clearInterval(this.timer);
2884
+ this.sessions.clear();
2885
+ }
2886
+ async findPersisted(hash) {
2887
+ let entries;
2888
+ try {
2889
+ entries = await readdir3(this.tempBase, { withFileTypes: true, encoding: "utf8" });
2890
+ } catch (error2) {
2891
+ if (error2.code === "ENOENT") return void 0;
2892
+ throw error2;
2893
+ }
2894
+ const matches2 = [];
2895
+ for (const entry of entries) {
2896
+ if (!entry.isDirectory() || !entry.name.startsWith("session-")) continue;
2897
+ const sessionDir = path9.join(this.tempBase, entry.name);
2898
+ if ((await lstat3(sessionDir)).isSymbolicLink()) continue;
2899
+ try {
2900
+ const manifestStore = new ManifestStore(path9.join(sessionDir, "manifest.json"));
2901
+ const manifest = await manifestStore.read();
2902
+ if (manifest.trustedSessionHash !== hash || manifest.status !== "active") continue;
2903
+ if (!manifest.waitingForReproduction && new Date(manifest.expiresAt).getTime() < this.clock.now().getTime())
2904
+ continue;
2905
+ const paths = this.pathsFromManifest(manifest);
2906
+ const secretStore = new SecretStore(paths.secretFile);
2907
+ const secret = await secretStore.read();
2908
+ const investigationStore = new InvestigationStore(paths.stateFile, this.clock);
2909
+ const recovery = await investigationStore.readRecovery();
2910
+ if (!recovery.ok) throw new DebugModeError(recovery.error.code, recovery.error.message);
2911
+ matches2.push({
2912
+ publicId: manifest.sessionId,
2913
+ trustedHash: hash,
2914
+ projectRoot: manifest.projectRoot,
2915
+ directory: manifest.projectRoot,
2916
+ paths,
2917
+ manifestStore,
2918
+ investigationStore,
2919
+ secretStore,
2920
+ secret,
2921
+ leases: /* @__PURE__ */ new Map()
2922
+ });
2923
+ } catch {
2924
+ }
2925
+ }
2926
+ if (matches2.length > 1) {
2927
+ throw new DebugModeError("SESSION_OWNERSHIP_MISMATCH", "Multiple session manifests match this trusted session");
2928
+ }
2929
+ return matches2[0];
2930
+ }
2931
+ pathsFromManifest(manifest) {
2932
+ return Object.freeze({
2933
+ baseDir: this.tempBase,
2934
+ sessionDir: manifest.sessionDir,
2935
+ projectRoot: manifest.projectRoot,
2936
+ manifestFile: path9.join(manifest.sessionDir, "manifest.json"),
2937
+ secretFile: path9.join(manifest.sessionDir, "secret.bin"),
2938
+ stateFile: path9.join(manifest.sessionDir, "investigation-state.json"),
2939
+ evidenceFile: path9.join(manifest.sessionDir, "evidence.ndjson")
2940
+ });
2941
+ }
2942
+ assertOpen() {
2943
+ if (this.closed) throw new DebugModeError("NO_ACTIVE_SESSION", "The session registry is closed");
2944
+ }
2945
+ };
2946
+
2947
+ // src/tools/cleanup-tool.ts
2948
+ import path10 from "path";
2949
+ import { tool } from "@opencode-ai/plugin";
2950
+
2951
+ // src/core/result.ts
2952
+ function success(data, warnings = []) {
2953
+ return { ok: true, data, warnings };
2954
+ }
2955
+ function failure(code, message, retryable, options = {}) {
2956
+ const error2 = {
2957
+ code,
2958
+ message: message.slice(0, 8192),
2959
+ retryable
2960
+ };
2961
+ if (options.action !== void 0) error2.action = options.action.slice(0, 8192);
2962
+ if (options.details !== void 0) error2.details = options.details;
2963
+ return { ok: false, error: error2 };
2964
+ }
2965
+
2966
+ // src/tools/common.ts
2967
+ function jsonSuccess(value) {
2968
+ return JSON.stringify(success(value));
2969
+ }
2970
+ function jsonFailure(error2, fallback = "Tool operation failed") {
2971
+ if (error2 instanceof DebugModeError) {
2972
+ return JSON.stringify(
2973
+ failure(error2.code, error2.message, error2.retryable, {
2974
+ ...error2.action === void 0 ? {} : { action: error2.action },
2975
+ ...error2.details === void 0 ? {} : { details: error2.details }
2976
+ })
2977
+ );
2978
+ }
2979
+ return JSON.stringify(failure("INTERNAL_ERROR", fallback, false));
2980
+ }
2981
+
2982
+ // src/tools/cleanup-tool.ts
2983
+ var schema = tool.schema;
2984
+ var reportSchema = schema.object({
2985
+ outcome: schema.enum(["completed", "unresolved", "abandoned", "escalated"]),
2986
+ rootCause: schema.string().max(8192),
2987
+ decidingEvidence: schema.array(schema.string().max(8192)).max(100),
2988
+ hypotheses: schema.array(
2989
+ schema.object({
2990
+ id: schema.string().max(64),
2991
+ status: schema.enum(["open", "confirmed", "eliminated"]),
2992
+ statement: schema.string().max(8192)
2993
+ }).strict()
2994
+ ).max(4),
2995
+ fix: schema.string().max(8192),
2996
+ changedFiles: schema.array(schema.string().max(8192)).max(200),
2997
+ verification: schema.array(schema.string().max(8192)).max(100)
2998
+ }).strict();
2999
+ function createCleanupTool(registry, cleanupFor) {
3000
+ return tool({
3001
+ description: "Tear down every owned debug resource and optionally retain a sanitized report",
3002
+ args: {
3003
+ reason: schema.enum(["completed", "unresolved", "abandoned", "escalated", "cancelled"]),
3004
+ finalReport: reportSchema,
3005
+ cleanCheck: schema.object({
3006
+ executable: schema.string().min(1).max(8192),
3007
+ args: schema.array(schema.string().max(8192)).max(256),
3008
+ cwd: schema.string().min(1).max(8192),
3009
+ timeoutMs: schema.number().int().min(1).max(3e5)
3010
+ }).strict().optional()
3011
+ },
3012
+ execute: async (args, context) => {
3013
+ try {
3014
+ const session = await registry.requireOwned(context.sessionID);
3015
+ const result = await cleanupFor(session).run({
3016
+ reason: args.reason,
3017
+ finalReport: args.finalReport,
3018
+ ...args.cleanCheck === void 0 ? {} : { cleanCheck: args.cleanCheck }
3019
+ });
3020
+ const sanitize = (location) => {
3021
+ if (location === void 0) return void 0;
3022
+ const absolute = path10.resolve(location);
3023
+ return absolute === session.projectRoot || isContained(session.projectRoot, absolute) ? path10.relative(session.projectRoot, absolute) || "." : void 0;
3024
+ };
3025
+ const resources = {
3026
+ ...result.resources,
3027
+ probes: result.resources.probes.map((value) => ({ ...value, location: sanitize(value.location) })),
3028
+ permissions: result.resources.permissions.map((value) => ({ ...value, location: sanitize(value.location) })),
3029
+ files: result.resources.files.map((value) => ({ ...value, location: sanitize(value.location) }))
3030
+ };
3031
+ return jsonSuccess({
3032
+ ...result,
3033
+ resources,
3034
+ remainingArtifacts: result.remainingArtifacts.map(sanitize).filter(Boolean)
3035
+ });
3036
+ } catch (error2) {
3037
+ return jsonFailure(error2, "Cleanup failed");
3038
+ }
3039
+ }
3040
+ });
3041
+ }
3042
+
3043
+ // src/tools/collector-tools.ts
3044
+ import { tool as tool2 } from "@opencode-ai/plugin";
3045
+ var schema2 = tool2.schema;
3046
+ function createCollectorStartTool(registry, collectorFor) {
3047
+ return tool2({
3048
+ description: "Start the authenticated loopback evidence collector",
3049
+ args: {
3050
+ runtime: schema2.enum(["web", "extension-background"]),
3051
+ transportTargetPath: schema2.string().min(1).max(8192).optional(),
3052
+ extensionManifestPath: schema2.string().min(1).max(8192).optional()
3053
+ },
3054
+ execute: async (args, context) => {
3055
+ try {
3056
+ const session = await registry.requireOwned(context.sessionID);
3057
+ const result = await collectorFor(session).start({
3058
+ runtime: args.runtime,
3059
+ ...args.transportTargetPath === void 0 ? {} : { transportTargetPath: args.transportTargetPath },
3060
+ ...args.extensionManifestPath === void 0 ? {} : { extensionManifestPath: args.extensionManifestPath }
3061
+ });
3062
+ await registry.touch(context.sessionID);
3063
+ return jsonSuccess(result);
3064
+ } catch (error2) {
3065
+ return jsonFailure(error2, "Collector startup failed");
3066
+ }
3067
+ }
3068
+ });
3069
+ }
3070
+
3071
+ // src/tools/evidence-tools.ts
3072
+ import { tool as tool3 } from "@opencode-ai/plugin";
3073
+ var schema3 = tool3.schema;
3074
+ function createEvidenceReadTool(registry, evidenceFor) {
3075
+ return tool3({
3076
+ description: "Read a bounded filtered page of sanitized local evidence",
3077
+ args: {
3078
+ runId: schema3.string().optional(),
3079
+ hypothesisId: schema3.string().optional(),
3080
+ probeId: schema3.string().optional(),
3081
+ from: schema3.string().optional(),
3082
+ to: schema3.string().optional(),
3083
+ keyword: schema3.string().max(8192).optional(),
3084
+ cursor: schema3.string().regex(/^\d+$/).optional(),
3085
+ limit: schema3.number().int().min(1).max(100).default(100)
3086
+ },
3087
+ execute: async (args, context) => {
3088
+ try {
3089
+ const session = await registry.requireOwned(context.sessionID);
3090
+ const result = await evidenceFor(session).read({
3091
+ sessionId: session.publicId,
3092
+ limit: args.limit,
3093
+ ...args.runId === void 0 ? {} : { runId: args.runId },
3094
+ ...args.hypothesisId === void 0 ? {} : { hypothesisId: args.hypothesisId },
3095
+ ...args.probeId === void 0 ? {} : { probeId: args.probeId },
3096
+ ...args.from === void 0 ? {} : { from: args.from },
3097
+ ...args.to === void 0 ? {} : { to: args.to },
3098
+ ...args.keyword === void 0 ? {} : { keyword: args.keyword },
3099
+ ...args.cursor === void 0 ? {} : { cursor: args.cursor }
3100
+ });
3101
+ await registry.touch(context.sessionID);
3102
+ return jsonSuccess(result);
3103
+ } catch (error2) {
3104
+ return jsonFailure(error2, "Evidence is unavailable");
3105
+ }
3106
+ }
3107
+ });
3108
+ }
3109
+
3110
+ // src/tools/probe-tools.ts
3111
+ import path11 from "path";
3112
+ import { tool as tool4 } from "@opencode-ai/plugin";
3113
+ var schema4 = tool4.schema;
3114
+ function createProbePrepareTool(registry, probesFor) {
3115
+ return tool4({
3116
+ description: "Prepare one safe owned JavaScript or TypeScript probe",
3117
+ args: {
3118
+ runId: schema4.string().regex(/^[A-Za-z0-9_-]+$/),
3119
+ hypothesisId: schema4.string().regex(/^[A-Za-z0-9_-]+$/),
3120
+ sourceFile: schema4.string().min(1).max(8192),
3121
+ sourceLine: schema4.number().int().positive(),
3122
+ sourceColumn: schema4.number().int().positive().optional(),
3123
+ message: schema4.string().min(1).max(8192),
3124
+ captures: schema4.array(
3125
+ schema4.object({ label: schema4.string().min(1).max(128), path: schema4.string().min(1).max(512) }).strict()
3126
+ ).max(20),
3127
+ transport: schema4.enum(["process", "http-web", "extension-background", "extension-content"]),
3128
+ sampling: schema4.union([
3129
+ schema4.object({ mode: schema4.literal("every"), n: schema4.number().int().min(1).max(1e4) }).strict(),
3130
+ schema4.object({ mode: schema4.literal("aggregate"), windowMs: schema4.number().int().min(100).max(6e4) }).strict()
3131
+ ])
3132
+ },
3133
+ execute: async (args, context) => {
3134
+ try {
3135
+ const session = await registry.requireOwned(context.sessionID);
3136
+ const { sourceColumn, ...required } = args;
3137
+ const probe = await probesFor(session).plan({
3138
+ ...required,
3139
+ ...sourceColumn === void 0 ? {} : { sourceColumn }
3140
+ });
3141
+ await registry.touch(context.sessionID);
3142
+ return jsonSuccess({
3143
+ probeId: probe.id,
3144
+ markerBlock: probe.markerBlock,
3145
+ source: path11.relative(session.projectRoot, probe.sourceFile),
3146
+ line: probe.sourceLine
3147
+ });
3148
+ } catch (error2) {
3149
+ return jsonFailure(error2, "Probe preparation failed");
3150
+ }
3151
+ }
3152
+ });
3153
+ }
3154
+ function createProbeRegisterTool(registry, probesFor) {
3155
+ return tool4({
3156
+ description: "Verify and register an exact owned probe marker",
3157
+ args: { probeId: schema4.string().regex(/^[A-Za-z0-9_-]+$/) },
3158
+ execute: async (args, context) => {
3159
+ try {
3160
+ const session = await registry.requireOwned(context.sessionID);
3161
+ const probe = await probesFor(session).register(args.probeId);
3162
+ await registry.touch(context.sessionID);
3163
+ return jsonSuccess({ probeId: probe.id, status: probe.status, validationStatus: probe.validationStatus });
3164
+ } catch (error2) {
3165
+ return jsonFailure(error2, "Probe registration failed");
3166
+ }
3167
+ }
3168
+ });
3169
+ }
3170
+
3171
+ // src/tools/run-tools.ts
3172
+ import path12 from "path";
3173
+ import { tool as tool5 } from "@opencode-ai/plugin";
3174
+ var schema5 = tool5.schema;
3175
+ var ApprovalClassSchema = schema5.enum([
3176
+ "local-deterministic",
3177
+ "credentials",
3178
+ "device",
3179
+ "external-state",
3180
+ "materially-different"
3181
+ ]);
3182
+ var ProcessArgs = {
3183
+ approvalClass: ApprovalClassSchema,
3184
+ purpose: schema5.enum(["instrumentation-check", "reproduction", "verification"]),
3185
+ probeIds: schema5.array(schema5.string().regex(/^[A-Za-z0-9_-]+$/)).max(100),
3186
+ executable: schema5.string().min(1).max(8192),
3187
+ args: schema5.array(schema5.string().max(8192)).max(256),
3188
+ cwd: schema5.string().min(1).max(8192),
3189
+ env: schema5.record(schema5.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/), schema5.string().max(8192)).refine((value) => Object.keys(value).length <= 256),
3190
+ runId: schema5.string().regex(/^[A-Za-z0-9_-]+$/),
3191
+ timeoutMs: schema5.number().int().min(1).max(3e5)
3192
+ };
3193
+ async function requestApproval(context, permission, executable, args) {
3194
+ try {
3195
+ await context.ask({
3196
+ permission,
3197
+ patterns: [[path12.basename(executable), ...args.map((value) => path12.basename(value))].join(" ").slice(0, 512)],
3198
+ always: [],
3199
+ metadata: { executable: path12.basename(executable), argumentCount: args.length }
3200
+ });
3201
+ } catch {
3202
+ throw new DebugModeError("COMMAND_REQUIRES_APPROVAL", "The requested process command was not approved");
3203
+ }
3204
+ }
3205
+ function serializeResult(result) {
3206
+ return JSON.stringify(success(result));
3207
+ }
3208
+ function createProcessCaptureTool(dependencies) {
3209
+ return tool5({
3210
+ description: "Run one supervised command and capture bounded runtime evidence",
3211
+ args: ProcessArgs,
3212
+ execute: async (args, context) => {
3213
+ try {
3214
+ const session = await dependencies.registry.requireOwned(context.sessionID);
3215
+ if (args.approvalClass !== "local-deterministic") {
3216
+ await requestApproval(context, "debug_process_external", args.executable, args.args);
3217
+ }
3218
+ const resolvedCwd = path12.resolve(args.cwd);
3219
+ const worktree = path12.resolve(context.worktree);
3220
+ if (resolvedCwd !== worktree && !isContained(worktree, resolvedCwd)) {
3221
+ await requestApproval(context, "external_directory", args.executable, args.args);
3222
+ }
3223
+ const probes = dependencies.probesFor(session);
3224
+ if (args.purpose === "reproduction") await probes.requireValidatedForRun(args.runId);
3225
+ const result = await dependencies.processFor(session).capture({
3226
+ runId: args.runId,
3227
+ executable: args.executable,
3228
+ args: args.args,
3229
+ cwd: resolvedCwd,
3230
+ env: args.env,
3231
+ timeoutMs: args.timeoutMs,
3232
+ probeIds: args.probeIds,
3233
+ purpose: args.purpose,
3234
+ signal: context.abort
3235
+ });
3236
+ if (args.purpose === "instrumentation-check" && result.exitCode === 0) await probes.validate(args.probeIds);
3237
+ return serializeResult(result);
3238
+ } catch (error2) {
3239
+ if (error2 instanceof DebugModeError) {
3240
+ return JSON.stringify(
3241
+ failure(error2.code, error2.message, error2.retryable, {
3242
+ ...error2.action === void 0 ? {} : { action: error2.action },
3243
+ ...error2.details === void 0 ? {} : { details: error2.details }
3244
+ })
3245
+ );
3246
+ }
3247
+ return JSON.stringify(failure("INTERNAL_ERROR", "Process capture failed", false));
3248
+ }
3249
+ }
3250
+ });
3251
+ }
3252
+ function createRunStartTool(registry, runFor) {
3253
+ return tool5({
3254
+ description: "Start a correlated pre-fix or post-fix reproduction run",
3255
+ args: {
3256
+ label: schema5.enum(["pre-fix", "post-fix"]),
3257
+ reproduction: schema5.string().min(1).max(8192),
3258
+ waitingForUser: schema5.boolean().default(false)
3259
+ },
3260
+ execute: async (args, context) => {
3261
+ try {
3262
+ const session = await registry.requireOwned(context.sessionID);
3263
+ const run = await runFor(session).start(args);
3264
+ await registry.touch(context.sessionID);
3265
+ return JSON.stringify(success({ runId: run.id, status: run.status, label: run.label }));
3266
+ } catch (error2) {
3267
+ if (error2 instanceof DebugModeError) return JSON.stringify(failure(error2.code, error2.message, error2.retryable));
3268
+ return JSON.stringify(failure("INTERNAL_ERROR", "Run could not be started", false));
3269
+ }
3270
+ }
3271
+ });
3272
+ }
3273
+
3274
+ // src/tools/session-tools.ts
3275
+ import { tool as tool6 } from "@opencode-ai/plugin";
3276
+ var schema6 = tool6.schema;
3277
+ function createSessionStartTool(registry) {
3278
+ return tool6({
3279
+ description: "Start an isolated runtime-debugging session",
3280
+ args: {
3281
+ keepArtifacts: schema6.boolean().default(false),
3282
+ retentionDestination: schema6.string().min(1).max(8192).optional()
3283
+ },
3284
+ execute: async (args, context) => {
3285
+ try {
3286
+ const session = await registry.start(
3287
+ context.sessionID,
3288
+ { directory: context.directory, worktree: context.worktree },
3289
+ {
3290
+ keepArtifacts: args.keepArtifacts,
3291
+ ...args.retentionDestination === void 0 ? {} : { retentionDestination: args.retentionDestination }
3292
+ }
3293
+ );
3294
+ return jsonSuccess({
3295
+ sessionId: session.publicId,
3296
+ status: "active",
3297
+ limits: LIMITS,
3298
+ capabilities: { process: true, web: true, extension: true, languages: ["javascript", "typescript"] }
3299
+ });
3300
+ } catch (error2) {
3301
+ return jsonFailure(error2, "Debug session could not be started");
3302
+ }
3303
+ }
3304
+ });
3305
+ }
3306
+ function createSessionStatusTool(registry) {
3307
+ return tool6({
3308
+ description: "Read the public status of the active debug session",
3309
+ args: {},
3310
+ execute: async (_args, context) => {
3311
+ try {
3312
+ const session = await registry.requireOwned(context.sessionID);
3313
+ const manifest = await session.manifestStore.read();
3314
+ const state = await session.investigationStore.readRecovery();
3315
+ return jsonSuccess({
3316
+ sessionId: session.publicId,
3317
+ status: manifest.status,
3318
+ phase: state.ok ? state.state.phase : "recovery-required",
3319
+ revision: state.ok ? state.state.revision : manifest.revision,
3320
+ collector: manifest.collector === null ? null : { status: manifest.collector.status, host: manifest.collector.host, port: manifest.collector.port },
3321
+ processCount: manifest.processes.length,
3322
+ probeCount: manifest.probes.length,
3323
+ counters: manifest.counters,
3324
+ limits: LIMITS
3325
+ });
3326
+ } catch (error2) {
3327
+ return jsonFailure(error2, "Debug session status is unavailable");
3328
+ }
3329
+ }
3330
+ });
3331
+ }
3332
+
3333
+ // src/tools/state-tools.ts
3334
+ import { tool as tool7 } from "@opencode-ai/plugin";
3335
+ var schema7 = tool7.schema;
3336
+ function createStateReadTool(registry) {
3337
+ return tool7({
3338
+ description: "Read and reconcile the durable investigation checkpoint",
3339
+ args: {},
3340
+ execute: async (_args, context) => {
3341
+ try {
3342
+ const session = await registry.requireOwned(context.sessionID);
3343
+ const result = await session.investigationStore.readRecovery();
3344
+ if (!result.ok) throw Object.assign(new Error(result.error.message), { code: result.error.code });
3345
+ return jsonSuccess({ state: result.state, recoveryWarnings: result.warnings });
3346
+ } catch (error2) {
3347
+ if (typeof error2 === "object" && error2 !== null && "code" in error2) {
3348
+ const value = error2;
3349
+ const { DebugModeError: DebugModeError2 } = await import("./errors-IQTPGK5R.js");
3350
+ return jsonFailure(new DebugModeError2(value.code, value.message), "Checkpoint is unavailable");
3351
+ }
3352
+ return jsonFailure(error2, "Checkpoint is unavailable");
3353
+ }
3354
+ }
3355
+ });
3356
+ }
3357
+ function createStateCheckpointTool(registry) {
3358
+ return tool7({
3359
+ description: "Atomically replace the durable investigation checkpoint",
3360
+ args: { expectedRevision: schema7.number().int().nonnegative(), state: schema7.unknown() },
3361
+ execute: async (args, context) => {
3362
+ try {
3363
+ const session = await registry.requireOwned(context.sessionID);
3364
+ const result = await session.investigationStore.checkpoint(
3365
+ args.expectedRevision,
3366
+ args.state
3367
+ );
3368
+ await registry.touch(context.sessionID);
3369
+ return jsonSuccess({ revision: result.state.revision, bytes: result.bytes });
3370
+ } catch (error2) {
3371
+ return jsonFailure(error2, "Checkpoint update failed");
3372
+ }
3373
+ }
3374
+ });
3375
+ }
3376
+
3377
+ // src/tools/index.ts
3378
+ function createDebugTools(dependencies) {
3379
+ return {
3380
+ debug_session_start: createSessionStartTool(dependencies.registry),
3381
+ debug_session_status: createSessionStatusTool(dependencies.registry),
3382
+ debug_state_read: createStateReadTool(dependencies.registry),
3383
+ debug_state_checkpoint: createStateCheckpointTool(dependencies.registry),
3384
+ debug_run_start: createRunStartTool(dependencies.registry, dependencies.runFor),
3385
+ debug_process_capture: createProcessCaptureTool(dependencies),
3386
+ debug_collector_start: createCollectorStartTool(dependencies.registry, dependencies.collectorFor),
3387
+ debug_probe_prepare: createProbePrepareTool(dependencies.registry, dependencies.probesFor),
3388
+ debug_probe_register: createProbeRegisterTool(dependencies.registry, dependencies.probesFor),
3389
+ debug_evidence_read: createEvidenceReadTool(dependencies.registry, dependencies.evidenceFor),
3390
+ debug_cleanup: createCleanupTool(dependencies.registry, dependencies.cleanupFor)
3391
+ };
3392
+ }
3393
+
3394
+ // src/plugin.ts
3395
+ async function updateManifest(session, mutate) {
3396
+ return session.manifestStore.modify(mutate);
3397
+ }
3398
+ function terminalReport(outcome, reason) {
3399
+ return {
3400
+ outcome,
3401
+ rootCause: reason,
3402
+ decidingEvidence: [],
3403
+ hypotheses: [],
3404
+ fix: "No additional fix was applied during lifecycle cleanup",
3405
+ changedFiles: [],
3406
+ verification: ["Package-owned resources were cleaned by the lifecycle hook"]
3407
+ };
3408
+ }
3409
+ function createDebugModePlugin(options = {}) {
3410
+ return async (input) => {
3411
+ const prompt = await readFile9(new URL("../assets/debug-agent.md", import.meta.url), "utf8");
3412
+ const tempBase = options.tempBase ?? path13.join(tmpdir(), TEMP_BASE_NAME);
3413
+ const clock = options.clock ?? systemClock;
3414
+ const recovery = await recoverOrphans({ tempBase, now: clock.now() }).catch((error2) => ({
3415
+ cleaned: [],
3416
+ ignored: [],
3417
+ errors: [{ directory: "<temp-base>", reason: error2 instanceof Error ? error2.name : "recovery-failed" }]
3418
+ }));
3419
+ if (recovery.errors.length > 0) {
3420
+ await input.client.app.log({
3421
+ body: {
3422
+ service: "opencode-debug-mode",
3423
+ level: "warn",
3424
+ message: `Orphan recovery reported ${recovery.errors.length} failure(s)`
3425
+ }
3426
+ });
3427
+ }
3428
+ const services = /* @__PURE__ */ new Map();
3429
+ let registry;
3430
+ const forSession = (session) => {
3431
+ const existing = services.get(session.publicId);
3432
+ if (existing !== void 0) return existing;
3433
+ const evidence = new EvidenceStore(
3434
+ session.paths.evidenceFile,
3435
+ async (counters) => {
3436
+ await updateManifest(session, (manifest) => ({ ...manifest, counters })).catch(() => void 0);
3437
+ },
3438
+ clock,
3439
+ async () => (await session.manifestStore.read()).counters
3440
+ );
3441
+ const runs = new RunService(
3442
+ session.manifestStore,
3443
+ clock,
3444
+ (kind) => registry.acquireLeaseForSession(session, kind)
3445
+ );
3446
+ const probes = new ProbeRegistry(session.manifestStore, session.projectRoot, async (id) => {
3447
+ const state = await session.investigationStore.read();
3448
+ return state.hypotheses.some((hypothesis) => hypothesis.id === id);
3449
+ });
3450
+ const sampleCounts = /* @__PURE__ */ new Map();
3451
+ const processService = new ProcessService({
3452
+ session,
3453
+ runs,
3454
+ evidence,
3455
+ probes,
3456
+ acquireLease: async () => registry.acquireLeaseForSession(session, "process")
3457
+ });
3458
+ let collectorServer;
3459
+ const collector = {
3460
+ start: async (request) => {
3461
+ const manifest = await session.manifestStore.read();
3462
+ if (manifest.collector !== null) throw new DebugModeError("COLLECTOR_EXISTS", "A collector is already active");
3463
+ const ingest = createIngestHandler({
3464
+ evidence,
3465
+ validateEvent: (event) => probes.validateEvent(event),
3466
+ sample: async (event) => {
3467
+ const probe = (await session.manifestStore.read()).probes.find(
3468
+ (candidate) => candidate.id === event.probeId
3469
+ );
3470
+ if (probe?.sampling.mode !== "every" || probe.sampling.n === 1) return false;
3471
+ const count = (sampleCounts.get(probe.id) ?? 0) + 1;
3472
+ sampleCounts.set(probe.id, count);
3473
+ return count % probe.sampling.n !== 0;
3474
+ }
3475
+ });
3476
+ collectorServer = new CollectorServer(
3477
+ createCollectorRouter({
3478
+ token: session.secret,
3479
+ ingest,
3480
+ onAuthenticated: () => registry.touchSession(session)
3481
+ }),
3482
+ async () => {
3483
+ await service.cleanup.run({
3484
+ reason: "collector-failure",
3485
+ finalReport: terminalReport("escalated", "Collector failed")
3486
+ });
3487
+ }
3488
+ );
3489
+ const handle = await collectorServer.start();
3490
+ let permissionChange;
3491
+ if (request.extensionManifestPath !== void 0) {
3492
+ if (request.runtime !== "extension-background") {
3493
+ await collectorServer.close();
3494
+ throw new DebugModeError(
3495
+ "PERMISSION_MISMATCH",
3496
+ "Extension permissions require extension-background runtime"
3497
+ );
3498
+ }
3499
+ const manifestPath = path13.resolve(session.projectRoot, request.extensionManifestPath);
3500
+ if (!isContained(session.projectRoot, manifestPath)) {
3501
+ await collectorServer.close();
3502
+ throw new DebugModeError("PERMISSION_MISMATCH", "Extension manifest must be inside the project");
3503
+ }
3504
+ const host = handle.host === "::1" ? "[::1]" : handle.host;
3505
+ permissionChange = await addLoopbackPermission(manifestPath, `http://${host}:${handle.port}/*`);
3506
+ }
3507
+ try {
3508
+ await updateManifest(session, (value) => ({
3509
+ ...value,
3510
+ collector: {
3511
+ id: handle.id,
3512
+ host: handle.host,
3513
+ port: handle.port,
3514
+ status: "ready",
3515
+ startedAt: clock.now().toISOString()
3516
+ },
3517
+ permissionChanges: permissionChange === void 0 ? value.permissionChanges : [...value.permissionChanges, permissionChange]
3518
+ }));
3519
+ } catch (error2) {
3520
+ if (permissionChange !== void 0) {
3521
+ await removeLoopbackPermission(permissionChange.manifestPath, permissionChange).catch(() => void 0);
3522
+ }
3523
+ await collectorServer.close();
3524
+ throw error2;
3525
+ }
3526
+ let helper;
3527
+ if (request.transportTargetPath !== void 0) {
3528
+ const transportHelper = new TransportHelper(session.projectRoot, async (owned) => {
3529
+ await updateManifest(session, (value) => ({
3530
+ ...value,
3531
+ ownedFiles: [...value.ownedFiles, { ...owned, kind: "transport-helper" }]
3532
+ }));
3533
+ });
3534
+ helper = await transportHelper.create({
3535
+ targetPath: request.transportTargetPath,
3536
+ host: handle.host,
3537
+ port: handle.port,
3538
+ token: session.secret,
3539
+ runtime: request.runtime
3540
+ });
3541
+ }
3542
+ return {
3543
+ collectorId: handle.id,
3544
+ host: handle.host,
3545
+ port: handle.port,
3546
+ status: handle.status,
3547
+ ...helper === void 0 ? {} : { helperImport: helper.requiredImport, helperPath: helper.relativePath }
3548
+ };
3549
+ }
3550
+ };
3551
+ const cleanup = new CleanupService(session, {
3552
+ collector: { close: async () => collectorServer?.close() }
3553
+ });
3554
+ const service = {
3555
+ evidence,
3556
+ runs,
3557
+ probes,
3558
+ process: processService,
3559
+ collector,
3560
+ cleanup,
3561
+ ...collectorServer === void 0 ? {} : { collectorServer }
3562
+ };
3563
+ services.set(session.publicId, service);
3564
+ return service;
3565
+ };
3566
+ registry = new SessionRegistry(tempBase, clock, async (session) => {
3567
+ const result = await forSession(session).cleanup.run({
3568
+ reason: "idle-expired",
3569
+ finalReport: terminalReport("abandoned", "Debug session expired after inactivity")
3570
+ });
3571
+ services.delete(session.publicId);
3572
+ if (result.status === "partial") {
3573
+ await input.client.app.log({
3574
+ body: {
3575
+ service: "opencode-debug-mode",
3576
+ level: "warn",
3577
+ message: "Idle session cleanup completed with partial failures"
3578
+ }
3579
+ });
3580
+ }
3581
+ });
3582
+ const tools = createDebugTools({
3583
+ registry,
3584
+ runFor: (session) => forSession(session).runs,
3585
+ processFor: (session) => forSession(session).process,
3586
+ collectorFor: (session) => forSession(session).collector,
3587
+ probesFor: (session) => forSession(session).probes,
3588
+ evidenceFor: (session) => forSession(session).evidence,
3589
+ cleanupFor: (session) => forSession(session).cleanup
3590
+ });
3591
+ const logCollision = (name) => {
3592
+ void input.client.app.log({
3593
+ body: { service: "opencode-debug-mode", level: "warn", message: `Replacing conflicting ${name} configuration` }
3594
+ });
3595
+ };
3596
+ return {
3597
+ config: async (config) => {
3598
+ if (config.agent?.debug !== void 0) logCollision("agent.debug");
3599
+ if (config.command?.debug !== void 0) logCollision("command.debug");
3600
+ config.agent ??= {};
3601
+ config.command ??= {};
3602
+ config.agent.debug = {
3603
+ mode: "primary",
3604
+ description: "Hypothesis-driven runtime debugging",
3605
+ prompt
3606
+ };
3607
+ config.command.debug = {
3608
+ description: "Start hypothesis-driven runtime debugging",
3609
+ agent: "debug",
3610
+ template: "$ARGUMENTS"
3611
+ };
3612
+ },
3613
+ tool: tools,
3614
+ event: async ({ event }) => {
3615
+ if (event.type !== "session.deleted") return;
3616
+ const trustedId = event.properties.info.id;
3617
+ try {
3618
+ const session = await registry.requireOwned(trustedId);
3619
+ await forSession(session).cleanup.run({
3620
+ reason: "session-deleted",
3621
+ finalReport: terminalReport("abandoned", "OpenCode session was deleted")
3622
+ });
3623
+ registry.forgetTrusted(trustedId);
3624
+ } catch (error2) {
3625
+ if (!(error2 instanceof DebugModeError && error2.code === "NO_ACTIVE_SESSION")) throw error2;
3626
+ }
3627
+ },
3628
+ "experimental.session.compacting": async ({ sessionID }, output) => {
3629
+ if (await registry.hasTrusted(sessionID)) {
3630
+ output.context.push(
3631
+ "An opencode-debug-mode investigation is active. Before any next action, call debug_state_read and reconcile its revision, phase, completed checks, evidence references, and nextAction. Do not repeat a conclusive check unless the checkpoint records invalidating evidence."
3632
+ );
3633
+ }
3634
+ },
3635
+ dispose: async () => {
3636
+ for (const session of registry.listActive()) {
3637
+ await forSession(session).cleanup.run({
3638
+ reason: "plugin-dispose",
3639
+ finalReport: terminalReport("abandoned", "OpenCode plugin was disposed")
3640
+ });
3641
+ }
3642
+ await registry.closeAll();
3643
+ }
3644
+ };
3645
+ };
3646
+ }
3647
+ var DebugModePlugin = createDebugModePlugin();
3648
+ export {
3649
+ DebugModePlugin,
3650
+ createDebugModePlugin
3651
+ };
3652
+ //# sourceMappingURL=index.js.map