mirror of
https://github.com/morluto/rea.git
synced 2026-11-06 23:06:18 +00:00
* fix(native): report PE input byte budgets as file-size limits PeResourcesProvider translated every ArtifactReaderFailure with reason "limit" into a memory resource constraint, so an image above the caller's max_file_bytes was reported as a memory problem with memory remediation advice. The bound that was exceeded is the input file's byte length, which the caller raises with a flag. Record the constrained bound on the failure the way ElectronFuseReader already does, declare it at the reader's two input-byte refusals and at the parser's metadata and entry budgets, and report that bound instead of assuming memory. Readers that do not declare a boundary are unchanged. Fixes #4192 * fix(native): keep caller-selected PE budget refusals actionable Address review findings on the budget classification: - withCleanup now forwards the constrained bound, so a file that grows past its byte limit while its handle also fails to close is still reported as a file-size refusal rather than a memory one. - Both caller-selected bounds carry their own remediation, pointing at max_file_bytes and max_entries instead of the file-size fallback's worker write-limit advice. - AnalysisResourceConstraintError can declare that retrying cannot succeed. The entry budget is deterministic for the same file and limit, so it no longer inherits admission capacity's retryable behaviour. The projection falls back to the previous resource-kind rule when a caller does not declare retryability, so other capacity refusals are unchanged. * fix(native): preserve budget retryability and name the schema ceiling Address the remaining review findings on the budget classification: - Failure redaction and the caller-copy path rebuilt resource constraint errors without forwarding retryable, so a deterministic capacity refusal projected as retryable again and automated callers could loop on it. Both constructors now forward the declared boolean. - Both remediations recommended raising a bound the caller had already set to the schema maximum. The maxima are now named constants shared with the schema, and the remediation names the operation's hard ceiling when the caller is already at it.
187 lines
6.0 KiB
TypeScript
187 lines
6.0 KiB
TypeScript
import type { AnalysisError } from "./analysisErrorBase.js";
|
|
import {
|
|
AnalysisAccessDeniedError,
|
|
AnalysisArtifactChangedError,
|
|
AnalysisInputError,
|
|
AnalysisOutputError,
|
|
AnalysisCapabilityUnavailableError,
|
|
AnalysisResourceConstraintError,
|
|
AnalysisCancelledError,
|
|
AnalysisTimeoutError,
|
|
} from "./analysisErrorCore.js";
|
|
import { redactExplicitText } from "./explicitSensitiveValues.js";
|
|
import type { JsonValue } from "./jsonValue.js";
|
|
import { ProviderAdapterError } from "./providerAdapterError.js";
|
|
import { ProviderCleanupError } from "./providerCleanupError.js";
|
|
|
|
/** Keep a real JSON-pointer ancestor when a property identity is explicitly excluded. */
|
|
export const redactExplicitPointer = (
|
|
pointer: string,
|
|
values: readonly string[],
|
|
): string => {
|
|
let parent = "";
|
|
for (const segment of pointer.split("/").slice(1)) {
|
|
const decoded = segment.replaceAll("~1", "/").replaceAll("~0", "~");
|
|
const child = `${parent}/${segment}`;
|
|
if (
|
|
values.some(
|
|
(literal) => decoded.includes(literal) || child.includes(literal),
|
|
)
|
|
)
|
|
return parent;
|
|
parent = child;
|
|
}
|
|
return parent;
|
|
};
|
|
|
|
/** Exclude explicit literals from common acquisition/decoder failures, preserving typed diagnostics. */
|
|
export const redactExplicitFailure = (
|
|
error: AnalysisError,
|
|
values: readonly string[],
|
|
): AnalysisError => {
|
|
if (values.length === 0) return error;
|
|
const text = (value: string): string => redactExplicitText(value, values);
|
|
const path = (
|
|
parts: readonly (string | number)[],
|
|
ordinaryNames: boolean,
|
|
): (string | number)[] => {
|
|
const retained: (string | number)[] = [];
|
|
for (const part of parts) {
|
|
if (typeof part !== "string") {
|
|
retained.push(part);
|
|
} else if (!ordinaryNames && part.startsWith("/")) {
|
|
const pointer = redactExplicitPointer(part, values);
|
|
retained.push(pointer);
|
|
if (pointer !== part) break;
|
|
} else {
|
|
if (values.some((literal) => part.includes(literal))) break;
|
|
retained.push(part);
|
|
}
|
|
}
|
|
return retained;
|
|
};
|
|
const diagnostics = (
|
|
value: Readonly<Record<string, JsonValue>>,
|
|
): Record<string, JsonValue> =>
|
|
Object.fromEntries(
|
|
Object.entries(value)
|
|
.filter(([key]) => !values.some((literal) => key.includes(literal)))
|
|
.map(([key, item]) => [key, visit(item)]),
|
|
);
|
|
const visit = (value: JsonValue): JsonValue => {
|
|
if (typeof value === "string") return text(value);
|
|
if (Array.isArray(value)) return value.map(visit);
|
|
if (typeof value === "object" && value !== null) return diagnostics(value);
|
|
return value;
|
|
};
|
|
const context = {
|
|
cause: error,
|
|
...(error.diagnostics === undefined
|
|
? {}
|
|
: { diagnostics: diagnostics(error.diagnostics) }),
|
|
...(error.capturedOutput === undefined
|
|
? {}
|
|
: {
|
|
capturedOutput: {
|
|
...error.capturedOutput,
|
|
stdout: text(error.capturedOutput.stdout),
|
|
stderr: text(error.capturedOutput.stderr),
|
|
},
|
|
}),
|
|
...(error.cleanup === undefined
|
|
? {}
|
|
: {
|
|
cleanup: {
|
|
reason: text(error.cleanup.reason),
|
|
resources: error.cleanup.resources.map(text),
|
|
},
|
|
}),
|
|
};
|
|
if (error instanceof AnalysisCancelledError)
|
|
return new AnalysisCancelledError(error.operation, {
|
|
...context,
|
|
partialObservation: error.partialObservation,
|
|
});
|
|
if (error instanceof AnalysisTimeoutError)
|
|
return new AnalysisTimeoutError(error.operation, error.timeoutMs, {
|
|
...context,
|
|
partialObservation: error.partialObservation,
|
|
});
|
|
if (error instanceof AnalysisAccessDeniedError)
|
|
return new AnalysisAccessDeniedError(
|
|
error.operation,
|
|
text(error.path),
|
|
error.systemCode,
|
|
context,
|
|
);
|
|
if (error instanceof AnalysisArtifactChangedError)
|
|
return new AnalysisArtifactChangedError(
|
|
error.operation,
|
|
text(error.path),
|
|
text(error.reason),
|
|
context,
|
|
);
|
|
if (error instanceof AnalysisInputError)
|
|
return new AnalysisInputError(
|
|
error.operation,
|
|
context,
|
|
error.issues.map((issue) => ({
|
|
...issue,
|
|
// Unknown-argument paths contain literal field names, including names
|
|
// beginning with '/'; they are not producer JSON-pointer coordinates.
|
|
path: path(issue.path, issue.reason === "unknown_argument"),
|
|
...(issue.message === undefined
|
|
? {}
|
|
: { message: text(issue.message) }),
|
|
...(issue.expected === undefined
|
|
? {}
|
|
: { expected: visit(issue.expected) }),
|
|
})),
|
|
);
|
|
if (error instanceof AnalysisOutputError)
|
|
return new AnalysisOutputError(
|
|
error.operation,
|
|
text(error.reason),
|
|
context,
|
|
);
|
|
if (error instanceof AnalysisCapabilityUnavailableError)
|
|
return new AnalysisCapabilityUnavailableError(
|
|
error.providerId,
|
|
error.operation,
|
|
text(error.reason),
|
|
{
|
|
...context,
|
|
...(error.userMessage === undefined
|
|
? {}
|
|
: { userMessage: text(error.userMessage) }),
|
|
...(error.retryable ? { retryable: true } : {}),
|
|
},
|
|
);
|
|
if (error instanceof AnalysisResourceConstraintError)
|
|
return new AnalysisResourceConstraintError(
|
|
error.operation,
|
|
error.resource,
|
|
text(error.reason),
|
|
error.reportedLimits === null ? null : diagnostics(error.reportedLimits),
|
|
{
|
|
...context,
|
|
...(error.remediationAction === undefined
|
|
? {}
|
|
: { remediationAction: text(error.remediationAction) }),
|
|
...(error.retryable === undefined
|
|
? {}
|
|
: { retryable: error.retryable }),
|
|
},
|
|
);
|
|
if (error instanceof ProviderCleanupError)
|
|
return new ProviderCleanupError(
|
|
error.providerId,
|
|
error.cleanupResources.map(text),
|
|
diagnostics(error.diagnostics ?? {}),
|
|
{ operation: error.operation, cause: error },
|
|
);
|
|
if (error instanceof ProviderAdapterError)
|
|
return new ProviderAdapterError(error.providerId, error.operation, context);
|
|
return error;
|
|
};
|