feat: IRC — add first-class channel support

Adds IRC as a first-class channel with core config surfaces (schema/hints/dock), plugin auto-enable detection, routing/policy alignment, and docs/tests.

Co-authored-by: Vignesh <vigneshnatarajan92@gmail.com>
This commit is contained in:
Vignesh
2026-02-10 15:33:57 -08:00
committed by GitHub
parent 90f58333e9
commit fa906b26ad
50 changed files with 4907 additions and 791 deletions

View File

@@ -10,6 +10,10 @@ import type {
ChannelPlugin,
ChannelThreadingAdapter,
} from "./plugins/types.js";
import {
resolveChannelGroupRequireMention,
resolveChannelGroupToolsPolicy,
} from "../config/group-policy.js";
import { resolveDiscordAccount } from "../discord/accounts.js";
import { resolveIMessageAccount } from "../imessage/accounts.js";
import { requireActivePluginRegistry } from "../plugins/runtime.js";
@@ -75,7 +79,6 @@ const formatLower = (allowFrom: Array<string | number>) =>
.map((entry) => String(entry).trim())
.filter(Boolean)
.map((entry) => entry.toLowerCase());
// Channel docks: lightweight channel metadata/behavior for shared code paths.
//
// Rules:
@@ -213,6 +216,73 @@ const DOCKS: Record<ChatChannelId, ChannelDock> = {
}),
},
},
irc: {
id: "irc",
capabilities: {
chatTypes: ["direct", "group"],
media: true,
blockStreaming: true,
},
outbound: { textChunkLimit: 350 },
streaming: {
blockStreamingCoalesceDefaults: { minChars: 300, idleMs: 1000 },
},
config: {
resolveAllowFrom: ({ cfg, accountId }) => {
const channel = cfg.channels?.irc;
const normalized = normalizeAccountId(accountId);
const account =
channel?.accounts?.[normalized] ??
channel?.accounts?.[
Object.keys(channel?.accounts ?? {}).find(
(key) => key.toLowerCase() === normalized.toLowerCase(),
) ?? ""
];
return (account?.allowFrom ?? channel?.allowFrom ?? []).map((entry) => String(entry));
},
formatAllowFrom: ({ allowFrom }) =>
allowFrom
.map((entry) => String(entry).trim())
.filter(Boolean)
.map((entry) =>
entry
.replace(/^irc:/i, "")
.replace(/^user:/i, "")
.toLowerCase(),
),
},
groups: {
resolveRequireMention: ({ cfg, accountId, groupId }) => {
if (!groupId) {
return true;
}
return resolveChannelGroupRequireMention({
cfg,
channel: "irc",
groupId,
accountId,
groupIdCaseInsensitive: true,
});
},
resolveToolPolicy: ({ cfg, accountId, groupId, senderId, senderName, senderUsername }) => {
if (!groupId) {
return undefined;
}
// IRC supports per-channel tool policies. Prefer the shared resolver so
// toolsBySender is honored consistently across surfaces.
return resolveChannelGroupToolsPolicy({
cfg,
channel: "irc",
groupId,
accountId,
groupIdCaseInsensitive: true,
senderId,
senderName,
senderUsername,
});
},
},
},
googlechat: {
id: "googlechat",
capabilities: {

View File

@@ -10,6 +10,7 @@ describe("channel registry", () => {
expect(normalizeChatChannelId("imsg")).toBe("imessage");
expect(normalizeChatChannelId("gchat")).toBe("googlechat");
expect(normalizeChatChannelId("google-chat")).toBe("googlechat");
expect(normalizeChatChannelId("internet-relay-chat")).toBe("irc");
expect(normalizeChatChannelId("web")).toBeNull();
});

View File

@@ -8,6 +8,7 @@ export const CHAT_CHANNEL_ORDER = [
"telegram",
"whatsapp",
"discord",
"irc",
"googlechat",
"slack",
"signal",
@@ -58,6 +59,16 @@ const CHAT_CHANNEL_META: Record<ChatChannelId, ChannelMeta> = {
blurb: "very well supported right now.",
systemImage: "bubble.left.and.bubble.right",
},
irc: {
id: "irc",
label: "IRC",
selectionLabel: "IRC (Server + Nick)",
detailLabel: "IRC",
docsPath: "/channels/irc",
docsLabel: "irc",
blurb: "classic IRC networks with DM/channel routing and pairing controls.",
systemImage: "network",
},
googlechat: {
id: "googlechat",
label: "Google Chat",
@@ -102,6 +113,7 @@ const CHAT_CHANNEL_META: Record<ChatChannelId, ChannelMeta> = {
export const CHAT_CHANNEL_ALIASES: Record<string, ChatChannelId> = {
imsg: "imessage",
"internet-relay-chat": "irc",
"google-chat": "googlechat",
gchat: "googlechat",
};

View File

@@ -0,0 +1,117 @@
import { describe, expect, it } from "vitest";
import { validateConfigObject } from "./config.js";
describe("config irc", () => {
it("accepts basic irc config", () => {
const res = validateConfigObject({
channels: {
irc: {
host: "irc.libera.chat",
nick: "openclaw-bot",
channels: ["#openclaw"],
},
},
});
expect(res.ok).toBe(true);
expect(res.config.channels?.irc?.host).toBe("irc.libera.chat");
expect(res.config.channels?.irc?.nick).toBe("openclaw-bot");
});
it('rejects irc.dmPolicy="open" without allowFrom "*"', () => {
const res = validateConfigObject({
channels: {
irc: {
dmPolicy: "open",
allowFrom: ["alice"],
},
},
});
expect(res.ok).toBe(false);
expect(res.issues[0]?.path).toBe("channels.irc.allowFrom");
});
it('accepts irc.dmPolicy="open" with allowFrom "*"', () => {
const res = validateConfigObject({
channels: {
irc: {
dmPolicy: "open",
allowFrom: ["*"],
},
},
});
expect(res.ok).toBe(true);
expect(res.config.channels?.irc?.dmPolicy).toBe("open");
});
it("accepts mixed allowFrom value types for IRC", () => {
const res = validateConfigObject({
channels: {
irc: {
allowFrom: [12345, "alice"],
groupAllowFrom: [67890, "alice!ident@example.org"],
groups: {
"#ops": {
allowFrom: [42, "alice"],
},
},
},
},
});
expect(res.ok).toBe(true);
expect(res.config.channels?.irc?.allowFrom).toEqual([12345, "alice"]);
expect(res.config.channels?.irc?.groupAllowFrom).toEqual([67890, "alice!ident@example.org"]);
expect(res.config.channels?.irc?.groups?.["#ops"]?.allowFrom).toEqual([42, "alice"]);
});
it("rejects nickserv register without registerEmail", () => {
const res = validateConfigObject({
channels: {
irc: {
nickserv: {
register: true,
password: "secret",
},
},
},
});
expect(res.ok).toBe(false);
expect(res.issues[0]?.path).toBe("channels.irc.nickserv.registerEmail");
});
it("accepts nickserv register with password and registerEmail", () => {
const res = validateConfigObject({
channels: {
irc: {
nickserv: {
register: true,
password: "secret",
registerEmail: "bot@example.com",
},
},
},
});
expect(res.ok).toBe(true);
expect(res.config.channels?.irc?.nickserv?.register).toBe(true);
});
it("accepts nickserv register with registerEmail only (password may come from env)", () => {
const res = validateConfigObject({
channels: {
irc: {
nickserv: {
register: true,
registerEmail: "bot@example.com",
},
},
},
});
expect(res.ok).toBe(true);
});
});

View File

@@ -20,6 +20,29 @@ export type ChannelGroupPolicy = {
type ChannelGroups = Record<string, ChannelGroupConfig>;
function resolveChannelGroupConfig(
groups: ChannelGroups | undefined,
groupId: string,
caseInsensitive = false,
): ChannelGroupConfig | undefined {
if (!groups) {
return undefined;
}
const direct = groups[groupId];
if (direct) {
return direct;
}
if (!caseInsensitive) {
return undefined;
}
const target = groupId.toLowerCase();
const matchedKey = Object.keys(groups).find((key) => key !== "*" && key.toLowerCase() === target);
if (!matchedKey) {
return undefined;
}
return groups[matchedKey];
}
export type GroupToolPolicySender = {
senderId?: string | null;
senderName?: string | null;
@@ -125,18 +148,18 @@ export function resolveChannelGroupPolicy(params: {
channel: GroupPolicyChannel;
groupId?: string | null;
accountId?: string | null;
groupIdCaseInsensitive?: boolean;
}): ChannelGroupPolicy {
const { cfg, channel } = params;
const groups = resolveChannelGroups(cfg, channel, params.accountId);
const allowlistEnabled = Boolean(groups && Object.keys(groups).length > 0);
const normalizedId = params.groupId?.trim();
const groupConfig = normalizedId && groups ? groups[normalizedId] : undefined;
const groupConfig = normalizedId
? resolveChannelGroupConfig(groups, normalizedId, params.groupIdCaseInsensitive)
: undefined;
const defaultConfig = groups?.["*"];
const allowAll = allowlistEnabled && Boolean(groups && Object.hasOwn(groups, "*"));
const allowed =
!allowlistEnabled ||
allowAll ||
(normalizedId ? Boolean(groups && Object.hasOwn(groups, normalizedId)) : false);
const allowed = !allowlistEnabled || allowAll || Boolean(groupConfig);
return {
allowlistEnabled,
allowed,
@@ -150,6 +173,7 @@ export function resolveChannelGroupRequireMention(params: {
channel: GroupPolicyChannel;
groupId?: string | null;
accountId?: string | null;
groupIdCaseInsensitive?: boolean;
requireMentionOverride?: boolean;
overrideOrder?: "before-config" | "after-config";
}): boolean {
@@ -180,6 +204,7 @@ export function resolveChannelGroupToolsPolicy(
channel: GroupPolicyChannel;
groupId?: string | null;
accountId?: string | null;
groupIdCaseInsensitive?: boolean;
} & GroupToolPolicySender,
): GroupToolPolicyConfig | undefined {
const { groupConfig, defaultConfig } = resolveChannelGroupPolicy(params);

View File

@@ -29,6 +29,19 @@ describe("applyPluginAutoEnable", () => {
expect(result.changes).toEqual([]);
});
it("configures irc as disabled when configured via env", () => {
const result = applyPluginAutoEnable({
config: {},
env: {
IRC_HOST: "irc.libera.chat",
IRC_NICK: "openclaw-bot",
},
});
expect(result.config.plugins?.entries?.irc?.enabled).toBe(false);
expect(result.changes.join("\n")).toContain("IRC configured, not enabled yet.");
});
it("configures provider auth plugins as disabled when profiles exist", () => {
const result = applyPluginAutoEnable({
config: {

View File

@@ -105,6 +105,23 @@ function isDiscordConfigured(cfg: OpenClawConfig, env: NodeJS.ProcessEnv): boole
return recordHasKeys(entry);
}
function isIrcConfigured(cfg: OpenClawConfig, env: NodeJS.ProcessEnv): boolean {
if (hasNonEmptyString(env.IRC_HOST) && hasNonEmptyString(env.IRC_NICK)) {
return true;
}
const entry = resolveChannelConfig(cfg, "irc");
if (!entry) {
return false;
}
if (hasNonEmptyString(entry.host) || hasNonEmptyString(entry.nick)) {
return true;
}
if (accountsHaveKeys(entry.accounts, ["host", "nick"])) {
return true;
}
return recordHasKeys(entry);
}
function isSlackConfigured(cfg: OpenClawConfig, env: NodeJS.ProcessEnv): boolean {
if (
hasNonEmptyString(env.SLACK_BOT_TOKEN) ||
@@ -189,6 +206,8 @@ export function isChannelConfigured(
return isTelegramConfigured(cfg, env);
case "discord":
return isDiscordConfigured(cfg, env);
case "irc":
return isIrcConfigured(cfg, env);
case "slack":
return isSlackConfigured(cfg, env);
case "signal":

786
src/config/schema.hints.ts Normal file
View File

@@ -0,0 +1,786 @@
import { IRC_FIELD_HELP, IRC_FIELD_LABELS } from "./schema.irc.js";
export type ConfigUiHint = {
label?: string;
help?: string;
group?: string;
order?: number;
advanced?: boolean;
sensitive?: boolean;
placeholder?: string;
itemTemplate?: unknown;
};
export type ConfigUiHints = Record<string, ConfigUiHint>;
const GROUP_LABELS: Record<string, string> = {
wizard: "Wizard",
update: "Update",
diagnostics: "Diagnostics",
logging: "Logging",
gateway: "Gateway",
nodeHost: "Node Host",
agents: "Agents",
tools: "Tools",
bindings: "Bindings",
audio: "Audio",
models: "Models",
messages: "Messages",
commands: "Commands",
session: "Session",
cron: "Cron",
hooks: "Hooks",
ui: "UI",
browser: "Browser",
talk: "Talk",
channels: "Messaging Channels",
skills: "Skills",
plugins: "Plugins",
discovery: "Discovery",
presence: "Presence",
voicewake: "Voice Wake",
};
const GROUP_ORDER: Record<string, number> = {
wizard: 20,
update: 25,
diagnostics: 27,
gateway: 30,
nodeHost: 35,
agents: 40,
tools: 50,
bindings: 55,
audio: 60,
models: 70,
messages: 80,
commands: 85,
session: 90,
cron: 100,
hooks: 110,
ui: 120,
browser: 130,
talk: 140,
channels: 150,
skills: 200,
plugins: 205,
discovery: 210,
presence: 220,
voicewake: 230,
logging: 900,
};
const FIELD_LABELS: Record<string, string> = {
"meta.lastTouchedVersion": "Config Last Touched Version",
"meta.lastTouchedAt": "Config Last Touched At",
"update.channel": "Update Channel",
"update.checkOnStart": "Update Check on Start",
"diagnostics.enabled": "Diagnostics Enabled",
"diagnostics.flags": "Diagnostics Flags",
"diagnostics.otel.enabled": "OpenTelemetry Enabled",
"diagnostics.otel.endpoint": "OpenTelemetry Endpoint",
"diagnostics.otel.protocol": "OpenTelemetry Protocol",
"diagnostics.otel.headers": "OpenTelemetry Headers",
"diagnostics.otel.serviceName": "OpenTelemetry Service Name",
"diagnostics.otel.traces": "OpenTelemetry Traces Enabled",
"diagnostics.otel.metrics": "OpenTelemetry Metrics Enabled",
"diagnostics.otel.logs": "OpenTelemetry Logs Enabled",
"diagnostics.otel.sampleRate": "OpenTelemetry Trace Sample Rate",
"diagnostics.otel.flushIntervalMs": "OpenTelemetry Flush Interval (ms)",
"diagnostics.cacheTrace.enabled": "Cache Trace Enabled",
"diagnostics.cacheTrace.filePath": "Cache Trace File Path",
"diagnostics.cacheTrace.includeMessages": "Cache Trace Include Messages",
"diagnostics.cacheTrace.includePrompt": "Cache Trace Include Prompt",
"diagnostics.cacheTrace.includeSystem": "Cache Trace Include System",
"agents.list.*.identity.avatar": "Identity Avatar",
"agents.list.*.skills": "Agent Skill Filter",
"gateway.remote.url": "Remote Gateway URL",
"gateway.remote.sshTarget": "Remote Gateway SSH Target",
"gateway.remote.sshIdentity": "Remote Gateway SSH Identity",
"gateway.remote.token": "Remote Gateway Token",
"gateway.remote.password": "Remote Gateway Password",
"gateway.remote.tlsFingerprint": "Remote Gateway TLS Fingerprint",
"gateway.auth.token": "Gateway Token",
"gateway.auth.password": "Gateway Password",
"tools.media.image.enabled": "Enable Image Understanding",
"tools.media.image.maxBytes": "Image Understanding Max Bytes",
"tools.media.image.maxChars": "Image Understanding Max Chars",
"tools.media.image.prompt": "Image Understanding Prompt",
"tools.media.image.timeoutSeconds": "Image Understanding Timeout (sec)",
"tools.media.image.attachments": "Image Understanding Attachment Policy",
"tools.media.image.models": "Image Understanding Models",
"tools.media.image.scope": "Image Understanding Scope",
"tools.media.models": "Media Understanding Shared Models",
"tools.media.concurrency": "Media Understanding Concurrency",
"tools.media.audio.enabled": "Enable Audio Understanding",
"tools.media.audio.maxBytes": "Audio Understanding Max Bytes",
"tools.media.audio.maxChars": "Audio Understanding Max Chars",
"tools.media.audio.prompt": "Audio Understanding Prompt",
"tools.media.audio.timeoutSeconds": "Audio Understanding Timeout (sec)",
"tools.media.audio.language": "Audio Understanding Language",
"tools.media.audio.attachments": "Audio Understanding Attachment Policy",
"tools.media.audio.models": "Audio Understanding Models",
"tools.media.audio.scope": "Audio Understanding Scope",
"tools.media.video.enabled": "Enable Video Understanding",
"tools.media.video.maxBytes": "Video Understanding Max Bytes",
"tools.media.video.maxChars": "Video Understanding Max Chars",
"tools.media.video.prompt": "Video Understanding Prompt",
"tools.media.video.timeoutSeconds": "Video Understanding Timeout (sec)",
"tools.media.video.attachments": "Video Understanding Attachment Policy",
"tools.media.video.models": "Video Understanding Models",
"tools.media.video.scope": "Video Understanding Scope",
"tools.links.enabled": "Enable Link Understanding",
"tools.links.maxLinks": "Link Understanding Max Links",
"tools.links.timeoutSeconds": "Link Understanding Timeout (sec)",
"tools.links.models": "Link Understanding Models",
"tools.links.scope": "Link Understanding Scope",
"tools.profile": "Tool Profile",
"tools.alsoAllow": "Tool Allowlist Additions",
"agents.list[].tools.profile": "Agent Tool Profile",
"agents.list[].tools.alsoAllow": "Agent Tool Allowlist Additions",
"tools.byProvider": "Tool Policy by Provider",
"agents.list[].tools.byProvider": "Agent Tool Policy by Provider",
"tools.exec.applyPatch.enabled": "Enable apply_patch",
"tools.exec.applyPatch.allowModels": "apply_patch Model Allowlist",
"tools.exec.notifyOnExit": "Exec Notify On Exit",
"tools.exec.approvalRunningNoticeMs": "Exec Approval Running Notice (ms)",
"tools.exec.host": "Exec Host",
"tools.exec.security": "Exec Security",
"tools.exec.ask": "Exec Ask",
"tools.exec.node": "Exec Node Binding",
"tools.exec.pathPrepend": "Exec PATH Prepend",
"tools.exec.safeBins": "Exec Safe Bins",
"tools.message.allowCrossContextSend": "Allow Cross-Context Messaging",
"tools.message.crossContext.allowWithinProvider": "Allow Cross-Context (Same Provider)",
"tools.message.crossContext.allowAcrossProviders": "Allow Cross-Context (Across Providers)",
"tools.message.crossContext.marker.enabled": "Cross-Context Marker",
"tools.message.crossContext.marker.prefix": "Cross-Context Marker Prefix",
"tools.message.crossContext.marker.suffix": "Cross-Context Marker Suffix",
"tools.message.broadcast.enabled": "Enable Message Broadcast",
"tools.web.search.enabled": "Enable Web Search Tool",
"tools.web.search.provider": "Web Search Provider",
"tools.web.search.apiKey": "Brave Search API Key",
"tools.web.search.maxResults": "Web Search Max Results",
"tools.web.search.timeoutSeconds": "Web Search Timeout (sec)",
"tools.web.search.cacheTtlMinutes": "Web Search Cache TTL (min)",
"tools.web.fetch.enabled": "Enable Web Fetch Tool",
"tools.web.fetch.maxChars": "Web Fetch Max Chars",
"tools.web.fetch.timeoutSeconds": "Web Fetch Timeout (sec)",
"tools.web.fetch.cacheTtlMinutes": "Web Fetch Cache TTL (min)",
"tools.web.fetch.maxRedirects": "Web Fetch Max Redirects",
"tools.web.fetch.userAgent": "Web Fetch User-Agent",
"gateway.controlUi.basePath": "Control UI Base Path",
"gateway.controlUi.root": "Control UI Assets Root",
"gateway.controlUi.allowedOrigins": "Control UI Allowed Origins",
"gateway.controlUi.allowInsecureAuth": "Allow Insecure Control UI Auth",
"gateway.controlUi.dangerouslyDisableDeviceAuth": "Dangerously Disable Control UI Device Auth",
"gateway.http.endpoints.chatCompletions.enabled": "OpenAI Chat Completions Endpoint",
"gateway.reload.mode": "Config Reload Mode",
"gateway.reload.debounceMs": "Config Reload Debounce (ms)",
"gateway.nodes.browser.mode": "Gateway Node Browser Mode",
"gateway.nodes.browser.node": "Gateway Node Browser Pin",
"gateway.nodes.allowCommands": "Gateway Node Allowlist (Extra Commands)",
"gateway.nodes.denyCommands": "Gateway Node Denylist",
"nodeHost.browserProxy.enabled": "Node Browser Proxy Enabled",
"nodeHost.browserProxy.allowProfiles": "Node Browser Proxy Allowed Profiles",
"skills.load.watch": "Watch Skills",
"skills.load.watchDebounceMs": "Skills Watch Debounce (ms)",
"agents.defaults.workspace": "Workspace",
"agents.defaults.repoRoot": "Repo Root",
"agents.defaults.bootstrapMaxChars": "Bootstrap Max Chars",
"agents.defaults.envelopeTimezone": "Envelope Timezone",
"agents.defaults.envelopeTimestamp": "Envelope Timestamp",
"agents.defaults.envelopeElapsed": "Envelope Elapsed",
"agents.defaults.memorySearch": "Memory Search",
"agents.defaults.memorySearch.enabled": "Enable Memory Search",
"agents.defaults.memorySearch.sources": "Memory Search Sources",
"agents.defaults.memorySearch.extraPaths": "Extra Memory Paths",
"agents.defaults.memorySearch.experimental.sessionMemory":
"Memory Search Session Index (Experimental)",
"agents.defaults.memorySearch.provider": "Memory Search Provider",
"agents.defaults.memorySearch.remote.baseUrl": "Remote Embedding Base URL",
"agents.defaults.memorySearch.remote.apiKey": "Remote Embedding API Key",
"agents.defaults.memorySearch.remote.headers": "Remote Embedding Headers",
"agents.defaults.memorySearch.remote.batch.concurrency": "Remote Batch Concurrency",
"agents.defaults.memorySearch.model": "Memory Search Model",
"agents.defaults.memorySearch.fallback": "Memory Search Fallback",
"agents.defaults.memorySearch.local.modelPath": "Local Embedding Model Path",
"agents.defaults.memorySearch.store.path": "Memory Search Index Path",
"agents.defaults.memorySearch.store.vector.enabled": "Memory Search Vector Index",
"agents.defaults.memorySearch.store.vector.extensionPath": "Memory Search Vector Extension Path",
"agents.defaults.memorySearch.chunking.tokens": "Memory Chunk Tokens",
"agents.defaults.memorySearch.chunking.overlap": "Memory Chunk Overlap Tokens",
"agents.defaults.memorySearch.sync.onSessionStart": "Index on Session Start",
"agents.defaults.memorySearch.sync.onSearch": "Index on Search (Lazy)",
"agents.defaults.memorySearch.sync.watch": "Watch Memory Files",
"agents.defaults.memorySearch.sync.watchDebounceMs": "Memory Watch Debounce (ms)",
"agents.defaults.memorySearch.sync.sessions.deltaBytes": "Session Delta Bytes",
"agents.defaults.memorySearch.sync.sessions.deltaMessages": "Session Delta Messages",
"agents.defaults.memorySearch.query.maxResults": "Memory Search Max Results",
"agents.defaults.memorySearch.query.minScore": "Memory Search Min Score",
"agents.defaults.memorySearch.query.hybrid.enabled": "Memory Search Hybrid",
"agents.defaults.memorySearch.query.hybrid.vectorWeight": "Memory Search Vector Weight",
"agents.defaults.memorySearch.query.hybrid.textWeight": "Memory Search Text Weight",
"agents.defaults.memorySearch.query.hybrid.candidateMultiplier":
"Memory Search Hybrid Candidate Multiplier",
"agents.defaults.memorySearch.cache.enabled": "Memory Search Embedding Cache",
"agents.defaults.memorySearch.cache.maxEntries": "Memory Search Embedding Cache Max Entries",
memory: "Memory",
"memory.backend": "Memory Backend",
"memory.citations": "Memory Citations Mode",
"memory.qmd.command": "QMD Binary",
"memory.qmd.includeDefaultMemory": "QMD Include Default Memory",
"memory.qmd.paths": "QMD Extra Paths",
"memory.qmd.paths.path": "QMD Path",
"memory.qmd.paths.pattern": "QMD Path Pattern",
"memory.qmd.paths.name": "QMD Path Name",
"memory.qmd.sessions.enabled": "QMD Session Indexing",
"memory.qmd.sessions.exportDir": "QMD Session Export Directory",
"memory.qmd.sessions.retentionDays": "QMD Session Retention (days)",
"memory.qmd.update.interval": "QMD Update Interval",
"memory.qmd.update.debounceMs": "QMD Update Debounce (ms)",
"memory.qmd.update.onBoot": "QMD Update on Startup",
"memory.qmd.update.waitForBootSync": "QMD Wait for Boot Sync",
"memory.qmd.update.embedInterval": "QMD Embed Interval",
"memory.qmd.update.commandTimeoutMs": "QMD Command Timeout (ms)",
"memory.qmd.update.updateTimeoutMs": "QMD Update Timeout (ms)",
"memory.qmd.update.embedTimeoutMs": "QMD Embed Timeout (ms)",
"memory.qmd.limits.maxResults": "QMD Max Results",
"memory.qmd.limits.maxSnippetChars": "QMD Max Snippet Chars",
"memory.qmd.limits.maxInjectedChars": "QMD Max Injected Chars",
"memory.qmd.limits.timeoutMs": "QMD Search Timeout (ms)",
"memory.qmd.scope": "QMD Surface Scope",
"auth.profiles": "Auth Profiles",
"auth.order": "Auth Profile Order",
"auth.cooldowns.billingBackoffHours": "Billing Backoff (hours)",
"auth.cooldowns.billingBackoffHoursByProvider": "Billing Backoff Overrides",
"auth.cooldowns.billingMaxHours": "Billing Backoff Cap (hours)",
"auth.cooldowns.failureWindowHours": "Failover Window (hours)",
"agents.defaults.models": "Models",
"agents.defaults.model.primary": "Primary Model",
"agents.defaults.model.fallbacks": "Model Fallbacks",
"agents.defaults.imageModel.primary": "Image Model",
"agents.defaults.imageModel.fallbacks": "Image Model Fallbacks",
"agents.defaults.humanDelay.mode": "Human Delay Mode",
"agents.defaults.humanDelay.minMs": "Human Delay Min (ms)",
"agents.defaults.humanDelay.maxMs": "Human Delay Max (ms)",
"agents.defaults.cliBackends": "CLI Backends",
"commands.native": "Native Commands",
"commands.nativeSkills": "Native Skill Commands",
"commands.text": "Text Commands",
"commands.bash": "Allow Bash Chat Command",
"commands.bashForegroundMs": "Bash Foreground Window (ms)",
"commands.config": "Allow /config",
"commands.debug": "Allow /debug",
"commands.restart": "Allow Restart",
"commands.useAccessGroups": "Use Access Groups",
"commands.ownerAllowFrom": "Command Owners",
"ui.seamColor": "Accent Color",
"ui.assistant.name": "Assistant Name",
"ui.assistant.avatar": "Assistant Avatar",
"browser.evaluateEnabled": "Browser Evaluate Enabled",
"browser.snapshotDefaults": "Browser Snapshot Defaults",
"browser.snapshotDefaults.mode": "Browser Snapshot Mode",
"browser.remoteCdpTimeoutMs": "Remote CDP Timeout (ms)",
"browser.remoteCdpHandshakeTimeoutMs": "Remote CDP Handshake Timeout (ms)",
"session.dmScope": "DM Session Scope",
"session.agentToAgent.maxPingPongTurns": "Agent-to-Agent Ping-Pong Turns",
"messages.ackReaction": "Ack Reaction Emoji",
"messages.ackReactionScope": "Ack Reaction Scope",
"messages.inbound.debounceMs": "Inbound Message Debounce (ms)",
"talk.apiKey": "Talk API Key",
"channels.whatsapp": "WhatsApp",
"channels.telegram": "Telegram",
"channels.telegram.customCommands": "Telegram Custom Commands",
"channels.discord": "Discord",
"channels.slack": "Slack",
"channels.mattermost": "Mattermost",
"channels.signal": "Signal",
"channels.imessage": "iMessage",
"channels.bluebubbles": "BlueBubbles",
"channels.msteams": "MS Teams",
...IRC_FIELD_LABELS,
"channels.telegram.botToken": "Telegram Bot Token",
"channels.telegram.dmPolicy": "Telegram DM Policy",
"channels.telegram.streamMode": "Telegram Draft Stream Mode",
"channels.telegram.draftChunk.minChars": "Telegram Draft Chunk Min Chars",
"channels.telegram.draftChunk.maxChars": "Telegram Draft Chunk Max Chars",
"channels.telegram.draftChunk.breakPreference": "Telegram Draft Chunk Break Preference",
"channels.telegram.retry.attempts": "Telegram Retry Attempts",
"channels.telegram.retry.minDelayMs": "Telegram Retry Min Delay (ms)",
"channels.telegram.retry.maxDelayMs": "Telegram Retry Max Delay (ms)",
"channels.telegram.retry.jitter": "Telegram Retry Jitter",
"channels.telegram.network.autoSelectFamily": "Telegram autoSelectFamily",
"channels.telegram.timeoutSeconds": "Telegram API Timeout (seconds)",
"channels.telegram.capabilities.inlineButtons": "Telegram Inline Buttons",
"channels.whatsapp.dmPolicy": "WhatsApp DM Policy",
"channels.whatsapp.selfChatMode": "WhatsApp Self-Phone Mode",
"channels.whatsapp.debounceMs": "WhatsApp Message Debounce (ms)",
"channels.signal.dmPolicy": "Signal DM Policy",
"channels.imessage.dmPolicy": "iMessage DM Policy",
"channels.bluebubbles.dmPolicy": "BlueBubbles DM Policy",
"channels.discord.dm.policy": "Discord DM Policy",
"channels.discord.retry.attempts": "Discord Retry Attempts",
"channels.discord.retry.minDelayMs": "Discord Retry Min Delay (ms)",
"channels.discord.retry.maxDelayMs": "Discord Retry Max Delay (ms)",
"channels.discord.retry.jitter": "Discord Retry Jitter",
"channels.discord.maxLinesPerMessage": "Discord Max Lines Per Message",
"channels.discord.intents.presence": "Discord Presence Intent",
"channels.discord.intents.guildMembers": "Discord Guild Members Intent",
"channels.discord.pluralkit.enabled": "Discord PluralKit Enabled",
"channels.discord.pluralkit.token": "Discord PluralKit Token",
"channels.slack.dm.policy": "Slack DM Policy",
"channels.slack.allowBots": "Slack Allow Bot Messages",
"channels.discord.token": "Discord Bot Token",
"channels.slack.botToken": "Slack Bot Token",
"channels.slack.appToken": "Slack App Token",
"channels.slack.userToken": "Slack User Token",
"channels.slack.userTokenReadOnly": "Slack User Token Read Only",
"channels.slack.thread.historyScope": "Slack Thread History Scope",
"channels.slack.thread.inheritParent": "Slack Thread Parent Inheritance",
"channels.mattermost.botToken": "Mattermost Bot Token",
"channels.mattermost.baseUrl": "Mattermost Base URL",
"channels.mattermost.chatmode": "Mattermost Chat Mode",
"channels.mattermost.oncharPrefixes": "Mattermost Onchar Prefixes",
"channels.mattermost.requireMention": "Mattermost Require Mention",
"channels.signal.account": "Signal Account",
"channels.imessage.cliPath": "iMessage CLI Path",
"agents.list[].skills": "Agent Skill Filter",
"agents.list[].identity.avatar": "Agent Avatar",
"discovery.mdns.mode": "mDNS Discovery Mode",
"plugins.enabled": "Enable Plugins",
"plugins.allow": "Plugin Allowlist",
"plugins.deny": "Plugin Denylist",
"plugins.load.paths": "Plugin Load Paths",
"plugins.slots": "Plugin Slots",
"plugins.slots.memory": "Memory Plugin",
"plugins.entries": "Plugin Entries",
"plugins.entries.*.enabled": "Plugin Enabled",
"plugins.entries.*.config": "Plugin Config",
"plugins.installs": "Plugin Install Records",
"plugins.installs.*.source": "Plugin Install Source",
"plugins.installs.*.spec": "Plugin Install Spec",
"plugins.installs.*.sourcePath": "Plugin Install Source Path",
"plugins.installs.*.installPath": "Plugin Install Path",
"plugins.installs.*.version": "Plugin Install Version",
"plugins.installs.*.installedAt": "Plugin Install Time",
};
const FIELD_HELP: Record<string, string> = {
"meta.lastTouchedVersion": "Auto-set when OpenClaw writes the config.",
"meta.lastTouchedAt": "ISO timestamp of the last config write (auto-set).",
"update.channel": 'Update channel for git + npm installs ("stable", "beta", or "dev").',
"update.checkOnStart": "Check for npm updates when the gateway starts (default: true).",
"gateway.remote.url": "Remote Gateway WebSocket URL (ws:// or wss://).",
"gateway.remote.tlsFingerprint":
"Expected sha256 TLS fingerprint for the remote gateway (pin to avoid MITM).",
"gateway.remote.sshTarget":
"Remote gateway over SSH (tunnels the gateway port to localhost). Format: user@host or user@host:port.",
"gateway.remote.sshIdentity": "Optional SSH identity file path (passed to ssh -i).",
"agents.list.*.skills":
"Optional allowlist of skills for this agent (omit = all skills; empty = no skills).",
"agents.list[].skills":
"Optional allowlist of skills for this agent (omit = all skills; empty = no skills).",
"agents.list[].identity.avatar":
"Avatar image path (relative to the agent workspace only) or a remote URL/data URL.",
"discovery.mdns.mode":
'mDNS broadcast mode ("minimal" default, "full" includes cliPath/sshPort, "off" disables mDNS).',
"gateway.auth.token":
"Required by default for gateway access (unless using Tailscale Serve identity); required for non-loopback binds.",
"gateway.auth.password": "Required for Tailscale funnel.",
"gateway.controlUi.basePath":
"Optional URL prefix where the Control UI is served (e.g. /openclaw).",
"gateway.controlUi.root":
"Optional filesystem root for Control UI assets (defaults to dist/control-ui).",
"gateway.controlUi.allowedOrigins":
"Allowed browser origins for Control UI/WebChat websocket connections (full origins only, e.g. https://control.example.com).",
"gateway.controlUi.allowInsecureAuth":
"Allow Control UI auth over insecure HTTP (token-only; not recommended).",
"gateway.controlUi.dangerouslyDisableDeviceAuth":
"DANGEROUS. Disable Control UI device identity checks (token/password only).",
"gateway.http.endpoints.chatCompletions.enabled":
"Enable the OpenAI-compatible `POST /v1/chat/completions` endpoint (default: false).",
"gateway.reload.mode": 'Hot reload strategy for config changes ("hybrid" recommended).',
"gateway.reload.debounceMs": "Debounce window (ms) before applying config changes.",
"gateway.nodes.browser.mode":
'Node browser routing ("auto" = pick single connected browser node, "manual" = require node param, "off" = disable).',
"gateway.nodes.browser.node": "Pin browser routing to a specific node id or name (optional).",
"gateway.nodes.allowCommands":
"Extra node.invoke commands to allow beyond the gateway defaults (array of command strings).",
"gateway.nodes.denyCommands":
"Commands to block even if present in node claims or default allowlist.",
"nodeHost.browserProxy.enabled": "Expose the local browser control server via node proxy.",
"nodeHost.browserProxy.allowProfiles":
"Optional allowlist of browser profile names exposed via the node proxy.",
"diagnostics.flags":
'Enable targeted diagnostics logs by flag (e.g. ["telegram.http"]). Supports wildcards like "telegram.*" or "*".',
"diagnostics.cacheTrace.enabled":
"Log cache trace snapshots for embedded agent runs (default: false).",
"diagnostics.cacheTrace.filePath":
"JSONL output path for cache trace logs (default: $OPENCLAW_STATE_DIR/logs/cache-trace.jsonl).",
"diagnostics.cacheTrace.includeMessages":
"Include full message payloads in trace output (default: true).",
"diagnostics.cacheTrace.includePrompt": "Include prompt text in trace output (default: true).",
"diagnostics.cacheTrace.includeSystem": "Include system prompt in trace output (default: true).",
"tools.exec.applyPatch.enabled":
"Experimental. Enables apply_patch for OpenAI models when allowed by tool policy.",
"tools.exec.applyPatch.allowModels":
'Optional allowlist of model ids (e.g. "gpt-5.2" or "openai/gpt-5.2").',
"tools.exec.notifyOnExit":
"When true (default), backgrounded exec sessions enqueue a system event and request a heartbeat on exit.",
"tools.exec.pathPrepend": "Directories to prepend to PATH for exec runs (gateway/sandbox).",
"tools.exec.safeBins":
"Allow stdin-only safe binaries to run without explicit allowlist entries.",
"tools.message.allowCrossContextSend":
"Legacy override: allow cross-context sends across all providers.",
"tools.message.crossContext.allowWithinProvider":
"Allow sends to other channels within the same provider (default: true).",
"tools.message.crossContext.allowAcrossProviders":
"Allow sends across different providers (default: false).",
"tools.message.crossContext.marker.enabled":
"Add a visible origin marker when sending cross-context (default: true).",
"tools.message.crossContext.marker.prefix":
'Text prefix for cross-context markers (supports "{channel}").',
"tools.message.crossContext.marker.suffix":
'Text suffix for cross-context markers (supports "{channel}").',
"tools.message.broadcast.enabled": "Enable broadcast action (default: true).",
"tools.web.search.enabled": "Enable the web_search tool (requires a provider API key).",
"tools.web.search.provider": 'Search provider ("brave" or "perplexity").',
"tools.web.search.apiKey": "Brave Search API key (fallback: BRAVE_API_KEY env var).",
"tools.web.search.maxResults": "Default number of results to return (1-10).",
"tools.web.search.timeoutSeconds": "Timeout in seconds for web_search requests.",
"tools.web.search.cacheTtlMinutes": "Cache TTL in minutes for web_search results.",
"tools.web.search.perplexity.apiKey":
"Perplexity or OpenRouter API key (fallback: PERPLEXITY_API_KEY or OPENROUTER_API_KEY env var).",
"tools.web.search.perplexity.baseUrl":
"Perplexity base URL override (default: https://openrouter.ai/api/v1 or https://api.perplexity.ai).",
"tools.web.search.perplexity.model":
'Perplexity model override (default: "perplexity/sonar-pro").',
"tools.web.fetch.enabled": "Enable the web_fetch tool (lightweight HTTP fetch).",
"tools.web.fetch.maxChars": "Max characters returned by web_fetch (truncated).",
"tools.web.fetch.maxCharsCap":
"Hard cap for web_fetch maxChars (applies to config and tool calls).",
"tools.web.fetch.timeoutSeconds": "Timeout in seconds for web_fetch requests.",
"tools.web.fetch.cacheTtlMinutes": "Cache TTL in minutes for web_fetch results.",
"tools.web.fetch.maxRedirects": "Maximum redirects allowed for web_fetch (default: 3).",
"tools.web.fetch.userAgent": "Override User-Agent header for web_fetch requests.",
"tools.web.fetch.readability":
"Use Readability to extract main content from HTML (fallbacks to basic HTML cleanup).",
"tools.web.fetch.firecrawl.enabled": "Enable Firecrawl fallback for web_fetch (if configured).",
"tools.web.fetch.firecrawl.apiKey": "Firecrawl API key (fallback: FIRECRAWL_API_KEY env var).",
"tools.web.fetch.firecrawl.baseUrl":
"Firecrawl base URL (e.g. https://api.firecrawl.dev or custom endpoint).",
"tools.web.fetch.firecrawl.onlyMainContent":
"When true, Firecrawl returns only the main content (default: true).",
"tools.web.fetch.firecrawl.maxAgeMs":
"Firecrawl maxAge (ms) for cached results when supported by the API.",
"tools.web.fetch.firecrawl.timeoutSeconds": "Timeout in seconds for Firecrawl requests.",
"channels.slack.allowBots":
"Allow bot-authored messages to trigger Slack replies (default: false).",
"channels.slack.thread.historyScope":
'Scope for Slack thread history context ("thread" isolates per thread; "channel" reuses channel history).',
"channels.slack.thread.inheritParent":
"If true, Slack thread sessions inherit the parent channel transcript (default: false).",
"channels.mattermost.botToken":
"Bot token from Mattermost System Console -> Integrations -> Bot Accounts.",
"channels.mattermost.baseUrl":
"Base URL for your Mattermost server (e.g., https://chat.example.com).",
"channels.mattermost.chatmode":
'Reply to channel messages on mention ("oncall"), on trigger chars (">" or "!") ("onchar"), or on every message ("onmessage").',
"channels.mattermost.oncharPrefixes": 'Trigger prefixes for onchar mode (default: [">", "!"]).',
"channels.mattermost.requireMention":
"Require @mention in channels before responding (default: true).",
"auth.profiles": "Named auth profiles (provider + mode + optional email).",
"auth.order": "Ordered auth profile IDs per provider (used for automatic failover).",
"auth.cooldowns.billingBackoffHours":
"Base backoff (hours) when a profile fails due to billing/insufficient credits (default: 5).",
"auth.cooldowns.billingBackoffHoursByProvider":
"Optional per-provider overrides for billing backoff (hours).",
"auth.cooldowns.billingMaxHours": "Cap (hours) for billing backoff (default: 24).",
"auth.cooldowns.failureWindowHours": "Failure window (hours) for backoff counters (default: 24).",
"agents.defaults.bootstrapMaxChars":
"Max characters of each workspace bootstrap file injected into the system prompt before truncation (default: 20000).",
"agents.defaults.repoRoot":
"Optional repository root shown in the system prompt runtime line (overrides auto-detect).",
"agents.defaults.envelopeTimezone":
'Timezone for message envelopes ("utc", "local", "user", or an IANA timezone string).',
"agents.defaults.envelopeTimestamp":
'Include absolute timestamps in message envelopes ("on" or "off").',
"agents.defaults.envelopeElapsed": 'Include elapsed time in message envelopes ("on" or "off").',
"agents.defaults.models": "Configured model catalog (keys are full provider/model IDs).",
"agents.defaults.memorySearch":
"Vector search over MEMORY.md and memory/*.md (per-agent overrides supported).",
"agents.defaults.memorySearch.sources":
'Sources to index for memory search (default: ["memory"]; add "sessions" to include session transcripts).',
"agents.defaults.memorySearch.extraPaths":
"Extra paths to include in memory search (directories or .md files; relative paths resolved from workspace).",
"agents.defaults.memorySearch.experimental.sessionMemory":
"Enable experimental session transcript indexing for memory search (default: false).",
"agents.defaults.memorySearch.provider":
'Embedding provider ("openai", "gemini", "voyage", or "local").',
"agents.defaults.memorySearch.remote.baseUrl":
"Custom base URL for remote embeddings (OpenAI-compatible proxies or Gemini overrides).",
"agents.defaults.memorySearch.remote.apiKey": "Custom API key for the remote embedding provider.",
"agents.defaults.memorySearch.remote.headers":
"Extra headers for remote embeddings (merged; remote overrides OpenAI headers).",
"agents.defaults.memorySearch.remote.batch.enabled":
"Enable batch API for memory embeddings (OpenAI/Gemini; default: true).",
"agents.defaults.memorySearch.remote.batch.wait":
"Wait for batch completion when indexing (default: true).",
"agents.defaults.memorySearch.remote.batch.concurrency":
"Max concurrent embedding batch jobs for memory indexing (default: 2).",
"agents.defaults.memorySearch.remote.batch.pollIntervalMs":
"Polling interval in ms for batch status (default: 2000).",
"agents.defaults.memorySearch.remote.batch.timeoutMinutes":
"Timeout in minutes for batch indexing (default: 60).",
"agents.defaults.memorySearch.local.modelPath":
"Local GGUF model path or hf: URI (node-llama-cpp).",
"agents.defaults.memorySearch.fallback":
'Fallback provider when embeddings fail ("openai", "gemini", "local", or "none").',
"agents.defaults.memorySearch.store.path":
"SQLite index path (default: ~/.openclaw/memory/{agentId}.sqlite).",
"agents.defaults.memorySearch.store.vector.enabled":
"Enable sqlite-vec extension for vector search (default: true).",
"agents.defaults.memorySearch.store.vector.extensionPath":
"Optional override path to sqlite-vec extension library (.dylib/.so/.dll).",
"agents.defaults.memorySearch.query.hybrid.enabled":
"Enable hybrid BM25 + vector search for memory (default: true).",
"agents.defaults.memorySearch.query.hybrid.vectorWeight":
"Weight for vector similarity when merging results (0-1).",
"agents.defaults.memorySearch.query.hybrid.textWeight":
"Weight for BM25 text relevance when merging results (0-1).",
"agents.defaults.memorySearch.query.hybrid.candidateMultiplier":
"Multiplier for candidate pool size (default: 4).",
"agents.defaults.memorySearch.cache.enabled":
"Cache chunk embeddings in SQLite to speed up reindexing and frequent updates (default: true).",
memory: "Memory backend configuration (global).",
"memory.backend": 'Memory backend ("builtin" for OpenClaw embeddings, "qmd" for QMD sidecar).',
"memory.citations": 'Default citation behavior ("auto", "on", or "off").',
"memory.qmd.command": "Path to the qmd binary (default: resolves from PATH).",
"memory.qmd.includeDefaultMemory":
"Whether to automatically index MEMORY.md + memory/**/*.md (default: true).",
"memory.qmd.paths":
"Additional directories/files to index with QMD (path + optional glob pattern).",
"memory.qmd.paths.path": "Absolute or ~-relative path to index via QMD.",
"memory.qmd.paths.pattern": "Glob pattern relative to the path root (default: **/*.md).",
"memory.qmd.paths.name":
"Optional stable name for the QMD collection (default derived from path).",
"memory.qmd.sessions.enabled":
"Enable QMD session transcript indexing (experimental, default: false).",
"memory.qmd.sessions.exportDir":
"Override directory for sanitized session exports before indexing.",
"memory.qmd.sessions.retentionDays":
"Retention window for exported sessions before pruning (default: unlimited).",
"memory.qmd.update.interval":
"How often the QMD sidecar refreshes indexes (duration string, default: 5m).",
"memory.qmd.update.debounceMs":
"Minimum delay between successive QMD refresh runs (default: 15000).",
"memory.qmd.update.onBoot": "Run QMD update once on gateway startup (default: true).",
"memory.qmd.update.waitForBootSync":
"Block startup until the boot QMD refresh finishes (default: false).",
"memory.qmd.update.embedInterval":
"How often QMD embeddings are refreshed (duration string, default: 60m). Set to 0 to disable periodic embed.",
"memory.qmd.update.commandTimeoutMs":
"Timeout for QMD maintenance commands like collection list/add (default: 30000).",
"memory.qmd.update.updateTimeoutMs": "Timeout for `qmd update` runs (default: 120000).",
"memory.qmd.update.embedTimeoutMs": "Timeout for `qmd embed` runs (default: 120000).",
"memory.qmd.limits.maxResults": "Max QMD results returned to the agent loop (default: 6).",
"memory.qmd.limits.maxSnippetChars": "Max characters per snippet pulled from QMD (default: 700).",
"memory.qmd.limits.maxInjectedChars": "Max total characters injected from QMD hits per turn.",
"memory.qmd.limits.timeoutMs": "Per-query timeout for QMD searches (default: 4000).",
"memory.qmd.scope":
"Session/channel scope for QMD recall (same syntax as session.sendPolicy; default: direct-only).",
"agents.defaults.memorySearch.cache.maxEntries":
"Optional cap on cached embeddings (best-effort).",
"agents.defaults.memorySearch.sync.onSearch":
"Lazy sync: schedule a reindex on search after changes.",
"agents.defaults.memorySearch.sync.watch": "Watch memory files for changes (chokidar).",
"agents.defaults.memorySearch.sync.sessions.deltaBytes":
"Minimum appended bytes before session transcripts trigger reindex (default: 100000).",
"agents.defaults.memorySearch.sync.sessions.deltaMessages":
"Minimum appended JSONL lines before session transcripts trigger reindex (default: 50).",
"plugins.enabled": "Enable plugin/extension loading (default: true).",
"plugins.allow": "Optional allowlist of plugin ids; when set, only listed plugins load.",
"plugins.deny": "Optional denylist of plugin ids; deny wins over allowlist.",
"plugins.load.paths": "Additional plugin files or directories to load.",
"plugins.slots": "Select which plugins own exclusive slots (memory, etc.).",
"plugins.slots.memory":
'Select the active memory plugin by id, or "none" to disable memory plugins.',
"plugins.entries": "Per-plugin settings keyed by plugin id (enable/disable + config payloads).",
"plugins.entries.*.enabled": "Overrides plugin enable/disable for this entry (restart required).",
"plugins.entries.*.config": "Plugin-defined config payload (schema is provided by the plugin).",
"plugins.installs":
"CLI-managed install metadata (used by `openclaw plugins update` to locate install sources).",
"plugins.installs.*.source": 'Install source ("npm", "archive", or "path").',
"plugins.installs.*.spec": "Original npm spec used for install (if source is npm).",
"plugins.installs.*.sourcePath": "Original archive/path used for install (if any).",
"plugins.installs.*.installPath":
"Resolved install directory (usually ~/.openclaw/extensions/<id>).",
"plugins.installs.*.version": "Version recorded at install time (if available).",
"plugins.installs.*.installedAt": "ISO timestamp of last install/update.",
"agents.list.*.identity.avatar":
"Agent avatar (workspace-relative path, http(s) URL, or data URI).",
"agents.defaults.model.primary": "Primary model (provider/model).",
"agents.defaults.model.fallbacks":
"Ordered fallback models (provider/model). Used when the primary model fails.",
"agents.defaults.imageModel.primary":
"Optional image model (provider/model) used when the primary model lacks image input.",
"agents.defaults.imageModel.fallbacks": "Ordered fallback image models (provider/model).",
"agents.defaults.cliBackends": "Optional CLI backends for text-only fallback (claude-cli, etc.).",
"agents.defaults.humanDelay.mode": 'Delay style for block replies ("off", "natural", "custom").',
"agents.defaults.humanDelay.minMs": "Minimum delay in ms for custom humanDelay (default: 800).",
"agents.defaults.humanDelay.maxMs": "Maximum delay in ms for custom humanDelay (default: 2500).",
"commands.native":
"Register native commands with channels that support it (Discord/Slack/Telegram).",
"commands.nativeSkills":
"Register native skill commands (user-invocable skills) with channels that support it.",
"commands.text": "Allow text command parsing (slash commands only).",
"commands.bash":
"Allow bash chat command (`!`; `/bash` alias) to run host shell commands (default: false; requires tools.elevated).",
"commands.bashForegroundMs":
"How long bash waits before backgrounding (default: 2000; 0 backgrounds immediately).",
"commands.config": "Allow /config chat command to read/write config on disk (default: false).",
"commands.debug": "Allow /debug chat command for runtime-only overrides (default: false).",
"commands.restart": "Allow /restart and gateway restart tool actions (default: false).",
"commands.useAccessGroups": "Enforce access-group allowlists/policies for commands.",
"commands.ownerAllowFrom":
"Explicit owner allowlist for owner-only tools/commands. Use channel-native IDs (optionally prefixed like \"whatsapp:+15551234567\"). '*' is ignored.",
"session.dmScope":
'DM session scoping: "main" keeps continuity; "per-peer", "per-channel-peer", or "per-account-channel-peer" isolates DM history (recommended for shared inboxes/multi-account).',
"session.identityLinks":
"Map canonical identities to provider-prefixed peer IDs for DM session linking (example: telegram:123456).",
"channels.telegram.configWrites":
"Allow Telegram to write config in response to channel events/commands (default: true).",
"channels.slack.configWrites":
"Allow Slack to write config in response to channel events/commands (default: true).",
"channels.mattermost.configWrites":
"Allow Mattermost to write config in response to channel events/commands (default: true).",
"channels.discord.configWrites":
"Allow Discord to write config in response to channel events/commands (default: true).",
"channels.whatsapp.configWrites":
"Allow WhatsApp to write config in response to channel events/commands (default: true).",
"channels.signal.configWrites":
"Allow Signal to write config in response to channel events/commands (default: true).",
"channels.imessage.configWrites":
"Allow iMessage to write config in response to channel events/commands (default: true).",
"channels.msteams.configWrites":
"Allow Microsoft Teams to write config in response to channel events/commands (default: true).",
...IRC_FIELD_HELP,
"channels.discord.commands.native": 'Override native commands for Discord (bool or "auto").',
"channels.discord.commands.nativeSkills":
'Override native skill commands for Discord (bool or "auto").',
"channels.telegram.commands.native": 'Override native commands for Telegram (bool or "auto").',
"channels.telegram.commands.nativeSkills":
'Override native skill commands for Telegram (bool or "auto").',
"channels.slack.commands.native": 'Override native commands for Slack (bool or "auto").',
"channels.slack.commands.nativeSkills":
'Override native skill commands for Slack (bool or "auto").',
"session.agentToAgent.maxPingPongTurns":
"Max reply-back turns between requester and target (05).",
"channels.telegram.customCommands":
"Additional Telegram bot menu commands (merged with native; conflicts ignored).",
"messages.ackReaction": "Emoji reaction used to acknowledge inbound messages (empty disables).",
"messages.ackReactionScope":
'When to send ack reactions ("group-mentions", "group-all", "direct", "all").',
"messages.inbound.debounceMs":
"Debounce window (ms) for batching rapid inbound messages from the same sender (0 to disable).",
"channels.telegram.dmPolicy":
'Direct message access control ("pairing" recommended). "open" requires channels.telegram.allowFrom=["*"].',
"channels.telegram.streamMode":
"Draft streaming mode for Telegram replies (off | partial | block). Separate from block streaming; requires private topics + sendMessageDraft.",
"channels.telegram.draftChunk.minChars":
'Minimum chars before emitting a Telegram draft update when channels.telegram.streamMode="block" (default: 200).',
"channels.telegram.draftChunk.maxChars":
'Target max size for a Telegram draft update chunk when channels.telegram.streamMode="block" (default: 800; clamped to channels.telegram.textChunkLimit).',
"channels.telegram.draftChunk.breakPreference":
"Preferred breakpoints for Telegram draft chunks (paragraph | newline | sentence). Default: paragraph.",
"channels.telegram.retry.attempts":
"Max retry attempts for outbound Telegram API calls (default: 3).",
"channels.telegram.retry.minDelayMs": "Minimum retry delay in ms for Telegram outbound calls.",
"channels.telegram.retry.maxDelayMs":
"Maximum retry delay cap in ms for Telegram outbound calls.",
"channels.telegram.retry.jitter": "Jitter factor (0-1) applied to Telegram retry delays.",
"channels.telegram.network.autoSelectFamily":
"Override Node autoSelectFamily for Telegram (true=enable, false=disable).",
"channels.telegram.timeoutSeconds":
"Max seconds before Telegram API requests are aborted (default: 500 per grammY).",
"channels.whatsapp.dmPolicy":
'Direct message access control ("pairing" recommended). "open" requires channels.whatsapp.allowFrom=["*"].',
"channels.whatsapp.selfChatMode": "Same-phone setup (bot uses your personal WhatsApp number).",
"channels.whatsapp.debounceMs":
"Debounce window (ms) for batching rapid consecutive messages from the same sender (0 to disable).",
"channels.signal.dmPolicy":
'Direct message access control ("pairing" recommended). "open" requires channels.signal.allowFrom=["*"].',
"channels.imessage.dmPolicy":
'Direct message access control ("pairing" recommended). "open" requires channels.imessage.allowFrom=["*"].',
"channels.bluebubbles.dmPolicy":
'Direct message access control ("pairing" recommended). "open" requires channels.bluebubbles.allowFrom=["*"].',
"channels.discord.dm.policy":
'Direct message access control ("pairing" recommended). "open" requires channels.discord.dm.allowFrom=["*"].',
"channels.discord.retry.attempts":
"Max retry attempts for outbound Discord API calls (default: 3).",
"channels.discord.retry.minDelayMs": "Minimum retry delay in ms for Discord outbound calls.",
"channels.discord.retry.maxDelayMs": "Maximum retry delay cap in ms for Discord outbound calls.",
"channels.discord.retry.jitter": "Jitter factor (0-1) applied to Discord retry delays.",
"channels.discord.maxLinesPerMessage": "Soft max line count per Discord message (default: 17).",
"channels.discord.intents.presence":
"Enable the Guild Presences privileged intent. Must also be enabled in the Discord Developer Portal. Allows tracking user activities (e.g. Spotify). Default: false.",
"channels.discord.intents.guildMembers":
"Enable the Guild Members privileged intent. Must also be enabled in the Discord Developer Portal. Default: false.",
"channels.discord.pluralkit.enabled":
"Resolve PluralKit proxied messages and treat system members as distinct senders.",
"channels.discord.pluralkit.token":
"Optional PluralKit token for resolving private systems or members.",
"channels.slack.dm.policy":
'Direct message access control ("pairing" recommended). "open" requires channels.slack.dm.allowFrom=["*"].',
};
const FIELD_PLACEHOLDERS: Record<string, string> = {
"gateway.remote.url": "ws://host:18789",
"gateway.remote.tlsFingerprint": "sha256:ab12cd34…",
"gateway.remote.sshTarget": "user@host",
"gateway.controlUi.basePath": "/openclaw",
"gateway.controlUi.root": "dist/control-ui",
"gateway.controlUi.allowedOrigins": "https://control.example.com",
"channels.mattermost.baseUrl": "https://chat.example.com",
"agents.list[].identity.avatar": "avatars/openclaw.png",
};
const SENSITIVE_PATTERNS = [/token/i, /password/i, /secret/i, /api.?key/i];
function isSensitiveConfigPath(path: string): boolean {
return SENSITIVE_PATTERNS.some((pattern) => pattern.test(path));
}
export function buildBaseHints(): ConfigUiHints {
const hints: ConfigUiHints = {};
for (const [group, label] of Object.entries(GROUP_LABELS)) {
hints[group] = {
label,
group: label,
order: GROUP_ORDER[group],
};
}
for (const [path, label] of Object.entries(FIELD_LABELS)) {
const current = hints[path];
hints[path] = current ? { ...current, label } : { label };
}
for (const [path, help] of Object.entries(FIELD_HELP)) {
const current = hints[path];
hints[path] = current ? { ...current, help } : { help };
}
for (const [path, placeholder] of Object.entries(FIELD_PLACEHOLDERS)) {
const current = hints[path];
hints[path] = current ? { ...current, placeholder } : { placeholder };
}
return hints;
}
export function applySensitiveHints(hints: ConfigUiHints): ConfigUiHints {
const next = { ...hints };
for (const key of Object.keys(next)) {
if (isSensitiveConfigPath(key)) {
next[key] = { ...next[key], sensitive: true };
}
}
return next;
}

26
src/config/schema.irc.ts Normal file
View File

@@ -0,0 +1,26 @@
export const IRC_FIELD_LABELS: Record<string, string> = {
"channels.irc": "IRC",
"channels.irc.dmPolicy": "IRC DM Policy",
"channels.irc.nickserv.enabled": "IRC NickServ Enabled",
"channels.irc.nickserv.service": "IRC NickServ Service",
"channels.irc.nickserv.password": "IRC NickServ Password",
"channels.irc.nickserv.passwordFile": "IRC NickServ Password File",
"channels.irc.nickserv.register": "IRC NickServ Register",
"channels.irc.nickserv.registerEmail": "IRC NickServ Register Email",
};
export const IRC_FIELD_HELP: Record<string, string> = {
"channels.irc.configWrites":
"Allow IRC to write config in response to channel events/commands (default: true).",
"channels.irc.dmPolicy":
'Direct message access control ("pairing" recommended). "open" requires channels.irc.allowFrom=["*"].',
"channels.irc.nickserv.enabled":
"Enable NickServ identify/register after connect (defaults to enabled when password is configured).",
"channels.irc.nickserv.service": "NickServ service nick (default: NickServ).",
"channels.irc.nickserv.password": "NickServ password used for IDENTIFY/REGISTER (sensitive).",
"channels.irc.nickserv.passwordFile": "Optional file path containing NickServ password.",
"channels.irc.nickserv.register":
"If true, send NickServ REGISTER on every connect. Use once for initial registration, then disable.",
"channels.irc.nickserv.registerEmail":
"Email used with NickServ REGISTER (required when register=true).",
};

View File

@@ -1,19 +1,10 @@
import type { ConfigUiHint, ConfigUiHints } from "./schema.hints.js";
import { CHANNEL_IDS } from "../channels/registry.js";
import { VERSION } from "../version.js";
import { applySensitiveHints, buildBaseHints } from "./schema.hints.js";
import { OpenClawSchema } from "./zod-schema.js";
export type ConfigUiHint = {
label?: string;
help?: string;
group?: string;
order?: number;
advanced?: boolean;
sensitive?: boolean;
placeholder?: string;
itemTemplate?: unknown;
};
export type ConfigUiHints = Record<string, ConfigUiHint>;
export type { ConfigUiHint, ConfigUiHints } from "./schema.hints.js";
export type ConfigSchema = ReturnType<typeof OpenClawSchema.toJSONSchema>;
@@ -45,745 +36,6 @@ export type ChannelUiMetadata = {
configUiHints?: Record<string, ConfigUiHint>;
};
const GROUP_LABELS: Record<string, string> = {
wizard: "Wizard",
update: "Update",
diagnostics: "Diagnostics",
logging: "Logging",
gateway: "Gateway",
nodeHost: "Node Host",
agents: "Agents",
tools: "Tools",
bindings: "Bindings",
audio: "Audio",
models: "Models",
messages: "Messages",
commands: "Commands",
session: "Session",
cron: "Cron",
hooks: "Hooks",
ui: "UI",
browser: "Browser",
talk: "Talk",
channels: "Messaging Channels",
skills: "Skills",
plugins: "Plugins",
discovery: "Discovery",
presence: "Presence",
voicewake: "Voice Wake",
};
const GROUP_ORDER: Record<string, number> = {
wizard: 20,
update: 25,
diagnostics: 27,
gateway: 30,
nodeHost: 35,
agents: 40,
tools: 50,
bindings: 55,
audio: 60,
models: 70,
messages: 80,
commands: 85,
session: 90,
cron: 100,
hooks: 110,
ui: 120,
browser: 130,
talk: 140,
channels: 150,
skills: 200,
plugins: 205,
discovery: 210,
presence: 220,
voicewake: 230,
logging: 900,
};
const FIELD_LABELS: Record<string, string> = {
"meta.lastTouchedVersion": "Config Last Touched Version",
"meta.lastTouchedAt": "Config Last Touched At",
"update.channel": "Update Channel",
"update.checkOnStart": "Update Check on Start",
"diagnostics.enabled": "Diagnostics Enabled",
"diagnostics.flags": "Diagnostics Flags",
"diagnostics.otel.enabled": "OpenTelemetry Enabled",
"diagnostics.otel.endpoint": "OpenTelemetry Endpoint",
"diagnostics.otel.protocol": "OpenTelemetry Protocol",
"diagnostics.otel.headers": "OpenTelemetry Headers",
"diagnostics.otel.serviceName": "OpenTelemetry Service Name",
"diagnostics.otel.traces": "OpenTelemetry Traces Enabled",
"diagnostics.otel.metrics": "OpenTelemetry Metrics Enabled",
"diagnostics.otel.logs": "OpenTelemetry Logs Enabled",
"diagnostics.otel.sampleRate": "OpenTelemetry Trace Sample Rate",
"diagnostics.otel.flushIntervalMs": "OpenTelemetry Flush Interval (ms)",
"diagnostics.cacheTrace.enabled": "Cache Trace Enabled",
"diagnostics.cacheTrace.filePath": "Cache Trace File Path",
"diagnostics.cacheTrace.includeMessages": "Cache Trace Include Messages",
"diagnostics.cacheTrace.includePrompt": "Cache Trace Include Prompt",
"diagnostics.cacheTrace.includeSystem": "Cache Trace Include System",
"agents.list.*.identity.avatar": "Identity Avatar",
"agents.list.*.skills": "Agent Skill Filter",
"gateway.remote.url": "Remote Gateway URL",
"gateway.remote.sshTarget": "Remote Gateway SSH Target",
"gateway.remote.sshIdentity": "Remote Gateway SSH Identity",
"gateway.remote.token": "Remote Gateway Token",
"gateway.remote.password": "Remote Gateway Password",
"gateway.remote.tlsFingerprint": "Remote Gateway TLS Fingerprint",
"gateway.auth.token": "Gateway Token",
"gateway.auth.password": "Gateway Password",
"tools.media.image.enabled": "Enable Image Understanding",
"tools.media.image.maxBytes": "Image Understanding Max Bytes",
"tools.media.image.maxChars": "Image Understanding Max Chars",
"tools.media.image.prompt": "Image Understanding Prompt",
"tools.media.image.timeoutSeconds": "Image Understanding Timeout (sec)",
"tools.media.image.attachments": "Image Understanding Attachment Policy",
"tools.media.image.models": "Image Understanding Models",
"tools.media.image.scope": "Image Understanding Scope",
"tools.media.models": "Media Understanding Shared Models",
"tools.media.concurrency": "Media Understanding Concurrency",
"tools.media.audio.enabled": "Enable Audio Understanding",
"tools.media.audio.maxBytes": "Audio Understanding Max Bytes",
"tools.media.audio.maxChars": "Audio Understanding Max Chars",
"tools.media.audio.prompt": "Audio Understanding Prompt",
"tools.media.audio.timeoutSeconds": "Audio Understanding Timeout (sec)",
"tools.media.audio.language": "Audio Understanding Language",
"tools.media.audio.attachments": "Audio Understanding Attachment Policy",
"tools.media.audio.models": "Audio Understanding Models",
"tools.media.audio.scope": "Audio Understanding Scope",
"tools.media.video.enabled": "Enable Video Understanding",
"tools.media.video.maxBytes": "Video Understanding Max Bytes",
"tools.media.video.maxChars": "Video Understanding Max Chars",
"tools.media.video.prompt": "Video Understanding Prompt",
"tools.media.video.timeoutSeconds": "Video Understanding Timeout (sec)",
"tools.media.video.attachments": "Video Understanding Attachment Policy",
"tools.media.video.models": "Video Understanding Models",
"tools.media.video.scope": "Video Understanding Scope",
"tools.links.enabled": "Enable Link Understanding",
"tools.links.maxLinks": "Link Understanding Max Links",
"tools.links.timeoutSeconds": "Link Understanding Timeout (sec)",
"tools.links.models": "Link Understanding Models",
"tools.links.scope": "Link Understanding Scope",
"tools.profile": "Tool Profile",
"tools.alsoAllow": "Tool Allowlist Additions",
"agents.list[].tools.profile": "Agent Tool Profile",
"agents.list[].tools.alsoAllow": "Agent Tool Allowlist Additions",
"tools.byProvider": "Tool Policy by Provider",
"agents.list[].tools.byProvider": "Agent Tool Policy by Provider",
"tools.exec.applyPatch.enabled": "Enable apply_patch",
"tools.exec.applyPatch.allowModels": "apply_patch Model Allowlist",
"tools.exec.notifyOnExit": "Exec Notify On Exit",
"tools.exec.approvalRunningNoticeMs": "Exec Approval Running Notice (ms)",
"tools.exec.host": "Exec Host",
"tools.exec.security": "Exec Security",
"tools.exec.ask": "Exec Ask",
"tools.exec.node": "Exec Node Binding",
"tools.exec.pathPrepend": "Exec PATH Prepend",
"tools.exec.safeBins": "Exec Safe Bins",
"tools.message.allowCrossContextSend": "Allow Cross-Context Messaging",
"tools.message.crossContext.allowWithinProvider": "Allow Cross-Context (Same Provider)",
"tools.message.crossContext.allowAcrossProviders": "Allow Cross-Context (Across Providers)",
"tools.message.crossContext.marker.enabled": "Cross-Context Marker",
"tools.message.crossContext.marker.prefix": "Cross-Context Marker Prefix",
"tools.message.crossContext.marker.suffix": "Cross-Context Marker Suffix",
"tools.message.broadcast.enabled": "Enable Message Broadcast",
"tools.web.search.enabled": "Enable Web Search Tool",
"tools.web.search.provider": "Web Search Provider",
"tools.web.search.apiKey": "Brave Search API Key",
"tools.web.search.maxResults": "Web Search Max Results",
"tools.web.search.timeoutSeconds": "Web Search Timeout (sec)",
"tools.web.search.cacheTtlMinutes": "Web Search Cache TTL (min)",
"tools.web.fetch.enabled": "Enable Web Fetch Tool",
"tools.web.fetch.maxChars": "Web Fetch Max Chars",
"tools.web.fetch.timeoutSeconds": "Web Fetch Timeout (sec)",
"tools.web.fetch.cacheTtlMinutes": "Web Fetch Cache TTL (min)",
"tools.web.fetch.maxRedirects": "Web Fetch Max Redirects",
"tools.web.fetch.userAgent": "Web Fetch User-Agent",
"gateway.controlUi.basePath": "Control UI Base Path",
"gateway.controlUi.root": "Control UI Assets Root",
"gateway.controlUi.allowedOrigins": "Control UI Allowed Origins",
"gateway.controlUi.allowInsecureAuth": "Allow Insecure Control UI Auth",
"gateway.controlUi.dangerouslyDisableDeviceAuth": "Dangerously Disable Control UI Device Auth",
"gateway.http.endpoints.chatCompletions.enabled": "OpenAI Chat Completions Endpoint",
"gateway.reload.mode": "Config Reload Mode",
"gateway.reload.debounceMs": "Config Reload Debounce (ms)",
"gateway.nodes.browser.mode": "Gateway Node Browser Mode",
"gateway.nodes.browser.node": "Gateway Node Browser Pin",
"gateway.nodes.allowCommands": "Gateway Node Allowlist (Extra Commands)",
"gateway.nodes.denyCommands": "Gateway Node Denylist",
"nodeHost.browserProxy.enabled": "Node Browser Proxy Enabled",
"nodeHost.browserProxy.allowProfiles": "Node Browser Proxy Allowed Profiles",
"skills.load.watch": "Watch Skills",
"skills.load.watchDebounceMs": "Skills Watch Debounce (ms)",
"agents.defaults.workspace": "Workspace",
"agents.defaults.repoRoot": "Repo Root",
"agents.defaults.bootstrapMaxChars": "Bootstrap Max Chars",
"agents.defaults.envelopeTimezone": "Envelope Timezone",
"agents.defaults.envelopeTimestamp": "Envelope Timestamp",
"agents.defaults.envelopeElapsed": "Envelope Elapsed",
"agents.defaults.memorySearch": "Memory Search",
"agents.defaults.memorySearch.enabled": "Enable Memory Search",
"agents.defaults.memorySearch.sources": "Memory Search Sources",
"agents.defaults.memorySearch.extraPaths": "Extra Memory Paths",
"agents.defaults.memorySearch.experimental.sessionMemory":
"Memory Search Session Index (Experimental)",
"agents.defaults.memorySearch.provider": "Memory Search Provider",
"agents.defaults.memorySearch.remote.baseUrl": "Remote Embedding Base URL",
"agents.defaults.memorySearch.remote.apiKey": "Remote Embedding API Key",
"agents.defaults.memorySearch.remote.headers": "Remote Embedding Headers",
"agents.defaults.memorySearch.remote.batch.concurrency": "Remote Batch Concurrency",
"agents.defaults.memorySearch.model": "Memory Search Model",
"agents.defaults.memorySearch.fallback": "Memory Search Fallback",
"agents.defaults.memorySearch.local.modelPath": "Local Embedding Model Path",
"agents.defaults.memorySearch.store.path": "Memory Search Index Path",
"agents.defaults.memorySearch.store.vector.enabled": "Memory Search Vector Index",
"agents.defaults.memorySearch.store.vector.extensionPath": "Memory Search Vector Extension Path",
"agents.defaults.memorySearch.chunking.tokens": "Memory Chunk Tokens",
"agents.defaults.memorySearch.chunking.overlap": "Memory Chunk Overlap Tokens",
"agents.defaults.memorySearch.sync.onSessionStart": "Index on Session Start",
"agents.defaults.memorySearch.sync.onSearch": "Index on Search (Lazy)",
"agents.defaults.memorySearch.sync.watch": "Watch Memory Files",
"agents.defaults.memorySearch.sync.watchDebounceMs": "Memory Watch Debounce (ms)",
"agents.defaults.memorySearch.sync.sessions.deltaBytes": "Session Delta Bytes",
"agents.defaults.memorySearch.sync.sessions.deltaMessages": "Session Delta Messages",
"agents.defaults.memorySearch.query.maxResults": "Memory Search Max Results",
"agents.defaults.memorySearch.query.minScore": "Memory Search Min Score",
"agents.defaults.memorySearch.query.hybrid.enabled": "Memory Search Hybrid",
"agents.defaults.memorySearch.query.hybrid.vectorWeight": "Memory Search Vector Weight",
"agents.defaults.memorySearch.query.hybrid.textWeight": "Memory Search Text Weight",
"agents.defaults.memorySearch.query.hybrid.candidateMultiplier":
"Memory Search Hybrid Candidate Multiplier",
"agents.defaults.memorySearch.cache.enabled": "Memory Search Embedding Cache",
"agents.defaults.memorySearch.cache.maxEntries": "Memory Search Embedding Cache Max Entries",
memory: "Memory",
"memory.backend": "Memory Backend",
"memory.citations": "Memory Citations Mode",
"memory.qmd.command": "QMD Binary",
"memory.qmd.includeDefaultMemory": "QMD Include Default Memory",
"memory.qmd.paths": "QMD Extra Paths",
"memory.qmd.paths.path": "QMD Path",
"memory.qmd.paths.pattern": "QMD Path Pattern",
"memory.qmd.paths.name": "QMD Path Name",
"memory.qmd.sessions.enabled": "QMD Session Indexing",
"memory.qmd.sessions.exportDir": "QMD Session Export Directory",
"memory.qmd.sessions.retentionDays": "QMD Session Retention (days)",
"memory.qmd.update.interval": "QMD Update Interval",
"memory.qmd.update.debounceMs": "QMD Update Debounce (ms)",
"memory.qmd.update.onBoot": "QMD Update on Startup",
"memory.qmd.update.waitForBootSync": "QMD Wait for Boot Sync",
"memory.qmd.update.embedInterval": "QMD Embed Interval",
"memory.qmd.update.commandTimeoutMs": "QMD Command Timeout (ms)",
"memory.qmd.update.updateTimeoutMs": "QMD Update Timeout (ms)",
"memory.qmd.update.embedTimeoutMs": "QMD Embed Timeout (ms)",
"memory.qmd.limits.maxResults": "QMD Max Results",
"memory.qmd.limits.maxSnippetChars": "QMD Max Snippet Chars",
"memory.qmd.limits.maxInjectedChars": "QMD Max Injected Chars",
"memory.qmd.limits.timeoutMs": "QMD Search Timeout (ms)",
"memory.qmd.scope": "QMD Surface Scope",
"auth.profiles": "Auth Profiles",
"auth.order": "Auth Profile Order",
"auth.cooldowns.billingBackoffHours": "Billing Backoff (hours)",
"auth.cooldowns.billingBackoffHoursByProvider": "Billing Backoff Overrides",
"auth.cooldowns.billingMaxHours": "Billing Backoff Cap (hours)",
"auth.cooldowns.failureWindowHours": "Failover Window (hours)",
"agents.defaults.models": "Models",
"agents.defaults.model.primary": "Primary Model",
"agents.defaults.model.fallbacks": "Model Fallbacks",
"agents.defaults.imageModel.primary": "Image Model",
"agents.defaults.imageModel.fallbacks": "Image Model Fallbacks",
"agents.defaults.humanDelay.mode": "Human Delay Mode",
"agents.defaults.humanDelay.minMs": "Human Delay Min (ms)",
"agents.defaults.humanDelay.maxMs": "Human Delay Max (ms)",
"agents.defaults.cliBackends": "CLI Backends",
"commands.native": "Native Commands",
"commands.nativeSkills": "Native Skill Commands",
"commands.text": "Text Commands",
"commands.bash": "Allow Bash Chat Command",
"commands.bashForegroundMs": "Bash Foreground Window (ms)",
"commands.config": "Allow /config",
"commands.debug": "Allow /debug",
"commands.restart": "Allow Restart",
"commands.useAccessGroups": "Use Access Groups",
"commands.ownerAllowFrom": "Command Owners",
"commands.allowFrom": "Command Access Allowlist",
"ui.seamColor": "Accent Color",
"ui.assistant.name": "Assistant Name",
"ui.assistant.avatar": "Assistant Avatar",
"browser.evaluateEnabled": "Browser Evaluate Enabled",
"browser.snapshotDefaults": "Browser Snapshot Defaults",
"browser.snapshotDefaults.mode": "Browser Snapshot Mode",
"browser.remoteCdpTimeoutMs": "Remote CDP Timeout (ms)",
"browser.remoteCdpHandshakeTimeoutMs": "Remote CDP Handshake Timeout (ms)",
"session.dmScope": "DM Session Scope",
"session.agentToAgent.maxPingPongTurns": "Agent-to-Agent Ping-Pong Turns",
"messages.ackReaction": "Ack Reaction Emoji",
"messages.ackReactionScope": "Ack Reaction Scope",
"messages.inbound.debounceMs": "Inbound Message Debounce (ms)",
"talk.apiKey": "Talk API Key",
"channels.whatsapp": "WhatsApp",
"channels.telegram": "Telegram",
"channels.telegram.customCommands": "Telegram Custom Commands",
"channels.discord": "Discord",
"channels.slack": "Slack",
"channels.mattermost": "Mattermost",
"channels.signal": "Signal",
"channels.imessage": "iMessage",
"channels.bluebubbles": "BlueBubbles",
"channels.msteams": "MS Teams",
"channels.telegram.botToken": "Telegram Bot Token",
"channels.telegram.dmPolicy": "Telegram DM Policy",
"channels.telegram.streamMode": "Telegram Draft Stream Mode",
"channels.telegram.draftChunk.minChars": "Telegram Draft Chunk Min Chars",
"channels.telegram.draftChunk.maxChars": "Telegram Draft Chunk Max Chars",
"channels.telegram.draftChunk.breakPreference": "Telegram Draft Chunk Break Preference",
"channels.telegram.retry.attempts": "Telegram Retry Attempts",
"channels.telegram.retry.minDelayMs": "Telegram Retry Min Delay (ms)",
"channels.telegram.retry.maxDelayMs": "Telegram Retry Max Delay (ms)",
"channels.telegram.retry.jitter": "Telegram Retry Jitter",
"channels.telegram.network.autoSelectFamily": "Telegram autoSelectFamily",
"channels.telegram.timeoutSeconds": "Telegram API Timeout (seconds)",
"channels.telegram.capabilities.inlineButtons": "Telegram Inline Buttons",
"channels.whatsapp.dmPolicy": "WhatsApp DM Policy",
"channels.whatsapp.selfChatMode": "WhatsApp Self-Phone Mode",
"channels.whatsapp.debounceMs": "WhatsApp Message Debounce (ms)",
"channels.signal.dmPolicy": "Signal DM Policy",
"channels.imessage.dmPolicy": "iMessage DM Policy",
"channels.bluebubbles.dmPolicy": "BlueBubbles DM Policy",
"channels.discord.dm.policy": "Discord DM Policy",
"channels.discord.retry.attempts": "Discord Retry Attempts",
"channels.discord.retry.minDelayMs": "Discord Retry Min Delay (ms)",
"channels.discord.retry.maxDelayMs": "Discord Retry Max Delay (ms)",
"channels.discord.retry.jitter": "Discord Retry Jitter",
"channels.discord.maxLinesPerMessage": "Discord Max Lines Per Message",
"channels.discord.intents.presence": "Discord Presence Intent",
"channels.discord.intents.guildMembers": "Discord Guild Members Intent",
"channels.discord.pluralkit.enabled": "Discord PluralKit Enabled",
"channels.discord.pluralkit.token": "Discord PluralKit Token",
"channels.slack.dm.policy": "Slack DM Policy",
"channels.slack.allowBots": "Slack Allow Bot Messages",
"channels.discord.token": "Discord Bot Token",
"channels.slack.botToken": "Slack Bot Token",
"channels.slack.appToken": "Slack App Token",
"channels.slack.userToken": "Slack User Token",
"channels.slack.userTokenReadOnly": "Slack User Token Read Only",
"channels.slack.thread.historyScope": "Slack Thread History Scope",
"channels.slack.thread.inheritParent": "Slack Thread Parent Inheritance",
"channels.mattermost.botToken": "Mattermost Bot Token",
"channels.mattermost.baseUrl": "Mattermost Base URL",
"channels.mattermost.chatmode": "Mattermost Chat Mode",
"channels.mattermost.oncharPrefixes": "Mattermost Onchar Prefixes",
"channels.mattermost.requireMention": "Mattermost Require Mention",
"channels.signal.account": "Signal Account",
"channels.imessage.cliPath": "iMessage CLI Path",
"agents.list[].skills": "Agent Skill Filter",
"agents.list[].identity.avatar": "Agent Avatar",
"discovery.mdns.mode": "mDNS Discovery Mode",
"plugins.enabled": "Enable Plugins",
"plugins.allow": "Plugin Allowlist",
"plugins.deny": "Plugin Denylist",
"plugins.load.paths": "Plugin Load Paths",
"plugins.slots": "Plugin Slots",
"plugins.slots.memory": "Memory Plugin",
"plugins.entries": "Plugin Entries",
"plugins.entries.*.enabled": "Plugin Enabled",
"plugins.entries.*.config": "Plugin Config",
"plugins.installs": "Plugin Install Records",
"plugins.installs.*.source": "Plugin Install Source",
"plugins.installs.*.spec": "Plugin Install Spec",
"plugins.installs.*.sourcePath": "Plugin Install Source Path",
"plugins.installs.*.installPath": "Plugin Install Path",
"plugins.installs.*.version": "Plugin Install Version",
"plugins.installs.*.installedAt": "Plugin Install Time",
};
const FIELD_HELP: Record<string, string> = {
"meta.lastTouchedVersion": "Auto-set when OpenClaw writes the config.",
"meta.lastTouchedAt": "ISO timestamp of the last config write (auto-set).",
"update.channel": 'Update channel for git + npm installs ("stable", "beta", or "dev").',
"update.checkOnStart": "Check for npm updates when the gateway starts (default: true).",
"gateway.remote.url": "Remote Gateway WebSocket URL (ws:// or wss://).",
"gateway.remote.tlsFingerprint":
"Expected sha256 TLS fingerprint for the remote gateway (pin to avoid MITM).",
"gateway.remote.sshTarget":
"Remote gateway over SSH (tunnels the gateway port to localhost). Format: user@host or user@host:port.",
"gateway.remote.sshIdentity": "Optional SSH identity file path (passed to ssh -i).",
"agents.list.*.skills":
"Optional allowlist of skills for this agent (omit = all skills; empty = no skills).",
"agents.list[].skills":
"Optional allowlist of skills for this agent (omit = all skills; empty = no skills).",
"agents.list[].identity.avatar":
"Avatar image path (relative to the agent workspace only) or a remote URL/data URL.",
"discovery.mdns.mode":
'mDNS broadcast mode ("minimal" default, "full" includes cliPath/sshPort, "off" disables mDNS).',
"gateway.auth.token":
"Required by default for gateway access (unless using Tailscale Serve identity); required for non-loopback binds.",
"gateway.auth.password": "Required for Tailscale funnel.",
"gateway.controlUi.basePath":
"Optional URL prefix where the Control UI is served (e.g. /openclaw).",
"gateway.controlUi.root":
"Optional filesystem root for Control UI assets (defaults to dist/control-ui).",
"gateway.controlUi.allowedOrigins":
"Allowed browser origins for Control UI/WebChat websocket connections (full origins only, e.g. https://control.example.com).",
"gateway.controlUi.allowInsecureAuth":
"Allow Control UI auth over insecure HTTP (token-only; not recommended).",
"gateway.controlUi.dangerouslyDisableDeviceAuth":
"DANGEROUS. Disable Control UI device identity checks (token/password only).",
"gateway.http.endpoints.chatCompletions.enabled":
"Enable the OpenAI-compatible `POST /v1/chat/completions` endpoint (default: false).",
"gateway.reload.mode": 'Hot reload strategy for config changes ("hybrid" recommended).',
"gateway.reload.debounceMs": "Debounce window (ms) before applying config changes.",
"gateway.nodes.browser.mode":
'Node browser routing ("auto" = pick single connected browser node, "manual" = require node param, "off" = disable).',
"gateway.nodes.browser.node": "Pin browser routing to a specific node id or name (optional).",
"gateway.nodes.allowCommands":
"Extra node.invoke commands to allow beyond the gateway defaults (array of command strings).",
"gateway.nodes.denyCommands":
"Commands to block even if present in node claims or default allowlist.",
"nodeHost.browserProxy.enabled": "Expose the local browser control server via node proxy.",
"nodeHost.browserProxy.allowProfiles":
"Optional allowlist of browser profile names exposed via the node proxy.",
"diagnostics.flags":
'Enable targeted diagnostics logs by flag (e.g. ["telegram.http"]). Supports wildcards like "telegram.*" or "*".',
"diagnostics.cacheTrace.enabled":
"Log cache trace snapshots for embedded agent runs (default: false).",
"diagnostics.cacheTrace.filePath":
"JSONL output path for cache trace logs (default: $OPENCLAW_STATE_DIR/logs/cache-trace.jsonl).",
"diagnostics.cacheTrace.includeMessages":
"Include full message payloads in trace output (default: true).",
"diagnostics.cacheTrace.includePrompt": "Include prompt text in trace output (default: true).",
"diagnostics.cacheTrace.includeSystem": "Include system prompt in trace output (default: true).",
"tools.exec.applyPatch.enabled":
"Experimental. Enables apply_patch for OpenAI models when allowed by tool policy.",
"tools.exec.applyPatch.allowModels":
'Optional allowlist of model ids (e.g. "gpt-5.2" or "openai/gpt-5.2").',
"tools.exec.notifyOnExit":
"When true (default), backgrounded exec sessions enqueue a system event and request a heartbeat on exit.",
"tools.exec.pathPrepend": "Directories to prepend to PATH for exec runs (gateway/sandbox).",
"tools.exec.safeBins":
"Allow stdin-only safe binaries to run without explicit allowlist entries.",
"tools.message.allowCrossContextSend":
"Legacy override: allow cross-context sends across all providers.",
"tools.message.crossContext.allowWithinProvider":
"Allow sends to other channels within the same provider (default: true).",
"tools.message.crossContext.allowAcrossProviders":
"Allow sends across different providers (default: false).",
"tools.message.crossContext.marker.enabled":
"Add a visible origin marker when sending cross-context (default: true).",
"tools.message.crossContext.marker.prefix":
'Text prefix for cross-context markers (supports "{channel}").',
"tools.message.crossContext.marker.suffix":
'Text suffix for cross-context markers (supports "{channel}").',
"tools.message.broadcast.enabled": "Enable broadcast action (default: true).",
"tools.web.search.enabled": "Enable the web_search tool (requires a provider API key).",
"tools.web.search.provider": 'Search provider ("brave" or "perplexity").',
"tools.web.search.apiKey": "Brave Search API key (fallback: BRAVE_API_KEY env var).",
"tools.web.search.maxResults": "Default number of results to return (1-10).",
"tools.web.search.timeoutSeconds": "Timeout in seconds for web_search requests.",
"tools.web.search.cacheTtlMinutes": "Cache TTL in minutes for web_search results.",
"tools.web.search.perplexity.apiKey":
"Perplexity or OpenRouter API key (fallback: PERPLEXITY_API_KEY or OPENROUTER_API_KEY env var).",
"tools.web.search.perplexity.baseUrl":
"Perplexity base URL override (default: https://openrouter.ai/api/v1 or https://api.perplexity.ai).",
"tools.web.search.perplexity.model":
'Perplexity model override (default: "perplexity/sonar-pro").',
"tools.web.fetch.enabled": "Enable the web_fetch tool (lightweight HTTP fetch).",
"tools.web.fetch.maxChars": "Max characters returned by web_fetch (truncated).",
"tools.web.fetch.maxCharsCap":
"Hard cap for web_fetch maxChars (applies to config and tool calls).",
"tools.web.fetch.timeoutSeconds": "Timeout in seconds for web_fetch requests.",
"tools.web.fetch.cacheTtlMinutes": "Cache TTL in minutes for web_fetch results.",
"tools.web.fetch.maxRedirects": "Maximum redirects allowed for web_fetch (default: 3).",
"tools.web.fetch.userAgent": "Override User-Agent header for web_fetch requests.",
"tools.web.fetch.readability":
"Use Readability to extract main content from HTML (fallbacks to basic HTML cleanup).",
"tools.web.fetch.firecrawl.enabled": "Enable Firecrawl fallback for web_fetch (if configured).",
"tools.web.fetch.firecrawl.apiKey": "Firecrawl API key (fallback: FIRECRAWL_API_KEY env var).",
"tools.web.fetch.firecrawl.baseUrl":
"Firecrawl base URL (e.g. https://api.firecrawl.dev or custom endpoint).",
"tools.web.fetch.firecrawl.onlyMainContent":
"When true, Firecrawl returns only the main content (default: true).",
"tools.web.fetch.firecrawl.maxAgeMs":
"Firecrawl maxAge (ms) for cached results when supported by the API.",
"tools.web.fetch.firecrawl.timeoutSeconds": "Timeout in seconds for Firecrawl requests.",
"channels.slack.allowBots":
"Allow bot-authored messages to trigger Slack replies (default: false).",
"channels.slack.thread.historyScope":
'Scope for Slack thread history context ("thread" isolates per thread; "channel" reuses channel history).',
"channels.slack.thread.inheritParent":
"If true, Slack thread sessions inherit the parent channel transcript (default: false).",
"channels.mattermost.botToken":
"Bot token from Mattermost System Console -> Integrations -> Bot Accounts.",
"channels.mattermost.baseUrl":
"Base URL for your Mattermost server (e.g., https://chat.example.com).",
"channels.mattermost.chatmode":
'Reply to channel messages on mention ("oncall"), on trigger chars (">" or "!") ("onchar"), or on every message ("onmessage").',
"channels.mattermost.oncharPrefixes": 'Trigger prefixes for onchar mode (default: [">", "!"]).',
"channels.mattermost.requireMention":
"Require @mention in channels before responding (default: true).",
"auth.profiles": "Named auth profiles (provider + mode + optional email).",
"auth.order": "Ordered auth profile IDs per provider (used for automatic failover).",
"auth.cooldowns.billingBackoffHours":
"Base backoff (hours) when a profile fails due to billing/insufficient credits (default: 5).",
"auth.cooldowns.billingBackoffHoursByProvider":
"Optional per-provider overrides for billing backoff (hours).",
"auth.cooldowns.billingMaxHours": "Cap (hours) for billing backoff (default: 24).",
"auth.cooldowns.failureWindowHours": "Failure window (hours) for backoff counters (default: 24).",
"agents.defaults.bootstrapMaxChars":
"Max characters of each workspace bootstrap file injected into the system prompt before truncation (default: 20000).",
"agents.defaults.repoRoot":
"Optional repository root shown in the system prompt runtime line (overrides auto-detect).",
"agents.defaults.envelopeTimezone":
'Timezone for message envelopes ("utc", "local", "user", or an IANA timezone string).',
"agents.defaults.envelopeTimestamp":
'Include absolute timestamps in message envelopes ("on" or "off").',
"agents.defaults.envelopeElapsed": 'Include elapsed time in message envelopes ("on" or "off").',
"agents.defaults.models": "Configured model catalog (keys are full provider/model IDs).",
"agents.defaults.memorySearch":
"Vector search over MEMORY.md and memory/*.md (per-agent overrides supported).",
"agents.defaults.memorySearch.sources":
'Sources to index for memory search (default: ["memory"]; add "sessions" to include session transcripts).',
"agents.defaults.memorySearch.extraPaths":
"Extra paths to include in memory search (directories or .md files; relative paths resolved from workspace).",
"agents.defaults.memorySearch.experimental.sessionMemory":
"Enable experimental session transcript indexing for memory search (default: false).",
"agents.defaults.memorySearch.provider":
'Embedding provider ("openai", "gemini", "voyage", or "local").',
"agents.defaults.memorySearch.remote.baseUrl":
"Custom base URL for remote embeddings (OpenAI-compatible proxies or Gemini overrides).",
"agents.defaults.memorySearch.remote.apiKey": "Custom API key for the remote embedding provider.",
"agents.defaults.memorySearch.remote.headers":
"Extra headers for remote embeddings (merged; remote overrides OpenAI headers).",
"agents.defaults.memorySearch.remote.batch.enabled":
"Enable batch API for memory embeddings (OpenAI/Gemini/Voyage; default: false).",
"agents.defaults.memorySearch.remote.batch.wait":
"Wait for batch completion when indexing (default: true).",
"agents.defaults.memorySearch.remote.batch.concurrency":
"Max concurrent embedding batch jobs for memory indexing (default: 2).",
"agents.defaults.memorySearch.remote.batch.pollIntervalMs":
"Polling interval in ms for batch status (default: 2000).",
"agents.defaults.memorySearch.remote.batch.timeoutMinutes":
"Timeout in minutes for batch indexing (default: 60).",
"agents.defaults.memorySearch.local.modelPath":
"Local GGUF model path or hf: URI (node-llama-cpp).",
"agents.defaults.memorySearch.fallback":
'Fallback provider when embeddings fail ("openai", "gemini", "local", or "none").',
"agents.defaults.memorySearch.store.path":
"SQLite index path (default: ~/.openclaw/memory/{agentId}.sqlite).",
"agents.defaults.memorySearch.store.vector.enabled":
"Enable sqlite-vec extension for vector search (default: true).",
"agents.defaults.memorySearch.store.vector.extensionPath":
"Optional override path to sqlite-vec extension library (.dylib/.so/.dll).",
"agents.defaults.memorySearch.query.hybrid.enabled":
"Enable hybrid BM25 + vector search for memory (default: true).",
"agents.defaults.memorySearch.query.hybrid.vectorWeight":
"Weight for vector similarity when merging results (0-1).",
"agents.defaults.memorySearch.query.hybrid.textWeight":
"Weight for BM25 text relevance when merging results (0-1).",
"agents.defaults.memorySearch.query.hybrid.candidateMultiplier":
"Multiplier for candidate pool size (default: 4).",
"agents.defaults.memorySearch.cache.enabled":
"Cache chunk embeddings in SQLite to speed up reindexing and frequent updates (default: true).",
memory: "Memory backend configuration (global).",
"memory.backend": 'Memory backend ("builtin" for OpenClaw embeddings, "qmd" for QMD sidecar).',
"memory.citations": 'Default citation behavior ("auto", "on", or "off").',
"memory.qmd.command": "Path to the qmd binary (default: resolves from PATH).",
"memory.qmd.includeDefaultMemory":
"Whether to automatically index MEMORY.md + memory/**/*.md (default: true).",
"memory.qmd.paths":
"Additional directories/files to index with QMD (path + optional glob pattern).",
"memory.qmd.paths.path": "Absolute or ~-relative path to index via QMD.",
"memory.qmd.paths.pattern": "Glob pattern relative to the path root (default: **/*.md).",
"memory.qmd.paths.name":
"Optional stable name for the QMD collection (default derived from path).",
"memory.qmd.sessions.enabled":
"Enable QMD session transcript indexing (experimental, default: false).",
"memory.qmd.sessions.exportDir":
"Override directory for sanitized session exports before indexing.",
"memory.qmd.sessions.retentionDays":
"Retention window for exported sessions before pruning (default: unlimited).",
"memory.qmd.update.interval":
"How often the QMD sidecar refreshes indexes (duration string, default: 5m).",
"memory.qmd.update.debounceMs":
"Minimum delay between successive QMD refresh runs (default: 15000).",
"memory.qmd.update.onBoot": "Run QMD update once on gateway startup (default: true).",
"memory.qmd.update.waitForBootSync":
"Block startup until the boot QMD refresh finishes (default: false).",
"memory.qmd.update.embedInterval":
"How often QMD embeddings are refreshed (duration string, default: 60m). Set to 0 to disable periodic embed.",
"memory.qmd.update.commandTimeoutMs":
"Timeout for QMD maintenance commands like collection list/add (default: 30000).",
"memory.qmd.update.updateTimeoutMs": "Timeout for `qmd update` runs (default: 120000).",
"memory.qmd.update.embedTimeoutMs": "Timeout for `qmd embed` runs (default: 120000).",
"memory.qmd.limits.maxResults": "Max QMD results returned to the agent loop (default: 6).",
"memory.qmd.limits.maxSnippetChars": "Max characters per snippet pulled from QMD (default: 700).",
"memory.qmd.limits.maxInjectedChars": "Max total characters injected from QMD hits per turn.",
"memory.qmd.limits.timeoutMs": "Per-query timeout for QMD searches (default: 4000).",
"memory.qmd.scope":
"Session/channel scope for QMD recall (same syntax as session.sendPolicy; default: direct-only).",
"agents.defaults.memorySearch.cache.maxEntries":
"Optional cap on cached embeddings (best-effort).",
"agents.defaults.memorySearch.sync.onSearch":
"Lazy sync: schedule a reindex on search after changes.",
"agents.defaults.memorySearch.sync.watch": "Watch memory files for changes (chokidar).",
"agents.defaults.memorySearch.sync.sessions.deltaBytes":
"Minimum appended bytes before session transcripts trigger reindex (default: 100000).",
"agents.defaults.memorySearch.sync.sessions.deltaMessages":
"Minimum appended JSONL lines before session transcripts trigger reindex (default: 50).",
"plugins.enabled": "Enable plugin/extension loading (default: true).",
"plugins.allow": "Optional allowlist of plugin ids; when set, only listed plugins load.",
"plugins.deny": "Optional denylist of plugin ids; deny wins over allowlist.",
"plugins.load.paths": "Additional plugin files or directories to load.",
"plugins.slots": "Select which plugins own exclusive slots (memory, etc.).",
"plugins.slots.memory":
'Select the active memory plugin by id, or "none" to disable memory plugins.',
"plugins.entries": "Per-plugin settings keyed by plugin id (enable/disable + config payloads).",
"plugins.entries.*.enabled": "Overrides plugin enable/disable for this entry (restart required).",
"plugins.entries.*.config": "Plugin-defined config payload (schema is provided by the plugin).",
"plugins.installs":
"CLI-managed install metadata (used by `openclaw plugins update` to locate install sources).",
"plugins.installs.*.source": 'Install source ("npm", "archive", or "path").',
"plugins.installs.*.spec": "Original npm spec used for install (if source is npm).",
"plugins.installs.*.sourcePath": "Original archive/path used for install (if any).",
"plugins.installs.*.installPath":
"Resolved install directory (usually ~/.openclaw/extensions/<id>).",
"plugins.installs.*.version": "Version recorded at install time (if available).",
"plugins.installs.*.installedAt": "ISO timestamp of last install/update.",
"agents.list.*.identity.avatar":
"Agent avatar (workspace-relative path, http(s) URL, or data URI).",
"agents.defaults.model.primary": "Primary model (provider/model).",
"agents.defaults.model.fallbacks":
"Ordered fallback models (provider/model). Used when the primary model fails.",
"agents.defaults.imageModel.primary":
"Optional image model (provider/model) used when the primary model lacks image input.",
"agents.defaults.imageModel.fallbacks": "Ordered fallback image models (provider/model).",
"agents.defaults.cliBackends": "Optional CLI backends for text-only fallback (claude-cli, etc.).",
"agents.defaults.humanDelay.mode": 'Delay style for block replies ("off", "natural", "custom").',
"agents.defaults.humanDelay.minMs": "Minimum delay in ms for custom humanDelay (default: 800).",
"agents.defaults.humanDelay.maxMs": "Maximum delay in ms for custom humanDelay (default: 2500).",
"commands.native":
"Register native commands with channels that support it (Discord/Slack/Telegram).",
"commands.nativeSkills":
"Register native skill commands (user-invocable skills) with channels that support it.",
"commands.text": "Allow text command parsing (slash commands only).",
"commands.bash":
"Allow bash chat command (`!`; `/bash` alias) to run host shell commands (default: false; requires tools.elevated).",
"commands.bashForegroundMs":
"How long bash waits before backgrounding (default: 2000; 0 backgrounds immediately).",
"commands.config": "Allow /config chat command to read/write config on disk (default: false).",
"commands.debug": "Allow /debug chat command for runtime-only overrides (default: false).",
"commands.restart": "Allow /restart and gateway restart tool actions (default: false).",
"commands.useAccessGroups": "Enforce access-group allowlists/policies for commands.",
"commands.ownerAllowFrom":
"Explicit owner allowlist for owner-only tools/commands. Use channel-native IDs (optionally prefixed like \"whatsapp:+15551234567\"). '*' is ignored.",
"commands.allowFrom":
'Per-provider allowlist restricting who can use slash commands. If set, overrides the channel\'s allowFrom for command authorization. Use \'*\' key for global default; provider-specific keys (e.g. \'discord\') override the global. Example: { "*": ["user1"], "discord": ["user:123"] }.',
"session.dmScope":
'DM session scoping: "main" keeps continuity; "per-peer", "per-channel-peer", or "per-account-channel-peer" isolates DM history (recommended for shared inboxes/multi-account).',
"session.identityLinks":
"Map canonical identities to provider-prefixed peer IDs for DM session linking (example: telegram:123456).",
"channels.telegram.configWrites":
"Allow Telegram to write config in response to channel events/commands (default: true).",
"channels.slack.configWrites":
"Allow Slack to write config in response to channel events/commands (default: true).",
"channels.mattermost.configWrites":
"Allow Mattermost to write config in response to channel events/commands (default: true).",
"channels.discord.configWrites":
"Allow Discord to write config in response to channel events/commands (default: true).",
"channels.whatsapp.configWrites":
"Allow WhatsApp to write config in response to channel events/commands (default: true).",
"channels.signal.configWrites":
"Allow Signal to write config in response to channel events/commands (default: true).",
"channels.imessage.configWrites":
"Allow iMessage to write config in response to channel events/commands (default: true).",
"channels.msteams.configWrites":
"Allow Microsoft Teams to write config in response to channel events/commands (default: true).",
"channels.discord.commands.native": 'Override native commands for Discord (bool or "auto").',
"channels.discord.commands.nativeSkills":
'Override native skill commands for Discord (bool or "auto").',
"channels.telegram.commands.native": 'Override native commands for Telegram (bool or "auto").',
"channels.telegram.commands.nativeSkills":
'Override native skill commands for Telegram (bool or "auto").',
"channels.slack.commands.native": 'Override native commands for Slack (bool or "auto").',
"channels.slack.commands.nativeSkills":
'Override native skill commands for Slack (bool or "auto").',
"session.agentToAgent.maxPingPongTurns":
"Max reply-back turns between requester and target (05).",
"channels.telegram.customCommands":
"Additional Telegram bot menu commands (merged with native; conflicts ignored).",
"messages.ackReaction": "Emoji reaction used to acknowledge inbound messages (empty disables).",
"messages.ackReactionScope":
'When to send ack reactions ("group-mentions", "group-all", "direct", "all").',
"messages.inbound.debounceMs":
"Debounce window (ms) for batching rapid inbound messages from the same sender (0 to disable).",
"channels.telegram.dmPolicy":
'Direct message access control ("pairing" recommended). "open" requires channels.telegram.allowFrom=["*"].',
"channels.telegram.streamMode":
"Draft streaming mode for Telegram replies (off | partial | block). Separate from block streaming; requires private topics + sendMessageDraft.",
"channels.telegram.draftChunk.minChars":
'Minimum chars before emitting a Telegram draft update when channels.telegram.streamMode="block" (default: 200).',
"channels.telegram.draftChunk.maxChars":
'Target max size for a Telegram draft update chunk when channels.telegram.streamMode="block" (default: 800; clamped to channels.telegram.textChunkLimit).',
"channels.telegram.draftChunk.breakPreference":
"Preferred breakpoints for Telegram draft chunks (paragraph | newline | sentence). Default: paragraph.",
"channels.telegram.retry.attempts":
"Max retry attempts for outbound Telegram API calls (default: 3).",
"channels.telegram.retry.minDelayMs": "Minimum retry delay in ms for Telegram outbound calls.",
"channels.telegram.retry.maxDelayMs":
"Maximum retry delay cap in ms for Telegram outbound calls.",
"channels.telegram.retry.jitter": "Jitter factor (0-1) applied to Telegram retry delays.",
"channels.telegram.network.autoSelectFamily":
"Override Node autoSelectFamily for Telegram (true=enable, false=disable).",
"channels.telegram.timeoutSeconds":
"Max seconds before Telegram API requests are aborted (default: 500 per grammY).",
"channels.whatsapp.dmPolicy":
'Direct message access control ("pairing" recommended). "open" requires channels.whatsapp.allowFrom=["*"].',
"channels.whatsapp.selfChatMode": "Same-phone setup (bot uses your personal WhatsApp number).",
"channels.whatsapp.debounceMs":
"Debounce window (ms) for batching rapid consecutive messages from the same sender (0 to disable).",
"channels.signal.dmPolicy":
'Direct message access control ("pairing" recommended). "open" requires channels.signal.allowFrom=["*"].',
"channels.imessage.dmPolicy":
'Direct message access control ("pairing" recommended). "open" requires channels.imessage.allowFrom=["*"].',
"channels.bluebubbles.dmPolicy":
'Direct message access control ("pairing" recommended). "open" requires channels.bluebubbles.allowFrom=["*"].',
"channels.discord.dm.policy":
'Direct message access control ("pairing" recommended). "open" requires channels.discord.dm.allowFrom=["*"].',
"channels.discord.retry.attempts":
"Max retry attempts for outbound Discord API calls (default: 3).",
"channels.discord.retry.minDelayMs": "Minimum retry delay in ms for Discord outbound calls.",
"channels.discord.retry.maxDelayMs": "Maximum retry delay cap in ms for Discord outbound calls.",
"channels.discord.retry.jitter": "Jitter factor (0-1) applied to Discord retry delays.",
"channels.discord.maxLinesPerMessage": "Soft max line count per Discord message (default: 17).",
"channels.discord.intents.presence":
"Enable the Guild Presences privileged intent. Must also be enabled in the Discord Developer Portal. Allows tracking user activities (e.g. Spotify). Default: false.",
"channels.discord.intents.guildMembers":
"Enable the Guild Members privileged intent. Must also be enabled in the Discord Developer Portal. Default: false.",
"channels.discord.pluralkit.enabled":
"Resolve PluralKit proxied messages and treat system members as distinct senders.",
"channels.discord.pluralkit.token":
"Optional PluralKit token for resolving private systems or members.",
"channels.slack.dm.policy":
'Direct message access control ("pairing" recommended). "open" requires channels.slack.dm.allowFrom=["*"].',
};
const FIELD_PLACEHOLDERS: Record<string, string> = {
"gateway.remote.url": "ws://host:18789",
"gateway.remote.tlsFingerprint": "sha256:ab12cd34…",
"gateway.remote.sshTarget": "user@host",
"gateway.controlUi.basePath": "/openclaw",
"gateway.controlUi.root": "dist/control-ui",
"gateway.controlUi.allowedOrigins": "https://control.example.com",
"channels.mattermost.baseUrl": "https://chat.example.com",
"agents.list[].identity.avatar": "avatars/openclaw.png",
};
const SENSITIVE_PATTERNS = [/token/i, /password/i, /secret/i, /api.?key/i];
function isSensitivePath(path: string): boolean {
return SENSITIVE_PATTERNS.some((pattern) => pattern.test(path));
}
type JsonSchemaObject = JsonSchemaNode & {
type?: string | string[];
properties?: Record<string, JsonSchemaObject>;
@@ -836,40 +88,6 @@ function mergeObjectSchema(base: JsonSchemaObject, extension: JsonSchemaObject):
return merged;
}
function buildBaseHints(): ConfigUiHints {
const hints: ConfigUiHints = {};
for (const [group, label] of Object.entries(GROUP_LABELS)) {
hints[group] = {
label,
group: label,
order: GROUP_ORDER[group],
};
}
for (const [path, label] of Object.entries(FIELD_LABELS)) {
const current = hints[path];
hints[path] = current ? { ...current, label } : { label };
}
for (const [path, help] of Object.entries(FIELD_HELP)) {
const current = hints[path];
hints[path] = current ? { ...current, help } : { help };
}
for (const [path, placeholder] of Object.entries(FIELD_PLACEHOLDERS)) {
const current = hints[path];
hints[path] = current ? { ...current, placeholder } : { placeholder };
}
return hints;
}
function applySensitiveHints(hints: ConfigUiHints): ConfigUiHints {
const next = { ...hints };
for (const key of Object.keys(next)) {
if (isSensitivePath(key)) {
next[key] = { ...next[key], sensitive: true };
}
}
return next;
}
function applyPluginHints(hints: ConfigUiHints, plugins: PluginUiMetadata[]): ConfigUiHints {
const next: ConfigUiHints = { ...hints };
for (const plugin of plugins) {

View File

@@ -2,6 +2,7 @@ import type { GroupPolicy } from "./types.base.js";
import type { DiscordConfig } from "./types.discord.js";
import type { GoogleChatConfig } from "./types.googlechat.js";
import type { IMessageConfig } from "./types.imessage.js";
import type { IrcConfig } from "./types.irc.js";
import type { MSTeamsConfig } from "./types.msteams.js";
import type { SignalConfig } from "./types.signal.js";
import type { SlackConfig } from "./types.slack.js";
@@ -41,6 +42,7 @@ export type ChannelsConfig = {
whatsapp?: WhatsAppConfig;
telegram?: TelegramConfig;
discord?: DiscordConfig;
irc?: IrcConfig;
googlechat?: GoogleChatConfig;
slack?: SlackConfig;
signal?: SignalConfig;

View File

@@ -25,6 +25,7 @@ export type HookMappingConfig = {
| "whatsapp"
| "telegram"
| "discord"
| "irc"
| "googlechat"
| "slack"
| "signal"

106
src/config/types.irc.ts Normal file
View File

@@ -0,0 +1,106 @@
import type {
BlockStreamingCoalesceConfig,
DmPolicy,
GroupPolicy,
MarkdownConfig,
} from "./types.base.js";
import type { ChannelHeartbeatVisibilityConfig } from "./types.channels.js";
import type { DmConfig } from "./types.messages.js";
import type { GroupToolPolicyBySenderConfig, GroupToolPolicyConfig } from "./types.tools.js";
export type IrcAccountConfig = {
/** Optional display name for this account (used in CLI/UI lists). */
name?: string;
/** Optional provider capability tags used for agent/runtime guidance. */
capabilities?: string[];
/** Markdown formatting overrides (tables). */
markdown?: MarkdownConfig;
/** Allow channel-initiated config writes (default: true). */
configWrites?: boolean;
/** If false, do not start this IRC account. Default: true. */
enabled?: boolean;
/** IRC server hostname (example: irc.libera.chat). */
host?: string;
/** IRC server port (default: 6697 with TLS, otherwise 6667). */
port?: number;
/** Use TLS for IRC connection (default: true). */
tls?: boolean;
/** IRC nickname to identify this bot. */
nick?: string;
/** IRC USER field username (defaults to nick). */
username?: string;
/** IRC USER field realname (default: OpenClaw). */
realname?: string;
/** Optional IRC server password (sensitive). */
password?: string;
/** Optional file path containing IRC server password. */
passwordFile?: string;
/** Optional NickServ identify/register settings. */
nickserv?: {
/** Enable NickServ identify/register after connect (default: enabled when password is set). */
enabled?: boolean;
/** NickServ service nick (default: NickServ). */
service?: string;
/** NickServ password (sensitive). */
password?: string;
/** Optional file path containing NickServ password. */
passwordFile?: string;
/** If true, send NickServ REGISTER on connect. */
register?: boolean;
/** Email used with NickServ REGISTER. */
registerEmail?: string;
};
/** Auto-join channel list at connect (example: ["#openclaw"]). */
channels?: string[];
/** Direct message access policy (default: pairing). */
dmPolicy?: DmPolicy;
/** Optional allowlist for inbound DM senders. */
allowFrom?: Array<string | number>;
/** Optional allowlist for IRC channel senders. */
groupAllowFrom?: Array<string | number>;
/**
* Controls how channel messages are handled:
* - "open": channels bypass allowFrom; mention-gating applies
* - "disabled": block all channel messages entirely
* - "allowlist": only allow channel messages from senders in groupAllowFrom/allowFrom
*/
groupPolicy?: GroupPolicy;
/** Max channel messages to keep as history context (0 disables). */
historyLimit?: number;
/** Max DM turns to keep as history context. */
dmHistoryLimit?: number;
/** Per-DM config overrides keyed by sender ID. */
dms?: Record<string, DmConfig>;
/** Outbound text chunk size (chars). Default: 350. */
textChunkLimit?: number;
/** Chunking mode: "length" (default) splits by size; "newline" splits on every newline. */
chunkMode?: "length" | "newline";
blockStreaming?: boolean;
/** Merge streamed block replies before sending. */
blockStreamingCoalesce?: BlockStreamingCoalesceConfig;
groups?: Record<
string,
{
requireMention?: boolean;
tools?: GroupToolPolicyConfig;
toolsBySender?: GroupToolPolicyBySenderConfig;
allowFrom?: Array<string | number>;
skills?: string[];
enabled?: boolean;
systemPrompt?: string;
}
>;
/** Optional mention patterns specific to IRC channel messages. */
mentionPatterns?: string[];
/** Heartbeat visibility settings for this channel. */
heartbeat?: ChannelHeartbeatVisibilityConfig;
/** Outbound response prefix override for this channel/account. */
responsePrefix?: string;
/** Max outbound media size in MB. */
mediaMaxMb?: number;
};
export type IrcConfig = {
/** Optional per-account IRC configuration (multi-account). */
accounts?: Record<string, IrcAccountConfig>;
} & IrcAccountConfig;

View File

@@ -12,6 +12,7 @@ export type QueueModeByProvider = {
whatsapp?: QueueMode;
telegram?: QueueMode;
discord?: QueueMode;
irc?: QueueMode;
googlechat?: QueueMode;
slack?: QueueMode;
signal?: QueueMode;

View File

@@ -14,6 +14,7 @@ export * from "./types.googlechat.js";
export * from "./types.gateway.js";
export * from "./types.hooks.js";
export * from "./types.imessage.js";
export * from "./types.irc.js";
export * from "./types.messages.js";
export * from "./types.models.js";
export * from "./types.node-host.js";

View File

@@ -309,6 +309,7 @@ export const QueueModeBySurfaceSchema = z
whatsapp: QueueModeSchema.optional(),
telegram: QueueModeSchema.optional(),
discord: QueueModeSchema.optional(),
irc: QueueModeSchema.optional(),
slack: QueueModeSchema.optional(),
mattermost: QueueModeSchema.optional(),
signal: QueueModeSchema.optional(),

View File

@@ -23,6 +23,7 @@ export const HookMappingSchema = z
z.literal("whatsapp"),
z.literal("telegram"),
z.literal("discord"),
z.literal("irc"),
z.literal("slack"),
z.literal("signal"),
z.literal("imessage"),

View File

@@ -622,6 +622,101 @@ export const SignalConfigSchema = SignalAccountSchemaBase.extend({
});
});
export const IrcGroupSchema = z
.object({
requireMention: z.boolean().optional(),
tools: ToolPolicySchema,
toolsBySender: ToolPolicyBySenderSchema,
skills: z.array(z.string()).optional(),
enabled: z.boolean().optional(),
allowFrom: z.array(z.union([z.string(), z.number()])).optional(),
systemPrompt: z.string().optional(),
})
.strict();
export const IrcNickServSchema = z
.object({
enabled: z.boolean().optional(),
service: z.string().optional(),
password: z.string().optional(),
passwordFile: z.string().optional(),
register: z.boolean().optional(),
registerEmail: z.string().optional(),
})
.strict();
export const IrcAccountSchemaBase = z
.object({
name: z.string().optional(),
capabilities: z.array(z.string()).optional(),
markdown: MarkdownConfigSchema,
enabled: z.boolean().optional(),
configWrites: z.boolean().optional(),
host: z.string().optional(),
port: z.number().int().min(1).max(65535).optional(),
tls: z.boolean().optional(),
nick: z.string().optional(),
username: z.string().optional(),
realname: z.string().optional(),
password: z.string().optional(),
passwordFile: z.string().optional(),
nickserv: IrcNickServSchema.optional(),
channels: z.array(z.string()).optional(),
dmPolicy: DmPolicySchema.optional().default("pairing"),
allowFrom: z.array(z.union([z.string(), z.number()])).optional(),
groupAllowFrom: z.array(z.union([z.string(), z.number()])).optional(),
groupPolicy: GroupPolicySchema.optional().default("allowlist"),
groups: z.record(z.string(), IrcGroupSchema.optional()).optional(),
mentionPatterns: z.array(z.string()).optional(),
historyLimit: z.number().int().min(0).optional(),
dmHistoryLimit: z.number().int().min(0).optional(),
dms: z.record(z.string(), DmConfigSchema.optional()).optional(),
textChunkLimit: z.number().int().positive().optional(),
chunkMode: z.enum(["length", "newline"]).optional(),
blockStreaming: z.boolean().optional(),
blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(),
mediaMaxMb: z.number().positive().optional(),
heartbeat: ChannelHeartbeatVisibilitySchema,
responsePrefix: z.string().optional(),
})
.strict();
export const IrcAccountSchema = IrcAccountSchemaBase.superRefine((value, ctx) => {
requireOpenAllowFrom({
policy: value.dmPolicy,
allowFrom: value.allowFrom,
ctx,
path: ["allowFrom"],
message: 'channels.irc.dmPolicy="open" requires channels.irc.allowFrom to include "*"',
});
if (value.nickserv?.register && !value.nickserv.registerEmail?.trim()) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["nickserv", "registerEmail"],
message: "channels.irc.nickserv.register=true requires channels.irc.nickserv.registerEmail",
});
}
});
export const IrcConfigSchema = IrcAccountSchemaBase.extend({
accounts: z.record(z.string(), IrcAccountSchema.optional()).optional(),
}).superRefine((value, ctx) => {
requireOpenAllowFrom({
policy: value.dmPolicy,
allowFrom: value.allowFrom,
ctx,
path: ["allowFrom"],
message: 'channels.irc.dmPolicy="open" requires channels.irc.allowFrom to include "*"',
});
if (value.nickserv?.register && !value.nickserv.registerEmail?.trim()) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["nickserv", "registerEmail"],
message: "channels.irc.nickserv.register=true requires channels.irc.nickserv.registerEmail",
});
}
});
export const IMessageAccountSchemaBase = z
.object({
name: z.string().optional(),

View File

@@ -6,6 +6,7 @@ import {
DiscordConfigSchema,
GoogleChatConfigSchema,
IMessageConfigSchema,
IrcConfigSchema,
MSTeamsConfigSchema,
SignalConfigSchema,
SlackConfigSchema,
@@ -29,6 +30,7 @@ export const ChannelsSchema = z
whatsapp: WhatsAppConfigSchema.optional(),
telegram: TelegramConfigSchema.optional(),
discord: DiscordConfigSchema.optional(),
irc: IrcConfigSchema.optional(),
googlechat: GoogleChatConfigSchema.optional(),
slack: SlackConfigSchema.optional(),
signal: SignalConfigSchema.optional(),