rea-agents 2.2.0 → 2.3.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 (165) hide show
  1. package/README.md +56 -9
  2. package/dist/application/ApplicationWorkflowEvidenceResolver.js +162 -0
  3. package/dist/application/BinarySession.js +9 -2
  4. package/dist/application/BinarySessionStatus.js +39 -1
  5. package/dist/application/CapabilityInventory.js +15 -0
  6. package/dist/application/CommandShimReplay.js +6 -3
  7. package/dist/application/CompositeProvider.js +5 -2
  8. package/dist/application/DirectAnalysis.js +1 -20
  9. package/dist/application/DirectAnalysisStatus.js +83 -0
  10. package/dist/application/Doctor.js +34 -25
  11. package/dist/application/DoctorDiagnostics.js +1 -4
  12. package/dist/application/DoctorProjection.js +43 -0
  13. package/dist/application/DoctorScope.js +102 -0
  14. package/dist/application/EvidenceReferenceResolver.js +26 -0
  15. package/dist/application/JavaScriptApplicationEvidence.js +1 -1
  16. package/dist/application/JavaScriptApplicationEvidenceGraph.js +10 -1
  17. package/dist/application/JavaScriptApplicationService.js +3 -2
  18. package/dist/application/JavaScriptApplicationWorkflowEvidence.js +26 -0
  19. package/dist/application/JavaScriptApplicationWorkflowService.js +49 -1
  20. package/dist/application/JavaScriptArtifactReconstruction.js +7 -0
  21. package/dist/application/JavaScriptModuleRelationships.js +25 -1
  22. package/dist/application/JavaScriptReturnShapeProjection.js +111 -0
  23. package/dist/application/JavaScriptSemanticGraphBuilder.js +294 -0
  24. package/dist/application/JavaScriptSemanticGraphConstruction.js +144 -0
  25. package/dist/application/JavaScriptSemanticGraphEvidence.js +86 -0
  26. package/dist/application/JavaScriptSemanticGraphFlowProjection.js +72 -0
  27. package/dist/application/JavaScriptSemanticGraphProjection.js +33 -0
  28. package/dist/application/JavaScriptSemanticTraceService.js +45 -0
  29. package/dist/application/LoopbackReplay.js +277 -133
  30. package/dist/application/LoopbackReplayRecorder.js +101 -0
  31. package/dist/application/PermissionAuthority.js +7 -3
  32. package/dist/application/ProcessCaptureJournal.js +95 -0
  33. package/dist/application/ProcessCaptureLifecycle.js +24 -13
  34. package/dist/application/ProcessCheckpoints.js +30 -4
  35. package/dist/application/ProcessCli.js +26 -4
  36. package/dist/application/ProcessHarness.js +20 -50
  37. package/dist/application/ProcessNormalization.js +3 -2
  38. package/dist/application/ProcessReactiveCoordinator.js +218 -0
  39. package/dist/application/ProcessReactiveEffects.js +133 -0
  40. package/dist/application/ProcessReactiveObservations.js +20 -0
  41. package/dist/application/ProcessSampling.js +2 -0
  42. package/dist/application/SessionProviderRouter.js +7 -6
  43. package/dist/application/Setup.js +11 -10
  44. package/dist/application/SetupPlan.js +1 -1
  45. package/dist/application/SetupRegistrationEnvironment.js +12 -0
  46. package/dist/application/TerminalRenderer.js +3 -1
  47. package/dist/browser/CdpEndpoint.js +37 -4
  48. package/dist/catalogIdentity.js +16 -7
  49. package/dist/cli/coreAnalysisCommands.js +34 -2
  50. package/dist/cli/javascriptApplicationAnalysis.js +19 -0
  51. package/dist/cli/setupCommands.js +81 -32
  52. package/dist/cli/utilityCommands.js +10 -2
  53. package/dist/cliApplicationCommands.js +79 -16
  54. package/dist/cliCommandNames.js +3 -0
  55. package/dist/cliElectronCommands.js +23 -33
  56. package/dist/cliObservationOptions.js +1 -1
  57. package/dist/cliOutput.js +56 -0
  58. package/dist/cliProcessCommands.js +32 -1
  59. package/dist/cliSetup.js +30 -21
  60. package/dist/contracts/applicationToolContracts.js +54 -7
  61. package/dist/contracts/applicationWorkflowInputContracts.js +119 -0
  62. package/dist/contracts/electronToolContracts.js +53 -32
  63. package/dist/contracts/javascriptApplicationWorkflowExamples.js +32 -9
  64. package/dist/contracts/processCaptureExample.js +1 -0
  65. package/dist/contracts/replayMachineExample.js +37 -0
  66. package/dist/contracts/toolContractExamples.js +2 -0
  67. package/dist/contracts/toolContracts.js +5 -1
  68. package/dist/contracts/toolEffects.js +3 -0
  69. package/dist/contracts/toolOutputSchemaGroups.js +2 -0
  70. package/dist/contracts/toolOutputSchemaPrimitives.js +40 -0
  71. package/dist/doctorRuntime.js +41 -0
  72. package/dist/domain/analysisErrorPresentation.js +5 -3
  73. package/dist/domain/analysisErrorProjection.js +7 -1
  74. package/dist/domain/changedBehavior.js +5 -4
  75. package/dist/domain/completionLedgerGeneration.js +190 -0
  76. package/dist/domain/errors.js +4 -2
  77. package/dist/domain/evidenceCompletionLedger.js +113 -0
  78. package/dist/domain/hopperStartupFailure.js +9 -0
  79. package/dist/domain/javascriptApplicationAnalysis.js +44 -2
  80. package/dist/domain/javascriptExportShapeComparison.js +145 -0
  81. package/dist/domain/javascriptExportShapeComparisonIdentity.js +11 -0
  82. package/dist/domain/javascriptExportShapeComparisonSchemas.js +277 -0
  83. package/dist/domain/javascriptExportShapeSelection.js +132 -0
  84. package/dist/domain/javascriptExportShapeVariants.js +272 -0
  85. package/dist/domain/javascriptRuntimeReconciliationParsing.js +2 -2
  86. package/dist/domain/javascriptSemanticAnalysis.js +17 -36
  87. package/dist/domain/javascriptSemanticCallResolution.js +129 -0
  88. package/dist/domain/javascriptSemanticCalls.js +269 -0
  89. package/dist/domain/javascriptSemanticGraph.js +356 -0
  90. package/dist/domain/javascriptSemanticGraphSchemas.js +329 -0
  91. package/dist/domain/javascriptSemanticGraphSerialization.js +23 -0
  92. package/dist/domain/javascriptSemanticIr.js +14 -1
  93. package/dist/domain/javascriptSemanticPrimitives.js +36 -0
  94. package/dist/domain/javascriptSemanticProjection.js +36 -4
  95. package/dist/domain/javascriptSemanticProvenance.js +29 -0
  96. package/dist/domain/javascriptSemanticQuery.js +294 -0
  97. package/dist/domain/javascriptSemanticQueryAssessment.js +74 -0
  98. package/dist/domain/javascriptSemanticQueryIdentity.js +86 -0
  99. package/dist/domain/javascriptSemanticQueryRelations.js +15 -0
  100. package/dist/domain/javascriptSemanticQuerySchemas.js +188 -0
  101. package/dist/domain/javascriptSemanticReturns.js +187 -0
  102. package/dist/domain/javascriptSemanticState.js +32 -0
  103. package/dist/domain/javascriptSemanticTraceSchemas.js +14 -0
  104. package/dist/domain/javascriptSemanticValues.js +54 -70
  105. package/dist/domain/processCapture.js +36 -0
  106. package/dist/domain/processCaptureValidation.js +101 -12
  107. package/dist/domain/processComparison.js +78 -25
  108. package/dist/domain/processObservation.js +153 -0
  109. package/dist/domain/processReactiveCheckpointDataflow.js +32 -0
  110. package/dist/domain/processReactiveMatching.js +210 -0
  111. package/dist/domain/processReactiveRuntime.js +109 -0
  112. package/dist/domain/processReactiveScenario.js +361 -0
  113. package/dist/domain/processReactiveScenarioPreflight.js +120 -0
  114. package/dist/domain/processReactiveTransition.js +159 -0
  115. package/dist/domain/processScenario.js +12 -0
  116. package/dist/domain/processTraceComparison.js +43 -0
  117. package/dist/domain/processTraceDimensionProjection.js +50 -0
  118. package/dist/domain/processTraceEvaluation.js +312 -0
  119. package/dist/domain/processTraceSpecification.js +373 -0
  120. package/dist/domain/reconstructionVerification.js +5 -4
  121. package/dist/domain/replayMachine.js +18 -2
  122. package/dist/domain/replayMachineRun.js +216 -0
  123. package/dist/domain/replayMachineRuntime.js +88 -9
  124. package/dist/domain/staticRuntimeCorrelation.js +4 -2
  125. package/dist/evaluation/CodexAgentEval.js +24 -0
  126. package/dist/generatedPackageMetadata.js +1 -1
  127. package/dist/ghidra/GhidraClient.js +13 -2
  128. package/dist/ghidra/GhidraDiagnostics.js +1 -0
  129. package/dist/ghidra/GhidraLauncher.js +7 -3
  130. package/dist/ghidra/GhidraProvider.js +8 -1
  131. package/dist/hopper/BridgeLauncher.js +56 -14
  132. package/dist/hopper/HopperClient.js +23 -10
  133. package/dist/hopper/HopperDiagnostics.js +14 -0
  134. package/dist/hopper/HopperProcessDiagnostic.js +14 -0
  135. package/dist/hopper/HopperProvider.js +8 -1
  136. package/dist/hopper/LinuxPrivateDisplayDiagnostic.js +72 -0
  137. package/dist/hopper/LinuxPrivateDisplayProbe.js +217 -0
  138. package/dist/main/transport.js +2 -0
  139. package/dist/mcpDoctor.js +310 -0
  140. package/dist/process/ProcessOwnership.js +200 -53
  141. package/dist/process/ProviderRunLineage.js +21 -0
  142. package/dist/server/createServer.js +4 -1
  143. package/dist/server/javascriptApplicationResult.js +22 -1
  144. package/dist/server/registerApplicationTools/characterization.js +3 -3
  145. package/dist/server/registerApplicationTools/compareExportShapes.js +37 -0
  146. package/dist/server/registerApplicationTools/compareVersions.js +12 -6
  147. package/dist/server/registerApplicationTools/controlledReplay.js +2 -2
  148. package/dist/server/registerApplicationTools/coverage.js +3 -3
  149. package/dist/server/registerApplicationTools/helpers.js +37 -25
  150. package/dist/server/registerApplicationTools/traceFeature.js +9 -5
  151. package/dist/server/registerApplicationTools/traceSemantics.js +30 -0
  152. package/dist/server/registerApplicationTools.js +4 -0
  153. package/dist/server/registerEvidenceResources.js +5 -1
  154. package/dist/server/registerProcessComparisonTool.js +17 -7
  155. package/dist/server/registerReplayMachineTool.js +20 -0
  156. package/dist/server/registerSessionTools.js +12 -7
  157. package/dist/server/sessionAvailabilityPolicy.js +2 -1
  158. package/dist/serverIdentity.js +19 -0
  159. package/package.json +62 -30
  160. package/scripts/hopper-demo-x11.py +459 -61
  161. package/scripts/prepack.mjs +55 -0
  162. package/scripts/prepare.mjs +28 -0
  163. package/scripts/rea.mjs +26 -5
  164. package/skills/reverse-engineer-anything/SKILL.md +2 -2
  165. package/skills/reverse-engineer-anything/references/javascript-applications.md +10 -0
