Files
PaperClipAI/packages/plugins/sdk/src/environment-sync-error.ts
T
Devin FoleyandPaperclip 465140596f Preserve bounded workspace sync diagnostics across RPC (#15481)
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>
2026-10-07 12:22:53 -07:00

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;
}