fix(artifacts): bind session inventories to target identity

This commit is contained in:
Codex
2026-10-14 02:01:13 +08:00
parent 781827b7b4
commit 346275525e
10 changed files with 70 additions and 45 deletions
+7 -1
View File
@@ -188,7 +188,10 @@ class ArtifactClient implements AnalysisClient {
const result = await extractArtifact(
{
inputPath: this.target.sourcePath ?? this.target.path,
expectedRootSha256: this.target.sha256,
...(this.target.sourcePath === undefined ||
this.target.sourcePath === this.target.path
? { expectedRootSha256: this.target.sha256 }
: {}),
outputRoot: parsed.output_root,
environment: this.environment,
integrityPolicy: parsed.integrity_policy,
@@ -391,6 +394,9 @@ class ArtifactClient implements AnalysisClient {
environment: this.environment,
...(options?.signal === undefined ? {} : { signal: options.signal }),
integrity: { mode: parsed.integrity_policy },
...(this.target.kind === "archive" && this.target.format === "ines"
? { expectedRootSha256: this.target.sha256 }
: {}),
});
}
}
+11 -1
View File
@@ -5,6 +5,7 @@ import {
type ArtifactInventoryOutput,
} from "../../domain/artifactGraph.js";
import { abortIfNeeded } from "../ArtifactHash.js";
import { ArtifactReaderFailure } from "../ArtifactReader.js";
import { scanCanonicalArtifactInventoryInScope } from "./scanCanonical.js";
import type { ArtifactInventoryOptions } from "./types.js";
import type { ArtifactInventorySnapshot } from "../../domain/artifactInventorySnapshot.js";
@@ -33,5 +34,14 @@ export const scanArtifactInventoryInScope = async (
): Promise<ArtifactInventorySnapshot> => {
abortIfNeeded(options.signal);
const path = await realpath(inputPath);
return scanCanonicalArtifactInventoryInScope(path, options);
const snapshot = await scanCanonicalArtifactInventoryInScope(path, options);
if (
options.expectedRootSha256 !== undefined &&
snapshot.manifest.root_sha256 !== options.expectedRootSha256
)
throw new ArtifactReaderFailure(
"integrity",
`Active artifact digest changed: expected ${options.expectedRootSha256}, observed ${snapshot.manifest.root_sha256}`,
);
return snapshot;
};
+2
View File
@@ -13,6 +13,8 @@ export const STRICT_INTEGRITY_POLICY: ArtifactIntegrityPolicy = {
/** Options shared by artifact inventory scans. */
export interface ArtifactInventoryOptions {
readonly resourceScope: ArtifactResourceScope;
/** Bind a session-owned leaf inventory to the admitted root digest. */
readonly expectedRootSha256?: string | undefined;
readonly signal?: AbortSignal | undefined;
readonly integrity?: ArtifactIntegrityPolicy | undefined;
readonly environment?: Readonly<NodeJS.ProcessEnv> | undefined;
+4 -21
View File
@@ -23,7 +23,7 @@ import { toolContract } from "../../src/contracts/toolContracts.js";
import { buildBinary } from "plist";
cliTest(
"extracts replacement iNES bytes from an already open plist through MCP",
"refuses extraction after an already open plist is replaced through MCP",
async () => {
const root = await createTestTempDirectory("rea-ines-plist-refresh-");
const path = join(root, "source.bin");
@@ -40,27 +40,10 @@ cliTest(
const bytes = inesFixture();
await writeFile(path, bytes);
const extracted = await call("extract_artifact", {});
expect(
extracted.isError,
JSON.stringify(extracted.structuredContent),
).not.toBe(true);
const result = artifactExtractionResultSchema.parse(
parseEvidence(extracted.structuredContent).normalized_result,
expect(extracted.isError).toBe(true);
expect(JSON.stringify(parseMcpToolError(extracted))).toContain(
"Active artifact digest changed",
);
try {
expect(result.manifest.root_format).toBe("ines");
expect(
Buffer.concat(
await Promise.all(
["header.bin", "prg/000.bin", "prg/001.bin", "chr/000.bin"].map(
(part) => readFile(join(result.output_root, part)),
),
),
),
).toEqual(bytes);
} finally {
await rm(result.output_root, { recursive: true, force: true });
}
await call("close_binary", {});
},
);
@@ -10,6 +10,7 @@ import { AsarArtifactReader } from "../../../../src/artifacts/AsarArtifactReader
import { SafeOutputTree } from "../../../../src/artifacts/SafeOutputTree.js";
import { ProviderCleanupError } from "../../../../src/domain/providerCleanupError.js";
import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js";
import { artifactGraphRootSha256 } from "../../../fixtures/artifactGraphRootSha256.js";
const deferred = () => {
let resolve: () => void = () => {};
@@ -29,11 +30,11 @@ const createArchive = async () => {
return { root, archive };
};
const createClient = (path: string) =>
const createClient = async (path: string) =>
new ArtifactProvider(process.env).createClient({
path,
sourcePath: path,
sha256: "0".repeat(64),
sha256: await artifactGraphRootSha256(path),
kind: "archive",
format: "asar",
});
@@ -41,7 +42,7 @@ const createClient = (path: string) =>
it("retains a failed reader close, blocks new work, and retries the same reader", async () => {
const { root, archive } = await createArchive();
const output = join(root, "output");
const client = createClient(archive);
const client = await createClient(archive);
const originalClose = AsarArtifactReader.prototype.close;
const closeOwners: AsarArtifactReader[] = [];
let readerCloseBlocked = true;
@@ -107,7 +108,7 @@ it("retains a failed reader close, blocks new work, and retries the same reader"
it("retains a failed rollback owner and preserves the materialization failure", async () => {
const { root, archive } = await createArchive();
const output = join(root, "output");
const client = createClient(archive);
const client = await createClient(archive);
const originalWrite = SafeOutputTree.prototype.write;
const originalRollback = SafeOutputTree.prototype.rollback;
const rollbackOwners: SafeOutputTree[] = [];
@@ -174,7 +175,7 @@ it.skipIf(process.platform === "win32")(
const { root, archive } = await createArchive();
const output = join(root, "output");
const blocker = join(output, "unowned.txt");
const client = createClient(archive);
const client = await createClient(archive);
const prototypeHandle = await open(join(root, "prototype-handle"), "w");
const handlePrototype = Object.getPrototypeOf(prototypeHandle) as Pick<
typeof prototypeHandle,
@@ -215,7 +216,7 @@ it.skipIf(process.platform === "win32")(
it("waits for admitted extraction before close and rejects later work", async () => {
const { root, archive } = await createArchive();
const output = join(root, "output");
const client = createClient(archive);
const client = await createClient(archive);
const originalWrite = SafeOutputTree.prototype.write;
const started = deferred();
const finish = deferred();
@@ -4,6 +4,7 @@ import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js";
import { artifactGraphRootSha256 } from "../../../fixtures/artifactGraphRootSha256.js";
import { ArtifactProvider } from "../../../../src/artifacts/ArtifactProvider.js";
import { artifactExtractionExecutionSchema } from "../../../../src/contracts/artifactToolContracts.js";
@@ -21,7 +22,7 @@ describe("artifact extraction", () => {
await writeFile(join(source, "payload.txt"), "committed\n");
const output = join(root, "output");
const client = new ArtifactProvider(process.env).createClient(
target(source, "directory"),
await target(source, "directory"),
);
const result = await client.execute("extract_artifact", {
@@ -40,7 +41,7 @@ describe("artifact extraction", () => {
await mkdir(join(source, "assets"), { recursive: true });
await writeFile(join(source, "assets", "selected.js"), "selected();\n");
await writeFile(join(source, "assets", "ignored.js"), "ignored();\n");
const targetValue = target(source, "directory");
const targetValue = await target(source, "directory");
const graph = await inventory(targetValue);
expect(
graph.occurrences.some(
@@ -136,13 +137,13 @@ const inventory = async (targetValue: BinaryTarget) => {
return artifactInventoryResultSchema.parse(result.value.result);
};
const target = (
const target = async (
path: string,
format: Extract<BinaryTarget, { kind: "archive" }>["format"] | "directory",
): BinaryTarget => ({
): Promise<BinaryTarget> => ({
path,
sourcePath: path,
sha256: "0".repeat(64),
sha256: await artifactGraphRootSha256(path),
kind: "archive",
format: format === "directory" ? "asar" : format,
});
@@ -13,6 +13,7 @@ import {
import { artifactExtractionResultSchema } from "../../../../src/domain/artifactGraph.js";
import { projectAnalysisError } from "../../../../src/domain/analysisErrorProjection.js";
import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js";
import { artifactGraphRootSha256 } from "../../../fixtures/artifactGraphRootSha256.js";
import { DirectoryArtifactReader } from "../../../../src/artifacts/DirectoryArtifactReader.js";
it.each(["file", "directory"] as const)(
@@ -180,12 +181,12 @@ const inventoriedExtraction = async (source: string, output: string) => {
);
};
const extract = (source: string, output: string) =>
const extract = async (source: string, output: string) =>
new ArtifactProvider(process.env)
.createClient({
path: source,
sourcePath: source,
sha256: "0".repeat(64),
sha256: await artifactGraphRootSha256(source),
kind: "archive",
format: "asar",
})
@@ -7,6 +7,7 @@ import { buildBinary } from "plist";
import { writeOrderedZip } from "../../../fixtures/artifactEntryOrder.js";
import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js";
import { artifactGraphRootSha256 } from "../../../fixtures/artifactGraphRootSha256.js";
import { parseBinaryTarget } from "../../../../src/application/BinaryTargetResolver.js";
import { inesFixture } from "../../../fixtures/ines.js";
@@ -122,9 +123,9 @@ describe("artifact extraction format support", () => {
const alias = join(root, "Alias.dmg");
await symlink(image, alias);
const output = join(root, "output");
const extract = (signal?: AbortSignal) =>
const extract = async (signal?: AbortSignal) =>
new ArtifactProvider(process.env)
.createClient(archiveTarget(alias, "dmg"))
.createClient(await archiveTarget(alias, "dmg"))
.execute(
"extract_artifact",
artifactExtractionExecutionSchema.parse({ output_root: output }),
@@ -158,7 +159,7 @@ describe("artifact extraction format support", () => {
);
await writeOrderedZip(archive, ["payload.txt"]);
const result = await new ArtifactProvider(process.env)
.createClient(archiveTarget(archive, format))
.createClient(await archiveTarget(archive, format))
.execute(
"extract_artifact",
artifactExtractionExecutionSchema.parse({
@@ -176,13 +177,13 @@ describe("artifact extraction format support", () => {
);
});
const archiveTarget = (
const archiveTarget = async (
path: string,
format: Extract<BinaryTarget, { kind: "archive" }>["format"],
): BinaryTarget => ({
): Promise<BinaryTarget> => ({
path,
sourcePath: path,
sha256: "0".repeat(64),
sha256: await artifactGraphRootSha256(path),
kind: "archive",
format,
});
@@ -4,6 +4,7 @@ import { describe, expect, it } from "vitest";
import { writeOrderedZip } from "../../../fixtures/artifactEntryOrder.js";
import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js";
import { artifactGraphRootSha256 } from "../../../fixtures/artifactGraphRootSha256.js";
import { ArtifactProvider } from "../../../../src/artifacts/ArtifactProvider.js";
import { artifactExtractionExecutionSchema } from "../../../../src/contracts/artifactToolContracts.js";
@@ -16,7 +17,7 @@ describe("artifact extraction identity", () => {
const source = join(root, "source.zip");
await writeOrderedZip(source, ["ä.js", "a.js", "Z.js"]);
const result = await new ArtifactProvider(process.env)
.createClient(archiveTarget(source))
.createClient(await archiveTarget(source))
.execute(
"extract_artifact",
artifactExtractionExecutionSchema.parse({
@@ -34,10 +35,10 @@ describe("artifact extraction identity", () => {
});
});
const archiveTarget = (path: string): BinaryTarget => ({
const archiveTarget = async (path: string): Promise<BinaryTarget> => ({
path,
sourcePath: path,
sha256: "0".repeat(64),
sha256: await artifactGraphRootSha256(path),
kind: "archive",
format: "zip",
});
+19
View File
@@ -0,0 +1,19 @@
import { ArtifactResourceScope } from "../../src/artifacts/ArtifactResourceScope.js";
import { scanArtifactInventory } from "../../src/artifacts/inventory/ArtifactInventory.js";
/** Compute the root identity used by artifact-provider extraction fixtures. */
export const artifactGraphRootSha256 = async (
path: string,
): Promise<string> => {
const resources = new ArtifactResourceScope();
try {
const snapshot = await scanArtifactInventory(path, {
resourceScope: resources,
environment: process.env,
integrity: { mode: "fail" },
});
return snapshot.manifest.root_sha256;
} finally {
await resources.close();
}
};