[GH-ISSUE #5979] [Bug] Malformed hdiutil inventory output loses its parse diagnostic #3648

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

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 hdiutil verify is followed by a successful simulated hdiutil info -plist response whose images value is a string rather than an array. NativeDmgArtifactReader.entries() rejects with a raw ZodError before any attach command.

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 inspect-artifact / MCP inspect_artifact on macOS. DMG extraction is intentionally unsupported and is outside this report.

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 hdiutil info schema error at the artifact boundary", async () => {
  expect(parseNativeDmgInfo(build({ images: [] })).images).toEqual([]);
  const calls: string[] = [];
  const host: NativeDmgHost = {
    run: async (args) => {
      calls.push(args[0]!);
      if (args[0] === "verify") return { stdout: "", exitCode: 0 };
      if (args[0] === "info") return { stdout: build({ images: "unexpected" }), exitCode: 0 };
      throw new Error("Unexpected command");
    },
  };
  const cause = await failure(new NativeDmgArtifactReader("image.dmg", {}, host));
  expect(cause).toMatchObject({ name: "ZodError" });
  expect(calls).toEqual(["verify", "info"]);
  checkProjection(cause, "hdiutil info -plist");
});
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 failure occurs before attachment, after the simulated verify success. No real DMG or naturally malformed system inventory was tested, and no mount was created. 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 failure occurs before attachment, after the simulated verify success. No real DMG or naturally malformed system inventory was tested, and no mount was created.

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: hdiutil info -plist
raw reader failure: ZodError
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 hdiutil info -plist, 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

#readInfo calls the plist/schema parser without wrapping exceptions. The baseline call precedes the attach try block. The same helper also serves ownership discovery, but that later path was not exercised.

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 reports #5546, #5549, #5547 and #5687 concern busy images, checksum recovery, extraction guidance and raw-image admission. PR #987 preserves failed-command output, and PR #1772 handles localized verification diagnostics; none types successful inventory-output parsing.

Originally created by @coygeek on GitHub (Oct 21, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/5979 ### 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 `hdiutil verify` is followed by a successful simulated `hdiutil info -plist` response whose `images` value is a string rather than an array. `NativeDmgArtifactReader.entries()` rejects with a raw `ZodError` before any attach command. 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 `inspect-artifact` / MCP `inspect_artifact` on macOS. DMG extraction is intentionally unsupported and is outside this report. ### 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 hdiutil info schema error at the artifact boundary", async () => { expect(parseNativeDmgInfo(build({ images: [] })).images).toEqual([]); const calls: string[] = []; const host: NativeDmgHost = { run: async (args) => { calls.push(args[0]!); if (args[0] === "verify") return { stdout: "", exitCode: 0 }; if (args[0] === "info") return { stdout: build({ images: "unexpected" }), exitCode: 0 }; throw new Error("Unexpected command"); }, }; const cause = await failure(new NativeDmgArtifactReader("image.dmg", {}, host)); expect(cause).toMatchObject({ name: "ZodError" }); expect(calls).toEqual(["verify", "info"]); checkProjection(cause, "hdiutil info -plist"); }); 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 failure occurs before attachment, after the simulated verify success. No real DMG or naturally malformed system inventory was tested, and no mount was created. 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 failure occurs before attachment, after the simulated verify success. No real DMG or naturally malformed system inventory was tested, and no mount was created. ### 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: hdiutil info -plist raw reader failure: ZodError 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 `hdiutil info -plist`, 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 [`#readInfo`](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/artifacts/NativeDmgArtifactReader.ts#L535-L540) calls the plist/schema parser without wrapping exceptions. The [baseline call](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/artifacts/NativeDmgArtifactReader.ts#L406-L416) precedes the attach try block. The same helper also serves ownership discovery, but that later path was not exercised. 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 reports #5546, #5549, #5547 and #5687 concern busy images, checksum recovery, extraction guidance and raw-image admission. PR #987 preserves failed-command output, and PR #1772 handles localized verification diagnostics; none types successful inventory-output parsing.
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#3648
No description provided.