rea-agents 1.6.0 → 1.7.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 (119) hide show
  1. package/README.md +129 -45
  2. package/bridge/ghidra/ReaGhidraBridge.java +2075 -0
  3. package/bridge/hopper_bridge.py +13 -3
  4. package/dist/application/AbortablePromise.js +25 -0
  5. package/dist/application/AnalysisProvider.js +3 -0
  6. package/dist/application/AnalysisProviderEvaluation.js +60 -0
  7. package/dist/application/AnalysisProviderRegistry.js +228 -0
  8. package/dist/application/AnalysisSnapshotCache.js +41 -21
  9. package/dist/application/AnalysisSnapshotFiles.js +5 -6
  10. package/dist/application/BinarySession.js +218 -101
  11. package/dist/application/CompositeProvider.js +20 -8
  12. package/dist/application/DirectAnalysis.js +49 -35
  13. package/dist/application/Doctor.js +54 -2
  14. package/dist/application/EnhancedTools.js +1 -1
  15. package/dist/application/InvestigationProviders.js +11 -0
  16. package/dist/application/JavaScriptArtifactAnalysis.js +299 -0
  17. package/dist/application/JavaScriptArtifactAnalysisTypes.js +1 -0
  18. package/dist/application/JavaScriptArtifactFiles.js +231 -0
  19. package/dist/application/JavaScriptArtifactGraphAccumulator.js +54 -0
  20. package/dist/application/JavaScriptArtifactGraphBuilder.js +101 -0
  21. package/dist/application/JavaScriptArtifactGraphContext.js +205 -0
  22. package/dist/application/JavaScriptArtifactGraphDocuments.js +108 -0
  23. package/dist/application/JavaScriptArtifactGraphEvidence.js +89 -0
  24. package/dist/application/JavaScriptArtifactGraphFindings.js +271 -0
  25. package/dist/application/JavaScriptArtifactGraphStructure.js +409 -0
  26. package/dist/application/JavaScriptArtifactReconstruction.js +74 -0
  27. package/dist/application/JavaScriptArtifactReconstructionInput.js +99 -0
  28. package/dist/application/MacHopper.js +20 -4
  29. package/dist/application/ProcessCaptureLifecycle.js +1 -1
  30. package/dist/application/ProcessHarness.js +1 -1
  31. package/dist/application/ProcessSampling.js +1 -1
  32. package/dist/application/SessionProviderRouter.js +219 -0
  33. package/dist/application/Setup.js +84 -60
  34. package/dist/application/SetupClients.js +1 -1
  35. package/dist/application/SetupPlan.js +9 -2
  36. package/dist/application/SupportedClients.js +40 -16
  37. package/dist/application/Upgrade.js +1 -1
  38. package/dist/application/runtime.js +9 -6
  39. package/dist/artifacts/AsarArtifactReader.js +23 -3
  40. package/dist/browser/CdpBrowserProvider.js +4 -2
  41. package/dist/browser/CdpElectronProvider.js +4 -2
  42. package/dist/browser/CdpEndpoint.js +14 -1
  43. package/dist/catalogIdentity.js +2 -41
  44. package/dist/cli.js +58 -34
  45. package/dist/cliBrowserAdvancedCommands.js +5 -4
  46. package/dist/cliBrowserCommands.js +5 -4
  47. package/dist/cliCommandNames.js +43 -0
  48. package/dist/cliElectronCommands.js +3 -2
  49. package/dist/cliEvidenceCommands.js +4 -3
  50. package/dist/cliInvestigationCommands.js +2 -1
  51. package/dist/cliPolicyCommands.js +2 -1
  52. package/dist/cliProcessCommands.js +3 -2
  53. package/dist/config.js +18 -2
  54. package/dist/contracts/browserToolContracts.js +5 -5
  55. package/dist/contracts/electronToolContracts.js +3 -3
  56. package/dist/contracts/enhancedInputs.js +2 -2
  57. package/dist/contracts/providerSelection.js +28 -0
  58. package/dist/contracts/sessionLifecycleInputs.js +2 -0
  59. package/dist/contracts/toolContracts.js +22 -20
  60. package/dist/contracts/toolOutputSchemas.js +117 -22
  61. package/dist/doctorRuntime.js +6 -0
  62. package/dist/domain/analysisProfile.js +78 -0
  63. package/dist/domain/analysisSnapshot.js +92 -46
  64. package/dist/domain/artifactGraph.js +1 -0
  65. package/dist/domain/binaryTarget.js +4 -29
  66. package/dist/domain/errors.js +81 -6
  67. package/dist/domain/evidence.js +44 -5
  68. package/dist/domain/evidenceBundle.js +9 -7
  69. package/dist/domain/functionComparison.js +9 -3
  70. package/dist/domain/functionComparisonNormalization.js +37 -0
  71. package/dist/domain/functionDossierEvidence.js +4 -1
  72. package/dist/domain/hopperValues.js +66 -9
  73. package/dist/domain/investigationWorkspace.js +2 -1
  74. package/dist/domain/javascriptApplicationEvidenceSchemas.js +361 -0
  75. package/dist/domain/javascriptApplicationGraph.js +281 -0
  76. package/dist/domain/javascriptApplicationGraphSchemas.js +132 -0
  77. package/dist/domain/javascriptAstFingerprint.js +143 -0
  78. package/dist/domain/javascriptStaticAnalysis.js +358 -0
  79. package/dist/domain/javascriptStaticAnalysisHelpers.js +264 -0
  80. package/dist/domain/javascriptStaticAnalysisState.js +17 -0
  81. package/dist/domain/javascriptStaticAnalysisTypes.js +1 -0
  82. package/dist/generatedPackageMetadata.js +1 -1
  83. package/dist/ghidra/GhidraAnalysisProfile.js +37 -0
  84. package/dist/ghidra/GhidraClient.js +400 -0
  85. package/dist/ghidra/GhidraClientTypes.js +1 -0
  86. package/dist/ghidra/GhidraDefaults.js +20 -0
  87. package/dist/ghidra/GhidraDiagnostics.js +43 -0
  88. package/dist/ghidra/GhidraDoctor.js +59 -0
  89. package/dist/ghidra/GhidraFunctionValues.js +264 -0
  90. package/dist/ghidra/GhidraInstallation.js +239 -0
  91. package/dist/ghidra/GhidraInventoryValues.js +217 -0
  92. package/dist/ghidra/GhidraLauncher.js +139 -0
  93. package/dist/ghidra/GhidraProvider.js +360 -0
  94. package/dist/ghidra/GhidraRequestQueue.js +98 -0
  95. package/dist/ghidra/GhidraResponseBuffer.js +31 -0
  96. package/dist/ghidra/GhidraResponseRouter.js +24 -0
  97. package/dist/ghidra/GhidraSessionError.js +39 -0
  98. package/dist/ghidra/GhidraSessionValues.js +62 -0
  99. package/dist/ghidra/GhidraSocketConnection.js +46 -0
  100. package/dist/ghidra/protocol.js +69 -0
  101. package/dist/hopper/BridgeLauncher.js +19 -54
  102. package/dist/hopper/HopperAnalysisProfile.js +106 -0
  103. package/dist/hopper/HopperClient.js +154 -162
  104. package/dist/hopper/HopperProvider.js +99 -7
  105. package/dist/hopper/HopperSessionValues.js +27 -0
  106. package/dist/hopper/HopperSocketConnection.js +31 -0
  107. package/dist/native/NativeMacOSProvider.js +3 -1
  108. package/dist/process/PendingOperations.js +59 -0
  109. package/dist/process/PrivateRuntimeRoot.js +33 -0
  110. package/dist/process/ProviderDeadline.js +95 -0
  111. package/dist/process/ProviderProcess.js +275 -0
  112. package/dist/server/createServer.js +1 -0
  113. package/dist/server/promptCompletion.js +9 -3
  114. package/dist/server/registerEnhancedTools.js +9 -6
  115. package/dist/server/registerOfficialTools.js +3 -0
  116. package/dist/server/registerSessionTools.js +6 -1
  117. package/package.json +8 -6
  118. package/scripts/rea.mjs +35 -1
  119. /package/dist/{application → process}/ProcessOwnership.js +0 -0
