mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
perf(doctor): delete the heavy doctor barrel and finish slimming enumeration (#120882)
* refactor(plugin-sdk): delete the heavy runtime-doctor barrel
Nothing may pull the state-db/kysely graph through a doctor barrel anymore.
The barrel's remaining heavy exports move to two narrow private-local
subpaths, each with a single purpose:
- doctor-repair-runtime: install-path diagnosis, plugin config removal, and
state-database schema detect/repair (matrix doctor, voice-call lazy import)
- plugin-state-store-runtime: the sync keyed-store factory. It stays out of
plugin-state-runtime because hot channel entrypoints import that at module
load and opening a store pulls the state-database graph.
Doctor closures also stop pulling ssrf-runtime (fetch-guard + gateway net)
for two legacy private-network helpers that live in the lighter ssrf-policy
subpath: mattermost, nextcloud-talk, tlon, matrix.
The closure guard now forbids the two new heavy subpaths instead of the
deleted barrel, so the invariant keeps being enforced where it still applies.
* perf(doctor): keep heavy graphs out of every doctor closure
Doctor enumeration cold-loads each declaring plugin's contract closure, so
one heavy import in a closure is paid by the whole sweep. Four barrels were
still dragging unrelated graphs in for trivial helpers; each is repaired at
the leaf rather than by caching downstream:
- Legacy private-network config migration moves to a config leaf. It only
reshapes records, but lived beside the SSRF runtime (DNS, proxy, logging),
costing mattermost ~2.7s. ssrf-policy re-exports it, surface unchanged.
- Streaming config readers move to a leaf. They read two config keys, but
streaming.ts also formats tool aggregates, pulling tool-display/logging/
acp-core; that cost slack ~2.3s.
- signal took the channel-secret barrel for isRecord; the canonical plugin
record guard is string-coerce-runtime (root AGENTS.md).
- llm-task took the provider-model barrel for parseModelRef, now a narrow
model-ref-parse subpath.
Full doctor enumeration of all 42 declaring plugins, built mode:
legacy config rules 6668ms -> 1265ms, state migrations 184ms -> 127ms.
No plugin remains an outlier; the slowest is now ~380ms against a ~200ms floor.
Public export surfaces of every touched SDK subpath are byte-identical
(verified by diffing built module exports before/after); the API baseline
hashes move only because re-exported declarations emit differently.
The closure guard gains rules for each repaired barrel so the invariant
holds for future closures.
* fix(release): exclude new private-local declarations from the published package
Same pack-path rule as c41da3759f: private-local subpaths ship without d.ts.
* fix(doctor): repair the closure guard violations that break main
The landed guard fails on main: three closures import heavy barrels for one
symbol each. Two more surfaced once the guard learned about the provider-model
barrel. Each gets a narrow subpath at the leaf:
- telegram sent-message-cache + state-migrations took the session-store barrel
(session accessor + state-db) for resolveStorePath -> session-store-paths
- discord thread-bindings.state took the channel-outbound barrel (reply
pipeline + channel registry) for one identity write -> outbound-echo-runtime
- discord model-picker took the provider-model barrel for normalizeProviderId,
which model-ref-parse now exposes beside parseModelRef
The guard also stops walking artifacts of plugins whose manifest declares no
doctor surface. Such a declaration gates the artifact off every enumeration
path exactly as resolvePluginDoctorContracts does, so its closure cost is never
paid; anthropic ("doctorContract": {}) was being held to a cost it cannot
incur. Absent declarations still load eagerly and stay enforced.
Side effect worth naming: discord's built doctor contract now loads again.
On main both discord and telegram fail to require in packaged builds (an
ESM-only transitive dep) and silently lose their repairs; this restores
discord and takes enumerated legacy config rules from 87 to 99. Telegram's
built artifact still pulls execa through dist chunking - a build-level defect
with a different owner, filed as follow-up.
This commit is contained in:
committed by
GitHub
parent
4e74ae229e
commit
6192673da4
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+19
-2
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
// Public package facade for runtime doctor contracts.
|
||||
|
||||
export * from "../../../src/plugin-sdk/runtime-doctor.js";
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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<string, unknown>;
|
||||
pathPrefix: string;
|
||||
changes: string[];
|
||||
}): { entry: Record<string, unknown>; 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.<id>.allowPrivateNetwork is legacy; use ${pathPrefix}.accounts.<id>.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<string, unknown> = { ...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,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -9,4 +9,4 @@
|
||||
export {
|
||||
getChannelStreamingConfigObject,
|
||||
resolveChannelStreamingNativeTransport,
|
||||
} from "../channels/streaming.js";
|
||||
} from "../channels/streaming-config-readers.js";
|
||||
|
||||
@@ -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";
|
||||
@@ -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";
|
||||
@@ -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";
|
||||
@@ -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";
|
||||
@@ -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,
|
||||
|
||||
@@ -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<string, unknown>): 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<string, unknown>).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<string, unknown>).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<string, unknown>).sample).toEqual({
|
||||
nested: {},
|
||||
accounts: { work: { nested: {} } },
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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";
|
||||
@@ -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";
|
||||
@@ -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<string, unknown>;
|
||||
pathPrefix: string;
|
||||
changes: string[];
|
||||
}): { entry: Record<string, unknown>; 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.<id>.allowPrivateNetwork is legacy; use ${pathPrefix}.accounts.<id>.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<string, unknown> = { ...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(
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -26,14 +26,50 @@ const FORBIDDEN_SPECIFIER_RULES = new Map<string, { reason: string; kinds: Set<C
|
||||
},
|
||||
],
|
||||
[
|
||||
"openclaw/plugin-sdk/runtime-doctor",
|
||||
"openclaw/plugin-sdk/doctor-repair-runtime",
|
||||
{
|
||||
reason:
|
||||
"the heavy doctor barrel makes doctor enumeration cold-load the state-db/kysely graph; " +
|
||||
"use openclaw/plugin-sdk/runtime-doctor-migrations, or defer heavy helpers behind a dynamic import",
|
||||
"install-path, uninstall, and state-db schema repair cold-load the state-db/kysely graph; " +
|
||||
"use openclaw/plugin-sdk/runtime-doctor-migrations, or defer the repair behind a dynamic import",
|
||||
kinds: new Set(["doctor-contract", "legacy-setup"]),
|
||||
},
|
||||
],
|
||||
[
|
||||
"openclaw/plugin-sdk/ssrf-runtime",
|
||||
{
|
||||
reason:
|
||||
"the SSRF runtime barrel cold-loads DNS, proxy state, and logging; " +
|
||||
"legacy private-network config migration lives in openclaw/plugin-sdk/runtime-doctor-migrations",
|
||||
kinds: new Set(["doctor-contract", "legacy-setup"]),
|
||||
},
|
||||
],
|
||||
[
|
||||
"openclaw/plugin-sdk/provider-model-shared",
|
||||
{
|
||||
reason:
|
||||
"the provider-model barrel cold-loads replay/endpoint/catalog helpers; " +
|
||||
"use openclaw/plugin-sdk/model-ref-parse for provider/model reference parsing",
|
||||
kinds: new Set(["doctor-contract"]),
|
||||
},
|
||||
],
|
||||
[
|
||||
"openclaw/plugin-sdk/channel-secret-basic-runtime",
|
||||
{
|
||||
reason:
|
||||
"the channel-secret barrel cold-loads secret-ref/account-routing modules; " +
|
||||
"the canonical record guard is openclaw/plugin-sdk/string-coerce-runtime",
|
||||
kinds: new Set(["doctor-contract"]),
|
||||
},
|
||||
],
|
||||
[
|
||||
"openclaw/plugin-sdk/plugin-state-store-runtime",
|
||||
{
|
||||
reason:
|
||||
"opening a keyed plugin-state store cold-loads the state-db/kysely graph; " +
|
||||
"keep the store behind the migration context or a dynamic import inside async bodies",
|
||||
kinds: new Set(["doctor-contract"]),
|
||||
},
|
||||
],
|
||||
[
|
||||
"openclaw/plugin-sdk/session-store-runtime",
|
||||
{
|
||||
@@ -221,7 +257,13 @@ function collectClosureEntries(): ClosureEntry[] {
|
||||
for (const record of loadBundledPluginManifestRegistry({ env }).plugins) {
|
||||
const pluginRoot = path.resolve(record.rootDir);
|
||||
const doctorContractPath = resolvePluginDoctorContractArtifactPath(pluginRoot);
|
||||
if (doctorContractPath) {
|
||||
// A declaration listing no surface gates the artifact off every enumeration
|
||||
// path, exactly as `resolvePluginDoctorContracts` does, so its closure cost
|
||||
// is never paid. Absent declarations still load eagerly and are enforced.
|
||||
const declaresAnyDoctorSurface =
|
||||
!record.doctorContract ||
|
||||
Object.values(record.doctorContract).some((value) => value ?? false);
|
||||
if (doctorContractPath && declaresAnyDoctorSurface) {
|
||||
entries.push({
|
||||
pluginId: record.id,
|
||||
pluginRoot,
|
||||
|
||||
Reference in New Issue
Block a user