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
|
df567ce2f4a4ba8a0937f825c46e83763412a724e36b72ce727dc4203c7dd134 module/channel-ingress-runtime
|
||||||
0e6efb79730fae59bb549ad00d9af2848c139b4bb1762e83b66886284e1c1421 module/channel-lifecycle
|
0e6efb79730fae59bb549ad00d9af2848c139b4bb1762e83b66886284e1c1421 module/channel-lifecycle
|
||||||
159d034b431d113f3a6dc41ec0bcadba2d6664051f158330b0e3dd3da8b5d42f module/channel-logging
|
159d034b431d113f3a6dc41ec0bcadba2d6664051f158330b0e3dd3da8b5d42f module/channel-logging
|
||||||
e649231e5136e8a8045c098754adad4b2e7bf7acc29c759e02015d244f7f107a module/channel-message
|
3aaa573834c68f71490a0b0a8687cd942122cdca03d3b08781cf52220ab71544 module/channel-message
|
||||||
146c87e18187fbf90d7780d946b614c89c415e084c7206de2f0be1928a2b3434 module/channel-outbound
|
9182ed800c19f6324654dc62f0c78fc11e6a5ef18f4387284066e5b2104da848 module/channel-outbound
|
||||||
930beff13ed42a138f65164013c82f4f4c96422292c5fc634a5edee1dce71367 module/channel-pairing
|
930beff13ed42a138f65164013c82f4f4c96422292c5fc634a5edee1dce71367 module/channel-pairing
|
||||||
ee4292b069d4d48cce4fc2dc26df5b5c87eb1fa4769f1f6be9a10c3e1221e1a9 module/channel-plugin-common
|
ee4292b069d4d48cce4fc2dc26df5b5c87eb1fa4769f1f6be9a10c3e1221e1a9 module/channel-plugin-common
|
||||||
94ef57c8f6087fcaa56e59e493c391a04377ed03f23c681edd8d8f6e2d64e0da module/channel-policy
|
94ef57c8f6087fcaa56e59e493c391a04377ed03f23c681edd8d8f6e2d64e0da module/channel-policy
|
||||||
@@ -47,8 +47,8 @@ f3cee48527f5cddde81912a588e72947d40443322b716871a8d9e5dc0c37c542 module/channel
|
|||||||
7c90157a95bc0523fc66b1f78ce140f7ec7dbf809dfa3a480244efc01f972754 module/channel-send-result
|
7c90157a95bc0523fc66b1f78ce140f7ec7dbf809dfa3a480244efc01f972754 module/channel-send-result
|
||||||
fb123c1b557ed2527e335f13c3d6de41ab0c3305151001cf2b1075d1da8034c5 module/channel-setup
|
fb123c1b557ed2527e335f13c3d6de41ab0c3305151001cf2b1075d1da8034c5 module/channel-setup
|
||||||
aeac9bb8127faf636dde79772a577d9cb003660ef3d1df03ddf606101020c402 module/channel-status
|
aeac9bb8127faf636dde79772a577d9cb003660ef3d1df03ddf606101020c402 module/channel-status
|
||||||
f008d1d9ea73cd17e9833143bd117436599438523041fd157b6b6cb17500ac06 module/channel-streaming
|
d70ef71f48f9fe5dc01bf13ba3a2ba515322f81dc8f32d08528a113fbc78e30e module/channel-streaming
|
||||||
d1ff8728d99acf8130b94db7ba75ad7050a549c96365f56b8d4f601a533ca947 module/channel-streaming-config
|
7f4714752b40f014605e2e7d46006b519972f81b528a286e1a40758493fdef5d module/channel-streaming-config
|
||||||
14f0103adb14627b662fbe9fdd5ed08596ed702a250853e8beb8bb8dd1361588 module/cli-argv
|
14f0103adb14627b662fbe9fdd5ed08596ed702a250853e8beb8bb8dd1361588 module/cli-argv
|
||||||
c89ec1b194b76f67a6f4dd108dccf460da6065646cba31374c8aa748f23a39e4 module/collection-runtime
|
c89ec1b194b76f67a6f4dd108dccf460da6065646cba31374c8aa748f23a39e4 module/collection-runtime
|
||||||
391e6f0c77da2e17a058fc5aea87fd193830b2a14e12fae77c7f63b0226f0bb5 module/command-auth
|
391e6f0c77da2e17a058fc5aea87fd193830b2a14e12fae77c7f63b0226f0bb5 module/command-auth
|
||||||
@@ -76,7 +76,7 @@ d117ebba8cc490501725778676a9d75855872b6e5fe2b2f64b1270d4808a2277 module/health
|
|||||||
f6e3c44e7d1090a97aca554a3c247219b8de78b3cb4399cac5efde8a0a6c1156 module/inbound-envelope
|
f6e3c44e7d1090a97aca554a3c247219b8de78b3cb4399cac5efde8a0a6c1156 module/inbound-envelope
|
||||||
1d5ca69fcf2a8476a2309dcd830fc3bbae04b7029fa30eea1081b6590a789f10 module/inbound-event-delivery
|
1d5ca69fcf2a8476a2309dcd830fc3bbae04b7029fa30eea1081b6590a789f10 module/inbound-event-delivery
|
||||||
36721c58f479fe9ca32737f67850f4607bf8753ef6b193ff129e7a9e5fdbc6bd module/inbound-reply-dispatch
|
36721c58f479fe9ca32737f67850f4607bf8753ef6b193ff129e7a9e5fdbc6bd module/inbound-reply-dispatch
|
||||||
58675872c8985b66cfa32f3a1085bfb6d40c5abbafb3cd59d97b7bdde09dbe1a module/infra-runtime
|
03f7e1190d4101c486908383da099d545e59a6a90f5699b397c976ce6624c029 module/infra-runtime
|
||||||
2e717cccb3db127aed0287d4ea14c41a8e64e46d60c728638153e31e2fb0d296 module/ingress-effect-once
|
2e717cccb3db127aed0287d4ea14c41a8e64e46d60c728638153e31e2fb0d296 module/ingress-effect-once
|
||||||
9389d91a090259f06e5ec2aae14c9857e45988d4edc4f91170cb8bc573b966a2 module/interactive-runtime
|
9389d91a090259f06e5ec2aae14c9857e45988d4edc4f91170cb8bc573b966a2 module/interactive-runtime
|
||||||
9dd66baf2def46386ad4706380e57f9068fa9bb3878be2d9ba961ec2f46d3d87 module/json-store
|
9dd66baf2def46386ad4706380e57f9068fa9bb3878be2d9ba961ec2f46d3d87 module/json-store
|
||||||
@@ -132,8 +132,8 @@ f112bdabc51ba8659b37d0a6f6a32a2b1d471e5b49b56e108bf750ec55a7ea71 module/session
|
|||||||
df263e722a36ffa168522fa739924bec6b4b122ac3f1e25497a30962bec30607 module/setup-tools
|
df263e722a36ffa168522fa739924bec6b4b122ac3f1e25497a30962bec30607 module/setup-tools
|
||||||
18e384ec43d9eaee52c8e286e127bda2048370e2337964a754d94b236724ca9e module/skill-commands-runtime
|
18e384ec43d9eaee52c8e286e127bda2048370e2337964a754d94b236724ca9e module/skill-commands-runtime
|
||||||
ae469f32799380e6b045abaefefee6eb3f00d714ffbf36b6eeef5025dc529472 module/speech-settings
|
ae469f32799380e6b045abaefefee6eb3f00d714ffbf36b6eeef5025dc529472 module/speech-settings
|
||||||
9e521fe9073dfd1a6a6855f909fa6befe8613e18403f0a65faaba973a8b630c1 module/ssrf-policy
|
92f8e3fe8b29fa5050f30bcc7a19f1085a590064d9e8b6ac54991e60a691a61e module/ssrf-policy
|
||||||
f85d5be0f635de6a77bbd9224a37c373998e098a8dcf2048ddc2c9ac735b47ef module/ssrf-runtime
|
19c2ac7f81de3203ab91fc88793cf7e0b5bf4eaa2d5d61f4ee9e9f34e3f56be8 module/ssrf-runtime
|
||||||
ff35f9f74d35d37a2eb6126b57f3dc5a4d580b6222fe3a34c9368779ad32eab7 module/state-paths
|
ff35f9f74d35d37a2eb6126b57f3dc5a4d580b6222fe3a34c9368779ad32eab7 module/state-paths
|
||||||
44cc071d9ba2baa26d6f33ba9407867fb07c33384f49d3e3d5210232022c1b5f module/status-helpers
|
44cc071d9ba2baa26d6f33ba9407867fb07c33384f49d3e3d5210232022c1b5f module/status-helpers
|
||||||
537047854c21ad20ea0572f8019503cbda3bbafece8194a28c30b0639bde2fde module/string-coerce-runtime
|
537047854c21ad20ea0572f8019503cbda3bbafece8194a28c30b0639bde2fde module/string-coerce-runtime
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { createHash } from "node:crypto";
|
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 {
|
export function normalizeModelRef(raw?: string): string | null {
|
||||||
const value = raw?.trim();
|
const value = raw?.trim();
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Discord plugin module implements thread bindings.state behavior.
|
// 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 { normalizeAccountId, resolveAgentIdFromSessionKey } from "openclaw/plugin-sdk/routing";
|
||||||
import {
|
import {
|
||||||
normalizeLowercaseStringOrEmpty,
|
normalizeLowercaseStringOrEmpty,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// LLM Task doctor contract migrates shipped plugin-local completion policy.
|
// LLM Task doctor contract migrates shipped plugin-local completion policy.
|
||||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
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";
|
import { asObjectRecord } from "openclaw/plugin-sdk/runtime-doctor-migrations";
|
||||||
|
|
||||||
const ENTRY_PATH = "plugins.entries.llm-task";
|
const ENTRY_PATH = "plugins.entries.llm-task";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// Matrix plugin module implements auth presence behavior.
|
// Matrix plugin module implements auth presence behavior.
|
||||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
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 {
|
import {
|
||||||
MATRIX_CREDENTIALS_MAX_ENTRIES,
|
MATRIX_CREDENTIALS_MAX_ENTRIES,
|
||||||
MATRIX_CREDENTIALS_NAMESPACE,
|
MATRIX_CREDENTIALS_NAMESPACE,
|
||||||
|
|||||||
@@ -9,12 +9,10 @@ import {
|
|||||||
defineKeyMoveMigration,
|
defineKeyMoveMigration,
|
||||||
hasLegacyAccountStreamingAliases,
|
hasLegacyAccountStreamingAliases,
|
||||||
normalizeChannelConfigEntries,
|
normalizeChannelConfigEntries,
|
||||||
stripRetiredChannelKeys,
|
|
||||||
} from "openclaw/plugin-sdk/runtime-doctor-migrations";
|
|
||||||
import {
|
|
||||||
hasLegacyFlatAllowPrivateNetworkAlias,
|
hasLegacyFlatAllowPrivateNetworkAlias,
|
||||||
migrateLegacyFlatAllowPrivateNetworkAlias,
|
migrateLegacyFlatAllowPrivateNetworkAlias,
|
||||||
} from "openclaw/plugin-sdk/ssrf-runtime";
|
stripRetiredChannelKeys,
|
||||||
|
} from "openclaw/plugin-sdk/runtime-doctor-migrations";
|
||||||
import { isRecord } from "./record-shared.js";
|
import { isRecord } from "./record-shared.js";
|
||||||
import type { MatrixStreamingMode } from "./types.js";
|
import type { MatrixStreamingMode } from "./types.js";
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
detectPluginInstallPathIssue,
|
detectPluginInstallPathIssue,
|
||||||
formatPluginInstallPathIssue,
|
formatPluginInstallPathIssue,
|
||||||
removePluginFromConfig,
|
removePluginFromConfig,
|
||||||
} from "openclaw/plugin-sdk/runtime-doctor";
|
} from "openclaw/plugin-sdk/doctor-repair-runtime";
|
||||||
import {
|
import {
|
||||||
legacyConfigRules as MATRIX_LEGACY_CONFIG_RULES,
|
legacyConfigRules as MATRIX_LEGACY_CONFIG_RULES,
|
||||||
normalizeCompatibilityConfig as normalizeMatrixCompatibilityConfig,
|
normalizeCompatibilityConfig as normalizeMatrixCompatibilityConfig,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
// heavy sync plugin-state store access.
|
// heavy sync plugin-state store access.
|
||||||
import { normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
import { normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
||||||
import type { PluginStateSyncKeyedStore } from "openclaw/plugin-sdk/plugin-state-runtime";
|
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 { getOptionalMatrixRuntime } from "../runtime.js";
|
||||||
import {
|
import {
|
||||||
MATRIX_CREDENTIALS_MAX_ENTRIES,
|
MATRIX_CREDENTIALS_MAX_ENTRIES,
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
// Mattermost plugin module implements doctor contract behavior.
|
// Mattermost plugin module implements doctor contract behavior.
|
||||||
import type { ChannelDoctorConfigMutation } from "openclaw/plugin-sdk/channel-contract";
|
import type { ChannelDoctorConfigMutation } from "openclaw/plugin-sdk/channel-contract";
|
||||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||||
import { defineChannelAliasMigration } from "openclaw/plugin-sdk/runtime-doctor-migrations";
|
import {
|
||||||
import { createLegacyPrivateNetworkDoctorContract } from "openclaw/plugin-sdk/ssrf-runtime";
|
createLegacyPrivateNetworkDoctorContract,
|
||||||
|
defineChannelAliasMigration,
|
||||||
|
} from "openclaw/plugin-sdk/runtime-doctor-migrations";
|
||||||
|
|
||||||
const networkContract = createLegacyPrivateNetworkDoctorContract({
|
const networkContract = createLegacyPrivateNetworkDoctorContract({
|
||||||
channelKey: "mattermost",
|
channelKey: "mattermost",
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
// Nextcloud Talk plugin module implements doctor contract behavior.
|
// Nextcloud Talk plugin module implements doctor contract behavior.
|
||||||
import type { ChannelDoctorConfigMutation } from "openclaw/plugin-sdk/channel-contract";
|
import type { ChannelDoctorConfigMutation } from "openclaw/plugin-sdk/channel-contract";
|
||||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||||
import { defineChannelAliasMigration } from "openclaw/plugin-sdk/runtime-doctor-migrations";
|
import {
|
||||||
import { createLegacyPrivateNetworkDoctorContract } from "openclaw/plugin-sdk/ssrf-runtime";
|
createLegacyPrivateNetworkDoctorContract,
|
||||||
|
defineChannelAliasMigration,
|
||||||
|
} from "openclaw/plugin-sdk/runtime-doctor-migrations";
|
||||||
|
|
||||||
const networkContract = createLegacyPrivateNetworkDoctorContract({
|
const networkContract = createLegacyPrivateNetworkDoctorContract({
|
||||||
channelKey: "nextcloud-talk",
|
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 { formatErrorMessage as formatQaErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||||
import { formatMemoryDreamingDay } from "openclaw/plugin-sdk/memory-core-host-status";
|
import { formatMemoryDreamingDay } from "openclaw/plugin-sdk/memory-core-host-status";
|
||||||
import { resolveSessionTranscriptsDirForAgent } from "openclaw/plugin-sdk/memory-host-core";
|
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 { buildAgentSessionKey } from "openclaw/plugin-sdk/routing";
|
||||||
import { createPluginStateSyncKeyedStore } from "openclaw/plugin-sdk/runtime-doctor";
|
|
||||||
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
|
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||||
import * as browserRuntime from "./browser-runtime.js";
|
import * as browserRuntime from "./browser-runtime.js";
|
||||||
import * as cronRunWait from "./cron-run-wait.js";
|
import * as cronRunWait from "./cron-run-wait.js";
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import type {
|
|||||||
ChannelDoctorConfigMutation,
|
ChannelDoctorConfigMutation,
|
||||||
ChannelDoctorLegacyConfigRule,
|
ChannelDoctorLegacyConfigRule,
|
||||||
} from "openclaw/plugin-sdk/channel-contract";
|
} 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 type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||||
import { defineChannelAliasMigration } from "openclaw/plugin-sdk/runtime-doctor-migrations";
|
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";
|
import { migrateLegacySignalTransportConfigSync } from "./src/config-compat.js";
|
||||||
|
|
||||||
const RETIRED_SIGNAL_ACCOUNT_TRANSPORT_FIELDS = [
|
const RETIRED_SIGNAL_ACCOUNT_TRANSPORT_FIELDS = [
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-resolution";
|
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-resolution";
|
||||||
// Signal compatibility migration moves shipped flat transport config into account ownership.
|
// Signal compatibility migration moves shipped flat transport config into account ownership.
|
||||||
import type { ChannelDoctorConfigMutation } from "openclaw/plugin-sdk/channel-contract";
|
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 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 type { SignalTransportConfig } from "./account-types.js";
|
||||||
import {
|
import {
|
||||||
allocateSignalManagedNativePort,
|
allocateSignalManagedNativePort,
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
requireAllowlistAllowFrom,
|
requireAllowlistAllowFrom,
|
||||||
requireOpenAllowFrom,
|
requireOpenAllowFrom,
|
||||||
} from "openclaw/plugin-sdk/channel-config-schema";
|
} 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 { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { signalChannelConfigUiHints } from "./config-ui-hints.js";
|
import { signalChannelConfigUiHints } from "./config-ui-hints.js";
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import {
|
|||||||
type ChannelIngressMonitorDeliveryResult,
|
type ChannelIngressMonitorDeliveryResult,
|
||||||
type ChannelIngressMonitorLifecycle,
|
type ChannelIngressMonitorLifecycle,
|
||||||
} from "openclaw/plugin-sdk/channel-outbound";
|
} 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 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 { normalizeNullableString as normalizeRawString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||||
import type { SignalSseEvent } from "./client-adapter.js";
|
import type { SignalSseEvent } from "./client-adapter.js";
|
||||||
import { getOptionalSignalRuntime } from "./runtime.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 { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||||
import type { PluginStateSyncKeyedStore } from "openclaw/plugin-sdk/plugin-state-runtime";
|
import type { PluginStateSyncKeyedStore } from "openclaw/plugin-sdk/plugin-state-runtime";
|
||||||
import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
|
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";
|
import { getTelegramRuntime } from "./runtime.js";
|
||||||
|
|
||||||
const TTL_MS = 24 * 60 * 60 * 1000;
|
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 { ChannelLegacyStateMigrationPlan } from "openclaw/plugin-sdk/channel-contract";
|
||||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||||
import { fileExists } from "openclaw/plugin-sdk/security-runtime";
|
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 { isRecord, uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||||
import { listTelegramAccountIds, resolveDefaultTelegramAccountId } from "./account-selection.js";
|
import { listTelegramAccountIds, resolveDefaultTelegramAccountId } from "./account-selection.js";
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Tlon plugin module implements doctor contract behavior.
|
// 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({
|
const contract = createLegacyPrivateNetworkDoctorContract({
|
||||||
channelKey: "tlon",
|
channelKey: "tlon",
|
||||||
|
|||||||
@@ -11,9 +11,6 @@
|
|||||||
"openclaw/plugin-sdk/sandbox": [
|
"openclaw/plugin-sdk/sandbox": [
|
||||||
"../packages/plugin-sdk/dist/src/plugin-sdk/sandbox.d.ts"
|
"../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": [
|
"openclaw/plugin-sdk/runtime-doctor-migrations": [
|
||||||
"../packages/plugin-sdk/dist/src/plugin-sdk/runtime-doctor-migrations.d.ts"
|
"../packages/plugin-sdk/dist/src/plugin-sdk/runtime-doctor-migrations.d.ts"
|
||||||
],
|
],
|
||||||
@@ -879,6 +876,21 @@
|
|||||||
],
|
],
|
||||||
"@openclaw/plugin-sdk/*": [
|
"@openclaw/plugin-sdk/*": [
|
||||||
"../dist/plugin-sdk/*.d.ts"
|
"../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 fs from "node:fs/promises";
|
||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import path from "node:path";
|
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 type { OpenClawConfig } from "openclaw/plugin-sdk/plugin-entry";
|
||||||
import { normalizeAgentId } from "openclaw/plugin-sdk/routing";
|
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 {
|
import {
|
||||||
archiveLegacyStateSource,
|
archiveLegacyStateSource,
|
||||||
type PluginDoctorStateMigration,
|
type PluginDoctorStateMigration,
|
||||||
@@ -304,7 +304,7 @@ export const stateMigrations: PluginDoctorStateMigration[] = [
|
|||||||
label: "Voice Call call log",
|
label: "Voice Call call log",
|
||||||
async detectLegacyState(params) {
|
async detectLegacyState(params) {
|
||||||
const { detectOpenClawStateDatabaseSchemaMigrations } =
|
const { detectOpenClawStateDatabaseSchemaMigrations } =
|
||||||
await import("openclaw/plugin-sdk/runtime-doctor");
|
await import("openclaw/plugin-sdk/doctor-repair-runtime");
|
||||||
const storePath = resolveVoiceCallStorePath(params);
|
const storePath = resolveVoiceCallStorePath(params);
|
||||||
const filePath = resolveVoiceCallLegacyCallLogPath(storePath);
|
const filePath = resolveVoiceCallLegacyCallLogPath(storePath);
|
||||||
const { entries } = await readLegacyCallRecords(filePath);
|
const { entries } = await readLegacyCallRecords(filePath);
|
||||||
@@ -330,7 +330,7 @@ export const stateMigrations: PluginDoctorStateMigration[] = [
|
|||||||
},
|
},
|
||||||
async migrateLegacyState(params) {
|
async migrateLegacyState(params) {
|
||||||
const { detectOpenClawStateDatabaseSchemaMigrations, repairOpenClawStateDatabaseSchema } =
|
const { detectOpenClawStateDatabaseSchemaMigrations, repairOpenClawStateDatabaseSchema } =
|
||||||
await import("openclaw/plugin-sdk/runtime-doctor");
|
await import("openclaw/plugin-sdk/doctor-repair-runtime");
|
||||||
const changes: string[] = [];
|
const changes: string[] = [];
|
||||||
const warnings: string[] = [];
|
const warnings: string[] = [];
|
||||||
const storePath = resolveVoiceCallStorePath(params);
|
const storePath = resolveVoiceCallStorePath(params);
|
||||||
|
|||||||
@@ -11,9 +11,6 @@
|
|||||||
"openclaw/plugin-sdk/sandbox": [
|
"openclaw/plugin-sdk/sandbox": [
|
||||||
"../../packages/plugin-sdk/dist/src/plugin-sdk/sandbox.d.ts"
|
"../../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": [
|
"openclaw/plugin-sdk/runtime-doctor-migrations": [
|
||||||
"../../packages/plugin-sdk/dist/src/plugin-sdk/runtime-doctor-migrations.d.ts"
|
"../../packages/plugin-sdk/dist/src/plugin-sdk/runtime-doctor-migrations.d.ts"
|
||||||
],
|
],
|
||||||
@@ -866,6 +863,21 @@
|
|||||||
],
|
],
|
||||||
"@openclaw/ollama/runtime-api.js": [
|
"@openclaw/ollama/runtime-api.js": [
|
||||||
"./.boundary-stubs/ollama-runtime-api.d.ts"
|
"./.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/response-limit-runtime.d.ts",
|
||||||
"!dist/plugin-sdk/retry-runtime.d.ts",
|
"!dist/plugin-sdk/retry-runtime.d.ts",
|
||||||
"!dist/plugin-sdk/runtime-doctor-migrations.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-doctor.d.ts",
|
||||||
"!dist/plugin-sdk/runtime-fetch.d.ts",
|
"!dist/plugin-sdk/runtime-fetch.d.ts",
|
||||||
"!dist/plugin-sdk/sandbox.d.ts",
|
"!dist/plugin-sdk/sandbox.d.ts",
|
||||||
@@ -402,8 +407,20 @@
|
|||||||
"types": "./dist/plugin-sdk/health.d.ts",
|
"types": "./dist/plugin-sdk/health.d.ts",
|
||||||
"default": "./dist/plugin-sdk/health.js"
|
"default": "./dist/plugin-sdk/health.js"
|
||||||
},
|
},
|
||||||
"./plugin-sdk/runtime-doctor": {
|
"./plugin-sdk/doctor-repair-runtime": {
|
||||||
"default": "./dist/plugin-sdk/runtime-doctor.js"
|
"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": {
|
"./plugin-sdk/runtime-doctor-migrations": {
|
||||||
"default": "./dist/plugin-sdk/runtime-doctor-migrations.js"
|
"default": "./dist/plugin-sdk/runtime-doctor-migrations.js"
|
||||||
|
|||||||
@@ -192,10 +192,6 @@
|
|||||||
"types": "./dist/src/plugin-sdk/provider-web-search-config-contract.d.ts",
|
"types": "./dist/src/plugin-sdk/provider-web-search-config-contract.d.ts",
|
||||||
"default": "./src/provider-web-search-config-contract.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": {
|
"./runtime-env": {
|
||||||
"types": "./dist/src/plugin-sdk/runtime-env.d.ts",
|
"types": "./dist/src/plugin-sdk/runtime-env.d.ts",
|
||||||
"default": "./src/runtime-env.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",
|
"routing",
|
||||||
"runtime",
|
"runtime",
|
||||||
"health",
|
"health",
|
||||||
"runtime-doctor",
|
"doctor-repair-runtime",
|
||||||
|
"model-ref-parse",
|
||||||
|
"outbound-echo-runtime",
|
||||||
|
"session-store-paths",
|
||||||
|
"plugin-state-store-runtime",
|
||||||
"runtime-doctor-migrations",
|
"runtime-doctor-migrations",
|
||||||
"runtime-env",
|
"runtime-env",
|
||||||
"proxy-capture",
|
"proxy-capture",
|
||||||
|
|||||||
@@ -40,6 +40,7 @@
|
|||||||
"delivery-queue-runtime",
|
"delivery-queue-runtime",
|
||||||
"direct-dm-guard-policy",
|
"direct-dm-guard-policy",
|
||||||
"directory-config-runtime",
|
"directory-config-runtime",
|
||||||
|
"doctor-repair-runtime",
|
||||||
"document-extractor",
|
"document-extractor",
|
||||||
"embedding-providers",
|
"embedding-providers",
|
||||||
"exec-approvals-runtime",
|
"exec-approvals-runtime",
|
||||||
@@ -80,12 +81,15 @@
|
|||||||
"message-tool-delivery-hints",
|
"message-tool-delivery-hints",
|
||||||
"migration",
|
"migration",
|
||||||
"migration-runtime",
|
"migration-runtime",
|
||||||
|
"model-ref-parse",
|
||||||
"music-generation",
|
"music-generation",
|
||||||
"node-host",
|
"node-host",
|
||||||
"number-runtime",
|
"number-runtime",
|
||||||
|
"outbound-echo-runtime",
|
||||||
"outbound-media",
|
"outbound-media",
|
||||||
"pair-loop-guard-runtime",
|
"pair-loop-guard-runtime",
|
||||||
"plugin-state-runtime",
|
"plugin-state-runtime",
|
||||||
|
"plugin-state-store-runtime",
|
||||||
"plugin-state-test-runtime",
|
"plugin-state-test-runtime",
|
||||||
"plugin-test-api",
|
"plugin-test-api",
|
||||||
"plugin-test-contracts",
|
"plugin-test-contracts",
|
||||||
@@ -127,15 +131,14 @@
|
|||||||
"qa-runtime",
|
"qa-runtime",
|
||||||
"realtime-bootstrap-context",
|
"realtime-bootstrap-context",
|
||||||
"realtime-transcription",
|
"realtime-transcription",
|
||||||
"realtime-voice-audio-queue",
|
|
||||||
"realtime-voice-activation",
|
|
||||||
"realtime-voice",
|
"realtime-voice",
|
||||||
|
"realtime-voice-activation",
|
||||||
|
"realtime-voice-audio-queue",
|
||||||
"reply-payload-testing",
|
"reply-payload-testing",
|
||||||
"reply-reference",
|
"reply-reference",
|
||||||
"request-url",
|
"request-url",
|
||||||
"response-limit-runtime",
|
"response-limit-runtime",
|
||||||
"retry-runtime",
|
"retry-runtime",
|
||||||
"runtime-doctor",
|
|
||||||
"runtime-doctor-migrations",
|
"runtime-doctor-migrations",
|
||||||
"runtime-fetch",
|
"runtime-fetch",
|
||||||
"sandbox",
|
"sandbox",
|
||||||
@@ -144,6 +147,7 @@
|
|||||||
"session-binding-runtime",
|
"session-binding-runtime",
|
||||||
"session-catalog-runtime",
|
"session-catalog-runtime",
|
||||||
"session-key-runtime",
|
"session-key-runtime",
|
||||||
|
"session-store-paths",
|
||||||
"session-transcript-hit",
|
"session-transcript-hit",
|
||||||
"session-transcript-runtime",
|
"session-transcript-runtime",
|
||||||
"session-visibility",
|
"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,
|
BlockStreamingChunkConfig,
|
||||||
BlockStreamingCoalesceConfig,
|
BlockStreamingCoalesceConfig,
|
||||||
ChannelStreamingCommandTextMode,
|
ChannelStreamingCommandTextMode,
|
||||||
ChannelStreamingConfig,
|
|
||||||
ChannelStreamingProgressConfig,
|
ChannelStreamingProgressConfig,
|
||||||
StreamingMode,
|
StreamingMode,
|
||||||
TextChunkMode,
|
TextChunkMode,
|
||||||
@@ -23,8 +22,16 @@ import {
|
|||||||
selectProgressLabel,
|
selectProgressLabel,
|
||||||
} from "../shared/progress-labels.js";
|
} from "../shared/progress-labels.js";
|
||||||
import { asBoolean } from "../utils/boolean.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 {
|
export type {
|
||||||
ChannelDeliveryStreamingConfig,
|
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(
|
export function resolveChannelStreamingChunkMode(
|
||||||
entry: StreamingCompatEntry | null | undefined,
|
entry: StreamingCompatEntry | null | undefined,
|
||||||
): TextChunkMode | undefined {
|
): TextChunkMode | undefined {
|
||||||
@@ -911,12 +911,6 @@ export function resolveChannelStreamingSuppressDefaultToolProgressMessages(
|
|||||||
return options?.previewToolProgressEnabled ?? resolveChannelStreamingPreviewToolProgress(entry);
|
return options?.previewToolProgressEnabled ?? resolveChannelStreamingPreviewToolProgress(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveChannelStreamingNativeTransport(
|
|
||||||
entry: StreamingCompatEntry | null | undefined,
|
|
||||||
): boolean | undefined {
|
|
||||||
return asBoolean(getChannelStreamingConfigObject(entry)?.nativeTransport);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resolveChannelPreviewStreamMode(
|
export function resolveChannelPreviewStreamMode(
|
||||||
entry: StreamingCompatEntry | null | undefined,
|
entry: StreamingCompatEntry | null | undefined,
|
||||||
defaultMode: StreamingMode,
|
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 {
|
export {
|
||||||
getChannelStreamingConfigObject,
|
getChannelStreamingConfigObject,
|
||||||
resolveChannelStreamingNativeTransport,
|
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 { collectProviderDangerousNameMatchingScopes } from "../config/dangerous-name-matching.js";
|
||||||
export { defineChannelAliasMigration } from "../config/channel-alias-migration.js";
|
export { defineChannelAliasMigration } from "../config/channel-alias-migration.js";
|
||||||
|
export {
|
||||||
|
createLegacyPrivateNetworkDoctorContract,
|
||||||
|
hasLegacyFlatAllowPrivateNetworkAlias,
|
||||||
|
migrateLegacyFlatAllowPrivateNetworkAlias,
|
||||||
|
} from "../config/legacy-private-network-migration.js";
|
||||||
export type {
|
export type {
|
||||||
ChannelAliasMigrationSpec,
|
ChannelAliasMigrationSpec,
|
||||||
StreamingAliasMode,
|
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 LookupFn,
|
||||||
type SsrFPolicy,
|
type SsrFPolicy,
|
||||||
} from "../infra/net/ssrf.js";
|
} from "../infra/net/ssrf.js";
|
||||||
import type {
|
|
||||||
ChannelDoctorConfigMutation,
|
|
||||||
ChannelDoctorLegacyConfigRule,
|
|
||||||
} from "./channel-contract.js";
|
|
||||||
import type { OpenClawConfig } from "./config-runtime.js";
|
|
||||||
|
|
||||||
export { isPrivateIpAddress, mergeSsrFPolicies };
|
export { isPrivateIpAddress, mergeSsrFPolicies };
|
||||||
export type { SsrFPolicy };
|
export type { SsrFPolicy };
|
||||||
@@ -69,147 +64,14 @@ export function ssrfPolicyFromDangerouslyAllowPrivateNetwork(
|
|||||||
return ssrfPolicyFromPrivateNetworkOptIn(dangerouslyAllowPrivateNetwork);
|
return ssrfPolicyFromPrivateNetworkOptIn(dangerouslyAllowPrivateNetwork);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Detects the retired flat `allowPrivateNetwork` key before doctor migration. */
|
// Config-shape doctor migration lives in a leaf module so doctor contract
|
||||||
export function hasLegacyFlatAllowPrivateNetworkAlias(value: unknown): boolean {
|
// closures never load this file's SSRF runtime graph; re-exported here for
|
||||||
const entry = asNullableRecord(value);
|
// existing callers.
|
||||||
return Boolean(entry && Object.hasOwn(entry, "allowPrivateNetwork"));
|
export {
|
||||||
}
|
createLegacyPrivateNetworkDoctorContract,
|
||||||
|
hasLegacyFlatAllowPrivateNetworkAlias,
|
||||||
/** Moves flat private-network config into `network.dangerouslyAllowPrivateNetwork`. */
|
migrateLegacyFlatAllowPrivateNetworkAlias,
|
||||||
export function migrateLegacyFlatAllowPrivateNetworkAlias(params: {
|
} from "../config/legacy-private-network-migration.js";
|
||||||
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,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @deprecated Use `ssrfPolicyFromDangerouslyAllowPrivateNetwork`. */
|
/** @deprecated Use `ssrfPolicyFromDangerouslyAllowPrivateNetwork`. */
|
||||||
export function ssrfPolicyFromAllowPrivateNetwork(
|
export function ssrfPolicyFromAllowPrivateNetwork(
|
||||||
|
|||||||
@@ -264,8 +264,8 @@ describe("opt-in extension package boundaries", () => {
|
|||||||
expect(packageJson.exports?.["./provider-web-search-config-contract"]?.types).toBe(
|
expect(packageJson.exports?.["./provider-web-search-config-contract"]?.types).toBe(
|
||||||
"./dist/src/plugin-sdk/provider-web-search-config-contract.d.ts",
|
"./dist/src/plugin-sdk/provider-web-search-config-contract.d.ts",
|
||||||
);
|
);
|
||||||
expect(packageJson.exports?.["./runtime-doctor"]?.types).toBe(
|
expect(packageJson.exports?.["./runtime-env"]?.types).toBe(
|
||||||
"./dist/src/plugin-sdk/runtime-doctor.d.ts",
|
"./dist/src/plugin-sdk/runtime-env.d.ts",
|
||||||
);
|
);
|
||||||
expect(packageJson.exports?.["./security-runtime"]?.types).toBe(
|
expect(packageJson.exports?.["./security-runtime"]?.types).toBe(
|
||||||
"./dist/src/plugin-sdk/security-runtime.d.ts",
|
"./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:
|
reason:
|
||||||
"the heavy doctor barrel makes doctor enumeration cold-load the state-db/kysely graph; " +
|
"install-path, uninstall, and state-db schema repair cold-load the state-db/kysely graph; " +
|
||||||
"use openclaw/plugin-sdk/runtime-doctor-migrations, or defer heavy helpers behind a dynamic import",
|
"use openclaw/plugin-sdk/runtime-doctor-migrations, or defer the repair behind a dynamic import",
|
||||||
kinds: new Set(["doctor-contract", "legacy-setup"]),
|
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",
|
"openclaw/plugin-sdk/session-store-runtime",
|
||||||
{
|
{
|
||||||
@@ -221,7 +257,13 @@ function collectClosureEntries(): ClosureEntry[] {
|
|||||||
for (const record of loadBundledPluginManifestRegistry({ env }).plugins) {
|
for (const record of loadBundledPluginManifestRegistry({ env }).plugins) {
|
||||||
const pluginRoot = path.resolve(record.rootDir);
|
const pluginRoot = path.resolve(record.rootDir);
|
||||||
const doctorContractPath = resolvePluginDoctorContractArtifactPath(pluginRoot);
|
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({
|
entries.push({
|
||||||
pluginId: record.id,
|
pluginId: record.id,
|
||||||
pluginRoot,
|
pluginRoot,
|
||||||
|
|||||||
Reference in New Issue
Block a user