@@ -0,0 +1,409 @@
1
+ import { addArtifactContainsEdge, addAstContainsEdge, addUnavailableStaticParseScope, artifactLocalIdentity, createElectronRoleNode, javascriptAnalysisCoverage, linkElectronRoleToAsset, moduleLookupKey, resolveArtifactPath, } from "./JavaScriptArtifactGraphContext.js";
2
+ import { artifactObservationEvidence, astObservationEvidence, completeReconstructionCoverage, staticInferenceEvidence, } from "./JavaScriptArtifactGraphEvidence.js";
3
+ /** Create the exact root artifact node from the inventory manifest. */
4
+ export const createJavaScriptArtifactRootNode = (accumulator, snapshot) => {
5
+ const root = snapshot.nodes.find(({ artifact_id: id }) => id === snapshot.manifest.root_artifact_id);
6
+ if (root === undefined)
7
+ throw new TypeError("Artifact inventory root is missing");
8
+ return accumulator.addNode({
9
+ kind: "artifact",
10
+ identity: {
11
+ strategy: "content-digest",
12
+ stability: "global-exact",
13
+ sha256: root.sha256,
14
+ },
15
+ observations: [
16
+ {
17
+ label: "artifact root",
18
+ properties: {
19
+ format: snapshot.manifest.root_format,
20
+ bytes: root.size,
21
+ inventory_manifest_id: snapshot.manifest.manifest_id,
22
+ inventory_graph_sha256: snapshot.manifest.graph_sha256,
23
+ inventory_artifact_id: root.artifact_id,
24
+ },
25
+ evidence: artifactObservationEvidence({
26
+ sha256: root.sha256,
27
+ path: "artifact-root",
28
+ operation: "inventory-root",
29
+ coverage: completeReconstructionCoverage(),
30
+ limitations: [
31
+ "artifact-root is a path-independent alias; the application result retains the canonical local input path.",
32
+ ],
33
+ }),
34
+ },
35
+ ],
36
+ });
37
+ };
38
+ /** Project nested ASAR containers inventoried inside a directory. */
39
+ export const addJavaScriptArtifactContainers = (context) => {
40
+ for (const container of context.fileSet.containers) {
41
+ const node = context.accumulator.addNode({
42
+ kind: "artifact",
43
+ identity: {
44
+ strategy: "content-digest",
45
+ stability: "global-exact",
46
+ sha256: container.sha256,
47
+ },
48
+ observations: [
49
+ {
50
+ label: container.path,
51
+ properties: containerProperties(container),
52
+ evidence: artifactObservationEvidence({
53
+ sha256: container.sha256,
54
+ path: container.path,
55
+ operation: "inventory-nested-asar",
56
+ coverage: completeReconstructionCoverage(),
57
+ }),
58
+ },
59
+ ],
60
+ });
61
+ context.containerNodes.set(container.sha256, node);
62
+ context.accumulator.addEdge({
63
+ source_node_id: context.root.node_id,
64
+ target_node_id: node.node_id,
65
+ relation: "contains",
66
+ properties: { path: container.path, format: "asar" },
67
+ evidence: artifactObservationEvidence({
68
+ sha256: context.snapshot.manifest.root_sha256,
69
+ path: container.path,
70
+ operation: "inventory-nested-asar",
71
+ coverage: completeReconstructionCoverage(),
72
+ }),
73
+ });
74
+ }
75
+ };
76
+ /** Project relevant files, ASAR entries, and explicit unavailable parse scopes. */
77
+ export const addJavaScriptArtifactFiles = (context) => {
78
+ for (const analyzed of context.analysis.files) {
79
+ const { file } = analyzed;
80
+ const target = createFileTarget(context, file, analyzed.javascript);
81
+ context.fileNodes.set(file.path, target);
82
+ if (file.kind === "javascript")
83
+ context.assetNodes.set(file.path, target);
84
+ const entry = createAsarEntry(context, file);
85
+ const parent = context.containerNodes.get(file.container_sha256) ?? context.root;
86
+ if (entry === undefined)
87
+ addArtifactContainsEdge(context, {
88
+ source: parent,
89
+ target,
90
+ file,
91
+ operation: "inventory-file",
92
+ });
93
+ else {
94
+ addArtifactContainsEdge(context, {
95
+ source: parent,
96
+ target: entry,
97
+ file,
98
+ operation: "inventory-entry",
99
+ });
100
+ context.accumulator.addEdge({
101
+ source_node_id: entry.node_id,
102
+ target_node_id: target.node_id,
103
+ relation: "maps_to",
104
+ properties: { sha256: file.sha256 },
105
+ evidence: artifactObservationEvidence({
106
+ sha256: file.container_sha256,
107
+ path: file.path,
108
+ operation: "map-entry-content",
109
+ coverage: completeReconstructionCoverage(),
110
+ }),
111
+ });
112
+ }
113
+ if (file.kind === "javascript" &&
114
+ (analyzed.javascript === null ||
115
+ analyzed.javascript.parse_status === "failed"))
116
+ addUnavailableStaticParseScope(context, {
117
+ file,
118
+ asset: target,
119
+ operation: "parse-javascript",
120
+ limitation: file.text.included
121
+ ? "JavaScript syntax could not be parsed."
122
+ : `JavaScript text was unavailable: ${file.text.reason}.`,
123
+ });
124
+ const packageValue = context.analysis.packages.find(({ path }) => path === file.path);
125
+ if (packageValue?.status !== undefined &&
126
+ packageValue.status !== "included")
127
+ addUnavailableStaticParseScope(context, {
128
+ file,
129
+ asset: target,
130
+ operation: "parse-package-json",
131
+ limitation: packageValue.limitation ?? "Package metadata could not be parsed.",
132
+ });
133
+ const sourceMap = context.analysis.source_maps.find(({ path }) => path === file.path);
134
+ if (sourceMap !== undefined &&
135
+ (sourceMap.status === "invalid" || sourceMap.status === "not-approved"))
136
+ addUnavailableStaticParseScope(context, {
137
+ file,
138
+ asset: target,
139
+ operation: "parse-local-source-map",
140
+ limitation: sourceMap.limitation ?? "Source-map content could not be parsed.",
141
+ });
142
+ }
143
+ };
144
+ /** Project package metadata and its declared Electron roles. */
145
+ export const addJavaScriptPackageNodes = (context) => {
146
+ const roots = [];
147
+ for (const packageValue of context.analysis.packages) {
148
+ const file = context.filesByPath.get(packageValue.path);
149
+ if (file === undefined)
150
+ continue;
151
+ const node = context.accumulator.addNode({
152
+ kind: "package",
153
+ identity: artifactLocalIdentity(file.sha256, "package-json", file.path),
154
+ observations: [
155
+ {
156
+ label: packageValue.name ?? file.path,
157
+ properties: {
158
+ path: file.path,
159
+ name: packageValue.name,
160
+ version: packageValue.version,
161
+ main: packageValue.main,
162
+ renderer: packageValue.renderer,
163
+ parse_status: packageValue.status,
164
+ },
165
+ evidence: artifactObservationEvidence({
166
+ sha256: file.sha256,
167
+ path: file.path,
168
+ operation: "parse-package-json",
169
+ coverage: completeReconstructionCoverage(),
170
+ limitations: packageValue.limitation === null ? [] : [packageValue.limitation],
171
+ }),
172
+ },
173
+ ],
174
+ });
175
+ if (roots.length === 0) {
176
+ roots.push(node);
177
+ context.accumulator.addEdge({
178
+ source_node_id: node.node_id,
179
+ target_node_id: context.root.node_id,
180
+ relation: "contains",
181
+ properties: { basis: "package-metadata-within-artifact" },
182
+ evidence: staticInferenceEvidence({
183
+ sha256: file.sha256,
184
+ path: file.path,
185
+ operation: "associate-package-artifact",
186
+ coverage: completeReconstructionCoverage(),
187
+ }),
188
+ });
189
+ }
190
+ else
191
+ addArtifactContainsEdge(context, {
192
+ source: context.root,
193
+ target: node,
194
+ file,
195
+ operation: "inventory-package",
196
+ });
197
+ addPackageRole(context, {
198
+ packageNode: node,
199
+ packageFile: file,
200
+ kind: "electron-main",
201
+ declaredPath: packageValue.main,
202
+ });
203
+ addPackageRole(context, {
204
+ packageNode: node,
205
+ packageFile: file,
206
+ kind: "electron-renderer",
207
+ declaredPath: packageValue.renderer,
208
+ });
209
+ }
210
+ return roots;
211
+ };
212
+ /** Project Webpack/Rspack chunk and factory literals recovered from AST. */
213
+ export const addJavaScriptBundlerNodes = (context) => {
214
+ for (const analyzed of context.analysis.files) {
215
+ const { file, javascript } = analyzed;
216
+ const asset = context.assetNodes.get(file.path);
217
+ if (javascript === null || asset === undefined)
218
+ continue;
219
+ const coverage = javascriptAnalysisCoverage(javascript, context.input);
220
+ for (const registration of javascript.bundler_registrations) {
221
+ const chunkKey = `${registration.runtime}:${registration.chunk_keys.join(",")}`;
222
+ const chunk = context.accumulator.addNode({
223
+ kind: "javascript-chunk",
224
+ identity: artifactLocalIdentity(file.sha256, "bundler-chunk", chunkKey),
225
+ observations: [
226
+ {
227
+ label: chunkKey,
228
+ properties: {
229
+ path: file.path,
230
+ bundler: registration.bundler,
231
+ runtime: registration.runtime,
232
+ chunk_keys: registration.chunk_keys,
233
+ omitted_chunk_keys: registration.omitted_chunk_keys,
234
+ unknown_chunk_keys: registration.unknown_chunk_keys,
235
+ module_count: registration.modules.length,
236
+ },
237
+ evidence: astObservationEvidence({
238
+ sha256: file.sha256,
239
+ path: file.path,
240
+ range: registration.location,
241
+ operation: "extract-bundler-registration",
242
+ coverage,
243
+ limitations: javascript.limitations,
244
+ }),
245
+ },
246
+ ],
247
+ });
248
+ addAstContainsEdge(context, {
249
+ source: asset,
250
+ target: chunk,
251
+ file,
252
+ range: registration.location,
253
+ coverage,
254
+ properties: { bundler: registration.bundler },
255
+ });
256
+ for (const moduleValue of registration.modules) {
257
+ const module = context.accumulator.addNode({
258
+ kind: "javascript-module",
259
+ identity: artifactLocalIdentity(file.sha256, `${registration.runtime}:module`, moduleValue.module_key),
260
+ observations: [
261
+ {
262
+ label: moduleValue.module_key,
263
+ properties: {
264
+ path: file.path,
265
+ bundler: registration.bundler,
266
+ runtime: registration.runtime,
267
+ module_key: moduleValue.module_key,
268
+ source_sha256: moduleValue.source_sha256,
269
+ structural_fingerprint_sha256: moduleValue.structural_fingerprint_sha256,
270
+ structural_fingerprint_algorithm: moduleValue.structural_fingerprint_algorithm,
271
+ structural_fingerprint_status: moduleValue.structural_fingerprint_status,
272
+ exports: moduleValue.exports,
273
+ exports_truncated: moduleValue.exports_truncated,
274
+ },
275
+ evidence: astObservationEvidence({
276
+ sha256: file.sha256,
277
+ path: file.path,
278
+ range: moduleValue.location,
279
+ operation: "extract-bundler-module",
280
+ coverage,
281
+ limitations: javascript.limitations,
282
+ }),
283
+ },
284
+ ],
285
+ });
286
+ context.moduleNodes.set(moduleLookupKey(file.path, moduleValue.module_key), module);
287
+ addAstContainsEdge(context, {
288
+ source: chunk,
289
+ target: module,
290
+ file,
291
+ range: moduleValue.location,
292
+ coverage,
293
+ properties: { module_key: moduleValue.module_key },
294
+ });
295
+ }
296
+ }
297
+ }
298
+ };
299
+ const createFileTarget = (context, file, javascript) => {
300
+ const kind = artifactFileNodeKind(file.kind);
301
+ return context.accumulator.addNode({
302
+ kind,
303
+ identity: {
304
+ strategy: "content-digest",
305
+ stability: "global-exact",
306
+ sha256: file.sha256,
307
+ },
308
+ observations: [
309
+ {
310
+ label: file.path,
311
+ properties: {
312
+ path: file.path,
313
+ bytes: file.bytes,
314
+ inventory_artifact_id: file.inventory_artifact_id,
315
+ unpacked: file.unpacked,
316
+ file_kind: file.kind,
317
+ text_status: file.text.included ? "included" : file.text.reason,
318
+ parse_status: javascript?.parse_status ?? null,
319
+ vendor_markers: javascript?.vendors ?? [],
320
+ },
321
+ evidence: artifactObservationEvidence({
322
+ sha256: file.sha256,
323
+ path: file.path,
324
+ operation: "inventory-relevant-file",
325
+ coverage: completeReconstructionCoverage(),
326
+ }),
327
+ },
328
+ ],
329
+ });
330
+ };
331
+ const artifactFileNodeKind = (kind) => kind === "javascript"
332
+ ? "javascript-asset"
333
+ : kind === "source-map"
334
+ ? "source-map"
335
+ : kind === "native-addon"
336
+ ? "native-addon"
337
+ : "artifact";
338
+ const createAsarEntry = (context, file) => {
339
+ const isAsar = context.snapshot.manifest.root_format === "asar" ||
340
+ file.container_sha256 !== context.snapshot.manifest.root_sha256;
341
+ if (!isAsar)
342
+ return undefined;
343
+ return context.accumulator.addNode({
344
+ kind: "asar-entry",
345
+ identity: {
346
+ strategy: "canonical-path",
347
+ stability: "artifact-version",
348
+ artifact_sha256: file.container_sha256,
349
+ path: file.path,
350
+ },
351
+ observations: [
352
+ {
353
+ label: file.path,
354
+ properties: {
355
+ entry_sha256: file.sha256,
356
+ bytes: file.bytes,
357
+ unpacked: file.unpacked,
358
+ inventory_artifact_id: file.inventory_artifact_id,
359
+ },
360
+ evidence: artifactObservationEvidence({
361
+ sha256: file.container_sha256,
362
+ path: file.path,
363
+ operation: "inventory-asar-entry",
364
+ coverage: completeReconstructionCoverage(),
365
+ }),
366
+ },
367
+ ],
368
+ });
369
+ };
370
+ const addPackageRole = (context, input) => {
371
+ if (input.declaredPath === null)
372
+ return;
373
+ const resolvedPath = resolveArtifactPath(input.declaredPath, input.packageFile.path, context.filesByPath);
374
+ const role = createElectronRoleNode(context, {
375
+ kind: input.kind,
376
+ anchor: input.packageFile,
377
+ declaredPath: input.declaredPath,
378
+ resolvedPath,
379
+ mechanism: input.kind === "electron-main"
380
+ ? "package.json:main"
381
+ : "package.json:renderer",
382
+ });
383
+ context.accumulator.addEdge({
384
+ source_node_id: input.packageNode.node_id,
385
+ target_node_id: role.node_id,
386
+ relation: "loads",
387
+ properties: {
388
+ declared_path: input.declaredPath,
389
+ resolved_path: resolvedPath,
390
+ },
391
+ evidence: staticInferenceEvidence({
392
+ sha256: input.packageFile.sha256,
393
+ path: input.packageFile.path,
394
+ operation: "discover-package-entrypoint",
395
+ coverage: completeReconstructionCoverage(),
396
+ }),
397
+ });
398
+ linkElectronRoleToAsset(context, {
399
+ role,
400
+ anchor: input.packageFile,
401
+ path: resolvedPath,
402
+ });
403
+ };
404
+ const containerProperties = (container) => ({
405
+ path: container.path,
406
+ format: "asar",
407
+ bytes: container.bytes,
408
+ inventory_artifact_id: container.inventory_artifact_id,
409
+ });
@@ -0,0 +1,74 @@
1
+ import { lstat, realpath } from "node:fs/promises";
2
+ import { performance } from "node:perf_hooks";
3
+ import { AsarArtifactReader } from "../artifacts/AsarArtifactReader.js";
4
+ import { ArtifactReaderFailure, } from "../artifacts/ArtifactReader.js";
5
+ import { DirectoryArtifactReader } from "../artifacts/DirectoryArtifactReader.js";
6
+ import { analyzeJavaScriptArtifactFiles } from "./JavaScriptArtifactAnalysis.js";
7
+ import { readJavaScriptArtifactFiles } from "./JavaScriptArtifactFiles.js";
8
+ import { buildJavaScriptArtifactGraph } from "./JavaScriptArtifactGraphBuilder.js";
9
+ import { artifactLimitsForReconstruction, javascriptArtifactReconstructionInputSchema, } from "./JavaScriptArtifactReconstructionInput.js";
10
+ import { scanCanonicalArtifactInventory } from "./ArtifactInventory.js";
11
+ /** Reconstruct one local ASAR or extracted directory without executing code. */
12
+ export const reconstructJavaScriptArtifact = async (rawInput, signal) => {
13
+ const input = javascriptArtifactReconstructionInputSchema.parse(rawInput);
14
+ abortIfNeeded(signal);
15
+ const path = await realpath(input.input_path);
16
+ const format = await resolveFormat(path, input);
17
+ const snapshot = await scanCanonicalArtifactInventory(path, artifactLimitsForReconstruction(input), signal);
18
+ if (snapshot.manifest.root_format !== format)
19
+ throw new ArtifactReaderFailure("format", `Artifact inventory classified ${path} as ${snapshot.manifest.root_format}, not ${format}`);
20
+ const reader = createReader(path, format);
21
+ try {
22
+ const files = await readJavaScriptArtifactFiles(reader, snapshot, input, signal);
23
+ const analysis = analyzeJavaScriptArtifactFiles(files, input, () => performance.now());
24
+ abortIfNeeded(signal);
25
+ const graph = buildJavaScriptArtifactGraph(snapshot, files, analysis, input);
26
+ return {
27
+ input_path: path,
28
+ format,
29
+ inventory_manifest_id: snapshot.manifest.manifest_id,
30
+ inventory_graph_sha256: snapshot.manifest.graph_sha256,
31
+ graph,
32
+ statistics: {
33
+ relevant_files: files.files.length,
34
+ nested_asar_containers: files.containers.length,
35
+ text_files_selected: files.text_files_selected,
36
+ text_bytes_read: files.text_bytes_read,
37
+ omitted_text_files: files.omitted_text_files,
38
+ limit_omitted_text_files: files.limit_omitted_text_files,
39
+ policy_filtered_text_files: files.policy_filtered_text_files,
40
+ invalid_utf8_files: files.invalid_utf8_files,
41
+ parsed_javascript_files: analysis.files.filter(({ javascript }) => javascript !== null).length,
42
+ visited_ast_nodes: analysis.visited_ast_nodes,
43
+ findings: analysis.findings,
44
+ modules: analysis.modules,
45
+ parse_failures: analysis.parse_failures,
46
+ truncated_scopes: analysis.truncated_scopes,
47
+ },
48
+ limitations: analysis.limitations,
49
+ };
50
+ }
51
+ finally {
52
+ await reader.close();
53
+ }
54
+ };
55
+ const resolveFormat = async (path, input) => {
56
+ const metadata = await lstat(path);
57
+ const observed = metadata.isDirectory()
58
+ ? "directory"
59
+ : metadata.isFile() && path.toLowerCase().endsWith(".asar")
60
+ ? "asar"
61
+ : undefined;
62
+ if (observed === undefined)
63
+ throw new ArtifactReaderFailure("format", `JavaScript artifact reconstruction accepts only a directory or .asar file: ${path}`);
64
+ if (input.format !== "auto" && input.format !== observed)
65
+ throw new ArtifactReaderFailure("format", `Requested ${input.format} input but observed ${observed}: ${path}`);
66
+ return observed;
67
+ };
68
+ const createReader = (path, format) => format === "asar"
69
+ ? new AsarArtifactReader(path)
70
+ : new DirectoryArtifactReader(path);
71
+ const abortIfNeeded = (signal) => {
72
+ if (signal?.aborted === true)
73
+ throw new ArtifactReaderFailure("cancelled", "JavaScript artifact reconstruction cancelled");
74
+ };
@@ -0,0 +1,99 @@
1
+ import { z } from "zod";
2
+ const MAX_GRAPH_NODES = 100_000;
3
+ const MAX_GRAPH_EDGES = 200_000;
4
+ const reconstructionLimitsSchema = z
5
+ .strictObject({
6
+ max_entries: z.number().int().min(1).max(1_000_000).default(8_000),
7
+ max_total_artifact_bytes: z
8
+ .number()
9
+ .int()
10
+ .min(1)
11
+ .max(Number.MAX_SAFE_INTEGER)
12
+ .default(512 * 1_024 * 1_024),
13
+ max_artifact_entry_bytes: z
14
+ .number()
15
+ .int()
16
+ .min(1)
17
+ .max(Number.MAX_SAFE_INTEGER)
18
+ .default(128 * 1_024 * 1_024),
19
+ max_compression_ratio: z.number().min(1).max(100_000).default(1_000),
20
+ max_depth: z.number().int().min(1).max(100).default(64),
21
+ max_path_bytes: z.number().int().min(1).max(65_535).default(4_096),
22
+ max_text_files: z.number().int().min(1).max(100_000).default(5_000),
23
+ max_total_text_bytes: z
24
+ .number()
25
+ .int()
26
+ .min(1)
27
+ .max(512 * 1_024 * 1_024)
28
+ .default(128 * 1_024 * 1_024),
29
+ max_text_file_bytes: z
30
+ .number()
31
+ .int()
32
+ .min(1)
33
+ .max(64 * 1_024 * 1_024)
34
+ .default(8 * 1_024 * 1_024),
35
+ max_ast_nodes: z.number().int().min(1).max(20_000_000).default(2_000_000),
36
+ max_findings: z.number().int().min(1).max(200_000).default(10_000),
37
+ max_modules: z.number().int().min(1).max(100_000).default(20_000),
38
+ max_source_map_sources: z.number().int().min(1).max(100_000).default(5_000),
39
+ max_parse_milliseconds: z
40
+ .number()
41
+ .int()
42
+ .min(1)
43
+ .max(300_000)
44
+ .default(30_000),
45
+ })
46
+ .superRefine((limits, context) => {
47
+ const projectedNodes = 7 * limits.max_entries +
48
+ limits.max_findings +
49
+ limits.max_modules +
50
+ limits.max_source_map_sources +
51
+ 1;
52
+ if (projectedNodes > MAX_GRAPH_NODES)
53
+ context.addIssue({
54
+ code: "custom",
55
+ path: ["max_entries"],
56
+ message: "Combined reconstruction limits can exceed the 100000-node graph contract",
57
+ });
58
+ const projectedEdges = 9 * limits.max_entries +
59
+ 5 * limits.max_findings +
60
+ limits.max_modules +
61
+ limits.max_source_map_sources;
62
+ if (projectedEdges > MAX_GRAPH_EDGES)
63
+ context.addIssue({
64
+ code: "custom",
65
+ path: ["max_entries"],
66
+ message: "Combined reconstruction limits can exceed the 200000-edge graph contract",
67
+ });
68
+ });
69
+ /** Local ASAR/directory reconstruction request with explicit source-map authority. */
70
+ export const javascriptArtifactReconstructionInputSchema = z.strictObject({
71
+ input_path: z.string().min(1).max(16_384),
72
+ format: z.enum(["auto", "asar", "directory"]).default("auto"),
73
+ source_map_read_approved: z.boolean().default(false),
74
+ limits: reconstructionLimitsSchema.default({
75
+ max_entries: 8_000,
76
+ max_total_artifact_bytes: 512 * 1_024 * 1_024,
77
+ max_artifact_entry_bytes: 128 * 1_024 * 1_024,
78
+ max_compression_ratio: 1_000,
79
+ max_depth: 64,
80
+ max_path_bytes: 4_096,
81
+ max_text_files: 5_000,
82
+ max_total_text_bytes: 128 * 1_024 * 1_024,
83
+ max_text_file_bytes: 8 * 1_024 * 1_024,
84
+ max_ast_nodes: 2_000_000,
85
+ max_findings: 10_000,
86
+ max_modules: 20_000,
87
+ max_source_map_sources: 5_000,
88
+ max_parse_milliseconds: 30_000,
89
+ }),
90
+ });
91
+ /** Project reconstruction limits into the shared artifact traversal policy. */
92
+ export const artifactLimitsForReconstruction = (input) => ({
93
+ maxEntries: input.limits.max_entries,
94
+ maxTotalBytes: input.limits.max_total_artifact_bytes,
95
+ maxEntryBytes: input.limits.max_artifact_entry_bytes,
96
+ maxCompressionRatio: input.limits.max_compression_ratio,
97
+ maxDepth: input.limits.max_depth,
98
+ maxPathBytes: input.limits.max_path_bytes,
99
+ });
@@ -11,7 +11,7 @@ const DOWNLOAD_PREFIX = "https://www.hopperapp.com:443/downloader/public/";
11
11
  const MAX_PACKAGE_BYTES = 100_000_000;
