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
@@ -6,10 +6,14 @@ from __future__ import annotations
6
6
  import argparse
7
7
  import ctypes
8
8
  import hashlib
9
+ import json
9
10
  import os
10
11
  from pathlib import Path
11
12
  import secrets
13
+ import stat
12
14
  import subprocess
15
+ import tempfile
16
+ import threading
13
17
  import time
14
18
 
15
19
 
@@ -31,12 +35,60 @@ UNSUPPORTED_DEMO_DIALOG = 76
31
35
  UNEXPECTED_DISPLAY_GEOMETRY = 77
32
36
  X11_INPUT_FAILED = 78
33
37
  RUNTIME_DEPENDENCY_UNAVAILABLE = 79
38
+ X11_SOCKET_DIRECTORY_UNUSABLE = 80
39
+ DIAGNOSTIC_PREFIX = "REA_X11_DIAGNOSTIC_V1="
40
+ X11_SOCKET_DIRECTORY = Path("/tmp/.X11-unix")
41
+ XVFB_STDERR_LIMIT = 16 * 1024
34
42
 
35
43
 
36
44
  class AdapterFailure(Exception):
37
- def __init__(self, code: int, message: str) -> None:
45
+ def __init__(
46
+ self,
47
+ code: int,
48
+ reason: str,
49
+ message: str,
50
+ stderr_bytes: int = 0,
51
+ stderr_truncated: bool = False,
52
+ ) -> None:
38
53
  super().__init__(message)
39
54
  self.code = code
55
+ self.reason = reason
56
+ self.stderr_bytes = stderr_bytes
57
+ self.stderr_truncated = stderr_truncated
58
+
59
+ def with_stderr(self, capture: BoundedStderr) -> AdapterFailure:
60
+ self.stderr_bytes, self.stderr_truncated, _ = capture.snapshot()
61
+ return self
62
+
63
+
64
+ class BoundedStderr:
65
+ def __init__(self, stream: object) -> None:
66
+ self._stream = stream
67
+ self._retained = bytearray()
68
+ self._bytes = 0
69
+ self._lock = threading.Lock()
70
+ self._thread = threading.Thread(target=self._drain, daemon=True)
71
+ self._thread.start()
72
+
73
+ def _drain(self) -> None:
74
+ while True:
75
+ chunk = self._stream.read(4096)
76
+ if not chunk:
77
+ return
78
+ with self._lock:
79
+ self._bytes += len(chunk)
80
+ remaining = XVFB_STDERR_LIMIT - len(self._retained)
81
+ if remaining > 0:
82
+ self._retained.extend(chunk[:remaining])
83
+
84
+ def snapshot(self) -> tuple[int, bool, str]:
85
+ with self._lock:
86
+ retained = bytes(self._retained)
87
+ total = self._bytes
88
+ return total, total > len(retained), retained.decode("utf-8", "replace")
89
+
90
+ def close(self) -> None:
91
+ self._thread.join(timeout=0.5)
40
92
 
41
93
 
42
94
  class XWindowAttributes(ctypes.Structure):
@@ -69,8 +121,18 @@ class XWindowAttributes(ctypes.Structure):
69
121
 
70
122
  def parse_args() -> argparse.Namespace:
71
123
  parser = argparse.ArgumentParser()
72
- parser.add_argument("--hopper", required=True)
73
- parser.add_argument("--socket", required=True)
124
+ parser.add_argument("--probe", action="store_true")
125
+ parser.add_argument("--hopper")
126
+ parser.add_argument("--socket")
127
+ parser.add_argument(
128
+ "--strategy",
129
+ choices=("direct", "user-mount-namespace"),
130
+ default="direct",
131
+ )
132
+ parser.add_argument("--mount-private-x11", action="store_true")
133
+ parser.add_argument("--xvfb", default="/usr/bin/Xvfb")
134
+ parser.add_argument("--xauth", default="/usr/bin/xauth")
135
+ parser.add_argument("--display-number", type=int)
74
136
  parser.add_argument("command", nargs=argparse.REMAINDER)
