[GH-ISSUE #7607] [Bug] Reference-source root failures recommend permission repair for symlink loops and oversized names #4538

Closed
opened 2026-10-31 00:50:36 +00:00 by bsakaguchi · 0 comments
Owner

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

Before submitting

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

Area

CLI

Observed behavior

Selecting a two-link symbolic-link cycle with node scripts/rea.mjs import-reference-source loop-a --json fails with an ELOOP diagnostic, but its recovery action says: Reference source files could not be read. Check directory permissions and try again. Selecting a single 300-character filename component produces ENAMETOOLONG and the same permission advice. Neither failure is caused by directory permissions.

Both responses use unreadable_output / execution_failure, retain the actual filesystem reason in their message, and omit argument issues. The misleading recovery instruction is the primary defect here. Missing and regular-file roots instead receive input errors, and a readable directory imports successfully.

Reproduction

  1. From the built baseline checkout, select Node.js v24.22.1 and run this inline fixture. This is the body of the independently executed standalone recipe; it creates all inputs, isolates HOME/XDG, checks both failures and three controls twice, and removes its fixture tree. REPRO_SCRATCH optionally chooses an existing disposable parent directory.
node --input-type=module <<'JS'
import { mkdtempSync, mkdirSync, writeFileSync, symlinkSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
import { spawnSync } from 'node:child_process';
import assert from 'node:assert/strict';
const cli = resolve('scripts/rea.mjs');
const work = mkdtempSync(join(process.env.REPRO_SCRATCH ?? tmpdir(), 'reference-root-'));
const env = { ...process.env };
for (const key of Object.keys(env)) {
  if (/^(REA_|HOPPER_|GHIDRA_)/.test(key)) delete env[key];
}
try {
  for (const key of ['HOME', 'XDG_CONFIG_HOME', 'XDG_DATA_HOME', 'XDG_CACHE_HOME', 'XDG_STATE_HOME']) {
    env[key] = join(work, key.toLowerCase());
    mkdirSync(env[key]);
  }
  symlinkSync('loop-b', join(work, 'loop-a'));
  symlinkSync('loop-a', join(work, 'loop-b'));
  mkdirSync(join(work, 'valid'));
  writeFileSync(join(work, 'valid', 'index.js'), 'export const value = 1;\n');
  writeFileSync(join(work, 'plain-file'), 'control\n');
  for (let attempt = 1; attempt <= 2; attempt++) {
    for (const [label, selected, expected] of [
      ['loop', 'loop-a', 'unreadable_output'],
      ['long-component', 'z'.repeat(300), 'unreadable_output'],
      ['missing-control', 'absent', 'invalid_request'],
      ['file-control', 'plain-file', 'invalid_request'],
      ['valid-control', 'valid', undefined],
    ]) {
      const run = spawnSync(process.execPath, [cli, 'import-reference-source', selected, '--json'], { cwd: work, env, encoding: 'utf8', timeout: 30000 });
      if (run.error) throw run.error;
      const result = JSON.parse(run.stdout);
      assert.equal(run.status, expected ? 1 : 0);
      assert.equal(result.code, expected);
      assert.equal(run.stderr, '');
      if (expected === 'unreadable_output') {
        assert.match(result.remediation.action, /Check directory permissions/);
        assert.match(result.message, label === 'loop' ? /ELOOP/ : /ENAMETOOLONG/);
        assert.equal(result.details.issues, undefined);
      }
      if (!expected) assert.equal(result.entries[0].path, 'index.js');
      console.log(JSON.stringify({ attempt, label, exit: run.status, code: result.code ?? 'success', category: result.category, remediation: result.remediation?.action }));
    }
  }
} finally {
  rmSync(work, { recursive: true, force: true });
}
JS
  1. Read the loop and long-component rows. Each exits 1 and reports the permission-repair action. Assertions also verify the retained ELOOP/ENAMETOOLONG reason, absence of details.issues, and empty stderr. Both attempts passed every assertion. An earlier independent replay reproduced each failure twice as well, for four observations per failing input overall.

Expected behavior

Recovery for an initially unresolvable source root should explain how to correct that path: repair the symbolic-link chain or select a path within the host's length limits. Permission checks should be recommended for permission failures. The documented import-failure result includes remediation, and the repository requires meaningful boundary failure reasons.

This report does not require a particular replacement code. An input refusal identifying root, or an I/O failure with suitable path-specific guidance, can communicate the required correction. Reference-import documentation does not explicitly prescribe invalid_request for these two errno values; the model-artifact selection contract applies to a different command family.

Impact and scope

P3: incorrect recovery text for invalid local selections that are already refused safely. A caller following only remediation.action is directed toward a repair unrelated to the reported host constraint. No downstream automated recovery, data loss, wrong successful analysis, or security-boundary crossing was observed. The actual errno remains available for a human to diagnose the problem.

Observed scope is the public CLI on macOS with initial root resolution failing. Readable-source, absent-root, and regular-file-root controls behaved consistently. A practical bypass was demonstrated by selecting a valid short directory, which returned its index.js inventory. Nested-entry failures, MCP transport, other operating systems and real-world frequency were not tested.

REA version

Exact node scripts/rea.mjs --version stdout:

6.8.0

The stdout ends with a newline. Source baseline: 65f1c447344a423fa2785847dbcea1e9a9e2454a; the checkout's prebuilt CLI was executed.

Runtime and platform

Node.js v24.22.1; macOS 27.0.1; darwin arm64. No analysis provider or GUI application was launched.

Target and artifact context

Self-authored loop-a -> loop-b -> loop-a links; a generated 300-character ASCII component argument; a regular text file; an absent path; and a directory containing one inert JavaScript source file. Source text was imported as data and never executed. All fixture trees were deleted.

Interface

CLI

Earliest failure stage

Request parsing or validation

Relevant output or evidence

The standalone recipe produced these rows in each of its two attempts:

selection        exit  code               category
loop             1     unreadable_output  execution_failure
long-component   1     unreadable_output  execution_failure
missing-control  1     invalid_request    invalid_input
file-control     1     invalid_request    invalid_input
valid-control    0     success

Both failing selections returned this exact recovery object:

{"action":"Reference source files could not be read. Check directory permissions and try again."}

The initial replay captured the complete loop message:

Reference source root could not be resolved: ELOOP: too many symbolic links encountered, stat 'loop-a' Reference source files could not be read. Check directory permissions and try again.

The oversized-name message similarly retained ENAMETOOLONG: name too long, stat and the selected component. Both included details.diagnostics.reference_source_code: "io". Thus the native reason is preserved; the defect is the recovery instruction appended to it.

Contract and source checks at the baseline:

  • AGENTS.md, Architecture and Cleanup, requires meaningful reasons for expected boundary failures. CONTRIBUTING.md, Choosing a contribution, explicitly recognizes incorrect user instructions as reportable.
  • docs/cli.md:637-641 documents the shared error result for import failures, including remediation. The filesystem-recovery passage at docs/cli.md:604-610 concerns inventory entries; it does not establish the root error-code expectation. docs/model-artifacts.md:170-172 is model-specific and supplies no binding reference-import classification.
  • src/cli/utilityCommands.ts:277-283 exposes the local source directory argument. src/application/ReferenceSourceImportPolicy.ts:78-107 performs initial root stat and forwards a recognized failure before policy reads or traversal.
  • src/reference/ReferenceSourceReaderErrors.ts:82-99 labels recognized root errno values other than ENOENT/ENOTDIR as io. src/application/ReferenceSourceImportTypes.ts:29-38 selects the generic permission advice; lines 92-98 project it as an output error. Its invalid-root branch at lines 77-82 instead supplies an input issue naming root.

Typed outcome or recovery expected

Retain the failure exit, original path and host reason. Make remediation.action address the link chain or path length, rather than directory permissions. Any category adjustment should preserve genuine permission, host-resource, cancellation, cleanup and nested partial-inventory behavior. No code change or restored behavior was tested.

Analysis or suspected cause

Source-derived explanation: the initial-root classifier groups these path-resolution failures with other I/O failures. That broad io outcome owns only one recovery string, so it always supplies permission advice regardless of the retained errno. The CLI observations establish the wrong instruction independently of a proposed implementation change.

Additional context

Closed #2551 required an oversized root to return a tagged reader Result retaining the native reason instead of rejecting. Its expected result explicitly allowed invalid-root or io, and its owner's closure was limited to that guarantee. The current result satisfies that closure. This report does not claim that fix regressed.

Closed #2652 and merged PR #2716 concern paths exceeding the limit later inside a valid tree. Their restoration check preserves readable siblings; this case fails before traversal. Closed #5399 concerned missing canonical fields, which are present here.

The path-classification precedents #5949, #4188, #6754 and #6740 concern plist, SQLite, entropy and EVM operations respectively. They do not cover this reference-import producer. Open #7404 and #7561 concern empty selections resolving to the working directory, a different input and restoration condition. Open/closed issue, PR and issue-comment searches found no matching report for this root-recovery instruction.

Triage assessment

  • Impact level: P3
  • Assessment status: supported
  • Impact basis: Initial invalid roots are safely refused and retain their actual host reason, but the recovery text recommends an unrelated permission repair. No consequential downstream behavior was demonstrated.
  • Workaround status: available
  • Workaround basis: The retained ELOOP/ENAMETOOLONG message identifies the path constraint; selecting the tested valid short directory succeeds and returns its source-file inventory. This bypasses the invalid selection without fixing the misleading recovery field.
Originally created by @coygeek on GitHub (Oct 27, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/7607 ### 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 CLI ### Observed behavior Selecting a two-link symbolic-link cycle with `node scripts/rea.mjs import-reference-source loop-a --json` fails with an `ELOOP` diagnostic, but its recovery action says: `Reference source files could not be read. Check directory permissions and try again.` Selecting a single 300-character filename component produces `ENAMETOOLONG` and the same permission advice. Neither failure is caused by directory permissions. Both responses use `unreadable_output` / `execution_failure`, retain the actual filesystem reason in their message, and omit argument issues. The misleading recovery instruction is the primary defect here. Missing and regular-file roots instead receive input errors, and a readable directory imports successfully. ### Reproduction 1. From the built baseline checkout, select Node.js v24.22.1 and run this inline fixture. This is the body of the independently executed standalone recipe; it creates all inputs, isolates HOME/XDG, checks both failures and three controls twice, and removes its fixture tree. REPRO_SCRATCH optionally chooses an existing disposable parent directory. ```sh node --input-type=module <<'JS' import { mkdtempSync, mkdirSync, writeFileSync, symlinkSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join, resolve } from 'node:path'; import { spawnSync } from 'node:child_process'; import assert from 'node:assert/strict'; const cli = resolve('scripts/rea.mjs'); const work = mkdtempSync(join(process.env.REPRO_SCRATCH ?? tmpdir(), 'reference-root-')); const env = { ...process.env }; for (const key of Object.keys(env)) { if (/^(REA_|HOPPER_|GHIDRA_)/.test(key)) delete env[key]; } try { for (const key of ['HOME', 'XDG_CONFIG_HOME', 'XDG_DATA_HOME', 'XDG_CACHE_HOME', 'XDG_STATE_HOME']) { env[key] = join(work, key.toLowerCase()); mkdirSync(env[key]); } symlinkSync('loop-b', join(work, 'loop-a')); symlinkSync('loop-a', join(work, 'loop-b')); mkdirSync(join(work, 'valid')); writeFileSync(join(work, 'valid', 'index.js'), 'export const value = 1;\n'); writeFileSync(join(work, 'plain-file'), 'control\n'); for (let attempt = 1; attempt <= 2; attempt++) { for (const [label, selected, expected] of [ ['loop', 'loop-a', 'unreadable_output'], ['long-component', 'z'.repeat(300), 'unreadable_output'], ['missing-control', 'absent', 'invalid_request'], ['file-control', 'plain-file', 'invalid_request'], ['valid-control', 'valid', undefined], ]) { const run = spawnSync(process.execPath, [cli, 'import-reference-source', selected, '--json'], { cwd: work, env, encoding: 'utf8', timeout: 30000 }); if (run.error) throw run.error; const result = JSON.parse(run.stdout); assert.equal(run.status, expected ? 1 : 0); assert.equal(result.code, expected); assert.equal(run.stderr, ''); if (expected === 'unreadable_output') { assert.match(result.remediation.action, /Check directory permissions/); assert.match(result.message, label === 'loop' ? /ELOOP/ : /ENAMETOOLONG/); assert.equal(result.details.issues, undefined); } if (!expected) assert.equal(result.entries[0].path, 'index.js'); console.log(JSON.stringify({ attempt, label, exit: run.status, code: result.code ?? 'success', category: result.category, remediation: result.remediation?.action })); } } } finally { rmSync(work, { recursive: true, force: true }); } JS ``` 2. Read the `loop` and `long-component` rows. Each exits 1 and reports the permission-repair action. Assertions also verify the retained `ELOOP`/`ENAMETOOLONG` reason, absence of `details.issues`, and empty stderr. Both attempts passed every assertion. An earlier independent replay reproduced each failure twice as well, for four observations per failing input overall. ### Expected behavior Recovery for an initially unresolvable source root should explain how to correct that path: repair the symbolic-link chain or select a path within the host's length limits. Permission checks should be recommended for permission failures. The documented import-failure result includes remediation, and the repository requires meaningful boundary failure reasons. This report does not require a particular replacement code. An input refusal identifying `root`, or an I/O failure with suitable path-specific guidance, can communicate the required correction. Reference-import documentation does not explicitly prescribe `invalid_request` for these two errno values; the model-artifact selection contract applies to a different command family. ### Impact and scope P3: incorrect recovery text for invalid local selections that are already refused safely. A caller following only `remediation.action` is directed toward a repair unrelated to the reported host constraint. No downstream automated recovery, data loss, wrong successful analysis, or security-boundary crossing was observed. The actual errno remains available for a human to diagnose the problem. Observed scope is the public CLI on macOS with initial root resolution failing. Readable-source, absent-root, and regular-file-root controls behaved consistently. A practical bypass was demonstrated by selecting a valid short directory, which returned its `index.js` inventory. Nested-entry failures, MCP transport, other operating systems and real-world frequency were not tested. ### REA version Exact `node scripts/rea.mjs --version` stdout: ```text 6.8.0 ``` The stdout ends with a newline. Source baseline: `65f1c447344a423fa2785847dbcea1e9a9e2454a`; the checkout's prebuilt CLI was executed. ### Runtime and platform Node.js v24.22.1; macOS 27.0.1; darwin arm64. No analysis provider or GUI application was launched. ### Target and artifact context Self-authored `loop-a -> loop-b -> loop-a` links; a generated 300-character ASCII component argument; a regular text file; an absent path; and a directory containing one inert JavaScript source file. Source text was imported as data and never executed. All fixture trees were deleted. ### Interface CLI ### Earliest failure stage Request parsing or validation ### Relevant output or evidence The standalone recipe produced these rows in each of its two attempts: ```text selection exit code category loop 1 unreadable_output execution_failure long-component 1 unreadable_output execution_failure missing-control 1 invalid_request invalid_input file-control 1 invalid_request invalid_input valid-control 0 success ``` Both failing selections returned this exact recovery object: ```json {"action":"Reference source files could not be read. Check directory permissions and try again."} ``` The initial replay captured the complete loop message: ```text Reference source root could not be resolved: ELOOP: too many symbolic links encountered, stat 'loop-a' Reference source files could not be read. Check directory permissions and try again. ``` The oversized-name message similarly retained `ENAMETOOLONG: name too long, stat` and the selected component. Both included `details.diagnostics.reference_source_code: "io"`. Thus the native reason is preserved; the defect is the recovery instruction appended to it. Contract and source checks at the baseline: - `AGENTS.md`, Architecture and Cleanup, requires meaningful reasons for expected boundary failures. `CONTRIBUTING.md`, Choosing a contribution, explicitly recognizes incorrect user instructions as reportable. - `docs/cli.md:637-641` documents the shared error result for import failures, including remediation. The filesystem-recovery passage at `docs/cli.md:604-610` concerns inventory entries; it does not establish the root error-code expectation. `docs/model-artifacts.md:170-172` is model-specific and supplies no binding reference-import classification. - `src/cli/utilityCommands.ts:277-283` exposes the local source directory argument. `src/application/ReferenceSourceImportPolicy.ts:78-107` performs initial root stat and forwards a recognized failure before policy reads or traversal. - `src/reference/ReferenceSourceReaderErrors.ts:82-99` labels recognized root errno values other than `ENOENT`/`ENOTDIR` as `io`. `src/application/ReferenceSourceImportTypes.ts:29-38` selects the generic permission advice; lines 92-98 project it as an output error. Its `invalid-root` branch at lines 77-82 instead supplies an input issue naming `root`. ### Typed outcome or recovery expected Retain the failure exit, original path and host reason. Make `remediation.action` address the link chain or path length, rather than directory permissions. Any category adjustment should preserve genuine permission, host-resource, cancellation, cleanup and nested partial-inventory behavior. No code change or restored behavior was tested. ### Analysis or suspected cause Source-derived explanation: the initial-root classifier groups these path-resolution failures with other I/O failures. That broad `io` outcome owns only one recovery string, so it always supplies permission advice regardless of the retained errno. The CLI observations establish the wrong instruction independently of a proposed implementation change. ### Additional context Closed [#2551](https://github.com/morluto/rea/issues/2551) required an oversized root to return a tagged reader Result retaining the native reason instead of rejecting. Its expected result explicitly allowed `invalid-root` or `io`, and its owner's closure was limited to that guarantee. The current result satisfies that closure. This report does not claim that fix regressed. Closed [#2652](https://github.com/morluto/rea/issues/2652) and merged [PR #2716](https://github.com/morluto/rea/pull/2716) concern paths exceeding the limit later inside a valid tree. Their restoration check preserves readable siblings; this case fails before traversal. Closed [#5399](https://github.com/morluto/rea/issues/5399) concerned missing canonical fields, which are present here. The path-classification precedents [#5949](https://github.com/morluto/rea/issues/5949), [#4188](https://github.com/morluto/rea/issues/4188), [#6754](https://github.com/morluto/rea/issues/6754) and [#6740](https://github.com/morluto/rea/issues/6740) concern plist, SQLite, entropy and EVM operations respectively. They do not cover this reference-import producer. Open [#7404](https://github.com/morluto/rea/issues/7404) and [#7561](https://github.com/morluto/rea/issues/7561) concern empty selections resolving to the working directory, a different input and restoration condition. Open/closed issue, PR and issue-comment searches found no matching report for this root-recovery instruction. ## Triage assessment - Impact level: P3 - Assessment status: supported - Impact basis: Initial invalid roots are safely refused and retain their actual host reason, but the recovery text recommends an unrelated permission repair. No consequential downstream behavior was demonstrated. - Workaround status: available - Workaround basis: The retained ELOOP/ENAMETOOLONG message identifies the path constraint; selecting the tested valid short directory succeeds and returns its source-file inventory. This bypasses the invalid selection without fixing the misleading recovery field.
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#4538
No description provided.