refactor(config): migrate plugin config access

This commit is contained in:
Peter Steinberger
2026-04-27 12:16:48 +01:00
parent 48ebed3ed3
commit 7f3f108521
531 changed files with 3502 additions and 1646 deletions

View File

@@ -1,4 +1,4 @@
3546f416ff22ead14952cd105c7b88e3b7b76d5ddc10269e73f69ed1950f0603 config-baseline.json
b29ade2d1d2415b030b4d5ec36097a93ab4ea943b7d2a52da95829be1c28fc2a config-baseline.core.json
5027142b42acd038bb3cd15e53a0d45293103448a3aee1072500352095e14242 config-baseline.json
ecb702eee54bcb697916944440e13208ac7a640a8e07f44072bb79e9284ca994 config-baseline.core.json
07963db49502132f26db396c56b36e018b110e6c55a68b3cb012d3ec96f43901 config-baseline.channel.json
ed65cefbef96f034ce2b73069d9d5bacc341a43489ff9b20a34d40956b877f79 config-baseline.plugin.json

View File

@@ -1,2 +1,2 @@
6eabbe9e1e568fa1bc02539bd21bb6cd463d609f2ad4573d0cbf116ce39a28f9 plugin-sdk-api-baseline.json
c5a5ba7c051ab741b1cdfb36b23f13e6aad9fbe17ba3fa92c4833c0490a35181 plugin-sdk-api-baseline.jsonl
74344f185b3149695443bf8815c9dd784daf9c0b8118ecc54129dc57899e9564 plugin-sdk-api-baseline.json
7b84c2f1e5743dac9c764fdee6d3b23e64553516c409f4a24f009a36c40d64e8 plugin-sdk-api-baseline.jsonl

View File

