rea-agents 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +95 -27
- package/bridge/hopper_bridge.py +172 -14
- package/dist/application/AnalysisSnapshotCache.js +131 -0
- package/dist/application/AnalysisSnapshotFiles.js +31 -0
- package/dist/application/ArtifactGraphConstruction.js +1 -1
- package/dist/application/ArtifactInventory.js +100 -24
- package/dist/application/AuthorizedArtifactInventory.js +16 -0
- package/dist/application/BinarySession.js +149 -36
- package/dist/application/BinarySessionPort.js +1 -0
- package/dist/application/BoundedJsonFiles.js +109 -0
- package/dist/application/CapabilityInventory.js +127 -0
- package/dist/application/ClientRegistrationStatus.js +69 -0
- package/dist/application/CrossVersionInventory.js +109 -0
- package/dist/application/CrossVersionInvestigation.js +358 -0
- package/dist/application/DirectAnalysis.js +205 -24
- package/dist/application/Doctor.js +104 -3
- package/dist/application/EvidenceBundleFiles.js +15 -108
- package/dist/application/EvidenceLedger.js +3 -2
- package/dist/application/InvestigationProviders.js +48 -0
- package/dist/application/InvestigationWorkspaceStore.js +214 -0
- package/dist/application/LinuxHopper.js +72 -48
- package/dist/application/PermissionAuthority.js +183 -0
- package/dist/application/PermissionConfiguration.js +26 -0
- package/dist/application/ProcessCaptureAuthority.js +2 -2
- package/dist/application/ProcessCaptureError.js +25 -0
- package/dist/application/ProcessCaptureLifecycle.js +22 -2
- package/dist/application/ProcessCli.js +111 -31
- package/dist/application/ProcessHarness.js +6 -3
- package/dist/application/ProgressReporter.js +30 -0
- package/dist/application/ProjectPermissionStore.js +110 -0
- package/dist/application/ReferenceSourceImportEntries.js +18 -3
- package/dist/application/ReferenceSourceImportTypes.js +32 -0
- package/dist/application/Setup.js +35 -101
- package/dist/application/SetupClients.js +19 -0
- package/dist/application/SetupInstallFailure.js +53 -0
- package/dist/application/SetupPlan.js +31 -0
- package/dist/application/SupportedClients.js +41 -0
- package/dist/application/Uninstall.js +34 -24
- package/dist/application/UnknownEvidence.js +33 -0
- package/dist/application/Upgrade.js +3 -1
- package/dist/application/runtime.js +2 -2
- package/dist/artifacts/ArtifactProvider.js +15 -8
- package/dist/catalogIdentity.js +121 -0
- package/dist/cli.js +76 -13
- package/dist/cliEvidenceCommands.js +52 -12
- package/dist/cliInvestigationCommands.js +137 -0
- package/dist/cliLogging.js +20 -3
- package/dist/cliOutput.js +41 -0
- package/dist/cliPolicyCommands.js +115 -0
- package/dist/config.js +102 -27
- package/dist/contracts/artifactToolContracts.js +14 -1
- package/dist/contracts/errorSchemas.js +98 -0
- package/dist/contracts/promptContracts.js +256 -0
- package/dist/contracts/sessionLifecycleInputs.js +11 -0
- package/dist/contracts/toolContractTypes.js +1 -0
- package/dist/contracts/toolContracts.js +20 -7
- package/dist/contracts/toolOutputSchemas.js +70 -9
- package/dist/domain/analysisSnapshot.js +149 -0
- package/dist/domain/artifactComparison.js +38 -11
- package/dist/domain/artifactGraph.js +25 -1
- package/dist/domain/artifactInventoryEvidence.js +18 -0
- package/dist/domain/changedBehavior.js +35 -10
- package/dist/domain/errors.js +384 -32
- package/dist/domain/evidence.js +4 -2
- package/dist/domain/evidenceBundle.js +28 -0
- package/dist/domain/hopperStartupFailure.js +55 -0
- package/dist/domain/investigationWorkspace.js +341 -0
- package/dist/domain/jsonValue.js +2 -0
- package/dist/domain/nativeInspection.js +3 -2
- package/dist/domain/permissionPolicy.js +157 -0
- package/dist/domain/processCapture.js +5 -4
- package/dist/domain/processComparison.js +6 -4
- package/dist/domain/reconstructionVerification.js +1 -1
- package/dist/domain/reconstructionVerificationSchemas.js +1 -0
- package/dist/domain/staticRuntimeCorrelation.js +5 -1
- package/dist/generatedPackageMetadata.js +9 -0
- package/dist/hopper/BridgeLauncher.js +51 -20
- package/dist/hopper/HopperClient.js +45 -3
- package/dist/hopper/HopperProvider.js +1 -0
- package/dist/identity.js +10 -3
- package/dist/logger.js +2 -2
- package/dist/main.js +139 -28
- package/dist/server/createServer.js +54 -5
- package/dist/server/mcpProgress.js +23 -0
- package/dist/server/promptCompletion.js +144 -0
- package/dist/server/registerArtifactComparisonTool.js +6 -2
- package/dist/server/registerBundleComparisonTool.js +6 -2
- package/dist/server/registerEnhancedTools.js +20 -7
- package/dist/server/registerEvidenceResources.js +302 -0
- package/dist/server/registerEvidenceTools.js +59 -8
- package/dist/server/registerFunctionComparisonTool.js +7 -3
- package/dist/server/registerInvestigationTools.js +111 -18
- package/dist/server/registerOfficialTools.js +25 -14
- package/dist/server/registerProcessComparisonTool.js +8 -10
- package/dist/server/registerPrompts.js +67 -0
- package/dist/server/registerSessionStatusTool.js +52 -0
- package/dist/server/registerSessionTools.js +178 -22
- package/dist/server/runDerivedOperation.js +35 -0
- package/dist/server/sessionEvidence.js +2 -8
- package/dist/server/sessionToolPolicies.js +1 -42
- package/dist/server/toolResult.js +34 -6
- package/dist/serverIdentity.js +70 -0
- package/install.sh +11 -11
- package/package.json +10 -6
- package/scripts/hopper-demo-x11.py +349 -0
- package/scripts/rea.mjs +6 -3
- package/skills/rea-analysis/SKILL.md +9 -2
|
@@ -30,10 +30,13 @@ export class HopperApplicationLauncher {
|
|
|
30
30
|
}
|
|
31
31
|
async launch(session, options = {}) {
|
|
32
32
|
const bootstrapPath = `${session.directory}/bootstrap.py`;
|
|
33
|
+
const ownsProcessLifetime = usesLinuxDemo(this.options);
|
|
33
34
|
const source = [
|
|
34
35
|
`REA_SOCKET = ${JSON.stringify(session.socketPath)}`,
|
|
35
36
|
`REA_TOKEN = ${JSON.stringify(session.token)}`,
|
|
36
37
|
`REA_RUN_ID = ${JSON.stringify(session.runId)}`,
|
|
38
|
+
`REA_TARGET_PATH = ${JSON.stringify(this.options.targetPath)}`,
|
|
39
|
+
`REA_OWNS_PROCESS_LIFETIME = ${ownsProcessLifetime ? "True" : "False"}`,
|
|
37
40
|
`exec(compile(open(${JSON.stringify(this.options.bridgeScriptPath)}, 'rb').read(), ${JSON.stringify(this.options.bridgeScriptPath)}, 'exec'))`,
|
|
38
41
|
"",
|
|
39
42
|
].join("\n");
|
|
@@ -59,25 +62,12 @@ export class HopperApplicationLauncher {
|
|
|
59
62
|
const prepared = await prepareHopperApplication(this.options.launcherPath, options.signal);
|
|
60
63
|
if (!prepared)
|
|
61
64
|
return err(new HopperCancelledError());
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
env: { ...process.env, REA_PROCESS_RUN_ID: session.runId },
|
|
66
|
-
});
|
|
67
|
-
const started = await new Promise((resolve) => {
|
|
68
|
-
const onSpawn = () => {
|
|
69
|
-
child.off("error", onError);
|
|
70
|
-
resolve(ok(undefined));
|
|
71
|
-
};
|
|
72
|
-
const onError = (cause) => {
|
|
73
|
-
child.off("spawn", onSpawn);
|
|
74
|
-
resolve(err(new HopperStartError({ cause })));
|
|
75
|
-
};
|
|
76
|
-
child.once("spawn", onSpawn);
|
|
77
|
-
child.once("error", onError);
|
|
78
|
-
});
|
|
65
|
+
const linuxDemo = linuxDemoLaunch(this.options, session, args);
|
|
66
|
+
const ownershipCommand = linuxDemo?.ownershipCommand ?? this.options.launcherPath;
|
|
67
|
+
const started = await spawnLauncher(linuxDemo?.command ?? this.options.launcherPath, linuxDemo?.args ?? args, session.runId);
|
|
79
68
|
if (!started.ok)
|
|
80
69
|
return started;
|
|
70
|
+
const child = started.value;
|
|
81
71
|
const pid = child.pid;
|
|
82
72
|
if (pid === undefined)
|
|
83
73
|
return err(new HopperStartError({ cause: new Error("missing launcher PID") }));
|
|
@@ -87,20 +77,21 @@ export class HopperApplicationLauncher {
|
|
|
87
77
|
pid,
|
|
88
78
|
process_group_id: pid,
|
|
89
79
|
parent_pid: process.pid,
|
|
90
|
-
launcher: this.options.launcherPath,
|
|
80
|
+
launcher: linuxDemo?.command ?? this.options.launcherPath,
|
|
91
81
|
created_at: new Date().toISOString(),
|
|
92
82
|
};
|
|
93
83
|
try {
|
|
94
84
|
await writeFileAtomic(join(session.directory, "ownership.json"), `${JSON.stringify(ownership)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
95
85
|
}
|
|
96
86
|
catch (cause) {
|
|
97
|
-
await cleanupOwnedProcessGroup(ownedProcessGroup(session, pid,
|
|
87
|
+
await cleanupOwnedProcessGroup(ownedProcessGroup(session, pid, ownershipCommand));
|
|
98
88
|
return err(new HopperStartError({ cause }));
|
|
99
89
|
}
|
|
100
90
|
return ok({
|
|
101
91
|
process: child,
|
|
102
92
|
ownsProcessLifetime: true,
|
|
103
|
-
|
|
93
|
+
shutdownByCleanup: ownsProcessLifetime,
|
|
94
|
+
cleanup: () => cleanupOwnedProcessGroup(ownedProcessGroup(session, pid, ownershipCommand)),
|
|
104
95
|
});
|
|
105
96
|
}
|
|
106
97
|
catch (cause) {
|
|
@@ -108,6 +99,46 @@ export class HopperApplicationLauncher {
|
|
|
108
99
|
}
|
|
109
100
|
}
|
|
110
101
|
}
|
|
102
|
+
const linuxDemoLaunch = (options, session, hopperArgs) => {
|
|
103
|
+
if (!usesLinuxDemo(options) || options.demoHelperPath === undefined)
|
|
104
|
+
return undefined;
|
|
105
|
+
return {
|
|
106
|
+
command: "/usr/bin/python3",
|
|
107
|
+
ownershipCommand: "/usr/bin/python3",
|
|
108
|
+
args: [
|
|
109
|
+
options.demoHelperPath,
|
|
110
|
+
"--hopper",
|
|
111
|
+
options.launcherPath,
|
|
112
|
+
"--socket",
|
|
113
|
+
session.socketPath,
|
|
114
|
+
"--",
|
|
115
|
+
options.launcherPath,
|
|
116
|
+
...hopperArgs,
|
|
117
|
+
],
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
const usesLinuxDemo = (options) => process.platform === "linux" &&
|
|
121
|
+
basename(options.launcherPath) === "Hopper" &&
|
|
122
|
+
options.demoHelperPath !== undefined;
|
|
123
|
+
const spawnLauncher = async (command, args, runId) => {
|
|
124
|
+
const child = spawn(command, args, {
|
|
125
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
126
|
+
detached: process.platform !== "win32",
|
|
127
|
+
env: { ...process.env, REA_PROCESS_RUN_ID: runId },
|
|
128
|
+
});
|
|
129
|
+
return new Promise((resolve) => {
|
|
130
|
+
const onSpawn = () => {
|
|
131
|
+
child.off("error", onError);
|
|
132
|
+
resolve(ok(child));
|
|
133
|
+
};
|
|
134
|
+
const onError = (cause) => {
|
|
135
|
+
child.off("spawn", onSpawn);
|
|
136
|
+
resolve(err(new HopperStartError({ cause })));
|
|
137
|
+
};
|
|
138
|
+
child.once("spawn", onSpawn);
|
|
139
|
+
child.once("error", onError);
|
|
140
|
+
});
|
|
141
|
+
};
|
|
111
142
|
const ownedProcessGroup = (session, pid, launcherPath) => ({
|
|
112
143
|
runId: session.runId,
|
|
113
144
|
leaderPid: pid,
|
|
@@ -3,11 +3,12 @@ import { access, mkdtemp, rm } from "node:fs/promises";
|
|
|
3
3
|
import { createConnection } from "node:net";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { join } from "node:path";
|
|
6
|
-
import { HopperCancelledError, HopperProcessError, HopperProtocolError, HopperStartError, HopperTimeoutError, } from "../domain/errors.js";
|
|
6
|
+
import { HopperCancelledError, HopperProcessError, HopperProtocolError, HopperStartError, hopperStartupFailure, HopperTimeoutError, } from "../domain/errors.js";
|
|
7
7
|
import { err, ok } from "../domain/result.js";
|
|
8
8
|
import { silentLogger } from "../logger.js";
|
|
9
9
|
import { parseResponseLine, responseResult } from "./protocol.js";
|
|
10
10
|
const MAX_LINE_BYTES = 10 * 1024 * 1024;
|
|
11
|
+
const SHUTDOWN_TIMEOUT_MS = 30_000;
|
|
11
12
|
const SESSION_ROOT = process.platform === "darwin" ? "/tmp" : tmpdir();
|
|
12
13
|
/**
|
|
13
14
|
* Owns one authenticated NDJSON-over-Unix-socket bridge session.
|
|
@@ -28,6 +29,7 @@ export class HopperClient {
|
|
|
28
29
|
#nextId = 1;
|
|
29
30
|
#buffer = "";
|
|
30
31
|
#closing = false;
|
|
32
|
+
#launcherExitCode;
|
|
31
33
|
#startPromise;
|
|
32
34
|
#closePromise;
|
|
33
35
|
constructor(options) {
|
|
@@ -57,6 +59,7 @@ export class HopperClient {
|
|
|
57
59
|
}
|
|
58
60
|
const socketPath = join(this.#directory, "bridge.sock");
|
|
59
61
|
this.#token = randomBytes(32).toString("hex");
|
|
62
|
+
this.#launcherExitCode = undefined;
|
|
60
63
|
const runId = randomUUID();
|
|
61
64
|
const launched = await this.#options.launcher.launch({
|
|
62
65
|
directory: this.#directory,
|
|
@@ -104,14 +107,30 @@ export class HopperClient {
|
|
|
104
107
|
this.#closing = true;
|
|
105
108
|
try {
|
|
106
109
|
const socket = this.#socket;
|
|
110
|
+
let cleanupAttempted = false;
|
|
107
111
|
if (socket !== undefined && !socket.destroyed) {
|
|
108
|
-
await this.#request("shutdown", {}, { timeoutMs:
|
|
112
|
+
const shutdown = await this.#request("shutdown", {}, { timeoutMs: SHUTDOWN_TIMEOUT_MS }).catch(() => err(new HopperProcessError(null)));
|
|
113
|
+
if (shutdown.ok &&
|
|
114
|
+
isCleanupRequired(shutdown.value) &&
|
|
115
|
+
this.#launch?.shutdownByCleanup === true &&
|
|
116
|
+
this.#launch.cleanup !== undefined) {
|
|
117
|
+
cleanupAttempted = true;
|
|
118
|
+
const cleanup = await this.#launch.cleanup();
|
|
119
|
+
if (!cleanup.cleaned) {
|
|
120
|
+
await this.#fallbackDocumentShutdown();
|
|
121
|
+
const retried = await this.#launch.cleanup();
|
|
122
|
+
if (!retried.cleaned)
|
|
123
|
+
this.#logger.warn({ reason: retried.reason }, "Owned launcher cleanup failed closed");
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
else if (!shutdown.ok || !isShutdownAcknowledgement(shutdown.value))
|
|
127
|
+
this.#logger.warn({ status: shutdown.ok ? "invalid-acknowledgement" : "failed" }, "Hopper document shutdown was not confirmed");
|
|
109
128
|
}
|
|
110
129
|
this.#failAll(new HopperProcessError(null));
|
|
111
130
|
socket?.destroy();
|
|
112
131
|
this.#socket = undefined;
|
|
113
132
|
if (this.#launch?.ownsProcessLifetime === true) {
|
|
114
|
-
if (this.#launch.cleanup !== undefined) {
|
|
133
|
+
if (this.#launch.cleanup !== undefined && !cleanupAttempted) {
|
|
115
134
|
const cleanup = await this.#launch.cleanup();
|
|
116
135
|
if (!cleanup.cleaned)
|
|
117
136
|
this.#logger.warn({ reason: cleanup.reason }, "Owned launcher cleanup failed closed");
|
|
@@ -132,6 +151,11 @@ export class HopperClient {
|
|
|
132
151
|
this.#closePromise = undefined;
|
|
133
152
|
}
|
|
134
153
|
}
|
|
154
|
+
async #fallbackDocumentShutdown() {
|
|
155
|
+
const fallback = await this.#request("shutdown_document", {}, { timeoutMs: SHUTDOWN_TIMEOUT_MS }).catch(() => err(new HopperProcessError(null)));
|
|
156
|
+
if (!fallback.ok || !isShutdownAcknowledgement(fallback.value))
|
|
157
|
+
this.#logger.warn({ status: fallback.ok ? "invalid-acknowledgement" : "failed" }, "Hopper document shutdown fallback was not confirmed");
|
|
158
|
+
}
|
|
135
159
|
async #connect(socketPath, signal) {
|
|
136
160
|
const deadline = Date.now() + this.#options.startupTimeoutMs;
|
|
137
161
|
while (Date.now() < deadline) {
|
|
@@ -139,6 +163,9 @@ export class HopperClient {
|
|
|
139
163
|
return err(new HopperCancelledError());
|
|
140
164
|
if (this.#closing)
|
|
141
165
|
return err(new HopperProcessError(null));
|
|
166
|
+
if (this.#launcherExitCode !== undefined &&
|
|
167
|
+
hopperStartupFailure(this.#launcherExitCode) !== undefined)
|
|
168
|
+
return err(new HopperProcessError(this.#launcherExitCode));
|
|
142
169
|
try {
|
|
143
170
|
await access(socketPath);
|
|
144
171
|
}
|
|
@@ -222,6 +249,7 @@ export class HopperClient {
|
|
|
222
249
|
});
|
|
223
250
|
});
|
|
224
251
|
launch.process.on("exit", (code) => {
|
|
252
|
+
this.#launcherExitCode = code;
|
|
225
253
|
this.#options.onDiagnostic?.({ type: "launcher-exit", code });
|
|
226
254
|
if (launch.ownsProcessLifetime && !this.#closing) {
|
|
227
255
|
this.#failAll(new HopperProcessError(code));
|
|
@@ -310,6 +338,20 @@ const connectOnce = async (socketPath) => new Promise((resolve) => {
|
|
|
310
338
|
resolve(err(new HopperStartError({ cause })));
|
|
311
339
|
});
|
|
312
340
|
});
|
|
341
|
+
const isShutdownAcknowledgement = (value) => {
|
|
342
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
343
|
+
return false;
|
|
344
|
+
if (value.shutdown !== true)
|
|
345
|
+
return false;
|
|
346
|
+
if (value.analysis_stopped === true && value.document_closed === true)
|
|
347
|
+
return true;
|
|
348
|
+
return false;
|
|
349
|
+
};
|
|
350
|
+
const isCleanupRequired = (value) => typeof value === "object" &&
|
|
351
|
+
value !== null &&
|
|
352
|
+
!Array.isArray(value) &&
|
|
353
|
+
value.shutdown === true &&
|
|
354
|
+
value.cleanup_required === true;
|
|
313
355
|
const parseServerInfo = (value, expectedRunId) => {
|
|
314
356
|
if (typeof value === "object" &&
|
|
315
357
|
value !== null &&
|
|
@@ -78,6 +78,7 @@ export class HopperProvider {
|
|
|
78
78
|
? this.config.hopperLoaderArgs
|
|
79
79
|
: target.loaderArgs,
|
|
80
80
|
bridgeScriptPath: fileURLToPath(new URL("../../bridge/hopper_bridge.py", import.meta.url)),
|
|
81
|
+
demoHelperPath: fileURLToPath(new URL("../../scripts/hopper-demo-x11.py", import.meta.url)),
|
|
81
82
|
}),
|
|
82
83
|
logger: this.logger.child({ layer: "bridge" }),
|
|
83
84
|
});
|
package/dist/identity.js
CHANGED
|
@@ -5,12 +5,19 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export const PRODUCT_IDENTITY = {
|
|
7
7
|
displayName: "REA",
|
|
8
|
-
packageName:
|
|
9
|
-
packageVersion:
|
|
8
|
+
packageName: PACKAGE_METADATA.name,
|
|
9
|
+
packageVersion: PACKAGE_METADATA.version,
|
|
10
10
|
cliBinary: "rea",
|
|
11
11
|
mcpCommand: "npx -y rea-agents mcp",
|
|
12
12
|
mcpServerKey: "rea",
|
|
13
13
|
skillName: "rea-analysis",
|
|
14
|
-
skillVersion:
|
|
14
|
+
skillVersion: PACKAGE_METADATA.skillVersion,
|
|
15
15
|
configFileName: "rea.json",
|
|
16
16
|
};
|
|
17
|
+
/** Exact SDK package identities, distinct from negotiated protocol version. */
|
|
18
|
+
export const SDK_IDENTITY = {
|
|
19
|
+
server: PACKAGE_METADATA.serverSdkVersion,
|
|
20
|
+
client_test: PACKAGE_METADATA.clientSdkVersion,
|
|
21
|
+
core: PACKAGE_METADATA.coreSdkVersion,
|
|
22
|
+
};
|
|
23
|
+
import { PACKAGE_METADATA } from "./generatedPackageMetadata.js";
|
package/dist/logger.js
CHANGED
|
@@ -5,7 +5,7 @@ const logLevelSchema = z
|
|
|
5
5
|
.default("info");
|
|
6
6
|
/** Parse the optional REA_LOG_LEVEL environment variable. */
|
|
7
7
|
export const parseLogLevel = (value) => logLevelSchema.parse(value);
|
|
8
|
-
/** Create a JSON logger
|
|
8
|
+
/** Create a JSON logger on stderr so protocol and CLI result stdout stay valid. */
|
|
9
9
|
export const createLogger = (mode, level) => pino({
|
|
10
10
|
level,
|
|
11
11
|
base: { application: "rea", mode },
|
|
@@ -22,6 +22,6 @@ export const createLogger = (mode, level) => pino({
|
|
|
22
22
|
],
|
|
23
23
|
censor: "[Redacted]",
|
|
24
24
|
},
|
|
25
|
-
}, pino.destination({ dest:
|
|
25
|
+
}, pino.destination({ dest: 2, sync: false }));
|
|
26
26
|
/** Logger used by embedders that have not opted into process output. */
|
|
27
27
|
export const silentLogger = pino({ level: "silent" });
|
package/dist/main.js
CHANGED
|
@@ -6,19 +6,49 @@ import { parseConfig } from "./config.js";
|
|
|
6
6
|
import { createBinarySession } from "./application/runtime.js";
|
|
7
7
|
import { createServer } from "./server/createServer.js";
|
|
8
8
|
import { createLogger } from "./logger.js";
|
|
9
|
+
import { projectAnalysisError } from "./domain/errors.js";
|
|
10
|
+
import { readProjectPermissionStore } from "./application/ProjectPermissionStore.js";
|
|
11
|
+
import { loadConfiguredPermissionAuthority } from "./application/PermissionConfiguration.js";
|
|
12
|
+
const MCP_CONNECTION_LOST = "REA lost its MCP client connection. Restart REA from your MCP client.";
|
|
13
|
+
const MCP_CONNECTION_START_FAILED = "REA could not start its MCP connection. Restart REA from your MCP client; run `rea doctor` if it fails again.";
|
|
14
|
+
const MCP_SHUTDOWN_FAILED = "REA could not close cleanly. End the REA process before starting it again.";
|
|
15
|
+
const SERVER_START_FAILED = "REA could not start. Run `rea doctor`, then restart REA from your MCP client.";
|
|
16
|
+
const runtimeDependencies = () => ({
|
|
17
|
+
env: process.env,
|
|
18
|
+
serve: serveStdio,
|
|
19
|
+
writeStderr: (text) => process.stderr.write(text),
|
|
20
|
+
setExitCode: (code) => {
|
|
21
|
+
process.exitCode = code;
|
|
22
|
+
},
|
|
23
|
+
registerShutdown: (handler) => {
|
|
24
|
+
process.once("SIGINT", handler);
|
|
25
|
+
process.once("SIGTERM", handler);
|
|
26
|
+
process.stdin.once("end", handler);
|
|
27
|
+
process.stdin.once("close", handler);
|
|
28
|
+
},
|
|
29
|
+
registerReload: (handler) => {
|
|
30
|
+
process.on("SIGHUP", handler);
|
|
31
|
+
return () => process.off("SIGHUP", handler);
|
|
32
|
+
},
|
|
33
|
+
});
|
|
9
34
|
/**
|
|
10
35
|
* Start the long-lived MCP adapter and install idempotent shutdown handlers.
|
|
11
36
|
* The adapter owns its BinarySession for the process lifetime; EOF and process
|
|
12
37
|
* signals close bridge resources without assuming ownership of the Hopper app.
|
|
13
38
|
*/
|
|
14
|
-
export const run = async () => {
|
|
15
|
-
const config = parseConfig(
|
|
39
|
+
export const run = async (dependencies = runtimeDependencies()) => {
|
|
40
|
+
const config = parseConfig(dependencies.env);
|
|
16
41
|
if (!config.ok) {
|
|
17
|
-
|
|
42
|
+
dependencies.writeStderr(`${projectAnalysisError(config.error).message}\n`);
|
|
18
43
|
return 1;
|
|
19
44
|
}
|
|
20
45
|
const logger = createLogger("mcp", config.value.logLevel);
|
|
21
46
|
const serverLogger = logger.child({ layer: "server" });
|
|
47
|
+
const permissionAuthority = await loadConfiguredPermissionAuthority(config.value);
|
|
48
|
+
if (!permissionAuthority.ok) {
|
|
49
|
+
dependencies.writeStderr(`${projectAnalysisError(permissionAuthority.error).message}\n`);
|
|
50
|
+
return 1;
|
|
51
|
+
}
|
|
22
52
|
const session = createBinarySession(config.value, logger);
|
|
23
53
|
if (config.value.hopperTargetPath !== undefined) {
|
|
24
54
|
const opened = await session.open(config.value.hopperTargetPath, {
|
|
@@ -27,59 +57,140 @@ export const run = async () => {
|
|
|
27
57
|
if (!opened.ok) {
|
|
28
58
|
await session.close();
|
|
29
59
|
serverLogger.error({ errorTag: opened.error._tag }, "Initial target failed to open");
|
|
30
|
-
|
|
60
|
+
dependencies.writeStderr(`${projectAnalysisError(opened.error).message}\n`);
|
|
31
61
|
return 1;
|
|
32
62
|
}
|
|
33
63
|
}
|
|
64
|
+
let currentConfig = config.value;
|
|
65
|
+
const runtimeProcessPolicy = {
|
|
66
|
+
...config.value.processExecutionPolicy,
|
|
67
|
+
executableRoots: [...config.value.processExecutionPolicy.executableRoots],
|
|
68
|
+
workingRoots: [...config.value.processExecutionPolicy.workingRoots],
|
|
69
|
+
allowedEnvironment: [
|
|
70
|
+
...config.value.processExecutionPolicy.allowedEnvironment,
|
|
71
|
+
],
|
|
72
|
+
};
|
|
73
|
+
const runtimeEvidencePolicy = {
|
|
74
|
+
...config.value.evidenceFilePolicy,
|
|
75
|
+
roots: [...config.value.evidenceFilePolicy.roots],
|
|
76
|
+
};
|
|
77
|
+
const runtimeSnapshotPolicy = {
|
|
78
|
+
...config.value.analysisSnapshotFilePolicy,
|
|
79
|
+
roots: [...config.value.analysisSnapshotFilePolicy.roots],
|
|
80
|
+
};
|
|
81
|
+
const runtimeInvestigationRoots = [...config.value.investigationInputRoots];
|
|
82
|
+
const liveServers = new Set();
|
|
34
83
|
let handle;
|
|
35
84
|
try {
|
|
36
|
-
handle =
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
85
|
+
handle = dependencies.serve(() => {
|
|
86
|
+
const server = createServer(session, session, {
|
|
87
|
+
logger,
|
|
88
|
+
processPolicy: runtimeProcessPolicy,
|
|
89
|
+
evidenceFilePolicy: runtimeEvidencePolicy,
|
|
90
|
+
investigationInputRoots: runtimeInvestigationRoots,
|
|
91
|
+
analysisSnapshotFilePolicy: runtimeSnapshotPolicy,
|
|
92
|
+
permissionAuthority: permissionAuthority.value,
|
|
93
|
+
artifactIntegrityContinueEnabled: () => currentConfig.artifactIntegrityContinueEnabled,
|
|
94
|
+
availabilityPolicy: () => ({
|
|
95
|
+
processCaptureEnabled: currentConfig.processExecutionPolicy.enabled,
|
|
96
|
+
evidenceFileRoots: currentConfig.evidenceFilePolicy.roots.length,
|
|
97
|
+
}),
|
|
98
|
+
});
|
|
99
|
+
liveServers.add(server);
|
|
100
|
+
return server;
|
|
101
|
+
}, {
|
|
41
102
|
onerror: () => {
|
|
42
|
-
serverLogger.error(
|
|
43
|
-
|
|
103
|
+
serverLogger.error(MCP_CONNECTION_LOST);
|
|
104
|
+
dependencies.writeStderr(`${MCP_CONNECTION_LOST}\n`);
|
|
44
105
|
},
|
|
45
106
|
});
|
|
46
107
|
}
|
|
47
108
|
catch {
|
|
48
109
|
await session.close();
|
|
49
|
-
serverLogger.error(
|
|
50
|
-
|
|
110
|
+
serverLogger.error(MCP_CONNECTION_START_FAILED);
|
|
111
|
+
dependencies.writeStderr(`${MCP_CONNECTION_START_FAILED}\n`);
|
|
51
112
|
return 1;
|
|
52
113
|
}
|
|
114
|
+
const unregisterReload = dependencies.registerReload?.(() => {
|
|
115
|
+
void (async () => {
|
|
116
|
+
const refreshed = parseConfig(dependencies.env);
|
|
117
|
+
if (!refreshed.ok) {
|
|
118
|
+
serverLogger.error("Reloaded permission policy is invalid");
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const reloaded = await permissionAuthority.value.reload(refreshed.value.permissionCeilings);
|
|
122
|
+
if (!reloaded.ok) {
|
|
123
|
+
serverLogger.error("Reloaded permission ceiling could not be applied");
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const administrators = await permissionAuthority.value.replaceAdministratorGrants(refreshed.value.administratorPermissionGrants);
|
|
127
|
+
if (!administrators.ok) {
|
|
128
|
+
serverLogger.error("Reloaded administrator grants could not be applied");
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (refreshed.value.permissionProjectRoot !== undefined &&
|
|
132
|
+
refreshed.value.permissionProjectStore !== undefined) {
|
|
133
|
+
const project = await readProjectPermissionStore(refreshed.value.permissionProjectStore, refreshed.value.permissionProjectRoot);
|
|
134
|
+
if (!project.ok) {
|
|
135
|
+
serverLogger.error("Reloaded project grants could not be read");
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const projects = await permissionAuthority.value.replaceProjectGrants(project.value?.grants ?? []);
|
|
139
|
+
if (!projects.ok) {
|
|
140
|
+
serverLogger.error("Reloaded project grants could not be applied");
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
const projects = await permissionAuthority.value.replaceProjectGrants([]);
|
|
146
|
+
if (!projects.ok) {
|
|
147
|
+
serverLogger.error("Reloaded project grants could not be cleared");
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
currentConfig = refreshed.value;
|
|
152
|
+
Object.assign(runtimeProcessPolicy, refreshed.value.processExecutionPolicy);
|
|
153
|
+
Object.assign(runtimeEvidencePolicy, refreshed.value.evidenceFilePolicy);
|
|
154
|
+
Object.assign(runtimeSnapshotPolicy, refreshed.value.analysisSnapshotFilePolicy);
|
|
155
|
+
runtimeInvestigationRoots.splice(0, runtimeInvestigationRoots.length, ...refreshed.value.investigationInputRoots);
|
|
156
|
+
for (const server of liveServers)
|
|
157
|
+
server.sendToolListChanged();
|
|
158
|
+
})();
|
|
159
|
+
}) ?? (() => undefined);
|
|
53
160
|
let shutdownPromise;
|
|
54
161
|
const shutdown = () => {
|
|
55
162
|
shutdownPromise ??= (async () => {
|
|
163
|
+
unregisterReload();
|
|
56
164
|
await handle.close();
|
|
57
165
|
await session.close();
|
|
166
|
+
permissionAuthority.value.clearSessionGrants();
|
|
58
167
|
})();
|
|
59
168
|
return shutdownPromise;
|
|
60
169
|
};
|
|
61
170
|
const requestShutdown = () => {
|
|
62
171
|
shutdown().catch(() => {
|
|
63
|
-
|
|
64
|
-
serverLogger.error(
|
|
65
|
-
|
|
172
|
+
dependencies.setExitCode(1);
|
|
173
|
+
serverLogger.error(MCP_SHUTDOWN_FAILED);
|
|
174
|
+
dependencies.writeStderr(`${MCP_SHUTDOWN_FAILED}\n`);
|
|
66
175
|
});
|
|
67
176
|
};
|
|
68
|
-
|
|
69
|
-
process.once("SIGTERM", requestShutdown);
|
|
70
|
-
process.stdin.once("end", requestShutdown);
|
|
71
|
-
process.stdin.once("close", requestShutdown);
|
|
177
|
+
dependencies.registerShutdown(requestShutdown);
|
|
72
178
|
return 0;
|
|
73
179
|
};
|
|
180
|
+
/** Run the executable boundary without exposing an unexpected startup cause. */
|
|
181
|
+
export const runEntrypoint = async (start = () => run(), writeStderr = (text) => process.stderr.write(text), setExitCode = (code) => {
|
|
182
|
+
process.exitCode = code;
|
|
183
|
+
}) => {
|
|
184
|
+
try {
|
|
185
|
+
setExitCode(await start());
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
writeStderr(`${SERVER_START_FAILED}\n`);
|
|
189
|
+
setExitCode(1);
|
|
190
|
+
}
|
|
191
|
+
};
|
|
74
192
|
const entryPath = process.argv[1];
|
|
75
193
|
if (entryPath !== undefined &&
|
|
76
194
|
pathToFileURL(realpathSync(entryPath)).href === import.meta.url) {
|
|
77
|
-
|
|
78
|
-
.then((exitCode) => {
|
|
79
|
-
process.exitCode = exitCode;
|
|
80
|
-
})
|
|
81
|
-
.catch(() => {
|
|
82
|
-
process.stderr.write("Unexpected server startup failure\n");
|
|
83
|
-
process.exitCode = 1;
|
|
84
|
-
});
|
|
195
|
+
void runEntrypoint();
|
|
85
196
|
}
|
|
@@ -6,24 +6,64 @@ import { registerSessionTools } from "./registerSessionTools.js";
|
|
|
6
6
|
import { registerNativeTools } from "./registerNativeTools.js";
|
|
7
7
|
import { registerArtifactTools } from "./registerArtifactTools.js";
|
|
8
8
|
import { silentLogger } from "../logger.js";
|
|
9
|
+
import { registerGuidedPrompts } from "./registerPrompts.js";
|
|
10
|
+
import { registerEvidenceResources } from "./registerEvidenceResources.js";
|
|
11
|
+
import { createServerIdentity } from "../serverIdentity.js";
|
|
9
12
|
/**
|
|
10
13
|
* Construct one MCP server without acquiring subprocess resources.
|
|
11
14
|
* Supplying a session adds target lifecycle tools; omitting it retains the
|
|
12
15
|
* fixed-target seam used by focused tests and embedders.
|
|
13
16
|
*/
|
|
14
17
|
export const createServer = (analysis, session, options = {}) => {
|
|
18
|
+
const startedAt = new Date().toISOString();
|
|
15
19
|
const logger = options.logger ?? silentLogger;
|
|
16
|
-
const server = new McpServer({
|
|
17
|
-
|
|
20
|
+
const server = new McpServer({
|
|
21
|
+
name: PRODUCT_IDENTITY.mcpServerKey,
|
|
22
|
+
version: PRODUCT_IDENTITY.packageVersion,
|
|
23
|
+
}, {
|
|
24
|
+
capabilities: {
|
|
25
|
+
tools: { listChanged: true },
|
|
26
|
+
resources: { listChanged: true },
|
|
27
|
+
},
|
|
18
28
|
instructions: session === undefined
|
|
19
29
|
? "Reverse-engineering tools for an active analysis target. Start with binary_overview."
|
|
20
30
|
: "Reverse-engineering tools for configured analysis providers. Open a target with open_binary, then start with binary_overview.",
|
|
21
31
|
});
|
|
32
|
+
session?.onAvailabilityChanged?.(() => server.sendToolListChanged());
|
|
33
|
+
server.registerResource("server-identity", "rea://server/identity", {
|
|
34
|
+
title: "REA server identity",
|
|
35
|
+
description: "Live package, SDK, protocol, and catalog identity.",
|
|
36
|
+
mimeType: "application/json",
|
|
37
|
+
}, (uri) => {
|
|
38
|
+
const client = server.server.getClientVersion();
|
|
39
|
+
return {
|
|
40
|
+
contents: [
|
|
41
|
+
{
|
|
42
|
+
uri: uri.href,
|
|
43
|
+
mimeType: "application/json",
|
|
44
|
+
text: JSON.stringify(createServerIdentity({
|
|
45
|
+
startedAt,
|
|
46
|
+
...(client === undefined ? {} : { client }),
|
|
47
|
+
...(server.server.getNegotiatedProtocolVersion() === undefined
|
|
48
|
+
? {}
|
|
49
|
+
: {
|
|
50
|
+
protocolVersion: server.server.getNegotiatedProtocolVersion(),
|
|
51
|
+
}),
|
|
52
|
+
}), null, 2),
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
};
|
|
56
|
+
});
|
|
22
57
|
const toolLogger = logger.child({ layer: "server" });
|
|
23
58
|
const activeTarget = session === undefined ? undefined : () => session.activeTarget();
|
|
24
59
|
const recordEvidence = session === undefined
|
|
25
60
|
? undefined
|
|
26
|
-
: (evidence) =>
|
|
61
|
+
: (evidence) => {
|
|
62
|
+
const recorded = session.recordEvidence(evidence);
|
|
63
|
+
if (recorded.ok && recorded.value === "added")
|
|
64
|
+
server.sendResourceListChanged();
|
|
65
|
+
return recorded;
|
|
66
|
+
};
|
|
27
67
|
registerOfficialTools(server, analysis, {
|
|
28
68
|
logger: toolLogger,
|
|
29
69
|
activeTarget,
|
|
@@ -49,8 +89,17 @@ export const createServer = (analysis, session, options = {}) => {
|
|
|
49
89
|
logger: toolLogger,
|
|
50
90
|
activeTarget,
|
|
51
91
|
recordEvidence,
|
|
92
|
+
...(options.permissionAuthority === undefined
|
|
93
|
+
? {}
|
|
94
|
+
: { permissionAuthority: options.permissionAuthority }),
|
|
52
95
|
});
|
|
53
|
-
|
|
54
|
-
|
|
96
|
+
registerGuidedPrompts(server, analysis, session);
|
|
97
|
+
if (session !== undefined) {
|
|
98
|
+
registerEvidenceResources(server, session);
|
|
99
|
+
registerSessionTools(server, session, toolLogger, {
|
|
100
|
+
...options,
|
|
101
|
+
startedAt,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
55
104
|
return server;
|
|
56
105
|
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { createProgressReporter, silentProgressReporter, } from "../application/ProgressReporter.js";
|
|
2
|
+
/** Adapt the request's negotiated MCP progress token to application progress. */
|
|
3
|
+
export const mcpProgressReporter = (context) => {
|
|
4
|
+
const token = context.mcpReq._meta?.progressToken;
|
|
5
|
+
if (token === undefined)
|
|
6
|
+
return silentProgressReporter;
|
|
7
|
+
return createProgressReporter(async (update) => {
|
|
8
|
+
try {
|
|
9
|
+
await context.mcpReq.notify({
|
|
10
|
+
method: "notifications/progress",
|
|
11
|
+
params: {
|
|
12
|
+
progressToken: token,
|
|
13
|
+
progress: update.completed,
|
|
14
|
+
...(update.total === null ? {} : { total: update.total }),
|
|
15
|
+
message: `${update.phase}: ${update.message}`,
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
// Progress is observational; transport failure cannot change tool truth.
|
|
21
|
+
}
|
|
22
|
+
}, { minimumIntervalMs: 100 });
|
|
23
|
+
};
|