From 0d59d6401f74f201e03b1a9aa23a3087d00842ba Mon Sep 17 00:00:00 2001 From: Br1an67 <932039080@qq.com> Date: Sun, 15 Mar 2026 17:14:33 +0800 Subject: [PATCH] fix: doctor gracefully skips channels with unresolved SecretRefs (closes #46154) noteSecurityWarnings called resolveDefaultChannelAccountContext which threw on unresolved SecretRefs (e.g. env-based Discord tokens), making openclaw doctor completely unusable. Now catches the error and skips the channel plugin, matching the documented read-only degradation behavior for doctor flows. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/.generated/config-baseline.json | 1616 ++++++++++++++++- extensions/device-pair/index.ts | 2 +- src/cli/devices-cli.ts | 2 +- src/commands/doctor-security.ts | 16 +- src/context-engine/registry.ts | 76 +- src/gateway/server-methods/devices.ts | 15 +- .../server.device-pair-approve-authz.test.ts | 132 ++ .../server/ws-connection/message-handler.ts | 2 +- src/infra/device-pairing.ts | 58 +- 9 files changed, 1887 insertions(+), 32 deletions(-) create mode 100644 src/gateway/server.device-pair-approve-authz.test.ts diff --git a/docs/.generated/config-baseline.json b/docs/.generated/config-baseline.json index cf872fcd62d..4974f3a410a 100644 --- a/docs/.generated/config-baseline.json +++ b/docs/.generated/config-baseline.json @@ -1112,6 +1112,18 @@ "help": "Minimum floor enforced for reserveTokens in Pi compaction paths (0 disables the floor guard). Use a non-zero floor to avoid over-aggressive compression under fluctuating token estimates.", "hasChildren": false }, + { + "path": "agents.defaults.compaction.timeoutSeconds", + "kind": "core", + "type": "integer", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": ["performance"], + "label": "Compaction Timeout (Seconds)", + "help": "Maximum time in seconds allowed for a single compaction operation before it is aborted (default: 900). Increase this for very large sessions that need more time to summarize, or decrease it to fail faster on unresponsive models.", + "hasChildren": false + }, { "path": "agents.defaults.contextPruning", "kind": "core", @@ -1554,7 +1566,7 @@ "deprecated": false, "sensitive": false, "tags": ["automation"], - "help": "Delivery target (\"last\", \"none\", or a channel id). Known channels: telegram, whatsapp, discord, irc, googlechat, slack, signal, imessage, line, zalouser, zalo, tlon, feishu, nextcloud-talk, msteams, bluebubbles, synology-chat, mattermost, twitch, matrix, nostr.", + "help": "Delivery target (\"last\", \"none\", or a channel id). Known channels: telegram, whatsapp, discord, irc, googlechat, slack, signal, imessage, line, bluebubbles, feishu, matrix, mattermost, msteams, nextcloud-talk, nostr, synology-chat, tlon, twitch, zalo, zalouser.", "hasChildren": false }, { @@ -3727,7 +3739,7 @@ "deprecated": false, "sensitive": false, "tags": ["automation"], - "help": "Delivery target (\"last\", \"none\", or a channel id). Known channels: telegram, whatsapp, discord, irc, googlechat, slack, signal, imessage, line, zalouser, zalo, tlon, feishu, nextcloud-talk, msteams, bluebubbles, synology-chat, mattermost, twitch, matrix, nostr.", + "help": "Delivery target (\"last\", \"none\", or a channel id). Known channels: telegram, whatsapp, discord, irc, googlechat, slack, signal, imessage, line, bluebubbles, feishu, matrix, mattermost, msteams, nextcloud-talk, nostr, synology-chat, tlon, twitch, zalo, zalouser.", "hasChildren": false }, { @@ -9590,6 +9602,26 @@ "tags": [], "hasChildren": false }, + { + "path": "channels.discord.accounts.*.healthMonitor", + "kind": "channel", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": true + }, + { + "path": "channels.discord.accounts.*.healthMonitor.enabled", + "kind": "channel", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, { "path": "channels.discord.accounts.*.heartbeat", "kind": "channel", @@ -12179,6 +12211,26 @@ "tags": [], "hasChildren": false }, + { + "path": "channels.discord.healthMonitor", + "kind": "channel", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": true + }, + { + "path": "channels.discord.healthMonitor.enabled", + "kind": "channel", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, { "path": "channels.discord.heartbeat", "kind": "channel", @@ -13391,6 +13443,46 @@ "tags": [], "hasChildren": true }, + { + "path": "channels.feishu.accounts.*.actions", + "kind": "channel", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": true + }, + { + "path": "channels.feishu.accounts.*.actions.reactions", + "kind": "channel", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.feishu.accounts.*.allowFrom", + "kind": "channel", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": true + }, + { + "path": "channels.feishu.accounts.*.allowFrom.*", + "kind": "channel", + "type": ["number", "string"], + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, { "path": "channels.feishu.accounts.*.appId", "kind": "channel", @@ -13436,7 +13528,87 @@ "kind": "channel", "type": "string", "required": true, - "enumValues": ["env", "file", "exec"], + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.feishu.accounts.*.blockStreamingCoalesce", + "kind": "channel", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": true + }, + { + "path": "channels.feishu.accounts.*.blockStreamingCoalesce.enabled", + "kind": "channel", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.feishu.accounts.*.blockStreamingCoalesce.maxDelayMs", + "kind": "channel", + "type": "integer", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.feishu.accounts.*.blockStreamingCoalesce.minDelayMs", + "kind": "channel", + "type": "integer", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.feishu.accounts.*.capabilities", + "kind": "channel", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": true + }, + { + "path": "channels.feishu.accounts.*.capabilities.*", + "kind": "channel", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.feishu.accounts.*.chunkMode", + "kind": "channel", + "type": "string", + "required": false, + "enumValues": ["length", "newline"], + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.feishu.accounts.*.configWrites", + "kind": "channel", + "type": "boolean", + "required": false, "deprecated": false, "sensitive": false, "tags": [], @@ -13453,6 +13625,67 @@ "tags": [], "hasChildren": false }, + { + "path": "channels.feishu.accounts.*.dmHistoryLimit", + "kind": "channel", + "type": "integer", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.feishu.accounts.*.dmPolicy", + "kind": "channel", + "type": "string", + "required": false, + "enumValues": ["open", "pairing", "allowlist"], + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.feishu.accounts.*.dms", + "kind": "channel", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": true + }, + { + "path": "channels.feishu.accounts.*.dms.*", + "kind": "channel", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": true + }, + { + "path": "channels.feishu.accounts.*.dms.*.enabled", + "kind": "channel", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.feishu.accounts.*.dms.*.systemPrompt", + "kind": "channel", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, { "path": "channels.feishu.accounts.*.domain", "kind": "channel", @@ -13509,7 +13742,334 @@ "kind": "channel", "type": "string", "required": true, - "enumValues": ["env", "file", "exec"], + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.feishu.accounts.*.groupAllowFrom", + "kind": "channel", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": true + }, + { + "path": "channels.feishu.accounts.*.groupAllowFrom.*", + "kind": "channel", + "type": ["number", "string"], + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.feishu.accounts.*.groupPolicy", + "kind": "channel", + "type": "string", + "required": false, + "enumValues": ["open", "allowlist", "disabled"], + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.feishu.accounts.*.groups", + "kind": "channel", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": true + }, + { + "path": "channels.feishu.accounts.*.groups.*", + "kind": "channel", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": true + }, + { + "path": "channels.feishu.accounts.*.groups.*.allowFrom", + "kind": "channel", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": true + }, + { + "path": "channels.feishu.accounts.*.groups.*.allowFrom.*", + "kind": "channel", + "type": ["number", "string"], + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.feishu.accounts.*.groups.*.enabled", + "kind": "channel", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.feishu.accounts.*.groups.*.groupSessionScope", + "kind": "channel", + "type": "string", + "required": false, + "enumValues": ["group", "group_sender", "group_topic", "group_topic_sender"], + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.feishu.accounts.*.groups.*.replyInThread", + "kind": "channel", + "type": "string", + "required": false, + "enumValues": ["disabled", "enabled"], + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.feishu.accounts.*.groups.*.requireMention", + "kind": "channel", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.feishu.accounts.*.groups.*.skills", + "kind": "channel", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": true + }, + { + "path": "channels.feishu.accounts.*.groups.*.skills.*", + "kind": "channel", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.feishu.accounts.*.groups.*.systemPrompt", + "kind": "channel", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.feishu.accounts.*.groups.*.tools", + "kind": "channel", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": true + }, + { + "path": "channels.feishu.accounts.*.groups.*.tools.allow", + "kind": "channel", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": true + }, + { + "path": "channels.feishu.accounts.*.groups.*.tools.allow.*", + "kind": "channel", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.feishu.accounts.*.groups.*.tools.deny", + "kind": "channel", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": true + }, + { + "path": "channels.feishu.accounts.*.groups.*.tools.deny.*", + "kind": "channel", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.feishu.accounts.*.groups.*.topicSessionMode", + "kind": "channel", + "type": "string", + "required": false, + "enumValues": ["disabled", "enabled"], + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.feishu.accounts.*.groupSenderAllowFrom", + "kind": "channel", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": true + }, + { + "path": "channels.feishu.accounts.*.groupSenderAllowFrom.*", + "kind": "channel", + "type": ["number", "string"], + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.feishu.accounts.*.groupSessionScope", + "kind": "channel", + "type": "string", + "required": false, + "enumValues": ["group", "group_sender", "group_topic", "group_topic_sender"], + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.feishu.accounts.*.heartbeat", + "kind": "channel", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": true + }, + { + "path": "channels.feishu.accounts.*.heartbeat.intervalMs", + "kind": "channel", + "type": "integer", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.feishu.accounts.*.heartbeat.visibility", + "kind": "channel", + "type": "string", + "required": false, + "enumValues": ["visible", "hidden"], + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.feishu.accounts.*.historyLimit", + "kind": "channel", + "type": "integer", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.feishu.accounts.*.httpTimeoutMs", + "kind": "channel", + "type": "integer", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.feishu.accounts.*.markdown", + "kind": "channel", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": true + }, + { + "path": "channels.feishu.accounts.*.markdown.mode", + "kind": "channel", + "type": "string", + "required": false, + "enumValues": ["native", "escape", "strip"], + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.feishu.accounts.*.markdown.tableMode", + "kind": "channel", + "type": "string", + "required": false, + "enumValues": ["native", "ascii", "simple"], + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.feishu.accounts.*.mediaMaxMb", + "kind": "channel", + "type": "number", + "required": false, "deprecated": false, "sensitive": false, "tags": [], @@ -13525,6 +14085,170 @@ "tags": [], "hasChildren": false }, + { + "path": "channels.feishu.accounts.*.reactionNotifications", + "kind": "channel", + "type": "string", + "required": false, + "enumValues": ["off", "own", "all"], + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.feishu.accounts.*.renderMode", + "kind": "channel", + "type": "string", + "required": false, + "enumValues": ["auto", "raw", "card"], + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.feishu.accounts.*.replyInThread", + "kind": "channel", + "type": "string", + "required": false, + "enumValues": ["disabled", "enabled"], + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.feishu.accounts.*.requireMention", + "kind": "channel", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.feishu.accounts.*.resolveSenderNames", + "kind": "channel", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.feishu.accounts.*.streaming", + "kind": "channel", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.feishu.accounts.*.textChunkLimit", + "kind": "channel", + "type": "integer", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.feishu.accounts.*.tools", + "kind": "channel", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": true + }, + { + "path": "channels.feishu.accounts.*.tools.chat", + "kind": "channel", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.feishu.accounts.*.tools.doc", + "kind": "channel", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.feishu.accounts.*.tools.drive", + "kind": "channel", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.feishu.accounts.*.tools.perm", + "kind": "channel", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.feishu.accounts.*.tools.scopes", + "kind": "channel", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.feishu.accounts.*.tools.wiki", + "kind": "channel", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.feishu.accounts.*.topicSessionMode", + "kind": "channel", + "type": "string", + "required": false, + "enumValues": ["disabled", "enabled"], + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.feishu.accounts.*.typingIndicator", + "kind": "channel", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, { "path": "channels.feishu.accounts.*.verificationToken", "kind": "channel", @@ -13560,7 +14284,6 @@ "kind": "channel", "type": "string", "required": true, - "enumValues": ["env", "file", "exec"], "deprecated": false, "sensitive": false, "tags": [], @@ -13596,6 +14319,26 @@ "tags": [], "hasChildren": false }, + { + "path": "channels.feishu.actions", + "kind": "channel", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": true + }, + { + "path": "channels.feishu.actions.reactions", + "kind": "channel", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, { "path": "channels.feishu.allowFrom", "kind": "channel", @@ -13661,7 +14404,66 @@ "kind": "channel", "type": "string", "required": true, - "enumValues": ["env", "file", "exec"], + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.feishu.blockStreamingCoalesce", + "kind": "channel", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": true + }, + { + "path": "channels.feishu.blockStreamingCoalesce.enabled", + "kind": "channel", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.feishu.blockStreamingCoalesce.maxDelayMs", + "kind": "channel", + "type": "integer", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.feishu.blockStreamingCoalesce.minDelayMs", + "kind": "channel", + "type": "integer", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.feishu.capabilities", + "kind": "channel", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": true + }, + { + "path": "channels.feishu.capabilities.*", + "kind": "channel", + "type": "string", + "required": false, "deprecated": false, "sensitive": false, "tags": [], @@ -13678,12 +14480,23 @@ "tags": [], "hasChildren": false }, + { + "path": "channels.feishu.configWrites", + "kind": "channel", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, { "path": "channels.feishu.connectionMode", "kind": "channel", "type": "string", - "required": false, + "required": true, "enumValues": ["websocket", "webhook"], + "defaultValue": "websocket", "deprecated": false, "sensitive": false, "tags": [], @@ -13713,8 +14526,49 @@ "path": "channels.feishu.dmPolicy", "kind": "channel", "type": "string", - "required": false, + "required": true, "enumValues": ["open", "pairing", "allowlist"], + "defaultValue": "pairing", + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.feishu.dms", + "kind": "channel", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": true + }, + { + "path": "channels.feishu.dms.*", + "kind": "channel", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": true + }, + { + "path": "channels.feishu.dms.*.enabled", + "kind": "channel", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.feishu.dms.*.systemPrompt", + "kind": "channel", + "type": "string", + "required": false, "deprecated": false, "sensitive": false, "tags": [], @@ -13724,13 +14578,63 @@ "path": "channels.feishu.domain", "kind": "channel", "type": "string", - "required": false, + "required": true, "enumValues": ["feishu", "lark"], "deprecated": false, "sensitive": false, "tags": [], "hasChildren": false }, + { + "path": "channels.feishu.dynamicAgentCreation", + "kind": "channel", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": true + }, + { + "path": "channels.feishu.dynamicAgentCreation.agentDirTemplate", + "kind": "channel", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.feishu.dynamicAgentCreation.enabled", + "kind": "channel", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.feishu.dynamicAgentCreation.maxAgents", + "kind": "channel", + "type": "integer", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.feishu.dynamicAgentCreation.workspaceTemplate", + "kind": "channel", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, { "path": "channels.feishu.enabled", "kind": "channel", @@ -13776,7 +14680,6 @@ "kind": "channel", "type": "string", "required": true, - "enumValues": ["env", "file", "exec"], "deprecated": false, "sensitive": false, "tags": [], @@ -13806,13 +14709,206 @@ "path": "channels.feishu.groupPolicy", "kind": "channel", "type": "string", - "required": false, + "required": true, "enumValues": ["open", "allowlist", "disabled"], "deprecated": false, "sensitive": false, "tags": [], "hasChildren": false }, + { + "path": "channels.feishu.groups", + "kind": "channel", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": true + }, + { + "path": "channels.feishu.groups.*", + "kind": "channel", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": true + }, + { + "path": "channels.feishu.groups.*.allowFrom", + "kind": "channel", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": true + }, + { + "path": "channels.feishu.groups.*.allowFrom.*", + "kind": "channel", + "type": ["number", "string"], + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.feishu.groups.*.enabled", + "kind": "channel", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.feishu.groups.*.groupSessionScope", + "kind": "channel", + "type": "string", + "required": false, + "enumValues": ["group", "group_sender", "group_topic", "group_topic_sender"], + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.feishu.groups.*.replyInThread", + "kind": "channel", + "type": "string", + "required": false, + "enumValues": ["disabled", "enabled"], + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.feishu.groups.*.requireMention", + "kind": "channel", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.feishu.groups.*.skills", + "kind": "channel", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": true + }, + { + "path": "channels.feishu.groups.*.skills.*", + "kind": "channel", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.feishu.groups.*.systemPrompt", + "kind": "channel", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.feishu.groups.*.tools", + "kind": "channel", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": true + }, + { + "path": "channels.feishu.groups.*.tools.allow", + "kind": "channel", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": true + }, + { + "path": "channels.feishu.groups.*.tools.allow.*", + "kind": "channel", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.feishu.groups.*.tools.deny", + "kind": "channel", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": true + }, + { + "path": "channels.feishu.groups.*.tools.deny.*", + "kind": "channel", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.feishu.groups.*.topicSessionMode", + "kind": "channel", + "type": "string", + "required": false, + "enumValues": ["disabled", "enabled"], + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.feishu.groupSenderAllowFrom", + "kind": "channel", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": true + }, + { + "path": "channels.feishu.groupSenderAllowFrom.*", + "kind": "channel", + "type": ["number", "string"], + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, { "path": "channels.feishu.groupSessionScope", "kind": "channel", @@ -13824,6 +14920,37 @@ "tags": [], "hasChildren": false }, + { + "path": "channels.feishu.heartbeat", + "kind": "channel", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": true + }, + { + "path": "channels.feishu.heartbeat.intervalMs", + "kind": "channel", + "type": "integer", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.feishu.heartbeat.visibility", + "kind": "channel", + "type": "string", + "required": false, + "enumValues": ["visible", "hidden"], + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, { "path": "channels.feishu.historyLimit", "kind": "channel", @@ -13834,6 +14961,48 @@ "tags": [], "hasChildren": false }, + { + "path": "channels.feishu.httpTimeoutMs", + "kind": "channel", + "type": "integer", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.feishu.markdown", + "kind": "channel", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": true + }, + { + "path": "channels.feishu.markdown.mode", + "kind": "channel", + "type": "string", + "required": false, + "enumValues": ["native", "escape", "strip"], + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.feishu.markdown.tableMode", + "kind": "channel", + "type": "string", + "required": false, + "enumValues": ["native", "ascii", "simple"], + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, { "path": "channels.feishu.mediaMaxMb", "kind": "channel", @@ -13844,6 +15013,18 @@ "tags": [], "hasChildren": false }, + { + "path": "channels.feishu.reactionNotifications", + "kind": "channel", + "type": "string", + "required": true, + "enumValues": ["off", "own", "all"], + "defaultValue": "own", + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, { "path": "channels.feishu.renderMode", "kind": "channel", @@ -13870,6 +15051,28 @@ "path": "channels.feishu.requireMention", "kind": "channel", "type": "boolean", + "required": true, + "defaultValue": true, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.feishu.resolveSenderNames", + "kind": "channel", + "type": "boolean", + "required": true, + "defaultValue": true, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.feishu.streaming", + "kind": "channel", + "type": "boolean", "required": false, "deprecated": false, "sensitive": false, @@ -13886,6 +15089,76 @@ "tags": [], "hasChildren": false }, + { + "path": "channels.feishu.tools", + "kind": "channel", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": true + }, + { + "path": "channels.feishu.tools.chat", + "kind": "channel", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.feishu.tools.doc", + "kind": "channel", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.feishu.tools.drive", + "kind": "channel", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.feishu.tools.perm", + "kind": "channel", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.feishu.tools.scopes", + "kind": "channel", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.feishu.tools.wiki", + "kind": "channel", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, { "path": "channels.feishu.topicSessionMode", "kind": "channel", @@ -13897,6 +15170,17 @@ "tags": [], "hasChildren": false }, + { + "path": "channels.feishu.typingIndicator", + "kind": "channel", + "type": "boolean", + "required": true, + "defaultValue": true, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, { "path": "channels.feishu.verificationToken", "kind": "channel", @@ -13932,7 +15216,6 @@ "kind": "channel", "type": "string", "required": true, - "enumValues": ["env", "file", "exec"], "deprecated": false, "sensitive": false, "tags": [], @@ -13952,7 +15235,8 @@ "path": "channels.feishu.webhookPath", "kind": "channel", "type": "string", - "required": false, + "required": true, + "defaultValue": "/feishu/events", "deprecated": false, "sensitive": false, "tags": [], @@ -14030,6 +15314,16 @@ "tags": [], "hasChildren": false }, + { + "path": "channels.googlechat.accounts.*.appPrincipal", + "kind": "channel", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, { "path": "channels.googlechat.accounts.*.audience", "kind": "channel", @@ -14386,6 +15680,26 @@ "tags": [], "hasChildren": false }, + { + "path": "channels.googlechat.accounts.*.healthMonitor", + "kind": "channel", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": true + }, + { + "path": "channels.googlechat.accounts.*.healthMonitor.enabled", + "kind": "channel", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, { "path": "channels.googlechat.accounts.*.historyLimit", "kind": "channel", @@ -14628,6 +15942,16 @@ "tags": [], "hasChildren": false }, + { + "path": "channels.googlechat.appPrincipal", + "kind": "channel", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, { "path": "channels.googlechat.audience", "kind": "channel", @@ -14994,6 +16318,26 @@ "tags": [], "hasChildren": false }, + { + "path": "channels.googlechat.healthMonitor", + "kind": "channel", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": true + }, + { + "path": "channels.googlechat.healthMonitor.enabled", + "kind": "channel", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, { "path": "channels.googlechat.historyLimit", "kind": "channel", @@ -15673,6 +17017,26 @@ "tags": [], "hasChildren": false }, + { + "path": "channels.imessage.accounts.*.healthMonitor", + "kind": "channel", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": true + }, + { + "path": "channels.imessage.accounts.*.healthMonitor.enabled", + "kind": "channel", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, { "path": "channels.imessage.accounts.*.heartbeat", "kind": "channel", @@ -16295,6 +17659,26 @@ "tags": [], "hasChildren": false }, + { + "path": "channels.imessage.healthMonitor", + "kind": "channel", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": true + }, + { + "path": "channels.imessage.healthMonitor.enabled", + "kind": "channel", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, { "path": "channels.imessage.heartbeat", "kind": "channel", @@ -20421,6 +21805,26 @@ "tags": [], "hasChildren": false }, + { + "path": "channels.msteams.healthMonitor", + "kind": "channel", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": true + }, + { + "path": "channels.msteams.healthMonitor.enabled", + "kind": "channel", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, { "path": "channels.msteams.heartbeat", "kind": "channel", @@ -22902,6 +24306,26 @@ "tags": [], "hasChildren": false }, + { + "path": "channels.signal.accounts.*.healthMonitor", + "kind": "channel", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": true + }, + { + "path": "channels.signal.accounts.*.healthMonitor.enabled", + "kind": "channel", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, { "path": "channels.signal.accounts.*.heartbeat", "kind": "channel", @@ -23594,6 +25018,26 @@ "tags": [], "hasChildren": false }, + { + "path": "channels.signal.healthMonitor", + "kind": "channel", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": true + }, + { + "path": "channels.signal.healthMonitor.enabled", + "kind": "channel", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, { "path": "channels.signal.heartbeat", "kind": "channel", @@ -24643,6 +26087,26 @@ "tags": [], "hasChildren": false }, + { + "path": "channels.slack.accounts.*.healthMonitor", + "kind": "channel", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": true + }, + { + "path": "channels.slack.accounts.*.healthMonitor.enabled", + "kind": "channel", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, { "path": "channels.slack.accounts.*.heartbeat", "kind": "channel", @@ -25903,6 +27367,26 @@ "tags": [], "hasChildren": false }, + { + "path": "channels.slack.healthMonitor", + "kind": "channel", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": true + }, + { + "path": "channels.slack.healthMonitor.enabled", + "kind": "channel", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, { "path": "channels.slack.heartbeat", "kind": "channel", @@ -27732,6 +29216,26 @@ "tags": [], "hasChildren": false }, + { + "path": "channels.telegram.accounts.*.healthMonitor", + "kind": "channel", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": true + }, + { + "path": "channels.telegram.accounts.*.healthMonitor.enabled", + "kind": "channel", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, { "path": "channels.telegram.accounts.*.heartbeat", "kind": "channel", @@ -29515,6 +31019,26 @@ "tags": [], "hasChildren": false }, + { + "path": "channels.telegram.healthMonitor", + "kind": "channel", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": true + }, + { + "path": "channels.telegram.healthMonitor.enabled", + "kind": "channel", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, { "path": "channels.telegram.heartbeat", "kind": "channel", @@ -31264,6 +32788,26 @@ "tags": [], "hasChildren": false }, + { + "path": "channels.whatsapp.accounts.*.healthMonitor", + "kind": "channel", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": true + }, + { + "path": "channels.whatsapp.accounts.*.healthMonitor.enabled", + "kind": "channel", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, { "path": "channels.whatsapp.accounts.*.heartbeat", "kind": "channel", @@ -31910,6 +33454,26 @@ "tags": [], "hasChildren": false }, + { + "path": "channels.whatsapp.healthMonitor", + "kind": "channel", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": true + }, + { + "path": "channels.whatsapp.healthMonitor.enabled", + "kind": "channel", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, { "path": "channels.whatsapp.heartbeat", "kind": "channel", @@ -34441,6 +36005,30 @@ "help": "Interval in minutes for automatic channel health probing and status updates. Use lower intervals for faster detection, or higher intervals to reduce periodic probe noise.", "hasChildren": false }, + { + "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 + }, + { + "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 + }, { "path": "gateway.controlUi", "kind": "core", @@ -37320,7 +38908,7 @@ "sensitive": false, "tags": ["advanced"], "label": "Group Mention Patterns", - "help": "Regex-like patterns used to detect explicit mentions/trigger phrases in group chats. Use precise patterns to reduce false positives in high-volume channels.", + "help": "Safe case-insensitive regex patterns used to detect explicit mentions/trigger phrases in group chats. Use precise patterns to reduce false positives in high-volume channels; invalid or unsafe nested-repetition patterns are ignored.", "hasChildren": true }, { diff --git a/extensions/device-pair/index.ts b/extensions/device-pair/index.ts index 7ba88842a7a..55229bb9e26 100644 --- a/extensions/device-pair/index.ts +++ b/extensions/device-pair/index.ts @@ -386,7 +386,7 @@ export default function register(api: OpenClawPluginApi) { return { text: "Pairing request not found." }; } const approved = await approveDevicePairing(pending.requestId); - if (!approved) { + if (!approved || approved.status === "forbidden") { return { text: "Pairing request not found." }; } const label = approved.device.displayName?.trim() || approved.device.deviceId; diff --git a/src/cli/devices-cli.ts b/src/cli/devices-cli.ts index 143d27b20ff..ac31d66d421 100644 --- a/src/cli/devices-cli.ts +++ b/src/cli/devices-cli.ts @@ -158,7 +158,7 @@ async function approvePairingWithFallback( defaultRuntime.log(theme.warn(FALLBACK_NOTICE)); } const approved = await approveDevicePairing(requestId); - if (!approved) { + if (!approved || approved.status === "forbidden") { return null; } return { diff --git a/src/commands/doctor-security.ts b/src/commands/doctor-security.ts index 5ba17c1c751..e3f44841415 100644 --- a/src/commands/doctor-security.ts +++ b/src/commands/doctor-security.ts @@ -189,8 +189,20 @@ export async function noteSecurityWarnings(cfg: OpenClawConfig) { if (!plugin.security) { continue; } - const { defaultAccountId, account, enabled, configured } = - await resolveDefaultChannelAccountContext(plugin, cfg); + let ctx: Awaited>; + try { + ctx = await resolveDefaultChannelAccountContext(plugin, cfg); + } catch { + // Unresolved SecretRefs (e.g. env-based tokens) are expected in + // read-only doctor flows — skip the plugin but surface a warning + // so the user knows security diagnostics were incomplete. + const label = plugin.meta.label ?? plugin.id; + warnings.push( + `- ${label}: skipped security checks (account could not be resolved — likely unresolved SecretRef).`, + ); + continue; + } + const { defaultAccountId, account, enabled, configured } = ctx; if (!enabled) { continue; } diff --git a/src/context-engine/registry.ts b/src/context-engine/registry.ts index d73266c62de..1701877790a 100644 --- a/src/context-engine/registry.ts +++ b/src/context-engine/registry.ts @@ -7,15 +7,28 @@ import type { ContextEngine } from "./types.js"; * Supports async creation for engines that need DB connections etc. */ export type ContextEngineFactory = () => ContextEngine | Promise; +export type ContextEngineRegistrationResult = { ok: true } | { ok: false; existingOwner: string }; + +type RegisterContextEngineForOwnerOptions = { + allowSameOwnerRefresh?: boolean; +}; // --------------------------------------------------------------------------- // Registry (module-level singleton) // --------------------------------------------------------------------------- const CONTEXT_ENGINE_REGISTRY_STATE = Symbol.for("openclaw.contextEngineRegistryState"); +const CORE_CONTEXT_ENGINE_OWNER = "core"; +const PUBLIC_CONTEXT_ENGINE_OWNER = "public-sdk"; type ContextEngineRegistryState = { - engines: Map; + engines: Map< + string, + { + factory: ContextEngineFactory; + owner: string; + } + >; }; // Keep context-engine registrations process-global so duplicated dist chunks @@ -26,24 +39,69 @@ function getContextEngineRegistryState(): ContextEngineRegistryState { }; if (!globalState[CONTEXT_ENGINE_REGISTRY_STATE]) { globalState[CONTEXT_ENGINE_REGISTRY_STATE] = { - engines: new Map(), + engines: new Map(), }; } return globalState[CONTEXT_ENGINE_REGISTRY_STATE]; } +function requireContextEngineOwner(owner: string): string { + const normalizedOwner = owner.trim(); + if (!normalizedOwner) { + throw new Error( + `registerContextEngineForOwner: owner must be a non-empty string, got ${JSON.stringify(owner)}`, + ); + } + return normalizedOwner; +} + /** - * Register a context engine implementation under the given id. + * Register a context engine implementation under an explicit trusted owner. */ -export function registerContextEngine(id: string, factory: ContextEngineFactory): void { - getContextEngineRegistryState().engines.set(id, factory); +export function registerContextEngineForOwner( + id: string, + factory: ContextEngineFactory, + owner: string, + opts?: RegisterContextEngineForOwnerOptions, +): ContextEngineRegistrationResult { + const normalizedOwner = requireContextEngineOwner(owner); + const registry = getContextEngineRegistryState().engines; + const existing = registry.get(id); + if ( + id === defaultSlotIdForKey("contextEngine") && + normalizedOwner !== CORE_CONTEXT_ENGINE_OWNER + ) { + return { ok: false, existingOwner: CORE_CONTEXT_ENGINE_OWNER }; + } + if (existing && existing.owner !== normalizedOwner) { + return { ok: false, existingOwner: existing.owner }; + } + if (existing && opts?.allowSameOwnerRefresh !== true) { + return { ok: false, existingOwner: existing.owner }; + } + registry.set(id, { factory, owner: normalizedOwner }); + return { ok: true }; +} + +/** + * Public SDK entry point for third-party registrations. + * + * This path is intentionally unprivileged: it cannot claim core-owned ids and + * it cannot safely refresh an existing registration because the caller's + * identity is not authenticated. + */ +export function registerContextEngine( + id: string, + factory: ContextEngineFactory, +): ContextEngineRegistrationResult { + return registerContextEngineForOwner(id, factory, PUBLIC_CONTEXT_ENGINE_OWNER); } /** * Return the factory for a registered engine, or undefined. */ export function getContextEngineFactory(id: string): ContextEngineFactory | undefined { - return getContextEngineRegistryState().engines.get(id); + return getContextEngineRegistryState().engines.get(id)?.factory; } /** @@ -73,13 +131,13 @@ export async function resolveContextEngine(config?: OpenClawConfig): Promise { + "device.pair.approve": async ({ params, respond, context, client }) => { if (!validateDevicePairApproveParams(params)) { respond( false, @@ -93,11 +93,22 @@ export const deviceHandlers: GatewayRequestHandlers = { return; } const { requestId } = params as { requestId: string }; - const approved = await approveDevicePairing(requestId); + const callerScopes = Array.isArray(client?.connect?.scopes) ? client.connect.scopes : []; + + // Scope validation and approval happen atomically inside the same lock. + const approved = await approveDevicePairing(requestId, { callerScopes }); if (!approved) { respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "unknown requestId")); return; } + if (approved.status === "forbidden") { + respond( + false, + undefined, + errorShape(ErrorCodes.INVALID_REQUEST, `missing scope: ${approved.missingScope}`), + ); + return; + } context.logGateway.info( `device pairing approved device=${approved.device.deviceId} role=${approved.device.role ?? "unknown"}`, ); diff --git a/src/gateway/server.device-pair-approve-authz.test.ts b/src/gateway/server.device-pair-approve-authz.test.ts new file mode 100644 index 00000000000..27ab8474215 --- /dev/null +++ b/src/gateway/server.device-pair-approve-authz.test.ts @@ -0,0 +1,132 @@ +import os from "node:os"; +import path from "node:path"; +import { describe, expect, test } from "vitest"; +import { WebSocket } from "ws"; +import { + loadOrCreateDeviceIdentity, + publicKeyRawBase64UrlFromPem, + type DeviceIdentity, +} from "../infra/device-identity.js"; +import { + approveDevicePairing, + getPairedDevice, + requestDevicePairing, + rotateDeviceToken, +} from "../infra/device-pairing.js"; +import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js"; +import { + connectOk, + installGatewayTestHooks, + rpcReq, + startServerWithClient, + trackConnectChallengeNonce, +} from "./test-helpers.js"; + +installGatewayTestHooks({ scope: "suite" }); + +function resolveDeviceIdentityPath(name: string): string { + const root = process.env.OPENCLAW_STATE_DIR ?? process.env.HOME ?? os.tmpdir(); + return path.join(root, "test-device-identities", `${name}.json`); +} + +function loadDeviceIdentity(name: string): { + identityPath: string; + identity: DeviceIdentity; + publicKey: string; +} { + const identityPath = resolveDeviceIdentityPath(name); + const identity = loadOrCreateDeviceIdentity(identityPath); + return { + identityPath, + identity, + publicKey: publicKeyRawBase64UrlFromPem(identity.publicKeyPem), + }; +} + +async function issuePairingScopedOperator(name: string): Promise<{ + identityPath: string; + deviceId: string; + token: string; +}> { + const loaded = loadDeviceIdentity(name); + const request = await requestDevicePairing({ + deviceId: loaded.identity.deviceId, + publicKey: loaded.publicKey, + role: "operator", + scopes: ["operator.admin"], + clientId: GATEWAY_CLIENT_NAMES.TEST, + clientMode: GATEWAY_CLIENT_MODES.TEST, + }); + await approveDevicePairing(request.request.requestId); + const rotated = await rotateDeviceToken({ + deviceId: loaded.identity.deviceId, + role: "operator", + scopes: ["operator.pairing"], + }); + expect(rotated).not.toBeNull(); + const token = rotated?.token ?? ""; + return { + identityPath: loaded.identityPath, + deviceId: loaded.identity.deviceId, + token: String(token), + }; +} + +async function openTrackedWs(port: number): Promise { + const ws = new WebSocket(`ws://127.0.0.1:${port}`); + trackConnectChallengeNonce(ws); + await new Promise((resolve, reject) => { + const timer = setTimeout(() => reject(new Error("timeout waiting for ws open")), 5_000); + ws.once("open", () => { + clearTimeout(timer); + resolve(); + }); + ws.once("error", (error) => { + clearTimeout(timer); + reject(error); + }); + }); + return ws; +} + +describe("gateway device.pair.approve caller scope guard", () => { + test("rejects approving device scopes above the caller session scopes", async () => { + const started = await startServerWithClient("secret"); + const approver = await issuePairingScopedOperator("approve-attacker"); + const pending = loadDeviceIdentity("approve-target"); + + let pairingWs: WebSocket | undefined; + try { + const request = await requestDevicePairing({ + deviceId: pending.identity.deviceId, + publicKey: pending.publicKey, + role: "operator", + scopes: ["operator.admin"], + clientId: GATEWAY_CLIENT_NAMES.TEST, + clientMode: GATEWAY_CLIENT_MODES.TEST, + }); + + pairingWs = await openTrackedWs(started.port); + await connectOk(pairingWs, { + skipDefaultAuth: true, + deviceToken: approver.token, + deviceIdentityPath: approver.identityPath, + scopes: ["operator.pairing"], + }); + + const approve = await rpcReq(pairingWs, "device.pair.approve", { + requestId: request.request.requestId, + }); + expect(approve.ok).toBe(false); + expect(approve.error?.message).toBe("missing scope: operator.admin"); + + const paired = await getPairedDevice(pending.identity.deviceId); + expect(paired).toBeNull(); + } finally { + pairingWs?.close(); + started.ws.close(); + await started.server.close(); + started.envSnapshot.restore(); + } + }); +}); diff --git a/src/gateway/server/ws-connection/message-handler.ts b/src/gateway/server/ws-connection/message-handler.ts index e0116190009..cf05c6e7396 100644 --- a/src/gateway/server/ws-connection/message-handler.ts +++ b/src/gateway/server/ws-connection/message-handler.ts @@ -745,7 +745,7 @@ export function attachGatewayWsMessageHandler(params: { const context = buildRequestContext(); if (pairing.request.silent === true) { const approved = await approveDevicePairing(pairing.request.requestId); - if (approved) { + if (approved && approved.status === "approved") { logGateway.info( `device pairing auto-approved device=${approved.device.deviceId} role=${approved.device.role ?? "unknown"}`, ); diff --git a/src/infra/device-pairing.ts b/src/infra/device-pairing.ts index 5bd2909a56e..b35fb7427af 100644 --- a/src/infra/device-pairing.ts +++ b/src/infra/device-pairing.ts @@ -78,6 +78,16 @@ type DevicePairingStateFile = { const PENDING_TTL_MS = 5 * 60 * 1000; +export type ApproveDevicePairingResult = + | { status: "approved"; requestId: string; device: PairedDevice } + | { status: "forbidden"; missingScope: string } + | null; + +type ApprovedDevicePairingResult = Extract< + NonNullable, + { status: "approved" } +>; + const withLock = createAsyncLock(); async function loadState(baseDir?: string): Promise { @@ -302,16 +312,60 @@ export async function requestDevicePairing( }); } +function resolveMissingRequestedScope(params: { + role: string; + requestedScopes: readonly string[]; + callerScopes: readonly string[]; +}): string | null { + for (const scope of params.requestedScopes) { + if ( + !roleScopesAllow({ + role: params.role, + requestedScopes: [scope], + allowedScopes: params.callerScopes, + }) + ) { + return scope; + } + } + return null; +} + export async function approveDevicePairing( requestId: string, baseDir?: string, -): Promise<{ requestId: string; device: PairedDevice } | null> { +): Promise; +export async function approveDevicePairing( + requestId: string, + options: { callerScopes?: readonly string[] }, + baseDir?: string, +): Promise; +export async function approveDevicePairing( + requestId: string, + optionsOrBaseDir?: { callerScopes?: readonly string[] } | string, + maybeBaseDir?: string, +): Promise { + const options = + typeof optionsOrBaseDir === "string" || optionsOrBaseDir === undefined + ? undefined + : optionsOrBaseDir; + const baseDir = typeof optionsOrBaseDir === "string" ? optionsOrBaseDir : maybeBaseDir; return await withLock(async () => { const state = await loadState(baseDir); const pending = state.pendingById[requestId]; if (!pending) { return null; } + if (pending.role && options?.callerScopes) { + const missingScope = resolveMissingRequestedScope({ + role: pending.role, + requestedScopes: normalizeDeviceAuthScopes(pending.scopes), + callerScopes: options.callerScopes, + }); + if (missingScope) { + return { status: "forbidden", missingScope }; + } + } const now = Date.now(); const existing = state.pairedByDeviceId[pending.deviceId]; const roles = mergeRoles(existing?.roles, existing?.role, pending.roles, pending.role); @@ -364,7 +418,7 @@ export async function approveDevicePairing( delete state.pendingById[requestId]; state.pairedByDeviceId[device.deviceId] = device; await persistState(state, baseDir); - return { requestId, device }; + return { status: "approved" as const, requestId, device }; }); }