mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
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:
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user