rea-agents 1.6.0 → 2.0.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 +157 -66
- package/bridge/ghidra/ReaGhidraBridge.java +2075 -0
- package/bridge/hopper_bridge.py +13 -3
- package/dist/application/AbortablePromise.js +25 -0
- package/dist/application/AnalysisProvider.js +3 -0
- package/dist/application/AnalysisProviderEvaluation.js +60 -0
- package/dist/application/AnalysisProviderRegistry.js +228 -0
- package/dist/application/AnalysisSnapshotCache.js +41 -21
- package/dist/application/AnalysisSnapshotFiles.js +5 -6
- package/dist/application/BinarySession.js +218 -101
- package/dist/application/CapabilityInventory.js +18 -0
- package/dist/application/CompositeProvider.js +20 -8
- package/dist/application/CrossVersionInvestigation.js +10 -6
- package/dist/application/DirectAnalysis.js +49 -35
- package/dist/application/Doctor.js +110 -8
- package/dist/application/ElectronBoundaryAnalysis.js +96 -0
- package/dist/application/ElectronBoundaryGraph.js +9 -0
- package/dist/application/ElectronBoundaryGraphContext.js +62 -0
- package/dist/application/ElectronBoundaryGraphIpc.js +242 -0
- package/dist/application/ElectronBoundaryGraphNative.js +86 -0
- package/dist/application/ElectronBoundaryGraphWindows.js +199 -0
- package/dist/application/EnhancedTools.js +39 -14
- package/dist/application/InvestigationProviders.js +41 -0
- package/dist/application/JavaScriptApplicationEvidence.js +26 -0
- package/dist/application/JavaScriptApplicationEvidenceGraph.js +89 -0
- package/dist/application/JavaScriptApplicationService.js +74 -0
- package/dist/application/JavaScriptApplicationWorkflowEvidence.js +29 -0
- package/dist/application/JavaScriptApplicationWorkflowService.js +90 -0
- package/dist/application/JavaScriptArtifactAnalysis.js +305 -0
- package/dist/application/JavaScriptArtifactAnalysisTypes.js +1 -0
- package/dist/application/JavaScriptArtifactFiles.js +231 -0
- package/dist/application/JavaScriptArtifactGraphAccumulator.js +54 -0
- package/dist/application/JavaScriptArtifactGraphBuilder.js +144 -0
- package/dist/application/JavaScriptArtifactGraphContext.js +205 -0
- package/dist/application/JavaScriptArtifactGraphDocuments.js +108 -0
- package/dist/application/JavaScriptArtifactGraphEvidence.js +99 -0
- package/dist/application/JavaScriptArtifactGraphFindings.js +271 -0
- package/dist/application/JavaScriptArtifactGraphStructure.js +409 -0
- package/dist/application/JavaScriptArtifactReconstruction.js +77 -0
- package/dist/application/JavaScriptArtifactReconstructionInput.js +18 -0
- package/dist/application/JavaScriptReplayPlanning.js +201 -0
- package/dist/application/JavaScriptReplayService.js +266 -0
- package/dist/application/JavaScriptRuntimeReconciliationEvidence.js +25 -0
- package/dist/application/JavaScriptRuntimeReconciliationService.js +27 -0
- package/dist/application/MacHopper.js +20 -4
- package/dist/application/ManagedApplicationGraphService.js +55 -0
- package/dist/application/ManagedMemberComparisonService.js +122 -0
- package/dist/application/ManagedNativeVerificationService.js +39 -0
- package/dist/application/ManagedReconstructionService.js +58 -0
- package/dist/application/ManagedRuntimeCorrelationService.js +103 -0
- package/dist/application/PermissionAuthority.js +1 -1
- package/dist/application/ProcessCaptureLifecycle.js +1 -1
- package/dist/application/ProcessHarness.js +1 -1
- package/dist/application/ProcessSampling.js +16 -12
- package/dist/application/ProjectPermissionStore.js +1 -0
- package/dist/application/SessionProviderRouter.js +219 -0
- package/dist/application/Setup.js +84 -60
- package/dist/application/SetupClients.js +1 -1
- package/dist/application/SetupPlan.js +9 -2
- package/dist/application/SupportedClients.js +40 -16
- package/dist/application/Upgrade.js +1 -1
- package/dist/application/runtime.js +11 -6
- package/dist/artifacts/AsarArtifactReader.js +23 -3
- package/dist/browser/CdpBrowserProvider.js +4 -2
- package/dist/browser/CdpCaptureEvents.js +49 -0
- package/dist/browser/CdpElectronInspection.js +106 -162
- package/dist/browser/CdpElectronProvider.js +9 -3
- package/dist/browser/CdpElectronScriptEvents.js +69 -0
- package/dist/browser/CdpElectronScripts.js +94 -0
- package/dist/browser/CdpElectronWorkers.js +56 -0
- package/dist/browser/CdpEndpoint.js +14 -1
- package/dist/browser/CdpPageCapture.js +8 -4
- package/dist/catalogIdentity.js +6 -42
- package/dist/cli.js +446 -34
- package/dist/cliApplicationCommands.js +62 -0
- package/dist/cliBrowserAdvancedCommands.js +5 -4
- package/dist/cliBrowserCommands.js +5 -4
- package/dist/cliCommandNames.js +56 -0
- package/dist/cliElectronCommands.js +106 -3
- package/dist/cliEvidenceCommands.js +4 -3
- package/dist/cliInvestigationCommands.js +2 -1
- package/dist/cliJsonInput.js +54 -0
- package/dist/cliPolicyCommands.js +2 -1
- package/dist/cliProcessCommands.js +3 -2
- package/dist/config.js +101 -2
- package/dist/contracts/applicationToolContracts.js +91 -0
- package/dist/contracts/artifactToolContracts.js +5 -9
- package/dist/contracts/browserToolContracts.js +14 -62
- package/dist/contracts/electronToolContracts.js +45 -19
- package/dist/contracts/enhancedInputs.js +2 -2
- package/dist/contracts/errorSchemas.js +1 -0
- package/dist/contracts/investigationExamples.js +0 -6
- package/dist/contracts/javascriptApplicationWorkflowExamples.js +46 -0
- package/dist/contracts/javascriptRuntimeReconciliationExample.js +208 -0
- package/dist/contracts/managedToolContracts.js +153 -0
- package/dist/contracts/managedWorkflowExamples.js +346 -0
- package/dist/contracts/managedWorkflowToolContracts.js +203 -0
- package/dist/contracts/nativeToolContracts.js +2 -6
- package/dist/contracts/promptContracts.js +18 -2
- package/dist/contracts/providerSelection.js +28 -0
- package/dist/contracts/sessionLifecycleInputs.js +2 -0
- package/dist/contracts/toolContractExamples.js +10 -25
- package/dist/contracts/toolContracts.js +69 -65
- package/dist/contracts/toolEffects.js +160 -0
- package/dist/contracts/toolOutputSchemas.js +152 -42
- package/dist/doctorRuntime.js +47 -0
- package/dist/domain/analysisProfile.js +78 -0
- package/dist/domain/analysisSnapshot.js +92 -46
- package/dist/domain/artifactComparison.js +3 -4
- package/dist/domain/artifactGraph.js +1 -0
- package/dist/domain/binaryTarget.js +4 -29
- package/dist/domain/browserObservation.js +3 -0
- package/dist/domain/bundleComparison.js +4 -4
- package/dist/domain/electronObservation.js +13 -0
- package/dist/domain/electronStaticAnalysis.js +9 -0
- package/dist/domain/electronStaticAnalysisBrowser.js +194 -0
- package/dist/domain/electronStaticAnalysisIpc.js +171 -0
- package/dist/domain/electronStaticAnalysisNative.js +164 -0
- package/dist/domain/electronStaticAnalysisTypes.js +1 -0
- package/dist/domain/electronStaticAnalysisValues.js +103 -0
- package/dist/domain/errors.js +148 -7
- package/dist/domain/evidence.js +44 -5
- package/dist/domain/evidenceBundle.js +9 -7
- package/dist/domain/functionComparison.js +9 -3
- package/dist/domain/functionComparisonNormalization.js +37 -0
- package/dist/domain/functionComparisonSchemas.js +3 -8
- package/dist/domain/functionDossierEvidence.js +4 -1
- package/dist/domain/hopperValues.js +66 -9
- package/dist/domain/inputIssueProjection.js +52 -0
- package/dist/domain/investigationWorkspace.js +2 -1
- package/dist/domain/javascriptApplicationAnalysis.js +155 -0
- package/dist/domain/javascriptApplicationChangeGraph.js +231 -0
- package/dist/domain/javascriptApplicationEvidenceSchemas.js +379 -0
- package/dist/domain/javascriptApplicationGraph.js +282 -0
- package/dist/domain/javascriptApplicationGraphSchemas.js +139 -0
- package/dist/domain/javascriptApplicationVersionComparison.js +116 -0
- package/dist/domain/javascriptApplicationVersionComparisonSchemas.js +139 -0
- package/dist/domain/javascriptApplicationVersionItems.js +221 -0
- package/dist/domain/javascriptApplicationVersionKeys.js +170 -0
- package/dist/domain/javascriptAstFingerprint.js +143 -0
- package/dist/domain/javascriptFeatureSeed.js +140 -0
- package/dist/domain/javascriptFeatureTrace.js +232 -0
- package/dist/domain/javascriptFeatureTraceSchemas.js +136 -0
- package/dist/domain/javascriptFeatureTraversal.js +88 -0
- package/dist/domain/javascriptNativeHandoff.js +85 -0
- package/dist/domain/javascriptReplay.js +361 -0
- package/dist/domain/javascriptRuntimeLoadState.js +106 -0
- package/dist/domain/javascriptRuntimeReconciliation.js +42 -0
- package/dist/domain/javascriptRuntimeReconciliationGraph.js +169 -0
- package/dist/domain/javascriptRuntimeReconciliationMatching.js +247 -0
- package/dist/domain/javascriptRuntimeReconciliationParsing.js +160 -0
- package/dist/domain/javascriptRuntimeReconciliationResult.js +119 -0
- package/dist/domain/javascriptRuntimeReconciliationRuntime.js +316 -0
- package/dist/domain/javascriptRuntimeReconciliationSchemas.js +245 -0
- package/dist/domain/javascriptRuntimeStaticCandidates.js +191 -0
- package/dist/domain/javascriptStaticAnalysis.js +346 -0
- package/dist/domain/javascriptStaticAnalysisFindings.js +39 -0
- package/dist/domain/javascriptStaticAnalysisHelpers.js +274 -0
- package/dist/domain/javascriptStaticAnalysisState.js +23 -0
- package/dist/domain/javascriptStaticAnalysisTypes.js +1 -0
- package/dist/domain/managedApplicationGraph.js +556 -0
- package/dist/domain/managedArtifact.js +444 -0
- package/dist/domain/managedMemberComparison.js +565 -0
- package/dist/domain/managedNativeVerification.js +408 -0
- package/dist/domain/managedReconstruction.js +183 -0
- package/dist/domain/managedRuntimeCorrelation.js +175 -0
- package/dist/domain/reconstructionVerificationSchemas.js +1 -3
- package/dist/dotnet/ManagedArtifactInspector.js +318 -0
- package/dist/dotnet/ManagedMemberInspector.js +887 -0
- package/dist/dotnet/ManagedMetadataHeaps.js +131 -0
- package/dist/dotnet/ManagedMetadataInventory.js +368 -0
- package/dist/dotnet/ManagedMetadataLayout.js +322 -0
- package/dist/dotnet/ManagedNativeBoundaryInspector.js +391 -0
- package/dist/dotnet/ManagedPeReader.js +157 -0
- package/dist/dotnet/ManagedReaderFailure.js +10 -0
- package/dist/dotnet/ManagedStaticProvider.js +203 -0
- package/dist/generatedPackageMetadata.js +2 -2
- package/dist/ghidra/GhidraAnalysisProfile.js +37 -0
- package/dist/ghidra/GhidraClient.js +400 -0
- package/dist/ghidra/GhidraClientTypes.js +1 -0
- package/dist/ghidra/GhidraDefaults.js +20 -0
- package/dist/ghidra/GhidraDiagnostics.js +43 -0
- package/dist/ghidra/GhidraDoctor.js +59 -0
- package/dist/ghidra/GhidraFunctionValues.js +264 -0
- package/dist/ghidra/GhidraInstallation.js +239 -0
- package/dist/ghidra/GhidraInventoryValues.js +217 -0
- package/dist/ghidra/GhidraLauncher.js +139 -0
- package/dist/ghidra/GhidraProvider.js +366 -0
- package/dist/ghidra/GhidraRequestQueue.js +98 -0
- package/dist/ghidra/GhidraResponseBuffer.js +31 -0
- package/dist/ghidra/GhidraResponseRouter.js +24 -0
- package/dist/ghidra/GhidraSessionError.js +39 -0
- package/dist/ghidra/GhidraSessionValues.js +62 -0
- package/dist/ghidra/GhidraSocketConnection.js +46 -0
- package/dist/ghidra/protocol.js +69 -0
- package/dist/hopper/BridgeLauncher.js +19 -54
- package/dist/hopper/HopperAnalysisProfile.js +106 -0
- package/dist/hopper/HopperClient.js +154 -162
- package/dist/hopper/HopperProvider.js +99 -7
- package/dist/hopper/HopperSessionValues.js +27 -0
- package/dist/hopper/HopperSocketConnection.js +31 -0
- package/dist/main.js +14 -0
- package/dist/native/NativeMacOSProvider.js +3 -1
- package/dist/process/PendingOperations.js +59 -0
- package/dist/process/PrivateRuntimeRoot.js +33 -0
- package/dist/process/ProviderDeadline.js +95 -0
- package/dist/process/ProviderProcess.js +275 -0
- package/dist/replay/JavaScriptReplayWorker.js +286 -0
- package/dist/replay/LinuxJavaScriptReplayRunner.js +434 -0
- package/dist/replay/LinuxRuntimeClosure.js +46 -0
- package/dist/replay/LinuxSeccompPolicy.js +40 -0
- package/dist/replay/ReplayWorkerProtocol.js +55 -0
- package/dist/replay/SystemJavaScriptReplayHost.js +182 -0
- package/dist/server/createServer.js +53 -0
- package/dist/server/promptCompletion.js +9 -3
- package/dist/server/registerApplicationTools.js +115 -0
- package/dist/server/registerArtifactComparisonTool.js +16 -9
- package/dist/server/registerBrowserTools.js +33 -8
- package/dist/server/registerBundleComparisonTool.js +17 -20
- package/dist/server/registerElectronTools.js +45 -3
- package/dist/server/registerEnhancedTools.js +67 -19
- package/dist/server/registerEvidenceTools.js +5 -1
- package/dist/server/registerFunctionComparisonTool.js +14 -7
- package/dist/server/registerInvestigationTools.js +21 -17
- package/dist/server/registerManagedTools.js +6 -0
- package/dist/server/registerManagedWorkflowTools.js +277 -0
- package/dist/server/registerOfficialTools.js +11 -8
- package/dist/server/registerProcessComparisonTool.js +25 -47
- package/dist/server/registerSessionStatusTool.js +7 -7
- package/dist/server/registerSessionTools.js +57 -87
- package/dist/server/sessionEvidence.js +14 -19
- package/dist/server/toolInputValidation.js +77 -0
- package/dist/server/toolRegistrationOptions.js +79 -2
- package/dist/server/toolResult.js +25 -17
- package/package.json +17 -7
- package/scripts/rea.mjs +36 -2
- package/skills/rea-analysis/SKILL.md +59 -3
- /package/dist/{application → process}/ProcessOwnership.js +0 -0
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { randomBytes, randomUUID } from "node:crypto";
|
|
2
|
-
import { access
|
|
3
|
-
import { createConnection } from "node:net";
|
|
2
|
+
import { access } from "node:fs/promises";
|
|
4
3
|
import { tmpdir } from "node:os";
|
|
5
4
|
import { join } from "node:path";
|
|
6
5
|
import { HopperCancelledError, HopperProcessError, HopperProtocolError, HopperStartError, hopperStartupFailure, HopperTimeoutError, } from "../domain/errors.js";
|
|
7
6
|
import { err, ok } from "../domain/result.js";
|
|
8
7
|
import { silentLogger } from "../logger.js";
|
|
8
|
+
import { PendingOperations } from "../process/PendingOperations.js";
|
|
9
|
+
import { PrivateRuntimeRoot } from "../process/PrivateRuntimeRoot.js";
|
|
10
|
+
import { ProviderStartupDeadline } from "../process/ProviderDeadline.js";
|
|
11
|
+
import { ProviderProcessSupervisor, } from "../process/ProviderProcess.js";
|
|
12
|
+
import { isHopperCleanupRequired, isHopperShutdownAcknowledgement, parseHopperServerInfo, } from "./HopperSessionValues.js";
|
|
13
|
+
import { connectHopperSocketOnce } from "./HopperSocketConnection.js";
|
|
9
14
|
import { parseResponseLine, responseResult } from "./protocol.js";
|
|
10
15
|
const MAX_LINE_BYTES = 10 * 1024 * 1024;
|
|
11
16
|
const SHUTDOWN_TIMEOUT_MS = 30_000;
|
|
@@ -20,11 +25,12 @@ const SESSION_ROOT = process.platform === "darwin" ? "/tmp" : tmpdir();
|
|
|
20
25
|
*/
|
|
21
26
|
export class HopperClient {
|
|
22
27
|
#options;
|
|
23
|
-
#pending = new
|
|
28
|
+
#pending = new PendingOperations();
|
|
24
29
|
#logger;
|
|
25
30
|
#socket;
|
|
26
31
|
#launch;
|
|
27
|
-
#
|
|
32
|
+
#process;
|
|
33
|
+
#runtimeRoot;
|
|
28
34
|
#token;
|
|
29
35
|
#nextId = 1;
|
|
30
36
|
#buffer = "";
|
|
@@ -33,6 +39,16 @@ export class HopperClient {
|
|
|
33
39
|
#startupController;
|
|
34
40
|
#startPromise;
|
|
35
41
|
#closePromise;
|
|
42
|
+
#onSocketData = (chunk) => {
|
|
43
|
+
this.#onData(chunk);
|
|
44
|
+
};
|
|
45
|
+
#onSocketError = () => {
|
|
46
|
+
this.#failAll(new HopperProcessError(null));
|
|
47
|
+
};
|
|
48
|
+
#onSocketClose = () => {
|
|
49
|
+
if (!this.#closing)
|
|
50
|
+
this.#failAll(new HopperProcessError(null));
|
|
51
|
+
};
|
|
36
52
|
constructor(options) {
|
|
37
53
|
this.#logger = options.logger ?? silentLogger;
|
|
38
54
|
this.#options = {
|
|
@@ -49,68 +65,104 @@ export class HopperClient {
|
|
|
49
65
|
if (this.#startPromise !== undefined)
|
|
50
66
|
return this.#startPromise;
|
|
51
67
|
const controller = new AbortController();
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
68
|
+
const onAbort = () => controller.abort(signal?.reason);
|
|
69
|
+
if (signal?.aborted === true)
|
|
70
|
+
onAbort();
|
|
71
|
+
else
|
|
72
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
73
|
+
const started = this.#start(controller.signal);
|
|
56
74
|
this.#startupController = controller;
|
|
57
75
|
this.#startPromise = started;
|
|
58
|
-
|
|
59
|
-
|
|
76
|
+
const release = () => {
|
|
77
|
+
signal?.removeEventListener("abort", onAbort);
|
|
78
|
+
};
|
|
79
|
+
const reset = () => {
|
|
80
|
+
if (this.#startPromise === started) {
|
|
60
81
|
this.#startPromise = undefined;
|
|
61
82
|
if (this.#startupController === controller) {
|
|
62
83
|
this.#startupController = undefined;
|
|
63
84
|
}
|
|
64
85
|
}
|
|
86
|
+
};
|
|
87
|
+
void started.then((result) => {
|
|
88
|
+
release();
|
|
89
|
+
if (!result.ok)
|
|
90
|
+
reset();
|
|
91
|
+
}, () => {
|
|
92
|
+
release();
|
|
93
|
+
reset();
|
|
65
94
|
});
|
|
66
95
|
return started;
|
|
67
96
|
}
|
|
68
97
|
async #start(signal) {
|
|
69
98
|
if (isAborted(signal))
|
|
70
99
|
return err(new HopperCancelledError());
|
|
71
|
-
if (this.#socket !== undefined || this.#
|
|
100
|
+
if (this.#socket !== undefined || this.#runtimeRoot !== undefined) {
|
|
72
101
|
return err(new HopperProtocolError("Hopper client is already started"));
|
|
73
102
|
}
|
|
103
|
+
const deadline = new ProviderStartupDeadline(this.#options.startupTimeoutMs, signal);
|
|
74
104
|
try {
|
|
75
|
-
|
|
105
|
+
return await this.#startWithin(deadline);
|
|
106
|
+
}
|
|
107
|
+
finally {
|
|
108
|
+
deadline.dispose();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
async #startWithin(deadline) {
|
|
112
|
+
try {
|
|
113
|
+
this.#runtimeRoot = await PrivateRuntimeRoot.create({
|
|
114
|
+
parent: SESSION_ROOT,
|
|
115
|
+
prefix: "rea-",
|
|
116
|
+
});
|
|
76
117
|
}
|
|
77
118
|
catch (cause) {
|
|
78
119
|
return err(new HopperStartError({ cause }));
|
|
79
120
|
}
|
|
80
|
-
if (
|
|
121
|
+
if (deadline.signal.aborted) {
|
|
81
122
|
await this.#cleanup();
|
|
82
|
-
return err(
|
|
123
|
+
return err(startupInterruption(deadline));
|
|
83
124
|
}
|
|
84
|
-
const socketPath = join(this.#
|
|
125
|
+
const socketPath = join(this.#runtimeRoot.path, "bridge.sock");
|
|
85
126
|
this.#token = randomBytes(32).toString("hex");
|
|
86
127
|
this.#launcherExitCode = undefined;
|
|
87
128
|
const runId = randomUUID();
|
|
88
|
-
const launched = await this.#options.launcher
|
|
89
|
-
|
|
129
|
+
const launched = await this.#options.launcher
|
|
130
|
+
.launch({
|
|
131
|
+
directory: this.#runtimeRoot.path,
|
|
90
132
|
socketPath,
|
|
91
133
|
token: this.#token,
|
|
92
134
|
runId,
|
|
93
|
-
},
|
|
135
|
+
}, { signal: deadline.signal })
|
|
136
|
+
.catch((cause) => err(new HopperStartError({ cause })));
|
|
94
137
|
if (!launched.ok) {
|
|
95
138
|
await this.#cleanup();
|
|
96
|
-
return
|
|
139
|
+
return deadline.signal.aborted
|
|
140
|
+
? err(startupInterruption(deadline))
|
|
141
|
+
: launched;
|
|
97
142
|
}
|
|
98
143
|
this.#launch = launched.value;
|
|
99
144
|
this.#attachLauncher(launched.value);
|
|
100
|
-
const connected = await this.#connect(socketPath,
|
|
145
|
+
const connected = await this.#connect(socketPath, deadline);
|
|
101
146
|
if (!connected.ok) {
|
|
102
147
|
await this.#cleanup();
|
|
103
148
|
return connected;
|
|
104
149
|
}
|
|
150
|
+
const remainingMs = deadline.remainingMs();
|
|
151
|
+
if (remainingMs <= 0) {
|
|
152
|
+
await this.#cleanup();
|
|
153
|
+
return err(new HopperTimeoutError(this.#options.startupTimeoutMs));
|
|
154
|
+
}
|
|
105
155
|
const health = await this.#request("health", {}, {
|
|
106
|
-
timeoutMs:
|
|
107
|
-
|
|
156
|
+
timeoutMs: remainingMs,
|
|
157
|
+
signal: deadline.signal,
|
|
108
158
|
});
|
|
109
159
|
if (!health.ok) {
|
|
110
160
|
await this.#cleanup();
|
|
111
|
-
return
|
|
161
|
+
return deadline.signal.aborted
|
|
162
|
+
? err(startupInterruption(deadline))
|
|
163
|
+
: health;
|
|
112
164
|
}
|
|
113
|
-
const parsed =
|
|
165
|
+
const parsed = parseHopperServerInfo(health.value, runId);
|
|
114
166
|
if (!parsed.ok)
|
|
115
167
|
await this.#cleanup();
|
|
116
168
|
return parsed;
|
|
@@ -148,43 +200,45 @@ export class HopperClient {
|
|
|
148
200
|
this.#closing = true;
|
|
149
201
|
try {
|
|
150
202
|
const socket = this.#socket;
|
|
151
|
-
let
|
|
203
|
+
let cleanupResult;
|
|
152
204
|
if (socket !== undefined && !socket.destroyed) {
|
|
153
205
|
const shutdown = await this.#request("shutdown", {}, { timeoutMs: SHUTDOWN_TIMEOUT_MS }).catch(() => err(new HopperProcessError(null)));
|
|
154
206
|
if (shutdown.ok &&
|
|
155
|
-
|
|
207
|
+
isHopperCleanupRequired(shutdown.value) &&
|
|
156
208
|
this.#launch?.shutdownByCleanup === true &&
|
|
157
209
|
this.#launch.cleanup !== undefined) {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
210
|
+
cleanupResult = await this.#launch
|
|
211
|
+
.cleanup()
|
|
212
|
+
.catch(providerCleanupFailure);
|
|
213
|
+
if (!cleanupResult.cleaned) {
|
|
161
214
|
await this.#fallbackDocumentShutdown();
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
215
|
+
cleanupResult = await this.#launch
|
|
216
|
+
.cleanup()
|
|
217
|
+
.catch(providerCleanupFailure);
|
|
165
218
|
}
|
|
166
219
|
}
|
|
167
|
-
else if (!shutdown.ok ||
|
|
220
|
+
else if (!shutdown.ok ||
|
|
221
|
+
!isHopperShutdownAcknowledgement(shutdown.value))
|
|
168
222
|
this.#logger.warn({ status: shutdown.ok ? "invalid-acknowledgement" : "failed" }, "Hopper document shutdown was not confirmed");
|
|
169
223
|
}
|
|
170
224
|
this.#failAll(new HopperProcessError(null));
|
|
225
|
+
if (socket !== undefined)
|
|
226
|
+
this.#detachSocket(socket);
|
|
171
227
|
socket?.destroy();
|
|
172
228
|
this.#socket = undefined;
|
|
173
|
-
if (this.#
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
this.#logger.warn({ reason: cleanup.reason }, "Owned launcher cleanup failed closed");
|
|
178
|
-
}
|
|
179
|
-
else
|
|
180
|
-
this.#launch.process.kill("SIGTERM");
|
|
229
|
+
if (this.#process !== undefined) {
|
|
230
|
+
const stopped = await this.#process.stop(cleanupResult === undefined ? {} : { cleanupResult });
|
|
231
|
+
if (stopped.status === "incomplete")
|
|
232
|
+
this.#logger.warn({ reason: stopped.reason }, "Owned launcher cleanup failed closed");
|
|
181
233
|
}
|
|
234
|
+
this.#process = undefined;
|
|
182
235
|
this.#launch = undefined;
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
}
|
|
236
|
+
const runtimeRoot = this.#runtimeRoot;
|
|
237
|
+
this.#runtimeRoot = undefined;
|
|
238
|
+
await runtimeRoot?.close();
|
|
187
239
|
this.#token = undefined;
|
|
240
|
+
this.#buffer = "";
|
|
241
|
+
this.#launcherExitCode = undefined;
|
|
188
242
|
}
|
|
189
243
|
finally {
|
|
190
244
|
this.#closing = false;
|
|
@@ -192,14 +246,13 @@ export class HopperClient {
|
|
|
192
246
|
}
|
|
193
247
|
async #fallbackDocumentShutdown() {
|
|
194
248
|
const fallback = await this.#request("shutdown_document", {}, { timeoutMs: SHUTDOWN_TIMEOUT_MS }).catch(() => err(new HopperProcessError(null)));
|
|
195
|
-
if (!fallback.ok || !
|
|
249
|
+
if (!fallback.ok || !isHopperShutdownAcknowledgement(fallback.value))
|
|
196
250
|
this.#logger.warn({ status: fallback.ok ? "invalid-acknowledgement" : "failed" }, "Hopper document shutdown fallback was not confirmed");
|
|
197
251
|
}
|
|
198
|
-
async #connect(socketPath,
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
return err(new HopperCancelledError());
|
|
252
|
+
async #connect(socketPath, deadline) {
|
|
253
|
+
while (deadline.remainingMs() > 0) {
|
|
254
|
+
if (deadline.signal.aborted)
|
|
255
|
+
return err(startupInterruption(deadline));
|
|
203
256
|
if (this.#closing)
|
|
204
257
|
return err(new HopperProcessError(null));
|
|
205
258
|
if (this.#launcherExitCode !== undefined &&
|
|
@@ -209,18 +262,18 @@ export class HopperClient {
|
|
|
209
262
|
await access(socketPath);
|
|
210
263
|
}
|
|
211
264
|
catch {
|
|
212
|
-
if (
|
|
213
|
-
return err(
|
|
265
|
+
if ((await deadline.wait(50)) === "aborted")
|
|
266
|
+
return err(startupInterruption(deadline));
|
|
214
267
|
continue;
|
|
215
268
|
}
|
|
216
|
-
const attempt = await
|
|
269
|
+
const attempt = await connectHopperSocketOnce(socketPath, deadline.signal);
|
|
217
270
|
if (attempt.ok) {
|
|
218
271
|
this.#socket = attempt.value;
|
|
219
272
|
this.#attachSocket(attempt.value);
|
|
220
273
|
return ok(undefined);
|
|
221
274
|
}
|
|
222
|
-
if (
|
|
223
|
-
return err(
|
|
275
|
+
if ((await deadline.wait(50)) === "aborted")
|
|
276
|
+
return err(startupInterruption(deadline));
|
|
224
277
|
}
|
|
225
278
|
return err(new HopperTimeoutError(this.#options.startupTimeoutMs));
|
|
226
279
|
}
|
|
@@ -235,28 +288,15 @@ export class HopperClient {
|
|
|
235
288
|
const id = this.#nextId++;
|
|
236
289
|
const startedAt = performance.now();
|
|
237
290
|
const timeoutMs = options.timeoutMs ?? this.#options.requestTimeoutMs;
|
|
238
|
-
const response =
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
};
|
|
244
|
-
const timer = setTimeout(() => {
|
|
245
|
-
this.#abandon(id, err(new HopperTimeoutError(timeoutMs)));
|
|
246
|
-
}, timeoutMs);
|
|
247
|
-
this.#pending.set(id, {
|
|
248
|
-
resolve,
|
|
249
|
-
timer,
|
|
250
|
-
signal: options.signal,
|
|
251
|
-
onAbort,
|
|
252
|
-
});
|
|
253
|
-
options.signal?.addEventListener("abort", onAbort ?? (() => undefined), {
|
|
254
|
-
once: true,
|
|
255
|
-
});
|
|
291
|
+
const response = this.#pending.wait(id, {
|
|
292
|
+
timeoutMs,
|
|
293
|
+
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
294
|
+
timeoutValue: () => err(new HopperTimeoutError(timeoutMs)),
|
|
295
|
+
cancelledValue: () => err(new HopperCancelledError()),
|
|
256
296
|
});
|
|
257
297
|
socket.write(`${JSON.stringify({ id, token, method, params })}\n`, (cause) => {
|
|
258
298
|
if (cause !== undefined && cause !== null)
|
|
259
|
-
this.#settle(id, err(new HopperProcessError(null)));
|
|
299
|
+
this.#pending.settle(id, err(new HopperProcessError(null)));
|
|
260
300
|
});
|
|
261
301
|
const result = await response;
|
|
262
302
|
this.#logger[result.ok ? "debug" : "warn"]({
|
|
@@ -269,31 +309,41 @@ export class HopperClient {
|
|
|
269
309
|
}
|
|
270
310
|
#attachSocket(socket) {
|
|
271
311
|
socket.setEncoding("utf8");
|
|
272
|
-
socket.on("data",
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
socket.
|
|
279
|
-
|
|
280
|
-
this.#failAll(new HopperProcessError(null));
|
|
281
|
-
});
|
|
312
|
+
socket.on("data", this.#onSocketData);
|
|
313
|
+
socket.on("error", this.#onSocketError);
|
|
314
|
+
socket.on("close", this.#onSocketClose);
|
|
315
|
+
}
|
|
316
|
+
#detachSocket(socket) {
|
|
317
|
+
socket.off("data", this.#onSocketData);
|
|
318
|
+
socket.off("error", this.#onSocketError);
|
|
319
|
+
socket.off("close", this.#onSocketClose);
|
|
282
320
|
}
|
|
283
321
|
#attachLauncher(launch) {
|
|
284
|
-
|
|
322
|
+
this.#process = new ProviderProcessSupervisor(launch, {
|
|
323
|
+
onDiagnostic: (event) => this.#onLauncherDiagnostic(launch, event),
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
#onLauncherDiagnostic(launch, event) {
|
|
327
|
+
if (event.type === "output" && event.stream === "stderr") {
|
|
285
328
|
this.#options.onDiagnostic?.({
|
|
286
329
|
type: "launcher-stderr",
|
|
287
|
-
bytes:
|
|
330
|
+
bytes: event.bytes,
|
|
288
331
|
});
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
if (event.type === "error") {
|
|
335
|
+
this.#logger.warn({ message: event.message }, "Hopper launcher process emitted an error");
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
if (event.type !== "exit")
|
|
339
|
+
return;
|
|
340
|
+
this.#launcherExitCode = event.code;
|
|
341
|
+
this.#options.onDiagnostic?.({
|
|
342
|
+
type: "launcher-exit",
|
|
343
|
+
code: event.code,
|
|
289
344
|
});
|
|
290
|
-
launch.
|
|
291
|
-
this.#
|
|
292
|
-
this.#options.onDiagnostic?.({ type: "launcher-exit", code });
|
|
293
|
-
if (launch.ownsProcessLifetime && !this.#closing) {
|
|
294
|
-
this.#failAll(new HopperProcessError(code));
|
|
295
|
-
}
|
|
296
|
-
});
|
|
345
|
+
if (launch.ownsProcessLifetime && !this.#closing)
|
|
346
|
+
this.#failAll(new HopperProcessError(event.code));
|
|
297
347
|
}
|
|
298
348
|
#onData(chunk) {
|
|
299
349
|
this.#buffer += chunk;
|
|
@@ -325,81 +375,23 @@ export class HopperClient {
|
|
|
325
375
|
}
|
|
326
376
|
return;
|
|
327
377
|
}
|
|
328
|
-
this.#settle(parsed.value.id, responseResult(parsed.value));
|
|
378
|
+
this.#pending.settle(parsed.value.id, responseResult(parsed.value));
|
|
329
379
|
}
|
|
330
380
|
#abortProtocol(message, cause) {
|
|
331
381
|
this.#failAll(new HopperProtocolError(message, { cause }));
|
|
332
382
|
this.#socket?.destroy();
|
|
333
383
|
}
|
|
334
|
-
#settle(id, result) {
|
|
335
|
-
const pending = this.#pending.get(id);
|
|
336
|
-
if (pending === undefined)
|
|
337
|
-
return;
|
|
338
|
-
this.#pending.delete(id);
|
|
339
|
-
clearTimeout(pending.timer);
|
|
340
|
-
if (pending.signal !== undefined && pending.onAbort !== undefined) {
|
|
341
|
-
pending.signal.removeEventListener("abort", pending.onAbort);
|
|
342
|
-
}
|
|
343
|
-
pending.resolve(result);
|
|
344
|
-
}
|
|
345
|
-
#abandon(id, result) {
|
|
346
|
-
if (!this.#pending.has(id))
|
|
347
|
-
return;
|
|
348
|
-
this.#settle(id, result);
|
|
349
|
-
}
|
|
350
384
|
#failAll(error) {
|
|
351
|
-
|
|
352
|
-
this.#settle(id, err(error));
|
|
385
|
+
this.#pending.failAll(() => err(error));
|
|
353
386
|
}
|
|
354
387
|
}
|
|
355
388
|
const isAborted = (signal) => signal?.aborted === true;
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
const onAbort = () => {
|
|
365
|
-
clearTimeout(timer);
|
|
366
|
-
resolve(false);
|
|
367
|
-
};
|
|
368
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
369
|
-
});
|
|
370
|
-
};
|
|
371
|
-
const connectOnce = async (socketPath) => new Promise((resolve) => {
|
|
372
|
-
const socket = createConnection(socketPath);
|
|
373
|
-
socket.once("connect", () => {
|
|
374
|
-
resolve(ok(socket));
|
|
375
|
-
});
|
|
376
|
-
socket.once("error", (cause) => {
|
|
377
|
-
socket.destroy();
|
|
378
|
-
resolve(err(new HopperStartError({ cause })));
|
|
379
|
-
});
|
|
389
|
+
const startupInterruption = (deadline) => deadline.cancelled
|
|
390
|
+
? new HopperCancelledError()
|
|
391
|
+
: 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",
|
|
380
397
|
});
|
|
381
|
-
const isShutdownAcknowledgement = (value) => {
|
|
382
|
-
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
383
|
-
return false;
|
|
384
|
-
if (value.shutdown !== true)
|
|
385
|
-
return false;
|
|
386
|
-
if (value.analysis_stopped === true && value.document_closed === true)
|
|
387
|
-
return true;
|
|
388
|
-
return false;
|
|
389
|
-
};
|
|
390
|
-
const isCleanupRequired = (value) => typeof value === "object" &&
|
|
391
|
-
value !== null &&
|
|
392
|
-
!Array.isArray(value) &&
|
|
393
|
-
value.shutdown === true &&
|
|
394
|
-
value.cleanup_required === true;
|
|
395
|
-
const parseServerInfo = (value, expectedRunId) => {
|
|
396
|
-
if (typeof value === "object" &&
|
|
397
|
-
value !== null &&
|
|
398
|
-
!Array.isArray(value) &&
|
|
399
|
-
value.name === "REA Hopper bridge" &&
|
|
400
|
-
typeof value.version === "string" &&
|
|
401
|
-
value.run_id === expectedRunId) {
|
|
402
|
-
return ok({ name: value.name, version: value.version });
|
|
403
|
-
}
|
|
404
|
-
return err(new HopperProtocolError("Hopper bridge health result is invalid"));
|
|
405
|
-
};
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import { fileURLToPath } from "node:url";
|
|
2
|
+
import { accessSync, constants } from "node:fs";
|
|
2
3
|
import { createAnalysisExecution } from "../application/AnalysisProvider.js";
|
|
3
4
|
import { AnalysisCapabilityUnavailableError } from "../domain/errors.js";
|
|
4
5
|
import { err } from "../domain/result.js";
|
|
5
6
|
import { ENHANCED_TOOL_CONTRACTS, OFFICIAL_TOOL_CONTRACTS, } from "../contracts/toolContracts.js";
|
|
6
7
|
import { HopperApplicationLauncher } from "./BridgeLauncher.js";
|
|
8
|
+
import { hopperLoaderArgsForTarget, resolveHopperAnalysisProfile, } from "./HopperAnalysisProfile.js";
|
|
7
9
|
import { HopperClient } from "./HopperClient.js";
|
|
8
|
-
|
|
10
|
+
/** Public identity committed by every Hopper-backed observation. */
|
|
11
|
+
export const HOPPER_PROVIDER_IDENTITY = Object.freeze({
|
|
9
12
|
id: "hopper",
|
|
10
13
|
name: "Hopper Disassembler",
|
|
11
14
|
version: null,
|
|
12
15
|
});
|
|
16
|
+
const IDENTITY = HOPPER_PROVIDER_IDENTITY;
|
|
13
17
|
const MUTATING_OPERATIONS = new Set([
|
|
14
18
|
"set_address_name",
|
|
15
19
|
"set_addresses_names",
|
|
@@ -18,11 +22,12 @@ const MUTATING_OPERATIONS = new Set([
|
|
|
18
22
|
"set_inline_comment",
|
|
19
23
|
"unset_bookmark",
|
|
20
24
|
]);
|
|
21
|
-
|
|
25
|
+
/** Tool contracts implemented directly by the Hopper adapter. */
|
|
26
|
+
export const HOPPER_PROVIDER_TOOL_CONTRACTS = Object.freeze([
|
|
22
27
|
...OFFICIAL_TOOL_CONTRACTS,
|
|
23
28
|
...ENHANCED_TOOL_CONTRACTS.filter((contract) => contract.name === "analyze_function"),
|
|
24
|
-
];
|
|
25
|
-
const CAPABILITIES = Object.freeze(
|
|
29
|
+
]);
|
|
30
|
+
const CAPABILITIES = Object.freeze(HOPPER_PROVIDER_TOOL_CONTRACTS.map((contract) => Object.freeze({
|
|
26
31
|
provider: IDENTITY,
|
|
27
32
|
operation: contract.name,
|
|
28
33
|
inputContractVersion: 1,
|
|
@@ -63,12 +68,98 @@ export class HopperProvider {
|
|
|
63
68
|
capabilities() {
|
|
64
69
|
return CAPABILITIES;
|
|
65
70
|
}
|
|
66
|
-
|
|
71
|
+
inspectAvailability() {
|
|
72
|
+
const diagnostics = {
|
|
73
|
+
launcher_path: this.config.hopperLauncherPath,
|
|
74
|
+
platform: process.platform,
|
|
75
|
+
};
|
|
76
|
+
if (process.platform !== "darwin" && process.platform !== "linux")
|
|
77
|
+
return {
|
|
78
|
+
status: "unavailable",
|
|
79
|
+
code: "unsupported_host",
|
|
80
|
+
reason: `Hopper integration is not supported on ${process.platform}.`,
|
|
81
|
+
diagnostics,
|
|
82
|
+
};
|
|
83
|
+
try {
|
|
84
|
+
accessSync(this.config.hopperLauncherPath, constants.X_OK);
|
|
85
|
+
return {
|
|
86
|
+
status: "available",
|
|
87
|
+
code: null,
|
|
88
|
+
reason: null,
|
|
89
|
+
diagnostics,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return {
|
|
94
|
+
status: "unavailable",
|
|
95
|
+
code: "executable_missing",
|
|
96
|
+
reason: `Hopper launcher is missing or not executable: ${this.config.hopperLauncherPath}`,
|
|
97
|
+
diagnostics,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
inspectTargetSupport(target) {
|
|
102
|
+
const diagnostics = {
|
|
103
|
+
target_kind: target.kind,
|
|
104
|
+
target_format: target.format,
|
|
105
|
+
architecture: target.architecture ?? null,
|
|
106
|
+
};
|
|
107
|
+
if (target.kind !== "executable" && target.kind !== "database")
|
|
108
|
+
return {
|
|
109
|
+
status: "unsupported",
|
|
110
|
+
code: "target_kind_unsupported",
|
|
111
|
+
reason: `Hopper cannot directly analyze ${target.kind} targets.`,
|
|
112
|
+
diagnostics,
|
|
113
|
+
};
|
|
114
|
+
if (target.kind === "database")
|
|
115
|
+
return {
|
|
116
|
+
status: "supported",
|
|
117
|
+
code: null,
|
|
118
|
+
reason: null,
|
|
119
|
+
diagnostics,
|
|
120
|
+
};
|
|
121
|
+
if (!["mach-o", "elf", "pe"].includes(target.format))
|
|
122
|
+
return {
|
|
123
|
+
status: "unsupported",
|
|
124
|
+
code: "target_format_unsupported",
|
|
125
|
+
reason: `Hopper cannot open ${target.format} through this adapter.`,
|
|
126
|
+
diagnostics,
|
|
127
|
+
};
|
|
128
|
+
if (target.architecture === undefined)
|
|
129
|
+
return {
|
|
130
|
+
status: "unsupported",
|
|
131
|
+
code: "architecture_unsupported",
|
|
132
|
+
reason: "Hopper requires a supported concrete target architecture.",
|
|
133
|
+
diagnostics,
|
|
134
|
+
};
|
|
135
|
+
return {
|
|
136
|
+
status: "supported",
|
|
137
|
+
code: null,
|
|
138
|
+
reason: null,
|
|
139
|
+
diagnostics,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
resolveAnalysisProfile(target, options) {
|
|
143
|
+
return resolveHopperAnalysisProfile(target, {
|
|
144
|
+
launcherPath: this.config.hopperLauncherPath,
|
|
145
|
+
loaderArgsOverride: this.config.hopperLoaderArgs,
|
|
146
|
+
provider: IDENTITY,
|
|
147
|
+
...(options?.signal === undefined ? {} : { signal: options.signal }),
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
createClient(target, profile) {
|
|
67
151
|
if (target.kind !== "executable" && target.kind !== "database")
|
|
68
152
|
return {
|
|
69
153
|
execute: (operation) => Promise.resolve(err(new AnalysisCapabilityUnavailableError(IDENTITY.id, operation, `Hopper cannot open ${target.kind} targets directly. Inventory or extract the artifact first.`))),
|
|
70
154
|
close: () => Promise.resolve(),
|
|
71
155
|
};
|
|
156
|
+
const derivedLoaderArgs = hopperLoaderArgsForTarget(target);
|
|
157
|
+
if (!derivedLoaderArgs.ok)
|
|
158
|
+
return {
|
|
159
|
+
execute: () => Promise.resolve(err(derivedLoaderArgs.error)),
|
|
160
|
+
close: () => Promise.resolve(),
|
|
161
|
+
};
|
|
162
|
+
const executionProvider = profile?.provider ?? IDENTITY;
|
|
72
163
|
const client = new HopperClient({
|
|
73
164
|
launcher: new HopperApplicationLauncher({
|
|
74
165
|
launcherPath: this.config.hopperLauncherPath,
|
|
@@ -76,7 +167,7 @@ export class HopperProvider {
|
|
|
76
167
|
targetKind: target.kind,
|
|
77
168
|
loaderArgs: this.config.hopperLoaderArgs.length > 0
|
|
78
169
|
? this.config.hopperLoaderArgs
|
|
79
|
-
:
|
|
170
|
+
: derivedLoaderArgs.value,
|
|
80
171
|
bridgeScriptPath: fileURLToPath(new URL("../../bridge/hopper_bridge.py", import.meta.url)),
|
|
81
172
|
...(process.platform === "linux"
|
|
82
173
|
? {
|
|
@@ -93,7 +184,8 @@ export class HopperProvider {
|
|
|
93
184
|
return result.ok
|
|
94
185
|
? {
|
|
95
186
|
ok: true,
|
|
96
|
-
value: createAnalysisExecution(result.value,
|
|
187
|
+
value: createAnalysisExecution(result.value, executionProvider, {
|
|
188
|
+
...(profile === undefined ? {} : { analysisProfile: profile }),
|
|
97
189
|
limitations: [
|
|
98
190
|
"Results depend on Hopper's completed static analysis.",
|
|
99
191
|
],
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { HopperProtocolError } from "../domain/errors.js";
|
|
2
|
+
import { err, ok } from "../domain/result.js";
|
|
3
|
+
/** Validate the bridge identity and exact per-launch run identifier. */
|
|
4
|
+
export const parseHopperServerInfo = (value, expectedRunId) => {
|
|
5
|
+
if (typeof value === "object" &&
|
|
6
|
+
value !== null &&
|
|
7
|
+
!Array.isArray(value) &&
|
|
8
|
+
value.name === "REA Hopper bridge" &&
|
|
9
|
+
typeof value.version === "string" &&
|
|
10
|
+
value.run_id === expectedRunId)
|
|
11
|
+
return ok({ name: value.name, version: value.version });
|
|
12
|
+
return err(new HopperProtocolError("Hopper bridge health result is invalid"));
|
|
13
|
+
};
|
|
14
|
+
/** Require Hopper to confirm both analysis and document shutdown. */
|
|
15
|
+
export const isHopperShutdownAcknowledgement = (value) => {
|
|
16
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
17
|
+
return false;
|
|
18
|
+
if (value.shutdown !== true)
|
|
19
|
+
return false;
|
|
20
|
+
return value.analysis_stopped === true && value.document_closed === true;
|
|
21
|
+
};
|
|
22
|
+
/** Detect the verified Linux bridge response that requires group cleanup. */
|
|
23
|
+
export const isHopperCleanupRequired = (value) => typeof value === "object" &&
|
|
24
|
+
value !== null &&
|
|
25
|
+
!Array.isArray(value) &&
|
|
26
|
+
value.shutdown === true &&
|
|
27
|
+
value.cleanup_required === true;
|