refactor: consolidate coercion contracts (#122458)

* refactor: consolidate coercion contracts

Centralize exact string, record, numeric, date, Boolean, argument, and structured-error coercions while preserving call-site semantics.

Migrate canonical-name collisions and deprecated internal SDK bypasses, deleting 55 net production/tooling lines. Expand declaration ownership enforcement to 101 allowed helpers and add a narrow export-completeness audit.

* fix: preserve standalone script coercions

Keep copied Control UI tooling self-contained and retain the trusted release harness module-relative source seam when the harness runs against an old target cwd.
This commit is contained in:
Peter Steinberger
2026-08-11 23:26:37 -07:00
committed by GitHub
parent 66fe424590
commit b080dd1e76
276 changed files with 1685 additions and 1663 deletions
+4 -42
View File
@@ -2,7 +2,7 @@ import { fork, type ChildProcess } from "node:child_process";
import { existsSync } from "node:fs";
import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { toErrorObject } from "../infra/errors.js";
import { toStructuredErrorObject } from "@openclaw/normalization-core/error-coercion";
import { createSubsystemLogger } from "../logging/subsystem.js";
import {
confirmOpenClawAgentDatabaseIntegrity,
@@ -25,44 +25,6 @@ export const OPENCLAW_DATABASE_VERIFY_INTERVAL_MS = 24 * 60 * 60_000;
const log = createSubsystemLogger("state/database-verify");
const DATABASE_VERIFY_CHILD_ARG = "--openclaw-database-verify-child";
const ERROR_OWNED_FIELDS = new Set(["cause", "message", "name", "stack"]);
const PROTOTYPE_MUTATING_FIELDS = new Set(["__proto__", "constructor", "prototype"]);
function toDatabaseVerifyError(error: unknown): Error {
if (error instanceof Error) {
return error;
}
const message = String(error);
if ((typeof error !== "object" || error === null) && typeof error !== "function") {
return toErrorObject(error, message);
}
const normalized = toErrorObject({}, message);
normalized.cause = error;
try {
const detailKeys = Reflect.ownKeys(error).filter(
(key) =>
(typeof key !== "string" ||
(!ERROR_OWNED_FIELDS.has(key) && !PROTOTYPE_MUTATING_FIELDS.has(key))) &&
Reflect.getOwnPropertyDescriptor(error, key)?.enumerable,
);
for (const key of detailKeys) {
try {
Object.defineProperty(normalized, key, {
value: Reflect.get(error, key),
writable: true,
enumerable: true,
configurable: true,
});
} catch {
// Skip fields whose getters or property definitions reject access.
}
}
} catch {
// Opaque proxies may reject enumeration; preserve the original failure as the cause.
}
return normalized;
}
function resolveDatabaseVerifyWorkerUrl(currentModuleUrl = import.meta.url): URL {
const currentPath = fileURLToPath(currentModuleUrl);
const normalized = currentPath.replaceAll(path.sep, "/");
@@ -104,7 +66,7 @@ export function runDatabaseVerifyWorker(
stdio: ["ignore", "ignore", "ignore", "ipc"],
});
} catch (error) {
return Promise.reject(toDatabaseVerifyError(error));
return Promise.reject(toStructuredErrorObject(error));
}
options.onWorker?.(worker);
@@ -156,7 +118,7 @@ export function runDatabaseVerifyWorker(
}
result = message;
});
worker.once("error", (error) => settle(() => reject(toDatabaseVerifyError(error))));
worker.once("error", (error) => settle(() => reject(toStructuredErrorObject(error))));
worker.once("disconnect", () => {
disconnected = true;
settleAfterExitAndDisconnect();
@@ -171,7 +133,7 @@ export function runDatabaseVerifyWorker(
return;
}
worker.kill();
settle(() => reject(toDatabaseVerifyError(error)));
settle(() => reject(toStructuredErrorObject(error)));
});
});
}
+3 -112
View File
@@ -71,122 +71,13 @@ async function captureDatabaseVerifyWorkerSendFailure(failure: unknown): Promise
}
describe("database verification error coercion", () => {
it("preserves existing Error identity without invoking custom toString", async () => {
class ThrowingToStringError extends Error {
override toString(): string {
throw new Error("unexpected stringification");
}
}
const cause = { code: "SQLITE_IOERR" };
const failure = new ThrowingToStringError("database failed", { cause });
failure.name = "DatabaseFailure";
const originalStack = failure.stack;
it("preserves structured send failures across the database-worker boundary", async () => {
const failure = { code: "SQLITE_IOERR", database: "state" };
const error = await captureDatabaseVerifyWorkerSendFailure(failure);
expect(error).toBe(failure);
expect(error).toMatchObject({
cause,
message: "database failed",
name: "DatabaseFailure",
stack: originalStack,
});
});
it("skips structured fields whose getters throw", async () => {
const failure = {
get details(): never {
throw new Error("unexpected structured field read");
},
code: "SQLITE_IOERR",
};
const error = await captureDatabaseVerifyWorkerSendFailure(failure);
expect(error).toMatchObject({ code: "SQLITE_IOERR" });
expect(error).not.toHaveProperty("details");
});
it("preserves the base Error when structured enumeration traps throw", async () => {
const handlers: ProxyHandler<{ code: string; status: number }>[] = [
{
ownKeys() {
throw new Error("unexpected ownKeys call");
},
},
{
ownKeys() {
return ["code", "status"];
},
getOwnPropertyDescriptor(target, key) {
if (key === "status") {
throw new Error("unexpected descriptor read");
}
return Reflect.getOwnPropertyDescriptor(target, key);
},
},
];
for (const handler of handlers) {
const failure = new Proxy({ code: "SQLITE_IOERR", status: 10 }, handler);
const error = await captureDatabaseVerifyWorkerSendFailure(failure);
expect(error).toMatchObject({ name: "Error", message: "[object Object]" });
expect(error.cause).toBe(failure);
expect(error).not.toHaveProperty("code");
expect(error).not.toHaveProperty("status");
}
});
it("preserves adapter-owned Error fields when structured failure fields collide", async () => {
const detailKey = Symbol("detail");
let reservedReads = 0;
const failure = {
get name() {
reservedReads += 1;
return "SpoofedError";
},
get message() {
reservedReads += 1;
return "spoofed message";
},
get cause() {
reservedReads += 1;
return "spoofed cause";
},
get stack() {
reservedReads += 1;
return "spoofed stack";
},
code: "SQLITE_IOERR",
details: { database: "state" },
[detailKey]: "symbol detail",
};
const error = await captureDatabaseVerifyWorkerSendFailure(failure);
expect(reservedReads).toBe(0);
expect(error.message).toBe("[object Object]");
expect(error).toMatchObject({ message: "[object Object]", code: "SQLITE_IOERR" });
expect(error.cause).toBe(failure);
expect(error.name).toBe("Error");
expect(error.stack).toContain("Error: [object Object]");
expect(error).toMatchObject({ code: "SQLITE_IOERR", details: { database: "state" } });
expect(Reflect.get(error, detailKey)).toBe("symbol detail");
});
it("rejects prototype-mutating structured failure fields", async () => {
const failure = { constructor: { polluted: true }, prototype: { polluted: true } };
Object.defineProperty(failure, "__proto__", {
value: { polluted: true },
enumerable: true,
});
const error = await captureDatabaseVerifyWorkerSendFailure(failure);
expect(Object.getPrototypeOf(error)).toBe(Error.prototype);
expect(Object.hasOwn(error, "__proto__")).toBe(false);
expect(Object.hasOwn(error, "constructor")).toBe(false);
expect(Object.hasOwn(error, "prototype")).toBe(false);
});
});
@@ -1,5 +1,6 @@
import type { DatabaseSync } from "node:sqlite";
import { safeParseJsonRecord } from "@openclaw/normalization-core";
import { asFiniteNumber } from "@openclaw/normalization-core/number-coercion";
import { asNullableRecord } from "@openclaw/normalization-core/record-coerce";
import { normalizeAgentRunTerminalReplySnapshot } from "../agents/agent-run-terminal-reply.js";
import { selectDeliverableSessionsReply } from "../agents/tools/sessions-send-tokens.js";
@@ -384,8 +385,7 @@ function textField(record: Record<string, unknown>, key: string): string | null
}
function numberField(record: Record<string, unknown>, key: string): number | null {
const value = record[key];
return typeof value === "number" && Number.isFinite(value) ? value : null;
return asFiniteNumber(record[key]) ?? null;
}
function recordField(record: Record<string, unknown>, key: string): Record<string, unknown> | null {