[GH-ISSUE #5970] [Bug] JavaScript recovery classifies input read permission denial as invalid_request #3637

Closed
opened 2026-10-31 00:42:37 +00:00 by bsakaguchi · 0 comments
Owner

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

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

An executed recover_javascript_sources MCP request on a real mode-000 JavaScript file returned invalid_request / invalid_input, issue reason invalid_value, and advice to inspect the input schema or CLI usage. A separate real readFile call confirmed EACCES on that same selected file before the MCP request.

The path and EACCES survive in the issue's message text, but the response has no structured system_code or filesystem-read boundary and does not provide read-access recovery. No producer process was launched. The normal readable-input MCP control succeeded.

Reproduction

In a prepared source checkout with locked dependencies and existing build artifacts, place the following test at tests/boundary/javascript-recovery/recoveryErrorClassification.repro.test.ts, then run it with Node.js 24.18.0:

node node_modules/vitest/vitest.mjs run tests/boundary/javascript-recovery/recoveryErrorClassification.repro.test.ts --maxWorkers=1

This uses the repository's simulated producer and real owned Node subprocesses. Wakaru was not installed. The test temporarily simulates Linux x64 only while the real command-admission function runs, restoring the host descriptors before subprocess creation. A local executable substitutes for limiter admission; no util-linux resource containment or real Wakaru behavior is claimed. The normal recovery control succeeds through the same MCP registration and SDK transports. The complete observation run contained seven passing assertions; the relevant subset is below. The assertions record the current incorrect result so a passing test here means the behavior reproduced.

import { Client, InMemoryTransport } from '@modelcontextprotocol/client';
import { access, chmod, readFile } from 'node:fs/promises';
import { join } from 'node:path';
import { expect, it, onTestFinished, vi } from 'vitest';
import * as commandModule from '../../../src/javascript/recovery/WakaruCommand.js';
import { WakaruProvider } from '../../../src/javascript/recovery/WakaruProvider.js';
import { JavaScriptRecoveryService } from '../../../src/application/javascript/JavaScriptRecoveryService.js';
import { projectAnalysisError } from '../../../src/domain/analysisErrorProjection.js';
import { createServer } from '../../../src/server/createServer.js';
import { createTestBinarySession } from '../../fixtures/binarySession.js';
import { recoveryFixture, assertRecoveryCleanup } from '../../fixtures/javascript-recovery/provider.js';

// Only platform admission is simulated; real filesystem and owned Node processes follow.
const originalResolve = commandModule.resolveWakaruCommand;
const setup = async (mode = 'normal', limiter?: string) => {
  const fixture = await recoveryFixture(mode);
  const provider = fixture.provider as WakaruProvider;
  Object.assign(provider.environment, {
    REA_JAVASCRIPT_PRLIMIT_COMMAND: limiter ?? join(fixture.root, 'fixture-wakaru'),
  });
  const spy = vi.spyOn(commandModule, 'resolveWakaruCommand').mockImplementation(async env => {
    const platform = Object.getOwnPropertyDescriptor(process, 'platform')!;
    const arch = Object.getOwnPropertyDescriptor(process, 'arch')!;
    Object.defineProperty(process, 'platform', { ...platform, value: 'linux' });
    Object.defineProperty(process, 'arch', { ...arch, value: 'x64' });
    try { return await originalResolve(env); }
    finally {
      Object.defineProperty(process, 'platform', platform);
      Object.defineProperty(process, 'arch', arch);
    }
  });
  onTestFinished(async () => { spy.mockRestore(); await provider.close(); });
  return { ...fixture, provider };
};
const callMcp = async (fixture: Awaited<ReturnType<typeof setup>>) => {
  const session = createTestBinarySession(() => { throw new Error('No native provider expected'); });
  const server = createServer({ kind: 'session', session }, { javascriptRecovery: fixture.provider });
  const client = new Client({ name: 'recovery-error-test', version: '1' });
  const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
  try {
    await server.connect(serverTransport); await client.connect(clientTransport);
    return await client.callTool({ name: 'recover_javascript_sources', arguments: {
      path: fixture.path, output_directory: fixture.output,
    }});
  } finally { await client.close(); await server.close(); await session.close(); }
};
it('control: normal simulated recovery succeeds through MCP', async () => {
  const fixture = await setup();
  const response = await callMcp(fixture);
  console.log('control', JSON.stringify({ isError: response.isError ?? false }));
  expect(response.isError).not.toBe(true);
  await assertRecoveryCleanup(fixture.launches);
});
it('real EACCES on selected input becomes invalid_request through MCP', async () => {
  const fixture = await setup();
  await chmod(fixture.path, 0);
  try {
    await expect(readFile(fixture.path)).rejects.toMatchObject({ code: 'EACCES' });
    const response = await callMcp(fixture);
    console.log('access-denied', JSON.stringify(response));
    expect(response.isError).toBe(true);
    expect(JSON.stringify(response)).toContain('invalid_request');
    expect(JSON.stringify(response)).toContain('EACCES');
    expect(fixture.launches).toHaveLength(0);
  } finally { await chmod(fixture.path, 0o600); }
});

Expected behavior

The shared AnalysisAccessDeniedError contract explicitly distinguishes a denied selected read from malformed caller input. Recovery should preserve access_denied, category unavailable, the actual OS code and selected path, and the existing read-access remediation. Missing/nonregular input can retain its separate input-error semantics.

Impact and scope

P2: typed consumers cannot distinguish a correct path whose read is denied from malformed request data without parsing message text, and receive schema-correction guidance instead of read-access guidance. A manual workaround is to use the retained EACCES text and restore access or select a readable file. The mode-000 denial reproduced on this non-root host; EPERM, additional selections and mid-stream permission changes were not exercised. No input corruption or producer launch occurred.

REA version

rea-agents 6.7.0; source revision 52415975942a4b9c53a817915b313ccc404f280e.

Runtime and platform

macOS 27.0.1 arm64; Node.js v24.18.0 (repository pin), Vitest 5.0.3. An earlier observation run also completed on Node.js v24.22.0. No real Wakaru binary was used.

Target and artifact context

A generated local regular UTF-8 JavaScript file with permissions changed to 000, restored in the test's finally block. EACCES was a real host filesystem error; only engine/platform admission was simulated.

Interface

MCP

Earliest failure stage

Request parsing or validation

Relevant output or evidence

Actual MCP error fields, with the fixture location abbreviated:

{
  "code": "invalid_request",
  "category": "invalid_input",
  "retryable": false,
  "recovery": { "kind": "inspect_input_contract", "operation": "recover_javascript_sources" },
  "details": {
    "operation": "recover_javascript_sources",
    "issues": [{
      "path": ["path"],
      "reason": "invalid_value",
      "message": "Cannot snapshot UTF-8 JavaScript input <fixture>/selected.js: EACCES: permission denied, open '<fixture>/selected.js'"
    }]
  }
}

The action directs callers to the schema/CLI usage and says to correct fields. details.system_code and details.boundary are absent. The independent readFile assertion confirmed the same input failed with code: "EACCES"; the provider-launch list remained empty.

Typed outcome or recovery expected

Return AnalysisAccessDeniedError / access_denied, category unavailable, retaining the selected path, observed system_code: "EACCES" (or EPERM when actually observed) and boundary: "filesystem-read", with read-access remediation.

Analysis or suspected cause

The public MCP registration forwards inputs and execution options to the shared recovery service. The low-level scanner admission catch rethrows the OS denial. The snapshot wrapper preserves cancellation and measured byte limits, then maps other causes to recoveryInputFailure. That helper unconditionally creates an invalid-value input issue. This source path explains the measured classification.

Additional context

#5623 concerns JavaScript application path-resolution diagnostics, PR #1197 concerns BinaryTargetError, and #4629/PR #4757 concern recovery input allowance/UTF-8. None changes denied recovery-input reads. Upstream main 25e1a24a3ed3671a0148757555f18d16c29d936a was fetched and relevant recovery source/classification mappings remain unchanged. That newer revision was not executed. No real Wakaru, Linux host, EPERM or fix was tested.

Originally created by @coygeek on GitHub (Oct 21, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/5970 ### 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 An executed `recover_javascript_sources` MCP request on a real mode-000 JavaScript file returned `invalid_request` / `invalid_input`, issue reason `invalid_value`, and advice to inspect the input schema or CLI usage. A separate real `readFile` call confirmed `EACCES` on that same selected file before the MCP request. The path and `EACCES` survive in the issue's message text, but the response has no structured `system_code` or filesystem-read boundary and does not provide read-access recovery. No producer process was launched. The normal readable-input MCP control succeeded. ### Reproduction In a prepared source checkout with locked dependencies and existing build artifacts, place the following test at `tests/boundary/javascript-recovery/recoveryErrorClassification.repro.test.ts`, then run it with Node.js 24.18.0: ```sh node node_modules/vitest/vitest.mjs run tests/boundary/javascript-recovery/recoveryErrorClassification.repro.test.ts --maxWorkers=1 ``` This uses the repository's simulated producer and real owned Node subprocesses. Wakaru was not installed. The test temporarily simulates Linux x64 only while the real command-admission function runs, restoring the host descriptors before subprocess creation. A local executable substitutes for limiter admission; no util-linux resource containment or real Wakaru behavior is claimed. The normal recovery control succeeds through the same MCP registration and SDK transports. The complete observation run contained seven passing assertions; the relevant subset is below. The assertions record the current incorrect result so a passing test here means the behavior reproduced. ```ts import { Client, InMemoryTransport } from '@modelcontextprotocol/client'; import { access, chmod, readFile } from 'node:fs/promises'; import { join } from 'node:path'; import { expect, it, onTestFinished, vi } from 'vitest'; import * as commandModule from '../../../src/javascript/recovery/WakaruCommand.js'; import { WakaruProvider } from '../../../src/javascript/recovery/WakaruProvider.js'; import { JavaScriptRecoveryService } from '../../../src/application/javascript/JavaScriptRecoveryService.js'; import { projectAnalysisError } from '../../../src/domain/analysisErrorProjection.js'; import { createServer } from '../../../src/server/createServer.js'; import { createTestBinarySession } from '../../fixtures/binarySession.js'; import { recoveryFixture, assertRecoveryCleanup } from '../../fixtures/javascript-recovery/provider.js'; // Only platform admission is simulated; real filesystem and owned Node processes follow. const originalResolve = commandModule.resolveWakaruCommand; const setup = async (mode = 'normal', limiter?: string) => { const fixture = await recoveryFixture(mode); const provider = fixture.provider as WakaruProvider; Object.assign(provider.environment, { REA_JAVASCRIPT_PRLIMIT_COMMAND: limiter ?? join(fixture.root, 'fixture-wakaru'), }); const spy = vi.spyOn(commandModule, 'resolveWakaruCommand').mockImplementation(async env => { const platform = Object.getOwnPropertyDescriptor(process, 'platform')!; const arch = Object.getOwnPropertyDescriptor(process, 'arch')!; Object.defineProperty(process, 'platform', { ...platform, value: 'linux' }); Object.defineProperty(process, 'arch', { ...arch, value: 'x64' }); try { return await originalResolve(env); } finally { Object.defineProperty(process, 'platform', platform); Object.defineProperty(process, 'arch', arch); } }); onTestFinished(async () => { spy.mockRestore(); await provider.close(); }); return { ...fixture, provider }; }; const callMcp = async (fixture: Awaited<ReturnType<typeof setup>>) => { const session = createTestBinarySession(() => { throw new Error('No native provider expected'); }); const server = createServer({ kind: 'session', session }, { javascriptRecovery: fixture.provider }); const client = new Client({ name: 'recovery-error-test', version: '1' }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); try { await server.connect(serverTransport); await client.connect(clientTransport); return await client.callTool({ name: 'recover_javascript_sources', arguments: { path: fixture.path, output_directory: fixture.output, }}); } finally { await client.close(); await server.close(); await session.close(); } }; it('control: normal simulated recovery succeeds through MCP', async () => { const fixture = await setup(); const response = await callMcp(fixture); console.log('control', JSON.stringify({ isError: response.isError ?? false })); expect(response.isError).not.toBe(true); await assertRecoveryCleanup(fixture.launches); }); it('real EACCES on selected input becomes invalid_request through MCP', async () => { const fixture = await setup(); await chmod(fixture.path, 0); try { await expect(readFile(fixture.path)).rejects.toMatchObject({ code: 'EACCES' }); const response = await callMcp(fixture); console.log('access-denied', JSON.stringify(response)); expect(response.isError).toBe(true); expect(JSON.stringify(response)).toContain('invalid_request'); expect(JSON.stringify(response)).toContain('EACCES'); expect(fixture.launches).toHaveLength(0); } finally { await chmod(fixture.path, 0o600); } }); ``` ### Expected behavior The shared [AnalysisAccessDeniedError contract](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/domain/analysisErrorCore.ts#L188-L202) explicitly distinguishes a denied selected read from malformed caller input. Recovery should preserve `access_denied`, category `unavailable`, the actual OS code and selected path, and the existing [read-access remediation](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/domain/analysisErrorPresentation.ts#L125-L128). Missing/nonregular input can retain its separate input-error semantics. ### Impact and scope P2: typed consumers cannot distinguish a correct path whose read is denied from malformed request data without parsing message text, and receive schema-correction guidance instead of read-access guidance. A manual workaround is to use the retained EACCES text and restore access or select a readable file. The mode-000 denial reproduced on this non-root host; EPERM, additional selections and mid-stream permission changes were not exercised. No input corruption or producer launch occurred. ### REA version rea-agents 6.7.0; source revision `52415975942a4b9c53a817915b313ccc404f280e`. ### Runtime and platform macOS 27.0.1 arm64; Node.js v24.18.0 (repository pin), Vitest 5.0.3. An earlier observation run also completed on Node.js v24.22.0. No real Wakaru binary was used. ### Target and artifact context A generated local regular UTF-8 JavaScript file with permissions changed to 000, restored in the test's finally block. EACCES was a real host filesystem error; only engine/platform admission was simulated. ### Interface MCP ### Earliest failure stage Request parsing or validation ### Relevant output or evidence Actual MCP error fields, with the fixture location abbreviated: ```json { "code": "invalid_request", "category": "invalid_input", "retryable": false, "recovery": { "kind": "inspect_input_contract", "operation": "recover_javascript_sources" }, "details": { "operation": "recover_javascript_sources", "issues": [{ "path": ["path"], "reason": "invalid_value", "message": "Cannot snapshot UTF-8 JavaScript input <fixture>/selected.js: EACCES: permission denied, open '<fixture>/selected.js'" }] } } ``` The action directs callers to the schema/CLI usage and says to correct fields. `details.system_code` and `details.boundary` are absent. The independent `readFile` assertion confirmed the same input failed with `code: "EACCES"`; the provider-launch list remained empty. ### Typed outcome or recovery expected Return `AnalysisAccessDeniedError` / `access_denied`, category `unavailable`, retaining the selected path, observed `system_code: "EACCES"` (or EPERM when actually observed) and `boundary: "filesystem-read"`, with read-access remediation. ### Analysis or suspected cause The public [MCP registration](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/server/registerJavaScriptRecoveryTool.ts#L19-L33) forwards inputs and execution options to the shared recovery service. The low-level [scanner admission catch](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/javascript/recovery/RecoveryFiles.ts#L44-L62) rethrows the OS denial. The [snapshot wrapper](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/javascript/recovery/RecoveryFiles.ts#L150-L176) preserves cancellation and measured byte limits, then maps other causes to `recoveryInputFailure`. That [helper](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/javascript/recovery/RecoveryFiles.ts#L326-L334) unconditionally creates an invalid-value input issue. This source path explains the measured classification. ### Additional context #5623 concerns JavaScript application path-resolution diagnostics, PR #1197 concerns BinaryTargetError, and #4629/PR #4757 concern recovery input allowance/UTF-8. None changes denied recovery-input reads. Upstream main `25e1a24a3ed3671a0148757555f18d16c29d936a` was fetched and relevant recovery source/classification mappings remain unchanged. That newer revision was not executed. No real Wakaru, Linux host, EPERM or fix was tested.
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#3637
No description provided.