[GH-ISSUE #7649] [Bug] Empty archive member is classified as an I/O failure instead of invalid input #4573

Closed
opened 2026-10-31 00:50:55 +00:00 by bsakaguchi · 0 comments
Owner

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

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

For a readable static app bundle, this public CLI request treats an empty archive selector as a file-access failure:

node scripts/rea.mjs inspect-keyed-archive BUNDLE "" --json

It exits 1 with artifact_operation_failed, category execution_failure, and details.reason: "io". The message and remediation tell the caller to check file access. The response contains neither a field issue identifying the empty argument nor input-contract recovery.

On the unchanged bundle, selecting Contents/Resources/sample.plist succeeds. Selecting . correctly returns invalid_request / invalid_input with an archive-selection issue and inspect_input_contract recovery. The CLI schema permits an empty archive string, whereas the domain schema and advertised MCP selector require at least one character.

Reproduction

  1. Use Node.js v24.22.1 with the built source revision below. From the checkout root, execute this block. It creates a bundle containing only XML plists, isolates product state, puts inert package-runner fixtures first on PATH, and removes the temporary files when finished.
python3 -I - <<'PYTHON'
import json, os, pathlib, plistlib, subprocess, tempfile
with tempfile.TemporaryDirectory() as directory:
    work = pathlib.Path(directory)
    app = work / 'Static.app'
    resources = app / 'Contents' / 'Resources'
    resources.mkdir(parents=True)
    (app / 'Contents' / 'Info.plist').write_bytes(plistlib.dumps({
        'CFBundleIdentifier': 'org.example.static'}))
    (resources / 'sample.plist').write_bytes(plistlib.dumps({
        '$archiver': 'NSKeyedArchiver', '$version': 100000,
        '$objects': ['$null', {'k': 'v'}],
        '$top': {'root': {'CF$UID': 1}}}))
    runners = work / 'bin'
    runners.mkdir()
    for name in ('npx', 'npm', 'bun'):
        stub = runners / name
        stub.write_text('#!/bin/sh\nexit 97\n')
        stub.chmod(0o755)
    env = {'PATH': str(runners) + os.pathsep + os.environ['PATH'],
           'REA_LOG_LEVEL': 'silent'}
    for key in ('HOME', 'XDG_CONFIG_HOME', 'XDG_CACHE_HOME', 'XDG_DATA_HOME',
                'XDG_STATE_HOME', 'XDG_RUNTIME_DIR', 'TMPDIR'):
        location = work / key.lower()
        location.mkdir()
        env[key] = str(location)
    for repeat in (1, 2):
        for member in ('', '.', 'Contents/Resources/sample.plist'):
            call = subprocess.run(['node', 'scripts/rea.mjs',
                'inspect-keyed-archive', str(app), member, '--json'],
                env=env, text=True, capture_output=True, timeout=60)
            result = json.loads(call.stdout)
            record = {'repeat': repeat, 'member': member,
                      'exit': call.returncode, 'stderr': call.stderr}
            if call.returncode:
                record['error'] = result
            else:
                record['archive_path'] = result['normalized_result']['archive_path']
                record['total_objects'] = result['normalized_result']['total_objects']
            print(json.dumps(record))
PYTHON
  1. Compare the three records on each pass: empty member returns artifact_operation_failed; dot returns invalid_request; the valid member exits 0 with two objects.
  2. Run node scripts/rea.mjs inspect-keyed-archive --schema and compare the archive property with the MCP schema excerpt in Evidence. The CLI property has no minimum length.

The exact inline-fixture Python block was executed twice per selector. A preceding independent driver also ran each selector twice: all four empty-member calls returned the same I/O classification, all four dot controls returned an input refusal, and all four valid-member controls succeeded. Stderr was empty for every call. No native target was opened.

Expected behavior

Reject an empty second argument as invalid caller input, identifying the archive argument and directing the caller to correct it. Preserve the successful relative-member selection.

The documented command contract uses the second argument to select one relative archive inside the bundle. The tool-design contract requires empty selections to be rejected and expected input failures to retain field correction metadata. The shared selector schema already requires a nonempty string, and MCP discovery advertises that constraint. The contract references and executed schema results are in Evidence below.

Impact and scope

P2: an invalid member request receives the wrong machine-readable failure category and recovery action. A caller following the response is directed toward file access instead of correcting the empty selection, and an automated client cannot recover using the usual field issues. The readable valid-member control rules out an actual access failure for this fixture.

The demonstrated scope is the bundle CLI on macOS. A safe caller workaround is to reject empty member strings before invocation; selecting the valid member succeeded in every control. No data loss, target execution, mutation, or hang was observed. Standalone plist targets, other platforms, and MCP execution with an invalid selector were not tested. MCP discovery was tested without opening a target.

REA version

Exact node scripts/rea.mjs --version stdout:

6.8.0

Source commit: 65f1c447344a423fa2785847dbcea1e9a9e2454a. The npm registry also reported 6.8.0 as the latest published rea-agents version; the executed artifact was this checkout's prebuilt runtime.

Runtime and platform

Node.js v24.22.1; macOS 27.0.1, build 26A434; darwin arm64. No external native analysis provider was used.

Target and artifact context

A synthetic Static.app directory containing Contents/Info.plist and Contents/Resources/sample.plist. The resource is a two-object XML NSKeyedArchiver archive. The bundle has no executable file or CFBundleExecutable declaration.

Interface

CLI

Earliest failure stage

Request parsing or validation

Relevant output or evidence

The executed inline reproduction returned this complete error for the empty member on both passes:

{"error":"Analysis failed","code":"artifact_operation_failed","category":"execution_failure","message":"Artifact could not be read or written. Check file access and try again.","retryable":false,"remediation":{"action":"Artifact could not be read or written. Check file access and try again."},"details":{"operation":"inspect_keyed_archive","reason":"io"}}

Reduced dot control, exit 1:

{"code":"invalid_request","category":"invalid_input","recovery":{"kind":"inspect_input_contract","operation":"inspect_keyed_archive"},"details":{"operation":"inspect_keyed_archive","issues":[{"path":["path"],"reason":"invalid_format","message":"Select a keyed archive path relative to the active app bundle."}]}}

The dot control's provider field name is the separate attribution defect in #7630. The empty member produces no such issue at all.

Successful relative-member control, reduced from the executed script:

{"repeat":1,"member":"Contents/Resources/sample.plist","exit":0,"stderr":"","archive_path":"Contents/Resources/sample.plist","total_objects":2}

Executed --schema output for the CLI argument:

archive:
  description: Relative archive path when the target is a bundle
  default: .
  type: string

An SDK stdio client connected to node scripts/rea.mjs mcp, called tools/list, and selected inspect_keyed_archive. Its advertised path property included:

{"type":"string","minLength":1}

This is an excerpt; the property also has a NUL-free pattern. The client then pinged and closed successfully. It made no target-opening or analysis request.

An executed source-domain safeParse with {path: ""} returned success: false with code: "too_small", minimum: 1, and path: ["path"]. Dot and the valid member both passed that schema probe.

At the baseline:

  • docs/native-investigation.md:611-614 documents relative member selection through the second CLI argument or MCP path.
  • docs/tool-design.md:339-351 requires empty-selection rejection, CLI contract parity, and field/index metadata for expected input failures.
  • src/cli/artifactCommands.ts:29-67 declares archive without a minimum and forwards it as provider path.
  • src/domain/apple/keyedArchive.ts:29-35 and src/domain/localPath.ts:19-20 impose the nonempty selector constraint. src/contracts/artifactToolContracts.ts:111-115 uses this schema for MCP.
  • src/artifacts/apple/KeyedArchiveReader.ts:340-358 parses that schema before its typed canonical-member refusal. Its wrapper at lines 187-225 retains the failure through cleanup.
  • src/artifacts/ArtifactOperationFailure.ts:53-57 preserves typed analysis errors and maps other exceptions to an artifact I/O failure.

Typed outcome or recovery expected

A validation refusal naming CLI archive, with a nonempty-string constraint and input-correction recovery. Either early CLI validation or a correctly projected shared input error would satisfy this contract. The current file-access remediation does not describe the caller's mistake.

Analysis or suspected cause

Source-derived explanation: the CLI and shared selector schemas disagree about empty strings. The CLI admits the argument and forwards it unchanged. The reader's shared-schema parse rejects it before the canonical-member check can create an AnalysisInputError. Artifact failure translation handles an untyped exception as I/O. The runtime output and direct schema probe agree with that path; no actual file-access error was induced.

Additional context

Read-only duplicate searches covered open/closed issues, issue comments, and open/closed/merged PRs. Related open #7630 asks for existing selection issues to identify CLI archive rather than provider path. That mapping-only repair leaves this empty-string failure intact because it has no input issue to rename. A broader validation repair could address both, but their closure checks differ.

Open #7377 concerns an empty first target resolving to the working directory. This reproduction supplies a valid first target and an empty second selector. Closed #6064 concerns real host permission denial, which is absent here.

Merged PR #1070 changes reader-generated selection errors to invalid input; its listed cases do not include an empty value rejected by the schema first. Merged PR #3606 adds NUL validation while retaining the preexisting nonempty domain constraint. Neither establishes that this exact CLI defect was previously fixed. No known-good version or regression claim is established.

Originally created by @coygeek on GitHub (Oct 28, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/7649 ### 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 For a readable static app bundle, this public CLI request treats an empty archive selector as a file-access failure: ```sh node scripts/rea.mjs inspect-keyed-archive BUNDLE "" --json ``` It exits 1 with `artifact_operation_failed`, category `execution_failure`, and `details.reason: "io"`. The message and remediation tell the caller to check file access. The response contains neither a field issue identifying the empty argument nor input-contract recovery. On the unchanged bundle, selecting `Contents/Resources/sample.plist` succeeds. Selecting `.` correctly returns `invalid_request` / `invalid_input` with an archive-selection issue and `inspect_input_contract` recovery. The CLI schema permits an empty `archive` string, whereas the domain schema and advertised MCP selector require at least one character. ### Reproduction 1. Use Node.js v24.22.1 with the built source revision below. From the checkout root, execute this block. It creates a bundle containing only XML plists, isolates product state, puts inert package-runner fixtures first on PATH, and removes the temporary files when finished. ```sh python3 -I - <<'PYTHON' import json, os, pathlib, plistlib, subprocess, tempfile with tempfile.TemporaryDirectory() as directory: work = pathlib.Path(directory) app = work / 'Static.app' resources = app / 'Contents' / 'Resources' resources.mkdir(parents=True) (app / 'Contents' / 'Info.plist').write_bytes(plistlib.dumps({ 'CFBundleIdentifier': 'org.example.static'})) (resources / 'sample.plist').write_bytes(plistlib.dumps({ '$archiver': 'NSKeyedArchiver', '$version': 100000, '$objects': ['$null', {'k': 'v'}], '$top': {'root': {'CF$UID': 1}}})) runners = work / 'bin' runners.mkdir() for name in ('npx', 'npm', 'bun'): stub = runners / name stub.write_text('#!/bin/sh\nexit 97\n') stub.chmod(0o755) env = {'PATH': str(runners) + os.pathsep + os.environ['PATH'], 'REA_LOG_LEVEL': 'silent'} for key in ('HOME', 'XDG_CONFIG_HOME', 'XDG_CACHE_HOME', 'XDG_DATA_HOME', 'XDG_STATE_HOME', 'XDG_RUNTIME_DIR', 'TMPDIR'): location = work / key.lower() location.mkdir() env[key] = str(location) for repeat in (1, 2): for member in ('', '.', 'Contents/Resources/sample.plist'): call = subprocess.run(['node', 'scripts/rea.mjs', 'inspect-keyed-archive', str(app), member, '--json'], env=env, text=True, capture_output=True, timeout=60) result = json.loads(call.stdout) record = {'repeat': repeat, 'member': member, 'exit': call.returncode, 'stderr': call.stderr} if call.returncode: record['error'] = result else: record['archive_path'] = result['normalized_result']['archive_path'] record['total_objects'] = result['normalized_result']['total_objects'] print(json.dumps(record)) PYTHON ``` 2. Compare the three records on each pass: empty member returns `artifact_operation_failed`; dot returns `invalid_request`; the valid member exits 0 with two objects. 3. Run `node scripts/rea.mjs inspect-keyed-archive --schema` and compare the `archive` property with the MCP schema excerpt in Evidence. The CLI property has no minimum length. The exact inline-fixture Python block was executed twice per selector. A preceding independent driver also ran each selector twice: all four empty-member calls returned the same I/O classification, all four dot controls returned an input refusal, and all four valid-member controls succeeded. Stderr was empty for every call. No native target was opened. ### Expected behavior Reject an empty second argument as invalid caller input, identifying the `archive` argument and directing the caller to correct it. Preserve the successful relative-member selection. The documented command contract uses the second argument to select one relative archive inside the bundle. The tool-design contract requires empty selections to be rejected and expected input failures to retain field correction metadata. The shared selector schema already requires a nonempty string, and MCP discovery advertises that constraint. The contract references and executed schema results are in Evidence below. ### Impact and scope P2: an invalid member request receives the wrong machine-readable failure category and recovery action. A caller following the response is directed toward file access instead of correcting the empty selection, and an automated client cannot recover using the usual field issues. The readable valid-member control rules out an actual access failure for this fixture. The demonstrated scope is the bundle CLI on macOS. A safe caller workaround is to reject empty member strings before invocation; selecting the valid member succeeded in every control. No data loss, target execution, mutation, or hang was observed. Standalone plist targets, other platforms, and MCP execution with an invalid selector were not tested. MCP discovery was tested without opening a target. ### REA version Exact `node scripts/rea.mjs --version` stdout: ```text 6.8.0 ``` Source commit: `65f1c447344a423fa2785847dbcea1e9a9e2454a`. The npm registry also reported 6.8.0 as the latest published rea-agents version; the executed artifact was this checkout's prebuilt runtime. ### Runtime and platform Node.js v24.22.1; macOS 27.0.1, build 26A434; darwin arm64. No external native analysis provider was used. ### Target and artifact context A synthetic `Static.app` directory containing `Contents/Info.plist` and `Contents/Resources/sample.plist`. The resource is a two-object XML NSKeyedArchiver archive. The bundle has no executable file or CFBundleExecutable declaration. ### Interface CLI ### Earliest failure stage Request parsing or validation ### Relevant output or evidence The executed inline reproduction returned this complete error for the empty member on both passes: ```json {"error":"Analysis failed","code":"artifact_operation_failed","category":"execution_failure","message":"Artifact could not be read or written. Check file access and try again.","retryable":false,"remediation":{"action":"Artifact could not be read or written. Check file access and try again."},"details":{"operation":"inspect_keyed_archive","reason":"io"}} ``` Reduced dot control, exit 1: ```json {"code":"invalid_request","category":"invalid_input","recovery":{"kind":"inspect_input_contract","operation":"inspect_keyed_archive"},"details":{"operation":"inspect_keyed_archive","issues":[{"path":["path"],"reason":"invalid_format","message":"Select a keyed archive path relative to the active app bundle."}]}} ``` The dot control's provider field name is the separate attribution defect in #7630. The empty member produces no such issue at all. Successful relative-member control, reduced from the executed script: ```json {"repeat":1,"member":"Contents/Resources/sample.plist","exit":0,"stderr":"","archive_path":"Contents/Resources/sample.plist","total_objects":2} ``` Executed `--schema` output for the CLI argument: ```yaml archive: description: Relative archive path when the target is a bundle default: . type: string ``` An SDK stdio client connected to `node scripts/rea.mjs mcp`, called `tools/list`, and selected `inspect_keyed_archive`. Its advertised `path` property included: ```json {"type":"string","minLength":1} ``` This is an excerpt; the property also has a NUL-free pattern. The client then pinged and closed successfully. It made no target-opening or analysis request. An executed source-domain `safeParse` with `{path: ""}` returned `success: false` with `code: "too_small"`, `minimum: 1`, and `path: ["path"]`. Dot and the valid member both passed that schema probe. At the baseline: - `docs/native-investigation.md:611-614` documents relative member selection through the second CLI argument or MCP `path`. - `docs/tool-design.md:339-351` requires empty-selection rejection, CLI contract parity, and field/index metadata for expected input failures. - `src/cli/artifactCommands.ts:29-67` declares `archive` without a minimum and forwards it as provider `path`. - `src/domain/apple/keyedArchive.ts:29-35` and `src/domain/localPath.ts:19-20` impose the nonempty selector constraint. `src/contracts/artifactToolContracts.ts:111-115` uses this schema for MCP. - `src/artifacts/apple/KeyedArchiveReader.ts:340-358` parses that schema before its typed canonical-member refusal. Its wrapper at lines 187-225 retains the failure through cleanup. - `src/artifacts/ArtifactOperationFailure.ts:53-57` preserves typed analysis errors and maps other exceptions to an artifact I/O failure. ### Typed outcome or recovery expected A validation refusal naming CLI `archive`, with a nonempty-string constraint and input-correction recovery. Either early CLI validation or a correctly projected shared input error would satisfy this contract. The current file-access remediation does not describe the caller's mistake. ### Analysis or suspected cause Source-derived explanation: the CLI and shared selector schemas disagree about empty strings. The CLI admits the argument and forwards it unchanged. The reader's shared-schema parse rejects it before the canonical-member check can create an `AnalysisInputError`. Artifact failure translation handles an untyped exception as I/O. The runtime output and direct schema probe agree with that path; no actual file-access error was induced. ### Additional context Read-only duplicate searches covered open/closed issues, issue comments, and open/closed/merged PRs. Related open [#7630](https://github.com/morluto/rea/issues/7630) asks for existing selection issues to identify CLI `archive` rather than provider `path`. That mapping-only repair leaves this empty-string failure intact because it has no input issue to rename. A broader validation repair could address both, but their closure checks differ. Open [#7377](https://github.com/morluto/rea/issues/7377) concerns an empty first target resolving to the working directory. This reproduction supplies a valid first target and an empty second selector. Closed [#6064](https://github.com/morluto/rea/issues/6064) concerns real host permission denial, which is absent here. Merged [PR #1070](https://github.com/morluto/rea/pull/1070) changes reader-generated selection errors to invalid input; its listed cases do not include an empty value rejected by the schema first. Merged [PR #3606](https://github.com/morluto/rea/pull/3606) adds NUL validation while retaining the preexisting nonempty domain constraint. Neither establishes that this exact CLI defect was previously fixed. No known-good version or regression claim is 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#4573
No description provided.