mirror of
https://github.com/moltbot/moltbot.git
synced 2026-05-13 15:47:28 +00:00
refactor: rebase runtime config writes
This commit is contained in:
@@ -2957,19 +2957,23 @@ export default definePluginEntry({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (action === "on" || action === "enable" || action === "enabled") {
|
if (action === "on" || action === "enable" || action === "enabled") {
|
||||||
const nextConfig = updateActiveMemoryGlobalEnabledInConfig(currentConfig, true);
|
await api.runtime.config.mutateConfigFile({
|
||||||
await api.runtime.config.replaceConfigFile({
|
|
||||||
nextConfig,
|
|
||||||
afterWrite: { mode: "auto" },
|
afterWrite: { mode: "auto" },
|
||||||
|
mutate: (draft) => {
|
||||||
|
const nextConfig = updateActiveMemoryGlobalEnabledInConfig(draft, true);
|
||||||
|
Object.assign(draft, nextConfig);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
refreshLiveConfigFromRuntime();
|
refreshLiveConfigFromRuntime();
|
||||||
return { text: "Active Memory: on globally." };
|
return { text: "Active Memory: on globally." };
|
||||||
}
|
}
|
||||||
if (action === "off" || action === "disable" || action === "disabled") {
|
if (action === "off" || action === "disable" || action === "disabled") {
|
||||||
const nextConfig = updateActiveMemoryGlobalEnabledInConfig(currentConfig, false);
|
await api.runtime.config.mutateConfigFile({
|
||||||
await api.runtime.config.replaceConfigFile({
|
|
||||||
nextConfig,
|
|
||||||
afterWrite: { mode: "auto" },
|
afterWrite: { mode: "auto" },
|
||||||
|
mutate: (draft) => {
|
||||||
|
const nextConfig = updateActiveMemoryGlobalEnabledInConfig(draft, false);
|
||||||
|
Object.assign(draft, nextConfig);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
refreshLiveConfigFromRuntime();
|
refreshLiveConfigFromRuntime();
|
||||||
return { text: "Active Memory: off globally." };
|
return { text: "Active Memory: off globally." };
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import {
|
|||||||
normalizeLowercaseStringOrEmpty,
|
normalizeLowercaseStringOrEmpty,
|
||||||
normalizeOptionalString,
|
normalizeOptionalString,
|
||||||
} from "openclaw/plugin-sdk/string-coerce-runtime";
|
} from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||||
import { getRuntimeConfig, replaceConfigFile } from "../config/config.js";
|
import { getRuntimeConfig, mutateConfigFile } from "../config/config.js";
|
||||||
import type { OpenClawConfig } from "../config/config.js";
|
import type { OpenClawConfig } from "../config/config.js";
|
||||||
import { resolveGatewayAuth } from "../gateway/auth.js";
|
import { resolveGatewayAuth } from "../gateway/auth.js";
|
||||||
import { ensureGatewayStartupAuth } from "../gateway/startup-auth.js";
|
import { ensureGatewayStartupAuth } from "../gateway/startup-auth.js";
|
||||||
@@ -77,19 +77,17 @@ async function generateAndPersistBrowserControlToken(params: {
|
|||||||
generatedToken?: string;
|
generatedToken?: string;
|
||||||
}> {
|
}> {
|
||||||
const token = generateBrowserControlToken();
|
const token = generateBrowserControlToken();
|
||||||
const nextCfg: OpenClawConfig = {
|
await mutateConfigFile({
|
||||||
...params.cfg,
|
afterWrite: { mode: "auto" },
|
||||||
gateway: {
|
mutate: (draft) => {
|
||||||
...params.cfg.gateway,
|
draft.gateway = {
|
||||||
|
...draft.gateway,
|
||||||
auth: {
|
auth: {
|
||||||
...params.cfg.gateway?.auth,
|
...draft.gateway?.auth,
|
||||||
token,
|
token,
|
||||||
},
|
},
|
||||||
},
|
|
||||||
};
|
};
|
||||||
await replaceConfigFile({
|
},
|
||||||
nextConfig: nextCfg,
|
|
||||||
afterWrite: { mode: "auto" },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Re-read to stay consistent with any concurrent config writer.
|
// Re-read to stay consistent with any concurrent config writer.
|
||||||
@@ -112,19 +110,17 @@ async function generateAndPersistBrowserControlPassword(params: {
|
|||||||
generatedToken?: string;
|
generatedToken?: string;
|
||||||
}> {
|
}> {
|
||||||
const password = generateBrowserControlToken();
|
const password = generateBrowserControlToken();
|
||||||
const nextCfg: OpenClawConfig = {
|
await mutateConfigFile({
|
||||||
...params.cfg,
|
afterWrite: { mode: "auto" },
|
||||||
gateway: {
|
mutate: (draft) => {
|
||||||
...params.cfg.gateway,
|
draft.gateway = {
|
||||||
|
...draft.gateway,
|
||||||
auth: {
|
auth: {
|
||||||
...params.cfg.gateway?.auth,
|
...draft.gateway?.auth,
|
||||||
password,
|
password,
|
||||||
},
|
},
|
||||||
},
|
|
||||||
};
|
};
|
||||||
await replaceConfigFile({
|
},
|
||||||
nextConfig: nextCfg,
|
|
||||||
afterWrite: { mode: "auto" },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Re-read to stay consistent with any concurrent config writer.
|
// Re-read to stay consistent with any concurrent config writer.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import fs from "node:fs";
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||||
import type { BrowserProfileConfig, OpenClawConfig } from "../config/config.js";
|
import type { BrowserProfileConfig, OpenClawConfig } from "../config/config.js";
|
||||||
import { getRuntimeConfig, replaceConfigFile } from "../config/config.js";
|
import { getRuntimeConfig, mutateConfigFile } from "../config/config.js";
|
||||||
import { deriveDefaultBrowserCdpPortRange } from "../config/port-defaults.js";
|
import { deriveDefaultBrowserCdpPortRange } from "../config/port-defaults.js";
|
||||||
import { formatErrorMessage } from "../infra/errors.js";
|
import { formatErrorMessage } from "../infra/errors.js";
|
||||||
import { resolveUserPath } from "../utils.js";
|
import { resolveUserPath } from "../utils.js";
|
||||||
@@ -165,20 +165,17 @@ export function createBrowserProfilesService(ctx: BrowserRouteContext) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextConfig: OpenClawConfig = {
|
await mutateConfigFile({
|
||||||
...cfg,
|
afterWrite: { mode: "auto" },
|
||||||
browser: {
|
mutate: (draft) => {
|
||||||
...cfg.browser,
|
draft.browser = {
|
||||||
|
...draft.browser,
|
||||||
profiles: {
|
profiles: {
|
||||||
...rawProfiles,
|
...(draft.browser?.profiles ?? {}),
|
||||||
[name]: profileConfig,
|
[name]: profileConfig,
|
||||||
},
|
},
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
},
|
||||||
await replaceConfigFile({
|
|
||||||
nextConfig,
|
|
||||||
afterWrite: { mode: "auto" },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
state.resolved.profiles[name] = profileConfig;
|
state.resolved.profiles[name] = profileConfig;
|
||||||
@@ -240,18 +237,15 @@ export function createBrowserProfilesService(ctx: BrowserRouteContext) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { [name]: _removed, ...remainingProfiles } = profiles;
|
await mutateConfigFile({
|
||||||
const nextConfig: OpenClawConfig = {
|
|
||||||
...cfg,
|
|
||||||
browser: {
|
|
||||||
...cfg.browser,
|
|
||||||
profiles: remainingProfiles,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
await replaceConfigFile({
|
|
||||||
nextConfig,
|
|
||||||
afterWrite: { mode: "auto" },
|
afterWrite: { mode: "auto" },
|
||||||
|
mutate: (draft) => {
|
||||||
|
const { [name]: _removed, ...remainingProfiles } = draft.browser?.profiles ?? {};
|
||||||
|
draft.browser = {
|
||||||
|
...draft.browser,
|
||||||
|
profiles: remainingProfiles,
|
||||||
|
};
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
delete state.resolved.profiles[name];
|
delete state.resolved.profiles[name];
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ export {
|
|||||||
getRuntimeConfig,
|
getRuntimeConfig,
|
||||||
getRuntimeConfigSnapshot,
|
getRuntimeConfigSnapshot,
|
||||||
getRuntimeConfigSourceSnapshot,
|
getRuntimeConfigSourceSnapshot,
|
||||||
|
mutateConfigFile,
|
||||||
replaceConfigFile,
|
replaceConfigFile,
|
||||||
type BrowserConfig,
|
type BrowserConfig,
|
||||||
type BrowserProfileConfig,
|
type BrowserProfileConfig,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export {
|
|||||||
getRuntimeConfigSnapshot,
|
getRuntimeConfigSnapshot,
|
||||||
getRuntimeConfigSourceSnapshot,
|
getRuntimeConfigSourceSnapshot,
|
||||||
} from "openclaw/plugin-sdk/runtime-config-snapshot";
|
} from "openclaw/plugin-sdk/runtime-config-snapshot";
|
||||||
export { replaceConfigFile } from "openclaw/plugin-sdk/config-mutation";
|
export { mutateConfigFile, replaceConfigFile } from "openclaw/plugin-sdk/config-mutation";
|
||||||
export {
|
export {
|
||||||
type BrowserConfig,
|
type BrowserConfig,
|
||||||
type BrowserProfileConfig,
|
type BrowserProfileConfig,
|
||||||
|
|||||||
@@ -101,15 +101,19 @@ export async function handleDreamingCommand(api: OpenClawPluginApi, ctx: PluginC
|
|||||||
return { text: "⚠️ /dreaming on|off requires operator.admin for gateway clients." };
|
return { text: "⚠️ /dreaming on|off requires operator.admin for gateway clients." };
|
||||||
}
|
}
|
||||||
const enabled = firstToken === "on";
|
const enabled = firstToken === "on";
|
||||||
const nextConfig = updateDreamingEnabledInConfig(currentConfig, enabled);
|
const committed = await api.runtime.config.mutateConfigFile({
|
||||||
await api.runtime.config.replaceConfigFile({
|
|
||||||
nextConfig,
|
|
||||||
afterWrite: { mode: "auto" },
|
afterWrite: { mode: "auto" },
|
||||||
|
mutate: (draft) => {
|
||||||
|
const nextConfig = updateDreamingEnabledInConfig(draft, enabled);
|
||||||
|
Object.assign(draft, nextConfig);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
text: [`Dreaming ${enabled ? "enabled" : "disabled"}.`, "", formatStatus(nextConfig)].join(
|
text: [
|
||||||
"\n",
|
`Dreaming ${enabled ? "enabled" : "disabled"}.`,
|
||||||
),
|
"",
|
||||||
|
formatStatus(committed.nextConfig),
|
||||||
|
].join("\n"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -52,23 +52,21 @@ export default defineBundledChannelEntry({
|
|||||||
},
|
},
|
||||||
updateConfigProfile: async (_accountId: string, profile: unknown) => {
|
updateConfigProfile: async (_accountId: string, profile: unknown) => {
|
||||||
const runtime = getNostrRuntime();
|
const runtime = getNostrRuntime();
|
||||||
const cfg = runtime.config.current() as OpenClawConfig;
|
|
||||||
|
|
||||||
const channels = (cfg.channels ?? {}) as Record<string, unknown>;
|
await runtime.config.mutateConfigFile({
|
||||||
|
afterWrite: { mode: "auto" },
|
||||||
|
mutate: (draft) => {
|
||||||
|
const channels = (draft.channels ?? {}) as Record<string, unknown>;
|
||||||
const nostrConfig = (channels.nostr ?? {}) as Record<string, unknown>;
|
const nostrConfig = (channels.nostr ?? {}) as Record<string, unknown>;
|
||||||
|
|
||||||
await runtime.config.replaceConfigFile({
|
draft.channels = {
|
||||||
nextConfig: {
|
|
||||||
...cfg,
|
|
||||||
channels: {
|
|
||||||
...channels,
|
...channels,
|
||||||
nostr: {
|
nostr: {
|
||||||
...nostrConfig,
|
...nostrConfig,
|
||||||
profile,
|
profile,
|
||||||
},
|
},
|
||||||
|
};
|
||||||
},
|
},
|
||||||
},
|
|
||||||
afterWrite: { mode: "auto" },
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
getAccountInfo: (accountId: string) => {
|
getAccountInfo: (accountId: string) => {
|
||||||
|
|||||||
@@ -230,13 +230,15 @@ async function disarmNow(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (removed.length > 0 || restored.length > 0) {
|
if (removed.length > 0 || restored.length > 0) {
|
||||||
const next = patchConfigNodeLists(cfg, {
|
await api.runtime.config.mutateConfigFile({
|
||||||
|
afterWrite: { mode: "auto" },
|
||||||
|
mutate: (draft) => {
|
||||||
|
const next = patchConfigNodeLists(draft, {
|
||||||
allowCommands: uniqSorted([...allow]),
|
allowCommands: uniqSorted([...allow]),
|
||||||
denyCommands: uniqSorted([...deny]),
|
denyCommands: uniqSorted([...deny]),
|
||||||
});
|
});
|
||||||
await api.runtime.config.replaceConfigFile({
|
Object.assign(draft, next);
|
||||||
nextConfig: next,
|
},
|
||||||
afterWrite: { mode: "auto" },
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
await writeArmState(statePath, null);
|
await writeArmState(statePath, null);
|
||||||
@@ -428,13 +430,15 @@ export default definePluginEntry({
|
|||||||
removedFromDeny.push(cmd);
|
removedFromDeny.push(cmd);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const next = patchConfigNodeLists(cfg, {
|
await api.runtime.config.mutateConfigFile({
|
||||||
|
afterWrite: { mode: "auto" },
|
||||||
|
mutate: (draft) => {
|
||||||
|
const next = patchConfigNodeLists(draft, {
|
||||||
allowCommands: uniqSorted([...allowSet]),
|
allowCommands: uniqSorted([...allowSet]),
|
||||||
denyCommands: uniqSorted([...denySet]),
|
denyCommands: uniqSorted([...denySet]),
|
||||||
});
|
});
|
||||||
await api.runtime.config.replaceConfigFile({
|
Object.assign(draft, next);
|
||||||
nextConfig: next,
|
},
|
||||||
afterWrite: { mode: "auto" },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await writeArmState(statePath, {
|
await writeArmState(statePath, {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { SlackEventMiddlewareArgs } from "@slack/bolt";
|
import type { SlackEventMiddlewareArgs } from "@slack/bolt";
|
||||||
import { resolveChannelConfigWrites } from "openclaw/plugin-sdk/channel-config-writes";
|
import { resolveChannelConfigWrites } from "openclaw/plugin-sdk/channel-config-writes";
|
||||||
import { replaceConfigFile } from "openclaw/plugin-sdk/config-mutation";
|
import { mutateConfigFile } from "openclaw/plugin-sdk/config-mutation";
|
||||||
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||||
import { getRuntimeConfig } from "openclaw/plugin-sdk/runtime-config-snapshot";
|
import { getRuntimeConfig } from "openclaw/plugin-sdk/runtime-config-snapshot";
|
||||||
import { danger, warn } from "openclaw/plugin-sdk/runtime-env";
|
import { danger, warn } from "openclaw/plugin-sdk/runtime-env";
|
||||||
@@ -145,9 +145,16 @@ export function registerSlackChannelEvents(params: {
|
|||||||
oldChannelId,
|
oldChannelId,
|
||||||
newChannelId,
|
newChannelId,
|
||||||
});
|
});
|
||||||
await replaceConfigFile({
|
await mutateConfigFile({
|
||||||
nextConfig: currentConfig,
|
|
||||||
afterWrite: { mode: "auto" },
|
afterWrite: { mode: "auto" },
|
||||||
|
mutate: (draft) => {
|
||||||
|
migrateSlackChannelConfig({
|
||||||
|
cfg: draft,
|
||||||
|
accountId: ctx.accountId,
|
||||||
|
oldChannelId,
|
||||||
|
newChannelId,
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
ctx.runtime.log?.(warn("[slack] Channel config migrated and saved successfully."));
|
ctx.runtime.log?.(warn("[slack] Channel config migrated and saved successfully."));
|
||||||
} else if (migration.skippedExisting) {
|
} else if (migration.skippedExisting) {
|
||||||
|
|||||||
@@ -209,24 +209,26 @@ export default definePluginEntry({
|
|||||||
return { text: `No voice found for ${hint}. Try: ${commandLabel} list` };
|
return { text: `No voice found for ${hint}. Try: ${commandLabel} list` };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await api.runtime.config.mutateConfigFile({
|
||||||
|
afterWrite: { mode: "auto" },
|
||||||
|
mutate: (draft) => {
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
...cfg,
|
...draft,
|
||||||
talk: {
|
talk: {
|
||||||
...cfg.talk,
|
...draft.talk,
|
||||||
provider: providerId,
|
provider: providerId,
|
||||||
providers: {
|
providers: {
|
||||||
...cfg.talk?.providers,
|
...draft.talk?.providers,
|
||||||
[providerId]: {
|
[providerId]: {
|
||||||
...cfg.talk?.providers?.[providerId],
|
...draft.talk?.providers?.[providerId],
|
||||||
voiceId: chosen.id,
|
voiceId: chosen.id,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
...(providerId === "elevenlabs" ? { voiceId: chosen.id } : {}),
|
...(providerId === "elevenlabs" ? { voiceId: chosen.id } : {}),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
await api.runtime.config.replaceConfigFile({
|
Object.assign(draft, nextConfig);
|
||||||
nextConfig,
|
},
|
||||||
afterWrite: { mode: "auto" },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const name = (chosen.name ?? "").trim() || "(unnamed)";
|
const name = (chosen.name ?? "").trim() || "(unnamed)";
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import type {
|
|||||||
TelegramGroupConfig,
|
TelegramGroupConfig,
|
||||||
TelegramTopicConfig,
|
TelegramTopicConfig,
|
||||||
} from "openclaw/plugin-sdk/config-contracts";
|
} from "openclaw/plugin-sdk/config-contracts";
|
||||||
import { replaceConfigFile } from "openclaw/plugin-sdk/config-mutation";
|
import { mutateConfigFile } from "openclaw/plugin-sdk/config-mutation";
|
||||||
import {
|
import {
|
||||||
buildPluginBindingResolvedText,
|
buildPluginBindingResolvedText,
|
||||||
parsePluginBindingApprovalCustomId,
|
parsePluginBindingApprovalCustomId,
|
||||||
@@ -2211,9 +2211,11 @@ export const registerTelegramHandlers = ({
|
|||||||
if (migration.migrated) {
|
if (migration.migrated) {
|
||||||
runtime.log?.(warn(`[telegram] Migrating group config from ${oldChatId} to ${newChatId}`));
|
runtime.log?.(warn(`[telegram] Migrating group config from ${oldChatId} to ${newChatId}`));
|
||||||
migrateTelegramGroupConfig({ cfg, accountId, oldChatId, newChatId });
|
migrateTelegramGroupConfig({ cfg, accountId, oldChatId, newChatId });
|
||||||
await replaceConfigFile({
|
await mutateConfigFile({
|
||||||
nextConfig: currentConfig,
|
|
||||||
afterWrite: { mode: "auto" },
|
afterWrite: { mode: "auto" },
|
||||||
|
mutate: (draft) => {
|
||||||
|
migrateTelegramGroupConfig({ cfg: draft, accountId, oldChatId, newChatId });
|
||||||
|
},
|
||||||
});
|
});
|
||||||
runtime.log?.(warn(`[telegram] Group config migrated and saved successfully`));
|
runtime.log?.(warn(`[telegram] Group config migrated and saved successfully`));
|
||||||
} else if (migration.skippedExisting) {
|
} else if (migration.skippedExisting) {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { ChannelId } from "../../channels/plugins/types.public.js";
|
|||||||
import { normalizeChannelId } from "../../channels/registry.js";
|
import { normalizeChannelId } from "../../channels/registry.js";
|
||||||
import {
|
import {
|
||||||
readConfigFileSnapshot,
|
readConfigFileSnapshot,
|
||||||
replaceConfigFile,
|
transformConfigFileWithRetry,
|
||||||
validateConfigObjectWithPlugins,
|
validateConfigObjectWithPlugins,
|
||||||
} from "../../config/config.js";
|
} from "../../config/config.js";
|
||||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||||
@@ -36,6 +36,8 @@ type ResolvedAllowlistName = {
|
|||||||
name?: string | null;
|
name?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class AllowlistCommandMutationError extends Error {}
|
||||||
|
|
||||||
type AllowlistCommand =
|
type AllowlistCommand =
|
||||||
| {
|
| {
|
||||||
action: "list";
|
action: "list";
|
||||||
@@ -457,6 +459,8 @@ export const handleAllowlistCommand: CommandHandler = async (params, allowTextCo
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
const applyConfigEdit = plugin.allowlist.applyConfigEdit;
|
||||||
|
const editScope = parsed.scope;
|
||||||
|
|
||||||
const snapshot = await readConfigFileSnapshot();
|
const snapshot = await readConfigFileSnapshot();
|
||||||
if (!snapshot.valid || !snapshot.parsed || typeof snapshot.parsed !== "object") {
|
if (!snapshot.valid || !snapshot.parsed || typeof snapshot.parsed !== "object") {
|
||||||
@@ -507,18 +511,42 @@ export const handleAllowlistCommand: CommandHandler = async (params, allowTextCo
|
|||||||
const configChanged = editResult.changed;
|
const configChanged = editResult.changed;
|
||||||
|
|
||||||
if (configChanged) {
|
if (configChanged) {
|
||||||
const validated = validateConfigObjectWithPlugins(parsedConfig);
|
try {
|
||||||
|
await transformConfigFileWithRetry({
|
||||||
|
base: "source",
|
||||||
|
afterWrite: { mode: "auto" },
|
||||||
|
transform: async (currentConfig) => {
|
||||||
|
const latestParsedConfig = structuredClone(currentConfig) as Record<string, unknown>;
|
||||||
|
const latestEditResult = await applyConfigEdit({
|
||||||
|
cfg: currentConfig,
|
||||||
|
parsedConfig: latestParsedConfig,
|
||||||
|
accountId,
|
||||||
|
scope: editScope,
|
||||||
|
action: parsed.action,
|
||||||
|
entry: parsed.entry,
|
||||||
|
});
|
||||||
|
if (!latestEditResult || latestEditResult.kind === "invalid-entry") {
|
||||||
|
throw new AllowlistCommandMutationError("Invalid allowlist entry.");
|
||||||
|
}
|
||||||
|
if (!latestEditResult.changed) {
|
||||||
|
return { nextConfig: currentConfig };
|
||||||
|
}
|
||||||
|
const validated = validateConfigObjectWithPlugins(latestParsedConfig);
|
||||||
if (!validated.ok) {
|
if (!validated.ok) {
|
||||||
const issue = validated.issues[0];
|
const issue = validated.issues[0];
|
||||||
return {
|
throw new AllowlistCommandMutationError(
|
||||||
shouldContinue: false,
|
`Config invalid after update (${issue.path}: ${issue.message}).`,
|
||||||
reply: { text: `⚠️ Config invalid after update (${issue.path}: ${issue.message}).` },
|
);
|
||||||
};
|
|
||||||
}
|
}
|
||||||
await replaceConfigFile({
|
return { nextConfig: validated.config };
|
||||||
nextConfig: validated.config,
|
},
|
||||||
afterWrite: { mode: "auto" },
|
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof AllowlistCommandMutationError) {
|
||||||
|
return { shouldContinue: false, reply: { text: `⚠️ ${error.message}` } };
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!configChanged && !shouldTouchStore) {
|
if (!configChanged && !shouldTouchStore) {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
} from "../../config/config-paths.js";
|
} from "../../config/config-paths.js";
|
||||||
import {
|
import {
|
||||||
readConfigFileSnapshot,
|
readConfigFileSnapshot,
|
||||||
replaceConfigFile,
|
transformConfigFileWithRetry,
|
||||||
validateConfigObjectWithPlugins,
|
validateConfigObjectWithPlugins,
|
||||||
} from "../../config/config.js";
|
} from "../../config/config.js";
|
||||||
import {
|
import {
|
||||||
@@ -31,6 +31,12 @@ import { parseConfigCommand } from "./config-commands.js";
|
|||||||
import { resolveConfigWriteDeniedText } from "./config-write-authorization.js";
|
import { resolveConfigWriteDeniedText } from "./config-write-authorization.js";
|
||||||
import { parseDebugCommand } from "./debug-commands.js";
|
import { parseDebugCommand } from "./debug-commands.js";
|
||||||
|
|
||||||
|
class ConfigCommandMutationError extends Error {}
|
||||||
|
|
||||||
|
function formatConfigCommandMutationError(error: unknown): string | null {
|
||||||
|
return error instanceof ConfigCommandMutationError ? error.message : null;
|
||||||
|
}
|
||||||
|
|
||||||
export const handleConfigCommand: CommandHandler = async (params, allowTextCommands) => {
|
export const handleConfigCommand: CommandHandler = async (params, allowTextCommands) => {
|
||||||
if (!allowTextCommands) {
|
if (!allowTextCommands) {
|
||||||
return null;
|
return null;
|
||||||
@@ -142,27 +148,42 @@ export const handleConfigCommand: CommandHandler = async (params, allowTextComma
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (configCommand.action === "unset") {
|
if (configCommand.action === "unset") {
|
||||||
const removed = unsetConfigValueAtPath(parsedBase, parsedWritePath ?? []);
|
const path = parsedWritePath ?? [];
|
||||||
|
let removed = false;
|
||||||
|
try {
|
||||||
|
const result = await transformConfigFileWithRetry<{ removed: boolean }>({
|
||||||
|
base: "source",
|
||||||
|
afterWrite: { mode: "auto" },
|
||||||
|
transform: (currentConfig) => {
|
||||||
|
const next = structuredClone(currentConfig) as Record<string, unknown>;
|
||||||
|
const removed = unsetConfigValueAtPath(next, path);
|
||||||
|
if (!removed) {
|
||||||
|
return { nextConfig: currentConfig, result: { removed: false } };
|
||||||
|
}
|
||||||
|
const validated = validateConfigObjectWithPlugins(next);
|
||||||
|
if (!validated.ok) {
|
||||||
|
const issue = validated.issues[0];
|
||||||
|
throw new ConfigCommandMutationError(
|
||||||
|
`Config invalid after unset (${issue.path}: ${issue.message}).`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return { nextConfig: validated.config, result: { removed: true } };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
removed = Boolean(result.result?.removed);
|
||||||
|
} catch (error) {
|
||||||
|
const message = formatConfigCommandMutationError(error);
|
||||||
|
if (message) {
|
||||||
|
return { shouldContinue: false, reply: { text: `⚠️ ${message}` } };
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
if (!removed) {
|
if (!removed) {
|
||||||
return {
|
return {
|
||||||
shouldContinue: false,
|
shouldContinue: false,
|
||||||
reply: { text: `⚙️ No config value found for ${configCommand.path}.` },
|
reply: { text: `⚙️ No config value found for ${configCommand.path}.` },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const validated = validateConfigObjectWithPlugins(parsedBase);
|
|
||||||
if (!validated.ok) {
|
|
||||||
const issue = validated.issues[0];
|
|
||||||
return {
|
|
||||||
shouldContinue: false,
|
|
||||||
reply: {
|
|
||||||
text: `⚠️ Config invalid after unset (${issue.path}: ${issue.message}).`,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
await replaceConfigFile({
|
|
||||||
nextConfig: validated.config,
|
|
||||||
afterWrite: { mode: "auto" },
|
|
||||||
});
|
|
||||||
return {
|
return {
|
||||||
shouldContinue: false,
|
shouldContinue: false,
|
||||||
reply: { text: `⚙️ Config updated: ${configCommand.path} removed.` },
|
reply: { text: `⚙️ Config updated: ${configCommand.path} removed.` },
|
||||||
@@ -170,21 +191,31 @@ export const handleConfigCommand: CommandHandler = async (params, allowTextComma
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (configCommand.action === "set") {
|
if (configCommand.action === "set") {
|
||||||
setConfigValueAtPath(parsedBase, parsedWritePath ?? [], configCommand.value);
|
const path = parsedWritePath ?? [];
|
||||||
const validated = validateConfigObjectWithPlugins(parsedBase);
|
try {
|
||||||
|
await transformConfigFileWithRetry({
|
||||||
|
base: "source",
|
||||||
|
afterWrite: { mode: "auto" },
|
||||||
|
transform: (currentConfig) => {
|
||||||
|
const next = structuredClone(currentConfig) as Record<string, unknown>;
|
||||||
|
setConfigValueAtPath(next, path, configCommand.value);
|
||||||
|
const validated = validateConfigObjectWithPlugins(next);
|
||||||
if (!validated.ok) {
|
if (!validated.ok) {
|
||||||
const issue = validated.issues[0];
|
const issue = validated.issues[0];
|
||||||
return {
|
throw new ConfigCommandMutationError(
|
||||||
shouldContinue: false,
|
`Config invalid after set (${issue.path}: ${issue.message}).`,
|
||||||
reply: {
|
);
|
||||||
text: `⚠️ Config invalid after set (${issue.path}: ${issue.message}).`,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
await replaceConfigFile({
|
return { nextConfig: validated.config };
|
||||||
nextConfig: validated.config,
|
},
|
||||||
afterWrite: { mode: "auto" },
|
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
const message = formatConfigCommandMutationError(error);
|
||||||
|
if (message) {
|
||||||
|
return { shouldContinue: false, reply: { text: `⚠️ ${message}` } };
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
const valueLabel =
|
const valueLabel =
|
||||||
typeof configCommand.value === "string"
|
typeof configCommand.value === "string"
|
||||||
? `"${configCommand.value}"`
|
? `"${configCommand.value}"`
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import type { ConfigSnapshotForInstallPersist } from "../../cli/plugins-install-
|
|||||||
import { refreshPluginRegistryAfterConfigMutation } from "../../cli/plugins-registry-refresh.js";
|
import { refreshPluginRegistryAfterConfigMutation } from "../../cli/plugins-registry-refresh.js";
|
||||||
import {
|
import {
|
||||||
readConfigFileSnapshot,
|
readConfigFileSnapshot,
|
||||||
replaceConfigFile,
|
transformConfigFileWithRetry,
|
||||||
validateConfigObjectWithPlugins,
|
validateConfigObjectWithPlugins,
|
||||||
} from "../../config/config.js";
|
} from "../../config/config.js";
|
||||||
import { assertConfigWriteAllowedInCurrentMode } from "../../config/nix-mode-write-guard.js";
|
import { assertConfigWriteAllowedInCurrentMode } from "../../config/nix-mode-write-guard.js";
|
||||||
@@ -54,6 +54,8 @@ function renderJsonBlock(label: string, value: unknown): string {
|
|||||||
return `${label}\n\`\`\`json\n${JSON.stringify(value, null, 2)}\n\`\`\``;
|
return `${label}\n\`\`\`json\n${JSON.stringify(value, null, 2)}\n\`\`\``;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class PluginsCommandMutationError extends Error {}
|
||||||
|
|
||||||
function buildPluginInspectJson(params: {
|
function buildPluginInspectJson(params: {
|
||||||
id: string;
|
id: string;
|
||||||
config: OpenClawConfig;
|
config: OpenClawConfig;
|
||||||
@@ -535,28 +537,36 @@ export const handlePluginsCommand: CommandHandler = async (params, allowTextComm
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let committedConfig: OpenClawConfig;
|
||||||
|
try {
|
||||||
|
const committed = await transformConfigFileWithRetry({
|
||||||
|
afterWrite: { mode: "auto" },
|
||||||
|
transform: (currentConfig) => {
|
||||||
const next = setPluginEnabledInConfig(
|
const next = setPluginEnabledInConfig(
|
||||||
structuredClone(loaded.config),
|
structuredClone(currentConfig),
|
||||||
plugin.id,
|
plugin.id,
|
||||||
pluginsCommand.action === "enable",
|
pluginsCommand.action === "enable",
|
||||||
);
|
);
|
||||||
const validated = validateConfigObjectWithPlugins(next);
|
const validated = validateConfigObjectWithPlugins(next);
|
||||||
if (!validated.ok) {
|
if (!validated.ok) {
|
||||||
const issue = validated.issues[0];
|
const issue = validated.issues[0];
|
||||||
return {
|
throw new PluginsCommandMutationError(
|
||||||
shouldContinue: false,
|
`Config invalid after /plugins ${pluginsCommand.action} (${issue.path}: ${issue.message}).`,
|
||||||
reply: {
|
);
|
||||||
text: `⚠️ Config invalid after /plugins ${pluginsCommand.action} (${issue.path}: ${issue.message}).`,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
await replaceConfigFile({
|
return { nextConfig: validated.config };
|
||||||
nextConfig: validated.config,
|
},
|
||||||
afterWrite: { mode: "auto" },
|
|
||||||
});
|
});
|
||||||
|
committedConfig = committed.nextConfig;
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof PluginsCommandMutationError) {
|
||||||
|
return { shouldContinue: false, reply: { text: `⚠️ ${error.message}` } };
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
let registryWarning: string | undefined;
|
let registryWarning: string | undefined;
|
||||||
await refreshPluginRegistryAfterConfigMutation({
|
await refreshPluginRegistryAfterConfigMutation({
|
||||||
config: validated.config,
|
config: committedConfig,
|
||||||
reason: "policy-changed",
|
reason: "policy-changed",
|
||||||
logger: {
|
logger: {
|
||||||
warn: (message) => {
|
warn: (message) => {
|
||||||
|
|||||||
@@ -210,6 +210,7 @@ describe("resolveGatewayInstallToken", () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
snapshot: baseSnapshot,
|
||||||
writeOptions: {
|
writeOptions: {
|
||||||
baseSnapshot,
|
baseSnapshot,
|
||||||
skipRuntimeSnapshotRefresh: true,
|
skipRuntimeSnapshotRefresh: true,
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ async function maybePersistAutoGeneratedGatewayInstallToken(params: {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
snapshot,
|
||||||
writeOptions: {
|
writeOptions: {
|
||||||
baseSnapshot: snapshot,
|
baseSnapshot: snapshot,
|
||||||
...prepared.writeOptions,
|
...prepared.writeOptions,
|
||||||
|
|||||||
@@ -68,6 +68,27 @@ vi.mock("../../config/config.js", async () => {
|
|||||||
writeConfigFile: mocks.writeConfigFile,
|
writeConfigFile: mocks.writeConfigFile,
|
||||||
replaceConfigFile: async (params: { nextConfig: unknown }) =>
|
replaceConfigFile: async (params: { nextConfig: unknown }) =>
|
||||||
await mocks.writeConfigFile(params.nextConfig),
|
await mocks.writeConfigFile(params.nextConfig),
|
||||||
|
mutateConfigFileWithRetry: async (params: {
|
||||||
|
mutate: (draft: Record<string, unknown>, context: unknown) => unknown;
|
||||||
|
}) => {
|
||||||
|
const draft = structuredClone(mocks.loadConfigReturn);
|
||||||
|
const result = await params.mutate(draft, {
|
||||||
|
snapshot: { path: "/tmp/openclaw/config.json" },
|
||||||
|
previousHash: "test-hash",
|
||||||
|
attempt: 0,
|
||||||
|
});
|
||||||
|
await mocks.writeConfigFile(draft);
|
||||||
|
return {
|
||||||
|
path: "/tmp/openclaw/config.json",
|
||||||
|
previousHash: "test-hash",
|
||||||
|
snapshot: { path: "/tmp/openclaw/config.json" },
|
||||||
|
nextConfig: draft,
|
||||||
|
result,
|
||||||
|
attempts: 1,
|
||||||
|
afterWrite: { mode: "auto" },
|
||||||
|
followUp: { action: "none" },
|
||||||
|
};
|
||||||
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import {
|
|||||||
listAgentEntries,
|
listAgentEntries,
|
||||||
pruneAgentConfig,
|
pruneAgentConfig,
|
||||||
} from "../../commands/agents.config.js";
|
} from "../../commands/agents.config.js";
|
||||||
import { replaceConfigFile } from "../../config/config.js";
|
import { mutateConfigFileWithRetry } from "../../config/config.js";
|
||||||
import {
|
import {
|
||||||
purgeAgentSessionStoreEntries,
|
purgeAgentSessionStoreEntries,
|
||||||
resolveSessionTranscriptsDirForAgent,
|
resolveSessionTranscriptsDirForAgent,
|
||||||
@@ -546,9 +546,22 @@ export const agentsHandlers: GatewayRequestHandlers = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await replaceConfigFile({
|
await mutateConfigFileWithRetry({
|
||||||
nextConfig,
|
|
||||||
afterWrite: { mode: "auto" },
|
afterWrite: { mode: "auto" },
|
||||||
|
mutate: (draft) => {
|
||||||
|
if (findAgentEntryIndex(listAgentEntries(draft), agentId) >= 0) {
|
||||||
|
throw new Error(`agent "${agentId}" already exists`);
|
||||||
|
}
|
||||||
|
const latestNextConfig = applyAgentConfig(draft, {
|
||||||
|
agentId,
|
||||||
|
name: safeName,
|
||||||
|
workspace: workspaceDir,
|
||||||
|
model,
|
||||||
|
identity,
|
||||||
|
agentDir,
|
||||||
|
});
|
||||||
|
Object.assign(draft, latestNextConfig);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
respond(true, { ok: true, agentId, name: safeName, workspace: workspaceDir, model }, undefined);
|
respond(true, { ok: true, agentId, name: safeName, workspace: workspaceDir, model }, undefined);
|
||||||
@@ -638,9 +651,21 @@ export const agentsHandlers: GatewayRequestHandlers = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await replaceConfigFile({
|
await mutateConfigFileWithRetry({
|
||||||
nextConfig,
|
|
||||||
afterWrite: { mode: "auto" },
|
afterWrite: { mode: "auto" },
|
||||||
|
mutate: (draft) => {
|
||||||
|
if (!isConfiguredAgent(draft, agentId)) {
|
||||||
|
throw new Error(`agent "${agentId}" not found`);
|
||||||
|
}
|
||||||
|
const latestNextConfig = applyAgentConfig(draft, {
|
||||||
|
agentId,
|
||||||
|
...(safeName ? { name: safeName } : {}),
|
||||||
|
...(workspaceDir ? { workspace: workspaceDir } : {}),
|
||||||
|
...(model ? { model } : {}),
|
||||||
|
...(identity ? { identity } : {}),
|
||||||
|
});
|
||||||
|
Object.assign(draft, latestNextConfig);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
respond(true, { ok: true, agentId }, undefined);
|
respond(true, { ok: true, agentId }, undefined);
|
||||||
@@ -667,30 +692,49 @@ export const agentsHandlers: GatewayRequestHandlers = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const deleteFiles = typeof params.deleteFiles === "boolean" ? params.deleteFiles : true;
|
const deleteFiles = typeof params.deleteFiles === "boolean" ? params.deleteFiles : true;
|
||||||
const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId);
|
const committed = await mutateConfigFileWithRetry({
|
||||||
const agentDir = resolveAgentDir(cfg, agentId);
|
|
||||||
const sessionsDir = resolveSessionTranscriptsDirForAgent(agentId);
|
|
||||||
|
|
||||||
const result = pruneAgentConfig(cfg, agentId);
|
|
||||||
await replaceConfigFile({
|
|
||||||
nextConfig: result.config,
|
|
||||||
afterWrite: { mode: "auto" },
|
afterWrite: { mode: "auto" },
|
||||||
|
mutate: (draft) => {
|
||||||
|
if (!isConfiguredAgent(draft, agentId)) {
|
||||||
|
throw new Error(`Agent "${agentId}" not found`);
|
||||||
|
}
|
||||||
|
const workspaceDir = resolveAgentWorkspaceDir(draft, agentId);
|
||||||
|
const agentDir = resolveAgentDir(draft, agentId);
|
||||||
|
const sessionsDir = resolveSessionTranscriptsDirForAgent(agentId);
|
||||||
|
const result = pruneAgentConfig(draft, agentId);
|
||||||
|
Object.assign(draft, result.config);
|
||||||
|
return {
|
||||||
|
workspaceDir,
|
||||||
|
agentDir,
|
||||||
|
sessionsDir,
|
||||||
|
removedBindings: result.removedBindings,
|
||||||
|
};
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
const deleteResult = committed.result;
|
||||||
|
if (!deleteResult) {
|
||||||
|
respond(false, undefined, errorShape(ErrorCodes.UNAVAILABLE, "agent delete did not commit"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Purge session store entries so orphaned sessions cannot be targeted (#65524).
|
// Purge session store entries so orphaned sessions cannot be targeted (#65524).
|
||||||
await purgeAgentSessionStoreEntries(cfg, agentId);
|
await purgeAgentSessionStoreEntries(cfg, agentId);
|
||||||
|
|
||||||
if (deleteFiles) {
|
if (deleteFiles) {
|
||||||
const workspaceSharedWith = findOverlappingWorkspaceAgentIds(cfg, agentId, workspaceDir);
|
const workspaceSharedWith = findOverlappingWorkspaceAgentIds(
|
||||||
|
committed.nextConfig,
|
||||||
|
agentId,
|
||||||
|
deleteResult.workspaceDir,
|
||||||
|
);
|
||||||
const deleteWorkspace = workspaceSharedWith.length === 0;
|
const deleteWorkspace = workspaceSharedWith.length === 0;
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
...(deleteWorkspace ? [moveToTrashBestEffort(workspaceDir)] : []),
|
...(deleteWorkspace ? [moveToTrashBestEffort(deleteResult.workspaceDir)] : []),
|
||||||
moveToTrashBestEffort(agentDir),
|
moveToTrashBestEffort(deleteResult.agentDir),
|
||||||
moveToTrashBestEffort(sessionsDir),
|
moveToTrashBestEffort(deleteResult.sessionsDir),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
respond(true, { ok: true, agentId, removedBindings: result.removedBindings }, undefined);
|
respond(true, { ok: true, agentId, removedBindings: deleteResult.removedBindings }, undefined);
|
||||||
},
|
},
|
||||||
"agents.files.list": async ({ params, respond, context }) => {
|
"agents.files.list": async ({ params, respond, context }) => {
|
||||||
if (!validateAgentsFilesListParams(params)) {
|
if (!validateAgentsFilesListParams(params)) {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { installSkill } from "../../agents/skills-install.js";
|
|||||||
import { buildWorkspaceSkillStatus } from "../../agents/skills-status.js";
|
import { buildWorkspaceSkillStatus } from "../../agents/skills-status.js";
|
||||||
import { loadWorkspaceSkillEntries, type SkillEntry } from "../../agents/skills.js";
|
import { loadWorkspaceSkillEntries, type SkillEntry } from "../../agents/skills.js";
|
||||||
import { listAgentWorkspaceDirs } from "../../agents/workspace-dirs.js";
|
import { listAgentWorkspaceDirs } from "../../agents/workspace-dirs.js";
|
||||||
import { replaceConfigFile } from "../../config/config.js";
|
import { mutateConfigFileWithRetry } from "../../config/config.js";
|
||||||
import { redactConfigObject, REDACTED_SENTINEL } from "../../config/redact-snapshot.js";
|
import { redactConfigObject, REDACTED_SENTINEL } from "../../config/redact-snapshot.js";
|
||||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||||
import { fetchClawHubSkillDetail } from "../../infra/clawhub.js";
|
import { fetchClawHubSkillDetail } from "../../infra/clawhub.js";
|
||||||
@@ -331,8 +331,10 @@ export const skillsHandlers: GatewayRequestHandlers = {
|
|||||||
apiKey?: string;
|
apiKey?: string;
|
||||||
env?: Record<string, string>;
|
env?: Record<string, string>;
|
||||||
};
|
};
|
||||||
const cfg = context.getRuntimeConfig();
|
const committed = await mutateConfigFileWithRetry({
|
||||||
const skills = cfg.skills ? { ...cfg.skills } : {};
|
afterWrite: { mode: "auto" },
|
||||||
|
mutate: (draft) => {
|
||||||
|
const skills = draft.skills ? { ...draft.skills } : {};
|
||||||
const entries = skills.entries ? { ...skills.entries } : {};
|
const entries = skills.entries ? { ...skills.entries } : {};
|
||||||
const current = entries[p.skillKey] ? { ...entries[p.skillKey] } : {};
|
const current = entries[p.skillKey] ? { ...entries[p.skillKey] } : {};
|
||||||
if (typeof p.enabled === "boolean") {
|
if (typeof p.enabled === "boolean") {
|
||||||
@@ -369,17 +371,13 @@ export const skillsHandlers: GatewayRequestHandlers = {
|
|||||||
}
|
}
|
||||||
entries[p.skillKey] = current;
|
entries[p.skillKey] = current;
|
||||||
skills.entries = entries;
|
skills.entries = entries;
|
||||||
const nextConfig: OpenClawConfig = {
|
draft.skills = skills;
|
||||||
...cfg,
|
return current;
|
||||||
skills,
|
},
|
||||||
};
|
|
||||||
await replaceConfigFile({
|
|
||||||
nextConfig,
|
|
||||||
afterWrite: { mode: "auto" },
|
|
||||||
});
|
});
|
||||||
respond(
|
respond(
|
||||||
true,
|
true,
|
||||||
{ ok: true, skillKey: p.skillKey, config: redactConfigObject(current) },
|
{ ok: true, skillKey: p.skillKey, config: redactConfigObject(committed.result ?? {}) },
|
||||||
undefined,
|
undefined,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user