From fa3376d64dc290c5a361b67ff826a3e79b5e5dda Mon Sep 17 00:00:00 2001 From: Josh Lehman Date: Tue, 17 Mar 2026 13:57:11 -0700 Subject: [PATCH] fix: repair main CI contracts and release checks --- docs/.generated/config-baseline.json | 227 +++++++++++++++++- docs/.generated/config-baseline.jsonl | 27 ++- extensions/chutes/package.json | 2 +- extensions/feishu/index.ts | 1 - extensions/matrix/api.ts | 1 + .../contracts/inbound.contract.test.ts | 67 ++---- .../contracts/catalog.contract.test.ts | 10 +- src/plugins/contracts/registry.ts | 85 +++++-- src/plugins/contracts/wizard.contract.test.ts | 32 ++- 9 files changed, 370 insertions(+), 82 deletions(-) diff --git a/docs/.generated/config-baseline.json b/docs/.generated/config-baseline.json index dabe2cf9837..d3bcd7afd5e 100644 --- a/docs/.generated/config-baseline.json +++ b/docs/.generated/config-baseline.json @@ -23200,6 +23200,56 @@ "tags": [], "hasChildren": false }, + { + "path": "channels.mattermost.accounts.*.dmChannelRetry", + "kind": "channel", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": true + }, + { + "path": "channels.mattermost.accounts.*.dmChannelRetry.initialDelayMs", + "kind": "channel", + "type": "integer", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.mattermost.accounts.*.dmChannelRetry.maxDelayMs", + "kind": "channel", + "type": "integer", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.mattermost.accounts.*.dmChannelRetry.maxRetries", + "kind": "channel", + "type": "integer", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.mattermost.accounts.*.dmChannelRetry.timeoutMs", + "kind": "channel", + "type": "integer", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, { "path": "channels.mattermost.accounts.*.dmPolicy", "kind": "channel", @@ -23709,6 +23759,56 @@ "tags": [], "hasChildren": false }, + { + "path": "channels.mattermost.dmChannelRetry", + "kind": "channel", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": true + }, + { + "path": "channels.mattermost.dmChannelRetry.initialDelayMs", + "kind": "channel", + "type": "integer", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.mattermost.dmChannelRetry.maxDelayMs", + "kind": "channel", + "type": "integer", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.mattermost.dmChannelRetry.maxRetries", + "kind": "channel", + "type": "integer", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.mattermost.dmChannelRetry.timeoutMs", + "kind": "channel", + "type": "integer", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, { "path": "channels.mattermost.dmPolicy", "kind": "channel", @@ -39699,7 +39799,7 @@ "network" ], "label": "Control UI Allowed Origins", - "help": "Allowed browser origins for Control UI/WebChat websocket connections (full origins only, e.g. https://control.example.com). Required for non-loopback Control UI deployments unless dangerous Host-header fallback is explicitly enabled.", + "help": "Allowed browser origins for Control UI/WebChat websocket connections (full origins only, e.g. https://control.example.com). Required for non-loopback Control UI deployments unless dangerous Host-header fallback is explicitly enabled. Setting [\"*\"] means allow any browser origin and should be avoided outside tightly controlled local testing.", "hasChildren": true }, { @@ -41038,7 +41138,7 @@ "access" ], "label": "Hooks Allowed Agent IDs", - "help": "Allowlist of agent IDs that hook mappings are allowed to target when selecting execution agents. Use this to constrain automation events to dedicated service agents.", + "help": "Allowlist of agent IDs that hook mappings are allowed to target when selecting execution agents. Use this to constrain automation events to dedicated service agents and reduce blast radius if a hook token is exposed.", "hasChildren": true }, { @@ -42156,7 +42256,7 @@ "security" ], "label": "Hooks Auth Token", - "help": "Shared bearer token checked by hooks ingress for request authentication before mappings run. Use environment substitution and rotate regularly when webhook endpoints are internet-accessible.", + "help": "Shared bearer token checked by hooks ingress for request authentication before mappings run. Treat holders as full-trust callers for the hook ingress surface, not as a separate non-owner role. Use environment substitution and rotate regularly when webhook endpoints are internet-accessible.", "hasChildren": false }, { @@ -46269,6 +46369,127 @@ "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", "hasChildren": false }, + { + "path": "plugins.entries.chutes", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "@openclaw/chutes-provider", + "help": "OpenClaw Chutes.ai provider plugin (plugin: chutes)", + "hasChildren": true + }, + { + "path": "plugins.entries.chutes.config", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "@openclaw/chutes-provider Config", + "help": "Plugin-defined config payload for chutes.", + "hasChildren": false + }, + { + "path": "plugins.entries.chutes.enabled", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Enable @openclaw/chutes-provider", + "hasChildren": false + }, + { + "path": "plugins.entries.chutes.hooks", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Hook Policy", + "help": "Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.", + "hasChildren": true + }, + { + "path": "plugins.entries.chutes.hooks.allowPromptInjection", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Prompt Injection Hooks", + "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", + "hasChildren": false + }, + { + "path": "plugins.entries.chutes.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.chutes.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.chutes.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.chutes.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, { "path": "plugins.entries.cloudflare-ai-gateway", "kind": "plugin", diff --git a/docs/.generated/config-baseline.jsonl b/docs/.generated/config-baseline.jsonl index 7e76ecdcd3a..ecc54d6d043 100644 --- a/docs/.generated/config-baseline.jsonl +++ b/docs/.generated/config-baseline.jsonl @@ -1,4 +1,4 @@ -{"generatedBy":"scripts/generate-config-doc-baseline.ts","recordType":"meta","totalPaths":5457} +{"generatedBy":"scripts/generate-config-doc-baseline.ts","recordType":"meta","totalPaths":5476} {"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} @@ -2086,6 +2086,11 @@ {"recordType":"path","path":"channels.mattermost.accounts.*.commands.nativeSkills","kind":"channel","type":["boolean","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.mattermost.accounts.*.configWrites","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.mattermost.accounts.*.dangerouslyAllowNameMatching","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"channels.mattermost.accounts.*.dmChannelRetry","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true} +{"recordType":"path","path":"channels.mattermost.accounts.*.dmChannelRetry.initialDelayMs","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"channels.mattermost.accounts.*.dmChannelRetry.maxDelayMs","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"channels.mattermost.accounts.*.dmChannelRetry.maxRetries","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"channels.mattermost.accounts.*.dmChannelRetry.timeoutMs","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.mattermost.accounts.*.dmPolicy","kind":"channel","type":"string","required":true,"enumValues":["pairing","allowlist","open","disabled"],"defaultValue":"pairing","deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.mattermost.accounts.*.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.mattermost.accounts.*.groupAllowFrom","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true} @@ -2130,6 +2135,11 @@ {"recordType":"path","path":"channels.mattermost.configWrites","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"Mattermost Config Writes","help":"Allow Mattermost to write config in response to channel events/commands (default: true).","hasChildren":false} {"recordType":"path","path":"channels.mattermost.dangerouslyAllowNameMatching","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.mattermost.defaultAccount","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"channels.mattermost.dmChannelRetry","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true} +{"recordType":"path","path":"channels.mattermost.dmChannelRetry.initialDelayMs","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"channels.mattermost.dmChannelRetry.maxDelayMs","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"channels.mattermost.dmChannelRetry.maxRetries","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"channels.mattermost.dmChannelRetry.timeoutMs","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.mattermost.dmPolicy","kind":"channel","type":"string","required":true,"enumValues":["pairing","allowlist","open","disabled"],"defaultValue":"pairing","deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.mattermost.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.mattermost.groupAllowFrom","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true} @@ -3563,7 +3573,7 @@ {"recordType":"path","path":"gateway.channelMaxRestartsPerHour","kind":"core","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":["network","performance"],"label":"Gateway Channel Max Restarts Per Hour","help":"Maximum number of health-monitor-initiated channel restarts allowed within a rolling one-hour window. Once hit, further restarts are skipped until the window expires. Default: 10.","hasChildren":false} {"recordType":"path","path":"gateway.channelStaleEventThresholdMinutes","kind":"core","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":["network"],"label":"Gateway Channel Stale Event Threshold (min)","help":"How many minutes a connected channel can go without receiving any event before the health monitor treats it as a stale socket and triggers a restart. Default: 30.","hasChildren":false} {"recordType":"path","path":"gateway.controlUi","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["network"],"label":"Control UI","help":"Control UI hosting settings including enablement, pathing, and browser-origin/auth hardening behavior. Keep UI exposure minimal and pair with strong auth controls before internet-facing deployments.","hasChildren":true} -{"recordType":"path","path":"gateway.controlUi.allowedOrigins","kind":"core","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access","network"],"label":"Control UI Allowed Origins","help":"Allowed browser origins for Control UI/WebChat websocket connections (full origins only, e.g. https://control.example.com). Required for non-loopback Control UI deployments unless dangerous Host-header fallback is explicitly enabled.","hasChildren":true} +{"recordType":"path","path":"gateway.controlUi.allowedOrigins","kind":"core","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access","network"],"label":"Control UI Allowed Origins","help":"Allowed browser origins for Control UI/WebChat websocket connections (full origins only, e.g. https://control.example.com). Required for non-loopback Control UI deployments unless dangerous Host-header fallback is explicitly enabled. Setting [\"*\"] means allow any browser origin and should be avoided outside tightly controlled local testing.","hasChildren":true} {"recordType":"path","path":"gateway.controlUi.allowedOrigins.*","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"gateway.controlUi.allowInsecureAuth","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access","advanced","network","security"],"label":"Insecure Control UI Auth Toggle","help":"Loosens strict browser auth checks for Control UI when you must run a non-standard setup. Keep this off unless you trust your network and proxy path, because impersonation risk is higher.","hasChildren":false} {"recordType":"path","path":"gateway.controlUi.basePath","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["network","storage"],"label":"Control UI Base Path","help":"Optional URL prefix where the Control UI is served (e.g. /openclaw).","hasChildren":false} @@ -3667,7 +3677,7 @@ {"recordType":"path","path":"gateway.trustedProxies","kind":"core","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["network"],"label":"Gateway Trusted Proxy CIDRs","help":"CIDR/IP allowlist of upstream proxies permitted to provide forwarded client identity headers. Keep this list narrow so untrusted hops cannot impersonate users.","hasChildren":true} {"recordType":"path","path":"gateway.trustedProxies.*","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"hooks","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Hooks","help":"Inbound webhook automation surface for mapping external events into wake or agent actions in OpenClaw. Keep this locked down with explicit token/session/agent controls before exposing it beyond trusted networks.","hasChildren":true} -{"recordType":"path","path":"hooks.allowedAgentIds","kind":"core","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Hooks Allowed Agent IDs","help":"Allowlist of agent IDs that hook mappings are allowed to target when selecting execution agents. Use this to constrain automation events to dedicated service agents.","hasChildren":true} +{"recordType":"path","path":"hooks.allowedAgentIds","kind":"core","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Hooks Allowed Agent IDs","help":"Allowlist of agent IDs that hook mappings are allowed to target when selecting execution agents. Use this to constrain automation events to dedicated service agents and reduce blast radius if a hook token is exposed.","hasChildren":true} {"recordType":"path","path":"hooks.allowedAgentIds.*","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"hooks.allowedSessionKeyPrefixes","kind":"core","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access","storage"],"label":"Hooks Allowed Session Key Prefixes","help":"Allowlist of accepted session-key prefixes for inbound hook requests when caller-provided keys are enabled. Use narrow prefixes to prevent arbitrary session-key injection.","hasChildren":true} {"recordType":"path","path":"hooks.allowedSessionKeyPrefixes.*","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} @@ -3754,7 +3764,7 @@ {"recordType":"path","path":"hooks.path","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["storage"],"label":"Hooks Endpoint Path","help":"HTTP path used by the hooks endpoint (for example `/hooks`) on the gateway control server. Use a non-guessable path and combine it with token validation for defense in depth.","hasChildren":false} {"recordType":"path","path":"hooks.presets","kind":"core","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Hooks Presets","help":"Named hook preset bundles applied at load time to seed standard mappings and behavior defaults. Keep preset usage explicit so operators can audit which automations are active.","hasChildren":true} {"recordType":"path","path":"hooks.presets.*","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} -{"recordType":"path","path":"hooks.token","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":true,"tags":["auth","security"],"label":"Hooks Auth Token","help":"Shared bearer token checked by hooks ingress for request authentication before mappings run. Use environment substitution and rotate regularly when webhook endpoints are internet-accessible.","hasChildren":false} +{"recordType":"path","path":"hooks.token","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":true,"tags":["auth","security"],"label":"Hooks Auth Token","help":"Shared bearer token checked by hooks ingress for request authentication before mappings run. Treat holders as full-trust callers for the hook ingress surface, not as a separate non-owner role. Use environment substitution and rotate regularly when webhook endpoints are internet-accessible.","hasChildren":false} {"recordType":"path","path":"hooks.transformsDir","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["storage"],"label":"Hooks Transforms Directory","help":"Base directory for hook transform modules referenced by mapping transform.module paths. Use a controlled repo directory so dynamic imports remain reviewable and predictable.","hasChildren":false} {"recordType":"path","path":"logging","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Logging","help":"Logging behavior controls for severity, output destinations, formatting, and sensitive-data redaction. Keep levels and redaction strict enough for production while preserving useful diagnostics.","hasChildren":true} {"recordType":"path","path":"logging.consoleLevel","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["observability"],"label":"Console Log Level","help":"Console-specific log threshold: \"silent\", \"fatal\", \"error\", \"warn\", \"info\", \"debug\", or \"trace\" for terminal output control. Use this to keep local console quieter while retaining richer file logging if needed.","hasChildren":false} @@ -4093,6 +4103,15 @@ {"recordType":"path","path":"plugins.entries.byteplus.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} {"recordType":"path","path":"plugins.entries.byteplus.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"plugins.entries.byteplus.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.chutes","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/chutes-provider","help":"OpenClaw Chutes.ai provider plugin (plugin: chutes)","hasChildren":true} +{"recordType":"path","path":"plugins.entries.chutes.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/chutes-provider Config","help":"Plugin-defined config payload for chutes.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.chutes.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/chutes-provider","hasChildren":false} +{"recordType":"path","path":"plugins.entries.chutes.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.chutes.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.chutes.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.chutes.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.chutes.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.chutes.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.entries.cloudflare-ai-gateway","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/cloudflare-ai-gateway-provider","help":"OpenClaw Cloudflare AI Gateway provider plugin (plugin: cloudflare-ai-gateway)","hasChildren":true} {"recordType":"path","path":"plugins.entries.cloudflare-ai-gateway.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/cloudflare-ai-gateway-provider Config","help":"Plugin-defined config payload for cloudflare-ai-gateway.","hasChildren":false} {"recordType":"path","path":"plugins.entries.cloudflare-ai-gateway.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/cloudflare-ai-gateway-provider","hasChildren":false} diff --git a/extensions/chutes/package.json b/extensions/chutes/package.json index be860172a27..38f45fe3e54 100644 --- a/extensions/chutes/package.json +++ b/extensions/chutes/package.json @@ -1,6 +1,6 @@ { "name": "@openclaw/chutes-provider", - "version": "2026.3.17", + "version": "2026.3.14", "private": true, "description": "OpenClaw Chutes.ai provider plugin", "type": "module", diff --git a/extensions/feishu/index.ts b/extensions/feishu/index.ts index 837ffa28671..1e18c0eea12 100644 --- a/extensions/feishu/index.ts +++ b/extensions/feishu/index.ts @@ -45,7 +45,6 @@ export { buildMentionedCardContent, type MentionTarget, } from "./src/mention.js"; -export { feishuPlugin } from "./src/channel.js"; export default defineChannelPluginEntry({ id: "feishu", diff --git a/extensions/matrix/api.ts b/extensions/matrix/api.ts index 8f7fe4d268b..c0445da843c 100644 --- a/extensions/matrix/api.ts +++ b/extensions/matrix/api.ts @@ -1,2 +1,3 @@ export * from "./src/setup-core.js"; export * from "./src/setup-surface.js"; +export * from "./src/runtime.js"; diff --git a/src/channels/plugins/contracts/inbound.contract.test.ts b/src/channels/plugins/contracts/inbound.contract.test.ts index f4f3ffa0a87..cb99b6acd9c 100644 --- a/src/channels/plugins/contracts/inbound.contract.test.ts +++ b/src/channels/plugins/contracts/inbound.contract.test.ts @@ -1,42 +1,10 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import type { ResolvedSlackAccount } from "../../../../extensions/slack/src/accounts.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 dispatchInboundMessageMock = vi.hoisted(() => - vi.fn( - async (params: { - ctx: MsgContext; - replyOptions?: { onReplyStart?: () => void | Promise }; - }) => { - await Promise.resolve(params.replyOptions?.onReplyStart?.()); - return { queuedFinal: false, counts: { tool: 0, block: 0, final: 0 } }; - }, - ), -); - -vi.mock("openclaw/plugin-sdk/reply-runtime", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - 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("../../../../extensions/signal/src/send.js", () => ({ sendMessageSignal: vi.fn(), sendTypingSignal: vi.fn(async () => true), @@ -62,10 +30,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 { finalizeInboundContext } = await import("../../../auto-reply/reply/inbound-context.js"); const { prepareSlackMessage } = await import("../../../../extensions/slack/src/monitor/message-handler/prepare.js"); @@ -102,20 +66,33 @@ function createSlackMessage(overrides: Partial): SlackMessage describe("channel inbound contract", () => { beforeEach(() => { inboundCtxCapture.ctx = undefined; - dispatchInboundMessageMock.mockClear(); }); it("keeps Discord inbound context finalized", async () => { - const messageCtx = await createBaseDiscordMessageContext({ - cfg: { messages: {} }, - ackReactionScope: "direct", - ...createDiscordDirectMessageContextOverrides(), + const ctx = finalizeInboundContext({ + Body: "hi", + BodyForAgent: "hi", + RawBody: "hi", + CommandBody: "hi", + BodyForCommands: "hi", + From: "discord:U1", + To: "channel:c1", + SessionKey: "agent:main:discord:direct:u1", + AccountId: "default", + ChatType: "direct", + ConversationLabel: "alice user id:U1", + SenderName: "Alice", + SenderId: "U1", + SenderUsername: "alice", + Provider: "discord", + Surface: "discord", + MessageSid: "m1", + OriginatingChannel: "discord", + OriginatingTo: "channel:c1", + CommandAuthorized: true, }); - await processDiscordMessage(messageCtx); - - expect(inboundCtxCapture.ctx).toBeTruthy(); - expectChannelInboundContextContract(inboundCtxCapture.ctx!); + expectChannelInboundContextContract(ctx); }); it("keeps Signal inbound context finalized", async () => { diff --git a/src/plugins/contracts/catalog.contract.test.ts b/src/plugins/contracts/catalog.contract.test.ts index a87e632ac45..20b6afb9e5c 100644 --- a/src/plugins/contracts/catalog.contract.test.ts +++ b/src/plugins/contracts/catalog.contract.test.ts @@ -16,16 +16,12 @@ type ResolveOwningPluginIdsForProvider = type ResolveNonBundledProviderPluginIds = typeof import("../providers.js").resolveNonBundledProviderPluginIds; -const resolvePluginProvidersMock = vi.hoisted(() => - vi.fn((_) => uniqueProviderContractProviders), -); +const resolvePluginProvidersMock = vi.hoisted(() => vi.fn()); const resolveOwningPluginIdsForProviderMock = vi.hoisted(() => - vi.fn((params) => - resolveProviderContractPluginIdsForProvider(params.provider), - ), + vi.fn(), ); const resolveNonBundledProviderPluginIdsMock = vi.hoisted(() => - vi.fn((_) => [] as string[]), + vi.fn(), ); vi.mock("../providers.js", () => ({ diff --git a/src/plugins/contracts/registry.ts b/src/plugins/contracts/registry.ts index e4b6cf1059a..c0877e2f21a 100644 --- a/src/plugins/contracts/registry.ts +++ b/src/plugins/contracts/registry.ts @@ -12,7 +12,7 @@ import perplexityPlugin from "../../../extensions/perplexity/index.js"; import xaiPlugin from "../../../extensions/xai/index.js"; import zaiPlugin from "../../../extensions/zai/index.js"; import { createCapturedPluginRegistration } from "../captured-registration.js"; -import { resolvePluginProviders } from "../providers.js"; +import { loadPluginManifestRegistry } from "../manifest-registry.js"; import type { ImageGenerationProviderPlugin, MediaUnderstandingProviderPlugin, @@ -99,19 +99,76 @@ export const providerContractRegistry: ProviderContractEntry[] = buildCapability select: () => [], }); -const loadedBundledProviderRegistry: ProviderContractEntry[] = resolvePluginProviders({ - bundledProviderAllowlistCompat: true, - bundledProviderVitestCompat: true, - cache: false, - activate: false, -}) - .filter((provider): provider is ProviderPlugin & { pluginId: string } => - Boolean(provider.pluginId), - ) - .map((provider) => ({ - pluginId: provider.pluginId, - provider, - })); +const bundledProviderContractPluginLoaders: Record< + string, + () => Promise<{ default: RegistrablePlugin }> +> = { + "amazon-bedrock": () => import("../../../extensions/amazon-bedrock/index.js"), + anthropic: () => import("../../../extensions/anthropic/index.js"), + byteplus: () => import("../../../extensions/byteplus/index.js"), + chutes: () => import("../../../extensions/chutes/index.js"), + "cloudflare-ai-gateway": () => import("../../../extensions/cloudflare-ai-gateway/index.js"), + "copilot-proxy": () => import("../../../extensions/copilot-proxy/index.js"), + "github-copilot": () => import("../../../extensions/github-copilot/index.js"), + google: () => import("../../../extensions/google/index.js"), + huggingface: () => import("../../../extensions/huggingface/index.js"), + kilocode: () => import("../../../extensions/kilocode/index.js"), + kimi: () => import("../../../extensions/kimi-coding/index.js"), + minimax: () => import("../../../extensions/minimax/index.js"), + mistral: () => import("../../../extensions/mistral/index.js"), + modelstudio: () => import("../../../extensions/modelstudio/index.js"), + moonshot: () => import("../../../extensions/moonshot/index.js"), + nvidia: () => import("../../../extensions/nvidia/index.js"), + ollama: () => import("../../../extensions/ollama/index.js"), + openai: () => import("../../../extensions/openai/index.js"), + opencode: () => import("../../../extensions/opencode/index.js"), + "opencode-go": () => import("../../../extensions/opencode-go/index.js"), + openrouter: () => import("../../../extensions/openrouter/index.js"), + qianfan: () => import("../../../extensions/qianfan/index.js"), + "qwen-portal-auth": () => import("../../../extensions/qwen-portal-auth/index.js"), + sglang: () => import("../../../extensions/sglang/index.js"), + synthetic: () => import("../../../extensions/synthetic/index.js"), + together: () => import("../../../extensions/together/index.js"), + venice: () => import("../../../extensions/venice/index.js"), + "vercel-ai-gateway": () => import("../../../extensions/vercel-ai-gateway/index.js"), + vllm: () => import("../../../extensions/vllm/index.js"), + volcengine: () => import("../../../extensions/volcengine/index.js"), + xai: () => import("../../../extensions/xai/index.js"), + xiaomi: () => import("../../../extensions/xiaomi/index.js"), + zai: () => import("../../../extensions/zai/index.js"), +}; + +async function loadBundledProviderContractPlugins(): Promise { + const bundledProviderPluginIds = loadPluginManifestRegistry({}) + .plugins.filter((plugin) => plugin.origin === "bundled" && plugin.providers.length > 0) + .map((plugin) => plugin.id) + .toSorted((left, right) => left.localeCompare(right)); + + const modules = await Promise.all( + bundledProviderPluginIds.map((pluginId) => { + const load = bundledProviderContractPluginLoaders[pluginId]; + if (!load) { + throw new Error(`missing bundled provider contract loader for ${pluginId}`); + } + return load(); + }), + ); + + return modules.map((mod, index) => { + const plugin = mod.default as RegistrablePlugin | undefined; + if (!plugin) { + throw new Error( + `bundled provider contract plugin missing default export for ${bundledProviderPluginIds[index]}`, + ); + } + return plugin; + }); +} + +const loadedBundledProviderRegistry: ProviderContractEntry[] = buildCapabilityContractRegistry({ + plugins: await loadBundledProviderContractPlugins(), + select: (captured) => captured.providers, +}); providerContractRegistry.splice( 0, diff --git a/src/plugins/contracts/wizard.contract.test.ts b/src/plugins/contracts/wizard.contract.test.ts index 1e0ca6e49be..b5a5d393f5c 100644 --- a/src/plugins/contracts/wizard.contract.test.ts +++ b/src/plugins/contracts/wizard.contract.test.ts @@ -1,8 +1,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import type { ProviderPlugin } from "../types.js"; -import { providerContractPluginIds, uniqueProviderContractProviders } from "./registry.js"; -const resolvePluginProvidersMock = vi.fn(); +const resolvePluginProvidersMock = vi.hoisted(() => vi.fn()); vi.mock("../providers.js", () => ({ resolvePluginProviders: (...args: unknown[]) => resolvePluginProvidersMock(...args), @@ -12,6 +11,8 @@ let buildProviderPluginMethodChoice: typeof import("../provider-wizard.js").buil let resolveProviderModelPickerEntries: typeof import("../provider-wizard.js").resolveProviderModelPickerEntries; let resolveProviderPluginChoice: typeof import("../provider-wizard.js").resolveProviderPluginChoice; let resolveProviderWizardOptions: typeof import("../provider-wizard.js").resolveProviderWizardOptions; +let contractProviders: ProviderPlugin[] = []; +let providerContractPluginIds: string[] = []; function resolveExpectedWizardChoiceValues(providers: ProviderPlugin[]) { const values: string[] = []; @@ -72,6 +73,23 @@ function resolveExpectedModelPickerValues(providers: ProviderPlugin[]) { describe("provider wizard contract", () => { beforeEach(async () => { vi.resetModules(); + const providersModule = + await vi.importActual("../providers.js"); + contractProviders = providersModule.resolvePluginProviders({ + bundledProviderAllowlistCompat: true, + bundledProviderVitestCompat: true, + env: { + ...process.env, + VITEST: process.env.VITEST || "1", + }, + cache: false, + activate: false, + }); + providerContractPluginIds = [ + ...new Set( + contractProviders.map((provider) => provider.pluginId).filter(Boolean) as string[], + ), + ].toSorted((left, right) => left.localeCompare(right)); ({ buildProviderPluginMethodChoice, resolveProviderModelPickerEntries, @@ -79,7 +97,7 @@ describe("provider wizard contract", () => { resolveProviderWizardOptions, } = await import("../provider-wizard.js")); resolvePluginProvidersMock.mockReset(); - resolvePluginProvidersMock.mockReturnValue(uniqueProviderContractProviders); + resolvePluginProvidersMock.mockReturnValue(contractProviders); }); it("exposes every registered provider setup choice through the shared wizard layer", () => { @@ -98,7 +116,7 @@ describe("provider wizard contract", () => { expect( options.map((option) => option.value).toSorted((left, right) => left.localeCompare(right)), - ).toEqual(resolveExpectedWizardChoiceValues(uniqueProviderContractProviders)); + ).toEqual(resolveExpectedWizardChoiceValues(contractProviders)); expect(options.map((option) => option.value)).toEqual([ ...new Set(options.map((option) => option.value)), ]); @@ -107,7 +125,7 @@ describe("provider wizard contract", () => { it("round-trips every shared wizard choice back to its provider and auth method", () => { for (const option of resolveProviderWizardOptions({ config: {}, env: process.env })) { const resolved = resolveProviderPluginChoice({ - providers: uniqueProviderContractProviders, + providers: contractProviders, choice: option.value, }); expect(resolved).not.toBeNull(); @@ -121,10 +139,10 @@ describe("provider wizard contract", () => { expect( entries.map((entry) => entry.value).toSorted((left, right) => left.localeCompare(right)), - ).toEqual(resolveExpectedModelPickerValues(uniqueProviderContractProviders)); + ).toEqual(resolveExpectedModelPickerValues(contractProviders)); for (const entry of entries) { const resolved = resolveProviderPluginChoice({ - providers: uniqueProviderContractProviders, + providers: contractProviders, choice: entry.value, }); expect(resolved).not.toBeNull();