From 4b2a4e8eaef2d7645db0014b2aee826cd1436997 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Sat, 24 Oct 2026 02:39:42 +0800 Subject: [PATCH] fix(evm): preserve reply allowance failures and selected limits --- docs/evm-bytecode.md | 12 ++++--- src/evm/EvmoleFailures.test.ts | 29 +++++++++++++++- src/evm/EvmoleFailures.ts | 21 ++++++++++++ src/evm/EvmoleInterfaceProvider.ts | 34 ++++++++++++------- .../evm/evmoleInterfaceProvider.test.ts | 12 +++++-- 5 files changed, 87 insertions(+), 21 deletions(-) diff --git a/docs/evm-bytecode.md b/docs/evm-bytecode.md index 781112bb6f..67c543257b 100644 --- a/docs/evm-bytecode.md +++ b/docs/evm-bytecode.md @@ -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 diff --git a/src/evm/EvmoleFailures.test.ts b/src/evm/EvmoleFailures.test.ts index ffdaf88d49..74c2108a44 100644 --- a/src/evm/EvmoleFailures.test.ts +++ b/src/evm/EvmoleFailures.test.ts @@ -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, diff --git a/src/evm/EvmoleFailures.ts b/src/evm/EvmoleFailures.ts index d258c85f5e..190e3b7f9e 100644 --- a/src/evm/EvmoleFailures.ts +++ b/src/evm/EvmoleFailures.ts @@ -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.`; diff --git a/src/evm/EvmoleInterfaceProvider.ts b/src/evm/EvmoleInterfaceProvider.ts index 8abe0dc380..d25f6591cc 100644 --- a/src/evm/EvmoleInterfaceProvider.ts +++ b/src/evm/EvmoleInterfaceProvider.ts @@ -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: { diff --git a/tests/boundary/evm/evmoleInterfaceProvider.test.ts b/tests/boundary/evm/evmoleInterfaceProvider.test.ts index 9340f1ec72..52327be449 100644 --- a/tests/boundary/evm/evmoleInterfaceProvider.test.ts +++ b/tests/boundary/evm/evmoleInterfaceProvider.test.ts @@ -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)