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.
- package/README.md +56 -9
- package/dist/application/ApplicationWorkflowEvidenceResolver.js +162 -0
- package/dist/application/BinarySession.js +9 -2
- package/dist/application/BinarySessionStatus.js +39 -1
- package/dist/application/CapabilityInventory.js +15 -0
- package/dist/application/CommandShimReplay.js +6 -3
- package/dist/application/CompositeProvider.js +5 -2
- package/dist/application/DirectAnalysis.js +1 -20
- package/dist/application/DirectAnalysisStatus.js +83 -0
- package/dist/application/Doctor.js +34 -25
- package/dist/application/DoctorDiagnostics.js +1 -4
- package/dist/application/DoctorProjection.js +43 -0
- package/dist/application/DoctorScope.js +102 -0
- package/dist/application/EvidenceReferenceResolver.js +26 -0
- package/dist/application/JavaScriptApplicationEvidence.js +1 -1
- package/dist/application/JavaScriptApplicationEvidenceGraph.js +10 -1
- package/dist/application/JavaScriptApplicationService.js +3 -2
- package/dist/application/JavaScriptApplicationWorkflowEvidence.js +26 -0
- package/dist/application/JavaScriptApplicationWorkflowService.js +49 -1
- package/dist/application/JavaScriptArtifactReconstruction.js +7 -0
- package/dist/application/JavaScriptModuleRelationships.js +25 -1
- package/dist/application/JavaScriptReturnShapeProjection.js +111 -0
- package/dist/application/JavaScriptSemanticGraphBuilder.js +294 -0
- package/dist/application/JavaScriptSemanticGraphConstruction.js +144 -0
- package/dist/application/JavaScriptSemanticGraphEvidence.js +86 -0
- package/dist/application/JavaScriptSemanticGraphFlowProjection.js +72 -0
- package/dist/application/JavaScriptSemanticGraphProjection.js +33 -0
- package/dist/application/JavaScriptSemanticTraceService.js +45 -0
- package/dist/application/LoopbackReplay.js +277 -133
- package/dist/application/LoopbackReplayRecorder.js +101 -0
- package/dist/application/PermissionAuthority.js +7 -3
- package/dist/application/ProcessCaptureJournal.js +95 -0
- package/dist/application/ProcessCaptureLifecycle.js +24 -13
- package/dist/application/ProcessCheckpoints.js +30 -4
- package/dist/application/ProcessCli.js +26 -4
- package/dist/application/ProcessHarness.js +20 -50
- package/dist/application/ProcessNormalization.js +3 -2
- package/dist/application/ProcessReactiveCoordinator.js +218 -0
- package/dist/application/ProcessReactiveEffects.js +133 -0
- package/dist/application/ProcessReactiveObservations.js +20 -0
- package/dist/application/ProcessSampling.js +2 -0
- package/dist/application/SessionProviderRouter.js +7 -6
- package/dist/application/Setup.js +11 -10
- package/dist/application/SetupPlan.js +1 -1
- package/dist/application/SetupRegistrationEnvironment.js +12 -0
- package/dist/application/TerminalRenderer.js +3 -1
- package/dist/browser/CdpEndpoint.js +37 -4
- package/dist/catalogIdentity.js +16 -7
- package/dist/cli/coreAnalysisCommands.js +34 -2
- package/dist/cli/javascriptApplicationAnalysis.js +19 -0
- package/dist/cli/setupCommands.js +81 -32
- package/dist/cli/utilityCommands.js +10 -2
- package/dist/cliApplicationCommands.js +79 -16
- package/dist/cliCommandNames.js +3 -0
- package/dist/cliElectronCommands.js +23 -33
- package/dist/cliObservationOptions.js +1 -1
- package/dist/cliOutput.js +56 -0
- package/dist/cliProcessCommands.js +32 -1
- package/dist/cliSetup.js +30 -21
- package/dist/contracts/applicationToolContracts.js +54 -7
- package/dist/contracts/applicationWorkflowInputContracts.js +119 -0
- package/dist/contracts/electronToolContracts.js +53 -32
- package/dist/contracts/javascriptApplicationWorkflowExamples.js +32 -9
- package/dist/contracts/processCaptureExample.js +1 -0
- package/dist/contracts/replayMachineExample.js +37 -0
- package/dist/contracts/toolContractExamples.js +2 -0
- package/dist/contracts/toolContracts.js +5 -1
- package/dist/contracts/toolEffects.js +3 -0
- package/dist/contracts/toolOutputSchemaGroups.js +2 -0
- package/dist/contracts/toolOutputSchemaPrimitives.js +40 -0
- package/dist/doctorRuntime.js +41 -0
- package/dist/domain/analysisErrorPresentation.js +5 -3
- package/dist/domain/analysisErrorProjection.js +7 -1
- package/dist/domain/changedBehavior.js +5 -4
- package/dist/domain/completionLedgerGeneration.js +190 -0
- package/dist/domain/errors.js +4 -2
- package/dist/domain/evidenceCompletionLedger.js +113 -0
- package/dist/domain/hopperStartupFailure.js +9 -0
- package/dist/domain/javascriptApplicationAnalysis.js +44 -2
- package/dist/domain/javascriptExportShapeComparison.js +145 -0
- package/dist/domain/javascriptExportShapeComparisonIdentity.js +11 -0
- package/dist/domain/javascriptExportShapeComparisonSchemas.js +277 -0
- package/dist/domain/javascriptExportShapeSelection.js +132 -0
- package/dist/domain/javascriptExportShapeVariants.js +272 -0
- package/dist/domain/javascriptRuntimeReconciliationParsing.js +2 -2
- package/dist/domain/javascriptSemanticAnalysis.js +17 -36
- package/dist/domain/javascriptSemanticCallResolution.js +129 -0
- package/dist/domain/javascriptSemanticCalls.js +269 -0
- package/dist/domain/javascriptSemanticGraph.js +356 -0
- package/dist/domain/javascriptSemanticGraphSchemas.js +329 -0
- package/dist/domain/javascriptSemanticGraphSerialization.js +23 -0
- package/dist/domain/javascriptSemanticIr.js +14 -1
- package/dist/domain/javascriptSemanticPrimitives.js +36 -0
- package/dist/domain/javascriptSemanticProjection.js +36 -4
- package/dist/domain/javascriptSemanticProvenance.js +29 -0
- package/dist/domain/javascriptSemanticQuery.js +294 -0
- package/dist/domain/javascriptSemanticQueryAssessment.js +74 -0
- package/dist/domain/javascriptSemanticQueryIdentity.js +86 -0
- package/dist/domain/javascriptSemanticQueryRelations.js +15 -0
- package/dist/domain/javascriptSemanticQuerySchemas.js +188 -0
- package/dist/domain/javascriptSemanticReturns.js +187 -0
- package/dist/domain/javascriptSemanticState.js +32 -0
- package/dist/domain/javascriptSemanticTraceSchemas.js +14 -0
- package/dist/domain/javascriptSemanticValues.js +54 -70
- package/dist/domain/processCapture.js +36 -0
- package/dist/domain/processCaptureValidation.js +101 -12
- package/dist/domain/processComparison.js +78 -25
- package/dist/domain/processObservation.js +153 -0
- package/dist/domain/processReactiveCheckpointDataflow.js +32 -0
- package/dist/domain/processReactiveMatching.js +210 -0
- package/dist/domain/processReactiveRuntime.js +109 -0
- package/dist/domain/processReactiveScenario.js +361 -0
- package/dist/domain/processReactiveScenarioPreflight.js +120 -0
- package/dist/domain/processReactiveTransition.js +159 -0
- package/dist/domain/processScenario.js +12 -0
- package/dist/domain/processTraceComparison.js +43 -0
- package/dist/domain/processTraceDimensionProjection.js +50 -0
- package/dist/domain/processTraceEvaluation.js +312 -0
- package/dist/domain/processTraceSpecification.js +373 -0
- package/dist/domain/reconstructionVerification.js +5 -4
- package/dist/domain/replayMachine.js +18 -2
- package/dist/domain/replayMachineRun.js +216 -0
- package/dist/domain/replayMachineRuntime.js +88 -9
- package/dist/domain/staticRuntimeCorrelation.js +4 -2
- package/dist/evaluation/CodexAgentEval.js +24 -0
- package/dist/generatedPackageMetadata.js +1 -1
- package/dist/ghidra/GhidraClient.js +13 -2
- package/dist/ghidra/GhidraDiagnostics.js +1 -0
- package/dist/ghidra/GhidraLauncher.js +7 -3
- package/dist/ghidra/GhidraProvider.js +8 -1
- package/dist/hopper/BridgeLauncher.js +56 -14
- package/dist/hopper/HopperClient.js +23 -10
- package/dist/hopper/HopperDiagnostics.js +14 -0
- package/dist/hopper/HopperProcessDiagnostic.js +14 -0
- package/dist/hopper/HopperProvider.js +8 -1
- package/dist/hopper/LinuxPrivateDisplayDiagnostic.js +72 -0
- package/dist/hopper/LinuxPrivateDisplayProbe.js +217 -0
- package/dist/main/transport.js +2 -0
- package/dist/mcpDoctor.js +310 -0
- package/dist/process/ProcessOwnership.js +200 -53
- package/dist/process/ProviderRunLineage.js +21 -0
- package/dist/server/createServer.js +4 -1
- package/dist/server/javascriptApplicationResult.js +22 -1
- package/dist/server/registerApplicationTools/characterization.js +3 -3
- package/dist/server/registerApplicationTools/compareExportShapes.js +37 -0
- package/dist/server/registerApplicationTools/compareVersions.js +12 -6
- package/dist/server/registerApplicationTools/controlledReplay.js +2 -2
- package/dist/server/registerApplicationTools/coverage.js +3 -3
- package/dist/server/registerApplicationTools/helpers.js +37 -25
- package/dist/server/registerApplicationTools/traceFeature.js +9 -5
- package/dist/server/registerApplicationTools/traceSemantics.js +30 -0
- package/dist/server/registerApplicationTools.js +4 -0
- package/dist/server/registerEvidenceResources.js +5 -1
- package/dist/server/registerProcessComparisonTool.js +17 -7
- package/dist/server/registerReplayMachineTool.js +20 -0
- package/dist/server/registerSessionTools.js +12 -7
- package/dist/server/sessionAvailabilityPolicy.js +2 -1
- package/dist/serverIdentity.js +19 -0
- package/package.json +62 -30
- package/scripts/hopper-demo-x11.py +459 -61
- package/scripts/prepack.mjs +55 -0
- package/scripts/prepare.mjs +28 -0
- package/scripts/rea.mjs +26 -5
- package/skills/reverse-engineer-anything/SKILL.md +2 -2
- package/skills/reverse-engineer-anything/references/javascript-applications.md +10 -0
|
@@ -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__(
|
|
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("--
|
|
73
|
-
parser.add_argument("--
|
|
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
|
|
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
|
-
|
|
195
|
-
|
|
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
|
-
|
|
524
|
+
authorize_display(authority, display_name, args.xauth)
|
|
200
525
|
try:
|
|
201
|
-
subprocess.
|
|
526
|
+
process = subprocess.Popen(
|
|
202
527
|
[
|
|
203
|
-
|
|
204
|
-
display_name,
|
|
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.
|
|
539
|
+
stderr=subprocess.PIPE,
|
|
209
540
|
)
|
|
210
|
-
except
|
|
541
|
+
except FileNotFoundError as error:
|
|
211
542
|
raise AdapterFailure(
|
|
212
|
-
|
|
543
|
+
RUNTIME_DEPENDENCY_UNAVAILABLE,
|
|
544
|
+
"missing_xvfb",
|
|
545
|
+
"Xvfb executable unavailable",
|
|
213
546
|
) from error
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
"
|
|
220
|
-
|
|
221
|
-
|
|
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.
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
|
243
|
-
|
|
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
|
-
|
|
247
|
-
|
|
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(
|
|
251
|
-
|
|
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,
|
|
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(
|
|
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,
|
|
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,
|
|
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,
|
|
687
|
+
PROCESS_OWNERSHIP_MISMATCH,
|
|
688
|
+
"process_ownership_mismatch",
|
|
689
|
+
"Hopper process ownership mismatch",
|
|
313
690
|
)
|
|
314
691
|
raise AdapterFailure(
|
|
315
|
-
UNSUPPORTED_DEMO_DIALOG,
|
|
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(
|
|
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
|
-
|
|
336
|
-
|
|
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
|
-
|
|
724
|
+
def main() -> int:
|
|
725
|
+
args = parse_args()
|
|
345
726
|
try:
|
|
346
|
-
|
|
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
|
-
|
|
349
|
-
|
|
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
|
+
}
|