[GH-ISSUE #5480] [Bug] Evidence export fails for valid long output paths and can leave empty staging directories #3333

Closed
opened 2026-10-31 00:39:15 +00:00 by bsakaguchi · 0 comments
Owner

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

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

node scripts/rea.mjs evidence-export SOURCE OUTPUT --json and node scripts/rea.mjs inspect-artifact TARGET --detail summary --evidence_output OUTPUT --json fail for new destination paths that the tested macOS filesystem accepts. Both report an access/permission error and produce no requested bundle. At one tested length, each also leaves an empty .rea-write-* directory. Exporting the same input to a shorter path succeeds.

This report was prepared with AI assistance and independently reproduced by a separate verifier. The submission agent checked upstream history and duplicates; it did not rerun the runtime reproduction.

Reproduction

From the baseline checkout with dependencies and the compiled CLI available, put the repository-pinned Node.js v24.18.0 on PATH and run the following Python 3 script. This is the final self-contained reproduction executed by the independent verifier. It creates a ZIP fixture, obtains valid Evidence through the public CLI, isolates HOME/XDG state for each REA invocation, and measures canonical parent-path byte lengths. The probe filename is six bytes longer than the requested export filename.

import json
import os
from pathlib import Path
import shutil
import subprocess
import tempfile
import zipfile

root = Path(tempfile.mkdtemp(prefix="rea-export-check-")).resolve()
env = os.environ.copy()
for key in ("HOME", "XDG_CONFIG_HOME", "XDG_DATA_HOME", "XDG_CACHE_HOME", "XDG_STATE_HOME"):
    directory = root / key.lower()
    directory.mkdir()
    env[key] = str(directory)


def rea(*args):
    result = subprocess.run(
        ["node", "scripts/rea.mjs", *map(str, args), "--json"],
        env=env, capture_output=True, text=True, timeout=60,
    )
    return result.returncode, json.loads(result.stdout)


with zipfile.ZipFile(root / "target.zip", "w") as archive:
    archive.writestr("hello.txt", "fixture data\n")
status, _ = rea("inspect-artifact", root / "target.zip", "--detail", "summary",
                "--evidence_output", root / "bundle.json")
assert status == 0
bundle = (root / "bundle.json").read_bytes()
for size in (951, 1002, 1007):
    parent = root / str(size)
    parent.mkdir()
    while len(os.fsencode(str(parent))) + 201 < size:
        parent /= "b" * 200
        parent.mkdir()
    remaining = size - len(os.fsencode(str(parent))) - 1
    if remaining > 0:
        parent /= "l" * remaining
        parent.mkdir()
    assert len(os.fsencode(str(parent))) == size
    destination = parent / "out.json"
    probe = parent / "out.json.probe"
    shutil.copyfile(root / "bundle.json", probe)
    assert probe.read_bytes() == bundle
    export_status, exported = rea("evidence-export", root / "bundle.json", destination)
    inspect_status, inspected = rea("inspect-artifact", root / "target.zip",
        "--detail", "summary", "--evidence_output", parent / "inspect.json")
    leftovers = [p for p in parent.iterdir() if p.name.startswith(".rea-write-")]
    print(json.dumps({
        "parent_bytes": size, "probe_created": True,
        "export_exit": export_status, "export_code": exported.get("code"),
        "export_message": exported.get("message"),
        "inspect_exit": inspect_status, "inspect_code": inspected.get("code"),
        "destination_exists": destination.exists(),
        "destination_matches": destination.read_bytes() == bundle if destination.exists() else None,
        "staging_directories": len(leftovers),
        "staging_contents": [list(p.iterdir()) for p in leftovers],
    }))

Each long-path case reproduced in an exploratory run and two runs with the pinned Node runtime. The concrete byte thresholds depend on the tested host's pathname limit.

Expected behavior

Publish complete canonical Evidence atomically to a new caller-selected output path the host can create, without an undocumented shorter limit imposed by staging. Preserve existing-destination protection unless overwrite is explicitly requested, and clean up owned temporary resources after a failed write. A real pathname-limit refusal should identify that constraint rather than advise fixing permissions.

Grounding: observed evidence-export --help advertises canonical atomic export; docs/cli.md:250-262 documents caller-selected output and explicit overwrite. AGENTS.md Product and Authority requires preserving caller-selected paths; docs/tool-design.md Define the contract and Own the lifecycle require justified limits and owned-resource handling.

Impact and scope

P2: two public Evidence export workflows fail for a bounded class of valid destinations, give misleading recovery advice, and can accumulate empty staging directories. No existing-file loss or security-boundary crossing was observed. A demonstrated workaround is choosing a shorter output path: the 951-byte parent control exported identical bundle bytes and left no staging entries. Other platforms and writer entry points were not exercised.

REA version

6.7.0; source commit 074768901f3cdc8205b61773fb1faf6f6c7b3ca8, using its prebuilt CLI. This was not a separate reproduction against the published npm package.

Upstream history was checked through 00a055be0fad728e1ba183c8de968d5538ba11ef: the cited writer and cleanup implementation is unchanged since the baseline. The later inspection-delivery change adds source Evidence to the bundle and does not change destination staging.

Runtime and platform

Node.js v24.18.0 (repository pin); macOS 27.0.1 arm64; Python 3 for fixture generation and path-byte measurements. No Hopper/Ghidra provider or external network request is needed for reproduction.

Target and artifact context

Locally generated ZIP containing hello.txt with synthetic text; a valid Evidence bundle generated by inspect-artifact. New destinations under canonical parent directories of 951, 1002 and 1007 bytes.

Interface

CLI

Earliest failure stage

Cleanup, cancellation, or lifecycle

Relevant output or evidence

Independent public CLI observations:

Canonical parent bytes Probe copy Both CLI exits Error code Requested outputs Staging after both calls
951 Succeeded, bytes matched 0 None Created; exported bytes matched source None
1002 Succeeded, bytes matched 1 cleanup_incomplete Absent Two empty directories
1007 Succeeded, bytes matched 1 execution_failure Absent None

Both failing lengths returned Evidence file could not be accessed. Check file permissions and try again. At parent length 1002, standalone export included operation: write, reason: io, cleanup: incomplete, and a cleanup_reason containing ENAMETOOLONG: name too long, lstat for the internal .rea-write-<suffix>/content path. The inspection command nests its primary write error under details.diagnostics.primary_error; its outer details are different.

At the 1002-byte parent, requested out.json is 1011 bytes and the successful out.json.probe is 1017 bytes. Source-derived calculation: the internal staging content pathname is 1028 bytes.

Failure source at the tested revision: src/cliEvidenceCommands.ts:30-45, src/application/EvidenceBundleCommands.ts:32-48, src/application/EvidenceBundleFiles.ts:115-145, src/application/JsonFiles.ts:198-212,288-295, src/filesystem/OwnedStagedOutput.ts:119-161, and src/cli/analysisEvidenceDelivery.ts:112-125.

Typed outcome or recovery expected

Successful canonical publication with no staging leftovers for all three host-creatable destinations. Preserve complete-byte atomic publication and existing-destination protection. If a genuine host pathname restriction prevents publication, return an accurate typed limitation and complete owned cleanup. Proposed restoration check: rerun the same fixture on the tested host, compare long-path bytes with the successful short-path control, and inspect staging for both public commands. No fix was tested.

Analysis or suspected cause

Source-derived reasoning: JsonFiles.ts admits the selected destination, then creates a sibling .rea-write- directory with a six-character suffix and appends /content. That internal pathname exceeds the tested host limit before the requested destination does. At parent length 1002, cleanup calls lstat on the overlong content pathname and rethrows a non-ENOENT error before removing the empty staging directory. At parent length 1007, staging-directory creation itself exceeds the limit.

Additional context

NOT_ENOUGH_INFO: no known-good REA version or regression window has been established. Other platforms remain untested. Related #4613 and merged PR #4764 concern FAT32/exFAT staged-file identity changes, while closed PR #3561 concerns symlink/parent traversal selection; those reports do not cover this pathname-length trigger.

Originally created by @coygeek on GitHub (Oct 20, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/5480 ### 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 `node scripts/rea.mjs evidence-export SOURCE OUTPUT --json` and `node scripts/rea.mjs inspect-artifact TARGET --detail summary --evidence_output OUTPUT --json` fail for new destination paths that the tested macOS filesystem accepts. Both report an access/permission error and produce no requested bundle. At one tested length, each also leaves an empty `.rea-write-*` directory. Exporting the same input to a shorter path succeeds. This report was prepared with AI assistance and independently reproduced by a separate verifier. The submission agent checked upstream history and duplicates; it did not rerun the runtime reproduction. ### Reproduction From the baseline checkout with dependencies and the compiled CLI available, put the repository-pinned Node.js v24.18.0 on PATH and run the following Python 3 script. This is the final self-contained reproduction executed by the independent verifier. It creates a ZIP fixture, obtains valid Evidence through the public CLI, isolates HOME/XDG state for each REA invocation, and measures canonical parent-path byte lengths. The probe filename is six bytes longer than the requested export filename. ```python import json import os from pathlib import Path import shutil import subprocess import tempfile import zipfile root = Path(tempfile.mkdtemp(prefix="rea-export-check-")).resolve() env = os.environ.copy() for key in ("HOME", "XDG_CONFIG_HOME", "XDG_DATA_HOME", "XDG_CACHE_HOME", "XDG_STATE_HOME"): directory = root / key.lower() directory.mkdir() env[key] = str(directory) def rea(*args): result = subprocess.run( ["node", "scripts/rea.mjs", *map(str, args), "--json"], env=env, capture_output=True, text=True, timeout=60, ) return result.returncode, json.loads(result.stdout) with zipfile.ZipFile(root / "target.zip", "w") as archive: archive.writestr("hello.txt", "fixture data\n") status, _ = rea("inspect-artifact", root / "target.zip", "--detail", "summary", "--evidence_output", root / "bundle.json") assert status == 0 bundle = (root / "bundle.json").read_bytes() for size in (951, 1002, 1007): parent = root / str(size) parent.mkdir() while len(os.fsencode(str(parent))) + 201 < size: parent /= "b" * 200 parent.mkdir() remaining = size - len(os.fsencode(str(parent))) - 1 if remaining > 0: parent /= "l" * remaining parent.mkdir() assert len(os.fsencode(str(parent))) == size destination = parent / "out.json" probe = parent / "out.json.probe" shutil.copyfile(root / "bundle.json", probe) assert probe.read_bytes() == bundle export_status, exported = rea("evidence-export", root / "bundle.json", destination) inspect_status, inspected = rea("inspect-artifact", root / "target.zip", "--detail", "summary", "--evidence_output", parent / "inspect.json") leftovers = [p for p in parent.iterdir() if p.name.startswith(".rea-write-")] print(json.dumps({ "parent_bytes": size, "probe_created": True, "export_exit": export_status, "export_code": exported.get("code"), "export_message": exported.get("message"), "inspect_exit": inspect_status, "inspect_code": inspected.get("code"), "destination_exists": destination.exists(), "destination_matches": destination.read_bytes() == bundle if destination.exists() else None, "staging_directories": len(leftovers), "staging_contents": [list(p.iterdir()) for p in leftovers], })) ``` Each long-path case reproduced in an exploratory run and two runs with the pinned Node runtime. The concrete byte thresholds depend on the tested host's pathname limit. ### Expected behavior Publish complete canonical Evidence atomically to a new caller-selected output path the host can create, without an undocumented shorter limit imposed by staging. Preserve existing-destination protection unless overwrite is explicitly requested, and clean up owned temporary resources after a failed write. A real pathname-limit refusal should identify that constraint rather than advise fixing permissions. Grounding: observed `evidence-export --help` advertises canonical atomic export; `docs/cli.md:250-262` documents caller-selected output and explicit overwrite. `AGENTS.md` Product and Authority requires preserving caller-selected paths; `docs/tool-design.md` Define the contract and Own the lifecycle require justified limits and owned-resource handling. ### Impact and scope P2: two public Evidence export workflows fail for a bounded class of valid destinations, give misleading recovery advice, and can accumulate empty staging directories. No existing-file loss or security-boundary crossing was observed. A demonstrated workaround is choosing a shorter output path: the 951-byte parent control exported identical bundle bytes and left no staging entries. Other platforms and writer entry points were not exercised. ### REA version 6.7.0; source commit `074768901f3cdc8205b61773fb1faf6f6c7b3ca8`, using its prebuilt CLI. This was not a separate reproduction against the published npm package. Upstream history was checked through `00a055be0fad728e1ba183c8de968d5538ba11ef`: the cited writer and cleanup implementation is unchanged since the baseline. The later inspection-delivery change adds source Evidence to the bundle and does not change destination staging. ### Runtime and platform Node.js v24.18.0 (repository pin); macOS 27.0.1 arm64; Python 3 for fixture generation and path-byte measurements. No Hopper/Ghidra provider or external network request is needed for reproduction. ### Target and artifact context Locally generated ZIP containing `hello.txt` with synthetic text; a valid Evidence bundle generated by `inspect-artifact`. New destinations under canonical parent directories of 951, 1002 and 1007 bytes. ### Interface CLI ### Earliest failure stage Cleanup, cancellation, or lifecycle ### Relevant output or evidence Independent public CLI observations: | Canonical parent bytes | Probe copy | Both CLI exits | Error code | Requested outputs | Staging after both calls | | --- | --- | --- | --- | --- | --- | | 951 | Succeeded, bytes matched | 0 | None | Created; exported bytes matched source | None | | 1002 | Succeeded, bytes matched | 1 | `cleanup_incomplete` | Absent | Two empty directories | | 1007 | Succeeded, bytes matched | 1 | `execution_failure` | Absent | None | Both failing lengths returned `Evidence file could not be accessed. Check file permissions and try again.` At parent length 1002, standalone export included `operation: write`, `reason: io`, `cleanup: incomplete`, and a `cleanup_reason` containing `ENAMETOOLONG: name too long, lstat` for the internal `.rea-write-<suffix>/content` path. The inspection command nests its primary write error under `details.diagnostics.primary_error`; its outer details are different. At the 1002-byte parent, requested `out.json` is 1011 bytes and the successful `out.json.probe` is 1017 bytes. Source-derived calculation: the internal staging content pathname is 1028 bytes. Failure source at the tested revision: `src/cliEvidenceCommands.ts:30-45`, `src/application/EvidenceBundleCommands.ts:32-48`, `src/application/EvidenceBundleFiles.ts:115-145`, `src/application/JsonFiles.ts:198-212,288-295`, `src/filesystem/OwnedStagedOutput.ts:119-161`, and `src/cli/analysisEvidenceDelivery.ts:112-125`. ### Typed outcome or recovery expected Successful canonical publication with no staging leftovers for all three host-creatable destinations. Preserve complete-byte atomic publication and existing-destination protection. If a genuine host pathname restriction prevents publication, return an accurate typed limitation and complete owned cleanup. Proposed restoration check: rerun the same fixture on the tested host, compare long-path bytes with the successful short-path control, and inspect staging for both public commands. No fix was tested. ### Analysis or suspected cause Source-derived reasoning: `JsonFiles.ts` admits the selected destination, then creates a sibling `.rea-write-` directory with a six-character suffix and appends `/content`. That internal pathname exceeds the tested host limit before the requested destination does. At parent length 1002, cleanup calls `lstat` on the overlong content pathname and rethrows a non-ENOENT error before removing the empty staging directory. At parent length 1007, staging-directory creation itself exceeds the limit. ### Additional context NOT_ENOUGH_INFO: no known-good REA version or regression window has been established. Other platforms remain untested. Related #4613 and merged PR #4764 concern FAT32/exFAT staged-file identity changes, while closed PR #3561 concerns symlink/parent traversal selection; those reports do not cover this pathname-length trigger.
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#3333
No description provided.