diff --git a/docs/.generated/plugin-sdk-api-baseline.sha256 b/docs/.generated/plugin-sdk-api-baseline.sha256 index 4e71e044cd43..ac230148515c 100644 --- a/docs/.generated/plugin-sdk-api-baseline.sha256 +++ b/docs/.generated/plugin-sdk-api-baseline.sha256 @@ -35,8 +35,8 @@ ba41c40956d6b4565605fa38c2d12f4b8471a0f8afe842798716b9032ee4d74a module/channel df567ce2f4a4ba8a0937f825c46e83763412a724e36b72ce727dc4203c7dd134 module/channel-ingress-runtime 0e6efb79730fae59bb549ad00d9af2848c139b4bb1762e83b66886284e1c1421 module/channel-lifecycle 159d034b431d113f3a6dc41ec0bcadba2d6664051f158330b0e3dd3da8b5d42f module/channel-logging -e649231e5136e8a8045c098754adad4b2e7bf7acc29c759e02015d244f7f107a module/channel-message -146c87e18187fbf90d7780d946b614c89c415e084c7206de2f0be1928a2b3434 module/channel-outbound +3aaa573834c68f71490a0b0a8687cd942122cdca03d3b08781cf52220ab71544 module/channel-message +9182ed800c19f6324654dc62f0c78fc11e6a5ef18f4387284066e5b2104da848 module/channel-outbound 930beff13ed42a138f65164013c82f4f4c96422292c5fc634a5edee1dce71367 module/channel-pairing ee4292b069d4d48cce4fc2dc26df5b5c87eb1fa4769f1f6be9a10c3e1221e1a9 module/channel-plugin-common 94ef57c8f6087fcaa56e59e493c391a04377ed03f23c681edd8d8f6e2d64e0da module/channel-policy @@ -47,8 +47,8 @@ f3cee48527f5cddde81912a588e72947d40443322b716871a8d9e5dc0c37c542 module/channel 7c90157a95bc0523fc66b1f78ce140f7ec7dbf809dfa3a480244efc01f972754 module/channel-send-result fb123c1b557ed2527e335f13c3d6de41ab0c3305151001cf2b1075d1da8034c5 module/channel-setup aeac9bb8127faf636dde79772a577d9cb003660ef3d1df03ddf606101020c402 module/channel-status -f008d1d9ea73cd17e9833143bd117436599438523041fd157b6b6cb17500ac06 module/channel-streaming -d1ff8728d99acf8130b94db7ba75ad7050a549c96365f56b8d4f601a533ca947 module/channel-streaming-config +d70ef71f48f9fe5dc01bf13ba3a2ba515322f81dc8f32d08528a113fbc78e30e module/channel-streaming +7f4714752b40f014605e2e7d46006b519972f81b528a286e1a40758493fdef5d module/channel-streaming-config 14f0103adb14627b662fbe9fdd5ed08596ed702a250853e8beb8bb8dd1361588 module/cli-argv c89ec1b194b76f67a6f4dd108dccf460da6065646cba31374c8aa748f23a39e4 module/collection-runtime 391e6f0c77da2e17a058fc5aea87fd193830b2a14e12fae77c7f63b0226f0bb5 module/command-auth @@ -76,7 +76,7 @@ d117ebba8cc490501725778676a9d75855872b6e5fe2b2f64b1270d4808a2277 module/health f6e3c44e7d1090a97aca554a3c247219b8de78b3cb4399cac5efde8a0a6c1156 module/inbound-envelope 1d5ca69fcf2a8476a2309dcd830fc3bbae04b7029fa30eea1081b6590a789f10 module/inbound-event-delivery 36721c58f479fe9ca32737f67850f4607bf8753ef6b193ff129e7a9e5fdbc6bd module/inbound-reply-dispatch -58675872c8985b66cfa32f3a1085bfb6d40c5abbafb3cd59d97b7bdde09dbe1a module/infra-runtime +03f7e1190d4101c486908383da099d545e59a6a90f5699b397c976ce6624c029 module/infra-runtime 2e717cccb3db127aed0287d4ea14c41a8e64e46d60c728638153e31e2fb0d296 module/ingress-effect-once 9389d91a090259f06e5ec2aae14c9857e45988d4edc4f91170cb8bc573b966a2 module/interactive-runtime 9dd66baf2def46386ad4706380e57f9068fa9bb3878be2d9ba961ec2f46d3d87 module/json-store @@ -132,8 +132,8 @@ f112bdabc51ba8659b37d0a6f6a32a2b1d471e5b49b56e108bf750ec55a7ea71 module/session df263e722a36ffa168522fa739924bec6b4b122ac3f1e25497a30962bec30607 module/setup-tools 18e384ec43d9eaee52c8e286e127bda2048370e2337964a754d94b236724ca9e module/skill-commands-runtime ae469f32799380e6b045abaefefee6eb3f00d714ffbf36b6eeef5025dc529472 module/speech-settings -9e521fe9073dfd1a6a6855f909fa6befe8613e18403f0a65faaba973a8b630c1 module/ssrf-policy -f85d5be0f635de6a77bbd9224a37c373998e098a8dcf2048ddc2c9ac735b47ef module/ssrf-runtime +92f8e3fe8b29fa5050f30bcc7a19f1085a590064d9e8b6ac54991e60a691a61e module/ssrf-policy +19c2ac7f81de3203ab91fc88793cf7e0b5bf4eaa2d5d61f4ee9e9f34e3f56be8 module/ssrf-runtime ff35f9f74d35d37a2eb6126b57f3dc5a4d580b6222fe3a34c9368779ad32eab7 module/state-paths 44cc071d9ba2baa26d6f33ba9407867fb07c33384f49d3e3d5210232022c1b5f module/status-helpers 537047854c21ad20ea0572f8019503cbda3bbafece8194a28c30b0639bde2fde module/string-coerce-runtime diff --git a/extensions/discord/src/monitor/model-picker-preference-primitives.ts b/extensions/discord/src/monitor/model-picker-preference-primitives.ts index 696e38b5e598..aaa4e8ed7ac8 100644 --- a/extensions/discord/src/monitor/model-picker-preference-primitives.ts +++ b/extensions/discord/src/monitor/model-picker-preference-primitives.ts @@ -1,5 +1,5 @@ import { createHash } from "node:crypto"; -import { normalizeProviderId } from "openclaw/plugin-sdk/provider-model-shared"; +import { normalizeProviderId } from "openclaw/plugin-sdk/model-ref-parse"; export function normalizeModelRef(raw?: string): string | null { const value = raw?.trim(); diff --git a/extensions/discord/src/monitor/thread-bindings.state.ts b/extensions/discord/src/monitor/thread-bindings.state.ts index fcd4941e6c69..2314ff51301d 100644 --- a/extensions/discord/src/monitor/thread-bindings.state.ts +++ b/extensions/discord/src/monitor/thread-bindings.state.ts @@ -1,5 +1,5 @@ // Discord plugin module implements thread bindings.state behavior. -import { recordOutboundMessageIdentity } from "openclaw/plugin-sdk/channel-outbound"; +import { recordOutboundMessageIdentity } from "openclaw/plugin-sdk/outbound-echo-runtime"; import { normalizeAccountId, resolveAgentIdFromSessionKey } from "openclaw/plugin-sdk/routing"; import { normalizeLowercaseStringOrEmpty, diff --git a/extensions/llm-task/doctor-contract-api.ts b/extensions/llm-task/doctor-contract-api.ts index c12cb20a1384..a7421bf2ba48 100644 --- a/extensions/llm-task/doctor-contract-api.ts +++ b/extensions/llm-task/doctor-contract-api.ts @@ -1,6 +1,6 @@ // LLM Task doctor contract migrates shipped plugin-local completion policy. import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; -import { parseModelRef } from "openclaw/plugin-sdk/provider-model-shared"; +import { parseModelRef } from "openclaw/plugin-sdk/model-ref-parse"; import { asObjectRecord } from "openclaw/plugin-sdk/runtime-doctor-migrations"; const ENTRY_PATH = "plugins.entries.llm-task"; diff --git a/extensions/matrix/auth-presence.ts b/extensions/matrix/auth-presence.ts index 362de3b73eed..518e1530b036 100644 --- a/extensions/matrix/auth-presence.ts +++ b/extensions/matrix/auth-presence.ts @@ -1,6 +1,6 @@ // Matrix plugin module implements auth presence behavior. import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; -import { createPluginStateSyncKeyedStore } from "openclaw/plugin-sdk/runtime-doctor"; +import { createPluginStateSyncKeyedStore } from "openclaw/plugin-sdk/plugin-state-store-runtime"; import { MATRIX_CREDENTIALS_MAX_ENTRIES, MATRIX_CREDENTIALS_NAMESPACE, diff --git a/extensions/matrix/src/doctor-contract.ts b/extensions/matrix/src/doctor-contract.ts index 21d367a32a56..c269f9c83cbf 100644 --- a/extensions/matrix/src/doctor-contract.ts +++ b/extensions/matrix/src/doctor-contract.ts @@ -9,12 +9,10 @@ import { defineKeyMoveMigration, hasLegacyAccountStreamingAliases, normalizeChannelConfigEntries, - stripRetiredChannelKeys, -} from "openclaw/plugin-sdk/runtime-doctor-migrations"; -import { hasLegacyFlatAllowPrivateNetworkAlias, migrateLegacyFlatAllowPrivateNetworkAlias, -} from "openclaw/plugin-sdk/ssrf-runtime"; + stripRetiredChannelKeys, +} from "openclaw/plugin-sdk/runtime-doctor-migrations"; import { isRecord } from "./record-shared.js"; import type { MatrixStreamingMode } from "./types.js"; diff --git a/extensions/matrix/src/doctor.ts b/extensions/matrix/src/doctor.ts index 65ba0602b03e..9f0a160b0953 100644 --- a/extensions/matrix/src/doctor.ts +++ b/extensions/matrix/src/doctor.ts @@ -5,7 +5,7 @@ import { detectPluginInstallPathIssue, formatPluginInstallPathIssue, removePluginFromConfig, -} from "openclaw/plugin-sdk/runtime-doctor"; +} from "openclaw/plugin-sdk/doctor-repair-runtime"; import { legacyConfigRules as MATRIX_LEGACY_CONFIG_RULES, normalizeCompatibilityConfig as normalizeMatrixCompatibilityConfig, diff --git a/extensions/matrix/src/matrix/credentials-read.ts b/extensions/matrix/src/matrix/credentials-read.ts index 0322edb8a754..cf5dd35f20d4 100644 --- a/extensions/matrix/src/matrix/credentials-read.ts +++ b/extensions/matrix/src/matrix/credentials-read.ts @@ -3,7 +3,7 @@ // heavy sync plugin-state store access. import { normalizeAccountId } from "openclaw/plugin-sdk/account-id"; import type { PluginStateSyncKeyedStore } from "openclaw/plugin-sdk/plugin-state-runtime"; -import { createPluginStateSyncKeyedStore } from "openclaw/plugin-sdk/runtime-doctor"; +import { createPluginStateSyncKeyedStore } from "openclaw/plugin-sdk/plugin-state-store-runtime"; import { getOptionalMatrixRuntime } from "../runtime.js"; import { MATRIX_CREDENTIALS_MAX_ENTRIES, diff --git a/extensions/mattermost/src/doctor-contract.ts b/extensions/mattermost/src/doctor-contract.ts index ff6549747faa..1ef72cb14945 100644 --- a/extensions/mattermost/src/doctor-contract.ts +++ b/extensions/mattermost/src/doctor-contract.ts @@ -1,8 +1,10 @@ // Mattermost plugin module implements doctor contract behavior. import type { ChannelDoctorConfigMutation } from "openclaw/plugin-sdk/channel-contract"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; -import { defineChannelAliasMigration } from "openclaw/plugin-sdk/runtime-doctor-migrations"; -import { createLegacyPrivateNetworkDoctorContract } from "openclaw/plugin-sdk/ssrf-runtime"; +import { + createLegacyPrivateNetworkDoctorContract, + defineChannelAliasMigration, +} from "openclaw/plugin-sdk/runtime-doctor-migrations"; const networkContract = createLegacyPrivateNetworkDoctorContract({ channelKey: "mattermost", diff --git a/extensions/nextcloud-talk/src/doctor-contract.ts b/extensions/nextcloud-talk/src/doctor-contract.ts index b3873a224d37..e90eca0fd2ca 100644 --- a/extensions/nextcloud-talk/src/doctor-contract.ts +++ b/extensions/nextcloud-talk/src/doctor-contract.ts @@ -1,8 +1,10 @@ // Nextcloud Talk plugin module implements doctor contract behavior. import type { ChannelDoctorConfigMutation } from "openclaw/plugin-sdk/channel-contract"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; -import { defineChannelAliasMigration } from "openclaw/plugin-sdk/runtime-doctor-migrations"; -import { createLegacyPrivateNetworkDoctorContract } from "openclaw/plugin-sdk/ssrf-runtime"; +import { + createLegacyPrivateNetworkDoctorContract, + defineChannelAliasMigration, +} from "openclaw/plugin-sdk/runtime-doctor-migrations"; const networkContract = createLegacyPrivateNetworkDoctorContract({ channelKey: "nextcloud-talk", diff --git a/extensions/qa-lab/src/suite-runtime-flow.ts b/extensions/qa-lab/src/suite-runtime-flow.ts index 9407ed2c0965..5df47a8c3f5f 100644 --- a/extensions/qa-lab/src/suite-runtime-flow.ts +++ b/extensions/qa-lab/src/suite-runtime-flow.ts @@ -7,8 +7,8 @@ import { resolveModelRefFromString } from "openclaw/plugin-sdk/agent-runtime"; import { formatErrorMessage as formatQaErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import { formatMemoryDreamingDay } from "openclaw/plugin-sdk/memory-core-host-status"; import { resolveSessionTranscriptsDirForAgent } from "openclaw/plugin-sdk/memory-host-core"; +import { createPluginStateSyncKeyedStore } from "openclaw/plugin-sdk/plugin-state-store-runtime"; import { buildAgentSessionKey } from "openclaw/plugin-sdk/routing"; -import { createPluginStateSyncKeyedStore } from "openclaw/plugin-sdk/runtime-doctor"; import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime"; import * as browserRuntime from "./browser-runtime.js"; import * as cronRunWait from "./cron-run-wait.js"; diff --git a/extensions/signal/doctor-contract-api.ts b/extensions/signal/doctor-contract-api.ts index e980ec8d0053..a57e6f50d743 100644 --- a/extensions/signal/doctor-contract-api.ts +++ b/extensions/signal/doctor-contract-api.ts @@ -3,9 +3,9 @@ import type { ChannelDoctorConfigMutation, ChannelDoctorLegacyConfigRule, } from "openclaw/plugin-sdk/channel-contract"; -import { isRecord } from "openclaw/plugin-sdk/channel-secret-basic-runtime"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; import { defineChannelAliasMigration } from "openclaw/plugin-sdk/runtime-doctor-migrations"; +import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime"; import { migrateLegacySignalTransportConfigSync } from "./src/config-compat.js"; const RETIRED_SIGNAL_ACCOUNT_TRANSPORT_FIELDS = [ diff --git a/extensions/signal/src/config-compat.ts b/extensions/signal/src/config-compat.ts index fd365d9b70b5..a7a1800150b9 100644 --- a/extensions/signal/src/config-compat.ts +++ b/extensions/signal/src/config-compat.ts @@ -1,8 +1,8 @@ import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-resolution"; // Signal compatibility migration moves shipped flat transport config into account ownership. import type { ChannelDoctorConfigMutation } from "openclaw/plugin-sdk/channel-contract"; -import { isRecord } from "openclaw/plugin-sdk/channel-secret-basic-runtime"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; +import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime"; import type { SignalTransportConfig } from "./account-types.js"; import { allocateSignalManagedNativePort, diff --git a/extensions/signal/src/config-schema.ts b/extensions/signal/src/config-schema.ts index cc885500132b..8cb18091eaed 100644 --- a/extensions/signal/src/config-schema.ts +++ b/extensions/signal/src/config-schema.ts @@ -16,7 +16,7 @@ import { requireAllowlistAllowFrom, requireOpenAllowFrom, } from "openclaw/plugin-sdk/channel-config-schema"; -import { isRecord } from "openclaw/plugin-sdk/channel-secret-basic-runtime"; +import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime"; import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime"; import { z } from "zod"; import { signalChannelConfigUiHints } from "./config-ui-hints.js"; diff --git a/extensions/signal/src/signal-ingress.ts b/extensions/signal/src/signal-ingress.ts index 6028f73ee451..9bbe1aa5e2af 100644 --- a/extensions/signal/src/signal-ingress.ts +++ b/extensions/signal/src/signal-ingress.ts @@ -6,8 +6,8 @@ import { type ChannelIngressMonitorDeliveryResult, type ChannelIngressMonitorLifecycle, } from "openclaw/plugin-sdk/channel-outbound"; -import { isRecord } from "openclaw/plugin-sdk/channel-secret-basic-runtime"; import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env"; +import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime"; import { normalizeNullableString as normalizeRawString } from "openclaw/plugin-sdk/string-coerce-runtime"; import type { SignalSseEvent } from "./client-adapter.js"; import { getOptionalSignalRuntime } from "./runtime.js"; diff --git a/extensions/telegram/src/sent-message-cache.ts b/extensions/telegram/src/sent-message-cache.ts index 303acbb114f7..f095614df0f9 100644 --- a/extensions/telegram/src/sent-message-cache.ts +++ b/extensions/telegram/src/sent-message-cache.ts @@ -5,7 +5,7 @@ import { resolveDefaultAgentId } from "openclaw/plugin-sdk/agent-scope-runtime"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; import type { PluginStateSyncKeyedStore } from "openclaw/plugin-sdk/plugin-state-runtime"; import { logVerbose } from "openclaw/plugin-sdk/runtime-env"; -import { resolveStorePath } from "openclaw/plugin-sdk/session-store-runtime"; +import { resolveStorePath } from "openclaw/plugin-sdk/session-store-paths"; import { getTelegramRuntime } from "./runtime.js"; const TTL_MS = 24 * 60 * 60 * 1000; diff --git a/extensions/telegram/src/state-migrations.ts b/extensions/telegram/src/state-migrations.ts index 97cd32ecd235..5e043efd2833 100644 --- a/extensions/telegram/src/state-migrations.ts +++ b/extensions/telegram/src/state-migrations.ts @@ -5,7 +5,7 @@ import { resolveDefaultAgentId } from "openclaw/plugin-sdk/agent-scope-runtime"; import type { ChannelLegacyStateMigrationPlan } from "openclaw/plugin-sdk/channel-contract"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; import { fileExists } from "openclaw/plugin-sdk/security-runtime"; -import { resolveStorePath } from "openclaw/plugin-sdk/session-store-runtime"; +import { resolveStorePath } from "openclaw/plugin-sdk/session-store-paths"; import { isRecord, uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime"; import { listTelegramAccountIds, resolveDefaultTelegramAccountId } from "./account-selection.js"; import { diff --git a/extensions/tlon/src/doctor-contract.ts b/extensions/tlon/src/doctor-contract.ts index 80951b451d10..f1cf24acd610 100644 --- a/extensions/tlon/src/doctor-contract.ts +++ b/extensions/tlon/src/doctor-contract.ts @@ -1,5 +1,5 @@ // Tlon plugin module implements doctor contract behavior. -import { createLegacyPrivateNetworkDoctorContract } from "openclaw/plugin-sdk/ssrf-runtime"; +import { createLegacyPrivateNetworkDoctorContract } from "openclaw/plugin-sdk/runtime-doctor-migrations"; const contract = createLegacyPrivateNetworkDoctorContract({ channelKey: "tlon", diff --git a/extensions/tsconfig.package-boundary.paths.json b/extensions/tsconfig.package-boundary.paths.json index 4a461631003f..4ec92fa6ba59 100644 --- a/extensions/tsconfig.package-boundary.paths.json +++ b/extensions/tsconfig.package-boundary.paths.json @@ -11,9 +11,6 @@ "openclaw/plugin-sdk/sandbox": [ "../packages/plugin-sdk/dist/src/plugin-sdk/sandbox.d.ts" ], - "openclaw/plugin-sdk/runtime-doctor": [ - "../packages/plugin-sdk/dist/src/plugin-sdk/runtime-doctor.d.ts" - ], "openclaw/plugin-sdk/runtime-doctor-migrations": [ "../packages/plugin-sdk/dist/src/plugin-sdk/runtime-doctor-migrations.d.ts" ], @@ -879,6 +876,21 @@ ], "@openclaw/plugin-sdk/*": [ "../dist/plugin-sdk/*.d.ts" + ], + "openclaw/plugin-sdk/doctor-repair-runtime": [ + "../packages/plugin-sdk/dist/src/plugin-sdk/doctor-repair-runtime.d.ts" + ], + "openclaw/plugin-sdk/plugin-state-store-runtime": [ + "../packages/plugin-sdk/dist/src/plugin-sdk/plugin-state-store-runtime.d.ts" + ], + "openclaw/plugin-sdk/model-ref-parse": [ + "../packages/plugin-sdk/dist/src/plugin-sdk/model-ref-parse.d.ts" + ], + "openclaw/plugin-sdk/outbound-echo-runtime": [ + "../packages/plugin-sdk/dist/src/plugin-sdk/outbound-echo-runtime.d.ts" + ], + "openclaw/plugin-sdk/session-store-paths": [ + "../packages/plugin-sdk/dist/src/plugin-sdk/session-store-paths.d.ts" ] } } diff --git a/extensions/voice-call/doctor-contract-api.ts b/extensions/voice-call/doctor-contract-api.ts index 0dfd896e0c61..c1e68b142443 100644 --- a/extensions/voice-call/doctor-contract-api.ts +++ b/extensions/voice-call/doctor-contract-api.ts @@ -2,11 +2,11 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; +// Doctor enumeration cold-loads this closure; the state-DB helpers stay behind a +// lazy doctor-repair-runtime import so enumeration never pulls the kysely/state-db graph. +import type { OpenClawStateDatabaseSchemaMigration } from "openclaw/plugin-sdk/doctor-repair-runtime"; import type { OpenClawConfig } from "openclaw/plugin-sdk/plugin-entry"; import { normalizeAgentId } from "openclaw/plugin-sdk/routing"; -// Doctor enumeration cold-loads this closure; the state-DB helpers stay behind a -// lazy runtime-doctor import so enumeration never pulls the kysely/state-db graph. -import type { OpenClawStateDatabaseSchemaMigration } from "openclaw/plugin-sdk/runtime-doctor"; import { archiveLegacyStateSource, type PluginDoctorStateMigration, @@ -304,7 +304,7 @@ export const stateMigrations: PluginDoctorStateMigration[] = [ label: "Voice Call call log", async detectLegacyState(params) { const { detectOpenClawStateDatabaseSchemaMigrations } = - await import("openclaw/plugin-sdk/runtime-doctor"); + await import("openclaw/plugin-sdk/doctor-repair-runtime"); const storePath = resolveVoiceCallStorePath(params); const filePath = resolveVoiceCallLegacyCallLogPath(storePath); const { entries } = await readLegacyCallRecords(filePath); @@ -330,7 +330,7 @@ export const stateMigrations: PluginDoctorStateMigration[] = [ }, async migrateLegacyState(params) { const { detectOpenClawStateDatabaseSchemaMigrations, repairOpenClawStateDatabaseSchema } = - await import("openclaw/plugin-sdk/runtime-doctor"); + await import("openclaw/plugin-sdk/doctor-repair-runtime"); const changes: string[] = []; const warnings: string[] = []; const storePath = resolveVoiceCallStorePath(params); diff --git a/extensions/xai/tsconfig.json b/extensions/xai/tsconfig.json index fe8173c9f426..f8534baa0471 100644 --- a/extensions/xai/tsconfig.json +++ b/extensions/xai/tsconfig.json @@ -11,9 +11,6 @@ "openclaw/plugin-sdk/sandbox": [ "../../packages/plugin-sdk/dist/src/plugin-sdk/sandbox.d.ts" ], - "openclaw/plugin-sdk/runtime-doctor": [ - "../../packages/plugin-sdk/dist/src/plugin-sdk/runtime-doctor.d.ts" - ], "openclaw/plugin-sdk/runtime-doctor-migrations": [ "../../packages/plugin-sdk/dist/src/plugin-sdk/runtime-doctor-migrations.d.ts" ], @@ -866,6 +863,21 @@ ], "@openclaw/ollama/runtime-api.js": [ "./.boundary-stubs/ollama-runtime-api.d.ts" + ], + "openclaw/plugin-sdk/doctor-repair-runtime": [ + "../../packages/plugin-sdk/dist/src/plugin-sdk/doctor-repair-runtime.d.ts" + ], + "openclaw/plugin-sdk/plugin-state-store-runtime": [ + "../../packages/plugin-sdk/dist/src/plugin-sdk/plugin-state-store-runtime.d.ts" + ], + "openclaw/plugin-sdk/model-ref-parse": [ + "../../packages/plugin-sdk/dist/src/plugin-sdk/model-ref-parse.d.ts" + ], + "openclaw/plugin-sdk/outbound-echo-runtime": [ + "../../packages/plugin-sdk/dist/src/plugin-sdk/outbound-echo-runtime.d.ts" + ], + "openclaw/plugin-sdk/session-store-paths": [ + "../../packages/plugin-sdk/dist/src/plugin-sdk/session-store-paths.d.ts" ] } } diff --git a/package.json b/package.json index 6dac0ae9d33a..f32a92e79673 100644 --- a/package.json +++ b/package.json @@ -183,6 +183,11 @@ "!dist/plugin-sdk/response-limit-runtime.d.ts", "!dist/plugin-sdk/retry-runtime.d.ts", "!dist/plugin-sdk/runtime-doctor-migrations.d.ts", + "!dist/plugin-sdk/doctor-repair-runtime.d.ts", + "!dist/plugin-sdk/model-ref-parse.d.ts", + "!dist/plugin-sdk/outbound-echo-runtime.d.ts", + "!dist/plugin-sdk/plugin-state-store-runtime.d.ts", + "!dist/plugin-sdk/session-store-paths.d.ts", "!dist/plugin-sdk/runtime-doctor.d.ts", "!dist/plugin-sdk/runtime-fetch.d.ts", "!dist/plugin-sdk/sandbox.d.ts", @@ -402,8 +407,20 @@ "types": "./dist/plugin-sdk/health.d.ts", "default": "./dist/plugin-sdk/health.js" }, - "./plugin-sdk/runtime-doctor": { - "default": "./dist/plugin-sdk/runtime-doctor.js" + "./plugin-sdk/doctor-repair-runtime": { + "default": "./dist/plugin-sdk/doctor-repair-runtime.js" + }, + "./plugin-sdk/model-ref-parse": { + "default": "./dist/plugin-sdk/model-ref-parse.js" + }, + "./plugin-sdk/outbound-echo-runtime": { + "default": "./dist/plugin-sdk/outbound-echo-runtime.js" + }, + "./plugin-sdk/session-store-paths": { + "default": "./dist/plugin-sdk/session-store-paths.js" + }, + "./plugin-sdk/plugin-state-store-runtime": { + "default": "./dist/plugin-sdk/plugin-state-store-runtime.js" }, "./plugin-sdk/runtime-doctor-migrations": { "default": "./dist/plugin-sdk/runtime-doctor-migrations.js" diff --git a/packages/plugin-sdk/package.json b/packages/plugin-sdk/package.json index bf67b680afef..c2a56cf142bc 100644 --- a/packages/plugin-sdk/package.json +++ b/packages/plugin-sdk/package.json @@ -192,10 +192,6 @@ "types": "./dist/src/plugin-sdk/provider-web-search-config-contract.d.ts", "default": "./src/provider-web-search-config-contract.ts" }, - "./runtime-doctor": { - "types": "./dist/src/plugin-sdk/runtime-doctor.d.ts", - "default": "./src/runtime-doctor.ts" - }, "./runtime-env": { "types": "./dist/src/plugin-sdk/runtime-env.d.ts", "default": "./src/runtime-env.ts" diff --git a/packages/plugin-sdk/src/runtime-doctor.ts b/packages/plugin-sdk/src/runtime-doctor.ts deleted file mode 100644 index 2eed1e2d45cc..000000000000 --- a/packages/plugin-sdk/src/runtime-doctor.ts +++ /dev/null @@ -1,3 +0,0 @@ -// Public package facade for runtime doctor contracts. - -export * from "../../../src/plugin-sdk/runtime-doctor.js"; diff --git a/scripts/lib/plugin-sdk-entrypoints.json b/scripts/lib/plugin-sdk-entrypoints.json index 2d7f11a9ed98..5dcb324bbec1 100644 --- a/scripts/lib/plugin-sdk-entrypoints.json +++ b/scripts/lib/plugin-sdk-entrypoints.json @@ -5,7 +5,11 @@ "routing", "runtime", "health", - "runtime-doctor", + "doctor-repair-runtime", + "model-ref-parse", + "outbound-echo-runtime", + "session-store-paths", + "plugin-state-store-runtime", "runtime-doctor-migrations", "runtime-env", "proxy-capture", diff --git a/scripts/lib/plugin-sdk-private-local-only-subpaths.json b/scripts/lib/plugin-sdk-private-local-only-subpaths.json index 2e982ce0e635..e9e5dd2f425b 100644 --- a/scripts/lib/plugin-sdk-private-local-only-subpaths.json +++ b/scripts/lib/plugin-sdk-private-local-only-subpaths.json @@ -40,6 +40,7 @@ "delivery-queue-runtime", "direct-dm-guard-policy", "directory-config-runtime", + "doctor-repair-runtime", "document-extractor", "embedding-providers", "exec-approvals-runtime", @@ -80,12 +81,15 @@ "message-tool-delivery-hints", "migration", "migration-runtime", + "model-ref-parse", "music-generation", "node-host", "number-runtime", + "outbound-echo-runtime", "outbound-media", "pair-loop-guard-runtime", "plugin-state-runtime", + "plugin-state-store-runtime", "plugin-state-test-runtime", "plugin-test-api", "plugin-test-contracts", @@ -127,15 +131,14 @@ "qa-runtime", "realtime-bootstrap-context", "realtime-transcription", - "realtime-voice-audio-queue", - "realtime-voice-activation", "realtime-voice", + "realtime-voice-activation", + "realtime-voice-audio-queue", "reply-payload-testing", "reply-reference", "request-url", "response-limit-runtime", "retry-runtime", - "runtime-doctor", "runtime-doctor-migrations", "runtime-fetch", "sandbox", @@ -144,6 +147,7 @@ "session-binding-runtime", "session-catalog-runtime", "session-key-runtime", + "session-store-paths", "session-transcript-hit", "session-transcript-runtime", "session-visibility", diff --git a/src/channels/streaming-config-readers.ts b/src/channels/streaming-config-readers.ts new file mode 100644 index 000000000000..3906a9a32393 --- /dev/null +++ b/src/channels/streaming-config-readers.ts @@ -0,0 +1,24 @@ +// Pure readers for a channel entry's `streaming` config object. +// +// Split out of `streaming.ts` because that module also formats tool aggregates +// and therefore value-loads the tool-display/logging graph. Doctor contract +// closures read streaming config during config-compat migration, and doctor +// enumeration cold-loads those closures for every declaring plugin. +import { asNullableRecord as asObjectRecord } from "@openclaw/normalization-core/record-coerce"; +import type { ChannelStreamingConfig } from "../config/types.base.js"; +import { asBoolean } from "../utils/boolean.js"; + +export type StreamingCompatEntry = { streaming?: unknown }; + +export function getChannelStreamingConfigObject( + entry: StreamingCompatEntry | null | undefined, +): ChannelStreamingConfig | undefined { + const streaming = asObjectRecord(entry?.streaming); + return streaming ? (streaming as ChannelStreamingConfig) : undefined; +} + +export function resolveChannelStreamingNativeTransport( + entry: StreamingCompatEntry | null | undefined, +): boolean | undefined { + return asBoolean(getChannelStreamingConfigObject(entry)?.nativeTransport); +} diff --git a/src/channels/streaming.ts b/src/channels/streaming.ts index 28ab7d181dc3..13aca3389c22 100644 --- a/src/channels/streaming.ts +++ b/src/channels/streaming.ts @@ -13,7 +13,6 @@ import type { BlockStreamingChunkConfig, BlockStreamingCoalesceConfig, ChannelStreamingCommandTextMode, - ChannelStreamingConfig, ChannelStreamingProgressConfig, StreamingMode, TextChunkMode, @@ -23,8 +22,16 @@ import { selectProgressLabel, } from "../shared/progress-labels.js"; import { asBoolean } from "../utils/boolean.js"; +import { + getChannelStreamingConfigObject, + type StreamingCompatEntry, +} from "./streaming-config-readers.js"; -export type StreamingCompatEntry = { streaming?: unknown }; +export { + getChannelStreamingConfigObject, + resolveChannelStreamingNativeTransport, +} from "./streaming-config-readers.js"; +export type { StreamingCompatEntry } from "./streaming-config-readers.js"; export type { ChannelDeliveryStreamingConfig, @@ -756,13 +763,6 @@ export function createChannelProgressDraftGate(params: { }; } -export function getChannelStreamingConfigObject( - entry: StreamingCompatEntry | null | undefined, -): ChannelStreamingConfig | undefined { - const streaming = asObjectRecord(entry?.streaming); - return streaming ? (streaming as ChannelStreamingConfig) : undefined; -} - export function resolveChannelStreamingChunkMode( entry: StreamingCompatEntry | null | undefined, ): TextChunkMode | undefined { @@ -911,12 +911,6 @@ export function resolveChannelStreamingSuppressDefaultToolProgressMessages( return options?.previewToolProgressEnabled ?? resolveChannelStreamingPreviewToolProgress(entry); } -export function resolveChannelStreamingNativeTransport( - entry: StreamingCompatEntry | null | undefined, -): boolean | undefined { - return asBoolean(getChannelStreamingConfigObject(entry)?.nativeTransport); -} - export function resolveChannelPreviewStreamMode( entry: StreamingCompatEntry | null | undefined, defaultMode: StreamingMode, diff --git a/src/config/legacy-private-network-migration.ts b/src/config/legacy-private-network-migration.ts new file mode 100644 index 000000000000..b653dc1c0f36 --- /dev/null +++ b/src/config/legacy-private-network-migration.ts @@ -0,0 +1,155 @@ +// Doctor migration for the retired flat `allowPrivateNetwork` channel key. +// +// Kept as a leaf beside the other channel alias migrations, not in +// `plugin-sdk/ssrf-policy`: these helpers only reshape config records, while +// that module value-loads the SSRF runtime (DNS, proxy state, logging). Doctor +// enumeration cold-loads every declaring plugin's contract closure, so a +// closure reaching this policy through the runtime graph paid for hundreds of +// modules it never called. +import { asNullableRecord } from "@openclaw/normalization-core/record-coerce"; +import type { + ChannelDoctorConfigMutation, + ChannelDoctorLegacyConfigRule, +} from "../plugin-sdk/channel-contract.js"; +import type { OpenClawConfig } from "./types.openclaw.js"; + +/** Detects the retired flat `allowPrivateNetwork` key before doctor migration. */ +export function hasLegacyFlatAllowPrivateNetworkAlias(value: unknown): boolean { + const entry = asNullableRecord(value); + return Boolean(entry && Object.hasOwn(entry, "allowPrivateNetwork")); +} + +/** Moves flat private-network config into `network.dangerouslyAllowPrivateNetwork`. */ +export function migrateLegacyFlatAllowPrivateNetworkAlias(params: { + entry: Record; + pathPrefix: string; + changes: string[]; +}): { entry: Record; changed: boolean } { + if (!hasLegacyFlatAllowPrivateNetworkAlias(params.entry)) { + return { entry: params.entry, changed: false }; + } + + const legacyAllowPrivateNetwork = params.entry.allowPrivateNetwork; + const currentNetworkRecord = asNullableRecord(params.entry.network); + const currentNetwork = currentNetworkRecord ? { ...currentNetworkRecord } : {}; + const currentDangerousAllowPrivateNetwork = currentNetwork.dangerouslyAllowPrivateNetwork; + + let resolvedDangerousAllowPrivateNetwork: unknown = currentDangerousAllowPrivateNetwork; + if (typeof currentDangerousAllowPrivateNetwork === "boolean") { + // The canonical key wins when both shapes are present. + resolvedDangerousAllowPrivateNetwork = currentDangerousAllowPrivateNetwork; + } else if (typeof legacyAllowPrivateNetwork === "boolean") { + resolvedDangerousAllowPrivateNetwork = legacyAllowPrivateNetwork; + } else if (currentDangerousAllowPrivateNetwork === undefined) { + resolvedDangerousAllowPrivateNetwork = legacyAllowPrivateNetwork; + } + + delete currentNetwork.dangerouslyAllowPrivateNetwork; + if (resolvedDangerousAllowPrivateNetwork !== undefined) { + currentNetwork.dangerouslyAllowPrivateNetwork = resolvedDangerousAllowPrivateNetwork; + } + + const nextEntry = { ...params.entry }; + delete nextEntry.allowPrivateNetwork; + if (Object.keys(currentNetwork).length > 0) { + nextEntry.network = currentNetwork; + } else { + delete nextEntry.network; + } + + params.changes.push( + `Moved ${params.pathPrefix}.allowPrivateNetwork → ${params.pathPrefix}.network.dangerouslyAllowPrivateNetwork (${String(resolvedDangerousAllowPrivateNetwork)}).`, + ); + return { entry: nextEntry, changed: true }; +} + +function hasLegacyAllowPrivateNetworkInAccounts(value: unknown): boolean { + const accounts = asNullableRecord(value); + return Boolean( + accounts && + Object.values(accounts).some((account) => + hasLegacyFlatAllowPrivateNetworkAlias(asNullableRecord(account) ?? {}), + ), + ); +} + +/** Build doctor rules that migrate legacy private-network aliases for one channel config. */ +export function createLegacyPrivateNetworkDoctorContract(params: { channelKey: string }): { + legacyConfigRules: ChannelDoctorLegacyConfigRule[]; + normalizeCompatibilityConfig: (params: { cfg: OpenClawConfig }) => ChannelDoctorConfigMutation; +} { + const pathPrefix = `channels.${params.channelKey}`; + return { + legacyConfigRules: [ + { + path: ["channels", params.channelKey], + message: `${pathPrefix}.allowPrivateNetwork is legacy; use ${pathPrefix}.network.dangerouslyAllowPrivateNetwork instead. Run "openclaw doctor --fix".`, + match: (value) => hasLegacyFlatAllowPrivateNetworkAlias(asNullableRecord(value) ?? {}), + }, + { + path: ["channels", params.channelKey, "accounts"], + message: `${pathPrefix}.accounts..allowPrivateNetwork is legacy; use ${pathPrefix}.accounts..network.dangerouslyAllowPrivateNetwork instead. Run "openclaw doctor --fix".`, + match: hasLegacyAllowPrivateNetworkInAccounts, + }, + ], + normalizeCompatibilityConfig: ({ cfg }) => { + const channels = asNullableRecord(cfg.channels); + const channelEntry = asNullableRecord(channels?.[params.channelKey]); + if (!channelEntry) { + return { config: cfg, changes: [] }; + } + + const changes: string[] = []; + let updatedChannel = channelEntry; + let changed = false; + + const topLevel = migrateLegacyFlatAllowPrivateNetworkAlias({ + entry: updatedChannel, + pathPrefix, + changes, + }); + updatedChannel = topLevel.entry; + changed = changed || topLevel.changed; + + const accounts = asNullableRecord(updatedChannel.accounts); + if (accounts) { + let accountsChanged = false; + const nextAccounts: Record = { ...accounts }; + for (const [accountId, accountValue] of Object.entries(accounts)) { + const account = asNullableRecord(accountValue); + if (!account) { + continue; + } + const migrated = migrateLegacyFlatAllowPrivateNetworkAlias({ + entry: account, + pathPrefix: `${pathPrefix}.accounts.${accountId}`, + changes, + }); + if (!migrated.changed) { + continue; + } + nextAccounts[accountId] = migrated.entry; + accountsChanged = true; + } + if (accountsChanged) { + updatedChannel = { ...updatedChannel, accounts: nextAccounts }; + changed = true; + } + } + + if (!changed) { + return { config: cfg, changes: [] }; + } + return { + config: { + ...cfg, + channels: { + ...cfg.channels, + [params.channelKey]: updatedChannel, + } as OpenClawConfig["channels"], + }, + changes, + }; + }, + }; +} diff --git a/src/plugin-sdk/channel-streaming-config.ts b/src/plugin-sdk/channel-streaming-config.ts index d95df7c71cc1..ccb0bb6200b2 100644 --- a/src/plugin-sdk/channel-streaming-config.ts +++ b/src/plugin-sdk/channel-streaming-config.ts @@ -9,4 +9,4 @@ export { getChannelStreamingConfigObject, resolveChannelStreamingNativeTransport, -} from "../channels/streaming.js"; +} from "../channels/streaming-config-readers.js"; diff --git a/src/plugin-sdk/doctor-repair-runtime.ts b/src/plugin-sdk/doctor-repair-runtime.ts new file mode 100644 index 000000000000..ffb1a88d822f --- /dev/null +++ b/src/plugin-sdk/doctor-repair-runtime.ts @@ -0,0 +1,21 @@ +/** + * Heavy doctor repair operations: plugin install-path diagnosis, plugin config + * removal, and OpenClaw state-database schema detection/repair. + * + * These pull the state-database and plugin-registry graphs, so they are kept off + * `runtime-doctor-migrations` (the dependency-light subpath doctor contract + * closures import). Doctor enumeration cold-loads those closures per plugin; + * only code that actually performs a repair should reach this subpath, and + * closure-resident callers must import it lazily. + */ + +export { + detectPluginInstallPathIssue, + formatPluginInstallPathIssue, +} from "../infra/plugin-install-path-warnings.js"; +export { removePluginFromConfig } from "../plugins/uninstall-config.js"; +export { + detectOpenClawStateDatabaseSchemaMigrations, + repairOpenClawStateDatabaseSchema, +} from "../state/openclaw-state-db.js"; +export type { OpenClawStateDatabaseSchemaMigration } from "../state/openclaw-state-db.js"; diff --git a/src/plugin-sdk/model-ref-parse.ts b/src/plugin-sdk/model-ref-parse.ts new file mode 100644 index 000000000000..721df9c4adca --- /dev/null +++ b/src/plugin-sdk/model-ref-parse.ts @@ -0,0 +1,9 @@ +// Canonical provider/model reference parsing and provider-id normalization. +// +// Split from the `provider-model-shared` barrel, which also value-loads replay +// policy, endpoint, and catalog-compat helpers. Doctor contract closures +// normalize model refs during config migration and doctor enumeration +// cold-loads those closures for every declaring plugin. + +export { normalizeProviderId } from "@openclaw/model-catalog-core/provider-id"; +export { parseModelRef } from "../agents/model-selection-normalize.js"; diff --git a/src/plugin-sdk/outbound-echo-runtime.ts b/src/plugin-sdk/outbound-echo-runtime.ts new file mode 100644 index 000000000000..d6d9679d06bf --- /dev/null +++ b/src/plugin-sdk/outbound-echo-runtime.ts @@ -0,0 +1,7 @@ +// Outbound message identity recording for echo suppression. +// +// Split from the `channel-outbound` barrel, which also value-loads the +// reply-pipeline/channel-registry graph that doctor enumeration must not +// cold-load from a legacy-setup closure. + +export { recordOutboundMessageIdentity } from "../channels/message/outbound-echo.js"; diff --git a/src/plugin-sdk/plugin-state-store-runtime.ts b/src/plugin-sdk/plugin-state-store-runtime.ts new file mode 100644 index 000000000000..ac78d591d7a6 --- /dev/null +++ b/src/plugin-sdk/plugin-state-store-runtime.ts @@ -0,0 +1,10 @@ +/** + * Synchronous keyed plugin-state store factory. + * + * Separate from `plugin-state-runtime`, which stays type-only plus light + * helpers because hot channel entrypoints import it at module load; opening a + * store pulls the state-database graph, so only callers that actually read or + * write state take that cost. + */ + +export { createPluginStateSyncKeyedStore } from "../plugin-state/plugin-state-store.js"; diff --git a/src/plugin-sdk/runtime-doctor-migrations.ts b/src/plugin-sdk/runtime-doctor-migrations.ts index 0c9c71019132..514384d26ffd 100644 --- a/src/plugin-sdk/runtime-doctor-migrations.ts +++ b/src/plugin-sdk/runtime-doctor-migrations.ts @@ -15,6 +15,11 @@ import { archiveLegacyStateSource } from "../plugins/doctor-state-migration-fs.j export { collectProviderDangerousNameMatchingScopes } from "../config/dangerous-name-matching.js"; export { defineChannelAliasMigration } from "../config/channel-alias-migration.js"; +export { + createLegacyPrivateNetworkDoctorContract, + hasLegacyFlatAllowPrivateNetworkAlias, + migrateLegacyFlatAllowPrivateNetworkAlias, +} from "../config/legacy-private-network-migration.js"; export type { ChannelAliasMigrationSpec, StreamingAliasMode, diff --git a/src/plugin-sdk/runtime-doctor.test.ts b/src/plugin-sdk/runtime-doctor.test.ts deleted file mode 100644 index 58e50c72cb64..000000000000 --- a/src/plugin-sdk/runtime-doctor.test.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { describe, expect, it } from "vitest"; -import type { OpenClawConfig } from "./config-contracts.js"; -import { - collectChannelAccountScopes, - defineKeyMoveMigration, - normalizeChannelConfigEntries, - stripRetiredChannelKeys, -} from "./runtime-doctor.js"; - -function cfgWith(entry: Record): OpenClawConfig { - return { channels: { sample: entry } } as never; -} - -describe("runtime-doctor channel helpers", () => { - it("collects the channel root and object-shaped accounts in config order", () => { - expect( - collectChannelAccountScopes({ - cfg: cfgWith({ accounts: { work: { enabled: true }, invalid: "skip" } }), - channelId: "sample", - }), - ).toEqual([ - { - prefix: "channels.sample", - pathSegments: ["channels", "sample"], - account: { accounts: { work: { enabled: true }, invalid: "skip" } }, - }, - { - prefix: "channels.sample.accounts.work", - pathSegments: ["channels", "sample", "accounts", "work"], - account: { enabled: true }, - }, - ]); - }); - - it("moves nested keys across wildcard entries and preserves canonical values", () => { - const migration = defineKeyMoveMigration({ - scope: ["groups", "*"], - from: ["allow"], - to: ["enabled"], - }); - const entry = { - groups: { - first: { allow: true }, - second: { allow: true, enabled: false }, - invalid: "skip", - }, - }; - const changes: string[] = []; - - expect(migration.hasLegacy(entry)).toBe(true); - expect(migration.normalize({ entry, pathPrefix: "channels.sample", changes })).toEqual({ - entry: { - groups: { - first: { enabled: true }, - second: { enabled: false }, - invalid: "skip", - }, - }, - changed: true, - }); - expect(changes).toEqual([ - "Moved channels.sample.groups.first.allow → channels.sample.groups.first.enabled.", - "Removed channels.sample.groups.second.allow (channels.sample.groups.second.enabled already set).", - ]); - }); - - it("supports mapped values, invalid removal, and empty-parent pruning", () => { - const migration = defineKeyMoveMigration({ - from: ["thread", "requireExplicitMention"], - to: ["implicitMentions", "threadParticipation"], - map: (value) => (typeof value === "boolean" ? { value: !value } : null), - pruneEmptySource: true, - }); - const changes: string[] = []; - const moved = migration.normalize({ - entry: { thread: { requireExplicitMention: true } }, - pathPrefix: "channels.sample", - changes, - }); - expect(moved.entry).toEqual({ implicitMentions: { threadParticipation: false } }); - expect( - migration.normalize({ - entry: { thread: { requireExplicitMention: "yes" } }, - pathPrefix: "channels.sample", - changes, - }).entry, - ).toEqual({}); - expect(changes).toEqual([ - "Moved channels.sample.thread.requireExplicitMention → channels.sample.implicitMentions.threadParticipation.", - "Removed invalid channels.sample.thread.requireExplicitMention value.", - ]); - }); - - it("normalizes the root and every object-shaped account in order", () => { - const changeLog: string[] = []; - const result = normalizeChannelConfigEntries({ - cfg: cfgWith({ legacy: true, accounts: { work: { legacy: false }, invalid: "skip" } }), - channelId: "sample", - changes: changeLog, - normalizeEntry: ({ entry, pathPrefix, changes }) => { - if (!Object.hasOwn(entry, "legacy")) { - return { entry, changed: false }; - } - const { legacy: _legacy, ...rest } = entry; - changes.push(`Removed ${pathPrefix}.legacy.`); - return { entry: rest, changed: true }; - }, - }); - - expect(result.changes).toBe(changeLog); - expect(result.changes).toEqual([ - "Removed channels.sample.legacy.", - "Removed channels.sample.accounts.work.legacy.", - ]); - expect((result.config.channels as Record).sample).toEqual({ - accounts: { work: {}, invalid: "skip" }, - }); - }); - - it("supports recursive and root-account-only retired key scopes", () => { - const source = cfgWith({ - retired: 1, - nested: { retired: 2 }, - accounts: { work: { retired: 3, nested: { retired: 4 } } }, - }); - const keys = new Set(["retired"]); - - const scoped = stripRetiredChannelKeys({ - cfg: source, - channelId: "sample", - keys, - scope: "root-and-accounts", - }); - expect((scoped.config.channels as Record).sample).toEqual({ - nested: { retired: 2 }, - accounts: { work: { nested: { retired: 4 } } }, - }); - - const recursive = stripRetiredChannelKeys({ - cfg: source, - channelId: "sample", - keys, - scope: "recursive", - }); - expect((recursive.config.channels as Record).sample).toEqual({ - nested: {}, - accounts: { work: { nested: {} } }, - }); - }); -}); diff --git a/src/plugin-sdk/runtime-doctor.ts b/src/plugin-sdk/runtime-doctor.ts deleted file mode 100644 index 817466610f0a..000000000000 --- a/src/plugin-sdk/runtime-doctor.ts +++ /dev/null @@ -1,56 +0,0 @@ -/** - * Runtime SDK subpath for plugin doctor migrations, compat checks, and uninstall helpers. - * - * This barrel value-loads the state DB and plugin state store graphs. Doctor - * contract closures (`doctor-contract-api.ts` and its imports) must use the - * dependency-light `runtime-doctor-migrations` subpath instead so doctor - * enumeration stays cheap; enumeration cold-loads those closures per plugin. - */ -export { - archiveLegacyStateSource, - asObjectRecord, - collectChannelAccountScopes, - collectProviderDangerousNameMatchingScopes, - defineChannelAliasMigration, - defineKeyMoveMigration, - defineLegacyJsonStateMigration, - definePluginDoctorMigrationFromPlans, - buildLegacyMigrationPreview, - hasLegacyAccountStreamingAliases, - hasLegacyStreamingAliases, - legacyStateFileExists, - materializeInheritedAccountStreaming, - normalizeChannelAccounts, - normalizeChannelConfigEntries, - normalizeLegacyChannelAliases, - normalizeLegacyDmAliases, - normalizeLegacyStreamingAliases, - resolveLegacyAliasStreamingMode, - stripRetiredChannelKeys, -} from "./runtime-doctor-migrations.js"; -export type { - ChannelAliasMigrationSpec, - CompatMutationResult, - DoctorSessionRouteStateOwner, - LegacyStreamingAliasOptions, - NormalizeChannelConfigEntryParams, - NormalizeLegacyChannelAccountParams, - OpenKeyedStoreOptions, - PluginDoctorStateMigration, - PluginDoctorStateMigrationContext, - PluginStateKeyedStore, - RetiredChannelKeyRemoval, - StreamingAliasMode, -} from "./runtime-doctor-migrations.js"; - -export { - detectPluginInstallPathIssue, - formatPluginInstallPathIssue, -} from "../infra/plugin-install-path-warnings.js"; -export { createPluginStateSyncKeyedStore } from "../plugin-state/plugin-state-store.js"; -export { - detectOpenClawStateDatabaseSchemaMigrations, - repairOpenClawStateDatabaseSchema, -} from "../state/openclaw-state-db.js"; -export type { OpenClawStateDatabaseSchemaMigration } from "../state/openclaw-state-db.js"; -export { removePluginFromConfig } from "../plugins/uninstall-config.js"; diff --git a/src/plugin-sdk/session-store-paths.ts b/src/plugin-sdk/session-store-paths.ts new file mode 100644 index 000000000000..53daebb3bf46 --- /dev/null +++ b/src/plugin-sdk/session-store-paths.ts @@ -0,0 +1,7 @@ +// Session store path resolution. +// +// Split from the `session-store-runtime` barrel, which also value-loads the +// session accessor/state-database graph. Doctor legacy-state closures only need +// to locate a store file, and doctor enumeration cold-loads those closures. + +export { resolveStorePath } from "../config/sessions/paths.js"; diff --git a/src/plugin-sdk/ssrf-policy.ts b/src/plugin-sdk/ssrf-policy.ts index e10e33d6f6c7..0807a19ece3d 100644 --- a/src/plugin-sdk/ssrf-policy.ts +++ b/src/plugin-sdk/ssrf-policy.ts @@ -10,11 +10,6 @@ import { type LookupFn, type SsrFPolicy, } from "../infra/net/ssrf.js"; -import type { - ChannelDoctorConfigMutation, - ChannelDoctorLegacyConfigRule, -} from "./channel-contract.js"; -import type { OpenClawConfig } from "./config-runtime.js"; export { isPrivateIpAddress, mergeSsrFPolicies }; export type { SsrFPolicy }; @@ -69,147 +64,14 @@ export function ssrfPolicyFromDangerouslyAllowPrivateNetwork( return ssrfPolicyFromPrivateNetworkOptIn(dangerouslyAllowPrivateNetwork); } -/** Detects the retired flat `allowPrivateNetwork` key before doctor migration. */ -export function hasLegacyFlatAllowPrivateNetworkAlias(value: unknown): boolean { - const entry = asNullableRecord(value); - return Boolean(entry && Object.hasOwn(entry, "allowPrivateNetwork")); -} - -/** Moves flat private-network config into `network.dangerouslyAllowPrivateNetwork`. */ -export function migrateLegacyFlatAllowPrivateNetworkAlias(params: { - entry: Record; - pathPrefix: string; - changes: string[]; -}): { entry: Record; changed: boolean } { - if (!hasLegacyFlatAllowPrivateNetworkAlias(params.entry)) { - return { entry: params.entry, changed: false }; - } - - const legacyAllowPrivateNetwork = params.entry.allowPrivateNetwork; - const currentNetworkRecord = asNullableRecord(params.entry.network); - const currentNetwork = currentNetworkRecord ? { ...currentNetworkRecord } : {}; - const currentDangerousAllowPrivateNetwork = currentNetwork.dangerouslyAllowPrivateNetwork; - - let resolvedDangerousAllowPrivateNetwork: unknown = currentDangerousAllowPrivateNetwork; - if (typeof currentDangerousAllowPrivateNetwork === "boolean") { - // The canonical key wins when both shapes are present. - resolvedDangerousAllowPrivateNetwork = currentDangerousAllowPrivateNetwork; - } else if (typeof legacyAllowPrivateNetwork === "boolean") { - resolvedDangerousAllowPrivateNetwork = legacyAllowPrivateNetwork; - } else if (currentDangerousAllowPrivateNetwork === undefined) { - resolvedDangerousAllowPrivateNetwork = legacyAllowPrivateNetwork; - } - - delete currentNetwork.dangerouslyAllowPrivateNetwork; - if (resolvedDangerousAllowPrivateNetwork !== undefined) { - currentNetwork.dangerouslyAllowPrivateNetwork = resolvedDangerousAllowPrivateNetwork; - } - - const nextEntry = { ...params.entry }; - delete nextEntry.allowPrivateNetwork; - if (Object.keys(currentNetwork).length > 0) { - nextEntry.network = currentNetwork; - } else { - delete nextEntry.network; - } - - params.changes.push( - `Moved ${params.pathPrefix}.allowPrivateNetwork → ${params.pathPrefix}.network.dangerouslyAllowPrivateNetwork (${String(resolvedDangerousAllowPrivateNetwork)}).`, - ); - return { entry: nextEntry, changed: true }; -} - -function hasLegacyAllowPrivateNetworkInAccounts(value: unknown): boolean { - const accounts = asNullableRecord(value); - return Boolean( - accounts && - Object.values(accounts).some((account) => - hasLegacyFlatAllowPrivateNetworkAlias(asNullableRecord(account) ?? {}), - ), - ); -} - -/** Build doctor rules that migrate legacy private-network aliases for one channel config. */ -export function createLegacyPrivateNetworkDoctorContract(params: { channelKey: string }): { - legacyConfigRules: ChannelDoctorLegacyConfigRule[]; - normalizeCompatibilityConfig: (params: { cfg: OpenClawConfig }) => ChannelDoctorConfigMutation; -} { - const pathPrefix = `channels.${params.channelKey}`; - return { - legacyConfigRules: [ - { - path: ["channels", params.channelKey], - message: `${pathPrefix}.allowPrivateNetwork is legacy; use ${pathPrefix}.network.dangerouslyAllowPrivateNetwork instead. Run "openclaw doctor --fix".`, - match: (value) => hasLegacyFlatAllowPrivateNetworkAlias(asNullableRecord(value) ?? {}), - }, - { - path: ["channels", params.channelKey, "accounts"], - message: `${pathPrefix}.accounts..allowPrivateNetwork is legacy; use ${pathPrefix}.accounts..network.dangerouslyAllowPrivateNetwork instead. Run "openclaw doctor --fix".`, - match: hasLegacyAllowPrivateNetworkInAccounts, - }, - ], - normalizeCompatibilityConfig: ({ cfg }) => { - const channels = asNullableRecord(cfg.channels); - const channelEntry = asNullableRecord(channels?.[params.channelKey]); - if (!channelEntry) { - return { config: cfg, changes: [] }; - } - - const changes: string[] = []; - let updatedChannel = channelEntry; - let changed = false; - - const topLevel = migrateLegacyFlatAllowPrivateNetworkAlias({ - entry: updatedChannel, - pathPrefix, - changes, - }); - updatedChannel = topLevel.entry; - changed = changed || topLevel.changed; - - const accounts = asNullableRecord(updatedChannel.accounts); - if (accounts) { - let accountsChanged = false; - const nextAccounts: Record = { ...accounts }; - for (const [accountId, accountValue] of Object.entries(accounts)) { - const account = asNullableRecord(accountValue); - if (!account) { - continue; - } - const migrated = migrateLegacyFlatAllowPrivateNetworkAlias({ - entry: account, - pathPrefix: `${pathPrefix}.accounts.${accountId}`, - changes, - }); - if (!migrated.changed) { - continue; - } - nextAccounts[accountId] = migrated.entry; - accountsChanged = true; - } - if (accountsChanged) { - updatedChannel = { ...updatedChannel, accounts: nextAccounts }; - changed = true; - } - } - - if (!changed) { - return { config: cfg, changes: [] }; - } - - return { - config: { - ...cfg, - channels: { - ...cfg.channels, - [params.channelKey]: updatedChannel, - } as OpenClawConfig["channels"], - }, - changes, - }; - }, - }; -} +// Config-shape doctor migration lives in a leaf module so doctor contract +// closures never load this file's SSRF runtime graph; re-exported here for +// existing callers. +export { + createLegacyPrivateNetworkDoctorContract, + hasLegacyFlatAllowPrivateNetworkAlias, + migrateLegacyFlatAllowPrivateNetworkAlias, +} from "../config/legacy-private-network-migration.js"; /** @deprecated Use `ssrfPolicyFromDangerouslyAllowPrivateNetwork`. */ export function ssrfPolicyFromAllowPrivateNetwork( diff --git a/src/plugins/contracts/extension-package-project-boundaries.test.ts b/src/plugins/contracts/extension-package-project-boundaries.test.ts index 5bcf81488e52..7557e447ad18 100644 --- a/src/plugins/contracts/extension-package-project-boundaries.test.ts +++ b/src/plugins/contracts/extension-package-project-boundaries.test.ts @@ -264,8 +264,8 @@ describe("opt-in extension package boundaries", () => { expect(packageJson.exports?.["./provider-web-search-config-contract"]?.types).toBe( "./dist/src/plugin-sdk/provider-web-search-config-contract.d.ts", ); - expect(packageJson.exports?.["./runtime-doctor"]?.types).toBe( - "./dist/src/plugin-sdk/runtime-doctor.d.ts", + expect(packageJson.exports?.["./runtime-env"]?.types).toBe( + "./dist/src/plugin-sdk/runtime-env.d.ts", ); expect(packageJson.exports?.["./security-runtime"]?.types).toBe( "./dist/src/plugin-sdk/security-runtime.d.ts", diff --git a/src/plugins/doctor-contract-closure-guard.test.ts b/src/plugins/doctor-contract-closure-guard.test.ts index 4a1559391500..89d6b0342ba9 100644 --- a/src/plugins/doctor-contract-closure-guard.test.ts +++ b/src/plugins/doctor-contract-closure-guard.test.ts @@ -26,14 +26,50 @@ const FORBIDDEN_SPECIFIER_RULES = new Map value ?? false); + if (doctorContractPath && declaresAnyDoctorSurface) { entries.push({ pluginId: record.id, pluginRoot,