diff --git a/docs/evm-bytecode.md b/docs/evm-bytecode.md index 2b9141f933..fa61b53db8 100644 --- a/docs/evm-bytecode.md +++ b/docs/evm-bytecode.md @@ -110,7 +110,9 @@ address-space limit and 30 CPU seconds. 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. Virtual +`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 soft/hard limits, reports its configured soft limits inline and leaves hard limits unchanged. WASM trap reservations are disabled diff --git a/src/evm/EvmoleFailures.test.ts b/src/evm/EvmoleFailures.test.ts index af800e1fa1..f4e566c0e1 100644 --- a/src/evm/EvmoleFailures.test.ts +++ b/src/evm/EvmoleFailures.test.ts @@ -2,6 +2,7 @@ import { expect, it } from "vitest"; import { evmInterfaceFailure } from "./EvmoleFailures.js"; import { OwnedCommandFailure } from "../process/OwnedCommand.js"; import { AnalysisCancelledError } from "../domain/analysisErrorCore.js"; +import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js"; import { projectAnalysisError } from "../domain/analysisErrorProjection.js"; import { DEFAULT_EVM_TIMEOUT_MS, @@ -106,6 +107,60 @@ it("says a CPU signal has no runtime override and timeout_ms does not raise it", ); }); +it("reports the fixed carrier input allowance as a resource constraint", () => { + const source = new ArtifactReaderFailure( + "limit", + "Selected artifact exceeds 4194304 bytes: /selected.bin", + { boundary: "file-size" }, + ); + const projected = projectAnalysisError( + evmInterfaceFailure(source, "artifact-read", "/selected.bin"), + ); + expect(projected).toMatchObject({ + code: "resource_constraint", + category: "resource_constraint", + retryable: false, + details: { + operation: "inspect_evm_interface", + resource: "file-size", + reported_limits: { maximum_input_bytes: 4194304 }, + }, + }); + expect(projected.message).toContain("4194304"); + expect(projected.message).toContain("no runtime override"); + expect(projected.message).toContain("timeout_ms does not raise it"); + expect(projected.remediation.action).toContain("4194304"); + expect(projected.remediation.action).toContain("no runtime override"); + expect(projected.remediation.action).toContain("Do not truncate"); + expect(projected.remediation.action).toContain( + "repeating the unchanged request fails the same way", + ); + expect(projected.recovery).toBeUndefined(); +}); + +it.each([ + { + reason: "path" as const, + message: "Expected a regular file without a symlink: /selected.bin", + }, + { reason: "format" as const, message: "Unsupported carrier format." }, +])("keeps reader $reason refusals as invalid input", ({ reason, message }) => { + const projected = projectAnalysisError( + evmInterfaceFailure( + new ArtifactReaderFailure(reason, message), + "artifact-read", + "/selected.bin", + ), + ); + expect(projected).toMatchObject({ + code: "invalid_request", + category: "invalid_input", + details: { + issues: [{ path: ["path"], reason: "invalid_format", message }], + }, + }); +}); + it.each([ { failure: "process" as const, cleanup: null, tag: "ProviderAdapterError" }, { failure: "timeout" as const, cleanup: null, tag: "AnalysisTimeoutError" }, diff --git a/src/evm/EvmoleFailures.ts b/src/evm/EvmoleFailures.ts index f2cc0021ef..15c3f4cdfa 100644 --- a/src/evm/EvmoleFailures.ts +++ b/src/evm/EvmoleFailures.ts @@ -173,28 +173,36 @@ export const evmInterfaceFailure = ( ); if ( phase === "artifact-read" && - ["limit", "path", "format"].includes(cause.reason) - ) { - const inputLimited = cause.reason === "limit"; - const message = inputLimited - ? `${cause.message} The input allowance is ${String(EVM_INTERFACE_LIMITS.inputBytes)} bytes. This release has no runtime override. timeout_ms does not raise it.` - : cause.message; + cause.reason === "limit" && + cause.boundary === "file-size" + ) + return new AnalysisResourceConstraintError( + "inspect_evm_interface", + "file-size", + `${cause.message} The input allowance is ${String(EVM_INTERFACE_LIMITS.inputBytes)} bytes. This release has no runtime override. timeout_ms does not raise it.`, + { maximum_input_bytes: EVM_INTERFACE_LIMITS.inputBytes }, + { + cause, + ...cleanupOptions, + retryable: false, + remediationAction: `Select a suitable complete carrier within the ${String(EVM_INTERFACE_LIMITS.inputBytes)}-byte input allowance. This release has no runtime override. timeout_ms does not raise it, and reply or diagnostic allowances do not admit a larger carrier. Do not truncate required bytes; repeating the unchanged request fails the same way.`, + }, + ); + if (phase === "artifact-read" && ["path", "format"].includes(cause.reason)) return new AnalysisInputError( "inspect_evm_interface", { cause, ...cleanupOptions, - ...(inputLimited ? { userMessage: message } : {}), }, [ { path: ["path"], - reason: inputLimited ? "out_of_range" : "invalid_format", - message, + reason: "invalid_format", + message: cause.message, }, ], ); - } } if (phase === "artifact-read" && cause instanceof Error && "code" in cause) { if (cause.code === "EACCES" || cause.code === "EPERM")