openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
2,420 lines • 92.8 kB
JavaScript
import { a as asNullableRecord, n as defineKeyMoveMigration, r as defineStrayPluginEntryConfigMigration, s as isRecord } from "./runtime-doctor-migrations-DJDQaWC5.js";
import { c as hasLegacyAccountStreamingAliases, f as uniqueStrings, h as normalizeOptionalString, m as normalizeOptionalLowercaseString, o as isBlockedObjectKey, p as normalizeLowercaseStringOrEmpty, r as normalizeChannelConfigEntries } from "./channel-doctor-helpers-CKvCIMDR.js";
import { t as defineChannelAliasMigration } from "./channel-alias-migration-Du1swkf7.js";
import { n as hasConfiguredSecretInput, t as ENV_SECRET_REF_ID_RE } from "./ansi-nho2vK_1.js";
import { n as normalizeAccountId, r as pruneMapToMaxSize } from "./fs-safe-defaults-BbLMaB4h.js";
import { _ as unknown, c as literal, d as object, f as preprocess, g as union, h as tuple, i as boolean, m as string, n as _null, o as discriminatedUnion, p as record, r as array, t as _enum, u as number, v as registry } from "./schemas-Dl-EotZT.js";
import { n as resolveGlobalSingleton } from "./plugin-cache-Dixl2R24.js";
import path from "node:path";
import milliseconds from "ms";
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.4.3/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 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/agents/sandbox/bind-spec.ts
/** Splits a bind spec while preserving Windows drive-letter prefixes in host paths. */
function splitSandboxBindSpec(spec) {
const separator = getHostContainerSeparatorIndex(spec);
if (separator === -1) return null;
const host = spec.slice(0, separator);
const rest = spec.slice(separator + 1);
const optionsStart = rest.indexOf(":");
if (optionsStart === -1) return {
host,
container: rest,
options: ""
};
return {
host,
container: rest.slice(0, optionsStart),
options: rest.slice(optionsStart + 1)
};
}
function getHostContainerSeparatorIndex(spec) {
const hasDriveLetterPrefix = /^[A-Za-z]:[\\/]/.test(spec);
for (let i = hasDriveLetterPrefix ? 2 : 0; i < spec.length; i += 1) if (spec[i] === ":") return i;
return -1;
}
//#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.trim()));
}
function isSandboxHostPathAbsolute(raw) {
const trimmed = stripWindowsNamespacePrefix(raw.trim());
return trimmed.startsWith("/") || isWindowsDriveAbsolutePath(trimmed);
}
//#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/cli/parse-duration.ts
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 parsed = milliseconds(`${value}${unit}`);
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(normalizeOptionalString(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/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 src/config/zod-schema.agent-model.ts
/** Schema for agent model config accepting a string or fallback object. */
const AgentModelSchema = union([string(), object({
primary: string().optional(),
fallbacks: array(string()).optional()
}).strict()]);
union([string(), object({
primary: string().optional(),
fallbacks: array(string()).optional(),
timeoutMs: number().int().positive().optional()
}).strict()]);
//#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) {
if (value.startsWith(".") || value.startsWith("~")) return true;
if (value.includes("/") || value.includes("\\")) return true;
return /^[A-Za-z]:[\\/]/.test(value);
}
/** Validates that a configured executable value cannot smuggle shell syntax. */
function isSafeExecutableValue(value) {
if (!value) return false;
const trimmed = value.trim();
if (!trimmed) return false;
if (trimmed.includes("\0")) return false;
if (CONTROL_CHARS.test(trimmed)) return false;
if (SHELL_METACHARS.test(trimmed)) return false;
if (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 src/secrets/ref-contract.ts
/**
* Runtime secret-reference grammar shared by config parsing, plugin SDK schemas,
* gateway parity checks, and resolver planning.
*/
const FILE_SECRET_REF_SEGMENT_PATTERN = /^(?:[^~]|~0|~1)*$/;
/** Shared alias grammar for env/file/exec/store secret provider names. */
const SECRET_PROVIDER_ALIAS_PATTERN = /^[a-z][a-z0-9_-]{0,63}$/;
const EXEC_SECRET_REF_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:/#-]{0,255}$/;
/** Validates file secret ref ids against the shared JSON-pointer-style contract. */
function isValidFileSecretRefId(value) {
if (value === "value") return true;
if (!value.startsWith("/")) return false;
return value.slice(1).split("/").every((segment) => FILE_SECRET_REF_SEGMENT_PATTERN.test(segment));
}
/** Validates exec secret ref ids and reports why invalid ids failed. */
function validateExecSecretRefId(value) {
if (!EXEC_SECRET_REF_ID_PATTERN.test(value)) return {
ok: false,
reason: "pattern"
};
for (const segment of value.split("/")) if (segment === "." || segment === "..") return {
ok: false,
reason: "traversal-segment"
};
return { ok: true };
}
/** Boolean convenience wrapper for callers that only need accept/reject behavior. */
function isValidExecSecretRefId(value) {
return validateExecSecretRefId(value).ok;
}
/** Formats the user-facing validation message for rejected exec secret ref ids. */
function formatExecSecretRefIdValidationMessage() {
return [
"Exec secret reference id must match /^[A-Za-z0-9][A-Za-z0-9._:/#-]{0,255}$/",
"and must not include \".\" or \"..\" path segments",
"(example: \"vault/openai/api-key\" or \"aws/secret#json_key\")."
].join(" ");
}
//#endregion
//#region src/config/model-provider-config.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",
"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",
"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/types.models.ts
/** Provider API adapter ids accepted by model/provider config and schema generation. */
const MODEL_APIS = [
"openai-completions",
"openai-responses",
"openai-chatgpt-responses",
"anthropic-messages",
"google-generative-ai",
"google-vertex",
"github-copilot",
"bedrock-converse-stream",
"ollama",
"azure-openai-responses"
];
/** Thinking/reasoning payload dialects emitted by OpenAI-compatible providers. */
const MODEL_THINKING_FORMATS = [
"openai",
"openrouter",
"deepseek",
"together",
"qwen",
"qwen-chat-template",
"zai"
];
//#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 object({
default: AllowDenyActionSchema.optional(),
rules: array(object({
action: AllowDenyActionSchema,
match: object({
channel: string().optional(),
chatType: AllowDenyChatTypeSchema,
keyPrefix: string().optional(),
rawKeyPrefix: string().optional()
}).strict().optional()
}).strict()).optional()
}).strict().optional();
}
//#endregion
//#region src/config/zod-schema.sensitive.ts
const sensitive = registry();
registry();
//#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);
}
const EnvSecretRefSchema = object({
source: literal("env"),
provider: string().regex(SECRET_PROVIDER_ALIAS_PATTERN, "Secret reference provider must match /^[a-z][a-z0-9_-]{0,63}$/ (example: \"default\")."),
id: string().regex(ENV_SECRET_REF_ID_RE, "Env secret reference id must match /^[A-Z][A-Z0-9_]{0,127}$/ (example: \"OPENAI_API_KEY\").")
}).strict();
const FileSecretRefSchema = object({
source: literal("file"),
provider: string().regex(SECRET_PROVIDER_ALIAS_PATTERN, "Secret reference provider must match /^[a-z][a-z0-9_-]{0,63}$/ (example: \"default\")."),
id: string().refine(isValidFileSecretRefId, "File secret reference id must be an absolute JSON pointer (example: \"/providers/openai/apiKey\"), or \"value\" for singleValue mode.")
}).strict();
const ExecSecretRefSchema = object({
source: literal("exec"),
provider: string().regex(SECRET_PROVIDER_ALIAS_PATTERN, "Secret reference provider must match /^[a-z][a-z0-9_-]{0,63}$/ (example: \"default\")."),
id: string().refine(isValidExecSecretRefId, formatExecSecretRefIdValidationMessage())
}).strict();
const StoreSecretRefSchema = object({
source: literal("store"),
provider: string().regex(SECRET_PROVIDER_ALIAS_PATTERN, "Secret reference provider must match /^[a-z][a-z0-9_-]{0,63}$/ (example: \"default\")."),
id: string().regex(ENV_SECRET_REF_ID_RE, "Store secret reference id must match /^[A-Z][A-Z0-9_]{0,127}$/ (example: \"OPENAI_API_KEY\").")
}).strict();
/** Config-level secret reference schema shared by model/provider/plugin credential fields. */
const SecretRefSchema = discriminatedUnion("source", [
EnvSecretRefSchema,
FileSecretRefSchema,
ExecSecretRefSchema,
StoreSecretRefSchema
]);
/** Accepts either legacy inline secret strings or structured secret references. */
const SecretInputSchema = union([string(), SecretRefSchema]);
/** Canonical operator-configurable SSRF policy shared by network-capable surfaces. */
const SsrFPolicyConfigSchema = object({
dangerouslyAllowPrivateNetwork: boolean().optional(),
allowRfc2544BenchmarkRange: boolean().optional(),
allowIpv6UniqueLocalRange: boolean().optional(),
allowedHostnames: array(string()).optional(),
blockedHostnames: array(string()).optional()
}).strict();
const SecretsEnvProviderSchema = object({
source: literal("env"),
allowlist: array(string().regex(ENV_SECRET_REF_ID_RE)).max(256).optional()
}).strict();
const SecretsFileProviderSchema = object({
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()
}).strict();
const SecretsManualExecProviderSchema = object({
source: literal("exec"),
command: string().min(1).refine((value) => isSafeExecutableValue(value), "secrets.providers.*.command is unsafe.").refine((value) => isAbsolutePath(value), "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((value) => isAbsolutePath(value), "trustedDirs entries must be absolute paths.")).max(64).optional()
}).strict();
const SecretsPluginIntegrationExecProviderSchema = object({
source: literal("exec"),
pluginIntegration: object({
pluginId: string().min(1).max(128),
integrationId: string().min(1).max(128)
}).strict()
}).strict();
const SecretsExecProviderSchema = union([SecretsManualExecProviderSchema, SecretsPluginIntegrationExecProviderSchema]);
const SecretsStoreProviderSchema = object({ source: literal("store") }).strict();
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
]);
object({
egressProxy: object({
enabled: boolean().optional(),
allowedHosts: array(EgressProxyExactHostSchema).max(256).optional(),
bypassHosts: array(EgressProxyExactHostSchema).max(256).optional()
}).strict().optional(),
providers: object({}).catchall(SecretProviderSchema).optional(),
defaults: object({
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()
}).strict().optional()
}).strict().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 RoutingPercentileCutoffsSchema = object({
p50: number().optional(),
p75: number().optional(),
p90: number().optional(),
p99: number().optional()
}).strict();
const OpenRouterRoutingSchema = object({
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(), object({
by: string().optional(),
partition: string().nullable().optional()
}).strict()]).optional(),
max_price: object({
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()
}).strict().optional(),
preferred_min_throughput: union([number(), RoutingPercentileCutoffsSchema]).optional(),
preferred_max_latency: union([number(), RoutingPercentileCutoffsSchema]).optional()
}).strict();
const VercelGatewayRoutingSchema = object({
only: array(string()).optional(),
order: array(string()).optional()
}).strict();
const ModelCompatSchema = object({
supportsStore: boolean().optional(),
supportsPromptCacheKey: boolean().optional(),
supportsDeveloperRole: boolean().optional(),
supportsReasoningEffort: boolean().optional(),
supportsTemperature: boolean().optional(),
supportsInstructions: boolean().optional(),
supportsUsageInStreaming: boolean().optional(),
supportsTools: boolean().optional(),
codeMode: _enum(["preferred", "capable"]).optional(),
supportsStrictMode: boolean().optional(),
supportsJsonSchemaResponseFormat: boolean().optional(),
requiresStringContent: boolean().optional(),
strictMessageKeys: boolean().optional(),
visibleReasoningDetailTypes: array(string().min(1)).optional(),
supportedReasoningEfforts: array(string().min(1)).optional(),
reasoningEffortMap: record(string().min(1), string().min(1)).optional(),
maxTokensField: union([literal("max_completion_tokens"), literal("max_tokens")]).optional(),
thinkingFormat: _enum(MODEL_THINKING_FORMATS).optional(),
requiresToolResultName: boolean().optional(),
requiresAssistantAfterToolResult: boolean().optional(),
requiresThinkingAsText: boolean().optional(),
requiresReasoningContentOnAssistantMessages: boolean().optional(),
toolSchemaProfile: string().optional(),
unsupportedToolSchemaKeywords: array(string().min(1)).optional(),
toolCallArgumentsEncoding: string().optional(),
requiresOpenAiAnthropicToolPayload: boolean().optional(),
openRouterRouting: OpenRouterRoutingSchema.optional(),
vercelGatewayRouting: VercelGatewayRoutingSchema.optional(),
zaiToolStream: boolean().optional(),
cacheControlFormat: literal("anthropic").optional(),
sendSessionAffinityHeaders: boolean().optional(),
sendSessionIdHeader: boolean().optional(),
supportsEagerToolInputStreaming: boolean().optional(),
supportsLongCacheRetention: boolean().optional()
}).strict().optional();
const ConfiguredProviderRequestTlsSchema = object({
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()
}).strict().optional();
const ConfiguredProviderRequestAuthSchema = union([
object({ mode: literal("provider-default") }).strict(),
object({
mode: literal("authorization-bearer"),
token: SecretInputSchema.register(sensitive)
}).strict(),
object({
mode: literal("header"),
headerName: string().min(1),
value: SecretInputSchema.register(sensitive),
prefix: string().optional()
}).strict()
]).optional();
const ConfiguredProviderRequestProxySchema = union([object({
mode: literal("env-proxy"),
tls: ConfiguredProviderRequestTlsSchema
}).strict(), object({
mode: literal("explicit-proxy"),
url: string().min(1),
tls: ConfiguredProviderRequestTlsSchema
}).strict()]).optional();
const ConfiguredProviderRequestFields = {
headers: record(string(), SecretInputSchema.register(sensitive)).optional(),
auth: ConfiguredProviderRequestAuthSchema,
proxy: ConfiguredProviderRequestProxySchema,
tls: ConfiguredProviderRequestTlsSchema
};
const ConfiguredProviderRequestSchema = object(ConfiguredProviderRequestFields).strict().optional();
const ConfiguredModelProviderRequestSchema = object({
...ConfiguredProviderRequestFields,
allowPrivateNetwork: boolean().optional()
}).strict().optional();
const ModelAgentRuntimePolicySchema = object({ id: string().optional() }).strict().optional();
const ModelImageInputSchema = object({
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()
}).strict();
const ModelMediaInputSchema = object({ image: ModelImageInputSchema.optional() }).strict();
const ThinkingLevelMapValueSchema = string().nullable();
const ThinkingLevelMapSchema = object({
off: ThinkingLevelMapValueSchema.optional(),
minimal: ThinkingLevelMapValueSchema.optional(),
low: ThinkingLevelMapValueSchema.optional(),
medium: ThinkingLevelMapValueSchema.optional(),
high: ThinkingLevelMapValueSchema.optional(),
xhigh: ThinkingLevelMapValueSchema.optional(),
max: ThinkingLevelMapValueSchema.optional()
}).strict();
const ModelDefinitionSchema = object({
id: string().min(1),
name: string().min(1),
api: ModelApiSchema.optional(),
baseUrl: string().min(1).optional(),
reasoning: boolean().optional(),
input: array(union([
literal("text"),
literal("image"),
literal("video"),
literal("audio")
])).optional(),
cost: object({
input: number().optional(),
output: number().optional(),
cacheRead: number().optional(),
cacheWrite: number().optional(),
tieredPricing: array(object({
input: number(),
output: number(),
cacheRead: number(),
cacheWrite: number(),
range: union([tuple([number(), number()]), tuple([number()])])
}).strict()).optional()
}).strict().optional(),
contextWindow: number().positive().optional(),
contextTokens: number().int().positive().optional(),
maxTokens: number().positive().optional(),
thinkingLevelMap: ThinkingLevelMapSchema.optional(),
params: record(string(), unknown()).optional(),
agentRuntime: ModelAgentRuntimePolicySchema,
headers: record(string(), string()).optional(),
compat: ModelCompatSchema,
mediaInput: ModelMediaInputSchema.optional(),
metadataSource: literal("models-add").optional()
}).strict();
const ModelProviderLocalServiceSchema = object({
command: string().min(1),
args: array(string()).optional(),
cwd: string().min(1).optional(),
env: record(string(), string().register(sensitive)).optional(),
healthUrl: string().min(1).optional(),
readyTimeoutMs: number().int().positive().optional(),
idleStopMs: number().int().nonnegative().optional()
}).strict().optional();
const ModelProviderSchema = object({
baseUrl: string().optional(),
apiKey: SecretInputSchema.optional().register(sensitive),
auth: union([
literal("api-key"),
literal("aws-sdk"),
literal("oauth"),
literal("token")
]).optional(),
api: ModelApiSchema.optional(),
maxTokens: number().positive().optional(),
timeoutSeconds: number().int().positive().optional(),
region: string().min(1).optional(),
injectNumCtxForOpenAICompat: boolean().optional(),
params: record(string(), unknown()).optional(),
agentRuntime: ModelAgentRuntimePolicySchema,
localService: ModelProviderLocalServiceSchema,
headers: record(string(), SecretInputSchema.register(sensitive)).optional(),
authHeader: boolean().optional(),
request: ConfiguredModelProviderRequestSchema,
models: array(ModelDefinitionSchema).optional()
}).strict();
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 = object({
enabled: boolean().optional(),
url: string().refine((value) => {
try {
const parsed = new URL(value);
return parsed.protocol === "https:" || parsed.protocol === "http:" && [
"localhost",
"127.0.0.1",
"[::1]"
].includes(parsed.hostname);
} catch {
return false;
}
}, { message: "models.catalogRefresh.url must use https, or http on localhost" }).optional()
}).strict().optional();
object({
mode: union([literal("merge"), literal("replace")]).optional(),
providers: ModelProvidersSchema.optional(),
catalogRefresh: ModelCatalogRefreshConfigSchema
}).strict().optional();
const VisibleRepliesValueSchema = _enum(["automatic", "message_tool"]);
const AmbientGroupInboundSchema = _enum(["user_request", "room_event"]);
const VisibleRepliesSchema = union([VisibleRepliesValueSchema, boolean()]).overwrite((value) => {
if (value === true) return "automatic";
if (value === false) return "message_tool";
return value;
});
const MentionPatternsModeSchema = union([literal("allow"), literal("deny")]);
const MentionPatternsPolicySchema = object({
mode: MentionPatternsModeSchema.optional(),
allowIn: array(string()).optional(),
denyIn: array(string()).optional()
}).strict();
const GroupChatSchema = object({
mentionPatterns: array(string()).optional(),
historyLimit: number().int().min(0).optional(),
unmentionedInbound: AmbientGroupInboundSchema.optional(),
visibleReplies: VisibleRepliesSchema.optional()
}).strict().optional();
const DmConfigSchema$1 = object({ historyLimit: number().int().min(0).optional() }).strict();
const IdentitySchema = object({
name: string().optional(),
theme: string().optional(),
emoji: string().optional(),
avatar: string().optional()
}).strict().optional();
const QueueModeSchema = union([
literal("steer"),
literal("followup"),
literal("collect"),
literal("interrupt")
]);
const QueueDropSchema = union([
literal("old"),
literal("new"),
literal("summarize")
]);
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$1 = object({
minChars: number().int().positive().optional(),
maxChars: number().int().positive().optional(),
idleMs: number().int().nonnegative().optional()
}).strict();
const TextChunkModeSchema = _enum(["length", "newline"]);
const ChannelStreamingBlockSchema = object({
enabled: boolean().optional(),
coalesce: BlockStreamingCoalesceSchema$1.optional()
}).strict();
/** Delivery-only nested streaming config for channels without preview modes. */
const ChannelDeliveryStreamingConfigSchema = object({
chunkMode: TextChunkModeSchema.optional(),
block: ChannelStreamingBlockSchema.optional()
}).strict();
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 = object({
minChars: number().int().positive().optional(),
maxChars: number().int().positive().optional(),
breakPreference: union([
literal("paragraph"),
literal("newline"),
literal("sentence")
]).optional()
}).strict();
const MarkdownTableModeSchema = _enum([
"off",
"bullets",
"code",
"block"
]);
object({ tables: MarkdownTableModeSchema.optional() }).strict().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 = object({
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()
}).strict();
const TtsConfigSchema = object({
auto: TtsAutoSchema.optional(),
enabled: boolean().optional(),
mode: TtsModeSchema.optional(),
provider: TtsProviderSchema.optional(),
persona: string().optional(),
personas: record(string(), TtsPersonaSchema).optional(),
summaryModel: string().optional(),
modelOverrides: object({
enabled: boolean().optional(),
allowText: boolean().optional(),
allowProvider: boolean().optional(),
allowVoice: boolean().optional(),
allowModelId: boolean().optional(),
allowVoiceSettings: boolean().optional(),
allowNormalization: boolean().optional(),
allowSeed: boolean().optional()
}).strict().optional(),
providers: record(string(), TtsProviderConfigSchema).optional(),
maxTextLength: number().int().min(1).optional(),
timeoutMs: number().int().min(1e3).max(12e4).optional()
}).strict().optional();
const HumanDelaySchema = object({
mode: union([
literal("off"),
literal("natural"),
literal("custom")
]).optional(),
minMs: number().int().nonnegative().optional(),
maxMs: number().int().nonnegative().optional()
}).strict();
_enum(["thread", "top-level"]);
const QueueModeBySurfaceSchema = object({
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()
}).strict().optional();
const DebounceMsBySurfaceSchema = record(string(), number().int().nonnegative()).optional();
object({
mode: QueueModeSchema.optional(),
byChannel: QueueModeBySurfaceSchema,
debounceMsByChannel: DebounceMsBySurfaceSchema,
cap: number().int().positive().optional(),
drop: QueueDropSchema.optional()
}).strict().optional();
object({
debounceMs: number().int().nonnegative().optional(),
byChannel: DebounceMsBySurfaceSchema
}).strict().optional();
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 = object({
mode: union([literal("first"), literal("all")]).optional(),
maxAttachments: number().int().positive().optional(),
prefer: union([
literal("first"),
literal("last"),
literal("path"),
literal("url")
]).optional()
}).strict().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 = {
prompt: string().optional(),
timeoutSeconds: number().int().positive().optional(),
language: string().optional(),
providerOptions: ProviderOptionsSchema,
baseUrl: string().optional(),
headers: record(string(), string()).optional(),
request: ConfiguredProviderRequestSchema
};
const MediaUnderstandingModelSchema = object({
provider: string().optional(),
model: string().optional(),
capabilities: MediaUnderstandingCapabilitiesSchema,
type: union([literal("provider"), literal("cli")]).optional(),
command: string().optional(),
args: array(string()).optional(),
maxChars: number().int().positive().optional(),
maxBytes: number().int().positive().optional(),
...MediaUnderstandingRuntimeFields,
profile: string().optional(),
preferredProfile: string().optional()
}).strict().optional();
const ToolsMediaCapabilitySchema = object({
enabled: boolean().optional(),
preferredModel: string().trim().min(1).optional(),
scope: MediaUnderstandingScopeSchema,
maxBytes: number().int().positive().optional(),
maxChars: number().int().positive().optional(),
...MediaUnderstandingRuntimeFields,
attachments: MediaUnderstandingAttachmentsSchema
}).strict().optional();
const ToolsMediaAudioSchema = object({
enabled: boolean().optional(),
preferredModel: string().trim().min(1).optional(),
scope: MediaUnderstandingScopeSchema,
maxBytes: number().int().positive().optional(),
maxChars: number().int().positive().optional(),
...MediaUnderstandingRuntimeFields,
attachments: MediaUnderstandingAttachmentsSchema,
echoTranscript: boolean().optional(),
echoFormat: string().optional()
}).strict().optional();
const ToolsMediaSchema = object({
models: array(MediaUnderstandingModelSchema).optional(),
concurrency: number().int().positive().optional(),
image: ToolsMediaCapabilitySchema.optional(),
audio: ToolsMediaAudioSchema.optional(),
video: ToolsMediaCapabilitySchema.optional()
}).strict().optional();
const LinkModelSchema = object({
type: literal("cli").optional(),
command: string().min(1),
args: array(string()).optional(),
timeoutSeconds: number().int().positive().optional()
}).strict();
const ToolsLinksSchema = object({
enabled: boolean().optional(),
scope: MediaUnderstandingScopeSchema,
maxLinks: number().int().positive().optional(),
timeoutSeconds: number().int().positive().optional(),
models: array(LinkModelSchema).optional()
}).strict().optional();
const NativeCommandsSettingSchema = union([boolean(), literal("auto")]);
object({
native: NativeCommandsSettingSchema.optional(),
nativeSkills: NativeCommandsSettingSchema.optional()
}).strict().optional();
//#endregion
//#region src/config/zod-schema.agent-runtime.ts
function validateSandboxBindEntries(binds, ctx) {
if (!binds) return;
for (let i = 0; i < binds.length; i += 1) {
const bind = normalizeOptionalString(binds[i]) ?? "";
if (!bind) {
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).trim();
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 AgentEntryEmbeddedAgentConfigSchema = object({ executionContract: union([literal("default"), literal("strict-agentic")]).optional() }).strict();
const AgentTtsConfigSchema = TtsConfigSchema.unwrap().extend({ prefsPath: string().optional() }).strict().optional();
const HeartbeatSchema = object({
every: string().optional(),
activeHours: object({
start: string().optional(),
end: string().optional(),
timezone: string().optional()
}).strict().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()
}).strict().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)$/;
const validateTime = (raw, opts, path) => {
if (!raw) return;
if (!timePattern.test(raw)) {
ctx.addIssue({
code: ZodIssueCode.custom,
path: ["activeHours", path],
message: "invalid time (use \"HH:MM\" 24h format)"
});
return;
}
const [hourStr, minuteStr] = raw.split(":");
const hour = Number(hourStr);
const minute = Number(minuteStr);
if (hour === 24 && minute !== 0) {
ctx.addIssue({
code: ZodIssueCode.custom,
path: ["activeHours", path],
message: "invalid time (24:00 is the only allowed 24:xx value)"
});
return;
}
if (hour === 24 && !opts.allow24) ctx.addIssue({
code: ZodIssueCode.custom,
path: ["activeHours", path],
message: "invalid time (start cannot be 24:00)"
});
};
validateTime(active.start, { allow24: false }, "start");
validateTime(active.end, { allow24: true }, "end");
}).optional();
const SandboxDockerSchema = object({
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(),
object({
soft: number().int().nonnegative().optional(),
hard: number().int().nonnegative().optional()
}).strict()
])).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()
}).strict().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 = object({
idleHours: number().int().nonnegative().optional(),
maxAgeDays: number().int().nonnegative().optional()
}).strict().optional();
const AgentContextLimitsSchema = object({
memoryGetMaxChars: number().int().min(1).max(25e4).optional(),
postCompactionMaxChars: number().int().min(1).max(5e4).optional()
}).strict().optional();
const AgentSkillsLimitsSchema = object({ maxSkillsPromptChars: number().int().min(0).optional() }).strict().optional();
const ToolPolicySchema$1 = object({
allow: array(string()).optional(),
alsoAllow: array(string()).optional(),
deny: array(string()).optional()
}).strict().superRefine((value, ctx) => {
if (value.allow && value.allow.length > 0 && value.alsoAllow && value.alsoAllow.length > 0) ctx.addIssue({
code: ZodIssueCode.custom,
message: "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((value) => {
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : void 0;
}).optional();
const CodexAllowedDomainsSchema = array(string()).transform((values) => {
const deduped = uniqueStrings(values.map((value) => value.trim()).filter((value) => value.length > 0));
return deduped.length > 0 ? deduped : void 0;
}).optional();
const CodexUserLocationSchema = object({
country: TrimmedOptionalConfigStringSchema,
region: TrimmedOptionalConfigStringSchema,
city: TrimmedOptionalConfigStringSchema,
timezone: TrimmedOptionalConfigStringSchema
}).strict().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: object({
enabled: boolean().optional(),
mode: union([literal("cached"), literal("live")]).optional(),
allowedDomains: CodexAllowedDomainsSchema,
contextSize: union([
literal("low"),
literal("medium"),
literal("high")
]).optional(),
userLocation: CodexUserLocationSchema
}).strict().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 = object({
enabled: boolean().optional(),
provider: string().optional(),
maxChars: number().int().positive().optional(),
maxCharsCap: number().int().positive().optional(),
maxResponseBytes: number().int().positive().optional(),
timeoutSeconds: number().int().positive().optional(),
cacheTtlMinutes: number().nonnegative().optional(),
maxRedirects: number().int().nonnegative().optional(),
userAgent: string().optional(),
headers: record(string(), string().register(sensitive)).optional(),
readability: boolean().optional(),
useTrustedEnvProxy: boolean().optional(),
ssrfPolicy: SsrFPolicyConfigSchema.optional()
}).strict().optional();
const ToolsWebSchema = object({
search: ToolsWebSearchSchema,
fetch: ToolsWebFetchSchema
}).strict().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 = object({
allow: array(string()).optional(),
alsoAllow: array(string()).optional(),
deny: array(string()).optional(),
profile: ToolProfileSchema
}).strict().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 = object({
enabled: boolean().optional(),
workspaceOnly: boolean().optional(),
allowModels: array(string()).optional()
}).strict().optional();
const ToolExecSafeBinProfileSchema = object({
minPositional: number().int().nonnegative().optional(),
maxPositional: number().int().nonnegative().optional(),
allowedValueFlags: array(string()).optional(),
deniedFlags: array(string()).optional()
}).strict();
const ToolExecBaseShape = {
host: _enum([
"auto",
"sandbox",
"gateway",
"node"
]).optional(),
mode: _enum([
"deny",
"allowlist",
"ask",
"auto",
"full"
]).optional(),
security: _enum([
"deny",
"allowlist",
"full"
]).optional(),
ask: _enum([
"off",
"on-miss",
"always"
]).optional(),
node: string().optional(),
pathPrepend: array(string()).optional(),
safeBins: array(string()).optional(),
strictInlineEval: boolean().optional(),
commandHighlighting: boolean().optional(),
grantExpiryDays: number().int().min(1).max(3650).optional(),
safeBinTrustedDirs: array(string()).optional(),
safeBinProfiles: record(string(), ToolExecSafeBinProfileSchema).optional(),
reviewer: object({
model: AgentModelSchema.optional(),
timeoutMs: number().int().positive().optional()
}).strict().optional(),
backgroundMs: number().int().positive().optional(),
approvalRunningNoticeMs: number().int().nonnegative().optional(),
timeoutSeconds: number().int().positive().optional(),
cleanupMs: number().int().positive().optional(),
notifyOnExit: boolean().optional(),
notifyOnExitEmptySuccess: boolean().optional(),
applyPatch: ToolExecApplyPatchSchema
};
function addExecPolicyModeConflictIssue(value, ctx) {
if (value.mode === void 0 || value.security === void 0 && value.ask === void 0) return;
ctx.addIssue({
code: ZodIssueCode.custom,
path: ["mode"],
message: "tools.exec.mode cannot be combined with tools.exec.security or tools.exec.ask"
});
}
const ToolExecSchema = object(ToolExecBaseShape).strict().superRefine(addExecPolicyModeConflictIssue).optional();
const ToolFsSchema = object({ workspaceOnly: boolean().optional() }).strict().optional();
const ToolLoopDetectionSchema = object({ enabled: boolean().optional() }).strict().optional();
const ToolSearchSchema = union([boolean(), object({
enabled: boolean().optional(),
mode: _enum([
"code",
"tools",
"directory"
]).optional(),
codeTimeoutMs: number().int().positive().optional(),
searchDefaultLimit: number().int().positive().optional(),
maxSearchLimit: number().int().positive().optional()
}).strict()]).optional();
const CodeModeSchema = union([
boolean(),
literal("auto"),
object({
enabled: union([boolean(), literal("auto")]).optional(),
runtime: literal("quickjs-wasi").optional(),
mode: literal("only").optional(),
languages: array(_enum(["javascript", "typescript"])).optional(),
timeoutMs: number().int().positive().optional(),
memoryLimitBytes: number().int().positive().optional(),
maxOutputBytes: number().int().positive().optional(),
maxSnapshotBytes: number().int().positive().optional(),
maxPendingToolCalls: number().int().positive().optional(),
snapshotTtlSeconds: number().int().positive().optional(),
searchDefaultLimit: number().int().positive().optional(),
maxSearchLimit: number().int().positive().optional()
}).strict()
]).optional();
const SwarmSchema = union([boolean(), object({
enabled: boolean().optional(),
maxConcurrent: number().int().positive().optional(),
maxChildrenPerGroup: number().int().positive().optional(),
maxTotalPerGroup: number().int().positive().optional(),
waitTimeoutSecondsMax: number().int().positive().optional(),
defaultAgentId: string().optional()
}).strict()]).optional();
const SandboxSshSchema = object({
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)
}).strict().optional();
const AgentSandboxSchema = object({
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
}).strict().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 = {
profile: ToolProfileSchema,
allow: array(string()).optional(),
alsoAllow: array(string()).optional(),
deny: array(string()).optional(),
byProvider: record(string(), ToolPolicyWithProfileSchema).optional(),
toolsBySender: ToolPolicyBySenderSchema
};
const MessageToolConfigSchema = object({
crossContext: object({
allowWithinProvider: boolean().optional(),
allowAcrossProviders: boolean().optional(),
marker: object({
enabled: boolean().optional(),
prefix: string().optional(),
suffix: string().optional()
}).strict().optional()
}).strict().optional(),
actions: object({ allow: array(string()).optional() }).strict().optional(),
broadcast: object({ enabled: boolean().optional() }).strict().optional()
}).strict().optional();
const GitHubToolIdentitySchema = object({
profileId: string().regex(MANAGED_GITHUB_PROFILE_ID_PATTERN),
kind: literal("oauth").optional(),
gitAuthor: object({
name: string().trim().min(1).optional(),
email: string().trim().min(1).optional()
}).strict().optional()
}).strict().optional();
const AgentToolsSchema = object({
...CommonToolPolicyFields,
codeMode: CodeModeSchema,
swarm: SwarmSchema,
elevated: object({
enabled: boolean().optional(),
allowFrom: ElevatedAllowFromSchema
}).strict().optional(),
exec: ToolExecSchema,
github: GitHubToolIdentitySchema,
fs: ToolFsSchema,
loopDetection: ToolLoopDetectionSchema,
message: MessageToolConfigSchema,
sandbox: object({ tools: ToolPolicySchema$1 }).strict().optional()
}).strict().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();
const MemorySearchSchema = object({
enabled: boolean().optional(),
rememberAcrossConversations: boolean().optional(),
sources: array(union([literal("memory"), literal("sessions")])).optional(),
extraPaths: array(union([string(), object({
path: string(),
pattern: string().optional()
}).strict()])).optional(),
multimodal: object({
enabled: boolean().optional(),
modalities: array(union([
literal("image"),
literal("audio"),
literal("all")
])).optional(),
maxFileBytes: number().int().positive().optional()
}).strict().optional(),
experimental: object({ sessionMemory: boolean().optional() }).strict().optional(),
provider: string().optional(),
remote: object({
baseUrl: string().optional(),
apiKey: SecretInputSchema.optional().register(sensitive),
headers: record(string(), string()).optional(),
batch: object({ enabled: boolean().optional() }).strict().optional()
}).strict().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: object({ modelPath: string().optional() }).strict().optional(),
store: object({
fts: object({ tokenizer: union([literal("unicode61"), literal("trigram")]).optional() }).strict().optional(),
vector: object({
enabled: boolean().optional(),
extensionPath: string().optional()
}).strict().optional()
}).strict().optional(),
query: object({
maxResults: number().int().positive().optional(),
minScore: number().min(0).max(1).optional()
}).strict().optional(),
cache: object({ enabled: boolean().optional() }).strict().optional()
}).strict().optional();
const AgentRuntimeAcpSchema = object({
agent: string().optional(),
backend: string().optional(),
mode: _enum(["persistent", "oneshot"]).optional(),
cwd: string().optional()
}).strict().optional();
const AgentRuntimeSchema = union([object({ type: literal("embedded") }).strict(), object({
type: literal("acp"),
acp: AgentRuntimeAcpSchema
}).strict()]).optional();
const AgentRuntimePolicySchema = object({ id: string().optional() }).strict().optional();
const AgentModelRuntimeEntrySchema = object({
alias: string().optional(),
params: record(string(), unknown()).optional(),
agentRuntime: AgentRuntimePolicySchema,
codeMode: boolean().optional(),
streaming: boolean().optional()
}).strict();
const AgentModelMapSchema = record(string(), AgentModelRuntimeEntrySchema).superRefine((models, ctx) => {
for (const [ref, entry] of Object.entries(models)) 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 = object({ allow: array(string()).optional() }).strict();
object({
id: string(),
name: string().optional(),
description: string().optional(),
workspace: string().optional(),
cwd: string().optional(),
agentDir: string().optional(),
model: AgentModelSchema.optional(),
utilityModel: string().optional(),
models: AgentModelMapSchema.optional(),
modelPolicy: AgentModelPolicySchema.optional(),
thinkingDefault: _enum([
"off",
"minimal",
"low",
"medium",
"high",
"xhigh",
"adaptive",
"max",
"ultra"
]).optional(),
verboseDefault: _enum([
"off",
"on",
"full"
]).optional(),
toolProgressDetail: _enum(["explain", "raw"]).optional(),
reasoningDefault: _enum([
"on",
"off",
"stream"
]).optional(),
fastModeDefault: union([boolean(), literal("auto")]).optional(),
contextInjection: union([
literal("always"),
literal("continuation-skip"),
literal("never")
]).optional(),
bootstrapMaxChars: number().int().positive().optional(),
bootstrapTotalMaxChars: number().int().positive().optional(),
experimental: object({ localModelLean: boolean().optional() }).strict().optional(),
skills: array(string()).optional(),
memory: object({ search: MemorySearchSchema }).strict().optional(),
humanDelay: HumanDelaySchema.optional(),
typingMode: TypingModeSchema.optional(),
tts: AgentTtsConfigSchema,
skillsLimits: AgentSkillsLimitsSchema,
contextLimits: AgentContextLimitsSchema,
heartbeat: HeartbeatSchema,
identity: IdentitySchema,
groupChat: GroupChatSchema.unwrap().omit({ visibleReplies: true }).optional(),
subagents: object({
delegationMode: _enum(["suggest", "prefer"]).optional(),
allowAgents: array(string()).optional(),
model: AgentModelSchema.optional(),
thinking: string().optional(),
requireAgentId: boolean().optional()
}).strict().optional(),
embeddedAgent: AgentEntryEmbeddedAgentConfigSchema.optional(),
sandbox: AgentSandboxSchema,
params: record(string(), unknown()).optional(),
tools: AgentToolsSchema,
runtime: AgentRuntimeSchema
}).strict();
object({
...CommonToolPolicyFields,
web: ToolsWebSchema,
github: GitHubToolIdentitySchema,
media: ToolsMediaSchema,
links: ToolsLinksSchema,
sessions: object({ visibility: _enum([
"self",
"tree",
"agent",
"all"
]).optional() }).strict().optional(),
loopDetection: ToolLoopDetectionSchema,
toolSearch: ToolSearchSchema,
codeMode: CodeModeSchema,
swarm: SwarmSchema,
message: MessageToolConfigSchema,
agentToAgent: object({
enabled: boolean().optional(),
allow: array(string()).optional()
}).strict().optional(),
elevated: object({
enabled: boolean().optional(),
allowFrom: ElevatedAllowFromSchema
}).strict().optional(),
exec: ToolExecSchema,
fs: ToolFsSchema,
subagents: object({ tools: ToolPolicySchema$1 }).strict().optional(),
sandbox: object({ tools: ToolPolicySchema$1 }).strict().optional(),
sessions_spawn: object({ attachments: object({
enabled: boolean().optional(),
maxTotalBytes: number().optional(),
maxFiles: number().optional(),
maxFileBytes: number().optional(),
retainOnSessionKeep: boolean().optional()
}).strict().optional() }).strict().optional(),
updatePlan: boolean().optional()
}).strict().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();
resolveGlobalSingleton(Symbol.for("openclaw.gatewayPluginMetadataOwners"), () => /* @__PURE__ */ new Set());
//#endregion
//#region src/plugins/plugin-cache-primitives.ts
/** Small process-local LRU cache for runtime registries and compiled validators. */
var PluginLruCache = class {
#maxEntries;
#entries = /* @__PURE__ */ new Map();
constructor(maxEntries) {
this.#maxEntries = normalizeMaxEntries(maxEntries, 1);
}
get size() {
return this.#entries.size;
}
clear() {
this.#entries.clear();
}
deleteValue(value) {
for (const [key, entry] of this.#entries) if (entry === value) this.#entries.delete(key);
}
/** 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) {
if (this.#entries.has(cacheKey)) this.#entries.delete(cacheKey);
this.#entries.set(cacheKey, value);
pruneMapToMaxSize(this.#entries, this.#maxEntries);
}
};
function normalizeMaxEntries(value, fallback) {
if (!Number.isFinite(value) || value <= 0) return fallback;
return Math.max(1, Math.floor(value));
}
new PluginLruCache(512);
//#endregion
//#region src/channels/plugins/config-schema.ts
/**
* Channel config schema helpers.
*
* Builds common zod/JSON schema shapes and parses runtime config issues for channel plugins.
*/
/** 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) {
const schemaWithJson = schema;
if (typeof schemaWithJson.toJSONSchema === "function") return {
schema: schemaWithJson.toJSONSchema({
target: "draft-07",
...options?.jsonSchemaMode ? { io: options.jsonSchemaMode } : {},
unrepresentable: "any"
}),
...options?.uiHints ? { uiHints: options.uiHints } : {},
runtime: { safeParse: (value) => safeParseRuntimeSchema(schema, value) }
};
return {
schema: {
type: "object",
additionalProperties: true
},
...options?.uiHints ? { uiHints: options.uiHints } : {},
runtime: { safeParse: (value) => safeParseRuntimeSchema(schema, value) }
};
}
union([boolean(), literal("auto")]);
const ExecApprovalForwardTargetSchema = object({
channel: string().min(1),
to: string().min(1),
accountId: string().optional(),
threadId: union([string(), number()]).optional()
}).strict();
const ExecApprovalForwardingSchema = object({
enabled: boolean().optional(),
mode: union([
literal("session"),
literal("targets"),
literal("both")
]).optional(),
agentFilter: array(string()).optional(),
sessionFilter: array(string()).optional(),
targets: array(ExecApprovalForwardTargetSchema).optional()
}).strict().optional();
object({
exec: ExecApprovalForwardingSchema,
plugin: ExecApprovalForwardingSchema
}).strict().optional();
//#endregion
//#region src/config/zod-schema.channels.ts
/** Optional heartbeat visibility controls shared by channel schemas. */
const ChannelHeartbeatVisibilitySchema$1 = object({
showOk: boolean().optional(),
showAlerts: boolean().optional(),
useIndicator: boolean().optional()
}).strict().optional();
object({ enabled: boolean().optional() }).strict().optional();
//#endregion
//#region src/config/zod-schema.implicit-mentions.ts
const ChannelImplicitMentionsSchema = object({
replyToBot: boolean().optional(),
quotedBot: boolean().optional(),
threadParticipation: boolean().optional()
}).strict();
//#endregion
//#region src/config/zod-schema.channels-config.ts
const ChannelModelByChannelSchema = record(string(), record(string(), string())).optional();
const ChannelBotLoopProtectionSchema = object({
enabled: boolean().optional(),
maxEventsPerWindow: number().int().positive().optional(),
windowSeconds: number().int().positive().optional(),
cooldownSeconds: number().int().positive().optional()
}).strict();
function addLegacyChannelAcpBindingIssues(value, ctx, path = []) {
if (!value || typeof value !== "object") return;
if (Array.isArray(value)) {
value.forEach((entry, index) => addLegacyChannelAcpBindingIssues(entry, ctx, [...path, index]));
return;
}
const record = value;
const bindings = record.bindings;
if (bindings && typeof bindings === "object" && !Array.isArray(bindings)) {
const acp = bindings.acp;
if (acp && typeof acp === "object") ctx.addIssue({
code: ZodIssueCode.custom,
path: [
...path,
"bindings",
"acp"
],
message: "Legacy channel-local ACP bindings were removed; use top-level bindings[] entries."
});
}
for (const [key, entry] of Object.entries(record)) addLegacyChannelAcpBindingIssues(entry, ctx, [...path, key]);
}
object({
defaults: object({
groupPolicy: GroupPolicySchema.optional(),
contextVisibility: ContextVisibilityModeSchema.optional(),
heartbeatVisibility: ChannelHeartbeatVisibilitySchema$1,
botLoopProtection: ChannelBotLoopProtectionSchema.optional(),
implicitMentions: ChannelImplicitMentionsSchema.optional()
}).strict().optional(),
modelByChannel: ChannelModelByChannelSchema
}).passthrough().superRefine((value, ctx) => {
addLegacyChannelAcpBindingIssues(value, ctx);
}).optional();
//#endregion
//#region src/config/zod-schema.channel-messaging-common.ts
const UnifiedStreamingModeSchema = _enum([
"off",
"partial",
"block",
"progress"
]);
const ChannelStreamingPreviewSchema = object({
chunk: BlockStreamingChunkSchema.optional(),
toolProgress: boolean().optional(),
commandText: _enum(["raw", "status"]).optional()
}).strict();
const ChannelStreamingProgressSchema = object({
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()
}).strict();
object({
mode: UnifiedStreamingModeSchema.optional(),
chunkMode: TextChunkModeSchema.optional(),
preview: ChannelStreamingPreviewSchema.optional(),
progress: ChannelStreamingProgressSchema.optional(),
block: ChannelStreamingBlockSchema.optional()
}).strict();
array(string()).optional();
array(union([string(), number()])).optional();
string().optional();
MentionPatternsPolicySchema.optional();
ChannelDeliveryStreamingConfigSchema.optional();
number().positive().optional();
ReplyToModeSchema.optional();
DmPolicySchema.optional().default("pairing"), GroupPolicySchema.optional().default("allowlist");
boolean().optional();
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 src/plugin-sdk/secret-input-schema.ts
/**
* Returns the shared secret-input schema for plaintext values and env/file/exec/store refs.
* Reusing this singleton preserves sensitive-path registration for config redaction.
*/
function buildSecretInputSchema() {
return secretInputSchema;
}
const providerSchema = string().regex(SECRET_PROVIDER_ALIAS_PATTERN, "Secret reference provider must match /^[a-z][a-z0-9_-]{0,63}$/ (example: \"default\").");
const secretInputSchema = union([string(), discriminatedUnion("source", [
object({
source: literal("env"),
provider: providerSchema,
id: string().regex(ENV_SECRET_REF_ID_RE, "Env secret reference id must match /^[A-Z][A-Z0-9_]{0,127}$/ (example: \"OPENAI_API_KEY\").")
}).strict(),
object({
source: literal("store"),
provider: providerSchema,
id: string().regex(ENV_SECRET_REF_ID_RE, "Store secret reference id must match /^[A-Z][A-Z0-9_]{0,127}$/ (example: \"OPENAI_API_KEY\").")
}).strict(),
object({
source: literal("file"),
provider: providerSchema,
id: string().refine(isValidFileSecretRefId, "File secret reference id must be an absolute JSON pointer (example: \"/providers/openai/apiKey\"), or \"value\" for singleValue mode.")
}).strict(),
object({
source: literal("exec"),
provider: providerSchema,
id: string().refine(isValidExecSecretRefId, formatExecSecretRefIdValidationMessage())
}).strict()
])]).register(sensitive);
//#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 BlockStreamingCoalesceSchema = object({
minChars: number().int().positive().optional(),
maxChars: number().int().positive().optional(),
idleMs: number().int().nonnegative().optional()
}).strict().optional();
const FeishuStreamingSchema = object({
mode: _enum(["off", "partial"]).optional(),
chunkMode: _enum(["length", "newline"]).optional(),
block: object({
enabled: boolean().optional(),
coalesce: BlockStreamingCoalesceSchema
}).strict().optional()
}).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 FeishuGroupSchema = buildGroupEntrySchema({
tools: ToolPolicySchema,
groupSessionScope: GroupSessionScopeSchema,
topicSessionMode: TopicSessionModeSchema,
replyInThread: ReplyInThreadSchema
}).omit({ toolsBySender: true });
const FeishuSharedConfigShape = {
webhookHost: string().optional(),
webhookPort: number().int().positive().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(),
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 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,
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 normalizeCompatibilityConfig({ cfg }) {
const aliases = streamingAliasMigration.normalizeChannelConfig({ cfg });
const entries = normalizeFeishuLegacyConfigEntries(aliases.config, aliases.changes);
const stray = feishuStrayEntryConfigMigration.normalizeConfig({ cfg: entries });
return {
config: stray.config,
changes: [...aliases.changes, ...stray.changes]
};
}
//#endregion
export { legacyConfigRules, normalizeCompatibilityConfig };