[GH-ISSUE #5982] [Bug] Unparseable lipo slice output becomes a generic artifact I/O failure #3650

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

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

Before submitting

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

Area

Evidence, artifacts, or reconstruction

Observed behavior

A successful simulated lipo -detailed_info capture contains a FAT header but no architecture records. MachOSliceArtifactReader.entries() rejects with TypeError("lipo output contained no architectures") while reading a synthetic two-slice universal Mach-O.

Passing that actual rejection through the production artifact failure translator and error projector produces artifact_operation_failed, category execution_failure, and details.reason: io. The producer diagnostic and original cause disappear. Both the message and remediation instruct the caller to check file access, although this failure concerns interpreting successful tool output.

Source tracing reaches this reader through artifact inspection of universal Mach-O files on macOS. Public CLI/MCP behavior was traced in source, not executed in this reproduction.

Reproduction

Executed as a focused production-boundary test using the repository's existing injectable host/runner seams. Save the following under src/artifacts/producerParse.test.ts in the baseline checkout with its locked dependencies installed, then run:

node node_modules/vitest/vitest.mjs run src/artifacts/producerParse.test.ts --maxWorkers=1

Both tests pass on the baseline: the first asserts the observed defective result, while the second confirms that an explicitly typed parse failure retains its diagnostic. Valid parser input is also checked before the malformed-response case. Remove the temporary test afterwards.

import { mkdtemp, writeFile, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { build } from "plist";
import { expect, it } from "vitest";
import { NativeDmgArtifactReader, type NativeDmgHost } from "./NativeDmgArtifactReader.js";
import { MachOSliceArtifactReader } from "./MachOSliceArtifactReader.js";
import { ArtifactReaderFailure, type ArtifactReader } from "./ArtifactReader.js";
import { translateArtifactFailure } from "./ArtifactOperationFailure.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import { parseNativeDmgInfo } from "./NativeDmgOwnership.js";
import { parseLipoOutput } from "../native/parsers/lipo.js";
import { ok } from "../domain/result.js";
import { CPU, fatImage, machoImage } from "./apple/MachoImage.fixture.js";
import type { NativeCommandRunner } from "../native/CommandRunner.js";

async function failure(reader: ArtifactReader): Promise<unknown> {
  try {
    for await (const _entry of reader.entries()) { /* consume */ }
    throw new Error("Expected iteration to fail");
  } catch (cause) {
    return cause;
  } finally {
    await reader.close();
  }
}
function checkProjection(cause: unknown, producer: string) {
  expect(cause).not.toBeInstanceOf(ArtifactReaderFailure);
  const translated = translateArtifactFailure("inspect_artifact", cause);
  expect(translated).toMatchObject({ reason: "io", detail: undefined });
  expect(translated.cause).toBeUndefined();
  const projected = projectAnalysisError(translated);
  expect(projected).toMatchObject({
    code: "artifact_operation_failed",
    category: "execution_failure",
    message: "Artifact could not be read or written. Check file access and try again.",
    details: { reason: "io" },
  });
  console.log(JSON.stringify({ producer, rawError: cause instanceof Error ? cause.name : typeof cause, projection: projected }));
}
it("drops the lipo architecture parse error at the artifact boundary", async () => {
  expect(parseLipoOutput("Non-fat file: input is architecture: arm64\n").architectures).toHaveLength(1);
  const root = await mkdtemp(join(process.env.REA_TEST_TMPDIR ?? tmpdir(), "parse-boundary-"));
  try {
    const binary = join(root, "input");
    await writeFile(binary, fatImage([
      { cpu: CPU.x86_64, bytes: machoImage({ cpu: CPU.x86_64 }) },
      { cpu: CPU.arm64, bytes: machoImage({ cpu: CPU.arm64 }) },
    ]));
    const calls: string[] = [];
    const runner: NativeCommandRunner = {
      run: async (tool, args) => {
        calls.push(tool);
        return ok({ tool, executable: "lipo", executableSha256: "1".repeat(64), toolVersion: null,
          versionReason: "fixture", arguments: args, stdout: "Fat header in: input\n", stderr: "",
          stdoutBytes: 21, stderrBytes: 0, exitCode: 0, signal: null });
      },
    };
    const cause = await failure(new MachOSliceArtifactReader(binary, {}, runner));
    expect(cause).toBeInstanceOf(TypeError);
    expect(cause).toMatchObject({ message: "lipo output contained no architectures" });
    expect(calls).toEqual(["lipo"]);
    checkProjection(cause, "lipo -detailed_info");
  } finally {
    await rm(root, { recursive: true, force: true });
  }
});
it("retains an explicitly typed producer parse failure as a control", () => {
  const original = new TypeError("producer parse failed");
  const typed = new ArtifactReaderFailure("format", "producer output malformed", { cause: original });
  const translated = translateArtifactFailure("inspect_artifact", typed);
  expect(translated).toMatchObject({ reason: "format", detail: "producer output malformed", cause: typed });
  console.log(JSON.stringify({ control: projectAnalysisError(translated) }));
  expect(projectAnalysisError(translated).details).toMatchObject({ reason: "format", detail: "producer output malformed" });
});

Expected behavior

Classify an unparseable external producer response as a typed artifact-reader failure with a producer-specific diagnostic and the original parse error retained as its cause. Preserve that diagnostic through the shared provider translation. Do not attribute this protocol/format failure to file access.

The repository boundary rule requires typed expected failures and meaningful reasons. The adjacent DMG attach parser already wraps malformed producer output with ArtifactReaderFailure("format", ..., { cause }).

Impact and scope

P2: clients lose the distinction between a producer parse failure and artifact I/O failure, including the diagnostic needed to investigate it. Misleading file-access advice sends recovery toward the wrong boundary. The fixture reproduces deterministically; frequency with real system tool output is unknown. No target mutation, incorrect successful inventory, or security boundary crossing was observed.

The input was built from the repository's Mach-O fixture helpers with x86_64 and arm64 slices. The test injected the capture through the existing command-runner dependency; no real lipo process was started. No end-to-end CLI/MCP execution or restored processing is claimed. A direct producer command could help with manual diagnosis, but that was not tested as a workaround.

REA version

rea-agents 6.7.0; source 52415975942a4b9c53a817915b313ccc404f280e.

Runtime and platform

macOS 27.0.1 arm64; Node.js v26.12.0; repository-locked Vitest 5.0.3. Simulated native command responses; no external analysis engine.

Target and artifact context

The input was built from the repository's Mach-O fixture helpers with x86_64 and arm64 slices. The test injected the capture through the existing command-runner dependency; no real lipo process was started.

Interface

Other

Earliest failure stage

Artifact or evidence registration

Relevant output or evidence

Focused test: 2 passed, including the typed failure control. Selected actual output:

producer: lipo -detailed_info
raw reader failure: TypeError: lipo output contained no architectures
code: artifact_operation_failed
category: execution_failure
details: {"operation":"inspect_artifact","reason":"io"}
message: Artifact could not be read or written. Check file access and try again.
remediation.action: Artifact could not be read or written. Check file access and try again.
translated detail: undefined
translated cause: undefined

The typed control instead preserves details.reason: format, details.detail: producer output malformed, and the original cause chain. Its top-level message remains generic format advice; preserving the diagnostic does not itself establish producer-specific top-level recovery wording.

Typed outcome or recovery expected

An ArtifactReaderFailure at the reader's producer boundary, using the appropriate format or availability reason, naming lipo -detailed_info, and retaining its cause. The projected error should retain a meaningful details.detail so callers can locate the failed boundary. No requirement to accept malformed output or continue with fabricated observations.

Analysis or suspected cause

entries wraps command failures but calls parseLipoOutput without a parse-failure boundary. The parser throws when no architectures were found.

The shared translation retains reason, message, and cause for ArtifactReaderFailure, but maps an untyped exception to a bare ArtifactOperationError(operation, "io"). That explains the observed loss. Type the parse failure where its producer context is known.

Additional context

Fetched origin/main at 25e1a24a3ed3671a0148757555f18d16c29d936a; none of the relevant reader, parser, or translation files changed from the tested baseline. No current-main runtime test is claimed. Last known good version: NOT_ENOUGH_INFO.

Related issues #760, #174, #563, #1168, #1765 and #4280, plus PRs #2605, #1982, #5254, #531 and #3283, concern echoed filenames, redundant parsing, numeric coercion, slice identity or unsupported structural slices. None types this successful-command parse rejection at the artifact-reader boundary.

Originally created by @coygeek on GitHub (Oct 21, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/5982 ### 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 Evidence, artifacts, or reconstruction ### Observed behavior A successful simulated `lipo -detailed_info` capture contains a FAT header but no architecture records. `MachOSliceArtifactReader.entries()` rejects with `TypeError("lipo output contained no architectures")` while reading a synthetic two-slice universal Mach-O. Passing that actual rejection through the production artifact failure translator and error projector produces `artifact_operation_failed`, category `execution_failure`, and `details.reason: io`. The producer diagnostic and original cause disappear. Both the message and remediation instruct the caller to check file access, although this failure concerns interpreting successful tool output. Source tracing reaches this reader through artifact inspection of universal Mach-O files on macOS. Public CLI/MCP behavior was traced in source, not executed in this reproduction. ### Reproduction Executed as a focused production-boundary test using the repository's existing injectable host/runner seams. Save the following under `src/artifacts/producerParse.test.ts` in the baseline checkout with its locked dependencies installed, then run: ```sh node node_modules/vitest/vitest.mjs run src/artifacts/producerParse.test.ts --maxWorkers=1 ``` Both tests pass on the baseline: the first asserts the observed defective result, while the second confirms that an explicitly typed parse failure retains its diagnostic. Valid parser input is also checked before the malformed-response case. Remove the temporary test afterwards. ```ts import { mkdtemp, writeFile, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { build } from "plist"; import { expect, it } from "vitest"; import { NativeDmgArtifactReader, type NativeDmgHost } from "./NativeDmgArtifactReader.js"; import { MachOSliceArtifactReader } from "./MachOSliceArtifactReader.js"; import { ArtifactReaderFailure, type ArtifactReader } from "./ArtifactReader.js"; import { translateArtifactFailure } from "./ArtifactOperationFailure.js"; import { projectAnalysisError } from "../domain/analysisErrorProjection.js"; import { parseNativeDmgInfo } from "./NativeDmgOwnership.js"; import { parseLipoOutput } from "../native/parsers/lipo.js"; import { ok } from "../domain/result.js"; import { CPU, fatImage, machoImage } from "./apple/MachoImage.fixture.js"; import type { NativeCommandRunner } from "../native/CommandRunner.js"; async function failure(reader: ArtifactReader): Promise<unknown> { try { for await (const _entry of reader.entries()) { /* consume */ } throw new Error("Expected iteration to fail"); } catch (cause) { return cause; } finally { await reader.close(); } } function checkProjection(cause: unknown, producer: string) { expect(cause).not.toBeInstanceOf(ArtifactReaderFailure); const translated = translateArtifactFailure("inspect_artifact", cause); expect(translated).toMatchObject({ reason: "io", detail: undefined }); expect(translated.cause).toBeUndefined(); const projected = projectAnalysisError(translated); expect(projected).toMatchObject({ code: "artifact_operation_failed", category: "execution_failure", message: "Artifact could not be read or written. Check file access and try again.", details: { reason: "io" }, }); console.log(JSON.stringify({ producer, rawError: cause instanceof Error ? cause.name : typeof cause, projection: projected })); } it("drops the lipo architecture parse error at the artifact boundary", async () => { expect(parseLipoOutput("Non-fat file: input is architecture: arm64\n").architectures).toHaveLength(1); const root = await mkdtemp(join(process.env.REA_TEST_TMPDIR ?? tmpdir(), "parse-boundary-")); try { const binary = join(root, "input"); await writeFile(binary, fatImage([ { cpu: CPU.x86_64, bytes: machoImage({ cpu: CPU.x86_64 }) }, { cpu: CPU.arm64, bytes: machoImage({ cpu: CPU.arm64 }) }, ])); const calls: string[] = []; const runner: NativeCommandRunner = { run: async (tool, args) => { calls.push(tool); return ok({ tool, executable: "lipo", executableSha256: "1".repeat(64), toolVersion: null, versionReason: "fixture", arguments: args, stdout: "Fat header in: input\n", stderr: "", stdoutBytes: 21, stderrBytes: 0, exitCode: 0, signal: null }); }, }; const cause = await failure(new MachOSliceArtifactReader(binary, {}, runner)); expect(cause).toBeInstanceOf(TypeError); expect(cause).toMatchObject({ message: "lipo output contained no architectures" }); expect(calls).toEqual(["lipo"]); checkProjection(cause, "lipo -detailed_info"); } finally { await rm(root, { recursive: true, force: true }); } }); it("retains an explicitly typed producer parse failure as a control", () => { const original = new TypeError("producer parse failed"); const typed = new ArtifactReaderFailure("format", "producer output malformed", { cause: original }); const translated = translateArtifactFailure("inspect_artifact", typed); expect(translated).toMatchObject({ reason: "format", detail: "producer output malformed", cause: typed }); console.log(JSON.stringify({ control: projectAnalysisError(translated) })); expect(projectAnalysisError(translated).details).toMatchObject({ reason: "format", detail: "producer output malformed" }); }); ``` ### Expected behavior Classify an unparseable external producer response as a typed artifact-reader failure with a producer-specific diagnostic and the original parse error retained as its cause. Preserve that diagnostic through the shared provider translation. Do not attribute this protocol/format failure to file access. The [repository boundary rule](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/AGENTS.md#L25-L31) requires typed expected failures and meaningful reasons. The adjacent [DMG attach parser](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/artifacts/NativeDmgArtifactReader.ts#L433-L442) already wraps malformed producer output with `ArtifactReaderFailure("format", ..., { cause })`. ### Impact and scope P2: clients lose the distinction between a producer parse failure and artifact I/O failure, including the diagnostic needed to investigate it. Misleading file-access advice sends recovery toward the wrong boundary. The fixture reproduces deterministically; frequency with real system tool output is unknown. No target mutation, incorrect successful inventory, or security boundary crossing was observed. The input was built from the repository's Mach-O fixture helpers with x86_64 and arm64 slices. The test injected the capture through the existing command-runner dependency; no real lipo process was started. No end-to-end CLI/MCP execution or restored processing is claimed. A direct producer command could help with manual diagnosis, but that was not tested as a workaround. ### REA version `rea-agents` 6.7.0; source `52415975942a4b9c53a817915b313ccc404f280e`. ### Runtime and platform macOS 27.0.1 arm64; Node.js v26.12.0; repository-locked Vitest 5.0.3. Simulated native command responses; no external analysis engine. ### Target and artifact context The input was built from the repository's Mach-O fixture helpers with x86_64 and arm64 slices. The test injected the capture through the existing command-runner dependency; no real lipo process was started. ### Interface Other ### Earliest failure stage Artifact or evidence registration ### Relevant output or evidence Focused test: 2 passed, including the typed failure control. Selected actual output: ```text producer: lipo -detailed_info raw reader failure: TypeError: lipo output contained no architectures code: artifact_operation_failed category: execution_failure details: {"operation":"inspect_artifact","reason":"io"} message: Artifact could not be read or written. Check file access and try again. remediation.action: Artifact could not be read or written. Check file access and try again. translated detail: undefined translated cause: undefined ``` The typed control instead preserves `details.reason: format`, `details.detail: producer output malformed`, and the original cause chain. Its top-level message remains generic format advice; preserving the diagnostic does not itself establish producer-specific top-level recovery wording. ### Typed outcome or recovery expected An `ArtifactReaderFailure` at the reader's producer boundary, using the appropriate format or availability reason, naming `lipo -detailed_info`, and retaining its cause. The projected error should retain a meaningful `details.detail` so callers can locate the failed boundary. No requirement to accept malformed output or continue with fabricated observations. ### Analysis or suspected cause [`entries`](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/artifacts/MachOSliceArtifactReader.ts#L82-L118) wraps command failures but calls `parseLipoOutput` without a parse-failure boundary. The [parser](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/native/parsers/lipo.ts#L109-L114) throws when no architectures were found. The [shared translation](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/artifacts/ArtifactOperationFailure.ts#L16-L49) retains reason, message, and cause for `ArtifactReaderFailure`, but maps an untyped exception to a bare `ArtifactOperationError(operation, "io")`. That explains the observed loss. Type the parse failure where its producer context is known. ### Additional context Fetched `origin/main` at `25e1a24a3ed3671a0148757555f18d16c29d936a`; none of the relevant reader, parser, or translation files changed from the tested baseline. No current-main runtime test is claimed. Last known good version: NOT_ENOUGH_INFO. Related issues #760, #174, #563, #1168, #1765 and #4280, plus PRs #2605, #1982, #5254, #531 and #3283, concern echoed filenames, redundant parsing, numeric coercion, slice identity or unsupported structural slices. None types this successful-command parse rejection at the artifact-reader boundary.
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#3650
No description provided.