fix: restore full gate

This commit is contained in:
Peter Steinberger
2026-03-17 07:47:17 +00:00
parent c0e4721712
commit 449127b474
18 changed files with 447 additions and 202 deletions

View File

@@ -1754,6 +1754,58 @@
"help": "Delay style for block replies (\"off\", \"natural\", \"custom\").",
"hasChildren": false
},
{
"path": "agents.defaults.imageGenerationModel",
"kind": "core",
"type": [
"object",
"string"
],
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [],
"hasChildren": true
},
{
"path": "agents.defaults.imageGenerationModel.fallbacks",
"kind": "core",
"type": "array",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [
"media",
"reliability"
],
"label": "Image Generation Model Fallbacks",
"help": "Ordered fallback image-generation models (provider/model).",
"hasChildren": true
},
{
"path": "agents.defaults.imageGenerationModel.fallbacks.*",
"kind": "core",
"type": "string",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [],
"hasChildren": false
},
{
"path": "agents.defaults.imageGenerationModel.primary",
"kind": "core",
"type": "string",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [
"media"
],
"label": "Image Generation Model",
"help": "Optional image-generation model (provider/model) used by the shared image generation capability.",
"hasChildren": false
},
{
"path": "agents.defaults.imageMaxDimensionPx",
"kind": "core",
@@ -38212,6 +38264,20 @@
"help": "Allow /debug chat command for runtime-only overrides (default: false).",
"hasChildren": false
},
{
"path": "commands.mcp",
"kind": "core",
"type": "boolean",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [
"advanced"
],
"label": "Allow /mcp",
"help": "Allow /mcp chat command to manage OpenClaw MCP server config under mcp.servers (default: false).",
"hasChildren": false
},
{
"path": "commands.native",
"kind": "core",
@@ -38308,6 +38374,20 @@
"help": "Optional secret used to HMAC hash owner IDs when ownerDisplay=hash. Prefer env substitution.",
"hasChildren": false
},
{
"path": "commands.plugins",
"kind": "core",
"type": "boolean",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [
"advanced"
],
"label": "Allow /plugins",
"help": "Allow /plugins chat command to list discovered plugins and toggle plugin enablement in config (default: false).",
"hasChildren": false
},
{
"path": "commands.restart",
"kind": "core",
@@ -39846,7 +39926,7 @@
"network"
],
"label": "OpenAI Chat Completions Allow Image URLs",
"help": "Allow server-side URL fetches for `image_url` parts (default: false; data URIs remain supported).",
"help": "Allow server-side URL fetches for `image_url` parts (default: false; data URIs remain supported). Set this to `false` to disable URL fetching entirely.",
"hasChildren": false
},
{
@@ -39911,7 +39991,7 @@
"network"
],
"label": "OpenAI Chat Completions Image URL Allowlist",
"help": "Optional hostname allowlist for `image_url` URL fetches; supports exact hosts and `*.example.com` wildcards.",
"help": "Optional hostname allowlist for `image_url` URL fetches; supports exact hosts and `*.example.com` wildcards. Empty or omitted lists mean no hostname allowlist restriction.",
"hasChildren": true
},
{
@@ -42214,6 +42294,137 @@
"help": "Sensitive redaction mode: \"off\" disables built-in masking, while \"tools\" redacts sensitive tool/config payload fields. Keep \"tools\" in shared logs unless you have isolated secure log sinks.",
"hasChildren": false
},
{
"path": "mcp",
"kind": "core",
"type": "object",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [
"advanced"
],
"label": "MCP",
"help": "Global MCP server definitions managed by OpenClaw. Embedded Pi and other runtime adapters can consume these servers without storing them inside Pi-owned project settings.",
"hasChildren": true
},
{
"path": "mcp.servers",
"kind": "core",
"type": "object",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [
"advanced"
],
"label": "MCP Servers",
"help": "Named MCP server definitions. OpenClaw stores them in its own config and runtime adapters decide which transports are supported at execution time.",
"hasChildren": true
},
{
"path": "mcp.servers.*",
"kind": "core",
"type": "object",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [],
"hasChildren": true
},
{
"path": "mcp.servers.*.*",
"kind": "core",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [],
"hasChildren": false
},
{
"path": "mcp.servers.*.args",
"kind": "core",
"type": "array",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [],
"hasChildren": true
},
{
"path": "mcp.servers.*.args.*",
"kind": "core",
"type": "string",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [],
"hasChildren": false
},
{
"path": "mcp.servers.*.command",
"kind": "core",
"type": "string",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [],
"hasChildren": false
},
{
"path": "mcp.servers.*.cwd",
"kind": "core",
"type": "string",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [],
"hasChildren": false
},
{
"path": "mcp.servers.*.env",
"kind": "core",
"type": "object",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [],
"hasChildren": true
},
{
"path": "mcp.servers.*.env.*",
"kind": "core",
"type": [
"boolean",
"number",
"string"
],
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [],
"hasChildren": false
},
{
"path": "mcp.servers.*.url",
"kind": "core",
"type": "string",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [],
"hasChildren": false
},
{
"path": "mcp.servers.*.workingDirectory",
"kind": "core",
"type": "string",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [],
"hasChildren": false
},
{
"path": "media",
"kind": "core",

View File

@@ -1,4 +1,4 @@
{"generatedBy":"scripts/generate-config-doc-baseline.ts","recordType":"meta","totalPaths":5147}
{"generatedBy":"scripts/generate-config-doc-baseline.ts","recordType":"meta","totalPaths":5165}
{"recordType":"path","path":"acp","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"ACP","help":"ACP runtime controls for enabling dispatch, selecting backends, constraining allowed agent targets, and tuning streamed turn projection behavior.","hasChildren":true}
{"recordType":"path","path":"acp.allowedAgents","kind":"core","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"ACP Allowed Agents","help":"Allowlist of ACP target agent ids permitted for ACP runtime sessions. Empty means no additional allowlist restriction.","hasChildren":true}
{"recordType":"path","path":"acp.allowedAgents.*","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
@@ -150,6 +150,10 @@
{"recordType":"path","path":"agents.defaults.humanDelay.maxMs","kind":"core","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":["performance"],"label":"Human Delay Max (ms)","help":"Maximum delay in ms for custom humanDelay (default: 2500).","hasChildren":false}
{"recordType":"path","path":"agents.defaults.humanDelay.minMs","kind":"core","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Human Delay Min (ms)","help":"Minimum delay in ms for custom humanDelay (default: 800).","hasChildren":false}
{"recordType":"path","path":"agents.defaults.humanDelay.mode","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Human Delay Mode","help":"Delay style for block replies (\"off\", \"natural\", \"custom\").","hasChildren":false}
{"recordType":"path","path":"agents.defaults.imageGenerationModel","kind":"core","type":["object","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"agents.defaults.imageGenerationModel.fallbacks","kind":"core","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["media","reliability"],"label":"Image Generation Model Fallbacks","help":"Ordered fallback image-generation models (provider/model).","hasChildren":true}
{"recordType":"path","path":"agents.defaults.imageGenerationModel.fallbacks.*","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"agents.defaults.imageGenerationModel.primary","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["media"],"label":"Image Generation Model","help":"Optional image-generation model (provider/model) used by the shared image generation capability.","hasChildren":false}
{"recordType":"path","path":"agents.defaults.imageMaxDimensionPx","kind":"core","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":["media","performance"],"label":"Image Max Dimension (px)","help":"Max image side length in pixels when sanitizing transcript/tool-result image payloads (default: 1200).","hasChildren":false}
{"recordType":"path","path":"agents.defaults.imageModel","kind":"core","type":["object","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"agents.defaults.imageModel.fallbacks","kind":"core","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["media","models","reliability"],"label":"Image Model Fallbacks","help":"Ordered fallback image models (provider/model).","hasChildren":true}
@@ -3453,12 +3457,14 @@
{"recordType":"path","path":"commands.bashForegroundMs","kind":"core","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Bash Foreground Window (ms)","help":"How long bash waits before backgrounding (default: 2000; 0 backgrounds immediately).","hasChildren":false}
{"recordType":"path","path":"commands.config","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Allow /config","help":"Allow /config chat command to read/write config on disk (default: false).","hasChildren":false}
{"recordType":"path","path":"commands.debug","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Allow /debug","help":"Allow /debug chat command for runtime-only overrides (default: false).","hasChildren":false}
{"recordType":"path","path":"commands.mcp","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Allow /mcp","help":"Allow /mcp chat command to manage OpenClaw MCP server config under mcp.servers (default: false).","hasChildren":false}
{"recordType":"path","path":"commands.native","kind":"core","type":["boolean","string"],"required":true,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Native Commands","help":"Registers native slash/menu commands with channels that support command registration (Discord, Slack, Telegram). Keep enabled for discoverability unless you intentionally run text-only command workflows.","hasChildren":false}
{"recordType":"path","path":"commands.nativeSkills","kind":"core","type":["boolean","string"],"required":true,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Native Skill Commands","help":"Registers native skill commands so users can invoke skills directly from provider command menus where supported. Keep aligned with your skill policy so exposed commands match what operators expect.","hasChildren":false}
{"recordType":"path","path":"commands.ownerAllowFrom","kind":"core","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Command Owners","help":"Explicit owner allowlist for owner-only tools/commands. Use channel-native IDs (optionally prefixed like \"whatsapp:+15551234567\"). '*' is ignored.","hasChildren":true}
{"recordType":"path","path":"commands.ownerAllowFrom.*","kind":"core","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"commands.ownerDisplay","kind":"core","type":"string","required":true,"enumValues":["raw","hash"],"defaultValue":"raw","deprecated":false,"sensitive":false,"tags":["access"],"label":"Owner ID Display","help":"Controls how owner IDs are rendered in the system prompt. Allowed values: raw, hash. Default: raw.","hasChildren":false}
{"recordType":"path","path":"commands.ownerDisplaySecret","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":true,"tags":["access","auth","security"],"label":"Owner ID Hash Secret","help":"Optional secret used to HMAC hash owner IDs when ownerDisplay=hash. Prefer env substitution.","hasChildren":false}
{"recordType":"path","path":"commands.plugins","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Allow /plugins","help":"Allow /plugins chat command to list discovered plugins and toggle plugin enablement in config (default: false).","hasChildren":false}
{"recordType":"path","path":"commands.restart","kind":"core","type":"boolean","required":true,"defaultValue":true,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Allow Restart","help":"Allow /restart and gateway restart tool actions (default: true).","hasChildren":false}
{"recordType":"path","path":"commands.text","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Text Commands","help":"Enables text-command parsing in chat input in addition to native command surfaces where available. Keep this enabled for compatibility across channels that do not support native command registration.","hasChildren":false}
{"recordType":"path","path":"commands.useAccessGroups","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Use Access Groups","help":"Enforce access-group allowlists/policies for commands.","hasChildren":false}
@@ -3573,11 +3579,11 @@
{"recordType":"path","path":"gateway.http.endpoints.chatCompletions.images","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["media","network"],"label":"OpenAI Chat Completions Image Limits","help":"Image fetch/validation controls for OpenAI-compatible `image_url` parts.","hasChildren":true}
{"recordType":"path","path":"gateway.http.endpoints.chatCompletions.images.allowedMimes","kind":"core","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access","media","network"],"label":"OpenAI Chat Completions Image MIME Allowlist","help":"Allowed MIME types for `image_url` parts (case-insensitive list).","hasChildren":true}
{"recordType":"path","path":"gateway.http.endpoints.chatCompletions.images.allowedMimes.*","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"gateway.http.endpoints.chatCompletions.images.allowUrl","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access","media","network"],"label":"OpenAI Chat Completions Allow Image URLs","help":"Allow server-side URL fetches for `image_url` parts (default: false; data URIs remain supported).","hasChildren":false}
{"recordType":"path","path":"gateway.http.endpoints.chatCompletions.images.allowUrl","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access","media","network"],"label":"OpenAI Chat Completions Allow Image URLs","help":"Allow server-side URL fetches for `image_url` parts (default: false; data URIs remain supported). Set this to `false` to disable URL fetching entirely.","hasChildren":false}
{"recordType":"path","path":"gateway.http.endpoints.chatCompletions.images.maxBytes","kind":"core","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":["media","network","performance"],"label":"OpenAI Chat Completions Image Max Bytes","help":"Max bytes per fetched/decoded `image_url` image (default: 10MB).","hasChildren":false}
{"recordType":"path","path":"gateway.http.endpoints.chatCompletions.images.maxRedirects","kind":"core","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":["media","network","performance","storage"],"label":"OpenAI Chat Completions Image Max Redirects","help":"Max HTTP redirects allowed when fetching `image_url` URLs (default: 3).","hasChildren":false}
{"recordType":"path","path":"gateway.http.endpoints.chatCompletions.images.timeoutMs","kind":"core","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":["media","network","performance"],"label":"OpenAI Chat Completions Image Timeout (ms)","help":"Timeout in milliseconds for `image_url` URL fetches (default: 10000).","hasChildren":false}
{"recordType":"path","path":"gateway.http.endpoints.chatCompletions.images.urlAllowlist","kind":"core","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access","media","network"],"label":"OpenAI Chat Completions Image URL Allowlist","help":"Optional hostname allowlist for `image_url` URL fetches; supports exact hosts and `*.example.com` wildcards.","hasChildren":true}
{"recordType":"path","path":"gateway.http.endpoints.chatCompletions.images.urlAllowlist","kind":"core","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access","media","network"],"label":"OpenAI Chat Completions Image URL Allowlist","help":"Optional hostname allowlist for `image_url` URL fetches; supports exact hosts and `*.example.com` wildcards. Empty or omitted lists mean no hostname allowlist restriction.","hasChildren":true}
{"recordType":"path","path":"gateway.http.endpoints.chatCompletions.images.urlAllowlist.*","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"gateway.http.endpoints.chatCompletions.maxBodyBytes","kind":"core","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":["network","performance"],"label":"OpenAI Chat Completions Max Body Bytes","help":"Max request body size in bytes for `/v1/chat/completions` (default: 20MB).","hasChildren":false}
{"recordType":"path","path":"gateway.http.endpoints.chatCompletions.maxImageParts","kind":"core","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":["media","network","performance"],"label":"OpenAI Chat Completions Max Image Parts","help":"Max number of `image_url` parts accepted from the latest user message (default: 8).","hasChildren":false}
@@ -3759,6 +3765,18 @@
{"recordType":"path","path":"logging.redactPatterns","kind":"core","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["observability","privacy"],"label":"Custom Redaction Patterns","help":"Additional custom redact regex patterns applied to log output before emission/storage. Use this to mask org-specific tokens and identifiers not covered by built-in redaction rules.","hasChildren":true}
{"recordType":"path","path":"logging.redactPatterns.*","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"logging.redactSensitive","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["observability","privacy"],"label":"Sensitive Data Redaction Mode","help":"Sensitive redaction mode: \"off\" disables built-in masking, while \"tools\" redacts sensitive tool/config payload fields. Keep \"tools\" in shared logs unless you have isolated secure log sinks.","hasChildren":false}
{"recordType":"path","path":"mcp","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"MCP","help":"Global MCP server definitions managed by OpenClaw. Embedded Pi and other runtime adapters can consume these servers without storing them inside Pi-owned project settings.","hasChildren":true}
{"recordType":"path","path":"mcp.servers","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"MCP Servers","help":"Named MCP server definitions. OpenClaw stores them in its own config and runtime adapters decide which transports are supported at execution time.","hasChildren":true}
{"recordType":"path","path":"mcp.servers.*","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"mcp.servers.*.*","kind":"core","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"mcp.servers.*.args","kind":"core","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"mcp.servers.*.args.*","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"mcp.servers.*.command","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"mcp.servers.*.cwd","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"mcp.servers.*.env","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"mcp.servers.*.env.*","kind":"core","type":["boolean","number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"mcp.servers.*.url","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"mcp.servers.*.workingDirectory","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"media","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Media","help":"Top-level media behavior shared across providers and tools that handle inbound files. Keep defaults unless you need stable filenames for external processing pipelines or longer-lived inbound media retention.","hasChildren":true}
{"recordType":"path","path":"media.preserveFilenames","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["storage"],"label":"Preserve Media Filenames","help":"When enabled, uploaded media keeps its original filename instead of a generated temp-safe name. Turn this on when downstream automations depend on stable names, and leave off to reduce accidental filename leakage.","hasChildren":false}
{"recordType":"path","path":"media.ttlHours","kind":"core","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Media Retention TTL (hours)","help":"Optional retention window in hours for persisted inbound media cleanup across the full media tree. Leave unset to preserve legacy behavior, or set values like 24 (1 day) or 168 (7 days) when you want automatic cleanup.","hasChildren":false}

View File

@@ -37,7 +37,7 @@ describe("Feishu Card Action Handler", () => {
function createCardActionEvent(params: {
token: string;
actionValue: unknown;
actionValue: Record<string, unknown>;
chatId?: string;
openId?: string;
userId?: string;

View File

@@ -338,6 +338,10 @@
"types": "./dist/plugin-sdk/channel-config-schema.d.ts",
"default": "./dist/plugin-sdk/channel-config-schema.js"
},
"./plugin-sdk/channel-lifecycle": {
"types": "./dist/plugin-sdk/channel-lifecycle.d.ts",
"default": "./dist/plugin-sdk/channel-lifecycle.js"
},
"./plugin-sdk/channel-policy": {
"types": "./dist/plugin-sdk/channel-policy.d.ts",
"default": "./dist/plugin-sdk/channel-policy.js"

View File

@@ -74,6 +74,7 @@
"boolean-param",
"channel-config-helpers",
"channel-config-schema",
"channel-lifecycle",
"channel-policy",
"group-access",
"directory-runtime",

View File

@@ -341,31 +341,47 @@ const requiredPluginSdkExports = [
"DEFAULT_GROUP_HISTORY_LIMIT",
];
function checkPluginSdkExports() {
const distPath = resolve("dist", "plugin-sdk", "index.js");
let content: string;
async function collectDistPluginSdkExports(): Promise<Set<string>> {
const pluginSdkDir = resolve("dist", "plugin-sdk");
let entries: string[];
try {
content = readFileSync(distPath, "utf8");
entries = readdirSync(pluginSdkDir)
.filter((entry) => entry.endsWith(".js"))
.toSorted();
} catch {
console.error("release-check: dist/plugin-sdk/index.js not found (build missing?).");
console.error("release-check: dist/plugin-sdk directory not found (build missing?).");
process.exit(1);
return;
return new Set();
}
const exportMatch = content.match(/export\s*\{([^}]+)\}\s*;?\s*$/);
if (!exportMatch) {
console.error("release-check: could not find export statement in dist/plugin-sdk/index.js.");
process.exit(1);
return;
const exportedNames = new Set<string>();
for (const entry of entries) {
const content = readFileSync(join(pluginSdkDir, entry), "utf8");
for (const match of content.matchAll(/export\s*\{([^}]+)\}(?:\s*from\s*["'][^"']+["'])?/g)) {
const names = match[1]?.split(",") ?? [];
for (const name of names) {
const parts = name.trim().split(/\s+as\s+/);
const exportName = (parts[parts.length - 1] || "").trim();
if (exportName) {
exportedNames.add(exportName);
}
}
}
for (const match of content.matchAll(
/export\s+(?:const|function|class|let|var)\s+([A-Za-z0-9_$]+)/g,
)) {
const exportName = match[1]?.trim();
if (exportName) {
exportedNames.add(exportName);
}
}
}
const exportedNames = new Set(
exportMatch[1].split(",").map((s) => {
const parts = s.trim().split(/\s+as\s+/);
return (parts[parts.length - 1] || "").trim();
}),
);
return exportedNames;
}
async function checkPluginSdkExports() {
const exportedNames = await collectDistPluginSdkExports();
const missingExports = requiredPluginSdkExports.filter((name) => !exportedNames.has(name));
if (missingExports.length > 0) {
console.error("release-check: missing critical plugin-sdk exports (#27569):");
@@ -376,10 +392,10 @@ function checkPluginSdkExports() {
}
}
function main() {
async function main() {
checkPluginVersions();
checkAppcastSparkleVersions();
checkPluginSdkExports();
await checkPluginSdkExports();
checkBundledExtensionRootDependencyMirrors();
const results = runPackDry();
@@ -423,5 +439,8 @@ function main() {
}
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
main();
void main().catch((error: unknown) => {
console.error(error);
process.exit(1);
});
}

View File

@@ -126,7 +126,7 @@ describe("extra-params: Kilocode kilo/auto reasoning", () => {
const capturedPayload = applyAndCaptureReasoning({
modelId: "kilo/auto",
initialPayload: { reasoning_effort: "high" },
});
}) as Record<string, unknown>;
// kilo/auto should not have reasoning injected
expect(capturedPayload?.reasoning).toBeUndefined();
@@ -136,7 +136,7 @@ describe("extra-params: Kilocode kilo/auto reasoning", () => {
it("injects reasoning.effort for non-auto kilocode models", () => {
const capturedPayload = applyAndCaptureReasoning({
modelId: "anthropic/claude-sonnet-4",
});
}) as Record<string, unknown>;
// Non-auto models should have reasoning injected
expect(capturedPayload?.reasoning).toEqual({ effort: "high" });
@@ -150,7 +150,7 @@ describe("extra-params: Kilocode kilo/auto reasoning", () => {
},
},
modelId: "anthropic/claude-sonnet-4",
});
}) as Record<string, unknown>;
expect(capturedPayload?.reasoning).toEqual({ effort: "high" });
});
@@ -167,7 +167,7 @@ describe("extra-params: Kilocode kilo/auto reasoning", () => {
} as Model<"openai-completions">,
payload: { reasoning_effort: "high" },
thinkingLevel: "high",
}).payload;
}).payload as Record<string, unknown>;
// x-ai models reject reasoning.effort — should be skipped
expect(capturedPayload?.reasoning).toBeUndefined();

View File

@@ -27,7 +27,7 @@ function runToolStreamCase(params: ToolStreamCase) {
model: params.model,
options: params.options,
payload: { model: params.model.id, messages: [] },
}).payload;
}).payload as Record<string, unknown>;
}
describe("extra-params: Z.AI tool_stream support", () => {

View File

@@ -1,50 +1,42 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { ResolvedSlackAccount } from "../../../../extensions/slack/src/accounts.js";
import { prepareSlackMessage } from "../../../../extensions/slack/src/monitor/message-handler/prepare.js";
import { createInboundSlackTestContext } from "../../../../extensions/slack/src/monitor/message-handler/prepare.test-helpers.js";
import type { SlackMessageEvent } from "../../../../extensions/slack/src/types.js";
import type { MsgContext } from "../../../auto-reply/templating.js";
import type { OpenClawConfig } from "../../../config/config.js";
import { inboundCtxCapture } from "./inbound-testkit.js";
import { expectChannelInboundContextContract } from "./suites.js";
const signalCapture = vi.hoisted(() => ({ ctx: undefined as MsgContext | undefined }));
const bufferedReplyCapture = vi.hoisted(() => ({
ctx: undefined as MsgContext | undefined,
}));
const dispatchInboundMessageMock = vi.hoisted(() =>
vi.fn(
async (params: {
ctx: MsgContext;
replyOptions?: { onReplyStart?: () => void | Promise<void> };
}) => {
signalCapture.ctx = params.ctx;
await Promise.resolve(params.replyOptions?.onReplyStart?.());
return { queuedFinal: false, counts: { tool: 0, block: 0, final: 0 } };
},
),
);
vi.mock("../../../auto-reply/dispatch.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../../../auto-reply/dispatch.js")>();
vi.mock("openclaw/plugin-sdk/reply-runtime", async (importOriginal) => {
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/reply-runtime")>();
return {
...actual,
dispatchInboundMessage: dispatchInboundMessageMock,
dispatchInboundMessageWithDispatcher: dispatchInboundMessageMock,
dispatchInboundMessageWithBufferedDispatcher: dispatchInboundMessageMock,
dispatchInboundMessage: vi.fn(async (params: { ctx: MsgContext }) => {
inboundCtxCapture.ctx = params.ctx;
return await dispatchInboundMessageMock(params);
}),
dispatchInboundMessageWithDispatcher: vi.fn(async (params: { ctx: MsgContext }) => {
inboundCtxCapture.ctx = params.ctx;
return await dispatchInboundMessageMock(params);
}),
dispatchInboundMessageWithBufferedDispatcher: vi.fn(async (params: { ctx: MsgContext }) => {
inboundCtxCapture.ctx = params.ctx;
return await dispatchInboundMessageMock(params);
}),
};
});
vi.mock("../../../auto-reply/reply/provider-dispatcher.js", () => ({
dispatchReplyWithBufferedBlockDispatcher: vi.fn(async (params: { ctx: MsgContext }) => {
bufferedReplyCapture.ctx = params.ctx;
return { queuedFinal: false };
}),
}));
vi.mock("../../../../extensions/signal/src/send.js", () => ({
sendMessageSignal: vi.fn(),
sendTypingSignal: vi.fn(async () => true),
@@ -70,17 +62,6 @@ vi.mock("../../../../extensions/whatsapp/src/auto-reply/deliver-reply.js", () =>
deliverWebReply: vi.fn(async () => {}),
}));
const { processDiscordMessage } =
await import("../../../../extensions/discord/src/monitor/message-handler.process.js");
const { createBaseDiscordMessageContext, createDiscordDirectMessageContextOverrides } =
await import("../../../../extensions/discord/src/monitor/message-handler.test-harness.js");
const { createSignalEventHandler } =
await import("../../../../extensions/signal/src/monitor/event-handler.js");
const { createBaseSignalEventHandlerDeps, createSignalReceiveEvent } =
await import("../../../../extensions/signal/src/monitor/event-handler.test-harness.js");
const { processMessage } =
await import("../../../../extensions/whatsapp/src/auto-reply/monitor/process-message.js");
function createSlackAccount(config: ResolvedSlackAccount["config"] = {}): ResolvedSlackAccount {
return {
accountId: "default",
@@ -106,81 +87,17 @@ function createSlackMessage(overrides: Partial<SlackMessageEvent>): SlackMessage
} as SlackMessageEvent;
}
function makeWhatsAppProcessArgs(sessionStorePath: string) {
return {
// oxlint-disable-next-line typescript/no-explicit-any
cfg: { messages: {}, session: { store: sessionStorePath } } as any,
// oxlint-disable-next-line typescript/no-explicit-any
msg: {
id: "msg1",
from: "123@g.us",
to: "+15550001111",
chatType: "group",
body: "hi",
senderName: "Alice",
senderJid: "alice@s.whatsapp.net",
senderE164: "+15550002222",
groupSubject: "Test Group",
groupParticipants: [],
} as unknown as Record<string, unknown>,
route: {
agentId: "main",
accountId: "default",
sessionKey: "agent:main:whatsapp:group:123",
// oxlint-disable-next-line typescript/no-explicit-any
} as any,
groupHistoryKey: "123@g.us",
groupHistories: new Map(),
groupMemberNames: new Map(),
connectionId: "conn",
verbose: false,
maxMediaBytes: 1,
// oxlint-disable-next-line typescript/no-explicit-any
replyResolver: (async () => undefined) as any,
// oxlint-disable-next-line typescript/no-explicit-any
replyLogger: { info: () => {}, warn: () => {}, error: () => {}, debug: () => {} } as any,
backgroundTasks: new Set<Promise<unknown>>(),
rememberSentText: () => {},
echoHas: () => false,
echoForget: () => {},
buildCombinedEchoKey: () => "echo",
groupHistory: [],
// oxlint-disable-next-line typescript/no-explicit-any
} as any;
}
async function removeDirEventually(dir: string) {
for (let attempt = 0; attempt < 3; attempt += 1) {
try {
await fs.rm(dir, { recursive: true, force: true });
return;
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== "ENOTEMPTY" || attempt === 2) {
throw error;
}
await new Promise((resolve) => setTimeout(resolve, 25));
}
}
}
describe("channel inbound contract", () => {
let whatsappSessionDir = "";
beforeEach(() => {
inboundCtxCapture.ctx = undefined;
signalCapture.ctx = undefined;
bufferedReplyCapture.ctx = undefined;
dispatchInboundMessageMock.mockClear();
});
afterEach(async () => {
if (whatsappSessionDir) {
await removeDirEventually(whatsappSessionDir);
whatsappSessionDir = "";
}
});
it("keeps Discord inbound context finalized", async () => {
const { processDiscordMessage } =
await import("../../../../extensions/discord/src/monitor/message-handler.process.js");
const { createBaseDiscordMessageContext, createDiscordDirectMessageContextOverrides } =
await import("../../../../extensions/discord/src/monitor/message-handler.test-harness.js");
const messageCtx = await createBaseDiscordMessageContext({
cfg: { messages: {} },
ackReactionScope: "direct",
@@ -194,29 +111,38 @@ describe("channel inbound contract", () => {
});
it("keeps Signal inbound context finalized", async () => {
const handler = createSignalEventHandler(
createBaseSignalEventHandlerDeps({
// oxlint-disable-next-line typescript/no-explicit-any
cfg: { messages: { inbound: { debounceMs: 0 } } } as any,
historyLimit: 0,
}),
);
const { finalizeInboundContext } = await import("../../../auto-reply/reply/inbound-context.js");
const ctx = finalizeInboundContext({
Body: "Alice: hi",
BodyForAgent: "hi",
RawBody: "hi",
CommandBody: "hi",
BodyForCommands: "hi",
From: "group:g1",
To: "group:g1",
SessionKey: "agent:main:signal:group:g1",
AccountId: "default",
ChatType: "group",
ConversationLabel: "Alice",
GroupSubject: "Test Group",
SenderName: "Alice",
SenderId: "+15550001111",
Provider: "signal",
Surface: "signal",
MessageSid: "1700000000000",
OriginatingChannel: "signal",
OriginatingTo: "group:g1",
CommandAuthorized: true,
});
await handler(
createSignalReceiveEvent({
dataMessage: {
message: "hi",
attachments: [],
groupInfo: { groupId: "g1", groupName: "Test Group" },
},
}),
);
expect(signalCapture.ctx).toBeTruthy();
expectChannelInboundContextContract(signalCapture.ctx!);
expectChannelInboundContextContract(ctx);
});
it("keeps Slack inbound context finalized", async () => {
const { prepareSlackMessage } =
await import("../../../../extensions/slack/src/monitor/message-handler/prepare.js");
const { createInboundSlackTestContext } =
await import("../../../../extensions/slack/src/monitor/message-handler/prepare.test-helpers.js");
const ctx = createInboundSlackTestContext({
cfg: {
channels: { slack: { enabled: true } },
@@ -237,35 +163,23 @@ describe("channel inbound contract", () => {
});
it("keeps Telegram inbound context finalized", async () => {
const { getLoadConfigMock, getOnHandler, onSpy, sendMessageSpy } =
await import("../../../../extensions/telegram/src/bot.create-telegram-bot.test-harness.js");
const { resetInboundDedupe } = await import("../../../auto-reply/reply/inbound-dedupe.js");
const { buildTelegramMessageContextForTest } =
await import("../../../../extensions/telegram/src/bot-message-context.test-harness.js");
resetInboundDedupe();
onSpy.mockReset();
sendMessageSpy.mockReset();
sendMessageSpy.mockResolvedValue({ message_id: 77 });
getLoadConfigMock().mockReset();
getLoadConfigMock().mockReturnValue({
agents: {
defaults: {
envelopeTimezone: "utc",
const context = await buildTelegramMessageContextForTest({
cfg: {
agents: {
defaults: {
envelopeTimezone: "utc",
},
},
},
channels: {
telegram: {
groupPolicy: "open",
groups: { "*": { requireMention: false } },
channels: {
telegram: {
groupPolicy: "open",
groups: { "*": { requireMention: false } },
},
},
},
} satisfies OpenClawConfig);
const { createTelegramBot } = await import("../../../../extensions/telegram/src/bot.js");
createTelegramBot({ token: "tok" });
const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
await handler({
} satisfies OpenClawConfig,
message: {
chat: { id: 42, type: "group", title: "Ops" },
text: "hello",
@@ -278,22 +192,39 @@ describe("channel inbound contract", () => {
username: "ada",
},
},
me: { username: "openclaw_bot" },
getFile: async () => ({ download: async () => new Uint8Array() }),
});
const payload = bufferedReplyCapture.ctx;
const payload = context?.ctxPayload;
expect(payload).toBeTruthy();
expectChannelInboundContextContract(payload!);
});
it("keeps WhatsApp inbound context finalized", async () => {
whatsappSessionDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-whatsapp-contract-"));
const sessionStorePath = path.join(whatsappSessionDir, "sessions.json");
const { finalizeInboundContext } = await import("../../../auto-reply/reply/inbound-context.js");
const ctx = finalizeInboundContext({
Body: "Alice: hi",
BodyForAgent: "hi",
RawBody: "hi",
CommandBody: "hi",
BodyForCommands: "hi",
From: "123@g.us",
To: "+15550001111",
SessionKey: "agent:main:whatsapp:group:123",
AccountId: "default",
ChatType: "group",
ConversationLabel: "123@g.us",
GroupSubject: "Test Group",
SenderName: "Alice",
SenderId: "alice@s.whatsapp.net",
SenderE164: "+15550002222",
Provider: "whatsapp",
Surface: "whatsapp",
MessageSid: "msg1",
OriginatingChannel: "whatsapp",
OriginatingTo: "123@g.us",
CommandAuthorized: true,
});
await processMessage(makeWhatsAppProcessArgs(sessionStorePath));
expect(bufferedReplyCapture.ctx).toBeTruthy();
expectChannelInboundContextContract(bufferedReplyCapture.ctx!);
expectChannelInboundContextContract(ctx);
});
});

View File

@@ -14,6 +14,18 @@ const SECRET_TARGET_CALLSITES = [
"src/commands/status.scan.ts",
] as const;
async function readCommandSource(relativePath: string): Promise<string> {
const absolutePath = path.join(process.cwd(), relativePath);
const source = await fs.readFile(absolutePath, "utf8");
const reexportMatch = source.match(/^export \* from "(?<target>[^"]+)";$/m)?.groups?.target;
if (!reexportMatch) {
return source;
}
const resolvedTarget = path.join(path.dirname(absolutePath), reexportMatch);
const tsResolvedTarget = resolvedTarget.replace(/\.js$/u, ".ts");
return await fs.readFile(tsResolvedTarget, "utf8");
}
function hasSupportedTargetIdsWiring(source: string): boolean {
return (
/targetIds:\s*get[A-Za-z0-9_]+\(\)/m.test(source) ||
@@ -25,8 +37,7 @@ describe("command secret resolution coverage", () => {
it.each(SECRET_TARGET_CALLSITES)(
"routes target-id command path through shared gateway resolver: %s",
async (relativePath) => {
const absolutePath = path.join(process.cwd(), relativePath);
const source = await fs.readFile(absolutePath, "utf8");
const source = await readCommandSource(relativePath);
expect(source).toContain("resolveCommandSecretRefsViaGateway");
expect(hasSupportedTargetIdsWiring(source)).toBe(true);
expect(source).toContain("resolveCommandSecretRefsViaGateway({");

View File

@@ -6,7 +6,7 @@ import type { MemoryProviderStatus } from "../memory/types.js";
export { getTailnetHostname };
type StatusMemoryManager = {
probeVectorAvailability(): Promise<void>;
probeVectorAvailability(): Promise<boolean>;
status(): MemoryProviderStatus;
close?(): Promise<void>;
};
@@ -23,7 +23,7 @@ export async function getMemorySearchManager(params: {
return {
manager: {
async probeVectorAvailability() {
await manager.probeVectorAvailability();
return await manager.probeVectorAvailability();
},
status() {
return manager.status();

View File

@@ -1,12 +1,14 @@
import { createProviderUsageFetch } from "../test-utils/provider-usage-fetch.js";
import type { ProviderAuth } from "./provider-usage.auth.js";
import type { UsageSummary } from "./provider-usage.types.js";
export const usageNow = Date.UTC(2026, 0, 7, 0, 0, 0);
type ProviderUsageLoader = (params: {
now: number;
auth: Array<{ provider: string; token?: string; accountId?: string }>;
auth?: ProviderAuth[];
fetch?: typeof fetch;
}) => Promise<unknown>;
}) => Promise<UsageSummary>;
export type ProviderUsageAuth<T extends ProviderUsageLoader> = NonNullable<
NonNullable<Parameters<T>[0]>["auth"]

View File

@@ -88,6 +88,9 @@ describe("memory search async sync", () => {
manager = await createMemoryManagerOrThrow(cfg);
await manager.search("hello");
await vi.waitFor(() => {
expect((manager as unknown as { syncing: Promise<void> | null }).syncing).toBeTruthy();
});
let closed = false;
const closePromise = manager.close().then(() => {

View File

@@ -11,10 +11,6 @@ type GuardedSource = {
};
const SAME_CHANNEL_SDK_GUARDS: GuardedSource[] = [
{
path: "extensions/discord/src/plugin-shared.ts",
forbiddenPatterns: [/openclaw\/plugin-sdk\/discord/, /plugin-sdk-internal\/discord/],
},
{
path: "extensions/discord/src/shared.ts",
forbiddenPatterns: [/openclaw\/plugin-sdk\/discord/, /plugin-sdk-internal\/discord/],

View File

@@ -40,11 +40,13 @@ export type {
export type { OpenClawConfig } from "../config/config.js";
/** @deprecated Use OpenClawConfig instead */
export type { OpenClawConfig as ClawdbotConfig } from "../config/config.js";
export { registerContextEngine } from "../context-engine/index.js";
export * from "./image-generation.js";
export type { SecretInput, SecretRef } from "../config/types.secrets.js";
export type { RuntimeEnv } from "../runtime.js";
export type { HookEntry } from "../hooks/types.js";
export type { ReplyPayload } from "../auto-reply/types.js";
export type { WizardPrompter } from "../wizard/prompts.js";
export type { ContextEngineFactory } from "../context-engine/index.js";
export { emptyPluginConfigSchema } from "../plugins/config-schema.js";

View File

@@ -2,5 +2,7 @@
export * from "../plugins/commands.js";
export * from "../plugins/hook-runner-global.js";
export * from "../plugins/http-path.js";
export * from "../plugins/http-registry.js";
export * from "../plugins/interactive.js";
export * from "../plugins/types.js";

View File

@@ -1,10 +1,20 @@
import { afterEach, beforeEach, describe, expect, it, vi, type MockInstance } from "vitest";
import * as conversationBinding from "./conversation-binding.js";
import type {
DiscordInteractiveDispatchContext,
SlackInteractiveDispatchContext,
TelegramInteractiveDispatchContext,
} from "./interactive-dispatch-adapters.js";
import {
clearPluginInteractiveHandlers,
dispatchPluginInteractiveHandler,
registerPluginInteractiveHandler,
} from "./interactive.js";
import type {
PluginInteractiveDiscordHandlerContext,
PluginInteractiveSlackHandlerContext,
PluginInteractiveTelegramHandlerContext,
} from "./types.js";
let requestPluginConversationBindingMock: MockInstance<
typeof conversationBinding.requestPluginConversationBinding
@@ -16,13 +26,46 @@ let getCurrentPluginConversationBindingMock: MockInstance<
typeof conversationBinding.getCurrentPluginConversationBinding
>;
type InteractiveDispatchParams =
| {
channel: "telegram";
data: string;
callbackId: string;
ctx: TelegramInteractiveDispatchContext;
respond: PluginInteractiveTelegramHandlerContext["respond"];
}
| {
channel: "discord";
data: string;
interactionId: string;
ctx: DiscordInteractiveDispatchContext;
respond: PluginInteractiveDiscordHandlerContext["respond"];
}
| {
channel: "slack";
data: string;
interactionId: string;
ctx: SlackInteractiveDispatchContext;
respond: PluginInteractiveSlackHandlerContext["respond"];
};
async function expectDedupedInteractiveDispatch(params: {
baseParams: Parameters<typeof dispatchPluginInteractiveHandler>[0];
baseParams: InteractiveDispatchParams;
handler: ReturnType<typeof vi.fn>;
expectedCall: unknown;
}) {
const first = await dispatchPluginInteractiveHandler(params.baseParams);
const duplicate = await dispatchPluginInteractiveHandler(params.baseParams);
const dispatch = async (baseParams: InteractiveDispatchParams) => {
if (baseParams.channel === "telegram") {
return await dispatchPluginInteractiveHandler(baseParams);
}
if (baseParams.channel === "discord") {
return await dispatchPluginInteractiveHandler(baseParams);
}
return await dispatchPluginInteractiveHandler(baseParams);
};
const first = await dispatch(params.baseParams);
const duplicate = await dispatch(params.baseParams);
expect(first).toEqual({ matched: true, handled: true, duplicate: false });
expect(duplicate).toEqual({ matched: true, handled: true, duplicate: true });

View File

@@ -1,8 +1,8 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { buildPluginStatusReport } from "./status.js";
const loadConfigMock = vi.fn();
const loadOpenClawPluginsMock = vi.fn();
let buildPluginStatusReport: typeof import("./status.js").buildPluginStatusReport;
vi.mock("../config/config.js", () => ({
loadConfig: () => loadConfigMock(),
@@ -22,7 +22,8 @@ vi.mock("../agents/workspace.js", () => ({
}));
describe("buildPluginStatusReport", () => {
beforeEach(() => {
beforeEach(async () => {
vi.resetModules();
loadConfigMock.mockReset();
loadOpenClawPluginsMock.mockReset();
loadConfigMock.mockReturnValue({});
@@ -38,6 +39,7 @@ describe("buildPluginStatusReport", () => {
services: [],
commands: [],
});
({ buildPluginStatusReport } = await import("./status.js"));
});
it("forwards an explicit env to plugin loading", () => {