refactor(plugin-sdk): split runtime helper seams

This commit is contained in:
Peter Steinberger
2026-04-04 08:53:12 +01:00
parent 470898b5e1
commit edfaa01d1d
57 changed files with 605 additions and 333 deletions

View File

@@ -31,6 +31,13 @@ type BundledChannelDiscoveryCandidate = {
};
};
const BUNDLED_CHANNEL_ENTRY_BASENAMES = [
"channel-entry.ts",
"channel-entry.mts",
"channel-entry.js",
"channel-entry.mjs",
] as const;
const log = createSubsystemLogger("channels");
function resolveChannelPluginModuleEntry(
@@ -171,6 +178,12 @@ function resolvePreferredBundledChannelSource(
candidate: BundledChannelDiscoveryCandidate,
manifest: ReturnType<typeof loadPluginManifestRegistry>["plugins"][number],
): string {
for (const basename of BUNDLED_CHANNEL_ENTRY_BASENAMES) {
const preferred = resolveCompiledBundledModulePath(path.resolve(candidate.rootDir, basename));
if (fs.existsSync(preferred)) {
return preferred;
}
}
const declaredEntry = candidate.packageManifest?.extensions?.find(
(entry): entry is string => typeof entry === "string" && entry.trim().length > 0,
);

View File

@@ -41,6 +41,15 @@ import { applyMergePatch } from "./merge-patch.js";
import { resolveConfigPath, resolveStateDir } from "./paths.js";
import { isBlockedObjectKey } from "./prototype-keys.js";
import { applyConfigOverrides } from "./runtime-overrides.js";
import {
clearRuntimeConfigSnapshot as clearRuntimeConfigSnapshotState,
getRuntimeConfigSnapshot as getRuntimeConfigSnapshotState,
getRuntimeConfigSnapshotRefreshHandler,
getRuntimeConfigSourceSnapshot as getRuntimeConfigSourceSnapshotState,
resetConfigRuntimeState as resetConfigRuntimeStateState,
setRuntimeConfigSnapshot as setRuntimeConfigSnapshotState,
setRuntimeConfigSnapshotRefreshHandler as setRuntimeConfigSnapshotRefreshHandlerState,
} from "./runtime-snapshot.js";
import type { OpenClawConfig, ConfigFileSnapshot, LegacyConfigIssue } from "./types.js";
import {
validateConfigObjectRawWithPlugins,
@@ -48,6 +57,15 @@ import {
} from "./validation.js";
import { shouldWarnOnTouchedVersion } from "./version.js";
export {
clearRuntimeConfigSnapshotState as clearRuntimeConfigSnapshot,
getRuntimeConfigSnapshotState as getRuntimeConfigSnapshot,
getRuntimeConfigSourceSnapshotState as getRuntimeConfigSourceSnapshot,
resetConfigRuntimeStateState as resetConfigRuntimeState,
setRuntimeConfigSnapshotState as setRuntimeConfigSnapshot,
setRuntimeConfigSnapshotRefreshHandlerState as setRuntimeConfigSnapshotRefreshHandler,
};
// Re-export for backwards compatibility
export { CircularIncludeError, ConfigIncludeError } from "./includes.js";
export { MissingEnvVarError } from "./env-substitution.js";
@@ -228,15 +246,6 @@ export type ReadConfigFileSnapshotForWriteResult = {
writeOptions: ConfigWriteOptions;
};
export type RuntimeConfigSnapshotRefreshParams = {
sourceConfig: OpenClawConfig;
};
export type RuntimeConfigSnapshotRefreshHandler = {
refresh: (params: RuntimeConfigSnapshotRefreshParams) => boolean | Promise<boolean>;
clearOnRefreshFailure?: () => void;
};
export type ConfigWriteNotification = {
configPath: string;
sourceConfig: OpenClawConfig;
@@ -2371,9 +2380,6 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
const AUTO_OWNER_DISPLAY_SECRET_BY_PATH = new Map<string, string>();
const AUTO_OWNER_DISPLAY_SECRET_PERSIST_IN_FLIGHT = new Set<string>();
const AUTO_OWNER_DISPLAY_SECRET_PERSIST_WARNED = new Set<string>();
let runtimeConfigSnapshot: OpenClawConfig | null = null;
let runtimeConfigSourceSnapshot: OpenClawConfig | null = null;
let runtimeConfigSnapshotRefreshHandler: RuntimeConfigSnapshotRefreshHandler | null = null;
const configWriteListeners = new Set<(event: ConfigWriteNotification) => void>();
function notifyConfigWriteListeners(event: ConfigWriteNotification): void {
@@ -2399,31 +2405,6 @@ export function registerConfigWriteListener(
};
}
export function setRuntimeConfigSnapshot(
config: OpenClawConfig,
sourceConfig?: OpenClawConfig,
): void {
runtimeConfigSnapshot = config;
runtimeConfigSourceSnapshot = sourceConfig ?? null;
}
export function resetConfigRuntimeState(): void {
runtimeConfigSnapshot = null;
runtimeConfigSourceSnapshot = null;
}
export function clearRuntimeConfigSnapshot(): void {
resetConfigRuntimeState();
}
export function getRuntimeConfigSnapshot(): OpenClawConfig | null {
return runtimeConfigSnapshot;
}
export function getRuntimeConfigSourceSnapshot(): OpenClawConfig | null {
return runtimeConfigSourceSnapshot;
}
function isCompatibleTopLevelRuntimeProjectionShape(params: {
runtimeSnapshot: OpenClawConfig;
candidate: OpenClawConfig;
@@ -2454,6 +2435,8 @@ function isCompatibleTopLevelRuntimeProjectionShape(params: {
}
export function projectConfigOntoRuntimeSourceSnapshot(config: OpenClawConfig): OpenClawConfig {
const runtimeConfigSnapshot = getRuntimeConfigSnapshotState();
const runtimeConfigSourceSnapshot = getRuntimeConfigSourceSnapshotState();
if (!runtimeConfigSnapshot || !runtimeConfigSourceSnapshot) {
return config;
}
@@ -2476,13 +2459,8 @@ export function projectConfigOntoRuntimeSourceSnapshot(config: OpenClawConfig):
return coerceConfig(applyMergePatch(runtimeConfigSourceSnapshot, runtimePatch));
}
export function setRuntimeConfigSnapshotRefreshHandler(
refreshHandler: RuntimeConfigSnapshotRefreshHandler | null,
): void {
runtimeConfigSnapshotRefreshHandler = refreshHandler;
}
export function loadConfig(): OpenClawConfig {
const runtimeConfigSnapshot = getRuntimeConfigSnapshotState();
if (runtimeConfigSnapshot) {
return runtimeConfigSnapshot;
}
@@ -2490,8 +2468,8 @@ export function loadConfig(): OpenClawConfig {
// First successful load becomes the process snapshot. Long-lived runtimes
// should swap this snapshot via explicit reload/watcher paths instead of
// reparsing openclaw.json on hot code paths.
setRuntimeConfigSnapshot(config);
return runtimeConfigSnapshot ?? config;
setRuntimeConfigSnapshotState(config);
return getRuntimeConfigSnapshotState() ?? config;
}
export function getRuntimeConfig(): OpenClawConfig {
@@ -2525,6 +2503,8 @@ export async function writeConfigFile(
): Promise<void> {
const io = createConfigIO();
let nextCfg = cfg;
const runtimeConfigSnapshot = getRuntimeConfigSnapshotState();
const runtimeConfigSourceSnapshot = getRuntimeConfigSourceSnapshotState();
const hadRuntimeSnapshot = Boolean(runtimeConfigSnapshot);
const hadBothSnapshots = Boolean(runtimeConfigSnapshot && runtimeConfigSourceSnapshot);
if (hadBothSnapshots) {
@@ -2538,20 +2518,21 @@ export async function writeConfigFile(
unsetPaths: options.unsetPaths,
});
const notifyCommittedWrite = () => {
if (!runtimeConfigSnapshot) {
const currentRuntimeConfig = getRuntimeConfigSnapshotState();
if (!currentRuntimeConfig) {
return;
}
notifyConfigWriteListeners({
configPath: io.configPath,
sourceConfig: nextCfg,
runtimeConfig: runtimeConfigSnapshot,
runtimeConfig: currentRuntimeConfig,
persistedHash: writeResult.persistedHash,
writtenAtMs: Date.now(),
});
};
// Keep the last-known-good runtime snapshot active until the specialized refresh path
// succeeds, so concurrent readers do not observe unresolved SecretRefs mid-refresh.
const refreshHandler = runtimeConfigSnapshotRefreshHandler;
const refreshHandler = getRuntimeConfigSnapshotRefreshHandler();
if (refreshHandler) {
try {
const refreshed = await refreshHandler.refresh({ sourceConfig: nextCfg });
@@ -2576,16 +2557,16 @@ export async function writeConfigFile(
// Refresh both snapshots from disk atomically so follow-up reads get normalized config and
// subsequent writes still get secret-preservation merge-patch (hadBothSnapshots stays true).
const fresh = io.loadConfig();
setRuntimeConfigSnapshot(fresh, nextCfg);
setRuntimeConfigSnapshotState(fresh, nextCfg);
notifyCommittedWrite();
return;
}
if (hadRuntimeSnapshot) {
const fresh = io.loadConfig();
setRuntimeConfigSnapshot(fresh);
setRuntimeConfigSnapshotState(fresh);
notifyCommittedWrite();
return;
}
setRuntimeConfigSnapshot(io.loadConfig());
setRuntimeConfigSnapshotState(io.loadConfig());
notifyCommittedWrite();
}

View File

@@ -31,7 +31,12 @@ function buildDefaultTableModes(): Map<string, MarkdownTableMode> {
);
}
export const DEFAULT_TABLE_MODES = buildDefaultTableModes();
let cachedDefaultTableModes: Map<string, MarkdownTableMode> | null = null;
function getDefaultTableModes(): Map<string, MarkdownTableMode> {
cachedDefaultTableModes ??= buildDefaultTableModes();
return cachedDefaultTableModes;
}
const isMarkdownTableMode = (value: unknown): value is MarkdownTableMode =>
value === "off" || value === "bullets" || value === "code" || value === "block";
@@ -62,7 +67,7 @@ export function resolveMarkdownTableMode(params: {
accountId?: string | null;
}): MarkdownTableMode {
const channel = normalizeChannelId(params.channel);
const defaultMode = channel ? (DEFAULT_TABLE_MODES.get(channel) ?? "code") : "code";
const defaultMode = channel ? (getDefaultTableModes().get(channel) ?? "code") : "code";
if (!channel || !params.cfg) {
return defaultMode;
}

View File

@@ -0,0 +1,49 @@
import type { OpenClawConfig } from "./types.js";
export type RuntimeConfigSnapshotRefreshParams = {
sourceConfig: OpenClawConfig;
};
export type RuntimeConfigSnapshotRefreshHandler = {
refresh: (params: RuntimeConfigSnapshotRefreshParams) => boolean | Promise<boolean>;
clearOnRefreshFailure?: () => void;
};
let runtimeConfigSnapshot: OpenClawConfig | null = null;
let runtimeConfigSourceSnapshot: OpenClawConfig | null = null;
let runtimeConfigSnapshotRefreshHandler: RuntimeConfigSnapshotRefreshHandler | null = null;
export function setRuntimeConfigSnapshot(
config: OpenClawConfig,
sourceConfig?: OpenClawConfig,
): void {
runtimeConfigSnapshot = config;
runtimeConfigSourceSnapshot = sourceConfig ?? null;
}
export function resetConfigRuntimeState(): void {
runtimeConfigSnapshot = null;
runtimeConfigSourceSnapshot = null;
}
export function clearRuntimeConfigSnapshot(): void {
resetConfigRuntimeState();
}
export function getRuntimeConfigSnapshot(): OpenClawConfig | null {
return runtimeConfigSnapshot;
}
export function getRuntimeConfigSourceSnapshot(): OpenClawConfig | null {
return runtimeConfigSourceSnapshot;
}
export function setRuntimeConfigSnapshotRefreshHandler(
refreshHandler: RuntimeConfigSnapshotRefreshHandler | null,
): void {
runtimeConfigSnapshotRefreshHandler = refreshHandler;
}
export function getRuntimeConfigSnapshotRefreshHandler(): RuntimeConfigSnapshotRefreshHandler | null {
return runtimeConfigSnapshotRefreshHandler;
}

View File

@@ -0,0 +1,118 @@
import type { OpenClawConfig } from "../config/config.js";
import { resolveStorePath } from "../config/sessions/paths.js";
import { loadSessionStore } from "../config/sessions/store-load.js";
import { normalizeOptionalAccountId } from "../routing/account-id.js";
import { parseAgentSessionKey } from "../routing/session-key.js";
import { normalizeMessageChannel } from "../utils/message-channel.js";
import type { ExecApprovalRequest } from "./exec-approvals.js";
import type { PluginApprovalRequest } from "./plugin-approvals.js";
type ApprovalRequestLike = ExecApprovalRequest | PluginApprovalRequest;
type ApprovalRequestSessionBinding = {
channel?: string;
accountId?: string;
};
function normalizeOptionalString(value?: string | null): string | undefined {
const normalized = value?.trim();
return normalized ? normalized : undefined;
}
function normalizeOptionalChannel(value?: string | null): string | undefined {
return normalizeMessageChannel(value);
}
function resolvePersistedApprovalRequestSessionBinding(params: {
cfg: OpenClawConfig;
request: ApprovalRequestLike;
}): ApprovalRequestSessionBinding | null {
const sessionKey = normalizeOptionalString(params.request.request.sessionKey);
if (!sessionKey) {
return null;
}
const parsed = parseAgentSessionKey(sessionKey);
const agentId = parsed?.agentId ?? params.request.request.agentId ?? "main";
const storePath = resolveStorePath(params.cfg.session?.store, { agentId });
const store = loadSessionStore(storePath);
const entry = store[sessionKey];
if (!entry) {
return null;
}
const channel = normalizeOptionalChannel(entry.origin?.provider ?? entry.lastChannel);
const accountId = normalizeOptionalAccountId(entry.origin?.accountId ?? entry.lastAccountId);
return channel || accountId ? { channel, accountId } : null;
}
export function resolveApprovalRequestAccountId(params: {
cfg: OpenClawConfig;
request: ApprovalRequestLike;
channel?: string | null;
}): string | null {
const expectedChannel = normalizeOptionalChannel(params.channel);
const turnSourceChannel = normalizeOptionalChannel(params.request.request.turnSourceChannel);
if (expectedChannel && turnSourceChannel && turnSourceChannel !== expectedChannel) {
return null;
}
const turnSourceAccountId = normalizeOptionalAccountId(
params.request.request.turnSourceAccountId,
);
if (turnSourceAccountId) {
return turnSourceAccountId;
}
const sessionBinding = resolvePersistedApprovalRequestSessionBinding(params);
const sessionChannel = sessionBinding?.channel;
if (expectedChannel && sessionChannel && sessionChannel !== expectedChannel) {
return null;
}
return sessionBinding?.accountId ?? null;
}
export function resolveApprovalRequestChannelAccountId(params: {
cfg: OpenClawConfig;
request: ApprovalRequestLike;
channel: string;
}): string | null {
const expectedChannel = normalizeOptionalChannel(params.channel);
if (!expectedChannel) {
return null;
}
return resolveApprovalRequestAccountId(params);
}
export function doesApprovalRequestMatchChannelAccount(params: {
cfg: OpenClawConfig;
request: ApprovalRequestLike;
channel: string;
accountId?: string | null;
}): boolean {
const expectedChannel = normalizeOptionalChannel(params.channel);
if (!expectedChannel) {
return false;
}
const turnSourceChannel = normalizeOptionalChannel(params.request.request.turnSourceChannel);
if (turnSourceChannel && turnSourceChannel !== expectedChannel) {
return false;
}
const turnSourceAccountId = normalizeOptionalAccountId(
params.request.request.turnSourceAccountId,
);
const expectedAccountId = normalizeOptionalAccountId(params.accountId);
if (turnSourceAccountId) {
return !expectedAccountId || expectedAccountId === turnSourceAccountId;
}
const sessionBinding = resolvePersistedApprovalRequestSessionBinding(params);
const sessionChannel = sessionBinding?.channel;
if (sessionChannel && sessionChannel !== expectedChannel) {
return false;
}
const boundAccountId = sessionBinding?.accountId;
return !expectedAccountId || !boundAccountId || expectedAccountId === boundAccountId;
}

View File

@@ -1,8 +1,9 @@
import type { OpenClawConfig } from "../config/config.js";
import { loadSessionStore, resolveStorePath } from "../config/sessions.js";
import { normalizeOptionalAccountId } from "../routing/account-id.js";
import { resolveStorePath } from "../config/sessions/paths.js";
import { loadSessionStore } from "../config/sessions/store-load.js";
import { parseAgentSessionKey } from "../routing/session-key.js";
import { normalizeMessageChannel } from "../utils/message-channel.js";
import { doesApprovalRequestMatchChannelAccount } from "./approval-request-account-binding.js";
import type { ExecApprovalRequest } from "./exec-approvals.js";
import { resolveSessionDeliveryTarget } from "./outbound/targets.js";
import type { PluginApprovalRequest } from "./plugin-approvals.js";
@@ -14,11 +15,6 @@ export type ExecApprovalSessionTarget = {
threadId?: number;
};
type ApprovalRequestSessionBinding = {
channel?: string;
accountId?: string;
};
type ApprovalRequestLike = ExecApprovalRequest | PluginApprovalRequest;
type ApprovalRequestOriginTargetResolver<TTarget> = {
cfg: OpenClawConfig;
@@ -115,28 +111,6 @@ export function resolveExecApprovalSessionTarget(params: {
};
}
function resolvePersistedApprovalRequestSessionBinding(params: {
cfg: OpenClawConfig;
request: ApprovalRequestLike;
}): ApprovalRequestSessionBinding | null {
const sessionKey = normalizeOptionalString(params.request.request.sessionKey);
if (!sessionKey) {
return null;
}
const parsed = parseAgentSessionKey(sessionKey);
const agentId = parsed?.agentId ?? params.request.request.agentId ?? "main";
const storePath = resolveStorePath(params.cfg.session?.store, { agentId });
const store = loadSessionStore(storePath);
const entry = store[sessionKey];
if (!entry) {
return null;
}
return {
channel: normalizeOptionalChannel(entry.origin?.provider ?? entry.lastChannel),
accountId: normalizeOptionalAccountId(entry.origin?.accountId ?? entry.lastAccountId),
};
}
export function resolveApprovalRequestSessionTarget(params: {
cfg: OpenClawConfig;
request: ApprovalRequestLike;
@@ -163,117 +137,6 @@ function resolveApprovalRequestStoredSessionTarget(params: {
});
}
// Account scoping uses the persisted same-channel binding first. The generic
// session target only backfills legacy sessions that never stored `origin.*`.
function resolveApprovalRequestAccountBinding(params: {
cfg: OpenClawConfig;
request: ApprovalRequestLike;
sessionTarget?: ExecApprovalSessionTarget | null;
}): ApprovalRequestSessionBinding | null {
const sessionBinding = resolvePersistedApprovalRequestSessionBinding(params);
const channel = normalizeOptionalChannel(
sessionBinding?.channel ?? params.sessionTarget?.channel,
);
const accountId = normalizeOptionalAccountId(
sessionBinding?.accountId ?? params.sessionTarget?.accountId,
);
return channel || accountId ? { channel, accountId } : null;
}
export function resolveApprovalRequestAccountId(params: {
cfg: OpenClawConfig;
request: ApprovalRequestLike;
channel?: string | null;
}): string | null {
const expectedChannel = normalizeOptionalChannel(params.channel);
const turnSourceChannel = normalizeOptionalChannel(params.request.request.turnSourceChannel);
if (expectedChannel && turnSourceChannel && turnSourceChannel !== expectedChannel) {
return null;
}
const turnSourceAccountId = normalizeOptionalAccountId(
params.request.request.turnSourceAccountId,
);
if (turnSourceAccountId) {
return turnSourceAccountId;
}
const sessionBinding = resolveApprovalRequestAccountBinding({
...params,
sessionTarget: resolveApprovalRequestSessionTarget(params),
});
const sessionChannel = sessionBinding?.channel;
if (expectedChannel && sessionChannel && sessionChannel !== expectedChannel) {
return null;
}
return sessionBinding?.accountId ?? null;
}
export function resolveApprovalRequestChannelAccountId(params: {
cfg: OpenClawConfig;
request: ApprovalRequestLike;
channel: string;
}): string | null {
const expectedChannel = normalizeOptionalChannel(params.channel);
if (!expectedChannel) {
return null;
}
const turnSourceChannel = normalizeOptionalChannel(params.request.request.turnSourceChannel);
if (!turnSourceChannel || turnSourceChannel === expectedChannel) {
return resolveApprovalRequestAccountId(params);
}
const sessionBinding = resolveApprovalRequestAccountBinding({
...params,
sessionTarget: resolveApprovalRequestStoredSessionTarget(params),
});
const sessionChannel = sessionBinding?.channel;
if (sessionChannel && sessionChannel !== expectedChannel) {
return null;
}
return sessionBinding?.accountId ?? null;
}
export function doesApprovalRequestMatchChannelAccount(params: {
cfg: OpenClawConfig;
request: ApprovalRequestLike;
channel: string;
accountId?: string | null;
}): boolean {
const expectedChannel = normalizeOptionalChannel(params.channel);
if (!expectedChannel) {
return false;
}
const turnSourceChannel = normalizeOptionalChannel(params.request.request.turnSourceChannel);
if (turnSourceChannel && turnSourceChannel !== expectedChannel) {
return false;
}
const turnSourceAccountId = normalizeOptionalAccountId(
params.request.request.turnSourceAccountId,
);
const expectedAccountId = normalizeOptionalAccountId(params.accountId);
if (turnSourceAccountId) {
return !expectedAccountId || expectedAccountId === turnSourceAccountId;
}
const sessionBinding = resolveApprovalRequestAccountBinding({
...params,
sessionTarget: resolveApprovalRequestSessionTarget(params),
});
const sessionChannel = sessionBinding?.channel;
if (sessionChannel && sessionChannel !== expectedChannel) {
return false;
}
const boundAccountId = sessionBinding?.accountId;
return !expectedAccountId || !boundAccountId || expectedAccountId === boundAccountId;
}
export function resolveApprovalRequestOriginTarget<TTarget>(
params: ApprovalRequestOriginTargetResolver<TTarget>,
): TTarget | null {

View File

@@ -36,6 +36,8 @@ function isApprovalTargetsMode(cfg: OpenClawConfig): boolean {
return execApprovals.mode === "targets" || execApprovals.mode === "both";
}
export { getExecApprovalReplyMetadata, matchesApprovalRequestFilters };
export function isChannelExecApprovalClientEnabledFromConfig(params: {
enabled?: ChannelExecApprovalEnableMode;
approverCount: number;

View File

@@ -0,0 +1,7 @@
export {
createChannelExecApprovalProfile,
getExecApprovalReplyMetadata,
isChannelExecApprovalClientEnabledFromConfig,
isChannelExecApprovalTargetRecipient,
matchesApprovalRequestFilters,
} from "./approval-client-helpers.js";

View File

@@ -0,0 +1,6 @@
export {
createApproverRestrictedNativeApprovalAdapter,
createApproverRestrictedNativeApprovalCapability,
createChannelApprovalCapability,
splitChannelApprovalCapability,
} from "./approval-delivery-helpers.js";

View File

@@ -0,0 +1,15 @@
export {
createChannelApproverDmTargetResolver,
createChannelNativeOriginTargetResolver,
} from "./approval-native-helpers.js";
export {
resolveApprovalRequestOriginTarget,
resolveApprovalRequestSessionTarget,
resolveExecApprovalSessionTarget,
type ExecApprovalSessionTarget,
} from "../infra/exec-approval-session-target.js";
export {
doesApprovalRequestMatchChannelAccount,
resolveApprovalRequestAccountId,
resolveApprovalRequestChannelAccountId,
} from "../infra/approval-request-account-binding.js";

View File

@@ -0,0 +1,15 @@
export {
buildExecApprovalPendingReplyPayload,
getExecApprovalApproverDmNoticeText,
getExecApprovalReplyMetadata,
type ExecApprovalPendingReplyParams,
type ExecApprovalReplyDecision,
type ExecApprovalReplyMetadata,
} from "../infra/exec-approval-reply.js";
export { resolveExecApprovalCommandDisplay } from "../infra/exec-approval-command-display.js";
export {
resolveExecApprovalAllowedDecisions,
resolveExecApprovalRequestAllowedDecisions,
type ExecApprovalDecision,
} from "../infra/exec-approvals.js";
export { buildPluginApprovalPendingReplyPayload } from "./approval-renderers.js";

View File

@@ -24,14 +24,16 @@ export {
createChannelNativeOriginTargetResolver,
} from "./approval-native-helpers.js";
export {
doesApprovalRequestMatchChannelAccount,
resolveApprovalRequestOriginTarget,
resolveApprovalRequestAccountId,
resolveApprovalRequestChannelAccountId,
resolveApprovalRequestSessionTarget,
resolveExecApprovalSessionTarget,
type ExecApprovalSessionTarget,
} from "../infra/exec-approval-session-target.js";
export {
doesApprovalRequestMatchChannelAccount,
resolveApprovalRequestAccountId,
resolveApprovalRequestChannelAccountId,
} from "../infra/approval-request-account-binding.js";
export {
buildPluginApprovalExpiredMessage,
buildPluginApprovalRequestMessage,

View File

@@ -443,21 +443,15 @@ export function defineChannelPluginEntry<TPlugin>({
registerCliMetadata,
registerFull,
}: DefineChannelPluginEntryOptions<TPlugin>): DefinedChannelPluginEntry<TPlugin> {
let resolvedConfigSchema: ChannelEntryConfigSchema<TPlugin> | undefined;
const getConfigSchema = (): ChannelEntryConfigSchema<TPlugin> => {
resolvedConfigSchema ??=
typeof configSchema === "function"
? configSchema()
: ((configSchema ?? emptyChannelConfigSchema()) as ChannelEntryConfigSchema<TPlugin>);
return resolvedConfigSchema;
};
const resolvedConfigSchema: ChannelEntryConfigSchema<TPlugin> =
typeof configSchema === "function"
? configSchema()
: ((configSchema ?? emptyChannelConfigSchema()) as ChannelEntryConfigSchema<TPlugin>);
const entry = {
id,
name,
description,
get configSchema() {
return getConfigSchema();
},
configSchema: resolvedConfigSchema,
register(api: OpenClawPluginApi) {
if (api.registrationMode === "cli-metadata") {
registerCliMetadata?.(api);

View File

@@ -2,8 +2,10 @@ import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { createJiti } from "jiti";
import { loadConfig, type OpenClawConfig } from "../config/config.js";
import JSON5 from "json5";
import { resolveConfigPath } from "../config/paths.js";
import { applyPluginAutoEnable } from "../config/plugin-auto-enable.js";
import type { OpenClawConfig } from "../config/types.js";
import { openBoundaryFileSync } from "../infra/boundary-file-read.js";
import { resolveBundledPluginsDir } from "../plugins/bundled-dir.js";
import { resolveBundledPluginPublicSurfacePath } from "../plugins/bundled-plugin-metadata.js";
@@ -136,8 +138,15 @@ function getJiti(modulePath: string) {
function readFacadeBoundaryConfigSafely(): OpenClawConfig {
try {
const config = loadConfig();
return config && typeof config === "object" ? config : EMPTY_FACADE_BOUNDARY_CONFIG;
const configPath = resolveConfigPath();
if (!fs.existsSync(configPath)) {
return EMPTY_FACADE_BOUNDARY_CONFIG;
}
const raw = fs.readFileSync(configPath, "utf8");
const parsed = JSON5.parse(raw);
return parsed && typeof parsed === "object"
? (parsed as OpenClawConfig)
: EMPTY_FACADE_BOUNDARY_CONFIG;
} catch {
return EMPTY_FACADE_BOUNDARY_CONFIG;
}

View File

@@ -1,2 +1,2 @@
export { getRuntimeConfigSnapshot } from "../config/io.js";
export { getRuntimeConfigSnapshot } from "../config/runtime-snapshot.js";
export type { OpenClawConfig } from "../config/types.js";