mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-15 20:53:23 +02:00
Preserve bounded error codes and HTTP/exit statuses across the environmentSyncOut worker RPC boundary, and revalidate that method-scoped envelope before attaching host restore diagnostics. Keep the original error and all recovery policy unchanged; do not transmit provider payloads or credentials. Verified real RPC roundtrip and privacy regressions, 95 focused tests, 68 independent tests, full typecheck/build and all exact-head CI. Greptile 5/5 with no unresolved comments. Co-Authored-By: Paperclip <noreply@paperclip.ing>
59 lines
3.0 KiB
TypeScript
59 lines
3.0 KiB
TypeScript
/** Diagnostic evidence only. Never use this envelope to authorize cleanup or retry. */
|
|
export interface PluginEnvironmentSyncErrorDiagnostic {
|
|
errorCode: string;
|
|
httpStatus?: number;
|
|
exitCode?: number;
|
|
}
|
|
|
|
const SCHEMA = "paperclip/environment-sync-error/v1";
|
|
// Match the host's workspace restore diagnostic allowlist. Both sides validate
|
|
// independently: plugin-supplied data must never widen the host's log surface.
|
|
const ERROR_CODES = new Set([
|
|
"ENOENT", "EACCES", "EPERM", "ENOSPC", "EIO", "EXDEV", "ENOTDIR", "EISDIR",
|
|
"ECONNRESET", "ECONNREFUSED", "ETIMEDOUT", "EPIPE", "ENOTFOUND", "EAI_AGAIN",
|
|
"ABORT_ERR", "UND_ERR_CONNECT_TIMEOUT", "UND_ERR_SOCKET",
|
|
]);
|
|
|
|
function field(value: unknown, key: string): unknown {
|
|
try { return value && typeof value === "object" ? (value as Record<string, unknown>)[key] : undefined; }
|
|
catch { return undefined; }
|
|
}
|
|
|
|
function integer(value: unknown, minimum: number, maximum: number): number | undefined {
|
|
return typeof value === "number" && Number.isInteger(value) && value >= minimum && value <= maximum
|
|
? value : undefined;
|
|
}
|
|
|
|
function sanitize(value: unknown): PluginEnvironmentSyncErrorDiagnostic | undefined {
|
|
const code = field(value, "errorCode");
|
|
const errorCode = typeof code === "string" && ERROR_CODES.has(code) ? code : "unknown";
|
|
const httpStatus = integer(field(value, "httpStatus"), 400, 599);
|
|
const exitCode = integer(field(value, "exitCode"), 1, 255);
|
|
if (errorCode === "unknown" && httpStatus === undefined && exitCode === undefined) return undefined;
|
|
return { errorCode, ...(httpStatus !== undefined ? { httpStatus } : {}), ...(exitCode !== undefined ? { exitCode } : {}) };
|
|
}
|
|
|
|
/** Capture known codes and numbers, never arbitrary error data, text, or names. */
|
|
export function environmentSyncErrorData(error: unknown): unknown {
|
|
const diagnostic: PluginEnvironmentSyncErrorDiagnostic = { errorCode: "unknown" };
|
|
let current = error;
|
|
// Bound nested SDK causes, including cyclic errors and throwing getters.
|
|
for (let depth = 0; depth < 4 && current && typeof current === "object"; depth++) {
|
|
const code = field(current, "code");
|
|
if (diagnostic.errorCode === "unknown" && typeof code === "string" && ERROR_CODES.has(code)) diagnostic.errorCode = code;
|
|
diagnostic.httpStatus ??= integer(field(current, "status"), 400, 599)
|
|
?? integer(field(current, "statusCode"), 400, 599)
|
|
?? integer(field(field(current, "response"), "status"), 400, 599);
|
|
diagnostic.exitCode ??= integer(field(current, "exitCode"), 1, 255) ?? integer(code, 1, 255);
|
|
current = field(current, "cause");
|
|
}
|
|
const safe = sanitize(diagnostic);
|
|
return safe ? { schema: SCHEMA, diagnostic: safe } : undefined;
|
|
}
|
|
|
|
/** Revalidate the worker envelope before adapting it into host-only diagnostics. */
|
|
export function readEnvironmentSyncErrorDiagnostic(error: unknown): PluginEnvironmentSyncErrorDiagnostic | undefined {
|
|
const data = field(error, "data");
|
|
return field(data, "schema") === SCHEMA ? sanitize(field(data, "diagnostic")) : undefined;
|
|
}
|