fix(entropy): retain completed file outcomes through cleanup

This commit is contained in:
morluto committed 2026-10-23 00:06:00 +08:00
1 parent 6d977db1b5
commit 5bda0a7dc3
2 files changed
+261 -130

No files matched your search

@@ -16,22 +16,26 @@ import {
import { createEvidence, type Evidence } from "../../domain/evidence.js";
import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js";
import {
byteEntropySchema,
computeBlockEntropy,
entropyFromCounts,
inspectByteEntropyInputSchema,
mergeEntropyRuns,
type ByteEntropy,
} from "../../domain/native/byteEntropy.js";
import { err, ok, type Result } from "../../domain/result.js";
import { err, type Result } from "../../domain/result.js";
import { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js";
import {
withArtifactFileHandle,
bindArtifactFileCleanup,
ArtifactFileCleanupFailure,
ArtifactFileCancelled,
assertArtifactReadActive,
artifactFileCancellationCause,
} from "../../artifacts/ArtifactFileOperation.js";
import { analysisErrorWithCleanupFailure } from "../../domain/analysisErrorCleanup.js";
import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js";
import { completeProviderEvidence } from "../ProviderOperationExecution.js";
/** Bound retained result rows before constructing the Evidence representation. */
export const MAX_BYTE_ENTROPY_BLOCKS = 32_768;
@@ -92,7 +96,9 @@ export class ByteEntropyService {
options?: ExecutionOptions,
): Promise<Result<Evidence, AnalysisError>> {
if (options?.signal?.aborted)
return err(new AnalysisCancelledError(OPERATION));
return err(
new AnalysisCancelledError(OPERATION, { cause: options.signal.reason }),
);
const parsed = inspectByteEntropyInputSchema.safeParse(rawInput);
if (!parsed.success)
return err(
@@ -105,7 +111,62 @@ export class ByteEntropyService {
return err(
invalid("path", "Expected an absolute filesystem path on this host."),
);
const evidenceFromReport = (report: ByteEntropy): Evidence =>
createEvidence(
{ path: input.path, format: "file", sha256: report.artifact.sha256 },
BYTE_ENTROPY_PROVIDER,
{
operation: OPERATION,
parameters: {
path: input.path,
block_size: input.block_size,
offset: input.offset,
...(input.length === undefined ? {} : { length: input.length }),
},
result: report,
rawResult: null,
confidence: "observed",
limitations: report.limitations,
locations: [{ kind: "artifact-path", path: input.path }],
},
);
// Cleanup errors erase the callback's generic type. Validate their completed
// value before publishing it as this operation's observation.
const completedObservation = (value: unknown): Evidence | undefined => {
const report = byteEntropySchema.safeParse(value);
return report.success && report.data.artifact.path === input.path
? evidenceFromReport(report.data)
: undefined;
};
let admitted = false;
const readFailure = (cause: unknown): AnalysisError => {
if (cause instanceof AnalysisError) return cause;
if (cause instanceof Error && "code" in cause) {
if (cause.code === "EACCES" || cause.code === "EPERM")
return new AnalysisAccessDeniedError(
OPERATION,
input.path,
cause.code,
{ cause },
);
if (cause.code === "ENOENT" || cause.code === "ENOTDIR")
return admitted
? new AnalysisArtifactChangedError(
OPERATION,
input.path,
"Selected file path changed during entropy inspection.",
{ cause },
)
: invalid("path", "Selected file could not be found.");
}
return new AnalysisOutputError(
OPERATION,
cause instanceof ArtifactReaderFailure
? cause.message
: "Could not read complete byte entropy evidence.",
{ cause },
);
};
try {
const before = await this.files.stat(input.path, { bigint: true });
if (!before.isFile()) throw invalid("path", "Expected a regular file.");
@@ -172,14 +233,14 @@ export class ByteEntropyService {
const hash = createHash("sha256");
let position = 0;
while (position < size) {
options?.signal?.throwIfAborted();
assertArtifactReadActive(options?.signal);
const selected = position >= input.offset && position < end;
const boundary =
position < input.offset ? input.offset : selected ? end : size;
const wanted = Math.min(input.block_size, boundary - position);
let filled = 0;
while (filled < wanted) {
options?.signal?.throwIfAborted();
assertArtifactReadActive(options?.signal);
const { bytesRead } = await file.read(
buffer,
filled,
@@ -205,7 +266,7 @@ export class ByteEntropyService {
!same(await this.files.stat(input.path, { bigint: true }))
)
throw changed();
options?.signal?.throwIfAborted();
assertArtifactReadActive(options?.signal);
const thresholds = { high: 7.2, low: 1.0 };
return {
artifact: {
@@ -228,70 +289,49 @@ export class ByteEntropyService {
};
},
);
if (options?.signal?.aborted)
return err(new AnalysisCancelledError(OPERATION));
return ok(
createEvidence(
{ path: input.path, format: "file", sha256: report.artifact.sha256 },
BYTE_ENTROPY_PROVIDER,
{
operation: OPERATION,
parameters: {
path: input.path,
block_size: input.block_size,
offset: input.offset,
...(input.length === undefined ? {} : { length: input.length }),
},
result: report,
rawResult: null,
confidence: "observed",
limitations: report.limitations,
locations: [{ kind: "artifact-path", path: input.path }],
},
),
return completeProviderEvidence(
evidenceFromReport(report),
options?.signal,
);
} catch (cause) {
if (cause instanceof ArtifactFileCleanupFailure) {
const outcome = cause.fileCleanupFailure.outcome;
const cleanup = new AnalysisOutputError(
OPERATION,
"Could not confirm closing the selected file.",
{ cause, cleanup: cause.cleanup },
{
cause,
cleanup: cause.cleanup,
...(outcome.kind === "completed"
? { partialObservation: completedObservation(outcome.value) }
: {}),
},
);
bindArtifactFileCleanup(cause, cleanup);
const outcome = cause.fileCleanupFailure.outcome;
if (
outcome.kind === "failed" &&
outcome.cause instanceof ArtifactReaderFailure &&
outcome.cause.reason === "cancelled"
) {
const cancelled = new AnalysisCancelledError(OPERATION, {
cause: outcome.cause.cancellation?.reason,
cause: artifactFileCancellationCause(outcome.cause),
cleanup: cause.cleanup,
}).retainSecondaryFailure(cleanup);
bindArtifactFileCleanup(cause, cancelled);
return err(cancelled);
}
if (options?.signal?.aborted && outcome.kind === "completed") {
const cancellation = cause.fileCleanupFailure.completionCancellation;
if (cancellation !== undefined && outcome.kind === "completed") {
const cancelled = new AnalysisCancelledError(OPERATION, {
cause: options.signal.reason,
cause: cancellation.reason,
partialObservation: cleanup.partialObservation,
cleanup: cause.cleanup,
}).retainSecondaryFailure(cleanup);
bindArtifactFileCleanup(cause, cancelled);
return err(cancelled);
}
if (outcome.kind === "failed") {
const primary =
outcome.cause instanceof AnalysisError
? outcome.cause
: outcome.cause instanceof ArtifactReaderFailure
? new AnalysisOutputError(OPERATION, outcome.cause.message, {
cause: outcome.cause,
})
: new AnalysisOutputError(
OPERATION,
"Could not read complete byte entropy evidence.",
{ cause: outcome.cause },
);
const primary = readFailure(outcome.cause);
return err(
analysisErrorWithCleanupFailure(
primary,
@@ -306,6 +346,14 @@ export class ByteEntropyService {
if (cause.reason === "cancelled") {
const cancelled = new AnalysisCancelledError(OPERATION, {
cause: artifactFileCancellationCause(cause),
...(cause instanceof ArtifactFileCancelled &&
cause.fileCancellation.outcome.kind === "completed"
? {
partialObservation: completedObservation(
cause.fileCancellation.outcome.value,
),
}
: {}),
});
bindArtifactFileCleanup(cause, cancelled);
return err(cancelled);
@@ -321,35 +369,7 @@ export class ByteEntropyService {
bindArtifactFileCleanup(cause, retry);
return err(retry);
}
if (options?.signal?.aborted)
return err(new AnalysisCancelledError(OPERATION, { cause }));
if (cause instanceof AnalysisError) return err(cause);
if (cause instanceof Error && "code" in cause) {
if (cause.code === "EACCES" || cause.code === "EPERM")
return err(
new AnalysisAccessDeniedError(OPERATION, input.path, cause.code, {
cause,
}),
);
if (cause.code === "ENOENT" || cause.code === "ENOTDIR")
return err(
admitted
? new AnalysisArtifactChangedError(
OPERATION,
input.path,
"Selected file path changed during entropy inspection.",
{ cause },
)
: invalid("path", "Selected file could not be found."),
);
}
return err(
new AnalysisOutputError(
OPERATION,
"Could not read complete byte entropy evidence.",
{ cause },
),
);
return err(readFailure(cause));
}
}
}