@@ -90,7 +90,13 @@ describe("active-memory plugin", () => {
resolveStateDir: () => stateDir,
},
config: {
current: () => configFile,
loadConfig: () => configFile,
replaceConfigFile: vi.fn(
async ({ nextConfig }: { nextConfig: Record<string, unknown> }) => {
configFile = nextConfig;
},
),
writeConfigFile: vi.fn(async (nextConfig: Record<string, unknown>) => {
configFile = nextConfig;
}),
@@ -275,7 +281,7 @@ describe("active-memory plugin", () => {
});
expect(offResult.text).toBe("Active Memory: off globally.");
expect(api.runtime.config.writeConfigFile).toHaveBeenCalledTimes(1);
expect(api.runtime.config.replaceConfigFile).toHaveBeenCalledTimes(1);
expect(configFile).toMatchObject({
plugins: {
entries: {

View File

@@ -1932,7 +1932,9 @@ export default definePluginEntry({
warnDeprecatedModelFallbackPolicy(api.pluginConfig);
const refreshLiveConfigFromRuntime = () => {
const livePluginConfig = resolveLivePluginConfigObject(
api.runtime.config?.loadConfig,
api.runtime.config?.current
? () => api.runtime.config.current() as OpenClawConfig
: undefined,
"active-memory",
api.pluginConfig as Record<string, unknown>,
);
@@ -1953,7 +1955,7 @@ export default definePluginEntry({
return { text: formatActiveMemoryCommandHelp() };
}
if (isGlobal) {
const currentConfig = api.runtime.config.loadConfig();
const currentConfig = api.runtime.config.current() as OpenClawConfig;
if (action === "status") {
return {
text: `Active Memory: ${isActiveMemoryGloballyEnabled(currentConfig) ? "on" : "off"} globally.`,
@@ -1961,13 +1963,19 @@ export default definePluginEntry({
}
if (action === "on" || action === "enable" || action === "enabled") {
const nextConfig = updateActiveMemoryGlobalEnabledInConfig(currentConfig, true);
await api.runtime.config.writeConfigFile(nextConfig);
await api.runtime.config.replaceConfigFile({
nextConfig,
afterWrite: { mode: "auto" },
});
refreshLiveConfigFromRuntime();
return { text: "Active Memory: on globally." };
}
if (action === "off" || action === "disable" || action === "disabled") {
const nextConfig = updateActiveMemoryGlobalEnabledInConfig(currentConfig, false);
await api.runtime.config.writeConfigFile(nextConfig);
await api.runtime.config.replaceConfigFile({
nextConfig,
afterWrite: { mode: "auto" },
});
refreshLiveConfigFromRuntime();
return { text: "Active Memory: off globally." };
}

View File

@@ -50,11 +50,24 @@ const { bluebubblesMessageActions } = await importFreshModule<typeof import("./a
"./actions.js?actions-test",
);
function requireDefined<T>(value: T | undefined, name: string): T {
if (value === undefined) {
throw new Error(`${name} is not registered`);
}
return value;
}
describe("bluebubblesMessageActions", () => {
const describeMessageTool = bluebubblesMessageActions.describeMessageTool!;
const supportsAction = bluebubblesMessageActions.supportsAction!;
const extractToolSend = bluebubblesMessageActions.extractToolSend!;
const handleAction = bluebubblesMessageActions.handleAction!;
const describeMessageTool = requireDefined(
bluebubblesMessageActions.describeMessageTool,
"describeMessageTool",
);
const supportsAction = requireDefined(bluebubblesMessageActions.supportsAction, "supportsAction");
const extractToolSend = requireDefined(
bluebubblesMessageActions.extractToolSend,
"extractToolSend",
);
const handleAction = requireDefined(bluebubblesMessageActions.handleAction, "handleAction");
const callHandleAction = (ctx: Omit<Parameters<typeof handleAction>[0], "channel">) =>
handleAction({ channel: "bluebubbles", ...ctx });
const blueBubblesConfig = (): OpenClawConfig => ({

View File

@@ -6,9 +6,9 @@ import {
browserSnapshot,
browserTabs,
getBrowserProfileCapabilities,
getRuntimeConfig,
imageResultFromFile,
jsonResult,
loadConfig,
normalizeOptionalString,
readStringValue,
resolveBrowserConfig,
@@ -22,8 +22,8 @@ const browserToolActionDeps = {
browserConsoleMessages,
browserSnapshot,
browserTabs,
getRuntimeConfig,
imageResultFromFile,
loadConfig,
};
const BROWSER_ACT_REQUEST_TIMEOUT_SLACK_MS = 5_000;
@@ -70,7 +70,7 @@ function existingSessionRejectsActTimeout(request: BrowserActRequest): boolean {
}
function usesExistingSessionProfile(profileName: string | undefined): boolean {
const cfg = browserToolActionDeps.loadConfig();
const cfg = browserToolActionDeps.getRuntimeConfig();
const resolved = resolveBrowserConfig(cfg.browser, cfg);
const profile = resolveProfile(resolved, profileName ?? resolved.defaultProfile);
return profile ? getBrowserProfileCapabilities(profile).usesChromeMcp : false;
@@ -91,7 +91,7 @@ function withConfiguredActTimeout(
return request;
}
const cfg = browserToolActionDeps.loadConfig();
const cfg = browserToolActionDeps.getRuntimeConfig();
const configuredTimeout =
normalizePositiveTimeoutMs(cfg.browser?.actionTimeoutMs) ?? DEFAULT_BROWSER_ACTION_TIMEOUT_MS;
return { ...typedRequest, timeoutMs: configuredTimeout } as BrowserActRequest;
@@ -122,7 +122,7 @@ export const __testing = {
browserSnapshot: typeof browserSnapshot;
browserTabs: typeof browserTabs;
imageResultFromFile: typeof imageResultFromFile;
loadConfig: typeof loadConfig;
getRuntimeConfig: typeof getRuntimeConfig;
}> | null,
) {
browserToolActionDeps.browserAct = overrides?.browserAct ?? browserAct;
@@ -132,7 +132,7 @@ export const __testing = {
browserToolActionDeps.browserTabs = overrides?.browserTabs ?? browserTabs;
browserToolActionDeps.imageResultFromFile =
overrides?.imageResultFromFile ?? imageResultFromFile;
browserToolActionDeps.loadConfig = overrides?.loadConfig ?? loadConfig;
browserToolActionDeps.getRuntimeConfig = overrides?.getRuntimeConfig ?? getRuntimeConfig;
},
};
@@ -250,7 +250,7 @@ function isChromeStaleTargetError(profile: string | undefined, err: unknown): bo
const msg = String(err);
return msg.includes("404:") && msg.includes("tab not found");
}
const cfg = browserToolActionDeps.loadConfig();
const cfg = browserToolActionDeps.getRuntimeConfig();
const resolved = resolveBrowserConfig(cfg.browser, cfg);
const browserProfile = resolveProfile(resolved, profile);
if (!browserProfile || !getBrowserProfileCapabilities(browserProfile).usesChromeMcp) {
@@ -326,7 +326,7 @@ export async function executeSnapshotAction(params: {
onTabActivity?: (targetId: string | undefined) => void;
}): Promise<AgentToolResult<unknown>> {
const { input, baseUrl, profile, proxyRequest } = params;
const snapshotDefaults = browserToolActionDeps.loadConfig().browser?.snapshotDefaults;
const snapshotDefaults = browserToolActionDeps.getRuntimeConfig().browser?.snapshotDefaults;
const format: "ai" | "aria" | undefined =
input.snapshotFormat === "ai" ? "ai" : input.snapshotFormat === "aria" ? "aria" : undefined;
const formatExplicit = format !== undefined;

View File

@@ -1,4 +1,4 @@
export { loadConfig } from "openclaw/plugin-sdk/browser-config-runtime";
export { getRuntimeConfig } from "openclaw/plugin-sdk/browser-config-runtime";
export {
callGatewayTool,
imageResultFromFile,

View File

@@ -142,7 +142,7 @@ vi.mock("openclaw/plugin-sdk/config-runtime", async () => {
);
return {
...actual,
loadConfig: configMocks.loadConfig,
getRuntimeConfig: configMocks.loadConfig,
};
});
@@ -193,6 +193,7 @@ vi.mock("./browser-tool.runtime.js", () => {
...configMocks,
...gatewayMocks,
...sessionTabRegistryMocks,
getRuntimeConfig: configMocks.loadConfig,
applyBrowserProxyPaths: vi.fn(),
getBrowserProfileCapabilities: (profile: Record<string, unknown>) => ({
usesChromeMcp: profile.driver === "existing-session",
@@ -269,7 +270,7 @@ function resetBrowserToolMocks() {
browserStatus: browserClientMocks.browserStatus as never,
browserStop: browserClientMocks.browserStop as never,
imageResultFromFile: toolCommonMocks.imageResultFromFile as never,
loadConfig: configMocks.loadConfig as never,
getRuntimeConfig: configMocks.loadConfig as never,
listNodes: nodesUtilsMocks.listNodes as never,
callGatewayTool: gatewayMocks.callGatewayTool as never,
trackSessionBrowserTab: sessionTabRegistryMocks.trackSessionBrowserTab as never,
@@ -280,7 +281,7 @@ function resetBrowserToolMocks() {
browserConsoleMessages: browserActionsMocks.browserConsoleMessages as never,
browserSnapshot: browserClientMocks.browserSnapshot as never,
browserTabs: browserClientMocks.browserTabs as never,
loadConfig: configMocks.loadConfig as never,
getRuntimeConfig: configMocks.loadConfig as never,
imageResultFromFile: toolCommonMocks.imageResultFromFile as never,
});
}

View File

@@ -26,11 +26,11 @@ import {
browserStatus,
browserStop,
callGatewayTool,
getRuntimeConfig,
getBrowserProfileCapabilities,
imageResultFromFile,
jsonResult,
listNodes,
loadConfig,
normalizeOptionalString,
persistBrowserProxyFiles,
readStringParam,
@@ -61,8 +61,8 @@ const browserToolDeps = {
browserStart,
browserStatus,
browserStop,
getRuntimeConfig,
imageResultFromFile,
loadConfig,
listNodes,
callGatewayTool,
touchSessionBrowserTab,
@@ -88,7 +88,7 @@ export const __testing = {
browserStatus: typeof browserStatus;
browserStop: typeof browserStop;
imageResultFromFile: typeof imageResultFromFile;
loadConfig: typeof loadConfig;
getRuntimeConfig: typeof getRuntimeConfig;
listNodes: typeof listNodes;
callGatewayTool: typeof callGatewayTool;
touchSessionBrowserTab: typeof touchSessionBrowserTab;
@@ -113,7 +113,7 @@ export const __testing = {
browserToolDeps.browserStatus = overrides?.browserStatus ?? browserStatus;
browserToolDeps.browserStop = overrides?.browserStop ?? browserStop;
browserToolDeps.imageResultFromFile = overrides?.imageResultFromFile ?? imageResultFromFile;
browserToolDeps.loadConfig = overrides?.loadConfig ?? loadConfig;
browserToolDeps.getRuntimeConfig = overrides?.getRuntimeConfig ?? getRuntimeConfig;
browserToolDeps.listNodes = overrides?.listNodes ?? listNodes;
browserToolDeps.callGatewayTool = overrides?.callGatewayTool ?? callGatewayTool;
browserToolDeps.touchSessionBrowserTab =
@@ -220,7 +220,7 @@ async function resolveBrowserNodeTarget(params: {
target?: "sandbox" | "host" | "node";
sandboxBridgeUrl?: string;
}): Promise<BrowserNodeTarget | null> {
const cfg = browserToolDeps.loadConfig();
const cfg = browserToolDeps.getRuntimeConfig();
const policy = cfg.gateway?.nodes?.browser;
const mode = policy?.mode ?? "auto";
if (mode === "off") {
@@ -340,7 +340,7 @@ function resolveBrowserBaseUrl(params: {
sandboxBridgeUrl?: string;
allowHostControl?: boolean;
}): string | undefined {
const cfg = loadConfig();
const cfg = getRuntimeConfig();
const resolved = resolveBrowserConfig(cfg.browser, cfg);
const normalizedSandbox = params.sandboxBridgeUrl?.trim() ?? "";
const target = params.target ?? (normalizedSandbox ? "sandbox" : "host");
@@ -369,7 +369,7 @@ function shouldPreferHostForProfile(profileName: string | undefined) {
if (!profileName) {
return false;
}
const cfg = browserToolDeps.loadConfig();
const cfg = browserToolDeps.getRuntimeConfig();
const resolved = resolveBrowserConfig(cfg.browser, cfg);
const profile = resolveProfile(resolved, profileName);
if (!profile) {
@@ -395,7 +395,7 @@ function usesExistingSessionManageFlow(params: { action: string; profileName?: s
if (!EXISTING_SESSION_MANAGE_ACTIONS.has(params.action)) {
return false;
}
const cfg = browserToolDeps.loadConfig();
const cfg = browserToolDeps.getRuntimeConfig();
const resolved = resolveBrowserConfig(cfg.browser, cfg);
const profile = resolveProfile(resolved, params.profileName ?? resolved.defaultProfile);
if (profile && getBrowserProfileCapabilities(profile).usesChromeMcp) {
@@ -448,7 +448,9 @@ export function createBrowserTool(opts?: {
const requestedNode = readStringParam(params, "node");
const requestedTimeoutMs = readToolTimeoutMs(params);
let target = readStringParam(params, "target") as "sandbox" | "host" | "node" | undefined;
const configuredNode = browserToolDeps.loadConfig().gateway?.nodes?.browser?.node?.trim();
const configuredNode = browserToolDeps
.getRuntimeConfig()
.gateway?.nodes?.browser?.node?.trim();
if (requestedNode && target && target !== "node") {
throw new Error('node is only supported with target="node".');

View File

@@ -217,7 +217,7 @@ describe("fetchBrowserJson loopback auth (bridge auth registry)", () => {
candidate === port ? { token: "registry-token" } : undefined,
);
const init = __test.withLoopbackBrowserAuth(`http://127.0.0.1:${port}/`, undefined, {
loadConfig: () => ({}),
getRuntimeConfig: () => ({}),
resolveBrowserControlAuth: () => ({}),
getBridgeAuthForPort,
});

View File

@@ -1,9 +1,10 @@
import fs from "node:fs/promises";
import net from "node:net";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { clearConfigCache } from "../../../../src/config/config.js";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { clearConfigCache, clearRuntimeConfigSnapshot } from "../../../../src/config/config.js";
import { createTempHomeEnv } from "../../test-support.js";
import { stopBrowserControlService } from "../control-service.js";
import { fetchBrowserJson } from "./client-fetch.js";
type TempHome = {
@@ -14,8 +15,18 @@ type TempHome = {
describe("browser client fetch attachOnly diagnostics", () => {
let tempHome: TempHome | undefined;
afterEach(async () => {
beforeEach(async () => {
vi.useRealTimers();
await stopBrowserControlService();
clearConfigCache();
clearRuntimeConfigSnapshot();
});
afterEach(async () => {
vi.useRealTimers();
await stopBrowserControlService();
clearConfigCache();
clearRuntimeConfigSnapshot();
await tempHome?.restore();
tempHome = undefined;
});
@@ -54,6 +65,7 @@ describe("browser client fetch attachOnly diagnostics", () => {
);
process.env.OPENCLAW_CONFIG_PATH = configPath;
clearConfigCache();
clearRuntimeConfigSnapshot();
try {
const thrown = await fetchBrowserJson("/tabs?profile=hung", { timeoutMs: 200 }).catch(

View File

@@ -49,6 +49,7 @@ vi.mock("../config/config.js", async () => {
const actual = await vi.importActual<typeof import("../config/config.js")>("../config/config.js");
return {
...actual,
getRuntimeConfig: mocks.loadConfig,
loadConfig: mocks.loadConfig,
};
});

View File

@@ -2,7 +2,7 @@ import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
import { formatCliCommand } from "../cli/command-format.js";
import { loadConfig } from "../config/config.js";
import { getRuntimeConfig } from "../config/config.js";
import { isLoopbackHost } from "../gateway/net.js";
import { getBridgeAuthForPort } from "./bridge-auth-registry.js";
import { resolveBrowserConfig, resolveProfile } from "./config.js";
@@ -19,7 +19,7 @@ class BrowserServiceError extends Error {
}
type LoopbackBrowserAuthDeps = {
loadConfig: typeof loadConfig;
getRuntimeConfig: typeof getRuntimeConfig;
resolveBrowserControlAuth: typeof resolveBrowserControlAuth;
getBridgeAuthForPort: typeof getBridgeAuthForPort;
};
@@ -50,7 +50,7 @@ function withLoopbackBrowserAuthImpl(
}
try {
const cfg = deps.loadConfig();
const cfg = deps.getRuntimeConfig();
const auth = deps.resolveBrowserControlAuth(cfg);
if (auth.token) {
headers.set("Authorization", `Bearer ${auth.token}`);
@@ -92,7 +92,7 @@ function withLoopbackBrowserAuth(
init: (RequestInit & { timeoutMs?: number }) | undefined,
): RequestInit & { timeoutMs?: number } {
return withLoopbackBrowserAuthImpl(url, init, {
loadConfig,
getRuntimeConfig,
resolveBrowserControlAuth,
getBridgeAuthForPort,
});
@@ -113,7 +113,7 @@ function resolveDispatcherBrowserControlOwnership(url: string): BrowserControlOw
return "unknown";
}
try {
const cfg = loadConfig();
const cfg = getRuntimeConfig();
const resolved = resolveBrowserConfig(cfg?.browser, cfg);
const parsed = new URL(url, "http://localhost");
const requestedProfile = parsed.searchParams.get("profile")?.trim();

View File

@@ -1,5 +1,5 @@
import { createConfigIO, getRuntimeConfigSnapshot, type OpenClawConfig } from "../config/config.js";
import { getRuntimeConfig, type OpenClawConfig } from "../config/config.js";
export function loadBrowserConfigForRuntimeRefresh(): OpenClawConfig {
return getRuntimeConfigSnapshot() ?? createConfigIO().loadConfig();
return getRuntimeConfig();
}

View File

@@ -3,8 +3,11 @@ import { expectGeneratedTokenPersistedToGatewayAuth } from "../../test-support.j
import type { OpenClawConfig } from "../config/config.js";
const mocks = vi.hoisted(() => ({
loadConfig: vi.fn<() => OpenClawConfig>(),
getRuntimeConfig: vi.fn<() => OpenClawConfig>(),
writeConfigFile: vi.fn<(cfg: OpenClawConfig) => Promise<void>>(async (_cfg) => {}),
replaceConfigFile: vi.fn(async ({ nextConfig }: { nextConfig: OpenClawConfig }) => {
await mocks.writeConfigFile(nextConfig);
}),
resolveGatewayAuth: vi.fn(
({
authConfig,
@@ -48,8 +51,8 @@ const mocks = vi.hoisted(() => ({
}));
vi.mock("../config/config.js", () => ({
loadConfig: mocks.loadConfig,
writeConfigFile: mocks.writeConfigFile,
getRuntimeConfig: mocks.getRuntimeConfig,
replaceConfigFile: mocks.replaceConfigFile,
}));
vi.mock("../gateway/startup-auth.js", () => ({
@@ -73,7 +76,7 @@ async function expectGeneratedBrowserAuthPersistence(params: {
mode: "none" | "trusted-proxy";
generatedAuthField: "token" | "password";
}) {
mocks.loadConfig.mockReturnValue(params.cfg);
mocks.getRuntimeConfig.mockReturnValue(params.cfg);
const result = await ensureBrowserControlAuth({ cfg: params.cfg, env: {} as NodeJS.ProcessEnv });
@@ -88,7 +91,7 @@ async function expectGeneratedBrowserAuthPersistence(params: {
}
async function expectUnresolvedBrowserSecretRefSkipsPersistence(cfg: OpenClawConfig) {
mocks.loadConfig.mockReturnValue(cfg);
mocks.getRuntimeConfig.mockReturnValue(cfg);
const result = await ensureBrowserControlAuth({ cfg, env: {} as NodeJS.ProcessEnv });
@@ -113,7 +116,7 @@ describe("ensureBrowserControlAuth", () => {
const result = await ensureBrowserControlAuth({ cfg, env: {} as NodeJS.ProcessEnv });
expect(result).toEqual({ auth: {} });
expect(mocks.loadConfig).not.toHaveBeenCalled();
expect(mocks.getRuntimeConfig).not.toHaveBeenCalled();
expect(mocks.writeConfigFile).not.toHaveBeenCalled();
expect(mocks.ensureGatewayStartupAuth).not.toHaveBeenCalled();
};
@@ -137,7 +140,7 @@ describe("ensureBrowserControlAuth", () => {
beforeEach(() => {
vi.restoreAllMocks();
mocks.loadConfig.mockClear();
mocks.getRuntimeConfig.mockClear();
mocks.writeConfigFile.mockClear();
mocks.resolveGatewayAuth.mockClear();
mocks.ensureGatewayStartupAuth.mockClear();
@@ -155,7 +158,7 @@ describe("ensureBrowserControlAuth", () => {
const result = await ensureBrowserControlAuth({ cfg, env: {} as NodeJS.ProcessEnv });
expect(result).toEqual({ auth: { token: "already-set" } });
expect(mocks.loadConfig).not.toHaveBeenCalled();
expect(mocks.getRuntimeConfig).not.toHaveBeenCalled();
expect(mocks.writeConfigFile).not.toHaveBeenCalled();
expect(mocks.ensureGatewayStartupAuth).not.toHaveBeenCalled();
});
@@ -260,7 +263,7 @@ describe("ensureBrowserControlAuth", () => {
enabled: true,
},
};
mocks.loadConfig.mockReturnValue({
mocks.getRuntimeConfig.mockReturnValue({
browser: {
enabled: true,
},
@@ -284,7 +287,7 @@ describe("ensureBrowserControlAuth", () => {
});
expect(result).toEqual({ auth: {} });
expect(mocks.loadConfig).not.toHaveBeenCalled();
expect(mocks.getRuntimeConfig).not.toHaveBeenCalled();
expect(mocks.writeConfigFile).not.toHaveBeenCalled();
expect(mocks.ensureGatewayStartupAuth).not.toHaveBeenCalled();
});
@@ -401,7 +404,7 @@ describe("ensureBrowserControlAuth", () => {
enabled: true,
},
};
mocks.loadConfig.mockReturnValue({
mocks.getRuntimeConfig.mockReturnValue({
gateway: {
auth: {
token: "latest-token",
@@ -436,7 +439,7 @@ describe("ensureBrowserControlAuth", () => {
},
},
};
mocks.loadConfig.mockReturnValue(cfg);
mocks.getRuntimeConfig.mockReturnValue(cfg);
mocks.ensureGatewayStartupAuth.mockRejectedValueOnce(new Error("MISSING_GW_TOKEN"));
await expect(ensureBrowserControlAuth({ cfg, env: {} as NodeJS.ProcessEnv })).rejects.toThrow(

View File

@@ -3,7 +3,7 @@ import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalString,
} from "openclaw/plugin-sdk/text-runtime";
import { loadConfig, writeConfigFile } from "../config/config.js";
import { getRuntimeConfig, replaceConfigFile } from "../config/config.js";
import type { OpenClawConfig } from "../config/config.js";
import { resolveGatewayAuth } from "../gateway/auth.js";
import { ensureGatewayStartupAuth } from "../gateway/startup-auth.js";
@@ -87,10 +87,13 @@ async function generateAndPersistBrowserControlToken(params: {
},
},
};
await writeConfigFile(nextCfg);
await replaceConfigFile({
nextConfig: nextCfg,
afterWrite: { mode: "auto" },
});
// Re-read to stay consistent with any concurrent config writer.
const persistedAuth = resolveBrowserControlAuth(loadConfig(), params.env);
const persistedAuth = resolveBrowserControlAuth(getRuntimeConfig(), params.env);
if (persistedAuth.token || persistedAuth.password) {
return {
auth: persistedAuth,
@@ -119,10 +122,13 @@ async function generateAndPersistBrowserControlPassword(params: {
},
},
};
await writeConfigFile(nextCfg);
await replaceConfigFile({
nextConfig: nextCfg,
afterWrite: { mode: "auto" },
});
// Re-read to stay consistent with any concurrent config writer.
const persistedAuth = resolveBrowserControlAuth(loadConfig(), params.env);
const persistedAuth = resolveBrowserControlAuth(getRuntimeConfig(), params.env);
if (persistedAuth.token || persistedAuth.password) {
return {
auth: persistedAuth,
@@ -155,7 +161,7 @@ export async function ensureBrowserControlAuth(params: {
}
// Re-read latest config to avoid racing with concurrent config writers.
const latestCfg = loadConfig();
const latestCfg = getRuntimeConfig();
const latestAuth = resolveBrowserControlAuth(latestCfg, env);
if (latestAuth.token || latestAuth.password) {
return { auth: latestAuth };

View File

@@ -21,6 +21,7 @@ vi.mock("../config/config.js", async () => {
const actual = await vi.importActual<typeof import("../config/config.js")>("../config/config.js");
return {
...actual,
getRuntimeConfig: mocks.loadConfig,
loadConfig: mocks.loadConfig,
};
});

View File

@@ -1,17 +1,25 @@
import fs from "node:fs";
import path from "node:path";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { loadConfig, writeConfigFile } from "../config/config.js";
import { getRuntimeConfig } from "../config/config.js";
import type { OpenClawConfig } from "../config/config.js";
import { resolveOpenClawUserDataDir } from "./chrome.js";
import type { BrowserRouteContext, BrowserServerState } from "./server-context.js";
import { movePathToTrash } from "./trash.js";
const configMocks = vi.hoisted(() => ({
writeConfigFile: vi.fn<(cfg: OpenClawConfig) => Promise<void>>(async (_cfg) => {}),
}));
const writeConfigFile = configMocks.writeConfigFile;
vi.mock("../config/config.js", async () => {
const actual = await vi.importActual<typeof import("../config/config.js")>("../config/config.js");
return {
...actual,
loadConfig: vi.fn(),
writeConfigFile: vi.fn(async () => {}),
getRuntimeConfig: vi.fn(),
replaceConfigFile: vi.fn(async ({ nextConfig }: { nextConfig: OpenClawConfig }) => {
await configMocks.writeConfigFile(nextConfig);
}),
};
});
@@ -52,7 +60,7 @@ async function createWorkProfileWithConfig(params: {
browserConfig: Record<string, unknown>;
}) {
const { ctx, state } = createCtx(params.resolved);
vi.mocked(loadConfig).mockReturnValue({ browser: params.browserConfig });
vi.mocked(getRuntimeConfig).mockReturnValue({ browser: params.browserConfig });
const service = createBrowserProfilesService(ctx);
const result = await service.createProfile({ name: "work" });
return { result, state };
@@ -116,7 +124,7 @@ describe("BrowserProfilesService", () => {
});
const { ctx } = createCtx(resolved);
vi.mocked(loadConfig).mockReturnValue({ browser: { profiles: {} } });
vi.mocked(getRuntimeConfig).mockReturnValue({ browser: { profiles: {} } });
const service = createBrowserProfilesService(ctx);
const result = await service.createProfile({
@@ -146,7 +154,7 @@ describe("BrowserProfilesService", () => {
});
const { ctx } = createCtx(resolved);
vi.mocked(loadConfig).mockReturnValue({
vi.mocked(getRuntimeConfig).mockReturnValue({
browser: {
ssrfPolicy: { dangerouslyAllowPrivateNetwork: false },
profiles: {},
@@ -167,7 +175,7 @@ describe("BrowserProfilesService", () => {
it("creates existing-session profiles as attach-only local entries", async () => {
const resolved = resolveBrowserConfig({});
const { ctx, state } = createCtx(resolved);
vi.mocked(loadConfig).mockReturnValue({ browser: { profiles: {} } });
vi.mocked(getRuntimeConfig).mockReturnValue({ browser: { profiles: {} } });
const service = createBrowserProfilesService(ctx);
const result = await service.createProfile({
@@ -202,7 +210,7 @@ describe("BrowserProfilesService", () => {
it("rejects driver=existing-session when cdpUrl is provided", async () => {
const resolved = resolveBrowserConfig({});
const { ctx } = createCtx(resolved);
vi.mocked(loadConfig).mockReturnValue({ browser: { profiles: {} } });
vi.mocked(getRuntimeConfig).mockReturnValue({ browser: { profiles: {} } });
const service = createBrowserProfilesService(ctx);
@@ -218,7 +226,7 @@ describe("BrowserProfilesService", () => {
it("creates existing-session profiles with an explicit userDataDir", async () => {
const resolved = resolveBrowserConfig({});
const { ctx, state } = createCtx(resolved);
vi.mocked(loadConfig).mockReturnValue({ browser: { profiles: {} } });
vi.mocked(getRuntimeConfig).mockReturnValue({ browser: { profiles: {} } });
const tempDir = fs.mkdtempSync(path.join("/tmp", "openclaw-profile-"));
const userDataDir = path.join(tempDir, "BraveSoftware", "Brave-Browser");
@@ -244,7 +252,7 @@ describe("BrowserProfilesService", () => {
it("rejects userDataDir for non-existing-session profiles", async () => {
const resolved = resolveBrowserConfig({});
const { ctx } = createCtx(resolved);
vi.mocked(loadConfig).mockReturnValue({ browser: { profiles: {} } });
vi.mocked(getRuntimeConfig).mockReturnValue({ browser: { profiles: {} } });
const tempDir = fs.mkdtempSync(path.join("/tmp", "openclaw-profile-"));
const userDataDir = path.join(tempDir, "BraveSoftware", "Brave-Browser");
@@ -268,7 +276,7 @@ describe("BrowserProfilesService", () => {
});
const { ctx } = createCtx(resolved);
vi.mocked(loadConfig).mockReturnValue({
vi.mocked(getRuntimeConfig).mockReturnValue({
browser: {
defaultProfile: "openclaw",
profiles: {
@@ -294,7 +302,7 @@ describe("BrowserProfilesService", () => {
});
const { ctx } = createCtx(resolved);
vi.mocked(loadConfig).mockReturnValue({
vi.mocked(getRuntimeConfig).mockReturnValue({
browser: {
defaultProfile: "openclaw",
profiles: {
@@ -329,7 +337,7 @@ describe("BrowserProfilesService", () => {
});
const { ctx } = createCtx(resolved);
vi.mocked(loadConfig).mockReturnValue({
vi.mocked(getRuntimeConfig).mockReturnValue({
browser: {
defaultProfile: "openclaw",
profiles: {

View File

@@ -2,7 +2,7 @@ import fs from "node:fs";
import path from "node:path";
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
import type { BrowserProfileConfig, OpenClawConfig } from "../config/config.js";
import { loadConfig, writeConfigFile } from "../config/config.js";
import { getRuntimeConfig, replaceConfigFile } from "../config/config.js";
import { deriveDefaultBrowserCdpPortRange } from "../config/port-defaults.js";
import { formatErrorMessage } from "../infra/errors.js";
import { resolveUserPath } from "../utils.js";
@@ -101,7 +101,7 @@ export function createBrowserProfilesService(ctx: BrowserRouteContext) {
throw new BrowserConflictError(`profile "${name}" already exists`);
}
const cfg = loadConfig();
const cfg = getRuntimeConfig();
const rawProfiles = cfg.browser?.profiles ?? {};
if (name in rawProfiles) {
throw new BrowserConflictError(`profile "${name}" already exists`);
@@ -176,7 +176,10 @@ export function createBrowserProfilesService(ctx: BrowserRouteContext) {
},
};
await writeConfigFile(nextConfig);
await replaceConfigFile({
nextConfig,
afterWrite: { mode: "auto" },
});
state.resolved.profiles[name] = profileConfig;
const resolved = resolveProfile(state.resolved, name);
@@ -207,7 +210,7 @@ export function createBrowserProfilesService(ctx: BrowserRouteContext) {
}
const state = ctx.state();
const cfg = loadConfig();
const cfg = getRuntimeConfig();
const profiles = cfg.browser?.profiles ?? {};
const defaultProfile = cfg.browser?.defaultProfile ?? state.resolved.defaultProfile;
if (name === defaultProfile) {
@@ -246,7 +249,10 @@ export function createBrowserProfilesService(ctx: BrowserRouteContext) {
},
};
await writeConfigFile(nextConfig);
await replaceConfigFile({
nextConfig,
afterWrite: { mode: "auto" },
});
delete state.resolved.profiles[name];
state.profiles.delete(name);

View File

@@ -91,9 +91,8 @@ export function refreshResolvedBrowserConfigFromDisk(params: {
return;
}
// Route-level browser config hot reload should observe on-disk changes immediately.
// The shared loadConfig() helper may return a cached snapshot for the configured TTL,
// which can leave request-time browser guards stale (for example evaluateEnabled).
// Route-level refresh should use the shared runtime config. Config mutations
// refresh that snapshot and decide whether the wider runtime should restart.
const cfg = loadBrowserConfigForRuntimeRefresh();
const freshResolved = resolveBrowserConfig(cfg.browser, cfg);
applyResolvedConfig(params.current, freshResolved);

View File

@@ -46,15 +46,9 @@ vi.mock("../config/config.js", async () => {
const actual = await vi.importActual<typeof import("../config/config.js")>("../config/config.js");
return {
...actual,
createConfigIO: () => ({
loadConfig: () => {
// Always return fresh config for createConfigIO to simulate fresh disk read
return buildConfig();
},
}),
getRuntimeConfigSnapshot: () => null,
loadConfig: () => {
// simulate stale loadConfig that doesn't see updates unless cache cleared
getRuntimeConfig: () => {
// simulate stale getRuntimeConfig that doesn't see updates unless cache cleared
if (!mockState.cachedConfig) {
mockState.cachedConfig = buildConfig();
}
@@ -68,7 +62,7 @@ vi.mock("./config-refresh-source.js", () => ({
loadBrowserConfigForRuntimeRefresh: () => buildConfig(),
}));
const { loadConfig } = await import("../config/config.js");
const { getRuntimeConfig } = await import("../config/config.js");
const { resolveBrowserConfig, resolveProfile } = await import("./config.js");
const { refreshResolvedBrowserConfigFromDisk, resolveBrowserProfileWithHotReload } =
await import("./resolved-config-refresh.js");
@@ -84,8 +78,8 @@ describe("server-context hot-reload profiles", () => {
it("forProfile hot-reloads newly added profiles from config", async () => {
// Start with only openclaw profile
// 1. Prime the cache by calling loadConfig() first
const cfg = loadConfig();
// 1. Prime the cache by calling getRuntimeConfig() first
const cfg = getRuntimeConfig();
const resolved = resolveBrowserConfig(cfg.browser, cfg);
// Verify cache is primed (without desktop)
@@ -109,12 +103,11 @@ describe("server-context hot-reload profiles", () => {
// 2. Simulate adding a new profile to config (like user editing openclaw.json)
mockState.cfgProfiles.desktop = { cdpUrl: "http://127.0.0.1:9222", color: "#0066CC" };
// 3. Verify without clearConfigCache, loadConfig() still returns stale cached value
const staleCfg = loadConfig();
// 3. Verify without clearConfigCache, getRuntimeConfig() still returns stale cached value
const staleCfg = getRuntimeConfig();
expect(staleCfg.browser?.profiles?.desktop).toBeUndefined(); // Cache is stale!
// 4. Hot-reload should read fresh config for the lookup (createConfigIO().loadConfig()),
// without flushing the global loadConfig cache.
// 4. Hot-reload uses the refresh source without flushing the global getRuntimeConfig cache.
const profile = resolveBrowserProfileWithHotReload({
current: state,
refreshConfigFromDisk: true,
@@ -126,14 +119,14 @@ describe("server-context hot-reload profiles", () => {
// 5. Verify the new profile was merged into the cached state
expect(state.resolved.profiles.desktop).toBeDefined();
// 6. Verify GLOBAL cache was NOT cleared - subsequent simple loadConfig() still sees STALE value
// 6. Verify GLOBAL cache was NOT cleared - subsequent simple getRuntimeConfig() still sees STALE value
// This confirms the fix: we read fresh config for the specific profile lookup without flushing the global cache
const stillStaleCfg = loadConfig();
const stillStaleCfg = getRuntimeConfig();
expect(stillStaleCfg.browser?.profiles?.desktop).toBeUndefined();
});
it("forProfile still throws for profiles that don't exist in fresh config", async () => {
const cfg = loadConfig();
const cfg = getRuntimeConfig();
const resolved = resolveBrowserConfig(cfg.browser, cfg);
const state = {
server: null,
@@ -152,8 +145,8 @@ describe("server-context hot-reload profiles", () => {
).toBeNull();
});
it("forProfile refreshes existing profile config after loadConfig cache updates", async () => {
const cfg = loadConfig();
it("forProfile refreshes existing profile config after getRuntimeConfig cache updates", async () => {
const cfg = getRuntimeConfig();
const resolved = resolveBrowserConfig(cfg.browser, cfg);
const state = {
server: null,
@@ -175,7 +168,7 @@ describe("server-context hot-reload profiles", () => {
});
it("listProfiles refreshes config before enumerating profiles", async () => {
const cfg = loadConfig();
const cfg = getRuntimeConfig();
const resolved = resolveBrowserConfig(cfg.browser, cfg);
const state = {
server: null,
@@ -196,7 +189,7 @@ describe("server-context hot-reload profiles", () => {
});
it("marks existing runtime state for reconcile when profile invariants change", async () => {
const cfg = loadConfig();
const cfg = getRuntimeConfig();
const resolved = resolveBrowserConfig(cfg.browser, cfg);
const openclawProfile = resolveProfile(resolved, "openclaw");
expect(openclawProfile).toBeTruthy();
@@ -234,7 +227,7 @@ describe("server-context hot-reload profiles", () => {
});
it("marks local managed runtime state for reconcile when profile headless changes", async () => {
const cfg = loadConfig();
const cfg = getRuntimeConfig();
const resolved = resolveBrowserConfig(cfg.browser, cfg);
const openclawProfile = resolveProfile(resolved, "openclaw");
expect(openclawProfile).toBeTruthy();
@@ -283,7 +276,7 @@ describe("server-context hot-reload profiles", () => {
executablePath: "/usr/bin/chrome-old",
};
mockState.cachedConfig = null;
const cfg = loadConfig();
const cfg = getRuntimeConfig();
const resolved = resolveBrowserConfig(cfg.browser, cfg);
const openclawProfile = resolveProfile(resolved, "openclaw");
expect(openclawProfile).toBeTruthy();
@@ -333,7 +326,7 @@ describe("server-context hot-reload profiles", () => {
driver: "existing-session",
};
const cfg = loadConfig();
const cfg = getRuntimeConfig();
const resolved = resolveBrowserConfig(cfg.browser, cfg);
const remoteProfile = resolveProfile(resolved, "remote");
expect(remoteProfile).toBeTruthy();
@@ -387,7 +380,7 @@ describe("server-context hot-reload profiles", () => {
headless: true,
};
const cfg = loadConfig();
const cfg = getRuntimeConfig();
const resolved = resolveBrowserConfig(cfg.browser, cfg);
const remoteProfile = resolveProfile(resolved, "remote");
expect(remoteProfile).toBeTruthy();

View File

@@ -27,16 +27,18 @@ vi.mock("../config/config.js", async () => {
const browserConfig = {
enabled: true,
};
const loadConfig = () => {
return {
browser: browserConfig,
...(mocks.gatewayAuthMode || mocks.gatewayAuthToken
? { gateway: { auth: { mode: mocks.gatewayAuthMode, token: mocks.gatewayAuthToken } } }
: {}),
};
};
return {
...actual,
loadConfig: () => {
return {
browser: browserConfig,
...(mocks.gatewayAuthMode || mocks.gatewayAuthToken
? { gateway: { auth: { mode: mocks.gatewayAuthMode, token: mocks.gatewayAuthToken } } }
: {}),
};
},
getRuntimeConfig: loadConfig,
loadConfig,
};
});

View File

@@ -426,6 +426,7 @@ vi.mock("../config/config.js", async () => {
loadConfig,
writeConfigFile,
})),
getRuntimeConfig: loadConfig,
getRuntimeConfigSnapshot: vi.fn(() => null),
loadConfig,
writeConfigFile,

View File

@@ -37,18 +37,20 @@ const routeCtxMocks = vi.hoisted(() => {
vi.mock("../config/config.js", async () => {
const actual = await vi.importActual<typeof import("../config/config.js")>("../config/config.js");
const loadConfig = () => ({
browser: {
enabled: true,
evaluateEnabled: false,
defaultProfile: "openclaw",
profiles: {
openclaw: { cdpPort: testPort + 1, color: "#FF4500" },
},
},
});
return {
...actual,
loadConfig: () => ({
browser: {
enabled: true,
evaluateEnabled: false,
defaultProfile: "openclaw",
profiles: {
openclaw: { cdpPort: testPort + 1, color: "#FF4500" },
},
},
}),
getRuntimeConfig: loadConfig,
loadConfig,
writeConfigFile: vi.fn(async () => {}),
};
});

View File

@@ -3,7 +3,7 @@ import {
isCronSessionKey,
isSubagentSessionKey,
} from "openclaw/plugin-sdk/routing";
import { loadConfig } from "../config/config.js";
import { getRuntimeConfig } from "../config/config.js";
import { resolveBrowserConfig, type ResolvedBrowserTabCleanupConfig } from "./config.js";
import { sweepTrackedBrowserTabs } from "./session-tab-registry.js";
@@ -22,7 +22,7 @@ export function isPrimaryTrackedBrowserSessionKey(sessionKey: string): boolean {
}
export function resolveBrowserTabCleanupRuntimeConfig(): ResolvedBrowserTabCleanupConfig {
const cfg = loadConfig();
const cfg = getRuntimeConfig();
return resolveBrowserConfig(cfg.browser, cfg).tabCleanup;
}

View File

@@ -20,9 +20,13 @@ vi.mock("../../../../src/cli/gateway-rpc.js", () => ({
callGatewayFromCli: gatewayMocks.callGatewayFromCli,
}));
const configMocks = vi.hoisted(() => ({
loadConfig: vi.fn(() => ({ browser: {} })),
}));
const configMocks = vi.hoisted(() => {
const loadConfig = vi.fn(() => ({ browser: {} }));
return {
getRuntimeConfig: loadConfig,
loadConfig,
};
});
vi.mock("../config/config.js", () => configMocks);
const sharedMocks = vi.hoisted(() => ({
@@ -51,7 +55,7 @@ const sharedMocks = vi.hoisted(() => ({
vi.spyOn(browserCliSharedModule, "callBrowserRequest").mockImplementation(
sharedMocks.callBrowserRequest,
);
vi.spyOn(cliCoreApiModule, "loadConfig").mockImplementation(configMocks.loadConfig);
vi.spyOn(cliCoreApiModule, "getRuntimeConfig").mockImplementation(configMocks.loadConfig);
vi.spyOn(cliCoreApiModule.defaultRuntime, "log").mockImplementation(runtime.log);
vi.spyOn(cliCoreApiModule.defaultRuntime, "writeJson").mockImplementation(runtime.writeJson);
vi.spyOn(cliCoreApiModule.defaultRuntime, "error").mockImplementation(runtime.error);

View File

@@ -5,7 +5,7 @@ import { callBrowserRequest, type BrowserParentOpts } from "./browser-cli-shared
import {
danger,
defaultRuntime,
loadConfig,
getRuntimeConfig,
shortenHomePath,
type SnapshotResult,
} from "./core-api.js";
@@ -81,7 +81,7 @@ export function registerBrowserInspectCommands(
const configMode =
!formatWasExplicit &&
format === "ai" &&
loadConfig().browser?.snapshotDefaults?.mode === "efficient"
getRuntimeConfig().browser?.snapshotDefaults?.mode === "efficient"
? "efficient"
: undefined;
const mode = opts.efficient === true || opts.mode === "efficient" ? "efficient" : configMode;

View File

@@ -1,8 +1,7 @@
export {
createConfigIO,
getRuntimeConfig,
getRuntimeConfigSnapshot,
loadConfig,
writeConfigFile,
replaceConfigFile,
type BrowserConfig,
type BrowserProfileConfig,
type OpenClawConfig,

View File

@@ -2,7 +2,7 @@ import { resolveBrowserConfig } from "./browser/config.js";
import { ensureBrowserControlAuth } from "./browser/control-auth.js";
import { createBrowserRuntimeState, stopBrowserRuntime } from "./browser/runtime-lifecycle.js";
import { type BrowserServerState, createBrowserRouteContext } from "./browser/server-context.js";
import { loadConfig } from "./config/config.js";
import { getRuntimeConfig } from "./config/config.js";
import { createSubsystemLogger } from "./logging/subsystem.js";
import { isDefaultBrowserPluginEnabled } from "./plugin-enabled.js";
@@ -26,7 +26,7 @@ export async function startBrowserControlServiceFromConfig(): Promise<BrowserSer
return state;
}
const cfg = loadConfig();
const cfg = getRuntimeConfig();
if (!isDefaultBrowserPluginEnabled(cfg)) {
return null;
}

View File

@@ -85,7 +85,7 @@ export {
theme,
} from "openclaw/plugin-sdk/browser-setup-tools";
export {
loadConfig,
getRuntimeConfig,
normalizePluginsConfig,
parseBooleanValue,
resolveEffectiveEnableState,

View File

@@ -9,9 +9,9 @@ import {
createBrowserControlContext,
createBrowserRouteDispatcher,
errorShape,
getRuntimeConfig,
isNodeCommandAllowed,
isPersistentBrowserProfileMutation,
loadConfig,
persistBrowserProxyFiles,
resolveNodeCommandAllowlist,
resolveRequestedBrowserProfile,
@@ -21,6 +21,7 @@ import {
withTimeout,
type GatewayRequestHandlers,
type NodeSession,
type OpenClawConfig,
} from "../core-api.js";
type BrowserRequestParams = {
@@ -88,7 +89,7 @@ function resolveBrowserNode(nodes: NodeSession[], query: string): NodeSession |
}
function resolveBrowserNodeTarget(params: {
cfg: ReturnType<typeof loadConfig>;
cfg: OpenClawConfig;
nodes: NodeSession[];
}): NodeSession | null {
const policy = params.cfg.gateway?.nodes?.browser;
@@ -171,7 +172,7 @@ export async function handleBrowserGatewayRequest({
return;
}
const cfg = loadConfig();
const cfg = getRuntimeConfig();
let nodeTarget: NodeSession | null = null;
try {
nodeTarget = resolveBrowserNodeTarget({

View File

@@ -27,6 +27,7 @@ const browserConfigMocks = vi.hoisted(() => ({
}));
vi.mock("openclaw/plugin-sdk/browser-config-runtime", () => ({
getRuntimeConfig: configMocks.loadConfig,
loadConfig: configMocks.loadConfig,
}));

View File

@@ -1,5 +1,5 @@
import fsPromises from "node:fs/promises";
import { loadConfig } from "openclaw/plugin-sdk/browser-config-runtime";
import { getRuntimeConfig } from "openclaw/plugin-sdk/browser-config-runtime";
import { withTimeout } from "openclaw/plugin-sdk/browser-node-runtime";
import { detectMime } from "openclaw/plugin-sdk/browser-setup-tools";
import { redactCdpUrl } from "../browser/cdp.helpers.js";
@@ -44,7 +44,7 @@ function normalizeProfileAllowlist(raw?: string[]): string[] {
}
function resolveBrowserProxyConfig() {
const cfg = loadConfig();
const cfg = getRuntimeConfig();
const proxy = cfg.nodeHost?.browserProxy;
const allowProfiles = normalizeProfileAllowlist(proxy?.allowProfiles);
const enabled = proxy?.enabled !== false;
@@ -64,7 +64,7 @@ async function ensureBrowserControlService(): Promise<void> {
return browserControlReady;
}
browserControlReady = (async () => {
const cfg = loadConfig();
const cfg = getRuntimeConfig();
const resolved = resolveBrowserConfig(cfg.browser, cfg);
if (!resolved.enabled) {
throw new Error("browser control disabled");
@@ -231,7 +231,7 @@ export async function runBrowserProxyCommand(paramsJSON?: string | null): Promis
}
await ensureBrowserControlService();
const cfg = loadConfig();
const cfg = getRuntimeConfig();
const resolved = resolveBrowserConfig(cfg.browser, cfg);
const method = typeof params.method === "string" ? params.method.toUpperCase() : "GET";
const path = normalizeBrowserRequestPath(pathValue);

View File

@@ -15,7 +15,7 @@ import {
installBrowserAuthMiddleware,
installBrowserCommonMiddleware,
} from "./browser/server-middleware.js";
import { loadConfig } from "./config/config.js";
import { getRuntimeConfig } from "./config/config.js";
import { createSubsystemLogger } from "./logging/subsystem.js";
import { isDefaultBrowserPluginEnabled } from "./plugin-enabled.js";
@@ -28,7 +28,7 @@ export async function startBrowserControlServerFromConfig(): Promise<BrowserServ
return state;
}
const cfg = loadConfig();
const cfg = getRuntimeConfig();
if (!isDefaultBrowserPluginEnabled(cfg)) {
return null;
}

View File

@@ -1,4 +1,7 @@
import { resolveLivePluginConfigObject } from "openclaw/plugin-sdk/config-runtime";
import {
resolveLivePluginConfigObject,
type OpenClawConfig,
} from "openclaw/plugin-sdk/config-runtime";
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import { createCodexAppServerAgentHarness } from "./harness.js";
import { buildCodexMediaUnderstandingProvider } from "./media-understanding-provider.js";
@@ -16,7 +19,9 @@ export default definePluginEntry({
register(api) {
const resolveCurrentPluginConfig = () =>
resolveLivePluginConfigObject(
api.runtime.config?.loadConfig,
api.runtime.config?.current
? () => api.runtime.config.current() as OpenClawConfig
: undefined,
"codex",
api.pluginConfig as Record<string, unknown>,
) ?? api.pluginConfig;

View File

@@ -256,7 +256,7 @@ describe("diffs plugin registration", () => {
},
runtime: {
config: {
loadConfig: () => configFile,
current: () => configFile,
},
} as never,
registerTool(tool: Parameters<OpenClawPluginApi["registerTool"]>[0]) {
@@ -384,7 +384,7 @@ describe("diffs plugin registration", () => {
},
runtime: {
config: {
loadConfig: () => configFile,
current: () => configFile,
},
} as never,
registerTool(tool: Parameters<OpenClawPluginApi["registerTool"]>[0]) {
@@ -521,7 +521,7 @@ describe("diffs plugin registration", () => {
},
runtime: {
config: {
loadConfig: () => configFile,
current: () => configFile,
},
} as never,
registerTool(tool: Parameters<OpenClawPluginApi["registerTool"]>[0]) {

View File

@@ -1,6 +1,10 @@
import path from "node:path";
import { resolveLivePluginConfigObject } from "openclaw/plugin-sdk/config-runtime";
import { resolvePreferredOpenClawTmpDir, type OpenClawPluginApi } from "../api.js";
import {
resolvePreferredOpenClawTmpDir,
type OpenClawConfig,
type OpenClawPluginApi,
} from "../api.js";
import {
resolveDiffsPluginDefaults,
resolveDiffsPluginSecurity,
@@ -18,12 +22,14 @@ export function registerDiffsPlugin(api: OpenClawPluginApi): void {
});
const resolveCurrentPluginConfig = () =>
resolveLivePluginConfigObject(
api.runtime.config?.loadConfig,
api.runtime.config?.current
? () => api.runtime.config.current() as OpenClawConfig
: undefined,
"diffs",
api.pluginConfig as Record<string, unknown>,
) ?? {};
const resolveCurrentAccessConfig = () => {
const currentConfig = api.runtime.config?.loadConfig?.() ?? api.config;
const currentConfig = (api.runtime.config?.current?.() ?? api.config) as OpenClawConfig;
const pluginConfig = resolveCurrentPluginConfig();
return {
allowRemoteViewer: resolveDiffsPluginSecurity(pluginConfig).allowRemoteViewer,

View File

@@ -991,7 +991,7 @@ function makeReactionListenerParams(overrides?: {
guildEntries?: Record<string, DiscordGuildEntryResolved>;
}) {
return {
cfg: {} as ReturnType<typeof import("openclaw/plugin-sdk/config-runtime").loadConfig>,
cfg: {} as import("openclaw/plugin-sdk/config-runtime").OpenClawConfig,
accountId: "acc-1",
runtime: {} as import("openclaw/plugin-sdk/runtime-env").RuntimeEnv,
botUserId: overrides?.botUserId ?? "bot-1",

View File

@@ -9,7 +9,7 @@ vi.mock("openclaw/plugin-sdk/config-runtime", async () => {
);
return {
...actual,
loadConfig: () => loadConfigMock(),
getRuntimeConfig: () => loadConfigMock(),
};
});

View File

@@ -39,7 +39,7 @@ import { resolveFetchedDiscordThreadLikeChannelContext } from "./thread-channel-
import { closeDiscordThreadSessions } from "./thread-session-close.js";
import { normalizeDiscordListenerTimeoutMs, runDiscordTaskWithTimeout } from "./timeouts.js";
type LoadedConfig = ReturnType<typeof import("openclaw/plugin-sdk/config-runtime").loadConfig>;
type LoadedConfig = OpenClawConfig;
type RuntimeEnv = import("openclaw/plugin-sdk/runtime-env").RuntimeEnv;
type Logger = ReturnType<typeof import("openclaw/plugin-sdk/runtime-env").createSubsystemLogger>;

View File

@@ -1,5 +1,5 @@
import type { ChannelType, Client, User } from "@buape/carbon";
import type { ReplyToMode } from "openclaw/plugin-sdk/config-runtime";
import type { OpenClawConfig, ReplyToMode } from "openclaw/plugin-sdk/config-runtime";
import type { SessionBindingRecord } from "openclaw/plugin-sdk/conversation-runtime";
import type { HistoryEntry } from "openclaw/plugin-sdk/reply-history";
import type { resolveAgentRoute } from "openclaw/plugin-sdk/routing";
@@ -11,9 +11,7 @@ import type { DiscordSenderIdentity } from "./sender-identity.js";
export type { DiscordSenderIdentity } from "./sender-identity.js";
import type { DiscordThreadChannel } from "./threading.js";
export type LoadedConfig = ReturnType<
typeof import("openclaw/plugin-sdk/config-runtime").loadConfig
>;
export type LoadedConfig = OpenClawConfig;
export type RuntimeEnv = import("openclaw/plugin-sdk/runtime-env").RuntimeEnv;
export type DiscordMessageEvent = import("./listeners.js").DiscordMessageEvent;

View File

@@ -24,7 +24,7 @@ import {
type CommandArgValues,
type CommandArgs,
} from "openclaw/plugin-sdk/command-auth";
import type { OpenClawConfig, loadConfig } from "openclaw/plugin-sdk/config-runtime";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
import { loadSessionStore, resolveStorePath } from "openclaw/plugin-sdk/config-runtime";
import type { ResolvedAgentRoute } from "openclaw/plugin-sdk/routing";
import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
@@ -64,7 +64,7 @@ type DiscordNativeChoiceInteraction =
const DISCORD_COMMAND_ARG_CUSTOM_ID_KEY = "cmdarg";
export type DiscordCommandArgContext = {
cfg: ReturnType<typeof loadConfig>;
cfg: OpenClawConfig;
discordConfig: DiscordConfig;
accountId: string;
sessionPrefix: string;
@@ -79,7 +79,7 @@ export type DispatchDiscordCommandInteractionParams = {
prompt: string;
command: ChatCommandDefinition;
commandArgs?: CommandArgs;
cfg: ReturnType<typeof loadConfig>;
cfg: OpenClawConfig;
discordConfig: DiscordConfig;
accountId: string;
sessionPrefix: string;
@@ -242,7 +242,7 @@ async function resolveDiscordModelPickerRouteState(params: {
| ButtonInteraction
| StringSelectMenuInteraction
| AutocompleteInteraction;
cfg: ReturnType<typeof loadConfig>;
cfg: OpenClawConfig;
accountId: string;
threadBindings: ThreadBindingManager;
enforceConfiguredBindingReadiness?: boolean;
@@ -283,7 +283,7 @@ async function resolveDiscordModelPickerRoute(params: {
| ButtonInteraction
| StringSelectMenuInteraction
| AutocompleteInteraction;
cfg: ReturnType<typeof loadConfig>;
cfg: OpenClawConfig;
accountId: string;
threadBindings: ThreadBindingManager;
}) {
@@ -293,7 +293,7 @@ async function resolveDiscordModelPickerRoute(params: {
export async function resolveDiscordNativeChoiceContext(params: {
interaction: DiscordNativeChoiceInteraction;
cfg: ReturnType<typeof loadConfig>;
cfg: OpenClawConfig;
accountId: string;
threadBindings: ThreadBindingManager;
}): Promise<{ provider?: string; model?: string } | null> {
@@ -340,7 +340,7 @@ export async function resolveDiscordNativeChoiceContext(params: {
}
function resolveDiscordModelPickerCurrentModel(params: {
cfg: ReturnType<typeof loadConfig>;
cfg: OpenClawConfig;
route: ResolvedAgentRoute;
data: Awaited<ReturnType<typeof loadDiscordModelPickerData>>;
}): string {
@@ -374,7 +374,7 @@ function resolveDiscordModelPickerCurrentModel(params: {
}
function resolveDiscordModelPickerCurrentRuntime(params: {
cfg: ReturnType<typeof loadConfig>;
cfg: OpenClawConfig;
route: ResolvedAgentRoute;
}): string {
try {
@@ -405,7 +405,7 @@ function resolveDiscordModelPickerCurrentRuntime(params: {
export async function replyWithDiscordModelPickerProviders(params: {
interaction: CommandInteraction | ButtonInteraction | StringSelectMenuInteraction;
cfg: ReturnType<typeof loadConfig>;
cfg: OpenClawConfig;
command: DiscordModelPickerCommandContext;
userId: string;
accountId: string;

View File

@@ -1,5 +1,5 @@
import { ChannelType } from "discord-api-types/v10";
import type { OpenClawConfig, loadConfig } from "openclaw/plugin-sdk/config-runtime";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
const { logVerboseMock } = vi.hoisted(() => ({
@@ -37,7 +37,7 @@ let createNoopThreadBindingManager: typeof import("./thread-bindings.js").create
function createNativeCommand(
name: string,
opts?: {
cfg?: ReturnType<typeof loadConfig>;
cfg?: OpenClawConfig;
discordConfig?: NonNullable<OpenClawConfig["channels"]>["discord"];
},
): ReturnType<typeof import("./native-command.js").createDiscordNativeCommand> {
@@ -47,7 +47,7 @@ function createNativeCommand(
if (!command) {
throw new Error(`missing native command: ${name}`);
}
const baseCfg: ReturnType<typeof loadConfig> = opts?.cfg ?? {};
const baseCfg: OpenClawConfig = opts?.cfg ?? {};
const discordConfig: NonNullable<OpenClawConfig["channels"]>["discord"] =
opts?.discordConfig ?? baseCfg.channels?.discord ?? {};
const cfg =
@@ -211,7 +211,7 @@ describe("createDiscordNativeCommand option wiring", () => {
discord: ["user:allowed-user"],
},
},
} as ReturnType<typeof loadConfig>,
} as OpenClawConfig,
});
const level = requireOption(command, "level");
const autocomplete = requireAutocomplete(level, "think level option did not wire autocomplete");
@@ -250,7 +250,7 @@ describe("createDiscordNativeCommand option wiring", () => {
},
},
},
} as ReturnType<typeof loadConfig>,
} as OpenClawConfig,
});
const level = requireOption(command, "level");
const autocomplete = requireAutocomplete(level, "think level option did not wire autocomplete");
@@ -284,7 +284,7 @@ describe("createDiscordNativeCommand option wiring", () => {
discord: ["user:allowed-user"],
},
},
} as ReturnType<typeof loadConfig>,
} as OpenClawConfig,
discordConfig,
});
const level = requireOption(command, "level");
@@ -304,7 +304,7 @@ describe("createDiscordNativeCommand option wiring", () => {
it("truncates Discord command and option descriptions to Discord's limit", () => {
const longDescription = "x".repeat(140);
const cfg = {} as ReturnType<typeof loadConfig>;
const cfg = {} as OpenClawConfig;
const discordConfig = {} as NonNullable<OpenClawConfig["channels"]>["discord"];
const command = createDiscordNativeCommand({
command: {

View File

@@ -2,8 +2,10 @@ import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { ChannelType, type AutocompleteInteraction } from "@buape/carbon";
import type { loadConfig } from "openclaw/plugin-sdk/config-runtime";
import { clearSessionStoreCacheForTest } from "openclaw/plugin-sdk/config-runtime";
import {
clearSessionStoreCacheForTest,
type OpenClawConfig,
} from "openclaw/plugin-sdk/config-runtime";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { createNoopThreadBindingManager } from "./thread-bindings.js";
@@ -92,7 +94,7 @@ vi.mock("openclaw/plugin-sdk/conversation-binding-runtime", async () => {
vi.mock("openclaw/plugin-sdk/agent-runtime", () => ({
normalizeProviderId: (value: string) => value.trim().toLowerCase(),
resolveDefaultModelForAgent: (params: { cfg: ReturnType<typeof loadConfig> }) => {
resolveDefaultModelForAgent: (params: { cfg: OpenClawConfig }) => {
const configuredModel = params.cfg.agents?.defaults?.model;
const primary =
typeof configuredModel === "string"
@@ -216,7 +218,7 @@ describe("discord native /think autocomplete", () => {
session: {
store: STORE_PATH,
},
} as ReturnType<typeof loadConfig>;
} as OpenClawConfig;
}
it("uses the session override context for /think choices", async () => {

View File

@@ -18,7 +18,7 @@ import {
resolveNativeCommandSessionTargets,
} from "openclaw/plugin-sdk/command-auth-native";
import { resolveDirectStatusReplyForSession } from "openclaw/plugin-sdk/command-status-runtime";
import type { OpenClawConfig, loadConfig } from "openclaw/plugin-sdk/config-runtime";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
import { buildPairingReply } from "openclaw/plugin-sdk/conversation-runtime";
import { isDangerousNameMatchingEnabled } from "openclaw/plugin-sdk/dangerous-name-runtime";
import { getAgentScopedMediaLocalRoots } from "openclaw/plugin-sdk/media-runtime";
@@ -199,7 +199,7 @@ function resolveDiscordNativeCommandAllowlistAccess(params: {
}
function resolveDiscordGuildNativeCommandAuthorized(params: {
cfg: ReturnType<typeof loadConfig>;
cfg: OpenClawConfig;
discordConfig: DiscordConfig;
useAccessGroups: boolean;
commandsAllowFromAccess: ReturnType<typeof resolveDiscordNativeCommandAllowlistAccess>;
@@ -261,7 +261,7 @@ function resolveDiscordGuildNativeCommandAuthorized(params: {
function buildDiscordCommandOptions(params: {
command: ChatCommandDefinition;
cfg: ReturnType<typeof loadConfig>;
cfg: OpenClawConfig;
authorizeChoiceContext?: (interaction: AutocompleteInteraction) => Promise<boolean>;
resolveChoiceContext?: (
interaction: AutocompleteInteraction,
@@ -397,7 +397,7 @@ function resolveDiscordNativeGroupDmAccess(params: {
async function resolveDiscordNativeAutocompleteAuthorized(params: {
interaction: AutocompleteInteraction;
cfg: ReturnType<typeof loadConfig>;
cfg: OpenClawConfig;
discordConfig: DiscordConfig;
accountId: string;
}): Promise<boolean> {
@@ -633,7 +633,7 @@ function createNativeCommandDefinition(command: NativeCommandSpec): ChatCommandD
export function createDiscordNativeCommand(params: {
command: NativeCommandSpec;
cfg: ReturnType<typeof loadConfig>;
cfg: OpenClawConfig;
discordConfig: DiscordConfig;
accountId: string;
sessionPrefix: string;
@@ -741,7 +741,7 @@ async function dispatchDiscordCommandInteraction(params: {
prompt: string;
command: ChatCommandDefinition;
commandArgs?: DiscordCommandArgs;
cfg: ReturnType<typeof loadConfig>;
cfg: OpenClawConfig;
discordConfig: DiscordConfig;
accountId: string;
sessionPrefix: string;

View File

@@ -22,7 +22,7 @@ import {
resolveNativeSkillsEnabled,
} from "openclaw/plugin-sdk/config-runtime";
import type { OpenClawConfig, ReplyToMode } from "openclaw/plugin-sdk/config-runtime";
import { loadConfig } from "openclaw/plugin-sdk/config-runtime";
import { getRuntimeConfig } from "openclaw/plugin-sdk/config-runtime";
import { createConnectedChannelStatusPatch } from "openclaw/plugin-sdk/gateway-runtime";
import { resolveTextChunkLimit } from "openclaw/plugin-sdk/reply-chunking";
import {
@@ -593,7 +593,7 @@ function isDiscordDisallowedIntentsError(err: unknown): boolean {
export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
const startupStartedAt = Date.now();
const cfg = opts.config ?? loadConfig();
const cfg = opts.config ?? getRuntimeConfig();
const account = (resolveDiscordAccountForTesting ?? resolveDiscordAccount)({
cfg,
accountId: opts.accountId,

View File

@@ -1140,11 +1140,14 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount, FeishuProbeResul
auth: {
login: async ({ cfg }) => {
const { createClackPrompter } = await import("openclaw/plugin-sdk/setup-runtime");
const { writeConfigFile } = await import("openclaw/plugin-sdk/config-runtime");
const { replaceConfigFile } = await import("openclaw/plugin-sdk/config-runtime");
const prompter = createClackPrompter();
const nextCfg = await runFeishuLogin({ cfg, prompter });
if (nextCfg !== cfg) {
await writeConfigFile(nextCfg);
await replaceConfigFile({
nextConfig: nextCfg,
afterWrite: { mode: "auto" },
});
}
},
},

View File

@@ -72,7 +72,10 @@ export async function maybeCreateDynamicAgent(params: {
],
};
await runtime.config.writeConfigFile(updatedCfg);
await runtime.config.replaceConfigFile({
nextConfig: updatedCfg,
afterWrite: { mode: "auto" },
});
return { created: true, updatedCfg, agentId };
}
@@ -115,7 +118,10 @@ export async function maybeCreateDynamicAgent(params: {
};
// Write updated config using PluginRuntime API
await runtime.config.writeConfigFile(updatedCfg);
await runtime.config.replaceConfigFile({
nextConfig: updatedCfg,
afterWrite: { mode: "auto" },
});
return { created: true, updatedCfg, agentId };
}

View File

@@ -6,7 +6,7 @@ import {
} from "openclaw/plugin-sdk/channel-inbound";
import { createChannelPairingChallengeIssuer } from "openclaw/plugin-sdk/channel-pairing";
import { createChannelReplyPipeline } from "openclaw/plugin-sdk/channel-reply-pipeline";
import { loadConfig } from "openclaw/plugin-sdk/config-runtime";
import { getRuntimeConfig } from "openclaw/plugin-sdk/config-runtime";
import {
resolveOpenProviderRuntimeGroupPolicy,
resolveDefaultGroupPolicy,
@@ -116,7 +116,7 @@ async function waitForWatchSubscribeRetryDelay(params: {
export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): Promise<void> {
const runtime = resolveRuntime(opts);
const cfg = opts.config ?? loadConfig();
const cfg = opts.config ?? getRuntimeConfig();
const accountInfo = resolveIMessageAccount({
cfg,
accountId: opts.accountId,

View File

@@ -1,5 +1,5 @@
import type { BaseProbeResult } from "openclaw/plugin-sdk/channel-contract";
import { loadConfig } from "openclaw/plugin-sdk/config-runtime";
import { getRuntimeConfig } from "openclaw/plugin-sdk/config-runtime";
import { runCommandWithTimeout } from "openclaw/plugin-sdk/process-runtime";
import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
import { detectBinary } from "openclaw/plugin-sdk/setup";
@@ -69,7 +69,7 @@ export async function probeIMessage(
timeoutMs?: number,
opts: IMessageProbeOptions = {},
): Promise<IMessageProbe> {
const cfg = opts.cliPath || opts.dbPath ? undefined : loadConfig();
const cfg = opts.cliPath || opts.dbPath ? undefined : getRuntimeConfig();
const cliPath = opts.cliPath?.trim() || cfg?.channels?.imessage?.cliPath?.trim() || "imsg";
const dbPath = opts.dbPath?.trim() || cfg?.channels?.imessage?.dbPath?.trim();
// Use explicit timeout if provided, otherwise fall back to config, then default

View File

@@ -35,7 +35,7 @@ export function resolveIrcInboundTarget(params: { target: string; senderNick: st
export async function monitorIrcProvider(opts: IrcMonitorOptions): Promise<{ stop: () => void }> {
const core = getIrcRuntime();
const cfg = opts.config ?? (core.config.loadConfig() as CoreConfig);
const cfg = opts.config ?? (core.config.current() as CoreConfig);
const account = resolveIrcAccount({
cfg,
accountId: opts.accountId,

View File

@@ -1,7 +1,7 @@
import type { webhook } from "@line/bot-sdk";
import type { NextFunction, Request, Response } from "express";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
import { loadConfig } from "openclaw/plugin-sdk/config-runtime";
import { getRuntimeConfig } from "openclaw/plugin-sdk/config-runtime";
import { DEFAULT_GROUP_HISTORY_LIMIT, type HistoryEntry } from "openclaw/plugin-sdk/reply-history";
import {
createNonExitingRuntime,
@@ -32,7 +32,7 @@ export interface LineBot {
export function createLineBot(opts: LineBotOptions): LineBot {
const runtime: RuntimeEnv = opts.runtime ?? createNonExitingRuntime();
const cfg = opts.config ?? loadConfig();
const cfg = opts.config ?? getRuntimeConfig();
const account = resolveLineAccount({
cfg,
accountId: opts.accountId,

View File

@@ -7,12 +7,12 @@ import { setLineRuntime } from "./runtime.js";
const DEFAULT_ACCOUNT_ID = "default";
type LineRuntimeMocks = {
writeConfigFile: ReturnType<typeof vi.fn>;
replaceConfigFile: ReturnType<typeof vi.fn>;
resolveLineAccount: ReturnType<typeof vi.fn>;
};
function createRuntime(): { runtime: PluginRuntime; mocks: LineRuntimeMocks } {
const writeConfigFile = vi.fn(async () => {});
const replaceConfigFile = vi.fn(async () => {});
const resolveLineAccount = vi.fn(
({ cfg, accountId }: { cfg: OpenClawConfig; accountId?: string }) => {
const lineConfig = (cfg.channels?.line ?? {}) as {
@@ -34,10 +34,10 @@ function createRuntime(): { runtime: PluginRuntime; mocks: LineRuntimeMocks } {
);
const runtime = {
config: { writeConfigFile },
config: { replaceConfigFile },
} as unknown as PluginRuntime;
return { runtime, mocks: { writeConfigFile, resolveLineAccount } };
return { runtime, mocks: { replaceConfigFile, resolveLineAccount } };
}
function resolveAccount(
@@ -89,7 +89,10 @@ describe("linePlugin gateway.logoutAccount", () => {
expect(result.cleared).toBe(true);
expect(result.loggedOut).toBe(true);
expect(mocks.writeConfigFile).toHaveBeenCalledWith({});
expect(mocks.replaceConfigFile).toHaveBeenCalledWith({
nextConfig: {},
afterWrite: { mode: "auto" },
});
});
it("clears tokenFile/secretFile on account logout", async () => {
@@ -112,7 +115,10 @@ describe("linePlugin gateway.logoutAccount", () => {
expect(result.cleared).toBe(true);
expect(result.loggedOut).toBe(true);
expect(mocks.writeConfigFile).toHaveBeenCalledWith({});
expect(mocks.replaceConfigFile).toHaveBeenCalledWith({
nextConfig: {},
afterWrite: { mode: "auto" },
});
});
it("does not write config when account has no token/secret fields", async () => {
@@ -134,6 +140,6 @@ describe("linePlugin gateway.logoutAccount", () => {
expect(result.cleared).toBe(false);
expect(result.loggedOut).toBe(true);
expect(mocks.writeConfigFile).not.toHaveBeenCalled();
expect(mocks.replaceConfigFile).not.toHaveBeenCalled();
});
});

View File

@@ -112,7 +112,10 @@ export const lineGatewayAdapter: NonNullable<ChannelPlugin<ResolvedLineAccount>[
delete nextCfg.channels;
}
}
await getLineRuntime().config.writeConfigFile(nextCfg);
await getLineRuntime().config.replaceConfigFile({
nextConfig: nextCfg,
afterWrite: { mode: "auto" },
});
}
const resolved = resolveLineAccount({

View File

@@ -8,7 +8,7 @@ const profileAction = "set-profile" as const;
const runtimeStub = {
config: {
loadConfig: () => ({}),
current: () => ({}),
},
media: {
loadWebMedia: async () => {

View File

@@ -22,7 +22,7 @@ const resolveMatrixAuthContextMock = vi.fn();
const matrixSetupApplyAccountConfigMock = vi.fn();
const matrixSetupValidateInputMock = vi.fn();
const matrixRuntimeLoadConfigMock = vi.fn();
const matrixRuntimeWriteConfigFileMock = vi.fn();
const matrixRuntimeReplaceConfigFileMock = vi.fn();
const resetMatrixRoomKeyBackupMock = vi.fn();
const restoreMatrixRoomKeyBackupMock = vi.fn();
const runMatrixSelfVerificationMock = vi.fn();
@@ -96,8 +96,8 @@ vi.mock("./setup-core.js", () => ({
vi.mock("./runtime.js", () => ({
getMatrixRuntime: () => ({
config: {
loadConfig: (...args: unknown[]) => matrixRuntimeLoadConfigMock(...args),
writeConfigFile: (...args: unknown[]) => matrixRuntimeWriteConfigFileMock(...args),
current: (...args: unknown[]) => matrixRuntimeLoadConfigMock(...args),
replaceConfigFile: (...args: unknown[]) => matrixRuntimeReplaceConfigFileMock(...args),
},
}),
}));
@@ -180,7 +180,7 @@ describe("matrix CLI verification commands", () => {
matrixSetupValidateInputMock.mockReturnValue(null);
matrixSetupApplyAccountConfigMock.mockImplementation(({ cfg }: { cfg: unknown }) => cfg);
matrixRuntimeLoadConfigMock.mockReturnValue({});
matrixRuntimeWriteConfigFileMock.mockResolvedValue(undefined);
matrixRuntimeReplaceConfigFileMock.mockResolvedValue(undefined);
resolveMatrixAuthContextMock.mockImplementation(
({ cfg, accountId }: { cfg: unknown; accountId?: string | null }) => ({
cfg,
@@ -1018,8 +1018,8 @@ describe("matrix CLI verification commands", () => {
}),
}),
);
expect(matrixRuntimeWriteConfigFileMock).toHaveBeenCalledWith(
expect.objectContaining({
expect(matrixRuntimeReplaceConfigFileMock).toHaveBeenCalledWith({
nextConfig: expect.objectContaining({
channels: {
matrix: {
accounts: {
@@ -1030,7 +1030,8 @@ describe("matrix CLI verification commands", () => {
},
},
}),
);
afterWrite: { mode: "auto" },
});
expect(console.log).toHaveBeenCalledWith("Saved matrix account: ops");
expect(console.log).toHaveBeenCalledWith(
"Bind this account to an agent: openclaw agents bind --agent <id> --bind matrix:ops",
@@ -1086,8 +1087,8 @@ describe("matrix CLI verification commands", () => {
{ from: "user" },
);
expect(matrixRuntimeWriteConfigFileMock).toHaveBeenCalledWith(
expect.objectContaining({
expect(matrixRuntimeReplaceConfigFileMock).toHaveBeenCalledWith({
nextConfig: expect.objectContaining({
channels: {
matrix: {
enabled: true,
@@ -1099,7 +1100,8 @@ describe("matrix CLI verification commands", () => {
},
},
}),
);
afterWrite: { mode: "auto" },
});
expect(bootstrapMatrixVerificationMock).toHaveBeenCalledWith({
accountId: "ops",
cfg: expect.objectContaining({
@@ -1159,8 +1161,8 @@ describe("matrix CLI verification commands", () => {
from: "user",
});
expect(matrixRuntimeWriteConfigFileMock).toHaveBeenCalledWith(
expect.objectContaining({
expect(matrixRuntimeReplaceConfigFileMock).toHaveBeenCalledWith({
nextConfig: expect.objectContaining({
channels: {
matrix: {
enabled: true,
@@ -1172,7 +1174,8 @@ describe("matrix CLI verification commands", () => {
},
},
}),
);
afterWrite: { mode: "auto" },
});
expect(bootstrapMatrixVerificationMock).toHaveBeenCalledWith({
accountId: "ops",
cfg: expect.objectContaining({
@@ -1378,7 +1381,7 @@ describe("matrix CLI verification commands", () => {
{ from: "user" },
);
expect(matrixRuntimeWriteConfigFileMock).toHaveBeenCalled();
expect(matrixRuntimeReplaceConfigFileMock).toHaveBeenCalled();
expect(process.exitCode).toBeUndefined();
expect(console.log).toHaveBeenCalledWith("Saved matrix account: ops");
expect(console.error).toHaveBeenCalledWith(
@@ -1408,7 +1411,7 @@ describe("matrix CLI verification commands", () => {
{ from: "user" },
);
expect(matrixRuntimeWriteConfigFileMock).toHaveBeenCalled();
expect(matrixRuntimeReplaceConfigFileMock).toHaveBeenCalled();
expect(process.exitCode).toBeUndefined();
const jsonOutput = stdoutWriteMock.mock.calls.at(-1)?.[0];
expect(typeof jsonOutput).toBe("string");
@@ -1558,7 +1561,7 @@ describe("matrix CLI verification commands", () => {
avatarUrl: "mxc://example/avatar",
}),
);
expect(matrixRuntimeWriteConfigFileMock).toHaveBeenCalled();
expect(matrixRuntimeReplaceConfigFileMock).toHaveBeenCalled();
expect(console.log).toHaveBeenCalledWith("Account: alerts");
expect(console.log).toHaveBeenCalledWith("Config path: channels.matrix.accounts.alerts");
});

View File

@@ -150,7 +150,7 @@ function resolveMatrixCliAccountContext(accountId?: string): {
accountId: string;
cfg: CoreConfig;
} {
const cfg = getMatrixRuntime().config.loadConfig() as CoreConfig;
const cfg = getMatrixRuntime().config.current() as CoreConfig;
return {
accountId: resolveMatrixAuthContext({ cfg, accountId }).accountId,
cfg,
@@ -284,7 +284,7 @@ async function addMatrixAccount(params: {
enableEncryption?: boolean;
}): Promise<MatrixCliAccountAddResult> {
const runtime = getMatrixRuntime();
const cfg = runtime.config.loadConfig() as CoreConfig;
const cfg = runtime.config.current() as CoreConfig;
if (!matrixSetupAdapter.applyAccountConfig) {
throw new Error("Matrix account setup is unavailable.");
}
@@ -325,7 +325,10 @@ async function addMatrixAccount(params: {
if (params.enableEncryption === true) {
updated = updateMatrixAccountConfig(updated, accountId, { encryption: true });
}
await runtime.config.writeConfigFile(updated as never);
await runtime.config.replaceConfigFile({
nextConfig: updated as never,
afterWrite: { mode: "auto" },
});
const accountConfig = resolveMatrixAccountConfig({ cfg: updated, accountId });
let verificationBootstrap: MatrixCliAccountAddResult["verificationBootstrap"] = {
@@ -362,11 +365,14 @@ async function addMatrixAccount(params: {
});
let resolvedAvatarUrl = synced.resolvedAvatarUrl;
if (synced.convertedAvatarFromHttp && synced.resolvedAvatarUrl) {
const latestCfg = runtime.config.loadConfig() as CoreConfig;
const latestCfg = runtime.config.current() as CoreConfig;
const withAvatar = updateMatrixAccountConfig(latestCfg, accountId, {
avatarUrl: synced.resolvedAvatarUrl,
});
await runtime.config.writeConfigFile(withAvatar as never);
await runtime.config.replaceConfigFile({
nextConfig: withAvatar as never,
afterWrite: { mode: "auto" },
});
resolvedAvatarUrl = synced.resolvedAvatarUrl;
}
profile = {
@@ -462,7 +468,7 @@ async function inspectMatrixDirectRoom(params: {
accountId: string;
userId: string;
}): Promise<MatrixCliDirectRoomInspection> {
const cfg = getMatrixRuntime().config.loadConfig() as CoreConfig;
const cfg = getMatrixRuntime().config.current() as CoreConfig;
const [{ withResolvedActionClient }, { inspectMatrixDirectRooms }] = await Promise.all([
loadMatrixActionClientModule(),
loadMatrixDirectManagementModule(),
@@ -492,7 +498,7 @@ async function repairMatrixDirectRoom(params: {
accountId: string;
userId: string;
}): Promise<MatrixCliDirectRoomRepair> {
const cfg = getMatrixRuntime().config.loadConfig() as CoreConfig;
const cfg = getMatrixRuntime().config.current() as CoreConfig;
const account = resolveMatrixAccount({ cfg, accountId: params.accountId });
const [{ withStartedActionClient }, { repairMatrixDirectRooms }] = await Promise.all([
loadMatrixActionClientModule(),
@@ -734,7 +740,10 @@ async function setupMatrixEncryption(params: {
? updateMatrixAccountConfig(cfg, accountId, { encryption: true })
: cfg;
if (encryptionChanged) {
await runtime.config.writeConfigFile(updated as never);
await runtime.config.replaceConfigFile({
nextConfig: updated as never,
afterWrite: { mode: "auto" },
});
}
const canUseExistingBootstrap =

View File

@@ -13,7 +13,7 @@ const MATRIX_ACTION_TEST_CFG = {
function installMatrixActionTestRuntime(): void {
setMatrixRuntime({
config: {
loadConfig: () => ({}),
current: () => ({}),
},
channel: {
text: {

View File

@@ -11,7 +11,7 @@ const loadConfigMock = vi.fn(() => ({
vi.mock("../../runtime.js", () => ({
getMatrixRuntime: () => ({
config: {
loadConfig: loadConfigMock,
current: loadConfigMock,
},
}),
}));

View File

@@ -81,7 +81,7 @@ export function primeMatrixClientResolverMocks(params?: {
loadConfigMock.mockReturnValue(cfg);
getMatrixRuntimeMock.mockReturnValue({
config: {
loadConfig: loadConfigMock,
current: loadConfigMock,
},
});
getActiveMatrixClientMock.mockReturnValue(null);

View File

@@ -2,6 +2,7 @@ import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { normalizeAccountId } from "openclaw/plugin-sdk/account-id";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
import {
requiresExplicitMatrixDefaultAccount,
resolveMatrixDefaultOrOnlyAccountId,
@@ -48,7 +49,7 @@ function resolveLegacyStoragePaths(env: NodeJS.ProcessEnv = process.env): {
}
function assertLegacyMigrationAccountSelection(params: { accountKey: string }): void {
const cfg = getMatrixRuntime().config.loadConfig();
const cfg = getMatrixRuntime().config.current() as OpenClawConfig;
if (!cfg.channels?.matrix || typeof cfg.channels.matrix !== "object") {
return;
}

View File

@@ -2,6 +2,7 @@ import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
import {
requiresExplicitMatrixDefaultAccount,
resolveMatrixDefaultOrOnlyAccountId,
@@ -54,7 +55,7 @@ function resolveLegacyMatrixCredentialsPath(env: NodeJS.ProcessEnv): string {
function shouldReadLegacyCredentialsForAccount(accountId?: string | null): boolean {
const normalizedAccountId = normalizeAccountId(accountId);
const cfg = getMatrixRuntime().config.loadConfig();
const cfg = getMatrixRuntime().config.current() as OpenClawConfig;
if (!cfg.channels?.matrix || typeof cfg.channels.matrix !== "object") {
return normalizedAccountId === DEFAULT_ACCOUNT_ID;
}

View File

@@ -130,7 +130,9 @@ vi.mock("./send.js", () => ({
sendSingleTextMessageMatrix: sendModuleMocks.sendSingleTextMessageMatrix,
}));
const runtimeStub = {
config: { loadConfig: () => loadConfigMock() },
config: {
current: () => loadConfigMock(),
},
channel: {
text: {
resolveTextChunkLimit: (cfg: unknown, channel: unknown, accountId?: unknown) =>

View File

@@ -128,7 +128,7 @@ export function createMatrixHandlerTestHarness(
} as never,
core: {
config: {
loadConfig: () => cfgForHandler,
current: () => cfgForHandler,
},
channel: {
pairing: {

View File

@@ -673,7 +673,7 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
};
const storeAllowFrom = isDirectMessage ? await readStoreAllowFrom() : [];
const roomUsers = roomConfig?.users ?? [];
const liveCfg = core.config.loadConfig() as CoreConfig;
const liveCfg = core.config.current() as CoreConfig;
const liveAccountAllowlists = resolveMatrixAccountAllowlistConfig({
cfg: liveCfg,
accountId,

View File

@@ -227,12 +227,13 @@ vi.mock("../../resolve-targets.js", () => ({
vi.mock("../../runtime.js", () => ({
getMatrixRuntime: () => ({
config: {
loadConfig: () => ({
current: () => ({
channels: {
matrix: hoisted.accountConfig,
},
}),
writeConfigFile: vi.fn(),
replaceConfigFile: vi.fn(),
mutateConfigFile: vi.fn(),
},
logging: {
getChildLogger: () => hoisted.logger,

View File

@@ -71,7 +71,7 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
throw new Error("Matrix provider requires Node (bun runtime not supported)");
}
const core = getMatrixRuntime();
let cfg = core.config.loadConfig() as CoreConfig;
let cfg = core.config.current() as CoreConfig;
if (cfg.channels?.["matrix"]?.enabled === false) {
return;
}
@@ -436,8 +436,13 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
accountConfig,
logger,
logVerboseMessage,
loadConfig: () => core.config.loadConfig() as CoreConfig,
writeConfigFile: async (nextCfg) => await core.config.writeConfigFile(nextCfg),
getRuntimeConfig: () => core.config.current() as CoreConfig,
replaceConfigFile: async (nextCfg) => {
await core.config.replaceConfigFile({
nextConfig: nextCfg,
afterWrite: { mode: "auto" },
});
},
loadWebMedia: async (url, maxBytes) => await core.media.loadWebMedia(url, maxBytes),
env: process.env,
abortSignal: opts.abortSignal,

View File

@@ -34,7 +34,7 @@ describe("deliverMatrixReplies", () => {
const runtimeStub = {
config: {
loadConfig: () => loadConfigMock(),
current: () => loadConfigMock(),
},
channel: {
text: {

View File

@@ -127,8 +127,8 @@ describe("runMatrixStartupMaintenance", () => {
error: vi.fn(),
},
logVerboseMessage: vi.fn(),
loadConfig: vi.fn(() => ({ channels: { matrix: {} } })),
writeConfigFile: vi.fn(async () => {}),
getRuntimeConfig: vi.fn(() => ({ channels: { matrix: {} } })),
replaceConfigFile: vi.fn(async () => {}),
loadWebMedia: vi.fn(async () => ({
buffer: Buffer.from("avatar"),
contentType: "image/png",
@@ -166,7 +166,7 @@ describe("runMatrixStartupMaintenance", () => {
"ops",
{ avatarUrl: "mxc://avatar" },
);
expect(params.writeConfigFile).toHaveBeenCalledWith(updatedCfg as never);
expect(params.replaceConfigFile).toHaveBeenCalledWith(updatedCfg as never);
expect(params.logVerboseMessage).toHaveBeenCalledWith(
"matrix: persisted converted avatar URL for account ops (mxc://avatar)",
);

View File

@@ -60,8 +60,8 @@ export async function runMatrixStartupMaintenance(
accountConfig: MatrixConfig;
logger: RuntimeLogger;
logVerboseMessage: (message: string) => void;
loadConfig: () => CoreConfig;
writeConfigFile: (cfg: never) => Promise<void>;
getRuntimeConfig: () => CoreConfig;
replaceConfigFile: (cfg: never) => Promise<void>;
loadWebMedia: (
url: string,
maxBytes: number,
@@ -93,11 +93,11 @@ export async function runMatrixStartupMaintenance(
profileSync.resolvedAvatarUrl &&
params.accountConfig.avatarUrl !== profileSync.resolvedAvatarUrl
) {
const latestCfg = params.loadConfig();
const latestCfg = params.getRuntimeConfig();
const updatedCfg = runtimeDeps.updateMatrixAccountConfig(latestCfg, params.accountId, {
avatarUrl: profileSync.resolvedAvatarUrl,
});
await params.writeConfigFile(updatedCfg as never);
await params.replaceConfigFile(updatedCfg as never);
throwIfMatrixStartupAborted(params.abortSignal);
params.logVerboseMessage(
`matrix: persisted converted avatar URL for account ${params.accountId} (${profileSync.resolvedAvatarUrl})`,

View File

@@ -53,7 +53,7 @@ vi.mock("./client-bootstrap.js", () => ({
const runtimeStub = {
config: {
loadConfig: () => loadConfigMock(),
current: () => loadConfigMock(),
},
media: {
loadWebMedia: (...args: unknown[]) => loadWebMediaMock(...args),

View File

@@ -27,7 +27,7 @@ export async function applyMatrixProfileUpdate(params: {
mediaLocalRoots?: readonly string[];
}): Promise<MatrixProfileUpdateResult> {
const runtime = getMatrixRuntime();
const persistedCfg = runtime.config.loadConfig() as CoreConfig;
const persistedCfg = runtime.config.current() as CoreConfig;
const accountId = normalizeAccountId(params.account);
const displayName = params.displayName?.trim() || null;
const avatarUrl = params.avatarUrl?.trim() || null;
@@ -50,7 +50,10 @@ export async function applyMatrixProfileUpdate(params: {
name: displayName ?? undefined,
avatarUrl: persistedAvatarUrl ?? undefined,
});
await runtime.config.writeConfigFile(updated as never);
await runtime.config.replaceConfigFile({
nextConfig: updated as never,
afterWrite: { mode: "auto" },
});
return {
accountId,

View File

@@ -13,11 +13,19 @@ type MatrixTestRuntimeOptions = {
stateDir?: string;
};
type MatrixRuntimeStub = {
config: Pick<PluginRuntime["config"], "current" | "mutateConfigFile" | "replaceConfigFile">;
channel?: PluginRuntime["channel"];
logging?: PluginRuntime["logging"];
state: Pick<NonNullable<PluginRuntime["state"]>, "resolveStateDir">;
};
export function installMatrixTestRuntime(options: MatrixTestRuntimeOptions = {}): void {
const defaultStateDirResolver: NonNullable<PluginRuntime["state"]>["resolveStateDir"] = (
_env,
homeDir,
) => options.stateDir ?? (homeDir ?? (() => "/tmp"))();
const getRuntimeConfig = () => options.cfg ?? {};
const logging: PluginRuntime["logging"] | undefined = options.logging
? ({
shouldLogVerbose: () => false,
@@ -30,16 +38,20 @@ export function installMatrixTestRuntime(options: MatrixTestRuntimeOptions = {})
} as PluginRuntime["logging"])
: undefined;
setMatrixRuntime({
const runtime: MatrixRuntimeStub = {
config: {
loadConfig: () => options.cfg ?? {},
current: getRuntimeConfig,
mutateConfigFile: vi.fn(),
replaceConfigFile: vi.fn(),
},
...(options.channel ? { channel: options.channel as PluginRuntime["channel"] } : {}),
...(logging ? { logging } : {}),
state: {
resolveStateDir: defaultStateDirResolver,
},
} as PluginRuntime);
};
setMatrixRuntime(runtime as unknown as PluginRuntime);
}
type MatrixMonitorTestRuntimeOptions = Pick<MatrixTestRuntimeOptions, "cfg" | "stateDir"> & {

View File

@@ -434,7 +434,7 @@ function buildMattermostWsUrl(baseUrl: string): string {
export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}): Promise<void> {
const core = getMattermostRuntime();
const runtime = resolveRuntime(opts);
const cfg = opts.config ?? core.config.loadConfig();
const cfg = (opts.config ?? core.config.current()) as OpenClawConfig;
const account = resolveMattermostAccount({
cfg,
accountId: opts.accountId,

View File

@@ -1,4 +1,3 @@
import { getRuntimeConfigSnapshot } from "openclaw/plugin-sdk/config-runtime";
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import { registerMemoryCli } from "./src/cli.js";
import { registerDreamingCommand } from "./src/dreaming-command.js";
@@ -41,23 +40,27 @@ export default definePluginEntry({
});
api.registerTool(
(ctx) =>
createMemorySearchTool({
config: ctx.runtimeConfig ?? ctx.config,
getConfig: () => getRuntimeConfigSnapshot() ?? ctx.runtimeConfig ?? ctx.config,
(ctx) => {
const getConfig = () => ctx.getRuntimeConfig?.() ?? ctx.runtimeConfig ?? ctx.config;
return createMemorySearchTool({
config: getConfig(),
getConfig,
agentSessionKey: ctx.sessionKey,
sandboxed: ctx.sandboxed,
}),
});
},
{ names: ["memory_search"] },
);
api.registerTool(
(ctx) =>
createMemoryGetTool({
config: ctx.runtimeConfig ?? ctx.config,
getConfig: () => getRuntimeConfigSnapshot() ?? ctx.runtimeConfig ?? ctx.config,
(ctx) => {
const getConfig = () => ctx.getRuntimeConfig?.() ?? ctx.runtimeConfig ?? ctx.config;
return createMemoryGetTool({
config: getConfig(),
getConfig,
agentSessionKey: ctx.sessionKey,
}),
});
},
{ names: ["memory_get"] },
);

View File

@@ -13,7 +13,7 @@ export {
withProgressTotals,
} from "openclaw/plugin-sdk/memory-core-host-runtime-cli";
export {
loadConfig,
getRuntimeConfig,
resolveDefaultAgentId,
resolveSessionTranscriptsDirForAgent,
resolveStateDir,

View File

@@ -9,10 +9,10 @@ import {
colorize,
defaultRuntime,
formatErrorMessage,
getRuntimeConfig,
getMemorySearchManager,
isRich,
listMemoryFiles,
loadConfig,
normalizeExtraMemoryPaths,
resolveCommandSecretRefsViaGateway,
resolveDefaultAgentId,
@@ -92,7 +92,7 @@ function getMemoryCommandSecretTargetIds(): Set<string> {
async function loadMemoryCommandConfig(commandName: string): Promise<LoadedMemoryCommandConfig> {
const { resolvedConfig, diagnostics } = await resolveCommandSecretRefsViaGateway({
config: loadConfig(),
config: getRuntimeConfig(),
commandName,
targetIds: getMemoryCommandSecretTargetIds(),
});

View File

@@ -12,7 +12,7 @@ import {
import { readShortTermRecallEntries, recordShortTermRecalls } from "./short-term-promotion.js";
const getMemorySearchManager = vi.hoisted(() => vi.fn());
const loadConfig = vi.hoisted(() => vi.fn(() => ({})));
const getRuntimeConfig = vi.hoisted(() => vi.fn(() => ({})));
const resolveDefaultAgentId = vi.hoisted(() => vi.fn(() => "main"));
const resolveCommandSecretRefsViaGateway = vi.hoisted(() =>
vi.fn(async ({ config }: { config: unknown }) => ({
@@ -34,7 +34,7 @@ vi.mock("./cli.host.runtime.js", async () => {
getMemorySearchManager,
isRich: runtimeCli.isRich,
listMemoryFiles: runtimeFiles.listMemoryFiles,
loadConfig,
getRuntimeConfig,
normalizeExtraMemoryPaths: runtimeFiles.normalizeExtraMemoryPaths,
resolveCommandSecretRefsViaGateway,
resolveDefaultAgentId,
@@ -73,7 +73,7 @@ beforeAll(async () => {
beforeEach(() => {
getMemorySearchManager.mockReset();
loadConfig.mockReset().mockReturnValue({});
getRuntimeConfig.mockReset().mockReturnValue({});
resolveDefaultAgentId.mockReset().mockReturnValue("main");
resolveCommandSecretRefsViaGateway.mockReset().mockImplementation(async ({ config }) => ({
resolvedConfig: config,
@@ -247,7 +247,7 @@ describe("memory cli", () => {
});
it("resolves configured memory SecretRefs through gateway snapshot", async () => {
loadConfig.mockReturnValue({
getRuntimeConfig.mockReturnValue({
agents: {
defaults: {
memorySearch: {

View File

@@ -25,7 +25,11 @@ function createHarness(initialConfig: OpenClawConfig = {}) {
const runtime = {
config: {
current: vi.fn(() => runtimeConfig),
loadConfig: vi.fn(() => runtimeConfig),
replaceConfigFile: vi.fn(async ({ nextConfig }: { nextConfig: OpenClawConfig }) => {
runtimeConfig = nextConfig;
}),
writeConfigFile: vi.fn(async (nextConfig: OpenClawConfig) => {
runtimeConfig = nextConfig;
}),
@@ -111,7 +115,7 @@ describe("memory-core /dreaming command", () => {
const result = await command.handler(createCommandContext("off"));
expect(runtime.config.writeConfigFile).toHaveBeenCalledTimes(1);
expect(runtime.config.replaceConfigFile).toHaveBeenCalledTimes(1);
expect(resolveStoredDreaming(getRuntimeConfig())).toMatchObject({
enabled: false,
frequency: "0 */6 * * *",
@@ -129,7 +133,7 @@ describe("memory-core /dreaming command", () => {
);
expect(result.text).toContain("requires operator.admin");
expect(runtime.config.writeConfigFile).not.toHaveBeenCalled();
expect(runtime.config.replaceConfigFile).not.toHaveBeenCalled();
});
it("blocks write-scoped gateway callers from persisting dreaming config", async () => {
@@ -142,7 +146,7 @@ describe("memory-core /dreaming command", () => {
);
expect(result.text).toContain("requires operator.admin");
expect(runtime.config.writeConfigFile).not.toHaveBeenCalled();
expect(runtime.config.replaceConfigFile).not.toHaveBeenCalled();
});
it("allows admin-scoped gateway callers to persist dreaming config", async () => {
@@ -154,7 +158,7 @@ describe("memory-core /dreaming command", () => {
}),
);
expect(runtime.config.writeConfigFile).toHaveBeenCalledTimes(1);
expect(runtime.config.replaceConfigFile).toHaveBeenCalledTimes(1);
expect(resolveStoredDreaming(getRuntimeConfig())).toMatchObject({
enabled: true,
});
@@ -187,7 +191,7 @@ describe("memory-core /dreaming command", () => {
expect(result.text).toContain("- enabled: off (America/Los_Angeles)");
expect(result.text).toContain("- sweep cadence: 15 */8 * * *");
expect(result.text).toContain("- promotion policy: score>=0.8, recalls>=3, uniqueQueries>=3");
expect(runtime.config.writeConfigFile).not.toHaveBeenCalled();
expect(runtime.config.replaceConfigFile).not.toHaveBeenCalled();
});
it("shows usage for invalid args and does not mutate config", async () => {
@@ -195,6 +199,6 @@ describe("memory-core /dreaming command", () => {
const result = await command.handler(createCommandContext("unknown-mode"));
expect(result.text).toContain("Usage: /dreaming status");
expect(runtime.config.writeConfigFile).not.toHaveBeenCalled();
expect(runtime.config.replaceConfigFile).not.toHaveBeenCalled();
});
});

View File

@@ -90,7 +90,7 @@ export function registerDreamingCommand(api: OpenClawPluginApi): void {
.split(/\s+/)
.filter(Boolean)
.map((token) => normalizeLowercaseStringOrEmpty(token));
const currentConfig = api.runtime.config.loadConfig();
const currentConfig = api.runtime.config.current() as OpenClawConfig;
if (
!firstToken ||
@@ -111,7 +111,10 @@ export function registerDreamingCommand(api: OpenClawPluginApi): void {
}
const enabled = firstToken === "on";
const nextConfig = updateDreamingEnabledInConfig(currentConfig, enabled);
await api.runtime.config.writeConfigFile(nextConfig);
await api.runtime.config.replaceConfigFile({
nextConfig,
afterWrite: { mode: "auto" },
});
return {
text: [
`Dreaming ${enabled ? "enabled" : "disabled"}.`,

View File

@@ -3,7 +3,7 @@ import type { Dirent } from "node:fs";
import fs from "node:fs/promises";
import path from "node:path";
import {
loadConfig,
getRuntimeConfig,
loadSessionStore,
resolveStorePath,
updateSessionStore,
@@ -717,7 +717,7 @@ async function normalizeSessionEntryPathForComparison(params: {
}
async function scrubDreamingNarrativeArtifacts(logger: Logger): Promise<void> {
const cfg = loadConfig();
const cfg = getRuntimeConfig();
const agentsDir = path.join(resolveStateDir(), "agents");
let agentEntries: Dirent[] = [];
try {

View File

@@ -1334,7 +1334,7 @@ describe("gateway startup reconciliation", () => {
const logger = createLogger();
const harness = createCronHarness();
const onMock = vi.fn();
const runtimeLoadConfig = vi.fn(
const runtimeCurrentConfig = vi.fn(
() =>
({
plugins: {
@@ -1370,7 +1370,7 @@ describe("gateway startup reconciliation", () => {
logger,
runtime: {
config: {
loadConfig: runtimeLoadConfig,
current: runtimeCurrentConfig,
},
},
on: onMock,
@@ -1395,7 +1395,7 @@ describe("gateway startup reconciliation", () => {
{ trigger: "heartbeat", workspaceDir: ".", sessionKey },
);
expect(runtimeLoadConfig).toHaveBeenCalled();
expect(runtimeCurrentConfig).toHaveBeenCalled();
expect(result).toEqual({
handled: true,
reason: "memory-core: short-term dreaming disabled",
@@ -1411,7 +1411,7 @@ describe("gateway startup reconciliation", () => {
const harness = createCronHarness();
const onMock = vi.fn();
const workspaceDir = await createTempWorkspace("memory-dreaming-live-config-workspace-");
const runtimeLoadConfig = vi.fn(
const runtimeCurrentConfig = vi.fn(
() =>
({
agents: {
@@ -1454,7 +1454,7 @@ describe("gateway startup reconciliation", () => {
logger,
runtime: {
config: {
loadConfig: runtimeLoadConfig,
current: runtimeCurrentConfig,
},
},
on: onMock,
@@ -1483,7 +1483,7 @@ describe("gateway startup reconciliation", () => {
handled: true,
reason: "memory-core: short-term dreaming processed",
});
expect(runtimeLoadConfig).toHaveBeenCalled();
expect(runtimeCurrentConfig).toHaveBeenCalled();
expect(logger.warn).not.toHaveBeenCalledWith(
"memory-core: dreaming promotion skipped because no memory workspace is available.",
);
@@ -1497,7 +1497,7 @@ describe("gateway startup reconciliation", () => {
const logger = createLogger();
const harness = createCronHarness();
const onMock = vi.fn();
const runtimeLoadConfig = vi.fn(
const runtimeCurrentConfig = vi.fn(
() =>
({
agents: {
@@ -1525,7 +1525,7 @@ describe("gateway startup reconciliation", () => {
logger,
runtime: {
config: {
loadConfig: runtimeLoadConfig,
current: runtimeCurrentConfig,
},
},
on: onMock,
@@ -1550,7 +1550,7 @@ describe("gateway startup reconciliation", () => {
{ trigger: "heartbeat", workspaceDir: ".", sessionKey },
);
expect(runtimeLoadConfig).toHaveBeenCalled();
expect(runtimeCurrentConfig).toHaveBeenCalled();
expect(result).toEqual({
handled: true,
reason: "memory-core: short-term dreaming disabled",

View File

@@ -681,7 +681,7 @@ export function registerShortTermPromotionDreaming(api: OpenClawPluginApi): void
let lastRuntimeCronRef: CronServiceLike | null = null;
const resolveCurrentConfig = (): OpenClawConfig =>
api.runtime.config?.loadConfig?.() ?? api.config;
(api.runtime.config?.current?.() ?? api.config) as OpenClawConfig;
const runtimeConfigKey = (config: ShortTermPromotionDreamingConfig): string =>
[

View File

@@ -491,7 +491,7 @@ describe("memory plugin e2e", () => {
},
runtime: {
config: {
loadConfig: () => configFile,
current: () => configFile,
},
},
logger,
@@ -616,7 +616,7 @@ describe("memory plugin e2e", () => {
},
runtime: {
config: {
loadConfig: () => configFile,
current: () => configFile,
},
},
logger: {
@@ -739,7 +739,7 @@ describe("memory plugin e2e", () => {
},
runtime: {
config: {
loadConfig: () => configFile,
current: () => configFile,
},
},
logger: {
@@ -964,7 +964,7 @@ describe("memory plugin e2e", () => {
},
runtime: {
config: {
loadConfig: () => configFile,
current: () => configFile,
},
},
logger: {
@@ -1100,7 +1100,7 @@ describe("memory plugin e2e", () => {
},
runtime: {
config: {
loadConfig: () => configFile,
current: () => configFile,
},
},
logger: {
@@ -1225,7 +1225,7 @@ describe("memory plugin e2e", () => {
},
runtime: {
config: {
loadConfig: () => configFile,
current: () => configFile,
},
},
logger: {

View File

@@ -9,7 +9,10 @@
import { randomUUID } from "node:crypto";
import type * as LanceDB from "@lancedb/lancedb";
import OpenAI from "openai";
import { resolveLivePluginConfigObject } from "openclaw/plugin-sdk/config-runtime";
import {
resolveLivePluginConfigObject,
type OpenClawConfig,
} from "openclaw/plugin-sdk/config-runtime";
import { ensureGlobalUndiciEnvProxyDispatcher } from "openclaw/plugin-sdk/runtime-env";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
import { Type } from "typebox";
@@ -381,7 +384,9 @@ export default definePluginEntry({
const autoCaptureCursors = new Map<string, AutoCaptureCursor>();
const resolveCurrentHookConfig = () => {
const runtimePluginConfig = resolveLivePluginConfigObject(
api.runtime.config?.loadConfig,
api.runtime.config?.current
? () => api.runtime.config.current() as OpenClawConfig
: undefined,
"memory-lancedb",
api.pluginConfig as Record<string, unknown>,
);

View File

@@ -11,6 +11,7 @@ const mocks = vi.hoisted(() => ({
}));
vi.mock("../../src/config/config.js", () => ({
getRuntimeConfig: mocks.loadConfig,
loadConfig: mocks.loadConfig,
}));

View File

@@ -16,6 +16,54 @@ import { appendItem } from "./helpers.js";
import { buildClaudePlan } from "./plan.js";
import { applyGeneratedSkillItem } from "./skills.js";
function withCachedConfigRuntime(
runtime: MigrationProviderContext["runtime"] | undefined,
fallbackConfig: MigrationProviderContext["config"],
): MigrationProviderContext["runtime"] | undefined {
if (!runtime) {
return undefined;
}
const configApi = runtime.config;
if (!configApi?.current || !configApi.mutateConfigFile) {
return runtime;
}
let cachedConfig: MigrationProviderContext["config"] | undefined;
const current = (): ReturnType<typeof configApi.current> => {
cachedConfig ??= structuredClone(
(configApi.current() ?? fallbackConfig) as MigrationProviderContext["config"],
);
return cachedConfig;
};
return {
...runtime,
config: {
...runtime.config,
current,
mutateConfigFile: async (params) => {
const result = await configApi.mutateConfigFile({
...params,
mutate: async (draft, context) => {
const mutationResult = await params.mutate(draft, context);
cachedConfig = structuredClone(draft);
return mutationResult;
},
});
cachedConfig = structuredClone(result.nextConfig);
return result;
},
...(configApi.replaceConfigFile
? {
replaceConfigFile: async (params) => {
const result = await configApi.replaceConfigFile(params);
cachedConfig = structuredClone(result.nextConfig);
return result;
},
}
: {}),
},
};
}
export async function applyClaudePlan(params: {
ctx: MigrationProviderContext;
plan?: MigrationPlan;
@@ -23,6 +71,8 @@ export async function applyClaudePlan(params: {
}): Promise<MigrationApplyResult> {
const plan = params.plan ?? (await buildClaudePlan(params.ctx));
const reportDir = params.ctx.reportDir ?? path.join(params.ctx.stateDir, "migration", "claude");
const runtime = withCachedConfigRuntime(params.ctx.runtime ?? params.runtime, params.ctx.config);
const applyCtx = { ...params.ctx, runtime };
const items: MigrationItem[] = [];
for (const item of plan.items) {
if (item.status !== "planned") {
@@ -30,12 +80,7 @@ export async function applyClaudePlan(params: {
continue;
}
if (item.kind === "config") {
items.push(
await applyConfigItem(
{ ...params.ctx, runtime: params.ctx.runtime ?? params.runtime },
item,
),
);
items.push(await applyConfigItem(applyCtx, item));
} else if (item.kind === "manual") {
items.push(applyManualItem(item));
} else if (item.action === "archive") {

View File

@@ -24,6 +24,13 @@ type MappedMcpSource = {
const CONFIG_RUNTIME_UNAVAILABLE = "config runtime unavailable";
const MISSING_CONFIG_PATCH = "missing config patch";
class ConfigPatchConflictError extends Error {
constructor(readonly reason: string) {
super(reason);
this.name = "ConfigPatchConflictError";
}
}
function readPath(root: Record<string, unknown>, path: readonly string[]): unknown {
let current: unknown = root;
for (const segment of path) {
@@ -304,18 +311,30 @@ export async function applyConfigItem(
if (!details) {
return markMigrationItemError(item, MISSING_CONFIG_PATCH);
}
if (!ctx.runtime?.config.writeConfigFile) {
const configApi = ctx.runtime?.config;
if (!configApi?.current || !configApi.mutateConfigFile) {
return markMigrationItemError(item, CONFIG_RUNTIME_UNAVAILABLE);
}
try {
const nextConfig = structuredClone(ctx.runtime.config.loadConfig?.() ?? ctx.config);
if (!ctx.overwrite && hasPatchConflict(nextConfig, details.path, details.value)) {
const currentConfig = configApi.current() as MigrationProviderContext["config"];
if (!ctx.overwrite && hasPatchConflict(currentConfig, details.path, details.value)) {
return markMigrationItemConflict(item, MIGRATION_REASON_TARGET_EXISTS);
}
writePath(nextConfig as Record<string, unknown>, details.path, details.value);
await ctx.runtime.config.writeConfigFile(nextConfig);
await configApi.mutateConfigFile({
base: "runtime",
afterWrite: { mode: "auto" },
mutate(draft) {
if (!ctx.overwrite && hasPatchConflict(draft, details.path, details.value)) {
throw new ConfigPatchConflictError(MIGRATION_REASON_TARGET_EXISTS);
}
writePath(draft as Record<string, unknown>, details.path, details.value);
},
});
return { ...item, status: "migrated" };
} catch (err) {
if (err instanceof ConfigPatchConflictError) {
return markMigrationItemConflict(item, err.reason);
}
return markMigrationItemError(item, err instanceof Error ? err.message : String(err));
}
}

View File

@@ -37,15 +37,63 @@ export function makeConfigRuntime(
config: OpenClawConfig,
onWrite?: (next: OpenClawConfig) => void,
): NonNullable<MigrationProviderContext["runtime"]> {
const commitConfig = (next: OpenClawConfig) => {
for (const key of Object.keys(config) as Array<keyof OpenClawConfig>) {
delete config[key];
}
Object.assign(config, next);
onWrite?.(next);
};
return {
config: {
loadConfig: () => config,
writeConfigFile: async (next: OpenClawConfig) => {
for (const key of Object.keys(config) as Array<keyof OpenClawConfig>) {
delete config[key];
}
Object.assign(config, next);
onWrite?.(next);
current: () => config,
mutateConfigFile: async ({
afterWrite,
mutate,
}: {
afterWrite?: unknown;
mutate: (draft: OpenClawConfig, context: unknown) => Promise<unknown> | void;
}) => {
const next = structuredClone(config);
const result = await mutate(next, {
snapshot: {
path: "/tmp/openclaw.json",
exists: true,
raw: "{}",
parsed: {},
valid: true,
issues: [],
warnings: [],
legacyIssues: [],
config: next,
resolved: next,
runtimeConfig: next,
sourceConfig: next,
},
previousHash: "test",
});
commitConfig(next);
return {
nextConfig: next,
afterWrite,
followUp: { mode: "auto", requiresRestart: false },
result,
};
},
replaceConfigFile: async ({
afterWrite,
nextConfig,
}: {
afterWrite?: unknown;
nextConfig: OpenClawConfig;
}) => {
commitConfig(nextConfig);
return {
nextConfig,
afterWrite,
followUp: { mode: "auto", requiresRestart: false },
};
},
},
} as NonNullable<MigrationProviderContext["runtime"]>;

View File

@@ -24,23 +24,46 @@ function withCachedConfigRuntime(
runtime: MigrationProviderContext["runtime"] | undefined,
fallbackConfig: MigrationProviderContext["config"],
): MigrationProviderContext["runtime"] | undefined {
if (!runtime?.config.writeConfigFile) {
if (!runtime) {
return undefined;
}
const configApi = runtime.config;
if (!configApi?.current || !configApi.mutateConfigFile) {
return runtime;
}
let cachedConfig: MigrationProviderContext["config"] | undefined;
const loadConfig = () => {
cachedConfig ??= structuredClone(runtime.config.loadConfig?.() ?? fallbackConfig);
const current = (): ReturnType<typeof configApi.current> => {
cachedConfig ??= structuredClone(
(configApi.current() ?? fallbackConfig) as MigrationProviderContext["config"],
);
return cachedConfig;
};
return {
...runtime,
config: {
...runtime.config,
loadConfig,
writeConfigFile: async (next, options) => {
cachedConfig = structuredClone(next);
await runtime.config.writeConfigFile(next, options);
current,
mutateConfigFile: async (params) => {
const result = await configApi.mutateConfigFile({
...params,
mutate: async (draft, context) => {
const mutationResult = await params.mutate(draft, context);
cachedConfig = structuredClone(draft);
return mutationResult;
},
});
cachedConfig = structuredClone(result.nextConfig);
return result;
},
...(configApi.replaceConfigFile
? {
replaceConfigFile: async (params) => {
const result = await configApi.replaceConfigFile(params);
cachedConfig = structuredClone(result.nextConfig);
return result;
},
}
: {}),
},
};
}

View File

@@ -1,22 +1,14 @@
import path from "node:path";
import type { OpenClawConfig } from "openclaw/plugin-sdk/provider-auth";
import { afterEach, describe, expect, it } from "vitest";
import { buildHermesMigrationProvider } from "./provider.js";
import { cleanupTempRoots, makeContext, makeTempRoot, writeFile } from "./test/provider-helpers.js";
function makeConfigRuntime(config: Record<string, unknown>) {
return {
config: {
loadConfig: () => config,
writeConfigFile: async (next: Record<string, unknown>) => {
Object.keys(config).forEach((key) => {
delete config[key];
});
Object.assign(config, next);
return next;
},
},
} as never;
}
import {
cleanupTempRoots,
makeConfigRuntime,
makeContext,
makeTempRoot,
writeFile,
} from "./test/provider-helpers.js";
describe("Hermes migration config mapping", () => {
afterEach(async () => {
@@ -125,9 +117,9 @@ describe("Hermes migration config mapping", () => {
const source = path.join(root, "hermes");
const workspaceDir = path.join(root, "workspace");
const stateDir = path.join(root, "state");
const config: Record<string, unknown> = {
const config = {
agents: { defaults: { workspace: workspaceDir } },
};
} as OpenClawConfig;
await writeFile(
path.join(source, "config.yaml"),
[

View File

@@ -23,6 +23,13 @@ type ConfigPatchDetails = {
const CONFIG_RUNTIME_UNAVAILABLE = "config runtime unavailable";
const MISSING_CONFIG_PATCH = "missing config patch";
class ConfigPatchConflictError extends Error {
constructor(readonly reason: string) {
super(reason);
this.name = "ConfigPatchConflictError";
}
}
function envKeyForProvider(providerId: string): string {
return `${providerId.toUpperCase().replaceAll(/[^A-Z0-9]/gu, "_")}_API_KEY`;
}
@@ -413,18 +420,30 @@ export async function applyConfigItem(
if (!details) {
return markMigrationItemError(item, MISSING_CONFIG_PATCH);
}
if (!ctx.runtime?.config.writeConfigFile) {
const configApi = ctx.runtime?.config;
if (!configApi?.current || !configApi.mutateConfigFile) {
return markMigrationItemError(item, CONFIG_RUNTIME_UNAVAILABLE);
}
try {
const nextConfig = structuredClone(ctx.runtime.config.loadConfig?.() ?? ctx.config);
if (!ctx.overwrite && hasPatchConflict(nextConfig, details.path, details.value)) {
const currentConfig = configApi.current() as MigrationProviderContext["config"];
if (!ctx.overwrite && hasPatchConflict(currentConfig, details.path, details.value)) {
return markMigrationItemConflict(item, MIGRATION_REASON_TARGET_EXISTS);
}
writePath(nextConfig as Record<string, unknown>, details.path, details.value);
await ctx.runtime.config.writeConfigFile(nextConfig);
await configApi.mutateConfigFile({
base: "runtime",
afterWrite: { mode: "auto" },
mutate(draft) {
if (!ctx.overwrite && hasPatchConflict(draft, details.path, details.value)) {
throw new ConfigPatchConflictError(MIGRATION_REASON_TARGET_EXISTS);
}
writePath(draft as Record<string, unknown>, details.path, details.value);
},
});
return { ...item, status: "migrated" };
} catch (err) {
if (err instanceof ConfigPatchConflictError) {
return markMigrationItemConflict(item, err.reason);
}
return markMigrationItemError(item, err instanceof Error ? err.message : String(err));
}
}

View File

@@ -13,13 +13,19 @@ describe("Hermes migration file and skill items", () => {
function configRuntime(config: Record<string, unknown>) {
return {
config: {
loadConfig: () => config,
writeConfigFile: async (next: Record<string, unknown>) => {
current: () => config,
mutateConfigFile: async ({
mutate,
}: {
mutate: (draft: Record<string, unknown>) => void | Promise<void>;
}) => {
const next = structuredClone(config);
await mutate(next);
Object.keys(config).forEach((key) => {
delete config[key];
});
Object.assign(config, next);
return next;
return { nextConfig: next };
},
},
} as never;

View File

@@ -58,6 +58,16 @@ export function resolveCurrentModelRef(ctx: MigrationProviderContext): string |
return resolveDefaultAgentModelState(ctx.config).effectivePrimary;
}
class ModelApplyAbortError extends Error {
constructor(
readonly status: "conflict" | "skipped",
readonly reason: string,
) {
super(reason);
this.name = "ModelApplyAbortError";
}
}
export async function applyModelItem(
ctx: MigrationProviderContext,
item: MigrationItem,
@@ -67,21 +77,40 @@ export async function applyModelItem(
return item;
}
try {
if (!ctx.runtime?.config.writeConfigFile) {
const configApi = ctx.runtime?.config;
if (!configApi?.current || !configApi.mutateConfigFile) {
return hermesItemError(item, HERMES_REASON_CONFIG_RUNTIME_UNAVAILABLE);
}
const nextConfig = structuredClone(ctx.runtime?.config.loadConfig?.() ?? ctx.config);
const currentState = resolveDefaultAgentModelState(nextConfig);
const currentState = resolveDefaultAgentModelState(
configApi.current() as MigrationProviderContext["config"],
);
if (currentState.effectivePrimary === details.model) {
return hermesItemSkipped(item, HERMES_REASON_ALREADY_CONFIGURED);
}
if (currentState.effectivePrimary && !ctx.overwrite) {
return hermesItemConflict(item, HERMES_REASON_DEFAULT_MODEL_CONFIGURED);
}
setAgentEffectiveModelPrimary(nextConfig, currentState.agentId, details.model);
await ctx.runtime.config.writeConfigFile(nextConfig);
await configApi.mutateConfigFile({
base: "runtime",
afterWrite: { mode: "auto" },
mutate(draft) {
const mutationState = resolveDefaultAgentModelState(draft);
if (mutationState.effectivePrimary === details.model) {
throw new ModelApplyAbortError("skipped", HERMES_REASON_ALREADY_CONFIGURED);
}
if (mutationState.effectivePrimary && !ctx.overwrite) {
throw new ModelApplyAbortError("conflict", HERMES_REASON_DEFAULT_MODEL_CONFIGURED);
}
setAgentEffectiveModelPrimary(draft, mutationState.agentId, details.model);
},
});
return { ...item, status: "migrated" };
} catch (err) {
if (err instanceof ModelApplyAbortError) {
return err.status === "conflict"
? hermesItemConflict(item, err.reason)
: hermesItemSkipped(item, err.reason);
}
return hermesItemError(item, err instanceof Error ? err.message : String(err));
}
}

View File

@@ -37,15 +37,46 @@ export function makeConfigRuntime(
config: OpenClawConfig,
onWrite?: (next: OpenClawConfig) => void,
): NonNullable<MigrationProviderContext["runtime"]> {
const commitConfig = (next: OpenClawConfig) => {
for (const key of Object.keys(config) as Array<keyof OpenClawConfig>) {
delete config[key];
}
Object.assign(config, next);
onWrite?.(next);
};
return {
config: {
loadConfig: () => config,
writeConfigFile: async (next: OpenClawConfig) => {
for (const key of Object.keys(config) as Array<keyof OpenClawConfig>) {
delete config[key];
}
Object.assign(config, next);
onWrite?.(next);
current: () => config,
mutateConfigFile: async ({
afterWrite,
mutate,
}: {
afterWrite?: unknown;
mutate: (draft: OpenClawConfig, context: unknown) => Promise<unknown> | void;
}) => {
const next = structuredClone(config);
const result = await mutate(next, {
previousHash: null,
snapshot: { config, raw: "", hash: null },
});
commitConfig(next);
return {
afterWrite,
followUp: { mode: "auto", requiresRestart: false },
nextConfig: next,
result,
};
},
replaceConfigFile: async ({
afterWrite,
nextConfig,
}: {
afterWrite?: unknown;
nextConfig: OpenClawConfig;
}) => {
commitConfig(nextConfig);
return { afterWrite, followUp: { mode: "auto", requiresRestart: false }, nextConfig };
},
},
} as NonNullable<MigrationProviderContext["runtime"]>;

View File

@@ -9,7 +9,7 @@ import {
} from "openclaw/plugin-sdk/reply-payload";
import { normalizeOptionalLowercaseString, sleep } from "openclaw/plugin-sdk/text-runtime";
import { loadWebMedia } from "openclaw/plugin-sdk/web-media";
import type { MarkdownTableMode, MSTeamsReplyStyle } from "../runtime-api.js";
import type { MarkdownTableMode, MSTeamsReplyStyle, OpenClawConfig } from "../runtime-api.js";
import type { MSTeamsAccessTokenProvider } from "./attachments/types.js";
import type { StoredConversationReference } from "./conversation-store.js";
import { classifyMSTeamsSendError } from "./errors.js";
@@ -238,7 +238,7 @@ export function renderReplyPayloadsToMessages(
const tableMode =
options.tableMode ??
getMSTeamsRuntime().channel.text.resolveMarkdownTableMode({
cfg: getMSTeamsRuntime().config.loadConfig(),
cfg: getMSTeamsRuntime().config.current() as OpenClawConfig,
channel: "msteams",
});

View File

@@ -94,7 +94,10 @@ export const nextcloudTalkGatewayAdapter: NonNullable<
const loggedOut = resolved.secretSource === "none";
if (changed) {
await getNextcloudTalkRuntime().config.writeConfigFile(nextCfg);
await getNextcloudTalkRuntime().config.replaceConfigFile({
nextConfig: nextCfg,
afterWrite: { mode: "auto" },
});
}
return {

View File

@@ -37,7 +37,7 @@ export async function monitorNextcloudTalkProvider(
opts: NextcloudTalkMonitorOptions,
): Promise<{ stop: () => void }> {
const core = getNextcloudTalkRuntime();
const cfg = opts.config ?? (core.config.loadConfig() as CoreConfig);
const cfg = opts.config ?? (core.config.current() as CoreConfig);
const account = resolveNextcloudTalkAccount({
cfg,
accountId: opts.accountId,

View File

@@ -2,7 +2,7 @@ import {
defineBundledChannelEntry,
loadBundledEntryExportSync,
} from "openclaw/plugin-sdk/channel-entry-contract";
import type { PluginRuntime, ResolvedNostrAccount } from "./api.js";
import type { OpenClawConfig, PluginRuntime, ResolvedNostrAccount } from "./api.js";
function createNostrProfileHttpHandler() {
return loadBundledEntryExportSync<
@@ -46,31 +46,34 @@ export default defineBundledChannelEntry({
const httpHandler = createNostrProfileHttpHandler()({
getConfigProfile: (accountId: string) => {
const runtime = getNostrRuntime();
const cfg = runtime.config.loadConfig();
const cfg = runtime.config.current() as OpenClawConfig;
const account = resolveNostrAccount({ cfg, accountId });
return account.profile;
},
updateConfigProfile: async (accountId: string, profile: unknown) => {
const runtime = getNostrRuntime();
const cfg = runtime.config.loadConfig();
const cfg = runtime.config.current() as OpenClawConfig;
const channels = (cfg.channels ?? {}) as Record<string, unknown>;
const nostrConfig = (channels.nostr ?? {}) as Record<string, unknown>;
await runtime.config.writeConfigFile({
...cfg,
channels: {
...channels,
nostr: {
...nostrConfig,
profile,
await runtime.config.replaceConfigFile({
nextConfig: {
...cfg,
channels: {
...channels,
nostr: {
...nostrConfig,
profile,
},
},
},
afterWrite: { mode: "auto" },
});
},
getAccountInfo: (accountId: string) => {
const runtime = getNostrRuntime();
const cfg = runtime.config.loadConfig();
const cfg = runtime.config.current() as OpenClawConfig;
const account = resolveNostrAccount({ cfg, accountId });
if (!account.configured || !account.publicKey) {
return null;

View File

@@ -5,8 +5,7 @@ import { getModel, type Api, type Model } from "@mariozechner/pi-ai";
import { AuthStorage, ModelRegistry } from "@mariozechner/pi-coding-agent";
import OpenAI from "openai";
import type { ResolvedTtsConfig } from "openclaw/plugin-sdk/agent-runtime";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
import { loadConfig } from "openclaw/plugin-sdk/config-runtime";
import { getRuntimeConfig, type OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
import { encodePngRgba, fillPixel } from "openclaw/plugin-sdk/media-runtime";
import { describe, expect, it } from "vitest";
import {
@@ -100,7 +99,7 @@ function createReferencePng(): Buffer {
}
function createLiveConfig(): OpenClawConfig {
const cfg = loadConfig();
const cfg = getRuntimeConfig();
return {
...cfg,
models: {

Some files were not shown because too many files have changed in this diff Show More