mirror of
https://fastgit.cc/github.com/openclaw/openclaw
synced 2026-04-30 22:12:32 +08:00
refactor(config): migrate plugin config access
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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." };
|
||||
}
|
||||
|
||||
@@ -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 => ({
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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".');
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -426,6 +426,7 @@ vi.mock("../config/config.js", async () => {
|
||||
loadConfig,
|
||||
writeConfigFile,
|
||||
})),
|
||||
getRuntimeConfig: loadConfig,
|
||||
getRuntimeConfigSnapshot: vi.fn(() => null),
|
||||
loadConfig,
|
||||
writeConfigFile,
|
||||
|
||||
@@ -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 () => {}),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
export {
|
||||
createConfigIO,
|
||||
getRuntimeConfig,
|
||||
getRuntimeConfigSnapshot,
|
||||
loadConfig,
|
||||
writeConfigFile,
|
||||
replaceConfigFile,
|
||||
type BrowserConfig,
|
||||
type BrowserProfileConfig,
|
||||
type OpenClawConfig,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ export {
|
||||
theme,
|
||||
} from "openclaw/plugin-sdk/browser-setup-tools";
|
||||
export {
|
||||
loadConfig,
|
||||
getRuntimeConfig,
|
||||
normalizePluginsConfig,
|
||||
parseBooleanValue,
|
||||
resolveEffectiveEnableState,
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -27,6 +27,7 @@ const browserConfigMocks = vi.hoisted(() => ({
|
||||
}));
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/browser-config-runtime", () => ({
|
||||
getRuntimeConfig: configMocks.loadConfig,
|
||||
loadConfig: configMocks.loadConfig,
|
||||
}));
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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]) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -9,7 +9,7 @@ vi.mock("openclaw/plugin-sdk/config-runtime", async () => {
|
||||
);
|
||||
return {
|
||||
...actual,
|
||||
loadConfig: () => loadConfigMock(),
|
||||
getRuntimeConfig: () => loadConfigMock(),
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -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>;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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" },
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -8,7 +8,7 @@ const profileAction = "set-profile" as const;
|
||||
|
||||
const runtimeStub = {
|
||||
config: {
|
||||
loadConfig: () => ({}),
|
||||
current: () => ({}),
|
||||
},
|
||||
media: {
|
||||
loadWebMedia: async () => {
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -13,7 +13,7 @@ const MATRIX_ACTION_TEST_CFG = {
|
||||
function installMatrixActionTestRuntime(): void {
|
||||
setMatrixRuntime({
|
||||
config: {
|
||||
loadConfig: () => ({}),
|
||||
current: () => ({}),
|
||||
},
|
||||
channel: {
|
||||
text: {
|
||||
|
||||
@@ -11,7 +11,7 @@ const loadConfigMock = vi.fn(() => ({
|
||||
vi.mock("../../runtime.js", () => ({
|
||||
getMatrixRuntime: () => ({
|
||||
config: {
|
||||
loadConfig: loadConfigMock,
|
||||
current: loadConfigMock,
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -81,7 +81,7 @@ export function primeMatrixClientResolverMocks(params?: {
|
||||
loadConfigMock.mockReturnValue(cfg);
|
||||
getMatrixRuntimeMock.mockReturnValue({
|
||||
config: {
|
||||
loadConfig: loadConfigMock,
|
||||
current: loadConfigMock,
|
||||
},
|
||||
});
|
||||
getActiveMatrixClientMock.mockReturnValue(null);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -128,7 +128,7 @@ export function createMatrixHandlerTestHarness(
|
||||
} as never,
|
||||
core: {
|
||||
config: {
|
||||
loadConfig: () => cfgForHandler,
|
||||
current: () => cfgForHandler,
|
||||
},
|
||||
channel: {
|
||||
pairing: {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -34,7 +34,7 @@ describe("deliverMatrixReplies", () => {
|
||||
|
||||
const runtimeStub = {
|
||||
config: {
|
||||
loadConfig: () => loadConfigMock(),
|
||||
current: () => loadConfigMock(),
|
||||
},
|
||||
channel: {
|
||||
text: {
|
||||
|
||||
@@ -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)",
|
||||
);
|
||||
|
||||
@@ -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})`,
|
||||
|
||||
@@ -53,7 +53,7 @@ vi.mock("./client-bootstrap.js", () => ({
|
||||
|
||||
const runtimeStub = {
|
||||
config: {
|
||||
loadConfig: () => loadConfigMock(),
|
||||
current: () => loadConfigMock(),
|
||||
},
|
||||
media: {
|
||||
loadWebMedia: (...args: unknown[]) => loadWebMediaMock(...args),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"> & {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"] },
|
||||
);
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ export {
|
||||
withProgressTotals,
|
||||
} from "openclaw/plugin-sdk/memory-core-host-runtime-cli";
|
||||
export {
|
||||
loadConfig,
|
||||
getRuntimeConfig,
|
||||
resolveDefaultAgentId,
|
||||
resolveSessionTranscriptsDirForAgent,
|
||||
resolveStateDir,
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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"}.`,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 =>
|
||||
[
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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>,
|
||||
);
|
||||
|
||||
@@ -11,6 +11,7 @@ const mocks = vi.hoisted(() => ({
|
||||
}));
|
||||
|
||||
vi.mock("../../src/config/config.js", () => ({
|
||||
getRuntimeConfig: mocks.loadConfig,
|
||||
loadConfig: mocks.loadConfig,
|
||||
}));
|
||||
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"]>;
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
[
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"]>;
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user