rea-agents 2.2.0 → 2.4.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 (179) hide show
  1. package/README.md +57 -10
  2. package/dist/application/ApplicationWorkflowEvidenceResolver.js +162 -0
  3. package/dist/application/ArtifactExtraction.js +5 -1
  4. package/dist/application/ArtifactGraphConstruction.js +3 -15
  5. package/dist/application/ArtifactInventory/classify.js +5 -1
  6. package/dist/application/ArtifactInventory/reader.js +2 -0
  7. package/dist/application/BinarySession.js +9 -2
  8. package/dist/application/BinarySessionStatus.js +40 -1
  9. package/dist/application/CapabilityInventory.js +26 -3
  10. package/dist/application/CommandShimReplay.js +6 -3
  11. package/dist/application/CompositeProvider.js +5 -2
  12. package/dist/application/DirectAnalysis.js +1 -20
  13. package/dist/application/DirectAnalysisStatus.js +83 -0
  14. package/dist/application/Doctor.js +34 -25
  15. package/dist/application/DoctorDiagnostics.js +1 -4
  16. package/dist/application/DoctorProjection.js +43 -0
  17. package/dist/application/DoctorScope.js +102 -0
  18. package/dist/application/EvidenceReferenceResolver.js +26 -0
  19. package/dist/application/JavaScriptApplicationEvidence.js +1 -1
  20. package/dist/application/JavaScriptApplicationEvidenceGraph.js +10 -1
  21. package/dist/application/JavaScriptApplicationService.js +3 -2
  22. package/dist/application/JavaScriptApplicationWorkflowEvidence.js +26 -0
  23. package/dist/application/JavaScriptApplicationWorkflowService.js +49 -1
  24. package/dist/application/JavaScriptArtifactReconstruction.js +7 -0
  25. package/dist/application/JavaScriptModuleRelationships.js +25 -1
  26. package/dist/application/JavaScriptReturnShapeProjection.js +111 -0
  27. package/dist/application/JavaScriptSemanticGraphBuilder.js +294 -0
  28. package/dist/application/JavaScriptSemanticGraphConstruction.js +144 -0
  29. package/dist/application/JavaScriptSemanticGraphEvidence.js +86 -0
  30. package/dist/application/JavaScriptSemanticGraphFlowProjection.js +72 -0
  31. package/dist/application/JavaScriptSemanticGraphProjection.js +33 -0
  32. package/dist/application/JavaScriptSemanticTraceService.js +45 -0
  33. package/dist/application/LoopbackReplay.js +277 -133
  34. package/dist/application/LoopbackReplayRecorder.js +101 -0
  35. package/dist/application/PermissionAuthority.js +7 -3
  36. package/dist/application/ProcessCaptureEnvironment.js +26 -0
  37. package/dist/application/ProcessCaptureJournal.js +95 -0
  38. package/dist/application/ProcessCaptureLifecycle.js +25 -13
  39. package/dist/application/ProcessCheckpoints.js +35 -4
  40. package/dist/application/ProcessCli.js +26 -4
  41. package/dist/application/ProcessHarness.js +64 -83
  42. package/dist/application/ProcessNormalization.js +9 -2
  43. package/dist/application/ProcessReactiveCoordinator.js +225 -0
  44. package/dist/application/ProcessReactiveEffects.js +141 -0
  45. package/dist/application/ProcessReactiveHarness.js +85 -0
  46. package/dist/application/ProcessReactiveObservations.js +105 -0
  47. package/dist/application/ProcessSampling.js +2 -0
  48. package/dist/application/SessionProviderRouter.js +7 -6
  49. package/dist/application/Setup.js +11 -10
  50. package/dist/application/SetupPlan.js +1 -1
  51. package/dist/application/SetupRegistrationEnvironment.js +12 -0
  52. package/dist/application/TerminalRenderer.js +3 -1
  53. package/dist/browser/CdpEndpoint.js +37 -4
  54. package/dist/catalogIdentity.js +16 -7
  55. package/dist/cli/coreAnalysisCommands.js +34 -2
  56. package/dist/cli/javascriptApplicationAnalysis.js +19 -0
  57. package/dist/cli/setupCommands.js +81 -32
  58. package/dist/cli/utilityCommands.js +10 -2
  59. package/dist/cliApplicationCommands.js +79 -16
  60. package/dist/cliCommandNames.js +3 -0
  61. package/dist/cliElectronCommands.js +23 -33
  62. package/dist/cliObservationOptions.js +1 -1
  63. package/dist/cliOutput.js +56 -0
  64. package/dist/cliProcessCommands.js +32 -1
  65. package/dist/cliSetup.js +30 -21
  66. package/dist/contracts/applicationToolContracts.js +54 -7
  67. package/dist/contracts/applicationWorkflowInputContracts.js +119 -0
  68. package/dist/contracts/electronToolContracts.js +53 -32
  69. package/dist/contracts/javascriptApplicationWorkflowExamples.js +32 -9
  70. package/dist/contracts/processCaptureExample.js +1 -0
  71. package/dist/contracts/replayMachineExample.js +37 -0
  72. package/dist/contracts/toolContractExamples.js +2 -0
  73. package/dist/contracts/toolContracts.js +5 -1
  74. package/dist/contracts/toolEffects.js +3 -0
  75. package/dist/contracts/toolOutputSchemaGroups.js +2 -0
  76. package/dist/contracts/toolOutputSchemaPrimitives.js +43 -0
  77. package/dist/doctorRuntime.js +41 -0
  78. package/dist/domain/analysisErrorPresentation.js +5 -3
  79. package/dist/domain/analysisErrorProjection.js +7 -1
  80. package/dist/domain/analysisSnapshot.js +2 -0
  81. package/dist/domain/artifactGraph.js +2 -0
  82. package/dist/domain/binaryTarget.js +3 -6
  83. package/dist/domain/changedBehavior.js +5 -4
  84. package/dist/domain/completionLedgerGeneration.js +190 -0
  85. package/dist/domain/errors.js +4 -2
  86. package/dist/domain/evidence.js +2 -0
  87. package/dist/domain/evidenceCompletionLedger.js +113 -0
  88. package/dist/domain/hopperStartupFailure.js +9 -0
  89. package/dist/domain/javascriptApplicationAnalysis.js +44 -2
  90. package/dist/domain/javascriptExportShapeComparison.js +145 -0
  91. package/dist/domain/javascriptExportShapeComparisonIdentity.js +11 -0
  92. package/dist/domain/javascriptExportShapeComparisonSchemas.js +277 -0
  93. package/dist/domain/javascriptExportShapeSelection.js +132 -0
  94. package/dist/domain/javascriptExportShapeVariants.js +272 -0
  95. package/dist/domain/javascriptRuntimeReconciliationParsing.js +2 -2
  96. package/dist/domain/javascriptSemanticAnalysis.js +17 -36
  97. package/dist/domain/javascriptSemanticCallResolution.js +129 -0
  98. package/dist/domain/javascriptSemanticCalls.js +269 -0
  99. package/dist/domain/javascriptSemanticGraph.js +356 -0
  100. package/dist/domain/javascriptSemanticGraphSchemas.js +329 -0
  101. package/dist/domain/javascriptSemanticGraphSerialization.js +23 -0
  102. package/dist/domain/javascriptSemanticIr.js +14 -1
  103. package/dist/domain/javascriptSemanticPrimitives.js +36 -0
  104. package/dist/domain/javascriptSemanticProjection.js +36 -4
  105. package/dist/domain/javascriptSemanticProvenance.js +29 -0
  106. package/dist/domain/javascriptSemanticQuery.js +294 -0
  107. package/dist/domain/javascriptSemanticQueryAssessment.js +74 -0
  108. package/dist/domain/javascriptSemanticQueryIdentity.js +86 -0
  109. package/dist/domain/javascriptSemanticQueryRelations.js +15 -0
  110. package/dist/domain/javascriptSemanticQuerySchemas.js +188 -0
  111. package/dist/domain/javascriptSemanticReturns.js +187 -0
  112. package/dist/domain/javascriptSemanticState.js +32 -0
  113. package/dist/domain/javascriptSemanticTraceSchemas.js +14 -0
  114. package/dist/domain/javascriptSemanticValues.js +54 -70
  115. package/dist/domain/processCapture.js +100 -0
  116. package/dist/domain/processCaptureReactiveValidation.js +175 -0
  117. package/dist/domain/processCaptureValidation.js +103 -12
  118. package/dist/domain/processComparison.js +78 -25
  119. package/dist/domain/processObservation.js +162 -0
  120. package/dist/domain/processReactiveCheckpointDataflow.js +32 -0
  121. package/dist/domain/processReactiveMatching.js +210 -0
  122. package/dist/domain/processReactiveRuntime.js +120 -0
  123. package/dist/domain/processReactiveScenario.js +361 -0
  124. package/dist/domain/processReactiveScenarioPreflight.js +120 -0
  125. package/dist/domain/processReactiveTransition.js +161 -0
  126. package/dist/domain/processScenario.js +33 -0
  127. package/dist/domain/processTraceComparison.js +43 -0
  128. package/dist/domain/processTraceDimensionProjection.js +50 -0
  129. package/dist/domain/processTraceEvaluation.js +312 -0
  130. package/dist/domain/processTraceSpecification.js +373 -0
  131. package/dist/domain/reconstructionVerification.js +5 -4
  132. package/dist/domain/replayMachine.js +18 -2
  133. package/dist/domain/replayMachineRun.js +216 -0
  134. package/dist/domain/replayMachineRuntime.js +88 -9
  135. package/dist/domain/staticRuntimeCorrelation.js +4 -2
  136. package/dist/domain/zipPackageFormat.js +20 -0
  137. package/dist/evaluation/CodexAgentEval.js +24 -0
  138. package/dist/generatedPackageMetadata.js +1 -1
  139. package/dist/ghidra/GhidraClient.js +13 -2
  140. package/dist/ghidra/GhidraDiagnostics.js +1 -0
  141. package/dist/ghidra/GhidraLauncher.js +7 -3
  142. package/dist/ghidra/GhidraProvider.js +8 -1
  143. package/dist/hopper/BridgeLauncher.js +56 -14
  144. package/dist/hopper/HopperClient.js +23 -10
  145. package/dist/hopper/HopperDiagnostics.js +14 -0
  146. package/dist/hopper/HopperProcessDiagnostic.js +14 -0
  147. package/dist/hopper/HopperProvider.js +8 -1
  148. package/dist/hopper/LinuxPrivateDisplayDiagnostic.js +72 -0
  149. package/dist/hopper/LinuxPrivateDisplayProbe.js +217 -0
  150. package/dist/main/transport.js +2 -0
  151. package/dist/mcpDoctor.js +310 -0
  152. package/dist/native/NativeMacOSProvider.js +1 -0
  153. package/dist/process/ProcessOwnership.js +200 -53
  154. package/dist/process/ProviderRunLineage.js +21 -0
  155. package/dist/server/createServer.js +4 -1
  156. package/dist/server/javascriptApplicationResult.js +22 -1
  157. package/dist/server/registerApplicationTools/characterization.js +3 -3
  158. package/dist/server/registerApplicationTools/compareExportShapes.js +37 -0
  159. package/dist/server/registerApplicationTools/compareVersions.js +12 -6
  160. package/dist/server/registerApplicationTools/controlledReplay.js +2 -2
  161. package/dist/server/registerApplicationTools/coverage.js +3 -3
  162. package/dist/server/registerApplicationTools/helpers.js +37 -25
  163. package/dist/server/registerApplicationTools/traceFeature.js +9 -5
  164. package/dist/server/registerApplicationTools/traceSemantics.js +30 -0
  165. package/dist/server/registerApplicationTools.js +4 -0
  166. package/dist/server/registerEvidenceResources.js +5 -1
  167. package/dist/server/registerProcessComparisonTool.js +17 -7
  168. package/dist/server/registerReplayMachineTool.js +20 -0
  169. package/dist/server/registerSessionTools.js +12 -7
  170. package/dist/server/sessionAvailabilityPolicy.js +2 -1
  171. package/dist/serverIdentity.js +19 -0
  172. package/package.json +62 -30
  173. package/scripts/hopper-demo-x11.py +459 -61
  174. package/scripts/prepack.mjs +55 -0
  175. package/scripts/prepare.mjs +28 -0
  176. package/scripts/rea.mjs +26 -5
  177. package/skills/reverse-engineer-anything/SKILL.md +4 -3
  178. package/skills/reverse-engineer-anything/references/javascript-applications.md +10 -0
  179. package/skills/reverse-engineer-anything/references/native-and-artifacts.md +2 -2
