[GH-ISSUE #5953] [Bug] Firmware source admission reports missing and unreadable inputs as provider failures #3624

Closed
opened 2026-10-31 00:42:28 +00:00 by bsakaguchi · 1 comment
Owner

Originally created by @coygeek on GitHub (Oct 21, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/5953

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Evidence, artifacts, or reconstruction

Observed behavior

inspect_firmware_regions and extract_firmware return execution_failure / execution_failure for a missing selected input and for a selected file whose read permission is denied. The errors name Binwalk or Unblob, recommend rea doctor, and omit structured input-field or access-denied metadata. These failures occur before even the engine version command: the producer launch count remained zero.

Both cases were executed through the production in-memory MCP server with real filesystem lookup/open errors. Binwalk and Unblob were simulated using the repository's producer fixture; a mock bypassed Linux/tool prerequisite resolution on macOS. Real engines, Linux prlimit, and the public CLI were not exercised.

Reproduction

Save the following test beside src/firmware/FirmwareCommand.ts as firmwarePathAdmission.test.ts in a source checkout with locked dependencies. Run ./node_modules/.bin/vitest run src/firmware/firmwarePathAdmission.test.ts --maxWorkers 1.

This input-only test was executed and passed both operation cases. Each case calls the tool with an absent path, then with the fixture's real input chmodded to 000, then restores permissions and calls the same tool with a readable input. Both refusals were also observed in an earlier combined run. Run as an ordinary user; a privileged account may bypass mode-bit restrictions.

The mock affects only tool/host admission. It leaves the selected-input filesystem reads, snapshot admission, error conversion, MCP dispatch, and result projection intact. The fixture launches owned Node subprocesses for the successful controls.

import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { chmod } from "node:fs/promises";
import { join } from "node:path";
import { expect, it, onTestFinished, vi } from "vitest";

vi.mock("./FirmwareCommand.js", async (importOriginal) => {
  const original = await importOriginal<typeof import("./FirmwareCommand.js")>();
  return { ...original, resolveFirmwareCommand: async (_env: unknown, engine: string) => ({
    command: engine === "binwalk" ? "/usr/bin/true" : process.execPath,
    limiter: "/fixture/prlimit", sha256: "0".repeat(64),
  }) };
});
import { createServer } from "../server/createServer.js";
import { createTestBinarySession } from "../../tests/fixtures/binarySession.js";
import { firmwareFixture, assertFirmwareCleanup } from "../../tests/fixtures/firmware/provider.js";

const setup = async () => {
  const fixture = await firmwareFixture();
  const session = createTestBinarySession(() => { throw new Error("No native target required"); });
  const server = createServer({ kind: "session", session }, { firmwareAnalysis: fixture.provider });
  const client = new Client({ name: "firmware-path-repro", version: "1" });
  onTestFinished(async () => { await client.close(); await server.close(); await session.close(); });
  const [a,b] = InMemoryTransport.createLinkedPair();
  await server.connect(b); await client.connect(a);
  return { fixture, client };
};
const errorOf = (response: any) => JSON.parse(response.content.find((x: any) => x.type === "text").text).error;

it.each(["inspect_firmware_regions", "extract_firmware"])("missing and unreadable input via %s", async (operation) => {
  const {fixture,client}=await setup();
  const invoke=(path: string) => client.callTool({name:operation,arguments:{path,...(operation === "extract_firmware" ? {output_directory:fixture.output}: {})}});
  for(const mode of ["missing", "unreadable"]){
    const input=mode === "missing" ? join(fixture.root,"absent.bin") : fixture.path;
    if(mode === "unreadable") await chmod(input,0);
    try {
      const response=await invoke(input);
      expect(response.isError).toBe(true);
      const error=errorOf(response);
      console.log("INPUT",operation,mode,JSON.stringify(error),"launches",fixture.launches.length);
      expect(error).toMatchObject({code:"execution_failure",category:"execution_failure",retryable:false});
      expect(error.details).not.toHaveProperty("issues");
      expect(error.details).not.toHaveProperty("system_code");
      expect(error.details.diagnostics.reason).toContain(mode === "missing" ? "ENOENT" : "EACCES");
      expect(fixture.launches).toHaveLength(0);
    } finally { if(mode === "unreadable") await chmod(input,0o600); }
  }
  const control=await invoke(fixture.path);
  expect(control.isError,JSON.stringify(control)).not.toBe(true);
  expect(control.structuredContent).toBeDefined();
  console.log("VALID CONTROL",operation,"successful", "launches",fixture.launches.length);
  await assertFirmwareCleanup(fixture.launches);
});

Expected behavior

Keep refusing missing or unreadable inputs, while identifying the source admission failure. A missing selected file should produce invalid_request / invalid_input with a path field issue. A filesystem permission denial should produce access_denied, preserving its selected path, errno, and filesystem-read boundary, with appropriate recovery.

The shared permission-error contract explicitly distinguishes a denied host filesystem read from malformed input. Its projection retains path, system_code, and boundary. The Go provider's admission mapping and Electron fuse mapping apply these distinctions to equivalent caller-selected file errors.

Impact and scope

P2: callers receive the wrong machine-readable diagnosis and provider-health recovery for an input that was never admitted. The raw diagnostic retains the errno as text, but structured field/access correction is absent. Refusal remains effective; no data corruption or security-boundary violation was observed.

A practical workaround was demonstrated: select the present readable fixture or restore ordinary read permission. Both tools then succeeded in the same MCP sessions. Natural incidence and downstream automated-repair consequences were not measured. EPERM, other path-resolution errors, other operating systems, and the public CLI were not individually tested.

REA version

rea-agents 6.7.0; source revision 52415975942a4b9c53a817915b313ccc404f280e.

Fetched origin/main at 25e1a24a3ed3671a0148757555f18d16c29d936a. Source comparison shows the input filesystem admission and generic fallback remain unchanged; adjacent changes concern timeout controls and captured-output projection. No runtime replay of that later revision is claimed.

Runtime and platform

Node.js v26.12.0; macOS 27.0.1, arm64. Repository Node pin: 24.18.0. Source tests with synthetic Binwalk/Unblob producers and mocked Linux/tool/limiter admission. Unblob was not installed.

Target and artifact context

Repository fixture containing a small nonempty regular file, plus a genuinely absent sibling path. Permission refusal used that same file with mode 000; permissions were restored in finally. No real firmware engine, device, or extracted-code execution.

Interface

MCP

Earliest failure stage

Setup or provider admission

Relevant output or evidence

The reduced input-only run passed one file and two tests. Each operation produced these selected fields for both missing and permission-denied inputs:

{
  "code": "execution_failure",
  "category": "execution_failure",
  "retryable": false
}

details.provider_id was binwalk for inspection and unblob for extraction. The missing-input diagnostic began ENOENT: no such file or directory, realpath; the unreadable-input diagnostic began EACCES: permission denied, open. No details.issues, details.system_code, or structured recovery was present. The message and remediation were:

Analysis could not complete. Run `rea doctor` and review the reported details; repeating the unchanged request is not expected to succeed.

The producer launch count was zero after both refusals. Selecting the readable file then succeeded, with two synthetic launches per operation (version and analysis); process/workspace cleanup assertions passed. These observations establish admission/error-projection behavior with simulated providers, not real-provider extraction correctness.

Typed outcome or recovery expected

For ENOENT, an input error identifying path, such as issues: [{ path: ["path"], reason: "invalid_value" }], and input-contract correction. For EACCES, access_denied with the actual system_code, selected path, and boundary: "filesystem-read". Preserve any independent cleanup failure and avoid classifying genuine producer failures as source-input errors.

Analysis or suspected cause

Source-derived: FirmwareProvider snapshots the input before running even the version command. FirmwareFiles performs realpath outside its admission catch and forwards regular-file acquisition errors through a mapper that only classifies NonRegularFileReadError. Raw ENOENT and EACCES therefore reach the ProviderAdapterError fallback.

Classification belongs at the selected-source acquisition boundary, where the caller field and operation are known. A blanket filesystem-errno mapping in the generic firmware fallback could incorrectly blame the source for unrelated workspace/publication failures.

Additional context

Related but distinct: #5746 and open PR #5902 address output-directory admission; #5846 concerns staging budgets; #5877 and #5402 concern range schema issues; closed #4619 concerns snapshot growth beyond the input allowance; #5876 concerns a missing producer report after execution; #1855 concerns contradictory retry prose. Open PR #5672 addresses source streaming during publication setup, and PR #4493 addresses descriptor-close retention. None restores selected-input ENOENT/EACCES classification before producer launch.

No known-good revision or completed repair was established.

Originally created by @coygeek on GitHub (Oct 21, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/5953 ### Before submitting - [x] I searched the open and closed issues for a related report. - [x] I removed credentials, tokens, private paths, and sensitive target data. ### Area Evidence, artifacts, or reconstruction ### Observed behavior `inspect_firmware_regions` and `extract_firmware` return `execution_failure` / `execution_failure` for a missing selected input and for a selected file whose read permission is denied. The errors name Binwalk or Unblob, recommend `rea doctor`, and omit structured input-field or access-denied metadata. These failures occur before even the engine version command: the producer launch count remained zero. Both cases were executed through the production in-memory MCP server with real filesystem lookup/open errors. Binwalk and Unblob were simulated using the repository's producer fixture; a mock bypassed Linux/tool prerequisite resolution on macOS. Real engines, Linux `prlimit`, and the public CLI were not exercised. ### Reproduction Save the following test beside `src/firmware/FirmwareCommand.ts` as `firmwarePathAdmission.test.ts` in a source checkout with locked dependencies. Run `./node_modules/.bin/vitest run src/firmware/firmwarePathAdmission.test.ts --maxWorkers 1`. This input-only test was executed and passed both operation cases. Each case calls the tool with an absent path, then with the fixture's real input chmodded to `000`, then restores permissions and calls the same tool with a readable input. Both refusals were also observed in an earlier combined run. Run as an ordinary user; a privileged account may bypass mode-bit restrictions. The mock affects only tool/host admission. It leaves the selected-input filesystem reads, snapshot admission, error conversion, MCP dispatch, and result projection intact. The fixture launches owned Node subprocesses for the successful controls. ```ts import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; import { chmod } from "node:fs/promises"; import { join } from "node:path"; import { expect, it, onTestFinished, vi } from "vitest"; vi.mock("./FirmwareCommand.js", async (importOriginal) => { const original = await importOriginal<typeof import("./FirmwareCommand.js")>(); return { ...original, resolveFirmwareCommand: async (_env: unknown, engine: string) => ({ command: engine === "binwalk" ? "/usr/bin/true" : process.execPath, limiter: "/fixture/prlimit", sha256: "0".repeat(64), }) }; }); import { createServer } from "../server/createServer.js"; import { createTestBinarySession } from "../../tests/fixtures/binarySession.js"; import { firmwareFixture, assertFirmwareCleanup } from "../../tests/fixtures/firmware/provider.js"; const setup = async () => { const fixture = await firmwareFixture(); const session = createTestBinarySession(() => { throw new Error("No native target required"); }); const server = createServer({ kind: "session", session }, { firmwareAnalysis: fixture.provider }); const client = new Client({ name: "firmware-path-repro", version: "1" }); onTestFinished(async () => { await client.close(); await server.close(); await session.close(); }); const [a,b] = InMemoryTransport.createLinkedPair(); await server.connect(b); await client.connect(a); return { fixture, client }; }; const errorOf = (response: any) => JSON.parse(response.content.find((x: any) => x.type === "text").text).error; it.each(["inspect_firmware_regions", "extract_firmware"])("missing and unreadable input via %s", async (operation) => { const {fixture,client}=await setup(); const invoke=(path: string) => client.callTool({name:operation,arguments:{path,...(operation === "extract_firmware" ? {output_directory:fixture.output}: {})}}); for(const mode of ["missing", "unreadable"]){ const input=mode === "missing" ? join(fixture.root,"absent.bin") : fixture.path; if(mode === "unreadable") await chmod(input,0); try { const response=await invoke(input); expect(response.isError).toBe(true); const error=errorOf(response); console.log("INPUT",operation,mode,JSON.stringify(error),"launches",fixture.launches.length); expect(error).toMatchObject({code:"execution_failure",category:"execution_failure",retryable:false}); expect(error.details).not.toHaveProperty("issues"); expect(error.details).not.toHaveProperty("system_code"); expect(error.details.diagnostics.reason).toContain(mode === "missing" ? "ENOENT" : "EACCES"); expect(fixture.launches).toHaveLength(0); } finally { if(mode === "unreadable") await chmod(input,0o600); } } const control=await invoke(fixture.path); expect(control.isError,JSON.stringify(control)).not.toBe(true); expect(control.structuredContent).toBeDefined(); console.log("VALID CONTROL",operation,"successful", "launches",fixture.launches.length); await assertFirmwareCleanup(fixture.launches); }); ``` ### Expected behavior Keep refusing missing or unreadable inputs, while identifying the source admission failure. A missing selected file should produce `invalid_request` / `invalid_input` with a `path` field issue. A filesystem permission denial should produce `access_denied`, preserving its selected path, errno, and `filesystem-read` boundary, with appropriate recovery. The [shared permission-error contract](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/domain/analysisErrorCore.ts#L188-L204) explicitly distinguishes a denied host filesystem read from malformed input. Its [projection](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/domain/analysisErrorProjection.ts#L318-L324) retains `path`, `system_code`, and `boundary`. The [Go provider's admission mapping](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/go/GoBinaryProvider.ts#L108-L125) and [Electron fuse mapping](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/electron/ElectronFuseFailures.ts#L137-L154) apply these distinctions to equivalent caller-selected file errors. ### Impact and scope P2: callers receive the wrong machine-readable diagnosis and provider-health recovery for an input that was never admitted. The raw diagnostic retains the errno as text, but structured field/access correction is absent. Refusal remains effective; no data corruption or security-boundary violation was observed. A practical workaround was demonstrated: select the present readable fixture or restore ordinary read permission. Both tools then succeeded in the same MCP sessions. Natural incidence and downstream automated-repair consequences were not measured. `EPERM`, other path-resolution errors, other operating systems, and the public CLI were not individually tested. ### REA version `rea-agents` 6.7.0; source revision `52415975942a4b9c53a817915b313ccc404f280e`. Fetched `origin/main` at `25e1a24a3ed3671a0148757555f18d16c29d936a`. Source comparison shows the input filesystem admission and generic fallback remain unchanged; adjacent changes concern timeout controls and captured-output projection. No runtime replay of that later revision is claimed. ### Runtime and platform Node.js v26.12.0; macOS 27.0.1, arm64. Repository Node pin: 24.18.0. Source tests with synthetic Binwalk/Unblob producers and mocked Linux/tool/limiter admission. Unblob was not installed. ### Target and artifact context Repository fixture containing a small nonempty regular file, plus a genuinely absent sibling path. Permission refusal used that same file with mode `000`; permissions were restored in `finally`. No real firmware engine, device, or extracted-code execution. ### Interface MCP ### Earliest failure stage Setup or provider admission ### Relevant output or evidence The reduced input-only run passed one file and two tests. Each operation produced these selected fields for both missing and permission-denied inputs: ```json { "code": "execution_failure", "category": "execution_failure", "retryable": false } ``` `details.provider_id` was `binwalk` for inspection and `unblob` for extraction. The missing-input diagnostic began `ENOENT: no such file or directory, realpath`; the unreadable-input diagnostic began `EACCES: permission denied, open`. No `details.issues`, `details.system_code`, or structured recovery was present. The message and remediation were: ```text Analysis could not complete. Run `rea doctor` and review the reported details; repeating the unchanged request is not expected to succeed. ``` The producer launch count was zero after both refusals. Selecting the readable file then succeeded, with two synthetic launches per operation (version and analysis); process/workspace cleanup assertions passed. These observations establish admission/error-projection behavior with simulated providers, not real-provider extraction correctness. ### Typed outcome or recovery expected For `ENOENT`, an input error identifying `path`, such as `issues: [{ path: ["path"], reason: "invalid_value" }]`, and input-contract correction. For `EACCES`, `access_denied` with the actual `system_code`, selected `path`, and `boundary: "filesystem-read"`. Preserve any independent cleanup failure and avoid classifying genuine producer failures as source-input errors. ### Analysis or suspected cause Source-derived: [FirmwareProvider snapshots the input](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/firmware/FirmwareProvider.ts#L335-L373) before running even the version command. [FirmwareFiles](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/firmware/FirmwareFiles.ts#L86-L122) performs `realpath` outside its admission catch and forwards regular-file acquisition errors through a mapper that [only classifies NonRegularFileReadError](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/firmware/FirmwareFiles.ts#L236-L244). Raw `ENOENT` and `EACCES` therefore reach the [ProviderAdapterError fallback](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/firmware/FirmwareFailures.ts#L122-L140). Classification belongs at the selected-source acquisition boundary, where the caller field and operation are known. A blanket filesystem-errno mapping in the generic firmware fallback could incorrectly blame the source for unrelated workspace/publication failures. ### Additional context Related but distinct: #5746 and open PR #5902 address output-directory admission; #5846 concerns staging budgets; #5877 and #5402 concern range schema issues; closed #4619 concerns snapshot growth beyond the input allowance; #5876 concerns a missing producer report after execution; #1855 concerns contradictory retry prose. Open PR #5672 addresses source streaming during publication setup, and PR #4493 addresses descriptor-close retention. None restores selected-input `ENOENT`/`EACCES` classification before producer launch. No known-good revision or completed repair was established.
Author
Owner

@coygeek commented on GitHub (Oct 24, 2026):

Checked the reported fix on main at d87e73f2db.

Firmware source snapshot admission now maps missing/invalid selected-path errno into a path AnalysisInputError, and EACCES/EPERM into AnalysisAccessDeniedError carrying path/system code. This mapping is scoped to source admission, so workspace/report/producer failures keep their identity. Regressions check missing source versus destination failure and real permission denial, separately from producer failures.

Implementation: FirmwareFiles.ts.

The relevant regression checks in firmwareReportFiles.test.ts passed in the focused test run on this revision. This closure is based on the current source and those checks; provider fixtures do not establish a real-engine run. Closing as completed on main.

<!-- gh-comment-id:6380731877 --> @coygeek commented on GitHub (Oct 24, 2026): Checked the reported fix on `main` at d87e73f2db50be72d33a4b3c7199af35518b441c. Firmware source snapshot admission now maps missing/invalid selected-path errno into a path AnalysisInputError, and EACCES/EPERM into AnalysisAccessDeniedError carrying path/system code. This mapping is scoped to source admission, so workspace/report/producer failures keep their identity. Regressions check missing source versus destination failure and real permission denial, separately from producer failures. Implementation: [FirmwareFiles.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/firmware/FirmwareFiles.ts#L314). The relevant regression checks in [firmwareReportFiles.test.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/tests/boundary/firmware/firmwareReportFiles.test.ts) passed in the focused test run on this revision. This closure is based on the current source and those checks; provider fixtures do not establish a real-engine run. Closing as completed on main.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#3624
No description provided.