From f4871eb86b43ddfc4503ec571b9582eeeae8772f Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 15 Aug 2026 01:03:26 -0700 Subject: [PATCH] refactor(types): remove chained assertions in runtime boundaries (#124082) --- .../reply/agent-runner.test-fixtures.ts | 4 +- src/cron/service/store.ts | 25 ++++++-- src/cron/store/row-codec.ts | 6 +- src/gateway/exec-approval-manager.ts | 20 ++++--- src/gateway/mcp-http.schema.ts | 4 +- src/gateway/question-manager.ts | 4 +- src/gateway/server-methods/cron.ts | 6 +- .../sessions-compaction-runner.ts | 5 +- src/gateway/server-reload-restart.ts | 10 +--- src/gateway/server-runtime-state-prepare.ts | 4 +- src/gateway/session-compaction-checkpoints.ts | 4 +- src/gateway/sessions-patch.ts | 2 +- .../worker-environments/node-worker-tunnel.ts | 5 +- src/gateway/ws-log.ts | 7 ++- src/infra/agent-activity-events.ts | 10 ++-- src/infra/exec-policy.ts | 16 ++++- src/infra/net/ssrf.ts | 18 ++---- src/infra/state-migrations.session-store.ts | 9 ++- src/logging/subsystem.ts | 5 +- src/mcp/plugin-tools-handlers.ts | 6 +- src/media-understanding/runner.entries.ts | 4 +- src/media/store.remote.runtime.ts | 4 +- .../realtime-local-audio-transport.ts | 3 +- src/plugins/capability-provider-runtime.ts | 59 ++++++++++--------- src/plugins/contracts/tts-contract-suites.ts | 21 +++---- .../embedding-provider-runtime-shared.ts | 27 +++++---- src/plugins/hook-isolation.ts | 7 +-- src/plugins/host-hook-state.ts | 4 +- src/plugins/session-catalog-history-import.ts | 4 +- src/proxy-capture/runtime.ts | 4 +- .../runtime-config-collectors-memory.ts | 10 +++- .../runtime-config-collectors-sandbox.ts | 12 ++-- src/tui/embedded-backend.ts | 2 +- src/tui/tui.ts | 27 +++++---- 34 files changed, 196 insertions(+), 162 deletions(-) diff --git a/src/auto-reply/reply/agent-runner.test-fixtures.ts b/src/auto-reply/reply/agent-runner.test-fixtures.ts index 7dad497ee192..50b3dc2f5e0d 100644 --- a/src/auto-reply/reply/agent-runner.test-fixtures.ts +++ b/src/auto-reply/reply/agent-runner.test-fixtures.ts @@ -17,7 +17,7 @@ export function createTestFollowupRun(overrides: Partial = { sessionFile: "/tmp/session.jsonl", workspaceDir: "/tmp", config: {}, - skillsSnapshot: {}, + skillsSnapshot: { prompt: "", skills: [] }, provider: "anthropic", model: "claude", thinkLevel: "low", @@ -29,7 +29,7 @@ export function createTestFollowupRun(overrides: Partial = { skipProviderRuntimeHints: true, ...overrides, }, - } as unknown as FollowupRun; + } satisfies FollowupRun; } export async function writeTestSessionStore( diff --git a/src/cron/service/store.ts b/src/cron/service/store.ts index 1646ef91c632..d4c5bbd5c6ea 100644 --- a/src/cron/service/store.ts +++ b/src/cron/service/store.ts @@ -1,4 +1,5 @@ /** Loads, normalizes, quarantines, and persists cron service store state. */ +import { isRecord } from "@openclaw/normalization-core/record-coerce"; import { normalizeCronJobIdentityFields } from "../normalize-job-identity.js"; import { normalizeCronJobInput } from "../normalize.js"; import { getInvalidPersistedCronJobReason } from "../persisted-shape.js"; @@ -134,6 +135,12 @@ function warnInvalidPersistedCronJob(params: { ); } +function isValidatedCronJob( + value: Record, +): value is CronJob & Record { + return getInvalidPersistedCronJobReason(value) === null; +} + /** Loads and normalizes the cron store, quarantining invalid persisted rows before runtime use. */ export async function ensureLoaded( state: CronServiceState, @@ -163,7 +170,7 @@ export async function ensureLoaded( const loadNowMs = state.deps.nowMs(); // Persisted cron rows are validated lazily, so treat them as raw records at the // store boundary and only trust the CronJob shape after validation below. - const loadedJobs = (loaded.store.jobs ?? []) as unknown as Record[]; + const loadedJobs = (loaded.store.jobs ?? []).filter(isRecord); const jobs: CronJob[] = []; const durableNextRunAtMsByJobId = new Map(); const quarantinedConfigJobs: QuarantinedCronConfigJob[] = [...loaded.invalidConfigRows]; @@ -190,11 +197,16 @@ export async function ensureLoaded( } const hydratedRaw = normalized ?? raw; let invalidReason = rawInvalidReason ?? getInvalidPersistedCronJobReason(hydratedRaw); - const hydratedSchedule = (hydratedRaw.schedule ?? {}) as Record; - if (!invalidReason && hydratedRaw.enabled !== false && hydratedSchedule.kind === "every") { + const hydratedSchedule = isRecord(hydratedRaw.schedule) ? hydratedRaw.schedule : {}; + if ( + !invalidReason && + isValidatedCronJob(hydratedRaw) && + hydratedRaw.enabled && + hydratedSchedule.kind === "every" + ) { try { assertTimeScheduleSatisfiable( - { ...(hydratedRaw as unknown as CronJob), state: {} }, + { ...hydratedRaw, state: {} }, loadNowMs, computeJobNextRunAtMs, ); @@ -226,7 +238,10 @@ export async function ensureLoaded( continue; } // Validated above, so the raw record is now a trusted CronJob. - const hydrated = hydratedRaw as unknown as CronJob; + if (!isValidatedCronJob(hydratedRaw)) { + continue; + } + const hydrated = hydratedRaw; jobs.push(hydrated); // Capture the value SQLite actually held before schedule-identity repair // mutates the runtime view. A later save can then publish that transition. diff --git a/src/cron/store/row-codec.ts b/src/cron/store/row-codec.ts index c1001097e659..29c6123125dc 100644 --- a/src/cron/store/row-codec.ts +++ b/src/cron/store/row-codec.ts @@ -193,13 +193,13 @@ function bindCronJobRow(storeKey: string, job: CronStoredJob, sortOrder: number) job_json: JSON.stringify(stripJobRuntimeFields(job)), state_json: JSON.stringify(job.state ?? {}), runtime_updated_at_ms: job.updatedAtMs, - schedule_identity: tryCronScheduleIdentity(job as unknown as Record) ?? null, + schedule_identity: tryCronScheduleIdentity({ ...job }) ?? null, sort_order: sortOrder, }; } function normalizeCronJobForSqlite(job: CronStoreFile["jobs"][number]): CronStoredJob | null { - const raw = structuredClone(job) as unknown as Record; + const raw: Record = { ...structuredClone(job) }; const hadDeleteAfterRun = Object.hasOwn(raw, "deleteAfterRun"); normalizeCronJobIdentityFields(raw); const normalized = normalizeCronJobInput(raw, { applyDefaults: true }); @@ -570,7 +570,7 @@ export function updateCronRuntimeRows( ...bindStateColumns(job.state ?? {}), state_json: JSON.stringify(job.state ?? {}), runtime_updated_at_ms: job.updatedAtMs, - schedule_identity: tryCronScheduleIdentity(job as unknown as Record), + schedule_identity: tryCronScheduleIdentity({ ...job }), }) .where("store_key", "=", storeKey) .where("job_id", "=", job.id), diff --git a/src/gateway/exec-approval-manager.ts b/src/gateway/exec-approval-manager.ts index 232752b4a168..79f757dd48c8 100644 --- a/src/gateway/exec-approval-manager.ts +++ b/src/gateway/exec-approval-manager.ts @@ -161,7 +161,7 @@ type PendingEntry = { record: ExecApprovalRecord; resolve: (decision: ExecApprovalDecision | null) => void; reject: (err: Error) => void; - timer: ReturnType; + timer: ReturnType | null; cleanupTimer: ReturnType | null; handoffRetainCount: number; handoffReleasedAtMs: number | null; @@ -367,7 +367,7 @@ export class ExecApprovalManager { record, resolve: resolvePromise!, reject: rejectPromise!, - timer: null as unknown as ReturnType, + timer: null, cleanupTimer: null, handoffRetainCount: 0, handoffReleasedAtMs: null, @@ -757,7 +757,9 @@ export class ExecApprovalManager { if (!pending || pending.record.resolvedAtMs !== undefined) { return false; } - clearTimeout(pending.timer); + if (pending.timer) { + clearTimeout(pending.timer); + } pending.record.resolvedAtMs = params.resolvedAtMs; if (params.decision === null) { delete pending.record.decision; @@ -875,12 +877,16 @@ export class ExecApprovalManager { } private scheduleExpiryTimer(entry: PendingEntry): void { - const timerDelayMs = resolveApprovalTimeoutMs(entry.record.expiresAtMs - Date.now()); - entry.timer = setTimeout(() => { + entry.timer = this.createExpiryTimer(entry.record); + } + + private createExpiryTimer(record: ExecApprovalRecord): ReturnType { + const timerDelayMs = resolveApprovalTimeoutMs(record.expiresAtMs - Date.now()); + return setTimeout(() => { try { - this.expireDue(entry.record.id); + this.expireDue(record.id); } catch (error) { - this.reportError(error, { approvalId: entry.record.id, operation: "expire" }); + this.reportError(error, { approvalId: record.id, operation: "expire" }); } }, timerDelayMs); } diff --git a/src/gateway/mcp-http.schema.ts b/src/gateway/mcp-http.schema.ts index 12e212da7746..48ca34a4febc 100644 --- a/src/gateway/mcp-http.schema.ts +++ b/src/gateway/mcp-http.schema.ts @@ -21,7 +21,7 @@ export type McpToolSchemaEntry = { function readLoopbackToolField(tool: McpLoopbackTool, key: "name" | "description" | "parameters") { try { - return (tool as unknown as Record)[key]; + return tool[key]; } catch { return undefined; } @@ -45,7 +45,7 @@ function readLoopbackToolDescription(tool: McpLoopbackTool): string | undefined function readLoopbackToolParameters(tool: McpLoopbackTool): Record | undefined { let value; try { - value = (tool as unknown as { parameters?: unknown }).parameters; + value = tool.parameters; } catch { return undefined; } diff --git a/src/gateway/question-manager.ts b/src/gateway/question-manager.ts index 662d6728af7a..496961328e88 100644 --- a/src/gateway/question-manager.ts +++ b/src/gateway/question-manager.ts @@ -116,15 +116,15 @@ export class QuestionManager { expiresAtMs, status: "pending", }; + const expiryTimer = setTimeout(() => this.expire(record.id), timeoutMs); const entry: QuestionEntry = { record, - expiryTimer: null as unknown as ReturnType, + expiryTimer, cleanupTimer: null, waiters: new Set(), onResolved: params.onResolved, }; this.entries.set(record.id, entry); - entry.expiryTimer = setTimeout(() => this.expire(record.id), timeoutMs); unrefTimer(entry.expiryTimer); return record; } diff --git a/src/gateway/server-methods/cron.ts b/src/gateway/server-methods/cron.ts index c50341a3b13a..172d46438ee8 100644 --- a/src/gateway/server-methods/cron.ts +++ b/src/gateway/server-methods/cron.ts @@ -888,6 +888,10 @@ export const cronHandlers: GatewayRequestHandlers = { if (!assertValidParams(candidate, validateCronUpdateParams, "cron.update", respond)) { return; } + if (!normalizedPatch) { + respondInvalidCronParams(respond, "cron.update", "patch did not normalize"); + return; + } const p = candidate as { id?: string; jobId?: string; @@ -912,7 +916,7 @@ export const cronHandlers: GatewayRequestHandlers = { ); return; } - const patch = p.patch as unknown as CronJobPatch; + const patch: CronJobPatch = normalizedPatch; const cfg = context.getRuntimeConfig(); const currentJob = await context.cron.readJob(jobId); if ( diff --git a/src/gateway/server-methods/sessions-compaction-runner.ts b/src/gateway/server-methods/sessions-compaction-runner.ts index ef9102cf212a..5a2160a93e5e 100644 --- a/src/gateway/server-methods/sessions-compaction-runner.ts +++ b/src/gateway/server-methods/sessions-compaction-runner.ts @@ -4,7 +4,7 @@ import { resolveAgentWorkspaceDir } from "../../agents/agent-scope.js"; import { compactEmbeddedAgentSession } from "../../agents/embedded-agent.js"; import { resolveManualCompactionCliTarget } from "../../agents/session-runtime-compat.js"; import { preflightManualSessionCompaction } from "../../agents/sessions/manual-compaction-preflight.js"; -import type { SessionEntry as AgentSessionEntry } from "../../agents/sessions/session-manager.js"; +import { isIndexedSessionEntry } from "../../agents/sessions/session-manager-codec.js"; import { resolveIngressWorkspaceOverrideForSessionRun } from "../../agents/spawned-context.js"; import { normalizeReasoningLevel, normalizeThinkLevel } from "../../auto-reply/thinking.js"; import type { SessionEntry } from "../../config/sessions.js"; @@ -14,7 +14,6 @@ import { resolveSessionTranscriptRuntimeTarget, } from "../../config/sessions/session-accessor.js"; import { - isCanonicalSessionTranscriptEntry, scanSessionTranscriptTree, selectSessionTranscriptTreePathNodes, } from "../../config/sessions/transcript-tree.js"; @@ -71,7 +70,7 @@ export async function preflightGatewaySessionCompaction( const tree = scanSessionTranscriptTree(transcriptEvents); const branch = selectSessionTranscriptTreePathNodes(tree, tree.leafId) .map((node) => node.entry) - .filter(isCanonicalSessionTranscriptEntry) as unknown as AgentSessionEntry[]; + .filter(isIndexedSessionEntry); const preflight = preflightManualSessionCompaction(branch, { enabled: true, reserveTokens: 0, diff --git a/src/gateway/server-reload-restart.ts b/src/gateway/server-reload-restart.ts index 5dfbceb55b07..904c42275c66 100644 --- a/src/gateway/server-reload-restart.ts +++ b/src/gateway/server-reload-restart.ts @@ -183,14 +183,8 @@ class GatewayRestartTransaction { acceptedConfig && configDebt.restartOwnedPaths.every((path) => isDeepStrictEqual( - getConfigValueAtPath( - configDebt.nextConfig as unknown as Record, - path.split("."), - ), - getConfigValueAtPath( - acceptedConfig as unknown as Record, - path.split("."), - ), + getConfigValueAtPath({ ...configDebt.nextConfig }, path.split(".")), + getConfigValueAtPath({ ...acceptedConfig }, path.split(".")), ), ); if (!retainsConfigDebt) { diff --git a/src/gateway/server-runtime-state-prepare.ts b/src/gateway/server-runtime-state-prepare.ts index ff78b91eb6ea..2aadc9dfdab8 100644 --- a/src/gateway/server-runtime-state-prepare.ts +++ b/src/gateway/server-runtime-state-prepare.ts @@ -211,9 +211,9 @@ export async function prepareGatewayKernelState(params: { const channelLogs = Object.fromEntries( listGatewayStartupChannelPlugins().map((plugin) => [plugin.id, logChannels.child(plugin.id)]), ) as Record>; - const channelRuntimeEnvs = Object.fromEntries( + const channelRuntimeEnvs: Partial> = Object.fromEntries( Object.entries(channelLogs).map(([id, logger]) => [id, runtimeForLogger(logger)]), - ) as unknown as Record; + ); const listStartupChannelGatewayMethods = () => { const methods: string[] = []; for (const plugin of listGatewayStartupChannelPlugins()) { diff --git a/src/gateway/session-compaction-checkpoints.ts b/src/gateway/session-compaction-checkpoints.ts index 41b91802059d..efde1b0a2c45 100644 --- a/src/gateway/session-compaction-checkpoints.ts +++ b/src/gateway/session-compaction-checkpoints.ts @@ -435,7 +435,7 @@ export async function readSessionLeafStateFromTranscriptAsync( } function readSessionLeafStateFromRecords( - records: readonly Record[], + records: readonly { type?: unknown; id?: unknown }[], ): { entryId: string; leafId: string | null } | null { let latestEntryId: string | undefined; for (const record of records) { @@ -620,7 +620,7 @@ async function captureCompactionCheckpointSnapshotAsync(params: { if (typeof params.sessionManager?.getEntries !== "function") { return null; } - const entryRecords = params.sessionManager.getEntries() as unknown as Record[]; + const entryRecords = params.sessionManager.getEntries(); const transcriptState = readSessionLeafStateFromRecords(entryRecords); const position = resolveCompactionCheckpointTranscriptPosition({ preferredLeafId: liveLeafId, diff --git a/src/gateway/sessions-patch.ts b/src/gateway/sessions-patch.ts index aceac97bd005..9cd1ba0012e0 100644 --- a/src/gateway/sessions-patch.ts +++ b/src/gateway/sessions-patch.ts @@ -239,7 +239,7 @@ export async function projectSessionsPatchEntry(params: { }; const existing = params.existingEntry - ? projectCanonicalSessionEntryShape(params.existingEntry as unknown as Record) + ? projectCanonicalSessionEntryShape({ ...params.existingEntry }) : undefined; // Existing entries without session ids are placeholder aliases; assigning an id makes them real. const next: SessionEntry = existing?.sessionId diff --git a/src/gateway/worker-environments/node-worker-tunnel.ts b/src/gateway/worker-environments/node-worker-tunnel.ts index 640d40673e54..c653c3eb014a 100644 --- a/src/gateway/worker-environments/node-worker-tunnel.ts +++ b/src/gateway/worker-environments/node-worker-tunnel.ts @@ -666,9 +666,8 @@ export function createNodeWorkerTunnelManager(options: NodeWorkerTunnelManagerOp abortController: new AbortController(), launchTasks: new Set>(), }; - const entry = { ...base, handle: undefined as unknown as WorkerTunnelHandle }; - const created = createHandle(entry, restoredWorkspace); - entry.handle = created.handle; + const created = createHandle(base, restoredWorkspace); + const entry = Object.assign(base, { handle: created.handle }); entries.set(entry.environmentId, entry); try { await created.validateRestoredWorkspace(); diff --git a/src/gateway/ws-log.ts b/src/gateway/ws-log.ts index f8a572e14478..dec8654eec83 100644 --- a/src/gateway/ws-log.ts +++ b/src/gateway/ws-log.ts @@ -1,3 +1,4 @@ +import { isRecord } from "@openclaw/normalization-core/record-coerce"; // Gateway WebSocket log formatting. // Redacts and compacts request/response/event metadata for console diagnostics. import { readStringValue } from "@openclaw/normalization-core/string-coerce"; @@ -164,7 +165,7 @@ function renderSingleErrorForLog(error: Error): string { if (error.message) { parts.push(error.message); } - const codeValue = (error as unknown as { code?: unknown }).code; + const codeValue = isRecord(error) ? error.code : undefined; const code = typeof codeValue === "string" || typeof codeValue === "number" ? String(codeValue) : ""; if (code) { @@ -175,12 +176,12 @@ function renderSingleErrorForLog(error: Error): string { function renderErrorChainForLog(error: Error): string { const segments: string[] = [renderSingleErrorForLog(error)]; - let current: unknown = (error as unknown as { cause?: unknown }).cause; + let current: unknown = error.cause; let depth = 0; while (current !== undefined && current !== null && depth < 8) { if (current instanceof Error) { segments.push(renderSingleErrorForLog(current)); - current = (current as unknown as { cause?: unknown }).cause; + current = current.cause; } else { segments.push(stringifyNonErrorCause(current)); current = undefined; diff --git a/src/infra/agent-activity-events.ts b/src/infra/agent-activity-events.ts index ce506a42975b..adb9dbb5468d 100644 --- a/src/infra/agent-activity-events.ts +++ b/src/infra/agent-activity-events.ts @@ -8,7 +8,7 @@ type AgentItemEventStatus = "running" | "completed" | "failed" | "blocked"; type AgentItemEventKind = "tool" | "command" | "patch" | "search" | "analysis" | (string & {}); /** Payload for a single item shown in the agent activity stream. */ -export type AgentItemEventData = { +export type AgentItemEventData = Record & { itemId: string; phase: AgentItemEventPhase; kind: AgentItemEventKind; @@ -32,7 +32,7 @@ export type AgentItemEventData = { }; /** Incremental command output payload associated with an item/tool call. */ -export type AgentCommandOutputEventData = { +export type AgentCommandOutputEventData = Record & { itemId: string; phase: "delta" | "end"; title: string; @@ -46,7 +46,7 @@ export type AgentCommandOutputEventData = { }; /** Patch summary payload emitted after an agent applies file changes. */ -export type AgentPatchSummaryEventData = { +export type AgentPatchSummaryEventData = Record & { itemId: string; phase: "end"; title: string; @@ -60,7 +60,7 @@ export type AgentPatchSummaryEventData = { type AgentActivityEventDataByStream = { item: AgentItemEventData; - approval: AgentApprovalEventData; + approval: AgentApprovalEventData & Record; command_output: AgentCommandOutputEventData; patch: AgentPatchSummaryEventData; }; @@ -79,7 +79,7 @@ export function emitAgentActivityEvent(params: AgentActivityEventParams): void { emitAgentEvent({ runId: params.runId, stream: params.stream, - data: params.data as unknown as Record, + data: params.data, ...(params.sessionKey ? { sessionKey: params.sessionKey } : {}), }); } diff --git a/src/infra/exec-policy.ts b/src/infra/exec-policy.ts index 4735fd0f4da5..aa599ee8272b 100644 --- a/src/infra/exec-policy.ts +++ b/src/infra/exec-policy.ts @@ -7,10 +7,20 @@ type ExecPolicyLayer = { ask?: ExecAsk; }; +type RequiredExecPolicy = Required>; + +export function applyExecPolicyLayer( + base: TBase, + layer?: ExecPolicyLayer, +): Omit & ExecPolicyLayer & RequiredExecPolicy; export function applyExecPolicyLayer( base: TBase, layer?: ExecPolicyLayer, -): TBase & ExecPolicyLayer { +): Omit & ExecPolicyLayer; +export function applyExecPolicyLayer( + base: ExecPolicyLayer, + layer?: ExecPolicyLayer, +): ExecPolicyLayer { if (!layer) { return base; } @@ -19,7 +29,7 @@ export function applyExecPolicyLayer( ...base, mode: layer.mode, ...resolveExecPolicyForMode(layer.mode), - } as unknown as TBase & ExecPolicyLayer; + }; } if (layer.security !== undefined || layer.ask !== undefined) { const { mode: _mode, ...baseWithoutMode } = base; @@ -27,7 +37,7 @@ export function applyExecPolicyLayer( ...baseWithoutMode, security: layer.security ?? base.security, ask: layer.ask ?? base.ask, - } as unknown as TBase & ExecPolicyLayer; + }; } return base; } diff --git a/src/infra/net/ssrf.ts b/src/infra/net/ssrf.ts index 2fe00508a78b..42f96b9e975a 100644 --- a/src/infra/net/ssrf.ts +++ b/src/infra/net/ssrf.ts @@ -1,6 +1,6 @@ // SSRF policy helpers validate hostnames/IP literals, build pinned DNS lookups, // and create dispatcher policies for guarded network fetches. -import { lookup as dnsLookupCb, type LookupAddress } from "node:dns"; +import { lookup as dnsLookupCb, type LookupAddress, type LookupOptions } from "node:dns"; import { lookup as dnsLookup } from "node:dns/promises"; import { extractEmbeddedIpv4FromIpv6, @@ -487,15 +487,6 @@ export function createPinnedLookup(params: { throw new Error(`Pinned lookup requires at least one address for ${params.hostname}`); } const fallback = params.fallback ?? dnsLookupCb; - const fallbackLookup = fallback as unknown as ( - hostname: string, - callback: LookupCallback, - ) => void; - const fallbackWithOptions = fallback as unknown as ( - hostname: string, - options: unknown, - callback: LookupCallback, - ) => void; const records = params.addresses.map((address) => ({ address, family: address.includes(":") ? 6 : 4, @@ -513,9 +504,12 @@ export function createPinnedLookup(params: { const normalized = normalizeHostname(host); if (!normalized || normalized !== normalizedHost) { if (typeof options === "function" || options === undefined) { - return fallbackLookup(host, cb); + return fallback(host, cb); } - return fallbackWithOptions(host, options, cb); + if (typeof options === "number") { + return fallback(host, options, cb); + } + return fallback(host, options as LookupOptions, cb); } const opts = diff --git a/src/infra/state-migrations.session-store.ts b/src/infra/state-migrations.session-store.ts index 67bd18394fb3..b0c678b1b24b 100644 --- a/src/infra/state-migrations.session-store.ts +++ b/src/infra/state-migrations.session-store.ts @@ -243,7 +243,8 @@ export function normalizeSessionEntry( entry: SessionEntryLike, sessionKey?: string, ): SessionEntry | null { - const shaped = normalizePersistedSessionEntryShape(entry, { sessionKey }); + const { room, ...entryWithoutRoom } = entry; + const shaped = normalizePersistedSessionEntryShape(entryWithoutRoom, { sessionKey }); if (!shaped) { return null; } @@ -254,11 +255,9 @@ export function normalizeSessionEntry( ? normalized.updatedAt : Date.now(); } - const rec = normalized as unknown as Record; - if (typeof rec.groupChannel !== "string" && typeof rec.room === "string") { - rec.groupChannel = rec.room; + if (typeof normalized.groupChannel !== "string" && typeof room === "string") { + normalized.groupChannel = room; } - delete rec.room; return normalized; } diff --git a/src/logging/subsystem.ts b/src/logging/subsystem.ts index 906754d67b93..56aca9f5d6ee 100644 --- a/src/logging/subsystem.ts +++ b/src/logging/subsystem.ts @@ -352,10 +352,7 @@ function logToFile( if (level === "silent") { return; } - const safeLevel = level; - const method = (fileLogger as unknown as Record)[safeLevel] as - | ((...args: unknown[]) => void) - | undefined; + const method = fileLogger[level]; if (typeof method !== "function") { return; } diff --git a/src/mcp/plugin-tools-handlers.ts b/src/mcp/plugin-tools-handlers.ts index ffd06cf3e0eb..e5252df052ed 100644 --- a/src/mcp/plugin-tools-handlers.ts +++ b/src/mcp/plugin-tools-handlers.ts @@ -18,6 +18,10 @@ type CallPluginToolParams = { arguments?: unknown; }; +type ToolWithBeforeToolCallHookContext = AnyAgentTool & { + [BEFORE_TOOL_CALL_HOOK_CONTEXT]?: unknown; +}; + function toMcpContentBlock(block: unknown): unknown { if (!isRecord(block)) { return { type: "text", text: coerceChatContentText(block) }; @@ -56,7 +60,7 @@ function resolveJsonSchemaForTool(tool: AnyAgentTool): Record { } function resolveBeforeToolCallRunId(tool: AnyAgentTool): string | undefined { - const context = (tool as unknown as Record)[BEFORE_TOOL_CALL_HOOK_CONTEXT]; + const context = (tool as ToolWithBeforeToolCallHookContext)[BEFORE_TOOL_CALL_HOOK_CONTEXT]; return isRecord(context) && typeof context.runId === "string" ? context.runId : undefined; } diff --git a/src/media-understanding/runner.entries.ts b/src/media-understanding/runner.entries.ts index 0e0c61d5a653..e1e66dac69ff 100644 --- a/src/media-understanding/runner.entries.ts +++ b/src/media-understanding/runner.entries.ts @@ -1049,7 +1049,7 @@ export async function runCliEntry(params: { }); const outputBase = path.join(outputDir, path.parse(mediaPath).name); - const templCtx: TemplateContext = { + const templCtx: TemplateContext & Record = { ...ctx, AttachmentPath: mediaPath, AttachmentUrl: params.attachment.url ?? params.attachment.path ?? mediaPath, @@ -1074,7 +1074,7 @@ export async function runCliEntry(params: { "MediaTranscribedIndexes", "MediaStaged", ]) { - delete (templCtx as unknown as Record)[key]; + delete templCtx[key]; } const argv = [command, ...args].map((part, index) => index === 0 ? part : applyTemplate(part, templCtx), diff --git a/src/media/store.remote.runtime.ts b/src/media/store.remote.runtime.ts index b4cabd5acd0f..5e6544ed2583 100644 --- a/src/media/store.remote.runtime.ts +++ b/src/media/store.remote.runtime.ts @@ -35,13 +35,13 @@ export async function saveRemoteMediaForStore(params: { }): Promise { const resolvePinned = params.resolvePinnedHostnameForTest; const lookupFn: LookupFn | undefined = resolvePinned - ? ((async (hostname: string) => { + ? async (hostname, _options) => { const pinned = await resolvePinned(hostname); return pinned.addresses.map((address) => ({ address, family: address.includes(":") ? 6 : 4, })); - }) as unknown as LookupFn) + } : undefined; const { id, path, size, contentType } = await saveRemoteMedia({ url: params.source, diff --git a/src/meeting-bot/realtime-local-audio-transport.ts b/src/meeting-bot/realtime-local-audio-transport.ts index 6449c36d0063..040077d35191 100644 --- a/src/meeting-bot/realtime-local-audio-transport.ts +++ b/src/meeting-bot/realtime-local-audio-transport.ts @@ -74,8 +74,7 @@ export function createLocalMeetingRealtimeAudioTransport(params: { const input = splitCommand(params.inputCommand); const output = splitCommand(params.outputCommand); const spawnFn: MeetingRealtimeAudioSpawn = - params.spawn ?? - ((command, args, options) => spawn(command, args, options) as unknown as BridgeProcess); + params.spawn ?? ((command, args, options) => spawn(command, args, options)); const spawnOutputProcess = () => spawnFn(output.command, output.args, { stdio: ["pipe", "ignore", "pipe"] }); let outputProcess = spawnOutputProcess(); diff --git a/src/plugins/capability-provider-runtime.ts b/src/plugins/capability-provider-runtime.ts index 670bae5bfdf8..6e2ac2905c48 100644 --- a/src/plugins/capability-provider-runtime.ts +++ b/src/plugins/capability-provider-runtime.ts @@ -1,3 +1,4 @@ +import { isRecord } from "@openclaw/normalization-core/record-coerce"; import { sortUniqueStrings } from "@openclaw/normalization-core/string-normalization"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { resolveVoiceModelRefs } from "../tts/voice-models.js"; @@ -42,11 +43,8 @@ type CapabilityContractKey = | "videoGenerationProviders" | "musicGenerationProviders"; -type ProviderFor = PluginRegistry[K][number] extends { - provider: infer T; -} - ? T - : never; +export type CapabilityProviderFor = + PluginRegistry[K][number]["provider"]; type CapabilityPluginResolution = { runtimePluginIds: string[]; bundledCompatPluginIds: string[]; @@ -149,24 +147,29 @@ function createCapabilityProviderLoadOptions(params: { function findProviderById( entries: PluginRegistry[K], providerId: string, -): ProviderFor | undefined { +): CapabilityProviderFor | undefined { const normalizedProviderId = normalizeCapabilityProviderId(providerId); if (!normalizedProviderId) { return undefined; } - const providerEntries = entries as unknown as Array<{ - provider: ProviderFor & { id?: unknown; aliases?: unknown }; - }>; - for (const entry of providerEntries) { + for (const entry of entries) { + const provider: unknown = entry.provider; + if (!isRecord(provider)) { + continue; + } if ( - typeof entry.provider.id === "string" && - normalizeCapabilityProviderId(entry.provider.id) === normalizedProviderId + typeof provider.id === "string" && + normalizeCapabilityProviderId(provider.id) === normalizedProviderId ) { - return entry.provider; + return entry.provider as CapabilityProviderFor; } } - for (const entry of providerEntries) { - const aliases = Array.isArray(entry.provider.aliases) ? entry.provider.aliases : []; + for (const entry of entries) { + const provider: unknown = entry.provider; + if (!isRecord(provider)) { + continue; + } + const aliases = Array.isArray(provider.aliases) ? provider.aliases : []; if ( aliases.some( (alias) => @@ -174,7 +177,7 @@ function findProviderById( normalizeCapabilityProviderId(alias) === normalizedProviderId, ) ) { - return entry.provider; + return entry.provider as CapabilityProviderFor; } } return undefined; @@ -183,12 +186,12 @@ function findProviderById( function mergeCapabilityProviders( left: PluginRegistry[K], right: PluginRegistry[K], -): ProviderFor[] { - const merged = new Map>(); - const unnamed: ProviderFor[] = []; +): CapabilityProviderFor[] { + const merged = new Map>(); + const unnamed: CapabilityProviderFor[] = []; const addEntries = (entries: PluginRegistry[K]) => { for (const entry of entries) { - const provider = entry.provider as ProviderFor & { id?: string }; + const provider = entry.provider as CapabilityProviderFor & { id?: string }; if (!provider.id) { unnamed.push(provider); continue; @@ -363,10 +366,10 @@ function filterLoadedProvidersForRequestedConfig { const provider = entry.provider as { id?: unknown; aliases?: unknown }; @@ -495,7 +498,7 @@ export function resolvePluginCapabilityProvider | undefined { +}): CapabilityProviderFor | undefined { if (shouldSkipCapabilityResolution(params)) { return undefined; } @@ -549,7 +552,7 @@ export function resolvePluginCapabilityProvider(params: { key: K; cfg?: OpenClawConfig; -}): ProviderFor[] { +}): CapabilityProviderFor[] { if (shouldSkipCapabilityResolution(params)) { return []; } @@ -573,12 +576,12 @@ export function resolvePluginCapabilityProviders 0 && params.key !== "memoryEmbeddingProviders") { if (!missingRequestedProviders && !shouldMergeManifestProvidersWhenActive(params.key)) { - return activeProviders.map((entry) => entry.provider) as ProviderFor[]; + return activeProviders.map((entry) => entry.provider) as CapabilityProviderFor[]; } if (missingRequestedProviders) { removeActiveProviderIds(missingRequestedProviders, activeProviders); if (missingRequestedProviders.size === 0) { - return activeProviders.map((entry) => entry.provider) as ProviderFor[]; + return activeProviders.map((entry) => entry.provider) as CapabilityProviderFor[]; } } } @@ -650,7 +653,7 @@ export function prepareMediaCapabilityProviders(params: { }) { const providers = ( key: K, - ): readonly ProviderFor[] | undefined => { + ): readonly CapabilityProviderFor[] | undefined => { if (shouldSkipCapabilityResolution({ key, cfg: params.cfg })) { return []; } @@ -687,7 +690,7 @@ export function prepareMediaCapabilityProviders(params: { } return Object.freeze( availableEntries.map((entry) => entry.provider), - ) as readonly ProviderFor[]; + ) as readonly CapabilityProviderFor[]; }; return Object.freeze({ mediaUnderstandingProviders: providers("mediaUnderstandingProviders"), diff --git a/src/plugins/contracts/tts-contract-suites.ts b/src/plugins/contracts/tts-contract-suites.ts index a0cebecd6492..94223f23d802 100644 --- a/src/plugins/contracts/tts-contract-suites.ts +++ b/src/plugins/contracts/tts-contract-suites.ts @@ -100,7 +100,7 @@ function asLegacyTtsConfig(value: unknown): OpenClawConfig { } function asLegacyOpenClawConfig(value: Record): OpenClawConfig { - return value as unknown as OpenClawConfig; + return asLegacyTtsConfig(value); } function mockCallAt(mock: { mock: { calls: Array> } }, index: number): unknown[] { @@ -182,11 +182,8 @@ async function withMockedSpeechFetch( audioLength: number, ) { const originalFetch = globalThis.fetch; - const fetchMock = vi.fn(async () => ({ - ok: true, - arrayBuffer: async () => new ArrayBuffer(audioLength), - })); - globalThis.fetch = fetchMock as unknown as typeof fetch; + const fetchMock = vi.fn(async () => new Response(new Uint8Array(audioLength))); + globalThis.fetch = fetchMock; try { await run(fetchMock); } finally { @@ -559,12 +556,12 @@ export function describeTtsConfigContract() { }, { name: "override", - cfg: { + cfg: asLegacyTtsConfig({ ...baseCfg, tts: { edge: { outputFormat: "audio-24khz-96kbitrate-mono-mp3" }, }, - } as unknown as OpenClawConfig, + }), expected: "audio-24khz-96kbitrate-mono-mp3", }, ] as const)("$name", ({ cfg, expected, name }) => { @@ -796,22 +793,22 @@ export function describeTtsConfigContract() { }, { name: "config wins over env", - cfg: { + cfg: asLegacyTtsConfig({ ...baseCfg, tts: { ...baseCfg.tts, openai: { baseUrl: "http://my-server:9000/v1" } }, - } as unknown as OpenClawConfig, + }), env: { OPENAI_TTS_BASE_URL: "http://localhost:8880/v1" }, expected: "http://my-server:9000/v1", }, { name: "config slash trimming", - cfg: { + cfg: asLegacyTtsConfig({ ...baseCfg, tts: { ...baseCfg.tts, openai: { baseUrl: "http://my-server:9000/v1///" }, }, - } as unknown as OpenClawConfig, + }), env: { OPENAI_TTS_BASE_URL: undefined }, expected: "http://my-server:9000/v1", }, diff --git a/src/plugins/embedding-provider-runtime-shared.ts b/src/plugins/embedding-provider-runtime-shared.ts index aed0e7308fa0..37f124c0280b 100644 --- a/src/plugins/embedding-provider-runtime-shared.ts +++ b/src/plugins/embedding-provider-runtime-shared.ts @@ -4,6 +4,7 @@ import type { OpenClawConfig } from "../config/types.openclaw.js"; import { resolvePluginCapabilityProvider, resolvePluginCapabilityProviders, + type CapabilityProviderFor, } from "./capability-provider-runtime.js"; type EmbeddingProviderCapabilityKey = "embeddingProviders" | "memoryEmbeddingProviders"; @@ -29,16 +30,18 @@ export function resolveRuntimeEmbeddingProviderLookupIds(params: { } /** Lists registered and plugin-contributed embedding provider adapters for a capability key. */ -export function listRuntimeEmbeddingProviderAdapters(params: { - key: EmbeddingProviderCapabilityKey; +export function listRuntimeEmbeddingProviderAdapters< + K extends EmbeddingProviderCapabilityKey, +>(params: { + key: K; cfg?: OpenClawConfig; - registered: TAdapter[]; -}): TAdapter[] { + registered: CapabilityProviderFor[]; +}): CapabilityProviderFor[] { const merged = new Map(params.registered.map((adapter) => [adapter.id, adapter])); const capabilityAdapters = resolvePluginCapabilityProviders({ key: params.key, cfg: params.cfg, - }) as unknown as TAdapter[]; + }); for (const adapter of capabilityAdapters) { if (!merged.has(adapter.id)) { merged.set(adapter.id, adapter); @@ -48,12 +51,16 @@ export function listRuntimeEmbeddingProviderAdapters(params: { - key: EmbeddingProviderCapabilityKey; +export function getRuntimeEmbeddingProviderAdapter< + K extends EmbeddingProviderCapabilityKey, +>(params: { + key: K; cfg?: OpenClawConfig; lookupIds: string[]; - getRegisteredProvider: (id: string) => RegisteredAdapterEntry | undefined; -}): TAdapter | undefined { + getRegisteredProvider: ( + id: string, + ) => RegisteredAdapterEntry> | undefined; +}): CapabilityProviderFor | undefined { // Resolve each exact id before trying the next configured alias. Otherwise a // registered alias can shadow a plugin-owned adapter for the requested id. for (const candidateId of params.lookupIds) { @@ -65,7 +72,7 @@ export function getRuntimeEmbeddingProviderAdapter): boolean { if (typeof SharedArrayBuffer !== "undefined" && value instanceof SharedArrayBuffer) { return true; @@ -22,7 +17,7 @@ function containsSharedMemory(value: unknown, seen: Set): boolean { } const webAssemblyMemory = ( globalThis as unknown as { - WebAssembly?: { Memory?: WebAssemblyMemoryConstructor }; + WebAssembly?: { Memory?: { new (...args: unknown[]): object; prototype: object } }; } ).WebAssembly?.Memory; if ( diff --git a/src/plugins/host-hook-state.ts b/src/plugins/host-hook-state.ts index e7462416a8bb..5fa280cfe585 100644 --- a/src/plugins/host-hook-state.ts +++ b/src/plugins/host-hook-state.ts @@ -29,6 +29,8 @@ const MAX_PLUGIN_NEXT_TURN_INJECTION_TEXT_LENGTH = 32 * 1024; const MAX_PLUGIN_NEXT_TURN_INJECTION_IDEMPOTENCY_KEY_LENGTH = 512; const MAX_PLUGIN_NEXT_TURN_INJECTIONS_PER_SESSION = 32; +type MutableSessionEntry = SessionEntry & Record; + function normalizeNamespace(value: string): string { return value.trim(); } @@ -326,7 +328,7 @@ export async function patchPluginSessionExtension(params: { }, (entry, context) => { params.assertCurrent?.(); - const entryRecord = entry as unknown as Record; + const entryRecord = entry as MutableSessionEntry; const pluginExtensions = { ...entry.pluginExtensions }; const pluginState = { ...pluginExtensions[pluginId] }; if (params.unset === true) { diff --git a/src/plugins/session-catalog-history-import.ts b/src/plugins/session-catalog-history-import.ts index 3e2ee0fe3809..7ef794a84173 100644 --- a/src/plugins/session-catalog-history-import.ts +++ b/src/plugins/session-catalog-history-import.ts @@ -177,9 +177,9 @@ export async function importSessionCatalogHistory(params: { continue; } const message = { - ...(imported as unknown as Record), + ...imported, idempotencyKey: `${params.catalogId}-catalog:${params.threadId}:${item.id ?? index}`, - } as unknown as AgentMessage; + }; await transcript.appendMessage({ message, idempotencyLookup: "scan", diff --git a/src/proxy-capture/runtime.ts b/src/proxy-capture/runtime.ts index d517c80bf883..69af4f8f5128 100644 --- a/src/proxy-capture/runtime.ts +++ b/src/proxy-capture/runtime.ts @@ -61,7 +61,7 @@ async function readCapturedResponseBodyBounded( maxBytes: number, ): Promise { const clone = response.clone(); - const body = (clone as unknown as { body?: ReadableStream | null }).body; + const body = clone.body; if (!body || typeof body.getReader !== "function") { // A real null-body Response consumes as empty. Response-like objects without // a stream cannot be read under a byte cap, so never call arrayBuffer(). @@ -76,7 +76,7 @@ async function readCapturedResponseBodyBounded( let stalled = false; try { while (true) { - let next: Awaited>; + let next: Awaited>; try { next = await readChunkWithIdleTimeout( reader, diff --git a/src/secrets/runtime-config-collectors-memory.ts b/src/secrets/runtime-config-collectors-memory.ts index 8e18ea5bf46d..2cbada5256fe 100644 --- a/src/secrets/runtime-config-collectors-memory.ts +++ b/src/secrets/runtime-config-collectors-memory.ts @@ -40,7 +40,11 @@ export function collectAgentMemorySearchAssignments(params: { let defaultApiKeyAssignmentCollected = false; const collectedDefaultHeaderKeys = new Set(); const collectForAgent = ({ entry: rawAgent, source }: ListedAgentEntry) => { - const rawAgentRecord = rawAgent as unknown as Record; + const rawAgentValue: unknown = rawAgent; + if (!isRecord(rawAgentValue)) { + return; + } + const rawAgentRecord = rawAgentValue; const agentMemory = isRecord(rawAgentRecord.memory) ? rawAgentRecord.memory : undefined; const memorySearch = isRecord(agentMemory?.search) ? agentMemory.search : undefined; const remote = isRecord(memorySearch?.remote) ? memorySearch.remote : undefined; @@ -48,7 +52,7 @@ export function collectAgentMemorySearchAssignments(params: { const agentPath = source.kind === "entries" ? `agents.entries.${source.key}` : `agents.list.${source.index}`; const active = - rawAgentRecord.enabled !== false && + rawAgentRecord["enabled"] !== false && (memorySearch?.enabled ?? defaultsMemorySearch?.enabled ?? true) !== false; const owner = { ownerKind: "capability", @@ -58,7 +62,7 @@ export function collectAgentMemorySearchAssignments(params: { contract: { defaults: defaultsMemorySearch, override: memorySearch, - agentEnabled: rawAgentRecord.enabled, + agentEnabled: rawAgentRecord["enabled"], }, } satisfies SecretAssignmentOwner; diff --git a/src/secrets/runtime-config-collectors-sandbox.ts b/src/secrets/runtime-config-collectors-sandbox.ts index 90ce08e721e5..49d0728be700 100644 --- a/src/secrets/runtime-config-collectors-sandbox.ts +++ b/src/secrets/runtime-config-collectors-sandbox.ts @@ -78,7 +78,11 @@ export function collectAgentSandboxAssignments(params: { for (const candidate of candidates) { const rawAgent = candidate.entry; - const rawAgentRecord = rawAgent as unknown as Record; + const rawAgentValue: unknown = rawAgent; + if (!isRecord(rawAgentValue)) { + continue; + } + const rawAgentRecord = rawAgentValue; const agentId = normalizeAgentId(candidate.entryId); if (seenAgentIds.has(agentId)) { continue; @@ -99,8 +103,8 @@ export function collectAgentSandboxAssignments(params: { ? (defaultsSandbox.scope as "agent" | "session" | "shared") : undefined, perSession: - typeof sandbox?.perSession === "boolean" - ? sandbox.perSession + typeof sandbox?.["perSession"] === "boolean" + ? sandbox["perSession"] : typeof defaultsSandbox?.perSession === "boolean" ? defaultsSandbox.perSession : undefined, @@ -112,7 +116,7 @@ export function collectAgentSandboxAssignments(params: { const owner = sandboxSecretOwner(agentId, { defaults: defaultsSandbox, override: sandbox, - agentEnabled: rawAgentRecord.enabled, + agentEnabled: rawAgentRecord["enabled"], }); for (const key of SANDBOX_SSH_SECRET_KEYS) { diff --git a/src/tui/embedded-backend.ts b/src/tui/embedded-backend.ts index 7acaf4248152..5bf7b303620f 100644 --- a/src/tui/embedded-backend.ts +++ b/src/tui/embedded-backend.ts @@ -797,7 +797,7 @@ export class EmbeddedTuiBackend implements TuiBackend { ok: true as const, path: target.storePath, key: target.canonicalKey ?? opts.key, - entry: applied.entry as unknown as Record, + entry: { ...applied.entry }, resolved: { modelProvider: resolved.provider, model: resolved.model, diff --git a/src/tui/tui.ts b/src/tui/tui.ts index 2895ee7892fa..e06d4e89441b 100644 --- a/src/tui/tui.ts +++ b/src/tui/tui.ts @@ -499,10 +499,10 @@ export function beginTuiShutdown(params: { clearTimeoutFn?: (timer: TuiProcessExitTimer) => void; setTimeoutFn?: TuiProcessExitTimeout; }): TuiProcessExitTimer { - const setTimeoutFn = - params.setTimeoutFn ?? - ((callback, timeoutMs) => setTimeout(callback, timeoutMs) as unknown as TuiProcessExitTimer); - const hardExitTimer = setTimeoutFn(params.forceExit, params.hardExitMs); + const hardExit = params.setTimeoutFn + ? { kind: "custom" as const, timer: params.setTimeoutFn(params.forceExit, params.hardExitMs) } + : { kind: "native" as const, timer: setTimeout(params.forceExit, params.hardExitMs) }; + const hardExitTimer = hardExit.timer; hardExitTimer.unref?.(); // Stop referenced animations before transport teardown can stall or redraw. params.disposeStatus(); @@ -529,10 +529,11 @@ export function beginTuiShutdown(params: { }) .finally(() => { if (params.keepHardExitArmed !== true) { - const clearTimeoutFn = - params.clearTimeoutFn ?? - ((timer) => clearTimeout(timer as unknown as ReturnType)); - clearTimeoutFn(hardExitTimer); + if (params.clearTimeoutFn) { + params.clearTimeoutFn(hardExitTimer); + } else if (hardExit.kind === "native") { + clearTimeout(hardExit.timer); + } } params.disposeStatus(); }) @@ -607,23 +608,23 @@ export function scheduleProcessExitAfterTuiReturn( } = {}, ): TuiProcessExitTimer { const delayMs = Math.max(0, Math.floor(params.delayMs ?? TUI_PROCESS_EXIT_AFTER_RETURN_MS)); - const setTimeoutFn = - params.setTimeoutFn ?? - ((callback, timeoutMs) => setTimeout(callback, timeoutMs) as unknown as TuiProcessExitTimer); const exit = params.exit ?? ((code?: number) => process.exit(code)); const writeStderr = params.writeStderr ?? ((text: string) => { process.stderr.write(text); }); - const timer = setTimeoutFn(() => { + const onTimeout = () => { try { writeStderr("openclaw tui forcing process exit after return\n"); } catch { // Best effort only; forced exit must not depend on stderr. } exit(0); - }, delayMs); + }; + const timer = params.setTimeoutFn + ? params.setTimeoutFn(onTimeout, delayMs) + : setTimeout(onTimeout, delayMs); timer.unref?.(); return timer; }