mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
fix(evm): probe the selected limiter before advertising availability
Share asynchronous executable selection between discovery and execution. Recheck it for default Linux x64 sessions, preserve caller-injected service policy and propagate cancellation through pending probes. Refs #5086. Focused owner, inventory and MCP controls passed; Linux-specific discovery and execution cases were skipped on Darwin arm64 and remain unverified.
This commit is contained in:
1 parent
2b2e362a00
commit
a74d859a19
7 files changed
+569
-58
No files matched your search
@@ -56,6 +56,8 @@ export type AvailabilityPolicy = {
|
||||
readonly signalRecordingEnabled?: boolean;
|
||||
readonly wasmArtifactEnabled?: boolean;
|
||||
readonly evmInterfaceEnabled?: boolean;
|
||||
/** Fresh owner observation of the selected required EVM limiter. */
|
||||
readonly evmLimiterAvailability?: ProviderAvailability;
|
||||
readonly processCaptureEnabled: boolean;
|
||||
readonly optionalProviderLoadFailures?: OptionalProviderLoadFailures;
|
||||
readonly binaryLayoutEnabled?: boolean;
|
||||
@@ -495,14 +497,18 @@ const workflowAvailabilityFor = ({
|
||||
};
|
||||
if (name === "inspect_pe_resources")
|
||||
return { reason: "available", remediation: null };
|
||||
if (name === "inspect_evm_interface")
|
||||
return policy.evmInterfaceEnabled === true
|
||||
? { reason: "available", remediation: null }
|
||||
: {
|
||||
reason: "provider_missing",
|
||||
remediation:
|
||||
"The bundled offline EVM interface profile is currently real-verified on Linux x64. No active binary target or chain endpoint is required.",
|
||||
};
|
||||
if (name === "inspect_evm_interface") {
|
||||
if (policy.evmInterfaceEnabled !== true)
|
||||
return {
|
||||
reason: "provider_missing",
|
||||
remediation:
|
||||
"The bundled offline EVM interface profile is currently real-verified on Linux x64. No active binary target or chain endpoint is required.",
|
||||
};
|
||||
const limiter = policy.evmLimiterAvailability;
|
||||
if (limiter?.status === "unavailable")
|
||||
return { reason: "provider_missing", remediation: limiter.reason };
|
||||
return { reason: "available", remediation: null };
|
||||
}
|
||||
if (name === "inspect_recorded_crash")
|
||||
return policy.recordedCrashEnabled === true
|
||||
? { reason: "available", remediation: null }
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { EvmInterfaceService } from "../application/evm/EvmInterfaceService.js";
|
||||
import { EvmoleInterfaceProvider } from "../evm/EvmoleInterfaceProvider.js";
|
||||
import {
|
||||
EvmoleInterfaceProvider,
|
||||
evmLimiterAvailability,
|
||||
} from "../evm/EvmoleInterfaceProvider.js";
|
||||
import { CLI_COMMANDS } from "../cliCommandNames.js";
|
||||
import { INSPECT_EVM_INTERFACE_CONTRACT } from "../contracts/evm/evmToolContracts.js";
|
||||
|
||||
@@ -9,6 +12,8 @@ export const createEvmInterfaceService = (
|
||||
): EvmInterfaceService =>
|
||||
new EvmInterfaceService(new EvmoleInterfaceProvider(environment));
|
||||
|
||||
export { evmLimiterAvailability };
|
||||
|
||||
/**
|
||||
* Static binding of the existing operation, command and fresh service factory.
|
||||
* The host owns injected service identity and its retryable cleanup lifetime.
|
||||
|
||||
@@ -7,6 +7,7 @@ import { z } from "zod";
|
||||
import {
|
||||
createAnalysisExecution,
|
||||
type ExecutionOptions,
|
||||
type ProviderAvailability,
|
||||
} from "../application/AnalysisProvider.js";
|
||||
import type {
|
||||
EvmInterfaceExecution,
|
||||
@@ -69,6 +70,100 @@ import {
|
||||
const OPERATION = "inspect_evm_interface";
|
||||
const HOST_REQUIREMENT =
|
||||
"Initial real-verified offline EVM interface profile supports Linux x64 only.";
|
||||
export const EVM_PRLIMIT_COMMAND_DEFAULT = "/usr/bin/prlimit";
|
||||
|
||||
export interface EvmLimiterProbeFileSystem {
|
||||
access(path: string, mode: number): Promise<void>;
|
||||
stat(path: string): Promise<{ isFile(): boolean }>;
|
||||
}
|
||||
|
||||
export interface EvmLimiterProbeOptions {
|
||||
readonly platform?: NodeJS.Platform;
|
||||
readonly arch?: string;
|
||||
readonly fs?: EvmLimiterProbeFileSystem;
|
||||
readonly signal?: AbortSignal;
|
||||
}
|
||||
|
||||
type EvmLimiterSelection = {
|
||||
readonly path: string;
|
||||
readonly diagnostics: Readonly<Record<string, string>>;
|
||||
};
|
||||
type EvmLimiterUnavailable = EvmLimiterSelection & {
|
||||
readonly reason: string;
|
||||
readonly cause: unknown;
|
||||
};
|
||||
|
||||
/** Select and validate the exact limiter using the same owner check at discovery and admission. */
|
||||
const selectEvmLimiter = async (
|
||||
environment: Readonly<NodeJS.ProcessEnv>,
|
||||
probes?: {
|
||||
readonly platform?: NodeJS.Platform;
|
||||
readonly arch?: string;
|
||||
readonly fs?: EvmLimiterProbeFileSystem;
|
||||
},
|
||||
signal?: AbortSignal,
|
||||
operation = OPERATION,
|
||||
): Promise<Result<EvmLimiterSelection, EvmLimiterUnavailable> | undefined> => {
|
||||
const throwIfAborted = (): void => {
|
||||
if (signal?.aborted)
|
||||
throw new AnalysisCancelledError(operation, { cause: signal.reason });
|
||||
};
|
||||
throwIfAborted();
|
||||
const platform = probes?.platform ?? process.platform;
|
||||
const arch = probes?.arch ?? process.arch;
|
||||
if (platform !== "linux" || arch !== "x64") return undefined;
|
||||
|
||||
const path =
|
||||
environment.REA_EVM_PRLIMIT_COMMAND ?? EVM_PRLIMIT_COMMAND_DEFAULT;
|
||||
const diagnostics = { limiter_path: path, platform, arch };
|
||||
try {
|
||||
if (!isAbsolute(path))
|
||||
throw new Error(
|
||||
"REA_EVM_PRLIMIT_COMMAND must be an absolute executable path.",
|
||||
);
|
||||
const fs = probes?.fs ?? { access, stat };
|
||||
await fs.access(path, constants.X_OK);
|
||||
throwIfAborted();
|
||||
if (!(await fs.stat(path)).isFile())
|
||||
throw new Error(
|
||||
"Configured prlimit must be a regular executable file, not a directory.",
|
||||
);
|
||||
throwIfAborted();
|
||||
return ok({ path, diagnostics });
|
||||
} catch (cause: unknown) {
|
||||
throwIfAborted();
|
||||
const reason = `Offline EVM interface inspection requires caller-supplied util-linux prlimit: ${path}; ${cause instanceof Error ? cause.message : String(cause)}`;
|
||||
return err({ path, diagnostics, reason, cause });
|
||||
}
|
||||
};
|
||||
|
||||
/** Fresh, process-free observation of the limiter prerequisite used by EVM execution. */
|
||||
export const evmLimiterAvailability = async (
|
||||
environment: Readonly<NodeJS.ProcessEnv>,
|
||||
probes?: EvmLimiterProbeOptions,
|
||||
): Promise<ProviderAvailability | undefined> => {
|
||||
const selected = await selectEvmLimiter(
|
||||
environment,
|
||||
probes,
|
||||
probes?.signal,
|
||||
"binary_session",
|
||||
);
|
||||
if (selected === undefined) return undefined;
|
||||
return selected.ok
|
||||
? {
|
||||
status: "available",
|
||||
code: null,
|
||||
reason: null,
|
||||
diagnostics: selected.value.diagnostics,
|
||||
}
|
||||
: {
|
||||
status: "unavailable",
|
||||
code: "executable_missing",
|
||||
reason: selected.error.reason,
|
||||
diagnostics: selected.error.diagnostics,
|
||||
};
|
||||
};
|
||||
|
||||
const replySchema = z.discriminatedUnion("ok", [
|
||||
z.strictObject({
|
||||
ok: z.literal(true),
|
||||
@@ -307,8 +402,8 @@ export class EvmoleInterfaceProvider implements EvmInterfacePort {
|
||||
let retainedOutput: AnalysisCapturedOutput | undefined;
|
||||
let phase: "configuration" | "artifact-read" | "worker" = "configuration";
|
||||
let workerLimits: EvmWorkerLimits | undefined;
|
||||
const limiter =
|
||||
this.environment.REA_EVM_PRLIMIT_COMMAND ?? "/usr/bin/prlimit";
|
||||
let limiter =
|
||||
this.environment.REA_EVM_PRLIMIT_COMMAND ?? EVM_PRLIMIT_COMMAND_DEFAULT;
|
||||
let result: Result<EvmInterfaceExecution, AnalysisError>;
|
||||
try {
|
||||
if (options?.signal?.aborted)
|
||||
@@ -322,25 +417,31 @@ export class EvmoleInterfaceProvider implements EvmInterfacePort {
|
||||
HOST_REQUIREMENT,
|
||||
{ userMessage: HOST_REQUIREMENT },
|
||||
);
|
||||
try {
|
||||
if (!isAbsolute(limiter))
|
||||
throw new Error(
|
||||
"REA_EVM_PRLIMIT_COMMAND must be an absolute executable path.",
|
||||
);
|
||||
await access(limiter, constants.X_OK);
|
||||
if (!(await stat(limiter)).isFile())
|
||||
throw new Error(
|
||||
"Configured prlimit must be a regular executable file, not a directory.",
|
||||
);
|
||||
} catch (cause: unknown) {
|
||||
const reason = `Offline EVM interface inspection requires caller-supplied util-linux prlimit: ${limiter}; ${cause instanceof Error ? cause.message : String(cause)}`;
|
||||
const selectedLimiter = await selectEvmLimiter(
|
||||
this.environment,
|
||||
undefined,
|
||||
options?.signal,
|
||||
);
|
||||
if (selectedLimiter === undefined)
|
||||
throw new AnalysisCapabilityUnavailableError(
|
||||
EVMOLE_PROVIDER_IDENTITY.id,
|
||||
OPERATION,
|
||||
reason,
|
||||
{ cause, userMessage: reason },
|
||||
HOST_REQUIREMENT,
|
||||
{ userMessage: HOST_REQUIREMENT },
|
||||
);
|
||||
if (!selectedLimiter.ok) {
|
||||
limiter = selectedLimiter.error.path;
|
||||
throw new AnalysisCapabilityUnavailableError(
|
||||
EVMOLE_PROVIDER_IDENTITY.id,
|
||||
OPERATION,
|
||||
selectedLimiter.error.reason,
|
||||
{
|
||||
cause: selectedLimiter.error.cause,
|
||||
userMessage: selectedLimiter.error.reason,
|
||||
},
|
||||
);
|
||||
}
|
||||
limiter = selectedLimiter.value.path;
|
||||
let inheritedLimits: string;
|
||||
try {
|
||||
inheritedLimits = await readFile("/proc/self/limits", "utf8");
|
||||
|
||||
@@ -14,7 +14,10 @@ import { createPeResourcesService } from "../composition/binaryDiagnostics.js";
|
||||
import type { PeResourcesService } from "../application/binaryDiagnostics/PeResourcesService.js";
|
||||
import { registerPeResourcesTool } from "./registerPeResourcesTool.js";
|
||||
import type { EvmInterfaceService } from "../application/evm/EvmInterfaceService.js";
|
||||
import { evmInterfaceModule } from "../composition/evm.js";
|
||||
import {
|
||||
evmInterfaceModule,
|
||||
evmLimiterAvailability,
|
||||
} from "../composition/evm.js";
|
||||
import { registerEvmTools } from "./registerEvmTools.js";
|
||||
import type { SqliteDatabaseService } from "../application/sqlite/SqliteDatabaseService.js";
|
||||
import { createSqliteDatabaseService } from "../composition/sqlite.js";
|
||||
@@ -252,6 +255,12 @@ const installSessionToolAvailability = (
|
||||
};
|
||||
return {
|
||||
policy,
|
||||
...(options.evmInterface !== undefined || !linuxX64
|
||||
? {}
|
||||
: {
|
||||
evmLimiterAvailability: (signal: AbortSignal) =>
|
||||
evmLimiterAvailability(environment, { signal }),
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -617,6 +626,9 @@ export const createServer = (
|
||||
...(availability === undefined
|
||||
? {}
|
||||
: { availabilityPolicy: availability.policy }),
|
||||
...(availability?.evmLimiterAvailability === undefined
|
||||
? {}
|
||||
: { evmLimiterAvailability: availability.evmLimiterAvailability }),
|
||||
androidAnalysisAvailability: (signal) =>
|
||||
android.inspectAvailability(signal),
|
||||
adbAnalysisAvailability: (signal) =>
|
||||
|
||||
@@ -35,6 +35,9 @@ export interface SessionAvailabilityProbes {
|
||||
readonly jebAnalysisAvailability?: (
|
||||
signal: AbortSignal,
|
||||
) => Promise<JebAnalysisAvailability>;
|
||||
readonly evmLimiterAvailability?: (
|
||||
signal: AbortSignal,
|
||||
) => Promise<ProviderAvailability | undefined>;
|
||||
}
|
||||
|
||||
/** Inputs required to register the binary session status tool. */
|
||||
@@ -46,21 +49,35 @@ export interface SessionStatusToolOptions extends SessionAvailabilityProbes {
|
||||
readonly availabilityPolicy: () => AvailabilityPolicy;
|
||||
}
|
||||
|
||||
const probeSessionAvailability = (
|
||||
probes: SessionAvailabilityProbes,
|
||||
signal: AbortSignal,
|
||||
): Promise<{
|
||||
readonly android: ProviderAvailability;
|
||||
readonly adb: ProviderAvailability | undefined;
|
||||
readonly apktool: ProviderAvailability | undefined;
|
||||
readonly jeb: JebAnalysisAvailability | undefined;
|
||||
readonly evmLimiter: ProviderAvailability | undefined;
|
||||
}> =>
|
||||
Promise.all([
|
||||
probes.androidAnalysisAvailability(signal),
|
||||
probes.adbAnalysisAvailability?.(signal) ?? undefined,
|
||||
probes.apktoolAnalysisAvailability?.(signal) ?? undefined,
|
||||
probes.jebAnalysisAvailability?.(signal) ?? undefined,
|
||||
probes.evmLimiterAvailability?.(signal) ?? undefined,
|
||||
] as const).then(([android, adb, apktool, jeb, evmLimiter]) => ({
|
||||
android,
|
||||
adb,
|
||||
apktool,
|
||||
jeb,
|
||||
evmLimiter,
|
||||
}));
|
||||
|
||||
/** Register the read-only provider and target status operation. */
|
||||
export const registerSessionStatusTool = (
|
||||
options: SessionStatusToolOptions,
|
||||
): void => {
|
||||
const {
|
||||
server,
|
||||
session,
|
||||
contract,
|
||||
startedAt,
|
||||
availabilityPolicy,
|
||||
androidAnalysisAvailability,
|
||||
adbAnalysisAvailability,
|
||||
apktoolAnalysisAvailability,
|
||||
jebAnalysisAvailability,
|
||||
} = options;
|
||||
const { server, session, contract, startedAt, availabilityPolicy } = options;
|
||||
server.registerTool(
|
||||
contract.name,
|
||||
toolRegistrationOptions(contract),
|
||||
@@ -69,22 +86,15 @@ export const registerSessionStatusTool = (
|
||||
context,
|
||||
server.server,
|
||||
);
|
||||
let androidAvailability: ProviderAvailability;
|
||||
let adbAvailability: ProviderAvailability | undefined;
|
||||
let apktoolAvailability: ProviderAvailability | undefined;
|
||||
let jebAvailability: JebAnalysisAvailability | undefined;
|
||||
let availabilityResults: Awaited<
|
||||
ReturnType<typeof probeSessionAvailability>
|
||||
>;
|
||||
let status: JsonValue;
|
||||
try {
|
||||
const [android, adb, apktool, jeb] = await Promise.all([
|
||||
androidAnalysisAvailability(context.mcpReq.signal),
|
||||
adbAnalysisAvailability?.(context.mcpReq.signal) ?? undefined,
|
||||
apktoolAnalysisAvailability?.(context.mcpReq.signal) ?? undefined,
|
||||
jebAnalysisAvailability?.(context.mcpReq.signal) ?? undefined,
|
||||
] as const);
|
||||
androidAvailability = android;
|
||||
adbAvailability = adb;
|
||||
apktoolAvailability = apktool;
|
||||
jebAvailability = jeb;
|
||||
availabilityResults = await probeSessionAvailability(
|
||||
options,
|
||||
context.mcpReq.signal,
|
||||
);
|
||||
status = await session.status({ signal: context.mcpReq.signal });
|
||||
} catch (cause) {
|
||||
if (!(cause instanceof AnalysisError)) throw cause;
|
||||
@@ -95,16 +105,19 @@ export const registerSessionStatusTool = (
|
||||
status,
|
||||
{
|
||||
...availabilityPolicy(),
|
||||
androidAnalysisAvailability: androidAvailability,
|
||||
...(adbAvailability === undefined
|
||||
androidAnalysisAvailability: availabilityResults.android,
|
||||
...(availabilityResults.adb === undefined
|
||||
? {}
|
||||
: { adbAnalysisAvailability: adbAvailability }),
|
||||
...(apktoolAvailability === undefined
|
||||
: { adbAnalysisAvailability: availabilityResults.adb }),
|
||||
...(availabilityResults.apktool === undefined
|
||||
? {}
|
||||
: { apktoolAnalysisAvailability: apktoolAvailability }),
|
||||
...(jebAvailability === undefined
|
||||
: { apktoolAnalysisAvailability: availabilityResults.apktool }),
|
||||
...(availabilityResults.jeb === undefined
|
||||
? {}
|
||||
: { jebAnalysisAvailability: jebAvailability }),
|
||||
: { jebAnalysisAvailability: availabilityResults.jeb }),
|
||||
...(availabilityResults.evmLimiter === undefined
|
||||
? {}
|
||||
: { evmLimiterAvailability: availabilityResults.evmLimiter }),
|
||||
},
|
||||
clientFeatures,
|
||||
);
|
||||
|
||||
@@ -0,0 +1,265 @@
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import { buildCapabilityInventory } from "../../../src/application/CapabilityInventory.js";
|
||||
import { AnalysisCancelledError } from "../../../src/domain/analysisErrorCore.js";
|
||||
import {
|
||||
EVM_PRLIMIT_COMMAND_DEFAULT,
|
||||
evmLimiterAvailability,
|
||||
} from "../../../src/evm/EvmoleInterfaceProvider.js";
|
||||
|
||||
const linux = { platform: "linux" as const, arch: "x64" };
|
||||
const status = { open: false, capabilities: [] };
|
||||
const basePolicy = { processCaptureEnabled: true, evmInterfaceEnabled: true };
|
||||
|
||||
it("probes the default limiter with executable and regular-file checks", async () => {
|
||||
const calls: unknown[][] = [];
|
||||
const availability = await evmLimiterAvailability(
|
||||
{},
|
||||
{
|
||||
...linux,
|
||||
fs: {
|
||||
access: async (path, mode) => {
|
||||
calls.push(["access", path, mode]);
|
||||
},
|
||||
stat: async (path) => {
|
||||
calls.push(["stat", path]);
|
||||
return { isFile: () => true };
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(availability).toMatchObject({
|
||||
status: "available",
|
||||
diagnostics: { limiter_path: EVM_PRLIMIT_COMMAND_DEFAULT },
|
||||
});
|
||||
expect(calls).toEqual([
|
||||
["access", EVM_PRLIMIT_COMMAND_DEFAULT, 1],
|
||||
["stat", EVM_PRLIMIT_COMMAND_DEFAULT],
|
||||
]);
|
||||
});
|
||||
|
||||
it("uses the exact absolute override and reports a failed executable check", async () => {
|
||||
const limiter = "/fixture/util-linux/prlimit";
|
||||
const availability = await evmLimiterAvailability(
|
||||
{ REA_EVM_PRLIMIT_COMMAND: limiter },
|
||||
{
|
||||
...linux,
|
||||
fs: {
|
||||
access: async (path) => {
|
||||
expect(path).toBe(limiter);
|
||||
throw Object.assign(new Error("permission denied"), {
|
||||
code: "EACCES",
|
||||
});
|
||||
},
|
||||
stat: async () => {
|
||||
throw new Error("stat must not run after access failure");
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(availability).toMatchObject({
|
||||
status: "unavailable",
|
||||
code: "executable_missing",
|
||||
reason: expect.stringContaining(limiter),
|
||||
diagnostics: { limiter_path: limiter },
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects a relative override and non-file without probing an alternate command", async () => {
|
||||
const relative = await evmLimiterAvailability(
|
||||
{ REA_EVM_PRLIMIT_COMMAND: "prlimit" },
|
||||
{
|
||||
...linux,
|
||||
fs: {
|
||||
access: async () => {
|
||||
throw new Error("relative paths must be rejected first");
|
||||
},
|
||||
stat: async () => ({ isFile: () => true }),
|
||||
},
|
||||
},
|
||||
);
|
||||
expect(relative).toMatchObject({
|
||||
status: "unavailable",
|
||||
diagnostics: { limiter_path: "prlimit" },
|
||||
reason: expect.stringContaining("absolute executable path"),
|
||||
});
|
||||
|
||||
const directory = await evmLimiterAvailability(
|
||||
{},
|
||||
{
|
||||
...linux,
|
||||
fs: {
|
||||
access: async () => undefined,
|
||||
stat: async () => ({ isFile: () => false }),
|
||||
},
|
||||
},
|
||||
);
|
||||
expect(directory).toMatchObject({
|
||||
status: "unavailable",
|
||||
reason: expect.stringContaining("regular executable file"),
|
||||
});
|
||||
});
|
||||
|
||||
it("rechecks on each observation so inventory follows the selected executable's current state", async () => {
|
||||
let accessible = true;
|
||||
const fs = {
|
||||
access: async () => {
|
||||
if (!accessible) throw new Error("ENOENT");
|
||||
},
|
||||
stat: async () => ({ isFile: () => true }),
|
||||
};
|
||||
const first = await evmLimiterAvailability({}, { ...linux, fs });
|
||||
accessible = false;
|
||||
const second = await evmLimiterAvailability({}, { ...linux, fs });
|
||||
|
||||
expect(first?.status).toBe("available");
|
||||
expect(second).toMatchObject({
|
||||
status: "unavailable",
|
||||
reason: expect.stringContaining("ENOENT"),
|
||||
});
|
||||
});
|
||||
|
||||
it("yields the event loop while the filesystem owner probe is pending", async () => {
|
||||
let release!: () => void;
|
||||
const pendingAccess = new Promise<void>((resolve) => {
|
||||
release = resolve;
|
||||
});
|
||||
let accessStarted!: () => void;
|
||||
const started = new Promise<void>((resolve) => {
|
||||
accessStarted = resolve;
|
||||
});
|
||||
const probe = evmLimiterAvailability(
|
||||
{},
|
||||
{
|
||||
...linux,
|
||||
fs: {
|
||||
access: async () => {
|
||||
accessStarted();
|
||||
await pendingAccess;
|
||||
},
|
||||
stat: async () => ({ isFile: () => true }),
|
||||
},
|
||||
},
|
||||
);
|
||||
await started;
|
||||
let timerRan = false;
|
||||
setImmediate(() => {
|
||||
timerRan = true;
|
||||
});
|
||||
await new Promise<void>((resolve) => setImmediate(resolve));
|
||||
expect(timerRan).toBe(true);
|
||||
release();
|
||||
expect(await probe).toMatchObject({ status: "available" });
|
||||
});
|
||||
|
||||
it.each(["access", "stat"] as const)(
|
||||
"preserves cancellation while the %s filesystem probe is pending",
|
||||
async (stage) => {
|
||||
let release!: () => void;
|
||||
const pendingProbe = new Promise<void>((resolve) => {
|
||||
release = resolve;
|
||||
});
|
||||
let probeStarted!: () => void;
|
||||
const started = new Promise<void>((resolve) => {
|
||||
probeStarted = resolve;
|
||||
});
|
||||
const controller = new AbortController();
|
||||
const abortReason = { source: "caller-stop" };
|
||||
const probe = evmLimiterAvailability(
|
||||
{},
|
||||
{
|
||||
...linux,
|
||||
signal: controller.signal,
|
||||
fs: {
|
||||
access: async () => {
|
||||
if (stage !== "access") return;
|
||||
probeStarted();
|
||||
await pendingProbe;
|
||||
},
|
||||
stat: async () => {
|
||||
if (stage === "access")
|
||||
throw new Error("stat must not run after access cancellation");
|
||||
probeStarted();
|
||||
await pendingProbe;
|
||||
return { isFile: () => true };
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
try {
|
||||
await started;
|
||||
controller.abort(abortReason);
|
||||
release();
|
||||
const error = await probe.catch((cause: unknown) => cause);
|
||||
expect(error).toBeInstanceOf(AnalysisCancelledError);
|
||||
expect(error).toMatchObject({ operation: "binary_session" });
|
||||
expect((error as AnalysisCancelledError).cause).toBe(abortReason);
|
||||
} finally {
|
||||
release();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it("leaves non-Linux-x64 host gates unchanged", async () => {
|
||||
const fs = {
|
||||
access: async () => {
|
||||
throw new Error("unsupported hosts must not probe prlimit");
|
||||
},
|
||||
stat: async () => ({ isFile: () => true }),
|
||||
};
|
||||
expect(
|
||||
await evmLimiterAvailability({}, { platform: "darwin", arch: "arm64", fs }),
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
await evmLimiterAvailability({}, { platform: "linux", arch: "arm64", fs }),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("projects owner prerequisite facts into capability availability", async () => {
|
||||
const unavailable = await evmLimiterAvailability(
|
||||
{},
|
||||
{
|
||||
...linux,
|
||||
fs: {
|
||||
access: async () => {
|
||||
throw new Error("ENOENT");
|
||||
},
|
||||
stat: async () => ({ isFile: () => true }),
|
||||
},
|
||||
},
|
||||
);
|
||||
const unavailableInventory = buildCapabilityInventory(status, {
|
||||
...basePolicy,
|
||||
...(unavailable === undefined
|
||||
? {}
|
||||
: { evmLimiterAvailability: unavailable }),
|
||||
});
|
||||
expect(
|
||||
unavailableInventory.find(({ name }) => name === "inspect_evm_interface"),
|
||||
).toMatchObject({
|
||||
available: false,
|
||||
reason: "provider_missing",
|
||||
remediation: expect.stringContaining("caller-supplied util-linux prlimit"),
|
||||
});
|
||||
|
||||
const available = await evmLimiterAvailability(
|
||||
{},
|
||||
{
|
||||
...linux,
|
||||
fs: {
|
||||
access: async () => undefined,
|
||||
stat: async () => ({ isFile: () => true }),
|
||||
},
|
||||
},
|
||||
);
|
||||
const availableInventory = buildCapabilityInventory(status, {
|
||||
...basePolicy,
|
||||
...(available === undefined ? {} : { evmLimiterAvailability: available }),
|
||||
});
|
||||
expect(
|
||||
availableInventory.find(({ name }) => name === "inspect_evm_interface"),
|
||||
).toMatchObject({ available: true, reason: "available", remediation: null });
|
||||
});
|
||||
@@ -0,0 +1,109 @@
|
||||
import { join } from "node:path";
|
||||
|
||||
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
|
||||
import { expect, it } from "vitest";
|
||||
import { z } from "zod";
|
||||
|
||||
import { EvmInterfaceService } from "../../../src/application/evm/EvmInterfaceService.js";
|
||||
import {
|
||||
expandToolAvailability,
|
||||
sessionProvider,
|
||||
} from "../../../src/contracts/toolOutputSchemaPrimitives.js";
|
||||
import { createServer } from "../../../src/server/createServer.js";
|
||||
import { createTestBinarySession } from "../../fixtures/binarySession.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
|
||||
const linuxX64 = process.platform === "linux" && process.arch === "x64";
|
||||
|
||||
const connect = async (
|
||||
options: Parameters<typeof createServer>[1],
|
||||
): Promise<{
|
||||
readonly client: Client;
|
||||
readonly server: ReturnType<typeof createServer>;
|
||||
readonly availability: () => Promise<Record<string, unknown> | undefined>;
|
||||
readonly close: () => Promise<void>;
|
||||
}> => {
|
||||
const session = createTestBinarySession(() => {
|
||||
throw new Error("binary analysis must not start during status discovery");
|
||||
});
|
||||
const server = createServer({ kind: "session", session }, options);
|
||||
const client = new Client({ name: "evm-limiter-availability", version: "1" });
|
||||
const [clientTransport, serverTransport] =
|
||||
InMemoryTransport.createLinkedPair();
|
||||
await server.connect(serverTransport);
|
||||
await client.connect(clientTransport);
|
||||
return {
|
||||
client,
|
||||
server,
|
||||
availability: async () => {
|
||||
const response = await client.callTool({
|
||||
name: "binary_session",
|
||||
arguments: {},
|
||||
});
|
||||
if (response.isError === true) throw new Error("binary_session failed");
|
||||
const parsed = z
|
||||
.object({ result: sessionProvider })
|
||||
.parse(response.structuredContent).result;
|
||||
return expandToolAvailability(parsed.tool_availability).find(
|
||||
({ name }) => name === "inspect_evm_interface",
|
||||
);
|
||||
},
|
||||
close: async () => {
|
||||
await Promise.allSettled([
|
||||
client.close(),
|
||||
server.close(),
|
||||
session.close(),
|
||||
]);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
it.runIf(linuxX64)(
|
||||
"reports a missing selected limiter in the live default-provider session inventory",
|
||||
async () => {
|
||||
const directory = await createTestTempDirectory("rea-evm-limiter-mcp-");
|
||||
const limiter = join(directory, "missing-prlimit");
|
||||
const harness = await connect({
|
||||
environment: {
|
||||
REA_EVM_PRLIMIT_COMMAND: limiter,
|
||||
REA_MCP_OUTPUT_SCHEMA_PROFILE: "off",
|
||||
},
|
||||
});
|
||||
try {
|
||||
expect(await harness.availability()).toMatchObject({
|
||||
available: false,
|
||||
reason: "provider_missing",
|
||||
remediation: expect.stringContaining(limiter),
|
||||
});
|
||||
} finally {
|
||||
await harness.close();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps a caller-injected EVM service independent of the default prlimit prerequisite", async () => {
|
||||
const directory = await createTestTempDirectory("rea-evm-limiter-injected-");
|
||||
const limiter = join(directory, "missing-prlimit");
|
||||
const service = new EvmInterfaceService({
|
||||
inspect: async () => {
|
||||
throw new Error("injected service must not be invoked by status");
|
||||
},
|
||||
close: async () => {},
|
||||
});
|
||||
const harness = await connect({
|
||||
environment: {
|
||||
REA_EVM_PRLIMIT_COMMAND: limiter,
|
||||
REA_MCP_OUTPUT_SCHEMA_PROFILE: "off",
|
||||
},
|
||||
evmInterface: service,
|
||||
});
|
||||
try {
|
||||
expect(await harness.availability()).toMatchObject({
|
||||
available: true,
|
||||
reason: "available",
|
||||
remediation: null,
|
||||
});
|
||||
} finally {
|
||||
await harness.close();
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user