[GH-ISSUE #5457] [Bug] environment-export reports an identity change when its destination is an existing file #3317

Closed
opened 2026-10-31 00:39:06 +00:00 by bsakaguchi · 3 comments
Owner

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

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

environment-export refuses an existing regular file at the bundle destination with Extraction directory identity changed. The file already existed before invocation and retained its contents, device and inode afterward. Both the top-level message and the directory issue contain this inaccurate explanation. Refusal, exit status and preservation of the file are correct.

Reproduction

An independent verifier ran the built public CLI at the source revision below, using the deterministic repository fixture. The verifier also replayed this recipe. Run from that checkout with its dependencies and runtime built:

case_dir="$(mktemp -d "${CASE_PARENT:-${TMPDIR:-.}}/rea-export.XXXXXX")"
mkdir -p "$case_dir"/{home,config,data,cache,state}
CASE_DIR="$case_dir" node --import tsx --input-type=module -e '
  import { writeEnvironmentServeFiles } from "./tests/fixtures/environmentServe.ts";
  await writeEnvironmentServeFiles(process.env.CASE_DIR);
'
printf 'occupied\n' > "$case_dir/existing-file"
mkdir "$case_dir/nonempty"
printf 'keep\n' > "$case_dir/nonempty/keep.txt"

run_export() {
  env -i PATH="$PATH" HOME="$case_dir/home" \
    XDG_CONFIG_HOME="$case_dir/config" XDG_DATA_HOME="$case_dir/data" \
    XDG_CACHE_HOME="$case_dir/cache" XDG_STATE_HOME="$case_dir/state" \
    TMPDIR="$case_dir" REA_LOG_LEVEL=silent NODE_DISABLE_COMPILE_CACHE=1 \
    node scripts/rea.mjs environment-export "$case_dir/manifest.json" \
    "$case_dir/$1" --evidence "$case_dir/capture.json" --json
}

run_export existing-file
printf 'exit=%s\n' "$?"
cat "$case_dir/existing-file"
run_export nonempty
printf 'exit=%s\n' "$?"
cat "$case_dir/nonempty/keep.txt"
run_export fresh-directory
printf 'exit=%s\n' "$?"

The occupied-file case returns the identity-change message and exit 1. The non-empty directory control returns Output directory is not empty, exits 1, and preserves keep.txt. The fresh-path control exports successfully with the same manifest/capture pair.

Expected behavior

Refuse a file-occupied destination with invalid_request, exit 1, and leave the file untouched. Explain that the destination is a regular file or that a directory is required, without asserting an identity change.

docs/cli.md:473-477 explicitly documents file-occupied output as a structured, non-writing refusal. docs/cli.md:398-406 documents no overwriting; AGENTS.md:31 requires meaningful failure reasons. These sources do not require an exact replacement sentence.

Impact and scope

P3 wording defect for a file occupying the requested bundle root. The verified runs refused safely; no corruption or security boundary crossing was observed. Choosing a fresh directory worked with the same input pair. Other platforms and actual directory replacement after admission were not tested.

REA version

6.7.0; source commit 074768901f3cdc8205b61773fb1faf6f6c7b3ca8.

Runtime and platform

Node.js v26.12.0; macOS darwin, arm64. NOT_ENOUGH_INFO: the verifier did not record the exact macOS release. No Hopper or Ghidra provider is involved.

Target and artifact context

Authenticated environment manifest/capture pair from tests/fixtures/environmentServe.ts; agent environment bundle export to an existing regular file. No private target data or external service is needed.

Interface

CLI

Earliest failure stage

Request parsing or validation

Relevant output or evidence

Selected fields from the occupied-file CLI response; the output path is replaced with <output-path>:

{
  "code": "invalid_request",
  "category": "invalid_input",
  "message": "Extraction directory identity changed: <output-path>",
  "details": {
    "operation": "environment-export",
    "issues": [{
      "path": ["directory"],
      "reason": "invalid_value",
      "message": "Extraction directory identity changed: <output-path>"
    }]
  }
}

The process exited 1 with empty stderr. Separate before/after checks found unchanged device and inode and the original occupied plus newline. The fresh control exited 0, returned status: "exported", and reported seven written bundle files.

Typed outcome or recovery expected

Keep invalid_request and the directory input issue, but preserve an accurate non-directory failure reason in both messages. Keep the existing file untouched.

Analysis or suspected cause

Source-derived reasoning: environmentBundleWrite.ts:91-95 under src/application/environment/ enables existing-empty admission. src/artifacts/SafeOutputTree.ts:118-134 then reads the initial root identity. src/artifacts/SafeOutputTreeIdentity.ts:51-56 calls replacedDirectory for an initial non-directory, before admitting any directory identity. src/application/environment/environmentExport.ts:305-315 retains that message in the caller-visible issue.

Additional context

Prepared with AI assistance and independently reproduced. Before submission, the latest published release was still 6.7.0. Fetching upstream main and checking commits since the tested baseline found no changes to the cited documentation or failure path; this submitter did not perform an additional runtime reproduction on main.

Proposed restoration checks: preserve the refusal and file contents while correcting both messages; retain fresh-path success and non-empty-directory refusal. A directory genuinely replaced after admission should still fail identity validation; that replacement control was not executed here.

Originally created by @coygeek on GitHub (Oct 20, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/5457 ### 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 `environment-export` refuses an existing regular file at the bundle destination with `Extraction directory identity changed`. The file already existed before invocation and retained its contents, device and inode afterward. Both the top-level message and the directory issue contain this inaccurate explanation. Refusal, exit status and preservation of the file are correct. ### Reproduction An independent verifier ran the built public CLI at the source revision below, using the deterministic repository fixture. The verifier also replayed this recipe. Run from that checkout with its dependencies and runtime built: ```sh case_dir="$(mktemp -d "${CASE_PARENT:-${TMPDIR:-.}}/rea-export.XXXXXX")" mkdir -p "$case_dir"/{home,config,data,cache,state} CASE_DIR="$case_dir" node --import tsx --input-type=module -e ' import { writeEnvironmentServeFiles } from "./tests/fixtures/environmentServe.ts"; await writeEnvironmentServeFiles(process.env.CASE_DIR); ' printf 'occupied\n' > "$case_dir/existing-file" mkdir "$case_dir/nonempty" printf 'keep\n' > "$case_dir/nonempty/keep.txt" run_export() { env -i PATH="$PATH" HOME="$case_dir/home" \ XDG_CONFIG_HOME="$case_dir/config" XDG_DATA_HOME="$case_dir/data" \ XDG_CACHE_HOME="$case_dir/cache" XDG_STATE_HOME="$case_dir/state" \ TMPDIR="$case_dir" REA_LOG_LEVEL=silent NODE_DISABLE_COMPILE_CACHE=1 \ node scripts/rea.mjs environment-export "$case_dir/manifest.json" \ "$case_dir/$1" --evidence "$case_dir/capture.json" --json } run_export existing-file printf 'exit=%s\n' "$?" cat "$case_dir/existing-file" run_export nonempty printf 'exit=%s\n' "$?" cat "$case_dir/nonempty/keep.txt" run_export fresh-directory printf 'exit=%s\n' "$?" ``` The occupied-file case returns the identity-change message and exit `1`. The non-empty directory control returns `Output directory is not empty`, exits `1`, and preserves `keep.txt`. The fresh-path control exports successfully with the same manifest/capture pair. ### Expected behavior Refuse a file-occupied destination with `invalid_request`, exit `1`, and leave the file untouched. Explain that the destination is a regular file or that a directory is required, without asserting an identity change. `docs/cli.md:473-477` explicitly documents file-occupied output as a structured, non-writing refusal. `docs/cli.md:398-406` documents no overwriting; `AGENTS.md:31` requires meaningful failure reasons. These sources do not require an exact replacement sentence. ### Impact and scope P3 wording defect for a file occupying the requested bundle root. The verified runs refused safely; no corruption or security boundary crossing was observed. Choosing a fresh directory worked with the same input pair. Other platforms and actual directory replacement after admission were not tested. ### REA version 6.7.0; source commit `074768901f3cdc8205b61773fb1faf6f6c7b3ca8`. ### Runtime and platform Node.js v26.12.0; macOS `darwin`, `arm64`. NOT_ENOUGH_INFO: the verifier did not record the exact macOS release. No Hopper or Ghidra provider is involved. ### Target and artifact context Authenticated environment manifest/capture pair from `tests/fixtures/environmentServe.ts`; agent environment bundle export to an existing regular file. No private target data or external service is needed. ### Interface CLI ### Earliest failure stage Request parsing or validation ### Relevant output or evidence Selected fields from the occupied-file CLI response; the output path is replaced with `<output-path>`: ```json { "code": "invalid_request", "category": "invalid_input", "message": "Extraction directory identity changed: <output-path>", "details": { "operation": "environment-export", "issues": [{ "path": ["directory"], "reason": "invalid_value", "message": "Extraction directory identity changed: <output-path>" }] } } ``` The process exited `1` with empty stderr. Separate before/after checks found unchanged device and inode and the original `occupied` plus newline. The fresh control exited `0`, returned `status: "exported"`, and reported seven written bundle files. ### Typed outcome or recovery expected Keep `invalid_request` and the directory input issue, but preserve an accurate non-directory failure reason in both messages. Keep the existing file untouched. ### Analysis or suspected cause Source-derived reasoning: `environmentBundleWrite.ts:91-95` under `src/application/environment/` enables existing-empty admission. `src/artifacts/SafeOutputTree.ts:118-134` then reads the initial root identity. `src/artifacts/SafeOutputTreeIdentity.ts:51-56` calls `replacedDirectory` for an initial non-directory, before admitting any directory identity. `src/application/environment/environmentExport.ts:305-315` retains that message in the caller-visible issue. ### Additional context Prepared with AI assistance and independently reproduced. Before submission, the latest published release was still 6.7.0. Fetching upstream main and checking commits since the tested baseline found no changes to the cited documentation or failure path; this submitter did not perform an additional runtime reproduction on main. Proposed restoration checks: preserve the refusal and file contents while correcting both messages; retain fresh-path success and non-empty-directory refusal. A directory genuinely replaced after admission should still fail identity validation; that replacement control was not executed here.
Author
Owner

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

Additional reproduced case (same guard): a bundle destination that is a symlink to an empty directory receives the same inaccurate identity-change diagnosis. Reproduced at 074768901f3cdc8205b61773fb1faf6f6c7b3ca8 with Node v26.12.0 on macOS arm64.

Using the authenticated fixture and run_export helper from the issue's reproduction, with fresh destination names:

mkdir "$case_dir/target-empty" "$case_dir/target-occupied"
printf 'keep\n' > "$case_dir/target-occupied/keep.txt"
ln -s target-empty "$case_dir/link-empty"
ln -s target-occupied "$case_dir/link-occupied"
run_export link-empty
run_export link-occupied
run_export target-empty

Both symlink runs exit 1 with invalid_request, invalid_input, retryable: true, and Extraction directory identity changed: <output-path> in both the top-level message and details.issues[0].message. Before/after checks confirmed unchanged symlinks and targets, including keep.txt. The direct empty-directory control exits 0 and exports seven files.

Source check: both links reach the same initial lstat non-directory guard in src/artifacts/SafeOutputTreeIdentity.ts:51-56, before the occupancy check in src/artifacts/SafeOutputTree.ts:135-139; src/application/environment/environmentExport.ts:305-315 projects the same message. This adds a diagnostic test case; it does not establish a requirement to accept symlink destinations or demonstrate an unintended write.

<!-- gh-comment-id:6291610533 --> @coygeek commented on GitHub (Oct 20, 2026): Additional reproduced case (same guard): a bundle destination that is a symlink to an empty directory receives the same inaccurate identity-change diagnosis. Reproduced at `074768901f3cdc8205b61773fb1faf6f6c7b3ca8` with Node v26.12.0 on macOS arm64. Using the authenticated fixture and `run_export` helper from the issue's reproduction, with fresh destination names: ```sh mkdir "$case_dir/target-empty" "$case_dir/target-occupied" printf 'keep\n' > "$case_dir/target-occupied/keep.txt" ln -s target-empty "$case_dir/link-empty" ln -s target-occupied "$case_dir/link-occupied" run_export link-empty run_export link-occupied run_export target-empty ``` Both symlink runs exit `1` with `invalid_request`, `invalid_input`, `retryable: true`, and `Extraction directory identity changed: <output-path>` in both the top-level message and `details.issues[0].message`. Before/after checks confirmed unchanged symlinks and targets, including `keep.txt`. The direct empty-directory control exits `0` and exports seven files. Source check: both links reach the same initial `lstat` non-directory guard in `src/artifacts/SafeOutputTreeIdentity.ts:51-56`, before the occupancy check in `src/artifacts/SafeOutputTree.ts:135-139`; `src/application/environment/environmentExport.ts:305-315` projects the same message. This adds a diagnostic test case; it does not establish a requirement to accept symlink destinations or demonstrate an unintended write.
Author
Owner

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

TL;DR: This still happens on main on Linux. Exporting onto a file that exists says the directory identity changed. The file is untouched, which is correct.

Checked on main b3aadaf47 (Node 24.18.0). environment-export with a regular file as the destination exits 1 with invalid_request. The message is Extraction directory identity changed: <path>. The directory issue repeats it. The file still reads occupied afterwards. A fresh path exits 0.

Not run

  • A control run on a non-empty directory. The symlink case from the comment. Inode and device comparison. macOS.

Thanks, coygeek, for the clear report.

Commands
printf 'occupied\n' > <case_dir>/existing-file
node scripts/rea.mjs environment-export <case_dir>/manifest.json <case_dir>/existing-file --evidence <case_dir>/capture.json --json
node scripts/rea.mjs environment-export <case_dir>/manifest.json <case_dir>/fresh-directory --evidence <case_dir>/capture.json --json

First run, exit 1, stderr 0 bytes:

{"error":"Application workflow failed","code":"invalid_request","category":"invalid_input","message":"Extraction directory identity changed: <case_dir>/existing-file","retryable":true,"remediation":{"action":"Read the declared input schema or CLI usage for environment-export; ... Correct the listed fields, then retry. Do not repeat the unchanged call."},"recovery":{"kind":"inspect_input_contract","operation":"environment-export"},"details":{"operation":"environment-export","issues":[{"path":["directory"],"reason":"invalid_value","message":"Extraction directory identity changed: <case_dir>/existing-file"}]}}

Second run, exit 0: "status":"exported". Isolated HOME, XDG and TMPDIR; run once per case.

Test setup
  • Box: Nobara 44 (Fedora 44 base), Linux 7.2.11
  • CPU: AMD Ryzen 9 5900X, 12 cores / 24 threads
  • RAM: 32 GB DDR4-3600
  • Node: 24.18.0
  • Head: b3aadaf47

🤖 Generated with Claude Code (Sonnet 5.5, verified by Opus 5.5) 🦊

<!-- gh-comment-id:6298171336 --> @CassieFox23 commented on GitHub (Oct 20, 2026): **TL;DR:** This still happens on main on Linux. Exporting onto a file that exists says the directory identity changed. The file is untouched, which is correct. Checked on main b3aadaf47 (Node 24.18.0). `environment-export` with a regular file as the destination exits 1 with `invalid_request`. The message is `Extraction directory identity changed: <path>`. The directory issue repeats it. The file still reads `occupied` afterwards. A fresh path exits 0. ### Not run - A control run on a non-empty directory. The symlink case from the comment. Inode and device comparison. macOS. Thanks, coygeek, for the clear report. <details> <summary>Commands</summary> ```sh printf 'occupied\n' > <case_dir>/existing-file node scripts/rea.mjs environment-export <case_dir>/manifest.json <case_dir>/existing-file --evidence <case_dir>/capture.json --json node scripts/rea.mjs environment-export <case_dir>/manifest.json <case_dir>/fresh-directory --evidence <case_dir>/capture.json --json ``` First run, exit 1, stderr 0 bytes: ```json {"error":"Application workflow failed","code":"invalid_request","category":"invalid_input","message":"Extraction directory identity changed: <case_dir>/existing-file","retryable":true,"remediation":{"action":"Read the declared input schema or CLI usage for environment-export; ... Correct the listed fields, then retry. Do not repeat the unchanged call."},"recovery":{"kind":"inspect_input_contract","operation":"environment-export"},"details":{"operation":"environment-export","issues":[{"path":["directory"],"reason":"invalid_value","message":"Extraction directory identity changed: <case_dir>/existing-file"}]}} ``` Second run, exit 0: `"status":"exported"`. Isolated HOME, XDG and TMPDIR; run once per case. </details> <details> <summary>Test setup</summary> - Box: Nobara 44 (Fedora 44 base), Linux 7.2.11 - CPU: AMD Ryzen 9 5900X, 12 cores / 24 threads - RAM: 32 GB DDR4-3600 - Node: 24.18.0 - Head: b3aadaf47 </details> 🤖 Generated with Claude Code (Sonnet 5.5, verified by Opus 5.5) 🦊
Author
Owner

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

Checked the reported fix on main at d87e73f2db.

Initial lstat non-directory refusal now says Output destination must be a directory; it is distinct from assertPathIdentity/assertHandleIdentity, which retain the genuine post-admission identity-change diagnosis. Existing-empty destination creation converts the initial path failure into a destination refusal, and outputDirectoryAdmissionRefusal uses the same message in top-level invalid input and directory issue. The environment export regression explicitly checks must be a directory, absence of identity changed, identical issue message, and preserved existing-file contents; it also retains nonempty directory refusal.

Implementation: SafeOutputTreeIdentity.ts, SafeOutputTreeIdentity.ts.

The relevant regression checks in environmentExport.test.ts, environmentExportCli.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:6380705262 --> @coygeek commented on GitHub (Oct 24, 2026): Checked the reported fix on `main` at d87e73f2db50be72d33a4b3c7199af35518b441c. Initial lstat non-directory refusal now says Output destination must be a directory; it is distinct from assertPathIdentity/assertHandleIdentity, which retain the genuine post-admission identity-change diagnosis. Existing-empty destination creation converts the initial path failure into a destination refusal, and outputDirectoryAdmissionRefusal uses the same message in top-level invalid input and directory issue. The environment export regression explicitly checks must be a directory, absence of identity changed, identical issue message, and preserved existing-file contents; it also retains nonempty directory refusal. Implementation: [SafeOutputTreeIdentity.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/artifacts/SafeOutputTreeIdentity.ts#L51), [SafeOutputTreeIdentity.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/artifacts/SafeOutputTreeIdentity.ts#L63). The relevant regression checks in [environmentExport.test.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/application/environment/environmentExport.test.ts), [environmentExportCli.test.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/tests/boundary/cli/environmentExportCli.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#3317
No description provided.