fix(model): retain parsed headers when hashing is interrupted

Share partial observation ownership across GGUF and Safetensors. Preserve completed header facts through cancellation and MCP error delivery without claiming finalized digests or file identity.

Fixes #6684
This commit is contained in:
morluto
2026-10-23 21:11:03 +08:00
parent 02940796f1
commit eed99a588e
5 changed files with 182 additions and 110 deletions
+6
View File
@@ -42,6 +42,12 @@ check descriptor and selected-path identity and metadata before completion;
this detects observed replacement or modification, not adversarial modification
that defeats filesystem metadata checks.
If full-file hashing fails or is cancelled after either format's header has
been validated, partial Evidence retains the parsed metadata and tensor directory.
Header coverage is complete; digests and final file identity remain unknown.
Safetensors header parsing is atomic, so interruption before validation does not
expose unvalidated JSON rows.
## Facts and selected queries
GGUF reports version, byte order, alignment, typed metadata, and each tensor's
+2 -2
View File
@@ -81,7 +81,7 @@ export const modelArtifactObjectSchema = z.strictObject({
limitations: z.array(z.string()),
});
/** Facts proven before an interrupted GGUF read reached final file identity checks. */
/** Facts proven before an interrupted model read reached final file identity checks. */
export const modelArtifactPartialTensorSchema = z.strictObject({
index: count,
name: z.string(),
@@ -106,7 +106,7 @@ export const modelArtifactPartialSchema = z.strictObject({
sha256: z.null(),
file_identity: z.literal("unverified"),
}),
format: z.literal("gguf"),
format: z.enum(["gguf", "safetensors"]),
version: count.nullable(),
byte_order: z.enum(["little", "big"]).nullable(),
alignment: count.nullable(),
+50 -47
View File
@@ -239,7 +239,8 @@ export class ModelArtifactProvider implements ModelArtifactPort {
if (
error.partialObservation === undefined &&
partialProgress !== undefined &&
(partialProgress.metadata.length > 0 ||
(partialProgress.coverage.header === "complete" ||
partialProgress.metadata.length > 0 ||
partialProgress.tensor_declarations.length > 0 ||
partialProgress.tensors.length > 0)
) {
@@ -277,62 +278,57 @@ export class ModelArtifactProvider implements ModelArtifactPort {
const first = await reader.take(4);
if (first.equals(Buffer.from([0x50, 0x4b, 3, 4]))) rejectPickle();
const format = first.equals(Buffer.from("GGUF")) ? "gguf" : "safetensors";
let partial: ModelArtifactPartial | undefined;
if (format === "gguf") {
partial = {
kind: "model-artifact-partial",
artifact: {
path: input.path,
observed_file_size: Number(initial.size),
header_sha256: null,
sha256: null,
file_identity: "unverified",
},
format,
version: null,
byte_order: null,
alignment: null,
declared_metadata_count: null,
declared_tensor_count: null,
metadata: [],
tensor_declarations: [],
tensors: [],
coverage: {
header: "partial",
metadata: "not-started",
tensors: "not-started",
},
limitations: [
"This partial observation contains only rows fully parsed before the interrupted read.",
"Header and full-file digests and final file identity are unknown because finalization did not complete.",
"No model behavior or tensor payload values were interpreted.",
],
};
retainProgress?.(partial);
}
const partial: ModelArtifactPartial = {
kind: "model-artifact-partial",
artifact: {
path: input.path,
observed_file_size: Number(initial.size),
header_sha256: null,
sha256: null,
file_identity: "unverified",
},
format,
version: null,
byte_order: null,
alignment: null,
declared_metadata_count: null,
declared_tensor_count: null,
metadata: [],
tensor_declarations: [],
tensors: [],
coverage: {
header: "partial",
metadata: "not-started",
tensors: "not-started",
},
limitations: [
"This partial observation contains only rows fully parsed before the interrupted read.",
"Header and full-file digests and final file identity are unknown because finalization did not complete.",
"No model behavior or tensor payload values were interpreted.",
],
};
retainProgress?.(partial);
const parsed =
format === "gguf"
? await new GgufHeader(reader).read({
onHeader: (info) => {
if (partial === undefined) return;
partial.version = info.version;
partial.byte_order = info.byte_order;
partial.declared_metadata_count = info.declared_metadata_count;
partial.declared_tensor_count = info.declared_tensor_count;
partial.coverage.metadata = "partial";
},
onMetadata: (row) => partial?.metadata.push(row),
onMetadata: (row) => partial.metadata.push(row),
onAlignment: (alignment) => {
if (partial !== undefined) partial.alignment = alignment;
partial.alignment = alignment;
},
onMetadataComplete: () => {
if (partial !== undefined) partial.coverage.metadata = "complete";
partial.coverage.metadata = "complete";
},
onTensorsStarted: () => {
if (partial !== undefined) partial.coverage.tensors = "partial";
partial.coverage.tensors = "partial";
},
onTensorDeclaration: (row) =>
partial?.tensor_declarations.push(row),
onTensorDeclaration: (row) => partial.tensor_declarations.push(row),
})
: await readSafetensorsHeader(reader, first);
const intervals = parsed.tensors
@@ -344,12 +340,19 @@ export class ModelArtifactProvider implements ModelArtifactPort {
rejectModel(`Overlapping tensor byte ranges at ${row.name}`);
end = Math.max(end, row.file_offset + (row.byte_length ?? 0));
}
if (partial !== undefined && format === "gguf") {
partial.tensor_declarations = [];
partial.tensors = parsed.tensors;
partial.coverage.tensors = "complete";
partial.coverage.header = "complete";
}
partial.version = parsed.version;
partial.byte_order = parsed.byte_order;
partial.alignment = parsed.alignment;
partial.metadata = parsed.metadata;
partial.declared_metadata_count = parsed.metadata.length;
partial.declared_tensor_count = parsed.tensors.length;
partial.tensor_declarations = [];
partial.tensors = parsed.tensors;
partial.coverage = {
header: "complete",
metadata: "complete",
tensors: "complete",
};
const header_length = reader.position;
const hashes = await reader.finish(input.hash === "full");
const after = await file.stat({ bigint: true });
@@ -10,45 +10,67 @@ import { registerModelArtifactTools } from "../../../src/server/registerModelArt
import { createToolResultDelivery } from "../../../src/server/toolResult.js";
import { silentLogger } from "../../../src/logger.js";
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
import { ggufFixture } from "../../fixtures/model/model.js";
import { ggufFixture, safetensorsFixture } from "../../fixtures/model/model.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { mcpTest } from "../../support/mcp/mcpFixture.js";
mcpTest.for([false, true])(
"projects partial model Evidence through an MCP tool error (retained=%s)",
async (retained, { mcp }) => {
const interruptedFixture = (fullHash: boolean) => {
const seed = ggufFixture();
const count = 200_000;
return fullHash
? safetensorsFixture(
JSON.stringify({
__metadata__: { author: "test" },
weight: {
dtype: "F32",
shape: [524288],
data_offsets: [0, 2 * 1024 * 1024],
},
}),
Buffer.alloc(2 * 1024 * 1024),
)
: ggufFixture({
tensors: [],
metadata: [
{ key: "general.name", type: 8, value: seed.text("First row") },
{
key: "custom.bools",
type: 9,
value: Buffer.concat([
seed.u32(7),
seed.u64(BigInt(count)),
Buffer.alloc(count),
]),
},
],
});
};
mcpTest.for([
{ retained: false, fullHash: false },
{ retained: true, fullHash: false },
{ retained: false, fullHash: true },
{ retained: true, fullHash: true },
])(
"projects partial model Evidence through an MCP tool error ($retained, fullHash=$fullHash)",
async ({ retained, fullHash }, { mcp }) => {
const root = await createTestTempDirectory("rea-model-mcp-partial-");
const path = join(root, "model.gguf");
const seed = ggufFixture();
const count = 200_000;
const fixture = ggufFixture({
tensors: [],
metadata: [
{ key: "general.name", type: 8, value: seed.text("First row") },
{
key: "custom.bools",
type: 9,
value: Buffer.concat([
seed.u32(7),
seed.u64(BigInt(count)),
Buffer.alloc(count),
]),
},
],
});
const path = join(root, fullHash ? "model.safetensors" : "model.gguf");
const fixture = interruptedFixture(fullHash);
await writeFile(path, fixture.bytes);
const arrayStart =
24 +
8 +
Buffer.byteLength("general.name") +
4 +
8 +
Buffer.byteLength("First row") +
8 +
Buffer.byteLength("custom.bools") +
4 +
4 +
8;
const arrayStart = fullHash
? fixture.header.length
: 24 +
8 +
Buffer.byteLength("general.name") +
4 +
8 +
Buffer.byteLength("First row") +
8 +
Buffer.byteLength("custom.bools") +
4 +
4 +
8;
const sample = await open(path, "r");
const prototype = Object.getPrototypeOf(sample) as {
read: (
@@ -103,7 +125,11 @@ mcpTest.for([false, true])(
try {
const response = await client.callTool({
name: "inspect_model_artifact",
arguments: { path, detail: "complete" },
arguments: {
path,
detail: "complete",
...(fullHash ? { hash: "full" } : {}),
},
});
if (!observedArrayChunk) throw new Error(JSON.stringify(response));
const projected = parseMcpToolError(response);
@@ -123,15 +149,30 @@ mcpTest.for([false, true])(
} else partial = parseEvidence(details.partial_observation);
expect(partial.normalized_result).toMatchObject({
kind: "model-artifact-partial",
metadata: [
{
index: 0,
key: "general.name",
value: "First row",
source_offset: 24,
source_length: 41,
},
],
...(fullHash
? {
format: "safetensors",
metadata: [
expect.objectContaining({ key: "author", value: "test" }),
],
tensors: [expect.objectContaining({ name: "weight" })],
coverage: {
header: "complete",
metadata: "complete",
tensors: "complete",
},
}
: {
metadata: [
{
index: 0,
key: "general.name",
value: "First row",
source_offset: 24,
source_length: 41,
},
],
}),
artifact: {
path,
header_sha256: null,
@@ -5,7 +5,7 @@ import { expect, it, vi } from "vitest";
import { ModelArtifactProvider } from "../../../src/model/ModelArtifactProvider.js";
import { parseEvidence } from "../../../src/domain/evidence.js";
import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js";
import { ggufFixture } from "../../fixtures/model/model.js";
import { ggufFixture, safetensorsFixture } from "../../fixtures/model/model.js";
const cancelledArrayFixture = (alignment?: number) => {
const seed = ggufFixture();
@@ -229,23 +229,40 @@ it("retains completed GGUF tensor declarations before later tensor bytes cancel"
}
});
it.each([32, 64])(
"retains resolved GGUF alignment %s when full hashing is cancelled",
async (alignment) => {
it.each([
{ format: "gguf", alignment: 32 },
{ format: "gguf", alignment: 64 },
{ format: "safetensors", alignment: null },
])(
"retains parsed $format facts with alignment $alignment when full hashing is cancelled",
async ({ format, alignment }) => {
const root = await mkdtemp(join(tmpdir(), "rea-model-hash-cancel-"));
const path = join(root, "model.gguf");
const fixture = ggufFixture({
...(alignment === 32 ? {} : { alignment }),
tensors: [
{
name: "weight",
type: 0,
shape: [524288n],
offset: 0n,
bytes: 2 * 1024 * 1024,
},
],
});
const path = join(root, `model.${format}`);
const fixture =
format === "safetensors"
? safetensorsFixture(
JSON.stringify({
__metadata__: { author: "test" },
weight: {
dtype: "F32",
shape: [524288],
data_offsets: [0, 2 * 1024 * 1024],
},
}),
Buffer.alloc(2 * 1024 * 1024),
)
: ggufFixture({
...(alignment === 64 ? { alignment } : {}),
tensors: [
{
name: "weight",
type: 0,
shape: [524288n],
offset: 0n,
bytes: 2 * 1024 * 1024,
},
],
});
await writeFile(path, fixture.bytes);
const sample = await open(path, "r");
const prototype = Object.getPrototypeOf(sample) as {
@@ -296,7 +313,12 @@ it.each([32, 64])(
parseEvidence(result.error.partialObservation).normalized_result,
).toMatchObject({
kind: "model-artifact-partial",
format,
alignment,
declared_tensor_count: 1,
tensors: [
expect.objectContaining({ name: "weight", shape: ["524288"] }),
],
coverage: {
header: "complete",
metadata: "complete",