diff --git a/src/application/EvidenceBundleCommands.ts b/src/application/EvidenceBundleCommands.ts index a72d17cb5..1255ea68f 100644 --- a/src/application/EvidenceBundleCommands.ts +++ b/src/application/EvidenceBundleCommands.ts @@ -11,6 +11,7 @@ import { compareBundles } from "../domain/bundleComparison.js"; import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js"; import { readEvidenceBundle, + readImmutableEvidenceBundle, writeEvidenceBundle, } from "./EvidenceBundleFiles.js"; @@ -33,7 +34,8 @@ export const exportEvidenceBundleCommand = async ( outputPath: string, overwrite: boolean, ): Promise> => { - const loaded = await readEvidenceBundle(sourcePath); + // The sealed snapshot lets the writer reuse this authentication. + const loaded = await readImmutableEvidenceBundle(sourcePath); if (!loaded.ok) return loaded; const resources = new ArtifactResourceScope(); const written = await writeEvidenceBundle( @@ -69,9 +71,11 @@ export const compareEvidenceBundlesCommand = async (input: { readonly leftPath: string; readonly rightPath: string; }): Promise> => { + // Sealed snapshots let comparison and its bundle digests reuse this + // authentication instead of re-validating every record twice more. const [left, right] = await Promise.all([ - readEvidenceBundle(input.leftPath), - readEvidenceBundle(input.rightPath), + readImmutableEvidenceBundle(input.leftPath), + readImmutableEvidenceBundle(input.rightPath), ]); if (!left.ok) return left; if (!right.ok) return right; diff --git a/src/process/ProviderDeadline.test.ts b/src/process/ProviderDeadline.test.ts index b43ce6654..0a9448150 100644 --- a/src/process/ProviderDeadline.test.ts +++ b/src/process/ProviderDeadline.test.ts @@ -33,3 +33,20 @@ it("aborts a bounded wait when its real monotonic deadline expires", async () => deadline.dispose(); } }); + +it("reports no remaining time once its timer fires before the monotonic deadline", () => { + vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout"] }); + const now = vi.spyOn(performance, "now").mockReturnValue(1_000); + const deadline = new ProviderStartupDeadline(40); + try { + // Timers may fire while the monotonic clock is still just short of the deadline. + now.mockReturnValue(1_039.6); + vi.advanceTimersByTime(40); + expect(deadline.signal.aborted).toBe(true); + expect(deadline.interruption).toBe("timeout"); + expect(deadline.remainingMs()).toBe(0); + } finally { + deadline.dispose(); + vi.useRealTimers(); + } +}); diff --git a/src/process/ProviderDeadline.ts b/src/process/ProviderDeadline.ts index 33cb3a8d0..a6f10779a 100644 --- a/src/process/ProviderDeadline.ts +++ b/src/process/ProviderDeadline.ts @@ -85,6 +85,9 @@ export class ProviderStartupDeadline { /** Milliseconds still available to the next startup phase. */ remainingMs(): number { + // Node timers can fire a fraction of a millisecond before the monotonic + // deadline, so an elapsed timer must not report time left to spend. + if (this.#interruption === "timeout") return 0; return Math.max(0, Math.ceil(this.#deadlineAt - performance.now())); } diff --git a/src/server/registerBundleComparisonTool.ts b/src/server/registerBundleComparisonTool.ts index 75ef2a618..3dbc92298 100644 --- a/src/server/registerBundleComparisonTool.ts +++ b/src/server/registerBundleComparisonTool.ts @@ -1,7 +1,7 @@ import type { EvidenceMcpServer } from "./EvidenceMcpServer.js"; import type { BinarySessionPort } from "../application/binary/BinarySessionPort.js"; -import { readEvidenceBundle } from "../application/EvidenceBundleFiles.js"; +import { readImmutableEvidenceBundle } from "../application/EvidenceBundleFiles.js"; import { toolContract } from "../contracts/toolContracts.js"; import { compareBundles } from "../domain/bundleComparison.js"; import { createEvidence } from "../domain/evidence.js"; @@ -33,12 +33,12 @@ export const registerBundleComparisonTool = ( context.mcpReq.signal, async () => { const [left, right] = await Promise.all([ - readEvidenceBundle( + readImmutableEvidenceBundle( input.left_bundle_path, context.mcpReq.signal, resources, ), - readEvidenceBundle( + readImmutableEvidenceBundle( input.right_bundle_path, context.mcpReq.signal, resources,