rea-agents 1.4.0 → 1.6.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 (123) hide show
  1. package/README.md +37 -13
  2. package/dist/application/AnalysisSnapshotCache.js +8 -6
  3. package/dist/application/ArtifactGraphConstruction.js +11 -2
  4. package/dist/application/ArtifactInventory.js +3 -2
  5. package/dist/application/AuthorizedArtifactInventory.js +2 -1
  6. package/dist/application/BinarySession.js +26 -11
  7. package/dist/application/BoundedJsonFiles.js +6 -29
  8. package/dist/application/BrowserEvidence.js +107 -0
  9. package/dist/application/BrowserObservationPort.js +1 -0
  10. package/dist/application/BrowserObservationService.js +116 -0
  11. package/dist/application/CapabilityInventory.js +14 -0
  12. package/dist/application/ClientConfigPath.js +33 -0
  13. package/dist/application/ConfiguredRoots.js +19 -0
  14. package/dist/application/CrossVersionInventory.js +16 -3
  15. package/dist/application/CrossVersionInvestigation.js +23 -62
  16. package/dist/application/CrossVersionInvestigationEvidence.js +64 -0
  17. package/dist/application/CrossVersionInvestigationReplay.js +170 -0
  18. package/dist/application/DeferredFileAuthorization.js +61 -0
  19. package/dist/application/DirectAnalysis.js +38 -18
  20. package/dist/application/Doctor.js +16 -11
  21. package/dist/application/ElectronEvidence.js +35 -0
  22. package/dist/application/ElectronObservationPort.js +1 -0
  23. package/dist/application/ElectronObservationService.js +46 -0
  24. package/dist/application/EvidenceLedger.js +11 -7
  25. package/dist/application/InvestigationWorkspaceStore.js +2 -26
  26. package/dist/application/PermissionAuthority.js +30 -0
  27. package/dist/application/ProcessCaptureAuthority.js +3 -2
  28. package/dist/application/ProcessCaptureLifecycle.js +19 -13
  29. package/dist/application/ProcessCheckpoints.js +1 -1
  30. package/dist/application/ProcessCli.js +17 -5
  31. package/dist/application/ProcessHarness.js +19 -26
  32. package/dist/application/ProcessNormalization.js +17 -2
  33. package/dist/application/ProcessOwnership.js +44 -23
  34. package/dist/application/ProcessSampling.js +111 -25
  35. package/dist/application/ProjectPermissionStore.js +107 -3
  36. package/dist/application/ReferenceSourceImport.js +13 -16
  37. package/dist/application/ReferenceSourceImportPolicy.js +3 -2
  38. package/dist/application/Setup.js +89 -30
  39. package/dist/application/SetupClients.js +7 -4
  40. package/dist/application/SetupInstallFailure.js +24 -0
  41. package/dist/application/SetupPlan.js +11 -7
  42. package/dist/application/SetupSkill.js +20 -3
  43. package/dist/application/TerminalRenderer.js +1 -1
  44. package/dist/application/Uninstall.js +20 -7
  45. package/dist/application/Upgrade.js +14 -3
  46. package/dist/artifacts/MachOSliceArtifactReader.js +1 -1
  47. package/dist/artifacts/NativeDmgArtifactReader.js +1 -1
  48. package/dist/artifacts/SafeOutputTree.js +3 -3
  49. package/dist/browser/CdpBrowserProvider.js +261 -0
  50. package/dist/browser/CdpCaptureCompleteness.js +90 -0
  51. package/dist/browser/CdpCaptureDocuments.js +299 -0
  52. package/dist/browser/CdpCaptureEvents.js +547 -0
  53. package/dist/browser/CdpCaptureStorage.js +53 -0
  54. package/dist/browser/CdpCaptureValues.js +68 -0
  55. package/dist/browser/CdpConnection.js +241 -0
  56. package/dist/browser/CdpElectronInspection.js +330 -0
  57. package/dist/browser/CdpElectronProvider.js +134 -0
  58. package/dist/browser/CdpEndpoint.js +173 -0
  59. package/dist/browser/CdpObservationSession.js +301 -0
  60. package/dist/browser/CdpOptionalCommand.js +15 -0
  61. package/dist/browser/CdpPageCapture.js +376 -0
  62. package/dist/browser/CdpSafeMetadata.js +203 -0
  63. package/dist/browser/CdpScreenshot.js +101 -0
  64. package/dist/browser/CdpTargetSession.js +47 -0
  65. package/dist/browser/CdpWebMcpDiscovery.js +242 -0
  66. package/dist/browser/ElectronFileScope.js +50 -0
  67. package/dist/browser/PngVisualDiff.js +177 -0
  68. package/dist/browser/SensitiveTextCapture.js +25 -0
  69. package/dist/browser/WebSourceMapFetcher.js +357 -0
  70. package/dist/catalogIdentity.js +10 -0
  71. package/dist/cli.js +6 -0
  72. package/dist/cliBrowserAdvancedCommands.js +169 -0
  73. package/dist/cliBrowserCommands.js +312 -0
  74. package/dist/cliElectronCommands.js +114 -0
  75. package/dist/cliInvestigationCommands.js +26 -34
  76. package/dist/cliPolicyCommands.js +60 -12
  77. package/dist/config.js +80 -1
  78. package/dist/contracts/browserToolContracts.js +381 -0
  79. package/dist/contracts/electronToolContracts.js +73 -0
  80. package/dist/contracts/errorSchemas.js +2 -0
  81. package/dist/contracts/toolContracts.js +5 -5
  82. package/dist/domain/browserCompleteness.js +101 -0
  83. package/dist/domain/browserObservation.js +587 -0
  84. package/dist/domain/browserSession.js +63 -0
  85. package/dist/domain/changedBehavior.js +4 -15
  86. package/dist/domain/electronObservation.js +162 -0
  87. package/dist/domain/errors.js +41 -0
  88. package/dist/domain/investigationWorkspace.js +4 -0
  89. package/dist/domain/jsonLimits.js +26 -0
  90. package/dist/domain/jsonShape.js +125 -0
  91. package/dist/domain/permissionPolicy.js +6 -2
  92. package/dist/domain/processCapture.js +3 -1
  93. package/dist/domain/processComparison.js +22 -34
  94. package/dist/domain/reconstructionVerification.js +2 -15
  95. package/dist/domain/reconstructionVerificationSchemas.js +2 -0
  96. package/dist/domain/referenceSourceGraph.js +3 -1
  97. package/dist/domain/staticRuntimeCorrelation.js +2 -0
  98. package/dist/domain/symbolAnalysis.js +1 -1
  99. package/dist/domain/webBundleAnalysis.js +195 -0
  100. package/dist/domain/webBundleAnalyzer.js +423 -0
  101. package/dist/domain/webCaptureDiff.js +184 -0
  102. package/dist/domain/webContentArtifact.js +47 -0
  103. package/dist/domain/webInventory.js +61 -0
  104. package/dist/domain/webMcpDiscovery.js +68 -0
  105. package/dist/domain/webScreenshot.js +114 -0
  106. package/dist/generatedPackageMetadata.js +2 -2
  107. package/dist/hopper/BridgeLauncher.js +2 -3
  108. package/dist/hopper/HopperClient.js +51 -11
  109. package/dist/hopper/HopperProvider.js +6 -1
  110. package/dist/main.js +42 -31
  111. package/dist/native/CommandRunner.js +11 -4
  112. package/dist/native/NativeMacOSProvider.js +3 -2
  113. package/dist/native/NativeMachoInspection.js +21 -6
  114. package/dist/server/createServer.js +14 -0
  115. package/dist/server/registerBrowserTools.js +95 -0
  116. package/dist/server/registerElectronTools.js +36 -0
  117. package/dist/server/registerInvestigationTools.js +31 -37
  118. package/dist/server/registerProcessComparisonTool.js +2 -0
  119. package/dist/server/registerSessionTools.js +16 -2
  120. package/dist/server/runDerivedOperation.js +10 -2
  121. package/package.json +6 -1
  122. package/scripts/rea.mjs +2 -2
  123. package/skills/rea-analysis/SKILL.md +28 -3
