[GH-ISSUE #6586] [Bug] Process path normalization aliases sibling names as the selected executable #3974

Closed
opened 2026-10-31 00:45:43 +00:00 by bsakaguchi · 0 comments
Owner

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

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

An executed source-function probe found that normalization.paths: true treats the selected executable as an unrestricted text prefix. Selecting .../original/tool rewrites the distinct sibling path .../original/tool-helper to <executable>-helper. Selecting .../rebuild/tool does the same to .../rebuild/tool-helper. The different original texts consequently become equal, and both scenarios produce the same comparison-contract commitment.

Both the bounded normalizer used by capture and normalizeProcessSamples produce that alias. A control with paths: false preserves the different paths. This is an observed production-source boundary result; no PTY capture, CLI/MCP call, helper process, or debugger/browser session was executed.

Reproduction

Use the source revision below and its locked dependencies. Save this inert probe as /tmp/path-prefix-probe.mts, then run ./node_modules/.bin/tsx /tmp/path-prefix-probe.mts "$PWD" from the checkout with its pinned Node.js selected. The paths are synthetic public fixture strings; no filesystem entry at those paths is required or created.

import { pathToFileURL } from 'node:url';
import { resolve } from 'node:path';
const repo = process.argv[2] ?? process.cwd();
const load = (name: string) => import(pathToFileURL(resolve(repo, name)).href);
const { parseProcessScenario, processComparisonContract, digestProcessCommitment } =
  await load('src/domain/process/processScenario.ts');
const { createBoundedProcessTextNormalizer, normalizeProcessText, normalizeProcessSamples } =
  await load('src/process/capture/ProcessNormalization.ts');
const root = '/tmp/rea-path-prefix-fixture';
const scenario = (executable: string, paths = true) => parseProcessScenario({
  executable, working_directory: root,
  normalization: { paths, pids: false, ports: false, patterns: [] },
});
const cases = ['original', 'rebuild'].map(name => {
  const selected = scenario(`${root}/${name}/tool`);
  const input = `${selected.executable}-helper`;
  const bounded = createBoundedProcessTextNormalizer(selected, '/tmp/rea-private', 41010)(input, 4096);
  return {
    executable: selected.executable, input,
    normalized: normalizeProcessText(input, selected, '/tmp/rea-private', 41010),
    bounded,
    disabled: normalizeProcessText(input, scenario(selected.executable, false), '/tmp/rea-private', 41010),
    comparison_contract: digestProcessCommitment(processComparisonContract(selected)),
    sample: normalizeProcessSamples([{ at_ms: 0, pid: 41010, parent_pid: 1, process_group_id: 41010, session_id: 41010, command: input }], selected, 41010)[0].command,
  };
});
console.log(JSON.stringify({ node: process.version, cases, raw_equal: cases[0].input === cases[1].input, normalized_equal: cases[0].normalized === cases[1].normalized, compatible_contracts: cases[0].comparison_contract === cases[1].comparison_contract }, null, 2));

The probe ran once before drafting, under Node.js v24.22.1. It exercises two selected-executable cases plus the disabled-normalization controls in the same invocation.

Expected behavior

Only the selected executable's own path should acquire <executable> identity. A different sibling filename must retain its distinguishing filename/path components. With the common working directory in this probe, directory normalization may produce <working-directory>/original/tool-helper and <working-directory>/rebuild/tool-helper; those results must remain different.

The owning function describes the aliases as selected identities. The process-capture contract uses normalized text for comparisons and trace assertions. Treating an unselected sibling as the selected executable removes meaningful distinctions before those consumers see the text. No known-good version was established.

Impact and scope

P2: valid output or sampled command strings containing another filename with the selected executable's path as a prefix receive an incorrect executable alias. The probe directly demonstrates a collision between different texts under compatible contracts. The downstream risk of an incorrectly unchanged terminal/trace comparison follows from the documented use of normalized text; a complete capture comparison was not executed.

Original PTY text is retained separately when capture changes a chunk, so this report does not claim raw-evidence loss or target mutation. Production frequency and full Linux/macOS capture behavior are NOT_ENOUGH_INFO; no real PTY or platform-specific capture run was made. Disabling path normalization is a verified source-function workaround for these inputs, with the tradeoff that desired path normalization is also disabled.

REA version

Audited main commit 46111dd091f2ae01baab9eb119b53b9bd309a05b, package 6.7.0. Not main-only at source level: src/process/capture/ProcessNormalization.ts has the same Git blob (1f5402993e78fa9bec759f8384b061f47d6849f8) at the published rea-agents-6.7.0 release commit 27dc42d10249f0410904a75c0997842eb03b931c. The installed/published package was not executed; release applicability is established from identical source, not a separate release runtime reproduction.

Runtime and platform

Node.js v24.22.1; Linux 7.2.8-9-omarchy, x86_64; locked checkout dependencies and its tsx executable. No Hopper, Ghidra, LLDB, GDB, browser, or PTY provider was used.

Target and artifact context

Inert synthetic absolute path strings for two selected executables and their distinct -helper sibling names. The scenario schema, source normalizers, process-sample projection, and comparison-contract commitment ran directly. No real target or user session was accessed.

Interface

Other

Earliest failure stage

Capture or observation

Relevant output or evidence

Reduced actual probe output:

{
  "original": {"normalized":"<executable>-helper","sample":"<executable>-helper","disabled":"/tmp/rea-path-prefix-fixture/original/tool-helper"},
  "rebuild": {"normalized":"<executable>-helper","sample":"<executable>-helper","disabled":"/tmp/rea-path-prefix-fixture/rebuild/tool-helper"},
  "raw_equal": false,
  "normalized_equal": true,
  "compatible_contracts": true
}

Both bounded results were {"state":"normalized","value":"<executable>-helper","utf8Bytes":19}.

  • Evidence provenance: observed source-function execution; capture/consumer wiring is source-derived.
  • Local verification: both production normalizer variants and sample-command normalization reproduced the prefix substitution, and the disabled control preserved the distinct strings.
  • Reproduction completeness: complete for the normalization collision; no end-to-end capture/comparison or installed-release execution.
  • The production harness selects this bounded normalizer, and the capture callback stores its result as frame data.

Typed outcome or recovery expected

Return a normalized text result that preserves the distinction between the selected executable path and a different sibling filename. Preserve original admitted text and existing normalization controls. No new error category is needed for these otherwise valid inputs.

Analysis or suspected cause

Source-derived: the alias matcher escapes each selected path and joins the alternatives into a global regular expression without checking path-component or filename boundaries. Longest-match ordering resolves overlapping selected aliases but cannot distinguish a complete selected path from the prefix of an unselected sibling.

Additional context

Open and closed issue searches used "normalization" "prefix", "normalization" "sibling", "process" "path" "substring", and in:title process normalization. No matching sibling-prefix report was found in the returned results. The coordinating agent also searched current open issues and related closed reports; no matching sibling-prefix report was found. A broader "normalization" "executable" query reached its 100-result cap and is not treated as exhaustive.

Related #4003 concerns the bounded port-normalization pipeline, #6443 concerns descendant PID replacement, and #5418 concerns rendered-budget omission wording. This probe disables port and PID normalization and uses a generous 4096-byte normalizer budget; none of those triggers is involved.

Last-known-good version: NOT_ENOUGH_INFO; no historical regression boundary was established. Prepared with AI assistance. No fix or source change is included.

Triage assessment

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: Executed production normalizers and sample projection assign selected-executable identity to different sibling paths, collapsing distinct texts under equal comparison contracts. Raw PTY loss and a full downstream comparison failure were not claimed.
  • Workaround status: available
  • Workaround basis: The executed paths: false controls preserve each input verbatim and keep the two sibling strings distinct, at the cost of disabling built-in path normalization.
Originally created by @coygeek on GitHub (Oct 23, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6586 ## 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 An executed source-function probe found that `normalization.paths: true` treats the selected executable as an unrestricted text prefix. Selecting `.../original/tool` rewrites the distinct sibling path `.../original/tool-helper` to `<executable>-helper`. Selecting `.../rebuild/tool` does the same to `.../rebuild/tool-helper`. The different original texts consequently become equal, and both scenarios produce the same comparison-contract commitment. Both the bounded normalizer used by capture and `normalizeProcessSamples` produce that alias. A control with `paths: false` preserves the different paths. This is an observed production-source boundary result; no PTY capture, CLI/MCP call, helper process, or debugger/browser session was executed. ## Reproduction Use the source revision below and its locked dependencies. Save this inert probe as `/tmp/path-prefix-probe.mts`, then run `./node_modules/.bin/tsx /tmp/path-prefix-probe.mts "$PWD"` from the checkout with its pinned Node.js selected. The paths are synthetic public fixture strings; no filesystem entry at those paths is required or created. ```ts import { pathToFileURL } from 'node:url'; import { resolve } from 'node:path'; const repo = process.argv[2] ?? process.cwd(); const load = (name: string) => import(pathToFileURL(resolve(repo, name)).href); const { parseProcessScenario, processComparisonContract, digestProcessCommitment } = await load('src/domain/process/processScenario.ts'); const { createBoundedProcessTextNormalizer, normalizeProcessText, normalizeProcessSamples } = await load('src/process/capture/ProcessNormalization.ts'); const root = '/tmp/rea-path-prefix-fixture'; const scenario = (executable: string, paths = true) => parseProcessScenario({ executable, working_directory: root, normalization: { paths, pids: false, ports: false, patterns: [] }, }); const cases = ['original', 'rebuild'].map(name => { const selected = scenario(`${root}/${name}/tool`); const input = `${selected.executable}-helper`; const bounded = createBoundedProcessTextNormalizer(selected, '/tmp/rea-private', 41010)(input, 4096); return { executable: selected.executable, input, normalized: normalizeProcessText(input, selected, '/tmp/rea-private', 41010), bounded, disabled: normalizeProcessText(input, scenario(selected.executable, false), '/tmp/rea-private', 41010), comparison_contract: digestProcessCommitment(processComparisonContract(selected)), sample: normalizeProcessSamples([{ at_ms: 0, pid: 41010, parent_pid: 1, process_group_id: 41010, session_id: 41010, command: input }], selected, 41010)[0].command, }; }); console.log(JSON.stringify({ node: process.version, cases, raw_equal: cases[0].input === cases[1].input, normalized_equal: cases[0].normalized === cases[1].normalized, compatible_contracts: cases[0].comparison_contract === cases[1].comparison_contract }, null, 2)); ``` The probe ran once before drafting, under Node.js `v24.22.1`. It exercises two selected-executable cases plus the disabled-normalization controls in the same invocation. ## Expected behavior Only the selected executable's own path should acquire `<executable>` identity. A different sibling filename must retain its distinguishing filename/path components. With the common working directory in this probe, directory normalization may produce `<working-directory>/original/tool-helper` and `<working-directory>/rebuild/tool-helper`; those results must remain different. The [owning function describes the aliases as selected identities](https://github.com/morluto/rea/blob/46111dd091f2ae01baab9eb119b53b9bd309a05b/src/process/capture/ProcessNormalization.ts#L289-L307). The [process-capture contract](https://github.com/morluto/rea/blob/46111dd091f2ae01baab9eb119b53b9bd309a05b/docs/process-capture.md#L264-L278) uses normalized text for comparisons and trace assertions. Treating an unselected sibling as the selected executable removes meaningful distinctions before those consumers see the text. No known-good version was established. ## Impact and scope P2: valid output or sampled command strings containing another filename with the selected executable's path as a prefix receive an incorrect executable alias. The probe directly demonstrates a collision between different texts under compatible contracts. The downstream risk of an incorrectly unchanged terminal/trace comparison follows from the documented use of normalized text; a complete capture comparison was not executed. Original PTY text is retained separately when capture changes a chunk, so this report does not claim raw-evidence loss or target mutation. Production frequency and full Linux/macOS capture behavior are `NOT_ENOUGH_INFO`; no real PTY or platform-specific capture run was made. Disabling path normalization is a verified source-function workaround for these inputs, with the tradeoff that desired path normalization is also disabled. ## REA version Audited main commit `46111dd091f2ae01baab9eb119b53b9bd309a05b`, package `6.7.0`. **Not main-only at source level:** `src/process/capture/ProcessNormalization.ts` has the same Git blob (`1f5402993e78fa9bec759f8384b061f47d6849f8`) at the published `rea-agents-6.7.0` release commit `27dc42d10249f0410904a75c0997842eb03b931c`. The installed/published package was not executed; release applicability is established from identical source, not a separate release runtime reproduction. ## Runtime and platform Node.js `v24.22.1`; Linux `7.2.8-9-omarchy`, x86_64; locked checkout dependencies and its `tsx` executable. No Hopper, Ghidra, LLDB, GDB, browser, or PTY provider was used. ## Target and artifact context Inert synthetic absolute path strings for two selected executables and their distinct `-helper` sibling names. The scenario schema, source normalizers, process-sample projection, and comparison-contract commitment ran directly. No real target or user session was accessed. ## Interface Other ## Earliest failure stage Capture or observation ## Relevant output or evidence Reduced actual probe output: ```json { "original": {"normalized":"<executable>-helper","sample":"<executable>-helper","disabled":"/tmp/rea-path-prefix-fixture/original/tool-helper"}, "rebuild": {"normalized":"<executable>-helper","sample":"<executable>-helper","disabled":"/tmp/rea-path-prefix-fixture/rebuild/tool-helper"}, "raw_equal": false, "normalized_equal": true, "compatible_contracts": true } ``` Both bounded results were `{"state":"normalized","value":"<executable>-helper","utf8Bytes":19}`. - Evidence provenance: observed source-function execution; capture/consumer wiring is source-derived. - Local verification: both production normalizer variants and sample-command normalization reproduced the prefix substitution, and the disabled control preserved the distinct strings. - Reproduction completeness: complete for the normalization collision; no end-to-end capture/comparison or installed-release execution. - [The production harness selects this bounded normalizer](https://github.com/morluto/rea/blob/46111dd091f2ae01baab9eb119b53b9bd309a05b/src/process/capture/ProcessHarness.ts#L292-L300), and [the capture callback stores its result as frame data](https://github.com/morluto/rea/blob/46111dd091f2ae01baab9eb119b53b9bd309a05b/src/process/capture/ProcessCaptureLifecycle.ts#L1449-L1467). ## Typed outcome or recovery expected Return a normalized text result that preserves the distinction between the selected executable path and a different sibling filename. Preserve original admitted text and existing normalization controls. No new error category is needed for these otherwise valid inputs. ## Analysis or suspected cause Source-derived: [the alias matcher](https://github.com/morluto/rea/blob/46111dd091f2ae01baab9eb119b53b9bd309a05b/src/process/capture/ProcessNormalization.ts#L308-L320) escapes each selected path and joins the alternatives into a global regular expression without checking path-component or filename boundaries. Longest-match ordering resolves overlapping selected aliases but cannot distinguish a complete selected path from the prefix of an unselected sibling. ## Additional context Open and closed issue searches used `"normalization" "prefix"`, `"normalization" "sibling"`, `"process" "path" "substring"`, and `in:title process normalization`. No matching sibling-prefix report was found in the returned results. The coordinating agent also searched current open issues and related closed reports; no matching sibling-prefix report was found. A broader `"normalization" "executable"` query reached its 100-result cap and is not treated as exhaustive. Related [#4003](https://github.com/morluto/rea/issues/4003) concerns the bounded port-normalization pipeline, [#6443](https://github.com/morluto/rea/issues/6443) concerns descendant PID replacement, and [#5418](https://github.com/morluto/rea/issues/5418) concerns rendered-budget omission wording. This probe disables port and PID normalization and uses a generous 4096-byte normalizer budget; none of those triggers is involved. Last-known-good version: `NOT_ENOUGH_INFO`; no historical regression boundary was established. Prepared with AI assistance. No fix or source change is included. ## Triage assessment - Impact level: P2 - Assessment status: supported - Impact basis: Executed production normalizers and sample projection assign selected-executable identity to different sibling paths, collapsing distinct texts under equal comparison contracts. Raw PTY loss and a full downstream comparison failure were not claimed. - Workaround status: available - Workaround basis: The executed `paths: false` controls preserve each input verbatim and keep the two sibling strings distinct, at the cost of disabling built-in path normalization.
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#3974
No description provided.