[GH-ISSUE #8839] [Bug] capture-process rewrites literal absence-marker text as a temporary-root alias #7772

Closed
opened 2026-11-04 00:49:10 +00:00 by bsakaguchi · 0 comments
Owner

Originally created by @coygeek on GitHub (Nov 4, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/8839

Before submitting

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

Area

Process capture or runtime observation

Observed behavior

With normalization.paths: true, a successful capture-process run changes the ordinary literal <no-temporary-root> to <temporary-root> in recorded input-event data and sampled command text. The literal is an argument and a non-sensitive input value, not a selected filesystem path. The target receives and echoes the original text, so terminal observations preserve <no-temporary-root> while the other recorded fields claim <temporary-root>.

This reproduced through the unmodified public CLI on the source baseline and a separately installed latest published rea-agents@6.8.0 package. The CLI and target exited 0. Disabling path normalization preserves the literal. A nearby literal, <no-temporary-roots>, is preserved with path normalization enabled.

Reproduction

From a built checkout of the revision below, with Node.js v24.22.1 selected on a supported macOS capture host, run this script. It creates ordinary fixtures, isolates HOME and client/cache paths, runs three real CLI captures, prints only the relevant fields, and removes its fixture directory. The target waits for one input event, echoes it, and exits.

fixture=$(mktemp -d)
node --input-type=module - "$fixture" "$PWD/scripts/rea.mjs" <<'JS'
import fs from 'node:fs';
import { spawnSync } from 'node:child_process';
const root = fs.realpathSync(process.argv[2]);
const cli = process.argv[3];
const env = { ...process.env };
for (const key of ['HOME', 'TMPDIR', 'XDG_CONFIG_HOME', 'XDG_CACHE_HOME', 'XDG_STATE_HOME', 'XDG_DATA_HOME']) {
  env[key] = `${root}/${key}`;
  fs.mkdirSync(env[key], { recursive: true });
}
try {
  for (const [name, paths, literal] of [
    ['sentinel', true, '<no-temporary-root>'],
    ['disabled', false, '<no-temporary-root>'],
    ['nearby', true, '<no-temporary-roots>'],
  ]) {
    const scenario = {
      executable: process.execPath,
      arguments: ['-e', "console.log(process.argv[1]); process.stdin.once('data', d => {console.log(d.toString()); process.exit(0)}); setTimeout(() => process.exit(2), 3000)", literal],
      working_directory: root,
      timeout_ms: 5000,
      events: [{ type: 'input', at_ms: 500, data: `x ${literal} y\n` }],
      normalization: { paths, pids: false, ports: false, patterns: [] },
    };
    const file = `${root}/${name}.json`;
    fs.writeFileSync(file, JSON.stringify(scenario));
    const run = spawnSync(process.execPath, [cli, 'capture-process', file, '--json'],
      { env, cwd: root, encoding: 'utf8', timeout: 30000, maxBuffer: 4000000 });
    if (run.error) throw run.error;
    if (run.status !== 0) throw new Error(run.stderr || run.stdout);
    const capture = JSON.parse(run.stdout).normalized_result;
    const sample = capture.process_samples.find(p => p.command.includes(' -e '));
    if (!sample) throw new Error('No argv-bearing process sample was observed');
    console.log(JSON.stringify({ case: name, cli_exit: run.status,
      target_exit: capture.exit.code,
      interaction: capture.interaction_events[0].data,
      command_suffix: sample.command.split(' ').at(-1),
      terminal_kept_literal: capture.frames.map(f => f.raw_data ?? f.data).join('').includes(literal),
      executable_aliased: sample.command.startsWith('<executable> ') }));
  }
} finally {
  fs.rmSync(root, { recursive: true, force: true });
}
JS

The three-case script ran once as printed against the baseline. An earlier equivalent fixture ran the failing case and both controls once against the baseline and once against the published package; both reproduced the same wrong fields. No provider response, helper-only caller, or source modification is involved.

Expected behavior

Preserve <no-temporary-root> in both recorded fields because this ordinary input literal is not any selected path identity. Path aliases should apply only to the selected executable, working directory, observation roots, or actual temporary root available to that projection. Successful normalization of the executable in the positive control should continue to work.

Expected source: the process-capture path-normalization contract says aliases apply to selected path identities at clear delimiters. The owning path normalizer describes the same selected-identity rule.

Impact and scope

P2: accepted captures silently record incorrect normalized input and command text for a bounded, uncommon literal. This can mislead consumers of those fields. The runtime controls demonstrate that paths: false avoids the rewrite, at the cost of disabling intended path aliasing. The target's input and echoed terminal text remain correct, and the scenario manifest retains the selected non-sensitive input; no irrecoverable source-data loss or target mutation is claimed.

The observed scope is local macOS CLI captures. Shared sample/interaction projection code also serves the other capture entry points, but MCP, Linux, partial-failure captures, and downstream comparison outcomes were not executed. Production frequency and the earliest affected release are unknown.

REA version

6.8.0, observed from both the source CLI and separately installed npm package. Source baseline and fetched origin/main: c3647ade1e557c4179a8b825a37e0c2193cd2526. npm view rea-agents version returned 6.8.0; the latest published package reproduced the failure.

Runtime and platform

Node.js v24.22.1; Darwin arm64; macOS 27.2. Real native PTY capture. No Hopper or Ghidra provider was involved.

Target and artifact context

Ordinary Node.js child process with a literal argv token and one non-sensitive scripted input event. Captured process Evidence JSON includes the normalized input event, sampled argv text and terminal output.

Interface

CLI

Earliest failure stage

Capture or observation

Relevant output or evidence

Actual reduced output from the inline reproduction:

{"case":"sentinel","cli_exit":0,"target_exit":0,"interaction":"x <temporary-root> y\n","command_suffix":"<temporary-root>","terminal_kept_literal":true,"executable_aliased":true}
{"case":"disabled","cli_exit":0,"target_exit":0,"interaction":"x <no-temporary-root> y\n","command_suffix":"<no-temporary-root>","terminal_kept_literal":true,"executable_aliased":false}
{"case":"nearby","cli_exit":0,"target_exit":0,"interaction":"x <no-temporary-roots> y\n","command_suffix":"<no-temporary-roots>","terminal_kept_literal":true,"executable_aliased":true}

For the failing case, the original input was "x <no-temporary-root> y\n" and the actual sampled argv ended with <no-temporary-root>. Both normalized fields instead contain <temporary-root>. The terminal_kept_literal check inspects retained PTY text; executable_aliased is a positive control that actual selected-path normalization remains active in the sentinel and nearby cases.

The existing focused normalization/journal tests passed: npm run test:focused -- src/process/capture/ProcessNormalization.test.ts src/process/capture/ProcessCaptureJournal.test.ts reported 2 files and 26 tests passed. Those existing tests do not detect this reproduced case.

Relevant source at the tested revision: interaction recording writes original input to the PTY, then normalizes its recorded value using the marker. Sample projection passes the same marker. The successful capture builder retains those fields in the result.

Typed outcome or recovery expected

Return successful capture Evidence whose path-normalized interaction and command values preserve this non-path literal. No new failure category is necessary for these accepted inputs. An absence marker must not acquire a real selected-path identity.

Analysis or suspected cause

Source-derived explanation: both the sample and interaction callers pass the string <no-temporary-root> in the temporary-root parameter position. The path-rule builder registers every nonempty value in that position as a matchable path mapping to <temporary-root>. The demonstrated input token meets its delimiter checks. Terminal normalization receives the actual capture temporary root, explaining why the terminal preserves this literal. The capture itself still allocates a real temporary root; the problem concerns the two projections' internal absence representation. No fix was implemented or verified.

Additional context

No matching open issue or PR was found after searching open and closed issues and all-state PRs for the exact marker, temporary-root, sentinel/process, normalization/process, capture-process/paths and interaction/normalization terms. Plausible reports and their discussions were read.

Closed #171 contains the marker in an example but concerns child PID normalization. Closed #6586 concerns sibling executable filenames. Closed #7584 and merged #7803 concern slash/dot directory spellings. Those different triggers do not explain the reproduced internal-marker alias. No historical last-known-good version was established.

Originally created by @coygeek on GitHub (Nov 4, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/8839 ### 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 Process capture or runtime observation ### Observed behavior With `normalization.paths: true`, a successful `capture-process` run changes the ordinary literal `<no-temporary-root>` to `<temporary-root>` in recorded input-event data and sampled command text. The literal is an argument and a non-sensitive input value, not a selected filesystem path. The target receives and echoes the original text, so terminal observations preserve `<no-temporary-root>` while the other recorded fields claim `<temporary-root>`. This reproduced through the unmodified public CLI on the source baseline and a separately installed latest published `rea-agents@6.8.0` package. The CLI and target exited 0. Disabling path normalization preserves the literal. A nearby literal, `<no-temporary-roots>`, is preserved with path normalization enabled. ### Reproduction From a built checkout of the revision below, with Node.js v24.22.1 selected on a supported macOS capture host, run this script. It creates ordinary fixtures, isolates HOME and client/cache paths, runs three real CLI captures, prints only the relevant fields, and removes its fixture directory. The target waits for one input event, echoes it, and exits. ```sh fixture=$(mktemp -d) node --input-type=module - "$fixture" "$PWD/scripts/rea.mjs" <<'JS' import fs from 'node:fs'; import { spawnSync } from 'node:child_process'; const root = fs.realpathSync(process.argv[2]); const cli = process.argv[3]; const env = { ...process.env }; for (const key of ['HOME', 'TMPDIR', 'XDG_CONFIG_HOME', 'XDG_CACHE_HOME', 'XDG_STATE_HOME', 'XDG_DATA_HOME']) { env[key] = `${root}/${key}`; fs.mkdirSync(env[key], { recursive: true }); } try { for (const [name, paths, literal] of [ ['sentinel', true, '<no-temporary-root>'], ['disabled', false, '<no-temporary-root>'], ['nearby', true, '<no-temporary-roots>'], ]) { const scenario = { executable: process.execPath, arguments: ['-e', "console.log(process.argv[1]); process.stdin.once('data', d => {console.log(d.toString()); process.exit(0)}); setTimeout(() => process.exit(2), 3000)", literal], working_directory: root, timeout_ms: 5000, events: [{ type: 'input', at_ms: 500, data: `x ${literal} y\n` }], normalization: { paths, pids: false, ports: false, patterns: [] }, }; const file = `${root}/${name}.json`; fs.writeFileSync(file, JSON.stringify(scenario)); const run = spawnSync(process.execPath, [cli, 'capture-process', file, '--json'], { env, cwd: root, encoding: 'utf8', timeout: 30000, maxBuffer: 4000000 }); if (run.error) throw run.error; if (run.status !== 0) throw new Error(run.stderr || run.stdout); const capture = JSON.parse(run.stdout).normalized_result; const sample = capture.process_samples.find(p => p.command.includes(' -e ')); if (!sample) throw new Error('No argv-bearing process sample was observed'); console.log(JSON.stringify({ case: name, cli_exit: run.status, target_exit: capture.exit.code, interaction: capture.interaction_events[0].data, command_suffix: sample.command.split(' ').at(-1), terminal_kept_literal: capture.frames.map(f => f.raw_data ?? f.data).join('').includes(literal), executable_aliased: sample.command.startsWith('<executable> ') })); } } finally { fs.rmSync(root, { recursive: true, force: true }); } JS ``` The three-case script ran once as printed against the baseline. An earlier equivalent fixture ran the failing case and both controls once against the baseline and once against the published package; both reproduced the same wrong fields. No provider response, helper-only caller, or source modification is involved. ### Expected behavior Preserve `<no-temporary-root>` in both recorded fields because this ordinary input literal is not any selected path identity. Path aliases should apply only to the selected executable, working directory, observation roots, or actual temporary root available to that projection. Successful normalization of the executable in the positive control should continue to work. Expected source: the [process-capture path-normalization contract](https://github.com/morluto/rea/blob/c3647ade1e557c4179a8b825a37e0c2193cd2526/docs/process-capture.md#L355-L363) says aliases apply to selected path identities at clear delimiters. The [owning path normalizer](https://github.com/morluto/rea/blob/c3647ade1e557c4179a8b825a37e0c2193cd2526/src/process/capture/ProcessNormalization.ts#L355-L375) describes the same selected-identity rule. ### Impact and scope P2: accepted captures silently record incorrect normalized input and command text for a bounded, uncommon literal. This can mislead consumers of those fields. The runtime controls demonstrate that `paths: false` avoids the rewrite, at the cost of disabling intended path aliasing. The target's input and echoed terminal text remain correct, and the scenario manifest retains the selected non-sensitive input; no irrecoverable source-data loss or target mutation is claimed. The observed scope is local macOS CLI captures. Shared sample/interaction projection code also serves the other capture entry points, but MCP, Linux, partial-failure captures, and downstream comparison outcomes were not executed. Production frequency and the earliest affected release are unknown. ### REA version `6.8.0`, observed from both the source CLI and separately installed npm package. Source baseline and fetched `origin/main`: `c3647ade1e557c4179a8b825a37e0c2193cd2526`. `npm view rea-agents version` returned `6.8.0`; the latest published package reproduced the failure. ### Runtime and platform Node.js `v24.22.1`; Darwin arm64; macOS `27.2`. Real native PTY capture. No Hopper or Ghidra provider was involved. ### Target and artifact context Ordinary Node.js child process with a literal argv token and one non-sensitive scripted input event. Captured process Evidence JSON includes the normalized input event, sampled argv text and terminal output. ### Interface CLI ### Earliest failure stage Capture or observation ### Relevant output or evidence Actual reduced output from the inline reproduction: ```jsonl {"case":"sentinel","cli_exit":0,"target_exit":0,"interaction":"x <temporary-root> y\n","command_suffix":"<temporary-root>","terminal_kept_literal":true,"executable_aliased":true} {"case":"disabled","cli_exit":0,"target_exit":0,"interaction":"x <no-temporary-root> y\n","command_suffix":"<no-temporary-root>","terminal_kept_literal":true,"executable_aliased":false} {"case":"nearby","cli_exit":0,"target_exit":0,"interaction":"x <no-temporary-roots> y\n","command_suffix":"<no-temporary-roots>","terminal_kept_literal":true,"executable_aliased":true} ``` For the failing case, the original input was `"x <no-temporary-root> y\n"` and the actual sampled argv ended with `<no-temporary-root>`. Both normalized fields instead contain `<temporary-root>`. The `terminal_kept_literal` check inspects retained PTY text; `executable_aliased` is a positive control that actual selected-path normalization remains active in the sentinel and nearby cases. The existing focused normalization/journal tests passed: `npm run test:focused -- src/process/capture/ProcessNormalization.test.ts src/process/capture/ProcessCaptureJournal.test.ts` reported 2 files and 26 tests passed. Those existing tests do not detect this reproduced case. Relevant source at the tested revision: [interaction recording](https://github.com/morluto/rea/blob/c3647ade1e557c4179a8b825a37e0c2193cd2526/src/process/capture/ProcessCaptureJournal.ts#L65-L104) writes original input to the PTY, then normalizes its recorded value using the marker. [Sample projection](https://github.com/morluto/rea/blob/c3647ade1e557c4179a8b825a37e0c2193cd2526/src/process/capture/ProcessNormalization.ts#L417-L438) passes the same marker. The [successful capture builder](https://github.com/morluto/rea/blob/c3647ade1e557c4179a8b825a37e0c2193cd2526/src/process/capture/ProcessCaptureLifecycle.ts#L356-L373) retains those fields in the result. ### Typed outcome or recovery expected Return successful capture Evidence whose path-normalized interaction and command values preserve this non-path literal. No new failure category is necessary for these accepted inputs. An absence marker must not acquire a real selected-path identity. ### Analysis or suspected cause Source-derived explanation: both the sample and interaction callers pass the string `<no-temporary-root>` in the temporary-root parameter position. [The path-rule builder](https://github.com/morluto/rea/blob/c3647ade1e557c4179a8b825a37e0c2193cd2526/src/process/capture/ProcessNormalization.ts#L356-L396) registers every nonempty value in that position as a matchable path mapping to `<temporary-root>`. The demonstrated input token meets its delimiter checks. Terminal normalization receives the actual capture temporary root, explaining why the terminal preserves this literal. The capture itself still allocates a real temporary root; the problem concerns the two projections' internal absence representation. No fix was implemented or verified. ### Additional context No matching open issue or PR was found after searching open and closed issues and all-state PRs for the exact marker, `temporary-root`, sentinel/process, normalization/process, capture-process/paths and interaction/normalization terms. Plausible reports and their discussions were read. Closed [#171](https://github.com/morluto/rea/issues/171) contains the marker in an example but concerns child PID normalization. Closed [#6586](https://github.com/morluto/rea/issues/6586) concerns sibling executable filenames. Closed [#7584](https://github.com/morluto/rea/issues/7584) and merged [#7803](https://github.com/morluto/rea/pull/7803) concern slash/dot directory spellings. Those different triggers do not explain the reproduced internal-marker alias. No historical last-known-good version was established.
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#7772
No description provided.