fix(evm): preserve reply allowance failures and selected limits

This commit is contained in:
morluto committed 2026-10-24 02:40:21 +08:00
1 parent 1ddb32956a
commit 4b2a4e8eae
5 files changed
+87 -21

No files matched your search

+8 -4
View File
@@ -108,15 +108,19 @@ CLI keeps its existing complete result.
## Resource and lifecycle limits
Inputs are limited to 4 MiB, complete replies to 16 MiB and combined diagnostic
output to 1 MiB. An owned worker has a 256 MiB JavaScript heap, 3 GiB virtual
address-space limit and 30 CPU seconds. The worker command deadline is
output to 1 MiB. An owned worker has a 256 MiB JavaScript heap; its profile
maxima are 3 GiB virtual address space and 30 CPU seconds, with tighter inherited
limits retained. The worker command deadline is
`timeout_ms` (default 30000 ms, integer 1 through 2147483647) on the next call,
or `--timeout-ms` on `rea inspect-evm-interface`. Omission keeps 30000 ms.
Those byte, address-space, and CPU ceilings have no runtime override, and
`timeout_ms` does not raise them. An oversized selected carrier returns a
non-retryable `resource_constraint` with resource `file-size` and its fixed
input allowance in `reported_limits.maximum_input_bytes`. Virtual
address space differs from resident memory. The worker retains inherited tighter
input allowance in `reported_limits.maximum_input_bytes`.
Complete-reply allowance refusals use the same non-retryable resource family,
with `reported_limits.maximum_reply_bytes`; malformed JSON remains unreadable
output. This refusal leaves no complete report to retrieve through another
delivery channel. Virtual address space differs from resident memory. The worker retains inherited tighter
soft/hard limits, reports its configured soft limits inline and leaves hard limits
unchanged. WASM trap reservations are disabled
so the engine can run within that virtual limit; actual memory is not equated to
+28 -1
View File
@@ -2,7 +2,10 @@ import { readFile, symlink } from "node:fs/promises";
import { join } from "node:path";
import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js";
import { expect, it } from "vitest";
import { evmInterfaceFailure } from "./EvmoleFailures.js";
import {
evmInterfaceFailure,
evmReplyBudgetFailure,
} from "./EvmoleFailures.js";
import { OwnedCommandFailure } from "../process/OwnedCommand.js";
import { AnalysisCancelledError } from "../domain/analysisErrorCore.js";
import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
@@ -20,6 +23,30 @@ const snapshot: ProviderProcessSnapshot = {
signal: null,
};
it("projects a complete EVM reply allowance refusal as a deterministic resource constraint", () => {
const capturedOutput = {
stdout: "kept",
stderr: "warning",
truncated: false,
};
const projected = projectAnalysisError(
evmReplyBudgetFailure(
"Complete interface evidence exceeds the 16 MiB reply budget.",
{ capturedOutput },
),
);
expect(projected).toMatchObject({
code: "resource_constraint",
retryable: false,
details: {
reported_limits: { maximum_reply_bytes: 16777216 },
captured_output: capturedOutput,
},
remediation: { action: expect.stringContaining("fixed report allowance") },
});
expect(projected.remediation.action).not.toContain("doctor");
});
it.each([
undefined,
null,
+21
View File
@@ -7,6 +7,7 @@ import {
import {
AnalysisError,
type AnalysisCapturedOutput,
type AnalysisErrorOptions,
} from "../domain/analysisErrorBase.js";
import type { ProviderProcessSnapshot } from "../process/ProviderProcess.js";
import {
@@ -32,6 +33,26 @@ import {
EVM_INTERFACE_LIMITS,
EVM_FILE_SIZE_FAILURE_EXIT,
} from "./EvmoleRelease.js";
import { fixedAllowanceOptions } from "../domain/fixedAllowanceRemediation.js";
/** Both a structured worker refusal and reply-file admission name this allowance. */
export const evmReplyBudgetFailure = (
reason: string,
options: AnalysisErrorOptions,
) =>
new AnalysisResourceConstraintError(
"inspect_evm_interface",
"file-size",
reason,
{ maximum_reply_bytes: EVM_INTERFACE_LIMITS.outputBytes },
{
...options,
...fixedAllowanceOptions(
`The complete EVMole worker reply allowance of ${String(EVM_INTERFACE_LIMITS.outputBytes)} bytes (this release has no runtime override)`,
"report",
),
},
);
const fixedEvmRecovery = (allowance: string): string =>
`${allowance}. This release has no runtime override. timeout_ms does not raise it. Repeating the unchanged request fails the same way.`;
+21 -13
View File
@@ -53,6 +53,7 @@ import type { ProviderProcessSupervisor } from "../process/ProviderProcess.js";
import {
evmInterfaceFailure,
capturedEvmOutput,
evmReplyBudgetFailure,
type EvmFileSizeFailureEvidence,
} from "./EvmoleFailures.js";
import {
@@ -582,15 +583,24 @@ export class EvmoleInterfaceProvider implements EvmInterfacePort {
bindArtifactFileCleanup(cause, failure);
throw failure;
}
const failure = new AnalysisOutputError(
OPERATION,
`Owned interface worker reply failed for ${input.path}: ${cause instanceof Error ? cause.message : String(cause)}`,
{
cause,
capturedOutput,
...(cleanup === undefined ? {} : { cleanup }),
},
);
const failure =
cause instanceof ArtifactReaderFailure &&
cause.reason === "limit" &&
cause.boundary === "file-size"
? evmReplyBudgetFailure(cause.message, {
cause,
capturedOutput,
...(cleanup === undefined ? {} : { cleanup }),
})
: new AnalysisOutputError(
OPERATION,
`Owned interface worker reply failed for ${input.path}: ${cause instanceof Error ? cause.message : String(cause)}`,
{
cause,
capturedOutput,
...(cleanup === undefined ? {} : { cleanup }),
},
);
if (cause instanceof ArtifactReaderFailure)
bindArtifactFileCleanup(cause, failure);
throw failure;
@@ -612,10 +622,8 @@ export class EvmoleInterfaceProvider implements EvmInterfacePort {
},
]);
if (reply.reason === "output-limit")
throw new AnalysisOutputError(OPERATION, reply.message, {
throw evmReplyBudgetFailure(reply.message, {
capturedOutput,
userMessage: `${reply.message} The reply allowance is ${String(EVM_INTERFACE_LIMITS.outputBytes)} bytes. This release has no runtime override.`,
remediationAction: `The EVMole reply allowance is ${String(EVM_INTERFACE_LIMITS.outputBytes)} bytes. This release has no runtime override. timeout_ms does not raise it. Repeating the unchanged request fails the same way.`,
});
throw new ProviderAdapterError(EVMOLE_PROVIDER_IDENTITY.id, OPERATION, {
diagnostics: {
@@ -653,7 +661,7 @@ export class EvmoleInterfaceProvider implements EvmInterfacePort {
"Bytecode kind, hardfork and deployed-contract authenticity are unknown. This inspection performs no target runtime execution, chain/RPC lookup or network request.",
"Original carrier and decoded bytecode SHA-256 are distinct identities; SHA-256 is not the Ethereum Keccak code hash.",
"Terminal CBOR metadata is an independently decoded candidate, not proof of Solidity origin, source identity or deployment. Nested or unsupported values retain explicit unavailable interpretation and original byte ranges.",
`The owned worker has a 256 MiB JavaScript heap, an address-space ceiling of ${String(EVM_INTERFACE_LIMITS.addressSpaceBytes)} bytes, and a CPU ceiling of ${String(EVM_CPU_SECONDS)} seconds. The command deadline is timeout_ms at ${String(timeoutMs)} ms (default ${String(DEFAULT_EVM_TIMEOUT_MS)}). Those byte, address-space, and CPU ceilings have no runtime override, and timeout_ms does not raise them. Virtual address space is distinct from resident memory; limits fail without partial success.`,
`The owned worker has a 256 MiB JavaScript heap. The profile maxima are ${String(EVM_INTERFACE_LIMITS.addressSpaceBytes)} address-space bytes and ${String(EVM_CPU_SECONDS)} CPU seconds; the configured soft limits above retain tighter inherited limits. The command deadline is timeout_ms at ${String(timeoutMs)} ms (default ${String(DEFAULT_EVM_TIMEOUT_MS)}). Those byte, address-space, and CPU maxima have no runtime override, and timeout_ms does not raise them. Virtual address space is distinct from resident memory; limits fail without partial success.`,
];
const validated = evmInterfaceSchema.safeParse({
artifact: {
@@ -377,6 +377,7 @@ it
"success",
"format",
"unsupported",
"output-limit",
"wrong-version",
"wrong-decoded-carrier",
"broken-reply",
@@ -409,11 +410,14 @@ it
scenario === "broken-reply"
? "broken"
: JSON.stringify(
["format", "unsupported"].includes(scenario)
["format", "unsupported", "output-limit"].includes(scenario)
? {
ok: false,
reason: scenario,
message: "Malformed hex carrier.",
message:
scenario === "output-limit"
? "Complete interface evidence exceeds the 16 MiB reply budget."
: "Malformed hex carrier.",
}
: {
ok: true,
@@ -465,7 +469,9 @@ it
? "AnalysisInputError"
: scenario === "unsupported"
? "AnalysisUnsupportedTargetError"
: "AnalysisOutputError",
: scenario === "output-limit"
? "AnalysisResourceConstraintError"
: "AnalysisOutputError",
},
});
if (!result.ok)