mirror of
https://github.com/morluto/rea
synced 2026-10-26 06:16:17 -04:00
fix(process): capture MCP process scenarios with the server environment (#6173)
This commit is contained in:
@@ -88,6 +88,7 @@ interface ProcessToolRegistration {
|
||||
typeof toolContract<"capture_process_scenario">
|
||||
>;
|
||||
readonly resourceScope: ProcessCaptureResourceScope;
|
||||
readonly environment: Readonly<NodeJS.ProcessEnv>;
|
||||
}
|
||||
|
||||
const registerProcessTools = ({
|
||||
@@ -96,6 +97,7 @@ const registerProcessTools = ({
|
||||
logger,
|
||||
captureContract,
|
||||
resourceScope,
|
||||
environment,
|
||||
}: ProcessToolRegistration): void => {
|
||||
const admission = withAdmittedAnalysis({ kind: "session", session });
|
||||
server.registerTool(
|
||||
@@ -117,7 +119,7 @@ const registerProcessTools = ({
|
||||
input,
|
||||
context.mcpReq.signal,
|
||||
process.platform,
|
||||
process.env,
|
||||
environment,
|
||||
undefined,
|
||||
undefined,
|
||||
progress,
|
||||
@@ -223,6 +225,8 @@ const registerOpenLifecycleTool = (
|
||||
/** Register MCP-only target lifecycle operations on a long-lived session. */
|
||||
export interface SessionToolOptions extends Partial<SessionAvailabilityProbes> {
|
||||
readonly artifactResources: ArtifactResourceScope;
|
||||
/** Server environment snapshot that process capture resolves and inherits. */
|
||||
readonly environment: Readonly<NodeJS.ProcessEnv>;
|
||||
readonly startedAt?: string;
|
||||
readonly availabilityPolicy?: () => AvailabilityPolicy;
|
||||
}
|
||||
@@ -381,6 +385,7 @@ export const registerSessionTools = (
|
||||
logger,
|
||||
captureContract,
|
||||
resourceScope: processResources,
|
||||
environment: options.environment,
|
||||
});
|
||||
const closeServer = server.close.bind(server);
|
||||
server.close = async () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { access } from "node:fs/promises";
|
||||
import { access, chmod, mkdir, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
|
||||
@@ -233,3 +233,55 @@ itWithCaptureCapability(
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
itWithCaptureCapability(
|
||||
"resolves and inherits the server's selected environment through MCP",
|
||||
async () => {
|
||||
const root = await createTestTempDirectory("rea-process-mcp-env-");
|
||||
const bin = join(root, "bin");
|
||||
const executable = join(bin, "rea-injected-process-probe");
|
||||
await mkdir(bin);
|
||||
await writeFile(
|
||||
executable,
|
||||
[
|
||||
"#!/bin/sh",
|
||||
'printf "%s|%s\\n" "$REA_CAPTURE_INJECTED_MARKER" "$REA_CAPTURE_SCENARIO_OVERRIDE"',
|
||||
].join("\n"),
|
||||
);
|
||||
await chmod(executable, 0o755);
|
||||
const session = createTestBinarySession(createCacheProvider([]));
|
||||
const server = createServer(
|
||||
{ kind: "session", session },
|
||||
{
|
||||
logger: silentLogger,
|
||||
environment: {
|
||||
PATH: bin,
|
||||
REA_CAPTURE_INJECTED_MARKER: "injected-value",
|
||||
REA_CAPTURE_SCENARIO_OVERRIDE: "host-value",
|
||||
},
|
||||
},
|
||||
);
|
||||
const client = new Client({ name: "process-mcp-env", version: "1" });
|
||||
resources.push(client, server, session);
|
||||
const [clientTransport, serverTransport] =
|
||||
InMemoryTransport.createLinkedPair();
|
||||
await server.connect(serverTransport);
|
||||
await client.connect(clientTransport);
|
||||
|
||||
const result = await client.callTool({
|
||||
name: "capture_process_scenario",
|
||||
arguments: {
|
||||
executable: "rea-injected-process-probe",
|
||||
working_directory: root,
|
||||
environment: { REA_CAPTURE_SCENARIO_OVERRIDE: "scenario-value" },
|
||||
},
|
||||
});
|
||||
expect(result.isError, JSON.stringify(result.content)).not.toBe(true);
|
||||
const capture = toolContract("capture_process_scenario").outputSchema.parse(
|
||||
result.structuredContent,
|
||||
).normalized_result;
|
||||
expect(capture.frames.map(({ data }) => data).join("")).toContain(
|
||||
"injected-value|scenario-value",
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
|
||||
import { afterEach, expect, it, vi } from "vitest";
|
||||
|
||||
import { AnalysisCapabilityUnavailableError } from "../../src/domain/analysisErrorCore.js";
|
||||
import { err } from "../../src/domain/result.js";
|
||||
import { captureProcessScenario } from "../../src/process/capture/ProcessHarness.js";
|
||||
import { createServer } from "../../src/server/createServer.js";
|
||||
import { createTestBinarySession } from "../fixtures/binarySession.js";
|
||||
|
||||
// Windows rejects capture before it reads the environment, so record the
|
||||
// environment the MCP adapter hands to the capture owner on every host.
|
||||
vi.mock(
|
||||
"../../src/process/capture/ProcessHarness.js",
|
||||
async (importOriginal) => ({
|
||||
...(await importOriginal<
|
||||
typeof import("../../src/process/capture/ProcessHarness.js")
|
||||
>()),
|
||||
captureProcessScenario: vi.fn(async () =>
|
||||
err(
|
||||
new AnalysisCapabilityUnavailableError(
|
||||
"rea-process",
|
||||
"capture_process_scenario",
|
||||
"recorded by test",
|
||||
),
|
||||
),
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
const resources: Array<{ close(): Promise<unknown> }> = [];
|
||||
afterEach(async () => {
|
||||
await Promise.all(resources.splice(0).map((resource) => resource.close()));
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
it("captures MCP process scenarios with the server's selected environment", async () => {
|
||||
vi.stubEnv("REA_CAPTURE_SELECTION_MARKER", "ambient");
|
||||
vi.stubEnv("REA_CAPTURE_AMBIENT_ONLY", "ambient");
|
||||
const session = createTestBinarySession(() => {
|
||||
throw new Error("Process capture must not launch a binary provider");
|
||||
});
|
||||
const server = createServer(
|
||||
{ kind: "session", session },
|
||||
{ environment: { REA_CAPTURE_SELECTION_MARKER: "selected" } },
|
||||
);
|
||||
const client = new Client({ name: "capture-environment", version: "1" });
|
||||
resources.push(client, server, session);
|
||||
const [clientTransport, serverTransport] =
|
||||
InMemoryTransport.createLinkedPair();
|
||||
await server.connect(serverTransport);
|
||||
await client.connect(clientTransport);
|
||||
|
||||
const result = await client.callTool({
|
||||
name: "capture_process_scenario",
|
||||
arguments: { executable: process.execPath },
|
||||
});
|
||||
|
||||
expect(result.isError).toBe(true);
|
||||
const environment = vi.mocked(captureProcessScenario).mock.lastCall?.[3];
|
||||
expect(environment).toEqual({ REA_CAPTURE_SELECTION_MARKER: "selected" });
|
||||
});
|
||||
Reference in New Issue
Block a user