fix(artifacts): bind session inventories to target identity
This commit is contained in:
@@ -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 }
|
||||
: {}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user