mirror of
https://github.com/morluto/rea.git
synced 2026-10-29 02:45:58 +00:00
Remove REA permission grants, scope ceilings, approval confirmations, and inert settings. Preserve selected local evidence, accept caller-chosen commands and paths, and report actual runtime failures. Harden provider results, schema serialization, snapshot evidence binding, archive paths, source maps, native screenshots, managed EH parsing, and cancellation cleanup. Regenerate tool metadata and document direct invocation. BREAKING CHANGE: Removed permission configuration and policy commands, approval fields, filesystem scope inputs, and fixed confirmation options. Process capture inherits the host environment and accepts command names; callers must refresh tool schemas and migrate removed fields.
220 lines
7.0 KiB
TypeScript
220 lines
7.0 KiB
TypeScript
import { rm, writeFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { pathToFileURL } from "node:url";
|
|
|
|
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
|
|
import { afterEach, describe, expect, test } from "vitest";
|
|
|
|
import { createTestBinarySession } from "../../fixtures/binarySession.js";
|
|
import { observeJavaScriptRuntime } from "../../../src/application/JavaScriptRuntimeObservationService.js";
|
|
import { V8InspectorProvider } from "../../../src/browser/V8InspectorProvider.js";
|
|
import {
|
|
javascriptRuntimeObservationSchema,
|
|
javascriptRuntimeTargetListSchema,
|
|
observeJavaScriptRuntimeInputSchema,
|
|
} from "../../../src/domain/javascriptRuntimeObservation.js";
|
|
import { createServer } from "../../../src/server/createServer.js";
|
|
import { observed } from "../../fixtures/analysisExecution.js";
|
|
import { startFakeV8Inspector } from "../../fixtures/fakeV8Inspector.js";
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
|
|
|
describe("JavaScript runtime observation MCP tools", () => {
|
|
const resources: Array<{ close(): Promise<unknown> }> = [];
|
|
const temporary: string[] = [];
|
|
|
|
afterEach(async () => {
|
|
await Promise.all(resources.splice(0).map((item) => item.close()));
|
|
await Promise.all(
|
|
temporary
|
|
.splice(0)
|
|
.map((path) => rm(path, { recursive: true, force: true })),
|
|
);
|
|
});
|
|
|
|
test("accepts long caller windows and complete observation metadata", () => {
|
|
const repeated = (count: number): string[] =>
|
|
Array.from({ length: count }, (_, index) => `item-${index}`);
|
|
const scope = {
|
|
inspector_endpoint: "http://127.0.0.1:9222",
|
|
};
|
|
expect(
|
|
observeJavaScriptRuntimeInputSchema.parse({
|
|
...scope,
|
|
target_id: "target",
|
|
observation_ms: 2_147_483_648,
|
|
}).observation_ms,
|
|
).toBe(2_147_483_648);
|
|
expect(
|
|
javascriptRuntimeTargetListSchema.parse({
|
|
runtime: {
|
|
product: "Node.js",
|
|
protocol_version: "1.0",
|
|
v8_version: null,
|
|
},
|
|
targets: [],
|
|
excluded: {
|
|
unsupported_location: 0,
|
|
unconnectable: 0,
|
|
},
|
|
limitations: repeated(101),
|
|
}).limitations,
|
|
).toHaveLength(101);
|
|
|
|
const unknowns = repeated(101);
|
|
const limitations = repeated(101);
|
|
expect(
|
|
javascriptRuntimeObservationSchema.parse({
|
|
runtime: {
|
|
product: "Node.js",
|
|
protocol_version: "1.0",
|
|
v8_version: null,
|
|
},
|
|
target: {
|
|
target_id: "target",
|
|
protocol_type: "node",
|
|
attached: false,
|
|
location: { kind: "file", file_path: "/tmp/rea-runtime/main.js" },
|
|
runtime_kind: "node",
|
|
runtime_kind_authority: "caller-declared-unverified",
|
|
},
|
|
capture: {
|
|
observation_ms: 10_001,
|
|
events_observed: 0,
|
|
events_retained: 0,
|
|
events_dropped: 0,
|
|
metadata_bytes_retained: 0,
|
|
truncated: false,
|
|
truncation_reasons: repeated(21),
|
|
},
|
|
scripts: {
|
|
items: [],
|
|
observed_total: 0,
|
|
excluded: {
|
|
unsupported_location: 0,
|
|
invalid_protocol_value: 0,
|
|
},
|
|
},
|
|
execution_contexts: [],
|
|
directly_observed: repeated(21),
|
|
unavailable_without_instrumentation: repeated(21),
|
|
unknowns,
|
|
limitations,
|
|
}),
|
|
).toMatchObject({ unknowns, limitations });
|
|
});
|
|
|
|
test("lists and observes one target as retained Evidence", async () => {
|
|
const root = await createTestTempDirectory("rea-v8-mcp-");
|
|
temporary.push(root);
|
|
const { entry, inspector, client } = await createObservationClient(
|
|
root,
|
|
resources,
|
|
);
|
|
|
|
const listed = await client.callTool({
|
|
name: "list_javascript_runtime_targets",
|
|
arguments: {
|
|
inspector_endpoint: inspector.endpoint,
|
|
},
|
|
});
|
|
expect(listed.isError).not.toBe(true);
|
|
expect(listed.structuredContent).toMatchObject({
|
|
result: {
|
|
targets: [{ target_id: inspector.targetId }],
|
|
},
|
|
});
|
|
const observedRuntime = await client.callTool({
|
|
name: "observe_javascript_runtime",
|
|
arguments: {
|
|
inspector_endpoint: inspector.endpoint,
|
|
target_id: inspector.targetId,
|
|
runtime_kind: "node",
|
|
observation_ms: 10,
|
|
},
|
|
});
|
|
expect(observedRuntime.isError).not.toBe(true);
|
|
expect(observedRuntime.structuredContent).toMatchObject({
|
|
result: {
|
|
target: {
|
|
target_id: inspector.targetId,
|
|
runtime_kind: "node",
|
|
runtime_kind_authority: "caller-declared-unverified",
|
|
},
|
|
scripts: {
|
|
items: [
|
|
expect.objectContaining({
|
|
location: { kind: "file", file_path: entry },
|
|
}),
|
|
],
|
|
},
|
|
},
|
|
});
|
|
const evidenceId = evidenceIdFrom(observedRuntime.structuredContent);
|
|
expect(observedRuntime.structuredContent).toMatchObject({
|
|
evidence: {
|
|
evidence_id: evidenceId,
|
|
operation: "observe_javascript_runtime",
|
|
parameters: expect.any(Object),
|
|
},
|
|
});
|
|
expect(new Set(inspector.commands.map(({ method }) => method))).toEqual(
|
|
new Set(["Runtime.enable", "Debugger.enable"]),
|
|
);
|
|
const direct = await observeDirectly(inspector);
|
|
expect(direct.ok).toBe(true);
|
|
if (direct.ok) expect(evidenceId).toBe(direct.value.evidence_id);
|
|
});
|
|
});
|
|
|
|
const createObservationClient = async (
|
|
root: string,
|
|
resources: Array<{ close(): Promise<unknown> }>,
|
|
) => {
|
|
const entry = join(root, "entry.js");
|
|
await writeFile(entry, "export const value = 1;\n");
|
|
const inspector = await startFakeV8Inspector({
|
|
targetUrl: pathToFileURL(entry).href,
|
|
});
|
|
resources.push(inspector);
|
|
const session = createTestBinarySession(() => ({
|
|
execute: () => Promise.resolve(observed(null)),
|
|
close: () => Promise.resolve(),
|
|
}));
|
|
const server = createServer(session, session, {
|
|
javascriptRuntimeObservation: new V8InspectorProvider(),
|
|
availabilityPolicy: () => ({
|
|
processCaptureEnabled: false,
|
|
investigationInputRoots: 0,
|
|
}),
|
|
});
|
|
const client = new Client({ name: "v8-mcp-test", version: "1" });
|
|
const [clientTransport, serverTransport] =
|
|
InMemoryTransport.createLinkedPair();
|
|
resources.push(client, server, session);
|
|
await server.connect(serverTransport);
|
|
await client.connect(clientTransport);
|
|
return { entry, inspector, client };
|
|
};
|
|
|
|
const observeDirectly = (
|
|
inspector: Awaited<ReturnType<typeof startFakeV8Inspector>>,
|
|
) =>
|
|
observeJavaScriptRuntime(
|
|
new V8InspectorProvider(),
|
|
observeJavaScriptRuntimeInputSchema.parse({
|
|
inspector_endpoint: inspector.endpoint,
|
|
target_id: inspector.targetId,
|
|
runtime_kind: "node",
|
|
observation_ms: 10,
|
|
}),
|
|
);
|
|
|
|
const evidenceIdFrom = (value: unknown): string => {
|
|
if (typeof value !== "object" || value === null)
|
|
throw new TypeError("Missing structured result");
|
|
const evidenceId = Reflect.get(value, "evidence_id");
|
|
if (typeof evidenceId !== "string")
|
|
throw new TypeError("Missing Evidence ID");
|
|
return evidenceId;
|
|
};
|