From 22958558eafdfccf71a7d5bbbcf360574a6b2e3c Mon Sep 17 00:00:00 2001 From: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com> Date: Fri, 9 Oct 2026 08:40:45 +0000 Subject: [PATCH] fix(artifacts): name only host-available workflows for non-archive plists inspect_keyed_archive runs on every host, but its remediation for an ordinary property list pointed to inspect_plist, which only macOS supports. On Linux that sent callers to a workflow the capability layer rejects. The domain error now names what the file is. The artifact reader picks the remediation from the provider's host: inspect_plist on macOS, and elsewhere the selection of an NSKeyedArchiver archive, with a note that inspect_plist requires macOS. decode_interface_builder, which is portable, stays the NIBArchive remediation. Co-Authored-By: Claude Opus 5.5 --- src/artifacts/ArtifactProvider.ts | 10 ++++-- src/artifacts/apple/KeyedArchiveReader.ts | 27 ++++++++++++++-- src/domain/apple/keyedArchive.ts | 6 ++-- .../filesystem/keyedArchiveSelection.test.ts | 31 +++++++++++++++++++ 4 files changed, 67 insertions(+), 7 deletions(-) diff --git a/src/artifacts/ArtifactProvider.ts b/src/artifacts/ArtifactProvider.ts index a711f266bf..11c14d6934 100644 --- a/src/artifacts/ArtifactProvider.ts +++ b/src/artifacts/ArtifactProvider.ts @@ -43,9 +43,11 @@ import { resolveArtifactIntegrityPolicy } from "./inventory/policy.js"; /** Read-only inventory and exclusively owned extraction provider. */ export class ArtifactProvider implements AnalysisProvider { readonly #capabilities: readonly CapabilityDescriptor[]; + readonly #platform: NodeJS.Platform; constructor(platform: NodeJS.Platform = process.platform) { this.#capabilities = artifactCapabilities(platform); + this.#platform = platform; } identity(): ProviderIdentity { @@ -57,12 +59,15 @@ export class ArtifactProvider implements AnalysisProvider { } createClient(target: BinaryTarget): AnalysisClient { - return new ArtifactClient(target); + return new ArtifactClient(target, this.#platform); } } class ArtifactClient implements AnalysisClient { - constructor(private readonly target: BinaryTarget) {} + constructor( + private readonly target: BinaryTarget, + private readonly platform: NodeJS.Platform, + ) {} async execute( operation: AnalysisOperation, @@ -146,6 +151,7 @@ class ArtifactClient implements AnalysisClient { parameters: standalone ? { ...parameters, path: basename(this.target.path) } : parameters, + platform: this.platform, ...(options?.signal === undefined ? {} : { signal: options.signal }), }); if (standalone && result.archive_sha256 !== this.target.sha256) diff --git a/src/artifacts/apple/KeyedArchiveReader.ts b/src/artifacts/apple/KeyedArchiveReader.ts index ddc8ea51c6..6d4af226d5 100644 --- a/src/artifacts/apple/KeyedArchiveReader.ts +++ b/src/artifacts/apple/KeyedArchiveReader.ts @@ -40,7 +40,7 @@ export const decodeKeyedArchiveBytes = ( if (bytes.subarray(0, 10).toString("ascii") === "NIBArchive") throw new KeyedArchiveKindError( "Selected file is a compiled NIBArchive, not a Foundation plist archive; decode it with decode_interface_builder", - "Decode compiled NIBArchive files with decode_interface_builder on the app bundle.", + "nib-archive", ); const binary = bytes.subarray(0, 8).toString("ascii") === "bplist00"; const xmlText = binary ? undefined : decodeXmlPlistText(bytes); @@ -87,12 +87,29 @@ const normalizePlist = (value: unknown): JsonValue => { return projected.value; }; +/** + * Point to the workflow that reads the selected kind of file on this host. + * decode_interface_builder is portable; inspect_plist requires macOS. + */ +const keyedArchiveKindRemediation = ( + kind: KeyedArchiveKindError["kind"], + platform: NodeJS.Platform, +): string => { + if (kind === "nib-archive") + return "Decode compiled NIBArchive files with decode_interface_builder on the app bundle."; + return platform === "darwin" + ? "Inspect an ordinary property list with inspect_plist; inspect_keyed_archive decodes NSKeyedArchiver archives only." + : "Select an NSKeyedArchiver archive; inspect_keyed_archive decodes only those. inspect_plist, which reads ordinary property lists, requires a macOS host."; +}; + /** Read exactly one regular, contained bundle entry without following symlinks. */ export const inspectBundleKeyedArchive = async (input: { bundlePath: string; targetSha256: string; parameters: unknown; signal?: AbortSignal; + /** Host whose available workflows the remediation names. */ + platform?: NodeJS.Platform; }) => { const selected = keyedArchiveInputSchema.parse(input.parameters); if ( @@ -159,7 +176,13 @@ export const inspectBundleKeyedArchive = async (input: { "inspect_keyed_archive", join(input.bundlePath, entry.path), cause.message, - { cause, remediationAction: cause.remediationAction }, + { + cause, + remediationAction: keyedArchiveKindRemediation( + cause.kind, + input.platform ?? process.platform, + ), + }, ); throw new ArtifactReaderFailure( cause instanceof RangeError ? "limit" : "format", diff --git a/src/domain/apple/keyedArchive.ts b/src/domain/apple/keyedArchive.ts index 8cd34cab01..52a52a1073 100644 --- a/src/domain/apple/keyedArchive.ts +++ b/src/domain/apple/keyedArchive.ts @@ -11,8 +11,8 @@ export class KeyedArchiveKindError extends TypeError { constructor( message: string, - /** The workflow that reads this kind of file. */ - readonly remediationAction: string, + /** What the selected file is instead. */ + readonly kind: "nib-archive" | "property-list", ) { super(message); } @@ -103,7 +103,7 @@ export const projectKeyedArchive = ( if (archive?.$archiver !== "NSKeyedArchiver") throw new KeyedArchiveKindError( `Selected property list is not an NSKeyedArchiver archive: ${archiverMismatch(archive)}`, - "Inspect an ordinary property list with inspect_plist; inspect_keyed_archive decodes NSKeyedArchiver archives only.", + "property-list", ); if (!Array.isArray(archive.$objects)) throw new TypeError("Expected NSKeyedArchiver with an $objects array"); diff --git a/tests/boundary/filesystem/keyedArchiveSelection.test.ts b/tests/boundary/filesystem/keyedArchiveSelection.test.ts index 7bc50b29c1..6f60133759 100644 --- a/tests/boundary/filesystem/keyedArchiveSelection.test.ts +++ b/tests/boundary/filesystem/keyedArchiveSelection.test.ts @@ -4,6 +4,7 @@ import { join } from "node:path"; import { build } from "plist"; import { expect, it } from "vitest"; +import { ArtifactProvider } from "../../../src/artifacts/ArtifactProvider.js"; import { ArtifactReaderFailure } from "../../../src/artifacts/ArtifactReader.js"; import { inspectBundleKeyedArchive } from "../../../src/artifacts/apple/KeyedArchiveReader.js"; import { AnalysisError } from "../../../src/domain/analysisErrorBase.js"; @@ -36,6 +37,7 @@ const inspect = (bundlePath: string, parameters: Record) => bundlePath, targetSha256: "a".repeat(64), parameters, + platform: "darwin", }); it.each([ @@ -120,6 +122,35 @@ it.each([ }, ); +it.each([ + ["darwin", "Inspect an ordinary property list with inspect_plist"], + [ + "linux", + "inspect_plist, which reads ordinary property lists, requires a macOS host", + ], +] as const)( + "names the property-list workflow available on a %s host", + async (platform, action) => { + const root = await createTestTempDirectory("rea-keyed-kind-"); + const path = join(root, "Defaults.plist"); + await writeFile(path, Buffer.from(build({ plain: true }))); + const result = await new ArtifactProvider(platform) + .createClient({ + path, + sha256: "0".repeat(64), + kind: "artifact", + format: "plist", + }) + .execute("inspect_keyed_archive", {}); + if (result.ok) throw new Error("Expected an unsupported target"); + expect(projectAnalysisError(result.error)).toMatchObject({ + code: "unsupported_target", + remediation: { action: expect.stringContaining(action) }, + details: { path }, + }); + }, +); + it("keeps a damaged NSKeyedArchiver archive a format failure", async () => { const root = await bundle(); await writeFile(