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