@@ -2,11 +2,12 @@ import { execFile } from "node:child_process";
2
2
  import { chmod, writeFile } from "node:fs/promises";
3
3
  import { basename, dirname, extname, join } from "node:path";
4
4
  import { promisify } from "node:util";
5
- import { HopperCancelledError, HopperStartError } from "../domain/errors.js";
5
+ import { HopperCancelledError, HopperProcessError, HopperStartError, } from "../domain/errors.js";
6
6
  import { err, ok } from "../domain/result.js";
7
7
  import { cleanupOwnedProcessGroup } from "../process/ProcessOwnership.js";
8
8
  import { spawnOwnedProviderProcess, } from "../process/ProviderProcess.js";
9
9
  import { waitForAbortableDelay } from "../process/ProviderDeadline.js";
10
+ import { selectLinuxPrivateDisplayStrategy, } from "./LinuxPrivateDisplayProbe.js";
10
11
  import writeFileAtomic from "write-file-atomic";
11
12
  const execFileAsync = promisify(execFile);
12
13
  const HOPPER_BACKGROUND_STARTUP_MS = 5_000;
@@ -26,8 +27,10 @@ const HOPPER_BACKGROUND_STARTUP_MS = 5_000;
26
27
  */
27
28
  export class HopperApplicationLauncher {
28
29
  options;
29
- constructor(options) {
30
+ dependencies;
31
+ constructor(options, dependencies = {}) {
30
32
  this.options = options;
33
+ this.dependencies = dependencies;
31
34
  }
32
35
  async launch(session, options = {}) {
33
36
  const bootstrapPath = `${session.directory}/bootstrap.py`;
@@ -60,10 +63,13 @@ export class HopperApplicationLauncher {
60
63
  try {
61
64
  if (options.signal?.aborted === true)
62
65
  return err(new HopperCancelledError());
66
+ const display = await this.#privateDisplay(options.signal);
67
+ if (!display.ok)
68
+ return err(display.error);
63
69
  const prepared = await prepareHopperApplication(this.options.launcherPath, options.signal);
64
70
  if (!prepared)
65
71
  return err(new HopperCancelledError());
66
- const linuxDemo = linuxDemoLaunch(this.options, session, args);
72
+ const linuxDemo = linuxDemoLaunch(this.options, session, args, display.strategy);
67
73
  const ownershipCommand = linuxDemo?.ownershipCommand ?? this.options.launcherPath;
68
74
  const started = await spawnOwnedProviderProcess({
69
75
  command: linuxDemo?.command ?? this.options.launcherPath,
@@ -92,6 +98,7 @@ export class HopperApplicationLauncher {
92
98
  return ok({
93
99
  process: child,
94
100
  ownsProcessLifetime: true,
101
+ ownership: started.ownership,
95
102
  shutdownByCleanup: ownsProcessLifetime,
96
103
  cleanup: () => cleanupOwnedProcessGroup(started.ownership),
97
104
  });
@@ -100,23 +107,58 @@ export class HopperApplicationLauncher {
100
107
  return err(hopperLaunchFailure(cause, options.signal));
101
108
  }
102
109
  }
110
+ async #privateDisplay(signal) {
111
+ if (!usesLinuxDemo(this.options))
112
+ return { ok: true, strategy: undefined };
113
+ const selection = await (this.dependencies.selectPrivateDisplay ??
114
+ selectLinuxPrivateDisplayStrategy)({
115
+ helperPath: this.options.demoHelperPath,
116
+ ...(signal === undefined ? {} : { signal }),
117
+ });
118
+ if (signal?.aborted === true)
119
+ return { ok: false, error: new HopperCancelledError() };
120
+ return selection.ok
121
+ ? { ok: true, strategy: selection.strategy }
122
+ : {
123
+ ok: false,
124
+ error: new HopperProcessError(selection.exitCode, selection.diagnostic),
125
+ };
126
+ }
103
127
  }
104
- const linuxDemoLaunch = (options, session, hopperArgs) => {
128
+ export const linuxDemoLaunch = (options, session, hopperArgs, strategy) => {
105
129
  if (!usesLinuxDemo(options) || options.demoHelperPath === undefined)
106
130
  return undefined;
131
+ const helperArguments = [
132
+ options.demoHelperPath,
133
+ "--strategy",
134
+ strategy ?? "direct",
135
+ ...(strategy === "user-mount-namespace" ? ["--mount-private-x11"] : []),
136
+ "--hopper",
137
+ options.launcherPath,
138
+ "--socket",
139
+ session.socketPath,
140
+ "--",
141
+ options.launcherPath,
142
+ ...hopperArgs,
143
+ ];
144
+ if (strategy === "user-mount-namespace")
145
+ return {
146
+ command: "/usr/bin/unshare",
147
+ ownershipCommand: "/usr/bin/python3",
148
+ args: [
149
+ "--user",
150
+ "--map-root-user",
151
+ "--mount",
152
+ "--propagation",
153
+ "private",
154
+ "/usr/bin/python3",
155
+ ...helperArguments,
156
+ ],
157
+ };
107
158
  return {
108
159
  command: "/usr/bin/python3",
109
160
  ownershipCommand: "/usr/bin/python3",
110
- args: [
111
- options.demoHelperPath,
112
- "--hopper",
113
- options.launcherPath,
114
- "--socket",
115
- session.socketPath,
116
- "--",
117
- options.launcherPath,
118
- ...hopperArgs,
119
- ],
161
+ args: helperArguments,
120
162
  };
121
163
  };
122
164
  const hopperLaunchFailure = (cause, signal) => signal?.aborted === true
@@ -8,13 +8,17 @@ import { silentLogger } from "../logger.js";
8
8
  import { PendingOperations } from "../process/PendingOperations.js";
9
9
  import { PrivateRuntimeRoot } from "../process/PrivateRuntimeRoot.js";
10
10
  import { ProviderStartupDeadline } from "../process/ProviderDeadline.js";
11
+ import { ProviderRunLineage } from "../process/ProviderRunLineage.js";
11
12
  import { ProviderProcessSupervisor, } from "../process/ProviderProcess.js";
13
+ import { createOwnedHopperShutdownDiagnostic, providerCleanupFailure, } from "./HopperDiagnostics.js";
12
14
  import { isHopperCleanupRequired, isHopperShutdownAcknowledgement, parseHopperServerInfo, } from "./HopperSessionValues.js";
13
15
  import { connectHopperSocketOnce } from "./HopperSocketConnection.js";
16
+ import { hopperLauncherFailureDiagnostic } from "./HopperProcessDiagnostic.js";
14
17
  import { parseResponseLine, responseResult } from "./protocol.js";
15
18
  const MAX_LINE_BYTES = 10 * 1024 * 1024;
16
19
  const SHUTDOWN_TIMEOUT_MS = 30_000;
17
20
  const SESSION_ROOT = process.platform === "darwin" ? "/tmp" : tmpdir();
21
+ /** Safe launcher telemetry; stderr content is intentionally never exposed. */
18
22
  /**
19
23
  * Owns one authenticated NDJSON-over-Unix-socket bridge session.
20
24
  *
@@ -32,10 +36,12 @@ export class HopperClient {
32
36
  #process;
33
37
  #runtimeRoot;
34
38
  #token;
39
+ #lineage = new ProviderRunLineage();
35
40
  #nextId = 1;
36
41
  #buffer = "";
37
42
  #closing = false;
38
43
  #launcherExitCode;
44
+ #launcherFailureDiagnostic;
39
45
  #startupController;
40
46
  #startPromise;
41
47
  #closePromise;
@@ -57,6 +63,10 @@ export class HopperClient {
57
63
  startupTimeoutMs: options.startupTimeoutMs ?? 120_000,
58
64
  };
59
65
  }
66
+ /** Latest token-verified launcher lineage for the active run. */
67
+ runtimeLineage() {
68
+ return this.#lineage.snapshot();
69
+ }
60
70
  /** Launch the bridge once and complete its authenticated health handshake. */
61
71
  start(signal) {
62
72
  if (this.#closePromise !== undefined) {
@@ -124,8 +134,10 @@ export class HopperClient {
124
134
  }
125
135
  const socketPath = join(this.#runtimeRoot.path, "bridge.sock");
126
136
  this.#token = randomBytes(32).toString("hex");
137
+ this.#lineage.reset();
127
138
  this.#launcherExitCode = undefined;
128
- const runId = randomUUID();
139
+ this.#launcherFailureDiagnostic = undefined;
140
+ const runId = this.#options.runId ?? randomUUID();
129
141
  const launched = await this.#options.launcher
130
142
  .launch({
131
143
  directory: this.#runtimeRoot.path,
@@ -163,7 +175,9 @@ export class HopperClient {
163
175
  : health;
164
176
  }
165
177
  const parsed = parseHopperServerInfo(health.value, runId);
166
- if (!parsed.ok)
178
+ if (parsed.ok)
179
+ await this.#lineage.observe(this.#launch);
180
+ else
167
181
  await this.#cleanup();
168
182
  return parsed;
169
183
  }
@@ -228,6 +242,9 @@ export class HopperClient {
228
242
  this.#socket = undefined;
229
243
  if (this.#process !== undefined) {
230
244
  const stopped = await this.#process.stop(cleanupResult === undefined ? {} : { cleanupResult });
245
+ const diagnostic = createOwnedHopperShutdownDiagnostic(this.#process.launch, stopped, cleanupResult);
246
+ this.#options.onDiagnostic?.(diagnostic);
247
+ this.#logger.info(diagnostic, "Owned Hopper launcher shutdown completed");
231
248
  if (stopped.status === "incomplete")
232
249
  this.#logger.warn({ reason: stopped.reason }, "Owned launcher cleanup failed closed");
233
250
  }
@@ -239,6 +256,7 @@ export class HopperClient {
239
256
  this.#token = undefined;
240
257
  this.#buffer = "";
241
258
  this.#launcherExitCode = undefined;
259
+ this.#launcherFailureDiagnostic = undefined;
242
260
  }
243
261
  finally {
244
262
  this.#closing = false;
@@ -257,7 +275,7 @@ export class HopperClient {
257
275
  return err(new HopperProcessError(null));
258
276
  if (this.#launcherExitCode !== undefined &&
259
277
  hopperStartupFailure(this.#launcherExitCode) !== undefined)
260
- return err(new HopperProcessError(this.#launcherExitCode));
278
+ return err(new HopperProcessError(this.#launcherExitCode, this.#launcherFailureDiagnostic));
261
279
  try {
262
280
  await access(socketPath);
263
281
  }
@@ -338,12 +356,13 @@ export class HopperClient {
338
356
  if (event.type !== "exit")
339
357
  return;
340
358
  this.#launcherExitCode = event.code;
359
+ this.#launcherFailureDiagnostic = hopperLauncherFailureDiagnostic(event);
341
360
  this.#options.onDiagnostic?.({
342
361
  type: "launcher-exit",
343
362
  code: event.code,
344
363
  });
345
364
  if (launch.ownsProcessLifetime && !this.#closing)
346
- this.#failAll(new HopperProcessError(event.code));
365
+ this.#failAll(new HopperProcessError(event.code, this.#launcherFailureDiagnostic));
347
366
  }
348
367
  #onData(chunk) {
349
368
  this.#buffer += chunk;
@@ -389,9 +408,3 @@ const isAborted = (signal) => signal?.aborted === true;
389
408
  const startupInterruption = (deadline) => deadline.cancelled
390
409
  ? new HopperCancelledError()
391
410
  : new HopperTimeoutError(deadline.timeoutMs);
392
- const providerCleanupFailure = (cause) => ({
393
- cleaned: false,
394
- reason: cause instanceof Error
395
- ? `owned provider cleanup failed: ${cause.message}`
396
- : "owned provider cleanup failed",
397
- });
@@ -0,0 +1,14 @@
1
+ /** Retain actionable ownership coordinates without exposing the run token. */
2
+ export const createOwnedHopperShutdownDiagnostic = (launch, stopped, cleanup) => ({
3
+ type: "owned-shutdown",
4
+ status: stopped.status,
5
+ launcher_pid: launch.ownership?.leaderPid ?? launch.process.pid ?? null,
6
+ process_group_id: launch.ownership?.processGroupId ?? null,
7
+ cleanup_signaled: cleanup?.cleaned === true ? cleanup.signaled : null,
8
+ reason: stopped.status === "incomplete" ? stopped.reason : null,
9
+ });
10
+ /** Convert an unexpected cleanup rejection without exposing a stack or token. */
11
+ export const providerCleanupFailure = (_cause) => ({
12
+ cleaned: false,
13
+ reason: "owned provider cleanup failed",
14
+ });
@@ -0,0 +1,14 @@
1
+ import { hopperStartupFailure, } from "../domain/errors.js";
2
+ import { parseLinuxPrivateDisplayDiagnostic } from "./LinuxPrivateDisplayDiagnostic.js";
3
+ /** Extract only a matching, versioned Linux adapter failure record. */
4
+ export const hopperLauncherFailureDiagnostic = (event) => {
5
+ const parsed = parseLinuxPrivateDisplayDiagnostic(event.snapshot.stderr.text, event.snapshot.stderr.truncated);
6
+ if (!parsed.ok)
7
+ return undefined;
8
+ const expected = hopperStartupFailure(event.code)?.code;
9
+ return parsed.value.operation === "launch" &&
10
+ parsed.value.status === "error" &&
11
+ parsed.value.failure_code === expected
12
+ ? parsed.value
13
+ : undefined;
14
+ };
@@ -147,7 +147,7 @@ export class HopperProvider {
147
147
  ...(options?.signal === undefined ? {} : { signal: options.signal }),
148
148
  });
149
149
  }
150
- createClient(target, profile) {
150
+ createClient(target, profile, context) {
151
151
  if (target.kind !== "executable" && target.kind !== "database")
152
152
  return {
153
153
  execute: (operation) => Promise.resolve(err(new AnalysisCapabilityUnavailableError(IDENTITY.id, operation, `Hopper cannot open ${target.kind} targets directly. Inventory or extract the artifact first.`))),
@@ -176,6 +176,7 @@ export class HopperProvider {
176
176
  }
177
177
  : { launchMode: "native" }),
178
178
  }),
179
+ ...(context === undefined ? {} : { runId: context.runId }),
179
180
  logger: this.logger.child({ layer: "bridge" }),
180
181
  });
181
182
  return {
@@ -193,6 +194,12 @@ export class HopperProvider {
193
194
  }
194
195
  : result;
195
196
  },
197
+ runtimeLineageSnapshots: () => {
198
+ const observation = client.runtimeLineage();
199
+ return observation === null
200
+ ? []
201
+ : [{ provider: executionProvider, observation }];
202
+ },
196
203
  close: () => client.close(),
197
204
  };
198
205
  }
@@ -0,0 +1,72 @@
1
+ import { z } from "zod";
2
+ import { isHopperStartupFailureCode, } from "../domain/hopperStartupFailure.js";
3
+ export const LINUX_PRIVATE_DISPLAY_DIAGNOSTIC_PREFIX = "REA_X11_DIAGNOSTIC_V1=";
4
+ const diagnosticSchema = z
5
+ .object({
6
+ schema_version: z.literal(1),
7
+ component: z.literal("hopper_private_display"),
8
+ operation: z.enum(["probe", "launch"]),
9
+ status: z.enum(["ready", "error"]),
10
+ failure_code: z.string().max(64).nullable(),
11
+ reason: z
12
+ .string()
13
+ .regex(/^[a-z0-9_]+$/u)
14
+ .max(64),
15
+ socket_directory: z.literal("/tmp/.X11-unix"),
16
+ socket_directory_mode: z
17
+ .string()
18
+ .regex(/^[0-7]{4}$/u)
19
+ .nullable(),
20
+ mount_read_only: z.boolean().nullable(),
21
+ effective_socket_directory_mode: z
22
+ .string()
23
+ .regex(/^[0-7]{4}$/u)
24
+ .nullable(),
25
+ effective_mount_read_only: z.boolean().nullable(),
26
+ wsl: z.boolean(),
27
+ strategy: z.enum(["direct", "user-mount-namespace", "unavailable"]),
28
+ fallback_reason: z
29
+ .string()
30
+ .regex(/^[a-z0-9_]+$/u)
31
+ .max(64)
32
+ .nullable(),
33
+ xvfb_stderr_bytes: z.number().int().nonnegative().safe(),
34
+ xvfb_stderr_truncated: z.boolean(),
35
+ })
36
+ .strict();
37
+ /** Parse exactly one bounded, versioned helper diagnostic from stderr. */
38
+ export const parseLinuxPrivateDisplayDiagnostic = (stderr, truncated) => {
39
+ if (truncated)
40
+ return { ok: false, reason: "diagnostic_truncated" };
41
+ const records = stderr
42
+ .split("\n")
43
+ .filter((line) => line.startsWith(LINUX_PRIVATE_DISPLAY_DIAGNOSTIC_PREFIX));
44
+ if (records.length === 0)
45
+ return { ok: false, reason: "diagnostic_missing" };
46
+ if (records.length !== 1)
47
+ return { ok: false, reason: "diagnostic_multiple" };
48
+ const record = records[0];
49
+ if (record === undefined)
50
+ return { ok: false, reason: "diagnostic_missing" };
51
+ try {
52
+ const parsed = diagnosticSchema.safeParse(JSON.parse(record.slice(LINUX_PRIVATE_DISPLAY_DIAGNOSTIC_PREFIX.length)));
53
+ if (!parsed.success)
54
+ return { ok: false, reason: "diagnostic_malformed" };
55
+ const code = parsed.data.failure_code;
56
+ if (code !== null && !isHopperStartupFailureCode(code))
57
+ return { ok: false, reason: "diagnostic_malformed" };
58
+ if ((parsed.data.status === "ready" && code !== null) ||
59
+ (parsed.data.status === "error" && code === null))
60
+ return { ok: false, reason: "diagnostic_malformed" };
61
+ return {
62
+ ok: true,
63
+ value: {
64
+ ...parsed.data,
65
+ failure_code: code,
66
+ },
67
+ };
68
+ }
69
+ catch {
70
+ return { ok: false, reason: "diagnostic_malformed" };
71
+ }
72
+ };
@@ -0,0 +1,217 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { hopperStartupExitCode, } from "../domain/hopperStartupFailure.js";
3
+ import { cleanupOwnedProcessGroup } from "../process/ProcessOwnership.js";
4
+ import { ProviderProcessSupervisor, spawnOwnedProviderProcess, } from "../process/ProviderProcess.js";
5
+ import { parseLinuxPrivateDisplayDiagnostic } from "./LinuxPrivateDisplayDiagnostic.js";
6
+ const PYTHON_PATH = "/usr/bin/python3";
7
+ const UNSHARE_PATH = "/usr/bin/unshare";
8
+ const DEFAULT_PROBE_TIMEOUT_MS = 5_000;
9
+ const PROBE_OUTPUT_LIMIT_BYTES = 64 * 1024;
10
+ /** Probe the exact Xvfb boundary and select the least-privileged viable strategy. */
11
+ export const selectLinuxPrivateDisplayStrategy = async (options) => {
12
+ const timeoutMs = options.timeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS;
13
+ const runner = options.runProbe ?? runLinuxPrivateDisplayProbe;
14
+ const direct = await evaluatedProbe("direct", options, timeoutMs, runner);
15
+ if (direct.ok)
16
+ return direct;
17
+ if (direct.diagnostic.failure_code !== "x11_socket_directory_unusable")
18
+ return unavailable(direct.diagnostic);
19
+ const isolated = await evaluatedProbe("user-mount-namespace", options, timeoutMs, runner);
20
+ if (!isolated.ok)
21
+ return unavailable({
22
+ ...direct.diagnostic,
23
+ strategy: "unavailable",
24
+ reason: "namespace_unavailable",
25
+ fallback_reason: isolated.diagnostic.reason,
26
+ effective_socket_directory_mode: isolated.diagnostic.effective_socket_directory_mode,
27
+ effective_mount_read_only: isolated.diagnostic.effective_mount_read_only,
28
+ xvfb_stderr_bytes: isolated.diagnostic.xvfb_stderr_bytes,
29
+ xvfb_stderr_truncated: isolated.diagnostic.xvfb_stderr_truncated,
30
+ });
31
+ return {
32
+ ok: true,
33
+ strategy: "user-mount-namespace",
34
+ diagnostic: {
35
+ ...isolated.diagnostic,
36
+ socket_directory_mode: direct.diagnostic.socket_directory_mode,
37
+ mount_read_only: direct.diagnostic.mount_read_only,
38
+ fallback_reason: null,
39
+ },
40
+ };
41
+ };
42
+ /** Run one helper probe in an owned process group with a hard output bound. */
43
+ export const runLinuxPrivateDisplayProbe = async (strategy, options) => {
44
+ if (options.signal?.aborted === true)
45
+ return emptyProcessResult({ cancelled: true });
46
+ const command = privateDisplayProbeCommand(options.helperPath, strategy);
47
+ let started;
48
+ try {
49
+ started = await spawnOwnedProviderProcess({
50
+ command: command.command,
51
+ arguments: command.arguments,
52
+ runId: randomUUID(),
53
+ expectedCommand: null,
54
+ });
55
+ }
56
+ catch {
57
+ return emptyProcessResult();
58
+ }
59
+ const launch = {
60
+ process: started.process,
61
+ ownsProcessLifetime: true,
62
+ cleanup: () => cleanupOwnedProcessGroup(started.ownership),
63
+ };
64
+ const supervisor = new ProviderProcessSupervisor(launch, {
65
+ maxOutputBytesPerStream: PROBE_OUTPUT_LIMIT_BYTES,
66
+ });
67
+ const outcome = await waitForProbeExit(supervisor, options.timeoutMs, options.signal);
68
+ let cleanupIncomplete = false;
69
+ if (outcome !== "exit") {
70
+ const stopped = await supervisor.stop();
71
+ cleanupIncomplete = stopped.status === "incomplete";
72
+ }
73
+ else
74
+ supervisor.dispose();
75
+ const snapshot = supervisor.snapshot();
76
+ return {
77
+ exitCode: snapshot.exitCode,
78
+ stderr: snapshot.stderr.text,
79
+ stderrBytes: snapshot.stderr.bytes,
80
+ stderrTruncated: snapshot.stderr.truncated,
81
+ timedOut: outcome === "timeout",
82
+ cancelled: outcome === "cancelled",
83
+ cleanupIncomplete,
84
+ };
85
+ };
86
+ const privateDisplayProbeCommand = (helperPath, strategy) => strategy === "direct"
87
+ ? {
88
+ command: PYTHON_PATH,
89
+ arguments: [helperPath, "--probe", "--strategy", "direct"],
90
+ }
91
+ : {
92
+ command: UNSHARE_PATH,
93
+ arguments: [
94
+ "--user",
95
+ "--map-root-user",
96
+ "--mount",
97
+ "--propagation",
98
+ "private",
99
+ PYTHON_PATH,
100
+ helperPath,
101
+ "--probe",
102
+ "--strategy",
103
+ "user-mount-namespace",
104
+ "--mount-private-x11",
105
+ ],
106
+ };
107
+ const evaluatedProbe = async (strategy, options, timeoutMs, runner) => {
108
+ let processResult;
109
+ try {
110
+ processResult = await runner(strategy, {
111
+ helperPath: options.helperPath,
112
+ timeoutMs,
113
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
114
+ });
115
+ }
116
+ catch {
117
+ return {
118
+ ok: false,
119
+ diagnostic: syntheticDiagnostic(strategy, "probe_failed"),
120
+ };
121
+ }
122
+ if (processResult.cancelled)
123
+ return {
124
+ ok: false,
125
+ diagnostic: syntheticDiagnostic(strategy, "cancelled"),
126
+ };
127
+ if (processResult.timedOut)
128
+ return {
129
+ ok: false,
130
+ diagnostic: syntheticDiagnostic(strategy, "probe_timeout"),
131
+ };
132
+ if (processResult.cleanupIncomplete)
133
+ return {
134
+ ok: false,
135
+ diagnostic: syntheticDiagnostic(strategy, "cleanup_incomplete"),
136
+ };
137
+ const parsed = parseLinuxPrivateDisplayDiagnostic(processResult.stderr, processResult.stderrTruncated);
138
+ if (!parsed.ok)
139
+ return {
140
+ ok: false,
141
+ diagnostic: syntheticDiagnostic(strategy, parsed.reason),
142
+ };
143
+ const diagnostic = parsed.value;
144
+ if (diagnostic.strategy !== strategy ||
145
+ diagnostic.operation !== "probe" ||
146
+ !exitMatchesDiagnostic(processResult.exitCode, diagnostic))
147
+ return {
148
+ ok: false,
149
+ diagnostic: syntheticDiagnostic(strategy, "diagnostic_malformed"),
150
+ };
151
+ return diagnostic.status === "ready"
152
+ ? { ok: true, strategy, diagnostic }
153
+ : { ok: false, diagnostic };
154
+ };
155
+ const exitMatchesDiagnostic = (exitCode, diagnostic) => diagnostic.status === "ready"
156
+ ? exitCode === 0
157
+ : diagnostic.failure_code !== null &&
158
+ hopperStartupExitCode(diagnostic.failure_code) === exitCode;
159
+ const syntheticDiagnostic = (strategy, reason, failureCode = "private_display_unavailable") => ({
160
+ schema_version: 1,
161
+ component: "hopper_private_display",
162
+ operation: "probe",
163
+ status: "error",
164
+ failure_code: failureCode,
165
+ reason,
166
+ socket_directory: "/tmp/.X11-unix",
167
+ socket_directory_mode: null,
168
+ mount_read_only: null,
169
+ effective_socket_directory_mode: null,
170
+ effective_mount_read_only: null,
171
+ wsl: false,
172
+ strategy,
173
+ fallback_reason: null,
174
+ xvfb_stderr_bytes: 0,
175
+ xvfb_stderr_truncated: false,
176
+ });
177
+ const unavailable = (diagnostic) => ({
178
+ ok: false,
179
+ strategy: "unavailable",
180
+ exitCode: diagnostic.failure_code === null
181
+ ? 70
182
+ : (hopperStartupExitCode(diagnostic.failure_code) ?? 70),
183
+ diagnostic: { ...diagnostic, strategy: "unavailable" },
184
+ });
185
+ const emptyProcessResult = (overrides = {}) => ({
186
+ exitCode: undefined,
187
+ stderr: "",
188
+ stderrBytes: 0,
189
+ stderrTruncated: false,
190
+ timedOut: false,
191
+ cancelled: false,
192
+ cleanupIncomplete: false,
193
+ ...overrides,
194
+ });
195
+ const waitForProbeExit = async (supervisor, timeoutMs, signal) => {
196
+ if (signal?.aborted === true)
197
+ return "cancelled";
198
+ let releaseAbort;
199
+ const aborted = new Promise((resolve) => {
200
+ if (signal === undefined)
201
+ return;
202
+ const listener = () => resolve("cancelled");
203
+ signal.addEventListener("abort", listener, { once: true });
204
+ releaseAbort = () => signal.removeEventListener("abort", listener);
205
+ });
206
+ try {
207
+ return await Promise.race([
208
+ supervisor
209
+ .waitForExit(Math.max(1, timeoutMs))
210
+ .then((exited) => (exited ? "exit" : "timeout")),
211
+ aborted,
212
+ ]);
213
+ }
214
+ finally {
215
+ releaseAbort?.();
216
+ }
217
+ };
@@ -24,6 +24,8 @@ export const startMcpTransport = async (dependencies, session, serverContext) =>
24
24
  .enabled,
25
25
  evidenceFileRoots: serverContext.runtimeState.currentConfig.evidenceFilePolicy
26
26
  .roots.length,
27
+ investigationInputRoots: serverContext.runtimeState.currentConfig.investigationInputRoots
28
+ .length,
27
29
  browserObservationEnabled: serverContext.runtimeState.currentConfig
28
30
  .browserObservationEnabled &&
29
31
  serverContext.runtimeState.currentConfig.browserCdpEndpoints