openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
2,770 lines • 113 kB
JavaScript
import { D as normalizeOptionalString, E as normalizeOptionalLowercaseString, S as normalizeChannelConfigEntries, T as normalizeLowercaseStringOrEmpty, a as defineChannelAliasMigration, b as hasLegacyAccountStreamingAliases, d as normalizeAccountId, j as isRecord, k as asNullableRecord, n as defineKeyMoveMigration, r as defineStrayPluginEntryConfigMigration, v as isBlockedObjectKey, w as normalizeUniqueStringEntries } from "./runtime-doctor-migrations-BhouWAUC.js";
import { t as createLegacyWebhookListenerDoctorContract } from "./legacy-webhook-listener-migration-Bu-ybctG.js";
import { a as ENV_SECRET_REF_ID_RE, o as SECRET_PROVIDER_ALIAS_PATTERN, t as hasConfiguredSecretInput } from "./types.secrets-pt3cVWr4.js";
import { o as createAccountListHelpers, s as hasConfiguredAccountValue } from "./account-core-bZmYRKLc.js";
import "./account-resolution-1w7yhOTy.js";
import { _ as toJSONSchema, c as number, d as record, f as strictObject, g as unknown, h as union, i as boolean, l as object, m as tuple, n as _null, p as string, r as array, s as literal, t as _enum, u as preprocess } from "./schemas-C5K39dqU.js";
import { n as sensitive, r as SecretInputSchema, t as buildSecretInputSchema } from "./secret-input-DnlqG81c.js";
import "./string-coerce-runtime-CM4JCyJk.js";
import "./ansi-twpzQrRE.js";
import path from "node:path";
import net from "node:net";
import { domainToASCII } from "node:url";
import "typebox/guard";
import "typebox/schema";
import "typebox/format";
//#region node_modules/.pnpm/zod@4.6.5/node_modules/zod/v4/classic/compat.js
/** @deprecated Use the raw string literal codes instead, e.g. "invalid_type". */
const ZodIssueCode = {
invalid_type: "invalid_type",
too_big: "too_big",
too_small: "too_small",
invalid_format: "invalid_format",
not_multiple_of: "not_multiple_of",
unrecognized_keys: "unrecognized_keys",
invalid_union: "invalid_union",
invalid_key: "invalid_key",
invalid_element: "invalid_element",
invalid_value: "invalid_value",
custom: "custom"
};
/** @deprecated Do not use. Stub definition, only included for zod-to-json-schema compatibility. */
var ZodFirstPartyTypeKind;
ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
//#endregion
//#region extensions/feishu/src/accounts.ts
const { listAccountIds: listFeishuAccountIds, resolveDefaultAccountId: resolveDefaultFeishuAccountId, resolveAccountConfig: resolveMergedFeishuAccountConfig } = createAccountListHelpers("feishu", {
allowUnlistedDefaultAccount: true,
omitKeys: ["defaultAccount"],
nestedObjectKeys: ["tools"],
hasImplicitDefaultAccount: (cfg) => {
const feishu = cfg.channels?.feishu;
return hasConfiguredAccountValue(feishu?.appId) && hasConfiguredAccountValue(feishu?.appSecret);
}
});
function mergeFeishuAccountConfig(cfg, accountId) {
const feishuCfg = cfg.channels?.feishu;
const merged = resolveMergedFeishuAccountConfig(cfg, accountId);
const topTools = feishuCfg?.tools;
if (merged.tools === void 0 && topTools !== void 0) return {
...merged,
tools: topTools
};
if (topTools?.bitable === false) return {
...merged,
tools: {
...merged.tools,
bitable: false
}
};
return merged;
}
//#endregion
//#region src/agents/sandbox/network-mode.ts
/**
* Docker network mode safety helpers.
*
* Flags host networking and container namespace joins because they bypass normal sandbox network isolation.
*/
/** Normalizes optional Docker network mode strings for policy checks. */
function normalizeNetworkMode(network) {
return normalizeOptionalLowercaseString(network) || void 0;
}
/** Returns the concrete block reason for dangerous network modes, if blocked. */
function getBlockedNetworkModeReason(params) {
const normalized = normalizeNetworkMode(params.network);
if (!normalized) return null;
if (normalized === "host") return "host";
if (normalized.startsWith("container:") && params.allowContainerNamespaceJoin !== true) return "container_namespace_join";
return null;
}
//#endregion
//#region src/infra/format-time/duration-units.ts
const durationUnitMs = {
year: 31536e6,
week: 6048e5,
day: 864e5,
hour: 36e5,
minute: 6e4,
second: 1e3,
millisecond: 1
};
//#endregion
//#region src/cli/parse-duration.ts
const DURATION_UNIT_MS = /* @__PURE__ */ new Map([
["ms", durationUnitMs.millisecond],
["s", durationUnitMs.second],
["m", durationUnitMs.minute],
["h", durationUnitMs.hour],
["d", durationUnitMs.day]
]);
function invalidDuration(raw, reason) {
const value = raw.trim() ? `"${raw}"` : "empty value";
const prefix = reason ? `Invalid duration (${reason}): ${value}.` : `Invalid duration: ${value}.`;
return /* @__PURE__ */ new Error(`${prefix} Use values like 500ms, 30s, 5m, 2h, or 1h30m.`);
}
function parseDurationToken(raw, value, unit) {
const multiplier = DURATION_UNIT_MS.get(unit);
if (multiplier === void 0 || value.length + unit.length > 100) throw invalidDuration(raw);
const parsed = Number(value) * multiplier;
if (!Number.isFinite(parsed) || parsed < 0) throw invalidDuration(raw);
return parsed;
}
function roundSafeDurationMs(raw, value) {
const ms = Math.round(value);
if (!Number.isSafeInteger(ms)) throw invalidDuration(raw);
return ms;
}
/** Parse a non-negative duration into milliseconds, supporting single and composite units. */
function parseDurationMs(raw, opts) {
const trimmed = normalizeLowercaseStringOrEmpty(raw);
if (!trimmed) throw invalidDuration(raw, "empty");
const single = /^(\d+(?:\.\d+)?)(ms|s|m|h|d)?$/.exec(trimmed);
if (single) return roundSafeDurationMs(raw, parseDurationToken(raw, single[1] ?? "", single[2] ?? opts?.defaultUnit ?? "ms"));
let totalMs = 0;
let consumed = 0;
for (const match of trimmed.matchAll(/(\d+(?:\.\d+)?)(ms|s|m|h|d)/g)) {
const [full, valueRaw, unitRaw] = match;
const index = match.index ?? -1;
if (!full || !valueRaw || !unitRaw || index < 0) throw invalidDuration(raw);
if (index !== consumed) throw invalidDuration(raw, "each composite segment needs a unit");
totalMs += parseDurationToken(raw, valueRaw, unitRaw);
consumed += full.length;
}
if (consumed !== trimmed.length || consumed === 0) throw invalidDuration(raw);
return roundSafeDurationMs(raw, totalMs);
}
//#endregion
//#region src/infra/exec-approvals-core.ts
function resolveExecModeFromPolicy(params) {
if (params.security === "deny") return "deny";
if (params.security === "allowlist" && params.ask === "off") return "allowlist";
if (params.security === "full" && params.ask !== "always") return "full";
return "ask";
}
function resolveExactExecModeFromPolicy(params) {
if (params.ask === "always" || params.security === "full" && params.ask === "on-miss") return null;
return resolveExecModeFromPolicy(params);
}
//#endregion
//#region src/config/github-identity-profile-id.ts
const MANAGED_GITHUB_PROFILE_ID_PATTERN = /^ghp_[a-f0-9]{32}$/u;
//#endregion
//#region src/config/web-search-legacy-provider-keys.ts
/** Legacy config keys that used to live under web search provider config. */
const LEGACY_WEB_SEARCH_PROVIDER_CONFIG_KEYS = /* @__PURE__ */ new Set([
"brave",
"duckduckgo",
"exa",
"firecrawl",
"gemini",
"grok",
"kimi",
"minimax",
"ollama",
"perplexity",
"searxng",
"tavily"
]);
//#endregion
//#region packages/model-catalog-core/src/provider-id.ts
function normalizeProviderId(provider) {
return normalizeLowercaseStringOrEmpty(provider);
}
//#endregion
//#region packages/model-catalog-core/src/model-catalog-refs.ts
/** Parse a strict provider/model reference without normalizing either segment. */
function parseProviderModelRef(value) {
const trimmed = value.trim();
const slashIndex = trimmed.indexOf("/");
if (slashIndex <= 0 || slashIndex >= trimmed.length - 1) return null;
const provider = trimmed.slice(0, slashIndex).trim();
const model = trimmed.slice(slashIndex + 1).trim();
return provider && model ? {
provider,
model
} : null;
}
//#endregion
//#region src/auto-reply/thinking.shared.ts
/** Canonical thinking level values accepted by chat commands and session state. */
const ALL_THINKING_LEVELS = [
"off",
"minimal",
"low",
"medium",
"high",
"xhigh",
"adaptive",
"max",
"ultra"
];
ALL_THINKING_LEVELS.join("|");
//#endregion
//#region src/config/zod-schema.agent-model.ts
/** Decision providers require an explicit model; an empty value disables the role. */
const DecisionModelSchema = string().trim().max(512).refine((value) => value === "" || parseProviderModelRef(value) !== null, "Expected provider/model, or an empty string to disable decision models.");
/** Schema for agent model config accepting a string or fallback object. */
const AgentModelSchema = union([string(), strictObject({
/** Primary model (provider/model). */
primary: string().optional(),
/** Per-agent model fallbacks (provider/model). */
fallbacks: array(string()).optional()
})]);
union([string(), strictObject({
primary: string().optional(),
/** Per-tool model fallbacks (provider/model). */
fallbacks: array(string()).optional(),
/** Optional provider request timeout in milliseconds for capabilities that support it. */
timeoutMs: number().int().positive().optional()
})]);
//#endregion
//#region src/config/zod-schema.agent-entry-base.ts
const AgentRuntimePolicySchema = strictObject({ id: string().optional() }).optional();
const AgentModelRuntimeEntrySchema = strictObject({
/** Optional display/lookup alias for this provider/model entry. */
alias: string().optional(),
/** Provider-specific API parameters (e.g., GLM-4.7 thinking mode). */
params: record(string(), unknown()).optional(),
/** Optional agent execution runtime for this specific provider/model entry. */
agentRuntime: AgentRuntimePolicySchema,
/** Additional explicit runtime choices in the model picker; does not change the default. */
pickerRuntimes: array(string().trim().min(1).max(128).regex(/^[a-z][a-z0-9-]*$/).refine((id) => id !== "auto" && id !== "default")).max(8).optional(),
/** OpenClaw Code Mode override; omitted inherits the enclosing activation policy. */
codeMode: boolean().optional(),
/** Enable streaming for this model (default: true, false for Ollama to avoid SDK issue #1205). */
streaming: boolean().optional()
});
const AgentModelMapSchema = record(string(), AgentModelRuntimeEntrySchema).superRefine((models, ctx) => {
for (const [ref, entry] of Object.entries(models)) {
if (entry.pickerRuntimes !== void 0 && (ref.includes("*") || !parseProviderModelRef(ref))) ctx.addIssue({
code: ZodIssueCode.custom,
path: [ref, "pickerRuntimes"],
message: "Picker runtimes require an exact provider/model entry."
});
if (entry.codeMode !== void 0 && (ref.includes("*") || !parseProviderModelRef(ref))) ctx.addIssue({
code: ZodIssueCode.custom,
path: [ref, "codeMode"],
message: "Code Mode requires an exact provider/model entry; wildcard and bare model keys are not supported."
});
}
});
const AgentModelPolicySchema = strictObject({
/** Model refs allowed for session/run overrides. Empty or omitted allows any model. */
allow: array(string()).optional() });
const AgentRuntimeAcpSchema = strictObject({
/** ACP harness adapter id (for example codex, claude). */
agent: string().optional(),
/** Optional ACP backend override for this agent runtime. */
backend: string().optional(),
/** Optional ACP session mode override. */
mode: _enum(["persistent", "oneshot"]).optional(),
/** Optional runtime working directory override. */
cwd: string().optional()
}).optional();
const AgentRuntimeSchema = union([strictObject({ type: literal("embedded") }), strictObject({
type: literal("acp"),
acp: AgentRuntimeAcpSchema
})]).optional();
const AgentEntryEmbeddedAgentConfigSchema = strictObject({ executionContract: union([literal("default"), literal("strict-agentic")]).optional() }).optional();
const AgentEntryBaseSchema = strictObject({
id: string(),
name: string().optional(),
description: string().optional(),
workspace: string().optional(),
cwd: string().optional(),
agentDir: string().optional(),
model: AgentModelSchema.optional(),
utilityModel: string().optional(),
decisionModel: DecisionModelSchema.optional(),
models: AgentModelMapSchema.optional(),
modelPolicy: AgentModelPolicySchema.optional(),
thinkingDefault: _enum(ALL_THINKING_LEVELS).optional(),
verboseDefault: _enum([
"off",
"on",
"full"
]).optional(),
toolProgressDetail: _enum(["explain", "raw"]).optional(),
reasoningDefault: _enum([
"on",
"off",
"stream"
]).optional(),
fastModeDefault: union([
boolean(),
literal("auto"),
literal("ultrafast")
]).optional(),
contextInjection: union([
literal("always"),
literal("continuation-skip"),
literal("never")
]).optional(),
bootstrapMaxChars: number().int().positive().optional(),
bootstrapTotalMaxChars: number().int().positive().optional(),
experimental: strictObject({ localModelLean: boolean().optional() }).optional(),
skills: array(string()).optional(),
subagents: strictObject({
delegationMode: _enum(["suggest", "prefer"]).optional(),
allowAgents: array(string()).optional(),
model: AgentModelSchema.optional(),
thinking: string().optional(),
requireAgentId: boolean().optional()
}).optional(),
embeddedAgent: AgentEntryEmbeddedAgentConfigSchema,
params: record(string(), unknown()).optional(),
runtime: AgentRuntimeSchema
});
//#endregion
//#region src/infra/exec-safety.ts
const SHELL_METACHARS = /[;&|`$<>]/;
const CONTROL_CHARS = /[\r\n]/;
const QUOTE_CHARS = /["']/;
const BARE_NAME_PATTERN = /^[A-Za-z0-9._+-]+$/;
function isLikelyPath(value) {
return value.startsWith(".") || value.startsWith("~") || value.includes("/") || value.includes("\\");
}
/** Validates that a configured executable value cannot smuggle shell syntax. */
function isSafeExecutableValue(value) {
if (!value) return false;
const trimmed = value.trim();
if (!trimmed || trimmed.includes("\0") || CONTROL_CHARS.test(trimmed) || SHELL_METACHARS.test(trimmed) || QUOTE_CHARS.test(trimmed)) return false;
if (isLikelyPath(trimmed)) return true;
if (trimmed.startsWith("-")) return false;
return BARE_NAME_PATTERN.test(trimmed);
}
//#endregion
//#region src/secrets/exact-hostname.ts
/**
* Canonical exact-host contract shared by per-secret destination bindings and the
* egress-proxy config allowlists: lowercase ASCII/punycode, unbracketed IP literals,
* no wildcard, scheme, path, or port. Throws with an operator-actionable message so
* config validation and the secret store surface the same policy.
*/
function normalizeExactAllowedHost(raw) {
const trimmed = raw.trim().toLowerCase().replace(/\.+$/u, "");
if (trimmed.includes("*")) throw new Error(`Allowed host "${raw}" cannot contain a wildcard; use one exact hostname.`);
const unbracketed = trimmed.startsWith("[") && trimmed.endsWith("]") ? trimmed.slice(1, -1) : trimmed;
if (net.isIP(unbracketed)) return unbracketed;
if (!unbracketed || unbracketed.includes(":") || /[\s/?#@]/u.test(unbracketed)) throw new Error(`Allowed host "${raw}" must be a hostname without a scheme, path, wildcard, or port.`);
const ascii = domainToASCII(unbracketed);
if (!ascii || ascii.length > 253 || ascii.split(".").some((label) => !label || label.length > 63 || label.startsWith("-") || label.endsWith("-") || !/^[a-z0-9-]+$/u.test(label))) throw new Error(`Allowed host "${raw}" is not a valid hostname.`);
return ascii;
}
//#endregion
//#region packages/llm-core/src/model-data.ts
const MODEL_DATA_APIS = [
"openai-completions",
"openai-responses",
"openai-chatgpt-responses",
"anthropic-messages",
"google-generative-ai",
"google-interactions",
"google-vertex",
"github-copilot",
"bedrock-converse-stream",
"ollama",
"pi-messages",
"azure-openai-responses"
];
const MODEL_DATA_THINKING_FORMATS = [
"openai",
"openrouter",
"deepseek",
"together",
"qwen",
"qwen-chat-template",
"zai"
];
//#endregion
//#region src/config/model-config-vocabulary.ts
/** Provider API adapter ids accepted by model/provider config and schema generation. */
const MODEL_APIS = [...MODEL_DATA_APIS];
/** Thinking/reasoning payload dialects emitted by OpenAI-compatible providers. */
const MODEL_THINKING_FORMATS = [...MODEL_DATA_THINKING_FORMATS];
//#endregion
//#region src/config/model-provider-overlay-ids.ts
const BUILT_IN_MODEL_PROVIDER_OVERLAY_IDS = /* @__PURE__ */ new Set([
"amazon-bedrock",
"amazon-bedrock-mantle",
"anthropic",
"anthropic-vertex",
"arcee",
"azure-openai-responses",
"byteplus",
"byteplus-plan",
"cerebras",
"chutes",
"claude-cli",
"clawrouter",
"cloudflare-ai-gateway",
"codex",
"comfy",
"copilot-proxy",
"dashscope",
"deepinfra",
"deepseek",
"fal",
"fireworks",
"github-copilot",
"gmi",
"gmi-cloud",
"gmicloud",
"google",
"google-antigravity",
"google-gemini-cli",
"google-vertex",
"groq",
"huggingface",
"kie",
"kilocode",
"kimi",
"kimi-coding",
"litellm",
"lmstudio",
"meta",
"microsoft-foundry",
"minimax",
"minimax-portal",
"mistral",
"modelstudio",
"moonshot",
"moonshot-ai",
"moonshotai",
"nvidia",
"novita",
"novita-ai",
"novitaai",
"ollama",
"ollama-cloud",
"openai",
"opencode",
"opencode-go",
"openrouter",
"qianfan",
"qwen",
"qwen-token-plan",
"qwencloud",
"runway",
"sglang",
"stepfun",
"stepfun-plan",
"synthetic",
"tencent-tokenhub",
"tencent-tokenplan",
"together",
"venice",
"vercel-ai-gateway",
"vllm",
"volcengine",
"volcengine-plan",
"vydra",
"x-ai",
"xai",
"xiaomi",
"xiaomi-token-plan",
"z.ai",
"z-ai",
"zai"
]);
/** Identifies provider overlays already known to the bundled config contract. */
function isBuiltInModelProviderOverlayId(providerId) {
return BUILT_IN_MODEL_PROVIDER_OVERLAY_IDS.has(normalizeProviderId(providerId));
}
//#endregion
//#region src/config/zod-schema.allowdeny.ts
const AllowDenyActionSchema = union([literal("allow"), literal("deny")]);
const AllowDenyChatTypeSchema = union([
literal("direct"),
literal("group"),
literal("channel")
]).optional();
function createAllowDenyChannelRulesSchema() {
return strictObject({
default: AllowDenyActionSchema.optional(),
rules: array(strictObject({
action: AllowDenyActionSchema,
match: strictObject({
channel: string().optional(),
chatType: AllowDenyChatTypeSchema,
keyPrefix: string().optional(),
rawKeyPrefix: string().optional()
}).optional()
})).optional()
}).optional();
}
//#endregion
//#region src/config/zod-schema.messages.ts
const VisibleRepliesValueSchema = _enum(["automatic", "message_tool"]);
const VisibleRepliesSchema = union([VisibleRepliesValueSchema, boolean()]).overwrite((value) => {
if (value === true) return "automatic";
if (value === false) return "message_tool";
return value;
});
const MentionPatternsPolicySchema = strictObject({
mode: union([literal("allow"), literal("deny")]).optional(),
allowIn: array(string()).optional(),
denyIn: array(string()).optional()
});
const GroupChatSchema = strictObject({
mentionPatterns: array(string()).optional(),
historyLimit: number().int().min(0).optional(),
unmentionedInbound: _enum(["user_request", "room_event"]).optional(),
visibleReplies: VisibleRepliesSchema.optional()
}).optional();
const DmConfigSchema$1 = strictObject({ historyLimit: number().int().min(0).optional() });
const QueueModeSchema = union([
literal("steer"),
literal("followup"),
literal("collect"),
literal("interrupt")
]);
const QueueDropSchema = union([
literal("old"),
literal("new"),
literal("summarize")
]);
const QueueModeBySurfaceSchema = strictObject({
whatsapp: QueueModeSchema.optional(),
telegram: QueueModeSchema.optional(),
discord: QueueModeSchema.optional(),
irc: QueueModeSchema.optional(),
googlechat: QueueModeSchema.optional(),
slack: QueueModeSchema.optional(),
mattermost: QueueModeSchema.optional(),
signal: QueueModeSchema.optional(),
imessage: QueueModeSchema.optional(),
msteams: QueueModeSchema.optional(),
webchat: QueueModeSchema.optional(),
matrix: QueueModeSchema.optional()
}).optional();
const DebounceMsBySurfaceSchema = record(string(), number().int().nonnegative()).optional();
const QueueSchema = strictObject({
mode: QueueModeSchema.optional(),
byChannel: QueueModeBySurfaceSchema,
debounceMsByChannel: DebounceMsBySurfaceSchema,
cap: number().int().positive().optional(),
drop: QueueDropSchema.optional()
}).optional();
const InboundDebounceSchema = strictObject({
debounceMs: number().int().nonnegative().optional(),
byChannel: DebounceMsBySurfaceSchema
}).optional();
const NativeCommandsSettingSchema = union([boolean(), literal("auto")]);
strictObject({
native: NativeCommandsSettingSchema.optional(),
nativeSkills: NativeCommandsSettingSchema.optional()
}).optional();
const ResponseUsageModeSchema = _enum([
"on",
"off",
"tokens",
"full"
]);
strictObject({
visibleReplies: VisibleRepliesSchema.optional(),
responsePrefix: string().optional(),
usageTemplate: union([string(), record(string(), unknown())]).optional(),
responseUsage: union([ResponseUsageModeSchema, record(string(), ResponseUsageModeSchema)]).optional(),
groupChat: GroupChatSchema,
queue: QueueSchema,
inbound: InboundDebounceSchema,
ackReaction: string().optional(),
ackReactionScope: _enum([
"group-mentions",
"group-all",
"direct",
"all",
"off",
"none"
]).optional(),
statusReactions: strictObject({ enabled: boolean().optional() }).optional()
}).optional();
const BroadcastStrategySchema = _enum(["parallel", "sequential"]);
const BroadcastGroupSchema = strictObject({
agents: array(string()).max(16),
mentionGating: boolean().optional(),
maxRounds: number().int().min(1).max(4).optional(),
maxTurns: number().int().min(1).max(32).optional()
});
object({ strategy: BroadcastStrategySchema.optional() }).catchall(union([array(string()), BroadcastGroupSchema])).superRefine((broadcast, ctx) => {
for (const [peerId, entry] of Object.entries(broadcast)) {
if (typeof entry !== "object") continue;
const qualified = /^[a-z][a-z0-9_-]*:.+$/i.test(peerId);
if (!Array.isArray(entry) && !qualified) ctx.addIssue({
code: ZodIssueCode.custom,
path: [peerId],
message: "Group options require a channel-qualified key (<channel>:<peerId>)."
});
else if (Array.isArray(entry) && qualified && entry.length > 16) ctx.addIssue({
code: ZodIssueCode.custom,
path: [peerId],
message: "Agent group threads support at most 16 agents."
});
}
}).optional();
//#endregion
//#region src/config/zod-schema.model-compat.ts
const RoutingPercentileCutoffsSchema = strictObject({
p50: number().optional(),
p75: number().optional(),
p90: number().optional(),
p99: number().optional()
});
const OpenRouterRoutingSchema = strictObject({
allow_fallbacks: boolean().optional(),
require_parameters: boolean().optional(),
data_collection: _enum(["deny", "allow"]).optional(),
zdr: boolean().optional(),
enforce_distillable_text: boolean().optional(),
order: array(string()).optional(),
only: array(string()).optional(),
ignore: array(string()).optional(),
quantizations: array(string()).optional(),
sort: union([string(), strictObject({
by: string().optional(),
partition: string().nullable().optional()
})]).optional(),
max_price: strictObject({
prompt: union([number(), string()]).optional(),
completion: union([number(), string()]).optional(),
image: union([number(), string()]).optional(),
audio: union([number(), string()]).optional(),
request: union([number(), string()]).optional()
}).optional(),
preferred_min_throughput: union([number(), RoutingPercentileCutoffsSchema]).optional(),
preferred_max_latency: union([number(), RoutingPercentileCutoffsSchema]).optional()
});
const VercelGatewayRoutingSchema = strictObject({
only: array(string()).optional(),
order: array(string()).optional()
});
/** Provider/model compatibility switches consumed by request builders and tool schema adapters. */
const ModelCompatSchema = strictObject({
/** Whether the provider supports the `store` field. Default: auto-detected from URL. */
supportsStore: boolean().optional(),
/** Whether provider accepts prompt-cache/session affinity keys. */
supportsPromptCacheKey: boolean().optional(),
/** Opts this model into stored HTTP continuation on a verified compatible endpoint. */
supportsResponsesContinuation: boolean().optional(),
/** Whether the provider supports the `developer` role (vs `system`). Default: auto-detected from URL. */
supportsDeveloperRole: boolean().optional(),
/** Whether the provider supports `reasoning_effort`. Default: auto-detected from URL. */
supportsReasoningEffort: boolean().optional(),
/** Whether the model accepts the `temperature` parameter. Default: true. */
supportsTemperature: boolean().optional(),
/**
* Whether the provider honors top-level `instructions`. Defaults to true only for verified
* native routes (OpenAI, xAI); every other route defaults to false and embeds the system
* prompt in `input` unless set true here after verifying against that endpoint.
*/
supportsInstructions: boolean().optional(),
/**
* Whether the provider supports `stream_options: { include_usage: true }` for token usage in
* streaming responses. Default: true.
*/
supportsUsageInStreaming: boolean().optional(),
/** Whether this model supports tool/function calling. */
supportsTools: boolean().optional(),
/** Code-mode tier consumed by `tools.codeMode.enabled: "auto"`; absent means "capable". */
codeMode: _enum(["preferred", "capable"]).optional(),
/** Whether the provider supports the `strict` field in tool definitions. Default: true. */
supportsStrictMode: boolean().optional(),
/**
* Whether the provider supports JSON Schema through `response_format`. Default: false for
* unknown compatible endpoints.
*/
supportsJsonSchemaResponseFormat: boolean().optional(),
/** Whether all message parts must be coerced to plain strings. */
requiresStringContent: boolean().optional(),
/** Whether unknown message payload keys must be stripped before requests. */
strictMessageKeys: boolean().optional(),
/** Reasoning detail block types safe to expose in visible transcripts. */
visibleReasoningDetailTypes: array(string().min(1)).optional(),
/** Provider-accepted reasoning effort labels. */
supportedReasoningEfforts: array(string().min(1)).optional(),
/** Per-level reasoning effort overrides, e.g. map "off" to "low" for models that cannot disable thinking. */
reasoningEffortMap: record(string().min(1), string().min(1)).optional(),
/** Which field to use for max tokens. Default: auto-detected from URL. */
maxTokensField: union([literal("max_completion_tokens"), literal("max_tokens")]).optional(),
/** Reasoning/thinking payload dialect for provider-compatible APIs. */
thinkingFormat: _enum(MODEL_THINKING_FORMATS).optional(),
/** Whether tool results require the `name` field. Default: auto-detected from URL. */
requiresToolResultName: boolean().optional(),
/**
* Whether a user message after tool results requires an assistant message in between. Default:
* auto-detected from URL.
*/
requiresAssistantAfterToolResult: boolean().optional(),
/**
* Whether thinking blocks must be converted to text blocks with <thinking> delimiters.
* Default: auto-detected from URL.
*/
requiresThinkingAsText: boolean().optional(),
/**
* Whether all replayed assistant messages must include an empty reasoning_content field when
* reasoning is enabled. Default: auto-detected from URL.
*/
requiresReasoningContentOnAssistantMessages: boolean().optional(),
/** Named tool-schema profile used by provider adapters. */
toolSchemaProfile: string().optional(),
/** JSON Schema keywords rejected by this provider's tool schema validator. */
unsupportedToolSchemaKeywords: array(string().min(1)).optional(),
/** Encoding expected for tool-call arguments in provider payloads. */
toolCallArgumentsEncoding: string().optional(),
/** Whether OpenAI-style calls must be reshaped to Anthropic-compatible tool payloads. */
requiresOpenAiAnthropicToolPayload: boolean().optional(),
/** OpenRouter-specific routing preferences. Only used when baseUrl points to OpenRouter. */
openRouterRouting: OpenRouterRoutingSchema.optional(),
/** Vercel AI Gateway routing preferences. Only used when baseUrl points to Vercel AI Gateway. */
vercelGatewayRouting: VercelGatewayRoutingSchema.optional(),
/** Whether z.ai supports top-level `tool_stream: true` for streaming tool call deltas. Default: false. */
zaiToolStream: boolean().optional(),
/**
* Cache control convention for prompt caching. "anthropic" applies Anthropic-style
* `cache_control` markers to the system prompt, last tool definition, and last user/assistant
* text content.
*/
cacheControlFormat: literal("anthropic").optional(),
/**
* Whether to send known session-affinity headers (`session_id`, `x-client-request-id`,
* `x-session-affinity`) from `options.sessionId` when caching is enabled. Default: false.
*/
sendSessionAffinityHeaders: boolean().optional(),
/**
* Whether to send the OpenAI `session_id` cache-affinity header from `options.sessionId` when
* caching is enabled. Default: true.
*/
sendSessionIdHeader: boolean().optional(),
/**
* Whether the provider accepts per-tool `eager_input_streaming`. When false, the Anthropic
* provider omits `tools[].eager_input_streaming` and sends the legacy
* `fine-grained-tool-streaming-2025-05-14` beta header for tool-enabled requests. Default:
* true.
*/
supportsEagerToolInputStreaming: boolean().optional(),
/**
* Whether the provider supports long prompt cache retention (`prompt_cache_retention: "24h"`
* or Anthropic-style `cache_control.ttl: "1h"`, depending on format). Default: true.
*/
supportsLongCacheRetention: boolean().optional()
}).optional();
//#endregion
//#region src/config/zod-schema.core.ts
const WINDOWS_ABS_PATH_PATTERN = /^[A-Za-z]:[\\/]/;
const WINDOWS_UNC_PATH_PATTERN = /^\\\\[^\\]+\\[^\\]+/;
function isAbsolutePath(value) {
return path.isAbsolute(value) || WINDOWS_ABS_PATH_PATTERN.test(value) || WINDOWS_UNC_PATH_PATTERN.test(value);
}
/** Canonical operator-configurable SSRF policy shared by network-capable surfaces. */
const SsrFPolicyConfigSchema = strictObject({
dangerouslyAllowPrivateNetwork: boolean().optional(),
allowRfc2544BenchmarkRange: boolean().optional(),
allowIpv6UniqueLocalRange: boolean().optional(),
allowedHostnames: array(string()).optional(),
blockedHostnames: array(string()).optional()
});
const SecretsEnvProviderSchema = strictObject({
source: literal("env"),
/** Optional env var allowlist (exact names). */
allowlist: array(string().regex(ENV_SECRET_REF_ID_RE)).max(256).optional()
});
const SecretsFileProviderSchema = strictObject({
source: literal("file"),
path: string().min(1),
mode: union([literal("singleValue"), literal("json")]).optional(),
timeoutMs: number().int().positive().max(12e4).optional(),
maxBytes: number().int().positive().max(20971520).optional()
});
const SecretsManualExecProviderSchema = strictObject({
source: literal("exec"),
command: string().min(1).refine(isSafeExecutableValue, "secrets.providers.*.command is unsafe.").refine(isAbsolutePath, "secrets.providers.*.command must be an absolute path."),
args: array(string().max(1024)).max(128).optional(),
timeoutMs: number().int().positive().max(12e4).optional(),
noOutputTimeoutMs: number().int().positive().max(12e4).optional(),
maxOutputBytes: number().int().positive().max(20971520).optional(),
jsonOnly: boolean().optional(),
env: record(string(), string()).optional(),
passEnv: array(string().regex(ENV_SECRET_REF_ID_RE)).max(128).optional(),
trustedDirs: array(string().min(1).refine(isAbsolutePath, "trustedDirs entries must be absolute paths.")).max(64).optional()
});
const SecretsPluginIntegrationExecProviderSchema = strictObject({
source: literal("exec"),
pluginIntegration: strictObject({
pluginId: string().min(1).max(128),
integrationId: string().min(1).max(128)
})
});
const SecretsExecProviderSchema = union([SecretsManualExecProviderSchema, SecretsPluginIntegrationExecProviderSchema]);
const SecretsStoreProviderSchema = strictObject({ source: literal("store") });
const EgressProxyExactHostSchema = string().trim().min(1).superRefine((host, ctx) => {
try {
normalizeExactAllowedHost(host);
} catch (error) {
ctx.addIssue({
code: "custom",
message: error instanceof Error ? error.message : "Invalid allowed host"
});
}
});
/** Schema for one configured env/file/exec/store secret provider entry. */
const SecretProviderSchema = union([
SecretsEnvProviderSchema,
SecretsFileProviderSchema,
SecretsExecProviderSchema,
SecretsStoreProviderSchema
]);
strictObject({
egressProxy: strictObject({
enabled: boolean().optional(),
allowedHosts: array(EgressProxyExactHostSchema).max(256).optional(),
bypassHosts: array(EgressProxyExactHostSchema).max(256).optional()
}).optional(),
providers: object({}).catchall(SecretProviderSchema).optional(),
defaults: strictObject({
env: string().regex(SECRET_PROVIDER_ALIAS_PATTERN).optional(),
file: string().regex(SECRET_PROVIDER_ALIAS_PATTERN).optional(),
exec: string().regex(SECRET_PROVIDER_ALIAS_PATTERN).optional(),
store: string().regex(SECRET_PROVIDER_ALIAS_PATTERN).optional()
}).optional()
}).optional();
const LEGACY_OPENAI_CODEX_RESPONSES_API = "openai-codex-responses";
const OPENAI_CHATGPT_RESPONSES_API = "openai-chatgpt-responses";
const ModelApiSchema = _enum(MODEL_APIS, { error: (issue) => issue.input === LEGACY_OPENAI_CODEX_RESPONSES_API ? `"${LEGACY_OPENAI_CODEX_RESPONSES_API}" is a removed api id; use "${OPENAI_CHATGPT_RESPONSES_API}"` : void 0 });
const ConfiguredProviderRequestTlsSchema = strictObject({
ca: SecretInputSchema.optional().register(sensitive),
cert: SecretInputSchema.optional().register(sensitive),
key: SecretInputSchema.optional().register(sensitive),
passphrase: SecretInputSchema.optional().register(sensitive),
serverName: string().optional(),
insecureSkipVerify: boolean().optional()
}).optional();
const ConfiguredProviderRequestAuthSchema = union([
strictObject({ mode: literal("provider-default") }),
strictObject({
mode: literal("authorization-bearer"),
token: SecretInputSchema.register(sensitive)
}),
strictObject({
mode: literal("header"),
headerName: string().min(1),
value: SecretInputSchema.register(sensitive),
prefix: string().optional()
})
]).optional();
const ConfiguredProviderRequestProxySchema = union([strictObject({
mode: literal("env-proxy"),
tls: ConfiguredProviderRequestTlsSchema
}), strictObject({
mode: literal("explicit-proxy"),
url: string().min(1),
tls: ConfiguredProviderRequestTlsSchema
})]).optional();
const ConfiguredProviderRequestFields = {
headers: record(string(), SecretInputSchema.register(sensitive)).optional(),
auth: ConfiguredProviderRequestAuthSchema,
proxy: ConfiguredProviderRequestProxySchema,
tls: ConfiguredProviderRequestTlsSchema
};
const ConfiguredProviderRequestSchema = strictObject(ConfiguredProviderRequestFields).optional();
const ConfiguredModelProviderRequestSchema = strictObject({
...ConfiguredProviderRequestFields,
allowPrivateNetwork: boolean().optional()
}).optional();
const ModelImageInputSchema = strictObject({
maxBytes: number().int().positive().optional(),
maxPixels: number().int().positive().optional(),
maxSidePx: number().int().positive().optional(),
preferredSidePx: number().int().positive().optional(),
tokenMode: union([
literal("tile"),
literal("detail"),
literal("provider")
]).optional()
});
const ModelMediaInputSchema = strictObject({ image: ModelImageInputSchema.optional() });
const ThinkingLevelMapValueSchema = string().nullable();
const ThinkingLevelMapSchema = strictObject({
off: ThinkingLevelMapValueSchema.optional(),
minimal: ThinkingLevelMapValueSchema.optional(),
low: ThinkingLevelMapValueSchema.optional(),
medium: ThinkingLevelMapValueSchema.optional(),
high: ThinkingLevelMapValueSchema.optional(),
xhigh: ThinkingLevelMapValueSchema.optional(),
max: ThinkingLevelMapValueSchema.optional()
});
const ModelDefinitionSchema = strictObject({
/** Provider-facing model id. */
id: string().min(1),
/** Human-readable display name. */
name: string().min(1),
/** Optional API adapter override for this model. */
api: ModelApiSchema.optional(),
/** Optional base URL override for this model. */
baseUrl: string().min(1).optional(),
reasoning: boolean().optional(),
input: array(union([
literal("text"),
literal("image"),
literal("video"),
literal("audio")
])).optional(),
cost: strictObject({
input: number().optional(),
output: number().optional(),
cacheRead: number().optional(),
cacheWrite: number().optional(),
tieredPricing: array(strictObject({
input: number(),
output: number(),
cacheRead: number(),
cacheWrite: number(),
range: union([tuple([number(), number()]), tuple([number()])])
})).optional()
}).optional(),
/** Provider/native maximum context window in tokens. */
contextWindow: number().positive().optional(),
/**
* Optional effective runtime cap used for compaction/session budgeting.
* Keeps provider/native contextWindow metadata intact while letting configs
* prefer a smaller practical window.
*/
contextTokens: number().int().positive().optional(),
maxTokens: number().positive().optional(),
/** Maps OpenClaw thinking levels to provider/model-specific values. */
thinkingLevelMap: ThinkingLevelMapSchema.optional(),
/** Provider-specific request/runtime parameters passed through to provider plugins. */
params: record(string(), unknown()).optional(),
/** Optional agent execution runtime override for this provider/model pair. */
agentRuntime: AgentRuntimePolicySchema,
/** Static headers merged into requests for this model. */
headers: record(string(), string()).optional(),
/** Provider compatibility flags for payload shaping and feature gating. */
compat: ModelCompatSchema,
/** Media input limits used by routing and preflight compression. */
mediaInput: ModelMediaInputSchema.optional(),
/** Metadata source marker for models added by CLI/catalog tooling. */
metadataSource: literal("models-add").optional()
});
const ModelProviderLocalServiceSchema = strictObject({
/** Executable started before model requests are sent. */
command: string().min(1),
/** Arguments passed without shell expansion. */
args: array(string()).optional(),
/** Working directory for the local service process. */
cwd: string().min(1).optional(),
/** Environment variables added to the service process. */
env: record(string(), string().register(sensitive)).optional(),
/** Optional health endpoint polled before the provider is considered ready. */
healthUrl: string().min(1).optional(),
/** Startup readiness timeout in milliseconds. */
readyTimeoutMs: number().int().positive().optional(),
/** Idle timeout in milliseconds before stopping the local service. */
idleStopMs: number().int().nonnegative().optional()
}).optional();
const ModelProviderSchema = strictObject({
baseUrl: string().optional(),
/** API key or secret reference for this provider. */
apiKey: SecretInputSchema.optional().register(sensitive),
/** Authentication mode used when resolving credentials for this provider. */
auth: union([
literal("api-key"),
literal("aws-sdk"),
literal("oauth"),
literal("token")
]).optional(),
/** Default API adapter for models under this provider. */
api: ModelApiSchema.optional(),
/** Provider-level default max output tokens. */
maxTokens: number().positive().optional(),
/** Provider request timeout in seconds. */
timeoutSeconds: number().int().positive().optional(),
/** Optional provider deployment/API region used by provider plugins that expose regional endpoints. */
region: string().min(1).optional(),
injectNumCtxForOpenAICompat: boolean().optional(),
/** Provider-specific runtime parameters interpreted by provider plugins. */
params: record(string(), unknown()).optional(),
/** Optional default agent execution runtime for models under this provider. */
agentRuntime: AgentRuntimePolicySchema,
/** Optional local service to start before calling this provider. */
localService: ModelProviderLocalServiceSchema,
/** Secret-bearing headers merged into provider requests. */
headers: record(string(), SecretInputSchema.register(sensitive)).optional(),
/** Whether default Authorization header injection is enabled. */
authHeader: boolean().optional(),
/** Provider request transport/retry overrides. */
request: ConfiguredModelProviderRequestSchema,
models: array(ModelDefinitionSchema).optional()
});
const ModelProvidersSchema = record(string(), ModelProviderSchema).superRefine((providers, ctx) => {
for (const [providerId, provider] of Object.entries(providers)) {
if (isBuiltInModelProviderOverlayId(providerId)) continue;
if (!provider.baseUrl) ctx.addIssue({
code: "custom",
path: [providerId, "baseUrl"],
message: "custom model providers must declare baseUrl; provider overlays without baseUrl are only supported for bundled providers"
});
if (!Array.isArray(provider.models)) ctx.addIssue({
code: "custom",
path: [providerId, "models"],
message: "custom model providers must declare models; provider overlays without models are only supported for bundled providers"
});
}
});
const ModelCatalogRefreshConfigSchema = strictObject({
/** Fetch model catalog updates from the hosted OpenClaw catalog. Default: true. */
enabled: boolean().optional(),
/** Override the hosted catalog URL (HTTPS mirrors, or localhost HTTP for testing). */
url: string().refine((value) => {
const parsed = URL.parse(value);
return parsed !== null && (parsed.protocol === "https:" || parsed.protocol === "http:" && [
"localhost",
"127.0.0.1",
"[::1]"
].includes(parsed.hostname));
}, { message: "models.catalogRefresh.url must use https, or http on localhost" }).optional()
}).optional();
strictObject({
/** Merge provider config with bundled catalogs or replace bundled catalogs entirely. */
mode: union([literal("merge"), literal("replace")]).optional(),
providers: ModelProvidersSchema.optional(),
/** Hosted model catalog refresh settings. */
catalogRefresh: ModelCatalogRefreshConfigSchema
}).optional();
const IdentitySchema = strictObject({
name: string().optional(),
theme: string().optional(),
emoji: string().optional(),
avatar: string().optional()
}).optional();
const ReplyToModeSchema = union([
literal("off"),
literal("first"),
literal("all"),
literal("batched")
]);
const TypingModeSchema = union([
literal("never"),
literal("instant"),
literal("thinking"),
literal("message")
]);
const GroupPolicySchema = _enum([
"open",
"disabled",
"allowlist"
]);
const DmPolicySchema = _enum([
"pairing",
"allowlist",
"open",
"disabled"
]);
const ContextVisibilityModeSchema = _enum([
"all",
"allowlist",
"allowlist_quote"
]);
const BlockStreamingCoalesceSchema = strictObject({
minChars: number().int().positive().optional(),
maxChars: number().int().positive().optional(),
idleMs: number().int().nonnegative().optional()
});
const TextChunkModeSchema = _enum(["length", "newline"]);
const ChannelStreamingBlockSchema = strictObject({
enabled: boolean().optional(),
coalesce: BlockStreamingCoalesceSchema.optional()
});
/** Delivery-only nested streaming config for channels without preview modes. */
const ChannelDeliveryStreamingConfigSchema = strictObject({
chunkMode: TextChunkModeSchema.optional(),
block: ChannelStreamingBlockSchema.optional()
});
number().int().min(0).optional(), number().int().min(0).optional(), ContextVisibilityModeSchema.optional(), record(string(), DmConfigSchema$1.optional()).optional(), number().int().positive().optional(), ChannelDeliveryStreamingConfigSchema.optional(), string().optional(), number().positive().optional();
const BlockStreamingChunkSchema = strictObject({
minChars: number().int().positive().optional(),
maxChars: number().int().positive().optional(),
breakPreference: union([
literal("paragraph"),
literal("newline"),
literal("sentence")
]).optional()
});
const MarkdownTableModeSchema = _enum([
"off",
"bullets",
"code",
"block"
]);
const MarkdownConfigSchema$1 = strictObject({ tables: MarkdownTableModeSchema.optional() }).optional();
const TtsProviderSchema = string().min(1);
const TtsModeSchema = _enum(["final", "all"]);
const TtsAutoSchema = _enum([
"off",
"always",
"inbound",
"tagged"
]);
const TtsProviderConfigSchema = object({ apiKey: SecretInputSchema.optional().register(sensitive) }).catchall(union([
string(),
number(),
boolean(),
_null(),
array(unknown()),
record(string(), unknown())
]));
const TtsPersonaSchema = strictObject({
label: string().optional(),
description: string().optional(),
provider: TtsProviderSchema.optional(),
fallbackPolicy: union([
literal("preserve-persona"),
literal("provider-defaults"),
literal("fail")
]).optional(),
providers: record(string(), TtsProviderConfigSchema).optional()
});
const TtsConfigSchema = strictObject({
auto: TtsAutoSchema.optional(),
enabled: boolean().optional(),
mode: TtsModeSchema.optional(),
provider: TtsProviderSchema.optional(),
persona: string().optional(),
personas: record(string(), TtsPersonaSchema).optional(),
summaryModel: string().optional(),
modelOverrides: strictObject({
enabled: boolean().optional(),
allowText: boolean().optional(),
allowProvider: boolean().optional(),
allowVoice: boolean().optional(),
allowModelId: boolean().optional(),
allowVoiceSettings: boolean().optional(),
allowNormalization: boolean().optional(),
allowSeed: boolean().optional()
}).optional(),
providers: record(string(), TtsProviderConfigSchema).optional(),
maxTextLength: number().int().min(1).optional(),
timeoutMs: number().int().min(1e3).max(12e4).optional()
}).optional();
const HumanDelaySchema = strictObject({
mode: union([
literal("off"),
literal("natural"),
literal("custom")
]).optional(),
minMs: number().int().nonnegative().optional(),
maxMs: number().int().nonnegative().optional()
});
_enum(["thread", "top-level"]);
string().regex(/^#?[0-9a-fA-F]{6}$/, "expected hex color (RRGGBB)");
string().refine(isSafeExecutableValue, "expected safe executable name or path");
const MediaUnderstandingScopeSchema = createAllowDenyChannelRulesSchema();
const MediaUnderstandingAttachmentsSchema = strictObject({
/** Select the first matching attachment or process multiple. */
mode: union([literal("first"), literal("all")]).optional(),
/** Max number of attachments to process (default: 1). */
maxAttachments: number().int().positive().optional(),
/** Attachment ordering preference. */
prefer: union([
literal("first"),
literal("last"),
literal("path"),
literal("url")
]).optional()
}).optional();
const MediaUnderstandingCapabilitiesSchema = array(union([
literal("image"),
literal("audio"),
literal("video")
])).optional();
const ProviderOptionValueSchema = union([
string(),
number(),
boolean()
]);
const ProviderOptionsSchema = record(string(), record(string(), ProviderOptionValueSchema)).optional();
const MediaUnderstandingRuntimeFields = {
/** Default prompt; model entries can override it. */
prompt: string().optional(),
/** Default timeout (seconds); model entries can override it. */
timeoutSeconds: number().int().positive().optional(),
/** Default language hint for audio transcription; model entries can override it. */
language: string().optional(),
/** Optional provider-specific query params (merged into requests). */
providerOptions: ProviderOptionsSchema,
/** Optional base URL override for provider requests. */
baseUrl: string().optional(),
/** Optional headers merged into provider requests. */
headers: record(string(), string()).optional(),
/** Optional request transport overrides for provider HTTP calls. */
request: ConfiguredProviderRequestSchema
};
const MediaUnderstandingModelSchema = strictObject({
/** provider API id (e.g. openai, google). */
provider: string().optional(),
/** Model id for provider-based understanding. */
model: string().optional(),
/** Optional capability tags for shared model lists. */
capabilities: MediaUnderstandingCapabilitiesSchema,
/** Use a CLI command instead of provider API. */
type: union([literal("provider"), literal("cli")]).optional(),
/** CLI binary (required when type=cli). */
command: string().optional(),
/** CLI args (template-enabled). */
args: array(string()).optional(),
/** Optional max output characters for this model entry. */
maxChars: number().int().positive().optional(),
/** Optional max bytes for this model entry. */
maxBytes: number().int().positive().optional(),
...MediaUnderstandingRuntimeFields,
/** Auth profile id to use for this provider. */
profile: string().optional(),
/** Preferred profile id if multiple are available. */
preferredProfile: string().optional()
}).optional();
const ToolsMediaCapabilitySchema = strictObject({
/** Enable media understanding when models are configured. */
enabled: boolean().optional(),
/** Prefer a matching shared model entry. */
preferredModel: string().trim().min(1).optional(),
/** Optional scope gating for understanding. */
scope: MediaUnderstandingScopeSchema,
/** Default max bytes to send. */
maxBytes: number().int().positive().optional(),
/** Default max output characters. */
maxChars: number().int().positive().optional(),
...MediaUnderstandingRuntimeFields,
/** Attachment selection policy. */
attachments: MediaUnderstandingAttachmentsSchema
}).optional();
const ToolsMediaAudioSchema = ToolsMediaCapabilitySchema.unwrap().extend({
/**
* Echo the audio transcript back to the originating chat before agent processing.
* Lets users verify what was heard. Default: false.
*/
echoTranscript: boolean().optional(),
/**
* Format string for the echoed transcript. Use `{transcript}` as placeholder.
* Default: '📝 "{transcript}"'
*/
echoFormat: string().optional()
}).optional();
const ToolsMediaSchema = strictObject({
models: array(MediaUnderstandingModelSchema).optional(),
concurrency: number().int().positive().optional(),
image: ToolsMediaCapabilitySchema.optional(),
audio: ToolsMediaAudioSchema.optional(),
video: ToolsMediaCapabilitySchema.optional()
}).optional();
const LinkModelSchema = strictObject({
/** Use a CLI command for link processing. */
type: literal("cli").optional(),
command: string().min(1),
args: array(string()).optional(),
timeoutSeconds: number().int().positive().optional()
});
const ToolsLinksSchema = strictObject({
/** Enable link understanding when models are configured. */
enabled: boolean().optional(),
scope: MediaUnderstandingScopeSchema,
/** Max number of links to process per message. */
maxLinks: number().int().positive().optional(),
timeoutSeconds: number().int().positive().optional(),
/** Ordered model list (fallbacks in order). */
models: array(LinkModelSchema).optional()
}).optional();
//#endregion
//#region src/config/zod-schema.memory-search.ts
const MemorySearchSchema = strictObject({
enabled: boolean().optional(),
rememberAcrossConversations: boolean().optional(),
sources: array(union([literal("memory"), literal("sessions")])).optional(),
extraPaths: array(union([string(), strictObject({
path: string(),
pattern: string().optional()
})])).optional(),
multimodal: strictObject({
enabled: boolean().optional(),
modalities: array(union([
literal("image"),
literal("audio"),
literal("all")
])).optional(),
maxFileBytes: number().int().positive().optional()
}).optional(),
experimental: strictObject({ sessionMemory: boolean().optional() }).optional(),
provider: string().optional(),
remote: strictObject({
baseUrl: string().optional(),
apiKey: SecretInputSchema.optional().register(sensitive),
headers: record(string(), string()).optional(),
batch: strictObject({ enabled: boolean().optional() }).optional()
}).optional(),
fallback: string().optional(),
model: string().optional(),
inputType: string().min(1).optional(),
queryInputType: string().min(1).optional(),
documentInputType: string().min(1).optional(),
outputDimensionality: number().int().positive().optional(),
local: strictObject({ modelPath: string().optional() }).optional(),
store: strictObject({
fts: strictObject({ tokenizer: union([literal("unicode61"), literal("trigram")]).optional() }).optional(),
vector: strictObject({
enabled: boolean().optional(),
extensionPath: string().optional()
}).optional()
}).optional(),
query: strictObject({
maxResults: number().int().positive().optional(),
minScore: number().min(0).max(1).optional()
}).optional(),
cache: strictObject({ enabled: boolean().optional() }).optional()
}).optional();
//#endregion
//#region src/agents/sandbox/bind-spec.ts
/** Splits a bind spec while preserving Windows drive-letter prefixes in host paths. */
function splitSandboxBindSpec(spec, options) {
const separator = getBindSeparatorIndex(spec);
if (separator === -1) return null;
const host = spec.slice(0, separator);
const rest = spec.slice(separator + 1);
const optionsStart = options?.allowWindowsContainerPath === true ? getBindSeparatorIndex(rest) : rest.indexOf(":");
if (optionsStart === -1) return {
host,
container: rest,
options: ""
};
return {
host,
container: rest.slice(0, optionsStart),
options: rest.slice(optionsStart + 1)
};
}
function getBindSeparatorIndex(spec) {
const hasDriveLetterPrefix = /^[A-Za-z]:[\\/]/.test(spec);
return spec.indexOf(":", hasDriveLetterPrefix ? 2 : 0);
}
//#endregion
//#region src/agents/sandbox/host-paths.ts
function stripWindowsNamespacePrefix(input) {
if (input.startsWith("\\\\?\\")) {
const withoutPrefix = input.slice(4);
if (withoutPrefix.toUpperCase().startsWith("UNC\\")) return `\\\\${withoutPrefix.slice(4)}`;
return withoutPrefix;
}
if (input.startsWith("//?/")) {
const withoutPrefix = input.slice(4);
if (withoutPrefix.toUpperCase().startsWith("UNC/")) return `//${withoutPrefix.slice(4)}`;
return withoutPrefix;
}
return input;
}
function isWindowsDriveAbsolutePath(raw) {
return /^[A-Za-z]:[\\/]/.test(stripWindowsNamespacePrefix(raw));
}
function isSandboxHostPathAbsolute(raw) {
const input = stripWindowsNamespacePrefix(raw);
return input.startsWith("/") || isWindowsDriveAbsolutePath(input);
}
//#endregion
//#region src/config/zod-schema.sandbox.ts
function validateSandboxBindEntries(binds, ctx) {
if (!binds) return;
for (let i = 0; i < binds.length; i += 1) {
const bind = binds[i] ?? "";
if (!bind.trim()) {
ctx.addIssue({
code: ZodIssueCode.custom,
path: ["binds", i],
message: "Sandbox security: bind mount entry must be a non-empty string."
});
continue;
}
const parsed = splitSandboxBindSpec(bind);
const source = parsed ? parsed.host : bind;
if (!isSandboxHostPathAbsolute(source)) ctx.addIssue({
code: ZodIssueCode.custom,
path: ["binds", i],
message: `Sandbox security: bind mount "${bind}" uses a non-absolute source path "${source}". Only absolute POSIX or Windows drive-letter paths are supported for sandbox binds.`
});
}
}
const SandboxDockerSchema = strictObject({
image: string().optional(),
containerPrefix: string().optional(),
workdir: string().optional(),
readOnlyRoot: boolean().optional(),
tmpfs: array(string()).optional(),
network: string().optional(),
user: string().optional(),
capDrop: array(string()).optional(),
env: record(string(), string()).optional(),
setupCommand: union([string(), array(string())]).transform((value) => Array.isArray(value) ? value.join("\n") : value).pipe(string()).optional(),
pidsLimit: number().int().positive().optional(),
memory: union([string(), number()]).optional(),
memorySwap: union([string(), number()]).optional(),
cpus: number().positive().optional(),
gpus: string().min(1).optional(),
ulimits: record(string(), union([
string(),
number(),
strictObject({
soft: number().int().nonnegative().optional(),
hard: number().int().nonnegative().optional()
})
])).optional(),
seccompProfile: string().optional(),
apparmorProfile: string().optional(),
dns: array(string()).optional(),
extraHosts: array(string()).optional(),
binds: array(string()).optional(),
dangerouslyAllowReservedContainerTargets: boolean().optional(),
dangerouslyAllowExternalBindSources: boolean().optional(),
dangerouslyAllowContainerNamespaceJoin: boolean().optional()
}).superRefine((data, ctx) => {
validateSandboxBindEntries(data.binds, ctx);
const blockedNetworkReason = getBlockedNetworkModeReason({
network: data.network,
allowContainerNamespaceJoin: data.dangerouslyAllowContainerNamespaceJoin === true
});
if (blockedNetworkReason === "host") ctx.addIssue({
code: ZodIssueCode.custom,
path: ["network"],
message: "Sandbox security: network mode \"host\" is blocked. Use \"bridge\" or \"none\" instead."
});
if (blockedNetworkReason === "container_namespace_join") ctx.addIssue({
code: ZodIssueCode.custom,
path: ["network"],
message: "Sandbox security: network mode \"container:*\" is blocked by default. Use a custom bridge network, or set dangerouslyAllowContainerNamespaceJoin=true only when you fully trust this runtime."
});
if (normalizeLowercaseStringOrEmpty(data.seccompProfile ?? "") === "unconfined") ctx.addIssue({
code: ZodIssueCode.custom,
path: ["seccompProfile"],
message: "Sandbox security: seccomp profile \"unconfined\" is blocked. Use a custom seccomp profile file or omit this setting."
});
if (normalizeLowercaseStringOrEmpty(data.apparmorProfile ?? "") === "unconfined") ctx.addIssue({
code: ZodIssueCode.custom,
path: ["apparmorProfile"],
message: "Sandbox security: AppArmor profile \"unconfined\" is blocked. Use a named AppArmor profile or omit this setting."
});
}).optional();
const SandboxBrowserSchema = object({
enabled: boolean().optional(),
image: string().optional(),
containerPrefix: string().optional(),
network: string().optional(),
cdpPort: number().int().positive().optional(),
cdpSourceRange: string().optional(),
vncPort: number().int().positive().optional(),
noVncPort: number().int().positive().optional(),
headless: boolean().optional(),
noVncEnabled: boolean().optional(),
allowHostControl: boolean().optional(),
autoStart: boolean().optional(),
autoStartTimeoutMs: number().int().positive().optional(),
binds: array(string()).optional()
}).superRefine((data, ctx) => {
validateSandboxBindEntries(data.binds, ctx);
if (normalizeLowercaseStringOrEmpty(data.network ?? "") === "host") ctx.addIssue({
code: ZodIssueCode.custom,
path: ["network"],
message: "Sandbox security: browser network mode \"host\" is blocked. Use \"bridge\" or a custom bridge network instead."
});
}).strict().optional();
const SandboxPruneSchema = strictObject({
idleHours: number().int().nonnegative().optional(),
maxAgeDays: number().int().nonnegative().optional()
}).optional();
//#endregion
//#region src/config/zod-schema.agent-runtime.ts
const AgentTtsConfigSchema = TtsConfigSchema.unwrap().extend({ prefsPath: string().optional() }).strict().optional();
const HeartbeatSchema = strictObject({
every: string().optional(),
activeHours: strictObject({
start: string().optional(),
end: string().optional(),
timezone: string().optional()
}).optional(),
model: string().optional(),
session: string().optional(),
target: string().optional(),
directPolicy: union([literal("allow"), literal("block")]).optional(),
to: string().optional(),
accountId: string().optional(),
prompt: string().optional(),
timeoutSeconds: number().int().positive().optional(),
lightContext: boolean().optional(),
isolatedSession: boolean().optional()
}).superRefine((val, ctx) => {
if (val.every) try {
parseDurationMs(val.every, { defaultUnit: "m" });
} catch {
ctx.addIssue({
code: ZodIssueCode.custom,
path: ["every"],
message: "invalid duration (use ms, s, m, h)"
});
}
const active = val.activeHours;
if (!active) return;
const timePattern = /^([01]\d|2[0-3]|24):([0-5]\d)$/;
for (const path of ["start", "end"]) {
const raw = active[path];
if (!raw) continue;
const match = timePattern.exec(raw);
let message;
if (!match) message = "invalid time (use \"HH:MM\" 24h format)";
else if (match[1] === "24" && match[2] !== "00") message = "invalid time (24:00 is the only allowed 24:xx value)";
else if (match[1] === "24" && path === "start") message = "invalid time (start cannot be 24:00)";
if (message) ctx.addIssue({
code: ZodIssueCode.custom,
path: ["activeHours", path],
message
});
}
}).optional();
const AgentContextLimitsSchema = strictObject({
/** Default max chars returned by memory_get before truncation metadata/notice (default: 12000). */
memoryGetMaxChars: number().int().min(1).max(25e4).optional(),
/** Max chars retained from post-compaction AGENTS.md context injection (default: 1800). */
postCompactionMaxChars: number().int().min(1).max(5e4).optional()
}).optional();
const AgentSkillsLimitsSchema = strictObject({ maxSkillsPromptChars: number().int().min(0).optional() }).optional();
const ToolPolicyBaseSchema = strictObject({
/** Exact tool names allowed in this policy scope. */
allow: array(string()).optional(),
/** Additional allowlist entries merged into the inherited policy. */
alsoAllow: array(string()).optional(),
/** Exact tool names denied after allow expansion; deny wins. */
deny: array(string()).optional()
});
const ToolPolicySchema$1 = ToolPolicyBaseSchema.superRefine((value, ctx) => {
addAllowAlsoAllowConflictIssue(value, ctx, "tools policy cannot set both allow and alsoAllow in the same scope (merge alsoAllow into allow, or remove allow and use profile + alsoAllow)");
}).optional();
const ToolPolicyBySenderSchema = record(string(), ToolPolicySchema$1).optional();
const TrimmedOptionalConfigStringSchema = string().transform(normalizeOptionalString).optional();
const CodexAllowedDomainsSchema = array(string()).transform((values) => {
const deduped = normalizeUniqueStringEntries(values);
return deduped.length > 0 ? deduped : void 0;
}).optional();
const CodexUserLocationSchema = strictObject({
country: TrimmedOptionalConfigStringSchema,
region: TrimmedOptionalConfigStringSchema,
city: TrimmedOptionalConfigStringSchema,
timezone: TrimmedOptionalConfigStringSchema
}).transform((value) => {
return value.country || value.region || value.city || value.timezone ? value : void 0;
}).optional();
const BLOCKED_WEB_SEARCH_KEYS_ISSUE_FIELD = "__openclawBlockedWebSearchKeys";
const ToolsWebSearchSchema = preprocess((value) => {
if (!isRecord(value)) return value;
const blockedKeys = Object.getOwnPropertyNames(value).filter((key) => isBlockedObjectKey(key));
if (blockedKeys.length === 0) return value;
return {
...value,
[BLOCKED_WEB_SEARCH_KEYS_ISSUE_FIELD]: blockedKeys
};
}, object({
enabled: boolean().optional(),
provider: string().optional(),
maxResults: number().int().positive().optional(),
timeoutSeconds: number().int().positive().optional(),
cacheTtlMinutes: number().nonnegative().optional(),
openaiCodex: strictObject({
enabled: boolean().optional(),
mode: union([literal("cached"), literal("live")]).optional(),
allowedDomains: CodexAllowedDomainsSchema,
contextSize: union([
literal("low"),
literal("medium"),
literal("high")
]).optional(),
userLocation: CodexUserLocationSchema
}).optional()
}).catchall(unknown()).superRefine((value, ctx) => {
const blockedKeys = value[BLOCKED_WEB_SEARCH_KEYS_ISSUE_FIELD];
if (Array.isArray(blockedKeys)) for (const key of blockedKeys) {
if (typeof key !== "string") continue;
ctx.addIssue({
code: ZodIssueCode.custom,
path: [key],
message: "tools.web.search must not contain blocked object keys"
});
}
for (const [key, entry] of Object.entries(value)) {
if (key === BLOCKED_WEB_SEARCH_KEYS_ISSUE_FIELD || isBlockedObjectKey(key)) continue;
if (key === "apiKey" || LEGACY_WEB_SEARCH_PROVIDER_CONFIG_KEYS.has(key) && isRecord(entry)) ctx.addIssue({
code: ZodIssueCode.custom,
path: [key],
message: "legacy web_search provider config must use plugins.entries.<plugin>.config.webSearch"
});
}
})).optional();
const ToolsWebFetchSchema = strictObject({
/** Enable web fetch tool (default: true). */
enabled: boolean().optional(),
/** Web fetch fallback provider id. */
provider: string().optional(),
/** Max characters to return from fetched content. */
maxChars: number().int().positive().optional(),
/** Hard cap for maxChars (tool or config), defaults to 20000. */
maxCharsCap: number().int().positive().optional(),
/** Max download size before truncation, defaults to 750000 bytes. */
maxResponseBytes: number().int().positive().optional(),
/** Timeout in seconds for fetch requests. */
timeoutSeconds: number().int().positive().optional(),
/** Cache TTL in minutes for fetched content. */
cacheTtlMinutes: number().nonnegative().optional(),
/** Maximum number of redirects to follow (default: 3). */
maxRedirects: number().int().nonnegative().optional(),
/** Override User-Agent header for fetch requests. */
userAgent: string().optional(),
/**
* Extra request headers sent with direct web_fetch requests. Every value is
* treated as sensitive in exposed config. Entries a request cannot carry are
* dropped with a warning at request time.
*/
headers: record(string(), string().register(sensitive)).optional(),
/** Use Readability to extract main content (default: true). */
readability: boolean().optional(),
/** Route web_fetch through a trusted HTTP(S) env proxy and let the proxy resolve DNS. Enable only when that proxy enforces outbound policy. */
useTrustedEnvProxy: boolean().optional(),
/** SSRF policy configuration for web_fetch. */
ssrfPolicy: SsrFPolicyConfigSchema.optional()
}).optional();
const ToolsWebSchema = strictObject({
search: ToolsWebSearchSchema,
fetch: ToolsWebFetchSchema
}).optional();
const ToolProfileSchema = union([
literal("minimal"),
literal("coding"),
literal("messaging"),
literal("full")
]).optional();
function addAllowAlsoAllowConflictIssue(value, ctx, message) {
if (value.allow && value.allow.length > 0 && value.alsoAllow && value.alsoAllow.length > 0) ctx.addIssue({
code: ZodIssueCode.custom,
message
});
}
const ToolPolicyWithProfileSchema = ToolPolicyBaseSchema.extend({ profile: ToolProfileSchema }).superRefine((value, ctx) => {
addAllowAlsoAllowConflictIssue(value, ctx, "tools.byProvider policy cannot set both allow and alsoAllow in the same scope (merge alsoAllow into allow, or remove allow and use profile + alsoAllow)");
});
const ElevatedAllowFromSchema = record(string(), array(union([string(), number()]))).optional();
const ToolExecApplyPatchSchema = strictObject({
/** Enable apply_patch for OpenAI models (default: true; set false to disable). */
enabled: boolean().optional(),
/**
* Restrict apply_patch paths to the workspace directory.
* Default: true (safer; does not affect read/write/edit).
*/
workspaceOnly: boolean().optional(),
/**
* Optional allowlist of model ids that can use apply_patch.
* Accepts either raw ids (e.g. "gpt-5.4") or full ids (e.g. "openai/gpt-5.4").
*/
allowModels: array(string()).optional()
}).optional();
const ToolExecSafeBinProfileSchema = strictObject({
minPositional: number().int().nonnegative().optional(),
maxPositional: number().int().nonnegative().optional(),
allowedValueFlags: array(string()).optional(),
deniedFlags: array(string()).optional()
});
const ToolExecBaseShape = {
/** Exec host routing (default: auto). */
host: _enum([
"auto",
"sandbox",
"gateway",
"node"
]).optional(),
/** Normalized exec policy mode. Prefer this over raw security/ask knobs. */
mode: _enum([
"deny",
"allowlist",
"ask",
"auto",
"full"
]).optional(),
/** Legacy exec security mode retained when no canonical mode can preserve policy. */
security: _enum([
"deny",
"allowlist",
"full"
]).optional(),
/** Legacy exec ask mode retained when no canonical mode can preserve policy. */
ask: _enum([
"off",
"on-miss",
"always"
]).optional(),
/** Default node binding for exec.host=node (node id/name). */
node: string().optional(),
/** Directories to prepend to PATH when running exec (gateway/sandbox). */
pathPrepend: array(string()).optional(),
/** Safe stdin-only binaries that can run without allowlist entries. */
safeBins: array(string()).optional(),
/**
* Require explicit approval for interpreter inline-eval forms (`python -c`, `node -e`, etc.).
* Prevents silent allowlist reuse and allow-always persistence for those forms.
*/
strictInlineEval: boolean().optional(),
/** Render parser-derived command highlights in exec approval prompts (default: false). */
commandHighlighting: boolean().optional(),
/**
* Default lifetime, in days, stamped onto standing grants minted by
* allow-always on automation approvals. Unset means grants live until
* revoked or the owning job changes. Terms freeze at mint; changing this
* affects only future grants.
*/
grantExpiryDays: number().int().min(1).max(3650).optional(),
/** Extra explicit directories trusted for safeBins path checks (never derived from PATH). */
safeBinTrustedDirs: array(string()).optional(),
/** Optional custom safe-bin profiles for entries in tools.exec.safeBins. */
safeBinProfiles: record(string(), ToolExecSafeBinProfileSchema).optional(),
/** Model-backed reviewer used by tools.exec.mode=auto before falling back to human approval. */
reviewer: strictObject({
/** Optional reviewer model override (provider/model or agent model config). */
model: AgentModelSchema.optional(),
/** Optional reasoning effort for model-backed approval reviews. */
thinking: _enum([
"minimal",
"low",
"medium",
"high",
"xhigh",
"max"
]).optional(),
/** Optional Fast processing for supported provider requests. */
fastMode: boolean().optional(),
/** Reviewer timeout in milliseconds (default: 30000). */
timeoutMs: number().int().positive().optional()
}).optional(),
/** Default time (ms) before an exec command auto-backgrounds. */
backgroundMs: number().int().positive().optional(),
/** Emit a running notice (ms) when approval-backed exec runs long (default: 10000, 0 = off). */
approvalRunningNoticeMs: number().int().nonnegative().optional(),
/** Default timeout (seconds) before auto-killing exec commands. */
timeoutSeconds: number().int().positive().optional(),
/** How long to keep finished sessions in memory (ms). */
cleanupMs: number().int().positive().optional(),
/** Emit a system event and heartbeat when a backgrounded exec exits. */
notifyOnExit: boolean().optional(),
/**
* Also emit success exit notifications when a backgrounded exec has no output.
* Default false to reduce context noise.
*/
notifyOnExitEmptySuccess: boolean().optional(),
/** apply_patch subtool configuration. */
applyPatch: ToolExecApplyPatchSchema
};
function addExecPolicyModeConflictIssue(value, ctx) {
if (value.mode === void 0 || value.security === void 0 && value.ask === void 0) return;
const exactMode = value.security !== void 0 && value.ask !== void 0 ? resolveExactExecModeFromPolicy({
security: value.security,
ask: value.ask
}) : null;
const repair = exactMode ? `Replace security/ask with mode="${exactMode}" (the equivalent of security="${value.security}" + ask="${value.ask}").` : value.security !== void 0 && value.ask !== void 0 ? "This security/ask pair has no exact mode equivalent. To keep this policy, retain both legacy fields and remove mode." : "The legacy policy is incomplete. Choose the intended security and ask values before converting; no mode equivalent can be inferred.";
ctx.addIssue({
code: ZodIssueCode.custom,
path: ["mode"],
message: `mode cannot be combined with security or ask in the same exec object. Update the deploy script, template, or patch at this scope. ${repair} Doctor migrates supported legacy policies to mode; run "openclaw doctor --fix" only when the saved file still needs migration.`
});
}
const ToolExecSchema = strictObject(ToolExecBaseShape).superRefine(addExecPolicyModeConflictIssue).optional();
const ToolFsSchema = strictObject({
/**
* Restrict filesystem tools (read/write/edit/apply_patch) to the agent workspace directory.
* Default: false (unrestricted, matches legacy behavior).
*/
workspaceOnly: boolean().optional() }).optional();
const ToolLoopDetectionSchema = strictObject({
/** Enable tool-loop protection (default: false). */
enabled: boolean().optional() }).optional();
const ToolSearchSchema = union([boolean(), strictObject({
/** Enable compact search/call cataloging for large tool sets. */
enabled: boolean().optional(),
/** Exposed model surface. "tools" exposes structured search/describe/call tools; "directory" keeps a bounded directory plus selected schemas visible while deferring the rest behind search/describe/call. */
mode: _enum(["tools", "directory"]).optional(),
/** Default search result count when the model omits a limit. Runtime clamps to maxSearchLimit. */
searchDefaultLimit: number().int().positive().optional(),
/** Maximum search result count. Runtime clamps to 1..50. */
maxSearchLimit: number().int().positive().optional()
})]).optional();
const CodeModeSchema = union([
boolean(),
literal("auto"),
strictObject({
/** Explicit object-form activation. Omitted stays off; "auto" engages catalog-preferred models. A completely absent global codeMode setting defaults separately to auto. */
enabled: union([boolean(), literal("auto")]).optional(),
/** Executor. Node is the default; QuickJS provides a separate WASM guest. */
executor: _enum(["node", "quickjs"]).optional(),
/** Model-facing mode. Only "only" is supported: expose exec/wait and hide normal tools. */
mode: literal("only").optional(),
/** Wall-clock limit in milliseconds for one exec or wait call. */
timeoutMs: number().int().positive().optional(),
/** QuickJS guest heap limit or best-effort Node worker V8 heap budget in bytes; excludes external buffers and process RSS. */
memoryLimitBytes: number().int().positive().optional(),
/** Maximum serialized output bytes. */
maxOutputBytes: number().int().positive().optional(),
/** Maximum serialized snapshot bytes. */
maxSnapshotBytes: number().int().positive().optional(),
/** Maximum concurrent nested tool calls. */
maxPendingToolCalls: number().int().positive().optional(),
/** Retention for suspended snapshots. */
snapshotTtlSeconds: number().int().positive().optional(),
/** Default search result count for catalog.search. */
searchDefaultLimit: number().int().positive().optional(),
/** Maximum search result count for catalog.search. */
maxSearchLimit: number().int().positive().optional()
})
]).optional();
const SwarmSchema = union([boolean(), strictObject({
/** Enable collector-mode subagents and agents_wait. Default: true. */
enabled: boolean().optional(),
/** Maximum concurrently running collector children per swarm group. */
maxConcurrent: number().int().positive().optional(),
/** Maximum live collector children per swarm group. */
maxChildrenPerGroup: number().int().positive().optional(),
/** Maximum lifetime collector spawns per swarm group. */
maxTotalPerGroup: number().int().positive().optional(),
/** Maximum agents_wait timeout in seconds. */
waitTimeoutSecondsMax: number().int().positive().optional(),
/** Default child agent id when sessions_spawn omits agentId. */
defaultAgentId: string().optional()
})]).optional();
const SandboxSshSchema = strictObject({
target: string().min(1).optional(),
command: string().min(1).optional(),
workspaceRoot: string().min(1).optional(),
strictHostKeyChecking: boolean().optional(),
updateHostKeys: boolean().optional(),
identityFile: string().min(1).optional(),
certificateFile: string().min(1).optional(),
knownHostsFile: string().min(1).optional(),
identityData: SecretInputSchema.optional().register(sensitive),
certificateData: SecretInputSchema.optional().register(sensitive),
knownHostsData: SecretInputSchema.optional().register(sensitive)
}).optional();
const AgentSandboxSchema = strictObject({
mode: union([
literal("off"),
literal("non-main"),
literal("all")
]).optional(),
backend: string().min(1).optional(),
workspaceAccess: union([
literal("none"),
literal("ro"),
literal("rw")
]).optional(),
sessionToolsVisibility: union([literal("spawned"), literal("all")]).optional(),
scope: union([
literal("session"),
literal("agent"),
literal("shared")
]).optional(),
workspaceRoot: string().optional(),
docker: SandboxDockerSchema,
ssh: SandboxSshSchema,
browser: SandboxBrowserSchema,
prune: SandboxPruneSchema
}).superRefine((data, ctx) => {
if (getBlockedNetworkModeReason({
network: data.browser?.network,
allowContainerNamespaceJoin: data.docker?.dangerouslyAllowContainerNamespaceJoin === true
}) === "container_namespace_join") ctx.addIssue({
code: ZodIssueCode.custom,
path: ["browser", "network"],
message: "Sandbox security: browser network mode \"container:*\" is blocked by default. Set sandbox.docker.dangerouslyAllowContainerNamespaceJoin=true only when you fully trust this runtime."
});
}).optional();
const CommonToolPolicyFields = {
/** Base tool profile applied before allow/deny lists. */
profile: ToolProfileSchema,
...ToolPolicyBaseSchema.shape,
/** Optional tool policy overrides keyed by provider id or "provider/model". */
byProvider: record(string(), ToolPolicyWithProfileSchema).optional(),
/** Per-sender tool policy overrides keyed by sender identity. */
toolsBySender: ToolPolicyBySenderSchema
};
const NestedToolPolicySchema = strictObject({ tools: ToolPolicySchema$1 }).optional();
const ElevatedToolsSchema = strictObject({
/** Enable or disable elevated mode (default: true). */
enabled: boolean().optional(),
/** Approved senders for /elevated (per-provider allowlists). */
allowFrom: ElevatedAllowFromSchema
}).optional();
const MessageToolConfigSchema = strictObject({
crossContext: strictObject({
/** Allow sends to other channels within the same provider (default: true). */
allowWithinProvider: boolean().optional(),
/** Allow sends across different providers (default: true). */
allowAcrossProviders: boolean().optional(),
/** Cross-context marker configuration. */
marker: strictObject({
/** Enable origin markers for cross-context sends (default: true). */
enabled: boolean().optional(),
/** Text prefix template, supports {channel}. */
prefix: string().optional(),
/** Text suffix template, supports {channel}. */
suffix: string().optional()
}).optional()
}).optional(),
actions: strictObject({
/** Message action names exposed and accepted by the message tool. */
allow: array(string()).optional() }).optional(),
broadcast: strictObject({
/** Enable broadcast action (default: true). */
enabled: boolean().optional() }).optional()
}).optional();
const GitHubToolIdentitySchema = strictObject({
/** Opaque generated directory version for atomic credential rotation. */
profileId: string().regex(MANAGED_GITHUB_PROFILE_ID_PATTERN),
/** OAuth generations retain a separate rotating refresh credential. */
kind: literal("oauth").optional(),
/** Optional process-local author identity for commits made by local tools. */
gitAuthor: strictObject({
name: string().trim().min(1).optional(),
email: string().trim().min(1).optional()
}).optional()
}).optional();
const AgentToolsSchema = strictObject({
...CommonToolPolicyFields,
/** Per-agent code mode override; merges over the top-level tools.codeMode config. */
codeMode: CodeModeSchema,
/** Per-agent swarm override; merges over the top-level tools.swarm config. */
swarm: SwarmSchema,
/** Per-agent elevated exec gate (can only further restrict global tools.elevated). */
elevated: ElevatedToolsSchema,
/** Exec tool defaults for this agent. */
exec: ToolExecSchema,
/** Complete per-agent GitHub CLI identity and Git author override. */
github: GitHubToolIdentitySchema.unwrap().extend({
/** Explicitly expose this agent's managed identity inside its own sandbox (default: false). */
allowInSandbox: boolean().optional() }).optional(),
/** Filesystem tool path guards. */
fs: ToolFsSchema,
/** Runtime loop detection for repetitive/ stuck tool-call patterns. */
loopDetection: ToolLoopDetectionSchema,
/** Message tool configuration for this agent. */
message: MessageToolConfigSchema,
sandbox: NestedToolPolicySchema
}).superRefine((value, ctx) => {
addAllowAlsoAllowConflictIssue(value, ctx, "agent tools cannot set both allow and alsoAllow in the same scope (merge alsoAllow into allow, or remove allow and use profile + alsoAllow)");
}).optional();
AgentEntryBaseSchema.extend({
memory: strictObject({ search: MemorySearchSchema }).optional(),
humanDelay: HumanDelaySchema.optional(),
typingMode: TypingModeSchema.optional(),
tts: AgentTtsConfigSchema,
skillsLimits: AgentSkillsLimitsSchema,
contextLimits: AgentContextLimitsSchema,
heartbeat: HeartbeatSchema,
identity: IdentitySchema,
groupChat: GroupChatSchema.unwrap().omit({ visibleReplies: true }).optional(),
sandbox: AgentSandboxSchema,
tools: AgentToolsSchema
}).strict();
strictObject({
...CommonToolPolicyFields,
web: ToolsWebSchema,
/** Managed local GitHub CLI identity and Git author; never overrides Git transport. */
github: GitHubToolIdentitySchema,
media: ToolsMediaSchema,
links: ToolsLinksSchema,
/**
* Session tool visibility controls which sessions can be targeted by session tools
* (sessions_list, sessions_history, sessions_search, sessions_send, session_status).
*
* Default: "all" (all sessions on the Gateway, with cross-agent access scoped by agentToAgent).
*/
sessions: strictObject({
/**
* - "self": only the current session
* - "tree": current session + sessions spawned by this session
* - "agent": any session belonging to the current agent id (can include other users)
* - "all": any session (default; cross-agent access is governed by tools.agentToAgent)
*/
visibility: _enum([
"self",
"tree",
"agent",
"all"
]).optional() }).optional(),
loopDetection: ToolLoopDetectionSchema,
/** Compact large OpenClaw, MCP, and client tool catalogs behind search/call tools. */
toolSearch: ToolSearchSchema,
/** Global Code Mode defaults and limits; agent/model settings can override activation. */
codeMode: CodeModeSchema,
/** Collector-mode subagents and wait controls. */
swarm: SwarmSchema,
/** Message tool configuration. */
message: MessageToolConfigSchema,
agentToAgent: strictObject({
/** Default: true. False blocks ordinary cross-agent session tool access; requester-owned native subagent and ACP child sessions remain reachable under tree/all visibility. */
enabled: boolean().optional(),
/**
* Agent ids or `*` glob patterns; the requesting and target agent must both match.
* Omitted or empty counts as unset: every agent pair is allowed by default; blank entries deny.
*/
allow: array(string()).optional()
}).optional(),
/** Elevated exec permissions for the host machine. */
elevated: ElevatedToolsSchema,
/** Exec tool defaults. */
exec: ToolExecSchema,
fs: ToolFsSchema,
/** Sub-agent tool policy defaults (deny wins; progress_card is always denied). */
subagents: NestedToolPolicySchema,
/** Sandbox tool policy defaults (deny wins). */
sandbox: NestedToolPolicySchema,
/** sessions_spawn tool configuration. */
sessions_spawn: strictObject({ attachments: strictObject({
/** Enable inline attachments for sessions_spawn. */
enabled: boolean().optional(),
maxTotalBytes: number().optional(),
maxFiles: number().optional(),
maxFileBytes: number().optional(),
retainOnSessionKeep: boolean().optional()
}).optional() }).optional(),
/** Unified progress_card status tool for parent sessions; enabled by default. False opts out. */
updatePlan: boolean().optional()
}).superRefine((value, ctx) => {
addAllowAlsoAllowConflictIssue(value, ctx, "tools cannot set both allow and alsoAllow in the same scope (merge alsoAllow into allow, or remove allow and use profile + alsoAllow)");
}).optional();
//#endregion
//#region src/infra/lru-cache.ts
/** Process-local LRU storage; callers own freshness and lifecycle. */
var LruCache = class {
#maxEntries;
#maxBytes;
#sizeOf;
#entries = /* @__PURE__ */ new Map();
#bytes = 0;
constructor(maxEntries, options) {
this.#maxEntries = Number.isFinite(maxEntries) && maxEntries > 0 ? Math.max(1, Math.floor(maxEntries)) : 1;
this.#maxBytes = options?.maxBytes ?? Number.POSITIVE_INFINITY;
this.#sizeOf = options?.sizeOf;
}
get size() {
return this.#entries.size;
}
clear() {
this.#entries.clear();
this.#bytes = 0;
}
keys() {
return this.#entries.keys();
}
delete(cacheKey) {
if (!this.#entries.has(cacheKey)) return false;
this.#bytes -= this.#sizeOf?.(this.#entries.get(cacheKey)) ?? 0;
return this.#entries.delete(cacheKey);
}
deleteValue(value) {
for (const [key, entry] of this.#entries) if (entry === value) this.delete(key);
}
/** Reads without promoting a value whose revalidation is still pending. */
peek(cacheKey) {
return this.#entries.get(cacheKey);
}
/** Returns a cached value and refreshes its recency when present. */
get(cacheKey) {
if (!this.#entries.has(cacheKey)) return;
const cached = this.#entries.get(cacheKey);
this.#entries.delete(cacheKey);
this.#entries.set(cacheKey, cached);
return cached;
}
/** Stores a value as the newest entry and evicts oldest entries past capacity. */
set(cacheKey, value) {
this.delete(cacheKey);
this.#entries.set(cacheKey, value);
this.#bytes += this.#sizeOf?.(value) ?? 0;
for (const key of this.#entries.keys()) {
if (this.#entries.size <= this.#maxEntries && this.#bytes <= this.#maxBytes) break;
this.delete(key);
}
}
};
new LruCache(512);
//#endregion
//#region src/channels/plugins/config-schema.ts
/** Shared allowlist entry shape for channel sender/user ids. */
const AllowFromEntrySchema = union([string(), number()]);
/** Optional allowlist array used by channel config schema builders. */
const AllowFromListSchema = array(AllowFromEntrySchema).optional();
/** Canonical per-group/room channel policy shape. */
const ChannelGroupEntrySchema = object({
requireMention: boolean().optional(),
tools: ToolPolicySchema$1,
toolsBySender: record(string(), ToolPolicySchema$1).optional(),
skills: array(string()).optional(),
enabled: boolean().optional(),
allowFrom: AllowFromListSchema,
systemPrompt: string().optional()
}).strict();
/** Extend the canonical group/room policy shape with channel-owned fields. */
function buildGroupEntrySchema(extraShape, options) {
const omitted = new Set(options?.omit ?? []);
const baseShape = Object.fromEntries(Object.entries(ChannelGroupEntrySchema.shape).filter(([key]) => !omitted.has(key)));
return object({
...baseShape,
...extraShape ?? {}
}).strict();
}
/** Add the standard accounts/defaultAccount envelope and optional shared account/root refinement. */
function buildMultiAccountChannelSchema(baseSchema, options = {}) {
const refine = options.refine;
const rawAccountSchema = options.accountSchema ?? baseSchema;
const accountSchema = refine ? rawAccountSchema.superRefine((value, ctx) => {
return refine(value, ctx);
}) : rawAccountSchema;
const accountValueSchema = options.optionalAccount ? accountSchema.optional() : accountSchema;
const accountsSchema = options.accountsMode === "catchall" ? object({}).catchall(accountValueSchema).optional() : record(string(), accountValueSchema).optional();
const channelSchema = baseSchema.extend({
accounts: accountsSchema,
defaultAccount: string().optional()
});
return refine ? channelSchema.superRefine((value, ctx) => {
return refine(value, ctx);
}) : channelSchema;
}
function cloneRuntimeIssue(issue) {
const record = issue && typeof issue === "object" ? issue : {};
const path = Array.isArray(record.path) ? record.path.filter((segment) => {
const kind = typeof segment;
return kind === "string" || kind === "number";
}) : void 0;
return {
...record,
...path ? { path } : {}
};
}
function safeParseRuntimeSchema(schema, value) {
const result = schema.safeParse(value);
if (result.success) return {
success: true,
data: result.data
};
return {
success: false,
issues: result.error.issues.map((issue) => cloneRuntimeIssue(issue))
};
}
/** Build a channel config schema from Zod, exporting JSON Schema when available. */
function buildChannelConfigSchema(schema, options) {
return {
schema: "_zod" in schema ? toJSONSchema(schema, {
target: "draft-07",
...options?.jsonSchemaMode ? { io: options.jsonSchemaMode } : {},
unrepresentable: "any"
}) : {
type: "object",
additionalProperties: true
},
...options?.uiHints ? { uiHints: options.uiHints } : {},
runtime: { safeParse: (value) => safeParseRuntimeSchema(schema, value) }
};
}
union([boolean(), literal("auto")]);
const ExecApprovalForwardTargetSchema = strictObject({
channel: string().min(1),
to: string().min(1),
accountId: string().optional(),
threadId: union([string(), number()]).optional()
});
const ExecApprovalForwardingSchema = strictObject({
enabled: boolean().optional(),
mode: union([
literal("session"),
literal("targets"),
literal("both")
]).optional(),
agentFilter: array(string()).optional(),
sessionFilter: array(string()).optional(),
targets: array(ExecApprovalForwardTargetSchema).optional()
}).optional();
const SlackPluginApproverSchema = string().regex(/^(?:team:T[A-Z0-9]+:user:)?[UW][A-Z0-9]+$/i);
const PluginSlackApproversSchema = strictObject({
approvers: array(SlackPluginApproverSchema).optional(),
plugins: record(string(), strictObject({
approvers: array(SlackPluginApproverSchema).optional(),
tools: record(string(), strictObject({ approvers: array(SlackPluginApproverSchema) })).optional()
})).optional()
});
const PluginApprovalConfigSchema = ExecApprovalForwardingSchema.unwrap().extend({ slack: PluginSlackApproversSchema.optional() }).optional();
strictObject({
exec: ExecApprovalForwardingSchema,
plugin: PluginApprovalConfigSchema
}).optional();
//#endregion
//#region src/config/zod-schema.channels.ts
/** Optional heartbeat visibility controls shared by channel schemas. */
const ChannelHeartbeatVisibilitySchema$1 = strictObject({
showOk: boolean().optional(),
showAlerts: boolean().optional(),
useIndicator: boolean().optional()
}).optional();
const ChannelHealthMonitorSchema = strictObject({ enabled: boolean().optional() }).optional();
strictObject({
enabled: boolean().optional(),
maxEventsPerWindow: number().int().positive().optional(),
windowSeconds: number().int().positive().optional(),
cooldownSeconds: number().int().positive().optional()
});
//#endregion
//#region src/config/zod-schema.channel-messaging-common.ts
const UnifiedStreamingModeSchema = _enum([
"off",
"partial",
"block",
"progress"
]);
strictObject({
enabled: boolean().optional(),
idleHours: number().nonnegative().optional(),
maxAgeHours: number().nonnegative().optional(),
spawnSessions: boolean().optional(),
defaultSpawnContext: _enum(["isolated", "fork"]).optional()
});
const ChannelStreamingPreviewSchema = strictObject({
chunk: BlockStreamingChunkSchema.optional(),
toolProgress: boolean().optional(),
commandText: _enum(["raw", "status"]).optional()
});
const ChannelStreamingProgressSchema = strictObject({
label: union([string(), literal(false)]).optional(),
labels: array(string()).optional(),
maxLines: number().int().positive().optional(),
maxLineChars: number().int().positive().optional(),
toolProgress: boolean().optional(),
commandText: _enum(["raw", "status"]).optional(),
commentary: boolean().optional(),
narration: boolean().optional()
});
strictObject({
mode: UnifiedStreamingModeSchema.optional(),
chunkMode: TextChunkModeSchema.optional(),
preview: ChannelStreamingPreviewSchema.optional(),
progress: ChannelStreamingProgressSchema.optional(),
block: ChannelStreamingBlockSchema.optional()
});
const CommonCapabilitiesSchema = array(string()).optional();
const CommonIdListSchema = array(union([string(), number()])).optional();
const CommonDefaultToSchema = string().optional();
const CommonMentionPatternsSchema = MentionPatternsPolicySchema.optional();
const CommonStreamingSchema = ChannelDeliveryStreamingConfigSchema.optional();
const CommonMediaMaxMbSchema = number().positive().optional();
const CommonReplyToModeSchema = ReplyToModeSchema.optional();
DmPolicySchema.optional().default("pairing"), GroupPolicySchema.optional().default("allowlist");
strictObject({
name: string().optional(),
capabilities: CommonCapabilitiesSchema,
markdown: MarkdownConfigSchema$1,
configWrites: boolean().optional(),
enabled: boolean().optional(),
dmPolicy: DmPolicySchema.optional(),
allowFrom: CommonIdListSchema,
defaultTo: CommonDefaultToSchema,
groupAllowFrom: CommonIdListSchema,
groupPolicy: GroupPolicySchema.optional(),
mentionPatterns: CommonMentionPatternsSchema,
contextVisibility: ContextVisibilityModeSchema.optional(),
historyLimit: number().int().min(0).optional(),
dmHistoryLimit: number().int().min(0).optional(),
dms: record(string(), DmConfigSchema$1.optional()).optional(),
textChunkLimit: number().int().positive().optional(),
streaming: CommonStreamingSchema,
heartbeatVisibility: ChannelHeartbeatVisibilitySchema$1,
healthMonitor: ChannelHealthMonitorSchema,
responsePrefix: string().optional(),
mediaMaxMb: CommonMediaMaxMbSchema,
replyToMode: CommonReplyToModeSchema
});
boolean().optional();
boolean().optional();
strictObject({
replyToBot: boolean().optional(),
quotedBot: boolean().optional(),
threadParticipation: boolean().optional()
});
//#endregion
//#region extensions/feishu/src/external-keys.ts
const FEISHU_EXTERNAL_KEY_PATTERN = /^(?!\s)(?![\s\S]*\s$)(?![\s\S]*\.\.)[^\p{Cc}\p{Cs}/\\]{1,512}$/u;
//#endregion
//#region extensions/feishu/src/webhook-path.ts
const DEFAULT_FEISHU_WEBHOOK_PATH = "/feishu/events";
/** Normalize trusted configuration only; incoming request targets must remain unmodified. */
function normalizeFeishuWebhookPath(value) {
const configured = value?.trim();
if (!configured) return DEFAULT_FEISHU_WEBHOOK_PATH;
try {
const parsed = new URL(configured, "http://localhost");
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
const emptyQuery = !parsed.search && parsed.href.endsWith("?") && !configured.includes("#") ? "?" : "";
return `${parsed.pathname}${parsed.search}${emptyQuery}`;
} catch {
return null;
}
}
//#endregion
//#region extensions/feishu/src/config-schema.ts
const ChannelActionsSchema = object({
reactions: boolean().optional(),
sticker: boolean().optional()
}).strict().optional();
const MAX_STICKER_SETS = 32;
const MAX_STICKERS_PER_SET = 256;
function canonicalTextPattern(maxLength) {
return new RegExp(`^(?!\\s)(?![\\s\\S]*\\s$)[^\\p{Cs}]{1,${maxLength}}$`, "u");
}
const FeishuStickerSetSchema = record(string().regex(FEISHU_EXTERNAL_KEY_PATTERN), array(string().regex(canonicalTextPattern(64))).min(1).max(8)).refine((set) => Object.keys(set).length <= MAX_STICKERS_PER_SET, { message: `At most ${MAX_STICKERS_PER_SET} stickers per bot set are allowed` }).meta({ maxProperties: MAX_STICKERS_PER_SET });
const FeishuStickerSetsSchema = record(string().regex(canonicalTextPattern(128)), FeishuStickerSetSchema).refine((sets) => Object.keys(sets).length <= MAX_STICKER_SETS, { message: `At most ${MAX_STICKER_SETS} bot sticker sets are allowed` }).meta({ maxProperties: MAX_STICKER_SETS });
const FeishuGroupPolicySchema = union([GroupPolicySchema, literal("allowall").transform(() => "open")]);
const FeishuDomainSchema = union([_enum(["feishu", "lark"]), string().regex(/^[Hh][Tt][Tt][Pp][Ss]:\/\//).url()]);
const FeishuConnectionModeSchema = _enum(["websocket", "webhook"]);
const FeishuWebhookPathSchema = string().refine((value) => normalizeFeishuWebhookPath(value) === value, { message: "webhookPath must be a canonical HTTP request path; run \"openclaw doctor --fix\" to repair it" });
const TtsOverrideSchema = object({
auto: _enum([
"off",
"always",
"inbound",
"tagged"
]).optional(),
enabled: boolean().optional(),
mode: _enum(["final", "all"]).optional(),
provider: string().optional(),
persona: string().optional(),
personas: record(string(), record(string(), unknown())).optional(),
summaryModel: string().optional(),
modelOverrides: record(string(), unknown()).optional(),
providers: record(string(), record(string(), unknown())).optional(),
prefsPath: string().optional(),
maxTextLength: number().int().min(1).optional(),
timeoutMs: number().int().min(1e3).max(12e4).optional()
}).strict().optional();
const ToolPolicySchema = object({
allow: array(string()).optional(),
deny: array(string()).optional()
}).strict().optional();
const DmConfigSchema = object({
enabled: boolean().optional(),
systemPrompt: string().optional()
}).strict().optional();
const MarkdownConfigSchema = object({
mode: _enum([
"native",
"escape",
"strip"
]).optional(),
tableMode: _enum([
"native",
"ascii",
"simple"
]).optional()
}).strict().optional();
const RenderModeSchema = _enum([
"auto",
"raw",
"card"
]).optional();
const FeishuStreamingSchema = object({
mode: _enum(["off", "partial"]).optional(),
...ChannelDeliveryStreamingConfigSchema.shape
}).strict().optional();
const ChannelHeartbeatVisibilitySchema = object({
visibility: _enum(["visible", "hidden"]).optional(),
intervalMs: number().int().positive().optional()
}).strict().optional();
/**
* Dynamic agent creation configuration.
* When enabled, a new agent is created for each unique DM user.
*/
const DynamicAgentCreationSchema = object({
enabled: boolean().optional(),
workspaceTemplate: string().optional(),
agentDirTemplate: string().optional(),
maxAgents: number().int().positive().optional()
}).strict().optional();
/**
* Feishu tools configuration.
* Controls which tool categories are enabled.
*
* Dependencies:
* - wiki requires doc (wiki content is edited via doc tools)
* - perm can work independently but is typically used with drive
*/
const FeishuToolsConfigSchema = object({
doc: boolean().optional(),
chat: boolean().optional(),
wiki: boolean().optional(),
drive: boolean().optional(),
perm: boolean().optional(),
scopes: boolean().optional(),
bitable: boolean().optional()
}).strict().optional();
/**
* Group session scope for routing Feishu group messages.
* - "group" (default): one session per group chat
* - "group_sender": one session per (group + sender)
* - "group_topic": one session per group topic thread (falls back to group if no topic)
* - "group_topic_sender": one session per (group + topic thread + sender),
* falls back to (group + sender) if no topic
*/
const GroupSessionScopeSchema = _enum([
"group",
"group_sender",
"group_topic",
"group_topic_sender"
]).optional();
/**
* @deprecated Use groupSessionScope instead.
*
* Topic session isolation mode for group chats.
* - "disabled" (default): All messages in a group share one session
* - "enabled": Messages in different topics get separate sessions
*
* Topic routing uses Feishu topic-group `thread_id` when the event identifies a
* native topic group, and keeps `root_id` precedence for normal groups so
* reply-created threads stay on the initiating message session.
*/
const TopicSessionModeSchema = _enum(["disabled", "enabled"]).optional();
const ReactionNotificationModeSchema = _enum([
"off",
"own",
"all"
]).optional();
/**
* Reply-in-thread mode for group chats.
* - "disabled" (default): Bot replies are normal inline replies
* - "enabled": Bot replies create or continue a Feishu topic thread
*
* When enabled, the Feishu reply API is called with `reply_in_thread: true`,
* causing the reply to appear as a topic (话题) under the original message.
*/
const ReplyInThreadSchema = _enum(["disabled", "enabled"]).optional();
const RequireMentionInBotThreadsSchema = boolean().optional().describe("Require mentions in threads started by this bot. False permits unmentioned messages; true requires a mention. Omit to preserve existing mention behavior.");
const FeishuGroupSchema = buildGroupEntrySchema({
tools: ToolPolicySchema,
requireMentionInBotThreads: RequireMentionInBotThreadsSchema,
groupSessionScope: GroupSessionScopeSchema,
topicSessionMode: TopicSessionModeSchema,
replyInThread: ReplyInThreadSchema
}).omit({ toolsBySender: true });
const FeishuSharedConfigShape = {
legacyWebhook: object({
port: number().int().min(1).max(65535),
host: string().optional()
}).strict().or(literal(false)).optional(),
capabilities: array(string()).optional(),
markdown: MarkdownConfigSchema,
configWrites: boolean().optional(),
contextVisibility: ContextVisibilityModeSchema.optional(),
replyToMode: ReplyToModeSchema.optional(),
responsePrefix: string().optional(),
dmPolicy: DmPolicySchema.optional(),
allowFrom: array(union([string(), number()])).optional(),
groupPolicy: FeishuGroupPolicySchema.optional(),
groupAllowFrom: array(union([string(), number()])).optional(),
groupSenderAllowFrom: array(union([string(), number()])).optional(),
requireMention: boolean().optional(),
requireMentionInBotThreads: RequireMentionInBotThreadsSchema,
groups: record(string(), FeishuGroupSchema.optional()).optional(),
historyLimit: number().int().min(0).optional(),
dmHistoryLimit: number().int().min(0).optional(),
dms: record(string(), DmConfigSchema).optional(),
textChunkLimit: number().int().positive().optional(),
mediaMaxMb: number().positive().optional(),
httpTimeoutMs: number().int().positive().max(3e5).optional(),
heartbeatVisibility: ChannelHeartbeatVisibilitySchema,
renderMode: RenderModeSchema,
streaming: FeishuStreamingSchema,
tools: FeishuToolsConfigSchema,
actions: ChannelActionsSchema,
replyInThread: ReplyInThreadSchema,
reactionNotifications: ReactionNotificationModeSchema,
typingIndicator: boolean().optional(),
resolveSenderNames: boolean().optional(),
allowBots: boolean().optional(),
vcAutoJoin: boolean().optional(),
tts: TtsOverrideSchema
};
/**
* Per-account configuration.
* All fields are optional - missing fields inherit from top-level config.
*/
const FeishuAccountConfigSchema = object({
enabled: boolean().optional(),
name: string().optional(),
appId: string().optional(),
appSecret: buildSecretInputSchema().optional(),
encryptKey: buildSecretInputSchema().optional(),
verificationToken: buildSecretInputSchema().optional(),
domain: FeishuDomainSchema.optional(),
connectionMode: FeishuConnectionModeSchema.optional(),
webhookPath: FeishuWebhookPathSchema.optional(),
...FeishuSharedConfigShape,
groupSessionScope: GroupSessionScopeSchema,
topicSessionMode: TopicSessionModeSchema
}).strict();
const FeishuConfigSchema = buildMultiAccountChannelSchema(object({
enabled: boolean().optional(),
defaultAccount: string().optional(),
stickerSets: FeishuStickerSetsSchema.optional(),
appId: string().optional(),
appSecret: buildSecretInputSchema().optional(),
encryptKey: buildSecretInputSchema().optional(),
verificationToken: buildSecretInputSchema().optional(),
domain: FeishuDomainSchema.optional().default("feishu"),
connectionMode: FeishuConnectionModeSchema.optional().default("websocket"),
webhookPath: FeishuWebhookPathSchema.optional().default(DEFAULT_FEISHU_WEBHOOK_PATH),
...FeishuSharedConfigShape,
dmPolicy: DmPolicySchema.optional().default("pairing"),
reactionNotifications: ReactionNotificationModeSchema.optional().default("own"),
groupPolicy: FeishuGroupPolicySchema.optional().default("allowlist"),
requireMention: boolean().optional(),
groupSessionScope: GroupSessionScopeSchema,
topicSessionMode: TopicSessionModeSchema,
dynamicAgentCreation: DynamicAgentCreationSchema,
typingIndicator: boolean().optional().default(true),
resolveSenderNames: boolean().optional().default(true)
}).strict(), {
accountSchema: FeishuAccountConfigSchema,
optionalAccount: true
}).superRefine((value, ctx) => {
const defaultAccount = value.defaultAccount?.trim();
if (defaultAccount && value.accounts && Object.keys(value.accounts).length > 0) {
const normalizedDefaultAccount = normalizeAccountId(defaultAccount);
if (!Object.hasOwn(value.accounts, normalizedDefaultAccount)) ctx.addIssue({
code: ZodIssueCode.custom,
path: ["defaultAccount"],
message: `channels.feishu.defaultAccount="${defaultAccount}" does not match a configured account key`
});
}
const defaultConnectionMode = value.connectionMode ?? "websocket";
const defaultVerificationTokenConfigured = hasConfiguredSecretInput(value.verificationToken);
const defaultEncryptKeyConfigured = hasConfiguredSecretInput(value.encryptKey);
if (defaultConnectionMode === "webhook") {
if (!defaultVerificationTokenConfigured) ctx.addIssue({
code: ZodIssueCode.custom,
path: ["verificationToken"],
message: "channels.feishu.connectionMode=\"webhook\" requires channels.feishu.verificationToken"
});
if (!defaultEncryptKeyConfigured) ctx.addIssue({
code: ZodIssueCode.custom,
path: ["encryptKey"],
message: "channels.feishu.connectionMode=\"webhook\" requires channels.feishu.encryptKey"
});
}
for (const [accountId, account] of Object.entries(value.accounts ?? {})) {
if (!account) continue;
if ((account.connectionMode ?? defaultConnectionMode) !== "webhook") continue;
const accountVerificationTokenConfigured = hasConfiguredSecretInput(account.verificationToken) || defaultVerificationTokenConfigured;
const accountEncryptKeyConfigured = hasConfiguredSecretInput(account.encryptKey) || defaultEncryptKeyConfigured;
if (!accountVerificationTokenConfigured) ctx.addIssue({
code: ZodIssueCode.custom,
path: [
"accounts",
accountId,
"verificationToken"
],
message: `channels.feishu.accounts.${accountId}.connectionMode="webhook" requires a verificationToken (account-level or top-level)`
});
if (!accountEncryptKeyConfigured) ctx.addIssue({
code: ZodIssueCode.custom,
path: [
"accounts",
accountId,
"encryptKey"
],
message: `channels.feishu.accounts.${accountId}.connectionMode="webhook" requires an encryptKey (account-level or top-level)`
});
}
if (value.dmPolicy === "open") {
if (!(value.allowFrom ?? []).some((entry) => String(entry).trim() === "*")) ctx.addIssue({
code: ZodIssueCode.custom,
path: ["allowFrom"],
message: "channels.feishu.dmPolicy=\"open\" requires channels.feishu.allowFrom to include \"*\""
});
}
});
buildChannelConfigSchema(FeishuConfigSchema, { jsonSchemaMode: "input" });
//#endregion
//#region extensions/feishu/src/doctor-contract.ts
const webhookListenerMigration = createLegacyWebhookListenerDoctorContract({
channelKey: "feishu",
defaultPort: 3e3,
defaultHost: "127.0.0.1"
});
const { historicalWebhookListener } = webhookListenerMigration;
const streamingAliasMigration = defineChannelAliasMigration({
channelId: "feishu",
streaming: { defaultMode: "partial" },
accountStreamingReplacesRoot: true
});
const LEGACY_COALESCE_FIELDS = [
"enabled",
"minDelayMs",
"maxDelayMs"
];
const LEGACY_HEARTBEAT_FIELDS = ["visibility", "intervalMs"];
const toolsBaseMigration = defineKeyMoveMigration({
from: ["tools", "base"],
to: ["tools", "bitable"],
match: (value) => typeof value === "boolean",
sourceOwn: false
});
function sanitizeLegacyHeartbeatFields(params) {
const heartbeat = asNullableRecord(params.entry.heartbeat);
if (!heartbeat || Object.keys(heartbeat).length > 0 && !LEGACY_HEARTBEAT_FIELDS.some((field) => Object.hasOwn(heartbeat, field))) return {
entry: params.entry,
changed: false
};
const next = { ...params.entry };
delete next.heartbeat;
params.changes.push(`Removed ${params.pathPrefix}.heartbeat (legacy Feishu fields were never read by runtime).`);
return {
entry: next,
changed: true
};
}
function sanitizeLegacyCoalesceFields(params) {
const streaming = asNullableRecord(params.entry.streaming);
const block = asNullableRecord(streaming?.block);
const coalesce = asNullableRecord(block?.coalesce);
if (!streaming || !block || !coalesce) return {
entry: params.entry,
changed: false
};
const removed = LEGACY_COALESCE_FIELDS.filter((field) => coalesce[field] !== void 0);
if (removed.length === 0) return {
entry: params.entry,
changed: false
};
const nextCoalesce = { ...coalesce };
for (const field of removed) delete nextCoalesce[field];
params.changes.push(`Removed ${params.pathPrefix}.streaming.block.coalesce.{${removed.join(",")}} (legacy Feishu-only fields; block delivery reads minChars/maxChars/idleMs).`);
return {
entry: {
...params.entry,
streaming: {
...streaming,
block: {
...block,
coalesce: nextCoalesce
}
}
},
changed: true
};
}
function hasLegacyWebhookPath(value) {
const path = asNullableRecord(value)?.webhookPath;
return typeof path === "string" && normalizeFeishuWebhookPath(path) !== path;
}
function normalizeLegacyWebhookPath(params) {
const path = params.entry.webhookPath;
if (typeof path !== "string") return {
entry: params.entry,
changed: false
};
const normalized = normalizeFeishuWebhookPath(path);
const canonical = normalized ?? "/feishu/events";
if (canonical === path) return {
entry: params.entry,
changed: false
};
params.changes.push(normalized === null ? `Reset invalid ${params.pathPrefix}.webhookPath to ${DEFAULT_FEISHU_WEBHOOK_PATH}.` : `Normalized ${params.pathPrefix}.webhookPath to its HTTP request path.`);
return {
entry: {
...params.entry,
webhookPath: canonical
},
changed: true
};
}
function normalizeFeishuLegacyConfigEntries(cfg, changes) {
return normalizeChannelConfigEntries({
cfg,
channelId: "feishu",
changes,
normalizeEntry: (params) => {
const tools = toolsBaseMigration.normalize(params);
const coalesce = sanitizeLegacyCoalesceFields({
...params,
entry: tools.entry
});
const heartbeat = sanitizeLegacyHeartbeatFields({
...params,
entry: coalesce.entry
});
const webhook = normalizeLegacyWebhookPath({
...params,
entry: heartbeat.entry
});
return {
entry: webhook.entry,
changed: tools.changed || coalesce.changed || heartbeat.changed || webhook.changed
};
}
}).config;
}
const feishuStrayEntryConfigMigration = defineStrayPluginEntryConfigMigration({
pluginId: "feishu",
channelId: "feishu",
validateMergedChannelConfig: (merged) => FeishuConfigSchema.safeParse(merged).success
});
const legacyConfigRules = [
...streamingAliasMigration.legacyConfigRules,
...webhookListenerMigration.legacyConfigRules,
feishuStrayEntryConfigMigration.legacyConfigRule,
{
path: ["channels", "feishu"],
message: "channels.feishu[.accounts.<id>].webhookPath must be a canonical HTTP request path; run \"openclaw doctor --fix\".",
match: (value) => {
const entry = asNullableRecord(value);
return hasLegacyWebhookPath(entry) || hasLegacyAccountStreamingAliases(entry?.accounts, hasLegacyWebhookPath);
}
},
{
path: ["channels", "feishu"],
message: "channels.feishu[.accounts.<id>].tools.base is legacy; use tools.bitable. Run \"openclaw doctor --fix\".",
match: (value) => {
const entry = asNullableRecord(value);
return toolsBaseMigration.hasLegacy(entry) || hasLegacyAccountStreamingAliases(entry?.accounts, toolsBaseMigration.hasLegacy);
}
}
];
function resolveHistoricalWebhookAccountIds(cfg) {
return cfg.channels?.feishu?.enabled === false ? [] : listFeishuAccountIds(cfg).filter((accountId) => {
const account = mergeFeishuAccountConfig(cfg, accountId);
return account.enabled !== false && account.connectionMode === "webhook";
});
}
function normalizeHistoricalWebhookConfig({ cfg }) {
const listener = webhookListenerMigration.normalizeCompatibilityConfig({ cfg });
return {
...listener,
historicalWebhookAccountIds: resolveHistoricalWebhookAccountIds(listener.config)
};
}
function normalizeCompatibilityConfig({ cfg }) {
const aliases = streamingAliasMigration.normalizeChannelConfig({ cfg });
const listener = webhookListenerMigration.normalizeCompatibilityConfig({ cfg: aliases.config });
const changes = [...aliases.changes, ...listener.changes];
const entries = normalizeFeishuLegacyConfigEntries(listener.config, changes);
const stray = feishuStrayEntryConfigMigration.normalizeConfig({ cfg: entries });
return {
config: stray.config,
changes: [...changes, ...stray.changes],
historicalWebhookAccountIds: resolveHistoricalWebhookAccountIds(stray.config)
};
}
//#endregion
export { historicalWebhookListener, legacyConfigRules, normalizeCompatibilityConfig, normalizeHistoricalWebhookConfig };