[GH-ISSUE #6446] [Bug] capture-process classifies observation-root access denial as execution_failure #3859

Closed
opened 2026-10-31 00:44:42 +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/6446

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

node scripts/rea.mjs capture-process scenario.json --format json returns process_capture_failed / execution_failure when an ancestor of a selected filesystem_observation_paths root denies filesystem search access. The response says no PTY process was spawned. It retains EACCES only in prose and repeats the OS error as remediation, losing the existing structured filesystem-read/access-denied outcome.

Selecting the same denied path as working_directory instead returns access_denied / unavailable, structured system_code: EACCES, and read-access recovery. Restoring ancestor permissions lets the original observation-root scenario complete successfully.

Reproduction

Use a non-root account on a POSIX host enforcing directory permissions. Fresh-checkout preparation is below; the observed runs used the already-built checkout at this revision.

git clone https://github.com/morluto/rea.git rea-repro
cd rea-repro
git checkout 9eb829aca34be36ff112826337996f6139f82eff
npm ci && npm run build:cached

From the checkout root, create the fixture inline and invoke the public CLI:

node --input-type=module <<'JS'
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { spawnSync } from 'node:child_process';

const cli = path.resolve('scripts/rea.mjs');
const work = fs.mkdtempSync(path.join(os.tmpdir(), 'rea-root-denial-'));
const locked = path.join(work, 'locked');
const home = path.join(work, 'home');
fs.mkdirSync(locked);
fs.mkdirSync(home);
const scenario = {
  executable: process.execPath,
  arguments: ['-e', "console.log('ran')"],
  working_directory: work,
  filesystem_observation_paths: ['./locked/sub'],
  timeout_ms: 5000,
  settle_ms: 100
};
function capture(label, input) {
  const file = path.join(work, 'scenario.json');
  fs.writeFileSync(file, JSON.stringify(input));
  const run = spawnSync(process.execPath,
    [cli, 'capture-process', file, '--format', 'json'],
    { encoding: 'utf8', env: { ...process.env, HOME: home },
      timeout: 30000, maxBuffer: 4000000 });
  if (run.error) throw run.error;
  const result = JSON.parse(run.stdout);
  console.log(JSON.stringify({ label, status: run.status,
    code: result.code, category: result.category,
    remediation: result.remediation, details: result.details,
    exit: result.normalized_result?.exit }));
}
try {
  fs.chmodSync(locked, 0);
  capture('denied observation root', scenario);
  capture('denied working directory', {
    ...scenario, working_directory: path.join(locked, 'sub'),
    filesystem_observation_paths: []
  });
  fs.chmodSync(locked, 0o755);
  capture('restored access', scenario);
} finally {
  fs.chmodSync(locked, 0o755);
  fs.rmSync(work, { recursive: true, force: true });
}
JS

The denied observation-root behavior reproduced in 3/3 executed CLI trials, including a run of this exact inline script. The denied-working-directory control and unchanged scenario after permission restoration each ran twice. Fresh-checkout installation and build preparation were not rerun.

Expected behavior

Continue refusing capture before launch, but classify the observed host permission denial as access_denied / unavailable, retaining the selected path, native code and filesystem-read boundary with access-recovery guidance.

The capture contract requires denied initial root lookups to fail. The domain error contract models denied selected reads as AnalysisAccessDeniedError. The same command already uses that type for selected-path permission denials.

Impact and scope

P2: structured callers must parse OS error text to distinguish an access denial from other capture failures, and receive no actionable access remediation. The observed failure is bounded and fails closed. No process launch, permission bypass, or data loss was observed. Restoring search permission on the owned ancestor was a demonstrated workaround: the unchanged scenario then returned normal Capture Evidence with child exit 0. This does not establish a remedy for directories the caller cannot change. Linux, Windows and MCP behavior were not executed.

REA version

6.7.0 (source 9eb829aca3)

Runtime and platform

Node.js v26.12.1; macOS 27.0.1; arm64. Built source runtime with installed locked dependencies.

Target and artifact context

Disposable local directory with a mode-000 ancestor and an absent selected descendant. The intended Node child only prints ran; the denied trial never launches it. No analysis provider or external service is involved.

Interface

CLI

Earliest failure stage

Capture or observation

Relevant output or evidence

Reduced observed denied-root response, with the fixture location generalized:

CLI exit: 1
code: process_capture_failed
category: execution_failure
message: Process filesystem snapshot failed: EACCES: permission denied, lstat '<fixture>/locked/sub'
remediation.action: same text as message
details.partial_observation.observations.target_pid:
  state: unavailable
  reason: No PTY process was spawned before the run failed.

The working-directory control returned access_denied, unavailable, details.system_code: EACCES, details.boundary: filesystem-read, and remediation beginning “Check the current process's read access”. After permission restoration, the original observation-root scenario returned CLI exit 0 and target exit 0.

Typed outcome or recovery expected

Preserve operation: capture_process_scenario, the selected filesystem path, native EACCES and the existing filesystem-read/access-denied recovery. An input-schema issue is unnecessary for a genuine host permission denial. Keep initial denied-root refusal effective and preserve successful capture after access is restored.

Analysis or suspected cause

Source-derived explanation: snapshotRoots throws the raw lookup cause on initial admission. Its catch wraps that cause as ProcessCaptureSnapshotFailure; the constructor preserves cancellation classification but otherwise constructs a generic process-capture failure. The native denial never becomes the existing access-denied error type.

Additional context

Related #2316 concerns executable and working-directory selection. #2545 concerns unreadable child-directory enumeration. #3238 and #6213 concern ancestor obstruction after root admission. #6117 concerns Electron application-path admission. None reports this initial process observation-root permission classification. NOT_ENOUGH_INFO for a last known good revision; no historical regression comparison was performed.

Originally created by @coygeek on GitHub (Oct 23, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6446 ### 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 `node scripts/rea.mjs capture-process scenario.json --format json` returns `process_capture_failed` / `execution_failure` when an ancestor of a selected `filesystem_observation_paths` root denies filesystem search access. The response says no PTY process was spawned. It retains `EACCES` only in prose and repeats the OS error as remediation, losing the existing structured filesystem-read/access-denied outcome. Selecting the same denied path as `working_directory` instead returns `access_denied` / `unavailable`, structured `system_code: EACCES`, and read-access recovery. Restoring ancestor permissions lets the original observation-root scenario complete successfully. ### Reproduction Use a non-root account on a POSIX host enforcing directory permissions. Fresh-checkout preparation is below; the observed runs used the already-built checkout at this revision. ```sh git clone https://github.com/morluto/rea.git rea-repro cd rea-repro git checkout 9eb829aca34be36ff112826337996f6139f82eff npm ci && npm run build:cached ``` From the checkout root, create the fixture inline and invoke the public CLI: ```sh node --input-type=module <<'JS' import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { spawnSync } from 'node:child_process'; const cli = path.resolve('scripts/rea.mjs'); const work = fs.mkdtempSync(path.join(os.tmpdir(), 'rea-root-denial-')); const locked = path.join(work, 'locked'); const home = path.join(work, 'home'); fs.mkdirSync(locked); fs.mkdirSync(home); const scenario = { executable: process.execPath, arguments: ['-e', "console.log('ran')"], working_directory: work, filesystem_observation_paths: ['./locked/sub'], timeout_ms: 5000, settle_ms: 100 }; function capture(label, input) { const file = path.join(work, 'scenario.json'); fs.writeFileSync(file, JSON.stringify(input)); const run = spawnSync(process.execPath, [cli, 'capture-process', file, '--format', 'json'], { encoding: 'utf8', env: { ...process.env, HOME: home }, timeout: 30000, maxBuffer: 4000000 }); if (run.error) throw run.error; const result = JSON.parse(run.stdout); console.log(JSON.stringify({ label, status: run.status, code: result.code, category: result.category, remediation: result.remediation, details: result.details, exit: result.normalized_result?.exit })); } try { fs.chmodSync(locked, 0); capture('denied observation root', scenario); capture('denied working directory', { ...scenario, working_directory: path.join(locked, 'sub'), filesystem_observation_paths: [] }); fs.chmodSync(locked, 0o755); capture('restored access', scenario); } finally { fs.chmodSync(locked, 0o755); fs.rmSync(work, { recursive: true, force: true }); } JS ``` The denied observation-root behavior reproduced in 3/3 executed CLI trials, including a run of this exact inline script. The denied-working-directory control and unchanged scenario after permission restoration each ran twice. Fresh-checkout installation and build preparation were not rerun. ### Expected behavior Continue refusing capture before launch, but classify the observed host permission denial as `access_denied` / `unavailable`, retaining the selected path, native code and filesystem-read boundary with access-recovery guidance. The [capture contract](https://github.com/morluto/rea/blob/9eb829aca34be36ff112826337996f6139f82eff/docs/process-capture.md#L257) requires denied initial root lookups to fail. The [domain error contract](https://github.com/morluto/rea/blob/9eb829aca34be36ff112826337996f6139f82eff/src/domain/analysisErrorCore.ts#L231) models denied selected reads as `AnalysisAccessDeniedError`. The same command already [uses that type for selected-path permission denials](https://github.com/morluto/rea/blob/9eb829aca34be36ff112826337996f6139f82eff/src/process/capture/ProcessScenarioRuntimeValidation.ts#L133). ### Impact and scope P2: structured callers must parse OS error text to distinguish an access denial from other capture failures, and receive no actionable access remediation. The observed failure is bounded and fails closed. No process launch, permission bypass, or data loss was observed. Restoring search permission on the owned ancestor was a demonstrated workaround: the unchanged scenario then returned normal Capture Evidence with child exit 0. This does not establish a remedy for directories the caller cannot change. Linux, Windows and MCP behavior were not executed. ### REA version 6.7.0 (source 9eb829aca34be36ff112826337996f6139f82eff) ### Runtime and platform Node.js v26.12.1; macOS 27.0.1; arm64. Built source runtime with installed locked dependencies. ### Target and artifact context Disposable local directory with a mode-000 ancestor and an absent selected descendant. The intended Node child only prints `ran`; the denied trial never launches it. No analysis provider or external service is involved. ### Interface CLI ### Earliest failure stage Capture or observation ### Relevant output or evidence Reduced observed denied-root response, with the fixture location generalized: ```text CLI exit: 1 code: process_capture_failed category: execution_failure message: Process filesystem snapshot failed: EACCES: permission denied, lstat '<fixture>/locked/sub' remediation.action: same text as message details.partial_observation.observations.target_pid: state: unavailable reason: No PTY process was spawned before the run failed. ``` The working-directory control returned `access_denied`, `unavailable`, `details.system_code: EACCES`, `details.boundary: filesystem-read`, and remediation beginning “Check the current process's read access”. After permission restoration, the original observation-root scenario returned CLI exit 0 and target exit 0. ### Typed outcome or recovery expected Preserve `operation: capture_process_scenario`, the selected filesystem path, native `EACCES` and the existing filesystem-read/access-denied recovery. An input-schema issue is unnecessary for a genuine host permission denial. Keep initial denied-root refusal effective and preserve successful capture after access is restored. ### Analysis or suspected cause Source-derived explanation: [snapshotRoots](https://github.com/morluto/rea/blob/9eb829aca34be36ff112826337996f6139f82eff/src/process/capture/FilesystemSnapshot.ts#L623) throws the raw lookup cause on initial admission. Its [catch](https://github.com/morluto/rea/blob/9eb829aca34be36ff112826337996f6139f82eff/src/process/capture/FilesystemSnapshot.ts#L654) wraps that cause as `ProcessCaptureSnapshotFailure`; the [constructor](https://github.com/morluto/rea/blob/9eb829aca34be36ff112826337996f6139f82eff/src/process/capture/FilesystemSnapshot.ts#L196) preserves cancellation classification but otherwise constructs a generic process-capture failure. The native denial never becomes the existing access-denied error type. ### Additional context Related [#2316](https://github.com/morluto/rea/issues/2316) concerns executable and working-directory selection. [#2545](https://github.com/morluto/rea/issues/2545) concerns unreadable child-directory enumeration. [#3238](https://github.com/morluto/rea/issues/3238) and [#6213](https://github.com/morluto/rea/issues/6213) concern ancestor obstruction after root admission. [#6117](https://github.com/morluto/rea/issues/6117) concerns Electron application-path admission. None reports this initial process observation-root permission classification. NOT_ENOUGH_INFO for a last known good revision; no historical regression comparison was performed.
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#3859
No description provided.