12
12
  const BUNDLE_IDENTIFIER = "com.cryptic-apps.hopper-web-4";
13
13
  /** Download, verify, and install Hopper into the current user's Applications directory. */
14
- export const installMacHopper = async (host = systemMacHopperInstallHost()) => {
14
+ export const installMacHopper = async (options = {}, host = systemMacHopperInstallHost()) => {
15
15
  let temporary;
16
16
  let mounted = false;
17
17
  try {
@@ -30,7 +30,8 @@ export const installMacHopper = async (host = systemMacHopperInstallHost()) => {
30
30
  if (!integrityMatches(archive.bytes, release))
31
31
  return { status: "failed", reason: "integrity" };
32
32
  const destination = host.destination();
33
- if (await host.destinationExists(destination))
33
+ const destinationExists = await host.destinationExists(destination);
34
+ if (destinationExists && options.replaceExisting !== true)
34
35
  return { status: "failed", reason: "destination_exists" };
35
36
  temporary = await host.createTemporaryDirectory();
36
37
  const packagePath = join(temporary, "hopper.dmg");
@@ -44,7 +45,7 @@ export const installMacHopper = async (host = systemMacHopperInstallHost()) => {
44
45
  if (source === undefined || !(await host.validBundle(source)))
45
46
  return { status: "failed", reason: "bundle" };
46
47
  const stage = `${destination}.rea-stage`;
47
- if (!(await host.installBundle(source, destination, stage)))
48
+ if (!(await host.installBundle(source, destination, stage, destinationExists)))
48
49
  return { status: "failed", reason: "copy" };
49
50
  const launcherPath = join(destination, "Contents/MacOS/hopper");
50
51
  if (!(await host.launcherReady(launcherPath)))
@@ -126,16 +127,31 @@ const systemMacHopperInstallHost = () => ({
126
127
  return false;
127
128
  }
128
129
  },
129
- async installBundle(source, destination, stage) {
130
+ async installBundle(source, destination, stage, replaceExisting) {
131
+ const previous = `${stage}-previous`;
130
132
  try {
131
133
  await mkdir(join(homedir(), "Applications"), { recursive: true });
132
134
  await rm(stage, { recursive: true, force: true });
135
+ await rm(previous, { recursive: true, force: true });
133
136
  await execFileAsync("ditto", [source, stage]);
137
+ await access(join(stage, "Contents/MacOS/hopper"));
138
+ if (replaceExisting)
139
+ await rename(destination, previous);
134
140
  await rename(stage, destination);
141
+ if (replaceExisting)
142
+ await rm(previous, { recursive: true, force: true });
135
143
  return true;
136
144
  }
137
145
  catch {
138
146
  await rm(stage, { recursive: true, force: true });
147
+ if (replaceExisting) {
148
+ try {
149
+ await rename(previous, destination);
150
+ }
151
+ catch {
152
+ // The original destination was not moved, or rollback is impossible.
153
+ }
154
+ }
139
155
  return false;
140
156
  }
141
157
  },
@@ -6,7 +6,7 @@ import { join } from "node:path";
6
6
  import { authorizeProcessScenario, digestProcessCommitment, processComparisonContract, processScenarioCommitment, } from "../domain/processCapture.js";
7
7
  import { PRODUCT_IDENTITY } from "../identity.js";
8
8
  import { snapshotRoots } from "./FilesystemSnapshot.js";
9
- import { cleanupOwnedProcessGroup, observeOwnedProcessGroup, } from "./ProcessOwnership.js";
9
+ import { cleanupOwnedProcessGroup, observeOwnedProcessGroup, } from "../process/ProcessOwnership.js";
10
10
  import { classifyFilesystemEffects, ProcessCheckpoints, } from "./ProcessCheckpoints.js";
11
11
  import { ProcessCaptureError } from "./ProcessCaptureError.js";
12
12
  import { assertNotCancelled, assertRealPathAuthority, } from "./ProcessCaptureAuthority.js";
@@ -10,7 +10,7 @@ import { ProcessCheckpoints } from "./ProcessCheckpoints.js";
10
10
  import { TerminalRenderer } from "./TerminalRenderer.js";
11
11
  import { startCommandShimReplay, } from "./CommandShimReplay.js";
12
12
  import { normalizeProcessText } from "./ProcessNormalization.js";
13
- import { selectCapturedProcessGroupIds } from "./ProcessOwnership.js";
13
+ import { selectCapturedProcessGroupIds } from "../process/ProcessOwnership.js";
14
14
  import { awaitTerminalExit, buildCaptureResult, captureTerminalFrames, createRunManifest, observeSettlement, prepareProcessCapture, releaseProcessResources, resolveProcessResult, } from "./ProcessCaptureLifecycle.js";
15
15
  import { assertNotCancelled } from "./ProcessCaptureAuthority.js";
16
16
  export { probeProcessCaptureCapability } from "./ProcessCaptureCapability.js";
@@ -1,7 +1,7 @@
1
1
  import { execFile } from "node:child_process";
2
2
  import { readFile, readdir } from "node:fs/promises";
3
3
  import { promisify } from "node:util";
4
- import { readProcessRunId } from "./ProcessOwnership.js";
4
+ import { readProcessRunId } from "../process/ProcessOwnership.js";
5
5
  const execFileAsync = promisify(execFile);
6
6
  const PROC_READ_CONCURRENCY = 64;
7
7
  /** Prove that forkpty completed session setup and installed the run token. */