75
137
  return parser.parse_args()
76
138
 
@@ -189,66 +251,369 @@ def configure_x11(x11: ctypes.CDLL, xtst: ctypes.CDLL) -> None:
189
251
  xtst.XTestFakeButtonEvent.restype = ctypes.c_int
190
252
 
191
253
 
192
- def start_xvfb(session: Path) -> tuple[subprocess.Popen[bytes], str, Path]:
254
+ def is_wsl() -> bool:
255
+ for path in (Path("/proc/sys/kernel/osrelease"), Path("/proc/version")):
256
+ try:
257
+ if "microsoft" in path.read_text().lower():
258
+ return True
259
+ except OSError:
260
+ continue
261
+ return False
262
+
263
+
264
+ def socket_directory_state() -> tuple[str | None, bool | None, bool]:
265
+ try:
266
+ value = X11_SOCKET_DIRECTORY.lstat()
267
+ except FileNotFoundError:
268
+ try:
269
+ read_only = bool(os.statvfs(X11_SOCKET_DIRECTORY.parent).f_flag & os.ST_RDONLY)
270
+ except OSError:
271
+ read_only = None
272
+ return None, read_only, False
273
+ except OSError:
274
+ return None, None, False
275
+ try:
276
+ read_only = bool(os.statvfs(X11_SOCKET_DIRECTORY).f_flag & os.ST_RDONLY)
277
+ except OSError:
278
+ read_only = None
279
+ return f"{stat.S_IMODE(value.st_mode):04o}", read_only, stat.S_ISDIR(value.st_mode)
280
+
281
+
282
+ def diagnostic(
283
+ args: argparse.Namespace,
284
+ operation: str,
285
+ status: str,
286
+ failure: AdapterFailure | None = None,
287
+ capture: BoundedStderr | None = None,
288
+ ) -> dict[str, object]:
289
+ mode, read_only, _ = socket_directory_state()
290
+ captured_bytes, captured_truncated, _ = (
291
+ (0, False, "") if capture is None else capture.snapshot()
292
+ )
293
+ return {
294
+ "schema_version": 1,
295
+ "component": "hopper_private_display",
296
+ "operation": operation,
297
+ "status": status,
298
+ "failure_code": None if failure is None else failure_name(failure.code),
299
+ "reason": "ready" if failure is None else failure.reason,
300
+ "socket_directory": str(X11_SOCKET_DIRECTORY),
301
+ "socket_directory_mode": mode,
302
+ "mount_read_only": read_only,
303
+ "effective_socket_directory_mode": mode,
304
+ "effective_mount_read_only": read_only,
305
+ "wsl": is_wsl(),
306
+ "strategy": args.strategy,
307
+ "fallback_reason": None,
308
+ "xvfb_stderr_bytes": captured_bytes
309
+ if failure is None
310
+ else failure.stderr_bytes,
311
+ "xvfb_stderr_truncated": captured_truncated
312
+ if failure is None
313
+ else failure.stderr_truncated,
314
+ }
315
+
316
+
317
+ def failure_name(code: int) -> str:
318
+ return {
319
+ PRIVATE_DISPLAY_UNAVAILABLE: "private_display_unavailable",
320
+ X11_AUTHORIZATION_FAILED: "x11_authorization_failed",
321
+ UNSUPPORTED_HOPPER_BUILD: "unsupported_hopper_build",
322
+ INVALID_LAUNCH_COMMAND: "invalid_launch_command",
323
+ PROCESS_OWNERSHIP_MISMATCH: "process_ownership_mismatch",
324
+ HOPPER_EXITED_DURING_STARTUP: "hopper_exited_during_startup",
325
+ UNSUPPORTED_DEMO_DIALOG: "unsupported_demo_dialog",
326
+ UNEXPECTED_DISPLAY_GEOMETRY: "unexpected_display_geometry",
327
+ X11_INPUT_FAILED: "x11_input_failed",
328
+ RUNTIME_DEPENDENCY_UNAVAILABLE: "runtime_dependency_unavailable",
329
+ X11_SOCKET_DIRECTORY_UNUSABLE: "x11_socket_directory_unusable",
330
+ }[code]
331
+
332
+
333
+ def emit_diagnostic(value: dict[str, object]) -> None:
334
+ print(
335
+ DIAGNOSTIC_PREFIX + json.dumps(value, sort_keys=True, separators=(",", ":")),
336
+ file=os.sys.stderr,
337
+ )
338
+
339
+
340
+ def mount_private_x11() -> None:
341
+ try:
342
+ mounted = subprocess.run(
343
+ [
344
+ "/usr/bin/mount",
345
+ "-t",
346
+ "tmpfs",
347
+ "-o",
348
+ "mode=1777,nosuid,nodev",
349
+ "rea-x11",
350
+ str(X11_SOCKET_DIRECTORY),
351
+ ],
352
+ check=False,
353
+ stdout=subprocess.DEVNULL,
354
+ stderr=subprocess.PIPE,
355
+ timeout=2,
356
+ )
357
+ except FileNotFoundError as error:
358
+ raise AdapterFailure(
359
+ RUNTIME_DEPENDENCY_UNAVAILABLE,
360
+ "missing_mount",
361
+ "mount executable unavailable",
362
+ ) from error
363
+ except subprocess.TimeoutExpired as error:
364
+ raise AdapterFailure(
365
+ X11_SOCKET_DIRECTORY_UNUSABLE,
366
+ "mount_failed",
367
+ "private X11 mount timed out",
368
+ ) from error
369
+ if mounted.returncode != 0:
370
+ raise AdapterFailure(
371
+ X11_SOCKET_DIRECTORY_UNUSABLE,
372
+ "mount_failed",
373
+ "private X11 mount failed",
374
+ )
375
+ mode, read_only, directory = socket_directory_state()
376
+ if not directory or mode != "1777" or read_only is not False:
377
+ raise AdapterFailure(
378
+ X11_SOCKET_DIRECTORY_UNUSABLE,
379
+ "mount_validation_failed",
380
+ "private X11 mount validation failed",
381
+ )
382
+
383
+
384
+ def stop_process(process: subprocess.Popen[bytes]) -> None:
385
+ if process.poll() is not None:
386
+ return
387
+ process.terminate()
388
+ try:
389
+ process.wait(timeout=2)
390
+ except subprocess.TimeoutExpired:
391
+ process.kill()
392
+ process.wait()
393
+
394
+
395
+ def authorize_display(
396
+ authority: Path,
397
+ display_name: str,
398
+ xauth_path: str,
399
+ ) -> None:
400
+ try:
401
+ authorized = subprocess.run(
402
+ [
403
+ xauth_path,
404
+ "-f",
405
+ str(authority),
406
+ "add",
407
+ display_name,
408
+ ".",
409
+ secrets.token_hex(16),
410
+ ],
411
+ check=False,
412
+ stdout=subprocess.DEVNULL,
413
+ stderr=subprocess.PIPE,
414
+ timeout=2,
415
+ )
416
+ except FileNotFoundError as error:
417
+ raise AdapterFailure(
418
+ RUNTIME_DEPENDENCY_UNAVAILABLE,
419
+ "missing_xauth",
420
+ "xauth executable unavailable",
421
+ ) from error
422
+ except subprocess.TimeoutExpired as error:
423
+ raise AdapterFailure(
424
+ X11_AUTHORIZATION_FAILED,
425
+ "xauth_failed",
426
+ "private X authorization timed out",
427
+ ) from error
428
+ if authorized.returncode != 0:
429
+ raise AdapterFailure(
430
+ X11_AUTHORIZATION_FAILED,
431
+ "xauth_failed",
432
+ "private X authorization failed",
433
+ )
434
+ try:
435
+ authority.chmod(0o600)
436
+ except OSError as error:
437
+ raise AdapterFailure(
438
+ X11_AUTHORIZATION_FAILED,
439
+ "xauth_failed",
440
+ "private X authorization file unavailable",
441
+ ) from error
442
+
443
+
444
+ def classify_xvfb_failure(capture: BoundedStderr) -> AdapterFailure:
445
+ stderr_bytes, stderr_truncated, text = capture.snapshot()
446
+ mode, read_only, directory = socket_directory_state()
447
+ if read_only is True:
448
+ return AdapterFailure(
449
+ X11_SOCKET_DIRECTORY_UNUSABLE,
450
+ "socket_directory_read_only",
451
+ "X11 socket directory is read-only",
452
+ stderr_bytes,
453
+ stderr_truncated,
454
+ )
455
+ if directory and mode != "1777":
456
+ return AdapterFailure(
457
+ X11_SOCKET_DIRECTORY_UNUSABLE,
458
+ "socket_directory_mode",
459
+ "X11 socket directory mode is not 1777",
460
+ stderr_bytes,
461
+ stderr_truncated,
462
+ )
463
+ if mode is not None and not directory:
464
+ return AdapterFailure(
465
+ X11_SOCKET_DIRECTORY_UNUSABLE,
466
+ "socket_directory_type",
467
+ "X11 socket path is not a directory",
468
+ stderr_bytes,
469
+ stderr_truncated,
470
+ )
471
+ lowered = text.lower()
472
+ if "error while loading shared libraries" in lowered:
473
+ return AdapterFailure(
474
+ RUNTIME_DEPENDENCY_UNAVAILABLE,
475
+ "missing_library",
476
+ "Xvfb shared library unavailable",
477
+ stderr_bytes,
478
+ stderr_truncated,
479
+ )
480
+ if any(
481
+ marker in lowered
482
+ for marker in (
483
+ "server is already active for display",
484
+ "address already in use",
485
+ "cannot establish any listening sockets",
486
+ )
487
+ ):
488
+ return AdapterFailure(
489
+ PRIVATE_DISPLAY_UNAVAILABLE,
490
+ "address_collision",
491
+ "private display address collision",
492
+ stderr_bytes,
493
+ stderr_truncated,
494
+ )
495
+ return AdapterFailure(
496
+ PRIVATE_DISPLAY_UNAVAILABLE,
497
+ "xvfb_startup_failed",
498
+ "private X display unavailable",
499
+ stderr_bytes,
500
+ stderr_truncated,
501
+ )
502
+
503
+
504
+ def start_xvfb(
505
+ session: Path,
506
+ args: argparse.Namespace,
507
+ ) -> tuple[subprocess.Popen[bytes], str, Path, BoundedStderr]:
193
508
  authority = session / "Xauthority"