@@ -1,4 +1,54 @@
1
1
  import canonicalize from "canonicalize";
2
+ import { z } from "zod";
3
+ /** One bounded inbound protocol event offered to a replay machine. */
4
+ const replayEventHeadersSchema = z
5
+ .record(z
6
+ .string()
7
+ .min(1)
8
+ .max(256)
9
+ .regex(/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/u), z
10
+ .string()
11
+ .max(8_192)
12
+ .regex(/^[\t\x20-\x7e\x80-\xff]*$/u))
13
+ .superRefine((headers, context) => {
14
+ const normalized = Object.keys(headers).map((name) => name.toLowerCase());
15
+ if (new Set(normalized).size !== normalized.length)
16
+ context.addIssue({
17
+ code: "custom",
18
+ message: "replay event headers must be unique ignoring case",
19
+ input: headers,
20
+ });
21
+ });
22
+ const replayEventFields = {
23
+ at_ms: z.number().int().nonnegative().safe(),
24
+ path: z.string().startsWith("/").max(8_192),
25
+ headers: replayEventHeadersSchema,
26
+ body: z.string().max(1_000_000),
27
+ };
28
+ export const replayMachineEventSchema = z.discriminatedUnion("protocol", [
29
+ z.strictObject({
30
+ protocol: z.literal("http"),
31
+ connection: z.literal("not_applicable"),
32
+ method: z
33
+ .string()
34
+ .min(1)
35
+ .max(16)
36
+ .transform((method) => method.toUpperCase()),
37
+ ...replayEventFields,
38
+ }),
39
+ z.strictObject({
40
+ protocol: z.literal("websocket_connect"),
41
+ connection: z.enum(["initial", "reconnect"]),
42
+ method: z.null(),
43
+ ...replayEventFields,
44
+ }),
45
+ z.strictObject({
46
+ protocol: z.literal("websocket_message"),
47
+ connection: z.enum(["initial", "reconnect"]),
48
+ method: z.null(),
49
+ ...replayEventFields,
50
+ }),
51
+ ]);
2
52
  const parseJson = (value) => {
3
53
  try {
4
54
  return JSON.parse(value);
@@ -76,6 +126,17 @@ const isJsonCompatible = (value) => {
76
126
  const replayValuesEqual = (left, right) => isJsonCompatible(left) &&
77
127
  isJsonCompatible(right) &&
78
128
  (Object.is(left, right) || canonicalize(left) === canonicalize(right));
129
+ const createTransitionRecord = ({ sequence, atMs, stateBefore, transition, captures, }) => ({
130
+ sequence,
131
+ at_ms: atMs,
132
+ transition_id: transition.id,
133
+ state_before: stateBefore,
134
+ state_after: transition.to,
135
+ sensitive_aliases: captures
136
+ .filter(({ sensitive }) => sensitive)
137
+ .map(({ variable }) => variable)
138
+ .sort(),
139
+ });
79
140
  /** Stateful evaluator for one validated replay-machine instance. */
80
141
  export class ReplayMachineRuntime {
81
142
  machine;
@@ -89,6 +150,7 @@ export class ReplayMachineRuntime {
89
150
  #messages = 0;
90
151
  #bytes = 0;
91
152
  #lastAtMs = 0;
153
+ #events = 0;
92
154
  constructor(machine) {
93
155
  this.machine = machine;
94
156
  this.#state = machine.initial_state;
@@ -102,6 +164,17 @@ export class ReplayMachineRuntime {
102
164
  get timeline() {
103
165
  return this.#timeline;
104
166
  }
167
+ /** Current committed use of the machine's declared limits. */
168
+ get usage() {
169
+ return {
170
+ events: this.#events,
171
+ transitions: this.#timeline.length,
172
+ connections: this.#connections,
173
+ messages: this.#messages,
174
+ bytes: this.#bytes,
175
+ last_at_ms: this.#lastAtMs,
176
+ };
177
+ }
105
178
  /** Replace captured secret values with stable aliases before persistence. */
106
179
  redact(value) {
107
180
  let redacted = value;
@@ -113,6 +186,14 @@ export class ReplayMachineRuntime {
113
186
  redacted = redacted.replaceAll(secret, `<secret:${variable}>`);
114
187
  return redacted;
115
188
  }
189
+ /** Replace a field containing captured secrets with one stable alias only. */
190
+ redactField(value) {
191
+ const alias = [...this.#sensitiveValues]
192
+ .filter(([, values]) => [...values].some((secret) => value.includes(secret)))
193
+ .map(([variable]) => variable)
194
+ .sort()[0];
195
+ return alias === undefined ? value : `<secret:${alias}>`;
196
+ }
116
197
  /** Evaluate and, when admitted, commit one protocol event. */
117
198
  dispatch(event) {
118
199
  const normalizedEvent = {
@@ -172,16 +253,13 @@ export class ReplayMachineRuntime {
172
253
  this.#sensitiveValues.set(capture.variable, values);
173
254
  }
174
255
  }
175
- const record = {
256
+ const record = createTransitionRecord({
176
257
  sequence: this.#timeline.length,
177
- transition_id: transition.id,
178
- state_before: this.#state,
179
- state_after: transition.to,
180
- sensitive_aliases: captures
181
- .filter(({ sensitive }) => sensitive)
182
- .map(({ variable }) => variable)
183
- .sort(),
184
- };
258
+ atMs: normalizedEvent.recorded_at_ms ?? normalizedEvent.at_ms,
259
+ stateBefore: this.#state,
260
+ transition,
261
+ captures,
262
+ });
185
263
  this.#uses.set(transition.id, used + 1);
186
264
  this.#stateVisits.set(transition.to, nextVisits);
187
265
  this.#state = transition.to;
@@ -207,6 +285,7 @@ export class ReplayMachineRuntime {
207
285
  this.#messages = messages;
208
286
  this.#bytes = bytes;
209
287
  this.#lastAtMs = event.at_ms;
288
+ this.#events += 1;
210
289
  return true;
211
290
  }
212
291
  #refusal(outcome) {
@@ -140,7 +140,7 @@ const EXPECTED = {
140
140
  "REA function comparison",
141
141
  ],
142
142
  compare_process_captures: [
143
- "rea.process-comparison/v3",
143
+ ["rea.process-comparison/v3", "rea.process-comparison/v4"],
144
144
  "rea-process",
145
145
  "REA deterministic process harness",
146
146
  ],
@@ -302,7 +302,9 @@ const parseRuntime = (input) => {
302
302
  const assertComparisonIdentity = (evidence) => {
303
303
  const expected = EXPECTED[evidence.operation];
304
304
  if (expected === undefined ||
305
- evidence.predicate_type !== expected[0] ||
305
+ !(Array.isArray(expected[0])
306
+ ? expected[0].includes(evidence.predicate_type)
307
+ : evidence.predicate_type === expected[0]) ||
306
308
  evidence.provider.id !== expected[1] ||
307
309
  evidence.provider.name !== expected[2] ||
308
310
  evidence.provider.version !==
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Classify a ZIP-backed package by its complete lower-cased path suffix.
3
+ *
4
+ * This is deliberately extension-only. Callers must separately verify ZIP
5
+ * magic before trusting the classification for a root input.
6
+ */
7
+ export const zipPackageFormatForPath = (path) => {
8
+ const lower = path.toLowerCase();
9
+ if (lower.endsWith(".zip"))
10
+ return "zip";
11
+ if (lower.endsWith(".ipa"))
12
+ return "ipa";
13
+ if (lower.endsWith(".apk") || lower.endsWith(".aab"))
14
+ return "apk";
15
+ if (lower.endsWith(".msix") || lower.endsWith(".msixbundle"))
16
+ return "msix";
17
+ if (lower.endsWith(".appx") || lower.endsWith(".appxbundle"))
18
+ return "appx";
19
+ return undefined;
20
+ };
@@ -26,6 +26,12 @@ const toolResultFailed = (item) => {
26
26
  const structured = record(result?.structured_content ?? result?.structuredContent);
27
27
  return result?.isError === true || structured?.error !== undefined;
28
28
  };
29
+ const toolResultErrorCode = (item) => {
30
+ const result = record(item.result ?? item.output);
31
+ const structured = record(result?.structured_content ?? result?.structuredContent);
32
+ const error = record(structured?.error ?? result?.error ?? item.error);
33
+ return textValue(error?.code) ?? null;
34
+ };
29
35
  const callFromItem = (value) => {
30
36
  const item = record(value);
31
37
  if (item?.type !== "mcp_tool_call")
@@ -40,6 +46,7 @@ const callFromItem = (value) => {
40
46
  arguments: item.arguments ?? item.input ?? {},
41
47
  evidenceIds: evidenceIdsFrom(item.result ?? item.output),
42
48
  error: toolResultFailed(item),
49
+ errorCode: toolResultErrorCode(item),
43
50
  };
44
51
  };
45
52
  const agentTextFromItem = (value) => {
@@ -102,6 +109,8 @@ export const evaluateCodexEvents = (events, expectedFirstTool, options = {}) =>
102
109
  signatures.set(signature, (signatures.get(signature) ?? 0) + 1);
103
110
  }
104
111
  const repeatedCallCount = [...signatures.values()].reduce((total, count) => total + Math.max(0, count - 1), 0);
112
+ const inputValidationFailureCount = reaCalls.filter(({ errorCode }) => errorCode === "invalid_request").length;
113
+ const requiredToolSubsequenceMet = containsOrderedSubsequence(reaCalls.filter(({ error }) => !error).map(({ tool }) => tool), options.requiredToolSubsequence ?? []);
105
114
  const finalMessage = state.finalMessages.at(-1) ?? "";
106
115
  const evidenceIds = [
107
116
  ...new Set(reaCalls.flatMap(({ evidenceIds: ids }) => ids)),
@@ -116,6 +125,8 @@ export const evaluateCodexEvents = (events, expectedFirstTool, options = {}) =>
116
125
  firstTool: reaCalls[0]?.tool ?? null,
117
126
  reaCalls,
118
127
  repeatedCallCount,
128
+ inputValidationFailureCount,
129
+ requiredToolSubsequenceMet,
119
130
  inputTokens: state.inputTokens,
120
131
  cachedInputTokens: state.cachedInputTokens,
121
132
  outputTokens: state.outputTokens,
@@ -126,7 +137,20 @@ export const evaluateCodexEvents = (events, expectedFirstTool, options = {}) =>
126
137
  completionQuality: finalMessage.trim().length >= 80 &&
127
138
  authorityHonesty &&
128
139
  contentCriteriaMet &&
140
+ requiredToolSubsequenceMet &&
141
+ (options.forbidInputValidationFailures !== true ||
142
+ inputValidationFailureCount === 0) &&
129
143
  (options.requireEvidence !== true || finalCitesEvidence),
130
144
  authorityHonesty,
131
145
  };
132
146
  };
147
+ const containsOrderedSubsequence = (values, required) => {
148
+ let index = 0;
149
+ for (const value of values) {
150
+ if (value === required[index])
151
+ index += 1;
152
+ if (index === required.length)
153
+ return true;
154
+ }
155
+ return required.length === 0;
156
+ };
@@ -1,7 +1,7 @@
1
1
  /** Generated from package.json and package-lock.json; do not edit. */
2
2
  export const PACKAGE_METADATA = {
3
3
  name: "rea-agents",
4
- version: "2.2.0", // x-release-please-version
4
+ version: "2.4.0", // x-release-please-version
5
5
  serverSdkVersion: "2.0.0-beta.4",
6
6
  clientSdkVersion: "2.0.0-beta.4",
7
7
  coreSdkVersion: "2.0.0-beta.4",
@@ -6,6 +6,7 @@ import { silentLogger } from "../logger.js";
6
6
  import { PendingOperations } from "../process/PendingOperations.js";
7
7
  import { PrivateRuntimeRoot } from "../process/PrivateRuntimeRoot.js";
8
8
  import { ProviderStartupDeadline } from "../process/ProviderDeadline.js";
9
+ import { ProviderRunLineage } from "../process/ProviderRunLineage.js";
9
10
  import { ProviderProcessSupervisor, } from "../process/ProviderProcess.js";
10
11
  import { GHIDRA_MAX_LINE_BYTES, GHIDRA_MAX_QUEUED_REQUESTS, GHIDRA_REQUEST_TIMEOUT_MS, GHIDRA_STARTUP_TIMEOUT_MS, } from "./GhidraDefaults.js";
11
12
  import { createGhidraDiagnostics } from "./GhidraDiagnostics.js";
@@ -33,6 +34,7 @@ export class GhidraClient {
33
34
  #snapshotPath;
34
35
  #token;
35
36
  #runId;
37
+ #lineage = new ProviderRunLineage();
36
38
  #nextId = 1;
37
39
  #closing = false;
38
40
  #processSnapshot;
@@ -145,6 +147,10 @@ export class GhidraClient {
145
147
  diagnostics() {
146
148
  return structuredClone(this.#diagnostics());
147
149
  }
150
+ /** Latest token-verified launcher lineage for the active run. */
151
+ runtimeLineage() {
152
+ return this.#lineage.snapshot();
153
+ }
148
154
  async #start(signal) {
149
155
  if (signal.aborted)
150
156
  return err(this.#failure("cancelled", "Ghidra startup was cancelled"));
@@ -188,7 +194,8 @@ export class GhidraClient {
188
194
  if (deadline.signal.aborted)
189
195
  return this.#startupInterrupted(deadline);
190
196
  this.#token = randomBytes(32).toString("hex");
191
- this.#runId = randomUUID();
197
+ this.#lineage.reset();
198
+ this.#runId = this.#options.runId ?? randomUUID();
192
199
  const launched = await this.#options.launcher
193
200
  .launch({
194
201
  runtimeRoot: this.#runtimeRoot.path,
@@ -221,7 +228,7 @@ export class GhidraClient {
221
228
  await this.#cleanup();
222
229
  return err(failure);
223
230
  }
224
- return completeGhidraStartupHandshake({
231
+ const completed = await completeGhidraStartupHandshake({
225
232
  deadline,
226
233
  request: (method, params, requestOptions) => this.#wire.request(method, params, requestOptions),
227
234
  parseSessionInfo: (value) => this.#parseSessionInfo(value),
@@ -229,6 +236,9 @@ export class GhidraClient {
229
236
  failure: this.#failure,
230
237
  startupTimeoutMs: this.#options.startupTimeoutMs,
231
238
  });
239
+ if (completed.ok)
240
+ await this.#lineage.observe(this.#launch);
241
+ return completed;
232
242
  }
233
243
  async #connect(endpoint, deadline) {
234
244
  const connected = await connectGhidraSocket({
@@ -373,6 +383,7 @@ export class GhidraClient {
373
383
  transport: this.#options.transport,
374
384
  providerVersion: this.#options.providerVersion,
375
385
  profileDigest: this.#options.profileDigest,
386
+ ...(this.#runId === undefined ? {} : { runId: this.#runId }),
376
387
  ...(this.#runtimeRoot === undefined
377
388
  ? {}
378
389
  : { runtimeRoot: this.#runtimeRoot.path }),
@@ -14,6 +14,7 @@ export const createGhidraDiagnostics = (options) => {
14
14
  transport: options.transport,
15
15
  provider_version: options.providerVersion,
16
16
  profile_digest: options.profileDigest,
17
+ ...(options.runId === undefined ? {} : { run_id: options.runId }),
17
18
  ...(options.runtimeRoot === undefined
18
19
  ? {}
19
20
  : {
@@ -91,9 +91,7 @@ export class GhidraHeadlessLauncher {
91
91
  }
92
92
  const spawned = started;
93
93
  return ok({
94
- process: spawned.process,
95
- ownsProcessLifetime: true,
96
- cleanup: () => cleanupStartedProcess(spawned, platform),
94
+ ...ownedGhidraProcess(spawned, platform),
97
95
  projectRoot: paths.projectRoot,
98
96
  ghidraLogPath: paths.ghidraLogPath,
99
97
  scriptLogPath: paths.scriptLogPath,
@@ -108,6 +106,12 @@ export class GhidraHeadlessLauncher {
108
106
  }
109
107
  }
110
108
  }
109
+ const ownedGhidraProcess = (spawned, platform) => ({
110
+ process: spawned.process,
111
+ ownsProcessLifetime: true,
112
+ ownership: spawned.ownership,
113
+ cleanup: () => cleanupStartedProcess(spawned, platform),
114
+ });
111
115
  /** Build a direct POSIX launch or a conservatively quoted Windows batch call. */
112
116
  export const ghidraHeadlessCommand = (options) => {
113
117
  if (options.platform !== "win32")
@@ -95,7 +95,7 @@ export class GhidraProvider {
95
95
  resolveAnalysisProfile(target, options) {
96
96
  return resolveGhidraAnalysisProfile(target, GHIDRA_PROVIDER_IDENTITY, this.#inspectInstallation(), options?.signal);
97
97
  }
98
- createClient(target, profile) {
98
+ createClient(target, profile, context) {
99
99
  const installation = this.#inspectInstallation();
100
100
  const prerequisites = ghidraClientPrerequisites(target, profile, installation);
101
101
  if (!prerequisites.ok)
@@ -118,6 +118,7 @@ export class GhidraProvider {
118
118
  : "unix-socket",
119
119
  providerVersion: prerequisites.value.providerVersion,
120
120
  profileDigest: committedProfile.digest,
121
+ ...(context === undefined ? {} : { runId: context.runId }),
121
122
  logger: this.logger.child({ layer: "ghidra-bridge" }),
122
123
  });
123
124
  return {
@@ -160,6 +161,12 @@ export class GhidraProvider {
160
161
  limitations: [...limitationsFor(operation), ...providerLimitations],
161
162
  }));
162
163
  },
164
+ runtimeLineageSnapshots: () => {
165
+ const observation = client.runtimeLineage?.() ?? null;
166
+ return observation === null
167
+ ? []
168
+ : [{ provider: committedProfile.provider, observation }];
169
+ },
163
170
  close: () => client.close(),
164
171
  };
165
172
  }
@@ -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
  }