[GH-ISSUE #5450] [Bug] MCP bundle comparison accepts relative paths and reads from the server working directory #3312

Closed
opened 2026-10-31 00:39:03 +00:00 by bsakaguchi · 2 comments
Owner

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

Before submitting

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

Area

MCP or agent integration

Observed behavior

compare_bundles accepts relative left_bundle_path and right_bundle_path values and reads a bundle from the MCP server's working directory, despite the documented absolute-host-path requirement. Both-relative, left-only-relative, and right-only-relative requests returned successful Evidence with normalized_result.status: "unchanged". Absolute comparison and import controls succeeded; the sibling import_evidence_bundle rejected its relative path.

Reproduction

An independent verifier executed this exact harness from the baseline checkout with dependencies and build artifacts present. It creates an empty canonical bundle, isolates the server's working directory and HOME/XDG directories, and calls the built stdio MCP entry point. No provider or network API is needed.

VERIFY_ROOT=$(mktemp -d) node --input-type=module <<'JS'
import { Client } from '@modelcontextprotocol/client';
import { StdioClientTransport } from '@modelcontextprotocol/client/stdio';
import { mkdir, writeFile } from 'node:fs/promises';
import { join, resolve } from 'node:path';

const root = resolve(process.env.VERIFY_ROOT);
const env = { PATH: process.env.PATH, REA_LOG_LEVEL: 'silent' };
for (const key of ['HOME', 'XDG_CONFIG_HOME', 'XDG_DATA_HOME',
                   'XDG_CACHE_HOME', 'XDG_STATE_HOME']) {
  env[key] = join(root, key);
  await mkdir(env[key], { recursive: true });
}
env.HOPPER_LAUNCHER_PATH = join(root, 'absent-provider');
const absolute = join(root, 'evidence.json');
await writeFile(absolute, JSON.stringify({ artifacts: [], providers: [],
  environments: [], scenarios: [], captures: [], unknowns: [], records: [] }));
const transport = new StdioClientTransport({
  command: process.execPath,
  args: [resolve('scripts/rea.mjs'), 'mcp'], cwd: root, env, stderr: 'pipe'
});
transport.stderr?.on('data', () => {});
const client = new Client({ name: 'bundle-path-check', version: '1' });
try {
  await client.connect(transport);
  for (const [label, left, right] of [
    ['relative-both', 'evidence.json', 'evidence.json'],
    ['relative-left', 'evidence.json', absolute],
    ['relative-right', absolute, 'evidence.json'],
    ['absolute-control', absolute, absolute]
  ]) {
    const result = await client.callTool({ name: 'compare_bundles', arguments: {
      left_bundle_path: left, right_bundle_path: right
    }});
    console.log(label, JSON.stringify({ isError: result.isError === true,
      status: result.structuredContent?.normalized_result?.status }));
  }
  for (const [label, path] of [
    ['relative-import', 'evidence.json'], ['absolute-import', absolute]
  ]) {
    const result = await client.callTool({ name: 'import_evidence_bundle',
      arguments: { path } });
    console.log(label, JSON.stringify({ isError: result.isError === true,
      mentionsAbsolute: JSON.stringify(result.content).includes('absolute') }));
  }
} finally {
  await client.close();
  await transport.close();
}
JS

All three relative comparison variants succeeded in the executed run. The output below shows each result and its controls.

Expected behavior

Reject a relative value in either comparison path field before reading a file, with validation identifying the offending field and the absolute-path requirement. Keep absolute comparisons successful and preserve CLI operator-relative paths.

The 6.0.0 filesystem-input breaking change in CHANGELOG.md:907 explicitly includes evidence bundle paths in the MCP absolute-host-path rule and preserves CLI operator-relative paths. Sibling import/export contracts enforce this at src/contracts/sessionToolSchemas.ts:29-38 and src/contracts/sessionToolContracts.ts:56-65 (baseline revision below).

Impact and scope

P2: a bounded path-validation defect with a verified workaround of supplying absolute paths. The observed relative requests read the disposable fixture from the server working directory. Selecting a same-named unintended file under another server launch directory is a source-derived consequence, not an observed user-data incident. Either field is affected independently. No privilege or isolation boundary crossing was observed. Other platforms and real-world frequency were not tested.

REA version

rea-agents 6.7.0; source commit 074768901f3cdc8205b61773fb1faf6f6c7b3ca8 (built source CLI).

Runtime and platform

Node.js v26.12.0; macOS arm64. NOT_ENOUGH_INFO: exact macOS release was not recorded in this verifier's notes. No Hopper/Ghidra provider was used.

Target and artifact context

Canonical empty Evidence bundle: { "artifacts": [], "providers": [], "environments": [], "scenarios": [], "captures": [], "unknowns": [], "records": [] }. Local stdio MCP transport with a controlled server working directory and isolated HOME/XDG directories; no binary or live target.

Interface

MCP

Earliest failure stage

Request parsing or validation

Relevant output or evidence

The independently executed harness printed:

relative-both {"isError":false,"status":"unchanged"}
relative-left {"isError":false,"status":"unchanged"}
relative-right {"isError":false,"status":"unchanged"}
absolute-control {"isError":false,"status":"unchanged"}
relative-import {"isError":true,"mentionsAbsolute":true}
absolute-import {"isError":false,"mentionsAbsolute":false}

All comparison cases returned the same bundle digests. In a separate executed compare_bundles call with both fields set to missing.json, the result was isError: true, with invalid_request, details.reason: "missing", and a server-working-directory-resolved absolute path. It did not reject the relative path shape. The relative import control returned plain MCP input-validation text identifying path and requiring an absolute filesystem path.

A separate CLI control, rea compare evidence.json evidence.json --json, run through the built source CLI from the fixture directory, exited successfully with top-level status: "unchanged".

Typed outcome or recovery expected

Return an MCP validation error identifying the relative field and requiring an absolute host filesystem path. A nonexistent relative filename should receive that correction before a missing-file read. The precise error envelope is not prescribed; the sibling import currently uses plain MCP validation text.

Analysis or suspected cause

Source-derived: src/contracts/sessionToolContracts.ts:115-118 registers compare_bundles with bundleComparisonInputSchema. At src/domain/bundleComparison.ts:27-29, both paths use a nonempty, NUL-free string schema without an absolute-path refinement. src/server/registerBundleComparisonTool.ts:35-46 reads both paths; src/application/JsonFiles.ts:98 resolves them through src/filesystem/callerPath.ts:16-35, whose default base is the server process working directory.

Additional context

Prepared with AI assistance and independently reproduced by a separate verifier. The submitter reviewed the executed commands and results; the runtime reproduction above is attributed to that verifier.

After fetching upstream main at 052956af3416fa01d93837e54fdec3ad2739f6d1, the comparison schema, handler and path-reading code remain unchanged from the tested baseline. The intervening session-contract change adds native-function matching and does not modify comparison path validation. No upstream runtime rerun is claimed. NOT_ENOUGH_INFO: first affected release and last known good version.

Originally created by @coygeek on GitHub (Oct 20, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/5450 ### 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 MCP or agent integration ### Observed behavior `compare_bundles` accepts relative `left_bundle_path` and `right_bundle_path` values and reads a bundle from the MCP server's working directory, despite the documented absolute-host-path requirement. Both-relative, left-only-relative, and right-only-relative requests returned successful Evidence with `normalized_result.status: "unchanged"`. Absolute comparison and import controls succeeded; the sibling `import_evidence_bundle` rejected its relative path. ### Reproduction An independent verifier executed this exact harness from the baseline checkout with dependencies and build artifacts present. It creates an empty canonical bundle, isolates the server's working directory and HOME/XDG directories, and calls the built stdio MCP entry point. No provider or network API is needed. ```sh VERIFY_ROOT=$(mktemp -d) node --input-type=module <<'JS' import { Client } from '@modelcontextprotocol/client'; import { StdioClientTransport } from '@modelcontextprotocol/client/stdio'; import { mkdir, writeFile } from 'node:fs/promises'; import { join, resolve } from 'node:path'; const root = resolve(process.env.VERIFY_ROOT); const env = { PATH: process.env.PATH, REA_LOG_LEVEL: 'silent' }; for (const key of ['HOME', 'XDG_CONFIG_HOME', 'XDG_DATA_HOME', 'XDG_CACHE_HOME', 'XDG_STATE_HOME']) { env[key] = join(root, key); await mkdir(env[key], { recursive: true }); } env.HOPPER_LAUNCHER_PATH = join(root, 'absent-provider'); const absolute = join(root, 'evidence.json'); await writeFile(absolute, JSON.stringify({ artifacts: [], providers: [], environments: [], scenarios: [], captures: [], unknowns: [], records: [] })); const transport = new StdioClientTransport({ command: process.execPath, args: [resolve('scripts/rea.mjs'), 'mcp'], cwd: root, env, stderr: 'pipe' }); transport.stderr?.on('data', () => {}); const client = new Client({ name: 'bundle-path-check', version: '1' }); try { await client.connect(transport); for (const [label, left, right] of [ ['relative-both', 'evidence.json', 'evidence.json'], ['relative-left', 'evidence.json', absolute], ['relative-right', absolute, 'evidence.json'], ['absolute-control', absolute, absolute] ]) { const result = await client.callTool({ name: 'compare_bundles', arguments: { left_bundle_path: left, right_bundle_path: right }}); console.log(label, JSON.stringify({ isError: result.isError === true, status: result.structuredContent?.normalized_result?.status })); } for (const [label, path] of [ ['relative-import', 'evidence.json'], ['absolute-import', absolute] ]) { const result = await client.callTool({ name: 'import_evidence_bundle', arguments: { path } }); console.log(label, JSON.stringify({ isError: result.isError === true, mentionsAbsolute: JSON.stringify(result.content).includes('absolute') })); } } finally { await client.close(); await transport.close(); } JS ``` All three relative comparison variants succeeded in the executed run. The output below shows each result and its controls. ### Expected behavior Reject a relative value in either comparison path field before reading a file, with validation identifying the offending field and the absolute-path requirement. Keep absolute comparisons successful and preserve CLI operator-relative paths. The 6.0.0 filesystem-input breaking change in `CHANGELOG.md:907` explicitly includes evidence bundle paths in the MCP absolute-host-path rule and preserves CLI operator-relative paths. Sibling import/export contracts enforce this at `src/contracts/sessionToolSchemas.ts:29-38` and `src/contracts/sessionToolContracts.ts:56-65` (baseline revision below). ### Impact and scope P2: a bounded path-validation defect with a verified workaround of supplying absolute paths. The observed relative requests read the disposable fixture from the server working directory. Selecting a same-named unintended file under another server launch directory is a source-derived consequence, not an observed user-data incident. Either field is affected independently. No privilege or isolation boundary crossing was observed. Other platforms and real-world frequency were not tested. ### REA version rea-agents 6.7.0; source commit `074768901f3cdc8205b61773fb1faf6f6c7b3ca8` (built source CLI). ### Runtime and platform Node.js v26.12.0; macOS arm64. NOT_ENOUGH_INFO: exact macOS release was not recorded in this verifier's notes. No Hopper/Ghidra provider was used. ### Target and artifact context Canonical empty Evidence bundle: `{ "artifacts": [], "providers": [], "environments": [], "scenarios": [], "captures": [], "unknowns": [], "records": [] }`. Local stdio MCP transport with a controlled server working directory and isolated HOME/XDG directories; no binary or live target. ### Interface MCP ### Earliest failure stage Request parsing or validation ### Relevant output or evidence The independently executed harness printed: ```text relative-both {"isError":false,"status":"unchanged"} relative-left {"isError":false,"status":"unchanged"} relative-right {"isError":false,"status":"unchanged"} absolute-control {"isError":false,"status":"unchanged"} relative-import {"isError":true,"mentionsAbsolute":true} absolute-import {"isError":false,"mentionsAbsolute":false} ``` All comparison cases returned the same bundle digests. In a separate executed `compare_bundles` call with both fields set to `missing.json`, the result was `isError: true`, with `invalid_request`, `details.reason: "missing"`, and a server-working-directory-resolved absolute path. It did not reject the relative path shape. The relative import control returned plain MCP input-validation text identifying `path` and requiring an absolute filesystem path. A separate CLI control, `rea compare evidence.json evidence.json --json`, run through the built source CLI from the fixture directory, exited successfully with top-level `status: "unchanged"`. ### Typed outcome or recovery expected Return an MCP validation error identifying the relative field and requiring an absolute host filesystem path. A nonexistent relative filename should receive that correction before a missing-file read. The precise error envelope is not prescribed; the sibling import currently uses plain MCP validation text. ### Analysis or suspected cause Source-derived: `src/contracts/sessionToolContracts.ts:115-118` registers `compare_bundles` with `bundleComparisonInputSchema`. At `src/domain/bundleComparison.ts:27-29`, both paths use a nonempty, NUL-free string schema without an absolute-path refinement. `src/server/registerBundleComparisonTool.ts:35-46` reads both paths; `src/application/JsonFiles.ts:98` resolves them through `src/filesystem/callerPath.ts:16-35`, whose default base is the server process working directory. ### Additional context Prepared with AI assistance and independently reproduced by a separate verifier. The submitter reviewed the executed commands and results; the runtime reproduction above is attributed to that verifier. After fetching upstream main at `052956af3416fa01d93837e54fdec3ad2739f6d1`, the comparison schema, handler and path-reading code remain unchanged from the tested baseline. The intervening session-contract change adds native-function matching and does not modify comparison path validation. No upstream runtime rerun is claimed. NOT_ENOUGH_INFO: first affected release and last known good version.
Author
Owner

@matbrgz commented on GitHub (Oct 20, 2026):

Cross-referencing with canonical issue #3924 ([Bug] MCP JavaScript application analysis accepts a relative input_path) and sibling report #5493 ([Bug] MCP input schemas omit host-absolute local path constraints).

Consolidated under Cluster CA (MCP Tool Input Schemas & Host-Absolute Path Constraints) in tracker #4330.

<!-- gh-comment-id:6303858424 --> @matbrgz commented on GitHub (Oct 20, 2026): Cross-referencing with canonical issue #3924 (`[Bug] MCP JavaScript application analysis accepts a relative input_path`) and sibling report #5493 (`[Bug] MCP input schemas omit host-absolute local path constraints`). Consolidated under **Cluster CA (MCP Tool Input Schemas & Host-Absolute Path Constraints)** in tracker #4330.
Author
Owner

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

Checked the reported fix on main at d87e73f2db.

compare_bundles now advertises and uses a dedicated MCP schema that overrides both path fields with the host-absolute local-path schema. The CLI/domain schema remains relative-path capable. Inspected SDK-boundary assertions reject both-relative and each independently relative field, identify the offending field and absolute requirement, validate the advertised JSON Schema, retain absolute comparison/import controls, and retain CLI relative resolution.

Implementation: sessionToolSchemas.ts, sessionToolContracts.ts.

The relevant regression checks in bundleComparisonMcp.test.ts, bundleComparisonCli.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:6380695858 --> @coygeek commented on GitHub (Oct 24, 2026): Checked the reported fix on `main` at d87e73f2db50be72d33a4b3c7199af35518b441c. compare_bundles now advertises and uses a dedicated MCP schema that overrides both path fields with the host-absolute local-path schema. The CLI/domain schema remains relative-path capable. Inspected SDK-boundary assertions reject both-relative and each independently relative field, identify the offending field and absolute requirement, validate the advertised JSON Schema, retain absolute comparison/import controls, and retain CLI relative resolution. Implementation: [sessionToolSchemas.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/contracts/sessionToolSchemas.ts#L35), [sessionToolContracts.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/contracts/sessionToolContracts.ts#L122). The relevant regression checks in [bundleComparisonMcp.test.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/tests/boundary/mcp/bundleComparisonMcp.test.ts), [bundleComparisonCli.test.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/tests/boundary/cli/bundleComparisonCli.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#3312
No description provided.