194
- for _ in range(20):
195
- display_number = 100 + secrets.randbelow(900)
509
+ collision: AdapterFailure | None = None
510
+ for _ in range(3):
511
+ display_number = args.display_number or (100 + secrets.randbelow(900))
196
512
  display_name = f":{display_number}"
197
- if Path(f"/tmp/.X11-unix/X{display_number}").exists():
513
+ if Path(f"/tmp/.X11-unix/X{display_number}").exists() or Path(
514
+ f"/tmp/.X{display_number}-lock"
515
+ ).exists():
516
+ collision = AdapterFailure(
517
+ PRIVATE_DISPLAY_UNAVAILABLE,
518
+ "address_collision",
519
+ "private display address collision",
520
+ )
521
+ if args.display_number is not None:
522
+ raise collision
198
523
  continue
199
- cookie = secrets.token_hex(16)
524
+ authorize_display(authority, display_name, args.xauth)
200
525
  try:
201
- subprocess.run(
526
+ process = subprocess.Popen(
202
527
  [
203
- "/usr/bin/xauth", "-f", str(authority), "add",
204
- display_name, ".", cookie,
528
+ args.xvfb,
529
+ display_name,
530
+ "-screen",
531
+ "0",
532
+ "1280x1024x24",
533
+ "-nolisten",
534
+ "tcp",
535
+ "-auth",
536
+ str(authority),
205
537
  ],
206
- check=True,
207
538
  stdout=subprocess.DEVNULL,
208
- stderr=subprocess.DEVNULL,
539
+ stderr=subprocess.PIPE,
209
540
  )
210
- except (OSError, subprocess.CalledProcessError) as error:
541
+ except FileNotFoundError as error:
211
542
  raise AdapterFailure(
212
- X11_AUTHORIZATION_FAILED, "private X authorization failed"
543
+ RUNTIME_DEPENDENCY_UNAVAILABLE,
544
+ "missing_xvfb",
545
+ "Xvfb executable unavailable",
213
546
  ) from error
214
- authority.chmod(0o600)
215
- process = subprocess.Popen(
216
- [
217
- "/usr/bin/Xvfb",
218
- display_name,
219
- "-screen",
220
- "0",
221
- "1280x1024x24",
222
- "-nolisten",
223
- "tcp",
224
- "-auth",
225
- str(authority),
226
- ],
227
- stdout=subprocess.DEVNULL,
228
- stderr=subprocess.DEVNULL,
229
- )
547
+ if process.stderr is None:
548
+ stop_process(process)
549
+ raise AdapterFailure(
550
+ PRIVATE_DISPLAY_UNAVAILABLE,
551
+ "xvfb_startup_failed",
552
+ "Xvfb stderr unavailable",
553
+ )
554
+ capture = BoundedStderr(process.stderr)
230
555
  deadline = time.monotonic() + 2
231
556
  socket = Path(f"/tmp/.X11-unix/X{display_number}")
232
557
  while time.monotonic() < deadline and process.poll() is None:
233
- if socket.exists():
234
- return process, display_name, authority
558
+ if socket.is_socket():
559
+ return process, display_name, authority, capture
235
560
  time.sleep(0.05)
236
- if process.poll() is None:
237
- process.terminate()
238
- process.wait()
239
- raise AdapterFailure(PRIVATE_DISPLAY_UNAVAILABLE, "private X display unavailable")
561
+ stop_process(process)
562
+ capture.close()
563
+ failure = classify_xvfb_failure(capture)
564
+ if failure.reason != "address_collision" or args.display_number is not None:
565
+ raise failure
566
+ collision = failure
567
+ raise collision or AdapterFailure(
568
+ PRIVATE_DISPLAY_UNAVAILABLE,
569
+ "address_collision",
570
+ "private display address collision",
571
+ )
240
572
 
241
573
 
242
- def main() -> int:
243
- args = parse_args()
574
+ def run_probe(args: argparse.Namespace) -> dict[str, object]:
575
+ with tempfile.TemporaryDirectory(prefix="rea-x11-probe-", dir="/tmp") as root:
576
+ xvfb, _, _, capture = start_xvfb(Path(root), args)
577
+ try:
578
+ stop_process(xvfb)
579
+ capture.close()
580
+ return diagnostic(args, "probe", "ready", capture=capture)
581
+ finally:
582
+ stop_process(xvfb)
583
+ capture.close()
584
+
585
+
586
+ def run_launch(args: argparse.Namespace) -> int:
587
+ if args.hopper is None or args.socket is None:
588
+ raise AdapterFailure(
589
+ INVALID_LAUNCH_COMMAND,
590
+ "invalid_launch_command",
591
+ "missing Hopper launch coordinates",
592
+ )
244
593
  hopper = Path(args.hopper)
245
594
  socket = Path(args.socket)
246
- if digest(hopper) not in SUPPORTED_HOPPER_SHA256:
247
- raise AdapterFailure(UNSUPPORTED_HOPPER_BUILD, "unsupported Hopper binary")
595
+ try:
596
+ supported = digest(hopper) in SUPPORTED_HOPPER_SHA256
597
+ except OSError as error:
598
+ raise AdapterFailure(
599
+ INVALID_LAUNCH_COMMAND,
600
+ "invalid_launch_command",
601
+ "Hopper binary unavailable",
602
+ ) from error
603
+ if not supported:
604
+ raise AdapterFailure(
605
+ UNSUPPORTED_HOPPER_BUILD,
606
+ "unsupported_hopper_build",
607
+ "unsupported Hopper binary",
608
+ )
248
609
  command = args.command[1:] if args.command[:1] == ["--"] else args.command
249
610
  if not command or Path(command[0]).resolve() != hopper.resolve():
250
- raise AdapterFailure(INVALID_LAUNCH_COMMAND, "invalid Hopper launch command")
251
- xvfb, display_name, authority = start_xvfb(socket.parent)
611
+ raise AdapterFailure(
612
+ INVALID_LAUNCH_COMMAND,
613
+ "invalid_launch_command",
614
+ "invalid Hopper launch command",
615
+ )
616
+ xvfb, display_name, authority, capture = start_xvfb(socket.parent, args)
252
617
  child: subprocess.Popen[bytes] | None = None
253
618
  display: int | None = None
254
619
  try:
@@ -273,19 +638,27 @@ def main() -> int:
273
638
  xtst = ctypes.CDLL("libXtst.so.6")
274
639
  except OSError as error:
275
640
  raise AdapterFailure(
276
- RUNTIME_DEPENDENCY_UNAVAILABLE, "X11 runtime unavailable"
641
+ RUNTIME_DEPENDENCY_UNAVAILABLE,
642
+ "missing_library",
643
+ "X11 runtime unavailable",
277
644
  ) from error
278
645
  configure_x11(x11, xtst)
279
646
  display = x11.XOpenDisplay(display_name.encode())
280
647
  if not display:
281
- raise AdapterFailure(PRIVATE_DISPLAY_UNAVAILABLE, "private X display unavailable")
648
+ raise AdapterFailure(
649
+ PRIVATE_DISPLAY_UNAVAILABLE,
650
+ "x11_connection_failed",
651
+ "private X display unavailable",
652
+ )
282
653
  root = x11.XDefaultRootWindow(display)
283
654
  if (
284
655
  x11.XDisplayWidth(display, 0),
285
656
  x11.XDisplayHeight(display, 0),
286
657
  ) != EXPECTED_SCREEN:
287
658
  raise AdapterFailure(
288
- UNEXPECTED_DISPLAY_GEOMETRY, "unexpected private display geometry"
659
+ UNEXPECTED_DISPLAY_GEOMETRY,
660
+ "unexpected_display_geometry",
661
+ "unexpected private display geometry",
289
662
  )
290
663
 
291
664
  deadline = time.monotonic() + 10
@@ -300,7 +673,9 @@ def main() -> int:
300
673
  time.sleep(0.1)
301
674
  continue
302
675
  raise AdapterFailure(
303
- HOPPER_EXITED_DURING_STARTUP, "owned Hopper process exited"
676
+ HOPPER_EXITED_DURING_STARTUP,
677
+ "hopper_exited_during_startup",
678
+ "owned Hopper process exited",
304
679
  )
305
680
  observed = windows(x11, display, root)
306
681
  if any(window[1:] == EXPECTED_DIALOG for window in observed):
@@ -309,10 +684,14 @@ def main() -> int:
309
684
  else:
310
685
  if not hopper_is_owned(leader, hopper):
311
686
  raise AdapterFailure(
312
- PROCESS_OWNERSHIP_MISMATCH, "Hopper process ownership mismatch"
687
+ PROCESS_OWNERSHIP_MISMATCH,
688
+ "process_ownership_mismatch",
689
+ "Hopper process ownership mismatch",
313
690
  )
314
691
  raise AdapterFailure(
315
- UNSUPPORTED_DEMO_DIALOG, "expected Hopper demo dialog not found"
692
+ UNSUPPORTED_DEMO_DIALOG,
693
+ "unsupported_demo_dialog",
694
+ "expected Hopper demo dialog not found",
316
695
  )
317
696
 
318
697
  input_results = (
@@ -321,29 +700,48 @@ def main() -> int:
321
700
  xtst.XTestFakeButtonEvent(display, 1, False, 0),
322
701
  )
323
702
  if not all(input_results):
324
- raise AdapterFailure(X11_INPUT_FAILED, "private X input failed")
703
+ raise AdapterFailure(
704
+ X11_INPUT_FAILED,
705
+ "x11_input_failed",
706
+ "private X input failed",
707
+ )
325
708
  x11.XFlush(display)
326
709
  x11.XCloseDisplay(display)
327
710
  display = None
328
711
  return child.wait()
712
+ except AdapterFailure as error:
713
+ raise error.with_stderr(capture)
329
714
  finally:
330
715
  if display is not None:
331
716
  x11.XCloseDisplay(display)
332
717
  if child is not None and child.poll() is None:
333
718
  child.terminate()
334
719
  child.wait()
335
- if xvfb.poll() is None:
336
- xvfb.terminate()
337
- try:
338
- xvfb.wait(timeout=2)
339
- except subprocess.TimeoutExpired:
340
- xvfb.kill()
341
- xvfb.wait()
720
+ stop_process(xvfb)
721
+ capture.close()
342
722
 
343
723
 
344
- if __name__ == "__main__":
724
+ def main() -> int:
725
+ args = parse_args()
345
726
  try:
346
- raise SystemExit(main())
727
+ if args.mount_private_x11:
728
+ mount_private_x11()
729
+ if args.probe:
730
+ emit_diagnostic(run_probe(args))
731
+ return 0
732
+ return run_launch(args)
347
733
  except AdapterFailure as error:
348
- print(str(error), file=os.sys.stderr)
349
- raise SystemExit(error.code) from None
734
+ emit_diagnostic(diagnostic(args, "probe" if args.probe else "launch", "error", error))
735
+ return error.code
736
+ except Exception:
737
+ failure = AdapterFailure(
738
+ PRIVATE_DISPLAY_UNAVAILABLE,
739
+ "unexpected_failure",
740
+ "private display adapter failed",
741
+ )
742
+ emit_diagnostic(diagnostic(args, "probe" if args.probe else "launch", "error", failure))
743
+ return failure.code
744
+
745
+
746
+ if __name__ == "__main__":
747
+ raise SystemExit(main())
@@ -0,0 +1,55 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { access } from "node:fs/promises";
3
+ import { dirname, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ const root = join(dirname(fileURLToPath(import.meta.url)), "..");
7
+
8
+ const pathExists = async (path) => {
9
+ try {
10
+ await access(path);
11
+ return true;
12
+ } catch (cause) {
13
+ if (
14
+ typeof cause === "object" &&
15
+ cause !== null &&
16
+ "code" in cause &&
17
+ cause.code === "ENOENT"
18
+ )
19
+ return false;
20
+ throw cause;
21
+ }
22
+ };
23
+
24
+ const run = (command, args) => {
25
+ const result = spawnSync(command, args, {
26
+ cwd: root,
27
+ encoding: "utf8",
28
+ maxBuffer: 4 * 1_024 * 1_024,
29
+ });
30
+ if (result.error !== undefined) throw result.error;
31
+ if (result.status !== 0) {
32
+ if (result.stdout.length > 0) process.stdout.write(result.stdout);
33
+ if (result.stderr.length > 0) process.stderr.write(result.stderr);
34
+ throw new Error(
35
+ `Prepack command failed with status ${String(result.status)}: ${command} ${args.join(" ")}`,
36
+ );
37
+ }
38
+ };
39
+
40
+ if (await pathExists(join(root, "src"))) {
41
+ const packageManagerPath = process.env.npm_execpath;
42
+ if (packageManagerPath === undefined)
43
+ throw new Error("Prepack could not resolve the active package manager");
44
+ run(process.execPath, [packageManagerPath, "run", "build:cached"]);
45
+ run(process.execPath, ["scripts/generate-skill-metadata.mjs", "--check"]);
46
+ } else {
47
+ for (const path of [
48
+ "dist/main.js",
49
+ "dist/mcpDoctor.js",
50
+ "dist/cli.js",
51
+ "dist/cliOutput.js",
52
+ "skills/reverse-engineer-anything/SKILL.md",
53
+ ])
54
+ await access(join(root, path));
55
+ }