Files
reverse-engineering-anything/src/domain/explicitSensitiveFailure.ts
T
吴佳浩Albenand吴佳浩 d73ed6fce2 fix(domain): preserve retryable capability failures through redaction (#2494)
redactExplicitFailure rebuilds each typed failure field by field so explicit
literals are removed while the projected code stays the same. The rebuild for
AnalysisCapabilityUnavailableError carried providerId, operation, reason and
userMessage but not retryable, and the constructor defaults that field to false.
Redacting a transient capability failure therefore made it permanent and changed
the remediation shown to the caller.

Pass the field through with the same conditional-spread shape used for
userMessage. The constructor recomputes retryable from cleanupIncomplete, and
cleanup is already part of the reconstructed context, so a value that was true
before redaction stays true and a false value stays absent.

Verified with 66 error projection, access-denied, redaction and capture-failure
tests, check:fast and verify:test-discovery on macOS arm64. The regression test
failed with "expected false to be true" before the change.

Co-authored-by: 吴佳浩 <wujiahao@supremind.com>
2026-10-13 14:43:10 +08:00

184 lines
5.9 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) }),
},
);
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;
};