@@ -0,0 +1,241 @@
1
+ import WebSocket, {} from "ws";
2
+ import { AnalysisCancelledError, AnalysisError, AnalysisTimeoutError, BrowserObservationError, } from "../domain/errors.js";
3
+ const CONNECT_TIMEOUT_MS = 5_000;
4
+ const COMMAND_TIMEOUT_MS = 5_000;
5
+ const MAX_PAYLOAD_BYTES = 16 * 1_024 * 1_024;
6
+ const MAX_PENDING_COMMANDS = 128;
7
+ /** Correlated, bounded JSON-RPC transport for one CDP target operation. */
8
+ export class CdpConnection {
9
+ socket;
10
+ operation;
11
+ #pending = new Map();
12
+ #listeners = new Set();
13
+ #disconnectListeners = new Set();
14
+ #nextId = 1;
15
+ #closed = false;
16
+ #protocolFailed = false;
17
+ constructor(socket, operation) {
18
+ this.socket = socket;
19
+ this.operation = operation;
20
+ socket.on("message", (data) => this.#receive(data));
21
+ socket.on("close", () => this.#disconnect());
22
+ socket.on("error", () => this.#disconnect());
23
+ }
24
+ /** Connect to one already-validated loopback CDP WebSocket. */
25
+ static async connect(url, operation, signal) {
26
+ if (signal?.aborted === true)
27
+ throw new AnalysisCancelledError(operation);
28
+ const socket = new WebSocket(url, {
29
+ handshakeTimeout: CONNECT_TIMEOUT_MS,
30
+ maxPayload: MAX_PAYLOAD_BYTES,
31
+ perMessageDeflate: false,
32
+ });
33
+ await waitForOpen(socket, operation, signal);
34
+ return new CdpConnection(socket, operation);
35
+ }
36
+ /** Subscribe to validated CDP event envelopes. */
37
+ onEvent(listener) {
38
+ this.#listeners.add(listener);
39
+ return () => this.#listeners.delete(listener);
40
+ }
41
+ /** Subscribe to an unexpected transport loss while an operation is active. */
42
+ onDisconnect(listener) {
43
+ if (this.#closed) {
44
+ listener();
45
+ return () => undefined;
46
+ }
47
+ this.#disconnectListeners.add(listener);
48
+ return () => this.#disconnectListeners.delete(listener);
49
+ }
50
+ /** Execute one bounded command, optionally within a flat target session. */
51
+ async send(method, params = {}, sessionId, signal) {
52
+ if (this.#protocolFailed)
53
+ throw new BrowserObservationError(this.operation, "protocol_error");
54
+ if (this.#closed || this.socket.readyState !== WebSocket.OPEN)
55
+ throw new BrowserObservationError(this.operation, "disconnected");
56
+ if (this.#pending.size >= MAX_PENDING_COMMANDS)
57
+ throw new BrowserObservationError(this.operation, "payload_limit");
58
+ if (signal?.aborted === true)
59
+ throw new AnalysisCancelledError(this.operation);
60
+ const id = this.#nextId;
61
+ this.#nextId += 1;
62
+ return await new Promise((resolve, reject) => {
63
+ const onAbort = () => {
64
+ const pending = this.#pending.get(id);
65
+ if (pending === undefined)
66
+ return;
67
+ clearTimeout(pending.timer);
68
+ pending.removeAbort();
69
+ this.#pending.delete(id);
70
+ reject(new AnalysisCancelledError(this.operation));
71
+ };
72
+ signal?.addEventListener("abort", onAbort, { once: true });
73
+ const timer = setTimeout(() => {
74
+ signal?.removeEventListener("abort", onAbort);
75
+ this.#pending.delete(id);
76
+ reject(new AnalysisTimeoutError(this.operation, COMMAND_TIMEOUT_MS));
77
+ }, COMMAND_TIMEOUT_MS);
78
+ this.#pending.set(id, {
79
+ resolve,
80
+ reject,
81
+ timer,
82
+ removeAbort: () => signal?.removeEventListener("abort", onAbort),
83
+ });
84
+ this.socket.send(JSON.stringify({
85
+ id,
86
+ method,
87
+ params,
88
+ ...(sessionId === undefined ? {} : { sessionId }),
89
+ }), (error) => {
90
+ if (error === undefined || error === null)
91
+ return;
92
+ const pending = this.#pending.get(id);
93
+ if (pending === undefined)
94
+ return;
95
+ this.#complete(id, pending);
96
+ reject(new BrowserObservationError(this.operation, "disconnected", {
97
+ cause: error,
98
+ }));
99
+ });
100
+ });
101
+ }
102
+ /** Close only REA's socket; never close the browser or selected page. */
103
+ async close() {
104
+ if (this.#closed)
105
+ return;
106
+ this.#closed = true;
107
+ this.#failPending("disconnected");
108
+ if (this.socket.readyState === WebSocket.CLOSED)
109
+ return;
110
+ await new Promise((resolve) => {
111
+ const timer = setTimeout(() => {
112
+ this.socket.terminate();
113
+ resolve();
114
+ }, 1_000);
115
+ this.socket.once("close", () => {
116
+ clearTimeout(timer);
117
+ resolve();
118
+ });
119
+ this.socket.close();
120
+ });
121
+ }
122
+ #receive(data) {
123
+ let message;
124
+ try {
125
+ message = JSON.parse(rawText(data));
126
+ }
127
+ catch {
128
+ this.#failPending("protocol_error");
129
+ return;
130
+ }
131
+ if (!isRecord(message)) {
132
+ this.#failPending("protocol_error");
133
+ return;
134
+ }
135
+ if ("id" in message) {
136
+ if (typeof message.id !== "number" ||
137
+ !Number.isSafeInteger(message.id) ||
138
+ message.id < 1) {
139
+ this.#failPending("protocol_error");
140
+ return;
141
+ }
142
+ this.#receiveResponse(message, message.id);
143
+ return;
144
+ }
145
+ if (typeof message.method !== "string" ||
146
+ message.method.length === 0 ||
147
+ ("params" in message && !isRecord(message.params)) ||
148
+ ("sessionId" in message && typeof message.sessionId !== "string")) {
149
+ this.#failPending("protocol_error");
150
+ return;
151
+ }
152
+ const event = {
153
+ method: message.method,
154
+ params: "params" in message ? message.params : {},
155
+ ...(typeof message.sessionId === "string"
156
+ ? { sessionId: message.sessionId }
157
+ : {}),
158
+ };
159
+ for (const listener of this.#listeners)
160
+ listener(event);
161
+ }
162
+ #receiveResponse(message, id) {
163
+ const pending = this.#pending.get(id);
164
+ if (pending === undefined)
165
+ return;
166
+ this.#complete(id, pending);
167
+ if ("error" in message) {
168
+ pending.reject(new BrowserObservationError(this.operation, "protocol_error"));
169
+ return;
170
+ }
171
+ pending.resolve("result" in message ? message.result : {});
172
+ }
173
+ #complete(id, pending) {
174
+ clearTimeout(pending.timer);
175
+ pending.removeAbort();
176
+ this.#pending.delete(id);
177
+ }
178
+ #failPending(reason) {
179
+ if (reason === "disconnected")
180
+ this.#closed = true;
181
+ else
182
+ this.#protocolFailed = true;
183
+ for (const [id, pending] of this.#pending) {
184
+ this.#complete(id, pending);
185
+ pending.reject(new BrowserObservationError(this.operation, reason));
186
+ }
187
+ }
188
+ #disconnect() {
189
+ const wasClosed = this.#closed;
190
+ this.#failPending("disconnected");
191
+ if (wasClosed)
192
+ return;
193
+ for (const listener of this.#disconnectListeners)
194
+ listener();
195
+ this.#disconnectListeners.clear();
196
+ }
197
+ }
198
+ const rawText = (data) => Array.isArray(data)
199
+ ? Buffer.concat(data).toString("utf8")
200
+ : Buffer.isBuffer(data)
201
+ ? data.toString("utf8")
202
+ : Buffer.from(data).toString("utf8");
203
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
204
+ const waitForOpen = async (socket, operation, signal) => await new Promise((resolve, reject) => {
205
+ const timer = setTimeout(() => {
206
+ cleanup();
207
+ terminateSocket(socket);
208
+ reject(new AnalysisTimeoutError(operation, CONNECT_TIMEOUT_MS));
209
+ }, CONNECT_TIMEOUT_MS);
210
+ const onOpen = () => {
211
+ cleanup();
212
+ resolve();
213
+ };
214
+ const onFailure = (cause) => {
215
+ cleanup();
216
+ reject(signal?.aborted === true
217
+ ? new AnalysisCancelledError(operation)
218
+ : new BrowserObservationError(operation, "endpoint_unreachable", {
219
+ cause,
220
+ }));
221
+ };
222
+ const onAbort = () => {
223
+ terminateSocket(socket);
224
+ onFailure(new AnalysisCancelledError(operation));
225
+ };
226
+ const cleanup = () => {
227
+ clearTimeout(timer);
228
+ socket.off("open", onOpen);
229
+ socket.off("error", onFailure);
230
+ signal?.removeEventListener("abort", onAbort);
231
+ };
232
+ socket.once("open", onOpen);
233
+ socket.once("error", onFailure);
234
+ signal?.addEventListener("abort", onAbort, { once: true });
235
+ if (signal?.aborted === true)
236
+ onAbort();
237
+ });
238
+ const terminateSocket = (socket) => {
239
+ socket.once("error", () => undefined);
240
+ socket.terminate();
241
+ };
@@ -0,0 +1,330 @@
1
+ import { createHash } from "node:crypto";
2
+ import canonicalize from "canonicalize";
3
+ import { BrowserObservationError } from "../domain/errors.js";
4
+ import { createWebTextArtifact } from "../domain/webContentArtifact.js";
5
+ import { CdpCaptureCompleteness } from "./CdpCaptureCompleteness.js";
6
+ import { boundedText, delayWithCancellation, numberValue, recordValue, recordsValue, requiredRecord, stringValue, } from "./CdpCaptureValues.js";
7
+ import { authorizedElectronFile, canonicalElectronRoots, } from "./ElectronFileScope.js";
8
+ /** Passively inspect one root-confined Electron file page. */
9
+ export const inspectCdpElectronPage = async (context) => {
10
+ const roots = await canonicalElectronRoots(context.input.allowed_file_roots);
11
+ const completeness = new CdpCaptureCompleteness();
12
+ const scripts = [];
13
+ let mainFrameId;
14
+ let navigationDuringCapture = false;
15
+ const removeListener = context.connection.onEvent((event) => {
16
+ if (event.sessionId !== context.sessionId)
17
+ return;
18
+ ingestScript(event, scripts, completeness);
19
+ if (mainFrameId !== undefined && navigatedFrameId(event) === mainFrameId)
20
+ navigationDuringCapture = true;
21
+ });
22
+ const startedAt = new Date().toISOString();
23
+ try {
24
+ await report(context.progress, 1, "Authorizing Electron file frames");
25
+ await context.connection.send("Page.enable", {}, context.sessionId, context.signal);
26
+ const before = await context.connection.send("Page.getFrameTree", {}, context.sessionId, context.signal);
27
+ const frames = await captureFrames(before, roots, context.input.limits.max_frames, completeness);
28
+ const main = frames[0];
29
+ if (main === undefined)
30
+ throw new BrowserObservationError("inspect_web_page", "target_not_allowed");
31
+ mainFrameId = main.frame_id;
32
+ await context.connection.send("Debugger.enable", { maxScriptsCacheSize: 4 * 1_024 * 1_024 }, context.sessionId, context.signal);
33
+ await delayWithCancellation(context.input.observation_ms, "inspect_electron_page", context.signal);
34
+ await report(context.progress, 2, "Capturing Electron structure");
35
+ const resources = await captureResources(context, roots, completeness);
36
+ const dom = await captureDom(context, roots, completeness);
37
+ const normalizedScripts = await captureScripts(context, roots, scripts, completeness);
38
+ const after = await context.connection.send("Page.getFrameTree", {}, context.sessionId, context.signal);
39
+ const afterMain = await mainFilePath(after, roots);
40
+ if (afterMain === undefined)
41
+ throw new BrowserObservationError("inspect_web_page", "target_not_allowed");
42
+ if (navigationDuringCapture || afterMain !== main.file_path)
43
+ throw new BrowserObservationError("inspect_web_page", "target_changed");
44
+ await report(context.progress, 3, "Electron inspection complete");
45
+ return {
46
+ schema_version: 1,
47
+ browser: context.discovery.version,
48
+ target: {
49
+ target_id: context.target.id,
50
+ type: context.target.type,
51
+ title: context.target.title.slice(0, 16_384),
52
+ file_path: main.file_path,
53
+ attached: context.target.attached,
54
+ },
55
+ capture_window: {
56
+ started_at: startedAt,
57
+ ended_at: new Date().toISOString(),
58
+ observation_ms: context.input.observation_ms,
59
+ },
60
+ completeness: completeness.snapshot(),
61
+ frames,
62
+ dom,
63
+ scripts: normalizedScripts,
64
+ resources,
65
+ limitations: [
66
+ "Only canonical file paths contained by an approved root are retained.",
67
+ "REA does not evaluate renderer JavaScript, invoke Electron APIs, navigate, click, or close the page.",
68
+ "Script contents require separate source-capture approval and remain byte bounded.",
69
+ ],
70
+ };
71
+ }
72
+ finally {
73
+ removeListener();
74
+ }
75
+ };
76
+ const navigatedFrameId = (event) => {
77
+ const params = recordValue(event.params);
78
+ if (params === undefined)
79
+ return undefined;
80
+ if (event.method === "Page.frameNavigated")
81
+ return stringValue(recordValue(params.frame)?.id);
82
+ if (event.method === "Page.navigatedWithinDocument")
83
+ return stringValue(params.frameId);
84
+ return undefined;
85
+ };
86
+ const ingestScript = (event, scripts, completeness) => {
87
+ if (event.method !== "Debugger.scriptParsed")
88
+ return;
89
+ const value = recordValue(event.params);
90
+ const scriptId = stringValue(value?.scriptId);
91
+ const rawUrl = stringValue(value?.url);
92
+ if (scriptId === undefined || scriptId.length > 256) {
93
+ completeness.exclude("scripts", "invalid_protocol_value");
94
+ return;
95
+ }
96
+ if (rawUrl === undefined || rawUrl === "") {
97
+ completeness.exclude("scripts", "unattributed_origin");
98
+ return;
99
+ }
100
+ if (scripts.length >= 2_000) {
101
+ completeness.drop("scripts");
102
+ return;
103
+ }
104
+ scripts.push({
105
+ scriptId,
106
+ rawUrl,
107
+ hash: (stringValue(value?.hash) ?? "").slice(0, 512),
108
+ length: nonnegativeInteger(value?.length),
109
+ isModule: value?.isModule === true,
110
+ language: boundedText(value?.scriptLanguage, 100),
111
+ });
112
+ };
113
+ const captureFrames = async (result, roots, maximum, completeness) => {
114
+ const frameTree = recordValue(requiredRecord(result).frameTree);
115
+ if (frameTree === undefined)
116
+ throw new BrowserObservationError("inspect_web_page", "protocol_error");
117
+ const pending = [{ tree: frameTree, parent: null }];
118
+ const frames = [];
119
+ while (pending.length > 0) {
120
+ const current = pending.shift();
121
+ if (current === undefined)
122
+ break;
123
+ const frame = recordValue(current.tree.frame);
124
+ const frameId = stringValue(frame?.id);
125
+ const path = await authorizedElectronFile(stringValue(frame?.url) ?? "", roots);
126
+ if (frameId === undefined || path === undefined) {
127
+ completeness.exclude("frames", "out_of_target_scope");
128
+ continue;
129
+ }
130
+ if (frames.length >= maximum) {
131
+ completeness.truncate("frames");
132
+ continue;
133
+ }
134
+ frames.push({
135
+ frame_id: frameId.slice(0, 256),
136
+ parent_frame_id: current.parent,
137
+ file_path: path,
138
+ });
139
+ for (const child of recordsValue(current.tree.childFrames))
140
+ pending.push({ tree: child, parent: frameId.slice(0, 256) });
141
+ }
142
+ return frames;
143
+ };
144
+ const captureResources = async (context, roots, completeness) => {
145
+ const result = await context.connection.send("Page.getResourceTree", {}, context.sessionId, context.signal);
146
+ const root = recordValue(requiredRecord(result).frameTree);
147
+ if (root === undefined)
148
+ throw new BrowserObservationError("inspect_web_page", "protocol_error");
149
+ const pending = [root];
150
+ const resources = [];
151
+ const seen = new Set();
152
+ while (pending.length > 0) {
153
+ const tree = pending.shift();
154
+ if (tree === undefined)
155
+ break;
156
+ for (const resource of recordsValue(tree.resources)) {
157
+ const path = await authorizedElectronFile(stringValue(resource.url) ?? "", roots);
158
+ if (path === undefined) {
159
+ completeness.exclude("resources", "out_of_target_scope");
160
+ continue;
161
+ }
162
+ const item = {
163
+ file_path: path,
164
+ type: (stringValue(resource.type) ?? "Other").slice(0, 100),
165
+ mime_type: (stringValue(resource.mimeType) ?? "").slice(0, 256),
166
+ content_size: numberValue(resource.contentSize) === undefined
167
+ ? null
168
+ : Math.max(0, numberValue(resource.contentSize) ?? 0),
169
+ };
170
+ const resourceKey = `electron_resource_${digest(item)}`;
171
+ if (seen.has(resourceKey))
172
+ continue;
173
+ seen.add(resourceKey);
174
+ if (resources.length >= context.input.limits.max_resources) {
175
+ completeness.truncate("resources");
176
+ continue;
177
+ }
178
+ resources.push({
179
+ resource_key: resourceKey,
180
+ ...item,
181
+ });
182
+ }
183
+ pending.push(...recordsValue(tree.childFrames));
184
+ }
185
+ return resources.sort((left, right) => left.resource_key.localeCompare(right.resource_key));
186
+ };
187
+ const captureDom = async (context, roots, completeness) => {
188
+ const result = requiredRecord(await context.connection.send("DOMSnapshot.captureSnapshot", { computedStyles: [], includePaintOrder: false, includeDOMRects: false }, context.sessionId, context.signal));
189
+ const strings = Array.isArray(result.strings)
190
+ ? result.strings.map((value) => (typeof value === "string" ? value : ""))
191
+ : [];
192
+ const nodes = [];
193
+ let total = 0;
194
+ for (const document of recordsValue(result.documents)) {
195
+ const documentUrl = stringAt(strings, document.documentURL);
196
+ if ((await authorizedElectronFile(documentUrl, roots)) === undefined) {
197
+ completeness.exclude("dom", "out_of_target_scope");
198
+ continue;
199
+ }
200
+ const nodeData = recordValue(document.nodes);
201
+ const nodeTypes = arrayValue(nodeData?.nodeType);
202
+ const nodeNames = arrayValue(nodeData?.nodeName);
203
+ const nodeValues = arrayValue(nodeData?.nodeValue);
204
+ const parents = arrayValue(nodeData?.parentIndex);
205
+ const attributes = arrayValue(nodeData?.attributes);
206
+ const baseIndex = nodes.length;
207
+ total += nodeTypes.length;
208
+ for (let index = 0; index < nodeTypes.length; index += 1) {
209
+ if (nodes.length >= context.input.limits.max_dom_nodes)
210
+ continue;
211
+ const attributeIndexes = arrayValue(attributes[index]);
212
+ const parent = integer(parents[index], -1);
213
+ nodes.push({
214
+ index: nodes.length,
215
+ parent_index: parent < 0 ? -1 : baseIndex + parent,
216
+ node_type: integer(nodeTypes[index], 0),
217
+ node_name: stringAt(strings, nodeNames[index]).slice(0, 1_024),
218
+ node_value_length: Buffer.byteLength(stringAt(strings, nodeValues[index])),
219
+ attribute_names: attributeIndexes
220
+ .filter((_value, attributeIndex) => attributeIndex % 2 === 0)
221
+ .map((value) => stringAt(strings, value).slice(0, 1_024))
222
+ .slice(0, 256),
223
+ });
224
+ }
225
+ }
226
+ if (total > nodes.length)
227
+ completeness.truncate("dom");
228
+ return { total_nodes: total, nodes };
229
+ };
230
+ const captureScripts = async (context, roots, scripts, completeness) => {
231
+ let total = 0;
232
+ let sourceBytes = 0;
233
+ const items = [];
234
+ const seen = new Set();
235
+ for (const script of scripts) {
236
+ const path = await authorizedElectronFile(script.rawUrl, roots);
237
+ if (path === undefined) {
238
+ completeness.exclude("scripts", "out_of_target_scope");
239
+ continue;
240
+ }
241
+ const identity = {
242
+ file_path: path,
243
+ cdp_hash: script.hash,
244
+ length: script.length,
245
+ is_module: script.isModule,
246
+ language: script.language,
247
+ };
248
+ const scriptKey = `electron_script_${digest(identity)}`;
249
+ if (seen.has(scriptKey))
250
+ continue;
251
+ seen.add(scriptKey);
252
+ total += 1;
253
+ if (items.length >= context.input.limits.max_scripts) {
254
+ completeness.drop("scripts");
255
+ continue;
256
+ }
257
+ let source = {
258
+ included: false,
259
+ reason: "source capture was not approved",
260
+ };
261
+ if (context.input.include_script_sources) {
262
+ if (script.length > context.input.limits.max_script_source_bytes ||
263
+ sourceBytes + script.length >
264
+ context.input.limits.max_total_script_source_bytes) {
265
+ source = {
266
+ included: false,
267
+ reason: "script source byte limit reached",
268
+ };
269
+ completeness.truncate("script_sources");
270
+ }
271
+ else {
272
+ const result = requiredRecord(await context.connection.send("Debugger.getScriptSource", { scriptId: script.scriptId }, context.sessionId, context.signal));
273
+ const text = stringValue(result.scriptSource) ?? "";
274
+ const bytes = Buffer.byteLength(text);
275
+ if (bytes > context.input.limits.max_script_source_bytes ||
276
+ sourceBytes + bytes >
277
+ context.input.limits.max_total_script_source_bytes) {
278
+ source = {
279
+ included: false,
280
+ reason: "script source byte limit reached",
281
+ };
282
+ completeness.truncate("script_sources");
283
+ }
284
+ else {
285
+ source = {
286
+ included: true,
287
+ artifact: createWebTextArtifact(text, "text/javascript"),
288
+ };
289
+ sourceBytes += bytes;
290
+ }
291
+ }
292
+ }
293
+ items.push({
294
+ script_key: scriptKey,
295
+ ...identity,
296
+ source,
297
+ });
298
+ }
299
+ if (!context.input.include_script_sources)
300
+ completeness.exclude("script_sources", "not_approved", total);
301
+ return {
302
+ total,
303
+ items: items.sort((left, right) => left.script_key.localeCompare(right.script_key)),
304
+ };
305
+ };
306
+ const mainFilePath = async (result, roots) => {
307
+ const frameTree = recordValue(requiredRecord(result).frameTree);
308
+ const frame = recordValue(frameTree?.frame);
309
+ return await authorizedElectronFile(stringValue(frame?.url) ?? "", roots);
310
+ };
311
+ const arrayValue = (value) => Array.isArray(value) ? value : [];
312
+ const stringAt = (strings, value) => strings[integer(value, -1)] ?? "";
313
+ const integer = (value, fallback) => {
314
+ const number = numberValue(value);
315
+ return number === undefined ? fallback : Math.trunc(number);
316
+ };
317
+ const nonnegativeInteger = (value) => Math.max(0, integer(value, 0));
318
+ const digest = (value) => {
319
+ const encoded = canonicalize(value);
320
+ if (encoded === undefined)
321
+ throw new TypeError("Expected canonical JSON");
322
+ return createHash("sha256").update(encoded).digest("hex");
323
+ };
324
+ const report = async (progress, completed, message) => await progress?.report({
325
+ phase: "browser_observation",
326
+ completed,
327
+ total: 3,
328
+ message,
329
+ ...(completed === 3 ? { terminal: true } : {}),
330
+ });