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:
morluto committed 2026-10-20 22:18:52 +08:00
1 parent 2b2e362a00
commit a74d859a19
7 files changed
+569 -58

No files matched your search

+14 -8
View File
@@ -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 }
+6 -1
View File
@@ -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.
+117 -16
View File
@@ -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");
+13 -1
View File
@@ -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) =>
+45 -32
View File
@@ -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();
}
});