rea-agents 1.3.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 +21 -0
- package/bridge/hopper_bridge.py +12 -3
- package/dist/application/ArtifactGraphConstruction.js +1 -1
- package/dist/application/ArtifactInventory.js +98 -26
- package/dist/application/AuthorizedArtifactInventory.js +2 -2
- package/dist/application/BinarySession.js +83 -0
- package/dist/application/CapabilityInventory.js +127 -0
- package/dist/application/ClientRegistrationStatus.js +69 -0
- package/dist/application/CrossVersionInventory.js +19 -8
- package/dist/application/CrossVersionInvestigation.js +68 -5
- package/dist/application/DirectAnalysis.js +97 -9
- package/dist/application/Doctor.js +74 -2
- package/dist/application/PermissionAuthority.js +183 -0
- package/dist/application/PermissionConfiguration.js +26 -0
- package/dist/application/ProcessCaptureError.js +13 -0
- package/dist/application/ProcessCaptureLifecycle.js +3 -1
- package/dist/application/ProcessCli.js +23 -10
- package/dist/application/ProcessHarness.js +4 -1
- package/dist/application/ProgressReporter.js +30 -0
- package/dist/application/ProjectPermissionStore.js +110 -0
- package/dist/application/Setup.js +2 -41
- package/dist/application/SupportedClients.js +41 -0
- package/dist/application/Uninstall.js +1 -1
- package/dist/application/Upgrade.js +2 -0
- package/dist/application/runtime.js +1 -1
- package/dist/artifacts/ArtifactProvider.js +13 -3
- package/dist/catalogIdentity.js +121 -0
- package/dist/cli.js +46 -3
- package/dist/cliEvidenceCommands.js +42 -1
- package/dist/cliInvestigationCommands.js +57 -6
- package/dist/cliLogging.js +20 -3
- package/dist/cliPolicyCommands.js +115 -0
- package/dist/cliProcessCommands.js +4 -14
- package/dist/config.js +63 -3
- package/dist/contracts/artifactToolContracts.js +14 -1
- package/dist/contracts/errorSchemas.js +98 -0
- package/dist/contracts/toolContracts.js +10 -1
- package/dist/contracts/toolOutputSchemas.js +62 -30
- 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 +5 -3
- package/dist/domain/errors.js +216 -3
- package/dist/domain/investigationWorkspace.js +131 -4
- package/dist/domain/permissionPolicy.js +157 -0
- package/dist/domain/processComparison.js +1 -0
- 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 +7 -3
- package/dist/hopper/HopperClient.js +32 -5
- package/dist/identity.js +10 -3
- package/dist/logger.js +2 -2
- package/dist/main.js +95 -7
- package/dist/server/createServer.js +52 -5
- package/dist/server/mcpProgress.js +23 -0
- package/dist/server/registerArtifactComparisonTool.js +6 -2
- package/dist/server/registerBundleComparisonTool.js +6 -2
- package/dist/server/registerEnhancedTools.js +18 -1
- package/dist/server/registerEvidenceResources.js +302 -0
- package/dist/server/registerEvidenceTools.js +58 -1
- package/dist/server/registerFunctionComparisonTool.js +6 -2
- package/dist/server/registerInvestigationTools.js +83 -10
- package/dist/server/registerOfficialTools.js +19 -1
- package/dist/server/registerProcessComparisonTool.js +7 -3
- package/dist/server/registerSessionStatusTool.js +44 -3
- package/dist/server/registerSessionTools.js +129 -7
- package/dist/server/runDerivedOperation.js +35 -0
- package/dist/server/toolResult.js +27 -3
- package/dist/serverIdentity.js +70 -0
- package/package.json +6 -4
|
@@ -30,11 +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)}`,
|
|
37
38
|
`REA_TARGET_PATH = ${JSON.stringify(this.options.targetPath)}`,
|
|
39
|
+
`REA_OWNS_PROCESS_LIFETIME = ${ownsProcessLifetime ? "True" : "False"}`,
|
|
38
40
|
`exec(compile(open(${JSON.stringify(this.options.bridgeScriptPath)}, 'rb').read(), ${JSON.stringify(this.options.bridgeScriptPath)}, 'exec'))`,
|
|
39
41
|
"",
|
|
40
42
|
].join("\n");
|
|
@@ -88,6 +90,7 @@ export class HopperApplicationLauncher {
|
|
|
88
90
|
return ok({
|
|
89
91
|
process: child,
|
|
90
92
|
ownsProcessLifetime: true,
|
|
93
|
+
shutdownByCleanup: ownsProcessLifetime,
|
|
91
94
|
cleanup: () => cleanupOwnedProcessGroup(ownedProcessGroup(session, pid, ownershipCommand)),
|
|
92
95
|
});
|
|
93
96
|
}
|
|
@@ -97,9 +100,7 @@ export class HopperApplicationLauncher {
|
|
|
97
100
|
}
|
|
98
101
|
}
|
|
99
102
|
const linuxDemoLaunch = (options, session, hopperArgs) => {
|
|
100
|
-
if (
|
|
101
|
-
basename(options.launcherPath) !== "Hopper" ||
|
|
102
|
-
options.demoHelperPath === undefined)
|
|
103
|
+
if (!usesLinuxDemo(options) || options.demoHelperPath === undefined)
|
|
103
104
|
return undefined;
|
|
104
105
|
return {
|
|
105
106
|
command: "/usr/bin/python3",
|
|
@@ -116,6 +117,9 @@ const linuxDemoLaunch = (options, session, hopperArgs) => {
|
|
|
116
117
|
],
|
|
117
118
|
};
|
|
118
119
|
};
|
|
120
|
+
const usesLinuxDemo = (options) => process.platform === "linux" &&
|
|
121
|
+
basename(options.launcherPath) === "Hopper" &&
|
|
122
|
+
options.demoHelperPath !== undefined;
|
|
119
123
|
const spawnLauncher = async (command, args, runId) => {
|
|
120
124
|
const child = spawn(command, args, {
|
|
121
125
|
stdio: ["ignore", "ignore", "pipe"],
|
|
@@ -107,16 +107,30 @@ export class HopperClient {
|
|
|
107
107
|
this.#closing = true;
|
|
108
108
|
try {
|
|
109
109
|
const socket = this.#socket;
|
|
110
|
+
let cleanupAttempted = false;
|
|
110
111
|
if (socket !== undefined && !socket.destroyed) {
|
|
111
112
|
const shutdown = await this.#request("shutdown", {}, { timeoutMs: SHUTDOWN_TIMEOUT_MS }).catch(() => err(new HopperProcessError(null)));
|
|
112
|
-
if (
|
|
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))
|
|
113
127
|
this.#logger.warn({ status: shutdown.ok ? "invalid-acknowledgement" : "failed" }, "Hopper document shutdown was not confirmed");
|
|
114
128
|
}
|
|
115
129
|
this.#failAll(new HopperProcessError(null));
|
|
116
130
|
socket?.destroy();
|
|
117
131
|
this.#socket = undefined;
|
|
118
132
|
if (this.#launch?.ownsProcessLifetime === true) {
|
|
119
|
-
if (this.#launch.cleanup !== undefined) {
|
|
133
|
+
if (this.#launch.cleanup !== undefined && !cleanupAttempted) {
|
|
120
134
|
const cleanup = await this.#launch.cleanup();
|
|
121
135
|
if (!cleanup.cleaned)
|
|
122
136
|
this.#logger.warn({ reason: cleanup.reason }, "Owned launcher cleanup failed closed");
|
|
@@ -137,6 +151,11 @@ export class HopperClient {
|
|
|
137
151
|
this.#closePromise = undefined;
|
|
138
152
|
}
|
|
139
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
|
+
}
|
|
140
159
|
async #connect(socketPath, signal) {
|
|
141
160
|
const deadline = Date.now() + this.#options.startupTimeoutMs;
|
|
142
161
|
while (Date.now() < deadline) {
|
|
@@ -319,12 +338,20 @@ const connectOnce = async (socketPath) => new Promise((resolve) => {
|
|
|
319
338
|
resolve(err(new HopperStartError({ cause })));
|
|
320
339
|
});
|
|
321
340
|
});
|
|
322
|
-
const isShutdownAcknowledgement = (value) =>
|
|
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" &&
|
|
323
351
|
value !== null &&
|
|
324
352
|
!Array.isArray(value) &&
|
|
325
353
|
value.shutdown === true &&
|
|
326
|
-
value.
|
|
327
|
-
value.document_closed === true;
|
|
354
|
+
value.cleanup_required === true;
|
|
328
355
|
const parseServerInfo = (value, expectedRunId) => {
|
|
329
356
|
if (typeof value === "object" &&
|
|
330
357
|
value !== null &&
|
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
|
@@ -7,6 +7,8 @@ import { createBinarySession } from "./application/runtime.js";
|
|
|
7
7
|
import { createServer } from "./server/createServer.js";
|
|
8
8
|
import { createLogger } from "./logger.js";
|
|
9
9
|
import { projectAnalysisError } from "./domain/errors.js";
|
|
10
|
+
import { readProjectPermissionStore } from "./application/ProjectPermissionStore.js";
|
|
11
|
+
import { loadConfiguredPermissionAuthority } from "./application/PermissionConfiguration.js";
|
|
10
12
|
const MCP_CONNECTION_LOST = "REA lost its MCP client connection. Restart REA from your MCP client.";
|
|
11
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.";
|
|
12
14
|
const MCP_SHUTDOWN_FAILED = "REA could not close cleanly. End the REA process before starting it again.";
|
|
@@ -24,6 +26,10 @@ const runtimeDependencies = () => ({
|
|
|
24
26
|
process.stdin.once("end", handler);
|
|
25
27
|
process.stdin.once("close", handler);
|
|
26
28
|
},
|
|
29
|
+
registerReload: (handler) => {
|
|
30
|
+
process.on("SIGHUP", handler);
|
|
31
|
+
return () => process.off("SIGHUP", handler);
|
|
32
|
+
},
|
|
27
33
|
});
|
|
28
34
|
/**
|
|
29
35
|
* Start the long-lived MCP adapter and install idempotent shutdown handlers.
|
|
@@ -38,6 +44,11 @@ export const run = async (dependencies = runtimeDependencies()) => {
|
|
|
38
44
|
}
|
|
39
45
|
const logger = createLogger("mcp", config.value.logLevel);
|
|
40
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
|
+
}
|
|
41
52
|
const session = createBinarySession(config.value, logger);
|
|
42
53
|
if (config.value.hopperTargetPath !== undefined) {
|
|
43
54
|
const opened = await session.open(config.value.hopperTargetPath, {
|
|
@@ -50,15 +61,44 @@ export const run = async (dependencies = runtimeDependencies()) => {
|
|
|
50
61
|
return 1;
|
|
51
62
|
}
|
|
52
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();
|
|
53
83
|
let handle;
|
|
54
84
|
try {
|
|
55
|
-
handle = dependencies.serve(() =>
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
+
}, {
|
|
62
102
|
onerror: () => {
|
|
63
103
|
serverLogger.error(MCP_CONNECTION_LOST);
|
|
64
104
|
dependencies.writeStderr(`${MCP_CONNECTION_LOST}\n`);
|
|
@@ -71,11 +111,59 @@ export const run = async (dependencies = runtimeDependencies()) => {
|
|
|
71
111
|
dependencies.writeStderr(`${MCP_CONNECTION_START_FAILED}\n`);
|
|
72
112
|
return 1;
|
|
73
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);
|
|
74
160
|
let shutdownPromise;
|
|
75
161
|
const shutdown = () => {
|
|
76
162
|
shutdownPromise ??= (async () => {
|
|
163
|
+
unregisterReload();
|
|
77
164
|
await handle.close();
|
|
78
165
|
await session.close();
|
|
166
|
+
permissionAuthority.value.clearSessionGrants();
|
|
79
167
|
})();
|
|
80
168
|
return shutdownPromise;
|
|
81
169
|
};
|
|
@@ -7,24 +7,63 @@ import { registerNativeTools } from "./registerNativeTools.js";
|
|
|
7
7
|
import { registerArtifactTools } from "./registerArtifactTools.js";
|
|
8
8
|
import { silentLogger } from "../logger.js";
|
|
9
9
|
import { registerGuidedPrompts } from "./registerPrompts.js";
|
|
10
|
+
import { registerEvidenceResources } from "./registerEvidenceResources.js";
|
|
11
|
+
import { createServerIdentity } from "../serverIdentity.js";
|
|
10
12
|
/**
|
|
11
13
|
* Construct one MCP server without acquiring subprocess resources.
|
|
12
14
|
* Supplying a session adds target lifecycle tools; omitting it retains the
|
|
13
15
|
* fixed-target seam used by focused tests and embedders.
|
|
14
16
|
*/
|
|
15
17
|
export const createServer = (analysis, session, options = {}) => {
|
|
18
|
+
const startedAt = new Date().toISOString();
|
|
16
19
|
const logger = options.logger ?? silentLogger;
|
|
17
|
-
const server = new McpServer({
|
|
18
|
-
|
|
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
|
+
},
|
|
19
28
|
instructions: session === undefined
|
|
20
29
|
? "Reverse-engineering tools for an active analysis target. Start with binary_overview."
|
|
21
30
|
: "Reverse-engineering tools for configured analysis providers. Open a target with open_binary, then start with binary_overview.",
|
|
22
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
|
+
});
|
|
23
57
|
const toolLogger = logger.child({ layer: "server" });
|
|
24
58
|
const activeTarget = session === undefined ? undefined : () => session.activeTarget();
|
|
25
59
|
const recordEvidence = session === undefined
|
|
26
60
|
? undefined
|
|
27
|
-
: (evidence) =>
|
|
61
|
+
: (evidence) => {
|
|
62
|
+
const recorded = session.recordEvidence(evidence);
|
|
63
|
+
if (recorded.ok && recorded.value === "added")
|
|
64
|
+
server.sendResourceListChanged();
|
|
65
|
+
return recorded;
|
|
66
|
+
};
|
|
28
67
|
registerOfficialTools(server, analysis, {
|
|
29
68
|
logger: toolLogger,
|
|
30
69
|
activeTarget,
|
|
@@ -50,9 +89,17 @@ export const createServer = (analysis, session, options = {}) => {
|
|
|
50
89
|
logger: toolLogger,
|
|
51
90
|
activeTarget,
|
|
52
91
|
recordEvidence,
|
|
92
|
+
...(options.permissionAuthority === undefined
|
|
93
|
+
? {}
|
|
94
|
+
: { permissionAuthority: options.permissionAuthority }),
|
|
53
95
|
});
|
|
54
96
|
registerGuidedPrompts(server, analysis, session);
|
|
55
|
-
if (session !== undefined)
|
|
56
|
-
|
|
97
|
+
if (session !== undefined) {
|
|
98
|
+
registerEvidenceResources(server, session);
|
|
99
|
+
registerSessionTools(server, session, toolLogger, {
|
|
100
|
+
...options,
|
|
101
|
+
startedAt,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
57
104
|
return server;
|
|
58
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
|
+
};
|
|
@@ -6,9 +6,10 @@ import { resolveSessionEvidence } from "./sessionEvidence.js";
|
|
|
6
6
|
import { ARTIFACT_COMPARISON_PROVIDER } from "./sessionToolPolicies.js";
|
|
7
7
|
import { toCallToolResult } from "./toolResult.js";
|
|
8
8
|
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
|
|
9
|
+
import { runDerivedOperation } from "./runDerivedOperation.js";
|
|
9
10
|
/** Register Evidence-backed deterministic artifact comparison. */
|
|
10
11
|
export const registerArtifactComparisonTool = (server, session, contract) => {
|
|
11
|
-
server.registerTool(contract.name, toolRegistrationOptions(contract), (input) => {
|
|
12
|
+
server.registerTool(contract.name, toolRegistrationOptions(contract), async (input, context) => {
|
|
12
13
|
const parsed = artifactComparisonInputSchema.parse(input);
|
|
13
14
|
const left = resolveSessionEvidence(session, parsed.left);
|
|
14
15
|
if (!left.ok)
|
|
@@ -16,7 +17,10 @@ export const registerArtifactComparisonTool = (server, session, contract) => {
|
|
|
16
17
|
const right = resolveSessionEvidence(session, parsed.right);
|
|
17
18
|
if (!right.ok)
|
|
18
19
|
return toCallToolResult(right, contract);
|
|
19
|
-
const
|
|
20
|
+
const computed = await runDerivedOperation(context, contract.name, () => compareArtifacts(left.value, right.value, parsed.offset, parsed.limit));
|
|
21
|
+
if (!computed.ok)
|
|
22
|
+
return toCallToolResult(computed, contract);
|
|
23
|
+
const comparison = computed.value;
|
|
20
24
|
const leftEvidenceIds = evidenceIds(parsed.left);
|
|
21
25
|
const rightEvidenceIds = evidenceIds(parsed.right);
|
|
22
26
|
const evidence = createEvidence(undefined, ARTIFACT_COMPARISON_PROVIDER, {
|
|
@@ -7,9 +7,10 @@ import { BUNDLE_COMPARISON_PROVIDER } from "./sessionToolPolicies.js";
|
|
|
7
7
|
import { toCallToolResult } from "./toolResult.js";
|
|
8
8
|
import { isSessionEvidence } from "./sessionEvidence.js";
|
|
9
9
|
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
|
|
10
|
+
import { runDerivedOperation } from "./runDerivedOperation.js";
|
|
10
11
|
/** Register canonical Evidence bundle comparison. */
|
|
11
12
|
export const registerBundleComparisonTool = (server, session, contract) => {
|
|
12
|
-
server.registerTool(contract.name, toolRegistrationOptions(contract), (input) => {
|
|
13
|
+
server.registerTool(contract.name, toolRegistrationOptions(contract), async (input, context) => {
|
|
13
14
|
const parsed = bundleComparisonInputSchema.parse(input);
|
|
14
15
|
const sourceIds = [
|
|
15
16
|
...new Set([
|
|
@@ -24,7 +25,10 @@ export const registerBundleComparisonTool = (server, session, contract) => {
|
|
|
24
25
|
.unknowns.map(({ revision_digest: digest }) => digest));
|
|
25
26
|
if ([...parsed.left.unknowns, ...parsed.right.unknowns].some(({ revision_digest: digest }) => !ownedUnknownDigests.has(digest)))
|
|
26
27
|
return toCallToolResult(err(new EvidenceIntegrityError("Bundle comparison input unknown history is not present in this session")), contract);
|
|
27
|
-
const
|
|
28
|
+
const computed = await runDerivedOperation(context, contract.name, () => compareBundles(parsed.left, parsed.right, parsed.record_pairs, parsed.offset, parsed.limit));
|
|
29
|
+
if (!computed.ok)
|
|
30
|
+
return toCallToolResult(computed, contract);
|
|
31
|
+
const comparison = computed.value;
|
|
28
32
|
const evidence = createEvidence(undefined, BUNDLE_COMPARISON_PROVIDER, {
|
|
29
33
|
predicateType: "rea.bundle-comparison/v1",
|
|
30
34
|
operation: contract.name,
|
|
@@ -7,6 +7,7 @@ import { createEvidence } from "../domain/evidence.js";
|
|
|
7
7
|
import { jsonObjectSchema } from "../domain/jsonValue.js";
|
|
8
8
|
import { enhancedInputSchemas } from "../contracts/enhancedInputs.js";
|
|
9
9
|
import { UnknownRegistryError } from "../domain/errors.js";
|
|
10
|
+
import { mcpProgressReporter } from "./mcpProgress.js";
|
|
10
11
|
const WORKFLOW_PROVIDER = {
|
|
11
12
|
id: "rea-workflow",
|
|
12
13
|
name: "REA composed investigation workflow",
|
|
@@ -32,9 +33,23 @@ const registerEnhancedTool = (server, services, contract, registration) => {
|
|
|
32
33
|
outputSchema: contract.outputSchema,
|
|
33
34
|
annotations: contract.annotations,
|
|
34
35
|
}, async (input, context) => {
|
|
36
|
+
const progress = mcpProgressReporter(context);
|
|
37
|
+
await progress.report({
|
|
38
|
+
phase: name,
|
|
39
|
+
completed: 0,
|
|
40
|
+
total: 1,
|
|
41
|
+
message: "started",
|
|
42
|
+
});
|
|
35
43
|
const parsedInput = jsonObjectSchema.parse(enhancedInputSchemas[name].parse(input));
|
|
36
44
|
const parameters = { ...parsedInput };
|
|
37
45
|
const result = await logToolExecution(registration.logger, name, () => services.execute(name, input, context.mcpReq.signal));
|
|
46
|
+
await progress.report({
|
|
47
|
+
phase: name,
|
|
48
|
+
completed: 1,
|
|
49
|
+
total: 1,
|
|
50
|
+
message: result.ok ? "completed" : "failed",
|
|
51
|
+
terminal: true,
|
|
52
|
+
});
|
|
38
53
|
if (result.ok) {
|
|
39
54
|
const evidence = createEvidence(registration.activeTarget?.(), WORKFLOW_PROVIDER, {
|
|
40
55
|
operation: name,
|
|
@@ -57,7 +72,9 @@ const registerEnhancedTool = (server, services, contract, registration) => {
|
|
|
57
72
|
});
|
|
58
73
|
if (!unknowns.ok)
|
|
59
74
|
return toCallToolResult(unknowns, contract);
|
|
60
|
-
return toCallToolResult({ ok: true, value: evidence }, contract
|
|
75
|
+
return toCallToolResult({ ok: true, value: evidence }, contract, {
|
|
76
|
+
evidenceResourcesAvailable: recorded !== undefined,
|
|
77
|
+
});
|
|
61
78
|
}
|
|
62
79
|
return toCallToolResult(result, contract);
|
|
63
80
|
});
|