From f7d7148cf0477a62e05de733bb4484ae674a4697 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 5 Jul 2026 00:32:47 -0400 Subject: [PATCH] docs: rewrite published docs grounded in current source (#100142) Source-grounded rewrite of 529 published docs pages with per-unit information-loss verification: 1,713 factual corrections cited to src/**, generated surfaces regenerated, frontmatter titles preserved for i18n, release notes pages untouched. All docs gates green. Closes #100141 --- docs/.generated/config-baseline.sha256 | 8 +- .../.generated/plugin-sdk-api-baseline.sha256 | 4 +- docs/.i18n/glossary.zh-CN.json | 152 ++ docs/AGENTS.md | 1 + docs/agent-runtime-architecture.md | 35 +- docs/announcements/bluebubbles-imessage.md | 18 +- docs/auth-credential-semantics.md | 107 +- docs/automation/auth-monitoring.md | 4 +- docs/automation/clawflow.md | 4 +- docs/automation/cron-jobs.md | 402 ++-- docs/automation/cron-vs-heartbeat.md | 3 +- docs/automation/gmail-pubsub.md | 6 +- docs/automation/hooks.md | 54 +- docs/automation/index.md | 8 +- docs/automation/poll.md | 4 +- docs/automation/standing-orders.md | 20 +- docs/automation/taskflow.md | 141 +- docs/automation/tasks.md | 152 +- docs/automation/troubleshooting.md | 2 +- docs/automation/webhook.md | 2 +- docs/channels/access-groups.md | 68 +- docs/channels/ambient-room-events.md | 27 +- docs/channels/bot-loop-protection.md | 93 +- docs/channels/broadcast-groups.md | 248 +-- docs/channels/channel-routing.md | 22 +- docs/channels/clickclack.md | 110 +- docs/channels/discord.md | 277 ++- docs/channels/feishu.md | 217 +-- docs/channels/googlechat.md | 171 +- docs/channels/group-messages.md | 42 +- docs/channels/groups.md | 132 +- docs/channels/imessage-from-bluebubbles.md | 191 +- docs/channels/imessage.md | 114 +- docs/channels/index.md | 71 +- docs/channels/irc.md | 50 +- docs/channels/line.md | 49 +- docs/channels/location.md | 22 +- docs/channels/matrix-migration.md | 158 +- docs/channels/matrix-presentation.md | 29 +- docs/channels/matrix-push-rules.md | 10 +- docs/channels/matrix.md | 289 ++- docs/channels/mattermost.md | 155 +- docs/channels/msteams.md | 381 ++-- docs/channels/nextcloud-talk.md | 62 +- docs/channels/nostr.md | 54 +- docs/channels/pairing.md | 22 +- docs/channels/qa-channel.md | 20 +- docs/channels/qqbot.md | 220 ++- docs/channels/raft.md | 56 +- docs/channels/signal.md | 228 +-- docs/channels/slack.md | 54 +- docs/channels/sms.md | 114 +- docs/channels/synology-chat.md | 61 +- docs/channels/telegram.md | 577 ++---- docs/channels/tlon.md | 240 +-- docs/channels/troubleshooting.md | 17 +- docs/channels/twitch.md | 214 +-- docs/channels/wechat.md | 22 +- docs/channels/whatsapp.md | 663 +++---- docs/channels/yuanbao.md | 168 +- docs/channels/zalo.md | 264 +-- docs/channels/zaloclawbot.md | 64 +- docs/channels/zalouser.md | 104 +- docs/ci.md | 229 +-- docs/clawhub/cli.md | 71 +- docs/clawhub/publishing.md | 67 +- docs/cli/acp.md | 156 +- docs/cli/agent.md | 62 +- docs/cli/agents.md | 163 +- docs/cli/approvals.md | 103 +- docs/cli/attach.md | 12 +- docs/cli/backup.md | 63 +- docs/cli/browser.md | 157 +- docs/cli/channels.md | 51 +- docs/cli/clawbot.md | 8 +- docs/cli/commitments.md | 21 +- docs/cli/completion.md | 34 +- docs/cli/config.md | 252 ++- docs/cli/configure.md | 82 +- docs/cli/crestodian.md | 214 +-- docs/cli/cron.md | 43 +- docs/cli/daemon.md | 59 +- docs/cli/dashboard.md | 18 +- docs/cli/devices.md | 317 ++-- docs/cli/directory.md | 34 +- docs/cli/dns.md | 38 +- docs/cli/docs.md | 20 +- docs/cli/doctor.md | 184 +- docs/cli/flows.md | 6 +- docs/cli/gateway.md | 239 +-- docs/cli/health.md | 26 +- docs/cli/hooks.md | 319 +--- docs/cli/index.md | 121 +- docs/cli/infer.md | 176 +- docs/cli/logs.md | 30 +- docs/cli/mcp.md | 62 +- docs/cli/memory.md | 255 +-- docs/cli/message.md | 408 ++--- docs/cli/migrate.md | 181 +- docs/cli/models.md | 249 +-- docs/cli/node.md | 19 +- docs/cli/nodes.md | 86 +- docs/cli/onboard.md | 184 +- docs/cli/pairing.md | 43 +- docs/cli/path.md | 263 +-- docs/cli/plugins.md | 277 ++- docs/cli/policy.md | 354 ++-- docs/cli/proxy.md | 89 +- docs/cli/qr.md | 76 +- docs/cli/reset.md | 34 +- docs/cli/sandbox.md | 203 +- docs/cli/secrets.md | 119 +- docs/cli/security.md | 114 +- docs/cli/sessions.md | 185 +- docs/cli/setup.md | 25 +- docs/cli/skills.md | 122 +- docs/cli/status.md | 99 +- docs/cli/system.md | 59 +- docs/cli/tasks.md | 49 +- docs/cli/transcripts.md | 111 +- docs/cli/tui.md | 48 +- docs/cli/uninstall.md | 38 +- docs/cli/update.md | 293 ++- docs/cli/voicecall.md | 30 +- docs/cli/webhooks.md | 74 +- docs/cli/wiki.md | 176 +- docs/cli/workboard.md | 99 +- docs/concepts/active-memory.md | 975 +++++----- docs/concepts/agent-loop.md | 198 +- docs/concepts/agent-runtimes.md | 233 ++- docs/concepts/agent-workspace.md | 28 +- docs/concepts/agent.md | 56 +- docs/concepts/architecture.md | 18 +- docs/concepts/channel-docking.md | 24 +- docs/concepts/commitments.md | 14 +- docs/concepts/compaction.md | 16 +- docs/concepts/context-engine.md | 24 +- docs/concepts/context.md | 6 +- docs/concepts/delegate-architecture.md | 94 +- docs/concepts/dreaming.md | 93 +- docs/concepts/experimental-features.md | 50 +- docs/concepts/features.md | 20 +- docs/concepts/mantis-slack-desktop-runbook.md | 141 +- docs/concepts/mantis.md | 848 +++------ docs/concepts/markdown-formatting.md | 130 +- docs/concepts/memory-builtin.md | 46 +- docs/concepts/memory-honcho.md | 37 +- docs/concepts/memory-qmd.md | 150 +- docs/concepts/memory-search.md | 154 +- docs/concepts/memory.md | 230 ++- docs/concepts/message-lifecycle-refactor.md | 1313 +++---------- docs/concepts/messages.md | 185 +- docs/concepts/model-failover.md | 98 +- docs/concepts/model-providers.md | 31 +- docs/concepts/models.md | 332 +--- docs/concepts/multi-agent.md | 169 +- docs/concepts/oauth.md | 154 +- docs/concepts/parallel-specialist-lanes.md | 19 +- .../concepts/personal-agent-benchmark-pack.md | 74 +- docs/concepts/presence.md | 29 +- docs/concepts/progress-drafts.md | 286 +-- docs/concepts/qa-e2e-automation.md | 877 +++++---- docs/concepts/qa-matrix.md | 64 +- docs/concepts/queue-steering.md | 54 +- docs/concepts/queue.md | 93 +- docs/concepts/retry.md | 29 +- docs/concepts/session-pruning.md | 73 +- docs/concepts/session-tool.md | 149 +- docs/concepts/session.md | 129 +- docs/concepts/soul.md | 77 +- docs/concepts/streaming.md | 322 ++-- docs/concepts/system-prompt.md | 306 +--- docs/concepts/timezone.md | 34 +- docs/concepts/typebox.md | 115 +- docs/concepts/typing-indicators.md | 42 +- docs/concepts/usage-tracking.md | 147 +- docs/date-time.md | 47 +- docs/debug/node-issue.md | 86 +- docs/diagnostics/flags.md | 117 +- docs/docs_map.md | 1410 +++++++------- docs/gateway/authentication.md | 231 +-- docs/gateway/background-process.md | 123 +- docs/gateway/bonjour.md | 246 +-- docs/gateway/bridge-protocol.md | 86 +- docs/gateway/cli-backends.md | 425 ++--- docs/gateway/config-agents.md | 206 ++- docs/gateway/config-channels.md | 76 +- docs/gateway/config-tools.md | 74 +- docs/gateway/configuration-examples.md | 20 +- docs/gateway/configuration-reference.md | 56 +- docs/gateway/configuration.md | 93 +- docs/gateway/diagnostics.md | 171 +- docs/gateway/discovery.md | 191 +- docs/gateway/doctor.md | 301 ++- docs/gateway/external-apps.md | 22 +- docs/gateway/gateway-lock.md | 58 +- docs/gateway/health.md | 46 +- docs/gateway/heartbeat.md | 54 +- docs/gateway/index.md | 95 +- docs/gateway/local-model-services.md | 73 +- docs/gateway/local-models.md | 160 +- docs/gateway/logging.md | 111 +- docs/gateway/multiple-gateways.md | 113 +- docs/gateway/openai-http-api.md | 372 ++-- docs/gateway/openresponses-http-api.md | 197 +- docs/gateway/openshell.md | 178 +- docs/gateway/opentelemetry.md | 230 +-- docs/gateway/operator-scopes.md | 114 +- docs/gateway/pairing.md | 125 +- docs/gateway/prometheus.md | 17 +- docs/gateway/protocol.md | 701 +++---- docs/gateway/remote-gateway-readme.md | 145 +- docs/gateway/remote.md | 162 +- .../sandbox-vs-tool-policy-vs-elevated.md | 46 +- docs/gateway/sandboxing.md | 317 +--- docs/gateway/secrets-plan-contract.md | 50 +- docs/gateway/secrets.md | 347 ++-- docs/gateway/security/audit-checks.md | 239 +-- docs/gateway/security/exposure-runbook.md | 97 +- docs/gateway/security/index.md | 1625 ++++++----------- .../security/secure-file-operations.md | 50 +- docs/gateway/security/shrinkwrap.md | 84 +- docs/gateway/tailscale.md | 123 +- docs/gateway/tools-invoke-http-api.md | 114 +- docs/gateway/troubleshooting.md | 156 +- docs/gateway/trusted-proxy-auth.md | 169 +- docs/help/debugging.md | 188 +- docs/help/environment.md | 15 +- docs/help/faq-first-run.md | 680 ++++--- docs/help/faq-models.md | 507 +++-- docs/help/faq.md | 1402 +++++--------- docs/help/index.md | 4 +- docs/help/scripts.md | 13 +- docs/help/testing-live.md | 186 +- docs/help/testing-updates-plugins.md | 48 +- docs/help/testing.md | 679 +++---- docs/help/troubleshooting.md | 330 ++-- docs/index.md | 12 +- docs/install/ansible.md | 96 +- docs/install/azure.md | 65 +- docs/install/bun.md | 12 +- docs/install/clawdock.md | 31 +- docs/install/development-channels.md | 126 +- docs/install/digitalocean.md | 22 +- docs/install/docker-vm-runtime.md | 68 +- docs/install/docker.md | 380 +--- docs/install/exe-dev.md | 165 +- docs/install/fly.md | 207 +-- docs/install/gcp.md | 201 +- docs/install/hetzner.md | 112 +- docs/install/hostinger.md | 14 +- docs/install/index.md | 42 +- docs/install/installer.md | 112 +- docs/install/kubernetes.md | 45 +- docs/install/macos-vm.md | 94 +- docs/install/migrating-claude.md | 4 +- docs/install/migrating-hermes.md | 29 +- docs/install/migrating.md | 6 +- docs/install/nix.md | 25 +- docs/install/node.md | 6 +- docs/install/northflank.mdx | 15 +- docs/install/oracle.md | 8 +- docs/install/podman.md | 193 +- docs/install/railway.mdx | 79 +- docs/install/raspberry-pi.md | 22 +- docs/install/render.mdx | 70 +- docs/install/uninstall.md | 15 +- docs/install/updating.md | 145 +- docs/install/upstash.md | 4 +- docs/logging.md | 77 +- docs/network.md | 10 +- docs/nodes/audio.md | 105 +- docs/nodes/camera.md | 112 +- docs/nodes/images.md | 67 +- docs/nodes/index.md | 203 +- docs/nodes/location-command.md | 39 +- docs/nodes/media-understanding.md | 282 ++- docs/nodes/talk.md | 118 +- docs/nodes/troubleshooting.md | 49 +- docs/nodes/voicewake.md | 79 +- docs/openclaw-agent-runtime.md | 69 +- docs/platforms/android.md | 145 +- docs/platforms/easyrunner.md | 42 +- docs/platforms/index.md | 2 +- docs/platforms/ios.md | 128 +- docs/platforms/linux.md | 121 +- docs/platforms/mac/bundled-gateway.md | 68 +- docs/platforms/mac/canvas.md | 97 +- docs/platforms/mac/child-process.md | 38 +- docs/platforms/mac/dev-setup.md | 60 +- docs/platforms/mac/health.md | 39 +- docs/platforms/mac/icon.md | 38 +- docs/platforms/mac/logging.md | 25 +- docs/platforms/mac/menu-bar.md | 85 +- docs/platforms/mac/peekaboo.md | 64 +- docs/platforms/mac/permissions.md | 34 +- docs/platforms/mac/remote.md | 91 +- docs/platforms/mac/signing.md | 40 +- docs/platforms/mac/skills.md | 22 +- docs/platforms/mac/voice-overlay.md | 57 +- docs/platforms/mac/voicewake.md | 62 +- docs/platforms/mac/webchat.md | 30 +- docs/platforms/mac/xpc.md | 27 +- docs/platforms/macos.md | 39 +- docs/platforms/windows.md | 170 +- docs/plugins/adding-capabilities.md | 50 +- docs/plugins/admin-http-rpc.md | 52 +- docs/plugins/architecture-internals.md | 297 +-- docs/plugins/architecture.md | 78 +- docs/plugins/building-extensions.md | 2 +- docs/plugins/building-plugins.md | 73 +- docs/plugins/bundles.md | 145 +- docs/plugins/cli-backend-plugins.md | 128 +- docs/plugins/codex-computer-use.md | 175 +- docs/plugins/codex-harness-reference.md | 335 ++-- docs/plugins/codex-harness-runtime.md | 288 ++- docs/plugins/codex-harness.md | 760 ++++---- docs/plugins/codex-native-plugins.md | 294 ++- docs/plugins/community.md | 41 +- docs/plugins/compatibility.md | 221 ++- docs/plugins/copilot.md | 428 ++--- docs/plugins/dependency-resolution.md | 198 +- docs/plugins/google-meet.md | 1190 ++++-------- docs/plugins/hooks.md | 394 ++-- docs/plugins/install-overrides.md | 44 +- docs/plugins/llama-cpp.md | 33 +- docs/plugins/manage-plugins.md | 144 +- docs/plugins/manifest.md | 734 +++----- docs/plugins/memory-lancedb.md | 297 ++- docs/plugins/memory-wiki.md | 484 ++--- docs/plugins/message-presentation.md | 49 +- docs/plugins/oc-path.md | 96 +- docs/plugins/plugin-permission-requests.md | 20 +- docs/plugins/reference/policy.md | 7 +- docs/plugins/sdk-agent-harness.md | 133 +- docs/plugins/sdk-channel-inbound.md | 44 +- docs/plugins/sdk-channel-ingress.md | 54 +- docs/plugins/sdk-channel-message.md | 16 +- docs/plugins/sdk-channel-outbound.md | 53 +- docs/plugins/sdk-channel-plugins.md | 552 +++--- docs/plugins/sdk-entrypoints.md | 269 +-- docs/plugins/sdk-migration.md | 658 ++++--- docs/plugins/sdk-overview.md | 74 +- docs/plugins/sdk-provider-plugins.md | 196 +- docs/plugins/sdk-runtime.md | 123 +- docs/plugins/sdk-setup.md | 64 +- docs/plugins/sdk-subpaths.md | 149 +- docs/plugins/sdk-testing.md | 123 +- docs/plugins/tool-plugins.md | 172 +- docs/plugins/voice-call.md | 334 ++-- docs/plugins/webhooks.md | 148 +- docs/plugins/workboard.md | 611 +++---- docs/plugins/zalouser.md | 38 +- docs/prose.md | 41 +- docs/providers/alibaba.md | 26 +- docs/providers/anthropic.md | 77 +- docs/providers/arcee.md | 26 +- docs/providers/azure-speech.md | 53 +- docs/providers/bedrock-mantle.md | 92 +- docs/providers/bedrock.md | 77 +- docs/providers/cerebras.md | 34 +- docs/providers/chutes.md | 70 +- docs/providers/claude-max-api-proxy.md | 110 +- docs/providers/clawrouter.md | 84 +- docs/providers/cloudflare-ai-gateway.md | 3 +- docs/providers/cohere.md | 11 +- docs/providers/comfy.md | 60 +- docs/providers/deepgram.md | 36 +- docs/providers/deepinfra.md | 47 +- docs/providers/deepseek.md | 48 +- docs/providers/ds4.md | 50 +- docs/providers/elevenlabs.md | 17 +- docs/providers/fal.md | 103 +- docs/providers/fireworks.md | 6 +- docs/providers/github-copilot.md | 63 +- docs/providers/gmi.md | 62 +- docs/providers/google.md | 27 +- docs/providers/gradium.md | 44 +- docs/providers/groq.md | 6 +- docs/providers/huggingface.md | 111 +- docs/providers/index.md | 10 +- docs/providers/inferrs.md | 86 +- docs/providers/inworld.md | 56 +- docs/providers/kilocode.md | 73 +- docs/providers/litellm.md | 90 +- docs/providers/lmstudio.md | 194 +- docs/providers/minimax.md | 193 +- docs/providers/mistral.md | 63 +- docs/providers/models.md | 15 +- docs/providers/moonshot.md | 78 +- docs/providers/novita.md | 77 +- docs/providers/nvidia.md | 86 +- docs/providers/ollama-cloud.md | 60 +- docs/providers/ollama.md | 686 +++---- docs/providers/openai.md | 719 ++++---- docs/providers/opencode-go.md | 84 +- docs/providers/opencode.md | 35 +- docs/providers/openrouter.md | 189 +- docs/providers/perplexity-provider.md | 107 +- docs/providers/pixverse.md | 57 +- docs/providers/qianfan.md | 45 +- docs/providers/qwen-oauth.md | 73 +- docs/providers/qwen.md | 198 +- docs/providers/runway.md | 4 +- docs/providers/senseaudio.md | 2 +- docs/providers/sglang.md | 4 +- docs/providers/stepfun.md | 52 +- docs/providers/synthetic.md | 18 +- docs/providers/tencent.md | 35 +- docs/providers/together.md | 37 +- docs/providers/venice.md | 238 +-- docs/providers/vercel-ai-gateway.md | 48 +- docs/providers/vllm.md | 160 +- docs/providers/volcengine.md | 55 +- docs/providers/vydra.md | 25 +- docs/providers/xai.md | 275 ++- docs/providers/xiaomi.md | 90 +- docs/providers/zai.md | 57 +- docs/reference/AGENTS.default.md | 66 +- docs/reference/RELEASING.md | 969 +++------- docs/reference/api-usage-costs.md | 204 +-- .../application-modernization-plan.md | 69 +- docs/reference/code-mode.md | 782 ++++---- docs/reference/credits.md | 8 +- docs/reference/device-models.md | 27 +- docs/reference/full-release-validation.md | 110 +- docs/reference/memory-config.md | 194 +- docs/reference/prompt-caching.md | 291 ++- docs/reference/release-performance-sweep.md | 89 +- docs/reference/rich-output-protocol.md | 55 +- docs/reference/rpc.md | 2 +- .../secret-placeholder-conventions.md | 2 +- .../reference/secretref-credential-surface.md | 25 +- .../session-management-compaction.md | 494 ++--- docs/reference/templates/AGENTS.dev.md | 13 +- docs/reference/templates/AGENTS.md | 185 +- docs/reference/templates/BOOT.md | 13 +- docs/reference/templates/BOOTSTRAP.md | 22 +- docs/reference/templates/HEARTBEAT.md | 11 +- docs/reference/templates/IDENTITY.dev.md | 3 +- docs/reference/templates/IDENTITY.md | 4 +- docs/reference/templates/SOUL.dev.md | 24 +- docs/reference/templates/SOUL.md | 14 +- docs/reference/templates/TOOLS.dev.md | 3 +- docs/reference/templates/TOOLS.md | 13 +- docs/reference/templates/USER.dev.md | 3 +- docs/reference/test.md | 319 ++-- docs/reference/token-use.md | 218 ++- docs/reference/transcript-hygiene.md | 235 ++- docs/reference/wizard.md | 93 +- docs/security/CONTRIBUTING-THREAT-MODEL.md | 74 +- docs/security/THREAT-MODEL-ATLAS.md | 616 +++---- docs/security/formal-verification.md | 167 +- docs/security/incident-response.md | 46 +- docs/security/network-proxy.md | 307 ++-- .../plugin-publish/minimal-package.json | 6 - docs/start/bootstrapping.md | 54 +- docs/start/docs-directory.md | 22 +- docs/start/getting-started.md | 10 +- docs/start/hubs.md | 14 +- docs/start/lore.md | 92 +- docs/start/onboarding-overview.md | 8 +- docs/start/onboarding.md | 51 +- docs/start/openclaw.md | 34 +- docs/start/setup.md | 16 +- docs/start/showcase.md | 2 +- docs/start/wizard-cli-automation.md | 157 +- docs/start/wizard-cli-reference.md | 116 +- docs/start/wizard.md | 157 +- docs/tools/acp-agents-setup.md | 102 +- docs/tools/acp-agents.md | 363 ++-- docs/tools/agent-send.md | 61 +- docs/tools/apply-patch.md | 14 +- docs/tools/brave-search.md | 3 +- docs/tools/browser-control.md | 34 +- docs/tools/browser-linux-troubleshooting.md | 129 +- docs/tools/browser-login.md | 51 +- ...wsl2-windows-remote-cdp-troubleshooting.md | 170 +- docs/tools/browser.md | 113 +- docs/tools/btw.md | 156 +- docs/tools/code-execution.md | 64 +- docs/tools/creating-skills.md | 33 +- docs/tools/diffs.md | 252 +-- docs/tools/duckduckgo-search.md | 33 +- docs/tools/elevated.md | 31 +- docs/tools/exa-search.md | 56 +- docs/tools/exec-approvals-advanced.md | 159 +- docs/tools/exec-approvals.md | 266 ++- docs/tools/exec.md | 185 +- docs/tools/firecrawl.md | 38 +- docs/tools/gemini-search.md | 5 +- docs/tools/goal.md | 189 +- docs/tools/grok-search.md | 60 +- docs/tools/image-generation.md | 69 +- docs/tools/index.md | 98 +- docs/tools/kimi-search.md | 82 +- docs/tools/llm-task.md | 68 +- docs/tools/lobster.md | 327 ++-- docs/tools/loop-detection.md | 107 +- docs/tools/media-overview.md | 53 +- docs/tools/minimax-search.md | 15 +- docs/tools/multi-agent-sandbox-tools.md | 18 +- docs/tools/music-generation.md | 118 +- docs/tools/ollama-search.md | 81 +- docs/tools/parallel-search.md | 115 +- docs/tools/pdf.md | 114 +- docs/tools/permission-modes.md | 28 +- docs/tools/perplexity-search.md | 41 +- docs/tools/plugin.md | 252 ++- docs/tools/reactions.md | 52 +- docs/tools/searxng-search.md | 40 +- docs/tools/skill-workshop.md | 181 +- docs/tools/skills-config.md | 156 +- docs/tools/skills.md | 77 +- docs/tools/slash-commands.md | 34 +- docs/tools/subagents.md | 167 +- docs/tools/tavily.md | 41 +- docs/tools/thinking.md | 12 +- docs/tools/tokenjuice.md | 2 +- docs/tools/tool-search.md | 5 +- docs/tools/trajectory.md | 119 +- docs/tools/tts.md | 163 +- docs/tools/video-generation.md | 90 +- docs/tools/web-fetch.md | 44 +- docs/tools/web.md | 200 +- docs/vps.md | 51 +- docs/web/control-ui.md | 185 +- docs/web/dashboard.md | 85 +- docs/web/index.md | 150 +- docs/web/tui.md | 65 +- docs/web/webchat.md | 62 +- 531 files changed, 31763 insertions(+), 40928 deletions(-) diff --git a/docs/.generated/config-baseline.sha256 b/docs/.generated/config-baseline.sha256 index 0ae2918a5093..772b1bbebfa8 100644 --- a/docs/.generated/config-baseline.sha256 +++ b/docs/.generated/config-baseline.sha256 @@ -1,4 +1,4 @@ -88a4b5241395f05e30f84b1f92be9517a64e35efe07a4012e760c160c320aecd config-baseline.json -ef148d059b1b73b3d5ed568bd5cdbf8d4681a41a110341016d2d0318749df064 config-baseline.core.json -3642204f860750da2ce0d43a338f982a206273084bf9afe645ebd759fed0a23a config-baseline.channel.json -e228f29f17758763a098b0ccd10c6db7fc9df84840437ed3f88a88cf945b8078 config-baseline.plugin.json +acdf418738f79d29f58cb6cfe5b7ab2d353cbcce2252861a4f527f85ea0c54af config-baseline.json +4c98c716bc78e65c274ec374757357c1dcc9b5ec75c9e00ea4c20851531b7d1a config-baseline.core.json +c68853362689981ac1cc1e55b9061286c2002104ff1c10bc44ee99a6080e169e config-baseline.channel.json +859aa272b0dad53b7080c6fefcf775347ae79a1998ec39dd18b732c90d9df90c config-baseline.plugin.json diff --git a/docs/.generated/plugin-sdk-api-baseline.sha256 b/docs/.generated/plugin-sdk-api-baseline.sha256 index 00335b7cfdfe..b06234b0a28b 100644 --- a/docs/.generated/plugin-sdk-api-baseline.sha256 +++ b/docs/.generated/plugin-sdk-api-baseline.sha256 @@ -1,2 +1,2 @@ -c5220c8ccb7c2c57fb62f689bba49fa34a062546bcac909f7d38bba61e99a585 plugin-sdk-api-baseline.json -41ed9c28192c9f9c47de551b854a880225212c441f92e527681462b7fc3a9bab plugin-sdk-api-baseline.jsonl +729181bf726ea51bebfa51c760eff8d5016933c21815e47197b5d05db8d549d5 plugin-sdk-api-baseline.json +d8add92c9c5445e8bdd8a8361cdd5a42e1ab5baa45cd16ea640152d6f577f1bf plugin-sdk-api-baseline.jsonl diff --git a/docs/.i18n/glossary.zh-CN.json b/docs/.i18n/glossary.zh-CN.json index 518de409c257..f990d5749a6f 100644 --- a/docs/.i18n/glossary.zh-CN.json +++ b/docs/.i18n/glossary.zh-CN.json @@ -1230,5 +1230,157 @@ { "source": "Docs map", "target": "文档地图" + }, + { + "source": "Additional provider variants", + "target": "其他提供商变体" + }, + { + "source": "Agent config reference", + "target": "Agent 配置参考" + }, + { + "source": "Background process", + "target": "后台进程" + }, + { + "source": "Channel outbound API", + "target": "渠道出站 API" + }, + { + "source": "Channel routing", + "target": "渠道路由" + }, + { + "source": "Channels Overview", + "target": "渠道概览" + }, + { + "source": "Compaction", + "target": "压缩" + }, + { + "source": "Debugging (--dev)", + "target": "调试(--dev)" + }, + { + "source": "Devices", + "target": "设备" + }, + { + "source": "Gateway CLI", + "target": "Gateway CLI" + }, + { + "source": "Gateway configuration", + "target": "Gateway 配置" + }, + { + "source": "Gmail Pub/Sub integration", + "target": "Gmail Pub/Sub 集成" + }, + { + "source": "Group chats", + "target": "群聊" + }, + { + "source": "Hooks", + "target": "Hooks" + }, + { + "source": "Incident response", + "target": "事件响应" + }, + { + "source": "Logging overview", + "target": "日志概览" + }, + { + "source": "Menu bar", + "target": "菜单栏" + }, + { + "source": "Message Presentation", + "target": "消息呈现" + }, + { + "source": "Models CLI reference", + "target": "模型 CLI 参考" + }, + { + "source": "Network proxy", + "target": "网络代理" + }, + { + "source": "Nodes CLI", + "target": "节点 CLI" + }, + { + "source": "Nodes overview", + "target": "节点概览" + }, + { + "source": "NovitaAI", + "target": "NovitaAI" + }, + { + "source": "Onboard", + "target": "引导设置" + }, + { + "source": "Operator scopes", + "target": "操作员权限范围" + }, + { + "source": "Scheduled tasks vs heartbeat", + "target": "定时任务与心跳对比" + }, + { + "source": "Scheduled tasks: Troubleshooting", + "target": "定时任务:故障排查" + }, + { + "source": "Session config", + "target": "会话配置" + }, + { + "source": "Signal reactions", + "target": "Signal 表情回应" + }, + { + "source": "Task Flow", + "target": "Task Flow" + }, + { + "source": "Telegram reaction notifications", + "target": "Telegram 表情回应通知" + }, + { + "source": "Threat model", + "target": "威胁模型" + }, + { + "source": "Timezones", + "target": "时区" + }, + { + "source": "Troubleshooting", + "target": "故障排查" + }, + { + "source": "Webhooks", + "target": "Webhooks" + }, + { + "source": "WhatsApp reaction level", + "target": "WhatsApp 表情回应级别" + }, + { + "source": "Zalo (official Bot/webhook channel)", + "target": "Zalo(官方 Bot/webhook 渠道)" + }, + { + "source": "Zalo personal channel config", + "target": "Zalo 个人渠道配置" } ] diff --git a/docs/AGENTS.md b/docs/AGENTS.md index 4954077257b9..40577a497543 100644 --- a/docs/AGENTS.md +++ b/docs/AGENTS.md @@ -15,6 +15,7 @@ This directory owns docs authoring, Mintlify link rules, and docs i18n policy. - For docs, UI copy, and picker lists, order services/providers alphabetically unless the section is explicitly describing runtime order or auto-detection order. - Keep bundled plugin naming consistent with the repo-wide plugin terminology rules in the root `AGENTS.md`. +- Generated docs, never hand-edit: `docs/plugins/reference/**`, `docs/plugins/reference.md`, and `docs/plugins/plugin-inventory.md` come from `pnpm plugins:inventory:gen`; `docs/docs_map.md` from `pnpm docs:map:gen`; `docs/maturity/**` from `pnpm maturity:render`. ## Internal Docs diff --git a/docs/agent-runtime-architecture.md b/docs/agent-runtime-architecture.md index b3ff05c7cfe1..52e422d04854 100644 --- a/docs/agent-runtime-architecture.md +++ b/docs/agent-runtime-architecture.md @@ -1,29 +1,32 @@ --- title: "Agent runtime architecture" -summary: "How OpenClaw runs the built-in agent runtime, providers, sessions, tools, and extensions." +summary: "How OpenClaw structures the built-in agent runtime: code layout, boundaries, resource manifests, and runtime selection." --- -OpenClaw owns the built-in agent runtime directly. The runtime code lives under `src/agents/`, model/provider helpers live under `src/llm/`, and plugin-facing contracts are exposed through `openclaw/plugin-sdk/*` barrels. +OpenClaw owns the built-in agent runtime. Runtime code lives under `src/agents/`, model/provider transport lives under `src/llm/`, and plugin-facing contracts are exposed through `openclaw/plugin-sdk/*` barrels. ## Runtime Layout -- `src/agents/embedded-agent-runner/`: built-in agent attempt loop, provider stream adapters, compaction, model selection, and session wiring. -- `src/agents/sessions/`: session persistence, extension loading, resource discovery, skills, prompts, themes, and TUI-backed tool renderers. -- `packages/agent-core/`: reusable agent core, lower-level harness types, messages, compaction helpers, prompt templates, and tool/session contracts. -- `src/agents/runtime/`: OpenClaw facade for `@openclaw/agent-core` plus local proxy utilities. -- `src/agents/agent-tools*.ts`: OpenClaw-owned tool definitions, schemas, policy, before/after hook adapters, and host edit support. -- `src/agents/agent-hooks/`: built-in runtime hooks such as compaction safeguards and context pruning. -- `src/llm/`: model/provider registry, transport helpers, and provider-specific stream implementations. +| Path | Owns | +| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `src/agents/embedded-agent-runner/` | Built-in attempt loop (`run.ts`, `run/`), model selection and provider normalization (`model*.ts`), per-provider request params (`extra-params.*`), compaction, transcript and session wiring. | +| `src/agents/sessions/` | Session persistence (`session-manager.ts`), resource discovery (`package-manager.ts`, `resource-loader.ts`), in-session `extensions` loading, prompt templates, skills, themes, and TUI-backed tool renderers (`tools/`). | +| `packages/agent-core/` | Reusable agent core (`@openclaw/agent-core`): agent loop, harness types, messages, compaction helpers, prompt templates, skills, and session storage contracts. | +| `src/agents/runtime/` | OpenClaw facade that wires `@openclaw/agent-core` to the plugin SDK LLM runtime and re-exports it plus local proxy utilities. | +| `src/agents/agent-tools*.ts` | OpenClaw-owned tool definitions, parameter schemas, tool policy, before/after tool-call adapters, and host/sandbox edit tools. | +| `src/agents/agent-hooks/` | Built-in runtime hooks: compaction safeguard, compaction instructions, context pruning. | +| `src/agents/harness/` | Harness registry, selection policy, and lifecycle for the built-in and plugin-registered harnesses. | +| `src/llm/` | Model/provider registry, transport helpers, and provider-specific stream implementations (`src/llm/providers/`). | ## Boundaries -Core code calls the built-in runtime through OpenClaw modules and SDK barrels, not through old external agent packages. Plugins use documented `openclaw/plugin-sdk/*` entrypoints and do not import `src/**` internals. +Core calls the built-in runtime through OpenClaw modules and SDK barrels; no external agent framework packages remain. Plugins use documented `openclaw/plugin-sdk/*` entrypoints and do not import `src/**` internals. -`@earendil-works/pi-tui` remains a third-party TUI dependency. It is used as a terminal component toolkit by the local TUI and session renderers; internalizing it would be a separate vendoring effort. +`@earendil-works/pi-tui` remains a third-party dependency: a terminal component toolkit used by the local TUI and session tool renderers. Internalizing it would be a separate vendoring effort. ## Manifests -Resource packages declare OpenClaw resources in package metadata: +Resource packages declare OpenClaw resources in `package.json` metadata. Entries are file paths or globs relative to the package root: ```json { @@ -36,11 +39,15 @@ Resource packages declare OpenClaw resources in package metadata: } ``` -The package manager also discovers conventional `extensions/`, `skills/`, `prompts/`, and `themes/` directories. +Resource types not listed in a manifest fall back to discovery of conventional `extensions/`, `skills/`, `prompts/`, and `themes/` directories. ## Runtime Selection -The default built-in runtime id is `openclaw`. Plugin harnesses can register additional runtime ids. `auto` selects a supporting plugin harness when one exists and otherwise uses the built-in OpenClaw runtime. +- The built-in runtime id is `openclaw`. The legacy alias `pi` normalizes to `openclaw`; `codex-app-server` normalizes to `codex`. +- Plugin harnesses register additional runtime ids (for example `codex`). +- Runtime policy is model/provider-scoped `agentRuntime.id` config (model entry wins over provider entry). Unset or `default` resolves to `auto`. +- `auto` selects a registered plugin harness that supports the provider/model, otherwise the built-in OpenClaw runtime. +- The `openai` provider on the official API endpoint defaults to the `codex` harness; custom `baseUrl` values keep their configured behavior. ## Related diff --git a/docs/announcements/bluebubbles-imessage.md b/docs/announcements/bluebubbles-imessage.md index 4849a5102f27..d63c3d05fa8b 100644 --- a/docs/announcements/bluebubbles-imessage.md +++ b/docs/announcements/bluebubbles-imessage.md @@ -9,17 +9,17 @@ title: "BlueBubbles removal and the imsg iMessage path" # BlueBubbles removal and the imsg iMessage path -OpenClaw no longer ships the BlueBubbles channel. iMessage support now runs through the bundled `imessage` plugin, which starts [`imsg`](https://github.com/steipete/imsg) locally or through an SSH wrapper and talks JSON-RPC over stdin/stdout. +OpenClaw no longer ships the BlueBubbles channel. iMessage support runs through the bundled `imessage` plugin: the Gateway spawns [`imsg`](https://github.com/steipete/imsg) as a child process, locally or through an SSH wrapper, and talks JSON-RPC over stdin/stdout. No server, no webhook, no port. If your config still contains `channels.bluebubbles`, migrate it to `channels.imessage`. The legacy `/channels/bluebubbles` docs URL redirects to [Coming from BlueBubbles](/channels/imessage-from-bluebubbles), which has the full config translation table and cutover checklist. ## What changed -- There is no BlueBubbles HTTP server, webhook route, REST password, or BlueBubbles plugin runtime in the supported OpenClaw iMessage path. +- The supported iMessage path has no BlueBubbles HTTP server, webhook route, REST password, or BlueBubbles plugin runtime. - OpenClaw reads and watches Messages through `imsg` on the Mac where Messages.app is signed in. - Basic send, receive, history, and media use the normal `imsg` surfaces and macOS permissions. -- Advanced actions such as threaded replies, tapbacks, edit, unsend, effects, read receipts, typing indicators, and group management require `imsg launch` with the private API bridge available. -- Linux and Windows gateways can still use iMessage by setting `channels.imessage.cliPath` to an SSH wrapper that runs `imsg` on the signed-in Mac. +- Advanced actions (threaded replies, tapbacks, edit, unsend, effects, read receipts, typing indicators, group management) need the private API bridge: run `imsg launch`, which requires SIP disabled. +- Linux and Windows gateways can still use iMessage by pointing `channels.imessage.cliPath` at an SSH wrapper that runs `imsg` on the signed-in Mac. ## What to do @@ -65,12 +65,12 @@ If your config still contains `channels.bluebubbles`, migrate it to `channels.im ## Migration notes -- `channels.bluebubbles.serverUrl` and `channels.bluebubbles.password` have no iMessage equivalent. -- `channels.bluebubbles.allowFrom`, `groupAllowFrom`, `groups`, `includeAttachments`, attachment roots, media size limits, chunking, and action toggles have iMessage equivalents. +- `channels.bluebubbles.serverUrl` and `channels.bluebubbles.password` have no iMessage equivalent; there is no server to reach or authenticate. +- `allowFrom`, `groupAllowFrom`, `groups`, `includeAttachments`, `attachmentRoots`, `mediaMaxMb`, `textChunkLimit`, and `actions.*` keep their meaning under `channels.imessage`. - `channels.imessage.includeAttachments` is still off by default. Set it explicitly if you expect inbound photos, voice memos, videos, or files to reach the agent. -- With `groupPolicy: "allowlist"`, copy the old `groups` block, including any `"*"` wildcard entry. Group sender allowlists and the group registry are separate gates. -- ACP bindings that matched `channel: "bluebubbles"` must be changed to `channel: "imessage"`. -- Old BlueBubbles session keys do not become iMessage session keys. Pairing approvals carry over by handle, but conversation history under BlueBubbles session keys does not. +- With `groupPolicy: "allowlist"`, copy the old `groups` block, including any `"*"` wildcard entry. Group sender allowlists and the group registry are separate gates; a `groups` block with entries but no matching `chat_id` (or no `"*"`) drops the message at runtime, and an empty `groups` block logs a startup warning even though sender filtering still lets messages through. +- ACP bindings with `match.channel: "bluebubbles"` must change to `"imessage"`. +- Old BlueBubbles session keys do not become iMessage session keys. Pairing approvals key off sender handles, so copied `allowFrom` entries keep working, but conversation history under BlueBubbles session keys does not carry over. ## See also diff --git a/docs/auth-credential-semantics.md b/docs/auth-credential-semantics.md index a072e63e7535..8a59a19616de 100644 --- a/docs/auth-credential-semantics.md +++ b/docs/auth-credential-semantics.md @@ -6,24 +6,28 @@ read_when: - Debugging model auth failures or profile order --- -This document defines the canonical credential eligibility and resolution semantics used across: +These semantics keep selection-time and runtime auth behavior aligned. They are shared by: -- `resolveAuthProfileOrder` -- `resolveApiKeyForProfile` -- `models status --probe` -- `doctor-auth` - -The goal is to keep selection-time and runtime behavior aligned. +- `resolveAuthProfileOrder` (profile ordering) +- `resolveApiKeyForProfile` (runtime credential resolution) +- `openclaw models status --probe` +- `openclaw doctor` auth checks (`doctor-auth`) ## Stable probe reason codes -- `ok` -- `excluded_by_auth_order` -- `missing_credential` -- `invalid_expires` -- `expired` -- `unresolved_ref` -- `no_model` +Probe results carry a `status` bucket (`ok`, `auth`, `rate_limit`, `billing`, `timeout`, `format`, `unknown`, `no_model`) plus a stable `reasonCode` when the probe never reached a model call: + +| `reasonCode` | Meaning | +| ------------------------ | ---------------------------------------------------------------------------- | +| `excluded_by_auth_order` | Profile omitted from the explicit auth order for its provider. | +| `missing_credential` | No inline credential or SecretRef is configured. | +| `expired` | Token `expires` is in the past. | +| `invalid_expires` | `expires` is not a valid positive Unix ms timestamp. | +| `unresolved_ref` | Configured SecretRef could not be resolved. | +| `ineligible_profile` | Profile is incompatible with provider config (includes malformed key input). | +| `no_model` | Credentials exist but no probeable model candidate resolved. | + +Eligibility checks report `ok` as the reason code for usable credentials. ## Token credentials @@ -31,84 +35,59 @@ Token credentials (`type: "token"`) support inline `token` and/or `tokenRef`. ### Eligibility rules -1. A token profile is ineligible when both `token` and `tokenRef` are absent. -2. `expires` is optional. -3. If `expires` is present, it must be a finite number greater than `0`. -4. If `expires` is invalid (`NaN`, `0`, negative, non-finite, or wrong type), the profile is ineligible with `invalid_expires`. -5. If `expires` is in the past, the profile is ineligible with `expired`. -6. `tokenRef` does not bypass `expires` validation. +1. A token profile is ineligible when both `token` and `tokenRef` are absent (`missing_credential`). +2. `expires` is optional. When present it must be a finite number of Unix epoch milliseconds greater than `0` and no larger than the maximum JavaScript `Date` timestamp (8640000000000000). +3. If `expires` is invalid (wrong type, `NaN`, `0`, negative, non-finite, or beyond that maximum), the profile is ineligible with `invalid_expires`. +4. If `expires` is in the past, the profile is ineligible with `expired`. +5. `tokenRef` does not bypass `expires` validation. ### Resolution rules 1. Resolver semantics match eligibility semantics for `expires`. -2. For eligible profiles, token material may be resolved from inline value or `tokenRef`. +2. For eligible profiles, token material may be resolved from the inline value or `tokenRef`. 3. Unresolvable refs produce `unresolved_ref` in `models status --probe` output. ## Agent copy portability -Agent auth inheritance is read-through. When an agent has no local profile, it -can resolve profiles from the default/main agent store at runtime without -copying secret material into its own `auth-profiles.json`. +Agent auth inheritance is read-through. When an agent has no local profile, it resolves profiles from the default/main agent store at runtime without copying secret material into its own credential store (`agents//agent/openclaw-agent.sqlite`). Explicit copy flows, such as `openclaw agents add`, use this portability policy: -- `api_key` profiles are portable unless `copyToAgents: false`. -- `token` profiles are portable unless `copyToAgents: false`. -- `oauth` profiles are not portable by default because refresh tokens can be - single-use or rotation-sensitive. -- Provider-owned OAuth flows may opt in with `copyToAgents: true` only when - copying refresh material across agents is known safe. +- `api_key` and `token` profiles are portable unless `copyToAgents: false`. +- `oauth` profiles are not portable by default because refresh tokens can be single-use or rotation-sensitive. +- Provider-owned OAuth flows may opt in with `copyToAgents: true` only when copying refresh material across agents is known safe; the opt-in only applies when the profile carries inline access/refresh material. -Non-portable profiles remain available through read-through inheritance unless -the target agent signs in separately and creates its own local profile. +Non-portable profiles remain available through read-through inheritance unless the target agent signs in separately and creates its own local profile. ## Config-only auth routes -`auth.profiles` entries with `mode: "aws-sdk"` are routing metadata, not stored -credentials. They are valid when the target provider uses -`models.providers..auth: "aws-sdk"` or plugin-owned Amazon Bedrock setup -AWS SDK route. These profile ids may appear in `auth.order` and session -overrides even when no matching entry exists in `auth-profiles.json`. +`auth.profiles` entries with `mode: "aws-sdk"` are routing metadata, not stored credentials. They are valid when the target provider uses `models.providers..auth: "aws-sdk"`, the route the plugin-owned Amazon Bedrock setup writes. These profile ids may appear in `auth.order` and session overrides even when no matching entry exists in the credential store. -Do not write `type: "aws-sdk"` into `auth-profiles.json`. If a legacy install -has such a marker, `openclaw doctor --fix` moves it to `auth.profiles` and -removes the marker from the credential store. +Do not write `type: "aws-sdk"` into the credential store; stored credentials are only `api_key`, `token`, or `oauth`. If a legacy `auth-profiles.json` has such a marker, `openclaw doctor --fix` moves it to `auth.profiles` and removes the marker from the store. ## Explicit auth order filtering -- When `auth.order.` or the auth-store order override is set for a - provider, `models status --probe` only probes profile ids that remain in the - resolved auth order for that provider. -- A stored profile for that provider that is omitted from the explicit order is - not silently tried later. Probe output reports it with - `reasonCode: excluded_by_auth_order` and the detail - `Excluded by auth.order for this provider.` +- When `auth.order.` or the auth-store order override is set for a provider, `models status --probe` only probes profile ids that remain in the resolved auth order for that provider. The stored override wins over `auth.order` config. +- A stored profile for that provider that is omitted from the explicit order is not silently tried later. Probe output reports it with `reasonCode: excluded_by_auth_order` and the detail `Excluded by auth.order for this provider.` ## Probe target resolution -- Probe targets can come from auth profiles, environment credentials, or - `models.json`. -- If a provider has credentials but OpenClaw cannot resolve a probeable model - candidate for it, `models status --probe` reports `status: no_model` with - `reasonCode: no_model`. +- Probe targets can come from auth profiles, environment credentials, or `models.json` (result `source`: `profile`, `env`, `models.json`). +- If a provider has credentials but OpenClaw cannot resolve a probeable model candidate for it, `models status --probe` reports `status: no_model` with `reasonCode: no_model`. ## External CLI credential discovery -- Runtime-only credentials owned by external CLIs are discovered only when the - provider, runtime, or auth profile is in scope for the current operation, or - when a stored local profile for that external source already exists. -- Auth-store callers should choose an explicit external-CLI discovery mode: - `none` for persisted/plugin auth only, `existing` for refreshing already - stored external CLI profiles, or `scoped` for a concrete provider/profile set. -- Read-only/status paths pass `allowKeychainPrompt: false`; they use file-backed - external CLI credentials only and do not read or reuse macOS Keychain results. +- Runtime-only credentials owned by external CLIs (Claude CLI for `claude-cli`, Codex CLI for `openai`, MiniMax CLI for `minimax-portal`) are discovered only when the provider, runtime, or auth profile is in scope for the current operation, or when a stored local profile for that external source already exists. +- Auth-store callers choose an explicit external-CLI discovery mode: `none` for persisted/plugin auth only, `existing` for refreshing already stored external CLI profiles, or `scoped` for a concrete provider/profile set. +- Read-only/status paths pass `allowKeychainPrompt: false`; they use file-backed external CLI credentials only and do not read or reuse macOS Keychain results. ## OAuth SecretRef Policy Guard -- SecretRef input is for static credentials only. -- If a profile credential is `type: "oauth"`, SecretRef objects are not supported for that profile credential material. +SecretRef input is for static credentials only. OAuth credentials are runtime-mutable (refresh flows persist rotated tokens), so SecretRef-backed OAuth material would split mutable state across stores. + +- If a profile credential is `type: "oauth"`, SecretRef objects are rejected for any credential material field on that profile. - If `auth.profiles..mode` is `"oauth"`, SecretRef-backed `keyRef`/`tokenRef` input for that profile is rejected. -- Violations are hard failures in startup/reload auth resolution paths. +- Violations are hard failures (thrown errors) in startup/reload secret preparation and profile resolution paths. ## Legacy-Compatible Messaging @@ -116,7 +95,7 @@ For script compatibility, probe errors keep this first line unchanged: `Auth profile credentials are missing or expired.` -Human-friendly detail and stable reason codes may be added on subsequent lines. +Human-friendly detail and the stable reason code follow on subsequent lines in the form `↳ Auth reason [code]: ...`. ## Related diff --git a/docs/automation/auth-monitoring.md b/docs/automation/auth-monitoring.md index 8de5b36ea427..e7e33ea809af 100644 --- a/docs/automation/auth-monitoring.md +++ b/docs/automation/auth-monitoring.md @@ -3,9 +3,9 @@ summary: "Redirect to /gateway/authentication" title: "Auth monitoring" --- -Auth monitoring lives under [Authentication](/gateway/authentication). +This page moved. Model provider authentication lives under [Authentication](/gateway/authentication); ops scripts for monitoring auth state live at [Auth monitoring scripts](/help/scripts#auth-monitoring-scripts). ## Related -- [Automation troubleshooting](/automation/troubleshooting) +- [Automation troubleshooting](/automation/cron-jobs#troubleshooting) - [Hooks](/automation/hooks) diff --git a/docs/automation/clawflow.md b/docs/automation/clawflow.md index c125c41094dc..2b7919da52fd 100644 --- a/docs/automation/clawflow.md +++ b/docs/automation/clawflow.md @@ -3,10 +3,10 @@ summary: "Redirect to Task Flow" title: "ClawFlow" --- -ClawFlow was renamed to [Task flow](/automation/taskflow). +ClawFlow was renamed to [Task Flow](/automation/taskflow). Use that page for durable multi-step flow orchestration. ## Related -- [Task flow](/automation/taskflow) +- [Task Flow](/automation/taskflow) - [Standing orders](/automation/standing-orders) - [Hooks](/automation/hooks) diff --git a/docs/automation/cron-jobs.md b/docs/automation/cron-jobs.md index 2e14a545b122..cff3036dce10 100644 --- a/docs/automation/cron-jobs.md +++ b/docs/automation/cron-jobs.md @@ -8,14 +8,14 @@ title: "Scheduled tasks" sidebarTitle: "Scheduled tasks" --- -Cron is the Gateway's built-in scheduler. It persists jobs, wakes the agent at the right time, and can deliver output back to a chat channel or webhook endpoint. +Cron is the Gateway's built-in scheduler. It persists jobs, wakes the agent at the right time, and can deliver output to a chat channel, a webhook, or nowhere. ## Quick start ```bash - openclaw cron create "2026-02-01T16:00:00Z" \ + openclaw cron create "2027-02-01T16:00:00Z" \ --name "Reminder" \ --session main \ --system-event "Reminder: check the cron docs draft" \ @@ -39,100 +39,116 @@ Cron is the Gateway's built-in scheduler. It persists jobs, wakes the agent at t ## How cron works -- Cron runs **inside the Gateway** process (not inside the model). -- Job definitions, runtime state, and run history persist in OpenClaw's shared SQLite state database so restarts do not lose schedules. -- On upgrade, run `openclaw doctor --fix` to import legacy `~/.openclaw/cron/jobs.json`, `jobs-state.json`, and `runs/*.jsonl` files into SQLite and rename them with a `.migrated` suffix. Malformed job rows are skipped from runtime and copied to `jobs-quarantine.json` for later repair or review. -- `cron.store` still names the logical cron store key and doctor import path. After import, editing that JSON file no longer changes active cron jobs; use `openclaw cron add|edit|remove` or the Gateway cron RPC methods instead. -- All cron executions create [background task](/automation/tasks) records. -- On Gateway startup, overdue isolated agent-turn jobs are rescheduled out of the channel-connect window instead of replaying immediately, so Discord/Telegram startup and native-command setup stay responsive after restarts. -- One-shot jobs (`--at`) auto-delete after success by default. -- Isolated cron runs best-effort close tracked browser tabs/processes for their `cron:` session when the run completes, so detached browser automation does not leave orphaned processes behind. -- Isolated cron runs that receive the narrow cron self-cleanup grant can still read scheduler status, a self-filtered list of their current job, and that job's run history, so status/heartbeat checks can inspect their own schedule without gaining broader cron mutation access. -- Isolated cron runs also guard against stale acknowledgement replies. If the first result is just an interim status update (`on it`, `pulling everything together`, and similar hints) and no descendant subagent run is still responsible for the final answer, OpenClaw re-prompts once for the actual result before delivery. -- Isolated cron runs use structured execution-denial metadata from the embedded run, including node-host `UNAVAILABLE` wrappers whose nested error message starts with `SYSTEM_RUN_DENIED` or `INVALID_REQUEST`, so a blocked command is not reported as a green run while ordinary assistant prose is not treated as a denial. -- Isolated cron runs also treat run-level agent failures as job errors even when no reply payload is produced, so model/provider failures increment error counters and trigger failure notifications instead of clearing the job as successful. -- When an isolated agent-turn job reaches `timeoutSeconds`, cron aborts the underlying agent run and gives it a short cleanup window. If the run does not drain, Gateway-owned cleanup force-clears that run's session ownership before cron records the timeout, so queued chat work is not left behind a stale processing session. -- If an isolated agent-turn stalls before the runner starts or before the first model call, cron records a phase-specific timeout such as `setup timed out before runner start` or `stalled before first model call (last phase: context-engine)`. These watchdogs cover embedded providers and CLI-backed providers before their external CLI process is actually started, and are capped independently from long `timeoutSeconds` values so cold-start/auth/context failures surface quickly instead of waiting for the full job budget. -- If you use system cron or another external scheduler to run `openclaw agent`, wrap it with a hard-kill escalation even though the CLI handles `SIGTERM`/`SIGINT`. Gateway-backed runs ask the Gateway to abort accepted runs; local and embedded fallback runs receive the same abort signal. For GNU `timeout`, prefer `timeout -k 60 600 openclaw agent ...` over plain `timeout 600 ...`; the `-k` value is the supervisor backstop if the process cannot drain. For systemd units, keep the same shape by using a `SIGTERM` stop signal plus a grace window such as `TimeoutStopSec` before any final kill. If a retry reuses a `--run-id` while the original Gateway run is still active, the duplicate is reported as in-flight instead of starting a second run. +- Cron runs **inside the Gateway process**, not inside the model. The Gateway must be running for schedules to fire. +- Job definitions, runtime state, and run history persist in OpenClaw's shared SQLite state database, so restarts do not lose schedules. +- Every cron execution creates a [background task](/automation/tasks) record. +- One-shot jobs (`--at`) auto-delete after success by default; pass `--keep-after-run` to keep them. +- Per-run wall-clock budget: `--timeout-seconds` when set. Otherwise, isolated/detached agent-turn jobs are bounded by cron's own 60-minute watchdog before the underlying agent-turn timeout (`agents.defaults.timeoutSeconds`, default 48 hours) would ever apply; command jobs default to 10 minutes. +- On Gateway startup, overdue isolated agent-turn jobs are rescheduled instead of replayed immediately, keeping model/tool bootstrap work out of the channel-connect window. +- If you drive `openclaw agent` from system cron or another external scheduler, wrap it with a hard-kill escalation even though the CLI already handles `SIGTERM`/`SIGINT`. Gateway-backed runs ask the Gateway to abort accepted runs; local and embedded fallback runs get the same abort signal. For GNU `timeout`, prefer `timeout -k 60 600 openclaw agent ...` over plain `timeout 600 ...` — the `-k` value is the backstop if the process cannot drain in time. For systemd units, use a `SIGTERM` stop signal with a grace window (`TimeoutStopSec`) before the final kill. Reusing a `--run-id` while the original Gateway run is still active reports the duplicate as in-flight instead of starting a second run. - + + + - Isolated runs best-effort close tracked browser tabs/processes for their `cron:` session on completion, and dispose any bundled MCP runtime instances created for the job through the same shared teardown path used by main-session and custom-session runs. Cleanup failures are ignored so the cron result still wins. + - Isolated runs with the narrow cron self-cleanup grant can read scheduler status, a self-filtered list containing only their own job, and that job's run history, and may remove only their own job. + - Isolated runs guard against stale acknowledgement replies: if the first result is only an interim status update (`on it`, `pulling everything together`, and similar hints) and no descendant subagent is still responsible for the final answer, OpenClaw re-prompts once for the actual result before delivery. + - Structured execution-denial metadata (including node-host `UNAVAILABLE` wrappers whose nested error starts with `SYSTEM_RUN_DENIED` or `INVALID_REQUEST`) is recognized so a blocked command is not reported as a green run, while ordinary assistant prose is not mistaken for a denial. + - Run-level agent failures count as job errors even with no reply payload, so model/provider failures increment error counters and trigger failure notifications instead of clearing the job as successful. + - When a job hits `timeoutSeconds`, cron aborts the run and gives it a short cleanup window. If it does not drain, Gateway-owned cleanup force-clears that run's session ownership before cron records the timeout, so queued chat work is not stuck behind a stale processing session. + - Setup/startup stalls get a phase-specific timeout (for example `cron: isolated agent setup timed out before runner start` or `cron: isolated agent run stalled before execution start (last phase: context-engine)`). These watchdogs cover embedded and CLI-backed providers even before their external CLI process starts, and are capped independently of long `timeoutSeconds` values so cold-start/auth/context failures surface quickly. - -Task reconciliation for cron is runtime-owned first, durable-history-backed second: an active cron task stays live while the cron runtime still tracks that job as running, even if an old child session row still exists. Once the runtime stops owning the job and the 5-minute grace window expires, maintenance checks persisted run logs and job state for the matching `cron::` run. If that durable history shows a terminal result, the task ledger is finalized from it; otherwise Gateway-owned maintenance can mark the task `lost`. Offline CLI audit can recover from durable history, but it does not treat its own empty in-process active-job set as proof that a Gateway-owned cron run is gone. - + + + Cron task reconciliation is runtime-owned first, durable-history-backed second: an active cron task stays live while the cron runtime still tracks that job as running, even if an old child session row still exists. Once the runtime stops owning the job and a 5-minute grace window expires, maintenance checks persisted run logs and job state for the matching `cron::` run. A terminal result there finalizes the task ledger; otherwise Gateway-owned maintenance can mark the task `lost`. Offline CLI audit can recover from durable history, but its own empty in-process active-job set is not proof a Gateway-owned run is gone. + + ## Schedule types -| Kind | CLI flag | Description | -| ------- | --------- | ------------------------------------------------------- | -| `at` | `--at` | One-shot timestamp (ISO 8601 or relative like `20m`) | -| `every` | `--every` | Fixed interval | -| `cron` | `--cron` | 5-field or 6-field cron expression with optional `--tz` | +| Kind | CLI flag | Description | +| --------- | ----------- | -------------------------------------------------------------------------------------------------------- | +| `at` | `--at` | One-shot timestamp (ISO 8601 or relative like `20m`) | +| `every` | `--every` | Fixed interval (`10m`, `1h`, `1d`) | +| `cron` | `--cron` | 5-field or 6-field cron expression with optional `--tz` | +| `on-exit` | `--on-exit` | Fire once when a watched command exits (event trigger; survives turn teardown; optional `--on-exit-cwd`) | -Timestamps without a timezone are treated as UTC. Add `--tz America/New_York` for local wall-clock scheduling. +Timestamps without a timezone are treated as UTC. Add `--tz America/New_York` to interpret an offset-less `--at` datetime, or to evaluate a cron expression, in that IANA timezone. Cron expressions without `--tz` use the Gateway host timezone. `--tz` is not valid with `--every` or `--on-exit`. -Recurring top-of-hour expressions are automatically staggered by up to 5 minutes to reduce load spikes. Use `--exact` to force precise timing or `--stagger 30s` for an explicit window. +Recurring top-of-hour expressions (minute `0` with a wildcard hour field) are automatically staggered by up to 5 minutes to reduce load spikes. Use `--exact` to force precise timing, or `--stagger 30s` for an explicit window (cron schedules only). ### Day-of-month and day-of-week use OR logic -Cron expressions are parsed by [croner](https://github.com/Hexagon/croner). When both the day-of-month and day-of-week fields are non-wildcard, croner matches when **either** field matches — not both. This is standard Vixie cron behavior. +Cron expressions are parsed by [croner](https://github.com/Hexagon/croner). When both the day-of-month and day-of-week fields are non-wildcard, croner matches when **either** field matches, not both. This is standard Vixie cron behavior. -``` +```bash # Intended: "9 AM on the 15th, only if it's a Monday" # Actual: "9 AM on every 15th, AND 9 AM on every Monday" 0 9 15 * 1 ``` -This fires ~5–6 times per month instead of 0–1 times per month. OpenClaw uses Croner's default OR behavior here. To require both conditions, use Croner's `+` day-of-week modifier (`0 9 15 * +1`) or schedule on one field and guard the other in your job's prompt or command. +This fires roughly 5-6 times a month instead of 0-1 times a month. To require both conditions, use croner's `+` day-of-week modifier (`0 9 15 * +1`), or schedule on one field and guard the other in your job's prompt or command. -## Execution styles +## Payloads -| Style | `--session` value | Runs in | Best for | -| --------------- | ------------------- | ------------------------ | ------------------------------- | -| Main session | `main` | Dedicated cron wake lane | Reminders, system events | -| Isolated | `isolated` | Dedicated `cron:` | Reports, background chores | -| Current session | `current` | Bound at creation time | Context-aware recurring work | -| Custom session | `session:custom-id` | Persistent named session | Workflows that build on history | +Every job carries exactly one payload kind, chosen by flag: - - - **Main session** jobs enqueue a system event into a cron-owned run lane and optionally wake the heartbeat (`--wake now` or `--wake next-heartbeat`). They can use the target main session's last delivery context for replies, but they do not append routine cron turns to the human chat lane and do not extend daily/idle reset freshness for the target session. **Isolated** jobs run a dedicated agent turn with a fresh session. **Custom sessions** (`session:xxx`) persist context across runs, enabling workflows like daily standups that build on previous summaries. +| Payload | Flag | Runs | +| ------------- | ---------------------------------------------- | ------------------------------------------------------- | +| System event | `--system-event ` | Enqueued into the main session, no model call by itself | +| Agent message | `--message ` | A model-backed agent turn | +| Command | `--command ` or `--command-argv ` | A shell/process on the Gateway host, no model call | - Main-session cron events are self-contained system-event reminders. They do - not automatically include the default heartbeat prompt's "Read - HEARTBEAT.md" instruction. If a recurring reminder should consult - `HEARTBEAT.md`, say that explicitly in the cron event text or in the - agent's own instructions. +### Agent-turn options - - - For isolated jobs, "fresh session" means a new transcript/session id for each run. OpenClaw may carry safe preferences such as thinking/fast/verbose settings, labels, and explicit user-selected model/auth overrides, but it does not inherit ambient conversation context from an older cron row: channel/group routing, send or queue policy, elevation, origin, or ACP runtime binding. Use `current` or `session:` when a recurring job should deliberately build on the same conversation context. - - - For isolated jobs, runtime teardown now includes best-effort browser cleanup for that cron session. Cleanup failures are ignored so the actual cron result still wins. + + Prompt text (required for isolated/current/custom-session jobs). + + + Model override; must resolve to an allowed model or the run fails with a validation error. + + + Per-job fallback model list, for example `--fallbacks openai/gpt-5.5,openrouter/meta-llama/llama-3.3-70b-instruct:free`. Pass `--fallbacks ""` for a strict run with no fallbacks. + + + On `cron edit`, removes the per-job fallback override so the job follows configured fallback precedence. Cannot combine with `--fallbacks`. + + + On `cron edit`, removes the per-job model override so the job follows normal cron model precedence (stored cron-session override, else agent/default model). Cannot combine with `--model`. + + + Thinking level override (`off|minimal|low|medium|high|xhigh|adaptive|max`). + + + On `cron edit`, removes the per-job thinking override. Cannot combine with `--thinking`. + + + Skip workspace bootstrap file injection. + + + Restrict which tools the job can use, for example `--tools exec,read`. + - Isolated cron runs also dispose any bundled MCP runtime instances created for the job through the shared runtime-cleanup path. This matches how main-session and custom-session MCP clients are torn down, so isolated cron jobs do not leak stdio child processes or long-lived MCP connections across runs. +`--model` sets the job's primary model; it does not replace a session `/model` override, so configured fallback chains still apply on top of it. An unresolved or disallowed model fails the run with an explicit validation error rather than silently falling back to the default. If a job has `--model` but no explicit or configured fallback list, OpenClaw passes an empty fallback override instead of silently appending the agent primary as a hidden retry target. - - - When isolated cron runs orchestrate subagents, delivery also prefers the final descendant output over stale parent interim text. If descendants are still running, OpenClaw suppresses that partial parent update instead of announcing it. +Model-selection precedence for isolated jobs, highest first: - For text-only Discord announce targets, OpenClaw sends the canonical final assistant text once instead of replaying both streamed/intermediate text payloads and the final answer. Media and structured Discord payloads are still delivered as separate payloads so attachments and components are not dropped. +1. Per-job payload `model` (explicit config; a disallowed model fails the run) +2. Gmail hook model override (only when the run came from Gmail and that override is allowed) +3. User-selected stored cron-session model override +4. Agent/default model selection - - +Fast mode follows the resolved live selection. If the selected model config has `params.fastMode`, isolated cron uses it by default; a stored session `fastMode` override (then an agent `fastModeDefault`) still wins over model config either direction. Auto mode uses the model's `params.fastAutoOnSeconds` cutoff, defaulting to 60 seconds. + +If a run hits a live model-switch handoff, cron retries with the switched provider/model and persists that selection (and any new auth profile) for the active run. Retries are bounded: after the initial attempt plus 2 switch retries, cron aborts instead of looping. + +Before an isolated run starts, OpenClaw checks reachable local endpoints for configured `api: "ollama"` and `api: "openai-completions"` providers whose `baseUrl` is loopback, private-network, or `.local`. This preflight walks the job's configured fallback chain and only marks the run `skipped` once every candidate is unreachable; `--fallbacks ""` keeps that walk strict to just the primary model. A down endpoint records the run as `skipped` with a clear error instead of starting a model call. The result is cached for 5 minutes per endpoint (not per job or model), so many due jobs sharing a dead local Ollama/vLLM/SGLang/LM Studio server cost one probe instead of a request storm. Skipped preflight runs do not increment execution-error backoff; set `failureAlert.includeSkipped` to opt into repeated skip alerts. ### Command payloads -Use command payloads for deterministic scripts that should run inside the Gateway scheduler without starting a model-backed isolated agent turn. Command jobs execute on the Gateway host, capture stdout/stderr, record the run in cron history, and reuse the same `announce`, `webhook`, and `none` delivery modes as isolated jobs. +Command payloads run deterministic scripts inside the Gateway scheduler without starting a model-backed turn. They execute on the Gateway host, capture stdout/stderr, record the run in cron history, and reuse the same `announce`, `webhook`, and `none` delivery modes as agent-turn jobs. -Command cron is an operator-admin Gateway automation surface, not an agent -`tools.exec` call. Creating, updating, removing, or manually running cron jobs -requires `operator.admin`; scheduled command runs later execute inside the -Gateway process as that admin-authored automation. Agent exec policy such as -`tools.exec.mode`, approval prompts, and per-agent tool allowlists governs -model-visible exec tools, not command cron payloads. +Command cron is an operator-admin Gateway automation surface, not an agent `tools.exec` call. Creating, updating, removing, or manually running cron jobs requires `operator.admin`; scheduled command runs later execute inside the Gateway process as that admin-authored automation. Agent exec policy (`tools.exec.mode`, approval prompts, per-agent tool allowlists) governs model-visible exec tools, not command cron payloads. ```bash @@ -145,58 +161,36 @@ openclaw cron create "*/15 * * * *" \ --to "-1001234567890" ``` -`--command ` stores `argv: ["sh", "-lc", ]`. Use `--command-argv '["node","scripts/report.mjs"]'` when you want exact argv execution without shell parsing. Optional `--command-env KEY=VALUE`, `--command-input`, `--timeout-seconds`, `--no-output-timeout-seconds`, and `--output-max-bytes` fields control the process environment, stdin, and output bounds. +`--command ` stores `argv: ["sh", "-lc", ]`. Use `--command-argv '["node","scripts/report.mjs"]'` for exact argv execution without shell parsing. Optional `--command-env KEY=VALUE` (repeatable), `--command-input`, `--timeout-seconds` (default 10 minutes), `--no-output-timeout-seconds`, and `--output-max-bytes` control the process environment, stdin, and output bounds. -If stdout is non-empty, that text is the delivered result. If stdout is empty and stderr is non-empty, stderr is delivered. If both streams are present, cron delivers a small `stdout:` / `stderr:` block. A zero exit code records the run as `ok`; non-zero exit, signal, timeout, or no-output timeout records `error` and can trigger failure alerts. A command that prints only `NO_REPLY` uses the normal cron silent-token suppression and posts nothing back to chat. +Delivered text is derived from process output: non-empty stdout wins; if stdout is empty and stderr is non-empty, stderr is delivered; if both are present, cron sends a small `stdout:` / `stderr:` block. Exit code `0` records the run `ok`; non-zero exit, signal, timeout, or no-output timeout records `error` and can trigger failure alerts. A command that prints only `NO_REPLY` uses the normal cron silent-token suppression and posts nothing back to chat. -### Payload options for isolated jobs +## Execution styles - - Prompt text (required for isolated). - - - Model override; uses the selected allowed model for the job. - - - Per-job fallback model list, for example `--fallbacks openrouter/gpt-4.1-mini,openai/gpt-5`. Pass `--fallbacks ""` for a strict run with no fallbacks. - - - On `cron edit`, removes the per-job fallback override so the job follows configured fallback precedence. Cannot be combined with `--fallbacks`. - - - On `cron edit`, removes the per-job model override so the job follows normal cron model-selection precedence (a stored cron-session override if set, otherwise the agent/default model). Cannot be combined with `--model`. - - - Thinking level override. - - - On `cron edit`, removes the per-job thinking override so the job follows normal cron thinking precedence. Cannot be combined with `--thinking`. - - - Skip workspace bootstrap file injection. - - - Restrict which tools the job can use, for example `--tools exec,read`. - +| Style | `--session` value | Runs in | Best for | +| --------------- | ------------------- | ------------------------ | ------------------------------- | +| Main session | `main` | Dedicated cron wake lane | Reminders, system events | +| Isolated | `isolated` | Dedicated `cron:` | Reports, background chores | +| Current session | `current` | Bound at creation time | Context-aware recurring work | +| Custom session | `session:custom-id` | Persistent named session | Workflows that build on history | -`--model` uses the selected allowed model as that job's primary model. It is not the same as a chat-session `/model` override: configured fallback chains still apply when the job primary fails. If the requested model is not allowed or cannot be resolved, cron fails the run with an explicit validation error instead of silently falling back to the job's agent/default model selection. + + + **Main session** jobs enqueue a system event into a cron-owned run lane and optionally wake the heartbeat (`--wake now` or `--wake next-heartbeat`). They can use the target main session's last delivery context for replies, but do not append routine cron turns to the human chat lane and do not extend daily/idle reset freshness for the target session. **Isolated** jobs run a dedicated agent turn with a fresh session. **Custom sessions** (`session:xxx`) persist context across runs, enabling workflows like daily standups that build on previous summaries. -Cron jobs can also carry payload-level `fallbacks`. When present, that list replaces the configured fallback chain for the job. Use `fallbacks: []` in the job payload/API when you want a strict cron run that tries only the selected model. If a job has `--model` but neither payload nor configured fallbacks, OpenClaw passes an explicit empty fallback override so the agent primary is not appended as a hidden extra retry target. + Main-session cron events are self-contained system-event reminders. They do not automatically include the default heartbeat prompt's "Read HEARTBEAT.md" instruction; say that explicitly in the cron event text if a reminder should consult `HEARTBEAT.md`. -Local-provider preflight checks walk configured fallbacks before marking a cron run `skipped`; `fallbacks: []` keeps that preflight path strict. + + + A new transcript/session id per run. OpenClaw carries safe preferences (thinking/fast/verbose settings, labels, explicit user-selected model/auth overrides), but does not inherit ambient conversation context from an older cron row: channel/group routing, send or queue policy, elevation, origin, or ACP runtime binding. Use `current` or `session:` when a recurring job should deliberately build on the same conversation context. + + + When isolated cron runs orchestrate subagents, delivery prefers the final descendant output over stale parent interim text. If descendants are still running, OpenClaw suppresses that partial parent update instead of announcing it. -Model-selection precedence for isolated jobs is: + For text-only Discord announce targets, OpenClaw sends the canonical final assistant text once instead of replaying both streamed/intermediate text and the final answer. Media and structured Discord payloads are still delivered separately so attachments and components are not dropped. -1. Gmail hook model override (when the run came from Gmail and that override is allowed) -2. Per-job payload `model` -3. User-selected stored cron session model override -4. Agent/default model selection - -Fast mode follows the resolved live selection too. If the selected model config has `params.fastMode`, isolated cron uses that by default. A stored session `fastMode` override still wins over config in either direction. Auto mode uses the selected model's `params.fastAutoOnSeconds` cutoff when present, defaulting to 60 seconds. - -If an isolated run hits a live model-switch handoff, cron retries with the switched provider/model and persists that live selection for the active run before retrying. When the switch also carries a new auth profile, cron persists that auth profile override for the active run too. Retries are bounded: after the initial attempt plus 2 switch retries, cron aborts instead of looping forever. - -Before an isolated cron run enters the agent runner, OpenClaw checks reachable local provider endpoints for configured `api: "ollama"` and `api: "openai-completions"` providers whose `baseUrl` is loopback, private-network, or `.local`. If that endpoint is down, the run is recorded as `skipped` with a clear provider/model error instead of starting a model call. The endpoint result is cached for 5 minutes, so many due jobs using the same dead local Ollama, vLLM, SGLang, or LM Studio server share one small probe instead of creating a request storm. Skipped provider-preflight runs do not increment execution-error backoff; enable `failureAlert.includeSkipped` when you want repeated skip notifications. + + ## Delivery and output @@ -206,38 +200,37 @@ Before an isolated cron run enters the agent runner, OpenClaw checks reachable l | `webhook` | POST finished event payload to a URL | | `none` | No runner fallback delivery | -Use `--announce --channel telegram --to "-1001234567890"` for channel delivery. For Telegram forum topics, use `-1001234567890:topic:123`; OpenClaw also accepts the Telegram-owned `-1001234567890:123` shorthand. Direct RPC/config callers may pass `delivery.threadId` as a string or number. Slack/Discord/Mattermost targets should use explicit prefixes (`channel:`, `user:`). Matrix room IDs are case-sensitive; use the exact room ID or `room:!room:server` form from Matrix. +Use `--announce --channel telegram --to "-1001234567890"` for channel delivery. For Telegram forum topics, use `-1001234567890:topic:123`; OpenClaw also accepts the Telegram-owned `-1001234567890:123` shorthand. Direct RPC/config callers may pass `delivery.threadId` as a string or number. Slack/Discord/Mattermost targets use explicit prefixes (`channel:`, `user:`). Matrix room IDs are case-sensitive; use the exact room ID or `room:!room:server` form from Matrix. -When announce delivery uses `channel: "last"` or omits `channel`, a provider-prefixed target such as `telegram:123` can select the channel before cron falls back to session history or a single configured channel. Only prefixes advertised by the loaded plugin are provider selectors. If `delivery.channel` is explicit, the target prefix must name the same provider; for example, `channel: "whatsapp"` with `to: "telegram:123"` is rejected instead of letting WhatsApp interpret the Telegram ID as a phone number. Target-kind and service prefixes such as `channel:`, `user:`, `imessage:`, and `sms:` remain channel-owned target syntax, not provider selectors. +When announce delivery uses `channel: "last"` or omits `channel`, a provider-prefixed target such as `telegram:123` can select the channel before cron falls back to session history or a single configured channel. Only prefixes advertised by the loaded plugin are provider selectors. If `delivery.channel` is explicit, the target prefix must name the same provider; `channel: "whatsapp"` with `to: "telegram:123"` is rejected instead of letting WhatsApp interpret the Telegram ID as a phone number. Target-kind and service prefixes (`channel:`, `user:`, `imessage:`, `sms:`) stay channel-owned target syntax, not provider selectors. -For isolated jobs, chat delivery is shared. If a chat route is available, the agent can use the `message` tool even when the job uses `--no-deliver`. If the agent sends to the configured/current target, OpenClaw skips the fallback announce. Otherwise `announce`, `webhook`, and `none` only control what the runner does with the final reply after the agent turn. +For isolated jobs, chat delivery is shared: if a chat route is available, the agent can use the `message` tool even with `--no-deliver`. If the agent sends to the configured/current target, OpenClaw skips the fallback announce. Otherwise `announce`, `webhook`, and `none` only control what the runner does with the final reply after the agent turn. When an agent creates an isolated reminder from an active chat, OpenClaw stores the preserved live delivery target for the fallback announce route. Internal session keys may be lowercase; provider delivery targets are not reconstructed from those keys when current chat context is available. Implicit announce delivery uses configured channel allowlists to validate and reroute stale targets. DM pairing-store approvals are not fallback automation recipients; set `delivery.to` or configure the channel `allowFrom` entry when a scheduled job should proactively send to a DM. -## Output language +### Failure notifications -Cron jobs do not infer a reply language from channel, locale, or previous -messages. Put the language rule in the scheduled message or template: +Failure notifications follow a separate destination path: + +- `cron.failureDestination` sets a global default for failure notifications. +- `job.delivery.failureDestination` overrides that per job. +- If neither is set and the job already delivers via `announce`, failure notifications fall back to that primary announce target. +- `delivery.failureDestination` is only supported on `sessionTarget="isolated"` jobs unless the primary delivery mode is `webhook`. +- `failureAlert.includeSkipped: true` opts a job or global cron alert policy into repeated skipped-run alerts. Skipped runs keep a separate consecutive-skip counter, so they do not affect execution-error backoff. +- `openclaw cron edit` exposes per-job alert tuning: `--failure-alert`/`--no-failure-alert`, `--failure-alert-after `, `--failure-alert-channel`, `--failure-alert-to`, `--failure-alert-cooldown`, `--failure-alert-include-skipped`/`--failure-alert-exclude-skipped`, `--failure-alert-mode`, and `--failure-alert-account-id`. + +### Output language + +Cron jobs do not infer a reply language from channel, locale, or previous messages. Put the language rule in the scheduled message or template: ```bash openclaw cron edit \ --message "Summarize the updates. Respond in Chinese; keep URLs, code, and product names unchanged." ``` -For template files, keep the language instruction in the rendered prompt and -verify placeholders such as `{{language}}` are filled before the job runs. If -the output mixes languages, make the rule explicit, for example: "Use Chinese -for narrative text and keep technical terms in English." - -Failure notifications follow a separate destination path: - -- `cron.failureDestination` sets a global default for failure notifications. -- `job.delivery.failureDestination` overrides that per job. -- If neither is set and the job already delivers via `announce`, failure notifications now fall back to that primary announce target. -- `delivery.failureDestination` is only supported on `sessionTarget="isolated"` jobs unless the primary delivery mode is `webhook`. -- `failureAlert.includeSkipped: true` opts a job or global cron alert policy into repeated skipped-run alerts. Skipped runs keep a separate consecutive skip counter, so they do not affect execution-error backoff. +For template files, keep the language instruction in the rendered prompt and verify placeholders such as `{{language}}` are filled before the job runs. If the output mixes languages, make the rule explicit, for example: "Use Chinese for narrative text and keep technical terms in English." ## CLI examples @@ -298,6 +291,68 @@ Failure notifications follow a separate destination path: +## Managing jobs + +```bash +# List all jobs +openclaw cron list + +# Get one stored job as JSON +openclaw cron get + +# Show one job, including resolved delivery route +openclaw cron show + +# Enable/disable without deleting +openclaw cron enable +openclaw cron disable + +# Edit a job +openclaw cron edit --message "Updated prompt" --model "opus" + +# Force run a job now +openclaw cron run + +# Force run a job now and wait for its terminal status +openclaw cron run --wait --wait-timeout 10m --poll-interval 2s + +# Run only if due +openclaw cron run --due + +# View run history +openclaw cron runs --id --limit 50 + +# View one exact run +openclaw cron runs --id --run-id + +# Delete a job +openclaw cron remove + +# Agent selection (multi-agent setups) +openclaw cron create "0 6 * * *" "Check ops queue" --name "Ops sweep" --session isolated --agent ops +openclaw cron edit --clear-agent +``` + +`openclaw cron run ` returns after enqueueing the manual run. Use `--wait` for shutdown hooks, maintenance scripts, or other automation that must block until the queued run finishes; it polls the returned `runId` (default timeout `10m`, poll interval `2s`) and exits `0` for status `ok`, non-zero for `error`, `skipped`, or a wait timeout. + +The agent `cron` tool returns compact job summaries (`id`, `name`, `enabled`, `nextRunAtMs`, `scheduleKind`, `lastRunStatus`) from `cron(action: "list")`; use `cron(action: "get", jobId: "...")` for one full job definition. Direct Gateway callers can pass `compact: true` to `cron.list`; omitting it preserves the full response with delivery previews. + +`openclaw cron create` is an alias for `openclaw cron add`. New jobs can use a positional schedule (`"0 9 * * 1"`, `"every 1h"`, `"20m"`, or an ISO timestamp) followed by a positional agent prompt. Use `--webhook ` on `cron add|create` or `cron edit` to POST the finished run payload to an HTTP endpoint; webhook delivery cannot combine with chat delivery flags (`--announce`, `--channel`, `--to`, `--thread-id`, `--account`). On `cron edit`, `--clear-channel`, `--clear-to`, `--clear-thread-id`, and `--clear-account` unset those routing fields individually (each rejected alongside its matching set flag) — distinct from `--no-deliver`, which only disables runner fallback delivery. + + +Model override note: + +- `openclaw cron add|edit --model ...` changes the job's selected model. +- If the model is allowed, that exact provider/model reaches the isolated agent run. +- If it is not allowed or cannot be resolved, cron fails the run with an explicit validation error. +- API `cron.update` payload patches can set `model: null` to clear a stored job model override. +- `openclaw cron edit --clear-model` clears that override from the CLI (same effect as the `model: null` patch) and cannot combine with `--model`. +- Configured fallback chains still apply because cron `--model` is a job primary, not a session `/model` override. +- `openclaw cron add|edit --fallbacks ...` sets payload `fallbacks`, replacing configured fallbacks for that job; `--fallbacks ""` disables fallback and makes the run strict. `openclaw cron edit --clear-fallbacks` clears the per-job override. +- A plain `--model` with no explicit or configured fallback list does not fall through to the agent primary as a silent extra retry target. + + + ## Webhooks Gateway can expose HTTP webhook endpoints for external triggers. Enable in config: @@ -347,19 +402,19 @@ Query-string tokens are rejected. curl -X POST http://127.0.0.1:18789/hooks/agent \ -H 'Authorization: Bearer SECRET' \ -H 'Content-Type: application/json' \ - -d '{"message":"Summarize inbox","name":"Email","model":"openai/gpt-5.4"}' + -d '{"message":"Summarize inbox","name":"Email","model":"openai/gpt-5.5"}' ``` - Fields: `message` (required), `name`, `agentId`, `wakeMode`, `deliver`, `channel`, `to`, `model`, `fallbacks`, `thinking`, `timeoutSeconds`. + Fields: `message` (required), `name`, `agentId`, `sessionKey` (requires `hooks.allowRequestSessionKey=true`), `idempotencyKey`, `wakeMode`, `deliver`, `channel`, `to`, `model`, `thinking`, `timeoutSeconds`. - Custom hook names are resolved via `hooks.mappings` in config. Mappings can transform arbitrary payloads into `wake` or `agent` actions with templates or code transforms. + Custom hook names resolve via `hooks.mappings` in config. Mappings can transform arbitrary payloads into `wake` or `agent` actions with templates or code transforms. -Keep hook endpoints behind loopback, tailnet, or trusted reverse proxy. +Keep hook endpoints behind loopback, tailnet, or a trusted reverse proxy. - Use a dedicated hook token; do not reuse gateway auth tokens. - Keep `hooks.path` on a dedicated subpath; `/` is rejected. @@ -384,7 +439,7 @@ Wire Gmail inbox triggers to OpenClaw via Google PubSub. openclaw webhooks gmail setup --account openclaw@gmail.com ``` -This writes `hooks.gmail` config, enables the Gmail preset, and uses Tailscale Funnel for the push endpoint. +This writes `hooks.gmail` config, enables the Gmail preset, and defaults to Tailscale Funnel for the push endpoint (`--tailscale funnel|serve|off`). ### Gateway auto-start @@ -434,64 +489,6 @@ When `hooks.enabled=true` and `hooks.gmail.account` is set, the Gateway starts ` } ``` -## Managing jobs - -```bash -# List all jobs -openclaw cron list - -# Get one stored job as JSON -openclaw cron get - -# Show one job, including resolved delivery route -openclaw cron show - -# Edit a job -openclaw cron edit --message "Updated prompt" --model "opus" - -# Force run a job now -openclaw cron run - -# Force run a job now and wait for its terminal status -openclaw cron run --wait --wait-timeout 10m --poll-interval 2s - -# Run only if due -openclaw cron run --due - -# View run history -openclaw cron runs --id --limit 50 - -# View one exact run -openclaw cron runs --id --run-id - -# Delete a job -openclaw cron remove - -# Agent selection (multi-agent setups) -openclaw cron create "0 6 * * *" "Check ops queue" --name "Ops sweep" --session isolated --agent ops -openclaw cron edit --clear-agent -``` - -`openclaw cron run ` returns after enqueueing the manual run. Use `--wait` for shutdown hooks, maintenance scripts, or other automation that must block until the queued run finishes. Wait mode polls the exact returned `runId`; it exits `0` for status `ok` and non-zero for `error`, `skipped`, or a wait timeout. - -The agent `cron` tool returns compact job summaries (`id`, `name`, `enabled`, `nextRunAtMs`, `scheduleKind`, `lastRunStatus`) from `cron(action: "list")`; use `cron(action: "get", jobId: "...")` for one full job definition. Direct Gateway callers can pass `compact: true` to `cron.list`; omitting it preserves the existing full response with delivery previews. - -`openclaw cron create` is an alias for `openclaw cron add`, and new jobs can use a positional schedule (`"0 9 * * 1"`, `"every 1h"`, `"20m"`, or an ISO timestamp) followed by a positional agent prompt. Use `--webhook ` on `cron add|create` or `cron edit` to POST the finished run payload to an HTTP endpoint. Webhook delivery cannot be combined with chat delivery flags such as `--announce`, `--channel`, `--to`, `--thread-id`, or `--account`. On `cron edit`, `--clear-channel`, `--clear-to`, `--clear-thread-id`, and `--clear-account` unset those routing fields individually (each rejected alongside its matching set flag), which is distinct from `--no-deliver` disabling runner fallback delivery. - - -Model override note: - -- `openclaw cron add|edit --model ...` changes the job's selected model. -- If the model is allowed, that exact provider/model reaches the isolated agent run. -- If it is not allowed or cannot be resolved, cron fails the run with an explicit validation error. -- API `cron.update` payload patches can set `model: null` to clear a stored job model override. -- `openclaw cron edit --clear-model` clears that override from the CLI (same effect as the `model: null` patch) and cannot be combined with `--model`. -- Configured fallback chains still apply because cron `--model` is a job primary, not a session `/model` override. -- `openclaw cron add|edit --fallbacks ...` sets payload `fallbacks`, replacing configured fallbacks for that job; `--fallbacks ""` disables fallback and makes the run strict. `openclaw cron edit --clear-fallbacks` clears the per-job override. -- A plain `--model` with no explicit or configured fallback list does not fall through to the agent primary as a silent extra retry target. - - - ## Configuration ```json5 @@ -502,8 +499,8 @@ Model override note: maxConcurrentRuns: 8, retry: { maxAttempts: 3, - backoffMs: [60000, 120000, 300000], - retryOn: ["rate_limit", "overloaded", "network", "server_error"], + backoffMs: [30000, 60000, 300000], + retryOn: ["rate_limit", "overloaded", "network", "timeout", "server_error"], }, webhookToken: "replace-with-dedicated-webhook-token", sessionRetention: "24h", @@ -512,21 +509,26 @@ Model override note: } ``` +The `retry` values above are the defaults: up to 3 retries with `30s/60s/5m` backoff, retrying all five transient categories. `webhookToken` is sent as `Authorization: Bearer ` on cron webhook POSTs. + `maxConcurrentRuns` limits both scheduled cron dispatch and isolated agent-turn execution, and defaults to 8. Isolated cron agent turns use the queue's dedicated `cron-nested` execution lane internally, so raising this value lets independent cron LLM runs progress in parallel instead of only starting their outer cron wrappers. The shared non-cron `nested` lane is not widened by this setting. -`cron.store` is a logical store key and legacy doctor import path. Run `openclaw doctor --fix` to import existing JSON stores into SQLite and archive them; future cron changes should go through the CLI or Gateway API. +`cron.store` is a logical store key and doctor migration path, not a live JSON file to hand-edit. Job data lives in SQLite; use the CLI or Gateway API for changes. Disable cron: `cron.enabled: false` or `OPENCLAW_SKIP_CRON=1`. - **One-shot retry**: transient errors (rate limit, overload, network, server error) retry up to 3 times with exponential backoff. Permanent errors disable immediately. + **One-shot retry**: transient errors (rate limit, overload, network, timeout, server error) retry up to `retry.maxAttempts` times (default 3) using `retry.backoffMs` (default 30s, 60s, 5m). Permanent errors disable the job immediately. - **Recurring retry**: exponential backoff (30s to 60m) between retries. Backoff resets after the next successful run. + **Recurring retry**: consecutive execution errors back off on an extended schedule (30s, 60s, 5m, 15m, 60m). Backoff resets after the next successful run. - `cron.sessionRetention` (default `24h`) prunes isolated run-session entries. `cron.runLog.keepLines` limits retained SQLite run-history rows per job; `maxBytes` is retained for config compatibility with older file-backed run logs. + `cron.sessionRetention` (default `24h`, `false` disables) prunes isolated run-session entries. `cron.runLog.keepLines` limits retained SQLite run-history rows per job; `maxBytes` is retained for config compatibility with older file-backed run logs. + + + On upgrade, run `openclaw doctor --fix` to import legacy `~/.openclaw/cron/jobs.json`, `jobs-state.json`, and `runs/*.jsonl` files into SQLite and rename them with a `.migrated` suffix. Malformed job rows are skipped from runtime and copied to `jobs-quarantine.json` for later repair or review. @@ -547,10 +549,10 @@ openclaw doctor - - Check `cron.enabled` and `OPENCLAW_SKIP_CRON` env var. + - Check `cron.enabled` and the `OPENCLAW_SKIP_CRON` env var. - Confirm the Gateway is running continuously. - For `cron` schedules, verify timezone (`--tz`) vs the host timezone. - - `reason: not-due` in run output means manual run was checked with `openclaw cron run --due` and the job was not due yet. + - `reason: not-due` in run output means the manual run was checked with `openclaw cron run --due` and the job was not due yet. @@ -558,7 +560,7 @@ openclaw doctor - Delivery target missing/invalid (`channel`/`to`) means outbound was skipped. - For Matrix, copied or legacy jobs with lowercased `delivery.to` room IDs can fail because Matrix room IDs are case-sensitive. Edit the job to the exact `!room:server` or `room:!room:server` value from Matrix. - Channel auth errors (`unauthorized`, `Forbidden`) mean delivery was blocked by credentials. - - If the isolated run returns only the silent token (`NO_REPLY` / `no_reply`), OpenClaw suppresses direct outbound delivery and also suppresses the fallback queued summary path, so nothing is posted back to chat. + - If the isolated run returns only the silent token (`NO_REPLY` / `no_reply`), OpenClaw suppresses direct outbound delivery and the fallback queued-summary path, so nothing is posted back to chat. - If the agent should message the user itself, check that the job has a usable route (`channel: "last"` with a previous chat, or an explicit channel/target). diff --git a/docs/automation/cron-vs-heartbeat.md b/docs/automation/cron-vs-heartbeat.md index 4f2ebf54faab..e5566fea0727 100644 --- a/docs/automation/cron-vs-heartbeat.md +++ b/docs/automation/cron-vs-heartbeat.md @@ -3,9 +3,10 @@ summary: "Redirect to /automation" title: "Cron vs heartbeat" --- -The decision guide for cron vs heartbeat lives under [Automation](/automation). +This page moved. See [Scheduled Tasks (Cron) vs Heartbeat](/automation#scheduled-tasks-cron-vs-heartbeat) for the decision table. ## Related - [Scheduled tasks](/automation/cron-jobs) +- [Heartbeat](/gateway/heartbeat) - [Background tasks](/automation/tasks) diff --git a/docs/automation/gmail-pubsub.md b/docs/automation/gmail-pubsub.md index 1b532074e3f5..947aca35330a 100644 --- a/docs/automation/gmail-pubsub.md +++ b/docs/automation/gmail-pubsub.md @@ -3,9 +3,9 @@ summary: "Redirect to /automation/cron-jobs" title: "Gmail PubSub" --- -This page moved to [Scheduled Tasks](/automation/cron-jobs#gmail-pubsub-integration). See [Scheduled Tasks](/automation/cron-jobs#gmail-pubsub-integration) for Gmail PubSub documentation. +This page moved to [Gmail PubSub integration](/automation/cron-jobs#gmail-pubsub-integration) on the Scheduled Tasks page. ## Related -- [Webhook](/automation/webhook) -- [Automation troubleshooting](/automation/troubleshooting) +- [Webhooks](/automation/cron-jobs#webhooks) +- [Troubleshooting](/automation/cron-jobs#troubleshooting) diff --git a/docs/automation/hooks.md b/docs/automation/hooks.md index fc1230176876..33123b450074 100644 --- a/docs/automation/hooks.md +++ b/docs/automation/hooks.md @@ -6,14 +6,14 @@ read_when: title: "Hooks" --- -Hooks are small scripts that run when something happens inside the Gateway. They can be discovered from directories and inspected with `openclaw hooks`. The Gateway loads internal hooks only after you enable hooks or configure at least one hook entry, hook pack, legacy handler, or extra hook directory. +Hooks are small scripts that run inside the Gateway when agent events fire: commands like `/new`, `/reset`, `/stop`, session compaction, gateway lifecycle, and message flow. They are discovered from directories and managed with `openclaw hooks`. The Gateway loads internal hooks only after you enable hooks or configure at least one hook entry, hook pack, legacy handler, or extra hook directory. There are two kinds of hooks in OpenClaw: -- **Internal hooks** (this page): run inside the Gateway when agent events fire, like `/new`, `/reset`, `/stop`, or lifecycle events. +- **Internal hooks** (this page): run inside the Gateway when agent events fire. - **Webhooks**: external HTTP endpoints that let other systems trigger work in OpenClaw. See [Webhooks](/automation/cron-jobs#webhooks). -Hooks can also be bundled inside plugins. `openclaw hooks list` shows both standalone hooks and plugin-managed hooks. +Hooks can also be bundled inside plugins. `openclaw hooks list` shows both standalone hooks and plugin-managed hooks (displayed as `plugin:`). ## Choose the right surface @@ -61,7 +61,7 @@ openclaw hooks info session-memory | `message:received` | Inbound message from any channel | | `message:transcribed` | After audio transcription completes | | `message:preprocessed` | After media and link preprocessing completes or is skipped | -| `message:sent` | Outbound message delivered | +| `message:sent` | Outbound send attempted (`context.success` has the result) | ## Writing hooks @@ -69,12 +69,14 @@ openclaw hooks info session-memory Each hook is a directory containing two files: -``` +```text my-hook/ ├── HOOK.md # Metadata + documentation └── handler.ts # Handler implementation ``` +The handler file can be `handler.ts`, `handler.js`, `index.ts`, or `index.js`. + ### HOOK.md format ```markdown @@ -100,6 +102,8 @@ Detailed documentation goes here. | `os` | Required platforms (e.g., `["darwin", "linux"]`) | | `requires` | Required `bins`, `anyBins`, `env`, or `config` paths | | `always` | Bypass eligibility checks (boolean) | +| `hookKey` | Config key override (defaults to the hook name) | +| `homepage` | Docs URL shown by `openclaw hooks info` | | `install` | Installation methods | ### Handler implementation @@ -120,20 +124,24 @@ const handler = async (event) => { export default handler; ``` -Each event includes: `type`, `action`, `sessionKey`, `timestamp`, `messages` (push replies here on replyable surfaces only), and `context` (event-specific data). Agent and tool plugin hook contexts can also include `trace`, a read-only W3C-compatible diagnostic trace context that plugins may pass into structured logs for OTEL correlation. +Each event includes: `type`, `action`, `sessionKey`, `timestamp`, `messages`, and `context` (event-specific data). Typed plugin hook contexts for agent and tool hooks can also include `trace`, a read-only W3C-compatible diagnostic trace context that plugins may pass into structured logs for OTEL correlation. -`event.messages` is only delivered automatically on replyable surfaces such as -`command:*` and `message:received`. Lifecycle-only events such as -`agent:bootstrap`, `session:*`, `gateway:*`, or `message:sent` do not have a -reply channel and ignore pushed messages. +Strings pushed to `event.messages` are delivered back to the chat only for +`command:new` and `command:reset` (routed as a reply to the originating +conversation) and for `session:compact:before` / `session:compact:after` +(sent as compaction status notices). All other events, including +`command:stop`, `message:*`, `agent:bootstrap`, `session:patch`, and +`gateway:*`, ignore pushed messages. ### Event context highlights -**Command events** (`command:new`, `command:reset`): `context.sessionEntry`, `context.previousSessionEntry`, `context.commandSource`, `context.workspaceDir`, `context.cfg`. +**Command events** (`command:new`, `command:reset`): `context.sessionEntry`, `context.previousSessionEntry`, `context.commandSource`, `context.senderId`, `context.workspaceDir`, `context.cfg`. + +**Command events** (`command:stop`): `context.sessionEntry`, `context.sessionId`, `context.commandSource`, `context.senderId`. **Message events** (`message:received`): `context.from`, `context.content`, `context.channelId`, `context.metadata` (provider-specific data including `senderId`, `senderName`, `guildId`). `context.content` prefers a nonblank command body for command-like messages, then falls back to the raw inbound body and generic body; it does not include agent-only enrichment such as thread history or link summaries. -**Message events** (`message:sent`): `context.to`, `context.content`, `context.success`, `context.channelId`. +**Message events** (`message:sent`): `context.to`, `context.content`, `context.success`, `context.channelId`, plus `context.error` when sending failed. **Message events** (`message:transcribed`): `context.transcript`, `context.from`, `context.channelId`, `context.mediaPath`. @@ -141,7 +149,7 @@ reply channel and ignore pushed messages. **Bootstrap events** (`agent:bootstrap`): `context.bootstrapFiles` (mutable array), `context.agentId`. -**Session patch events** (`session:patch`): `context.sessionEntry`, `context.patch` (only changed fields), `context.cfg`. Only privileged clients can trigger patch events. +**Session patch events** (`session:patch`): `context.sessionEntry`, `context.patch` (only changed fields), `context.cfg`. Only privileged clients can trigger patch events; the context is a clone, so handlers cannot mutate the live session entry. **Compaction events**: `session:compact:before` includes `messageCount`, `tokenCount`. `session:compact:after` adds `compactedCount`, `summaryLength`, `tokensBefore`, `tokensAfter`. @@ -181,11 +189,11 @@ Between the `gateway:shutdown` (or `gateway:pre-restart`) event and the rest of ## Hook discovery -Hooks are discovered from these directories, in order of increasing override precedence: +Hooks are discovered from four sources: 1. **Bundled hooks**: shipped with OpenClaw -2. **Plugin hooks**: hooks bundled inside installed plugins -3. **Managed hooks**: `~/.openclaw/hooks/` (user-installed, shared across workspaces). Extra directories from `hooks.internal.load.extraDirs` share this precedence. +2. **Plugin hooks**: bundled inside installed plugins; can override bundled hooks with the same name +3. **Managed hooks**: `~/.openclaw/hooks/` (user-installed, shared across workspaces); can override bundled and plugin hooks. Extra directories from `hooks.internal.load.extraDirs` share this precedence. 4. **Workspace hooks**: `/hooks/` (per-agent, disabled by default until explicitly enabled) Workspace hooks can add new hook names but cannot override bundled, managed, or plugin-provided hooks with the same name. @@ -200,7 +208,7 @@ Hook packs are npm packages that export hooks via `openclaw.hooks` in `package.j openclaw plugins install ``` -Npm specs are registry-only (package name + optional exact version or dist-tag). Git/URL/file specs and semver ranges are rejected. +Npm specs are registry-only (package name + optional exact version or dist-tag). Git/URL/file specs and semver ranges are rejected. The older `openclaw hooks install` and `openclaw hooks update` commands are deprecated aliases for `openclaw plugins install` / `openclaw plugins update`. ## Bundled hooks @@ -222,7 +230,7 @@ openclaw hooks enable ### session-memory details -Extracts the last 15 user/assistant messages and saves to `/memory/YYYY-MM-DD-HHMM.md` using the host local date. Memory capture runs in the background so `/new` and `/reset` acknowledgements are not delayed by transcript reads or optional slug generation. Set `hooks.internal.entries.session-memory.llmSlug: true` to generate descriptive filename slugs with the configured model. Requires `workspace.dir` to be configured. +Extracts the last user/assistant messages (default 15, configurable with `hooks.internal.entries.session-memory.messages`) and saves them to `/memory/YYYY-MM-DD-HHMM.md` using the host local date. Memory capture runs in the background so `/new` and `/reset` acknowledgements are not delayed by transcript reads or optional slug generation. Set `hooks.internal.entries.session-memory.llmSlug: true` to generate descriptive filename slugs with the configured model (falls back to timestamp slugs when unavailable). Requires `workspace.dir` to be configured. @@ -243,13 +251,13 @@ Extracts the last 15 user/assistant messages and saves to `/memory/YY } ``` -Paths resolve relative to workspace. Only recognized bootstrap basenames are loaded (`AGENTS.md`, `SOUL.md`, `TOOLS.md`, `IDENTITY.md`, `USER.md`, `HEARTBEAT.md`, `BOOTSTRAP.md`, `MEMORY.md`). +`patterns` and `files` are accepted as aliases of `paths`. Paths resolve relative to the workspace and must stay inside it. Only recognized bootstrap basenames are loaded (`AGENTS.md`, `SOUL.md`, `TOOLS.md`, `IDENTITY.md`, `USER.md`, `HEARTBEAT.md`, `BOOTSTRAP.md`, `MEMORY.md`). ### command-logger details -Logs every slash command to `~/.openclaw/logs/commands.log`. +Logs every slash command as a JSON line (timestamp, action, session key, sender ID, source) to `~/.openclaw/logs/commands.log`. @@ -261,7 +269,7 @@ Sends short status messages into the current conversation when OpenClaw starts a ### boot-md details -Runs `BOOT.md` from the active workspace when the gateway starts. +Runs `BOOT.md` at gateway startup for each configured agent scope, if the file exists in that agent's resolved workspace. ## Plugin hooks @@ -293,7 +301,7 @@ For the complete plugin hook reference, see [Plugin hooks](/plugins/hooks). } ``` -Per-hook environment variables: +Per-hook environment values satisfy a hook's `requires.env` eligibility checks (alongside the process environment), and handlers can read them from their hook config entry: ```json { @@ -377,7 +385,7 @@ Check for missing binaries (PATH), environment variables, config values, or OS c 1. Verify the hook is enabled: `openclaw hooks list` 2. Restart your gateway process so hooks reload. -3. Check gateway logs: `./scripts/clawlog.sh | grep hook` +3. Check gateway logs: `openclaw logs --follow | grep -i hook` ## Related diff --git a/docs/automation/index.md b/docs/automation/index.md index a255afed2053..1899e1490b0b 100644 --- a/docs/automation/index.md +++ b/docs/automation/index.md @@ -9,8 +9,8 @@ title: "Automation" --- OpenClaw runs work in the background through tasks, scheduled jobs, inferred -commitments, event hooks, and standing instructions. This page helps you choose -the right mechanism and understand how they fit together. +commitments, event hooks, and standing instructions. Use this page to pick the +right mechanism. ## Quick decision guide @@ -101,8 +101,8 @@ See [Standing Orders](/automation/standing-orders). Internal hooks are event-driven scripts triggered by agent lifecycle events (`/new`, `/reset`, `/stop`), session compaction, gateway startup, and message -flow. They are automatically discovered from directories and can be managed -with `openclaw hooks`. For in-process tool-call interception, use +flow. They are discovered from hook directories and managed with +`openclaw hooks`. For in-process tool-call interception, use [Plugin hooks](/plugins/hooks). See [Hooks](/automation/hooks). diff --git a/docs/automation/poll.md b/docs/automation/poll.md index fd52ff04e6e1..d35bf8a44350 100644 --- a/docs/automation/poll.md +++ b/docs/automation/poll.md @@ -3,10 +3,10 @@ summary: "Redirect to /cli/message" title: "Polls" --- -This page moved to [Message tool](/cli/message). See [Message tool](/cli/message) for poll documentation. +This page moved. Poll documentation, including `openclaw message poll` flags and per-channel limits, lives in [Message tool](/cli/message). ## Related -- [Webhook](/automation/webhook) +- [Webhooks](/automation/cron-jobs#webhooks) - [Scheduled tasks](/automation/cron-jobs) - [Background tasks](/automation/tasks) diff --git a/docs/automation/standing-orders.md b/docs/automation/standing-orders.md index 46c9bf2916a5..ac4e3616945a 100644 --- a/docs/automation/standing-orders.md +++ b/docs/automation/standing-orders.md @@ -7,25 +7,13 @@ read_when: title: "Standing orders" --- -Standing orders grant your agent **permanent operating authority** for defined programs. Instead of giving individual task instructions each time, you define programs with clear scope, triggers, and escalation rules - and the agent executes autonomously within those boundaries. - -This is the difference between telling your assistant "send the weekly report" every Friday vs. granting standing authority: "You own the weekly report. Compile it every Friday, send it, and only escalate if something looks wrong." +Standing orders grant your agent **permanent operating authority** for defined programs. Instead of prompting the agent for each task, you define programs with clear scope, triggers, and escalation rules, and the agent executes autonomously within those boundaries: "You own the weekly report. Compile it every Friday, send it, and only escalate if something looks wrong." ## Why standing orders -**Without standing orders:** +**Without standing orders:** you prompt the agent for every task, routine work gets forgotten or delayed, and you become the bottleneck. -- You must prompt the agent for every task -- The agent sits idle between requests -- Routine work gets forgotten or delayed -- You become the bottleneck - -**With standing orders:** - -- The agent executes autonomously within defined boundaries -- Routine work happens on schedule without prompting -- You only get involved for exceptions and approvals -- The agent fills idle time productively +**With standing orders:** the agent executes autonomously within defined boundaries, routine work happens on schedule, and you only get involved for exceptions and approvals. ## How they work @@ -73,7 +61,7 @@ Put standing orders in `AGENTS.md` to guarantee they're loaded every session. Th Standing orders define **what** the agent is authorized to do. [Cron jobs](/automation/cron-jobs) define **when** it happens. They work together: -``` +```text Standing Order: "You own the daily inbox triage" ↓ Cron Job (8 AM daily): "Execute inbox triage per standing orders" diff --git a/docs/automation/taskflow.md b/docs/automation/taskflow.md index 882f04ff00f7..f9e9a00662ea 100644 --- a/docs/automation/taskflow.md +++ b/docs/automation/taskflow.md @@ -7,18 +7,82 @@ read_when: title: "Task flow" --- -Task Flow is the flow orchestration substrate that sits above [background tasks](/automation/tasks). It manages durable multi-step flows with their own state, revision tracking, and sync semantics while individual tasks remain the unit of detached work. +Task Flow is the orchestration layer above [background tasks](/automation/tasks). A flow is a durable record of multi-step work with its own status, JSON state, revision counter, and linked task records. Flows survive gateway restarts; individual tasks remain the unit of detached work. ## When to use Task Flow -Use Task Flow when work spans multiple sequential or branching steps and you need durable progress tracking across gateway restarts. For single background operations, a plain [task](/automation/tasks) is sufficient. +| Scenario | Use | +| ----------------------------------------- | ------------------------------------------- | +| Single background job | Plain task | +| Multi-step pipeline driven by plugin code | Task Flow (managed) | +| Detached ACP or subagent spawn | Task Flow (mirrored, created automatically) | +| One-shot reminder | Cron job | -| Scenario | Use | -| ------------------------------------- | -------------------- | -| Single background job | Plain task | -| Multi-step pipeline (A then B then C) | Task Flow (managed) | -| Observe externally created tasks | Task Flow (mirrored) | -| One-shot reminder | Cron job | +## Sync modes + +### Managed mode + +A managed flow has a controller: plugin code that creates the flow through the plugin runtime Task Flow API with a goal and a required controller id, then drives it explicitly. + +- Each step runs as a background task created under the flow; the flow's owner key and requester origin carry over to child tasks. +- The controller advances the flow between `running`, `waiting`, and terminal states, and stores arbitrary JSON step state on the flow record. +- Every mutation passes the flow's expected revision. A stale write is rejected as a revision conflict instead of clobbering newer state. +- Once cancellation is requested, new child tasks are refused, and the flow finalizes as `cancelled` when no child task remains active. + +Example: a weekly report flow that (1) gathers data, (2) generates the report, and (3) delivers it, one background task per step: + +``` +Flow: weekly-report + Step 1: gather-data → task created → succeeded + Step 2: generate-report → task created → succeeded + Step 3: deliver → task created → running +``` + +### Mirrored mode + +OpenClaw creates a mirrored one-task flow automatically when a detached ACP or subagent run starts (session-scoped tasks with deliverable completion). The flow record mirrors its single backing task - status, goal, and timing - so detached spawns get a stable flow handle for status and retry surfaces without a controller. Mirrored flows show sync mode `task_mirrored` in the CLI. + +## Flow statuses + +| Status | Meaning | +| ----------- | -------------------------------------------------------------------------- | +| `queued` | Created, not yet progressing | +| `running` | Flow is actively progressing | +| `waiting` | Managed flow is parked on wait metadata (timer, external event) | +| `blocked` | A step finished without a usable result; `blockedTaskId`/summary say which | +| `succeeded` | Completed successfully | +| `failed` | Completed with an error | +| `cancelled` | Cancel requested and all child tasks settled | +| `lost` | Flow lost its authoritative backing state | + +## Durable state and revision tracking + +Flow records persist in the shared SQLite state database (`~/.openclaw/state/openclaw.sqlite`, `flow_runs` table) alongside task records, so progress survives gateway restarts. Each write bumps the flow's `revision`; concurrent writers that pass a stale expected revision get a conflict and must re-read. WAL growth is bounded by SQLite autocheckpointing plus periodic passive checkpoints, with truncate checkpoints on shutdown. The legacy `flows/registry.sqlite` sidecar from older installs is imported by `openclaw doctor`. + +## Cancel behavior + +`openclaw tasks flow cancel` sets a sticky cancel intent on the flow, cancels its active child tasks, and refuses new managed child tasks. Once no child task remains active, the flow finalizes as `cancelled` - immediately, or via the maintenance sweep if children take longer to settle. The intent is persisted, so a cancelled flow stays cancelled even if the gateway restarts before all child tasks have terminated. + +## CLI commands + +```bash +# List active and recent flows +openclaw tasks flow list [--status ] [--json] + +# Show details for a specific flow +openclaw tasks flow show [--json] + +# Cancel a running flow and its active tasks +openclaw tasks flow cancel +``` + +| Command | Description | +| --------------------------------- | ----------------------------------------------------------------------- | +| `openclaw tasks flow list` | Tracked flows with sync mode, status, revision, controller, task counts | +| `openclaw tasks flow show ` | Inspect one flow by flow id or owner key, including linked tasks | +| `openclaw tasks flow cancel ` | Cancel a running flow and its active tasks | + +Flows are also covered by `openclaw tasks audit` (stale or broken flow findings) and `openclaw tasks maintenance` (finalizes stuck cancels, prunes terminal flows after 7 days). ## Reliable scheduled workflow pattern @@ -43,7 +107,7 @@ openclaw cron add \ --to "channel:C1234567890" ``` -Use `session:` instead of `isolated` when the recurring workflow needs deliberate history, previous run summaries, or standing context. Use `isolated` when each run should start fresh and all required state is explicit in the workflow. +Use `--session session:` instead of `isolated` when the recurring workflow needs deliberate history, previous run summaries, or standing context. Use `isolated` when each run should start fresh and all required state is explicit in the workflow. Inside the workflow, put reliability checks before the LLM summary step: @@ -92,64 +156,13 @@ Have the workflow reject or mark stale items before summarization. The LLM step For reusable team or community workflows, package the CLI, `.lobster` files, and any setup notes as a skill or plugin and publish it through [ClawHub](/clawhub). Keep workflow-specific guardrails in that package unless the plugin API is missing a needed generic capability. -## Sync modes - -### Managed mode - -Task Flow owns the lifecycle end-to-end. It creates tasks as flow steps, drives them to completion, and advances the flow state automatically. - -Example: a weekly report flow that (1) gathers data, (2) generates the report, and (3) delivers it. Task Flow creates each step as a background task, waits for completion, then moves to the next step. - -``` -Flow: weekly-report - Step 1: gather-data → task created → succeeded - Step 2: generate-report → task created → succeeded - Step 3: deliver → task created → running -``` - -### Mirrored mode - -Task Flow observes externally created tasks and keeps flow state in sync without taking ownership of task creation. This is useful when tasks originate from cron jobs, CLI commands, or other sources and you want a unified view of their progress as a flow. - -Example: three independent cron jobs that together form a "morning ops" routine. A mirrored flow tracks their collective progress without controlling when or how they run. - -## Durable state and revision tracking - -Each flow persists its own state and tracks revisions so progress survives gateway restarts. Revision tracking enables conflict detection when multiple sources attempt to advance the same flow concurrently. -The flow registry uses SQLite with bounded write-ahead-log maintenance, including -periodic and shutdown checkpoints, so long-running gateways do not retain -unbounded `registry.sqlite-wal` sidecar files. - -## Cancel behavior - -`openclaw tasks flow cancel` sets a sticky cancel intent on the flow. Active tasks within the flow are cancelled, and no new steps are started. The cancel intent persists across restarts, so a cancelled flow stays cancelled even if the gateway restarts before all child tasks have terminated. - -## CLI commands - -```bash -# List active and recent flows -openclaw tasks flow list - -# Show details for a specific flow -openclaw tasks flow show - -# Cancel a running flow and its active tasks -openclaw tasks flow cancel -``` - -| Command | Description | -| --------------------------------- | --------------------------------------------- | -| `openclaw tasks flow list` | Shows tracked flows with status and sync mode | -| `openclaw tasks flow show ` | Inspect one flow by flow id or lookup key | -| `openclaw tasks flow cancel ` | Cancel a running flow and its active tasks | - ## How flows relate to tasks Flows coordinate tasks, not replace them. A single flow may drive multiple background tasks over its lifetime. Use `openclaw tasks` to inspect individual task records and `openclaw tasks flow` to inspect the orchestrating flow. ## Related -- [Background Tasks](/automation/tasks) — the detached work ledger that flows coordinate -- [CLI: tasks](/cli/tasks) — CLI command reference for `openclaw tasks flow` -- [Automation Overview](/automation) — all automation mechanisms at a glance -- [Cron Jobs](/automation/cron-jobs) — scheduled jobs that may feed into flows +- [Background Tasks](/automation/tasks) - the detached work ledger that flows coordinate +- [CLI: tasks](/cli/tasks) - CLI command reference for `openclaw tasks flow` +- [Automation Overview](/automation) - all automation mechanisms at a glance +- [Cron Jobs](/automation/cron-jobs) - scheduled jobs that may feed into flows diff --git a/docs/automation/tasks.md b/docs/automation/tasks.md index d4d6d42524a9..a9078f616f68 100644 --- a/docs/automation/tasks.md +++ b/docs/automation/tasks.md @@ -1,5 +1,5 @@ --- -summary: "Background task tracking for ACP runs, subagents, isolated cron jobs, and CLI operations" +summary: "Background task tracking for ACP runs, subagents, cron executions, and CLI operations" read_when: - Inspecting background work in progress or recently completed - Debugging delivery failures for detached agent runs @@ -12,12 +12,12 @@ sidebarTitle: "Background tasks" Looking for scheduling? See [Automation](/automation) for choosing the right mechanism. This page is the activity ledger for background work, not the scheduler. -Background tasks track work that runs **outside your main conversation session**: ACP runs, subagent spawns, isolated cron job executions, and CLI-initiated operations. +Background tasks track work that runs **outside your main conversation session**: ACP runs, subagent spawns, cron job executions, and CLI-initiated operations. Tasks do **not** replace sessions, cron jobs, or heartbeats - they are the **activity ledger** that records what detached work happened, when, and whether it succeeded. -Not every agent run creates a task. Heartbeat turns and normal interactive chat do not. All cron executions, ACP spawns, subagent spawns, and CLI agent commands do. +Not every agent run creates a task. Heartbeat turns and normal interactive chat do not. All cron executions, ACP spawns, subagent spawns, and gateway-dispatched CLI agent commands do. ## TL;DR @@ -25,17 +25,13 @@ Not every agent run creates a task. Heartbeat turns and normal interactive chat - Tasks are **records**, not schedulers - cron and heartbeat decide _when_ work runs, tasks track _what happened_. - ACP, subagents, all cron jobs, and CLI operations create tasks. Heartbeat turns do not. - Each task moves through `queued → running → terminal` (succeeded, failed, timed_out, cancelled, or lost). -- Cron tasks stay live while the cron runtime still owns the job; if the - in-memory runtime state is gone, task maintenance first checks durable cron - run history before marking a task lost. -- Completion is push-driven: detached work can notify directly or wake the - requester session/heartbeat when it finishes, so status polling loops are - usually the wrong shape. +- Cron tasks stay live while the cron runtime still owns the job; if the in-memory runtime state is gone, task maintenance first checks durable cron run history before marking a task lost. +- Completion is push-driven: detached work can notify directly or wake the requester session/heartbeat when it finishes, so status polling loops are usually the wrong shape. - Isolated cron runs and subagent completions best-effort clean up tracked browser tabs/processes for their child session before final cleanup bookkeeping. - Isolated cron delivery suppresses stale interim parent replies while descendant subagent work is still draining, and it prefers final descendant output when that arrives before delivery. - Completion notifications are delivered directly to a channel or queued for the next heartbeat. - `openclaw tasks list` shows all tasks; `openclaw tasks audit` surfaces issues. -- Terminal records are kept for 7 days, then automatically pruned. +- Terminal records are kept for 7 days (`lost` records for 24 hours), then automatically pruned. ## Quick start @@ -53,7 +49,7 @@ Not every agent run creates a task. Heartbeat turns and normal interactive chat ```bash - # Show details for a specific task (by ID, run ID, or session key) + # Show details for a specific task (by task ID, run ID, or session key) openclaw tasks show ``` @@ -100,13 +96,13 @@ Not every agent run creates a task. Heartbeat turns and normal interactive chat - Main-session cron tasks use `silent` notify policy by default - they create records for tracking but do not generate notifications. Isolated cron tasks also default to `silent` but are more visible because they run in their own session. + Cron tasks (main-session and isolated) use `silent` notify policy - they create records for tracking but do not generate task notifications of their own; cron owns its delivery path. Session-backed `image_generate`, `music_generate`, and `video_generate` runs also use `silent` notify policy. They still create task records, but completion is handed back to the original agent session as an internal wake so the agent can write the follow-up message and attach the finished media itself. The requester agent follows its normal visible-reply contract: automatic final reply when configured, or `message(action="send")` plus `NO_REPLY` when the session requires message-tool replies. If the requester session is no longer active or its active wake fails, and the completion agent misses some or all generated media, OpenClaw sends an idempotent direct fallback with only the missing media to the original channel target. - While a session-backed media-generation task is still active, media tools also act as guardrails for accidental retries. Repeated `image_generate` calls for the same prompt return the matching active task status, while a distinct image prompt can start its own task. `music_generate` and `video_generate` calls still return the active task status for that session instead of starting a second concurrent generation. Use `action: "status"` when you want an explicit progress/status lookup from the agent side. + While a session-backed media-generation task is still active, `image_generate`, `music_generate`, and `video_generate` guard against accidental retries: repeating the call for the same prompt/request returns the matching active task status instead of starting a duplicate, while a distinct prompt can start its own task. Use `action: "status"` when you want an explicit progress/status lookup from the agent side. - Heartbeat turns - main-session; see [Heartbeat](/gateway/heartbeat) @@ -125,49 +121,43 @@ stateDiagram-v2 running --> succeeded : completes ok running --> failed : error running --> timed_out : timeout exceeded + queued --> cancelled : operator cancels running --> cancelled : operator cancels - queued --> lost : session gone > 5 min - running --> lost : session gone > 5 min + queued --> lost : backing state gone > 5 min + running --> lost : backing state gone > 5 min ``` -| Status | What it means | -| ----------- | -------------------------------------------------------------------------- | -| `queued` | Created, waiting for the agent to start | -| `running` | Agent turn is actively executing | -| `succeeded` | Completed successfully | -| `failed` | Completed with an error | -| `timed_out` | Exceeded the configured timeout | -| `cancelled` | Stopped by the operator via `openclaw tasks cancel` | -| `lost` | The runtime lost authoritative backing state after a 5-minute grace period | +| Status | What it means | +| ----------- | --------------------------------------------------------------------------- | +| `queued` | Created, waiting for the agent to start | +| `running` | Agent turn is actively executing | +| `succeeded` | Completed successfully | +| `failed` | Completed with an error | +| `timed_out` | Exceeded the configured timeout | +| `cancelled` | Stopped by the operator via `openclaw tasks cancel`, or the run was aborted | +| `lost` | The runtime lost authoritative backing state after a 5-minute grace period | -Transitions happen automatically - when the associated agent run ends, the task status updates to match. +Transitions happen automatically - agent run lifecycle events (start, end, error) update the task status; you do not manage it manually. -Agent run completion is authoritative for active task records. A successful detached run finalizes as `succeeded`, ordinary run errors finalize as `failed`, and timeout or abort outcomes finalize as `timed_out`. If an operator already cancelled the task, or the runtime already recorded a stronger terminal state such as `failed`, `timed_out`, or `lost`, a later success signal does not downgrade that terminal status. +Agent run completion is authoritative for active task records. A successful detached run finalizes as `succeeded`, ordinary run errors finalize as `failed`, timeouts finalize as `timed_out`, and cancel/abort outcomes finalize as `cancelled`. Once a task is terminal, later lifecycle signals do not downgrade it - an operator-cancelled or already-`failed`/`timed_out`/`lost` task stays that way even if a success signal arrives afterwards. `lost` is runtime-aware: -- ACP tasks: backing ACP child session metadata disappeared. -- Subagent tasks: backing child session disappeared from the target agent store. -- Cron tasks: the cron runtime no longer tracks the job as active and durable - cron run history does not show a terminal result for that run. Offline CLI - audit does not treat its own empty in-process cron runtime state as authority. -- CLI tasks: tasks with a run id/source id use the live run context, so - lingering child-session or chat-session rows do not keep them alive after the - gateway-owned run disappears. Legacy CLI tasks without run identity still fall - back to the child session. Gateway-backed `openclaw agent` runs also finalize - from their run result, so completed runs do not sit active until the sweeper - marks them `lost`. +- ACP tasks: only a live in-process ACP turn in the Gateway proves the run is alive; persisted session metadata alone does not. Offline CLI audit stays conservative and never reclaims ACP tasks. +- Subagent tasks: backing child session disappeared from the target agent store (or carries a restart-recovery tombstone). +- Cron tasks: the cron runtime no longer tracks the job as active and durable cron run history does not show a terminal result for that run. Offline CLI audit does not treat its own empty in-process cron runtime state as authority. +- CLI tasks: tasks with a run id/source id use the live run context, so lingering child-session or chat-session rows do not keep them alive after the gateway-owned run disappears. Legacy CLI tasks without run identity still fall back to the child session. Gateway-backed `openclaw agent` runs also finalize from their run result, so completed runs do not sit active until the sweeper marks them `lost`. ## Delivery and notifications When a task reaches a terminal state, OpenClaw notifies you. There are two delivery paths: -**Direct delivery** - if the task has a channel target (the `requesterOrigin`), the completion message goes straight to that channel (Telegram, Discord, Slack, etc.). Group and channel task completions are instead routed through the requester session so the parent agent can write the visible reply. For subagent completions, OpenClaw also preserves bound thread/topic routing when available and can fill a missing `to` / account from the requester session's stored route (`lastChannel` / `lastTo` / `lastAccountId`) before giving up on direct delivery. +**Direct delivery** - if the task has a channel target (the `requesterOrigin`), the completion message goes straight to that channel (Discord, Slack, Telegram, etc.). Group and channel task completions are instead routed through the requester session so the parent agent can write the visible reply. For subagent completions, OpenClaw also preserves bound thread/topic routing when available and can fill a missing `to` / account from the requester session's stored route (`lastChannel` / `lastTo` / `lastAccountId`) before giving up on direct delivery. **Session-queued delivery** - if direct delivery fails or no origin is set, the update is queued as a system event in the requester's session and surfaces on the next heartbeat. -Task completion triggers an immediate heartbeat wake so you see the result quickly - you do not have to wait for the next scheduled heartbeat tick. +Session-queued task completions trigger an immediate heartbeat wake, so you see the result quickly - you do not have to wait for the next scheduled heartbeat tick. That means the usual workflow is push-based: start detached work once, then let the runtime wake or notify you on completion. Poll task state only when you need debugging, intervention, or an explicit audit. @@ -176,11 +166,11 @@ That means the usual workflow is push-based: start detached work once, then let Control how much you hear about each task: -| Policy | What is delivered | -| --------------------- | ----------------------------------------------------------------------- | -| `done_only` (default) | Only terminal state (succeeded, failed, etc.) - **this is the default** | -| `state_changes` | Every state transition and progress update | -| `silent` | Nothing at all | +| Policy | What is delivered | +| --------------------- | ------------------------------------------------------- | +| `done_only` (default) | Only terminal state (succeeded, failed, etc.) | +| `state_changes` | Every state transition and progress update | +| `silent` | Nothing at all (default for cron, CLI, and media tasks) | Change the policy while a task is running: @@ -196,12 +186,12 @@ openclaw tasks notify state_changes openclaw tasks list [--runtime ] [--status ] [--json] ``` - Output columns: Task ID, Kind, Status, Delivery, Run ID, Child Session, Summary. + Output columns: Task, Kind, Status, Delivery, Run, Child Session, Summary. Bare `openclaw tasks` behaves like `openclaw tasks list`. ```bash - openclaw tasks show + openclaw tasks show [--json] ``` The lookup token accepts a task ID, run ID, or session key. Shows the full record including timing, delivery state, error, and terminal summary. @@ -212,7 +202,7 @@ openclaw tasks notify state_changes openclaw tasks cancel ``` - For ACP and subagent tasks, this kills the child session. For CLI-tracked tasks, cancellation is recorded in the task registry (there is no separate child runtime handle). Status transitions to `cancelled` and a delivery notification is sent when applicable. + For ACP and subagent tasks, this kills the child session; ACP and cron cancellations route through the running Gateway (`tasks.cancel`). For CLI-tracked tasks, cancellation is recorded in the task registry (there is no separate child runtime handle). Status transitions to `cancelled` and a delivery notification is sent when applicable. @@ -222,10 +212,12 @@ openclaw tasks notify state_changes ```bash - openclaw tasks audit [--json] + openclaw tasks audit [--severity ] [--code ] [--limit ] [--json] ``` - Surfaces operational issues. Findings also appear in `openclaw status` when issues are detected. + Surfaces operational issues for tasks **and** TaskFlows in one report. Findings also appear in `openclaw status` when issues are detected. + + Task findings: | Finding | Severity | Trigger | | ------------------------- | ---------- | ------------------------------------------------------------------------------------------------------------ | @@ -236,6 +228,18 @@ openclaw tasks notify state_changes | `missing_cleanup` | warn | Terminal task with no cleanup timestamp | | `inconsistent_timestamps` | warn | Timeline violation (for example ended before started) | + TaskFlow findings: + + | Finding | Severity | Trigger | + | ---------------------- | ---------- | --------------------------------------------------------------------------- | + | `restore_failed` | error | Flow registry restore from SQLite failed | + | `stale_running` | error | Running flow has not advanced for more than 30 minutes | + | `stale_waiting` | warn | Waiting flow has not advanced for more than 30 minutes | + | `stale_blocked` | warn | Blocked flow has not advanced for more than 30 minutes | + | `cancel_stuck` | warn | Cancel requested over 5 minutes ago, no active child tasks, still nonterminal | + | `missing_linked_tasks` | warn/error | Stale managed flow with no linked tasks or wait state | + | `blocked_task_missing` | warn | Blocked flow points at a task id that no longer exists | + ```bash @@ -243,13 +247,13 @@ openclaw tasks notify state_changes openclaw tasks maintenance --apply [--json] ``` - Use this to preview or apply reconciliation, cleanup stamping, and pruning for tasks, Task Flow state, and stale cron run session registry rows. + Use this to preview or apply reconciliation, cleanup stamping, and pruning for tasks, TaskFlow state, and stale cron run session registry rows. Reconciliation is runtime-aware: - - ACP/subagent tasks check their backing child session. + - ACP tasks require a live in-process turn in the Gateway; subagent tasks check their backing child session. - Subagent tasks whose child session has a restart-recovery tombstone are marked lost instead of being treated as recoverable backing sessions. - - Cron tasks check whether the cron runtime still owns the job, then recover terminal status from persisted cron run logs/job state before falling back to `lost`. Only the Gateway process is authoritative for the in-memory cron active-job set; offline CLI audit uses durable history but does not mark a cron task lost solely because that local Set is empty. + - Cron tasks check whether the cron runtime still owns the job, then recover terminal status from persisted cron run logs/job state before falling back to `lost`. Only the Gateway process is authoritative for the in-memory cron active-job set; offline CLI audit uses durable history but does not mark a cron task lost solely because that local set is empty. - CLI tasks with run identity check the owning live run context, not just child-session or chat-session rows. Completion cleanup is also runtime-aware: @@ -260,7 +264,7 @@ openclaw tasks notify state_changes - Subagent completion delivery uses the child's latest visible assistant text only. Tool/toolResult output is not promoted into child result text. Terminal failed runs announce failure status without replaying captured reply text. - Cleanup failures do not mask the real task outcome. - When applying maintenance, OpenClaw also removes stale `cron::run:` session registry rows older than 7 days, while preserving rows for currently running cron jobs and leaving non-cron session rows untouched. + When applying maintenance, OpenClaw also removes stale `cron::run:` session registry rows older than 7 days, while preserving rows for currently running cron jobs and leaving non-cron session rows untouched. @@ -270,14 +274,14 @@ openclaw tasks notify state_changes openclaw tasks flow cancel ``` - Use these when the orchestrating Task Flow is the thing you care about rather than one individual background task record. + The flow lookup token accepts a flow id or owner key. Use these when the orchestrating [Task Flow](/automation/taskflow) is the thing you care about rather than one individual background task record. ## Chat task board (`/tasks`) -Use `/tasks` in any chat session to see background tasks linked to that session. The board shows active and recently completed tasks with runtime, status, timing, and progress or error detail. +Use `/tasks` in any chat session to see background tasks linked to that session. The board shows up to five active and recently completed tasks with runtime, status, timing, and progress or error detail. When the current session has no visible linked tasks, `/tasks` falls back to agent-local task counts so you still get an overview without leaking other-session details. @@ -285,49 +289,45 @@ For the full operator ledger, use the CLI: `openclaw tasks list`. ## Status integration (task pressure) -`openclaw status` includes an at-a-glance task summary: +`openclaw status` includes an at-a-glance task line: ``` -Tasks: 3 queued · 2 running · 1 issues +Tasks 2 active · 1 queued · 1 running · 1 issue · audit clean · 6 tracked ``` -The summary reports: +The summary counts active work (`queued` + `running`), failures (`failed` + `timed_out` + `lost`), audit findings, and total tracked records; the JSON payload also breaks counts down by runtime (`acp`, `subagent`, `cron`, `cli`). -- **active** - count of `queued` + `running` -- **failures** - count of `failed` + `timed_out` + `lost` -- **byRuntime** - breakdown by `acp`, `subagent`, `cron`, `cli` - -Both `/status` and the `session_status` tool use a cleanup-aware task snapshot: active tasks are preferred, stale completed rows are hidden, and recent failures only surface when no active work remains. This keeps the status card focused on what matters right now. +Both `/status` and the `session_status` tool use a cleanup-aware task snapshot: active tasks are preferred, expired rows are hidden, and terminal tasks only appear for a short recent window (5 minutes), with failures focused when no active work remains. This keeps the status card on what matters right now. ## Storage and maintenance ### Where tasks live -Task records persist in SQLite at: +Task records and delivery state persist in the shared OpenClaw SQLite state database: ``` -$OPENCLAW_STATE_DIR/tasks/runs.sqlite +~/.openclaw/state/openclaw.sqlite (tables: task_runs, task_delivery_state, flow_runs) ``` -The registry loads into memory at gateway start and syncs writes to SQLite for durability across restarts. -The Gateway keeps the SQLite write-ahead log bounded by using SQLite's default -autocheckpoint threshold plus periodic `PASSIVE` checkpoints. Shutdown and -explicit maintenance checkpoints still use `TRUNCATE` so normal closes can -reclaim WAL space without making the background sweeper wait on active readers. +Set `OPENCLAW_STATE_DIR` to move the whole state root (default `~/.openclaw`) elsewhere; the shared database path moves with it. + +The registry loads into memory on first use and persists every write back to SQLite, so records survive gateway restarts. WAL growth stays bounded through SQLite's default autocheckpoint threshold plus periodic `PASSIVE` checkpoints; shutdown and explicit maintenance checkpoints use `TRUNCATE` so normal closes reclaim WAL space without making the background sweeper wait on active readers. + +Legacy sidecar stores from older installs (`tasks/runs.sqlite`, `flows/registry.sqlite`) are imported into the shared database by `openclaw doctor`. ### Automatic maintenance -A sweeper runs every **60 seconds** and handles four things: +A sweeper runs every **60 seconds** (first pass about 5 seconds after gateway start) and handles four things: - Checks whether active tasks still have authoritative runtime backing. ACP/subagent tasks use child-session state, cron tasks use active-job ownership, and CLI tasks with run identity use the owning run context. If that backing state is gone for more than 5 minutes, the task is marked `lost`. + Checks whether active tasks still have authoritative runtime backing. ACP tasks require a live in-process turn, subagent tasks use child-session state, cron tasks use active-job ownership plus durable run history, and CLI tasks with run identity use the owning run context. If backing state is gone for more than 5 minutes (30 minutes for childless native subagent tasks), the task is marked `lost`. Closes terminal or orphaned parent-owned one-shot ACP sessions, and closes stale terminal or orphaned persistent ACP sessions only when no active conversation binding remains. - Sets a `cleanupAfter` timestamp on terminal tasks (endedAt + 7 days). During retention, lost tasks still appear in audit as warnings; after `cleanupAfter` expires or when cleanup metadata is missing, they are errors. + Sets a `cleanupAfter` timestamp on terminal tasks (terminal time + retention window). During retention, lost tasks still appear in audit as warnings; after `cleanupAfter` expires or when cleanup metadata is missing, they become errors. Deletes records past their `cleanupAfter` date. @@ -335,7 +335,7 @@ A sweeper runs every **60 seconds** and handles four things: -**Retention:** terminal task records are kept for **7 days**, then automatically pruned. No configuration needed. +**Retention:** terminal task records are kept for **7 days** (`lost` records for **24 hours**), then automatically pruned. No configuration needed. ## How tasks relate to other systems @@ -344,11 +344,9 @@ A sweeper runs every **60 seconds** and handles four things: [Task Flow](/automation/taskflow) is the flow orchestration layer above background tasks. A single flow may coordinate multiple tasks over its lifetime using managed or mirrored sync modes. Use `openclaw tasks` to inspect individual task records and `openclaw tasks flow` to inspect the orchestrating flow. - See [Task Flow](/automation/taskflow) for details. - - Cron job definitions, runtime execution state, and run history live in OpenClaw's shared SQLite state database. **Every** cron execution creates a task record - both main-session and isolated. Main-session cron tasks default to `silent` notify policy so they track without generating notifications. + Cron job definitions, runtime execution state, and run history live in OpenClaw's shared SQLite state database. **Every** cron execution creates a task record - both main-session and isolated - with `silent` notify policy, so cron runs are tracked without generating task notifications of their own. See [Cron Jobs](/automation/cron-jobs). diff --git a/docs/automation/troubleshooting.md b/docs/automation/troubleshooting.md index 7a9fd28cbb2a..df21ee6609d3 100644 --- a/docs/automation/troubleshooting.md +++ b/docs/automation/troubleshooting.md @@ -3,7 +3,7 @@ summary: "Redirect to /automation/cron-jobs" title: "Automation troubleshooting" --- -This page moved to [Scheduled Tasks](/automation/cron-jobs#troubleshooting). See [Scheduled Tasks](/automation/cron-jobs#troubleshooting) for troubleshooting documentation. +This page moved. Automation troubleshooting now lives at [Scheduled Tasks](/automation/cron-jobs#troubleshooting). ## Related diff --git a/docs/automation/webhook.md b/docs/automation/webhook.md index b3336be27f3f..9d6ddc313100 100644 --- a/docs/automation/webhook.md +++ b/docs/automation/webhook.md @@ -3,7 +3,7 @@ summary: "Redirect to /automation/cron-jobs" title: "Webhooks" --- -This page moved to [Scheduled Tasks](/automation/cron-jobs#webhooks). See [Scheduled Tasks](/automation/cron-jobs#webhooks) for webhook documentation. +This page moved. Webhook documentation now lives at [Scheduled Tasks](/automation/cron-jobs#webhooks). ## Related diff --git a/docs/channels/access-groups.md b/docs/channels/access-groups.md index 5e7ec0001f8f..32e47fd670b7 100644 --- a/docs/channels/access-groups.md +++ b/docs/channels/access-groups.md @@ -7,15 +7,15 @@ read_when: title: "Access groups" --- -Access groups are named sender lists you define once and reference from channel allowlists with `accessGroup:`. +Access groups are named sender lists you define once under `accessGroups` and reference from channel allowlists with `accessGroup:`. Use them when the same people should be allowed across several message channels, or when one trusted set should apply to both DMs and group sender authorization. -Access groups do not grant access by themselves. A group only matters when an allowlist field references it. +A group grants nothing by itself. It only matters where an allowlist field references it. ## Static message sender groups -Static sender groups use `type: "message.senders"`. +Static sender groups use `type: "message.senders"`. `members` is keyed by message-channel id, plus `"*"` for entries shared by every channel: ```json5 { @@ -33,16 +33,12 @@ Static sender groups use `type: "message.senders"`. } ``` -Member lists are keyed by message-channel id: +| Key | Meaning | +| -------------------------- | --------------------------------------------------------------------------- | +| `"*"` | Shared entries checked for every message channel that references the group. | +| `discord`, `telegram`, ... | Entries checked only for that channel's allowlist matching. | -| Key | Meaning | -| ---------- | ----------------------------------------------------------------------- | -| `"*"` | Shared entries checked for every message channel that references group. | -| `discord` | Entries checked only for Discord allowlist matching. | -| `telegram` | Entries checked only for Telegram allowlist matching. | -| `whatsapp` | Entries checked only for WhatsApp allowlist matching. | - -Entries are matched with the destination channel's normal `allowFrom` rules. OpenClaw does not translate sender ids between channels. If Alice has a Telegram id and a Discord id, list both ids under the appropriate keys. +Entries are matched with the destination channel's normal `allowFrom` rules. OpenClaw does not translate sender ids between channels: if Alice has a Telegram id and a Discord id, list both ids under the matching channel keys. ## Reference groups from allowlists @@ -93,7 +89,7 @@ Group sender allowlist example: groupAllowFrom: ["accessGroup:oncall"], }, googlechat: { - spaces: { + groups: { "spaces/AAA": { users: ["accessGroup:oncall"], }, @@ -118,33 +114,14 @@ You can mix groups and direct entries: ## Supported message-channel paths -Access groups are available in shared message-channel authorization paths, including: +Access groups work in the shared message-channel authorization paths: - DM sender allowlists such as `channels..allowFrom` - group sender allowlists such as `channels..groupAllowFrom` -- channel-specific per-room sender allowlists that use the same sender matching rules +- channel-specific per-room sender allowlists that use the same sender matching rules (for example Google Chat `groups..users`) - command authorization paths that reuse message-channel sender allowlists -Channel support depends on whether that channel is wired through the shared OpenClaw sender-authorization helpers. Current bundled support includes Discord, Feishu, Google Chat, iMessage, LINE, Mattermost, Microsoft Teams, Nextcloud Talk, Nostr, QQBot, Signal, WhatsApp, Zalo, and Zalo Personal. Static `message.senders` groups are designed to be channel-agnostic, so new message channels should support them by using the shared plugin SDK helpers instead of custom allowlist expansion. - -## Plugin diagnostics - -Plugin authors can inspect structured access-group state without expanding it back into a flat allowlist: - -```typescript -import { resolveAccessGroupAllowFromState } from "openclaw/plugin-sdk/security-runtime"; - -const state = await resolveAccessGroupAllowFromState({ - accessGroups: cfg.accessGroups, - allowFrom: channelConfig.allowFrom, - channel: "my-channel", - accountId: "default", - senderId, - isSenderAllowed, -}); -``` - -The result reports referenced, matched, missing, unsupported, and failed groups. Use this when you need diagnostics or conformance tests. Use `expandAllowFromWithAccessGroups(...)` only for compatibility paths that still expect a flat `allowFrom` array. +Channel support depends on whether that channel is wired through the shared OpenClaw sender-authorization helpers. Current bundled support includes ClickClack, Discord, Feishu, Google Chat, iMessage, IRC, LINE, Mattermost, Microsoft Teams, Nextcloud Talk, Nostr, QQ Bot, Signal, Slack, SMS, Telegram, WhatsApp, Zalo, and Zalo Personal. Static `message.senders` groups are channel-agnostic, so new message channels get them by using the shared plugin SDK ingress helpers instead of custom allowlist expansion. ## Discord channel audiences @@ -169,7 +146,7 @@ Discord also supports a dynamic access group type: } ``` -`discord.channelAudience` means "allow Discord DM senders who can currently view this guild channel." OpenClaw resolves the sender through Discord at authorization time and applies Discord `ViewChannel` permission rules. +`discord.channelAudience` means "allow Discord DM senders who can currently view this guild channel." OpenClaw resolves the sender through Discord at authorization time and applies Discord `ViewChannel` permission rules. `membership` is optional and defaults to `canViewChannel`. Use this when a Discord channel is already the source of truth for a team, such as `#maintainers` or `#on-call`. @@ -181,6 +158,25 @@ Requirements and failure behavior: More Discord-specific examples: [Discord access control](/channels/discord#access-control-and-routing) +## Plugin diagnostics + +Plugin authors can inspect structured access-group state without expanding it back into a flat allowlist: + +```typescript +import { resolveAccessGroupAllowFromState } from "openclaw/plugin-sdk/access-groups"; + +const state = await resolveAccessGroupAllowFromState({ + accessGroups: cfg.accessGroups, + allowFrom: channelConfig.allowFrom, + channel: "my-channel", + accountId: "default", + senderId, + isSenderAllowed, +}); +``` + +The result reports referenced, matched, missing, unsupported, and failed groups. Use it for diagnostics or conformance tests. Use `expandAllowFromWithAccessGroups(...)` only for compatibility paths that still expect a flat `allowFrom` array. + ## Security notes - Access groups are allowlist aliases, not roles. They do not create owners, approve pairing requests, or grant tool permissions by themselves. diff --git a/docs/channels/ambient-room-events.md b/docs/channels/ambient-room-events.md index 162cc12ceb9b..8a739b2d64b9 100644 --- a/docs/channels/ambient-room-events.md +++ b/docs/channels/ambient-room-events.md @@ -10,7 +10,7 @@ sidebarTitle: "Ambient room events" Ambient room events let OpenClaw process unmentioned group or channel chatter as quiet context. The agent can update memory and session state, but the room stays silent unless the agent explicitly calls the `message` tool. -For always-on group chats, this is the recommended mode: combine `messages.groupChat.unmentionedInbound: "room_event"` with `messages.groupChat.visibleReplies: "message_tool"`. Use it when the agent should listen, decide when a reply is useful, and avoid the old prompt pattern of answering `NO_REPLY`. +For always-on group chats, combine `messages.groupChat.unmentionedInbound: "room_event"` with `messages.groupChat.visibleReplies: "message_tool"`. The agent listens, decides when a reply is useful, and never needs the old prompt pattern of answering `NO_REPLY`. Supported today: Discord guild channels, Slack channels and private channels, Slack multi-person DMs, and Telegram groups or supergroups. Other group channels keep their existing group behavior unless their channel page says they support ambient room events. @@ -30,9 +30,9 @@ Set the global group-chat behavior: } ``` -Then configure the room itself as always-on by disabling mention gating for that room. The channel must still be allowed by its normal `groupPolicy`, room allowlist, and sender allowlist. +Then make the room always-on by disabling mention gating for that room. The room must still pass its normal `groupPolicy`, room allowlist, and sender allowlist. -After saving the config, the Gateway hot-reloads `messages` settings. Restart only when file watching or config reload is disabled. +After saving the config, the Gateway hot-applies `messages` settings. Restart only when file watching or config reload is disabled (`gateway.reload.mode: "off"`). ## What changes @@ -40,7 +40,7 @@ With `messages.groupChat.unmentionedInbound: "room_event"`: - unmentioned allowed group or channel messages become quiet room events - mentioned messages stay user requests -- text commands and native commands stay user requests +- text control commands and native commands stay user requests - abort or stop requests stay user requests - direct messages stay user requests @@ -71,17 +71,17 @@ Room events use strict visible delivery. Final assistant text is private. The ag } ``` -Use per-channel Discord config when only one channel should be ambient: +Use per-channel Discord config when only one channel should be ambient. Under `groupPolicy: "allowlist"`, listing the channel is what allows it (`enabled: false` disables an entry): ```json5 { channels: { discord: { + groupPolicy: "allowlist", guilds: { "": { channels: { "": { - allow: true, requireMention: false, }, }, @@ -94,7 +94,7 @@ Use per-channel Discord config when only one channel should be ambient: ## Slack example -Slack channel allowlists are ID-first. Use channel IDs such as `C12345678`, not `#channel-name`. +Slack channel allowlists are ID-first. Use channel IDs such as `C12345678`, not `#channel-name`. Listing the channel under `channels.slack.channels` is what allows it (`enabled: false` disables an entry): ```json5 { @@ -110,7 +110,6 @@ Slack channel allowlists are ID-first. Use channel IDs such as `C12345678`, not groupPolicy: "allowlist", channels: { "": { - allow: true, requireMention: false, }, }, @@ -176,17 +175,15 @@ The agent-specific `agents.list[].groupChat.unmentionedInbound` value overrides ## Visible reply modes -`messages.groupChat.visibleReplies` defaults to `"automatic"` for normal group/channel user requests. Keep that default when you want final assistant text to post visibly without requiring an explicit message-tool call. +`messages.groupChat.visibleReplies` defaults to `"automatic"` for normal group/channel user requests. Keep that default when final assistant text should post visibly without an explicit message-tool call. -For ambient always-on rooms, `messages.groupChat.visibleReplies: "message_tool"` is still recommended, especially with latest-generation, tool-reliable models such as GPT 5.5. It lets the agent decide when to speak by calling the message tool. If the model returns final text without calling the tool, OpenClaw keeps that final text private and logs suppressed delivery metadata. +For ambient always-on rooms, `messages.groupChat.visibleReplies: "message_tool"` is still recommended, especially with latest-generation, tool-reliable models such as GPT 5.5. It lets the agent decide when to speak by calling the message tool. If the model returns final text without calling the tool, OpenClaw keeps that final text private and logs suppressed-delivery metadata. -Room events stay strict even when other group requests use automatic replies. Unmentioned ambient room events still require `message(action=send)` for visible output. +Room events stay strict even when other group requests use automatic replies. Unmentioned ambient room events always require `message(action=send)` for visible output. ## History -`messages.groupChat.historyLimit` controls the global group history default. Channels can override it with `channels..historyLimit`, and some channels also support per-account history limits. - -Set `historyLimit: 0` to disable group history context. +`messages.groupChat.historyLimit` sets the global group history default (50 when unset; must be a positive integer). Channels can override it with `channels..historyLimit`, and some channels also support per-account history limits. Set the channel-level `historyLimit: 0` to disable group history context for that channel. Supported room-event channels keep recent ambient room messages as context. Telegram keeps an always-on rolling per-group window bounded by `historyLimit`; user-request turns select entries after the bot's last recorded reply, while room-event turns receive the full recent window so the model can see its own recent posts. The retired Telegram `includeGroupHistoryContext` mode key is removed by `openclaw doctor --fix`. @@ -202,7 +199,7 @@ If the room shows typing or token usage but no visible message: If Telegram ambient rooms do not trigger at all, check BotFather privacy mode and verify the Gateway is receiving normal group messages. -If Slack ambient rooms do not trigger, verify the channel key is the Slack channel ID and the app has the required `channels:history` or `groups:history` scope for that room type. +If Slack ambient rooms do not trigger, verify the channel key is the Slack channel ID and the app has the history scope for that room type: `channels:history` (public), `groups:history` (private), or `mpim:history` (multi-person DMs). ## Related diff --git a/docs/channels/bot-loop-protection.md b/docs/channels/bot-loop-protection.md index e05fee4118ac..8400d64483a6 100644 --- a/docs/channels/bot-loop-protection.md +++ b/docs/channels/bot-loop-protection.md @@ -7,35 +7,26 @@ title: "Bot loop protection" sidebarTitle: "Bot loop protection" --- -# Bot loop protection +OpenClaw can accept messages written by other bots on channels that support `allowBots`. When that path is enabled, pair loop protection prevents two bot identities from replying to each other indefinitely. -OpenClaw can accept messages written by other bots on channels that support `allowBots`. -When that path is enabled, pair loop protection prevents two bot identities from -replying to each other indefinitely. - -The guard is enforced by the core inbound reply runner. Each supporting channel -maps its own inbound event into generic facts: account or scope, conversation id, -sender bot id, and receiver bot id. Core then tracks the participant pair in both -directions, applies a sliding-window budget, and suppresses the pair during a -cooldown after the budget is exceeded. +The guard is enforced by the core inbound reply runner. Each supporting channel maps its inbound event into generic facts: account or scope, conversation id, sender bot id, and receiver bot id. Core tracks the participant pair in both directions (A to B and B to A count as the same pair), applies a sliding-window budget, and suppresses the pair during a cooldown after the budget is exceeded. ## Defaults -Pair loop protection is active when a channel lets bot-authored messages reach -dispatch. Built-in defaults are: +Pair loop protection is active whenever a channel lets bot-authored messages reach dispatch. Built-in defaults: -- `maxEventsPerWindow: 20` - a bot pair can exchange 20 events within the window -- `windowSeconds: 60` - sliding window length -- `cooldownSeconds: 60` - suppression time after the pair exceeds the budget +| Key | Default | Meaning | +| -------------------- | ------- | --------------------------------------------------- | +| `enabled` | `true` | Guard active for channels that support it. | +| `maxEventsPerWindow` | `20` | Events a bot pair can exchange within the window. | +| `windowSeconds` | `60` | Sliding window length. | +| `cooldownSeconds` | `60` | Suppression time after the pair exceeds the budget. | -The guard does not affect normal human-authored messages, single-bot deployments, -self-message filtering, or one-shot bot replies that stay under the budget. +The guard does not affect human-authored messages, single-bot deployments, self-message filtering, or bot replies that stay under the budget. ## Configure shared defaults -Set `channels.defaults.botLoopProtection` once to give every supporting channel -the same baseline. Channel and account overrides can still tune individual -surfaces. +Set `channels.defaults.botLoopProtection` once to give every supporting channel the same baseline. Channel, account, and room overrides can still tune individual surfaces. ```json5 { @@ -51,18 +42,17 @@ surfaces. } ``` -Set `enabled: false` only when your channel policy intentionally allows -bot-to-bot conversations without automatic suppression. +Set `enabled: false` only when your channel policy intentionally allows bot-to-bot conversations without automatic suppression. -## Override per channel or account +## Override per channel, account, or room -Supporting channels layer their own config over the shared default. Precedence is: +Supporting channels layer their own config over the shared default, key by key. Precedence, narrowest first: -- `channels...botLoopProtection`, when the channel supports per-conversation overrides -- `channels..accounts..botLoopProtection`, when the channel supports accounts -- `channels..botLoopProtection`, when the channel supports top-level defaults -- `channels.defaults.botLoopProtection` -- built-in defaults +1. `channels...botLoopProtection`, when the channel supports per-conversation overrides +2. `channels..accounts..botLoopProtection`, when the channel supports accounts +3. `channels..botLoopProtection`, when the channel supports top-level defaults +4. `channels.defaults.botLoopProtection` +5. built-in defaults ```json5 { @@ -77,7 +67,7 @@ Supporting channels layer their own config over the shared default. Precedence i maxEventsPerWindow: 8, }, accounts: { - molty: { + secondary: { allowBots: "mentions", botLoopProtection: { maxEventsPerWindow: 5, @@ -86,22 +76,6 @@ Supporting channels layer their own config over the shared default. Precedence i }, }, }, - slack: { - allowBots: "mentions", - botLoopProtection: { - maxEventsPerWindow: 8, - }, - }, - matrix: { - allowBots: "mentions", - groups: { - "!roomid:example.org": { - botLoopProtection: { - maxEventsPerWindow: 5, - }, - }, - }, - }, googlechat: { allowBots: true, groups: { @@ -112,6 +86,22 @@ Supporting channels layer their own config over the shared default. Precedence i }, }, }, + matrix: { + allowBots: "mentions", + groups: { + "!roomid:example.org": { + botLoopProtection: { + maxEventsPerWindow: 5, + }, + }, + }, + }, + slack: { + allowBots: "mentions", + botLoopProtection: { + maxEventsPerWindow: 8, + }, + }, }, } ``` @@ -119,13 +109,10 @@ Supporting channels layer their own config over the shared default. Precedence i ## Channel support - Discord: native `author.bot` facts, keyed by Discord account, channel, and bot pair. -- Slack: native `bot_id` facts for accepted bot-authored messages, keyed by Slack account, channel, and bot pair. -- Matrix: configured Matrix bot accounts, keyed by Matrix account, room, and configured bot pair. - Google Chat: native `sender.type=BOT` facts for accepted bot-authored messages, keyed by account, space, and bot pair. +- Matrix: configured Matrix bot accounts, keyed by Matrix account, room, and configured bot pair. +- Slack: native `bot_id` facts for accepted bot-authored messages, keyed by Slack account, channel, and bot pair. -Channels that do not expose a reliable inbound bot identity keep using their -normal self-message and access-policy filters. They should not opt into this -guard until they can identify both participants in the bot pair. +Channels that do not expose a reliable inbound bot identity keep using their normal self-message and access-policy filters. They should not opt into this guard until they can identify both participants in the bot pair. -See [SDK runtime](/plugins/sdk-runtime#reusable-runtime-utilities) for plugin -implementation details. +See [SDK runtime](/plugins/sdk-runtime#reusable-runtime-utilities) for plugin implementation details. diff --git a/docs/channels/broadcast-groups.md b/docs/channels/broadcast-groups.md index bbbd776248d0..1624ab5a9361 100644 --- a/docs/channels/broadcast-groups.md +++ b/docs/channels/broadcast-groups.md @@ -9,73 +9,25 @@ sidebarTitle: "Broadcast groups" --- -**Status:** Experimental. Added in 2026.1.9. +**Status:** Experimental. Added in 2026.1.9. WhatsApp (web channel) only. ## Overview -Broadcast Groups enable multiple agents to process and respond to the same message simultaneously. This allows you to create specialized agent teams that work together in a single WhatsApp group or DM — all using one phone number. +Broadcast groups run **multiple agents** on the same inbound message. Each agent processes the message in its own isolated session and posts its own reply, so one WhatsApp number can host a team of specialized agents in a single group chat or DM. -Current scope: **WhatsApp only** (web channel). - -Broadcast groups are evaluated after channel allowlists and group activation rules. In WhatsApp groups, this means broadcasts happen when OpenClaw would normally reply (for example: on mention, depending on your group settings). +Broadcast groups are evaluated after channel allowlists and group activation rules. In WhatsApp groups, broadcasts happen when OpenClaw would normally reply (for example: on mention, depending on your group settings). They only change **which agents run**, never whether a message is eligible for processing. The live WhatsApp QA lane includes `whatsapp-broadcast-group-fanout`, which verifies that one mentioned group message can produce distinct visible replies from two configured agents. -## Use cases - - - - Deploy multiple agents with atomic, focused responsibilities: - - ``` - Group: "Development Team" - Agents: - - CodeReviewer (reviews code snippets) - - DocumentationBot (generates docs) - - SecurityAuditor (checks for vulnerabilities) - - TestGenerator (suggests test cases) - ``` - - Each agent processes the same message and provides its specialized perspective. - - - - ``` - Group: "International Support" - Agents: - - Agent_EN (responds in English) - - Agent_DE (responds in German) - - Agent_ES (responds in Spanish) - ``` - - - ``` - Group: "Customer Support" - Agents: - - SupportAgent (provides answer) - - QAAgent (reviews quality, only responds if issues found) - ``` - - - ``` - Group: "Project Management" - Agents: - - TaskTracker (updates task database) - - TimeLogger (logs time spent) - - ReportGenerator (creates summaries) - ``` - - - ## Configuration ### Basic setup -Add a top-level `broadcast` section (next to `bindings`). Keys are WhatsApp peer ids: +Add a top-level `broadcast` section (next to `bindings`). Keys are WhatsApp peer ids, values are arrays of agent ids: - group chats: group JID (e.g. `120363403215116621@g.us`) -- DMs: E.164 phone number (e.g. `+15551234567`) +- DMs: sender E.164 phone number (e.g. `+15551234567`) ```json { @@ -85,40 +37,27 @@ Add a top-level `broadcast` section (next to `bindings`). Keys are WhatsApp peer } ``` -**Result:** When OpenClaw would reply in this chat, it will run all three agents. +**Result:** when OpenClaw would reply in this chat, it runs all three agents. + +Every listed agent id must exist in `agents.list`: config validation reports unknown ids, and the runtime skips them with a `Broadcast agent not found in agents.list; skipping` warning. ### Processing strategy -Control how agents process messages: +`broadcast.strategy` sets how agents process the message: - - - All agents process simultaneously: +| Strategy | Behavior | +| -------------------- | --------------------------------------------------------------------- | +| `parallel` (default) | All agents process simultaneously; replies arrive in any order. | +| `sequential` | Agents process in array order; each waits for the previous to finish. | - ```json - { - "broadcast": { - "strategy": "parallel", - "120363403215116621@g.us": ["alfred", "baerbel"] - } - } - ``` - - - - Agents process in order (one waits for previous to finish): - - ```json - { - "broadcast": { - "strategy": "sequential", - "120363403215116621@g.us": ["alfred", "baerbel"] - } - } - ``` - - - +```json +{ + "broadcast": { + "strategy": "sequential", + "120363403215116621@g.us": ["alfred", "baerbel"] + } +} +``` ### Complete example @@ -173,6 +112,7 @@ Control how agents process messages: - All listed agents process the message. - Each agent has its own session key and isolated context. - Agents process in parallel (default) or sequentially. + - Audio attachments are transcribed once before fan-out, so agents share one transcript instead of making separate STT calls. @@ -189,18 +129,14 @@ Broadcast groups do not bypass channel allowlists or group activation rules (men Each agent in a broadcast group maintains completely separate: - **Session keys** (`agent:alfred:whatsapp:group:120363...` vs `agent:baerbel:whatsapp:group:120363...`) -- **Conversation history** (agent doesn't see other agents' messages) +- **Conversation history** (an agent does not see other agents' replies) - **Workspace** (separate sandboxes if configured) - **Tool access** (different allow/deny lists) -- **Memory/context** (separate IDENTITY.md, SOUL.md, etc.) -- **Group context buffer** (recent group messages used for context) is shared per peer, so all broadcast agents see the same context when triggered +- **Memory/context** (separate `IDENTITY.md`, `SOUL.md`, etc.) -This allows each agent to have: +One exception is shared on purpose: the **group context buffer** (recent group messages used for context) is shared per peer, so all broadcast agents see the same context when triggered. It is cleared once after the fan-out completes. -- Different personalities -- Different tool access (e.g., read-only vs. read-write) -- Different models (e.g., opus vs. sonnet) -- Different skills installed +This allows each agent to have different personalities, models, skills, and tool access (for example read-only vs. read-write). ### Example: isolated sessions @@ -208,66 +144,57 @@ In group `120363403215116621@g.us` with agents `["alfred", "baerbel"]`: - ``` + ```text Session: agent:alfred:whatsapp:group:120363403215116621@g.us History: [user message, alfred's previous responses] - Workspace: /Users/user/openclaw-alfred/ + Workspace: ~/openclaw-alfred/ Tools: read, write, exec ``` - - ``` + + ```text Session: agent:baerbel:whatsapp:group:120363403215116621@g.us History: [user message, baerbel's previous responses] - Workspace: /Users/user/openclaw-baerbel/ + Workspace: ~/openclaw-baerbel/ Tools: read only ``` +## Use cases + +- **Specialized agent teams**: a dev group where `code-reviewer`, `security-auditor`, `test-generator`, and `docs-checker` each answer the same message from their own angle. +- **Multi-language support**: one support chat with `support-en`, `support-de`, `support-es` responding in their languages. +- **Quality assurance**: `support-agent` answers while `qa-agent` reviews and only responds when it finds issues. +- **Task automation**: `task-tracker`, `time-logger`, and `report-generator` all consume the same status update. + ## Best practices - Design each agent with a single, clear responsibility: - - ```json - { - "broadcast": { - "DEV_GROUP": ["formatter", "linter", "tester"] - } - } - ``` - - ✅ **Good:** Each agent has one job. ❌ **Bad:** One generic "dev-helper" agent. - + Give each agent a single, clear responsibility (`formatter`, `linter`, `tester`) instead of one generic "dev-helper" agent. - - Make it clear what each agent does: - + ```json { "agents": { - "security-scanner": { "name": "Security Scanner" }, - "code-formatter": { "name": "Code Formatter" }, - "test-generator": { "name": "Test Generator" } + "list": [ + { "id": "security-scanner", "name": "Security Scanner" }, + { "id": "code-formatter", "name": "Code Formatter" }, + { "id": "test-generator", "name": "Test Generator" } + ] } } ``` - - Give agents only the tools they need: - ```json { "agents": { - "reviewer": { - "tools": { "allow": ["read", "exec"] } - }, - "fixer": { - "tools": { "allow": ["read", "write", "edit", "exec"] } - } + "list": [ + { "id": "reviewer", "tools": { "allow": ["read", "exec"] } }, + { "id": "fixer", "tools": { "allow": ["read", "write", "edit", "exec"] } } + ] } } ``` @@ -276,21 +203,10 @@ In group `120363403215116621@g.us` with agents `["alfred", "baerbel"]`: - With many agents, consider: - - - Using `"strategy": "parallel"` (default) for speed - - Limiting broadcast groups to 5-10 agents - - Using faster models for simpler agents - + With many agents, prefer `"strategy": "parallel"` (default), keep broadcast groups to a handful of agents, and use faster models for simpler agents. - - Agents fail independently. One agent's error doesn't block others: - - ``` - Message → [Agent A ✓, Agent B ✗ error, Agent C ✓] - Result: Agent A and C respond, Agent B logs error - ``` - + + Agents fail independently. One agent's error is logged (`Broadcast agent failed: ...`) and does not block the others. @@ -298,12 +214,7 @@ In group `120363403215116621@g.us` with agents `["alfred", "baerbel"]`: ### Providers -Broadcast groups currently work with: - -- ✅ WhatsApp (implemented) -- 🚧 Telegram (planned) -- 🚧 Discord (planned) -- 🚧 Slack (planned) +Broadcast groups are currently implemented for WhatsApp (web channel) only. Other channels ignore the `broadcast` config. ### Routing @@ -323,7 +234,7 @@ Broadcast groups work alongside existing routing: } ``` -- `GROUP_A`: Only alfred responds (normal routing). +- `GROUP_A`: only alfred responds (normal routing). - `GROUP_B`: agent1 AND agent2 respond (broadcast). @@ -336,30 +247,27 @@ Broadcast groups work alongside existing routing: **Check:** - 1. Agent IDs exist in `agents.list`. - 2. Peer ID format is correct (e.g., `120363403215116621@g.us`). - 3. Agents are not in deny lists. + 1. Agent IDs exist in `agents.list` (config validation rejects unknown ids). + 2. Peer ID format is correct (group JID like `120363403215116621@g.us`, or E.164 like `+15551234567` for DMs). + 3. The message passed normal gating (mention/activation rules still apply). **Debug:** ```bash - tail -f ~/.openclaw/logs/gateway.log | grep broadcast + openclaw logs --follow | grep -i broadcast ``` + A successful fan-out logs `Broadcasting message to agents ()`. + - **Cause:** Peer ID might be in ordinary route bindings but not `broadcast`, or it might match an exclusive configured ACP binding. + **Cause:** the peer ID might be in ordinary route bindings but not `broadcast`, or it might match an exclusive configured ACP binding. - **Fix:** Add ordinary route-bound peers to broadcast config, or remove/change the configured ACP binding if fan-out broadcast is desired. + **Fix:** add ordinary route-bound peers to the broadcast config, or remove/change the configured ACP binding if fan-out broadcast is desired. - If slow with many agents: - - - Reduce number of agents per group. - - Use lighter models (sonnet instead of opus). - - Check sandbox startup time. - + If slow with many agents: reduce the number of agents per group, use lighter models, and check sandbox startup time. @@ -401,17 +309,10 @@ Broadcast groups work alongside existing routing: } ``` - **User sends:** Code snippet. - - **Responses:** - - - code-formatter: "Fixed indentation and added type hints" - - security-scanner: "⚠️ SQL injection vulnerability in line 12" - - test-coverage: "Coverage is 45%, missing tests for error cases" - - docs-checker: "Missing docstring for function `process_data`" + One code snippet in the group produces four replies: formatting fixes, a security finding, a coverage gap, and a docs nit. - + ```json { "broadcast": { @@ -449,24 +350,15 @@ interface OpenClawConfig { How to process agents. `parallel` runs all agents simultaneously; `sequential` runs them in array order. - WhatsApp group JID, E.164 number, or other peer ID. Value is the array of agent IDs that should process messages. + WhatsApp group JID or E.164 phone number. Value is the array of agent IDs that should all process messages from that peer. ## Limitations -1. **Max agents:** No hard limit, but 10+ agents may be slow. -2. **Shared context:** Agents don't see each other's responses (by design). -3. **Message ordering:** Parallel responses may arrive in any order. -4. **Rate limits:** All agents count toward WhatsApp rate limits. - -## Future enhancements - -Planned features: - -- [ ] Shared context mode (agents see each other's responses) -- [ ] Agent coordination (agents can signal each other) -- [ ] Dynamic agent selection (choose agents based on message content) -- [ ] Agent priorities (some agents respond before others) +1. **Max agents:** no hard limit, but many agents (10+) can be slow. +2. **Shared context:** agents do not see each other's responses (by design). +3. **Message ordering:** parallel responses may arrive in any order. +4. **Rate limits:** all replies come from one WhatsApp account, so every agent's reply counts toward the same WhatsApp rate limits. ## Related diff --git a/docs/channels/channel-routing.md b/docs/channels/channel-routing.md index e0b268387901..562dd6e4edaf 100644 --- a/docs/channels/channel-routing.md +++ b/docs/channels/channel-routing.md @@ -13,11 +13,11 @@ host configuration. ## Key terms -- **Channel**: `telegram`, `whatsapp`, `discord`, `irc`, `googlechat`, `slack`, `signal`, `imessage`, `line`, plus plugin channels. `webchat` is the internal WebChat UI channel and is not a configurable outbound channel. +- **Channel**: a bundled channel plugin such as `discord`, `googlechat`, `imessage`, `irc`, `line`, `signal`, `slack`, `telegram`, or `whatsapp`, plus installed plugin channels. `webchat` is the internal WebChat UI channel and is not a configurable outbound channel. - **AccountId**: per-channel account instance (when supported). - Optional channel default account: `channels..defaultAccount` chooses which account is used when an outbound path does not specify `accountId`. - - In multi-account setups, set an explicit default (`defaultAccount` or `accounts.default`) when two or more accounts are configured. Without it, fallback routing may pick the first normalized account ID. + - In multi-account setups, set an explicit default (`defaultAccount` or an account named `default`) when two or more accounts are configured. Without it, fallback routing may pick the first normalized account ID. - **AgentId**: an isolated workspace + session store ("brain"). - **SessionKey**: the bucket key used to store context and control concurrency. @@ -33,6 +33,11 @@ Direct messages collapse to the agent's **main** session by default: - `agent::` (default: `agent:main:main`) +`session.dmScope` controls DM collapsing: `main` (default) shares one main +session, while `per-peer`, `per-channel-peer`, and `per-account-channel-peer` +keep DMs in separate sessions. A route binding can override the scope for its +matched peers via `bindings[].session.dmScope`. + Even when direct-message conversation history is shared with main, sandbox and tool policy use a derived per-account direct-chat runtime key for external DMs so channel-originated messages are not treated like local main-session runs. @@ -78,12 +83,13 @@ Routing picks **one agent** for each inbound message: 1. **Exact peer match** (`bindings` with `peer.kind` + `peer.id`). 2. **Parent peer match** (thread inheritance). -3. **Guild + roles match** (Discord) via `guildId` + `roles`. -4. **Guild match** (Discord) via `guildId`. -5. **Team match** (Slack) via `teamId`. -6. **Account match** (`accountId` on the channel). -7. **Channel match** (any account on that channel, `accountId: "*"`). -8. **Default agent** (`agents.list[].default`, else first list entry, fallback to `main`). +3. **Peer wildcard match** (`peer.id: "*"` for a peer kind). +4. **Guild + roles match** (Discord) via `guildId` + `roles`. +5. **Guild match** (Discord) via `guildId`. +6. **Team match** (Slack) via `teamId`. +7. **Account match** (`accountId` on the channel). +8. **Channel match** (any account on that channel, `accountId: "*"`). +9. **Default agent** (`agents.list[].default`, else first list entry, fallback to `main`). When a binding includes multiple match fields (`peer`, `guildId`, `teamId`, `roles`), **all provided fields must match** for that binding to apply. diff --git a/docs/channels/clickclack.md b/docs/channels/clickclack.md index a2a5e5e96868..00191759cd50 100644 --- a/docs/channels/clickclack.md +++ b/docs/channels/clickclack.md @@ -12,11 +12,11 @@ Use this when you want an OpenClaw agent to appear as a ClickClack bot user. Cli ## Quick setup -Create a bot token in ClickClack: +Create a bot token on the ClickClack server: ```bash clickclack admin bot create \ - --workspace \ + --workspace \ --name "OpenClaw" \ --handle openclaw \ --scopes bot:write \ @@ -29,24 +29,13 @@ Configure OpenClaw: ```json5 { - plugins: { - entries: { - clickclack: { - llm: { - allowAgentIdOverride: true, - }, - }, - }, - }, channels: { clickclack: { enabled: true, - baseUrl: "https://app.clickclack.chat", + baseUrl: "https://clickclack.example.com", token: { source: "env", provider: "default", id: "CLICKCLACK_BOT_TOKEN" }, workspace: "default", defaultTo: "channel:general", - agentId: "clickclack-bot", - replyMode: "model", }, }, } @@ -59,6 +48,24 @@ export CLICKCLACK_BOT_TOKEN="ccb_..." openclaw gateway ``` +An account counts as configured only when `baseUrl`, `token`, and `workspace` are all set. `workspace` accepts a workspace id (`wsp_...`), slug, or name; the gateway resolves it to the id at startup. + +### Account config keys + +| Key | Default | Notes | +| ----------------------- | ------------------- | --------------------------------------------------------------------------------------- | +| `baseUrl` | none (required) | ClickClack server URL. | +| `token` | none (required) | Plain string or secret ref (`source: "env" \| "file" \| "exec"`). | +| `workspace` | none (required) | Workspace id, slug, or name. | +| `replyMode` | `"agent"` | `"agent"` runs the full agent pipeline; `"model"` sends short direct model completions. | +| `defaultTo` | `"channel:general"` | Target used when an outbound path gives no target. | +| `allowFrom` | `["*"]` | User-id allowlist for inbound DMs and channel messages. | +| `botUserId` | auto-detected | Resolved from the bot token identity at startup. | +| `agentId` | route default | Pin this account's inbound messages to one agent. | +| `toolsAllow` | none | Tool allowlist for agent replies from this account. | +| `model`, `systemPrompt` | none | Used by `replyMode: "model"` completions. | +| `reconnectMs` | `1500` | Realtime reconnect delay (100 to 60000). | + If `plugins.allow` is a non-empty restrictive list, explicitly selecting ClickClack in channel setup or running `openclaw plugins enable clickclack` appends `clickclack` to that list. Onboarding installation uses the same @@ -71,6 +78,41 @@ plugin-install policy and also records ClickClack in an existing allowlist. Each account opens its own ClickClack realtime connection and uses its own bot token. +```json5 +{ + channels: { + clickclack: { + enabled: true, + baseUrl: "https://clickclack.example.com", + defaultAccount: "service", + accounts: { + service: { + token: { source: "env", provider: "default", id: "CLICKCLACK_SERVICE_BOT_TOKEN" }, + workspace: "default", + defaultTo: "channel:general", + agentId: "service-bot", + }, + support: { + token: { source: "env", provider: "default", id: "CLICKCLACK_SUPPORT_BOT_TOKEN" }, + workspace: "default", + defaultTo: "dm:usr_...", + agentId: "support-bot", + }, + }, + }, + }, +} +``` + +## Reply modes + +- `replyMode: "agent"` (default) dispatches inbound messages through the normal agent pipeline, including session recording and tool policy. +- `replyMode: "model"` skips the agent pipeline and uses the plugin runtime's `llm.complete` for short direct bot replies (optionally shaped by `model` and `systemPrompt`). + +Model mode runs completions against the resolved bot agent id, which requires +the explicit `plugins.entries.clickclack.llm.allowAgentIdOverride: true` trust +bit: + ```json5 { plugins: { @@ -82,43 +124,19 @@ Each account opens its own ClickClack realtime connection and uses its own bot t }, }, }, - channels: { - clickclack: { - enabled: true, - baseUrl: "https://app.clickclack.chat", - defaultAccount: "service", - accounts: { - service: { - token: { source: "env", provider: "default", id: "CLICKCLACK_SERVICE_BOT_TOKEN" }, - workspace: "default", - defaultTo: "channel:general", - agentId: "service-bot", - replyMode: "model", - }, - peter: { - token: { source: "env", provider: "default", id: "CLICKCLACK_PETER_BOT_TOKEN" }, - workspace: "default", - defaultTo: "dm:usr_...", - agentId: "peter-bot", - replyMode: "model", - }, - }, - }, - }, } ``` -`replyMode: "model"` uses `api.runtime.llm.complete` directly for short bot replies. -When an account sets `agentId`, OpenClaw requires the explicit -`plugins.entries.clickclack.llm.allowAgentIdOverride` trust bit so the plugin -can run completions for that bot agent. Keep it off if you only use the default -agent route. +Keep the trust bit off if you only use the default `agent` reply mode; it is +not needed there. ## Targets - `channel:` sends to a workspace channel. Bare targets default to `channel:`. - `dm:` creates or reuses a direct conversation with that user. -- `thread:` replies in an existing thread. +- `thread:` replies in the thread rooted at that message. + +Explicit outbound targets may also carry the `clickclack:` or `cc:` provider prefix. Examples: @@ -140,7 +158,7 @@ OpenClaw only needs `bot:write` for normal agent chat. ## Troubleshooting -- `ClickClack is not configured`: set `channels.clickclack.token` or `CLICKCLACK_BOT_TOKEN`. -- `workspace not found`: set `workspace` to the workspace id or slug returned by ClickClack. -- No inbound replies: confirm the token has realtime read access and the bot is not replying to its own messages. +- `ClickClack is not configured for account ""`: set `baseUrl`, `token` (for example via `CLICKCLACK_BOT_TOKEN`), and `workspace` for that account. +- `ClickClack workspace not found: `: set `workspace` to the workspace id, slug, or name returned by ClickClack. +- No inbound replies: confirm the token has realtime read access and note that the bot ignores its own messages and messages from other bots. - Channel sends fail: verify the bot is a member of the workspace and has `bot:write`. diff --git a/docs/channels/discord.md b/docs/channels/discord.md index 690147fe0fdb..95d48b851588 100644 --- a/docs/channels/discord.md +++ b/docs/channels/discord.md @@ -1,11 +1,11 @@ --- -summary: "Discord bot support status, capabilities, and configuration" +summary: "Discord bot setup, config keys, components, voice, and troubleshooting" read_when: - Working on Discord channel features title: "Discord" --- -Ready for DMs and guild channels via the official Discord gateway. +OpenClaw connects to Discord as a bot over the official Discord gateway. DMs and guild channels are supported. @@ -21,45 +21,41 @@ Ready for DMs and guild channels via the official Discord gateway. ## Quick setup -You will need to create a new application with a bot, add the bot to your server, and pair it to OpenClaw. We recommend adding your bot to your own private server. If you don't have one yet, [create one first](https://support.discord.com/hc/en-us/articles/204849977-How-do-I-create-a-server) (choose **Create My Own > For me and my friends**). +Create a Discord application with a bot, add the bot to your server, and pair it with OpenClaw. Use a private server if you can; [create one first](https://support.discord.com/hc/en-us/articles/204849977-How-do-I-create-a-server) (**Create My Own > For me and my friends**) if needed. - Go to the [Discord Developer Portal](https://discord.com/developers/applications) and click **New Application**. Name it something like "OpenClaw". + In the [Discord Developer Portal](https://discord.com/developers/applications), click **New Application** and name it (for example "OpenClaw"). - Click **Bot** on the sidebar. Set the **Username** to whatever you call your OpenClaw agent. + Open **Bot** in the sidebar and set the **Username** to your agent's name. - Still on the **Bot** page, scroll down to **Privileged Gateway Intents** and enable: + Still on the **Bot** page, under **Privileged Gateway Intents** enable: - **Message Content Intent** (required) - - **Server Members Intent** (recommended; required for role allowlists and name-to-ID matching) - - **Presence Intent** (optional; only needed for presence updates) + - **Server Members Intent** (recommended; required for role allowlists, name-to-ID matching, and channel-audience access groups) + - **Presence Intent** (optional; only for presence updates) - Scroll back up on the **Bot** page and click **Reset Token**. + On the **Bot** page, click **Reset Token** and copy the token. Despite the name, this generates your first token — nothing is being "reset." - Copy the token and save it somewhere. This is your **Bot Token** and you will need it shortly. - - Click **OAuth2** on the sidebar. You'll generate an invite URL with the right permissions to add the bot to your server. - - Scroll down to **OAuth2 URL Generator** and enable: + Open **OAuth2** in the sidebar. In the **OAuth2 URL Generator**, enable the scopes: - `bot` - `applications.commands` - A **Bot Permissions** section will appear below. Enable at least: + In the **Bot Permissions** section that appears, enable at least: **General Permissions** - View Channels @@ -71,34 +67,33 @@ You will need to create a new application with a bot, add the bot to your server - Attach Files - Add Reactions (optional) - This is the baseline set for normal text channels. If you plan to post in Discord threads, including forum or media channel workflows that create or continue a thread, also enable **Send Messages in Threads**. - Copy the generated URL at the bottom, paste it into your browser, select your server, and click **Continue** to connect. You should now see your bot in the Discord server. + That is the baseline for normal text channels. If the bot will post in threads — including forum or media channel workflows that create or continue a thread — also enable **Send Messages in Threads**. + + Copy the generated URL, open it in a browser, select your server, and click **Continue**. The bot should now appear in your server. - Back in the Discord app, you need to enable Developer Mode so you can copy internal IDs. + In the Discord app, enable Developer Mode so you can copy IDs: - 1. Click **User Settings** (gear icon next to your avatar) → Scroll to **Developer** in sidebar → toggle on **Developer Mode** - - *(Note: On the Discord mobile app, Developer Mode is under **App Settings** → **Advanced**)* - - 2. Right-click your **server icon** in the sidebar → **Copy Server ID** + 1. **User Settings** (gear icon) → **Developer** → toggle on **Developer Mode** + *(on mobile: **App Settings** → **Advanced**)* + 2. Right-click your **server icon** → **Copy Server ID** 3. Right-click your **own avatar** → **Copy User ID** - Save your **Server ID** and **User ID** alongside your Bot Token — you'll send all three to OpenClaw in the next step. + Keep the Server ID and User ID with your bot token; you need all three next. - For pairing to work, Discord needs to allow your bot to DM you. Right-click your **server icon** → **Privacy Settings** → toggle on **Direct Messages**. + For pairing to work, Discord must let the bot DM you. Right-click your **server icon** → **Privacy Settings** → toggle on **Direct Messages**. - This lets server members (including bots) send you DMs. Keep this enabled if you want to use Discord DMs with OpenClaw. If you only plan to use guild channels, you can disable DMs after pairing. + Keep this on if you use Discord DMs with OpenClaw. If you only use guild channels, you can disable it after pairing. - Your Discord bot token is a secret (like a password). Set it on the machine running OpenClaw before messaging your agent. + The bot token is a secret. Set it on the machine running OpenClaw before messaging your agent: ```bash export DISCORD_BOT_TOKEN="YOUR_BOT_TOKEN" @@ -117,9 +112,9 @@ openclaw config patch --file ./discord.patch.json5 openclaw gateway ``` - If OpenClaw is already running as a background service, restart it via the OpenClaw Mac app or by stopping and restarting the `openclaw gateway run` process. - For managed service installs, run `openclaw gateway install` from a shell where `DISCORD_BOT_TOKEN` is present, or store the variable in `~/.openclaw/.env`, so the service can resolve the env SecretRef after restart. - If your host is blocked or rate-limited by Discord's startup application lookup, set the Discord application/client ID from the Developer Portal so startup can skip that REST call. Use `channels.discord.applicationId` for the default account, or `channels.discord.accounts..applicationId` when you run multiple Discord bots. + If OpenClaw already runs as a background service, restart it via the OpenClaw Mac app or by stopping and restarting the `openclaw gateway run` process. + For managed service installs, run `openclaw gateway install` from a shell where `DISCORD_BOT_TOKEN` is set, or store the variable in `~/.openclaw/.env` so the service can resolve the env SecretRef after restart. + If your host is blocked or rate-limited by Discord's startup application lookup, set the application/client ID from the Developer Portal so startup can skip that REST call: `channels.discord.applicationId` for the default account, or `channels.discord.accounts..applicationId` per bot. @@ -127,12 +122,12 @@ openclaw gateway - Chat with your OpenClaw agent on any existing channel (e.g. Telegram) and tell it. If Discord is your first channel, use the CLI / config tab instead. + Chat with your OpenClaw agent on an existing channel (for example Telegram) and tell it. If Discord is your first channel, use the CLI / config tab instead. > "I already set my Discord bot token in config. Please finish Discord setup with User ID `` and Server ID ``." - If you prefer file-based config, set: + File-based config: ```json5 { @@ -155,9 +150,9 @@ openclaw gateway DISCORD_BOT_TOKEN=... ``` - For scripted or remote setup, write the same JSON5 block with `openclaw config patch --file ./discord.patch.json5 --dry-run` and then rerun without `--dry-run`. Plaintext `token` values are supported. SecretRef values are also supported for `channels.discord.token` across env/file/exec providers. See [Secrets Management](/gateway/secrets). + For scripted or remote setup, write the same JSON5 block with `openclaw config patch --file ./discord.patch.json5 --dry-run`, then rerun without `--dry-run`. Plaintext `token` strings work too, and SecretRef values are supported for `channels.discord.token` across env/file/exec providers. See [Secrets Management](/gateway/secrets). - For multiple Discord bots, keep each bot token and application ID under its account. A top-level `channels.discord.applicationId` is inherited by accounts, so only set it there when every account should use the same application ID. + For multiple Discord bots, keep each bot token and application ID under its account. A top-level `channels.discord.applicationId` is inherited by accounts, so only set it there when every account uses the same application ID. ```json5 { @@ -185,7 +180,7 @@ DISCORD_BOT_TOKEN=... - Wait until the gateway is running, then DM your bot in Discord. It will respond with a pairing code. + Once the gateway is running, DM your bot in Discord. It replies with a pairing code. @@ -203,26 +198,24 @@ openclaw pairing approve discord - Pairing codes expire after 1 hour. - - You should now be able to chat with your agent in Discord via DM. + Pairing codes expire after 1 hour. After approval, chat with your agent in a Discord DM. -Token resolution is account-aware. Config token values win over env fallback. `DISCORD_BOT_TOKEN` is only used for the default account. -If two enabled Discord accounts resolve to the same bot token, OpenClaw starts only one gateway monitor for that token. A config-sourced token wins over the default env fallback; otherwise the first enabled account wins and the duplicate account is reported disabled. -For advanced outbound calls (message tool/channel actions), an explicit per-call `token` is used for that call. This applies to send and read/probe-style actions (for example read/search/fetch/thread/pins/permissions). Account policy/retry settings still come from the selected account in the active runtime snapshot. +Token resolution is account-aware. Config token values win over the env fallback, and `DISCORD_BOT_TOKEN` is only used for the default account. +If two enabled Discord accounts resolve to the same bot token, OpenClaw starts only one gateway monitor for that token: a config-sourced token wins over the env fallback; otherwise the first enabled account wins and the duplicate account is reported disabled with reason `duplicate bot token`. +For advanced outbound calls (message tool/channel actions), an explicit per-call `token` is used for that call. This applies to send and read/probe-style actions (read/search/fetch/thread/pins/permissions). Account policy/retry settings still come from the selected account in the active runtime snapshot. ## Recommended: Set up a guild workspace -Once DMs are working, you can set up your Discord server as a full workspace where each channel gets its own agent session with its own context. This is recommended for private servers where it's just you and your bot. +Once DMs work, you can turn your server into a full workspace where each channel gets its own agent session with its own context. Recommended for private servers where it is just you and your bot. - This enables your agent to respond in any channel on your server, not just DMs. + This lets your agent respond in any channel on your server, not just DMs. @@ -252,7 +245,7 @@ Once DMs are working, you can set up your Discord server as a full workspace whe - By default, your agent only responds in guild channels when @mentioned. For a private server, you probably want it to respond to every message. + By default, the agent only responds in guild channels when @mentioned. On a private server you probably want it to respond to every message. In guild channels, normal replies post automatically by default. For shared always-on rooms, opt into `messages.groupChat.visibleReplies: "message_tool"` so the agent can lurk and only post when it decides a channel reply is useful. This works best with latest-generation, tool-reliable models such as GPT 5.5. Ambient room events stay quiet unless the tool sends. See [Ambient room events](/channels/ambient-room-events) for the full lurk-mode config. @@ -287,53 +280,48 @@ Once DMs are working, you can set up your Discord server as a full workspace whe - By default, long-term memory (MEMORY.md) only loads in DM sessions. Guild channels do not auto-load MEMORY.md. + Long-term memory (MEMORY.md) only auto-loads in DM sessions; guild channels do not load it. > "When I ask questions in Discord channels, use memory_search or memory_get if you need long-term context from MEMORY.md." - If you need shared context in every channel, put the stable instructions in `AGENTS.md` or `USER.md` (they are injected for every session). Keep long-term notes in `MEMORY.md` and access them on demand with memory tools. + For shared context in every channel, put stable instructions in `AGENTS.md` or `USER.md` (injected for every session). Keep long-term notes in `MEMORY.md` and access them on demand with memory tools. -Now create some channels on your Discord server and start chatting. Your agent can see the channel name, and each channel gets its own isolated session — so you can set up `#coding`, `#home`, `#research`, or whatever fits your workflow. +Now create channels and start chatting. The agent sees the channel name, and each channel is an isolated session — set up `#coding`, `#home`, `#research`, or whatever fits your workflow. ## Runtime model - Gateway owns the Discord connection. - Reply routing is deterministic: Discord inbound replies back to Discord. -- Discord guild/channel metadata is added to the model prompt as untrusted - context, not as a user-visible reply prefix. If a model copies that envelope - back, OpenClaw strips the copied metadata from outbound replies and from - future replay context. +- Discord guild/channel metadata is added to the model prompt as untrusted context, not as a user-visible reply prefix. If a model copies that envelope back, OpenClaw strips the copied metadata from outbound replies and from future replay context. - By default (`session.dmScope=main`), direct chats share the agent main session (`agent:main:main`). - Guild channels are isolated session keys (`agent::discord:channel:`). - Group DMs are ignored by default (`channels.discord.dm.groupEnabled=false`). - Native slash commands run in isolated command sessions (`agent::discord:slash:`), while still carrying `CommandTargetSessionKey` to the routed conversation session. -- Text-only cron/heartbeat announce delivery to Discord uses the final - assistant-visible answer once. Media and structured component payloads remain - multi-message when the agent emits multiple deliverable payloads. +- Text-only cron/heartbeat announce delivery to Discord collapses to the final assistant-visible answer, sent once. Media and structured component payloads remain multi-message when the agent emits multiple deliverable payloads. ## Forum channels Discord forum and media channels only accept thread posts. OpenClaw supports two ways to create them: -- Send a message to the forum parent (`channel:`) to auto-create a thread. The thread title uses the first non-empty line of your message. +- Send a message to the forum parent (`channel:`) to auto-create a thread. The thread title is the first non-empty line of the message (truncated to Discord's 100-character thread-name limit). - Use `openclaw message thread create` to create a thread directly. Do not pass `--message-id` for forum channels. -Example: send to forum parent to create a thread +Send to the forum parent to create a thread: ```bash openclaw message send --channel discord --target channel: \ --message "Topic title\nBody of the post" ``` -Example: create a forum thread explicitly +Create a forum thread explicitly: ```bash openclaw message thread create --channel discord --target channel: \ @@ -344,7 +332,7 @@ Forum parents do not accept Discord components. If you need components, send to ## Interactive components -OpenClaw supports Discord components v2 containers for agent messages. Use the message tool with a `components` payload. Interaction results are routed back to the agent as normal inbound messages and follow the existing Discord `replyToMode` settings. +OpenClaw supports Discord components v2 containers for agent messages. Use the message tool with a `components` payload. Interaction results route back to the agent as normal inbound messages and follow the existing Discord `replyToMode` settings. Supported blocks: @@ -354,11 +342,11 @@ Supported blocks: By default, components are single use. Set `components.reusable=true` to allow buttons, selects, and forms to be used multiple times until they expire. -To restrict who can click a button, set `allowedUsers` on that button (Discord user IDs, tags, or `*`). When configured, unmatched users receive an ephemeral denial. +To restrict who can click a button, set `allowedUsers` on that button (Discord user IDs, tags, or `*`). Unmatched users receive an ephemeral denial. -Component callbacks expire after 30 minutes by default. Set `channels.discord.agentComponents.ttlMs` to change that callback registry lifetime for the default Discord account, or `channels.discord.accounts..agentComponents.ttlMs` to override one account in a multi-account setup. The value is milliseconds, must be a positive integer, and is capped at `86400000` (24 hours). Longer TTLs are useful for review or approval workflows that need buttons to remain usable, but they also extend the window where an old Discord message can still trigger an action. Prefer the shortest TTL that fits the workflow, and keep the default when stale callbacks would be surprising. +Component callbacks expire after 30 minutes by default. Set `channels.discord.agentComponents.ttlMs` to change the callback registry lifetime for the default account, or `channels.discord.accounts..agentComponents.ttlMs` per account. The value is milliseconds, must be a positive integer, and is capped at `86400000` (24 hours). Longer TTLs suit review/approval workflows that need buttons to stay usable, but they extend the window in which an old Discord message can still trigger an action. Prefer the shortest TTL that fits, and keep the default when stale callbacks would be surprising. -The `/model` and `/models` slash commands open an interactive model picker with provider, model, and compatible runtime dropdowns plus a Submit step. `/models add` is deprecated and now returns a deprecation message instead of registering models from chat. The picker reply is ephemeral and only the invoking user can use it. Discord select menus are limited to 25 options, so add `provider/*` entries to `agents.defaults.models` when you want the picker to show dynamically discovered models only for selected providers such as `openai` or `vllm`. +The `/model` and `/models` slash commands open an interactive model picker with provider, model, and compatible runtime dropdowns plus a Submit step. `/models add` is deprecated and returns a deprecation message instead of registering models from chat. The picker reply is ephemeral and only usable by the invoking user. Discord select menus are limited to 25 options, so add `provider/*` entries to `agents.defaults.models` when you want the picker to show dynamically discovered models only for selected providers such as `openai` or `vllm`. File attachments: @@ -433,7 +421,7 @@ Example: `channels.discord.dmPolicy` controls DM access. `channels.discord.allowFrom` is the canonical DM allowlist. - `pairing` (default) - - `allowlist` + - `allowlist` (requires at least one `allowFrom` sender) - `open` (requires `channels.discord.allowFrom` to include `"*"`) - `disabled` @@ -446,7 +434,7 @@ Example: - Named accounts inherit `channels.discord.allowFrom` when their own `allowFrom` and legacy `dm.allowFrom` are unset. - Named accounts do not inherit `channels.discord.accounts.default.allowFrom`. - Legacy `channels.discord.dm.policy` and `channels.discord.dm.allowFrom` still read for compatibility. `openclaw doctor --fix` migrates them to `dmPolicy` and `allowFrom` when it can do so without changing access. + Legacy `channels.discord.dm.policy` and `channels.discord.dm.allowFrom` are still read for compatibility. `openclaw doctor --fix` migrates them to `dmPolicy` and `allowFrom` when it can do so without changing access. DM target format for delivery: @@ -460,7 +448,7 @@ Example: Discord DMs and text command authorization can use dynamic `accessGroup:` entries in `channels.discord.allowFrom`. - Access group names are shared across message channels. Use `type: "message.senders"` for a static group whose members are expressed in each channel's normal `allowFrom` syntax, or `type: "discord.channelAudience"` when a Discord channel's current `ViewChannel` audience should define membership dynamically. Shared access-group behavior is documented here: [Access groups](/channels/access-groups). + Access group names are shared across message channels. Use `type: "message.senders"` for a static group whose members are expressed in each channel's normal `allowFrom` syntax, or `type: "discord.channelAudience"` when a Discord channel's current `ViewChannel` audience should define membership dynamically. Shared access-group behavior: [Access groups](/channels/access-groups). ```json5 { @@ -528,7 +516,7 @@ Example: Lookups fail closed. If Discord returns `Missing Access`, the member lookup fails, or the channel belongs to a different guild, the DM sender is treated as unauthorized. - Enable the Discord Developer Portal **Server Members Intent** for the bot when using channel-audience access groups. DMs do not include guild member state, so OpenClaw resolves the member through Discord REST at authorization time. + Enable the Discord Developer Portal **Server Members Intent** when using channel-audience access groups. DMs do not include guild member state, so OpenClaw resolves the member through Discord REST at authorization time. @@ -564,8 +552,8 @@ Example: users: ["987654321098765432"], roles: ["123456789012345678"], channels: { - general: { allow: true }, - help: { allow: true, requireMention: true }, + general: { enabled: true }, + help: { enabled: true, requireMention: true }, }, }, }, @@ -574,6 +562,8 @@ Example: } ``` + The legacy per-channel `allow` key is migrated to `enabled` by `openclaw doctor --fix`. + If you only set `DISCORD_BOT_TOKEN` and do not create a `channels.discord` block, runtime fallback is `groupPolicy="allowlist"` (with a warning in logs), even if `channels.defaults.groupPolicy` is `open`. @@ -632,13 +622,10 @@ Use `bindings[].match.roles` to route Discord guild members to different agents - Per-channel override: `channels.discord.commands.native`. - `commands.native=false` skips Discord slash-command registration and cleanup during startup. Previously registered commands may remain visible in Discord until you remove them from the Discord app. - Native command auth uses the same Discord allowlists/policies as normal message handling. -- Commands may still be visible in Discord UI for users who are not authorized; execution still enforces OpenClaw auth and returns "not authorized". +- Commands may still be visible in the Discord UI for unauthorized users; execution enforces OpenClaw auth and replies "not authorized". +- Default slash command settings: `ephemeral: true` (`channels.discord.slashCommand.ephemeral`). -See [Slash commands](/tools/slash-commands) for command catalog and behavior. - -Default slash command settings: - -- `ephemeral: true` +See [Slash commands](/tools/slash-commands) for the command catalog and behavior. ## Feature details @@ -651,24 +638,17 @@ Default slash command settings: Controlled by `channels.discord.replyToMode`: - - `off` (default) - - `first` - - `all` - - `batched` - - Note: `off` disables implicit reply threading. Explicit `[[reply_to_*]]` tags are still honored. - `first` always attaches the implicit native reply reference to the first outbound Discord message for the turn. - `batched` only attaches Discord's implicit native reply reference when the - inbound event was a debounced batch of multiple messages. This is useful - when you want native replies mainly for ambiguous bursty chats, not every - single-message turn. + - `off` (default): no implicit reply threading; explicit `[[reply_to_*]]` tags are still honored + - `first`: attaches the implicit native reply reference to the first outbound Discord message of the turn + - `all`: attaches it to every outbound message + - `batched`: attaches it only when the inbound event was a debounced batch of multiple messages — useful when you want native replies mainly for ambiguous bursty chats, not every single-message turn Message IDs are surfaced in context/history so agents can target specific messages. - Discord generates rich link embeds for URLs by default. OpenClaw suppresses those generated embeds on outbound Discord messages by default, so agent-sent URLs stay as plain links unless you opt in: + Discord generates rich link embeds for URLs by default. OpenClaw suppresses those generated embeds on outbound Discord messages by default, so agent-sent URLs stay plain links unless you opt in: ```json5 { @@ -685,9 +665,7 @@ Default slash command settings: - OpenClaw can stream draft replies by sending a temporary message and editing it as text arrives. `channels.discord.streaming` takes `off` | `partial` | `block` | `progress` (default). `progress` keeps one editable status draft and updates it with tool progress until final delivery; the shared starter label is a rolling line, so it scrolls away like the rest once enough work appears. `streamMode` is a legacy runtime alias. Run `openclaw doctor --fix` to rewrite persisted config to the canonical key. - - Set `channels.discord.streaming.mode` to `off` to disable Discord preview edits. If Discord block streaming is explicitly enabled, OpenClaw skips the preview stream to avoid double-streaming. + OpenClaw can stream draft replies by sending a temporary message and editing it as text arrives. `channels.discord.streaming.mode` takes `off` | `partial` | `block` | `progress` (default when no `streaming`/legacy `streamMode` key is set). `streamMode` is a legacy alias; run `openclaw doctor --fix` to rewrite persisted config to the canonical nested `streaming` shape. ```json5 { @@ -708,8 +686,10 @@ Default slash command settings: } ``` + - `off` disables Discord preview edits. - `partial` edits a single preview message as tokens arrive. - - `block` emits draft-sized chunks (use `draftChunk` to tune size and breakpoints, clamped to `textChunkLimit`). + - `block` emits draft-sized chunks; tune size and breakpoints with `streaming.preview.chunk` (`minChars`, `maxChars`, `breakPreference`), clamped to `textChunkLimit`. When block streaming is explicitly enabled, OpenClaw skips the preview stream to avoid double-streaming. + - `progress` keeps one editable status draft and updates it with tool progress until final delivery; the shared starter label is a rolling line, so it scrolls away like the rest once enough work appears. - Media, error, and explicit-reply finals cancel pending preview edits. - `streaming.preview.toolProgress` (default `true`) controls whether tool/progress updates reuse the preview message. - Tool/progress rows render as compact emoji + title + detail when available, for example `🛠️ Bash: run tests` or `🔎 Web Search: for "query"`. @@ -735,7 +715,7 @@ Default slash command settings: } ``` - Preview streaming is text-only; media replies fall back to normal delivery. When `block` streaming is explicitly enabled, OpenClaw skips the preview stream to avoid double-streaming. + Preview streaming is text-only; media replies fall back to normal delivery. @@ -754,8 +734,8 @@ Default slash command settings: Thread behavior: - Discord threads route as channel sessions and inherit parent channel config unless overridden. - - Thread sessions inherit the parent channel's session-level `/model` selection as a model-only fallback; thread-local `/model` selections still take precedence and parent transcript history is not copied unless transcript inheritance is enabled. - - `channels.discord.thread.inheritParent` (default `false`) opts new auto-threads into seeding from the parent transcript. Per-account overrides live under `channels.discord.accounts..thread.inheritParent`. + - Thread sessions inherit the parent channel's session-level `/model` selection as a model-only fallback; thread-local `/model` selections take precedence, and parent transcript history is not copied unless transcript inheritance is enabled. + - `channels.discord.thread.inheritParent` (default `false`) opts new auto-threads into seeding from the parent transcript. Per-account override: `channels.discord.accounts..thread.inheritParent`. - Message-tool reactions can resolve `user:` DM targets. - `guilds..channels..requireMention: false` is preserved during reply-stage activation fallback. @@ -801,8 +781,7 @@ Default slash command settings: Notes: - - `session.threadBindings.*` sets global defaults. - - `channels.discord.threadBindings.*` overrides Discord behavior. + - `session.threadBindings.*` sets global defaults; `channels.discord.threadBindings.*` overrides Discord behavior. - `spawnSessions` controls auto-create/bind threads for `sessions_spawn({ thread: true })` and ACP thread spawns. Default: `true`. - `defaultSpawnContext` controls native subagent context for thread-bound spawns. Default: `"fork"`. - Deprecated `spawnSubagentSessions`/`spawnAcpSessions` keys are migrated by `openclaw doctor --fix`. @@ -815,11 +794,7 @@ Default slash command settings: For stable "always-on" ACP workspaces, configure top-level typed ACP bindings targeting Discord conversations. - Config path: - - - `bindings[]` with `type: "acp"` and `match.channel: "discord"` - - Example: + Config path: `bindings[]` with `type: "acp"` and `match.channel: "discord"`. ```json5 { @@ -878,7 +853,7 @@ Default slash command settings: - Per-guild reaction notification mode: + Per-guild reaction notification mode (`guilds..reactionNotifications`): - `off` - `own` (default) @@ -890,7 +865,7 @@ Default slash command settings: - `ackReaction` sends an acknowledgement emoji while OpenClaw is processing an inbound message. + `ackReaction` sends an acknowledgement emoji while OpenClaw processes an inbound message. Resolution order: @@ -907,9 +882,7 @@ Default slash command settings: - Channel-initiated config writes are enabled by default. - - This affects `/config set|unset` flows (when command features are enabled). + Channel-initiated config writes are enabled by default. This affects `/config set|unset` flows (when command features are enabled). Disable: @@ -927,7 +900,7 @@ Default slash command settings: Route Discord gateway WebSocket traffic and startup REST lookups (application ID + allowlist resolution) through an HTTP(S) proxy with `channels.discord.proxy`. - Discord Gateway WebSocket proxying is explicit; WebSocket connections do not inherit ambient proxy environment variables from the Gateway process. Startup REST lookups use this proxy when `channels.discord.proxy` is configured. + Discord gateway WebSocket proxying is explicit; WebSocket connections do not inherit ambient proxy environment variables from the Gateway process. Startup REST lookups use this proxy when `channels.discord.proxy` is configured. ```json5 { @@ -977,8 +950,8 @@ Default slash command settings: - allowlists can use `pk:` - member display names are matched by name/slug only when `channels.discord.dangerouslyAllowNameMatching: true` - - lookups use original message ID and are time-window constrained - - if lookup fails, proxied messages are treated as bot messages and dropped unless `allowBots=true` + - lookups query the PluralKit API with the original message ID + - if lookup fails, proxied messages are treated as bot messages and dropped unless `allowBots` lets them through @@ -990,7 +963,7 @@ Default slash command settings: channels: { discord: { mentionAliases: { - Vladislava: "123456789012345678", + SupportLead: "123456789012345678", }, accounts: { ops: { @@ -1009,7 +982,7 @@ Default slash command settings: Presence updates are applied when you set a status or activity field, or when you enable auto presence. - Status only example: + Status only: ```json5 { @@ -1021,7 +994,7 @@ Default slash command settings: } ``` - Activity example (custom status is the default activity type): + Activity (custom status is the default activity type when `activity` is set): ```json5 { @@ -1034,7 +1007,7 @@ Default slash command settings: } ``` - Streaming example: + Streaming: ```json5 { @@ -1051,13 +1024,13 @@ Default slash command settings: Activity type map: - 0: Playing - - 1: Streaming (requires `activityUrl`) + - 1: Streaming (requires `activityUrl`; `activityUrl` in turn requires `activityType: 1`) - 2: Listening - 3: Watching - 4: Custom (uses the activity text as the status state; emoji is optional) - 5: Competing - Auto presence example (runtime health signal): + Auto presence (runtime health signal): ```json5 { @@ -1074,7 +1047,7 @@ Default slash command settings: } ``` - Auto presence maps runtime availability to Discord status: healthy => online, degraded or unknown => idle, exhausted or unavailable => dnd. Optional text overrides: + Auto presence maps runtime availability to Discord status: healthy => online, degraded or unknown => idle, exhausted or unavailable => dnd. Defaults: `intervalMs` 30000, `minUpdateIntervalMs` 15000 (must be less than or equal to `intervalMs`). Optional text overrides: - `autoPresence.healthyText` - `autoPresence.degradedText` @@ -1094,19 +1067,11 @@ Default slash command settings: Discord auto-enables native exec approvals when `enabled` is unset or `"auto"` and at least one approver can be resolved, either from `execApprovals.approvers` or from `commands.ownerAllowFrom`. Discord does not infer exec approvers from channel `allowFrom`, legacy `dm.allowFrom`, or direct-message `defaultTo`. Set `enabled: false` to disable Discord as a native approval client explicitly. - For sensitive owner-only group commands such as `/diagnostics` and `/export-trajectory`, OpenClaw sends approval prompts and final results privately. It tries Discord DM first when the invoking owner has a Discord owner route; if that is not available, it falls back to the first available owner route from `commands.ownerAllowFrom`, such as Telegram. + For sensitive owner-only group commands such as `/diagnostics` and `/export-trajectory`, OpenClaw sends approval prompts and final results privately. It tries Discord DM first when the invoking owner has a Discord owner route; otherwise it falls back to the first available owner route from `commands.ownerAllowFrom`, such as Telegram. When `target` is `channel` or `both`, the approval prompt is visible in the channel. Only resolved approvers can use the buttons; other users receive an ephemeral denial. Approval prompts include the command text, so only enable channel delivery in trusted channels. If the channel ID cannot be derived from the session key, OpenClaw falls back to DM delivery. - Discord also renders the shared approval buttons used by other chat channels. The native Discord adapter mainly adds approver DM routing and channel fanout. - When those buttons are present, they are the primary approval UX; OpenClaw - should only include a manual `/approve` command when the tool result says - chat approvals are unavailable or manual approval is the only path. - If the Discord native approval runtime is not active, OpenClaw keeps the - local deterministic `/approve ` prompt visible. If the - runtime is active but a native card cannot be delivered to any target, - OpenClaw sends a same-chat fallback notice with the exact `/approve` - command from the pending approval. + Discord renders the shared approval buttons used by other chat channels; the native Discord adapter mainly adds approver DM routing and channel fanout. When those buttons are present, they are the primary approval UX; OpenClaw should only include a manual `/approve` command when the tool result says chat approvals are unavailable or manual approval is the only path. If the Discord native approval runtime is not active, OpenClaw keeps the local deterministic `/approve ` prompt visible. If the runtime is active but a native card cannot be delivered to any target, OpenClaw sends a same-chat fallback notice with the exact `/approve` command from the pending approval. Gateway auth and approval resolution follow the shared Gateway client contract (`plugin:` IDs resolve through `plugin.approval.resolve`; other IDs through `exec.approval.resolve`). Approvals expire after 30 minutes by default. @@ -1117,7 +1082,7 @@ Default slash command settings: ## Tools and action gates -Discord message actions include messaging, channel admin, moderation, presence, and metadata actions. +Discord message actions cover messaging, channel admin, moderation, presence, and metadata. Core examples: @@ -1143,9 +1108,8 @@ Default gate behavior: OpenClaw uses Discord components v2 for exec approvals and cross-context markers. Discord message actions can also accept `components` for custom UI (advanced; requires constructing a component payload via the discord tool), while legacy `embeds` remain available but are not recommended. -- `channels.discord.ui.components.accentColor` sets the accent color used by Discord component containers (hex). -- Set per account with `channels.discord.accounts..ui.components.accentColor`. -- `channels.discord.agentComponents.ttlMs` controls how long sent Discord component callbacks remain registered (default `1800000`, maximum `86400000`). Set per account with `channels.discord.accounts..agentComponents.ttlMs`. +- `channels.discord.ui.components.accentColor` sets the accent color used by Discord component containers (hex). Per account: `channels.discord.accounts..ui.components.accentColor`. +- `channels.discord.agentComponents.ttlMs` controls how long sent Discord component callbacks remain registered (default `1800000`, maximum `86400000`). Per account: `channels.discord.accounts..agentComponents.ttlMs`. - `embeds` are ignored when components v2 are present. - Plain URL previews are suppressed by default. Set `suppressEmbeds: false` on a message action when a single outbound link should expand. @@ -1188,7 +1152,7 @@ Use `/vc join|leave|status` to control sessions. The command uses the account de /vc leave ``` -To inspect the bot's effective permissions before joining, run: +To inspect the bot's effective permissions before joining: ```bash openclaw channels capabilities --channel discord --target channel: @@ -1232,40 +1196,37 @@ Auto-join example: Notes: -- `voice.tts` overrides `messages.tts` for `stt-tts` voice playback only. Realtime modes use `voice.realtime.speakerVoice`. +- Discord voice is opt-in for text-only configs; set `channels.discord.voice.enabled=true` (or keep an existing `channels.discord.voice` block) to enable `/vc` commands, the voice runtime, and the `GuildVoiceStates` gateway intent. `channels.discord.intents.voiceStates` can explicitly override the intent subscription; leave it unset to follow effective voice enablement. - `voice.mode` controls the conversation path. The default is `agent-proxy`: a realtime voice front end handles turn timing, interruption, and playback, delegates substantive work to the routed OpenClaw agent through `openclaw_agent_consult`, and treats the result like a typed Discord prompt from that speaker. `stt-tts` keeps the older batch STT plus TTS flow. `bidi` lets the realtime model converse directly while exposing `openclaw_agent_consult` for the OpenClaw brain. - `voice.agentSession` controls which OpenClaw conversation receives voice turns. Leave it unset for the voice channel's own session, or set `{ mode: "target", target: "channel:" }` to make the voice channel act as the microphone/speaker extension of an existing Discord text channel session such as `#maintainers`. - `voice.model` overrides the OpenClaw agent brain for Discord voice responses and realtime consults. Leave it unset to inherit the routed agent model. It is separate from `voice.realtime.model`. -- `voice.followUsers` lets the bot join, move, and leave Discord voice with selected users. See [Follow users in voice](#follow-users-in-voice) for behavior rules and examples. +- `voice.followUsers` lets the bot join, move, and leave Discord voice with selected users. See [Follow users in voice](#follow-users-in-voice). - `agent-proxy` routes speech through `discord-voice`, which preserves normal owner/tool authorization for the speaker and target session but hides the agent `tts` tool because Discord voice owns playback. By default, `agent-proxy` gives the consult full owner-equivalent tool access for owner speakers (`voice.realtime.toolPolicy: "owner"`) and strongly prefers consulting the OpenClaw agent before substantive answers (`voice.realtime.consultPolicy: "always"`). In that default `always` mode, the realtime layer does not auto-speak filler before the consult answer; it captures and transcribes speech, then speaks the routed OpenClaw answer. If multiple forced consult answers finish while Discord is still playing the first answer, later exact-speech answers are queued until playback idles instead of replacing speech mid-sentence. - In `stt-tts` mode, STT uses `tools.media.audio`; `voice.model` does not affect transcription. - In realtime modes, `voice.realtime.provider`, `voice.realtime.model`, and `voice.realtime.speakerVoice` configure the realtime audio session. For OpenAI Realtime 2 plus the Codex brain, use `voice.realtime.model: "gpt-realtime-2"` and `voice.model: "openai/gpt-5.5"`. -- Realtime voice modes include small `IDENTITY.md`, `USER.md`, and `SOUL.md` profile files in the realtime provider instructions by default so fast direct turns keep the same identity, user grounding, and persona as the routed OpenClaw agent. Set `voice.realtime.bootstrapContextFiles` to a subset to customize this, or `[]` to disable it. The supported realtime bootstrap files are limited to those profile files; `AGENTS.md` stays in the normal agent context. The injected profile context does not replace `openclaw_agent_consult` for workspace work, current facts, memory lookup, or tool-backed actions. +- Realtime voice modes include small `IDENTITY.md`, `USER.md`, and `SOUL.md` profile files in the realtime provider instructions by default so fast direct turns keep the same identity, user grounding, and persona as the routed OpenClaw agent. Set `voice.realtime.bootstrapContextFiles` to a subset to customize this, or `[]` to disable it. Only those profile files are supported; `AGENTS.md` stays in the normal agent context. The injected profile context does not replace `openclaw_agent_consult` for workspace work, current facts, memory lookup, or tool-backed actions. - In OpenAI `agent-proxy` realtime mode, set `voice.realtime.requireWakeName: true` to keep Discord realtime voice silent until a transcript starts or ends with a wake name. Configured wake names must be one or two words. If `voice.realtime.wakeNames` is unset, OpenClaw uses the routed agent `name` plus `OpenClaw`, falling back to the agent id plus `OpenClaw`. Wake-name gating disables realtime provider auto-response, routes accepted turns through the OpenClaw agent consult path, and gives a short spoken acknowledgement when a leading wake name is recognized from partial transcription before the final transcript arrives. - The OpenAI realtime provider accepts current Realtime 2 event names and legacy Codex-compatible aliases for output audio and transcript events, so compatible provider snapshots can drift without dropping assistant audio. - `voice.realtime.bargeIn` controls whether Discord speaker-start events interrupt active realtime playback. If unset, it follows the realtime provider's input-audio interruption setting. - `voice.realtime.minBargeInAudioEndMs` controls the minimum assistant playback duration before an OpenAI realtime barge-in truncates audio. Default: `250`. Set `0` for immediate interruption in low-echo rooms, or raise it for echo-heavy speaker setups. -- For an OpenAI voice on Discord playback, set `voice.tts.provider: "openai"` and choose a Text-to-speech voice under `voice.tts.providers.openai.speakerVoice`. `cedar` is a good masculine-sounding choice on the current OpenAI TTS model. +- `voice.tts` overrides `messages.tts` for `stt-tts` voice playback only; realtime modes use `voice.realtime.speakerVoice` instead. For an OpenAI voice on Discord playback, set `voice.tts.provider: "openai"` and choose a Text-to-speech voice under `voice.tts.providers.openai.speakerVoice`. `cedar` is a good masculine-sounding choice on the current OpenAI TTS model. - Per-channel Discord `systemPrompt` overrides apply to voice transcript turns for that voice channel. - Voice transcript turns derive owner status from Discord `allowFrom` (or `dm.allowFrom`) for owner-gated commands and channel actions. Agent tool visibility follows the configured tool policy for the routed session. -- Discord voice is opt-in for text-only configs; set `channels.discord.voice.enabled=true` (or keep an existing `channels.discord.voice` block) to enable `/vc` commands, the voice runtime, and the `GuildVoiceStates` gateway intent. -- `channels.discord.intents.voiceStates` can explicitly override voice-state intent subscription. Leave it unset for the intent to follow effective voice enablement. - If `voice.autoJoin` has multiple entries for the same guild, OpenClaw joins the last configured channel for that guild. - `voice.allowedChannels` is an optional residency allowlist. Leave it unset to allow `/vc join` into any authorized Discord voice channel. When set, `/vc join`, startup auto-join, and bot voice-state moves are restricted to the listed `{ guildId, channelId }` entries. Set it to an empty array to deny all Discord voice joins. If Discord moves the bot outside the allowlist, OpenClaw leaves that channel and rejoins the configured auto-join target when one is available. -- `voice.daveEncryption` and `voice.decryptionFailureTolerance` pass through to `@discordjs/voice` join options. -- `@discordjs/voice` defaults are `daveEncryption=true` and `decryptionFailureTolerance=24` if unset. +- `voice.daveEncryption` and `voice.decryptionFailureTolerance` pass through to `@discordjs/voice` join options; the upstream defaults are `daveEncryption=true` and `decryptionFailureTolerance=24`. - OpenClaw uses the bundled `libopus-wasm` codec for Discord voice receive and realtime raw PCM playback. It ships a pinned libopus WebAssembly build and does not require native opus addons. - `voice.connectTimeoutMs` controls the initial `@discordjs/voice` Ready wait for `/vc join` and auto-join attempts. Default: `30000`. - `voice.reconnectGraceMs` controls how long OpenClaw waits for a disconnected voice session to begin reconnecting before destroying it. Default: `15000`. - In `stt-tts` mode, voice playback does not stop just because another user starts speaking. To avoid feedback loops, OpenClaw ignores new voice capture while TTS is playing; speak after playback finishes for the next turn. Realtime modes forward speaker starts as barge-in signals to the realtime provider. - In realtime modes, echo from speakers into an open mic can look like barge-in and interrupt playback. For echo-heavy Discord rooms, set `voice.realtime.providers.openai.interruptResponseOnInputAudio: false` to keep OpenAI from auto-interrupting on input audio. Add `voice.realtime.bargeIn: true` if you still want Discord speaker-start events to interrupt active playback. The OpenAI realtime bridge ignores playback truncations shorter than `voice.realtime.minBargeInAudioEndMs` as likely echo/noise and logs them as skipped instead of clearing Discord playback. -- `voice.captureSilenceGraceMs` controls how long OpenClaw waits after Discord reports a speaker has stopped before finalizing that audio segment for STT. Default: `2000`; raise this if Discord splits normal pauses into choppy partial transcripts. +- `voice.captureSilenceGraceMs` controls how long OpenClaw waits after Discord reports a speaker has stopped before finalizing that audio segment for STT. Default: `2000`; raise it if Discord splits normal pauses into choppy partial transcripts. - When ElevenLabs is the selected TTS provider, Discord voice playback uses streaming TTS and starts from the provider response stream. Providers without streaming support fall back to the synthesized temp-file path. -- OpenClaw also watches receive decrypt failures and auto-recovers by leaving/rejoining the voice channel after repeated failures in a short window. +- OpenClaw watches receive decrypt failures and auto-recovers by leaving/rejoining the voice channel after repeated failures in a short window. - If receive logs repeatedly show `DecryptionFailed(UnencryptedWhenPassthroughDisabled)` after updating, collect a dependency report and logs. The bundled `@discordjs/voice` line includes the upstream padding fix from discord.js PR #11449, which closed discord.js issue #11419. - `The operation was aborted` receive events are expected when OpenClaw finalizes a captured speaker segment; they are verbose diagnostics, not warnings. - Verbose Discord voice logs include a bounded one-line STT transcript preview for each accepted speaker segment, so debugging shows both the user side and the agent reply side without dumping unbounded transcript text. -- In `agent-proxy` mode, forced consult fallback skips likely incomplete transcript fragments such as text ending in `...` or a trailing connector like `and`, plus obvious non-actionable closings like “be right back” or “bye”. Logs show `forced agent consult skipped reason=...` when this prevents a stale queued answer. +- In `agent-proxy` mode, forced consult fallback skips likely incomplete transcript fragments such as text ending in `...` or a trailing connector like "and", plus obvious non-actionable closings like "be right back" or "bye". Logs show `forced agent consult skipped reason=...` when this prevents a stale queued answer. ### Follow users in voice @@ -1534,7 +1495,7 @@ message(action="send", channel="discord", target="channel:123", path="/path/to/a - verify `groupPolicy` - verify guild allowlist under `channels.discord.guilds` - - if guild `channels` map exists, only listed channels are allowed + - if a guild `channels` map exists, only listed channels are allowed - verify `requireMention` behavior and mention patterns Useful checks: @@ -1551,7 +1512,7 @@ openclaw logs --follow Common causes: - `groupPolicy="allowlist"` without matching guild/channel allowlist - - `requireMention` configured in the wrong place (must be under `channels.discord.guilds` or channel entry) + - `requireMention` configured in the wrong place (must be under `channels.discord.guilds` or a channel entry) - sender blocked by guild/channel `users` allowlist @@ -1672,16 +1633,16 @@ openclaw logs --follow maxEventsPerWindow: 4, }, accounts: { - mantis: { - // Mantis listens to other bots only when they mention her. + alpha: { + // Alpha listens to other bots only when they mention it. allowBots: "mentions", }, - molty: { - // Molty listens to all bot-authored Discord messages. + bravo: { + // Bravo listens to all bot-authored Discord messages. allowBots: true, mentionAliases: { - // Lets Molty write a Mantis Discord mention with the configured user id. - Mantis: "MANTIS_DISCORD_USER_ID", + // Lets Bravo write an Alpha Discord mention with the configured user id. + Alpha: "ALPHA_DISCORD_USER_ID", }, botLoopProtection: { // Allow up to five messages per minute before suppressing the pair. @@ -1717,17 +1678,17 @@ Primary reference: [Configuration reference - Discord](/gateway/config-channels# -- startup/auth: `enabled`, `token`, `accounts.*`, `allowBots` -- policy: `groupPolicy`, `dm.*`, `guilds.*`, `guilds.*.channels.*` -- command: `commands.native`, `commands.useAccessGroups`, `configWrites`, `slashCommand.*` -- event queue: `eventQueue.listenerTimeout` (listener budget), `eventQueue.maxQueueSize`, `eventQueue.maxConcurrency` -- gateway: `gatewayInfoTimeoutMs`, `gatewayReadyTimeoutMs`, `gatewayRuntimeReadyTimeoutMs` +- startup/auth: `enabled`, `token`, `applicationId`, `accounts.*`, `allowBots` +- policy: `groupPolicy`, `dmPolicy`, `allowFrom`, `dm.*`, `guilds.*`, `guilds.*.channels.*` +- command: `commands.native`, `commands.useAccessGroups` (global), `configWrites`, `slashCommand.ephemeral` +- event queue: `eventQueue.listenerTimeout` (listener budget, default `120000`), `eventQueue.maxQueueSize` (default `10000`), `eventQueue.maxConcurrency` (default `50`) +- gateway: `proxy`, `gatewayInfoTimeoutMs`, `gatewayReadyTimeoutMs`, `gatewayRuntimeReadyTimeoutMs` - reply/history: `replyToMode`, `historyLimit`, `dmHistoryLimit`, `dms.*.historyLimit` -- delivery: `textChunkLimit`, `chunkMode`, `maxLinesPerMessage` -- streaming: `streaming` (legacy alias: `streamMode`), `streaming.preview.toolProgress`, `draftChunk`, `blockStreaming`, `blockStreamingCoalesce` -- media/retry: `mediaMaxMb` (caps outbound Discord uploads, default `100MB`), `retry` +- delivery: `textChunkLimit` (default `2000`), `maxLinesPerMessage` (default `17`) +- streaming: `streaming.mode`, `streaming.chunkMode`, `streaming.preview.*`, `streaming.progress.*`, `streaming.block.*` (legacy flat `streamMode`, `draftChunk`, `blockStreaming`, `blockStreamingCoalesce`, `chunkMode` keys are migrated into `streaming.*` by `openclaw doctor --fix`) +- media/retry: `mediaMaxMb` (caps outbound Discord uploads, default `100`), `retry` - actions: `actions.*` -- presence: `activity`, `status`, `activityType`, `activityUrl` +- presence: `activity`, `status`, `activityType`, `activityUrl`, `autoPresence.*` - UI: `ui.components.accentColor` - features: `threadBindings`, top-level `bindings[]` (`type: "acp"`), `pluralkit`, `execApprovals`, `intents`, `agentComponents.enabled`, `agentComponents.ttlMs`, `heartbeat`, `responsePrefix` @@ -1737,7 +1698,7 @@ Primary reference: [Configuration reference - Discord](/gateway/config-channels# - Treat bot tokens as secrets (`DISCORD_BOT_TOKEN` preferred in supervised environments). - Grant least-privilege Discord permissions. -- If command deploy/state is stale, restart gateway and re-check with `openclaw channels status --probe`. +- If command deploy/state is stale, restart the gateway and re-check with `openclaw channels status --probe`. ## Related diff --git a/docs/channels/feishu.md b/docs/channels/feishu.md index ab773cb64c3b..0b68a7fff69c 100644 --- a/docs/channels/feishu.md +++ b/docs/channels/feishu.md @@ -6,11 +6,9 @@ read_when: title: Feishu --- -Feishu/Lark is an all-in-one collaboration platform where teams chat, share documents, manage calendars, and get work done together. +OpenClaw connects to Feishu/Lark (the all-in-one collaboration platform) through the official `@openclaw/feishu` plugin: bot DMs, group chats, streaming card replies, and Feishu doc/wiki/drive/Bitable tools. -**Status:** production-ready for bot DMs + group chats. WebSocket is the default mode; webhook mode is optional. - ---- +**Status:** production-ready for bot DMs + group chats. WebSocket is the default event transport (no public URL needed); webhook mode is optional. ## Quick start @@ -23,9 +21,14 @@ Requires OpenClaw 2026.5.29 or above. Run `openclaw --version` to check. Upgrade ```bash openclaw channels login --channel feishu ``` - Choose manual setup to paste an App ID and App Secret from Feishu Open Platform, or choose QR setup to create a bot automatically. If the domestic Feishu mobile app does not react to the QR code, rerun setup and choose manual setup. - - + This installs the `@openclaw/feishu` plugin if it is missing, then walks through setup: + +- **Manual setup**: paste an App ID and App Secret from Feishu Open Platform (`https://open.feishu.cn`) or Lark Developer (`https://open.larksuite.com`). +- **QR setup**: scan a QR code in the Feishu app to create a bot automatically. This flow locks DMs to your own account (`dmPolicy: "allowlist"` with your `open_id`). + +The wizard also asks for the API domain (Feishu vs Lark) and the group policy. If the domestic Feishu mobile app does not react to the QR code, rerun setup and choose manual setup. + + ```bash openclaw gateway restart @@ -33,17 +36,17 @@ Requires OpenClaw 2026.5.29 or above. Run `openclaw --version` to check. Upgrade ---- - ## Access control ### Direct messages -Configure `dmPolicy` to control who can DM the bot: +Configure `channels.feishu.dmPolicy` (default: `pairing`) to control who can DM the bot: -- `"pairing"` - unknown users receive a pairing code; approve via CLI -- `"allowlist"` - only users listed in `allowFrom` can chat -- `"open"` - allow public DMs only when `allowFrom` includes `"*"`; with restrictive entries, only matching users can chat +| Value | Behavior | +| ------------- | ------------------------------------------------------------------------------------------------------------- | +| `"pairing"` | Unknown users receive a pairing code; approve via CLI | +| `"allowlist"` | Only users listed in `allowFrom` can chat | +| `"open"` | Public DMs; config validation requires `allowFrom` to include `"*"`. Non-wildcard entries still narrow access | **Approve a pairing request:** @@ -54,7 +57,7 @@ openclaw pairing approve feishu ### Group chats -**Group policy** (`channels.feishu.groupPolicy`): +**Group policy** (`channels.feishu.groupPolicy`, default: `allowlist`): | Value | Behavior | | ------------- | -------------------------------------------------------------------------------------------- | @@ -62,17 +65,12 @@ openclaw pairing approve feishu | `"allowlist"` | Only respond to groups in `groupAllowFrom` or explicitly configured under `groups.` | | `"disabled"` | Disable all group messages; explicit `groups.` entries do not override this | -Default: `allowlist` - **Mention requirement** (`channels.feishu.requireMention`): -- `true` - require @mention (default) -- `false` - respond without @mention -- Per-group override: `channels.feishu.groups..requireMention` +- Default: @mention required, except when the effective group policy is `"open"`; there it defaults to `false` so messages that cannot carry mentions (for example images) still reach the agent. +- Set `true` or `false` explicitly to override; per-group override: `channels.feishu.groups..requireMention`. - Broadcast-only `@all` and `@_all` are not treated as bot mentions. A message that mentions both `@all` and the bot directly still counts as a bot mention. ---- - ## Group configuration examples ### Allow all groups, no @mention required @@ -81,7 +79,7 @@ Default: `allowlist` { channels: { feishu: { - groupPolicy: "open", + groupPolicy: "open", // requireMention defaults to false under "open" }, }, } @@ -150,7 +148,7 @@ In `allowlist` mode, you can also admit a group by adding an explicit `groups. @@ -176,8 +174,6 @@ Look for `open_id` in the log output. You can also check pending pairing request openclaw pairing list feishu ``` ---- - ## Common commands | Command | Description | @@ -190,8 +186,6 @@ openclaw pairing list feishu Feishu/Lark does not support native slash-command menus, so send these as plain text messages. ---- - ## Troubleshooting ### Bot does not respond in group chats @@ -223,8 +217,6 @@ Feishu/Lark does not support native slash-command menus, so send these as plain 2. Update the value in your config 3. Restart the gateway: `openclaw gateway restart` ---- - ## Advanced configuration ### Multiple accounts @@ -257,20 +249,18 @@ Feishu/Lark does not support native slash-command menus, so send these as plain } ``` -`defaultAccount` controls which account is used when outbound APIs do not specify an `accountId`. -`accounts..tts` uses the same shape as `messages.tts` and deep-merges over -global TTS config, so multi-bot Feishu setups can keep shared provider -credentials globally while overriding only voice, model, persona, or auto mode -per account. +`defaultAccount` controls which account is used when outbound APIs do not specify an `accountId`. Account entries inherit top-level settings; most top-level keys can be overridden per account. +`accounts..tts` uses the same shape as `messages.tts` and deep-merges over global TTS config, so multi-bot Feishu setups can keep shared provider credentials globally while overriding only voice, model, persona, or auto mode per account. ### Message limits -- `textChunkLimit` - outbound text chunk size (default: `2000` chars) +- `textChunkLimit` - outbound text chunk size (default: `4000` chars) +- `chunkMode` - `"length"` (default) splits at the limit; `"newline"` prefers newline boundaries - `mediaMaxMb` - media upload/download limit (default: `30` MB) ### Streaming -Feishu/Lark supports streaming replies via interactive cards. When enabled, the bot updates the card in real time as it generates text. +Feishu/Lark supports streaming replies via interactive cards (Card Kit streaming API). When enabled, the bot updates the card in real time as it generates text. ```json5 { @@ -283,7 +273,7 @@ Feishu/Lark supports streaming replies via interactive cards. When enabled, the } ``` -Set `streaming: false` to send the complete reply in one message. `blockStreaming` is off by default; enable it only when you want completed assistant blocks flushed before the final reply. +Set `streaming: false` to send the complete reply in one message; `renderMode: "raw"` (plain text instead of cards) also disables streaming cards. `blockStreaming` is off by default; enable it only when you want completed assistant blocks flushed before the final reply. ### Quota optimization @@ -303,6 +293,37 @@ Reduce the number of Feishu/Lark API calls with two optional flags: } ``` +### Group session scope and topic threads + +`channels.feishu.groupSessionScope` (top-level, per account, or per group) controls how group messages map to agent sessions: + +| Value | Session | +| ---------------------- | ---------------------------------------------------------------- | +| `"group"` (default) | One session per group chat | +| `"group_sender"` | One session per (group + sender) | +| `"group_topic"` | One session per topic thread; falls back to the group session | +| `"group_topic_sender"` | One session per (topic + sender); falls back to (group + sender) | + +For the topic scopes, native Feishu/Lark topic groups use the event `thread_id` (`omt_*`) as the canonical topic session key. If a native topic starter event omits `thread_id`, OpenClaw hydrates it from Feishu before routing the turn. Normal group replies that OpenClaw turns into threads keep using the reply root message ID (`om_*`) so the first turn and follow-up turns stay in the same session. + +Set `replyInThread: "enabled"` (top-level or per group) to make bot replies create or continue a Feishu topic thread instead of replying inline. `topicSessionMode` is the deprecated predecessor of `groupSessionScope`; prefer `groupSessionScope`. + +### Feishu workspace tools + +The plugin ships agent tools for Feishu documents, chats, knowledge base, cloud storage, permissions, and Bitable, plus matching skills (`feishu-doc`, `feishu-drive`, `feishu-perm`, `feishu-wiki`). Tool families are gated by `channels.feishu.tools`: + +| Key | Tools | Default | +| --------------- | --------------------------------------------- | ------------------- | +| `tools.doc` | `feishu_doc` document operations | `true` | +| `tools.chat` | `feishu_chat` chat info + member queries | `true` | +| `tools.wiki` | `feishu_wiki` knowledge base (requires `doc`) | `true` | +| `tools.drive` | `feishu_drive` cloud storage | `true` | +| `tools.perm` | `feishu_perm` permission management | `false` (sensitive) | +| `tools.scopes` | `feishu_app_scopes` app scope diagnostics | `true` | +| `tools.bitable` | `feishu_bitable_*` Bitable/Base operations | `true` | + +`tools.base` is an alias for `tools.bitable`; the explicit `bitable` value wins when both are set. Per-account gates live under `accounts..tools`. + ### ACP sessions Feishu/Lark supports ACP for DMs and group thread messages. Feishu/Lark ACP is text-command driven - there are no native slash-command menus, so use `/acp ...` messages directly in the conversation. @@ -401,8 +422,6 @@ Routing fields: See [Get group/user IDs](#get-groupuser-ids) for lookup tips. ---- - ## Per-user agent isolation (Dynamic Agent Creation) Enable `dynamicAgentCreation` to automatically create **isolated agent instances** for each DM user. Each user gets their own: @@ -475,6 +494,7 @@ Template variables: | Value | Behavior | Best for | | ---------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------ | | `"main"` | Each user's DM maps to their agent's main session | Single-user bots where you want `USER.md` / `SOUL.md` to auto-load | +| `"per-peer"` | Each peer gets a separate session (regardless of channel) | Isolation keyed by sender identity only | | `"per-channel-peer"` | Each (channel + user) combination gets a separate session | Public multi-user bots needing stronger isolation | | `"per-account-channel-peer"` | Each (account + channel + user) combination gets a separate session | Multi-account bots needing account-level session isolation | @@ -484,18 +504,6 @@ Template variables: Use `"per-account-channel-peer"` when named Feishu accounts should keep separate sessions for the same sender. Dynamic bindings preserve the account scope. -```json5 -{ - session: { - // For single-user personal bots: enables auto bootstrap loading - dmScope: "main", - - // For public multi-user bots: stronger isolation - // dmScope: "per-channel-peer", - }, -} -``` - ### Typical multi-user deployment ```json5 @@ -528,10 +536,10 @@ Use `"per-account-channel-peer"` when named Feishu accounts should keep separate Check gateway logs to confirm dynamic creation is working: -``` +```text feishu: creating dynamic agent "feishu-ou_xxxxxx" for user ou_xxxxxx -workspace: /Users/you/.openclaw/workspace-feishu-ou_xxxxxx -feishu: dynamic agent created, new route: agent:feishu-ou_xxxxxx:main + workspace: /home/user/.openclaw/workspace-feishu-ou_xxxxxx + agentDir: /home/user/.openclaw/agents/feishu-ou_xxxxxx/agent ``` List all created workspaces: @@ -544,54 +552,65 @@ ls -la ~/.openclaw/workspace-* - **Workspace isolation**: Each user gets their own workspace directory and agent instance. Users cannot see each other's conversation history or files within the normal messaging flow. - **Security boundary**: This is a messaging-context isolation mechanism, not a hostile co-tenant security boundary. The agent process and host environment are shared. +- **Config writes must stay enabled**: Dynamic agent creation writes agents and bindings into the config; it is skipped when `channels.feishu.configWrites` is `false` (default: enabled). - **`bindings` should be empty**: Dynamic agents auto-register their own bindings - **Upgrade path**: Existing manual bindings continue to work alongside dynamic agents - **`session.dmScope` is global**: This affects all channels, not just Feishu ---- - ## Configuration reference Full configuration: [Gateway configuration](/gateway/configuration) -| Setting | Description | Default | -| -------------------------------------------------------- | -------------------------------------------------------------------------------- | ------------------------------------ | -| `channels.feishu.enabled` | Enable/disable the channel | `true` | -| `channels.feishu.domain` | API domain (`feishu` or `lark`) | `feishu` | -| `channels.feishu.connectionMode` | Event transport (`websocket` or `webhook`) | `websocket` | -| `channels.feishu.defaultAccount` | Default account for outbound routing | `default` | -| `channels.feishu.verificationToken` | Required for webhook mode | - | -| `channels.feishu.encryptKey` | Required for webhook mode | - | -| `channels.feishu.webhookPath` | Webhook route path | `/feishu/events` | -| `channels.feishu.webhookHost` | Webhook bind host | `127.0.0.1` | -| `channels.feishu.webhookPort` | Webhook bind port | `3000` | -| `channels.feishu.accounts..appId` | App ID | - | -| `channels.feishu.accounts..appSecret` | App Secret | - | -| `channels.feishu.accounts..domain` | Per-account domain override | `feishu` | -| `channels.feishu.accounts..tts` | Per-account TTS override | `messages.tts` | -| `channels.feishu.dmPolicy` | DM policy | `pairing` | -| `channels.feishu.allowFrom` | DM allowlist (open_id list) | - | -| `channels.feishu.groupPolicy` | Group policy | `allowlist` | -| `channels.feishu.groupAllowFrom` | Group allowlist | - | -| `channels.feishu.requireMention` | Require @mention in groups | `true` | -| `channels.feishu.groups..requireMention` | Per-group @mention override; explicit IDs also admit the group in allowlist mode | inherited | -| `channels.feishu.groups..enabled` | Enable/disable a specific group | `true` | -| `channels.feishu.dynamicAgentCreation.enabled` | Enable automatic per-user agent creation | `false` | -| `channels.feishu.dynamicAgentCreation.workspaceTemplate` | Path template for dynamic agent workspaces | `~/.openclaw/workspace-{agentId}` | -| `channels.feishu.dynamicAgentCreation.agentDirTemplate` | Agent directory name template | `~/.openclaw/agents/{agentId}/agent` | -| `channels.feishu.dynamicAgentCreation.maxAgents` | Maximum number of dynamic agents to create | unlimited | -| `channels.feishu.textChunkLimit` | Message chunk size | `2000` | -| `channels.feishu.mediaMaxMb` | Media size limit | `30` | -| `channels.feishu.streaming` | Streaming card output | `true` | -| `channels.feishu.blockStreaming` | Completed-block reply streaming | `false` | -| `channels.feishu.typingIndicator` | Send typing reactions | `true` | -| `channels.feishu.resolveSenderNames` | Resolve sender display names | `true` | -| `channels.feishu.tools.bitable` | Enable Bitable/Base tools | `true` | -| `channels.feishu.tools.base` | Alias for `channels.feishu.tools.bitable`; explicit `bitable` wins when both set | `true` | -| `channels.feishu.accounts..tools.bitable` | Per-account Bitable/Base tool gate | inherited | -| `channels.feishu.accounts..tools.base` | Per-account alias for `tools.bitable` | inherited | - ---- +| Setting | Description | Default | +| -------------------------------------------------------- | ------------------------------------------------------------------------------------ | ------------------------------------ | +| `channels.feishu.enabled` | Enable/disable the channel | `true` | +| `channels.feishu.domain` | API domain (`feishu`, `lark`, or an `https://` base URL) | `feishu` | +| `channels.feishu.connectionMode` | Event transport (`websocket` or `webhook`) | `websocket` | +| `channels.feishu.defaultAccount` | Default account for outbound routing | `default` | +| `channels.feishu.verificationToken` | Required for webhook mode | - | +| `channels.feishu.encryptKey` | Required for webhook mode | - | +| `channels.feishu.webhookPath` | Webhook route path | `/feishu/events` | +| `channels.feishu.webhookHost` | Webhook bind host | `127.0.0.1` | +| `channels.feishu.webhookPort` | Webhook bind port | `3000` | +| `channels.feishu.accounts..appId` | App ID | - | +| `channels.feishu.accounts..appSecret` | App Secret | - | +| `channels.feishu.accounts..domain` | Per-account domain override | `feishu` | +| `channels.feishu.accounts..tts` | Per-account TTS override | `messages.tts` | +| `channels.feishu.dmPolicy` | DM policy (`pairing`, `allowlist`, `open`) | `pairing` | +| `channels.feishu.allowFrom` | DM allowlist (open_id list) | - | +| `channels.feishu.groupPolicy` | Group policy (`open`, `allowlist`, `disabled`) | `allowlist` | +| `channels.feishu.groupAllowFrom` | Group allowlist | - | +| `channels.feishu.groupSenderAllowFrom` | Sender allowlist applied to all groups | - | +| `channels.feishu.requireMention` | Require @mention in groups | `true` (`false` when policy `open`) | +| `channels.feishu.groups..requireMention` | Per-group @mention override; explicit IDs also admit the group in allowlist mode | inherited | +| `channels.feishu.groups..enabled` | Enable/disable a specific group | `true` | +| `channels.feishu.groups..allowFrom` | Per-group sender allowlist (overrides `groupSenderAllowFrom`) | - | +| `channels.feishu.groupSessionScope` | Group session mapping (`group`, `group_sender`, `group_topic`, `group_topic_sender`) | `group` | +| `channels.feishu.replyInThread` | Bot replies create/continue topic threads (`disabled`, `enabled`) | `disabled` | +| `channels.feishu.reactionNotifications` | Inbound reaction events (`off`, `own`, `all`) | `own` | +| `channels.feishu.dynamicAgentCreation.enabled` | Enable automatic per-user agent creation | `false` | +| `channels.feishu.dynamicAgentCreation.workspaceTemplate` | Path template for dynamic agent workspaces | `~/.openclaw/workspace-{agentId}` | +| `channels.feishu.dynamicAgentCreation.agentDirTemplate` | Agent directory name template | `~/.openclaw/agents/{agentId}/agent` | +| `channels.feishu.dynamicAgentCreation.maxAgents` | Maximum number of dynamic agents to create | unlimited | +| `channels.feishu.textChunkLimit` | Message chunk size | `4000` | +| `channels.feishu.chunkMode` | Chunk splitting (`length` or `newline`) | `length` | +| `channels.feishu.mediaMaxMb` | Media size limit | `30` | +| `channels.feishu.renderMode` | Reply rendering (`auto`, `raw`, `card`) | `auto` | +| `channels.feishu.streaming` | Streaming card output | `true` | +| `channels.feishu.blockStreaming` | Completed-block reply streaming | `false` | +| `channels.feishu.typingIndicator` | Send typing reactions | `true` | +| `channels.feishu.resolveSenderNames` | Resolve sender display names | `true` | +| `channels.feishu.configWrites` | Allow channel-initiated config writes (needed by dynamic agents) | `true` | +| `channels.feishu.tools.doc` | Enable document tools | `true` | +| `channels.feishu.tools.chat` | Enable chat info tools | `true` | +| `channels.feishu.tools.wiki` | Enable knowledge base tools (requires `doc`) | `true` | +| `channels.feishu.tools.drive` | Enable cloud storage tools | `true` | +| `channels.feishu.tools.perm` | Enable permission management tools | `false` | +| `channels.feishu.tools.scopes` | Enable app scopes diagnostic tool | `true` | +| `channels.feishu.tools.bitable` | Enable Bitable/Base tools | `true` | +| `channels.feishu.tools.base` | Alias for `channels.feishu.tools.bitable`; explicit `bitable` wins when both set | `true` | +| `channels.feishu.accounts..tools.bitable` | Per-account Bitable/Base tool gate | inherited | +| `channels.feishu.accounts..tools.base` | Per-account alias for `tools.bitable` | inherited | ## Supported message types @@ -638,14 +657,8 @@ conversion fails, OpenClaw falls back to a file attachment and logs the reason. - ✅ Thread replies - ✅ Media replies stay thread-aware when replying to a thread message -For `groupSessionScope: "group_topic"` and `"group_topic_sender"`, native -Feishu/Lark topic groups use the event `thread_id` (`omt_*`) as the canonical -topic session key. If a native topic starter event omits `thread_id`, OpenClaw -hydrates it from Feishu before routing the turn. Normal group replies that -OpenClaw turns into threads keep using the reply root message ID (`om_*`) so the -first turn and follow-up turn stay in the same session. - ---- +Topic-group session routing is covered under +[Group session scope and topic threads](#group-session-scope-and-topic-threads). ## Related diff --git a/docs/channels/googlechat.md b/docs/channels/googlechat.md index 7d5175c20147..2945643c57d7 100644 --- a/docs/channels/googlechat.md +++ b/docs/channels/googlechat.md @@ -5,12 +5,10 @@ read_when: title: "Google Chat" --- -Status: downloadable plugin for DMs + spaces via Google Chat API webhooks (HTTP only). +Google Chat runs as the official `@openclaw/googlechat` plugin: DMs and spaces through Google Chat API webhooks (HTTP endpoint only, no Pub/Sub). ## Install -Install Google Chat before configuring the channel: - ```bash openclaw plugins install @openclaw/googlechat ``` @@ -29,109 +27,86 @@ openclaw plugins install ./path/to/local/googlechat-plugin 2. Create a **Service Account**: - Press **Create Credentials** > **Service Account**. - Name it whatever you want (e.g., `openclaw-chat`). - - Leave permissions blank (press **Continue**). - - Leave principals with access blank (press **Done**). -3. Create and download the **JSON Key**: - - In the list of service accounts, click on the one you just created. - - Go to the **Keys** tab. - - Click **Add Key** > **Create new key**. - - Select **JSON** and press **Create**. + - Leave permissions and principals blank (**Continue**, then **Done**). +3. Create and download the **JSON key**: + - Click the new service account > **Keys** tab > **Add Key** > **Create new key** > **JSON** > **Create**. 4. Store the downloaded JSON file on your gateway host (e.g., `~/.openclaw/googlechat-service-account.json`). 5. Create a Google Chat app in the [Google Cloud Console Chat Configuration](https://console.cloud.google.com/apis/api/chat.googleapis.com/hangouts-chat): - - Fill in the **Application info**: - - **App name**: (e.g. `OpenClaw`) - - **Avatar URL**: (e.g. `https://openclaw.ai/logo.png`) - - **Description**: (e.g. `Personal AI Assistant`) + - Fill in **Application info** (app name, avatar URL, description). - Enable **Interactive features**. - Under **Functionality**, check **Join spaces and group conversations**. - Under **Connection settings**, select **HTTP endpoint URL**. - - Under **Triggers**, select **Use a common HTTP endpoint URL for all triggers** and set it to your gateway's public URL followed by `/googlechat`. - - _Tip: Run `openclaw status` to find your gateway's public URL._ - - Under **Visibility**, check **Make this Chat app available to specific people and groups in ``**. - - Enter your email address (e.g. `user@example.com`) in the text box. - - Click **Save** at the bottom. -6. **Enable the app status**: - - After saving, **refresh the page**. - - Look for the **App status** section (usually near the top or bottom after saving). - - Change the status to **Live - available to users**. - - Click **Save** again. -7. Configure OpenClaw with the service account path + webhook audience: - - Env: `GOOGLE_CHAT_SERVICE_ACCOUNT_FILE=/path/to/service-account.json` - - Or config: `channels.googlechat.serviceAccountFile: "/path/to/service-account.json"`. -8. Set the webhook audience type + value (matches your Chat app config). -9. Start the gateway. Google Chat will POST to your webhook path. + - Under **Triggers**, select **Use a common HTTP endpoint URL for all triggers** and set it to your public gateway URL followed by `/googlechat` (see [Public URL](#public-url-webhook-only)). + - Under **Visibility**, check **Make this Chat app available to specific people and groups in ``** and enter your email address. + - Click **Save**. +6. Enable the app status: refresh the page, find **App status**, set it to **Live - available to users**, and **Save** again. +7. Configure OpenClaw with the service account and the webhook audience (must match the Chat app config): + - Env: `GOOGLE_CHAT_SERVICE_ACCOUNT_FILE=/path/to/service-account.json` (default account only), or + - Config: see [Config highlights](#config-highlights). `openclaw channels add --channel googlechat` also accepts `--audience-type`, `--audience`, `--webhook-path`, and `--webhook-url`. +8. Start the gateway. Google Chat will POST to your webhook path (default `/googlechat`). ## Add to Google Chat -Once the gateway is running and your email is added to the visibility list: +Once the gateway is running and your email is on the visibility list: 1. Go to [Google Chat](https://chat.google.com/). 2. Click the **+** (plus) icon next to **Direct Messages**. -3. In the search bar (where you usually add people), type the **App name** you configured in the Google Cloud Console. - - **Note**: The bot will _not_ appear in the "Marketplace" browse list because it is a private app. You must search for it by name. -4. Select your bot from the results. -5. Click **Add** or **Chat** to start a 1:1 conversation. -6. Send "Hello" to trigger the assistant! +3. Search for the **App name** you configured in the Google Cloud Console. + - The bot does _not_ appear in the Marketplace browse list because it is a private app; search for it by name. +4. Select the bot, click **Add** or **Chat**, and send a message. ## Public URL (Webhook-only) -Google Chat webhooks require a public HTTPS endpoint. For security, **only expose the `/googlechat` path** to the internet. Keep the OpenClaw dashboard and other sensitive endpoints on your private network. +Google Chat webhooks require a public HTTPS endpoint. For security, expose **only the `/googlechat` path** to the internet and keep the OpenClaw dashboard and other endpoints private. ### Option A: Tailscale Funnel (Recommended) -Use Tailscale Serve for the private dashboard and Funnel for the public webhook path. This keeps `/` private while exposing only `/googlechat`. +Use Tailscale Serve for the private dashboard and Funnel for the public webhook path. -1. **Check what address your gateway is bound to:** +1. Check what address your gateway is bound to: ```bash ss -tlnp | grep 18789 ``` - Note the IP address (e.g., `127.0.0.1`, `0.0.0.0`, or your Tailscale IP like `100.x.x.x`). + Note the IP (e.g., `127.0.0.1`, `0.0.0.0`, or a Tailscale `100.x.x.x` address). -2. **Expose the dashboard to the tailnet only (port 8443):** +2. Expose the dashboard to the tailnet only (port 8443): ```bash # If bound to localhost (127.0.0.1 or 0.0.0.0): tailscale serve --bg --https 8443 http://127.0.0.1:18789 - # If bound to Tailscale IP only (e.g., 100.106.161.80): - tailscale serve --bg --https 8443 http://100.106.161.80:18789 + # If bound to a Tailscale IP only: + tailscale serve --bg --https 8443 http://100.x.x.x:18789 ``` -3. **Expose only the webhook path publicly:** +3. Expose only the webhook path publicly: ```bash # If bound to localhost (127.0.0.1 or 0.0.0.0): tailscale funnel --bg --set-path /googlechat http://127.0.0.1:18789/googlechat - # If bound to Tailscale IP only (e.g., 100.106.161.80): - tailscale funnel --bg --set-path /googlechat http://100.106.161.80:18789/googlechat + # If bound to a Tailscale IP only: + tailscale funnel --bg --set-path /googlechat http://100.x.x.x:18789/googlechat ``` -4. **Authorize the node for Funnel access:** - If prompted, visit the authorization URL shown in the output to enable Funnel for this node in your tailnet policy. +4. If prompted, visit the authorization URL shown in the output to enable Funnel for this node. -5. **Verify the configuration:** +5. Verify: ```bash tailscale serve status tailscale funnel status ``` -Your public webhook URL will be: -`https://..ts.net/googlechat` +Your public webhook URL is `https://..ts.net/googlechat`; the dashboard stays tailnet-only at `https://..ts.net:8443/`. Use the public URL (without `:8443`) in the Google Chat app config. -Your private dashboard stays tailnet-only: -`https://..ts.net:8443/` - -Use the public URL (without `:8443`) in the Google Chat app config. - -> Note: This configuration persists across reboots. To remove it later, run `tailscale funnel reset` and `tailscale serve reset`. +> Note: This configuration persists across reboots. Remove it later with `tailscale funnel reset` and `tailscale serve reset`. ### Option B: Reverse Proxy (Caddy) -If you use a reverse proxy like Caddy, only proxy the specific path: +Proxy only the webhook path: ```caddy your-domain.com { @@ -139,39 +114,42 @@ your-domain.com { } ``` -With this config, any request to `your-domain.com/` will be ignored or returned as 404, while `your-domain.com/googlechat` is safely routed to OpenClaw. +Requests to `your-domain.com/` are ignored or 404, while `your-domain.com/googlechat` routes to OpenClaw. ### Option C: Cloudflare Tunnel -Configure your tunnel's ingress rules to only route the webhook path: +Configure the tunnel ingress rules to route only the webhook path: - **Path**: `/googlechat` -> `http://localhost:18789/googlechat` -- **Default Rule**: HTTP 404 (Not Found) +- **Default rule**: HTTP 404 (Not Found) ## How it works -1. Google Chat sends webhook POSTs to the gateway. Each request includes an `Authorization: Bearer ` header. - - OpenClaw verifies bearer auth before reading/parsing full webhook bodies when the header is present. - - Google Workspace Add-on requests that carry `authorizationEventObject.systemIdToken` in the body are supported via a stricter pre-auth body budget. -2. OpenClaw verifies the token against the configured `audienceType` + `audience`: +1. Google Chat POSTs JSON to the gateway webhook path (POST only, JSON content type required, per-IP rate limited). +2. OpenClaw authenticates every request before dispatch: + - Chat app events carry `Authorization: Bearer `; the token is verified before the full body is parsed. + - Google Workspace Add-on events carry the token in the body (`authorizationEventObject.systemIdToken`) and are read under a stricter pre-auth budget (16 KB, 3 s) before verification. +3. The token is checked against `audienceType` + `audience`: - `audienceType: "app-url"` → audience is your HTTPS webhook URL. - `audienceType: "project-number"` → audience is the Cloud project number. -3. Messages are routed by space: - - DMs use session key `agent::googlechat:direct:`. - - Spaces use session key `agent::googlechat:group:`. -4. DM access is pairing by default. Unknown senders receive a pairing code; approve with: + - Add-on tokens under `app-url` additionally require `appPrincipal` set to the app's numeric OAuth 2.0 client ID (21 digits, not an email); otherwise verification fails with a logged warning. +4. Messages route by space: + - Spaces get per-space sessions `agent::googlechat:group:`; replies go to the message thread. + - DMs collapse into the agent's main session by default; set `session.dmScope` for per-peer DM sessions (see [Session](/concepts/session)). +5. DM access is pairing by default. Unknown senders receive a pairing code; approve with: - `openclaw pairing approve googlechat ` -5. Group spaces require @-mention by default. Use `botUser` if mention detection needs the app's user name. -6. When an exec or plugin approval request starts from Google Chat and a stable `users/` approver is configured, OpenClaw posts a native Google Chat approval card in the originating space or thread. The card buttons use opaque callback tokens, and the manual `/approve ` prompt is only shown when native approval delivery is unavailable. +6. Group spaces require @-mention by default. Mentions are detected from Chat `USER_MENTION` annotations targeting the app; set `botUser` (e.g., `users/1234567890`) if detection needs the app's user resource name. +7. When an exec or plugin approval starts from Google Chat and a stable `users/` approver is configured, OpenClaw posts a native approval card (`cardsV2`) in the originating space or thread. Card buttons carry opaque callback tokens; the manual `/approve ` prompt appears only when native delivery is unavailable. ## Targets Use these identifiers for delivery and allowlists: - Direct messages: `users/` (recommended). -- Raw email `name@example.com` is mutable and only used for direct allowlist matching when `channels.googlechat.dangerouslyAllowNameMatching: true`. -- Deprecated: `users/` is treated as a user id, not an email allowlist. - Spaces: `spaces/`. +- Raw email `name@example.com` is mutable and only used for allowlist matching when `channels.googlechat.dangerouslyAllowNameMatching: true`. +- Deprecated: `users/` is treated as a user id, not an email allowlist entry. +- Prefixes `googlechat:`, `google-chat:`, and `gchat:` are accepted and stripped. ## Config highlights @@ -184,6 +162,7 @@ Use these identifiers for delivery and allowlists: // or serviceAccountRef: { source: "file", provider: "filemain", id: "/channels/googlechat/serviceAccount" } audienceType: "app-url", audience: "https://gateway.example.com/googlechat", + appPrincipal: "123456789012345678901", // add-on verification only; numeric OAuth client ID webhookPath: "/googlechat", botUser: "users/1234567890", // optional; helps mention detection allowBots: false, @@ -210,16 +189,16 @@ Use these identifiers for delivery and allowlists: Notes: -- Service account credentials can also be passed inline with `serviceAccount` (JSON string). -- `serviceAccountRef` is also supported (env/file SecretRef), including per-account refs under `channels.googlechat.accounts..serviceAccountRef`. -- Default webhook path is `/googlechat` if `webhookPath` isn't set. -- `dangerouslyAllowNameMatching` re-enables mutable email principal matching for allowlists (break-glass compatibility mode). -- Reactions are available via the `reactions` tool and `channels action` when `actions.reactions` is enabled. +- Service account credentials: `serviceAccountFile` (path), `serviceAccount` (inline JSON string or object), or `serviceAccountRef` (env/file SecretRef). Env vars `GOOGLE_CHAT_SERVICE_ACCOUNT` (inline JSON) and `GOOGLE_CHAT_SERVICE_ACCOUNT_FILE` (path) apply to the default account only. Multi-account setups use `channels.googlechat.accounts.` with the same keys, including per-account `serviceAccountRef`. +- Default webhook path is `/googlechat` when `webhookPath` is unset; `webhookUrl` can supply the path instead. +- Group keys must be stable space ids (`spaces/`). Display-name keys are deprecated and logged as such. +- `dangerouslyAllowNameMatching` re-enables mutable email principal matching for allowlists (break-glass compatibility mode); doctor warns about email entries. +- Reactions are enabled by default and exposed through the `reactions` tool and `channels action`; disable with `actions.reactions: false`. - Native approval cards use Google Chat `cardsV2` button clicks, not reaction events. Approvers come from `dm.allowFrom` or `defaultTo` and must be stable numeric `users/` values. -- Message actions expose `send` for text and `upload-file` for explicit attachment sends. `upload-file` accepts `media` / `filePath` / `path` plus optional `message`, `filename`, and thread targeting. -- `typingIndicator` supports `message` (default), `none`, and `reaction` (reaction requires user OAuth). -- Attachments are downloaded through the Chat API and stored in the media pipeline (size capped by `mediaMaxMb`). -- Bot-authored Google Chat messages are ignored by default. If you intentionally set `allowBots: true`, accepted bot-authored messages use shared [bot loop protection](/channels/bot-loop-protection). Configure `channels.defaults.botLoopProtection`, then override with `channels.googlechat.botLoopProtection` or `channels.googlechat.groups..botLoopProtection` when one space needs a different budget. +- Message actions expose `send` for text and `upload-file` for explicit attachment sends. `upload-file` accepts `media` / `filePath` / `path` plus optional `message`, `filename`, and thread targeting (`threadId` / `replyTo`). +- `typingIndicator`: `message` (default) posts a `_ is typing..._` placeholder and edits it into the first reply; `none` disables it; `reaction` requires user OAuth and currently falls back to `message` with a logged error under service-account auth. +- Inbound attachments (first attachment per message) are downloaded through the Chat API into the media pipeline, capped by `mediaMaxMb` (default 20). +- Bot-authored messages are ignored by default. With `allowBots: true`, accepted bot messages use shared [bot loop protection](/channels/bot-loop-protection): configure `channels.defaults.botLoopProtection`, then override with `channels.googlechat.botLoopProtection` or `channels.googlechat.groups..botLoopProtection`. Secrets reference details: [Secrets Management](/gateway/secrets). @@ -229,13 +208,13 @@ Secrets reference details: [Secrets Management](/gateway/secrets). If Google Cloud Logs Explorer shows errors like: -``` +```text status code: 405, reason phrase: HTTP error response: HTTP/1.1 405 Method Not Allowed ``` -This means the webhook handler isn't registered. Common causes: +The webhook handler is not registered. Common causes: -1. **Channel not configured**: The `channels.googlechat` section is missing from your config. Verify with: +1. **Channel not configured**: the `channels.googlechat` section is missing. Verify with: ```bash openclaw config get channels.googlechat @@ -243,7 +222,7 @@ This means the webhook handler isn't registered. Common causes: If it returns "Config path not found", add the configuration (see [Config highlights](#config-highlights)). -2. **Plugin not enabled**: Check plugin status: +2. **Plugin not enabled**: check plugin status: ```bash openclaw plugins list | grep googlechat @@ -251,7 +230,7 @@ This means the webhook handler isn't registered. Common causes: If it shows "disabled", add `plugins.entries.googlechat.enabled: true` to your config. -3. **Gateway not restarted**: After adding config, restart the gateway: +3. **Gateway not restarted** after config changes: ```bash openclaw gateway restart @@ -266,21 +245,17 @@ openclaw channels status ### Other issues -- Check `openclaw channels status --probe` for auth errors or missing audience config. -- If no messages arrive, confirm the Chat app's webhook URL + event subscriptions. -- If mention gating blocks replies, set `botUser` to the app's user resource name and verify `requireMention`. -- Use `openclaw logs --follow` while sending a test message to see if requests reach the gateway. - -Related docs: - -- [Gateway configuration](/gateway/configuration) -- [Security](/gateway/security) -- [Reactions](/tools/reactions) +- `openclaw channels status --probe` surfaces auth errors and missing audience config (`audience` and `audienceType` are both required). +- If no messages arrive, confirm the Chat app's webhook URL and trigger configuration. +- If mention gating blocks replies, set `botUser` to the app's user resource name and check `requireMention`. +- `openclaw logs --follow` while sending a test message shows whether requests reach the gateway. ## Related - [Channels Overview](/channels) — all supported channels -- [Pairing](/channels/pairing) — DM authentication and pairing flow -- [Groups](/channels/groups) — group chat behavior and mention gating - [Channel Routing](/channels/channel-routing) — session routing for messages +- [Gateway configuration](/gateway/configuration) +- [Groups](/channels/groups) — group chat behavior and mention gating +- [Pairing](/channels/pairing) — DM authentication and pairing flow +- [Reactions](/tools/reactions) - [Security](/gateway/security) — access model and hardening diff --git a/docs/channels/group-messages.md b/docs/channels/group-messages.md index ac8642d77e1a..fed236c363eb 100644 --- a/docs/channels/group-messages.md +++ b/docs/channels/group-messages.md @@ -8,27 +8,28 @@ title: "WhatsApp group messages" sidebarTitle: "WhatsApp groups" --- -For the cross-channel groups model (Discord, iMessage, Matrix, Microsoft Teams, Signal, Slack, Telegram, WhatsApp, Zalo), see [Groups](/channels/groups). This page covers the WhatsApp-specific behavior on top of that model: activation, group allowlists, per-group session keys, and pending-message context injection. +For the cross-channel groups model (Discord, iMessage, Matrix, Microsoft Teams, QQBot, Signal, Slack, Telegram, WhatsApp, Zalo), see [Groups](/channels/groups). This page covers the WhatsApp-specific behavior on top of that model: activation, group allowlists, per-group session keys, and pending-message context injection. Goal: let OpenClaw sit in WhatsApp groups, wake up only when pinged, and keep that thread separate from the personal DM session. -`agents.list[].groupChat.mentionPatterns` is also used by Telegram, Discord, Slack, and iMessage. For multi-agent setups, set it per agent, or use `messages.groupChat.mentionPatterns` as a global fallback. +`agents.list[].groupChat.mentionPatterns` is shared with the other channels' mention gating. For multi-agent setups, set it per agent, or use `messages.groupChat.mentionPatterns` as a global fallback. With neither set, patterns are derived from the agent identity name/emoji. ## Behavior -- Activation modes: `mention` (default) or `always`. `mention` requires a ping (real WhatsApp @-mentions via `mentionedJids`, safe regex patterns, or the bot's E.164 anywhere in the text). `always` wakes the agent on every message but it should reply only when it can add meaningful value; otherwise it returns the exact silent token `NO_REPLY` / `no_reply`. Defaults can be set in config (`channels.whatsapp.groups`) and overridden per group via `/activation`. When `channels.whatsapp.groups` is set, it also acts as a group allowlist (include `"*"` to allow all). +- Activation modes: `mention` (default) or `always`. `mention` requires a ping: a real WhatsApp @-mention (`mentionedJids`), a configured regex pattern, the bot's E.164 digits anywhere in the text, or a quoted reply to one of the bot's messages (except shared-number self-chat setups). `always` wakes the agent on every message, but the injected group prompt tells it to reply only when it adds value and to return the exact silent token `NO_REPLY` (case-insensitive) otherwise. Defaults come from config (`channels.whatsapp.groups` `requireMention`) and can be overridden per group via `/activation`. +- Group allowlist: when `channels.whatsapp.groups` is set, only listed group JIDs are admitted (include `"*"` to allow all); messages from unlisted groups are dropped with a log hint. - Group policy: `channels.whatsapp.groupPolicy` controls whether group messages are accepted (`open|disabled|allowlist`). `allowlist` uses `channels.whatsapp.groupAllowFrom` (fallback: explicit `channels.whatsapp.allowFrom`). Default is `allowlist` (blocked until you add senders). -- Per-group sessions: session keys look like `agent::whatsapp:group:` so commands such as `/verbose on`, `/trace on`, or `/think high` (sent as standalone messages) are scoped to that group; personal DM state is untouched. Heartbeats are skipped for group threads. -- Context injection: **pending-only** group messages (default 50) that _did not_ trigger a run are prefixed under `[Chat messages since your last reply - for context]`, with the triggering line under `[Current message - respond to this]`. Messages already in the session are not re-injected. -- Sender surfacing: every group batch now ends with `[from: Sender Name (+E164)]` so OpenClaw knows who is speaking. -- Ephemeral/view-once: we unwrap those before extracting text/mentions, so pings inside them still trigger. -- Group system prompt: on the first turn of a group session (and whenever `/activation` changes the mode) we inject a short blurb into the system prompt like `You are replying inside the WhatsApp group "". Group members: Alice (+44...), Bob (+43...), ... Activation: trigger-only ... Address the specific sender noted in the message context.` If metadata isn't available we still tell the agent it's a group chat. +- Per-group sessions: session keys look like `agent::whatsapp:group:` (non-default accounts append `:thread:whatsapp-account-`), so directives such as `/verbose on`, `/trace on`, or `/think high` (sent as standalone messages) are scoped to that group; personal DM state is untouched. +- Context injection: **pending-only** group messages (default 50) that _did not_ trigger a run are prefixed under `[Chat messages since your last reply - for context]`, with the triggering line under `[Current message - respond to this]`. The pending window is cleared after the run; messages already in the session are not re-injected. +- Sender attribution: each group line carries the sender label inside the message envelope, e.g. `[WhatsApp ] Alice (+447700900123): text`, and sender identity plus group subject/members ride along in the untrusted conversation-metadata block. +- Ephemeral/view-once: wrappers are unwrapped before extracting text/mentions, so pings inside them still trigger. +- Group system prompt: the first turn of a group session (and any turn after `/activation` changes the mode) injects activation guidance into the system prompt (`Activation: trigger-only ...` or `Activation: always-on ...`, plus "address the specific sender"). Persistent group-chat delivery guidance ("You are in a WhatsApp group chat...") is always included. ## Config example (WhatsApp) -Add a `groupChat` block to `~/.openclaw/openclaw.json` so display-name pings work even when WhatsApp strips the visual `@` in the text body: +Make display-name pings work even when WhatsApp strips the visual `@` from the text body: ```json5 { @@ -37,6 +38,7 @@ Add a `groupChat` block to `~/.openclaw/openclaw.json` so display-name pings wor groups: { "*": { requireMention: true }, }, + historyLimit: 50, // pending group context window (default 50) }, }, agents: { @@ -44,7 +46,6 @@ Add a `groupChat` block to `~/.openclaw/openclaw.json` so display-name pings wor { id: "main", groupChat: { - historyLimit: 50, mentionPatterns: ["@?openclaw", "\\+?15555550123"], }, }, @@ -57,6 +58,7 @@ Notes: - The regexes are case-insensitive and use the same safe-regex guardrails as other config regex surfaces; invalid patterns and unsafe nested repetition are ignored. - WhatsApp still sends canonical mentions via `mentionedJids` when someone taps the contact, so the number fallback is rarely needed but is a useful safety net. +- The pending-context window resolves as `channels.whatsapp.accounts..historyLimit` → `channels.whatsapp.historyLimit` → `messages.groupChat.historyLimit` → 50. ### Activation command (owner-only) @@ -65,28 +67,28 @@ Use the group chat command: - `/activation mention` - `/activation always` -Only the owner number (from `channels.whatsapp.allowFrom`, or the bot's own E.164 when unset) can change this. Send `/status` as a standalone message in the group to see the current activation mode. +Only owner numbers (from `channels.whatsapp.allowFrom`, or the bot's own E.164 when unset) can change this; `/activation` from anyone else is ignored and stored as context only. Send `/status` as a standalone message in the group to see the current activation mode. ## How to use 1. Add your WhatsApp account (the one running OpenClaw) to the group. -2. Say `@openclaw …` (or include the number). Only allowlisted senders can trigger it unless you set `groupPolicy: "open"`. -3. The agent prompt will include recent group context plus the trailing `[from: …]` marker so it can address the right person. -4. Session-level directives (`/verbose on`, `/trace on`, `/think high`, `/new` or `/reset`, `/compact`) apply only to that group's session; send them as standalone messages so they register. Your personal DM session remains independent. +2. Say `@openclaw ...` (or include the number). Only allowlisted senders can trigger it unless you set `groupPolicy: "open"`. +3. The agent prompt includes the pending group context plus sender-labeled lines so it can address the right person. +4. Session directives (`/verbose on`, `/trace on`, `/think high`, `/new` or `/reset`, `/compact`) apply only to that group's session; send them as standalone messages so they register. Your personal DM session stays independent. ## Testing / verification - Manual smoke: - Send an `@openclaw` ping in the group and confirm a reply that references the sender name. - - Send a second ping and verify the history block is included then cleared on the next turn. -- Check gateway logs (run with `--verbose`) to see `inbound web message` entries showing `from: ` and the `[from: …]` suffix. + - Send a second ping and verify the history block is included, then cleared on the next turn. +- Check gateway logs (run with `--verbose`) for `inbound web message` entries showing `from: ` and the sender-labeled body. ## Known considerations -- Heartbeats are intentionally skipped for groups to avoid noisy broadcasts. -- Echo suppression uses the combined batch string; if you send identical text twice without mentions, only the first will get a response. -- Session store entries will appear as `agent::whatsapp:group:` in the session store (`~/.openclaw/agents//sessions/sessions.json` by default); a missing entry just means the group hasn't triggered a run yet. -- Typing indicators in groups follow `agents.defaults.typingMode`. When visible replies are opted into message-tool-only mode, typing starts immediately by default so group members can see the agent is working even if no automatic final reply is posted. Explicit typing-mode config still wins. +- Heartbeats run in the agent's main session; group sessions never get heartbeat runs. +- Echo suppression remembers the combined prompt (history + current message) per session so the bot's own delivered messages do not retrigger it; an identical repeated batch can be skipped as an echo. +- Session store entries appear as `agent::whatsapp:group:` in the session store (`~/.openclaw/agents//sessions/sessions.json` by default); a missing entry just means the group has not triggered a run yet. +- Typing indicators follow `session.typingMode` / `agents.defaults.typingMode`. When visible replies are opted into message-tool-only mode, typing starts immediately by default so group members can see the agent working even if no automatic final reply is posted. Explicit typing-mode config still wins. ## Related diff --git a/docs/channels/groups.md b/docs/channels/groups.md index 0a10a9b2b415..39b0e4dc1f2c 100644 --- a/docs/channels/groups.md +++ b/docs/channels/groups.md @@ -7,19 +7,19 @@ title: "Groups" sidebarTitle: "Groups" --- -OpenClaw treats group chats consistently across surfaces: Discord, iMessage, Matrix, Microsoft Teams, QQBot, Signal, Slack, Telegram, WhatsApp, Zalo. +OpenClaw applies the same group rules across group-capable channels, including Discord, iMessage, Matrix, Microsoft Teams, QQBot, Signal, Slack, Telegram, WhatsApp, and Zalo. For always-on rooms that should provide quiet context unless the agent explicitly sends a visible message, see [Ambient room events](/channels/ambient-room-events). ## Beginner intro (2 minutes) -OpenClaw "lives" on your own messaging accounts. There is no separate WhatsApp bot user. If **you** are in a group, OpenClaw can see that group and respond there. +OpenClaw "lives" on your own messaging accounts. There is no separate WhatsApp bot user: if **you** are in a group, OpenClaw can see that group and respond there. Default behavior: -- Groups are restricted (`groupPolicy: "allowlist"`). -- Replies require a mention unless you explicitly disable mention gating. -- Visible replies in groups/channels use the `message` tool by default. +- Groups are restricted (`groupPolicy: "allowlist"`); group senders are blocked until allowlisted. +- Replies require a mention unless you disable mention gating for a group. +- Final reply text posts to the room automatically (`visibleReplies: "automatic"`). Translation: allowlisted senders can trigger OpenClaw by mentioning it. @@ -34,7 +34,7 @@ Translation: allowlisted senders can trigger OpenClaw by mentioning it. Quick flow (what happens to a group message): -``` +```text groupPolicy? disabled -> drop groupPolicy? allowlist -> group allowed? no -> drop requireMention? yes -> mentioned? no -> store for context only @@ -44,30 +44,23 @@ always-on group chatter -> user request, or room event when configured ## Visible replies -For normal group/channel requests, OpenClaw defaults to `messages.groupChat.visibleReplies: "automatic"`. Final assistant text posts through the legacy visible reply path unless you opt the room into message-tool-only output. +For normal group/channel requests, OpenClaw defaults to `messages.groupChat.visibleReplies: "automatic"`: the final assistant text posts to the room as the visible reply. -Use `messages.groupChat.visibleReplies: "message_tool"` when a shared room should let the agent decide when to speak by calling `message(action=send)`. This works best for group rooms backed by latest-generation, tool-reliable models such as GPT 5.5. If the model misses that tool and returns substantive final text, OpenClaw keeps that final text private instead of posting it to the room. +Use `messages.groupChat.visibleReplies: "message_tool"` when a shared room should let the agent decide when to speak by calling `message(action=send)`. This works best with tool-reliable models (for example GPT 5.5). If the model misses the tool and returns substantive final text, OpenClaw keeps that text private instead of posting it to the room. -Use `"automatic"` for weaker models or runtimes that do not reliably understand tool-only delivery. In automatic mode, the agent's final assistant text is the visible source reply path, so a model that cannot consistently call `message(action=send)` can still answer normally. +Use `"automatic"` for models or runtimes that do not reliably follow tool-only delivery: normal text finals post directly to the room, and the agent may still call `message(action=send)` for files, images, or other attachments that cannot ride along with the final text. -In automatic mode, normal text final replies are posted directly to the room. If the visible reply needs files, images, or other attachments, the agent may still use `message(action=send)` for that attachment instead of trying to force it through the final text reply. +If the message tool is unavailable under the active tool policy, OpenClaw falls back to automatic visible replies instead of silently suppressing the response. `openclaw doctor` warns about this mismatch. -If the message tool is unavailable under the active tool policy, OpenClaw falls -back to automatic visible replies instead of silently suppressing the response. -`openclaw doctor` warns about this mismatch. +For direct chats and any other source event, `messages.visibleReplies: "message_tool"` applies the same tool-only behavior globally; `messages.groupChat.visibleReplies` remains the more specific override for group/channel rooms. Internal WebChat direct turns default to automatic final-reply delivery so Pi and Codex receive the same visible-reply contract. -For direct chats and any other source event, use `messages.visibleReplies: "message_tool"` to apply the same tool-only visible-reply behavior globally. Internal WebChat direct turns default to automatic final-reply delivery so Pi and Codex receive the same visible-reply contract. Set `messages.visibleReplies: "message_tool"` to intentionally require `message(action=send)` for visible output. `messages.groupChat.visibleReplies` remains the more specific override for group/channel rooms. - -This replaces the old pattern of forcing the model to answer `NO_REPLY` for most lurk-mode turns. In tool-only mode, the prompt does not define a `NO_REPLY` contract. Doing nothing visible simply means not calling the message tool. +Tool-only mode replaces the old pattern of forcing the model to answer `NO_REPLY` for most lurk-mode turns. In tool-only mode the prompt does not define a `NO_REPLY` contract; doing nothing visible simply means not calling the message tool. Plugin-owned conversation bindings are the exception. Once a plugin binds a thread and claims the inbound turn, the plugin's returned reply is the visible binding response; it does not need `message(action=send)`. That reply is plugin runtime output, not private model final text. Typing indicators are still sent for direct group requests. Ambient always-on room events, when enabled, stay strict and quiet unless the agent calls the message tool. -Sessions suppress verbose tool/progress summaries by default. Use `/verbose on` -to show those summaries for the current session while debugging, and -`/verbose off` to return to final-reply-only behavior. The same verbose state -applies across direct chats, groups, channels, and forum topics. +Sessions suppress verbose tool/progress summaries by default. Use `/verbose on` (or `/verbose full`) to show them for the current session while debugging, and `/verbose off` to return to final-reply-only behavior. Verbose state is per session and works the same in direct chats, groups, channels, and forum topics. To submit unmentioned always-on group chatter as quiet room context instead of user requests, use [Ambient room events](/channels/ambient-room-events): @@ -81,9 +74,7 @@ To submit unmentioned always-on group chatter as quiet room context instead of u } ``` -The default is `unmentionedInbound: "user_request"`. - -Mentioned messages, commands, abort requests, and DMs stay user requests. +The default is `unmentionedInbound: "user_request"`. Mentioned messages, commands, abort requests, and DMs stay user requests. To require visible output to go through the message tool for group/channel requests: @@ -97,10 +88,7 @@ To require visible output to go through the message tool for group/channel reque } ``` -The gateway hot-reloads `messages` config after the file is saved. Restart only -when file watching or config reload is disabled in the deployment. - -To require visible output to go through the message tool for every source chat: +To require it for every source chat: ```json5 { @@ -110,32 +98,26 @@ To require visible output to go through the message tool for every source chat: } ``` -Native slash commands (Discord, Telegram, and other surfaces with native command support) bypass `visibleReplies: "message_tool"` and always reply visibly so the channel-native command UI gets the response it expects. This applies to validated native command turns only; text-typed `/...` commands and ordinary chat turns still follow the configured group default. +The gateway picks up `messages` config changes without a restart after the file is saved. Restart only when config reload is disabled (`gateway.reload.mode: "off"`). + +Command turns bypass `visibleReplies: "message_tool"` and always reply visibly: native slash commands (Discord, Telegram, and other surfaces with native command support) and authorized text `/...` commands both post their response to the source chat. Unauthorized text `/...` turns in groups stay message-tool-only; ordinary chat turns follow the configured default. ## Context visibility and allowlists Two different controls are involved in group safety: - **Trigger authorization**: who can trigger the agent (`groupPolicy`, `groups`, `groupAllowFrom`, channel-specific allowlists). -- **Context visibility**: what supplemental context is injected into the model (reply text, quotes, thread history, forwarded metadata). +- **Context visibility**: what supplemental context is injected into the model (reply/quote text, thread history, forwarded metadata). -By default, OpenClaw prioritizes normal chat behavior and keeps context mostly as received. This means allowlists primarily decide who can trigger actions, not a universal redaction boundary for every quoted or historical snippet. +By default OpenClaw keeps context as received: allowlists decide who can trigger actions, not what quoted or historical snippets the model sees. To also filter supplemental context, set `contextVisibility`: - - - - Some channels already apply sender-based filtering for supplemental context in specific paths (for example Slack thread seeding, Matrix reply/thread lookups). - - Other channels still pass quote/reply/forward context through as received. +| Mode | Behavior | +| ------------------- | -------------------------------------------------------------------------------- | +| `"all"` (default) | Keep supplemental context as received. | +| `"allowlist"` | Only inject history/thread/quote/forwarded context from allowlisted senders. | +| `"allowlist_quote"` | `allowlist`, plus keep the explicitly quoted/replied-to message from any sender. | - - - - `contextVisibility: "all"` (default) keeps current as-received behavior. - - `contextVisibility: "allowlist"` filters supplemental context to allowlisted senders. - - `contextVisibility: "allowlist_quote"` is `allowlist` plus one explicit quote/reply exception. - - Until this hardening model is implemented consistently across channels, expect differences by surface. - - - +Set it per channel (`channels..contextVisibility`), per account (`channels..accounts..contextVisibility`), or globally (`channels.defaults.contextVisibility`). Channels that fetch supplemental context (Discord, Feishu, iMessage, Matrix, Microsoft Teams, Signal, Slack, Telegram, WhatsApp) apply the policy when building inbound context; unknown policy combinations fail closed and omit the context. ![Group message flow](/images/groups-flow.svg) @@ -155,8 +137,8 @@ For reusable sender allowlists, see [Access groups](/channels/access-groups). - Group sessions use `agent:::group:` session keys (rooms/channels use `agent:::channel:`). - Telegram forum topics add `:topic:` to the group id so each topic has its own session. -- Direct chats use the main session (or per-sender if configured). -- Heartbeats are skipped for group sessions. +- Direct chats use the main session (or per-sender sessions if `session.dmScope` is configured). +- Heartbeats run in the configured heartbeat session (default: the agent main session); group sessions do not run their own heartbeats. @@ -235,7 +217,7 @@ Related: ## Display labels - UI labels use `displayName` when available, formatted as `:`. -- `#room` is reserved for rooms/channels; group chats use `g-` (lowercase, spaces -> `-`, keep `#@+._-`). +- `#room` is reserved for rooms/channels; group chats use `g-` (lowercase, spaces -> `-`, keep `#@+._-`). Very long opaque ids are shortened into a stable token instead of leaking full route ids into the UI. ## Group policy @@ -250,7 +232,7 @@ Control how group/room messages are handled per channel: }, telegram: { groupPolicy: "disabled", - groupAllowFrom: ["123456789"], // numeric Telegram user id (wizard can resolve @username) + groupAllowFrom: ["123456789"], // numeric Telegram user id (setup resolves @username) }, signal: { groupPolicy: "disabled", @@ -267,12 +249,12 @@ Control how group/room messages are handled per channel: discord: { groupPolicy: "allowlist", guilds: { - GUILD_ID: { channels: { help: { allow: true } } }, + GUILD_ID: { channels: { help: { enabled: true } } }, }, }, slack: { groupPolicy: "allowlist", - channels: { "#general": { allow: true } }, + channels: { "#general": { enabled: true } }, }, matrix: { groupPolicy: "allowlist", @@ -300,11 +282,11 @@ Control how group/room messages are handled per channel: - DM pairing approvals (`*-allowFrom` store entries) apply to DM access only; group sender authorization stays explicit to group allowlists. - Discord: allowlist uses `channels.discord.guilds..channels`. - Slack: allowlist uses `channels.slack.channels`. - - Matrix: allowlist uses `channels.matrix.groups`. Prefer room IDs or aliases; joined-room name lookup is best-effort, and unresolved names are ignored at runtime. Use `channels.matrix.groupAllowFrom` to restrict senders; per-room `users` allowlists are also supported. - - Group DMs are controlled separately (`channels.discord.dm.*`, `channels.slack.dm.*`). - - Telegram allowlist can match user IDs (`"123456789"`, `"telegram:123456789"`, `"tg:123456789"`) or usernames (`"@alice"` or `"alice"`); prefixes are case-insensitive. + - Matrix: allowlist uses `channels.matrix.groups`. Use room IDs (`!room:server`) or aliases (`#alias:server`); room-name keys match only with `channels.matrix.dangerouslyAllowNameMatching: true`, and unresolved entries are ignored at runtime. Use `channels.matrix.groupAllowFrom` to restrict senders; per-room `users` allowlists are also supported. + - Group DMs are controlled separately (`channels.discord.dm.*`, `channels.slack.dm.*`: `groupEnabled`, `groupChannels`). + - Telegram: sender allowlists accept numeric user IDs only (`"123456789"`; `telegram:`/`tg:` prefixes are stripped case-insensitively). `@username` entries do not match at runtime and log a warning; setup resolves `@username` to IDs. Negative chat IDs belong under `channels.telegram.groups`, not sender allowlists. - Default is `groupPolicy: "allowlist"`; if your group allowlist is empty, group messages are blocked. - - Runtime safety: when a provider block is completely missing (`channels.` absent), group policy falls back to a fail-closed mode (typically `allowlist`) instead of inheriting `channels.defaults.groupPolicy`. + - Runtime safety: when a provider block is completely missing (`channels.` absent), group policy fails closed to `allowlist` instead of inheriting `channels.defaults.groupPolicy`, and the gateway logs the fallback once per account. @@ -327,7 +309,7 @@ Quick mental model (evaluation order for group messages): Group messages require a mention unless overridden per group. Defaults live per subsystem under `*.groups."*"`. -Replying to a bot message counts as an implicit mention when the channel supports reply metadata. Quoting a bot message can also count as an implicit mention on channels that expose quote metadata. Current built-in cases include Telegram, WhatsApp, Slack, Discord, Microsoft Teams, and ZaloUser. +Replying to a bot message counts as an implicit mention when the channel exposes reply metadata; quoting a bot message can also count on channels that expose quote metadata. Current built-in cases: Discord, Microsoft Teams, QQBot, Slack, Telegram, WhatsApp, and Zalo personal. ```json5 { @@ -367,20 +349,11 @@ Replying to a bot message counts as an implicit mention when the channel support ## Scope configured mention patterns -Configured `mentionPatterns` are regex fallback triggers. Use them when the -platform does not expose a native bot mention, or when you want plain text such -as `openclaw:` to count as a mention. Native platform mentions are separate: -when Discord, Slack, Telegram, Matrix, or another channel can prove the message -explicitly mentioned the bot, that native mention still triggers even if -configured regex patterns are denied. +Configured `mentionPatterns` are regex fallback triggers. Use them when the platform does not expose a native bot mention, or when plain text such as `openclaw:` should count as a mention. Native platform mentions are separate: when Discord, Slack, Telegram, Matrix, or another channel can prove the message explicitly mentioned the bot, that native mention still triggers even where configured regex patterns are denied. -By default, configured mention patterns apply everywhere that channel passes -provider and conversation facts into mention detection. To keep broad patterns -from waking the agent in every group, scope them per channel with -`channels..mentionPatterns`. +By default, configured mention patterns apply everywhere the channel passes provider and conversation facts into mention detection. To keep broad patterns from waking the agent in every group, scope them per channel with `channels..mentionPatterns`. -Use `mode: "deny"` when regex mention patterns should be off by default for a -channel, then opt in specific rooms with `allowIn`: +Use `mode: "deny"` when regex mention patterns should be off by default for a channel, then opt in specific rooms with `allowIn`: ```json5 { @@ -400,8 +373,7 @@ channel, then opt in specific rooms with `allowIn`: } ``` -Use the default `mode: "allow"` (or omit `mode`) when regex mention patterns -should apply broadly, then turn them off in noisy rooms with `denyIn`: +Use the default `mode: "allow"` (or omit `mode`) when regex mention patterns should apply broadly, then turn them off in noisy rooms with `denyIn`: ```json5 { @@ -439,18 +411,12 @@ Supported scoped regex policy today: | Telegram | Group chat IDs, or `chatId:topic:threadId` for forum topics. | | WhatsApp | WhatsApp conversation IDs such as `123@g.us`. | -Account-level channel configs can set the same policy under -`channels..accounts..mentionPatterns` when that channel -supports multiple accounts. Account policy takes precedence over the top-level -channel policy for that account. +Account-level channel configs can set the same policy under `channels..accounts..mentionPatterns` when that channel supports multiple accounts. Account policy takes precedence over the top-level channel policy for that account. - - `mentionPatterns` are case-insensitive safe regex patterns; invalid patterns and unsafe nested-repetition forms are ignored. - - Surfaces that provide explicit mentions still pass; configured regex patterns are a fallback. - - `channels..mentionPatterns.mode: "deny"` disables configured mention patterns by default for that channel; opt selected conversations back in with `allowIn`. - - `channels..mentionPatterns.denyIn` disables configured mention patterns for specific conversation IDs while native platform @mentions still pass. - - Per-agent override: `agents.list[].groupChat.mentionPatterns` (useful when multiple agents share a group). + - `mentionPatterns` are case-insensitive safe regex patterns; invalid patterns and unsafe nested-repetition forms are ignored (with a warning). + - Pattern precedence: `agents.list[].groupChat.mentionPatterns` (useful when multiple agents share a group) overrides `messages.groupChat.mentionPatterns`; when neither is set, patterns are derived from the agent identity name/emoji. - Mention gating is only enforced when mention detection is possible (native mentions or `mentionPatterns` are configured). - Allowlisting a group or sender does not disable mention gating; set that group's `requireMention` to `false` when all messages should trigger. - Automatic group chat prompt context carries the resolved silent-reply instruction every turn; workspace files should not duplicate `NO_REPLY` mechanics. @@ -467,8 +433,8 @@ channel policy for that account. Some channel configs support restricting which tools are available **inside a specific group/room/channel**. -- `tools`: allow/deny tools for the whole group. -- `toolsBySender`: per-sender overrides within the group. Use explicit key prefixes: `channel::`, `id:`, `e164:`, `username:`, `name:`, and `"*"` wildcard. Channel ids use canonical OpenClaw channel ids; aliases such as `teams` normalize to `msteams`. Legacy unprefixed keys are still accepted and matched as `id:` only. +- `tools`: allow/deny tools for the whole group (`allow`, `alsoAllow`, `deny`; deny wins). +- `toolsBySender`: per-sender overrides within the group. Use explicit key prefixes: `channel::`, `id:`, `e164:`, `username:`, `name:`, and `"*"` wildcard. Channel ids use canonical OpenClaw channel ids; aliases such as `teams` normalize to `msteams`. Legacy unprefixed keys are still accepted, matched as `id:` only, and log a deprecation warning. Resolution order (most specific wins): @@ -571,12 +537,12 @@ Common intents (copy/paste): ## Activation (owner-only) -Group owners can toggle per-group activation: +Group owners can toggle per-group activation with a standalone message: - `/activation mention` - `/activation always` -Owner is determined by `channels.whatsapp.allowFrom` (or the bot's self E.164 when unset). Send the command as a standalone message. Other surfaces currently ignore `/activation`. +`/activation` is a core owner-gated command and only applies in group chats. Owner means the sender matches the channel's `allowFrom` / `commands.ownerAllowFrom` (when no allowlist is configured, the account's own id counts as owner). The stored mode overrides that group's `requireMention` on channels that consult it (Google Chat, QQBot, Telegram, WhatsApp), and the group system-prompt intro reflects the active mode everywhere. ## Context fields @@ -588,7 +554,7 @@ Group inbound payloads set: - `WasMentioned` (mention gating result) - Telegram forum topics also include `MessageThreadId` and `IsForum`. -The agent system prompt includes a group intro on the first turn of a new group session. It reminds the model to respond like a human, minimize empty lines and follow normal chat spacing, and avoid typing literal `\n` sequences. Non-Telegram groups also discourage Markdown tables; Telegram rich-text guidance comes from the Telegram channel prompt. Channel-sourced group names and participant labels are rendered as fenced untrusted metadata, not inline system instructions. +The agent system prompt includes a group intro on the first turn of a new group session (and after `/activation` changes). It reminds the model to respond like a human, minimize empty lines and follow normal chat spacing, and avoid typing literal `\n` sequences. Non-Telegram groups also discourage Markdown tables; Telegram rich-text guidance comes from the Telegram channel prompt. Channel-sourced group names and participant labels are rendered as fenced untrusted metadata, not inline system instructions. ## iMessage specifics diff --git a/docs/channels/imessage-from-bluebubbles.md b/docs/channels/imessage-from-bluebubbles.md index 033c3e898d9c..7ccb8de73782 100644 --- a/docs/channels/imessage-from-bluebubbles.md +++ b/docs/channels/imessage-from-bluebubbles.md @@ -1,5 +1,5 @@ --- -summary: "Migrate old BlueBubbles configs to the bundled iMessage plugin without losing pairing, allowlists, or group bindings." +summary: "Translate old BlueBubbles configs to the bundled iMessage plugin: key mapping, group allowlist gates, and cutover verification." read_when: - Planning a move from BlueBubbles to the bundled iMessage plugin - Translating BlueBubbles config keys to iMessage equivalents @@ -7,9 +7,9 @@ read_when: title: "Coming from BlueBubbles" --- -The bundled `imessage` plugin now reaches the same private API surface as BlueBubbles (`react`, `edit`, `unsend`, `reply`, `sendWithEffect`, group management, attachments) by driving [`steipete/imsg`](https://github.com/steipete/imsg) over JSON-RPC. If you already run a Mac with `imsg` installed, you can drop the BlueBubbles server and let the plugin talk to Messages.app directly. +BlueBubbles support was removed. OpenClaw supports iMessage only through the bundled `imessage` plugin, which drives [`steipete/imsg`](https://github.com/steipete/imsg) over JSON-RPC and reaches the same private API surface BlueBubbles had (`react`, `edit`, `unsend`, `reply`, `sendWithEffect`, native polls, group management, attachments). One CLI binary replaces the BlueBubbles server + client app + webhook plumbing: no REST endpoint, no webhook auth. -BlueBubbles support was removed. OpenClaw supports iMessage through `imsg` only. This guide is for migrating old `channels.bluebubbles` configs to `channels.imessage`; there is no other supported migration path. +This guide migrates old `channels.bluebubbles` configs to `channels.imessage`. There is no other supported migration path. On current OpenClaw a leftover `channels.bluebubbles` block is inert — no runtime reads it. For the short announcement and operator summary, see [BlueBubbles removal and the imsg iMessage path](/announcements/bluebubbles-imessage). @@ -17,21 +17,15 @@ For the short announcement and operator summary, see [BlueBubbles removal and th ## Migration checklist -Use this checklist when you already know your old BlueBubbles config and want the shortest safe path: +The shortest safe path when you already know your old BlueBubbles config: -1. Verify `imsg` directly on the Mac that runs Messages.app (`imsg chats`, `imsg history`, `imsg send`, and `imsg rpc --help`). +1. Verify `imsg` directly on the Mac that runs Messages.app (`imsg chats`, `imsg history`, `imsg send`, `imsg rpc --help`). 2. Copy behavior keys from `channels.bluebubbles` to `channels.imessage`: `dmPolicy`, `allowFrom`, `groupPolicy`, `groupAllowFrom`, `groups`, `includeAttachments`, `attachmentRoots`, `mediaMaxMb`, `textChunkLimit`, `coalesceSameSenderDms`, and `actions`. 3. Drop transport keys that no longer exist: `serverUrl`, `password`, webhook URLs, and BlueBubbles server setup. 4. If the Gateway is not running on the Messages Mac, set `channels.imessage.cliPath` to an SSH wrapper and set `remoteHost` for remote attachment fetches. -5. With the Gateway stopped, enable `channels.imessage`, then run `openclaw channels status --probe --channel imessage`. +5. Enable `channels.imessage`, restart the Gateway, then run `openclaw channels status --probe --channel imessage`. 6. Test one DM, one allowed group, attachments if enabled, and every private API action you expect the agent to use. -7. Delete the BlueBubbles server and old `channels.bluebubbles` config after the iMessage path is verified. - -## When this migration makes sense - -- You already run `imsg` on the same Mac (or one reachable over SSH) where Messages.app is signed in. -- You want one fewer moving part — no separate BlueBubbles server, no REST endpoint to authenticate, no webhook plumbing. Single CLI binary instead of a server + client app + helper. -- You are on a [supported macOS / `imsg` build](/channels/imessage#requirements-and-permissions-macos) where the private API probe reports `available: true`. +7. Delete the BlueBubbles server and the old `channels.bluebubbles` config after the iMessage path is verified. ## What imsg does @@ -40,7 +34,7 @@ Use this checklist when you already know your old BlueBubbles config and want th - Reads come from `~/Library/Messages/chat.db` using a read-only SQLite handle. - Live inbound messages come from `imsg watch` / `watch.subscribe`, which follows `chat.db` filesystem events with a polling fallback. - Sends use Messages.app automation for normal text and file sends. -- Advanced actions use `imsg launch` to inject the `imsg` helper into Messages.app. That is what unlocks read receipts, typing indicators, rich sends, edit, unsend, threaded reply, tapbacks, and group management. +- Advanced actions use `imsg launch` to inject the `imsg` helper into Messages.app. That is what unlocks read receipts, typing indicators, rich sends, edit, unsend, threaded reply, tapbacks, polls, and group management. - Linux builds can inspect a copied `chat.db`, but cannot send, watch the live Mac database, or drive Messages.app. For OpenClaw iMessage, run `imsg` on the signed-in Mac or through an SSH wrapper to that Mac. ## Before you start @@ -65,7 +59,7 @@ Use this checklist when you already know your old BlueBubbles config and want th imsg rpc --help ``` - Replace `42` with a real chat id from `imsg chats`. Sending requires Automation permission for Messages.app. If OpenClaw will run through SSH, run these commands through the same SSH wrapper or user context that OpenClaw will use. If reads/probes work but sends fail with AppleEvents `-1743`, check whether Automation landed on `/usr/libexec/sshd-keygen-wrapper`; see [SSH wrapper sends fail with AppleEvents -1743](/channels/imessage#ssh-wrapper-sends-fail-with-appleevents-1743). + Replace `42` with a real chat id from `imsg chats`. Sending requires Automation permission for Messages.app. If OpenClaw will run through SSH, run these commands through the same SSH wrapper or user context that OpenClaw will use. If reads work but sends fail with AppleEvents `-1743`, check whether Automation landed on `/usr/libexec/sshd-keygen-wrapper`; see [SSH wrapper sends fail with AppleEvents -1743](/channels/imessage#requirements-and-permissions-macos). 3. Enable the private API bridge when you need advanced actions: @@ -74,70 +68,71 @@ Use this checklist when you already know your old BlueBubbles config and want th imsg status --json ``` - `imsg launch` requires SIP to be disabled. Basic send, history, and watch work without `imsg launch`; advanced actions do not. + `imsg launch` requires SIP to be disabled (and on modern macOS, library validation relaxed — see [Enabling the imsg private API](/channels/imessage#enabling-the-imsg-private-api)). Basic send, history, and watch work without `imsg launch`; advanced actions do not. -4. After you add an enabled `channels.imessage` config, verify the bridge through OpenClaw: +4. After you enable `channels.imessage` and start the Gateway, verify the bridge through OpenClaw: ```bash openclaw channels status --probe ``` - You want `imessage.privateApi.available: true`. If it reports `false`, fix that first — see [Capability detection](/channels/imessage#private-api-actions). `channels status --probe` only probes configured, enabled accounts. + The iMessage account should report `works`; with `--json`, the probe payload includes `privateApi.available: true`. If it reports `false`, fix that first — see [Capability detection](/channels/imessage#private-api-actions). Probing needs a reachable Gateway (the CLI falls back to config-only output otherwise) and only probes configured, enabled accounts. 5. Snapshot your config: ```bash - cp ~/.openclaw/openclaw.json5 ~/.openclaw/openclaw.json5.bak + cp ~/.openclaw/openclaw.json ~/.openclaw/openclaw.json.bak ``` ## Config translation -iMessage and BlueBubbles share a lot of channel-level config. The keys that change are mostly transport (REST server vs local CLI). Behavior keys (`dmPolicy`, `groupPolicy`, `allowFrom`, etc.) keep the same meaning. +iMessage and BlueBubbles share most channel-level behavior keys. What changes is transport (REST server vs local CLI) and the group registry key format. -| BlueBubbles | bundled iMessage | Notes | -| ---------------------------------------------------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `channels.bluebubbles.enabled` | `channels.imessage.enabled` | Same semantics. | -| `channels.bluebubbles.serverUrl` | _(removed)_ | No REST server — the plugin spawns `imsg rpc` over stdio. | -| `channels.bluebubbles.password` | _(removed)_ | No webhook authentication needed. | -| _(implicit)_ | `channels.imessage.cliPath` | Path to `imsg` (default `imsg`); use a wrapper script for SSH. | -| _(implicit)_ | `channels.imessage.dbPath` | Optional Messages.app `chat.db` override; auto-detected when omitted. | -| _(implicit)_ | `channels.imessage.remoteHost` | `host` or `user@host` — only needed when `cliPath` is an SSH wrapper and you want SCP attachment fetches. | -| `channels.bluebubbles.dmPolicy` | `channels.imessage.dmPolicy` | Same values (`pairing` / `allowlist` / `open` / `disabled`). | -| `channels.bluebubbles.allowFrom` | `channels.imessage.allowFrom` | Pairing approvals carry over by handle, not by token. | -| `channels.bluebubbles.groupPolicy` | `channels.imessage.groupPolicy` | Same values (`allowlist` / `open` / `disabled`). | -| `channels.bluebubbles.groupAllowFrom` | `channels.imessage.groupAllowFrom` | Same. | -| `channels.bluebubbles.groups` | `channels.imessage.groups` | **Copy this verbatim, including any `groups: { "*": { ... } }` wildcard entry.** Per-group `requireMention`, `tools`, `toolsBySender` carry over. With `groupPolicy: "allowlist"`, an empty or missing `groups` block silently drops every group message — see "Group registry footgun" below. | -| `channels.bluebubbles.sendReadReceipts` | `channels.imessage.sendReadReceipts` | Default `true`. With the bundled plugin this only fires when the private API probe is up. | -| `channels.bluebubbles.includeAttachments` | `channels.imessage.includeAttachments` | Same shape, **same off-by-default**. If you had attachments flowing on BlueBubbles you must re-set this explicitly on the iMessage block — it does not carry over implicitly, and inbound photos/media will be silently dropped with no `Inbound message` log line until you do. | -| `channels.bluebubbles.attachmentRoots` | `channels.imessage.attachmentRoots` | Local roots; same wildcard rules. | -| _(N/A)_ | `channels.imessage.remoteAttachmentRoots` | Only used when `remoteHost` is set for SCP fetches. | -| `channels.bluebubbles.mediaMaxMb` | `channels.imessage.mediaMaxMb` | Default 16 MB on iMessage (BlueBubbles default was 8 MB). Set explicitly if you want to keep the lower cap. | -| `channels.bluebubbles.textChunkLimit` | `channels.imessage.textChunkLimit` | Default 4000 on both. | -| `channels.bluebubbles.coalesceSameSenderDms` | `channels.imessage.coalesceSameSenderDms` | Same opt-in. DM-only — group chats keep instant per-message dispatch on both channels. Widens the default inbound debounce to 7000 ms when enabled without an explicit `messages.inbound.byChannel.imessage` or global `messages.inbound.debounceMs`. See [iMessage docs § Coalescing split-send DMs](/channels/imessage#coalescing-split-send-dms-command--url-in-one-composition). | -| `channels.bluebubbles.enrichGroupParticipantsFromContacts` | _(N/A)_ | iMessage already reads sender display names from `chat.db`. | -| `channels.bluebubbles.actions.*` | `channels.imessage.actions.*` | Per-action toggles: `reactions`, `edit`, `unsend`, `reply`, `sendWithEffect`, `renameGroup`, `setGroupIcon`, `addParticipant`, `removeParticipant`, `leaveGroup`, `sendAttachment`. | +| BlueBubbles | bundled iMessage | Notes | +| ---------------------------------------------------------- | ----------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `channels.bluebubbles.enabled` | `channels.imessage.enabled` | Same semantics (default `true` once the block exists). | +| `channels.bluebubbles.serverUrl` | _(removed)_ | No REST server — the plugin spawns `imsg rpc` over stdio. | +| `channels.bluebubbles.password` | _(removed)_ | No webhook authentication needed. | +| _(implicit)_ | `channels.imessage.cliPath` | Path to `imsg` (default `imsg`); use a wrapper script for SSH. | +| _(implicit)_ | `channels.imessage.dbPath` | Optional Messages.app `chat.db` override; auto-detected when omitted. | +| _(implicit)_ | `channels.imessage.remoteHost` | `host` or `user@host` — only needed when `cliPath` is an SSH wrapper and you want SCP attachment fetches. | +| `channels.bluebubbles.dmPolicy` | `channels.imessage.dmPolicy` | Same values (`pairing` / `allowlist` / `open` / `disabled`); default `pairing`. | +| `channels.bluebubbles.allowFrom` | `channels.imessage.allowFrom` | Same handle formats (`+15555550123`, `user@example.com`). Pairing-store approvals do not transfer — see below. | +| `channels.bluebubbles.groupPolicy` | `channels.imessage.groupPolicy` | Same values (`allowlist` / `open` / `disabled`); default `allowlist`. | +| `channels.bluebubbles.groupAllowFrom` | `channels.imessage.groupAllowFrom` | Same. Does not fall back to `allowFrom` — an empty `groupAllowFrom` under `groupPolicy: "allowlist"` drops all groups regardless of `allowFrom`. | +| `channels.bluebubbles.groups` | `channels.imessage.groups` | Copy the `"*"` wildcard entry verbatim; re-key per-group entries by numeric iMessage `chat_id` — see "Group registry footgun". `requireMention`, `tools`, `toolsBySender`, `systemPrompt` carry over. | +| `channels.bluebubbles.sendReadReceipts` | `channels.imessage.sendReadReceipts` | Default `true`. With the bundled plugin this only fires when the private API probe is up. | +| `channels.bluebubbles.includeAttachments` | `channels.imessage.includeAttachments` | Same shape, same off-by-default. If attachments flowed on BlueBubbles, set this explicitly — inbound photos/media are silently dropped (no `Inbound message` log line) until you do. | +| `channels.bluebubbles.attachmentRoots` | `channels.imessage.attachmentRoots` | Local roots; same wildcard rules. | +| _(N/A)_ | `channels.imessage.remoteAttachmentRoots` | Only used when `remoteHost` is set for SCP fetches. | +| `channels.bluebubbles.mediaMaxMb` | `channels.imessage.mediaMaxMb` | Default 16 MB on iMessage (BlueBubbles default was 8 MB). Set explicitly to keep the lower cap. | +| `channels.bluebubbles.textChunkLimit` | `channels.imessage.textChunkLimit` | Default 4000 on both. | +| `channels.bluebubbles.coalesceSameSenderDms` | `channels.imessage.coalesceSameSenderDms` | Same opt-in. DM-only — groups keep per-message dispatch. Widens the default inbound debounce to 7000 ms unless `messages.inbound.byChannel.imessage` or a global `messages.inbound.debounceMs` is set. See [Coalescing split-send DMs](/channels/imessage#coalescing-split-send-dms-command--url-in-one-composition). | +| `channels.bluebubbles.enrichGroupParticipantsFromContacts` | _(N/A)_ | `imsg` already surfaces sender display names from `chat.db`. | +| `channels.bluebubbles.actions.*` | `channels.imessage.actions.*` | Same per-action toggles (`reactions`, `edit`, `unsend`, `reply`, `sendWithEffect`, `renameGroup`, `setGroupIcon`, `addParticipant`, `removeParticipant`, `leaveGroup`, `sendAttachment`) plus new `polls`. All default to enabled; private API actions still require the bridge. | Multi-account configs (`channels.bluebubbles.accounts.*`) translate one-to-one to `channels.imessage.accounts.*`. ## Group registry footgun -The bundled iMessage plugin runs **two** separate group allowlist gates back-to-back. Both must pass for a group message to reach the agent: +The bundled iMessage plugin runs two group gates back to back. A group message must pass both to reach the agent: -1. **Sender / chat-target allowlist** (`channels.imessage.groupAllowFrom`) — checked by `isAllowedIMessageSender`. Matches inbound messages by sender handle, `chat_guid`, `chat_identifier`, or `chat_id`. Same shape as BlueBubbles. -2. **Group registry** (`channels.imessage.groups`) — checked by `resolveChannelGroupPolicy` from `inbound-processing.ts:199`. With `groupPolicy: "allowlist"`, this gate requires either: - - a `groups: { "*": { ... } }` wildcard entry (sets `allowAll = true`), or - - an explicit per-`chat_id` entry under `groups`. +1. **Sender / chat-target allowlist** (`channels.imessage.groupAllowFrom`) — matches the sender handle or the chat target (`chat_id:`, `chat_guid:`, `chat_identifier:` entries). Does not fall back to `allowFrom`: with `groupPolicy: "allowlist"` and an empty `groupAllowFrom`, every group message drops here regardless of `allowFrom`. +2. **Group registry** (`channels.imessage.groups`) — keyed by numeric iMessage `chat_id`: + - No `groups` block (or an empty one): groups pass this gate as long as gate 1 has a non-empty `groupAllowFrom`; sender filtering governs access. The gateway still logs a startup warning nudging you to add a `groups` block. + - `groups` with entries but no `"*"`: only the listed `chat_id` keys pass. Listing any group turns the registry into an allowlist even under `groupPolicy: "open"`. + - `groups: { "*": { ... } }`: every group passes this gate. -If gate 1 passes but gate 2 fails, the message is dropped. The plugin emits two `warn`-level signals so this is no longer silent at default log level: +The migration trap: BlueBubbles keyed `groups` entries by chat GUID / chat identifier, while the iMessage registry keys by numeric `chat_id`. Per-group entries copied verbatim create a non-empty registry whose keys never match, so every group message drops at gate 2. Copy the `"*"` wildcard verbatim; re-key specific group entries with `chat_id` values from `imsg chats`. -- A one-time startup `warn` per account when `groupPolicy: "allowlist"` is set but `channels.imessage.groups` is empty (no `"*"` wildcard, no per-`chat_id` entries) — fired before any messages land. -- A one-time per-`chat_id` `warn` the first time a specific group is dropped at runtime, naming the chat_id and the exact key to add to `groups` to allow it. +Both drop paths are visible at the default log level via `warn` lines: -DMs continue to work because they take a different code path. +- Once per account at startup, when `groupPolicy: "allowlist"` is set and `channels.imessage.groups` is empty: `imessage: groupPolicy="allowlist" but channels.imessage.groups is empty for account ""`. +- Once per `chat_id` at runtime, when the registry drops a group: `imessage: dropping group message from chat_id= ... not in channels.imessage.groups allowlist`, naming the exact key to add. -This is the most common BlueBubbles → bundled-iMessage migration failure mode: operators copy `groupAllowFrom` and `groupPolicy` but skip the `groups` block, because BlueBubbles' `groups: { "*": { "requireMention": true } }` looks like an unrelated mention setting. It's actually load-bearing for the registry gate. +DMs keep working either way — they take a different code path, so DM success does not prove group routing. -The minimum config to keep group messages flowing after `groupPolicy: "allowlist"`: +The safe minimum with `groupPolicy: "allowlist"`: ```json5 { @@ -153,96 +148,72 @@ The minimum config to keep group messages flowing after `groupPolicy: "allowlist } ``` -`requireMention: true` under `*` is harmless when no mention patterns are configured: the runtime sets `canDetectMention = false` and short-circuits the mention drop at `inbound-processing.ts:512`. With mention patterns configured (`agents.list[].groupChat.mentionPatterns`), it works as expected. - -If the gateway logs `imessage: dropping group message from chat_id=` or the startup line `imessage: groupPolicy="allowlist" but channels.imessage.groups is empty`, gate 2 is dropping — add the `groups` block. +`requireMention: true` under `"*"` is harmless when no mention patterns are configured: the runtime cannot detect mentions without patterns and skips the mention drop. With `agents.list[].groupChat.mentionPatterns` (fallback `messages.groupChat.mentionPatterns`) configured, mention gating applies as expected. ## Step-by-step -1. Add an iMessage block alongside the existing BlueBubbles block. Keep it disabled while the Gateway is still routing BlueBubbles traffic: +1. Translate the config. Keep the new block disabled while you edit; the old `channels.bluebubbles` block is ignored by current OpenClaw and can sit alongside as reference: ```json5 { channels: { - bluebubbles: { - enabled: true, - // ... existing config ... - }, imessage: { - enabled: false, + enabled: false, // flip to true when ready to cut over cliPath: "/opt/homebrew/bin/imsg", dmPolicy: "pairing", allowFrom: ["+15555550123"], // copy from bluebubbles.allowFrom groupPolicy: "allowlist", groupAllowFrom: [], // copy from bluebubbles.groupAllowFrom - groups: { "*": { requireMention: true } }, // copy from bluebubbles.groups — silently drops groups if missing, see "Group registry footgun" above - actions: { - reactions: true, - edit: true, - unsend: true, - reply: true, - sendWithEffect: true, - sendAttachment: true, - }, + groups: { "*": { requireMention: true } }, // wildcard copies verbatim; re-key per-chat entries by chat_id + // actions default to enabled; set individual toggles false to disable }, }, } ``` -2. **Probe before traffic matters** — stop the Gateway, temporarily enable the iMessage block, and confirm iMessage reports healthy from the CLI: +2. **Cut over and probe.** Set `channels.imessage.enabled: true`, restart the Gateway, and confirm the channel reports healthy: ```bash - openclaw gateway stop - # edit config: channels.imessage.enabled = true - openclaw channels status --probe --channel imessage # expect imessage.privateApi.available: true + openclaw gateway restart + openclaw channels status --probe --channel imessage # expect "works"; --json shows privateApi.available: true ``` - `channels status --probe` only probes configured, enabled accounts. Do not restart the Gateway with both BlueBubbles and iMessage enabled unless you intentionally want both channel monitors running. If you are not cutting over immediately, set `channels.imessage.enabled` back to `false` before restarting the Gateway. Use the direct `imsg` commands in [Before you start](#before-you-start) to validate the Mac before enabling OpenClaw traffic. + The probe requires a reachable Gateway and only probes configured, enabled accounts. Use the direct `imsg` commands in [Before you start](#before-you-start) to validate the Mac itself. -3. **Cut over.** Once the enabled iMessage account reports healthy, remove the BlueBubbles config and keep iMessage enabled: +3. **Verify DMs.** Send the agent a direct message; confirm the reply lands. - ```json5 - { - channels: { - imessage: { enabled: true /* ... */ }, - }, - } - ``` +4. **Verify groups separately.** DMs and groups take different code paths — DM success does not prove groups are routing. Send a message in an allowed group chat and confirm the reply lands. If the group goes silent (no agent reply, no error), check the gateway log for the two `warn` lines from "Group registry footgun" above; either one means the `groups` block is missing, empty, or keyed wrong. - Restart the gateway. Inbound iMessage traffic now flows through the bundled plugin. +5. **Verify the action surface.** From a paired DM, ask the agent to react, edit, unsend, reply, send a photo, and (in a group) rename the group or add/remove a participant. Each action should land natively in Messages.app. If any action throws `iMessage requires the imsg private API bridge`, run `imsg launch` again and refresh with `openclaw channels status --probe`. -4. **Verify DMs.** Send the agent a direct message; confirm the reply lands. - -5. **Verify groups separately.** DMs and groups take different code paths — DM success does not prove groups are routing. Send the agent a message in a paired group chat and confirm the reply lands. If the group goes silent (no agent reply, no error), check the gateway log for `imessage: dropping group message from chat_id=` or the startup `imessage: groupPolicy="allowlist" but channels.imessage.groups is empty` line — both fire at the default log level. If either appears, your `groups` block is missing or empty — see "Group registry footgun" above. - -6. **Verify the action surface** — from a paired DM, ask the agent to react, edit, unsend, reply, send a photo, and (in a group) rename the group / add or remove a participant. Each action should land natively in Messages.app. If any throws "iMessage `` requires the imsg private API bridge", run `imsg launch` again and refresh `channels status --probe`. - -7. **Remove the BlueBubbles server and config** once iMessage DMs, groups, and actions are verified. OpenClaw will not use `channels.bluebubbles`. +6. **Remove the BlueBubbles server and the `channels.bluebubbles` block** once iMessage DMs, groups, and actions are verified. OpenClaw does not read `channels.bluebubbles`. ## Action parity at a glance -| Action | legacy BlueBubbles | bundled iMessage | -| --------------------------------------------------- | ----------------------------------- | ----------------------------------------------------------------------------- | -| Send text / SMS fallback | ✅ | ✅ | -| Send media (photo, video, file, voice) | ✅ | ✅ | -| Threaded reply (`reply_to_guid`) | ✅ | ✅ (closes [#51892](https://github.com/openclaw/openclaw/issues/51892)) | -| Tapback (`react`) | ✅ | ✅ | -| Edit / unsend (macOS 13+ recipients) | ✅ | ✅ | -| Send with screen effect | ✅ | ✅ (closes part of [#9394](https://github.com/openclaw/openclaw/issues/9394)) | -| Rich text bold / italic / underline / strikethrough | ✅ | ✅ (typed-run formatting via attributedBody) | -| Rename group / set group icon | ✅ | ✅ | -| Add / remove participant, leave group | ✅ | ✅ | -| Read receipts and typing indicator | ✅ | ✅ (gated on private API probe) | -| Same-sender DM coalescing | ✅ | ✅ (DM-only; opt-in via `channels.imessage.coalesceSameSenderDms`) | -| Inbound recovery after a restart | ✅ (webhook replay + history fetch) | ✅ (automatic: replay missed via since_rowid + dedupe; wider window on local) | +| Action | legacy BlueBubbles | bundled iMessage | +| --------------------------------------------------- | ------------------ | ----------------------------------------------------------------------------- | +| Send text / SMS fallback | ✅ | ✅ | +| Send media (photo, video, file, voice) | ✅ | ✅ | +| Threaded reply (`reply_to_guid`) | ✅ | ✅ (closes [#51892](https://github.com/openclaw/openclaw/issues/51892)) | +| Tapback (`react`) | ✅ | ✅ | +| Edit / unsend (macOS 13+ recipients) | ✅ | ✅ | +| Send with screen effect | ✅ | ✅ (closes part of [#9394](https://github.com/openclaw/openclaw/issues/9394)) | +| Rich text bold / italic / underline / strikethrough | ✅ | ✅ (typed-run formatting via attributedBody) | +| Native Messages polls (create and vote) | ❌ | ✅ (`actions.polls`; recipients need iOS/macOS 26+ for native rendering) | +| Rename group / set group icon | ✅ | ✅ | +| Add / remove participant, leave group | ✅ | ✅ | +| Read receipts and typing indicator | ✅ | ✅ (gated on private API probe) | +| Same-sender DM coalescing | ✅ | ✅ (DM-only; opt-in via `channels.imessage.coalesceSameSenderDms`) | +| Inbound recovery after a restart | ✅ | ✅ (automatic: `since_rowid` replay + GUID dedupe; wider window on local) | -iMessage recovers messages missed while the gateway was down: on startup it replays from the last dispatched rowid via `imsg watch.subscribe` `since_rowid` and dedupes by GUID, while a stale-backlog age fence suppresses the Push-flush "backlog bomb". This runs over the `imsg` RPC connection, so it works for remote SSH `cliPath` setups too; local setups get a wider recovery window because they can read `chat.db`. See [Inbound recovery after a bridge or gateway restart](/channels/imessage#inbound-recovery-after-a-bridge-or-gateway-restart). +iMessage recovers messages missed while the gateway was down: on startup it replays from the last dispatched rowid via `imsg watch.subscribe` `since_rowid`, dedupes by GUID, and a stale-backlog age fence suppresses the Push-flush "backlog bomb". This runs over the `imsg` RPC connection, so it works for remote SSH `cliPath` setups too; local setups get a wider recovery window because they can read `chat.db`. See [Inbound recovery after a bridge or gateway restart](/channels/imessage#inbound-recovery-after-a-bridge-or-gateway-restart). ## Pairing, sessions, and ACP bindings -- **Pairing approvals** carry over by handle. You do not need to re-approve known senders — `channels.imessage.allowFrom` recognizes the same `+15555550123` / `user@example.com` strings BlueBubbles used. -- **Sessions** stay scoped per agent + chat. DMs collapse into the agent main session under default `session.dmScope=main`; group sessions stay isolated per `chat_id`. The session keys differ (`agent::imessage:group:` vs the BlueBubbles equivalent) — old conversation history under BlueBubbles session keys does not carry into iMessage sessions. -- **ACP bindings** referencing `match.channel: "bluebubbles"` need to be updated to `"imessage"`. The `match.peer.id` shapes (`chat_id:`, `chat_guid:`, `chat_identifier:`, bare handle) are identical. +- **Allowlists carry over by handle.** `channels.imessage.allowFrom` recognizes the same `+15555550123` / `user@example.com` strings BlueBubbles used — copy them verbatim. +- **Pairing-store approvals do not transfer.** The pairing store is per channel and nothing migrates the old BlueBubbles store. Senders who were approved only through pairing must pair once more under iMessage, or you add their handles to `allowFrom`. +- **Sessions** stay scoped per agent + chat. DMs collapse into the agent main session under default `session.dmScope=main`; group sessions stay isolated per `chat_id` (`agent::imessage:group:`). Old conversation history under BlueBubbles session keys does not carry into iMessage sessions. +- **ACP bindings** referencing `match.channel: "bluebubbles"` must change to `"imessage"`. The `match.peer.id` shapes (`chat_id:`, `chat_guid:`, `chat_identifier:`, bare handle) are identical. ## No rollback channel diff --git a/docs/channels/imessage.md b/docs/channels/imessage.md index aab9273c32c4..27d7d3552670 100644 --- a/docs/channels/imessage.md +++ b/docs/channels/imessage.md @@ -16,7 +16,7 @@ For OpenClaw iMessage deployments, use `imsg` on a signed-in macOS Messages host BlueBubbles support was removed. Migrate `channels.bluebubbles` configs to `channels.imessage`; OpenClaw supports iMessage through `imsg` only. Start with [BlueBubbles removal and the imsg iMessage path](/announcements/bluebubbles-imessage) for the short announcement, or [Coming from BlueBubbles](/channels/imessage-from-bluebubbles) for the full migration table. -Status: native external CLI integration. Gateway spawns `imsg rpc` and communicates over JSON-RPC on stdio (no separate daemon/port). Advanced actions require `imsg launch` and a successful private API probe. +Status: native external CLI integration. The Gateway spawns `imsg rpc` and speaks JSON-RPC over stdio — no separate daemon or port. Advanced actions require `imsg launch` and a successful private API probe. @@ -104,8 +104,8 @@ exec ssh -T gateway-host imsg "$@" cliPath: "~/.openclaw/scripts/imsg-ssh", remoteHost: "user@gateway-host", // used for SCP attachment fetches includeAttachments: true, - // Optional: override allowed attachment roots. - // Defaults include /Users/*/Library/Messages/Attachments + // Optional: extra allowed attachment roots (merged with the default + // /Users/*/Library/Messages/Attachments). attachmentRoots: ["/Users/*/Library/Messages/Attachments"], remoteAttachmentRoots: ["/Users/*/Library/Messages/Attachments"], }, @@ -114,7 +114,7 @@ exec ssh -T gateway-host imsg "$@" ``` If `remoteHost` is not set, OpenClaw attempts to auto-detect it by parsing the SSH wrapper script. - `remoteHost` must be `host` or `user@host` (no spaces or SSH options). + `remoteHost` must be `host` or `user@host` (no spaces or SSH options); unsafe values are ignored. OpenClaw uses strict host-key checking for SCP, so the relay host key must already exist in `~/.ssh/known_hosts`. Attachment paths are validated against allowed roots (`attachmentRoots` / `remoteAttachmentRoots`). @@ -138,10 +138,10 @@ A wrapper that buffers stdin until a large block fills will produce symptoms tha - Messages must be signed in on the Mac running `imsg`. - Full Disk Access is required for the process context running OpenClaw/`imsg` (Messages DB access). - Automation permission is required to send messages through Messages.app. -- For advanced actions (react / edit / unsend / threaded reply / effects / polls / group ops), System Integrity Protection must be disabled — see [Enabling the imsg private API](#enabling-the-imsg-private-api) below. Basic text and media send/receive work without it. +- For advanced actions (react / edit / unsend / threaded reply / effects / polls / group ops), System Integrity Protection must be disabled — see [Enabling the imsg private API](#enabling-the-imsg-private-api). Basic text and media send/receive work without it. -Permissions are granted per process context. If gateway runs headless (LaunchAgent/SSH), run a one-time interactive command in that same context to trigger prompts: +Permissions are granted per process context. If the gateway runs headless (LaunchAgent/SSH), run a one-time interactive command in that same context to trigger prompts: ```bash imsg chats --limit 1 @@ -179,9 +179,9 @@ Use one of the supported `imsg` process contexts instead: `imsg` ships in two operational modes: - **Basic mode** (default, no SIP changes needed): outbound text and media via `send`, inbound watch/history, chat list. This is what you get out of the box from a fresh `brew install steipete/tap/imsg` plus the standard macOS permissions above. -- **Private API mode**: `imsg` injects a helper dylib into `Messages.app` to call internal `IMCore` functions. This is what unlocks `react`, `edit`, `unsend`, `reply` (threaded), `sendWithEffect`, `poll` and `poll-vote` (native Messages polls), `renameGroup`, `setGroupIcon`, `addParticipant`, `removeParticipant`, `leaveGroup`, plus typing indicators and read receipts. +- **Private API mode**: `imsg` injects a helper dylib into `Messages.app` to call internal `IMCore` functions. This unlocks `react`, `edit`, `unsend`, `reply` (threaded), `sendWithEffect`, `poll` and `poll-vote` (native Messages polls), `renameGroup`, `setGroupIcon`, `addParticipant`, `removeParticipant`, `leaveGroup`, plus typing indicators and read receipts. -To reach the advanced action surface that this channel page documents, you need Private API mode. The `imsg` README is explicit about the requirement: +The advanced action surface on this page requires Private API mode. The `imsg` README is explicit about the requirement: > Advanced features such as `read`, `typing`, `launch`, bridge-backed rich send, message mutation, and chat management are opt-in. They require SIP to be disabled and a helper dylib to be injected into `Messages.app`. `imsg launch` refuses to inject when SIP is enabled. @@ -190,7 +190,7 @@ The helper-injection technique uses `imsg`'s own dylib to reach Messages private **Disabling SIP is a real security tradeoff.** SIP is one of macOS's core protections against running modified system code; turning it off system-wide opens up additional attack surface and side effects. Notably, **disabling SIP on Apple Silicon Macs also disables the ability to install and run iOS apps on your Mac**. -Treat this as a deliberate operational choice, not a default. If your threat model can't tolerate SIP being off, bundled iMessage is limited to basic mode — text and media send/receive only, no reactions / edit / unsend / effects / group ops. +Treat this as a deliberate operational choice, not a default. If your threat model cannot tolerate SIP being off, bundled iMessage is limited to basic mode — text and media send/receive only, no reactions / edit / unsend / effects / group ops. ### Setup @@ -218,14 +218,10 @@ Treat this as a deliberate operational choice, not a default. If your threat mod **macOS 26 (Tahoe), verified on 26.5.1:** SIP off **plus** the `DisableLibraryValidation` command above is sufficient to inject the helper across 26.0 through 26.5.x. **No boot-args are required.** The plist is the decisive factor and the most common missing step when injection fails on Tahoe: - **With the plist:** `imsg launch` injects and `imsg status` reports `advanced_features: true`. - - **Without the plist (even with SIP off):** `imsg launch` fails with `Failed to launch: Timeout waiting for Messages.app to initialize`. AMFI rejects the adhoc helper at load, so the bridge never becomes ready and the launch times out. That timeout is the symptom most people hit on Tahoe, and the fix is the plist above, not anything more drastic. - - This was confirmed with a controlled before/after on macOS 26.5.1 (Apple Silicon): with the plist, the dylib maps into `Messages.app` and the bridge comes up; remove the plist and reboot, and `imsg launch` produces the timeout failure above with the dylib not mapped. + - **Without the plist (even with SIP off):** `imsg launch` fails with `Failed to launch: Timeout waiting for Messages.app to initialize`. AMFI rejects the adhoc helper at load, so the bridge never becomes ready and the launch times out. That timeout is the symptom most people hit on Tahoe; the fix is the plist above, not anything more drastic. If `imsg launch` injection or specific `selectors` start returning false after a macOS upgrade, this gate is the usual cause. Check your SIP and library-validation state before assuming the SIP step itself failed. If those settings are correct and the bridge still cannot inject, collect `imsg status --json` plus the `imsg launch` output and report it to the `imsg` project instead of weakening additional system-wide security controls. - Follow Apple's Recovery-mode flow for your Mac to disable SIP before running `imsg launch`. - 3. **Inject the helper.** With SIP disabled and Messages.app signed in: ```bash @@ -244,12 +240,12 @@ Treat this as a deliberate operational choice, not a default. If your threat mod If `openclaw channels status --probe` reports the channel as `works` but specific actions throw "iMessage `` requires the imsg private API bridge" at dispatch time, run `imsg launch` again — the helper can fall out (Messages.app restart, OS update, etc.) and the cached `available: true` status will keep advertising actions until the next probe refreshes. -### When you can't disable SIP +### When SIP stays enabled -If SIP-disabled isn't acceptable for your threat model: +If disabling SIP is not acceptable for your threat model: - `imsg` falls back to basic mode — text + media + receive only. -- The OpenClaw plugin still advertises text/media send and inbound monitoring; it just hides `react`, `edit`, `unsend`, `reply`, `sendWithEffect`, and group ops from the action surface (per the per-method capability gate). +- The OpenClaw plugin still advertises text/media send and inbound monitoring; it hides `react`, `edit`, `unsend`, `reply`, `sendWithEffect`, and group ops from the action surface (per the per-method capability gate). - You can run a separate non-Apple-Silicon Mac (or a dedicated bot Mac) with SIP off for the iMessage workload, while keeping SIP enabled on your primary devices. See [Dedicated bot macOS user (separate iMessage identity)](#deployment-patterns) below. ## Access control and routing @@ -259,7 +255,7 @@ If SIP-disabled isn't acceptable for your threat model: `channels.imessage.dmPolicy` controls direct messages: - `pairing` (default) - - `allowlist` + - `allowlist` (requires at least one `allowFrom` entry) - `open` (requires `allowFrom` to include `"*"`) - `disabled` @@ -272,7 +268,7 @@ If SIP-disabled isn't acceptable for your threat model: `channels.imessage.groupPolicy` controls group handling: - - `allowlist` (default when configured) + - `allowlist` (default) - `open` - `disabled` @@ -280,23 +276,23 @@ If SIP-disabled isn't acceptable for your threat model: `groupAllowFrom` entries can also reference static sender access groups (`accessGroup:`). - Runtime fallback: if `groupAllowFrom` is unset, iMessage group sender checks use `allowFrom`; set `groupAllowFrom` when DM and group admission should differ. + Runtime fallback: if `groupAllowFrom` is unset, iMessage group sender checks use `allowFrom`; set `groupAllowFrom` when DM and group admission should differ. An explicitly empty `groupAllowFrom: []` does not fall back — it blocks all group senders under `allowlist`. Runtime note: if `channels.imessage` is completely missing, runtime falls back to `groupPolicy="allowlist"` and logs a warning (even if `channels.defaults.groupPolicy` is set). - Group routing has **two** allowlist gates running back-to-back, and both must pass: + Group routing under `groupPolicy: "allowlist"` runs **two** gates back-to-back: - 1. **Sender / chat-target allowlist** (`channels.imessage.groupAllowFrom`) — handle, `chat_guid`, `chat_identifier`, or `chat_id`. - 2. **Group registry** (`channels.imessage.groups`) — with `groupPolicy: "allowlist"`, this gate requires either a `groups: { "*": { ... } }` wildcard entry (sets `allowAll = true`), or an explicit per-`chat_id` entry under `groups`. + 1. **Sender allowlist** (`channels.imessage.groupAllowFrom`) — handle, `accessGroup:`, `chat_guid`, `chat_identifier`, or `chat_id`. Empty (and no `allowFrom` fallback) means every group message is dropped (`groupPolicy allowlist (empty groupAllowFrom)` in verbose logs). + 2. **Group registry** (`channels.imessage.groups`) — enforced once the map has entries: the chat must match an explicit per-`chat_id` entry or a `groups: { "*": { ... } }` wildcard (`allowAll`). A group chat that matches neither is dropped. When `groups` is empty or missing, the sender allowlist alone decides admission. - If gate 2 has nothing in it, every group message is dropped. The plugin emits two `warn`-level signals at the default log level: + The plugin emits `warn`-level signals at the default log level: - - one-time per account at startup: `imessage: groupPolicy="allowlist" but channels.imessage.groups is empty for account ""` - - one-time per `chat_id` at runtime: `imessage: dropping group message from chat_id= ...` + - one-time per account at startup when `groups` is empty: `imessage: groupPolicy="allowlist" but channels.imessage.groups is empty for account ""` + - one-time per `chat_id` when the registry gate drops a chat: `imessage: dropping group message from chat_id= ...` - DMs continue to work because they take a different code path. + DMs are unaffected — they take a different code path. - Minimum config to keep groups flowing under `groupPolicy: "allowlist"`: + Recommended config for group flow under `groupPolicy: "allowlist"`: ```json5 { @@ -310,7 +306,7 @@ If SIP-disabled isn't acceptable for your threat model: } ``` - If those `warn` lines appear in the gateway log, gate 2 is dropping — add the `groups` block. + If the per-`chat_id` drop lines appear in the gateway log, the registry gate is dropping that chat — add it to `groups` (or add the `"*"` wildcard). Mention gating for groups: @@ -318,12 +314,11 @@ If SIP-disabled isn't acceptable for your threat model: - iMessage has no native mention metadata - mention detection uses regex patterns (`agents.list[].groupChat.mentionPatterns`, fallback `messages.groupChat.mentionPatterns`) - with no configured patterns, mention gating cannot be enforced - - Control commands from authorized senders can bypass mention gating in groups. + - control commands from authorized senders bypass mention gating Per-group `systemPrompt`: - Each entry under `channels.imessage.groups.*` accepts an optional `systemPrompt` string. The value is injected into the agent's system prompt on every turn that handles a message in that group. Resolution mirrors the per-group prompt resolution used by `channels.whatsapp.groups`: + Each entry under `channels.imessage.groups.*` accepts an optional `systemPrompt` string, injected into the agent's system prompt on every turn that handles a message in that group. Resolution mirrors `channels.whatsapp.groups`: 1. **Group-specific system prompt** (`groups[""].systemPrompt`): used when the specific group entry exists in the map **and** its `systemPrompt` key is defined. If `systemPrompt` is an empty string (`""`) the wildcard is suppressed and no system prompt is applied to that group. 2. **Group wildcard system prompt** (`groups["*"].systemPrompt`): used when the specific group entry is absent from the map entirely, or when it exists but defines no `systemPrompt` key. @@ -350,7 +345,7 @@ If SIP-disabled isn't acceptable for your threat model: } ``` - Per-group prompts only apply to group messages — direct messages in this channel are unaffected. + Per-group prompts only apply to group messages — direct messages are unaffected. @@ -370,7 +365,7 @@ If SIP-disabled isn't acceptable for your threat model: ## ACP conversation bindings -Legacy iMessage chats can also be bound to ACP sessions. +iMessage chats can be bound to ACP sessions. Fast operator flow: @@ -379,7 +374,7 @@ Fast operator flow: - `/new` and `/reset` reset the same bound ACP session in place. - `/acp close` closes the ACP session and removes the binding. -Configured persistent bindings are supported through top-level `bindings[]` entries with `type: "acp"` and `match.channel: "imessage"`. +Configured persistent bindings use top-level `bindings[]` entries with `type: "acp"` and `match.channel: "imessage"`. `match.peer.id` can use: @@ -431,7 +426,7 @@ See [ACP Agents](/tools/acp-agents) for shared ACP binding behavior. 1. Create/sign in a dedicated macOS user. 2. Sign into Messages with the bot Apple ID in that user. 3. Install `imsg` in that user. - 4. Create SSH wrapper so OpenClaw can run `imsg` in that user context. + 4. Create an SSH wrapper so OpenClaw can run `imsg` in that user context. 5. Point `channels.imessage.accounts..cliPath` and `.dbPath` to that user profile. First run may require GUI approvals (Automation + Full Disk Access) in that bot user session. @@ -496,17 +491,19 @@ See [ACP Agents](/tools/acp-agents) for shared ACP binding behavior. - attachment paths must match allowed roots: - `channels.imessage.attachmentRoots` (local) - `channels.imessage.remoteAttachmentRoots` (remote SCP mode) - - default root pattern: `/Users/*/Library/Messages/Attachments` + - configured roots extend the default root pattern `/Users/*/Library/Messages/Attachments` (merged, not replaced) - SCP uses strict host-key checking (`StrictHostKeyChecking=yes`) - outbound media size uses `channels.imessage.mediaMaxMb` (default 16 MB) - + - text chunk limit: `channels.imessage.textChunkLimit` (default 4000) - chunk mode: `channels.imessage.chunkMode` - `length` (default) - `newline` (paragraph-first splitting) + - outbound markdown bold/italic/underline/strikethrough is converted to native styled text (macOS 15+ recipients render the styling; older recipients see plain text without the markers); markdown tables are converted per the channel markdown table mode + - `channels.imessage.sendTransport` (`auto` default, `bridge`, `applescript`) selects how `imsg` delivers sends @@ -534,6 +531,8 @@ See [ACP Agents](/tools/acp-agents) for shared ACP binding behavior. When `imsg launch` is running and `openclaw channels status --probe` reports `privateApi.available: true`, the message tool can use iMessage-native actions in addition to normal text sends. +All actions are enabled by default; use `channels.imessage.actions` to turn individual actions off: + ```json5 { channels: { @@ -559,13 +558,13 @@ When `imsg launch` is running and `openclaw channels status --probe` reports `pr - - **react**: Add/remove iMessage tapbacks (`messageId`, `emoji`, `remove`). Supported tapbacks map to love, like, dislike, laugh, emphasize, and question. - - **reply**: Send a threaded reply to an existing message (`messageId`, `text` or `message`, plus `chatGuid`, `chatId`, `chatIdentifier`, or `to`). - - **sendWithEffect**: Send text with an iMessage effect (`text` or `message`, `effect` or `effectId`). - - **edit**: Edit a sent message on supported macOS/private API versions (`messageId`, `text` or `newText`). - - **unsend**: Retract a sent message on supported macOS/private API versions (`messageId`). + - **react**: Add/remove iMessage tapbacks (`messageId`, `emoji`, `remove`). Supported tapbacks map to love, like, dislike, laugh, emphasize, and question. Removing without an emoji clears whichever tapback was set. + - **reply**: Send a threaded reply to an existing message (`messageId`, `text` or `message`, plus `chatGuid`, `chatId`, `chatIdentifier`, or `to`). Reply-with-attachment additionally needs an `imsg` build whose `send-rich` supports `--file`. + - **sendWithEffect**: Send text with an iMessage effect (`text` or `message`, `effect` or `effectId`). Short names: slam, loud, gentle, invisibleink, confetti, lasers, fireworks, balloon, heart, echo, happybirthday, shootingstar, sparkles, spotlight. + - **edit**: Edit a sent message on supported macOS/private API versions (`messageId`, `text` or `newText`). Only messages the gateway itself sent can be edited. + - **unsend**: Retract a sent message on supported macOS/private API versions (`messageId`). Only messages the gateway itself sent can be unsent. - **upload-file**: Send media/files (`buffer` as base64 or a hydrated `media`/`path`/`filePath`, `filename`, optional `asVoice`). Legacy alias: `sendAttachment`. - - **renameGroup**, **setGroupIcon**, **addParticipant**, **removeParticipant**, **leaveGroup**: Manage group chats when the current target is a group conversation. + - **renameGroup**, **setGroupIcon**, **addParticipant**, **removeParticipant**, **leaveGroup**: Manage group chats when the current target is a group conversation. These mutate the host's Messages identity, so they require an owner sender or an `operator.admin` Gateway client. - **poll**: Create a native Apple Messages poll (`pollQuestion`, `pollOption` repeated 2 to 12 times, plus `chatGuid`, `chatId`, `chatIdentifier`, or `to`). Recipients on iOS/iPadOS/macOS 26+ see and vote on it natively; older OS versions get a "Sent a poll" text fallback. Requires `selectors.pollPayloadMessage`. - **poll-vote**: Vote on an existing poll (`pollId` or `messageId`, plus exactly one of `pollOptionIndex`, `pollOptionId`, or `pollOptionText`). Requires `selectors.pollVoteMessage` and the `poll.vote` RPC method. @@ -574,7 +573,7 @@ When `imsg launch` is running and `openclaw channels status --probe` reports `pr - Inbound iMessage context includes both short `MessageSid` values and full message GUIDs when available. Short IDs are scoped to the recent SQLite-backed reply cache and are checked against the current chat before use. If a short ID has expired or belongs to another chat, retry with the full `MessageSidFull`. + Inbound iMessage context includes both short `MessageSid` values and full message GUIDs (`MessageSidFull`) when available. Short IDs are scoped to the recent SQLite-backed reply cache and are checked against the current chat before use. If a short ID has expired or belongs to another chat, retry with the full `MessageSidFull`. @@ -596,7 +595,7 @@ When `imsg launch` is running and `openclaw channels status --probe` reports `pr } ``` - Older `imsg` builds that pre-date the per-method capability list will gate off typing/read silently; OpenClaw logs a one-time warning per restart so the missing receipt is attributable. + Older `imsg` builds that pre-date the per-method capability list gate off typing/read silently; OpenClaw logs a one-time warning per restart so the missing receipt is attributable. @@ -620,13 +619,14 @@ When `imsg launch` is running and `openclaw channels status --probe` reports `pr - `👎` (Dislike tapback) → `deny` - `allow-always` remains a manual fallback: send `/approve allow-always` as a regular reply. - Reaction handling requires the reacting user's handle to be an explicit approver. The approver list is read from `channels.imessage.allowFrom` (or `channels.imessage.accounts..allowFrom`); add the user's phone number in E.164 form or their Apple ID email. The wildcard entry `"*"` is honored but allows any sender to approve. The reaction shortcut intentionally bypasses `reactionNotifications`, `dmPolicy`, and `groupAllowFrom` because the explicit-approver allowlist is the only gate that matters for approval resolution. + Reaction handling requires the reacting user's handle to be an explicit approver. The approver list is read from `channels.imessage.allowFrom` (or `channels.imessage.accounts..allowFrom`); add the user's phone number in E.164 form or their Apple ID email (chat targets such as `chat_id:*` are not valid approver entries). The wildcard entry `"*"` is honored but allows any sender to approve; an empty approver list disables the reaction shortcut entirely. The reaction shortcut intentionally bypasses `reactionNotifications`, `dmPolicy`, and `groupAllowFrom` because the explicit-approver allowlist is the only gate that matters for approval resolution. - **Behavior change with this release:** When `channels.imessage.allowFrom` is non-empty, the `/approve ` text command is now authorized against that approver list (not the broader DM allowlist). Senders permitted on the DM allowlist but not in `allowFrom` will receive an explicit denial. Add every operator who should be able to approve via `/approve` (and via reactions) to `allowFrom` to preserve the previous behavior. When `allowFrom` is empty the legacy "same-chat fallback" stays in effect and `/approve` continues to authorize anyone the DM allowlist permits. + `/approve` text command authorization follows the same list: when `channels.imessage.allowFrom` is non-empty, `/approve ` is authorized against that approver list (not the broader DM allowlist), and senders permitted on the DM allowlist but not in `allowFrom` receive an explicit denial. When `allowFrom` is empty, the same-chat fallback stays in effect and `/approve` authorizes anyone the DM allowlist permits. Add every operator who should approve — via `/approve` or via reactions — to `allowFrom`. Operator notes: - - The reaction binding is stored both in memory (with TTL matched to the approval expiry) and in the gateway's persistent keyed store, so a tapback that lands shortly after a gateway restart still resolves the approval. - - Cross-device `is_from_me=true` tapbacks (the operator's own reaction on a paired Apple device) are intentionally ignored so the bot cannot self-approve. + - The reaction binding is stored both in memory and in the gateway's persistent keyed store (TTL matched to the approval expiry), and the gateway also polls pending prompts for tapbacks, so a tapback that lands shortly after a gateway restart still resolves the approval. + - The operator's own `is_from_me=true` tapback (for example from a paired Apple device) resolves the approval when that handle is an explicit approver. + - Approval prompts route into a group conversation only when explicit approvers are configured; otherwise any group member could approve. - Legacy text-style tapbacks (`Liked "…"` plain text from very old Apple clients) cannot resolve approvals because they carry no message GUID; reaction resolution requires the structured tapback metadata that current macOS / iOS clients emit. @@ -657,7 +657,7 @@ When a user types a command and a URL together — e.g. `Dump https://example.co 1. A text message (`"Dump"`). 2. A URL-preview balloon (`"https://..."`) with OG-preview images as attachments. -The two rows arrive at OpenClaw ~0.8-2.0 s apart on most setups. Without coalescing, the agent receives the command alone on turn 1, replies (often "send me the URL"), and only sees the URL on turn 2 — at which point the command context is already lost. This is Apple's send pipeline, not anything OpenClaw or `imsg` introduces. +The two rows arrive at OpenClaw ~0.8-2.0 s apart on most setups. Without coalescing, the agent gets the command alone on turn 1 (and often replies "send me the URL") before the URL arrives on turn 2. This is Apple's send pipeline, not anything OpenClaw or `imsg` introduces. `channels.imessage.coalesceSameSenderDms` opts a DM into buffering consecutive same-sender rows. When `imsg` exposes the structural URL-preview marker `balloon_bundle_id: "com.apple.messages.URLBalloonProvider"` on one of the source rows, OpenClaw merges only that real split-send and keeps any other buffered rows as separate turns. On older `imsg` builds that emit no balloon metadata at all, OpenClaw cannot tell a split-send from separate sends, so it falls back to merging the bucket. That preserves the pre-metadata behavior rather than regressing `Dump ` split-sends into two turns. Group chats continue to dispatch per-message so multi-user turn structure is preserved. @@ -705,11 +705,11 @@ The two rows arrive at OpenClaw ~0.8-2.0 s apart on most setups. Without coalesc - - **Precise merging needs current `imsg` payload metadata.** When the URL row includes `balloon_bundle_id`, only that real split-send merges and other buffered rows stay separate. On older `imsg` builds that expose no balloon metadata, OpenClaw falls back to merging the buffered bucket so `Dump ` split-sends are not regressed into two turns (interim back-compat, removed once `imsg` coalesces split-sends upstream). + - **Precise merging needs current `imsg` payload metadata.** With `balloon_bundle_id` present, only the real split-send merges; the metadata-less fallback merge described above is interim back-compat, removed once `imsg` coalesces split-sends upstream. - **Added latency for DM messages.** With the flag on, every DM (including standalone control commands and single-text follow-ups) waits up to the debounce window before dispatching, in case a URL-preview row is coming. Group-chat messages keep instant dispatch. - **Merged output is bounded.** Merged text caps at 4000 chars with an explicit `…[truncated]` marker; attachments cap at 20; source entries cap at 10 (first-plus-latest retained beyond that). Every source GUID is tracked in `coalescedMessageGuids` for downstream telemetry. - **DM-only.** Group chats fall through to per-message dispatch so the bot stays responsive when multiple people are typing. - - **Opt-in, per-channel.** Other channels (Telegram, WhatsApp, Slack, …) are unaffected. Legacy BlueBubbles configs that set `channels.bluebubbles.coalesceSameSenderDms` should migrate that value to `channels.imessage.coalesceSameSenderDms`. + - **Opt-in, per-channel.** Other channels (Discord, Slack, Telegram, WhatsApp, …) are unaffected. Legacy BlueBubbles configs that set `channels.bluebubbles.coalesceSameSenderDms` should migrate that value to `channels.imessage.coalesceSameSenderDms`. @@ -733,7 +733,7 @@ The "Flag on" column shows behavior on an `imsg` build that emits `balloon_bundl iMessage recovers messages missed while the gateway was down, and at the same time suppresses the stale "backlog bomb" Apple can flush after a Push recovery. The default behavior is always on, built on the inbound dedupe. - **Replay dedupe.** Every dispatched inbound message is recorded by its Apple GUID in persistent plugin state (`imessage.inbound-dedupe`), claimed at ingestion and committed after handling (released on a transient failure so it can retry). Anything already handled is dropped instead of dispatched twice. This is what lets recovery replay aggressively without per-message bookkeeping. -- **Downtime recovery.** On startup the monitor remembers the last dispatched `chat.db` rowid (a persisted per-account cursor) and passes it to `imsg watch.subscribe` as `since_rowid`, so imsg replays the rows that landed while the gateway was down, then tails live. Replay is bounded to the most recent rows and to messages up to ~2 hours old, and the dedupe drops anything already handled. +- **Downtime recovery.** On startup the monitor remembers the last dispatched `chat.db` rowid (a persisted per-account cursor) and passes it to `imsg watch.subscribe` as `since_rowid`, so imsg replays the rows that landed while the gateway was down, then tails live. Replay is bounded to the most recent 500 rows and to messages up to ~2 hours old, and the dedupe drops anything already handled. - **Stale-backlog age fence.** Rows above the startup boundary are genuinely live; one whose send date is more than ~15 minutes older than its arrival is the Push-flush backlog and is suppressed. Replayed rows (at or below the boundary) use the wider recovery window instead, so a recently-missed message is delivered while ancient history is not. Recovery works over both local and remote `cliPath` setups, because `since_rowid` replay runs over the same `imsg` RPC connection. The difference is the window: when the gateway can read `chat.db` (local), it anchors the startup rowid boundary, caps the replay span, and delivers missed messages up to a couple of hours old. Over a remote SSH `cliPath` it cannot read the database, so the replay is uncapped and every row uses the live age fence — it still recovers recently-missed messages and still suppresses old backlog, just with the narrower live window. Run the gateway on the Messages Mac for the wider recovery window. @@ -742,13 +742,13 @@ Recovery works over both local and remote `cliPath` setups, because `since_rowid Suppressed backlog is logged at the default level, never silently dropped (the `recovery` flag shows which window applied): -``` +```text imessage: suppressed stale inbound backlog account= sent= recovery= ( suppressed since start) ``` ### Migration -`channels.imessage.catchup.*` is deprecated — downtime recovery is now automatic and needs no config for new setups. Existing configs with `catchup.enabled: true` remain honored as a compatibility profile for the recovery replay window. Disabled catchup blocks (`enabled: false` or no `enabled: true`) are retired; `openclaw doctor --fix` removes those. +`channels.imessage.catchup.*` is deprecated — downtime recovery is automatic and needs no config for new setups. Existing configs with `catchup.enabled: true` remain honored as a compatibility profile for the recovery replay window. Disabled catchup blocks (`enabled: false` or no `enabled: true`) are retired; `openclaw doctor --fix` removes those. ## Troubleshooting @@ -762,7 +762,7 @@ imessage: suppressed stale inbound backlog account= sent= recovery= diff --git a/docs/channels/index.md b/docs/channels/index.md index c015eb1c87f1..2a0ced184824 100644 --- a/docs/channels/index.md +++ b/docs/channels/index.md @@ -9,6 +9,42 @@ title: "Chat channels" OpenClaw can talk to you on any chat app you already use. Each channel connects via the Gateway. Text is supported everywhere; media and reactions vary by channel. +iMessage, Telegram, and the WebChat UI ship with the core install. Channels marked +"official plugin" install with one command (`openclaw plugins install @openclaw/`) +or on demand during `openclaw onboard` / `openclaw channels add`, then need a Gateway +restart. "External plugin" channels are maintained outside the OpenClaw repo. + +## Supported channels + +- [Discord](/channels/discord) - Discord Bot API + Gateway; supports servers, channels, and DMs (official plugin). +- [Feishu](/channels/feishu) - Feishu/Lark bot via WebSocket (official plugin). +- [Google Chat](/channels/googlechat) - Google Chat API app via HTTP webhook (official plugin). +- [iMessage](/channels/imessage) - Included in core. Native macOS integration via the `imsg` bridge on a signed-in Mac (or SSH wrapper when the Gateway runs elsewhere), including private API actions for replies, tapbacks, effects, attachments, and group management. +- [IRC](/channels/irc) - Classic IRC servers; channels + DMs with pairing/allowlist controls (official plugin). +- [LINE](/channels/line) - LINE Messaging API bot (official plugin). +- [Matrix](/channels/matrix) - Matrix protocol (official plugin). +- [Mattermost](/channels/mattermost) - Bot API + WebSocket; channels, groups, DMs (official plugin). +- [Microsoft Teams](/channels/msteams) - Bot Framework; enterprise support (official plugin). +- [Nextcloud Talk](/channels/nextcloud-talk) - Self-hosted chat via Nextcloud Talk (official plugin). +- [Nostr](/channels/nostr) - Decentralized DMs via NIP-04 (official plugin). +- [QQ Bot](/channels/qqbot) - QQ Bot API; private chat, group chat, and rich media (official plugin). +- [Raft](/channels/raft) - Raft CLI wake bridge for human and agent collaboration (official plugin). +- [Signal](/channels/signal) - signal-cli; privacy-focused (official plugin). +- [Slack](/channels/slack) - Bolt SDK; workspace apps (official plugin). +- [SMS](/channels/sms) - Twilio-backed SMS through the Gateway webhook (official plugin). +- [Synology Chat](/channels/synology-chat) - Synology NAS Chat via outgoing+incoming webhooks (official plugin). +- [Telegram](/channels/telegram) - Included in core. Bot API via grammY; supports groups. +- [Tlon](/channels/tlon) - Urbit-based messenger (official plugin). +- [Twitch](/channels/twitch) - Twitch chat via IRC connection (official plugin). +- [Voice Call](/plugins/voice-call) - Telephony via Plivo, Telnyx, or Twilio (official plugin). +- [WebChat](/web/webchat) - Included in core. Gateway WebChat UI over WebSocket. +- [WeChat](/channels/wechat) - Tencent iLink bot via QR login; private chats only (external plugin). +- [WhatsApp](/channels/whatsapp) - Most popular; uses Baileys and requires QR pairing (official plugin). +- [Yuanbao](/channels/yuanbao) - Tencent Yuanbao bot (external plugin). +- [Zalo](/channels/zalo) - Zalo Bot API; Vietnam's popular messenger (official plugin). +- [Zalo ClawBot](/channels/zaloclawbot) - Personal Zalo assistant via QR login; owner-bound (external plugin). +- [Zalo Personal](/channels/zalouser) - Zalo personal account via QR login (official plugin). + ## Delivery notes - Telegram replies that contain markdown image syntax, such as `![alt](url)`, @@ -25,42 +61,11 @@ Text is supported everywhere; media and reactions vary by channel. so unmentioned room chatter becomes quiet context unless the agent sends with the `message` tool. -## Supported channels - -- [Discord](/channels/discord) - Discord Bot API + Gateway; supports servers, channels, and DMs. -- [Feishu](/channels/feishu) - Feishu/Lark bot via WebSocket (bundled plugin). -- [Google Chat](/channels/googlechat) - Google Chat API app via HTTP webhook (downloadable plugin). -- [iMessage](/channels/imessage) - Native macOS integration via the `imsg` bridge on a signed-in Mac (or SSH wrapper when the Gateway runs elsewhere), including private API actions for replies, tapbacks, effects, attachments, and group management. Preferred for new OpenClaw iMessage setups when host permissions and Messages access fit. -- [IRC](/channels/irc) - Classic IRC servers; channels + DMs with pairing/allowlist controls. -- [LINE](/channels/line) - LINE Messaging API bot (downloadable plugin). -- [Matrix](/channels/matrix) - Matrix protocol (downloadable plugin). -- [Mattermost](/channels/mattermost) - Bot API + WebSocket; channels, groups, DMs (downloadable plugin). -- [Microsoft Teams](/channels/msteams) - Bot Framework; enterprise support (bundled plugin). -- [Nextcloud Talk](/channels/nextcloud-talk) - Self-hosted chat via Nextcloud Talk (bundled plugin). -- [Nostr](/channels/nostr) - Decentralized DMs via NIP-04 (bundled plugin). -- [QQ Bot](/channels/qqbot) - QQ Bot API; private chat, group chat, and rich media (bundled plugin). -- [Raft](/channels/raft) - Raft CLI wake bridge for human and agent collaboration (external plugin). -- [Signal](/channels/signal) - signal-cli; privacy-focused. -- [Slack](/channels/slack) - Bolt SDK; workspace apps. -- [SMS](/channels/sms) - Twilio-backed SMS through the Gateway webhook (official plugin). -- [Synology Chat](/channels/synology-chat) - Synology NAS Chat via outgoing+incoming webhooks (bundled plugin). -- [Telegram](/channels/telegram) - Bot API via grammY; supports groups. -- [Tlon](/channels/tlon) - Urbit-based messenger (bundled plugin). -- [Twitch](/channels/twitch) - Twitch chat via IRC connection (bundled plugin). -- [Voice Call](/plugins/voice-call) - Telephony via Plivo or Twilio (plugin, installed separately). -- [WebChat](/web/webchat) - Gateway WebChat UI over WebSocket. -- [WeChat](/channels/wechat) - Tencent iLink Bot plugin via QR login; private chats only (external plugin). -- [WhatsApp](/channels/whatsapp) - Most popular; uses Baileys and requires QR pairing. -- [Yuanbao](/channels/yuanbao) - Tencent Yuanbao bot (external plugin). -- [Zalo](/channels/zalo) - Zalo Bot API; Vietnam's popular messenger (bundled plugin). -- [Zalo ClawBot](/channels/zaloclawbot) - Personal Zalo assistant via QR login; owner-bound (external plugin). -- [Zalo Personal](/channels/zalouser) - Zalo personal account via QR login (bundled plugin). - ## Notes - Channels can run simultaneously; configure multiple and OpenClaw will route per chat. -- Fastest setup is usually **Telegram** (simple bot token). WhatsApp requires QR pairing and - stores more state on disk. +- Fastest setup is usually **Telegram** (simple bot token, no plugin install). WhatsApp + requires QR pairing and stores more state on disk. - Group behavior varies by channel; see [Groups](/channels/groups). - DM pairing and allowlists are enforced for safety; see [Security](/gateway/security). - Troubleshooting: [Channel troubleshooting](/channels/troubleshooting). diff --git a/docs/channels/irc.md b/docs/channels/irc.md index cfeb3597f3aa..3498e85e0274 100644 --- a/docs/channels/irc.md +++ b/docs/channels/irc.md @@ -17,8 +17,7 @@ Install the official IRC plugin, then configure it under `channels.irc`. openclaw plugins install @openclaw/irc ``` -2. Enable IRC config in `~/.openclaw/openclaw.json`. -3. Set at least: +2. Set at least host, nick, and the channels to join in `~/.openclaw/openclaw.json`: ```json5 { @@ -35,18 +34,32 @@ openclaw plugins install @openclaw/irc } ``` -Prefer a private IRC server for bot coordination. If you intentionally use a public IRC network, common choices include Libera.Chat, OFTC, and Snoonet. Avoid predictable public channels for bot or swarm backchannel traffic. - -4. Start/restart gateway: +3. Start/restart the Gateway: ```bash openclaw gateway run ``` +Prefer a private IRC server for bot coordination. If you intentionally use a public IRC network, common choices include Libera.Chat, OFTC, and Snoonet. Avoid predictable public channels for bot or swarm backchannel traffic. + +## Connection settings + +| Key | Default | Notes | +| ----------------------------- | ----------------------------- | ----------------------------------------------------------- | +| `host` | none (required) | IRC server hostname | +| `port` | `6697` with TLS, `6667` plain | 1-65535 | +| `tls` | `true` | Set `false` only for intentional plaintext | +| `nick` | none (required) | Bot nick | +| `username` | nick, else `openclaw` | IRC username | +| `realname` | `OpenClaw` | Realname/GECOS field | +| `password` / `passwordFile` | none | Server password; file must be a regular file | +| `channels` | none | Channels to join (`["#openclaw"]`) | +| `accounts` / `defaultAccount` | none | Multi-account setup; env vars fill only the default account | + ## Security defaults - IRC uses raw TCP/TLS sockets outside OpenClaw operator-managed forward proxy routing. In deployments that require all egress through that forward proxy, set `channels.irc.enabled=false` unless direct IRC egress is explicitly approved. -- `channels.irc.dmPolicy` defaults to `"pairing"`. +- `channels.irc.dmPolicy` defaults to `"pairing"`: unknown DM senders get a pairing code you approve with `openclaw pairing approve irc `. - `channels.irc.groupPolicy` defaults to `"allowlist"`. - With `groupPolicy="allowlist"`, set `channels.irc.groups` to define allowed channels. - Use TLS (`channels.irc.tls=true`) unless you intentionally accept plaintext transport. @@ -62,7 +75,7 @@ Config keys: - DM allowlist (DM sender access): `channels.irc.allowFrom` - Group sender allowlist (channel sender access): `channels.irc.groupAllowFrom` -- Per-channel controls (channel + sender + mention rules): `channels.irc.groups["#channel"]` +- Per-channel controls (channel + sender + mention rules): `channels.irc.groups["#channel"]` with `requireMention`, `allowFrom`, `enabled`, `tools`, `toolsBySender`, `skills`, and `systemPrompt` - `channels.irc.groupPolicy="open"` allows unconfigured channels (**still mention-gated by default**) Allowlist entries should use stable sender identities (`nick!user@host`). @@ -79,7 +92,7 @@ If you see logs like: - setting `channels.irc.groupAllowFrom` (global for all channels), or - setting per-channel sender allowlists: `channels.irc.groups["#channel"].allowFrom` -Example (allow anyone in `#tuirc-dev` to talk to the bot): +Example (allow anyone in `#openclaw` to talk to the bot): ```json5 { @@ -87,7 +100,7 @@ Example (allow anyone in `#tuirc-dev` to talk to the bot): irc: { groupPolicy: "allowlist", groups: { - "#tuirc-dev": { allowFrom: ["*"] }, + "#openclaw": { allowFrom: ["*"] }, }, }, }, @@ -96,7 +109,7 @@ Example (allow anyone in `#tuirc-dev` to talk to the bot): ## Reply triggering (mentions) -Even if a channel is allowed (via `groupPolicy` + `groups`) and the sender is allowed, OpenClaw defaults to **mention-gating** in group contexts. +Even if a channel is allowed (via `groupPolicy` + `groups`) and the sender is allowed, OpenClaw defaults to **mention-gating** in group contexts. The bot counts as mentioned when the message contains the connected bot nick or matches your configured mention patterns. That means you may see logs like `drop channel … (missing-mention)` unless the message includes a mention pattern that matches the bot. @@ -108,7 +121,7 @@ To make the bot reply in an IRC channel **without needing a mention**, disable m irc: { groupPolicy: "allowlist", groups: { - "#tuirc-dev": { + "#openclaw": { requireMention: false, allowFrom: ["*"], }, @@ -145,7 +158,7 @@ To reduce risk, restrict tools for that channel. channels: { irc: { groups: { - "#tuirc-dev": { + "#openclaw": { allowFrom: ["*"], tools: { deny: ["group:runtime", "group:fs", "gateway", "nodes", "cron", "browser"], @@ -166,13 +179,13 @@ Use `toolsBySender` to apply a stricter policy to `"*"` and a looser one to your channels: { irc: { groups: { - "#tuirc-dev": { + "#openclaw": { allowFrom: ["*"], toolsBySender: { "*": { deny: ["group:runtime", "group:fs", "gateway", "nodes", "cron", "browser"], }, - "id:eigen": { + "id:alice": { deny: ["gateway", "nodes", "cron"], }, }, @@ -185,9 +198,8 @@ Use `toolsBySender` to apply a stricter policy to `"*"` and a looser one to your Notes: -- `toolsBySender` keys should use `id:` for IRC sender identity values: - `id:eigen` or `id:eigen!~eigen@174.127.248.171` for stronger matching. -- Legacy unprefixed keys are still accepted and matched as `id:` only. +- `toolsBySender` keys should use explicit prefixes (`channel:`, `id:`, `e164:`, `username:`, `name:`). For IRC use `id:` with the sender identity value: `id:alice` or `id:alice!~alice@203.0.113.7` for stronger matching. +- Legacy unprefixed keys are still accepted, matched as `id:` only, and emit a deprecation warning. - The first matching sender policy wins; `"*"` is the wildcard fallback. For more on group access vs mention-gating (and how they interact), see: [/channels/groups](/channels/groups). @@ -210,7 +222,9 @@ To identify with NickServ after connect: } ``` -Optional one-time registration on connect: +NickServ identify runs by default whenever a password is set (`enabled` only needs to be `false` to opt out). `service` defaults to `NickServ`; `passwordFile` is an alternative to inline `password`. + +Optional one-time registration on connect (`register: true` requires `registerEmail`): ```json5 { diff --git a/docs/channels/line.md b/docs/channels/line.md index 7785e8e3a270..7d23c8cebaae 100644 --- a/docs/channels/line.md +++ b/docs/channels/line.md @@ -8,12 +8,12 @@ title: LINE --- LINE connects to OpenClaw via the LINE Messaging API. The plugin runs as a webhook -receiver on the gateway and uses your channel access token + channel secret for +receiver on the Gateway and uses your channel access token + channel secret for authentication. -Status: downloadable plugin. Direct messages, group chats, media, locations, Flex -messages, template messages, and quick replies are supported. Reactions and threads -are not supported. +Status: official plugin, installed separately. Direct messages, group chats, media, +locations, Flex messages, template messages, and quick replies are supported. +Reactions and threads are not supported. ## Install @@ -38,19 +38,19 @@ openclaw plugins install ./path/to/local/line-plugin 4. Enable **Use webhook** in the Messaging API settings. 5. Set the webhook URL to your gateway endpoint (HTTPS required): -``` +```text https://gateway-host/line/webhook ``` -The gateway responds to LINE's webhook verification (GET) and acknowledges signed +The Gateway answers LINE's webhook verification (GET) and acknowledges signed inbound events (POST) immediately after signature and payload validation; agent processing continues asynchronously. If you need a custom path, set `channels.line.webhookPath` or `channels.line.accounts..webhookPath` and update the URL accordingly. -Security note: +Security notes: -- LINE signature verification is body-dependent (HMAC over the raw body), so OpenClaw applies strict pre-auth body limits and timeout before verification. +- LINE signature verification is body-dependent (HMAC over the raw body), so OpenClaw applies a strict pre-auth body limit (64 KB) and read timeout before verification. - OpenClaw processes webhook events from the verified raw request bytes. Upstream middleware-transformed `req.body` values are ignored for signature-integrity safety. ## Configure @@ -105,6 +105,7 @@ Token/secret files: ``` `tokenFile` and `secretFile` must point to regular files. Symlinks are rejected. +Inline config values win over files; env vars are the last fallback for the default account. Multiple accounts: @@ -127,7 +128,7 @@ Multiple accounts: ## Access control Direct messages default to pairing. Unknown senders get a pairing code and their -messages are ignored until approved. +messages are ignored until approved: ```bash openclaw pairing list line @@ -136,12 +137,12 @@ openclaw pairing approve line Allowlists and policies: -- `channels.line.dmPolicy`: `pairing | allowlist | open | disabled` +- `channels.line.dmPolicy`: `pairing | allowlist | open | disabled` (default `pairing`) - `channels.line.allowFrom`: allowlisted LINE user IDs for DMs; `dmPolicy: "open"` requires `["*"]` -- `channels.line.groupPolicy`: `allowlist | open | disabled` +- `channels.line.groupPolicy`: `allowlist | open | disabled` (default `allowlist`) - `channels.line.groupAllowFrom`: allowlisted LINE user IDs for groups -- Per-group overrides: `channels.line.groups..allowFrom` -- Static sender access groups can be referenced from `allowFrom`, `groupAllowFrom`, and per-group `allowFrom` with `accessGroup:`. +- Per-group overrides: `channels.line.groups..allowFrom` (plus `enabled`, `requireMention`, `systemPrompt`, `skills`) +- Static sender access groups can be referenced from `allowFrom`, `groupAllowFrom`, and per-group `allowFrom` with `accessGroup:`; see [Access groups](/channels/access-groups). - Runtime note: if `channels.line` is completely missing, runtime falls back to `groupPolicy="allowlist"` for group checks (even if `channels.defaults.groupPolicy` is set). LINE IDs are case-sensitive. Valid IDs look like: @@ -159,8 +160,7 @@ LINE IDs are case-sensitive. Valid IDs look like: animation while the agent works. - Media downloads are capped by `channels.line.mediaMaxMb` (default 10). - Inbound media is saved under `~/.openclaw/media/inbound/` before it is passed - to the agent, matching the shared media store used by other bundled channel - plugins. + to the agent, matching the shared media store used by other channel plugins. ## Channel data (rich messages) @@ -200,7 +200,7 @@ messages. The LINE plugin also ships a `/card` command for Flex message presets: -``` +```text /card info "Welcome" "Thanks for joining!" ``` @@ -215,15 +215,20 @@ See [ACP agents](/tools/acp-agents) for details. ## Outbound media -The LINE plugin supports sending images, videos, and audio files through the agent message tool. Media is sent via the LINE-specific delivery path with appropriate preview and tracking handling: +The LINE plugin sends images, videos, and audio through the agent message tool: -- **Images**: sent as LINE image messages with automatic preview generation. -- **Videos**: sent with explicit preview and content-type handling. -- **Audio**: sent as LINE audio messages. +- **Images**: sent as LINE image messages; the preview image defaults to the media URL. +- **Videos**: require a preview image; set `channelData.line.previewImageUrl` to an image URL. +- **Audio**: sent as LINE audio messages; duration defaults to 60 seconds unless `channelData.line.durationMs` is set. -Outbound media URLs must be public HTTPS URLs. OpenClaw validates the target hostname before handing the URL to LINE and rejects loopback, link-local, and private-network targets. +The media kind is taken from `channelData.line.mediaKind` when set, otherwise inferred +from the other LINE options or the URL file suffix, with image as the fallback. -Generic media sends fall back to the existing image-only route when a LINE-specific path is not available. +Outbound media URLs must be public HTTPS URLs of at most 2000 characters. OpenClaw +validates the target hostname before handing the URL to LINE and rejects loopback, +link-local, and private-network targets. + +Generic media sends without LINE-specific options use the image route. ## Troubleshooting diff --git a/docs/channels/location.md b/docs/channels/location.md index a9000aab7bec..9e6cdc2f052e 100644 --- a/docs/channels/location.md +++ b/docs/channels/location.md @@ -1,5 +1,5 @@ --- -summary: "Inbound channel location parsing (Telegram/WhatsApp/Matrix) and context fields" +summary: "Inbound channel location parsing (Telegram, WhatsApp, Matrix, LINE) and context fields" read_when: - Adding or modifying channel location parsing - Using location context fields in agent prompts or tools @@ -13,22 +13,23 @@ OpenClaw normalizes shared locations from chat channels into: Currently supported: -- **Telegram** (location pins + venues + live locations) -- **WhatsApp** (locationMessage + liveLocationMessage) +- **LINE** (location messages with title/address) - **Matrix** (`m.location` with `geo_uri`) +- **Telegram** (location pins + venues + live locations) +- **WhatsApp** (`locationMessage` + `liveLocationMessage`) ## Text formatting -Locations are rendered as friendly lines without brackets: +Locations are rendered as friendly lines without brackets. Coordinates use six decimal places; accuracy is rounded to whole meters: - Pin: - `📍 48.858844, 2.294351 ±12m` -- Named place: +- Named place (same line; the name/address go to the metadata block only): - `📍 48.858844, 2.294351 ±12m` - Live share: - `🛰 Live location: 48.858844, 2.294351 ±12m` -If the channel includes a label, address, or caption/comment, it is preserved in the context payload and appears in the prompt as fenced untrusted JSON: +If the channel includes a label, address, or caption/comment, it is preserved in the context payload and appears in the prompt as fenced untrusted JSON (fields are omitted when absent): ````text Location (untrusted metadata): @@ -36,6 +37,8 @@ Location (untrusted metadata): { "latitude": 48.858844, "longitude": 2.294351, + "accuracy_m": 12, + "source": "place", "name": "Eiffel Tower", "address": "Champ de Mars, Paris", "caption": "Meet here" @@ -56,13 +59,16 @@ When a location is present, these fields are added to `ctx`: - `LocationIsLive` (boolean) - `LocationCaption` (string; optional) +When the channel does not set an explicit source, OpenClaw infers it: live shares become `live`, locations with a name or address become `place`, everything else is `pin`. + The prompt renderer treats `LocationName`, `LocationAddress`, and `LocationCaption` as untrusted metadata and serializes them through the same bounded JSON path used for other channel context. ## Channel notes -- **Telegram**: venues map to `LocationName/LocationAddress`; live locations use `live_period`. +- **LINE**: location message `title`/`address` map to `LocationName`/`LocationAddress`; no live locations. +- **Matrix**: `geo_uri` is parsed as a pin location; the `u` (uncertainty) parameter maps to `LocationAccuracy`, the event body populates `LocationCaption`, altitude is ignored, and `LocationIsLive` is always false. +- **Telegram**: venues map to `LocationName`/`LocationAddress`; live locations are detected via `live_period`. - **WhatsApp**: `locationMessage.comment` and `liveLocationMessage.caption` populate `LocationCaption`. -- **Matrix**: `geo_uri` is parsed as a pin location; altitude is ignored and `LocationIsLive` is always false. ## Related diff --git a/docs/channels/matrix-migration.md b/docs/channels/matrix-migration.md index 9170a259f891..fb80a7f3db61 100644 --- a/docs/channels/matrix-migration.md +++ b/docs/channels/matrix-migration.md @@ -19,19 +19,18 @@ For most users, the upgrade is in place: You do not need to rename config keys or reinstall the plugin under a new name. The root `openclaw` package no longer bundles Matrix runtime code or Matrix SDK dependencies. If `openclaw channels status` shows Matrix is configured but the -plugin is missing after an update, run `openclaw doctor --fix` or +plugin is not installed, run `openclaw doctor --fix` or `openclaw plugins install @openclaw/matrix`; do not install Matrix SDK packages into the root OpenClaw package. ## What the migration does automatically -When the gateway starts, and when you run [`openclaw doctor --fix`](/gateway/doctor), OpenClaw tries to repair old Matrix state automatically. -Before any actionable Matrix migration step mutates on-disk state, OpenClaw creates or reuses a focused recovery snapshot. +Matrix migration runs when the gateway starts (through the loaded Matrix plugin), when you run [`openclaw doctor --fix`](/gateway/doctor), and as a fallback when the Matrix client starts and still finds old on-disk state. Before any actionable migration step mutates on-disk state, OpenClaw creates or reuses a focused recovery snapshot. When you use `openclaw update`, the exact trigger depends on how OpenClaw is installed: -- source installs run `openclaw doctor --fix` during the update flow, then restart the gateway by default -- package-manager installs update the package, run a non-interactive doctor pass, then rely on the default gateway restart so startup can finish Matrix migration +- source installs run a non-interactive `openclaw doctor --fix` pass during the update flow, then restart the gateway by default +- package-manager installs update the package, run `openclaw doctor --non-interactive --fix`, then rely on the default gateway restart so startup can finish Matrix migration - if you use `openclaw update --no-restart`, startup-backed Matrix migration is deferred until you later run `openclaw doctor --fix` and restart the gateway Automatic migration covers: @@ -39,10 +38,10 @@ Automatic migration covers: - creating or reusing a pre-migration snapshot under `~/Backups/openclaw-migrations/` - reusing your cached Matrix credentials - keeping the same account selection and `channels.matrix` config -- moving the oldest flat Matrix sync store into the current account-scoped location -- moving the oldest flat Matrix crypto store into the current account-scoped location when the target account can be resolved safely +- moving the old flat Matrix sync store and crypto store into the current account-scoped location when the target account can be resolved safely +- importing file-based sidecar state (`bot-storage.json` sync cache, `recovery-key.json`, `legacy-crypto-migration.json`, IndexedDB snapshots) into Matrix SQLite state; migrated files are archived with a `.migrated` suffix - extracting a previously saved Matrix room-key backup decryption key from the old rust crypto store, when that key exists locally -- reusing the most complete existing token-hash storage root for the same Matrix account, homeserver, and user when the access token changes later +- reusing the most complete existing token-hash storage root for the same Matrix account, homeserver, user, and device when the access token changes later - scanning sibling token-hash storage roots for pending encrypted-state restore metadata when the Matrix access token changed but the account/device identity stayed the same - restoring backed-up room keys into the new crypto store on the next Matrix startup @@ -55,7 +54,7 @@ Snapshot details: About multi-account upgrades: -- the oldest flat Matrix store (`~/.openclaw/matrix/bot-storage.json` and `~/.openclaw/matrix/crypto/`) came from a single-store layout, so OpenClaw can only migrate it into one resolved Matrix account target +- the flat Matrix store (`~/.openclaw/matrix/bot-storage.json` and `~/.openclaw/matrix/crypto/`) came from a single-store layout, so OpenClaw can only migrate it into one resolved Matrix account target - already account-scoped legacy Matrix stores are detected and prepared per configured Matrix account ## What the migration cannot do automatically @@ -68,14 +67,11 @@ OpenClaw cannot automatically recover: - local-only room keys that were never backed up - encrypted state when the target Matrix account cannot be resolved yet because `homeserver`, `userId`, or `accessToken` are still unavailable +- encrypted state when the old crypto store has no recorded device ID for the account - automatic migration of one shared flat Matrix store when multiple Matrix accounts are configured but `channels.matrix.defaultAccount` is not set -- custom plugin path installs that are pinned to a repo path instead of the standard Matrix package +- custom plugin path installs that are pinned to a repo path instead of the standard Matrix package (surfaced by `openclaw doctor`) - a missing recovery key when the old store had backed-up keys but did not keep the decryption key locally -Current warning scope: - -- custom Matrix plugin path installs are surfaced by both gateway startup and `openclaw doctor` - If your old installation had local-only encrypted history that was never backed up, some older encrypted messages may remain unreadable after the upgrade. ## Recommended upgrade flow @@ -122,8 +118,8 @@ If your old installation had local-only encrypted history that was never backed ``` Accept the request in another Matrix client, compare the emoji or decimals, - and type `yes` only when they match. The command exits successfully only - after `Cross-signing verified` becomes `yes`. + and type `yes` only when they match. The command waits for full Matrix + identity trust before reporting success. 8. If you are intentionally abandoning unrecoverable old history and want a fresh backup baseline for future messages, run: @@ -131,6 +127,8 @@ If your old installation had local-only encrypted history that was never backed openclaw matrix verify backup reset --yes ``` + Add `--rotate-recovery-key` only when the old recovery key should stop unlocking the fresh backup. + 9. If no server-side key backup exists yet, create one for future recoveries: ```bash @@ -141,10 +139,9 @@ If your old installation had local-only encrypted history that was never backed Encrypted migration is a two-stage process: -1. Startup or `openclaw doctor --fix` creates or reuses the pre-migration snapshot if encrypted migration is actionable. -2. Startup or `openclaw doctor --fix` inspects the old Matrix crypto store through the active Matrix plugin install. -3. If a backup decryption key is found, OpenClaw writes it into the new recovery-key flow and marks room-key restore as pending. -4. On the next Matrix startup, OpenClaw restores backed-up room keys into the new crypto store automatically. +1. Startup or `openclaw doctor --fix` creates or reuses the pre-migration snapshot if encrypted migration is actionable, then inspects the old Matrix rust crypto store through the crypto inspector bundled with the Matrix plugin. +2. If a backup decryption key is found, OpenClaw imports it into Matrix SQLite state and marks room-key restore as pending. +3. On the next Matrix startup, OpenClaw restores backed-up room keys into the new crypto store automatically. Pending restore state is also picked up from sibling token-hash storage roots when the access token rotated in between. If the old store reports room keys that were never backed up, OpenClaw warns instead of pretending recovery succeeded. @@ -152,19 +149,14 @@ If the old store reports room keys that were never backed up, OpenClaw warns ins ### Upgrade and detection messages -`Matrix plugin upgraded in place.` +`Matrix plugin upgraded in place.` (doctor) or `matrix: plugin upgraded in place for account "..."` (startup) - Meaning: the old on-disk Matrix state was detected and migrated into the current layout. - What to do: nothing unless the same output also includes warnings. -`Matrix migration snapshot created before applying Matrix upgrades.` +`Matrix migration snapshot created before applying Matrix upgrades.` / `Matrix migration snapshot reused before applying Matrix upgrades.` -- Meaning: OpenClaw created a recovery archive before mutating Matrix state. -- What to do: keep the printed archive path until you confirm migration succeeded. - -`Matrix migration snapshot reused before applying Matrix upgrades.` - -- Meaning: OpenClaw found an existing Matrix migration snapshot marker and reused that archive instead of creating a duplicate backup. +- Meaning: doctor created a recovery archive before mutating Matrix state, or found an existing snapshot marker and reused that archive instead of creating a duplicate backup. Startup logs the same as `matrix: created pre-migration backup snapshot: ...` / `matrix: reusing existing pre-migration backup snapshot: ...`. - What to do: keep the printed archive path until you confirm migration succeeded. `Legacy Matrix state detected at ... but channels.matrix is not configured yet.` @@ -182,7 +174,9 @@ If the old store reports room keys that were never backed up, OpenClaw warns ins - Meaning: OpenClaw found one shared flat Matrix store, but it refuses to guess which named Matrix account should receive it. - What to do: set `channels.matrix.defaultAccount` to the intended account, then rerun `openclaw doctor --fix` or restart the gateway. -`Matrix legacy sync store not migrated because the target already exists (...)` +The same three warnings also appear with the prefix `Legacy Matrix encrypted state detected at ...` when the blocked store is the old encrypted crypto store. + +`Matrix legacy sync store not migrated because the target already exists (...)` / `Matrix legacy crypto store not migrated because the target already exists (...)` - Meaning: the new account-scoped location already has a sync or crypto store, so OpenClaw did not overwrite it automatically. - What to do: verify that the current account is the correct one before manually removing or moving the conflicting target. @@ -192,36 +186,16 @@ If the old store reports room keys that were never backed up, OpenClaw warns ins - Meaning: OpenClaw tried to move old Matrix state but the filesystem operation failed. - What to do: inspect filesystem permissions and disk state, then rerun `openclaw doctor --fix`. -`Legacy Matrix encrypted state detected at ... but channels.matrix is not configured yet.` - -- Meaning: OpenClaw found an old encrypted Matrix store, but there is no current Matrix config to attach it to. -- What to do: configure `channels.matrix`, then rerun `openclaw doctor --fix` or restart the gateway. - -`Legacy Matrix encrypted state detected at ... but the account-scoped target could not be resolved yet (need homeserver, userId, and access token for channels.matrix...).` - -- Meaning: the encrypted store exists, but OpenClaw cannot safely decide which current account/device it belongs to. -- What to do: start the gateway once with a working Matrix login, or rerun `openclaw doctor --fix` after cached credentials are available. - -`Legacy Matrix encrypted state detected at ... but multiple Matrix accounts are configured and channels.matrix.defaultAccount is not set.` - -- Meaning: OpenClaw found one shared flat legacy crypto store, but it refuses to guess which named Matrix account should receive it. -- What to do: set `channels.matrix.defaultAccount` to the intended account, then rerun `openclaw doctor --fix` or restart the gateway. - `Matrix migration warnings are present, but no on-disk Matrix mutation is actionable yet. No pre-migration snapshot was needed.` -- Meaning: OpenClaw detected old Matrix state, but the migration is still blocked on missing identity or credential data. +- Meaning: OpenClaw detected old Matrix state, but the migration is still blocked on missing identity or credential data. Startup logs this as `matrix: migration remains in a warning-only state; no pre-migration snapshot was needed yet`. - What to do: finish Matrix login or config setup, then rerun `openclaw doctor --fix` or restart the gateway. -`Legacy Matrix encrypted state was detected, but the Matrix plugin helper is unavailable. Install or repair @openclaw/matrix so OpenClaw can inspect the old rust crypto store before upgrading.` +`Legacy Matrix encrypted state was detected, but the Matrix crypto inspector is unavailable.` -- Meaning: OpenClaw found old encrypted Matrix state, but it could not load the helper entrypoint from the Matrix plugin that normally inspects that store. +- Meaning: OpenClaw found old encrypted Matrix state, but the Matrix plugin build is missing the crypto inspector module that inspects the old rust crypto store. - What to do: reinstall or repair the Matrix plugin (`openclaw plugins install @openclaw/matrix`, or `openclaw plugins install ./path/to/local/matrix-plugin` for a repo checkout), then rerun `openclaw doctor --fix` or restart the gateway. -`Matrix plugin helper path is unsafe: ... Reinstall @openclaw/matrix and try again.` - -- Meaning: OpenClaw found a helper file path that escapes the plugin root or fails plugin boundary checks, so it refused to import it. -- What to do: reinstall the Matrix plugin from a trusted path, then rerun `openclaw doctor --fix` or restart the gateway. - `- Failed creating a Matrix migration snapshot before repair: ...` `- Skipping Matrix migration changes for now. Resolve the snapshot failure, then rerun "openclaw doctor --fix".` @@ -231,12 +205,12 @@ If the old store reports room keys that were never backed up, OpenClaw warns ins `Failed migrating legacy Matrix client storage: ...` -- Meaning: the Matrix client-side fallback found old flat storage, but the move failed. OpenClaw now aborts that fallback instead of silently starting with a fresh store. +- Meaning: the Matrix client-side fallback found old storage, but the migration failed. OpenClaw rolls back completed moves and aborts that fallback instead of silently starting with a fresh store. This error also appears when the flat store targets a different account than the one currently starting. - What to do: inspect filesystem permissions or conflicts, keep the old state intact, and retry after fixing the error. `Matrix is installed from a custom path: ...` -- Meaning: Matrix is pinned to a path install, so mainline updates do not automatically replace it with the repo's standard Matrix package. +- Meaning: Matrix is pinned to a path install, so mainline updates do not automatically replace it with the default Matrix package. - What to do: reinstall with `openclaw plugins install @openclaw/matrix` when you want to return to the default Matrix plugin. ### Encrypted-state recovery messages @@ -248,22 +222,27 @@ If the old store reports room keys that were never backed up, OpenClaw warns ins `matrix: N legacy local-only room key(s) were never backed up and could not be restored automatically` -- Meaning: some old room keys existed only in the old local store and had never been uploaded to Matrix backup. +- Meaning: some old room keys existed only in the old local store and had never been uploaded to Matrix backup. During preparation the same limit is reported as `Legacy Matrix encrypted state for account "..." contains N room key(s) that were never backed up.` - What to do: expect some old encrypted history to remain unavailable unless you can recover those keys manually from another verified client. -`Legacy Matrix encrypted state for account "..." has backed-up room keys, but no local backup decryption key was found. Ask the operator to run "openclaw matrix verify backup restore --recovery-key-stdin" after upgrade if they have the recovery key.` +`Legacy Matrix encrypted state detected at ... but no device ID was found for account "..."` + +- Meaning: the old crypto store does not record which Matrix device it belonged to, so OpenClaw cannot inspect it safely. +- What to do: old encrypted history cannot be recovered automatically; OpenClaw continues without it. + +`Legacy Matrix encrypted state for account "..." has backed-up room keys, but no local backup decryption key was found. Ask the operator to run "openclaw matrix verify backup restore --recovery-key " after upgrade if they have the recovery key.` - Meaning: backup exists, but OpenClaw could not recover the recovery key automatically. -- What to do: run `printf '%s\n' "$MATRIX_RECOVERY_KEY" | openclaw matrix verify backup restore --recovery-key-stdin`. +- What to do: run `printf '%s\n' "$MATRIX_RECOVERY_KEY" | openclaw matrix verify backup restore --recovery-key-stdin` (preferred over passing the key as an argument). `Failed inspecting legacy Matrix encrypted state for account "..." (...): ...` - Meaning: OpenClaw found the old encrypted store, but it could not inspect it safely enough to prepare recovery. - What to do: rerun `openclaw doctor --fix`. If it repeats, keep the old state directory intact and recover using another verified Matrix client plus `printf '%s\n' "$MATRIX_RECOVERY_KEY" | openclaw matrix verify backup restore --recovery-key-stdin`. -`Legacy Matrix backup key was found for account "...", but .../recovery-key.json already contains a different recovery key. Leaving the existing file unchanged.` +`Legacy Matrix backup key was found for account "...", but Matrix SQLite state already contains a different recovery key. Leaving the existing state unchanged.` -- Meaning: OpenClaw detected a backup key conflict and refused to overwrite the current recovery-key file automatically. +- Meaning: OpenClaw detected a backup key conflict and refused to overwrite the current recovery-key state automatically. - What to do: verify which recovery key is correct before retrying any restore command. `Legacy Matrix encrypted state for account "..." cannot be fully converted automatically because the old rust crypto store does not expose all local room keys for export.` @@ -278,30 +257,19 @@ If the old store reports room keys that were never backed up, OpenClaw warns ins ### Manual recovery messages -`Backup key is not loaded on this device. Run 'openclaw matrix verify backup restore' to load it and restore old room keys.` +`openclaw matrix verify status` and `openclaw matrix verify backup status` print a `Backup issue:` line plus `Next steps:` guidance when the room-key backup is not healthy on this device: -- Meaning: OpenClaw knows you should have a backup key, but it is not active on this device. -- What to do: run `openclaw matrix verify backup restore`, or set `MATRIX_RECOVERY_KEY` and run `printf '%s\n' "$MATRIX_RECOVERY_KEY" | openclaw matrix verify backup restore --recovery-key-stdin` if needed. +| Backup issue | Meaning | Fix | +| --------------------------------------------------------------------- | -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | +| `no room-key backup exists on the homeserver` | nothing to restore from | `openclaw matrix verify bootstrap` to create a room key backup | +| `backup decryption key is not loaded on this device` | key exists but is not active here | `openclaw matrix verify backup restore`; if it still cannot load the key, pipe the recovery key via `--recovery-key-stdin` | +| `backup decryption key could not be loaded from secret storage (...)` | secret storage load failed or is unsupported | pipe the recovery key: `printf '%s\n' "$MATRIX_RECOVERY_KEY" \| openclaw matrix verify backup restore --recovery-key-stdin` | +| `backup key mismatch (...)` | stored key does not match the active server backup | rerun `verify backup restore --recovery-key-stdin` with the active server backup key, or `verify backup reset --yes` for a fresh baseline | +| `backup signature chain is not trusted by this device` | device does not trust the cross-signing chain yet | `verify device --recovery-key-stdin`, then `verify self` from another verified client if trust is still incomplete | +| `backup exists but is not active on this device` | server backup present, local session inactive | verify the device first, then recheck with `openclaw matrix verify backup status` | +| `backup trust state could not be fully determined` | diagnostics were inconclusive | `openclaw matrix verify status --verbose` | -`Store a recovery key with 'openclaw matrix verify device --recovery-key-stdin', then run 'openclaw matrix verify backup restore'.` - -- Meaning: this device does not currently have the recovery key stored. -- What to do: set `MATRIX_RECOVERY_KEY`, run `printf '%s\n' "$MATRIX_RECOVERY_KEY" | openclaw matrix verify device --recovery-key-stdin`, then restore the backup. - -`Backup key mismatch on this device. Re-run 'openclaw matrix verify device --recovery-key-stdin' with the matching recovery key.` - -- Meaning: the stored key does not match the active Matrix backup. -- What to do: set `MATRIX_RECOVERY_KEY` to the correct key and run `printf '%s\n' "$MATRIX_RECOVERY_KEY" | openclaw matrix verify device --recovery-key-stdin`. - -If you accept losing unrecoverable old encrypted history, you can instead reset the -current backup baseline with `openclaw matrix verify backup reset --yes`. When the -stored backup secret is broken, that reset may also recreate secret storage so the -new backup key can load correctly after restart. - -`Backup trust chain is not verified on this device. Re-run 'openclaw matrix verify device --recovery-key-stdin'.` - -- Meaning: the backup exists, but this device does not trust the cross-signing chain strongly enough yet. -- What to do: set `MATRIX_RECOVERY_KEY` and run `printf '%s\n' "$MATRIX_RECOVERY_KEY" | openclaw matrix verify device --recovery-key-stdin`. +Other recovery errors: `Matrix recovery key is required` @@ -311,36 +279,24 @@ new backup key can load correctly after restart. `Invalid Matrix recovery key: ...` - Meaning: the provided key could not be parsed or did not match the expected format. -- What to do: retry with the exact recovery key from your Matrix client or recovery-key file. +- What to do: retry with the exact recovery key from your Matrix client or recovery-key export. `Matrix recovery key was applied, but this device still lacks full Matrix identity trust.` -- Meaning: OpenClaw could apply the recovery key, but Matrix still has not - established full cross-signing identity trust for this device. Check the - command output for `Recovery key accepted`, `Backup usable`, - `Cross-signing verified`, and `Device verified by owner`. -- What to do: run `openclaw matrix verify self`, accept the request in another - Matrix client, compare the SAS, and type `yes` only when it matches. The - command waits for full Matrix identity trust before reporting success. Use - `printf '%s\n' "$MATRIX_RECOVERY_KEY" | openclaw matrix verify bootstrap --recovery-key-stdin --force-reset-cross-signing` - only when you intentionally want to replace the current cross-signing identity. +- Meaning: the recovery key unlocked usable backup material, but Matrix has not established full cross-signing identity trust for this device. Check the command output for `Recovery key accepted`, `Backup usable`, `Cross-signing verified`, and `Device verified by owner`. +- What to do: run `openclaw matrix verify self`, accept the request in another Matrix client, compare the SAS, and type `yes` only when it matches. Use `printf '%s\n' "$MATRIX_RECOVERY_KEY" | openclaw matrix verify bootstrap --recovery-key-stdin --force-reset-cross-signing` only when you intentionally want to replace the current cross-signing identity. -`Matrix key backup is not active on this device after loading from secret storage.` - -- Meaning: secret storage did not produce an active backup session on this device. -- What to do: verify the device first, then recheck with `openclaw matrix verify backup status`. - -`Matrix crypto backend cannot load backup keys from secret storage. Verify this device with 'openclaw matrix verify device --recovery-key-stdin' first.` - -- Meaning: this device cannot restore from secret storage until device verification is complete. -- What to do: run `printf '%s\n' "$MATRIX_RECOVERY_KEY" | openclaw matrix verify device --recovery-key-stdin` first. +If you accept losing unrecoverable old encrypted history, you can instead reset the +current backup baseline with `openclaw matrix verify backup reset --yes`. When the +stored backup secret is broken, that reset also repairs secret storage so the +new backup key can load correctly after restart. ### Custom plugin install messages `Matrix is installed from a custom path that no longer exists: ...` - Meaning: your plugin install record points at a local path that is gone. -- What to do: reinstall with `openclaw plugins install @openclaw/matrix`, or if you are running from a repo checkout, `openclaw plugins install ./path/to/local/matrix-plugin`. +- What to do: reinstall with `openclaw plugins install @openclaw/matrix`, or if you are running from a repo checkout, `openclaw plugins install ./path/to/local/matrix-plugin`. `openclaw doctor --fix` can also remove the stale Matrix plugin references for you. ## If encrypted history still does not come back diff --git a/docs/channels/matrix-presentation.md b/docs/channels/matrix-presentation.md index ca1c36d6f2ce..e805ad2c0432 100644 --- a/docs/channels/matrix-presentation.md +++ b/docs/channels/matrix-presentation.md @@ -6,18 +6,16 @@ read_when: title: "Matrix presentation metadata" --- -OpenClaw can attach normalized `MessagePresentation` metadata to outbound Matrix `m.room.message` events under `com.openclaw.presentation`. +OpenClaw attaches normalized `MessagePresentation` metadata to outbound Matrix `m.room.message` events under the `com.openclaw.presentation` content key. -Stock Matrix clients continue to render the plain text `body`. OpenClaw-aware clients can read the structured metadata and render native UI such as buttons, selects, context rows, and dividers. +Stock Matrix clients keep rendering the plain text `body`. OpenClaw-aware clients can read the structured metadata and render native UI such as buttons, selects, context rows, and dividers. ## Event content -The metadata is stored in Matrix event content: - ```json { "msgtype": "m.text", - "body": "Select model\n\n- DeepSeek: /model deepseek/deepseek-chat", + "body": "Select model\n\nChoose model:\n- DeepSeek", "com.openclaw.presentation": { "version": 1, "type": "message.presentation", @@ -39,28 +37,37 @@ The metadata is stored in Matrix event content: } ``` -`version` is the Matrix presentation metadata schema version. `type` is a stable discriminator for OpenClaw-aware clients. Clients should ignore unknown `type` values, unknown versions they cannot safely interpret, and unknown block types. +- `version` is the metadata schema version; the current version is `1`. `type` is a stable discriminator, always `"message.presentation"`. The Matrix adapter only emits payloads with exactly this version and type; clients should likewise ignore unknown versions they cannot safely interpret, unknown `type` values, and unknown block types. +- `title` and `tone` (`info`, `success`, `warning`, `danger`, `neutral`) are optional hints. +- Buttons and select options can carry a typed `action` (`{ "type": "command", "command": "/..." }` or `{ "type": "callback", "value": "..." }`) alongside the legacy string `value`. Prefer `action` when both are present. ## Fallback behavior OpenClaw always renders a readable plain text fallback into `body`. The structured metadata is additive and must not be required for basic Matrix interoperability. -Unsupported clients should continue to show the fallback text. OpenClaw-aware clients may prefer the structured metadata for display while preserving the fallback text for copy, search, notifications, and accessibility. +Fallback rendering rules: + +- `title`, `text`, and `context` content renders as plain lines. +- Buttons with a `command` action render as ``label: `/command` `` so the command stays copyable. Buttons with a `callback` action or only a legacy `value` render label-only so opaque callback values stay private; disabled buttons are always label-only. URL and web-app buttons render as `label: URL`. +- Select blocks render the placeholder (or `Options:`) as a heading plus label-only option lines. +- If nothing renders, for example a divider-only presentation, the body falls back to `---`. + +Unsupported clients keep showing the fallback text. OpenClaw-aware clients may prefer the structured metadata for display while preserving the fallback for copy, search, notifications, and accessibility. ## Supported blocks -The Matrix outbound adapter advertises support for: +The Matrix outbound adapter advertises native support for: - `buttons` - `select` - `context` - `divider` -Clients should treat these blocks as best-effort presentation hints. Unknown fields and unknown block types should be ignored rather than causing the full message to fail rendering. +`text` blocks are always supported through the fallback body. Treat all blocks as best-effort presentation hints; ignore unknown fields and block types rather than failing the whole message. ## Interactions -This metadata does not add Matrix callback semantics. Button and select option values are fallback interaction payloads, usually slash commands or text commands. A Matrix client that wants to support interaction can send the selected value back to the room as a normal message. +This metadata does not add Matrix callback semantics. Button and select values are fallback interaction payloads, usually slash commands or text commands. A Matrix client that wants to support interaction resolves the control value (`action.command`, then `action.value`, then `value`) and sends it back to the room as a normal message. For example, a button with value `/model deepseek/deepseek-chat` can be handled by sending that value as an encrypted Matrix text message in the same room. @@ -72,6 +79,6 @@ Approval prompts use the dedicated `com.openclaw.approval` metadata because appr ## Media messages -When a reply contains multiple media URLs, OpenClaw sends one Matrix event per media URL. Presentation metadata is attached only to the first media event so clients have one stable structured payload and duplicate renderers are avoided. +When a reply contains multiple media URLs, OpenClaw sends one Matrix event per media URL. Caption text and presentation metadata attach only to the first event so clients get one stable structured payload without duplicate renderers. The same rule applies when long text is chunked across events: the metadata rides on the first event only. Keep presentation metadata compact. Large user-visible text should stay in `body` and use the normal Matrix text chunking path. diff --git a/docs/channels/matrix-push-rules.md b/docs/channels/matrix-push-rules.md index 34081375c87b..796be04b2172 100644 --- a/docs/channels/matrix-push-rules.md +++ b/docs/channels/matrix-push-rules.md @@ -6,7 +6,9 @@ read_when: title: "Matrix push rules for quiet previews" --- -When `channels.matrix.streaming` is `"quiet"`, OpenClaw edits a single preview event in place and marks the finalized edit with a custom content flag. Matrix clients notify on the final edit only if a per-user push rule matches that flag. This page is for operators who self-host Matrix and want to install that rule for each recipient account. +When `channels.matrix.streaming` is `"quiet"`, OpenClaw streams the reply by editing a single preview event in place. Previews are sent as non-notifying `m.notice` events, and the finalized edit is marked with `content["com.openclaw.finalized_preview"] = true`. Matrix clients notify on that final edit only if a per-user push rule matches the marker. This page is for operators who self-host Matrix and want to install that rule for each recipient account. + +`streaming: "progress"` finalizes its drafts through the same path, so the same rule also fires for progress-mode finalized edits. If you only want stock Matrix notification behavior, use `streaming: "partial"` or leave streaming off. See [Matrix channel setup](/channels/matrix#streaming-previews). @@ -16,7 +18,7 @@ If you only want stock Matrix notification behavior, use `streaming: "partial"` - bot user = the OpenClaw Matrix account that sends the reply - use the recipient user's access token for the API calls below - match `sender` in the push rule against the bot user's full MXID -- the recipient account must already have working pushers — quiet preview rules only work when normal Matrix push delivery is healthy +- the recipient account must already have working pushers; quiet preview rules only work when normal Matrix push delivery is healthy ## Steps @@ -64,7 +66,7 @@ If no pushers come back, fix normal Matrix push delivery for this account before - OpenClaw marks finalized text-only preview edits with `content["com.openclaw.finalized_preview"] = true`. Install a rule that matches that marker plus the bot MXID as sender: + Install a rule that matches the finalized-preview marker plus the bot MXID as sender: ```bash curl -sS -X PUT \ @@ -122,7 +124,7 @@ To remove the rule later, `DELETE` the same rule URL with the recipient's token. Push rules are keyed by `ruleId`: re-running `PUT` against the same ID updates a single rule. For multiple OpenClaw bots notifying the same recipient, create one rule per bot with a distinct sender match. -New user-defined `override` rules are inserted ahead of default suppress rules, so no extra ordering parameter is needed. The rule only affects text-only preview edits that can be finalized in place; media fallbacks and stale-preview fallbacks use normal Matrix delivery. +New user-defined `override` rules are inserted ahead of server-default suppress rules, so no extra ordering parameter is needed. The rule only affects text-only preview edits that can be finalized in place; media replies, stale-preview fallbacks, and final texts that would activate Matrix mentions are delivered as normal notifying messages instead. ## Homeserver notes diff --git a/docs/channels/matrix.md b/docs/channels/matrix.md index 8fd541864e1f..b504794f39ec 100644 --- a/docs/channels/matrix.md +++ b/docs/channels/matrix.md @@ -6,33 +6,24 @@ read_when: title: "Matrix" --- -Matrix is a downloadable channel plugin for OpenClaw. -It uses the official `matrix-js-sdk` and supports DMs, rooms, threads, media, reactions, polls, location, and E2EE. +Matrix is a downloadable channel plugin (`@openclaw/matrix`) built on the official `matrix-js-sdk`. It supports DMs, rooms, threads, media, reactions, polls, location, and E2EE. ## Install -Install Matrix from ClawHub before configuring the channel: - ```bash openclaw plugins install @openclaw/matrix ``` -Bare plugin specs try ClawHub first, then npm fallback. To force the registry source, use `openclaw plugins install clawhub:@openclaw/matrix` or `openclaw plugins install npm:@openclaw/matrix`. +Bare plugin specs try ClawHub first, then npm fallback. Force a source with `openclaw plugins install clawhub:@openclaw/matrix` or `npm:@openclaw/matrix`. From a local checkout: `openclaw plugins install ./path/to/local/matrix-plugin`. -From a local checkout: - -```bash -openclaw plugins install ./path/to/local/matrix-plugin -``` - -`plugins install` registers and enables the plugin, so no separate `openclaw plugins enable matrix` step is needed. The plugin still does nothing until you configure the channel below. See [Plugins](/tools/plugin) for general plugin behavior and install rules. +`plugins install` registers and enables the plugin; no separate `enable` step is needed. The channel still does nothing until configured below. See [Plugins](/tools/plugin) for general install rules. ## Setup 1. Create a Matrix account on your homeserver. -2. Configure `channels.matrix` with either `homeserver` + `accessToken`, or `homeserver` + `userId` + `password`. +2. Configure `channels.matrix` with `homeserver` + `accessToken`, or `homeserver` + `userId` + `password`. 3. Restart the gateway. -4. Start a DM with the bot, or invite it to a room (see [auto-join](#auto-join) - fresh invites only land when `autoJoin` allows them). +4. Start a DM with the bot, or invite it to a room. Fresh invites only land when [`autoJoin`](#auto-join) allows them. ### Interactive setup @@ -41,9 +32,7 @@ openclaw channels add openclaw configure --section channels ``` -The wizard asks for: homeserver URL, auth method (access token or password), user ID (password auth only), optional device name, whether to enable E2EE, and whether to configure room access and auto-join. - -If matching `MATRIX_*` env vars already exist and the selected account has no saved auth, the wizard offers an env-var shortcut. To resolve room names before saving an allowlist, run `openclaw channels resolve --channel matrix "Project Room"`. When E2EE is enabled, the wizard writes the config and runs the same bootstrap as [`openclaw matrix encryption setup`](#encryption-and-verification). +The wizard asks for homeserver URL, auth method (token or password), user ID (password auth only), optional device name, whether to enable E2EE, and room access/auto-join. If matching `MATRIX_*` env vars already exist and the account has no saved auth, the wizard offers an env-var shortcut. Resolve room names before saving an allowlist with `openclaw channels resolve --channel matrix "Project Room"`. Enabling E2EE in the wizard runs the same bootstrap as [`openclaw matrix encryption setup`](#encryption-and-verification). ### Minimal config @@ -62,7 +51,7 @@ Token-based: } ``` -Password-based (the token is cached after first login): +Password-based (token is cached after first login): ```json5 { @@ -80,14 +69,12 @@ Password-based (the token is cached after first login): ### Auto-join -`channels.matrix.autoJoin` defaults to `off`. With the default, the bot will not appear in new rooms or DMs from fresh invites until you join manually. - -OpenClaw cannot tell at invite time whether an invited room is a DM or a group, so all invites - including DM-style invites - go through `autoJoin` first. `dm.policy` only applies later, after the bot has joined and the room has been classified. +`channels.matrix.autoJoin` defaults to `"off"`: the bot will not appear in new rooms or DMs from fresh invites until you join manually. OpenClaw cannot tell at invite time whether an invite is a DM or a group, so every invite goes through `autoJoin` first; `dm.policy` only applies later, after the bot has joined and the room is classified. -Set `autoJoin: "allowlist"` plus `autoJoinAllowlist` to restrict which invites the bot accepts, or `autoJoin: "always"` to accept every invite. +Set `autoJoin: "allowlist"` plus `autoJoinAllowlist` to restrict accepted invites, or `autoJoin: "always"` to accept every invite. -`autoJoinAllowlist` only accepts stable targets: `!roomId:server`, `#alias:server`, or `*`. Plain room names are rejected; alias entries are resolved against the homeserver, not against state claimed by the invited room. +`autoJoinAllowlist` accepts only `!roomId:server`, `#alias:server`, or `*`. Plain room names are rejected; aliases resolve against the homeserver, not against state the invited room claims. ```json5 @@ -104,46 +91,38 @@ Set `autoJoin: "allowlist"` plus `autoJoinAllowlist` to restrict which invites t } ``` -To accept every invite, use `autoJoin: "always"`. - ### Allowlist target formats -DM and room allowlists are best populated with stable IDs: - -- DMs (`dm.allowFrom`, `groupAllowFrom`, `groups..users`): use `@user:server`. Display names are ignored by default because they are mutable; set `dangerouslyAllowNameMatching: true` only when you explicitly need compatibility with display-name entries. -- Room allowlist keys (`groups`, legacy `rooms`): use `!room:server` or `#alias:server`. Plain room names are ignored by default; set `dangerouslyAllowNameMatching: true` only when you explicitly need compatibility with joined-room name lookup. -- Invite allowlists (`autoJoinAllowlist`): use `!room:server`, `#alias:server`, or `*`. Plain room names are rejected. +- DMs (`dm.allowFrom`, `groupAllowFrom`, `groups..users`): use `@user:server`. Display names are ignored by default (mutable); set `dangerouslyAllowNameMatching: true` only for explicit display-name compatibility. +- Room allowlist keys (`groups`, legacy alias `rooms`): use `!room:server` or `#alias:server`. Plain names are ignored unless `dangerouslyAllowNameMatching: true`. +- Invite allowlists (`autoJoinAllowlist`): use `!room:server`, `#alias:server`, or `*`. Plain names are always rejected. ### Account ID normalization -The wizard converts a friendly name into a normalized account ID. For example, `Ops Bot` becomes `ops-bot`. Punctuation is escaped in scoped env-var names so that two accounts cannot collide: `-` → `_X2D_`, so `ops-prod` maps to `MATRIX_OPS_X2D_PROD_*`. +The wizard converts a friendly name into a normalized account ID (`Ops Bot` -> `ops-bot`). Punctuation is hex-escaped in scoped env-var names so accounts cannot collide: `-` (0x2D) becomes `_X2D_`, so `ops-prod` maps to env prefix `MATRIX_OPS_X2D_PROD_`. ### Cached credentials -Matrix stores cached credentials under `~/.openclaw/credentials/matrix/`: - -- default account: `credentials.json` -- named accounts: `credentials-.json` - -When cached credentials exist there, OpenClaw treats Matrix as configured even if the access token is not in the config file - that covers setup, `openclaw doctor`, and channel-status probes. +Matrix caches credentials under `~/.openclaw/credentials/matrix/`: `credentials.json` for the default account, `credentials-.json` for named accounts. When cached credentials exist, OpenClaw treats Matrix as configured even without an `accessToken` in the config file - this covers setup, `openclaw doctor`, and channel-status probes. ### Environment variables -Used when the equivalent config key is not set. The default account uses unprefixed names; named accounts use the account ID inserted before the suffix. +Config-key-backed env vars, used when the equivalent config key is unset. The default account uses unprefixed names; named accounts insert the account token before the suffix (see [normalization](#account-id-normalization)). -| Default account | Named account (`` is the normalized account ID) | -| --------------------- | --------------------------------------------------- | -| `MATRIX_HOMESERVER` | `MATRIX__HOMESERVER` | -| `MATRIX_ACCESS_TOKEN` | `MATRIX__ACCESS_TOKEN` | -| `MATRIX_USER_ID` | `MATRIX__USER_ID` | -| `MATRIX_PASSWORD` | `MATRIX__PASSWORD` | -| `MATRIX_DEVICE_ID` | `MATRIX__DEVICE_ID` | -| `MATRIX_DEVICE_NAME` | `MATRIX__DEVICE_NAME` | -| `MATRIX_RECOVERY_KEY` | `MATRIX__RECOVERY_KEY` | +| Default account | Named account (`` = account token) | +| --------------------- | -------------------------------------- | +| `MATRIX_HOMESERVER` | `MATRIX__HOMESERVER` | +| `MATRIX_ACCESS_TOKEN` | `MATRIX__ACCESS_TOKEN` | +| `MATRIX_USER_ID` | `MATRIX__USER_ID` | +| `MATRIX_PASSWORD` | `MATRIX__PASSWORD` | +| `MATRIX_DEVICE_ID` | `MATRIX__DEVICE_ID` | +| `MATRIX_DEVICE_NAME` | `MATRIX__DEVICE_NAME` | -For account `ops`, the names become `MATRIX_OPS_HOMESERVER`, `MATRIX_OPS_ACCESS_TOKEN`, and so on. The recovery-key env vars are read by recovery-aware CLI flows (`verify backup restore`, `verify device`, `verify bootstrap`) when you pipe the key in via `--recovery-key-stdin`. +For account `ops`, names become `MATRIX_OPS_HOMESERVER`, `MATRIX_OPS_ACCESS_TOKEN`, and so on. `MATRIX_HOMESERVER` (and any `*_HOMESERVER` scoped variant) cannot be set from a workspace `.env`; see [Workspace `.env` files](/gateway/security). -`MATRIX_HOMESERVER` cannot be set from a workspace `.env`; see [Workspace `.env` files](/gateway/security). + +The recovery key is not a config-backed env var: OpenClaw never reads it from the environment itself. CLI guidance text suggests piping it through a shell variable named `MATRIX_RECOVERY_KEY` for the default account, or `MATRIX_RECOVERY_KEY_` (plain uppercased account ID, no hex-escaping) for a named account - see [Verify this device with a recovery key](#verify-this-device-with-a-recovery-key). + ## Configuration example @@ -182,7 +161,7 @@ A practical baseline with DM pairing, room allowlist, and E2EE: ## Streaming previews -Matrix reply streaming is opt-in. `streaming` controls how OpenClaw delivers the in-flight assistant reply; `blockStreaming` controls whether each completed block is preserved as its own Matrix message. +Matrix reply streaming is opt-in. `streaming` controls how OpenClaw delivers the in-flight assistant reply; `blockStreaming` controls whether each completed block is kept as its own Matrix message. ```json5 { @@ -194,8 +173,7 @@ Matrix reply streaming is opt-in. `streaming` controls how OpenClaw delivers the } ``` -To keep live answer previews but hide interim tool/progress lines, use object -form: +To keep live answer previews but hide interim tool/progress lines, use object form: ```json5 { @@ -212,7 +190,7 @@ form: } ``` -The full object form accepts `{ mode, preview, progress }`: +Full object form accepts `{ mode, preview, progress }`: ```json5 { @@ -233,20 +211,20 @@ The full object form accepts `{ mode, preview, progress }`: } ``` -- `progress.label`: a custom label, `"auto"` or unset to choose from configured or built-in labels, or `false` to hide the label line. -- `progress.labels`: candidate labels used only when `label` is `"auto"` or unset. Leave unset for built-in defaults. -- `progress.maxLines`: maximum rolling progress lines kept in the draft. After this limit, older lines are trimmed. -- `progress.maxLineChars`: maximum characters per compact progress line before truncation. +- `progress.label`: custom label, `"auto"`/unset to pick a configured or built-in label, or `false` to hide it. +- `progress.labels`: candidates used only when `label` is `"auto"` or unset. +- `progress.maxLines`: max rolling progress lines kept in the draft; older lines are trimmed past this. +- `progress.maxLineChars`: max characters per compact progress line before truncation. - `progress.toolProgress`: when `true` (default), live tool/progress activity appears in the draft. -| `streaming` | Behavior | -| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `"off"` (default) | Wait for the full reply, send once. `true` ↔ `"partial"`, `false` ↔ `"off"`. | -| `"partial"` | Edit one normal text message in place as the model writes the current block. Stock Matrix clients may notify on the first preview, not the final edit. | -| `"quiet"` | Same as `"partial"` but the message is a non-notifying notice. Recipients only get a notification once a per-user push rule matches the finalized edit (see below). | -| `"progress"` | Sends individual compact progress lines using a progress draft. | +| `streaming` | Behavior | +| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `"off"` (default) | Wait for the full reply, send once. `true` <-> `"partial"`, `false` <-> `"off"`. | +| `"partial"` | Edit one normal text message in place as the model writes the current block. Stock clients may notify on the first preview, not the final edit. | +| `"quiet"` | Same as `"partial"` but the message is a non-notifying notice. Recipients are notified once a per-user push rule matches the finalized edit (see below). | +| `"progress"` | Sends individual compact progress lines using a progress draft. | -`blockStreaming` is independent of `streaming`: +`blockStreaming` (default `false`) is independent of `streaming`: | `streaming` | `blockStreaming: true` | `blockStreaming: false` (default) | | ----------------------- | ------------------------------------------------------------------- | ---------------------------------------------------- | @@ -256,39 +234,35 @@ The full object form accepts `{ mode, preview, progress }`: Notes: - If a preview grows past Matrix's per-event size limit, OpenClaw stops preview streaming and falls back to final-only delivery. -- Media replies always send attachments normally. If a stale preview can no longer be reused safely, OpenClaw redacts it before sending the final media reply. -- Tool-progress preview updates are enabled by default when Matrix preview streaming is active. Set `streaming.preview.toolProgress: false` to keep preview edits for answer text but leave tool progress on the normal delivery path. -- Preview edits cost extra Matrix API calls. Leave `streaming: "off"` if you want the most conservative rate-limit profile. +- Media replies always send attachments normally; if a stale preview cannot be reused safely, OpenClaw redacts it before sending the final media reply. +- Tool-progress preview updates are on by default when preview streaming is active. Set `streaming.preview.toolProgress: false` to keep preview edits for answer text but leave tool progress on the normal delivery path. +- Preview edits cost extra Matrix API calls. Leave `streaming: "off"` for the most conservative rate-limit profile. ## Voice messages -Inbound Matrix voice notes are transcribed before the room mention gate. This lets a voice note that says the bot name trigger the agent in a `requireMention: true` room, and it gives the agent the transcript instead of only an audio attachment placeholder. +Inbound Matrix voice notes are transcribed before the room mention gate, so a voice note saying the bot name can trigger the agent in a `requireMention: true` room, and the agent gets the transcript instead of only an audio attachment placeholder. -Matrix uses the shared audio media provider configured under `tools.media.audio`, such as OpenAI `gpt-4o-mini-transcribe`. See [Media tools overview](/tools/media-overview) for provider setup and limits. - -Behavior details: +Matrix uses the shared audio media provider under `tools.media.audio`, such as OpenAI `gpt-4o-mini-transcribe`. See [Media tools overview](/tools/media-overview) for provider setup and limits. - `m.audio` events and `m.file` events with an `audio/*` MIME type are eligible. - In encrypted rooms, OpenClaw decrypts the attachment through the existing Matrix media path before transcription. -- The transcript is marked as machine-generated and untrusted in the agent prompt. -- The attachment is marked as already transcribed so downstream media tools do not transcribe the same voice note again. +- The transcript is marked machine-generated and untrusted in the agent prompt. +- The attachment is marked as already transcribed so downstream media tools do not transcribe it again. - Set `tools.media.audio.enabled: false` to disable audio transcription globally. ## Approval metadata -Matrix native approval prompts are normal `m.room.message` events with OpenClaw-specific custom event content under `com.openclaw.approval`. Matrix permits custom event-content keys, so stock clients still render the text body while OpenClaw-aware clients can read the structured approval id, kind, state, available decisions, and exec/plugin details. +Matrix native approval prompts are normal `m.room.message` events with OpenClaw-specific content under the `com.openclaw.approval` key. Stock clients still render the text body; OpenClaw-aware clients can read the structured approval id, kind, state, decisions, and exec/plugin details. -When an approval prompt is too long for one Matrix event, OpenClaw chunks the visible text and attaches `com.openclaw.approval` to the first chunk only. Reactions for allow/deny decisions are bound to that first event, so long prompts keep the same approval target as single-event prompts. +When a prompt is too long for one Matrix event, OpenClaw chunks the visible text and attaches `com.openclaw.approval` to the first chunk only. Allow/deny reactions bind to that first event, so long prompts keep the same approval target as single-event prompts. ### Self-hosted push rules for quiet finalized previews -`streaming: "quiet"` only notifies recipients once a block or turn is finalized - a per-user push rule has to match the finalized preview marker. See [Matrix push rules for quiet previews](/channels/matrix-push-rules) for the full recipe (recipient token, pusher check, rule install, per-homeserver notes). +`streaming: "quiet"` only notifies recipients once a block or turn is finalized - a per-user push rule must match the finalized preview marker. See [Matrix push rules for quiet previews](/channels/matrix-push-rules) for the full recipe. ## Bot-to-bot rooms -By default, Matrix messages from other configured OpenClaw Matrix accounts are ignored. - -Use `allowBots` when you intentionally want inter-agent Matrix traffic: +By default, Matrix messages from other configured OpenClaw Matrix accounts are ignored. Use `allowBots` to intentionally allow inter-agent traffic: ```json5 { @@ -306,19 +280,19 @@ Use `allowBots` when you intentionally want inter-agent Matrix traffic: ``` - `allowBots: true` accepts messages from other configured Matrix bot accounts in allowed rooms and DMs. -- `allowBots: "mentions"` accepts those messages only when they visibly mention this bot in rooms. DMs are still allowed. +- `allowBots: "mentions"` accepts those messages only when they visibly mention this bot in rooms; DMs are still allowed regardless. - `groups..allowBots` overrides the account-level setting for one room. -- Accepted configured-bot messages use shared [bot loop protection](/channels/bot-loop-protection). Configure `channels.defaults.botLoopProtection`, then override with `channels.matrix.botLoopProtection` or `channels.matrix.groups..botLoopProtection` when one room needs a different budget. +- Accepted configured-bot messages use shared [bot loop protection](/channels/bot-loop-protection). Configure `channels.defaults.botLoopProtection`, then override per-account with `channels.matrix.botLoopProtection` or per-room with `channels.matrix.groups..botLoopProtection`. - OpenClaw still ignores messages from the same Matrix user ID to avoid self-reply loops. -- Matrix does not expose a native bot flag here; OpenClaw treats "bot-authored" as "sent by another configured Matrix account on this OpenClaw gateway". +- Matrix has no native bot flag; OpenClaw treats "bot-authored" as "sent by another configured Matrix account on this OpenClaw gateway". Use strict room allowlists and mention requirements when enabling bot-to-bot traffic in shared rooms. ## Encryption and verification -In encrypted (E2EE) rooms, outbound image events use `thumbnail_file` so image previews are encrypted alongside the full attachment. Unencrypted rooms still use plain `thumbnail_url`. No configuration is needed - the plugin detects E2EE state automatically. +In encrypted (E2EE) rooms, outbound image events use `thumbnail_file` so image previews are encrypted alongside the full attachment; unencrypted rooms use plain `thumbnail_url`. No configuration is needed - the plugin detects E2EE state automatically. -All `openclaw matrix` commands accept `--verbose` (full diagnostics), `--json` (machine-readable output), and `--account ` (multi-account setups). Output is concise by default with quiet internal SDK logging. The examples below show the canonical form; add the flags as needed. +All `openclaw matrix` commands accept `--verbose` (full diagnostics), `--json` (machine-readable output), and `--account ` (multi-account setups). Output is concise by default. ### Enable encryption @@ -328,8 +302,8 @@ openclaw matrix encryption setup Bootstraps secret storage and cross-signing, creates a room-key backup if needed, then prints status and next steps. Useful flags: -- `--recovery-key ` apply a recovery key before bootstrapping (prefer the stdin form documented below) -- `--force-reset-cross-signing` discard the current cross-signing identity and create a new one (use only intentionally) +- `--recovery-key ` apply a recovery key before bootstrapping (prefer the stdin form below) +- `--force-reset-cross-signing` discard the current cross-signing identity and create a new one (intentional use only) For a new account, enable E2EE at creation time: @@ -340,9 +314,7 @@ openclaw matrix account add \ --enable-e2ee ``` -`--encryption` is an alias for `--enable-e2ee`. - -Manual config equivalent: +`--encryption` is an alias for `--enable-e2ee`. Manual config equivalent: ```json5 { @@ -371,13 +343,13 @@ openclaw matrix verify status --include-recovery-key --json - `Cross-signing verified`: the SDK reports verification via cross-signing - `Signed by owner`: signed by your own self-signing key (diagnostic only) -`Verified by owner` becomes `yes` only when `Cross-signing verified` is `yes`. Local trust or an owner signature alone is not enough. +`Verified by owner` is `yes` only when `Cross-signing verified` is `yes`; local trust or an owner signature alone is not enough. `--allow-degraded-local-state` returns best-effort diagnostics without preparing the Matrix account first; useful for offline or partially-configured probes. ### Verify this device with a recovery key -The recovery key is sensitive - pipe it via stdin instead of passing it on the command line. Set `MATRIX_RECOVERY_KEY` (or `MATRIX__RECOVERY_KEY` for a named account): +Pipe the recovery key via stdin instead of passing it on the command line: ```bash printf '%s\n' "$MATRIX_RECOVERY_KEY" | openclaw matrix verify device --recovery-key-stdin @@ -395,9 +367,9 @@ It exits non-zero when full identity trust is incomplete, even if the recovery k openclaw matrix verify self ``` -`verify self` waits for `Cross-signing verified: yes` before it exits successfully. Use `--timeout-ms ` to tune the wait. +`verify self` waits for `Cross-signing verified: yes` before exiting successfully. Use `--timeout-ms ` to tune the wait. -The literal-key form `openclaw matrix verify device ""` is also accepted, but the key ends up in your shell history. +The literal-key form `openclaw matrix verify device ""` also works, but the key ends up in shell history. ### Bootstrap or repair cross-signing @@ -405,7 +377,7 @@ The literal-key form `openclaw matrix verify device ""` is also ac openclaw matrix verify bootstrap ``` -`verify bootstrap` is the repair and setup command for encrypted accounts. In order, it: +The repair/setup command for encrypted accounts. In order, it: - bootstraps secret storage, reusing an existing recovery key when possible - bootstraps cross-signing and uploads missing public keys @@ -416,8 +388,8 @@ If the homeserver requires UIA to upload cross-signing keys, OpenClaw tries no-a Useful flags: -- `--recovery-key-stdin` (pair with `printf '%s\n' "$MATRIX_RECOVERY_KEY" | …`) or `--recovery-key ` -- `--force-reset-cross-signing` to discard the current cross-signing identity (intentional only; requires the active recovery key to be stored or supplied with `--recovery-key-stdin`) +- `--recovery-key-stdin` (pair with `printf '%s\n' "$MATRIX_RECOVERY_KEY" | ...`) or `--recovery-key ` +- `--force-reset-cross-signing` to discard the current cross-signing identity (intentional only; requires the active recovery key stored or supplied with `--recovery-key-stdin`) ### Room-key backup @@ -426,7 +398,7 @@ openclaw matrix verify backup status printf '%s\n' "$MATRIX_RECOVERY_KEY" | openclaw matrix verify backup restore --recovery-key-stdin ``` -`backup status` shows whether a server-side backup exists and whether this device can decrypt it. `backup restore` imports backed-up room keys into the local crypto store; if the recovery key is already on disk you can omit `--recovery-key-stdin`. +`backup status` shows whether a server-side backup exists and whether this device can decrypt it. `backup restore` imports backed-up room keys into the local crypto store; omit `--recovery-key-stdin` if the recovery key is already on disk. To replace a broken backup with a fresh baseline (accepts losing unrecoverable old history; can also recreate secret storage if the current backup secret is unloadable): @@ -434,7 +406,7 @@ To replace a broken backup with a fresh baseline (accepts losing unrecoverable o openclaw matrix verify backup reset --yes ``` -Add `--rotate-recovery-key` only when you intentionally want the previous recovery key to stop unlocking the fresh backup baseline. +Add `--rotate-recovery-key` only when the previous recovery key should intentionally stop unlocking the fresh backup baseline. ### Listing, requesting, and responding to verifications @@ -449,7 +421,7 @@ openclaw matrix verify request --own-user openclaw matrix verify request --user-id @ops:example.org --device-id ABCDEF ``` -Sends a verification request from this OpenClaw account. `--own-user` requests self-verification (you accept the prompt in another Matrix client of the same user); `--user-id`/`--device-id`/`--room-id` target someone else. `--own-user` cannot be combined with the other targeting flags. +Sends a verification request from this account. `--own-user` requests self-verification (accept the prompt in another Matrix client of the same user); `--user-id`/`--device-id`/`--room-id` target someone else. `--own-user` cannot combine with the other targeting flags. For lower-level lifecycle handling - typically while shadowing inbound requests from another client - these commands act on a specific request `` (printed by `verify list` and `verify request`): @@ -466,13 +438,13 @@ For lower-level lifecycle handling - typically while shadowing inbound requests ### Multi-account notes -Without `--account `, Matrix CLI commands use the implicit default account. If you have multiple named accounts and have not set `channels.matrix.defaultAccount`, they will refuse to guess and ask you to choose. When E2EE is disabled or unavailable for a named account, errors point at that account's config key, for example `channels.matrix.accounts.assistant.encryption`. +Without `--account `, Matrix CLI commands use the implicit default account. With multiple named accounts and no `channels.matrix.defaultAccount`, commands refuse to guess and ask you to choose. When E2EE is disabled or unavailable for a named account, errors point at that account's config key, for example `channels.matrix.accounts.assistant.encryption`. With `encryption: true`, `startupVerification` defaults to `"if-unverified"`. On startup an unverified device requests self-verification in another Matrix client, skipping duplicates and applying a cooldown (24 hours by default). Tune with `startupVerificationCooldownHours` or disable with `startupVerification: "off"`. - Startup also runs a conservative crypto bootstrap pass that reuses the current secret storage and cross-signing identity. If bootstrap state is broken, OpenClaw attempts a guarded repair even without `channels.matrix.password`; if the homeserver requires password UIA, startup logs a warning and stays non-fatal. Already-owner-signed devices are preserved. + Startup also runs a conservative crypto bootstrap pass reusing the current secret storage and cross-signing identity. If bootstrap state is broken, OpenClaw attempts a guarded repair even without `channels.matrix.password`; if the homeserver requires password UIA, startup logs a warning and stays non-fatal. Already-owner-signed devices are preserved. See [Matrix migration](/channels/matrix-migration) for the full upgrade flow. @@ -534,27 +506,25 @@ openclaw matrix devices prune-stale ## Profile management -Update the Matrix self-profile for the selected account: - ```bash openclaw matrix profile set --name "OpenClaw Assistant" openclaw matrix profile set --avatar-url https://cdn.example.org/avatar.png ``` -You can pass both options in one call. Matrix accepts `mxc://` avatar URLs directly; when you pass `http://` or `https://`, OpenClaw uploads the file first and stores the resolved `mxc://` URL into `channels.matrix.avatarUrl` (or the per-account override). +Pass both options in one call. Matrix accepts `mxc://` avatar URLs directly; passing `http://`/`https://` uploads the file first and stores the resolved `mxc://` URL into `channels.matrix.avatarUrl` (or the per-account override). ## Threads -Matrix supports native Matrix threads for both automatic replies and message-tool sends. Two independent knobs control behavior: +Matrix supports native threads for both automatic replies and message-tool sends. Two independent knobs control behavior: ### Session routing (`sessionScope`) `dm.sessionScope` decides how Matrix DM rooms map to OpenClaw sessions: - `"per-user"` (default): all DM rooms with the same routed peer share one session. -- `"per-room"`: each Matrix DM room gets its own session key, even when the peer is the same. +- `"per-room"`: each Matrix DM room gets its own session key, even for the same peer. -Explicit conversation bindings always win over `sessionScope`, so bound rooms and threads keep their chosen target session. +Explicit conversation bindings always win over `sessionScope`; bound rooms and threads keep their chosen target session. ### Reply threading (`threadReplies`) @@ -562,7 +532,7 @@ Explicit conversation bindings always win over `sessionScope`, so bound rooms an - `"off"`: replies are top-level. Inbound threaded messages stay on the parent session. - `"inbound"`: reply inside a thread only when the inbound message was already in that thread. -- `"always"`: reply inside a thread rooted at the triggering message; that conversation is routed through a matching thread-scoped session from the first trigger onward. +- `"always"`: reply inside a thread rooted at the triggering message; that conversation routes through a matching thread-scoped session from the first trigger onward. `dm.threadReplies` overrides this for DMs only - for example, keep room threads isolated while keeping DMs flat. @@ -570,44 +540,39 @@ Explicit conversation bindings always win over `sessionScope`, so bound rooms an - Inbound threaded messages include the thread root message as extra agent context. - Message-tool sends auto-inherit the current Matrix thread when targeting the same room (or the same DM user target), unless an explicit `threadId` is provided. -- DM user-target reuse only kicks in when the current session metadata proves the same DM peer on the same Matrix account; otherwise OpenClaw falls back to normal user-scoped routing. +- DM user-target reuse only kicks in when current session metadata proves the same DM peer on the same Matrix account; otherwise OpenClaw falls back to normal user-scoped routing. - `/focus`, `/unfocus`, `/agents`, `/session idle`, `/session max-age`, and thread-bound `/acp spawn` all work in Matrix rooms and DMs. - Top-level `/focus` creates a new Matrix thread and binds it to the target session when `threadBindings.spawnSessions` is enabled. - Running `/focus` or `/acp spawn --thread here` inside an existing Matrix thread binds that thread in place. -When OpenClaw detects a Matrix DM room colliding with another DM room on the same shared session, it posts a one-time `m.notice` in that room pointing to the `/focus` escape hatch and suggesting a `dm.sessionScope` change. The notice only appears when thread bindings are enabled. +When OpenClaw detects a Matrix DM room colliding with another DM room on the same shared session, it posts a one-time `m.notice` pointing to the `/focus` escape hatch and suggesting a `dm.sessionScope` change. The notice only appears when thread bindings are enabled. ## ACP conversation bindings -Matrix rooms, DMs, and existing Matrix threads can be turned into durable ACP workspaces without changing the chat surface. +Matrix rooms, DMs, and existing Matrix threads can become durable ACP workspaces without changing the chat surface. Fast operator flow: -- Run `/acp spawn codex --bind here` inside the Matrix DM, room, or existing thread you want to keep using. -- In a top-level Matrix DM or room, the current DM/room stays the chat surface and future messages route to the spawned ACP session. -- Inside an existing Matrix thread, `--bind here` binds that current thread in place. +- Run `/acp spawn codex --bind here` inside the Matrix DM, room, or existing thread to keep using. +- In a top-level DM or room, the current DM/room stays the chat surface and future messages route to the spawned ACP session. +- Inside an existing thread, `--bind here` binds that current thread in place. - `/new` and `/reset` reset the same bound ACP session in place. - `/acp close` closes the ACP session and removes the binding. -Notes: - -- `--bind here` does not create a child Matrix thread. -- `threadBindings.spawnSessions` gates `/acp spawn --thread auto|here`, where OpenClaw needs to create or bind a child Matrix thread. +`--bind here` does not create a child Matrix thread. `threadBindings.spawnSessions` gates `/acp spawn --thread auto|here`, where OpenClaw needs to create or bind a child thread. ### Thread binding config -Matrix inherits global defaults from `session.threadBindings`, and also supports per-channel overrides: +Matrix inherits global defaults from `session.threadBindings` and supports per-channel overrides: - `threadBindings.enabled` - `threadBindings.idleHours` - `threadBindings.maxAgeHours` -- `threadBindings.spawnSessions` +- `threadBindings.spawnSessions`: gates both subagent and ACP thread spawns. +- `threadBindings.spawnSubagentSessions` / `threadBindings.spawnAcpSessions`: narrower overrides for subagent-only or ACP-only spawns. - `threadBindings.defaultSpawnContext` -Matrix thread-bound session spawns default on: - -- Set `threadBindings.spawnSessions: false` to block top-level `/focus` and `/acp spawn --thread auto|here` from creating/binding Matrix threads. -- Set `threadBindings.defaultSpawnContext: "isolated"` when native subagent thread spawns should not fork the parent transcript. +Matrix thread-bound session spawns default on. Set `threadBindings.spawnSessions: false` to block top-level `/focus` and `/acp spawn --thread auto|here` from creating/binding Matrix threads. Set `threadBindings.defaultSpawnContext: "isolated"` when native subagent thread spawns should not fork the parent transcript. ## Reactions @@ -622,19 +587,19 @@ Outbound reaction tooling is gated by `channels.matrix.actions.reactions`: **Resolution order** (first defined value wins): -| Setting | Order | -| ----------------------- | -------------------------------------------------------------------------------- | -| `ackReaction` | per-account → channel → `messages.ackReaction` → agent identity emoji fallback | -| `ackReactionScope` | per-account → channel → `messages.ackReactionScope` → default `"group-mentions"` | -| `reactionNotifications` | per-account → channel → default `"own"` | +| Setting | Order | +| ----------------------- | ----------------------------------------------------------------------------------- | +| `ackReaction` | per-account -> channel -> `messages.ackReaction` -> agent identity emoji fallback | +| `ackReactionScope` | per-account -> channel -> `messages.ackReactionScope` -> default `"group-mentions"` | +| `reactionNotifications` | per-account -> channel -> default `"own"` | -`reactionNotifications: "own"` forwards added `m.reaction` events when they target bot-authored Matrix messages; `"off"` disables reaction system events. Reaction removals are not synthesized into system events because Matrix surfaces those as redactions, not as standalone `m.reaction` removals. +`reactionNotifications: "own"` forwards added `m.reaction` events when they target bot-authored Matrix messages; `"off"` disables reaction system events. Reaction removals are not synthesized into system events - Matrix surfaces those as redactions, not as standalone `m.reaction` removals. ## History context -- `channels.matrix.historyLimit` controls how many recent room messages are included as `InboundHistory` when a Matrix room message triggers the agent. Falls back to `messages.groupChat.historyLimit`; if both are unset, the effective default is `0`. Set `0` to disable. -- Matrix room history is room-only. DMs keep using normal session history. -- Matrix room history is pending-only: OpenClaw buffers room messages that did not trigger a reply yet, then snapshots that window when a mention or other trigger arrives. +- `channels.matrix.historyLimit` controls how many recent room messages are included as `InboundHistory` when a room message triggers the agent. Falls back to `messages.groupChat.historyLimit`; effective default `0` if both are unset (disabled). +- Matrix room history is room-only; DMs keep using normal session history. +- Room history is pending-only: OpenClaw buffers room messages that did not trigger a reply yet, then snapshots that window when a mention or other trigger arrives. - The current trigger message is not included in `InboundHistory`; it stays in the main inbound body for that turn. - Retries of the same Matrix event reuse the original history snapshot instead of drifting forward to newer room messages. @@ -646,8 +611,7 @@ Matrix supports the shared `contextVisibility` control for supplemental room con - `contextVisibility: "allowlist"` filters supplemental context to senders allowed by the active room/user allowlist checks. - `contextVisibility: "allowlist_quote"` behaves like `allowlist`, but still keeps one explicit quoted reply. -This setting affects supplemental context visibility, not whether the inbound message itself can trigger a reply. -Trigger authorization still comes from `groupPolicy`, `groups`, `groupAllowFrom`, and DM policy settings. +This affects supplemental context visibility only, not whether the inbound message itself can trigger a reply. Trigger authorization still comes from `groupPolicy`, `groups`, `groupAllowFrom`, and DM policy settings. ## DM and room policy @@ -693,13 +657,13 @@ openclaw pairing list matrix openclaw pairing approve matrix ``` -If an unapproved Matrix user keeps messaging you before approval, OpenClaw reuses the same pending pairing code and may send a reminder reply after a short cooldown instead of minting a new code. +If an unapproved Matrix user keeps messaging before approval, OpenClaw reuses the same pending pairing code and may send a reminder reply after a short cooldown instead of minting a new code. See [Pairing](/channels/pairing) for the shared DM pairing flow and storage layout. ## Direct room repair -If direct-message state drifts out of sync, OpenClaw can end up with stale `m.direct` mappings that point at old solo rooms instead of the live DM. Inspect the current mapping for a peer: +If direct-message state drifts, OpenClaw can end up with stale `m.direct` mappings pointing at old solo rooms instead of the live DM. Inspect the current mapping for a peer: ```bash openclaw matrix direct inspect --user-id @alice:example.org @@ -713,7 +677,7 @@ openclaw matrix direct repair --user-id @alice:example.org Both commands accept `--account ` for multi-account setups. The repair flow: -- prefers a strict 1:1 DM that is already mapped in `m.direct` +- prefers a strict 1:1 DM already mapped in `m.direct` - falls back to any currently joined strict 1:1 DM with that user - creates a fresh direct room and rewrites `m.direct` if no healthy DM exists @@ -723,9 +687,9 @@ It does not delete old rooms automatically. It picks the healthy DM and updates Matrix can act as a native approval client. Configure under `channels.matrix.execApprovals` (or `channels.matrix.accounts..execApprovals` for a per-account override): -- `enabled`: deliver approvals through Matrix-native prompts. When unset or `"auto"`, Matrix auto-enables once at least one approver can be resolved. Set `false` to disable explicitly. -- `approvers`: Matrix user IDs (`@owner:example.org`) allowed to approve exec requests. Optional - falls back to `channels.matrix.dm.allowFrom`. -- `target`: where prompts go. `"dm"` (default) sends to approver DMs; `"channel"` sends to the originating Matrix room or DM; `"both"` sends to both. +- `enabled`: deliver approvals through Matrix-native prompts. Unset or `"auto"` auto-enables once at least one approver can be resolved; set `false` to disable explicitly. +- `approvers`: Matrix user IDs (`@owner:example.org`) allowed to approve exec requests. Falls back to `channels.matrix.dm.allowFrom`. +- `target`: where prompts go. `"dm"` (default) sends to approver DMs; `"channel"` sends to the originating room or DM; `"both"` sends to both. - `agentFilter` / `sessionFilter`: optional allowlists for which agents/sessions trigger Matrix delivery. Authorization differs slightly between approval kinds: @@ -735,9 +699,9 @@ Authorization differs slightly between approval kinds: Both kinds share Matrix reaction shortcuts and message updates. Approvers see reaction shortcuts on the primary approval message: -- `✅` allow once -- `❌` deny -- `♾️` allow always (when the effective exec policy allows it) +- ✅ allow once +- ❌ deny +- ♾️ allow always (when the effective exec policy allows it) Fallback slash commands: `/approve allow-once`, `/approve allow-always`, `/approve deny`. @@ -747,7 +711,7 @@ Related: [Exec approvals](/tools/exec-approvals). ## Slash commands -Slash commands (`/new`, `/reset`, `/model`, `/focus`, `/unfocus`, `/agents`, `/session`, `/acp`, `/approve`, etc.) work directly in DMs. In rooms, OpenClaw also recognizes commands that are prefixed with the bot's own Matrix mention, so `@bot:server /new` triggers the command path without a custom mention regex. This keeps the bot responsive to the room-style `@mention /command` posts that Element and similar clients emit when a user tab-completes the bot before typing the command. +Slash commands (`/new`, `/reset`, `/model`, `/focus`, `/unfocus`, `/agents`, `/session`, `/acp`, `/approve`, etc.) work directly in DMs. In rooms, OpenClaw also recognizes commands prefixed with the bot's own Matrix mention, so `@bot:server /new` triggers the command path without a custom mention regex - this keeps the bot responsive to the room-style `@mention /command` posts that Element and similar clients emit when a user tab-completes the bot before typing the command. Authorization rules still apply: command senders must satisfy the same DM or room allowlist/owner policies as plain messages. @@ -790,7 +754,7 @@ Authorization rules still apply: command senders must satisfy the same DM or roo - Set `defaultAccount` to pick the named account that implicit routing, probing, and CLI commands prefer. - If you have multiple accounts and one is literally named `default`, OpenClaw uses it implicitly even when `defaultAccount` is unset. -- If you have multiple named accounts and no default is selected, CLI commands refuse to guess - set `defaultAccount` or pass `--account `. +- With multiple named accounts and no default selected, CLI commands refuse to guess - set `defaultAccount` or pass `--account `. - The top-level `channels.matrix.*` block is only treated as the implicit `default` account when its auth is complete (`homeserver` + `accessToken`, or `homeserver` + `userId` + `password`). Named accounts remain discoverable from `homeserver` + `userId` once cached credentials cover auth. **Promotion:** @@ -801,11 +765,9 @@ See [Configuration reference](/gateway/config-channels#multi-account-all-channel ## Private/LAN homeservers -By default, OpenClaw blocks private/internal Matrix homeservers for SSRF protection unless you -explicitly opt in per account. +By default, OpenClaw blocks private/internal Matrix homeservers for SSRF protection unless you opt in per account. -If your homeserver runs on localhost, a LAN/Tailscale IP, or an internal hostname, enable -`network.dangerouslyAllowPrivateNetwork` for that Matrix account: +If your homeserver runs on localhost, a LAN/Tailscale IP, or an internal hostname, enable `network.dangerouslyAllowPrivateNetwork` for that account: ```json5 { @@ -831,8 +793,7 @@ openclaw matrix account add \ --access-token syt_ops_xxx ``` -This opt-in only allows trusted private/internal targets. Public cleartext homeservers such as -`http://matrix.example.org:8008` remain blocked. Prefer `https://` whenever possible. +This opt-in only allows trusted private/internal targets. Public cleartext homeservers such as `http://matrix.example.org:8008` remain blocked. Prefer `https://` whenever possible. ## Proxying Matrix traffic @@ -850,21 +811,17 @@ If your Matrix deployment needs an explicit outbound HTTP(S) proxy, set `channel } ``` -Named accounts can override the top-level default with `channels.matrix.accounts..proxy`. -OpenClaw uses the same proxy setting for runtime Matrix traffic and account status probes. +Named accounts can override the top-level default with `channels.matrix.accounts..proxy`. OpenClaw uses the same proxy setting for runtime Matrix traffic and account status probes. ## Target resolution -Matrix accepts these target forms anywhere OpenClaw asks you for a room or user target: +Matrix accepts these target forms anywhere OpenClaw asks for a room or user target: - Users: `@user:server`, `user:@user:server`, or `matrix:user:@user:server` - Rooms: `!room:server`, `room:!room:server`, or `matrix:room:!room:server` - Aliases: `#alias:server`, `channel:#alias:server`, or `matrix:channel:#alias:server` -Matrix room IDs are case-sensitive. Use the exact room ID casing from Matrix -when configuring explicit delivery targets, cron jobs, bindings, or allowlists. -OpenClaw keeps internal session keys canonical for storage, so those lowercase -keys are not a reliable source for Matrix delivery IDs. +Matrix room IDs are case-sensitive. Use the exact room ID casing from Matrix when configuring explicit delivery targets, cron jobs, bindings, or allowlists. OpenClaw keeps internal session keys canonical for storage, so those lowercase keys are not a reliable source for Matrix delivery IDs. Live directory lookup uses the logged-in Matrix account: @@ -874,7 +831,7 @@ Live directory lookup uses the logged-in Matrix account: ## Configuration reference -Allowlist-style user fields (`groupAllowFrom`, `dm.allowFrom`, `groups..users`) accept full Matrix user IDs (safest). Non-ID user entries are ignored by default. If you set `dangerouslyAllowNameMatching: true`, exact Matrix directory display-name matches are resolved at startup and whenever the allowlist changes while the monitor is running; entries that cannot be resolved are ignored at runtime. +Allowlist-style user fields (`groupAllowFrom`, `dm.allowFrom`, `groups..users`) accept full Matrix user IDs (safest). Non-ID entries are ignored by default. If `dangerouslyAllowNameMatching: true` is set, exact Matrix directory display-name matches are resolved at startup and whenever the allowlist changes while the monitor is running; unresolvable entries are ignored at runtime. Room allowlist keys (`groups`, legacy `rooms`) should be room IDs or aliases. Plain room-name keys are ignored by default; `dangerouslyAllowNameMatching: true` restores best-effort lookup against joined room names. @@ -915,22 +872,22 @@ Room allowlist keys (`groups`, legacy `rooms`) should be room IDs or aliases. Pl - `allowlistOnly`: when `true`, forces all active DM policies (except `"disabled"`) and `"open"` group policies to `"allowlist"`. Does not change `"disabled"` policies. - `dangerouslyAllowNameMatching`: when `true`, allows Matrix display-name directory lookup for user allowlist entries and joined-room name lookup for room allowlist keys. Prefer full `@user:server` IDs and room IDs or aliases. - `autoJoin`: `"always"`, `"allowlist"`, or `"off"`. Default: `"off"`. Applies to every Matrix invite, including DM-style invites. -- `autoJoinAllowlist`: rooms/aliases allowed when `autoJoin` is `"allowlist"`. Alias entries are resolved against the homeserver, not against state claimed by the invited room. +- `autoJoinAllowlist`: rooms/aliases allowed when `autoJoin` is `"allowlist"`. Alias entries resolve against the homeserver, not against state claimed by the invited room. - `contextVisibility`: supplemental context visibility (`"all"` default, `"allowlist"`, `"allowlist_quote"`). ### Reply behavior -- `replyToMode`: `"off"`, `"first"`, `"all"`, or `"batched"`. -- `threadReplies`: `"off"`, `"inbound"`, or `"always"`. +- `replyToMode`: `"off"` (default), `"first"`, `"all"`, or `"batched"`. +- `threadReplies`: `"off"` (top-level default resolves to `"inbound"` unless explicitly set), `"inbound"`, or `"always"`. - `threadBindings`: per-channel overrides for thread-bound session routing and lifecycle. -- `streaming`: `"off"` (default), `"partial"`, `"quiet"`, `"progress"`, or object form `{ mode, preview: { toolProgress }, progress: { label, labels, maxLines, maxLineChars, toolProgress } }`. `true` ↔ `"partial"`, `false` ↔ `"off"`. -- `blockStreaming`: when `true`, completed assistant blocks are kept as separate progress messages. +- `streaming`: `"off"` (default), `"partial"`, `"quiet"`, `"progress"`, or object form `{ mode, preview: { toolProgress }, progress: { label, labels, maxLines, maxLineChars, toolProgress } }`. `true` <-> `"partial"`, `false` <-> `"off"`. +- `blockStreaming`: when `true`, completed assistant blocks are kept as separate progress messages. Default: `false`. - `markdown`: optional Markdown rendering config for outbound text. - `responsePrefix`: optional string prepended to outbound replies. - `textChunkLimit`: outbound chunk size in characters when `chunkMode: "length"`. Default: `4000`. - `chunkMode`: `"length"` (default, splits by character count) or `"newline"` (splits at line boundaries). - `historyLimit`: number of recent room messages included as `InboundHistory` when a room message triggers the agent. Falls back to `messages.groupChat.historyLimit`; effective default `0` (disabled). -- `mediaMaxMb`: media size cap in MB for outbound sends and inbound processing. +- `mediaMaxMb`: media size cap in MB for outbound sends and inbound processing. Default: `20`. ### Reaction settings diff --git a/docs/channels/mattermost.md b/docs/channels/mattermost.md index 02417370c08e..1ffa7b0f12c3 100644 --- a/docs/channels/mattermost.md +++ b/docs/channels/mattermost.md @@ -7,12 +7,10 @@ title: "Mattermost" sidebarTitle: "Mattermost" --- -Status: downloadable plugin (bot token + WebSocket events). Channels, groups, and DMs are supported. Mattermost is a self-hostable team messaging platform; see the official site at [mattermost.com](https://mattermost.com) for product details and downloads. +Status: downloadable plugin (bot token + WebSocket events). Channels, private channels, group DMs, and DMs are supported. Mattermost is a self-hostable team messaging platform ([mattermost.com](https://mattermost.com)). ## Install -Install Mattermost before configuring the channel: - ```bash @@ -35,10 +33,10 @@ Details: [Plugins](/tools/plugin) Install `@openclaw/mattermost` with the command above, then restart the Gateway if it is already running. - Create a Mattermost bot account and copy the **bot token**. + Create a Mattermost bot account, copy the **bot token**, and add the bot to the teams and channels it should read. - Copy the Mattermost **base URL** (e.g., `https://chat.example.com`). + Copy the Mattermost **base URL** (e.g., `https://chat.example.com`). A trailing `/api/v4` is stripped automatically. Minimal config: @@ -56,12 +54,22 @@ Details: [Plugins](/tools/plugin) } ``` + Non-interactive alternative: + + ```bash + openclaw channels add --channel mattermost --bot-token --http-url https://chat.example.com + ``` + + +Self-hosted Mattermost on a private/LAN/tailnet address: outbound Mattermost API requests pass through an SSRF guard that blocks private and internal IPs by default. Opt in with `channels.mattermost.network.dangerouslyAllowPrivateNetwork: true` (per account: `channels.mattermost.accounts..network.dangerouslyAllowPrivateNetwork`). + + ## Native slash commands -Native slash commands are opt-in. When enabled, OpenClaw registers `oc_*` slash commands via the Mattermost API and receives callback POSTs on the gateway HTTP server. +Native slash commands are opt-in. When enabled, OpenClaw registers `oc_*` slash commands on every team the bot is a member of and receives callback POSTs on the gateway HTTP server. ```json5 { @@ -79,15 +87,20 @@ Native slash commands are opt-in. When enabled, OpenClaw registers `oc_*` slash } ``` +Registered commands: `/oc_status`, `/oc_model`, `/oc_models`, `/oc_new`, `/oc_help`, `/oc_think`, `/oc_reasoning`, `/oc_verbose`, `/oc_queue`. With `nativeSkills: true`, skill commands are also registered as `/oc_`. + - - `native: "auto"` defaults to disabled for Mattermost. Set `native: true` to enable. - - If `callbackUrl` is omitted, OpenClaw derives one from gateway host/port + `callbackPath`. + - `native` and `nativeSkills` default to `"auto"`, which resolves to disabled for Mattermost. Set them to `true` explicitly. + - `callbackPath` defaults to `/api/channels/mattermost/command`. + - If `callbackUrl` is omitted, OpenClaw derives `http://:`. Wildcard bind hosts (`0.0.0.0`, `::`) fall back to `localhost`. - For multi-account setups, `commands` can be set at the top level or under `channels.mattermost.accounts..commands` (account values override top-level fields). + - Existing slash commands with the same trigger created by other integrations are left untouched (registration skips them); commands the bot created are updated or recreated when the callback URL drifts. - Command callbacks are validated with the per-command tokens returned by Mattermost when OpenClaw registers `oc_*` commands. - - OpenClaw refreshes current Mattermost command registration before accepting each callback so stale tokens from deleted or regenerated slash commands stop being accepted without a gateway restart. + - OpenClaw refreshes current Mattermost command registration before accepting each callback, so stale tokens from deleted or regenerated slash commands stop being accepted without a gateway restart. - Callback validation fails closed if the Mattermost API cannot confirm the command is still current; failed validations are cached briefly, concurrent lookups are coalesced, and fresh lookup starts are rate-limited per command to bound replay pressure. - Slash callbacks fail closed when registration failed, startup was partial, or the callback token does not match the resolved command's registered token (a token valid for one command cannot reach upstream validation for a different command). + - Accepted callbacks are acknowledged with an ephemeral "Processing..." reply; the real answer arrives as a normal message. @@ -119,7 +132,7 @@ Set these on the gateway host if you prefer env vars: Env vars apply only to the **default** account (`default`). Other accounts must use config values. -`MATTERMOST_URL` cannot be set from a workspace `.env`; see [Workspace `.env` files](/gateway/security). +`MATTERMOST_URL` cannot be set from a workspace `.env`; see [Workspace .env files](/gateway/security). ## Chat modes @@ -145,7 +158,7 @@ Config example: channels: { mattermost: { chatmode: "onchar", - oncharPrefixes: [">", "!"], + oncharPrefixes: [">", "!"], // default }, }, } @@ -154,8 +167,8 @@ Config example: Notes: - `onchar` still responds to explicit @mentions. -- `channels.mattermost.requireMention` is honored for legacy configs but `chatmode` is preferred. -- After the bot sends a visible reply in a channel thread, later messages in that same thread are answered without a new @mention or `onchar` prefix, so multi-turn thread conversations keep flowing. Participation is remembered for 7 days of thread inactivity (refreshed on each reply) and persists across gateway restarts. Threads the bot has only observed are unaffected; start a new top-level message to require an explicit mention again. +- `channels.mattermost.requireMention` is still honored, but `chatmode` is preferred. Per-channel `groups..requireMention` settings win over both. +- After the bot sends a visible reply in a channel thread, later messages in that same thread are answered without a new @mention or `onchar` prefix, so multi-turn thread conversations keep flowing. Participation is remembered for 7 days after the bot last replied in that thread and persists across gateway restarts. Threads the bot has only observed are unaffected; start a new top-level message to require an explicit mention again. ## Threading and sessions @@ -163,11 +176,9 @@ Use `channels.mattermost.replyToMode` to control whether channel and group repli - `off` (default): only reply in a thread when the inbound post is already in one. - `first`: for top-level channel/group posts, start a thread under that post and route the conversation to a thread-scoped session. -- `all`: same behavior as `first` for Mattermost today. +- `all` and `batched`: same behavior as `first` for Mattermost today, because once Mattermost has a thread root, follow-up chunks and media continue in that same thread. - Direct messages ignore this setting and stay non-threaded. -Config example: - ```json5 { channels: { @@ -178,19 +189,16 @@ Config example: } ``` -Notes: - -- Thread-scoped sessions use the triggering post id as the thread root. -- `first` and `all` are currently equivalent because once Mattermost has a thread root, follow-up chunks and media continue in that same thread. +Thread-scoped sessions use the triggering post id as the thread root. ## Access control (DMs) -- Default: `channels.mattermost.dmPolicy = "pairing"` (unknown senders get a pairing code). +- Default: `channels.mattermost.dmPolicy = "pairing"` (unknown senders get a pairing code). Other values: `allowlist`, `open`, `disabled`. - Approve via: - `openclaw pairing list mattermost` - `openclaw pairing approve mattermost ` -- Public DMs: `channels.mattermost.dmPolicy="open"` plus `channels.mattermost.allowFrom=["*"]`. -- `channels.mattermost.allowFrom` accepts `accessGroup:` entries. See [Access groups](/channels/access-groups). +- Public DMs: `channels.mattermost.dmPolicy="open"` plus `channels.mattermost.allowFrom=["*"]` (the config schema enforces the wildcard). +- `channels.mattermost.allowFrom` accepts user ids (recommended) and `accessGroup:` entries. See [Access groups](/channels/access-groups). ## Channels (groups) @@ -200,7 +208,8 @@ Notes: - Per-channel mention overrides live under `channels.mattermost.groups..requireMention` or `channels.mattermost.groups["*"].requireMention` for a default. - `@username` matching is mutable and only enabled when `channels.mattermost.dangerouslyAllowNameMatching: true`. - Open channels: `channels.mattermost.groupPolicy="open"` (mention-gated). -- Runtime note: if `channels.mattermost` is completely missing, runtime falls back to `groupPolicy="allowlist"` for group checks (even if `channels.defaults.groupPolicy` is set). +- Resolution order: `channels.mattermost.groupPolicy`, then `channels.defaults.groupPolicy`, then `"allowlist"`. +- Runtime note: if the `channels.mattermost` section is completely missing, runtime fails closed to `groupPolicy="allowlist"` for group checks (even if `channels.defaults.groupPolicy` is set) and logs a one-time warning. Example: @@ -222,9 +231,14 @@ Example: Use these target formats with `openclaw message send` or cron/webhooks: -- `channel:` for a channel -- `user:` for a DM -- `@username` for a DM (resolved via the Mattermost API) +| Target | Delivers to | +| ----------------------------------- | ------------------------------------------------------------- | +| `channel:` | Channel by id | +| `channel:` or `#channel-name` | Channel by name, searched across the teams the bot belongs to | +| `user:` or `mattermost:` | DM with that user | +| `@username` | DM (username resolved via the Mattermost API) | + +Outbound sends support at most one attachment per message; split multiple files into separate sends. Bare opaque IDs (like `64ifufp...`) are **ambiguous** in Mattermost (user ID vs channel ID). @@ -241,7 +255,7 @@ If you need deterministic behavior, always use the explicit prefixes (`user: When OpenClaw sends to a Mattermost DM target and needs to resolve the direct channel first, it retries transient direct-channel creation failures by default. -Use `channels.mattermost.dmChannelRetry` to tune that behavior globally for the Mattermost plugin, or `channels.mattermost.accounts..dmChannelRetry` for one account. +Use `channels.mattermost.dmChannelRetry` to tune that behavior globally for the Mattermost plugin, or `channels.mattermost.accounts..dmChannelRetry` for one account. Defaults: ```json5 { @@ -261,14 +275,14 @@ Use `channels.mattermost.dmChannelRetry` to tune that behavior globally for the Notes: - This applies only to DM channel creation (`/api/v4/channels/direct`), not every Mattermost API call. -- Retries apply to transient failures such as rate limits, 5xx responses, and network or timeout errors. +- Retries use exponential backoff with jitter and apply to transient failures such as rate limits, 5xx responses, and network or timeout errors. - 4xx client errors other than `429` are treated as permanent and are not retried. ## Preview streaming Mattermost streams thinking, tool activity, and partial reply text into a single **draft preview post** that finalizes in place when the final answer is safe to send. The preview updates on the same post id instead of spamming the channel with per-chunk messages. Media/error finals cancel pending preview edits and use normal delivery instead of flushing a throwaway preview post. -Enable via `channels.mattermost.streaming`: +Preview streaming is **on by default** in `partial` mode. Configure via `channels.mattermost.streaming` (a mode string, boolean, or an object like `{ mode: "progress" }`): ```json5 { @@ -282,7 +296,7 @@ Enable via `channels.mattermost.streaming`: - - `partial` is the usual choice: one preview post that is edited as the reply grows, then finalized with the complete answer. + - `partial` (default): one preview post that is edited as the reply grows, then finalized with the complete answer. - `block` uses append-style draft chunks inside the preview post. - `progress` shows a status preview while generating and only posts the final answer at completion. - `off` disables preview streaming. @@ -302,11 +316,11 @@ Enable via `channels.mattermost.streaming`: - `messageId` is the Mattermost post id. - `emoji` accepts names like `thumbsup` or `:+1:` (colons are optional). - Set `remove=true` (boolean) to remove a reaction. -- Reaction add/remove events are forwarded as system events to the routed agent session. +- Reaction add/remove events are forwarded as system events to the routed agent session, subject to the same DM/group policy checks as messages. Examples: -``` +```text message action=react channel=mattermost target=channel: messageId= emoji=thumbsup message action=react channel=mattermost target=channel: messageId= emoji=thumbsup remove=true ``` @@ -320,9 +334,28 @@ Config: Send messages with clickable buttons. When a user clicks a button, the agent receives the selection and can respond. -Normal agent replies can also include semantic `presentation` payloads. OpenClaw renders value buttons as Mattermost interactive buttons, keeps URL buttons visible in the message text, and downgrades select menus to readable text. +Buttons come from the semantic `presentation` payload (in normal agent replies and in `message action=send`). OpenClaw renders value buttons as Mattermost interactive buttons, keeps URL buttons visible in the message text, and downgrades select menus to readable text. -Enable buttons by adding `inlineButtons` to the channel capabilities: +```text +message action=send channel=mattermost target=channel: presentation={"blocks":[{"type":"buttons","buttons":[{"label":"Yes","value":"yes"},{"label":"No","value":"no"}]}]} +``` + +Presentation button fields: + + + Display label (alias: `text`). + + + Value sent back on click, used as the action ID (aliases: `callback_data`, `callbackData`). Required for a clickable button unless `url` is set. + + + Link button; rendered as `label: url` text in the message body instead of an interactive button. + + + Button style. Mattermost applies default styling to values it does not support. + + +To advertise button support in the agent system prompt, add `inlineButtons` to the channel capabilities: ```json5 { @@ -334,48 +367,35 @@ Enable buttons by adding `inlineButtons` to the channel capabilities: } ``` -Use `message action=send` with a `buttons` parameter. Buttons are a 2D array (rows of buttons): - -``` -message action=send channel=mattermost target=channel: buttons=[[{"text":"Yes","callback_data":"yes"},{"text":"No","callback_data":"no"}]] -``` - -Button fields: - - - Display label. - - - Value sent back on click (used as the action ID). - - - Button style. - - When a user clicks a button: + + The clicker must pass the same DM/group policy checks as a message sender; unauthorized clicks get an ephemeral notice and are ignored. + All buttons are replaced with a confirmation line (e.g., "✓ **Yes** selected by @user"). - The agent receives the selection as an inbound message and responds. + The agent receives the selection as an inbound message (plus a system event) and responds. - Button callbacks use HMAC-SHA256 verification (automatic, no config needed). - - Mattermost strips callback data from its API responses (security feature), so all buttons are removed on click - partial removal is not possible. + - The whole attachment block is replaced on click, so all buttons are removed together - partial removal is not possible. - Action IDs containing hyphens or underscores are sanitized automatically (Mattermost routing limitation). + - Clicks whose `action_id` does not match an action on the original post are rejected with `403` ("Unknown action"). - `channels.mattermost.capabilities`: array of capability strings. Add `"inlineButtons"` to enable the buttons tool description in the agent system prompt. - `channels.mattermost.interactions.callbackBaseUrl`: optional external base URL for button callbacks (for example `https://gateway.example.com`). Use this when Mattermost cannot reach the gateway at its bind host directly. - In multi-account setups, you can also set the same field under `channels.mattermost.accounts..interactions.callbackBaseUrl`. - - If `interactions.callbackBaseUrl` is omitted, OpenClaw derives the callback URL from `gateway.customBindHost` + `gateway.port`, then falls back to `http://localhost:`. + - If `interactions.callbackBaseUrl` is omitted, OpenClaw derives the callback URL from `gateway.customBindHost` + `gateway.port` (default 18789), then falls back to `http://localhost:`. The callback path is `/mattermost/interactions/`. - Reachability rule: the button callback URL must be reachable from the Mattermost server. `localhost` only works when Mattermost and OpenClaw run on the same host/network namespace. + - `channels.mattermost.interactions.allowedSourceIps`: source-IP allowlist for button callbacks. Without it, only loopback sources (`127.0.0.1`, `::1`) are accepted, so a remote Mattermost server must be allowlisted here or its clicks are rejected with `403`. Behind a reverse proxy, also set `gateway.trustedProxies` so the real client IP is derived from forwarded headers. - If your callback target is private/tailnet/internal, add its host/domain to Mattermost `ServiceSettings.AllowedUntrustedInternalConnections`. @@ -403,7 +423,7 @@ External scripts and webhooks can post buttons directly via the Mattermost REST integration: { url: "https://gateway.example.com/mattermost/interactions/default", context: { - action_id: "mybutton01", // must match button id (for name lookup) + action_id: "mybutton01", // must match button id action: "approve", // ... any custom fields ... _token: "", // see HMAC section below @@ -424,8 +444,9 @@ External scripts and webhooks can post buttons directly via the Mattermost REST 2. Every action needs `type: "button"` - without it, clicks are swallowed silently. 3. Every action needs an `id` field - Mattermost ignores actions without IDs. 4. Action `id` must be **alphanumeric only** (`[a-zA-Z0-9]`). Hyphens and underscores break Mattermost's server-side action routing (returns 404). Strip them before use. -5. `context.action_id` must match the button's `id` so the confirmation message shows the button name (e.g., "Approve") instead of a raw ID. +5. `context.action_id` must match the button's `id`; the gateway rejects clicks whose `action_id` does not exist on the post. 6. `context.action_id` is required - the interaction handler returns 400 without it. +7. The callback source IP must be allowed (see `interactions.allowedSourceIps` above). @@ -435,13 +456,13 @@ The gateway verifies button clicks with HMAC-SHA256. External scripts must gener - `HMAC-SHA256(key="openclaw-mattermost-interactions", data=botToken)` + `HMAC-SHA256(key="openclaw-mattermost-interactions", data=botToken)`, hex-encoded. Build the context object with all fields **except** `_token`. - Serialize with **sorted keys** and **no spaces** (the gateway uses `JSON.stringify` with sorted keys, which produces compact output). + Serialize with **recursively sorted keys** and **no spaces** (the gateway canonicalizes nested objects too and produces compact JSON). `HMAC-SHA256(key=secret, data=serializedContext)` @@ -501,6 +522,8 @@ Mattermost supports multiple accounts under `channels.mattermost.accounts`: } ``` +Account values override top-level fields; `channels.mattermost.defaultAccount` picks which account is used when none is specified. + ## Troubleshooting @@ -510,6 +533,7 @@ Mattermost supports multiple accounts under `channels.mattermost.accounts`: - Check the bot token, base URL, and whether the account is enabled. - Multi-account issues: env vars only apply to the `default` account. + - Private/LAN Mattermost hosts need `network.dangerouslyAllowPrivateNetwork: true` (the SSRF guard blocks private IPs by default). @@ -519,17 +543,18 @@ Mattermost supports multiple accounts under `channels.mattermost.accounts`: - Mattermost still has old commands pointing at a previous callback target - the gateway restarted without reactivating slash commands - If native slash commands stop working, check logs for `mattermost: failed to register slash commands` or `mattermost: native slash commands enabled but no commands could be registered`. - - If `callbackUrl` is omitted and logs warn that the callback resolved to `http://127.0.0.1:18789/...`, that URL is probably only reachable when Mattermost runs on the same host/network namespace as OpenClaw. Set an explicit externally reachable `commands.callbackUrl` instead. + - If `callbackUrl` is omitted and logs warn that the callback resolved to a loopback URL like `http://localhost:18789/...`, that URL is probably only reachable when Mattermost runs on the same host/network namespace as OpenClaw. Set an explicit externally reachable `commands.callbackUrl` instead. - - Buttons appear as white boxes: the agent may be sending malformed button data. Check that each button has both `text` and `callback_data` fields. - - Buttons render but clicks do nothing: verify `AllowedUntrustedInternalConnections` in Mattermost server config includes `127.0.0.1 localhost`, and that `EnablePostActionIntegration` is `true` in ServiceSettings. + - Buttons appear as white boxes or not at all: the button data is malformed. Each presentation button needs a `label` and a `value` (buttons missing either are dropped). + - Buttons render but clicks do nothing: verify the gateway is reachable from the Mattermost server, the Mattermost server IP is included in `channels.mattermost.interactions.allowedSourceIps` (only loopback is accepted without it), and `ServiceSettings.AllowedUntrustedInternalConnections` includes the callback host for private targets. - Buttons return 404 on click: the button `id` likely contains hyphens or underscores. Mattermost's action router breaks on non-alphanumeric IDs. Use `[a-zA-Z0-9]` only. + - Gateway logs `rejected callback source`: the click came from an IP outside `interactions.allowedSourceIps`. Allowlist the Mattermost server or your ingress, and set `gateway.trustedProxies` behind a reverse proxy. - Gateway logs `invalid _token`: HMAC mismatch. Check that you sign all context fields (not a subset), use sorted keys, and use compact JSON (no spaces). See the HMAC section above. - Gateway logs `missing _token in context`: the `_token` field is not in the button's context. Ensure it is included when building the integration payload. - - Confirmation shows raw ID instead of button name: `context.action_id` does not match the button's `id`. Set both to the same sanitized value. - - Agent doesn't know about buttons: add `capabilities: ["inlineButtons"]` to the Mattermost channel config. + - Gateway rejects the click with `Unknown action`: `context.action_id` does not match any action `id` on the post. Set both to the same sanitized value. + - Agent does not offer buttons: add `capabilities: ["inlineButtons"]` to the Mattermost channel config. diff --git a/docs/channels/msteams.md b/docs/channels/msteams.md index 746e8f0d19f0..3cac2ced644b 100644 --- a/docs/channels/msteams.md +++ b/docs/channels/msteams.md @@ -9,20 +9,17 @@ Status: text + DM attachments are supported; channel/group file sending requires ## Bundled plugin -Microsoft Teams ships as a bundled plugin in current OpenClaw releases, so no -separate install is required in the normal packaged build. +Microsoft Teams ships as a bundled plugin in current OpenClaw releases; no separate install is required in the normal packaged build. -If you are on an older build or a custom install that excludes bundled Teams, -install the npm package directly: +On an older build or a custom install that excludes bundled Teams, install the npm package directly: ```bash openclaw plugins install @openclaw/msteams ``` -Use the bare package to follow the current official release tag. Pin an exact -version only when you need a reproducible install. +Use the bare package to follow the current official release tag. Pin an exact version only when you need a reproducible install. -Local checkout (when running from a git repo): +Local checkout (running from a git repo): ```bash openclaw plugins install ./path/to/local/msteams-plugin @@ -32,7 +29,7 @@ Details: [Plugins](/tools/plugin) ## Quick setup -The [`@microsoft/teams.cli`](https://www.npmjs.com/package/@microsoft/teams.cli) handles bot registration, manifest creation, and credential generation in a single command. +[`@microsoft/teams.cli`](https://www.npmjs.com/package/@microsoft/teams.cli) handles bot registration, manifest creation, and credential generation in one command. **1. Install and log in** @@ -46,9 +43,9 @@ teams status # verify you're logged in and see your tenant info The Teams CLI is currently in preview. Commands and flags may change between releases. -**2. Start a tunnel** (Teams can't reach localhost) +**2. Start a tunnel** (Teams cannot reach localhost) -Install and authenticate the devtunnel CLI if you haven't already ([getting started guide](https://learn.microsoft.com/en-us/azure/developer/dev-tunnels/get-started)). +Install and authenticate the devtunnel CLI if needed ([getting started guide](https://learn.microsoft.com/en-us/azure/developer/dev-tunnels/get-started)). ```bash # One-time setup (persistent URL across sessions): @@ -61,10 +58,10 @@ devtunnel host my-openclaw-bot ``` -`--allow-anonymous` is required because Teams cannot authenticate with devtunnels. Each incoming bot request is still validated by the Teams SDK automatically. +`--allow-anonymous` is required because Teams cannot authenticate with devtunnels. Each incoming bot request is still validated by the Teams SDK. -Alternatives: `ngrok http 3978` or `tailscale funnel 3978` (but these may change URLs each session). +Alternatives: `ngrok http 3978` or `tailscale funnel 3978` (URLs may change each session). **3. Create the app** @@ -74,14 +71,7 @@ teams app create \ --endpoint "https:///api/messages" ``` -This single command: - -- Creates an Entra ID (Azure AD) application -- Generates a client secret -- Builds and uploads a Teams app manifest (with icons) -- Registers the bot (Teams-managed by default - no Azure subscription needed) - -The output will show `CLIENT_ID`, `CLIENT_SECRET`, `TENANT_ID`, and a **Teams App ID** - note these for the next steps. It also offers to install the app in Teams directly. +This creates an Entra ID (Azure AD) application, generates a client secret, builds and uploads a Teams app manifest (with icons), and registers a Teams-managed bot (no Azure subscription needed). The output includes `CLIENT_ID`, `CLIENT_SECRET`, `TENANT_ID`, and a **Teams App ID**; it also offers to install the app in Teams directly. **4. Configure OpenClaw** using the credentials from the output: @@ -103,7 +93,7 @@ Or use environment variables directly: `MSTEAMS_APP_ID`, `MSTEAMS_APP_PASSWORD`, **5. Install the app in Teams** -`teams app create` will prompt you to install the app - select "Install in Teams". If you skipped it, you can get the link later: +`teams app create` prompts you to install the app; select "Install in Teams". To get the install link later: ```bash teams app get --install-link @@ -115,9 +105,9 @@ teams app get --install-link teams app doctor ``` -This runs diagnostics across bot registration, AAD app config, manifest validity, and SSO setup. +Runs diagnostics across bot registration, AAD app config, manifest validity, and SSO setup. -For production deployments, consider using [federated authentication](/channels/msteams#federated-authentication-certificate-plus-managed-identity) (certificate or managed identity) instead of client secrets. +For production, consider [federated authentication](#federated-authentication-certificate-plus-managed-identity) (certificate or managed identity) instead of client secrets. Group chats are blocked by default (`channels.msteams.groupPolicy: "allowlist"`). To allow group replies, set `channels.msteams.groupAllowFrom`, or use `groupPolicy: "open"` to allow any member (mention-gated). @@ -131,7 +121,7 @@ Group chats are blocked by default (`channels.msteams.groupPolicy: "allowlist"`) ## Config writes -By default, Microsoft Teams is allowed to write config updates triggered by `/config set|unset` (requires `commands.config: true`). +By default, Microsoft Teams can write config updates triggered by `/config set|unset` (requires `commands.config: true`). Disable with: @@ -147,15 +137,15 @@ Disable with: - Default: `channels.msteams.dmPolicy = "pairing"`. Unknown senders are ignored until approved. - `channels.msteams.allowFrom` should use stable AAD object IDs or static sender access groups such as `accessGroup:core-team`. -- Do not rely on UPN/display-name matching for allowlists - they can change. OpenClaw disables direct name matching by default; opt in explicitly with `channels.msteams.dangerouslyAllowNameMatching: true`. +- Do not rely on UPN/display-name matching for allowlists; they can change. OpenClaw disables direct name matching by default; opt in with `channels.msteams.dangerouslyAllowNameMatching: true`. - The wizard can resolve names to IDs via Microsoft Graph when credentials allow. **Group access** -- Default: `channels.msteams.groupPolicy = "allowlist"` (blocked unless you add `groupAllowFrom`). Use `channels.defaults.groupPolicy` to override the default when unset. +- Default: `channels.msteams.groupPolicy = "allowlist"` (blocked unless you add `groupAllowFrom`). `channels.defaults.groupPolicy` can override the shared default when `channels.msteams.groupPolicy` is unset. - `channels.msteams.groupAllowFrom` controls which senders or static sender access groups can trigger in group chats/channels (falls back to `channels.msteams.allowFrom`). - Set `groupPolicy: "open"` to allow any member (still mention-gated by default). -- To allow **no channels**, set `channels.msteams.groupPolicy: "disabled"`. +- To block **all** channels, set `channels.msteams.groupPolicy: "disabled"`. Example: @@ -170,14 +160,13 @@ Example: } ``` -**Teams + channel allowlist** +**Team + channel allowlist** - Scope group/channel replies by listing teams and channels under `channels.msteams.teams`. -- Keys should use stable Teams conversation IDs from Teams links, not mutable display names. +- Use stable Teams conversation IDs from Teams links as keys, not mutable display names (see [Team and Channel IDs](#team-and-channel-ids-common-gotcha)). - When `groupPolicy="allowlist"` and a teams allowlist is present, only listed teams/channels are accepted (mention-gated). - The configure wizard accepts `Team/Channel` entries and stores them for you. -- On startup, OpenClaw resolves team/channel and user allowlist names to IDs (when Graph permissions allow) - and logs the mapping; unresolved team/channel names are kept as typed but ignored for routing by default unless `channels.msteams.dangerouslyAllowNameMatching: true` is enabled. +- On startup, OpenClaw resolves team/channel and user allowlist names to IDs (when Graph permissions allow) and logs the mapping. Unresolved names are kept as typed but ignored for routing unless `channels.msteams.dangerouslyAllowNameMatching: true` is set. Example: @@ -201,13 +190,11 @@ Example:
Manual setup (without the Teams CLI) -If you can't use the Teams CLI, you can set up the bot manually through the Azure Portal. - ### How it works 1. Ensure the Microsoft Teams plugin is available (bundled in current releases). 2. Create an **Azure Bot** (App ID + secret + tenant ID). -3. Build a **Teams app package** that references the bot and includes the RSC permissions below. +3. Build a **Teams app package** referencing the bot, including the RSC permissions below. 4. Upload/install the Teams app into a team (or personal scope for DMs). 5. Configure `msteams` in `~/.openclaw/openclaw.json` (or env vars) and start the gateway. 6. The gateway listens for Bot Framework webhook traffic on `/api/messages` by default. @@ -223,44 +210,42 @@ If you can't use the Teams CLI, you can set up the bot manually through the Azur | **Subscription** | Select your Azure subscription | | **Resource group** | Create new or use existing | | **Pricing tier** | **Free** for dev/testing | - | **Type of App** | **Single Tenant** (recommended - see note below) | + | **Type of App** | **Single Tenant** (recommended; see note below) | | **Creation type** | **Create new Microsoft App ID** | Creation of new multi-tenant bots was deprecated after 2025-07-31. Use **Single Tenant** for new bots. -3. Click **Review + create** → **Create** (wait ~1-2 minutes) +3. Click **Review + create** then **Create** (~1-2 minutes). -### Step 2: Get Credentials +### Step 2: Get credentials -1. Go to your Azure Bot resource → **Configuration** -2. Copy **Microsoft App ID** → this is your `appId` -3. Click **Manage Password** → go to the App Registration -4. Under **Certificates & secrets** → **New client secret** → copy the **Value** → this is your `appPassword` -5. Go to **Overview** → copy **Directory (tenant) ID** → this is your `tenantId` +1. Azure Bot resource → **Configuration** → copy **Microsoft App ID** (your `appId`). +2. **Manage Password** → App Registration → **Certificates & secrets** → **New client secret** → copy the **Value** (your `appPassword`). +3. **Overview** → copy **Directory (tenant) ID** (your `tenantId`). -### Step 3: Configure Messaging Endpoint +### Step 3: Configure messaging endpoint -1. In Azure Bot → **Configuration** -2. Set **Messaging endpoint** to your webhook URL: +1. Azure Bot → **Configuration**. +2. Set **Messaging endpoint**: - Production: `https://your-domain.com/api/messages` - - Local dev: Use a tunnel (see [Local Development](#local-development-tunneling) below) + - Local dev: use a tunnel (see [Local development](#local-development-tunneling)) -### Step 4: Enable Teams Channel +### Step 4: Enable Teams channel -1. In Azure Bot → **Channels** -2. Click **Microsoft Teams** → Configure → Save -3. Accept the Terms of Service +1. Azure Bot → **Channels**. +2. Click **Microsoft Teams** → Configure → Save. +3. Accept the Terms of Service. -### Step 5: Build Teams App Manifest +### Step 5: Build Teams app manifest - Include a `bot` entry with `botId = `. - Scopes: `personal`, `team`, `groupChat`. -- `supportsFiles: true` (required for personal scope file handling). -- Add RSC permissions (see [RSC Permissions](#current-teams-rsc-permissions-manifest)). +- `supportsFiles: true` (required for personal-scope file handling). +- Add RSC permissions (see [RSC permissions](#current-teams-rsc-permissions-manifest)). - Create icons: `outline.png` (32x32) and `color.png` (192x192). -- Zip all three files together: `manifest.json`, `outline.png`, `color.png`. +- Zip `manifest.json`, `outline.png`, and `color.png` together. ### Step 6: Configure OpenClaw @@ -280,17 +265,15 @@ Creation of new multi-tenant bots was deprecated after 2025-07-31. Use **Single Environment variables: `MSTEAMS_APP_ID`, `MSTEAMS_APP_PASSWORD`, `MSTEAMS_TENANT_ID`. -### Step 7: Run the Gateway +### Step 7: Run the gateway -The Teams channel starts automatically when the plugin is available and `msteams` config exists with credentials. +The Teams channel starts automatically when the plugin is available and `msteams` config has credentials.
## Federated authentication (certificate plus managed identity) -> Added in 2026.4.11 - -For production deployments, OpenClaw supports **federated authentication** as a more secure alternative to client secrets. Two methods are available: +For production, OpenClaw supports **federated authentication** as an alternative to client secrets, via `channels.msteams.authType: "federated"`. Two methods: ### Option A: Certificate-based authentication @@ -299,7 +282,7 @@ Use a PEM certificate registered with your Entra ID app registration. **Setup:** 1. Generate or obtain a certificate (PEM format with private key). -2. In Entra ID → App Registration → **Certificates & secrets** → **Certificates** → Upload the public certificate. +2. Entra ID → App Registration → **Certificates & secrets** → **Certificates** → upload the public certificate. **Config:** @@ -325,20 +308,20 @@ Use a PEM certificate registered with your Entra ID app registration. ### Option B: Azure Managed Identity -Use Azure Managed Identity for passwordless authentication. This is ideal for deployments on Azure infrastructure (AKS, App Service, Azure VMs) where a managed identity is available. +Use Azure Managed Identity for passwordless authentication on Azure infrastructure (AKS, App Service, Azure VMs). **How it works:** -1. The bot pod/VM has a managed identity (system-assigned or user-assigned). -2. A **federated identity credential** links the managed identity to the Entra ID app registration. -3. At runtime, OpenClaw uses `@azure/identity` to acquire tokens from the Azure IMDS endpoint (`169.254.169.254`). +1. The bot pod/VM has a managed identity (system- or user-assigned). +2. A federated identity credential links the managed identity to the Entra ID app registration. +3. At runtime, OpenClaw uses `@azure/identity` to acquire tokens from the Azure IMDS endpoint. 4. The token is passed to the Teams SDK for bot authentication. **Prerequisites:** -- Azure infrastructure with managed identity enabled (AKS workload identity, App Service, VM) -- Federated identity credential created on the Entra ID app registration -- Network access to IMDS (`169.254.169.254:80`) from the pod/VM +- Azure infrastructure with managed identity enabled (AKS workload identity, App Service, VM). +- Federated identity credential created on the Entra ID app registration. +- Network access to IMDS (`169.254.169.254:80`) from the pod/VM. **Config (system-assigned managed identity):** @@ -357,31 +340,15 @@ Use Azure Managed Identity for passwordless authentication. This is ideal for de } ``` -**Config (user-assigned managed identity):** - -```json5 -{ - channels: { - msteams: { - enabled: true, - appId: "", - tenantId: "", - authType: "federated", - useManagedIdentity: true, - managedIdentityClientId: "", - webhook: { port: 3978, path: "/api/messages" }, - }, - }, -} -``` +**Config (user-assigned managed identity):** add `managedIdentityClientId: ""` to the block above. **Env vars:** - `MSTEAMS_AUTH_TYPE=federated` - `MSTEAMS_USE_MANAGED_IDENTITY=true` -- `MSTEAMS_MANAGED_IDENTITY_CLIENT_ID=` (only for user-assigned) +- `MSTEAMS_MANAGED_IDENTITY_CLIENT_ID=` (user-assigned only) -### AKS Workload Identity Setup +### AKS Workload Identity setup For AKS deployments using workload identity: @@ -416,7 +383,7 @@ For AKS deployments using workload identity: azure.workload.identity/use: "true" ``` -5. **Ensure network access** to IMDS (`169.254.169.254`) - if using NetworkPolicy, add an egress rule allowing traffic to `169.254.169.254/32` on port 80. +5. **Allow network access** to IMDS (`169.254.169.254`): if using NetworkPolicy, add an egress rule for `169.254.169.254/32` on port 80. ### Auth type comparison @@ -426,11 +393,13 @@ For AKS deployments using workload identity: | **Certificate** | `authType: "federated"` + `certificatePath` | No shared secret over network | Certificate management overhead | | **Managed Identity** | `authType: "federated"` + `useManagedIdentity` | Passwordless, no secrets to manage | Azure infrastructure required | -**Default behavior:** When `authType` is not set, OpenClaw defaults to client secret authentication. Existing configurations continue to work without changes. +`certificateThumbprint` can be set alongside `certificatePath` but is not read by the auth path today; it is accepted for forward compatibility only. + +**Default:** when `authType` is unset, OpenClaw uses client-secret authentication (`appPassword`). Existing configs keep working unchanged. ## Local development (tunneling) -Teams can't reach `localhost`. Use a persistent dev tunnel so your URL stays the same across sessions: +Teams cannot reach `localhost`. Use a persistent dev tunnel so the URL stays stable across sessions: ```bash # One-time setup: @@ -443,13 +412,13 @@ devtunnel host my-openclaw-bot Alternatives: `ngrok http 3978` or `tailscale funnel 3978` (URLs may change each session). -If your tunnel URL changes, update the endpoint: +If the tunnel URL changes, update the endpoint: ```bash teams app update --endpoint "https:///api/messages" ``` -## Testing the Bot +## Testing the bot **Run diagnostics:** @@ -461,41 +430,41 @@ Checks bot registration, AAD app, manifest, and SSO configuration in one pass. **Send a test message:** -1. Install the Teams app (use the install link from `teams app get --install-link`) -2. Find the bot in Teams and send a DM -3. Check gateway logs for incoming activity +1. Install the Teams app (install link from `teams app get --install-link`). +2. Find the bot in Teams and send a DM. +3. Check gateway logs for incoming activity. ## Environment variables -All config keys can be set via environment variables instead: +These auth-related config keys can be set via environment variables instead of `openclaw.json` (other config keys, such as `groupPolicy` or `historyLimit`, are config-only): -- `MSTEAMS_APP_ID` -- `MSTEAMS_APP_PASSWORD` -- `MSTEAMS_TENANT_ID` -- `MSTEAMS_AUTH_TYPE` (optional: `"secret"` or `"federated"`) -- `MSTEAMS_CERTIFICATE_PATH` (federated + certificate) -- `MSTEAMS_CERTIFICATE_THUMBPRINT` (optional, not required for auth) -- `MSTEAMS_USE_MANAGED_IDENTITY` (federated + managed identity) -- `MSTEAMS_MANAGED_IDENTITY_CLIENT_ID` (user-assigned MI only) +| Env var | Config key | Notes | +| ------------------------------------ | ------------------------- | ----------------------------------- | +| `MSTEAMS_APP_ID` | `appId` | | +| `MSTEAMS_APP_PASSWORD` | `appPassword` | | +| `MSTEAMS_TENANT_ID` | `tenantId` | | +| `MSTEAMS_AUTH_TYPE` | `authType` | `"secret"` or `"federated"` | +| `MSTEAMS_CERTIFICATE_PATH` | `certificatePath` | federated + certificate | +| `MSTEAMS_CERTIFICATE_THUMBPRINT` | `certificateThumbprint` | accepted, not required for auth | +| `MSTEAMS_USE_MANAGED_IDENTITY` | `useManagedIdentity` | federated + managed identity | +| `MSTEAMS_MANAGED_IDENTITY_CLIENT_ID` | `managedIdentityClientId` | user-assigned managed identity only | ## Member info action -OpenClaw exposes a Graph-backed `member-info` action for Microsoft Teams so agents and automations can resolve channel member details (display name, email, role) directly from Microsoft Graph. +OpenClaw exposes a Graph-backed `member-info` message action for Microsoft Teams so agents and automations can resolve channel member details (display name, email, job title, UPN, office location) directly from Microsoft Graph. Requirements: -- `Member.Read.Group` RSC permission (already in the recommended manifest) -- For cross-team lookups: `User.Read.All` Graph Application permission with admin consent +- `Member.Read.Group` RSC permission (already in the recommended manifest). +- For cross-team lookups: `User.Read.All` Graph Application permission with admin consent. -The action is gated by `channels.msteams.actions.memberInfo` (default: enabled when Graph credentials are available). +The action runs whenever Graph credentials are configured; it fails with a Graph auth error when they are not. There is no separate `channels.msteams.actions.memberInfo` toggle. ## History context -- `channels.msteams.historyLimit` controls how many recent channel/group messages are wrapped into the prompt. -- Falls back to `messages.groupChat.historyLimit`. Set `0` to disable (default 50). +- `channels.msteams.historyLimit` controls how many recent channel/group messages are wrapped into the prompt. Falls back to `messages.groupChat.historyLimit`, then defaults to 50. Set `0` to disable. - Fetched thread history is filtered by sender allowlists (`allowFrom` / `groupAllowFrom`), so thread context seeding only includes messages from allowed senders. -- Quoted attachment context (`ReplyTo*` derived from Teams reply HTML) is currently passed as received. -- In other words, allowlists gate who can trigger the agent; only specific supplemental context paths are filtered today. +- Quoted attachment context (parsed from the Skype Reply-schema HTML in a reply's own attachments) is passed through unfiltered; only thread-history seeding applies the sender-allowlist filter today. - DM history can be limited with `channels.msteams.dmHistoryLimit` (user turns). Per-user overrides: `channels.msteams.dms[""].historyLimit`. ## Current Teams RSC permissions (manifest) @@ -516,7 +485,7 @@ These are the **existing resourceSpecific permissions** in our Teams app manifes - `ChatMessage.Read.Chat` (Application) - receive all group chat messages without @mention -To add RSC permissions via the Teams CLI: +Add RSC permissions via the Teams CLI: ```bash teams app rsc add ChannelMessage.Read.Group --type Application @@ -578,12 +547,10 @@ Minimal, valid example with the required fields. Replace IDs and URLs. - `webApplicationInfo.id` **must** match the Azure Bot App ID. - `bots[].scopes` must include the surfaces you plan to use (`personal`, `team`, `groupChat`). - `bots[].supportsFiles: true` is required for file handling in personal scope. -- `authorization.permissions.resourceSpecific` must include channel read/send if you want channel traffic. +- `authorization.permissions.resourceSpecific` must include channel read/send for channel traffic. ### Updating an existing app -To update an already-installed Teams app (e.g., to add RSC permissions): - ```bash # Download, edit, and re-upload the manifest teams app manifest download manifest.json @@ -592,17 +559,17 @@ teams app manifest upload manifest.json # Version is auto-bumped if content changed ``` -After updating, reinstall the app in each team for new permissions to take effect, and **fully quit and relaunch Teams** (not just close the window) to clear cached app metadata. +After updating, reinstall the app in each team, and **fully quit and relaunch Teams** (not just close the window) to clear cached app metadata.
Manual manifest update (without CLI) -1. Update your `manifest.json` with the new settings -2. **Increment the `version` field** (e.g., `1.0.0` → `1.1.0`) -3. **Re-zip** the manifest with icons (`manifest.json`, `outline.png`, `color.png`) +1. Update `manifest.json` with the new settings. +2. **Increment the `version` field** (e.g., `1.0.0` → `1.1.0`). +3. **Re-zip** the manifest with icons (`manifest.json`, `outline.png`, `color.png`). 4. Upload the new zip: - - **Teams Admin Center:** Teams apps → Manage apps → find your app → Upload new version - - **Sideload:** In Teams → Apps → Manage your apps → Upload a custom app + - **Teams Admin Center:** Teams apps → Manage apps → find your app → Upload new version. + - **Sideload:** Teams → Apps → Manage your apps → Upload a custom app.
@@ -618,53 +585,52 @@ Works: Does NOT work: -- Channel/group **image or file contents** (payload only includes HTML stub). +- Channel/group **image or file contents** (payload only includes an HTML stub). - Downloading attachments stored in SharePoint/OneDrive. -- Reading message history (beyond the live webhook event). +- Reading message history beyond the live webhook event. ### With **Teams RSC + Microsoft Graph Application permissions** Adds: -- Downloading hosted contents (images pasted into messages). +- Downloading hosted content (images pasted into messages). - Downloading file attachments stored in SharePoint/OneDrive. - Reading channel/chat message history via Graph. ### RSC vs Graph API -| Capability | RSC Permissions | Graph API | +| Capability | RSC permissions | Graph API | | ----------------------- | -------------------- | ----------------------------------- | | **Real-time messages** | Yes (via webhook) | No (polling only) | | **Historical messages** | No | Yes (can query history) | | **Setup complexity** | App manifest only | Requires admin consent + token flow | | **Works offline** | No (must be running) | Yes (query anytime) | -**Bottom line:** RSC is for real-time listening; Graph API is for historical access. For catching up on missed messages while offline, you need Graph API with `ChannelMessage.Read.All` (requires admin consent). +**Bottom line:** RSC is for real-time listening; Graph API is for historical access. To catch up on missed messages while offline, you need Graph API with `ChannelMessage.Read.All` (requires admin consent). ## Graph-enabled media + history (required for channels) -If you need images/files in **channels** or want to fetch **message history**, you must enable Microsoft Graph permissions and grant admin consent. +For images/files in **channels**, or to fetch **message history**, enable Microsoft Graph permissions and grant admin consent: -1. In Entra ID (Azure AD) **App Registration**, add Microsoft Graph **Application permissions**: +1. Entra ID (Azure AD) **App Registration** → add Graph **Application permissions**: - `ChannelMessage.Read.All` (channel attachments + history) - `Chat.Read.All` or `ChatMessage.Read.All` (group chats) 2. **Grant admin consent** for the tenant. 3. Bump the Teams app **manifest version**, re-upload, and **reinstall the app in Teams**. 4. **Fully quit and relaunch Teams** to clear cached app metadata. -**Additional permission for user mentions:** User @mentions work out of the box for users in the conversation. However, if you want to dynamically search and mention users who are **not in the current conversation**, add `User.Read.All` (Application) permission and grant admin consent. +**User mentions:** @mentions work out of the box for users already in the conversation. To dynamically search and mention users **not in the current conversation**, add `User.Read.All` (Application) permission and grant admin consent. ## Known limitations ### Webhook timeouts -Teams delivers messages via HTTP webhook. If processing takes too long (e.g., slow LLM responses), you may see: +Teams delivers messages via HTTP webhook. OpenClaw applies fixed HTTP server timeouts to that webhook listener: 30s inactivity, 30s total request, 15s to receive headers. If agent processing takes longer than the client's own retry window, you may see: -- Gateway timeouts -- Teams retrying the message (causing duplicates) -- Dropped replies +- Teams retrying the message (causing duplicates). +- Dropped replies. -OpenClaw handles this by returning quickly and sending replies proactively, but very slow responses may still cause issues. +OpenClaw acks the webhook quickly (before agent processing finishes) and sends replies proactively once the agent responds, but very slow agent runs can still surface retries/duplicates on the Teams side. ### Teams cloud and service URL support @@ -679,7 +645,7 @@ For non-public Teams clouds, set `cloud` and the matching proactive boundary whe - `channels.msteams.cloud` selects the Teams SDK cloud preset for authentication, JWT validation, token services, and Graph scope. - `channels.msteams.serviceUrl` selects the Bot Connector endpoint boundary used to validate stored conversation references before proactive sends, edits, deletes, cards, polls, file-consent messages, and queued long-running replies. It is required for USGov and DoD SDK clouds. For China/21Vianet, OpenClaw uses the SDK `China` preset and accepts stored/configured service URLs only on Azure China Bot Framework channel hosts. -Microsoft publishes the global proactive Bot Connector endpoints in the [Create the conversation](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/conversations/send-proactive-messages?tabs=dotnet#create-the-conversation) section of the Teams proactive messaging docs. Use the incoming activity's `serviceUrl` when available; if you need a global proactive endpoint, use Microsoft's table. +Microsoft publishes the global proactive Bot Connector endpoints in the [Create the conversation](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/conversations/send-proactive-messages?tabs=dotnet#create-the-conversation) section of the Teams proactive messaging docs. Use the incoming activity's `serviceUrl` when available; otherwise use Microsoft's table below. | Teams environment | OpenClaw config | Proactive `serviceUrl` | | ----------------- | ----------------------------------------------------------- | -------------------------------------------------- | @@ -689,7 +655,7 @@ Microsoft publishes the global proactive Bot Connector endpoints in the [Create | DoD | `cloud: "USGovDoD"` + `serviceUrl` | `https://smba.infra.dod.teams.microsoft.us/teams` | | China/21Vianet | `cloud: "China"` | use the incoming activity's `serviceUrl` | -Example for GCC, where Microsoft documents a separate proactive service URL but the Teams SDK does not expose a separate GCC cloud preset: +Example for GCC, where Microsoft documents a separate proactive service URL but the Teams SDK exposes no separate GCC cloud preset: ```json { @@ -716,35 +682,36 @@ Example for GCC High: `channels.msteams.serviceUrl` is restricted to supported Microsoft Teams Bot Connector hosts. When a service URL is configured, OpenClaw checks that the stored conversation `serviceUrl` uses the same host before proactive sends, edits, deletes, cards, polls, or queued long-running replies run. With the default public-cloud config, OpenClaw fails closed if a stored conversation points outside the public Teams Connector host. Receive a fresh message from the conversation after changing cloud/service URL settings so the stored conversation reference is current. -China/21Vianet does not have a separate global proactive `smba` URL in Microsoft's Teams proactive endpoint table. Configure `cloud: "China"` so the Teams SDK uses Azure China auth, token, and JWT endpoints. Proactive sends then require a stored conversation reference from an incoming China Teams activity, or an explicitly configured service URL, on the Azure China Bot Framework channel boundary (`*.botframework.azure.cn`). Graph-backed Teams helpers are currently disabled for `cloud: "China"` until OpenClaw routes Graph requests through the Azure China Graph endpoint. +China/21Vianet has no separate global proactive `smba` URL in Microsoft's Teams proactive endpoint table. Configure `cloud: "China"` so the Teams SDK uses Azure China auth, token, and JWT endpoints. Proactive sends then require a stored conversation reference from an incoming China Teams activity, or an explicitly configured service URL, on the Azure China Bot Framework channel boundary (`*.botframework.azure.cn`). Graph-backed Teams helpers are disabled for `cloud: "China"` until OpenClaw routes Graph requests through the Azure China Graph endpoint. ### Formatting Teams markdown is more limited than Slack or Discord: -- Basic formatting works: **bold**, _italic_, `code`, links -- Complex markdown (tables, nested lists) may not render correctly -- Adaptive Cards are supported for polls and semantic presentation sends (see below) +- Basic formatting works: **bold**, _italic_, `code`, links. +- Complex markdown (tables, nested lists) may not render correctly. +- Adaptive Cards are supported for polls and semantic presentation sends (see below). ## Configuration -Key settings (see `/gateway/configuration` for shared channel patterns): +Key settings (see [/gateway/configuration](/gateway/configuration) for shared channel patterns): - `channels.msteams.enabled`: enable/disable the channel. - `channels.msteams.appId`, `channels.msteams.appPassword`, `channels.msteams.tenantId`: bot credentials. -- `channels.msteams.cloud`: Teams SDK cloud environment (`Public`, `USGov`, `USGovDoD`, or `China`; default `Public`). Set this with `serviceUrl` for USGov/DoD SDK clouds; China uses the SDK preset and stored Azure China Bot Framework conversation references, with Graph-backed helpers disabled until Azure China Graph routing is implemented. -- `channels.msteams.serviceUrl`: Bot Connector service URL boundary for SDK proactive operations. Public cloud uses the SDK default; set this for GCC (`https://smba.infra.gcc.teams.microsoft.com/teams`), GCC High, or DoD. China accepts Azure China Bot Framework channel hosts when the stored conversation reference comes from Teams operated by 21Vianet. -- `channels.msteams.webhook.port` (default `3978`) -- `channels.msteams.webhook.path` (default `/api/messages`) -- `channels.msteams.dmPolicy`: `pairing | allowlist | open | disabled` (default: pairing) +- `channels.msteams.cloud`: Teams SDK cloud environment (`Public`, `USGov`, `USGovDoD`, or `China`; default `Public`). Set with `serviceUrl` for USGov/DoD SDK clouds; China uses the SDK preset and stored Azure China Bot Framework conversation references, with Graph-backed helpers disabled until Azure China Graph routing ships. +- `channels.msteams.serviceUrl`: Bot Connector service URL boundary for SDK proactive operations. Public cloud uses the SDK default; set for GCC (`https://smba.infra.gcc.teams.microsoft.com/teams`), GCC High, or DoD. China accepts Azure China Bot Framework channel hosts when the stored conversation reference comes from Teams operated by 21Vianet. +- `channels.msteams.webhook.port` (default `3978`). +- `channels.msteams.webhook.path` (default `/api/messages`). +- `channels.msteams.dmPolicy`: `pairing | allowlist | open | disabled` (default `pairing`). - `channels.msteams.allowFrom`: DM allowlist (AAD object IDs recommended). The wizard resolves names to IDs during setup when Graph access is available. - `channels.msteams.dangerouslyAllowNameMatching`: break-glass toggle to re-enable mutable UPN/display-name matching and direct team/channel name routing. -- `channels.msteams.textChunkLimit`: outbound text chunk size. +- `channels.msteams.textChunkLimit`: outbound text chunk size in characters (default `4000`, and hard-capped at `4000` regardless of a higher configured value). - `channels.msteams.chunkMode`: `length` (default) or `newline` to split on blank lines (paragraph boundaries) before length chunking. -- `channels.msteams.mediaAllowHosts`: allowlist for inbound attachment hosts (defaults to Microsoft/Teams domains). +- `channels.msteams.mediaAllowHosts`: allowlist for inbound attachment hosts (defaults to Microsoft/Teams domains: Graph, SharePoint/OneDrive, Teams CDN, Bot Framework, Azure Media Services). - `channels.msteams.mediaAuthAllowHosts`: allowlist for attaching Authorization headers on media retries (defaults to Graph + Bot Framework hosts). -- `channels.msteams.requireMention`: require @mention in channels/groups (default true). -- `channels.msteams.replyStyle`: `thread | top-level` (see [Reply Style](#reply-style-threads-vs-posts)). +- `channels.msteams.mediaMaxMb`: per-channel media size limit override in MB. Falls back to `agents.defaults.mediaMaxMb` when unset. +- `channels.msteams.requireMention`: require @mention in channels/groups (default `true`). +- `channels.msteams.replyStyle`: `thread | top-level` (see [Reply style](#reply-style-threads-vs-posts)). - `channels.msteams.teams..replyStyle`: per-team override. - `channels.msteams.teams..requireMention`: per-team override. - `channels.msteams.teams..tools`: default per-team tool policy overrides (`allow`/`deny`/`alsoAllow`) used when a channel override is missing. @@ -753,15 +720,17 @@ Key settings (see `/gateway/configuration` for shared channel patterns): - `channels.msteams.teams..channels..requireMention`: per-channel override. - `channels.msteams.teams..channels..tools`: per-channel tool policy overrides (`allow`/`deny`/`alsoAllow`). - `channels.msteams.teams..channels..toolsBySender`: per-channel per-sender tool policy overrides (`"*"` wildcard supported). -- `toolsBySender` keys should use explicit prefixes: - `channel:`, `id:`, `e164:`, `username:`, `name:` (legacy unprefixed keys still map to `id:` only). -- `channels.msteams.actions.memberInfo`: enable or disable the Graph-backed member info action (default: enabled when Graph credentials are available). +- `toolsBySender` keys should use explicit prefixes: `channel:`, `id:`, `e164:`, `username:`, `name:` (legacy unprefixed keys still map to `id:` only). - `channels.msteams.authType`: authentication type - `"secret"` (default) or `"federated"`. - `channels.msteams.certificatePath`: path to PEM certificate file (federated + certificate auth). -- `channels.msteams.certificateThumbprint`: certificate thumbprint (optional, not required for auth). +- `channels.msteams.certificateThumbprint`: certificate thumbprint; accepted, not required for auth. - `channels.msteams.useManagedIdentity`: enable managed identity auth (federated mode). - `channels.msteams.managedIdentityClientId`: client ID for user-assigned managed identity. - `channels.msteams.sharePointSiteId`: SharePoint site ID for file uploads in group chats/channels (see [Sending files in group chats](#sending-files-in-group-chats)). +- `channels.msteams.welcomeCard`, `channels.msteams.groupWelcomeCard`, `channels.msteams.promptStarters`: welcome Adaptive Card shown on first DM/group contact, and its suggested prompt buttons. +- `channels.msteams.responsePrefix`: text prefixed to outbound replies. +- `channels.msteams.feedbackEnabled` (default `true`), `channels.msteams.feedbackReflection` (default `true`), `channels.msteams.feedbackReflectionCooldownMs`: thumbs-up/down feedback on replies and the negative-feedback reflection follow-up. +- `channels.msteams.sso`, `channels.msteams.delegatedAuth`: Bot Framework OAuth connection and delegated Graph scopes for SSO-backed flows; `sso.enabled: true` requires `sso.connectionName`. ## Routing and sessions @@ -773,19 +742,19 @@ Key settings (see `/gateway/configuration` for shared channel patterns): ## Reply style: threads vs posts -Teams recently introduced two channel UI styles over the same underlying data model: +Teams has two channel UI styles over the same underlying data model: | Style | Description | Recommended `replyStyle` | | ------------------------ | --------------------------------------------------------- | ------------------------ | | **Posts** (classic) | Messages appear as cards with threaded replies underneath | `thread` (default) | | **Threads** (Slack-like) | Messages flow linearly, more like Slack | `top-level` | -**The problem:** The Teams API does not expose which UI style a channel uses. If you use the wrong `replyStyle`: +**The problem:** the Teams API does not expose which UI style a channel uses. If you use the wrong `replyStyle`: -- `thread` in a Threads-style channel → replies appear nested awkwardly -- `top-level` in a Posts-style channel → replies appear as separate top-level posts instead of in-thread +- `thread` in a Threads-style channel → replies appear nested awkwardly. +- `top-level` in a Posts-style channel → replies appear as separate top-level posts instead of in-thread. -**Solution:** Configure `replyStyle` per-channel based on how the channel is set up: +**Solution:** configure `replyStyle` per-channel based on how the channel is set up: ```json5 { @@ -810,38 +779,40 @@ Teams recently introduced two channel UI styles over the same underlying data mo When the bot sends a reply into a channel, `replyStyle` is resolved from the most specific override down to the default. The first non-`undefined` value wins: -1. **Per-channel** — `channels.msteams.teams..channels..replyStyle` -2. **Per-team** — `channels.msteams.teams..replyStyle` -3. **Global** — `channels.msteams.replyStyle` -4. **Implicit default** — derived from `requireMention`: +1. **Per-channel** - `channels.msteams.teams..channels..replyStyle` +2. **Per-team** - `channels.msteams.teams..replyStyle` +3. **Global** - `channels.msteams.replyStyle` +4. **Implicit default** - derived from `requireMention`: - `requireMention: true` → `thread` - `requireMention: false` → `top-level` -If you set `requireMention: false` globally without an explicit `replyStyle`, mentions in Posts-style channels will surface as top-level posts even when the inbound was a thread reply. Pin `replyStyle: "thread"` at the global, team, or channel level to avoid surprises. +If you set `requireMention: false` globally without an explicit `replyStyle`, mentions in Posts-style channels surface as top-level posts even when the inbound was a thread reply. Pin `replyStyle: "thread"` at the global, team, or channel level to avoid surprises. + +For proactive sends into a stored channel conversation (queued tool-call replies, long-running agents), the same team/channel resolution applies; group chats and personal (DM) conversations always resolve to `top-level` for proactive sends regardless of `replyStyle`. ### Thread context preservation -When `replyStyle: "thread"` is in effect and the bot was @mentioned from inside a channel thread, OpenClaw re-attaches the original thread root to the outbound conversation reference (`19:…@thread.tacv2;messageid=`) so the reply lands inside the same thread. This holds for both live (in-turn) sends and proactive sends made after the Bot Framework turn context has expired (e.g., long-running agents, queued tool-call replies via `mcp__openclaw__message`). +When `replyStyle: "thread"` is in effect and the bot was @mentioned from inside a channel thread, OpenClaw re-attaches the original thread root to the outbound conversation reference (`19:...@thread.tacv2;messageid=`) so the reply lands inside the same thread. This holds for both live (in-turn) sends and proactive sends made after the Bot Framework turn context has expired (e.g., long-running agents, queued tool-call replies via `mcp__openclaw__message`). The thread root is taken from the stored `threadId` on the conversation reference. Older stored references that predate `threadId` fall back to `activityId` (whatever inbound activity last seeded the conversation), so existing deployments keep working without a re-seed. -When `replyStyle: "top-level"` is in effect, channel-thread inbounds are intentionally answered as new top-level posts — no thread suffix is attached. This is the correct behavior for Threads-style channels; if you see top-level posts where you expected threaded replies, your `replyStyle` is set incorrectly for that channel. +When `replyStyle: "top-level"` is in effect, channel-thread inbounds are intentionally answered as new top-level posts; no thread suffix is attached. This is correct for Threads-style channels; top-level posts where you expected threaded replies means `replyStyle` is set incorrectly for that channel. ## Attachments and images **Current limitations:** -- **DMs:** Images and file attachments work via Teams bot file APIs. -- **Channels/groups:** Attachments live in M365 storage (SharePoint/OneDrive). The webhook payload only includes an HTML stub, not the actual file bytes. **Graph API permissions are required** to download channel attachments. -- For explicit file-first sends, use `action=upload-file` with `media` / `filePath` / `path`; optional `message` becomes the accompanying text/comment, and `filename` overrides the uploaded name. +- **DMs:** images and file attachments work via Teams bot file APIs. +- **Channels/groups:** attachments live in M365 storage (SharePoint/OneDrive). The webhook payload only includes an HTML stub, not the actual file bytes. **Graph API permissions are required** to download channel attachments. +- For explicit file-first sends, use `action=upload-file` with `media` / `filePath` / `path`; optional `message` becomes the accompanying text/comment, and `filename` (or `title`) overrides the uploaded name. -Without Graph permissions, channel messages with images will be received as text-only (the image content is not accessible to the bot). +Without Graph permissions, channel messages with images arrive as text-only (the image content is not accessible to the bot). By default, OpenClaw only downloads media from Microsoft/Teams hostnames. Override with `channels.msteams.mediaAllowHosts` (use `["*"]` to allow any host). Authorization headers are only attached for hosts in `channels.msteams.mediaAuthAllowHosts` (defaults to Graph + Bot Framework hosts). Keep this list strict (avoid multi-tenant suffixes). ## Sending files in group chats -Bots can send files in DMs using the FileConsentCard flow (built-in). However, **sending files in group chats/channels** requires additional setup: +Bots can send files in DMs using the built-in FileConsentCard flow. **Sending files in group chats/channels** requires additional setup: | Context | How files are sent | Setup needed | | ------------------------ | -------------------------------------------- | ----------------------------------------------- | @@ -851,16 +822,14 @@ Bots can send files in DMs using the FileConsentCard flow (built-in). However, * ### Why group chats need SharePoint -Bots don't have a personal OneDrive drive (the `/me/drive` Graph API endpoint doesn't work for application identities). To send files in group chats/channels, the bot uploads to a **SharePoint site** and creates a sharing link. +Bots do not have a personal OneDrive drive (`/me/drive` does not work for application identities). To send files in group chats/channels, the bot uploads to a **SharePoint site** and creates a sharing link. ### Setup 1. **Add Graph API permissions** in Entra ID (Azure AD) → App Registration: - - `Sites.ReadWrite.All` (Application) - upload files to SharePoint - - `Chat.Read.All` (Application) - optional, enables per-user sharing links - + - `Sites.ReadWrite.All` (Application) - upload files to SharePoint. + - `Chat.Read.All` (Application) - optional, enables per-user sharing links. 2. **Grant admin consent** for the tenant. - 3. **Get your SharePoint site ID:** ```bash @@ -895,7 +864,7 @@ Bots don't have a personal OneDrive drive (the `/me/drive` Graph API endpoint do | `Sites.ReadWrite.All` only | Organization-wide sharing link (anyone in org can access) | | `Sites.ReadWrite.All` + `Chat.Read.All` | Per-user sharing link (only chat members can access) | -Per-user sharing is more secure as only the chat participants can access the file. If `Chat.Read.All` permission is missing, the bot falls back to organization-wide sharing. +Per-user sharing is more secure since only chat participants can access the file. If `Chat.Read.All` is missing, the bot falls back to organization-wide sharing. ### Fallback behavior @@ -914,17 +883,17 @@ Uploaded files are stored in a `/OpenClawShared/` folder in the configured Share OpenClaw sends Teams polls as Adaptive Cards (there is no native Teams poll API). -- CLI: `openclaw message poll --channel msteams --target conversation: ...` +- CLI: `openclaw message poll --channel msteams --target conversation: --poll-question "..." --poll-option "..." --poll-option "..."`. - Votes are recorded by the gateway in OpenClaw plugin-state SQLite under `state/openclaw.sqlite`. - Existing `msteams-polls.json` files are imported by `openclaw doctor --fix`, not by the running plugin. - The gateway must stay online to record votes. -- Polls do not auto-post result summaries yet, and there is no supported poll-results CLI yet. +- Polls do not auto-post result summaries, and there is no poll-results CLI yet. ## Presentation cards Send semantic presentation payloads to Teams users or conversations using the `message` tool, CLI, or normal reply delivery. OpenClaw renders them as Teams Adaptive Cards from the generic presentation contract. -The `presentation` parameter accepts semantic blocks. When `presentation` is provided, the message text is optional. Buttons render as Adaptive Card submit or URL actions. Select menus are not native in the Teams renderer yet, so OpenClaw downgrades them to readable text before delivery. +The `presentation` parameter accepts semantic blocks. When `presentation` is provided, the message text is optional. Buttons render as Adaptive Card submit or URL actions. Select menus are not native in the Teams renderer, so OpenClaw downgrades them to readable text before delivery. **Agent tool:** @@ -954,12 +923,12 @@ For target format details, see [Target formats](#target-formats) below. MSTeams targets use prefixes to distinguish between users and conversations: -| Target type | Format | Example | -| ------------------- | -------------------------------- | --------------------------------------------------- | -| User (by ID) | `user:` | `user:40a1a0ed-4ff2-4164-a219-55518990c197` | -| User (by name) | `user:` | `user:John Smith` (requires Graph API) | -| Group/channel | `conversation:` | `conversation:19:abc123...@thread.tacv2` | -| Group/channel (raw) | `` | `19:abc123...@thread.tacv2` (if contains `@thread`) | +| Target type | Format | Example | +| ------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------ | +| User (by ID) | `user:` | `user:40a1a0ed-4ff2-4164-a219-55518990c197` | +| User (by name) | `user:` | `user:John Smith` (requires Graph API) | +| Group/channel | `conversation:` | `conversation:19:abc123...@thread.tacv2` | +| Group/channel (raw) | `` | `19:abc123...@thread.tacv2`, `19:...@unq.gbl.spaces`, or a bare `a:`/`8:orgid:`/`29:` Bot Framework id | **CLI examples:** @@ -1007,8 +976,8 @@ Without the `user:` prefix, names default to group or team resolution. Always us ## Proactive messaging -- Proactive messages are only possible **after** a user has interacted, because we store conversation references at that point. -- See `/gateway/configuration` for `dmPolicy` and allowlist gating. +- Proactive messages are only possible **after** a user has interacted, because OpenClaw stores conversation references at that point. +- See [/gateway/configuration](/gateway/configuration) for `dmPolicy` and allowlist gating. ## Team and Channel IDs (Common Gotcha) @@ -1016,7 +985,7 @@ The `groupId` query parameter in Teams URLs is **NOT** the team ID used for conf **Team URL:** -``` +```text https://teams.microsoft.com/l/team/19%3ABk4j...%40thread.tacv2/conversations?groupId=... └────────────────────────────┘ Team conversation ID (URL-decode this) @@ -1024,7 +993,7 @@ https://teams.microsoft.com/l/team/19%3ABk4j...%40thread.tacv2/conversations?gro **Channel URL:** -``` +```text https://teams.microsoft.com/l/channel/19%3A15bc...%40thread.tacv2/ChannelName?groupId=... └─────────────────────────┘ Channel ID (URL-decode this) @@ -1032,15 +1001,15 @@ https://teams.microsoft.com/l/channel/19%3A15bc...%40thread.tacv2/ChannelName?gr **For config:** -- Team key = path segment after `/team/` (URL-decoded, e.g., `19:Bk4j...@thread.tacv2`; older tenants may show `@thread.skype`, which is also valid) -- Channel key = path segment after `/channel/` (URL-decoded) +- Team key = path segment after `/team/` (URL-decoded, e.g., `19:Bk4j...@thread.tacv2`; older tenants may show `@thread.skype`, which is also valid). +- Channel key = path segment after `/channel/` (URL-decoded). - **Ignore** the `groupId` query parameter for OpenClaw routing. It is the Microsoft Entra group ID, not the Bot Framework conversation ID used in incoming Teams activities. ## Private channels Bots have limited support in private channels: -| Feature | Standard Channels | Private Channels | +| Feature | Standard channels | Private channels | | ---------------------------- | ----------------- | ---------------------- | | Bot installation | Yes | Limited | | Real-time messages (webhook) | Yes | May not work | @@ -1048,11 +1017,11 @@ Bots have limited support in private channels: | @mentions | Yes | If bot is accessible | | Graph API history | Yes | Yes (with permissions) | -**Workarounds if private channels don't work:** +**Workarounds if private channels do not work:** -1. Use standard channels for bot interactions -2. Use DMs - users can always message the bot directly -3. Use Graph API for historical access (requires `ChannelMessage.Read.All`) +1. Use standard channels for bot interactions. +2. Use DMs; users can always message the bot directly. +3. Use Graph API for historical access (requires `ChannelMessage.Read.All`). ## Troubleshooting @@ -1061,21 +1030,21 @@ Bots have limited support in private channels: - **Images not showing in channels:** Graph permissions or admin consent missing. Reinstall the Teams app and fully quit/reopen Teams. - **No responses in channel:** mentions are required by default; set `channels.msteams.requireMention=false` or configure per team/channel. - **Version mismatch (Teams still shows old manifest):** remove + re-add the app and fully quit Teams to refresh. -- **401 Unauthorized from webhook:** Expected when testing manually without Azure JWT - means endpoint is reachable but auth failed. Use Azure Web Chat to test properly. +- **401 Unauthorized from webhook:** expected when testing manually without an Azure JWT; means the endpoint is reachable but auth failed. Use Azure Web Chat to test properly. ### Manifest upload errors -- **"Icon file cannot be empty":** The manifest references icon files that are 0 bytes. Create valid PNG icons (32x32 for `outline.png`, 192x192 for `color.png`). -- **"webApplicationInfo.Id already in use":** The app is still installed in another team/chat. Find and uninstall it first, or wait 5-10 minutes for propagation. -- **"Something went wrong" on upload:** Upload via [https://admin.teams.microsoft.com](https://admin.teams.microsoft.com) instead, open browser DevTools (F12) → Network tab, and check the response body for the actual error. -- **Sideload failing:** Try "Upload an app to your org's app catalog" instead of "Upload a custom app" - this often bypasses sideload restrictions. +- **"Icon file cannot be empty":** the manifest references icon files that are 0 bytes. Create valid PNG icons (32x32 for `outline.png`, 192x192 for `color.png`). +- **"webApplicationInfo.Id already in use":** the app is still installed in another team/chat. Find and uninstall it first, or wait 5-10 minutes for propagation. +- **"Something went wrong" on upload:** upload via [https://admin.teams.microsoft.com](https://admin.teams.microsoft.com) instead, open browser DevTools (F12) → Network tab, and check the response body for the actual error. +- **Sideload failing:** try "Upload an app to your org's app catalog" instead of "Upload a custom app"; this often bypasses sideload restrictions. ### RSC permissions not working -1. Verify `webApplicationInfo.id` matches your bot's App ID exactly -2. Re-upload the app and reinstall in the team/chat -3. Check if your org admin has blocked RSC permissions -4. Confirm you're using the right scope: `ChannelMessage.Read.Group` for teams, `ChatMessage.Read.Chat` for group chats +1. Verify `webApplicationInfo.id` matches your bot's App ID exactly. +2. Re-upload the app and reinstall in the team/chat. +3. Check if your org admin has blocked RSC permissions. +4. Confirm you are using the right scope: `ChannelMessage.Read.Group` for teams, `ChatMessage.Read.Chat` for group chats. ## References diff --git a/docs/channels/nextcloud-talk.md b/docs/channels/nextcloud-talk.md index 0d20edf85da6..b0f534142d92 100644 --- a/docs/channels/nextcloud-talk.md +++ b/docs/channels/nextcloud-talk.md @@ -5,50 +5,41 @@ read_when: title: "Nextcloud Talk" --- -Status: bundled plugin (webhook bot). Direct messages, rooms, reactions, and markdown messages are supported. +Nextcloud Talk is a downloadable channel plugin (`@openclaw/nextcloud-talk`) that connects OpenClaw to a self-hosted Nextcloud instance through a Talk webhook bot. Direct messages, rooms, reactions, and markdown messages are supported; media goes out as URLs. -## Bundled plugin - -Nextcloud Talk ships as a bundled plugin in current OpenClaw releases, so -normal packaged builds do not need a separate install. - -If you are on an older build or a custom install that excludes Nextcloud Talk, -install the npm package directly: - -Install via CLI (npm registry): +## Install ```bash openclaw plugins install @openclaw/nextcloud-talk ``` -Use the bare package to follow the current official release tag. Pin an exact -version only when you need a reproducible install. +Use the bare package spec to follow the current official release tag. Pin an exact version only when you need a reproducible install. -Local checkout (when running from a git repo): +From a local checkout (dev workflows): ```bash openclaw plugins install ./path/to/local/nextcloud-talk-plugin ``` -Details: [Plugins](/tools/plugin) +Restart the gateway after installing. Details: [Plugins](/tools/plugin) ## Quick setup (beginner) -1. Ensure the Nextcloud Talk plugin is available. - - Current packaged OpenClaw releases already bundle it. - - Older/custom installs can add it manually with the commands above. +1. Install the plugin (above). 2. On your Nextcloud server, create a bot: ```bash ./occ talk:bot:install "OpenClaw" "" "" --feature webhook --feature response --feature reaction ``` + Keep `--feature response`: without it, outbound replies fail with 401. Repair an existing bot with `./occ talk:bot:state --feature webhook --feature response --feature reaction 1`. + 3. Enable the bot in the target room settings. 4. Configure OpenClaw: - Config: `channels.nextcloud-talk.baseUrl` + `channels.nextcloud-talk.botSecret` - Or env: `NEXTCLOUD_TALK_BOT_SECRET` (default account only) - CLI setup: + CLI setup (`--url`/`--token` are aliases for the explicit fields; `nc-talk` and `nc` work as channel aliases): ```bash openclaw channels add --channel nextcloud-talk \ @@ -92,9 +83,11 @@ Minimal config: ## Notes - Bots cannot initiate DMs. The user must message the bot first. -- Webhook URL must be reachable by the Gateway; set `webhookPublicUrl` if behind a proxy. -- Media uploads are not supported by the bot API; media is sent as URLs. -- The webhook payload does not distinguish DMs vs rooms; set `apiUser` + `apiPassword` to enable room-type lookups (otherwise DMs are treated as rooms). +- The webhook URL must be reachable from the Nextcloud server; set `webhookPublicUrl` when the gateway sits behind a proxy. Webhook requests are HMAC-SHA256 signed with the bot secret; invalid signatures are rejected and rate limited. +- Media uploads are not supported by the bot API; outbound media is appended as an `Attachment: ` line. +- The webhook payload does not distinguish DMs from rooms; set `apiUser` + `apiPassword` to enable room-type lookups (cached about 5 minutes). Without them, every conversation is treated as a room. +- Outbound requests go through the SSRF guard. For a Nextcloud host on a trusted private/internal network, opt in with `channels.nextcloud-talk.network.dangerouslyAllowPrivateNetwork: true`. +- With `apiUser`/`apiPassword` and `webhookPublicUrl` set, `openclaw channels status` probes the bot and warns when the `response` feature is missing. ## Access control (DMs) @@ -103,12 +96,12 @@ Minimal config: - `openclaw pairing list nextcloud-talk` - `openclaw pairing approve nextcloud-talk ` - Public DMs: `channels.nextcloud-talk.dmPolicy="open"` plus `channels.nextcloud-talk.allowFrom=["*"]`. -- `allowFrom` matches Nextcloud user IDs only; display names are ignored. +- `allowFrom` matches Nextcloud user IDs only (lowercased); display names are ignored. ## Rooms (groups) - Default: `channels.nextcloud-talk.groupPolicy = "allowlist"` (mention-gated). -- Allowlist rooms with `channels.nextcloud-talk.rooms`: +- Allowlist rooms with `channels.nextcloud-talk.rooms`, keyed by room token; `"*"` sets a wildcard default: ```json5 { @@ -122,6 +115,7 @@ Minimal config: } ``` +- Per-room keys: `requireMention` (default true), `enabled` (false disables the room), `allowFrom` (per-room sender allowlist), `tools` (allow/deny tool overrides), `skills` (limit loaded skills), `systemPrompt`. - To allow no rooms, keep the allowlist empty or set `channels.nextcloud-talk.groupPolicy="disabled"`. ## Capabilities @@ -143,29 +137,33 @@ Provider options: - `channels.nextcloud-talk.enabled`: enable/disable channel startup. - `channels.nextcloud-talk.baseUrl`: Nextcloud instance URL. -- `channels.nextcloud-talk.botSecret`: bot shared secret. +- `channels.nextcloud-talk.botSecret`: bot shared secret (string or secret reference). - `channels.nextcloud-talk.botSecretFile`: regular-file secret path. Symlinks are rejected. -- `channels.nextcloud-talk.apiUser`: API user for room lookups (DM detection). +- `channels.nextcloud-talk.apiUser`: API user for room lookups (DM detection) and the status probe. - `channels.nextcloud-talk.apiPassword`: API/app password for room lookups. - `channels.nextcloud-talk.apiPasswordFile`: API password file path. - `channels.nextcloud-talk.webhookPort`: webhook listener port (default: 8788). - `channels.nextcloud-talk.webhookHost`: webhook host (default: 0.0.0.0). - `channels.nextcloud-talk.webhookPath`: webhook path (default: /nextcloud-talk-webhook). - `channels.nextcloud-talk.webhookPublicUrl`: externally reachable webhook URL. -- `channels.nextcloud-talk.dmPolicy`: `pairing | allowlist | open | disabled`. -- `channels.nextcloud-talk.allowFrom`: DM allowlist (user IDs). `open` requires `"*"`. -- `channels.nextcloud-talk.groupPolicy`: `allowlist | open | disabled`. -- `channels.nextcloud-talk.groupAllowFrom`: group allowlist (user IDs). -- `channels.nextcloud-talk.rooms`: per-room settings and allowlist. +- `channels.nextcloud-talk.dmPolicy`: `pairing | allowlist | open | disabled` (default: pairing). `open` requires `allowFrom=["*"]`. +- `channels.nextcloud-talk.allowFrom`: DM allowlist (user IDs). +- `channels.nextcloud-talk.groupPolicy`: `allowlist | open | disabled` (default: allowlist). +- `channels.nextcloud-talk.groupAllowFrom`: room sender allowlist (user IDs); falls back to `allowFrom` when unset. +- `channels.nextcloud-talk.rooms`: per-room settings and allowlist (see above). - Static sender access groups can be referenced from `allowFrom` and `groupAllowFrom` with `accessGroup:`. - `channels.nextcloud-talk.historyLimit`: group history limit (0 disables). - `channels.nextcloud-talk.dmHistoryLimit`: DM history limit (0 disables). -- `channels.nextcloud-talk.dms`: per-DM overrides (historyLimit). -- `channels.nextcloud-talk.textChunkLimit`: outbound text chunk size (chars). +- `channels.nextcloud-talk.dms`: per-DM overrides keyed by user ID (`historyLimit`). +- `channels.nextcloud-talk.textChunkLimit`: outbound text chunk size in chars (default: 4000). - `channels.nextcloud-talk.chunkMode`: `length` (default) or `newline` to split on blank lines (paragraph boundaries) before length chunking. - `channels.nextcloud-talk.blockStreaming`: disable block streaming for this channel. - `channels.nextcloud-talk.blockStreamingCoalesce`: block streaming coalesce tuning. +- `channels.nextcloud-talk.responsePrefix`: outbound reply prefix. +- `channels.nextcloud-talk.markdown.tables`: markdown table rendering mode (`off | bullets | code | block`). - `channels.nextcloud-talk.mediaMaxMb`: inbound media cap (MB). +- `channels.nextcloud-talk.network.dangerouslyAllowPrivateNetwork`: allow private/internal Nextcloud hosts past the SSRF guard. +- `channels.nextcloud-talk.accounts.`: per-account overrides (same keys); `defaultAccount` picks the default. Env vars `NEXTCLOUD_TALK_BOT_SECRET` / `NEXTCLOUD_TALK_API_PASSWORD` apply to the default account only. ## Related diff --git a/docs/channels/nostr.md b/docs/channels/nostr.md index 44f0a1702410..e6a361cb9297 100644 --- a/docs/channels/nostr.md +++ b/docs/channels/nostr.md @@ -6,35 +6,23 @@ read_when: title: "Nostr" --- -**Status:** Optional bundled plugin (disabled by default until configured). +Nostr is a downloadable channel plugin (`@openclaw/nostr`) that lets OpenClaw receive and answer NIP-04 encrypted direct messages over Nostr relays. One account per gateway; DMs only. -Nostr is a decentralized protocol for social networking. This channel enables OpenClaw to receive and respond to encrypted direct messages (DMs) via NIP-04. - -## Bundled plugin - -Current OpenClaw releases ship Nostr as a bundled plugin, so normal packaged -builds do not need a separate install. - -### Older/custom installs - -- Onboarding (`openclaw onboard`) and `openclaw channels add` still surface - Nostr from the shared channel catalog. -- If your build excludes bundled Nostr, install the npm package directly. +## Install ```bash openclaw plugins install @openclaw/nostr ``` -Use the bare package to follow the current official release tag. Pin an exact -version only when you need a reproducible install. +Use the bare package spec to follow the current official release tag. Pin an exact version only when you need a reproducible install. -Use a local checkout (dev workflows): +From a local checkout (dev workflows): ```bash openclaw plugins install --link ``` -Restart the Gateway after installing or enabling plugins. +Restart the gateway after installing or enabling plugins. Onboarding (`openclaw onboard`) and `openclaw channels add` surface Nostr from the shared channel catalog once the plugin is installed. ### Non-interactive setup @@ -43,7 +31,7 @@ openclaw channels add --channel nostr --private-key "$NOSTR_PRIVATE_KEY" openclaw channels add --channel nostr --private-key "$NOSTR_PRIVATE_KEY" --relay-urls "wss://relay.damus.io,wss://relay.primal.net" ``` -Use `--use-env` to keep `NOSTR_PRIVATE_KEY` in the environment instead of storing the key in config. +Use `--use-env` to keep `NOSTR_PRIVATE_KEY` in the environment instead of storing the key in config (default account only). ## Quick setup @@ -72,19 +60,19 @@ nak key generate export NOSTR_PRIVATE_KEY="nsec1..." ``` -4. Restart the Gateway. +4. Restart the gateway. ## Configuration reference -| Key | Type | Default | Description | -| ------------ | -------- | ------------------------------------------- | ----------------------------------- | -| `privateKey` | string | required | Private key in `nsec` or hex format | -| `relays` | string[] | `['wss://relay.damus.io', 'wss://nos.lol']` | Relay URLs (WebSocket) | -| `dmPolicy` | string | `pairing` | DM access policy | -| `allowFrom` | string[] | `[]` | Allowed sender pubkeys | -| `enabled` | boolean | `true` | Enable/disable channel | -| `name` | string | - | Display name | -| `profile` | object | - | NIP-01 profile metadata | +| Key | Type | Default | Description | +| ------------ | -------- | ------------------------------------------- | -------------------------------------------------------- | +| `privateKey` | string | required | Private key in `nsec` or hex format; secret refs allowed | +| `relays` | string[] | `['wss://relay.damus.io', 'wss://nos.lol']` | Relay URLs (WebSocket) | +| `dmPolicy` | string | `pairing` | DM access policy | +| `allowFrom` | string[] | `[]` | Allowed sender pubkeys | +| `enabled` | boolean | `true` | Enable/disable channel | +| `name` | string | - | Display name | +| `profile` | object | - | NIP-01 profile metadata | ## Profile metadata @@ -129,8 +117,8 @@ Notes: Enforcement notes: - Inbound event signatures are verified before sender policy and NIP-04 decryption, so forged events are rejected early. -- Pairing replies are sent without processing the original DM body. -- Inbound DMs are rate-limited and oversized payloads are dropped before decrypt. +- Pairing replies are sent without decrypting or processing the original DM body. +- Inbound DMs are rate-limited (globally and per sender) and oversized payloads are dropped before decrypt. ### Allowlist example @@ -206,8 +194,8 @@ docker run -p 7777:7777 ghcr.io/hoytech/strfry ### Manual test -1. Note the bot pubkey (npub) from logs. -2. Open a Nostr client (Damus, Amethyst, etc.). +1. Note the bot pubkey from gateway logs or `openclaw channels status` (hex; convert to npub in your client if needed). +2. Open a Nostr client (Amethyst, Damus, etc.). 3. DM the bot pubkey. 4. Verify the response. @@ -218,7 +206,7 @@ docker run -p 7777:7777 ghcr.io/hoytech/strfry - Verify the private key is valid. - Ensure relay URLs are reachable and use `wss://` (or `ws://` for local). - Confirm `enabled` is not `false`. -- Check Gateway logs for relay connection errors. +- Check gateway logs for relay connection errors. ### Not sending responses diff --git a/docs/channels/pairing.md b/docs/channels/pairing.md index d4d131636031..d6768a1db8a1 100644 --- a/docs/channels/pairing.md +++ b/docs/channels/pairing.md @@ -30,7 +30,7 @@ Pairing codes: - 8 characters, uppercase, no ambiguous chars (`0O1I`). - **Expire after 1 hour**. The bot only sends the pairing message when a new request is created (roughly once per hour per sender). -- Pending DM pairing requests are capped at **3 per channel** by default; additional requests are ignored until one expires or is approved. +- Pending DM pairing requests are capped at **3 per channel account**; additional requests are ignored until one expires or is approved. ### Approve a sender @@ -39,13 +39,15 @@ openclaw pairing list telegram openclaw pairing approve telegram ``` +Add `--notify` to the approve command to tell the requester on the same channel. Multi-account channels take `--account `. + If no command owner is configured yet, approving a DM pairing code also bootstraps `commands.ownerAllowFrom` to the approved sender, such as `telegram:123456789`. That gives first-time setups an explicit owner for privileged commands and exec approval prompts. After an owner exists, later pairing approvals only grant DM access; they do not add more owners. -Supported channels: `discord`, `feishu`, `googlechat`, `imessage`, `irc`, `line`, `matrix`, `mattermost`, `msteams`, `nextcloud-talk`, `nostr`, `openclaw-weixin`, `signal`, `slack`, `synology-chat`, `telegram`, `twitch`, `whatsapp`, `zalo`, `zalouser`. +Supported channels (any installed channel plugin that declares pairing; external plugins such as `openclaw-weixin` can add more): `discord`, `feishu`, `googlechat`, `imessage`, `irc`, `line`, `matrix`, `mattermost`, `msteams`, `nextcloud-talk`, `nostr`, `signal`, `slack`, `sms`, `synology-chat`, `telegram`, `twitch`, `whatsapp`, `zalo`, `zalouser`. ### Reusable sender groups @@ -81,14 +83,14 @@ Access groups are documented in detail here: [Access groups](/channels/access-gr Stored under `~/.openclaw/credentials/`: - Pending requests: `-pairing.json` -- Approved allowlist store: - - Default account: `-allowFrom.json` - - Non-default account: `--allowFrom.json` +- Approved allowlist store: `--allowFrom.json` (approvals for the + default account use `-default-allowFrom.json`) Account scoping behavior: - Non-default accounts read/write only their scoped allowlist file. -- Default account uses the channel-scoped unscoped allowlist file. +- The default account also keeps honoring a legacy unscoped `-allowFrom.json` + file from older installs; entries from both files are merged on read. Treat these as sensitive (they gate access to your assistant). @@ -131,14 +133,16 @@ If you use the `device-pair` plugin, you can do first-time device pairing entire 1. In Telegram, message your bot: `/pair` 2. The bot replies with two messages: an instruction message and a separate **setup code** message (easy to copy/paste in Telegram). 3. On your phone, open the OpenClaw iOS app → Settings → Gateway. -4. Scan the QR code or paste the setup code and connect. +4. Scan the QR code (`/pair qr`) or paste the setup code and connect. 5. The official mobile app connects automatically. If `/pair pending` shows a request, review its role and scopes before approving it. The setup code is a base64-encoded JSON payload that contains: - `url`: the Gateway WebSocket URL (`ws://...` or `wss://...`) -- `bootstrapToken`: a short-lived single-device bootstrap token used for the initial pairing handshake +- `bootstrapToken`: a single-use bootstrap token for the initial pairing handshake (expires after 10 minutes; `expiresAtMs` is included in the payload) + +Run `/pair cleanup` to invalidate unused setup codes once pairing finishes. That bootstrap token carries the built-in pairing bootstrap profile: @@ -209,7 +213,7 @@ approval. Stored under `~/.openclaw/devices/`: -- `pending.json` (short-lived; pending requests expire) +- `pending.json` (short-lived; pending requests expire after 5 minutes) - `paired.json` (paired devices + tokens) ### Notes diff --git a/docs/channels/qa-channel.md b/docs/channels/qa-channel.md index 9439d0275e63..c1133de07279 100644 --- a/docs/channels/qa-channel.md +++ b/docs/channels/qa-channel.md @@ -7,7 +7,7 @@ read_when: - You are iterating on end-to-end QA automation --- -`qa-channel` is a bundled synthetic message transport for automated OpenClaw QA. It is not a production channel - it exists to exercise the same channel plugin boundary used by real transports while keeping state deterministic and fully inspectable. +`qa-channel` is a repo-local synthetic message transport for automated OpenClaw QA (`extensions/qa-channel`, private package, excluded from packaged installs). It is not a production channel - it exists to exercise the same channel plugin boundary used by real transports while keeping state deterministic and fully inspectable. ## What it does @@ -40,18 +40,20 @@ Account keys: - `enabled` - master toggle for this account. - `name` - optional display label. -- `baseUrl` - synthetic bus URL. -- `botUserId` - Matrix-style bot user id used in target grammar. -- `botDisplayName` - display name for outbound messages. -- `pollTimeoutMs` - long-poll wait window. Integer between 100 and 30000. -- `allowFrom` - sender allowlist (user ids or `"*"`). Direct messages and - allowlisted group policy both use these synthetic sender ids. +- `baseUrl` - synthetic bus URL. The account counts as configured once this is set. +- `botUserId` - synthetic bot user id used in target grammar (default: `openclaw`). +- `botDisplayName` - display name for outbound messages (default: `OpenClaw QA`). +- `pollTimeoutMs` - long-poll wait window. Integer between 100 and 30000 (default: 1000). +- `allowFrom` - sender allowlist (user ids or `"*"`; default: `["*"]`). DMs are + always `open` policy; allowlisted group policy also uses these synthetic + sender ids. - `groupPolicy` - shared-room policy: `"open"` (default), `"allowlist"`, or `"disabled"`. - `groupAllowFrom` - optional shared-room sender allowlist. When omitted under `"allowlist"`, QA Channel falls back to `allowFrom`. - `groups..requireMention` - require a bot mention before replying in a - specific group/channel room. `groups."*"` sets the default. + specific group/channel room (default: false). `groups."*"` sets the default; + per-room `tools` / `toolsBySender` set tool policy overrides. - `defaultTo` - fallback target when none is supplied. - `actions.messages` / `actions.reactions` / `actions.search` / `actions.threads` - per-action tool gating. @@ -68,7 +70,7 @@ Host-side self-check (writes a Markdown report under `.artifacts/qa-e2e/`): pnpm qa:e2e ``` -This routes through `qa-lab`, starts the in-repo QA bus, boots the bundled `qa-channel` runtime slice, and runs a deterministic self-check. +This routes through `qa-lab`, starts the in-repo QA bus, boots the `qa-channel` runtime slice, and runs a deterministic self-check. Full repo-backed scenario suite: diff --git a/docs/channels/qqbot.md b/docs/channels/qqbot.md index e01c0d1aff77..3f3c0cdb3197 100644 --- a/docs/channels/qqbot.md +++ b/docs/channels/qqbot.md @@ -7,17 +7,17 @@ read_when: title: QQ bot --- -QQ Bot connects to OpenClaw via the official QQ Bot API (WebSocket gateway). The -plugin supports C2C private chat, group @messages, and guild channel messages with -rich media (images, voice, video, files). +QQ Bot connects to OpenClaw via the official QQ Bot API (WebSocket gateway). +C2C private chat and group `@`-mentions are the primary chat types, with rich +media (images, voice, video, files). Guild channel messages are supported for +text and remote-URL images only; voice, video, file uploads, and local/Base64 +images are not available in guild channels. Reactions and threads are not +supported anywhere. -Status: downloadable plugin. Direct messages, group chats, guild channels, and -media are supported. Reactions and threads are not supported. +Status: official downloadable plugin. ## Install -Install QQ Bot before setup: - ```bash openclaw plugins install @openclaw/qqbot ``` @@ -29,8 +29,9 @@ openclaw plugins install @openclaw/qqbot 2. Click **Create Bot** to create a new QQ bot. 3. Find **AppID** and **AppSecret** on the bot's settings page and copy them. -> AppSecret is not stored in plaintext — if you leave the page without saving it, -> you'll have to regenerate a new one. + +AppSecret is not stored in plaintext. If you leave the page without saving it, you'll have to regenerate a new one. + 4. Add the channel: @@ -40,13 +41,17 @@ openclaw channels add --channel qqbot --token "AppID:AppSecret" 5. Restart the Gateway. -Interactive setup paths: +Interactive setup: ```bash openclaw channels add -openclaw configure --section channels ``` +The wizard also offers QR-code binding as an alternative to typing AppID/AppSecret +manually: scan the code with the phone app tied to the target QQ Bot to complete +binding. OpenClaw persists the returned credentials under the account's config +scope. + ## Configure Minimal config: @@ -63,7 +68,7 @@ Minimal config: } ``` -Default-account env vars: +Default-account env vars (top-level account only): - `QQBOT_APP_ID` - `QQBOT_CLIENT_SECRET` @@ -98,12 +103,24 @@ Env SecretRef AppSecret: Notes: -- Env fallback applies to the default QQ Bot account only. -- `openclaw channels add --channel qqbot --token-file ...` provides the - AppSecret only; the AppID must already be set in config or `QQBOT_APP_ID`. -- `clientSecret` also accepts SecretRef input, not just a plaintext string. -- Legacy `secretref:/...` marker strings are not valid `clientSecret` values; - use structured SecretRef objects like the example above. +- `openclaw channels add --channel qqbot --token-file ...` sets the AppSecret + only; `appId` must already be set in config or `QQBOT_APP_ID`. +- `clientSecret` accepts a plaintext string, a file path (`clientSecretFile`), + or a structured SecretRef object. +- Legacy `secretref:...` / `secretref-env:...` marker strings are rejected for + `clientSecret`; use a structured SecretRef object instead. + +### Access policy + +- `allowFrom` / `groupAllowFrom` gate who can chat with the bot in C2C / + group contexts. `dmPolicy` / `groupPolicy` (`open` | `allowlist` | `disabled`) + control the enforcement mode. `dmPolicy` defaults to `allowlist` once + `allowFrom` has a concrete (non-wildcard) entry, otherwise `open`. + `groupPolicy` defaults to `allowlist` once either `groupAllowFrom` or + `allowFrom` has a concrete entry, otherwise `open`. +- "Auth: allowlist" slash commands require an explicit non-wildcard entry in + `allowFrom` (or `groupAllowFrom` for group invocations) regardless of + `dmPolicy` / `groupPolicy` — see [Slash commands](#slash-commands). ### Multi-account setup @@ -128,8 +145,9 @@ Run multiple QQ bots under a single OpenClaw instance: } ``` -Each account launches its own WebSocket connection and maintains an independent -token cache (isolated by `appId`). +Each account owns an isolated WebSocket connection, API client, and token +cache, keyed by `appId`. Log lines are tagged with the owning account id so +diagnostics stay separable when you run several bots under one Gateway. Add a second bot via CLI: @@ -139,8 +157,8 @@ openclaw channels add --channel qqbot --account bot2 --token "222222222:secret-o ### Group chats -QQ Bot group chat support uses QQ group OpenIDs, not display names. Add the bot -to a group, then mention it or configure the group to run without a mention. +Group support uses QQ group OpenIDs, not display names. Add the bot to a +group, then mention it or configure the group to run without a mention. ```json5 { @@ -169,31 +187,27 @@ to a group, then mention it or configure the group to run without a mention. } ``` -`groups["*"]` sets defaults for every group, and a concrete -`groups.GROUP_OPENID` entry overrides those defaults for one group. Group -settings include: +`groups["*"]` sets defaults for every group; a concrete `groups.GROUP_OPENID` +entry overrides those defaults for one group. Group settings: -- `requireMention`: require an @mention before the bot replies. Default: `true`. -- `commandLevel`: control which built-in slash commands can run in groups. - Default: `all`, which preserves the pre-existing QQBot group behavior when the - setting is omitted. -- `ignoreOtherMentions`: drop messages that mention someone else but not the bot. -- `historyLimit`: keep recent non-mention group messages as context for the next mentioned turn. Set `0` to disable. -- `tools`: allow/deny tools for the whole group. -- `toolsBySender`: per-sender group tool overrides; see [Groups](/channels/groups#groupchannel-tool-restrictions-optional). -- `name`: friendly label used in logs and group context. -- `prompt`: per-group behavior prompt appended to the agent context. +| Field | Default | Description | +| --------------------- | ---------------- | -------------------------------------------------------------------------------------------------- | +| `requireMention` | `true` | Require an `@`-mention before the bot replies. | +| `commandLevel` | `all` | Which built-in slash commands can run in the group (see below). | +| `ignoreOtherMentions` | `false` | Drop messages that mention someone else but not the bot. | +| `historyLimit` | `50` | Recent non-mention messages kept as context for the next mentioned turn. `0` disables history. | +| `tools` | — | Allow/deny tools for the whole group. | +| `toolsBySender` | — | Per-sender tool overrides; see [Groups](/channels/groups#groupchannel-tool-restrictions-optional). | +| `name` | openid prefix | Friendly label used in logs and group context. | +| `prompt` | built-in default | Per-group behavior prompt appended to the agent context. | `commandLevel` accepts: -- `all`: keep recognized built-in commands available as before. Some commands may - stay hidden from menus, but authorized users can still run them in the group. -- `safety`: allow common collaboration commands such as `/help`, `/btw`, and - `/stop`; ask users to run sensitive commands such as `/config`, `/tools`, and - `/bash` in private chat. -- `strict`: only allow the group-session controls needed for strict group - operation. `/stop` still stays urgent so an authorized sender can interrupt an - active run. +| Level | Behavior | +| -------- | --------------------------------------------------------------------------------------------------------------------------------------------- | +| `all` | Existing built-in commands stay available. Some stay hidden from menus but authorized users can still run them in the group. | +| `safety` | `/help`, `/btw`, `/stop` stay visible in the group; sensitive commands (`/config`, `/tools`, `/bash`, etc.) must be run in private chat. | +| `strict` | Only group-session controls needed for strict operation are allowed. `/stop` still works so an authorized sender can interrupt an active run. | Old QQBot `toolPolicy` entries are retired. Run `openclaw doctor --fix` to migrate them to `tools`. @@ -201,9 +215,10 @@ Activation modes are `mention` and `always`. `requireMention: true` maps to `mention`; `requireMention: false` maps to `always`. A session-level activation override, when present, wins over config. -The inbound queue is per peer. Group peers get a larger queue cap, keep human -messages ahead of bot-authored chatter when full, and merge bursts of normal -group messages into one attributed turn. Slash commands still run one by one. +The inbound queue is per peer. Group peers get a larger queue cap (50 vs. 20 +for direct peers), evict bot-authored messages before human ones when full, +and merge bursts of normal group messages into one attributed turn. Slash +commands run one by one, independent of any merge batch. ### Voice (STT / TTS) @@ -241,14 +256,13 @@ STT and TTS support two-level configuration with priority fallback: } ``` -Set `enabled: false` on either to disable. -Account-level TTS overrides use the same shape as `messages.tts` and deep-merge -over the channel/global TTS config. +Set `enabled: false` on either to disable. Account-level TTS overrides use the +same shape as `messages.tts` and deep-merge over channel/global TTS config. -Inbound QQ voice attachments are exposed to agents as audio media metadata while -keeping raw voice files out of generic `MediaPaths`. `[[audio_as_voice]]` plain -text replies synthesize TTS and send a native QQ voice message when TTS is -configured. +Inbound QQ voice attachments are exposed to agents as audio media metadata +while keeping raw voice files out of generic `MediaPaths`. `[[audio_as_voice]]` +in a plain-text reply synthesizes TTS and sends a native QQ voice message when +TTS is configured. Outbound audio upload/transcode behavior can also be tuned with `channels.qqbot.audioFormatPolicy`: @@ -265,66 +279,76 @@ Outbound audio upload/transcode behavior can also be tuned with | `qqbot:group:GROUP_OPENID` | Group chat | | `qqbot:channel:CHANNEL_ID` | Guild channel | -> Each bot has its own set of user OpenIDs. An OpenID received by Bot A **cannot** -> be used to send messages via Bot B. + +Each bot has its own set of user OpenIDs. An OpenID received by Bot A **cannot** be used to send messages via Bot B. + ## Slash commands Built-in commands intercepted before the AI queue: -| Command | Description | -| -------------- | -------------------------------------------------------------------------------------------------------- | -| `/bot-ping` | Latency test | -| `/bot-version` | Show the OpenClaw framework version | -| `/bot-help` | List all commands | -| `/bot-me` | Show the sender's QQ user ID (openid) for `allowFrom`/`groupAllowFrom` setup | -| `/bot-upgrade` | Show the QQBot upgrade guide link | -| `/bot-logs` | Export recent gateway logs as a file | -| `/bot-approve` | Approve a pending QQ Bot action (for example, confirming a C2C or group upload) through the native flow. | +| Command | Auth | Scope | Description | +| -------------------- | --------- | ------------ | ------------------------------------------------------------------------------ | +| `/bot-ping` | — | any | Latency test | +| `/bot-help` | — | any | List all commands | +| `/bot-me` | — | private only | Show the sender's QQ user ID (openid) for `allowFrom` / `groupAllowFrom` setup | +| `/bot-version` | — | private only | Show the OpenClaw framework version and plugin version | +| `/bot-upgrade` | — | private only | Show the QQBot upgrade guide link | +| `/bot-approve` | allowlist | private only | Manage command-execution approval config (on / off / always / reset / status) | +| `/bot-logs` | allowlist | private only | Export recent gateway logs as a file | +| `/bot-clear-storage` | allowlist | private only | Delete cached downloads under the QQBot media directory | +| `/bot-streaming` | allowlist | private only | Toggle C2C streaming replies | +| `/bot-group-allways` | allowlist | private only | Toggle the default group activation mode (mention-required vs. always-on) | Append `?` to any command for usage help (for example `/bot-upgrade ?`). -Admin commands (`/bot-me`, `/bot-upgrade`, `/bot-logs`, `/bot-clear-storage`, `/bot-streaming`, `/bot-approve`) are direct-message-only and require the sender's openid in an explicit non-wildcard `allowFrom` list. A wildcard `allowFrom: ["*"]` permits chat but does not grant admin command access. Group messages match against `groupAllowFrom` first and fall back to `allowFrom`. Running an admin command in a group returns a hint rather than silently dropping. +"Auth: allowlist" commands additionally require the sender's openid in an +explicit non-wildcard `allowFrom` list (`groupAllowFrom` takes precedence for +group-issued commands, falling back to `allowFrom`). A wildcard +`allowFrom: ["*"]` permits chat but not these commands. Running one of them +outside private chat, or without authorization, returns a hint rather than +silently dropping the message. + +`/bot-me`, `/bot-version`, and `/bot-upgrade` are private-chat-only but do not +require the allowlist — any C2C sender can run them. When QQ Bot exec approvals use the default same-chat fallback, native approval -button clicks follow the same explicit non-wildcard command allowlist. To grant -approval-only access without broader command access, configure -`channels.qqbot.execApprovals.approvers`. +button clicks follow the same explicit non-wildcard command allowlist. To +grant approval-only access without broader command access, configure +`channels.qqbot.execApprovals.approvers`. Native exec approvals are enabled by +default. -## Engine architecture +## Media and storage -QQ Bot ships as a self-contained engine inside the plugin: - -- Each account owns an isolated resource stack (WebSocket connection, API client, token cache, media storage root) keyed by `appId`. Accounts never share inbound/outbound state. -- The multi-account logger tags log lines with the owning account so diagnostics stay separable when you run several bots under one gateway. -- Inbound, outbound, and gateway bridge paths share a single media payload root under `~/.openclaw/media`, so uploads, downloads, and transcode caches land under one guarded directory instead of a per-subsystem tree. -- Rich media delivery goes through one `sendMedia` path for C2C and group targets. Local files and buffers above the large-file threshold use QQ's chunked upload endpoints, while smaller payloads use the one-shot media API. -- Credentials can be backed up and restored as part of standard OpenClaw credential snapshots; the engine re-attaches each account's resource stack on restore without requiring a fresh QR-code pair. - -## QR-code onboarding - -As an alternative to pasting `AppID:AppSecret` manually, the engine supports a QR-code onboarding flow for linking a QQ Bot to OpenClaw: - -1. Run the QQ Bot setup path (for example `openclaw channels add --channel qqbot`) and pick the QR-code flow when prompted. -2. Scan the generated QR code with the phone app tied to the target QQ Bot. -3. Approve the pairing on the phone. OpenClaw persists the returned credentials into `credentials/` under the right account scope. - -Approval prompts generated by the bot itself (for example, "allow this action?" flows exposed by the QQ Bot API) surface as native OpenClaw prompts that you can accept with `/bot-approve` rather than replying through the raw QQ client. +- Inbound, outbound, and gateway-bridge media share one payload root under + `~/.openclaw/media/qqbot` (honoring `OPENCLAW_HOME` when set), so uploads, + downloads, and transcode caches stay under one guarded directory. +- Rich media delivery for C2C and group targets goes through one `sendMedia` + path. Local files and in-memory buffers of 5 MiB or more use QQ's + chunked upload endpoints; smaller payloads and remote-URL/Base64 sources use + the one-shot upload API. +- If a hot upgrade interrupts the Gateway before it finishes writing + `openclaw.json`, the plugin restores the last-known `appId` / `clientSecret` + for that account from an internal snapshot on the next start (never + overwriting an intentional config change), so re-scanning the QR code is not + required. ## Troubleshooting -- **Bot replies "gone to Mars":** credentials not configured or Gateway not started. -- **No inbound messages:** verify `appId` and `clientSecret` are correct, and the - bot is enabled on the QQ Open Platform. -- **Repeated self-replies:** OpenClaw records QQ outbound ref indexes as - bot-authored and ignores inbound events whose current `msgIdx` matches that - same bot account. This prevents platform echo loops while still allowing users - to quote or reply to previous bot messages. -- **Setup with `--token-file` still shows unconfigured:** `--token-file` only sets - the AppSecret. You still need `appId` in config or `QQBOT_APP_ID`. -- **Proactive messages not arriving:** QQ may intercept bot-initiated messages if - the user hasn't interacted recently. -- **Voice not transcribed:** ensure STT is configured and the provider is reachable. +- **Gateway does not start / no inbound messages:** verify `appId` and + `clientSecret` are correct and the bot is enabled on the QQ Open Platform. + A missing credential surfaces as "QQBot not configured (missing appId or + clientSecret)". +- **Setup with `--token-file` still shows unconfigured:** `--token-file` only + sets the AppSecret. `appId` must still be set in config or `QQBOT_APP_ID`. +- **Bursty group replies collide:** the inbound queue evicts bot-authored + messages ahead of human ones when a peer's queue fills up, and merges + bursts of normal (non-command) group messages into one attributed turn, so + a flood of bot chatter should not starve human messages. +- **Proactive messages not arriving:** QQ may block bot-initiated messages if + the user has not interacted recently. +- **Voice not transcribed:** ensure STT is configured and the provider is + reachable. ## Related diff --git a/docs/channels/raft.md b/docs/channels/raft.md index 8a06b2c2fc53..248ded1350cd 100644 --- a/docs/channels/raft.md +++ b/docs/channels/raft.md @@ -8,9 +8,9 @@ title: "Raft" sidebarTitle: "Raft" --- -Raft support connects an OpenClaw agent to a Raft External Agent through the local -Raft CLI. Raft sends authenticated wake hints to the Gateway. The agent then uses -the Raft CLI to check and send messages. +Raft connects an OpenClaw agent to a Raft External Agent through the local +Raft CLI. Raft sends authenticated wake hints to the Gateway; the agent then +uses the Raft CLI to check and send messages. Direct chat only (no groups). ## Install @@ -26,11 +26,13 @@ Details: [Plugins](/tools/plugin) ## Prerequisites - A Raft workspace with an External Agent. -- The Raft CLI installed on the same host as the OpenClaw Gateway. -- A Raft CLI profile that is already signed in and associated with that External Agent. +- The Raft CLI installed on the same host as the OpenClaw Gateway, on the + service's `PATH`. +- A Raft CLI profile that is already signed in and associated with that + External Agent. -The plugin does not store Raft credentials. The Raft CLI keeps that authentication -in its own profile. +The plugin does not store Raft credentials; the Raft CLI keeps that +authentication in its own profile. ## Configure @@ -73,28 +75,32 @@ Use a named account when one Gateway connects to more than one Raft External Age } ``` -The interactive setup flow records the same profile: +Interactive setup records the same profile: ```bash -openclaw channels setup raft +openclaw channels add --channel raft ``` -## How It Works +## How it works When the Gateway starts, the plugin: 1. Opens a loopback-only HTTP wake endpoint on an ephemeral port. 2. Starts `raft --profile agent bridge` with that endpoint and a per-process token. -3. Accepts only authenticated, content-free wake hints with a replay identity from the local bridge. -4. Requires one of `eventId`, `attemptId`, `messageId`, `delivery_id`, `wake_id`, or `id`. -5. Deduplicates recent retried wake deliveries by bridge event id, including across Gateway restarts. -6. Returns a stable runtime session for the current bridge and an empty activity-drain batch for the Raft CLI protocol. -7. Starts one serialized OpenClaw agent turn for each accepted wake. +3. Accepts only authenticated, content-free wake hints with a replay identity + from the local bridge. +4. Requires one of `eventId`, `attemptId`, `messageId`, `delivery_id`, + `wake_id`, or `id` on every wake payload. +5. Deduplicates retried wake deliveries by bridge event id for 24 hours, + including across Gateway restarts. +6. Returns a stable runtime session for the current bridge and an empty + activity-drain batch for the Raft CLI protocol. +7. Starts one serialized OpenClaw agent turn per accepted wake. -The bridge owns Raft delivery retries and reconnects. The OpenClaw turn receives -only a wake notice, not a copied Raft message body. It uses the CLI to read -pending messages and to send its response: +The bridge owns Raft delivery retries and reconnects. The OpenClaw turn +receives only a wake notice, not a copied Raft message body. It uses the CLI +to read pending messages and to send its response: ```bash raft --profile openclaw message check @@ -102,9 +108,7 @@ raft --profile openclaw message send ``` -Raft is not a normal push-message transport. OpenClaw does not automatically -send the model's final text back through the bridge, so the agent must use the -Raft CLI after processing a wake. +Raft is not a push-message transport. OpenClaw does not automatically send the model's final text back through the bridge, so the agent must use the Raft CLI after processing a wake. ## Verify @@ -116,9 +120,9 @@ openclaw channels status --probe openclaw plugins inspect raft --runtime --json ``` -Then send a message to the Raft External Agent. The Gateway log should show the -Raft bridge starting, followed by an inbound wake. The agent should use the -configured Raft profile to check its pending messages. +Then send a message to the Raft External Agent. The Gateway log should show +the Raft bridge starting, followed by an inbound wake. The agent should use +the configured Raft profile to check its pending messages. ## Troubleshooting @@ -135,8 +139,8 @@ configured Raft profile to check its pending messages. This is expected when the agent does not invoke the Raft CLI. The wake bridge does not carry message bodies or automatic final replies. Check the - agent's tool policy and ensure it can run `raft --profile message - check` and `message send`. + agent's tool policy and ensure it can run `raft --profile + message check` and `message send`.
diff --git a/docs/channels/signal.md b/docs/channels/signal.md index beec7080fb74..d35389fbe92c 100644 --- a/docs/channels/signal.md +++ b/docs/channels/signal.md @@ -6,32 +6,51 @@ read_when: title: "Signal" --- -Status: external CLI integration. Gateway talks to `signal-cli` over HTTP — either native daemon (JSON-RPC + SSE) or bbernhard/signal-cli-rest-api container (REST + WebSocket). +Signal is a downloadable channel plugin (`@openclaw/signal`). The gateway talks to `signal-cli` over HTTP: either the native daemon (JSON-RPC + SSE) or the [bbernhard/signal-cli-rest-api](https://github.com/bbernhard/signal-cli-rest-api) container (REST + WebSocket). OpenClaw does not embed libsignal. -## Prerequisites +## The number model (read this first) -- OpenClaw installed on your server (Linux flow below tested on Ubuntu 24). -- One of: - - `signal-cli` available on the host (native mode), **or** - - `bbernhard/signal-cli-rest-api` Docker container (container mode). -- A phone number that can receive one verification SMS (for SMS registration path). -- Browser access for Signal captcha (`signalcaptchas.org`) during registration. +- The gateway connects to a **Signal device**: the `signal-cli` account. +- Running the bot on **your personal Signal account** makes it ignore your own messages (loop protection). +- For "I text the bot and it replies," use a **separate bot number**. -## Quick setup (beginner) - -1. Use a **separate Signal number** for the bot (recommended). -2. Install the OpenClaw plugin: +## Install ```bash openclaw plugins install @openclaw/signal ``` -3. Install `signal-cli` (Java required if you use the JVM build). -4. Choose one setup path: - - **Path A (QR link):** `signal-cli link -n "OpenClaw"` and scan with Signal. - - **Path B (SMS register):** register a dedicated number with captcha + SMS verification. -5. Configure OpenClaw and restart the gateway. -6. Send a first DM and approve pairing (`openclaw pairing approve signal `). +Bare plugin specs try ClawHub first, then npm fallback. Force a source with `openclaw plugins install clawhub:@openclaw/signal` or `npm:@openclaw/signal`. `plugins install` registers and enables the plugin; no separate `enable` step is needed. See [Plugins](/tools/plugin) for general install rules. + +## Quick setup + + + + Use a **separate Signal number** for the bot (recommended). + + + ```bash + openclaw plugins install @openclaw/signal + ``` + + + ```bash + openclaw channels add + ``` + The wizard detects whether `signal-cli` is on `PATH` and, when missing, offers to install it: downloads the official native GraalVM build on Linux x86-64, or installs via Homebrew on macOS and other architectures. It then prompts for the bot number and `signal-cli` path. + + + - **QR link (fastest):** `signal-cli link -n "OpenClaw"`, then scan with Signal. See [Path A](#setup-path-a-link-existing-signal-account-qr). + - **SMS registration:** dedicated number with captcha + SMS verification. See [Path B](#setup-path-b-register-dedicated-bot-number-sms-linux). + + + + ```bash + openclaw gateway call channels.status --params '{"probe":true}' + ``` + Send a first DM and approve pairing: `openclaw pairing approve signal `. + + Minimal config: @@ -49,8 +68,6 @@ Minimal config: } ``` -Field reference: - | Field | Description | | ------------ | ------------------------------------------------- | | `account` | Bot phone number in E.164 format (`+15551234567`) | @@ -59,61 +76,25 @@ Field reference: | `dmPolicy` | DM access policy (`pairing` recommended) | | `allowFrom` | Phone numbers or `uuid:` values allowed to DM | +Multi-account support: use `channels.signal.accounts` with per-account config and optional `name`. See [Multi-account channels](/gateway/config-channels#multi-account-all-channels) for the shared pattern. + ## What it is -- Signal channel via `signal-cli` (not embedded libsignal). - Deterministic routing: replies always go back to Signal. - DMs share the agent's main session; groups are isolated (`agent::signal:group:`). - -## Config writes - -By default, Signal is allowed to write config updates triggered by `/config set|unset` (requires `commands.config: true`). - -Disable with: - -```json5 -{ - channels: { signal: { configWrites: false } }, -} -``` - -## The number model (important) - -- The gateway connects to a **Signal device** (the `signal-cli` account). -- If you run the bot on **your personal Signal account**, it will ignore your own messages (loop protection). -- For "I text the bot and it replies," use a **separate bot number**. +- By default, Signal may write config updates triggered by `/config set|unset` (requires `commands.config: true`). Disable with `channels.signal.configWrites: false`. ## Setup path A: link existing Signal account (QR) -1. Install `signal-cli` (JVM or native build). -2. Link a bot account: - - `signal-cli link -n "OpenClaw"` then scan the QR in Signal. +1. Install `signal-cli` (JVM or native build), or let `openclaw channels add` install it for you. +2. Link a bot account: `signal-cli link -n "OpenClaw"`, then scan the QR in Signal. 3. Configure Signal and start the gateway. -Example: - -```json5 -{ - channels: { - signal: { - enabled: true, - account: "+15551234567", - cliPath: "signal-cli", - dmPolicy: "pairing", - allowFrom: ["+15557654321"], - }, - }, -} -``` - -Multi-account support: use `channels.signal.accounts` with per-account config and optional `name`. See [`gateway/configuration`](/gateway/config-channels#multi-account-all-channels) for the shared pattern. - ## Setup path B: register dedicated bot number (SMS, Linux) -Use this when you want a dedicated bot number instead of linking an existing Signal app account. +Use this for a dedicated bot number instead of linking an existing Signal app account. The flow below is tested on Ubuntu 24. -1. Get a number that can receive SMS (or voice verification for landlines). - - Use a dedicated bot number to avoid account/session conflicts. +1. Get a number that can receive SMS (or voice verification for landlines). A dedicated bot number avoids account/session conflicts. 2. Install `signal-cli` on the gateway host: ```bash @@ -124,8 +105,7 @@ sudo ln -sf /opt/signal-cli /usr/local/bin/ signal-cli --version ``` -If you use the JVM build (`signal-cli-${VERSION}.tar.gz`), install JRE 25+ first. -Keep `signal-cli` updated; upstream notes that old releases can break as Signal server APIs change. +If you use the JVM build (`signal-cli-${VERSION}.tar.gz`), install a JRE first. Keep `signal-cli` updated; upstream notes old releases can break as Signal server APIs change. 3. Register and verify the number: @@ -133,19 +113,19 @@ Keep `signal-cli` updated; upstream notes that old releases can break as Signal signal-cli -a + register ``` -If captcha is required: +If captcha is required (browser access is needed to complete this step): 1. Open `https://signalcaptchas.org/registration/generate.html`. -2. Complete captcha, copy the `signalcaptcha://...` link target from "Open Signal". -3. Run from the same external IP as the browser session when possible. -4. Run registration again immediately (captcha tokens expire quickly): +2. Complete the captcha, copy the `signalcaptcha://...` link target from "Open Signal". +3. Run from the same external IP as the browser session when possible (captcha tokens expire quickly). +4. Register and verify immediately: ```bash signal-cli -a + register --captcha '' signal-cli -a + verify ``` -4. Configure OpenClaw, restart gateway, verify channel: +4. Configure OpenClaw, restart the gateway, verify the channel: ```bash # If you run the gateway as a user systemd service: @@ -158,11 +138,11 @@ openclaw channels status --probe 5. Pair your DM sender: - Send any message to the bot number. - - Approve code on the server: `openclaw pairing approve signal `. + - Approve on the server: `openclaw pairing approve signal `. - Save the bot number as a contact on your phone to avoid "Unknown contact". -Registering a phone number account with `signal-cli` can de-authenticate the main Signal app session for that number. Prefer a dedicated bot number, or use QR link mode if you need to keep your existing phone app setup. +Registering a phone number account with `signal-cli` can de-authenticate the main Signal app session for that number. Prefer a dedicated bot number, or use QR link mode to keep your existing phone app setup. Upstream references: @@ -173,7 +153,7 @@ Upstream references: ## External daemon mode (httpUrl) -If you want to manage `signal-cli` yourself (slow JVM cold starts, container init, or shared CPUs), run the daemon separately and point OpenClaw at it: +To manage `signal-cli` yourself (slow JVM cold starts, container init, shared CPUs), run the daemon separately and point OpenClaw at it: ```json5 { @@ -186,11 +166,11 @@ If you want to manage `signal-cli` yourself (slow JVM cold starts, container ini } ``` -This skips auto-spawn and the startup wait inside OpenClaw. For slow starts when auto-spawning, set `channels.signal.startupTimeoutMs`. +This skips auto-spawn and OpenClaw's startup wait. For slow auto-spawned starts, set `channels.signal.startupTimeoutMs`. ## Container mode (bbernhard/signal-cli-rest-api) -Instead of running `signal-cli` natively, you can use the [bbernhard/signal-cli-rest-api](https://github.com/bbernhard/signal-cli-rest-api) Docker container. This wraps `signal-cli` behind a REST API and WebSocket interface. +Instead of running `signal-cli` natively, use the [bbernhard/signal-cli-rest-api](https://github.com/bbernhard/signal-cli-rest-api) Docker container, which wraps `signal-cli` behind a REST + WebSocket interface. Requirements: @@ -226,7 +206,7 @@ OpenClaw config: } ``` -The `apiMode` field controls which protocol OpenClaw uses: +`apiMode` controls which protocol OpenClaw uses: | Value | Behavior | | ------------- | ------------------------------------------------------------------------------------ | @@ -234,15 +214,15 @@ The `apiMode` field controls which protocol OpenClaw uses: | `"native"` | Force native signal-cli (JSON-RPC at `/api/v1/rpc`, SSE at `/api/v1/events`) | | `"container"` | Force bbernhard container (REST at `/v2/send`, WebSocket at `/v1/receive/{account}`) | -When `apiMode` is `"auto"`, OpenClaw caches the detected mode for 30 seconds to avoid repeated probes. Container receive is only selected for streaming after `/v1/receive/{account}` upgrades to WebSocket, which requires `MODE=json-rpc`. +When `apiMode` is `"auto"`, OpenClaw caches the detected mode for 30 seconds per daemon URL to avoid repeated probes (native wins when both transports are healthy). Container receive is only selected for streaming after `/v1/receive/{account}` upgrades to WebSocket, which requires `MODE=json-rpc`. -Container mode supports the same Signal channel operations as native mode where the container exposes matching APIs: sends, receives, attachments, typing indicators, read/viewed receipts, reactions, groups, and styled text. OpenClaw translates its native Signal RPC calls into the container's REST payloads, including `group.{base64(internal_id)}` group IDs and `text_mode: "styled"` for formatted text. +Container mode supports the same Signal operations as native mode where the container exposes matching APIs: sends, receives, attachments, typing indicators, read/viewed receipts, reactions, groups, and styled text. OpenClaw translates native Signal RPC calls into the container's REST payloads, including `group.{base64(internal_id)}` group IDs and `text_mode: "styled"` for formatted text. Operational notes: -- Use `autoStart: false` with container mode. OpenClaw should not spawn a native daemon when `apiMode: "container"` is selected. -- Use `MODE=json-rpc` for receiving. `MODE=normal` can make `/v1/about` look healthy, but `/v1/receive/{account}` does not WebSocket-upgrade, so OpenClaw will not select container receive streaming in `auto` mode. -- Set `apiMode: "container"` when you know the `httpUrl` points at bbernhard's REST API. Set `apiMode: "native"` when you know it points at native `signal-cli` JSON-RPC/SSE. Use `"auto"` when the deployment may vary. +- Use `autoStart: false` with container mode; OpenClaw should not spawn a native daemon when `apiMode: "container"` is selected. +- Use `MODE=json-rpc` for receiving. `MODE=normal` can make `/v1/about` look healthy, but `/v1/receive/{account}` will not WebSocket-upgrade, so OpenClaw will not select container receive streaming in `auto` mode. +- Set `apiMode: "container"` when `httpUrl` points at the bbernhard REST API, `"native"` when it points at native `signal-cli` JSON-RPC/SSE, and `"auto"` when the deployment may vary. - Container attachment downloads honor the same media byte limits as native mode. Oversized responses are rejected before being fully buffered when the server sends `Content-Length`, and while streaming otherwise. ## Access control (DMs + groups) @@ -250,10 +230,8 @@ Operational notes: DMs: - Default: `channels.signal.dmPolicy = "pairing"`. -- Unknown senders receive a pairing code; messages are ignored until approved (codes expire after 1 hour). -- Approve via: - - `openclaw pairing list signal` - - `openclaw pairing approve signal ` +- Unknown senders get a pairing code; messages are ignored until approved (codes expire after 1 hour). +- Approve via `openclaw pairing list signal` and `openclaw pairing approve signal `. - Pairing is the default token exchange for Signal DMs. Details: [Pairing](/channels/pairing) - UUID-only senders (from `sourceUuid`) are stored as `uuid:` in `channels.signal.allowFrom`. @@ -263,7 +241,7 @@ Groups: - `channels.signal.groupAllowFrom` controls which groups or senders can trigger group replies when `allowlist` is set; entries can be Signal group IDs (raw, `group:`, or `signal:group:`), sender phone numbers, `uuid:` values, or `*`. - `channels.signal.groups["" | "*"]` can override group behavior with `requireMention`, `tools`, and `toolsBySender`. - Use `channels.signal.accounts..groups` for per-account overrides in multi-account setups. -- Allowlisting a Signal group through `groupAllowFrom` does not disable mention gating by itself. A specifically configured `channels.signal.groups[""]` entry processes every group message unless `requireMention=true` is set. +- Allowlisting a group through `groupAllowFrom` does not disable mention gating by itself. A specifically configured `channels.signal.groups[""]` entry processes every group message unless `requireMention: true` is explicitly set. - Runtime note: if `channels.signal` is completely missing, runtime falls back to `groupPolicy="allowlist"` for group checks (even if `channels.defaults.groupPolicy` is set). ## How it works (behavior) @@ -277,7 +255,7 @@ Groups: - Outbound text is chunked to `channels.signal.textChunkLimit` (default 4000). - Optional newline chunking: set `channels.signal.chunkMode="newline"` to split on blank lines (paragraph boundaries) before length chunking. -- Attachments supported (base64 fetched from `signal-cli`). +- Attachments are supported (base64 fetched from `signal-cli`). - Voice-note attachments use the `signal-cli` filename as a MIME fallback when `contentType` is missing, so audio transcription can still classify AAC voice memos. - Default media cap: `channels.signal.mediaMaxMb` (default 8). - Use `channels.signal.ignoreAttachments` to skip downloading media. @@ -287,36 +265,25 @@ Groups: - **Typing indicators**: OpenClaw sends typing signals via `signal-cli sendTyping` and refreshes them while a reply is running. - **Read receipts**: when `channels.signal.sendReadReceipts` is true, OpenClaw forwards read receipts for allowed DMs. -- Signal-cli does not expose read receipts for groups. +- `signal-cli` does not expose read receipts for groups. ## Lifecycle status reactions -Set `messages.statusReactions.enabled: true` to let Signal show the shared -queued/thinking/tool/compaction/done/error reaction lifecycle on inbound turns. -Signal uses the inbound message timestamp as the reaction target; group -reactions are sent with the Signal group id plus the original sender as the -target author. +Set `messages.statusReactions.enabled: true` to let Signal show the shared queued/thinking/tool/compaction/done/error reaction lifecycle on inbound turns. Signal uses the inbound message timestamp as the reaction target; group reactions are sent with the Signal group ID plus the original sender as the target author. -Status reactions also require an ack reaction and a matching -`messages.ackReactionScope` (`direct`, `group-all`, `group-mentions`, or `all`). -Set `channels.signal.reactionLevel: "off"` to disable Signal status reactions. -The message-tool `react` action remains stricter: it requires -`reactionLevel: "minimal"` or `"extensive"`. +Status reactions also require an ack reaction and a matching `messages.ackReactionScope` (`direct`, `group-all`, `group-mentions`, or `all`). Set `channels.signal.reactionLevel: "off"` to disable Signal status reactions. -`messages.removeAckAfterReply: true` clears the final status reaction after the -configured hold time. Otherwise Signal restores the initial ack reaction after -the final done/error state. +`messages.removeAckAfterReply: true` clears the final status reaction after the configured hold time. Otherwise Signal restores the initial ack reaction after the final done/error state. ## Reactions (message tool) -- Use `message action=react` with `channel=signal`. -- Targets: sender E.164 or UUID (use `uuid:` from pairing output; bare UUID works too). +Use `message action=react` with `channel=signal`. + +- Targets: sender E.164 or UUID (use `uuid:` from pairing output; a bare UUID also works). - `messageId` is the Signal timestamp for the message you're reacting to. - Group reactions require `targetAuthor` or `targetAuthorUuid`. -Examples: - -``` +```text message action=react channel=signal target=uuid:123e4567-e89b-12d3-a456-426614174000 messageId=1737630212345 emoji=🔥 message action=react channel=signal target=+15551234567 messageId=1737630212345 emoji=🔥 remove=true message action=react channel=signal target=signal:group: targetAuthor=uuid: messageId=1737630212345 emoji=✅ @@ -325,25 +292,20 @@ message action=react channel=signal target=signal:group: targetAuthor=u Config: - `channels.signal.actions.reactions`: enable/disable reaction actions (default true). -- `channels.signal.reactionLevel`: `off | ack | minimal | extensive`. - - `off`/`ack` disables agent reactions (message tool `react` will error). +- `channels.signal.reactionLevel`: `off | ack | minimal | extensive` (default `minimal`). + - `off`/`ack` disables agent reactions (message tool `react` errors). - `minimal`/`extensive` enables agent reactions and sets the guidance level. - Per-account overrides: `channels.signal.accounts..actions.reactions`, `channels.signal.accounts..reactionLevel`. ## Approval reactions -Signal exec and plugin approval prompts use the top-level `approvals.exec` and -`approvals.plugin` routing blocks. Signal does not have a -`channels.signal.execApprovals` block. +Signal exec and plugin approval prompts use the top-level `approvals.exec` and `approvals.plugin` routing blocks. Signal has no `channels.signal.execApprovals` block. - `👍` approves once. - `👎` denies. - Use `/approve allow-always` when a request offers persistent approval. -Approval reaction resolution requires explicit Signal approvers from -`channels.signal.allowFrom`, `channels.signal.defaultTo`, or the matching account-level fields. -Direct same-chat exec approval prompts can still suppress the duplicate local `/approve` fallback -without explicit approvers; no-approver group approvals keep the local fallback visible. +Approval reaction resolution requires explicit Signal approvers from `channels.signal.allowFrom`, `channels.signal.defaultTo`, or the matching account-level fields. Direct same-chat exec approval prompts can still suppress the duplicate local `/approve` fallback without explicit approvers; no-approver group approvals keep the local fallback visible. ## Delivery targets (CLI/cron) @@ -354,8 +316,7 @@ without explicit approvers; no-approver group approvals keep the local fallback ## Aliases -Configure aliases when you want stable names for recurring Signal targets. -Aliases are OpenClaw-side config only; they do not create or edit Signal contacts. +Configure aliases for stable names on recurring Signal targets. Aliases are OpenClaw-side config only; they do not create or edit Signal contacts. ```json5 { @@ -399,10 +360,7 @@ Per-account aliases inherit the top-level aliases and can add or override names: } ``` -`openclaw directory peers list --channel signal` and -`openclaw directory groups list --channel signal` list configured aliases. The -Signal directory is config-backed; it does not live-query Signal contacts or -mutate the Signal account. +`openclaw directory peers list --channel signal` and `openclaw directory groups list --channel signal` list configured aliases. The Signal directory is config-backed; it does not live-query Signal contacts or mutate the Signal account. ## Troubleshooting @@ -438,7 +396,7 @@ pgrep -af signal-cli grep -i "signal" "/tmp/openclaw/openclaw-$(date +%Y-%m-%d).log" | tail -20 ``` -For triage flow: [/channels/troubleshooting](/channels/troubleshooting). +For triage flow: [Channels Troubleshooting](/channels/troubleshooting). ## Security notes @@ -459,26 +417,30 @@ Provider options: - `channels.signal.cliPath`: path to `signal-cli`. - `channels.signal.configPath`: optional `signal-cli --config` directory. - `channels.signal.httpUrl`: full daemon URL (overrides host/port). -- `channels.signal.httpHost`, `channels.signal.httpPort`: daemon bind (default 127.0.0.1:8080). +- `channels.signal.httpHost`, `channels.signal.httpPort`: daemon bind (default `127.0.0.1:8080`). - `channels.signal.autoStart`: auto-spawn daemon (default true if `httpUrl` unset). -- `channels.signal.startupTimeoutMs`: startup wait timeout in ms (cap 120000). +- `channels.signal.startupTimeoutMs`: startup wait timeout in ms (min 1000, cap 120000; default 30000). - `channels.signal.receiveMode`: `on-start | manual`. - `channels.signal.ignoreAttachments`: skip attachment downloads. - `channels.signal.ignoreStories`: ignore stories from the daemon. - `channels.signal.sendReadReceipts`: forward read receipts. - `channels.signal.dmPolicy`: `pairing | allowlist | open | disabled` (default: pairing). -- `channels.signal.allowFrom`: DM allowlist (E.164 or `uuid:`). `open` requires `"*"`. Signal has no usernames; use phone/UUID ids. +- `channels.signal.allowFrom`: DM allowlist (E.164 or `uuid:`). `open` requires `"*"`. Signal has no usernames; use phone/UUID IDs. - `channels.signal.aliases`: OpenClaw-side aliases for DM or group delivery targets. - `channels.signal.groupPolicy`: `open | allowlist | disabled` (default: allowlist). - `channels.signal.groupAllowFrom`: group allowlist; accepts Signal group IDs (raw, `group:`, or `signal:group:`), sender E.164 numbers, or `uuid:` values. -- `channels.signal.groups`: per-group overrides keyed by Signal group id (or `"*"`). Supported fields: `requireMention`, `tools`, `toolsBySender`. +- `channels.signal.groups`: per-group overrides keyed by Signal group ID (or `"*"`). Supported fields: `requireMention`, `tools`, `toolsBySender`. - `channels.signal.accounts..groups`: per-account version of `channels.signal.groups` for multi-account setups. - `channels.signal.accounts..aliases`: per-account aliases, merged with top-level aliases. - `channels.signal.historyLimit`: max group messages to include as context (0 disables). - `channels.signal.dmHistoryLimit`: DM history limit in user turns. Per-user overrides: `channels.signal.dms[""].historyLimit`. -- `channels.signal.textChunkLimit`: outbound chunk size (chars). +- `channels.signal.textChunkLimit`: outbound chunk size in characters (default 4000). - `channels.signal.chunkMode`: `length` (default) or `newline` to split on blank lines (paragraph boundaries) before length chunking. -- `channels.signal.mediaMaxMb`: inbound/outbound media cap (MB). +- `channels.signal.mediaMaxMb`: inbound/outbound media cap in MB (default 8). +- `channels.signal.reactionLevel`: `off | ack | minimal | extensive` (default `minimal`). See [Reactions](#reactions-message-tool). +- `channels.signal.reactionNotifications`: `off | own | all | allowlist` (default `own`) - when the agent is notified of incoming reactions from others. +- `channels.signal.reactionAllowlist`: senders whose reactions notify the agent when `reactionNotifications: "allowlist"`. +- `channels.signal.blockStreaming`, `channels.signal.blockStreamingCoalesce`: block-mode streaming controls shared across channels. See [Streaming](/concepts/streaming). Related global options: @@ -488,8 +450,8 @@ Related global options: ## Related -- [Channels Overview](/channels) — all supported channels -- [Pairing](/channels/pairing) — DM authentication and pairing flow -- [Groups](/channels/groups) — group chat behavior and mention gating -- [Channel Routing](/channels/channel-routing) — session routing for messages -- [Security](/gateway/security) — access model and hardening +- [Channels Overview](/channels) - all supported channels +- [Pairing](/channels/pairing) - DM authentication and pairing flow +- [Groups](/channels/groups) - group chat behavior and mention gating +- [Channel Routing](/channels/channel-routing) - session routing for messages +- [Security](/gateway/security) - access model and hardening diff --git a/docs/channels/slack.md b/docs/channels/slack.md index b524fa32713e..ab7de25967d9 100644 --- a/docs/channels/slack.md +++ b/docs/channels/slack.md @@ -5,7 +5,7 @@ read_when: title: "Slack" --- -Production-ready for DMs and channels via Slack app integrations. Default mode is Socket Mode; HTTP Request URLs are also supported. Relay mode is intended for managed deployments where a trusted router owns Slack ingress. +Slack support covers DMs and channels via Slack app integrations. Default transport is Socket Mode; HTTP Request URLs are also supported. Relay mode is for managed deployments where a trusted router owns Slack ingress. @@ -19,9 +19,9 @@ Production-ready for DMs and channels via Slack app integrations. Default mode i -## Choosing Socket Mode or HTTP Request URLs +## Choosing a transport -Both transports are production-ready and reach feature parity for messaging, slash commands, App Home, and interactivity. Pick by deployment shape, not features. +Socket Mode and HTTP Request URLs reach feature parity for messaging, slash commands, App Home, and interactivity. Pick by deployment shape, not features. | Concern | Socket Mode (default) | HTTP Request URLs | | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | @@ -47,10 +47,7 @@ Both transports are production-ready and reach feature parity for messaging, sla ### Relay mode -Relay mode separates Slack ingress from the OpenClaw gateway. A trusted router owns the -single Slack Socket Mode connection, chooses a destination gateway, and forwards a typed -event over an authenticated websocket. The gateway continues to use its bot token for -outbound Slack Web API calls. +Relay mode separates Slack ingress from the OpenClaw gateway. A trusted router owns the single Slack Socket Mode connection, chooses a destination gateway, and forwards a typed event over an authenticated websocket. The gateway still uses its own bot token for outbound Slack Web API calls. ```json5 { @@ -68,23 +65,15 @@ outbound Slack Web API calls. } ``` -The relay URL must use `wss://` unless it targets localhost. Treat the bearer token and -router route table as part of the Slack authorization boundary: routed events enter the -normal Slack message handler as authorized activations. A router-provided `slack_identity` -in the websocket `hello` frame can set the default outbound username and icon; an explicit -identity supplied by the caller still wins. The relay connection reconnects with the same -bounded backoff timing used by Socket Mode and clears the router-provided identity whenever -it disconnects. +The relay URL must use `wss://` unless it targets localhost. Treat the bearer token and router route table as part of the Slack authorization boundary: routed events enter the normal Slack message handler as authorized activations. A router-provided `slack_identity` in the websocket `hello` frame can set the default outbound username and icon; an explicit identity supplied by the caller still wins. The relay connection reconnects with the same bounded backoff timing as Socket Mode and clears the router-provided identity whenever it disconnects. ## Install -Install Slack before configuring the channel: - ```bash openclaw plugins install @openclaw/slack ``` -`plugins install` registers and enables the plugin. The plugin still does nothing until you configure the Slack app and channel settings below. See [Plugins](/tools/plugin) for general plugin behavior and install rules. +`plugins install` registers and enables the plugin. It does nothing until you configure the Slack app and channel settings below. See [Plugins](/tools/plugin) for general plugin install rules. ## Quick setup @@ -484,7 +473,7 @@ openclaw gateway - The three URL fields (`slash_commands[].url`, `event_subscriptions.request_url`, and `interactivity.request_url` / `message_menu_options_url`) all point at the same OpenClaw endpoint. Slack's manifest schema requires them named separately, but OpenClaw routes by payload type so a single `webhookPath` (default `/slack/events`) is enough. Slash commands without `slash_commands[].url` will silently no-op in HTTP mode. + The three URL fields (`slash_commands[].url`, `event_subscriptions.request_url`, and `interactivity.request_url` / `message_menu_options_url`) all point at the same OpenClaw endpoint. Slack's manifest schema requires them named separately, but OpenClaw routes by payload type so a single `webhookPath` (default `/slack/events`) is enough. Slash commands without `slash_commands[].url` silently no-op in HTTP mode. After Slack creates the app: @@ -714,7 +703,7 @@ The default manifest enables the Slack App Home **Home** tab and subscribes to ` Multiple [native slash commands](#commands-and-slash-behavior) can be used instead of a single configured command with nuance: - Use `/agentstatus` instead of `/status` because the `/status` command is reserved. - - No more than 25 slash commands can be made available at once. + - No more than 25 slash commands can be registered on a Slack app at once (Slack platform limit). Replace your existing `features.slash_commands` section with a subset of [available commands](/tools/slash-commands#command-list): @@ -902,8 +891,8 @@ The default manifest enables the Slack App Home **Home** tab and subscribes to ` - `botToken`, `appToken`, `signingSecret`, `relay.authToken`, and `userToken` accept plaintext strings or SecretRef objects. - Config tokens override env fallback. -- `SLACK_BOT_TOKEN` / `SLACK_APP_TOKEN` env fallback applies only to the default account. -- `userToken` is config-only (no env fallback) and defaults to read-only behavior (`userTokenReadOnly: true`). +- `SLACK_BOT_TOKEN`, `SLACK_APP_TOKEN`, and `SLACK_USER_TOKEN` env fallback each apply only to the default account. +- `userToken` defaults to read-only behavior (`userTokenReadOnly: true`). Status snapshot behavior: @@ -1045,7 +1034,7 @@ Current Slack message actions include `send`, `upload-file`, `download-file`, `r - `toolsBySender` key format: `channel:`, `id:`, `e164:`, `username:`, `name:`, or `"*"` wildcard (legacy unprefixed keys still map to `id:` only) - `ignoreOtherMentions` defaults to `false`. When `true`, channel messages that mention another user or user group but not this bot are stored as pending context and not handled. DMs and group DMs are unaffected. The filter requires a bot user ID from `auth.test`; if that identity is unavailable, messages pass through unchanged. + `ignoreOtherMentions` (default `false`) drops channel messages that mention another user or user group but not this bot. DMs and group DMs (MPIMs) are unaffected. The filter requires a resolved bot user ID from `auth.test`; if that identity is unavailable (for example a user-token-only identity), the gate fails open and messages pass through unchanged. `allowBots` is conservative for channels and private channels: bot-authored room messages are accepted only when the sending bot is explicitly listed in that room's `users` allowlist, or when at least one explicit Slack owner ID from `channels.slack.allowFrom` is currently a room member. Wildcards and display-name owner entries do not satisfy owner presence. Owner presence uses Slack `conversations.members`; make sure the app has the matching read scope for the room type (`channels:read` for public channels, `groups:read` for private channels). If the member lookup fails, OpenClaw drops the bot-authored room message. @@ -1207,7 +1196,7 @@ Legacy keys: - `channels.slack.streamMode` (`replace | status_final | append`) is a legacy runtime alias for `channels.slack.streaming.mode`. - boolean `channels.slack.streaming` is a legacy runtime alias for `channels.slack.streaming.mode` and `channels.slack.streaming.nativeTransport`. -- legacy `channels.slack.nativeStreaming` is a runtime alias for `channels.slack.streaming.nativeTransport`. +- top-level `channels.slack.chunkMode` and `channels.slack.nativeStreaming` are legacy runtime aliases for `channels.slack.streaming.chunkMode` and `channels.slack.streaming.nativeTransport`. - Run `openclaw doctor --fix` to rewrite persisted Slack streaming config to the canonical keys. ## Typing reaction fallback @@ -1237,8 +1226,8 @@ Notes: - - text chunks use `channels.slack.textChunkLimit` (default 4000) - - `channels.slack.chunkMode="newline"` enables paragraph-first splitting + - text chunks use `channels.slack.textChunkLimit` (default `8000`, capped at Slack's own message-length limit) + - `channels.slack.streaming.chunkMode="newline"` enables paragraph-first splitting - file sends use Slack upload APIs and can include thread replies (`thread_ts`) - outbound media cap follows `channels.slack.mediaMaxMb` when configured; otherwise channel sends use MIME-kind defaults from media pipeline @@ -1276,12 +1265,12 @@ Native commands require [additional manifest settings](#additional-manifest-sett /help ``` -Native argument menus use an adaptive rendering strategy that shows a confirmation modal before dispatching a selected option value: +Native argument menus render as one of the following, in priority order: -- up to 5 options: button blocks -- 6-100 options: static select menu -- more than 100 options: external select with async option filtering when interactivity options handlers are available -- exceeded Slack limits: encoded option values fall back to buttons +- 3-5 short-enough options: an overflow ("...") menu +- more than 100 options, with async option filtering available: external select +- 1-2 options, or any option whose encoded value is too long for a select: button blocks +- otherwise (6-100 options, or more than 100 without async filtering): static select menu, chunked at 100 options per menu ```txt /think @@ -1469,7 +1458,7 @@ Primary reference: [Configuration reference - Slack](/gateway/config-channels#sl - compatibility toggle: `dangerouslyAllowNameMatching` (break-glass; keep off unless needed) - channel access: `groupPolicy`, `channels.*`, `channels.*.users`, `channels.*.requireMention` - threading/history: `replyToMode`, `replyToModeByChatType`, `thread.*`, `historyLimit`, `dmHistoryLimit`, `dms.*.historyLimit` -- delivery: `textChunkLimit`, `chunkMode`, `mediaMaxMb`, `streaming`, `streaming.nativeTransport`, `streaming.preview.toolProgress` +- delivery: `textChunkLimit`, `streaming.chunkMode`, `mediaMaxMb`, `streaming`, `streaming.nativeTransport`, `streaming.preview.toolProgress` - unfurls: `unfurlLinks` (default: `false`), `unfurlMedia` for `chat.postMessage` link/media preview control; set `unfurlLinks: true` to opt back into link previews - ops/features: `configWrites`, `commands.native`, `slashCommand.*`, `actions.*`, `userToken`, `userTokenReadOnly` @@ -1640,9 +1629,6 @@ When a single Slack message contains multiple file attachments: - [Media understanding pipeline](/nodes/media-understanding) - [PDF tool](/tools/pdf) -- Epic: [#51349](https://github.com/openclaw/openclaw/issues/51349) — Slack attachment vision enablement -- Regression tests: [#51353](https://github.com/openclaw/openclaw/issues/51353) -- Live verification: [#51354](https://github.com/openclaw/openclaw/issues/51354) ## Related diff --git a/docs/channels/sms.md b/docs/channels/sms.md index 8fa6d548c6ff..cec10b8c015a 100644 --- a/docs/channels/sms.md +++ b/docs/channels/sms.md @@ -6,7 +6,9 @@ read_when: title: "SMS" --- -OpenClaw can receive and send SMS through a Twilio phone number or Messaging Service. The Gateway registers an inbound webhook route, validates Twilio request signatures by default, and sends replies back through Twilio's Messages API. +OpenClaw receives and sends SMS through a Twilio phone number or Messaging Service. The Gateway registers an inbound webhook route (default `/webhooks/sms`), validates Twilio request signatures by default, and sends replies back through Twilio's Messages API. + +Status: official plugin, installed separately. Text only: no MMS/media, direct messages only. @@ -28,9 +30,9 @@ You need: - A Twilio account with an SMS-capable phone number, or a Twilio Messaging Service. - The Twilio Account SID and Auth Token. - A public HTTPS URL that reaches your OpenClaw Gateway. -- A sender policy choice: `pairing` for private use, `allowlist` for preapproved phone numbers, or `open` only for intentionally public SMS access. +- A sender policy choice: `pairing` (default) for private use, `allowlist` for preapproved phone numbers, or `open` only for intentionally public SMS access. -Use one Twilio number for both SMS and Voice Call if the number has both capabilities. Configure the SMS webhook and Voice webhook separately in Twilio; this page only covers the SMS webhook. +One Twilio number can serve both SMS and [Voice Call](/plugins/voice-call) if it has both capabilities. The SMS webhook and Voice webhook are configured separately in Twilio and use separate Gateway paths; this page only covers the SMS webhook. ## Quick Setup @@ -91,7 +93,7 @@ https://gateway.example.com/webhooks/sms - Your public URL must route the SMS path to the Gateway process. If you use Tailscale Funnel for local testing, expose `/webhooks/sms` explicitly: + Your public URL must route the SMS path to the Gateway process (default port `18789`). If you use Tailscale Funnel for local testing, expose `/webhooks/sms` explicitly: ```bash tailscale funnel --bg --set-path /webhooks/sms http://127.0.0.1:/webhooks/sms @@ -122,6 +124,24 @@ openclaw pairing approve sms ## Configuration Examples +All keys live under `channels.sms` (and per account under `channels.sms.accounts.`): + +| Key | Default | Purpose | +| --------------------------------------- | --------------- | ------------------------------------------------------------------- | +| `enabled` | `true` | Enable or disable the channel/account. | +| `accountSid` | — | Twilio Account SID (`AC...`). | +| `authToken` | — | Twilio Auth Token; plaintext string or SecretRef. | +| `fromNumber` | — | E.164 sender number. | +| `messagingServiceSid` | — | Messaging Service SID (`MG...`) used when no `fromNumber` resolves. | +| `defaultTo` | — | Default destination when a send flow omits an explicit target. | +| `webhookPath` | `/webhooks/sms` | Gateway HTTP path for inbound Twilio webhooks. | +| `publicWebhookUrl` | — | Public URL configured in Twilio; required for signature validation. | +| `dangerouslyDisableSignatureValidation` | `false` | Skip `X-Twilio-Signature` checks; local tunnel testing only. | +| `dmPolicy` | `"pairing"` | `pairing`, `allowlist`, `open`, or `disabled`. | +| `allowFrom` | `[]` | Allowed sender numbers in E.164, or `"*"` with `dmPolicy: "open"`. | +| `textChunkLimit` | `1500` | Maximum characters per outbound SMS chunk. | +| `accounts`, `defaultAccount` | — | Multi-account map and default account id. | + ### Config file Use config-file setup when you want the channel definition to travel with the Gateway config: @@ -143,7 +163,19 @@ Use config-file setup when you want the channel definition to travel with the Ga ### Environment variables -Use env setup for single-account deployments where secrets come from the host environment: +Environment variables apply to the default account only; config values take precedence over env values. + +| Variable | Maps to | +| ----------------------------------------------- | -------------------------------------------------- | +| `TWILIO_ACCOUNT_SID` | `accountSid` | +| `TWILIO_AUTH_TOKEN` | `authToken` | +| `TWILIO_PHONE_NUMBER` (alias `TWILIO_SMS_FROM`) | `fromNumber` | +| `TWILIO_MESSAGING_SERVICE_SID` | `messagingServiceSid` | +| `SMS_PUBLIC_WEBHOOK_URL` | `publicWebhookUrl` | +| `SMS_WEBHOOK_PATH` | `webhookPath` | +| `SMS_ALLOWED_USERS` | `allowFrom` (comma-separated) | +| `SMS_TEXT_CHUNK_LIMIT` | `textChunkLimit` | +| `SMS_DANGEROUSLY_DISABLE_SIGNATURE_VALIDATION` | `dangerouslyDisableSignatureValidation` (`"true"`) | ```bash export TWILIO_ACCOUNT_SID="ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" @@ -165,11 +197,9 @@ Then enable the channel in config: } ``` -`TWILIO_SMS_FROM` is accepted as an alias for `TWILIO_PHONE_NUMBER`. Use `TWILIO_MESSAGING_SERVICE_SID` instead of a phone-number sender when Twilio should choose the sender from a Messaging Service. - ### SecretRef auth token -`authToken` can be a SecretRef. Use this when the Gateway should resolve the Twilio Auth Token from the OpenClaw secrets runtime instead of storing plaintext config: +`authToken` can be a SecretRef (`source: "env" | "file" | "exec"`). Use this when the Gateway should resolve the Twilio Auth Token from the OpenClaw secrets runtime instead of storing plaintext config: ```json5 { @@ -188,26 +218,6 @@ Then enable the channel in config: The referenced environment variable or secret provider must be visible to the Gateway runtime. Restart managed Gateway processes after changing host environment variables. -### Allowlist-only private number - -Use `allowlist` when only known phone numbers should be able to talk to the agent: - -```json5 -{ - channels: { - sms: { - enabled: true, - accountSid: "ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", - authToken: "twilio-auth-token", - fromNumber: "+15551234567", - publicWebhookUrl: "https://gateway.example.com/webhooks/sms", - dmPolicy: "allowlist", - allowFrom: ["+15557654321"], - }, - }, -} -``` - ### Messaging Service sender Use `messagingServiceSid` instead of `fromNumber` when Twilio should choose the sender through a Messaging Service: @@ -252,22 +262,38 @@ Set `defaultTo` when automation or agent-initiated delivery should have a defaul `channels.sms.dmPolicy` controls direct SMS access: -- `pairing` (default) -- `allowlist` (requires at least one sender in `allowFrom`) -- `open` (requires `allowFrom` to include `"*"`) -- `disabled` +- `pairing` (default): unknown senders get a pairing code; approve with `openclaw pairing approve sms `. +- `allowlist`: only senders in `allowFrom` are processed. An empty `allowFrom` rejects every sender (the Gateway logs a startup warning). +- `open`: config validation requires `allowFrom` to include `"*"`. Without the wildcard, only listed numbers can chat. +- `disabled`: all inbound DMs are dropped. -`allowFrom` entries should be E.164 phone numbers such as `+15551234567`. `sms:` prefixes are accepted and normalized. For a private assistant, prefer `dmPolicy: "allowlist"` with explicit phone numbers. +`allowFrom` entries should be E.164 phone numbers such as `+15551234567`. `sms:` and `twilio-sms:` prefixes are accepted and normalized. For a private assistant, prefer `dmPolicy: "allowlist"` with explicit phone numbers: + +```json5 +{ + channels: { + sms: { + enabled: true, + accountSid: "ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + authToken: "twilio-auth-token", + fromNumber: "+15551234567", + publicWebhookUrl: "https://gateway.example.com/webhooks/sms", + dmPolicy: "allowlist", + allowFrom: ["+15557654321"], + }, + }, +} +``` ## Sending SMS -Outbound SMS targets use the `sms:` service prefix with the SMS channel selected: +With the SMS channel selected, targets accept bare E.164 numbers or the `sms:` prefix: ```bash openclaw message send --channel sms --target sms:+15551234567 --message "hello" ``` -When channel selection is implicit, `twilio-sms:+15551234567` selects this channel without taking over the existing channel-owned `sms:` service prefix used by iMessage. +When channel selection is implicit, the `twilio-sms:` prefix selects this channel without taking over the `sms:` service prefix, which iMessage uses to pick carrier SMS delivery for its own targets: ```bash openclaw message send --target twilio-sms:+15551234567 --message "hello" @@ -277,14 +303,14 @@ The CLI requires an explicit `--target`. `defaultTo` is for automation and agent Agent replies from inbound SMS conversations automatically go back to the sender through the configured Twilio sender. -SMS output is plain text. OpenClaw strips markdown, flattens fenced code blocks, preserves readable links, and chunks long replies before sending them through Twilio. +SMS output is plain text. OpenClaw strips markdown, flattens fenced code blocks, rewrites links as `label (url)`, and splits long replies into chunks of at most `textChunkLimit` characters (default 1500) before sending them through Twilio. ## Verify Setup After the Gateway starts: 1. Confirm the Gateway log shows the SMS webhook route. -2. Run a Twilio-side probe: +2. Run a Twilio-side probe (checks the configured Twilio webhook URL/method and recent inbound errors): ```bash openclaw channels capabilities --channel sms @@ -319,6 +345,14 @@ The first message should create a pairing request. The second message should rec By default, OpenClaw validates `X-Twilio-Signature` using `publicWebhookUrl` and `authToken`. Keep `publicWebhookUrl` byte-for-byte aligned with the URL configured in Twilio, including scheme, host, path, and query string. +The webhook route also enforces, independent of signature validation: + +- `POST` only. +- Rate limit of 30 requests per minute per source IP (HTTP 429 above that). +- The payload `AccountSid` must match the configured `accountSid` (HTTP 403 otherwise). +- Replayed `MessageSid` values are deduplicated for 10 minutes. +- Request bodies over 32 KB are rejected. + For local tunnel testing only, you can set: ```json5 @@ -358,7 +392,7 @@ Use `accounts` when you operate more than one Twilio number: } ``` -Each account should use a distinct `webhookPath`. +Each account must use a distinct `webhookPath`; the Gateway refuses to register a webhook route whose path is already owned by another account. `TWILIO_*`/`SMS_*` environment fallbacks apply only to the default account; set `defaultAccount` to change which account that is. ## Troubleshooting @@ -366,6 +400,8 @@ Each account should use a distinct `webhookPath`. Check that `publicWebhookUrl` exactly matches the URL configured in Twilio, including scheme, host, path, and query string. Twilio signs the public URL string, so proxy rewrites and alternate hostnames can break signature validation. +A 403 with `Invalid account` means the inbound payload's `AccountSid` does not match the configured `accountSid`; check that the webhook points at the account that owns the number. + ### No pairing request appears Check the Twilio number's **Messaging** webhook URL and method. It must point to the SMS webhook URL and use `POST`. Also confirm the Gateway is reachable from the public internet or through your tunnel. @@ -377,6 +413,8 @@ If the Twilio message log shows error `11200`, Twilio accepted the inbound SMS b - The tunnel or reverse proxy exposes the exact `webhookPath`; for Tailscale Funnel, run `tailscale funnel status` and confirm `/webhooks/sms` is listed. - `publicWebhookUrl` uses the same scheme, host, path, and query string Twilio sends, so signature validation can reproduce the signed URL. +`openclaw channels status --channel sms --probe` surfaces both mismatched Twilio webhook settings and recent `11200` errors. + ### Outbound sends fail Confirm `accountSid`, `authToken`, and either `fromNumber` or `messagingServiceSid` are resolved. If you use a trial Twilio account, the destination number may need to be verified in Twilio before outbound SMS will send. diff --git a/docs/channels/synology-chat.md b/docs/channels/synology-chat.md index 9dabd066d580..7c67701821ca 100644 --- a/docs/channels/synology-chat.md +++ b/docs/channels/synology-chat.md @@ -6,19 +6,17 @@ read_when: title: "Synology Chat" --- -Status: bundled plugin direct-message channel using Synology Chat webhooks. -The plugin accepts inbound messages from Synology Chat outgoing webhooks and sends replies -through a Synology Chat incoming webhook. +Synology Chat connects to OpenClaw through a webhook pair: a Synology Chat outgoing webhook posts inbound direct messages to the Gateway, and replies go back through a Synology Chat incoming webhook. -## Bundled plugin +Status: official plugin, installed separately. Direct messages only; text and URL-based file sends are supported. -Synology Chat ships as a bundled plugin in current OpenClaw releases, so normal -packaged builds do not need a separate install. +## Install -If you are on an older build or a custom install that excludes Synology Chat, -install it manually: +```bash +openclaw plugins install @openclaw/synology-chat +``` -Install from a local checkout: +Local checkout (when running from a git repo): ```bash openclaw plugins install ./path/to/local/synology-chat-plugin @@ -28,21 +26,17 @@ Details: [Plugins](/tools/plugin) ## Quick setup -1. Ensure the Synology Chat plugin is available. - - Current packaged OpenClaw releases already bundle it. - - Older/custom installs can add it manually from a source checkout with the command above. - - `openclaw onboard` now shows Synology Chat in the same channel setup list as `openclaw channels add`. - - Non-interactive setup: `openclaw channels add --channel synology-chat --token --url ` +1. Install the plugin (above). 2. In Synology Chat integrations: - Create an incoming webhook and copy its URL. - Create an outgoing webhook with your secret token. -3. Point the outgoing webhook URL to your OpenClaw gateway: +3. Point the outgoing webhook URL to your OpenClaw Gateway: - `https://gateway-host/webhook/synology` by default. - Or your custom `channels.synology-chat.webhookPath`. -4. Finish setup in OpenClaw. - - Guided: `openclaw onboard` +4. Finish setup in OpenClaw. Synology Chat appears in the same channel setup list in both flows: + - Guided: `openclaw onboard` or `openclaw channels add` - Direct: `openclaw channels add --channel synology-chat --token --url ` -5. Restart gateway and send a DM to the Synology Chat bot. +5. Restart the Gateway and send a DM to the Synology Chat bot. Webhook auth details: @@ -54,6 +48,7 @@ Webhook auth details: - `x-openclaw-token` - `Authorization: Bearer ` - Empty or missing tokens fail closed. +- Payloads may be `application/x-www-form-urlencoded` or `application/json`; `token`, `user_id`, and `text` are required. Minimal config: @@ -87,34 +82,30 @@ For the default account, you can use env vars: Config values override env vars. -`SYNOLOGY_CHAT_INCOMING_URL` cannot be set from a workspace `.env`; see [Workspace `.env` files](/gateway/security). +`SYNOLOGY_CHAT_INCOMING_URL` and `SYNOLOGY_NAS_HOST` cannot be set from a workspace `.env`; see [Workspace `.env` files](/gateway/security#workspace-env-files). ## DM policy and access control -- `dmPolicy: "allowlist"` is the recommended default. +- Supported `dmPolicy` values: `allowlist` (default), `open`, and `disabled`. Synology Chat has no pairing flow; approve senders by adding their numeric Synology user IDs to `allowedUserIds`. - `allowedUserIds` accepts a list (or comma-separated string) of Synology user IDs. -- In `allowlist` mode, an empty `allowedUserIds` list is treated as misconfiguration and the webhook route will not start (use `dmPolicy: "open"` with `allowedUserIds: ["*"]` for allow-all). -- `dmPolicy: "open"` allows public DMs only when `allowedUserIds` includes `"*"`; with restrictive entries, only matching users can chat. +- In `allowlist` mode, an empty `allowedUserIds` list is treated as misconfiguration and the webhook route will not start. +- `dmPolicy: "open"` allows public DMs only when `allowedUserIds` includes `"*"`; with restrictive entries, only matching users can chat. `open` with an empty `allowedUserIds` list also refuses to start the route. - `dmPolicy: "disabled"` blocks DMs. - Reply recipient binding stays on stable numeric `user_id` by default. `channels.synology-chat.dangerouslyAllowNameMatching: true` is break-glass compatibility mode that re-enables mutable username/nickname lookup for reply delivery. -- Pairing approvals work with: - - `openclaw pairing list synology-chat` - - `openclaw pairing approve synology-chat ` ## Outbound delivery -Use numeric Synology Chat user IDs as targets. +Use numeric Synology Chat user IDs as targets. The `synology-chat:`, `synology_chat:`, and `synology:` prefixes are accepted. Examples: ```bash -openclaw message send --channel synology-chat --target 123456 --text "Hello from OpenClaw" -openclaw message send --channel synology-chat --target synology-chat:123456 --text "Hello again" -openclaw message send --channel synology-chat --target synology:123456 --text "Short prefix" +openclaw message send --channel synology-chat --target 123456 --message "Hello from OpenClaw" +openclaw message send --channel synology-chat --target synology-chat:123456 --message "Hello again" +openclaw message send --channel synology-chat --target synology:123456 --message "Short prefix" ``` -Media sends are supported by URL-based file delivery. -Outbound file URLs must use `http` or `https`, and private or otherwise blocked network targets are rejected before OpenClaw forwards the URL to the NAS webhook. +Outbound text is chunked at 2000 characters. Media sends are supported by URL-based file delivery: the NAS downloads and attaches the file (max 32 MB). Outbound file URLs must use `http` or `https`, and private or otherwise blocked network targets are rejected before OpenClaw forwards the URL to the NAS webhook. ## Multi-account @@ -122,7 +113,7 @@ Multiple Synology Chat accounts are supported under `channels.synology-chat.acco Each account can override token, incoming URL, webhook path, DM policy, and limits. Direct-message sessions are isolated per account and user, so the same numeric `user_id` on two different Synology accounts does not share transcript state. -Give each enabled account a distinct `webhookPath`. OpenClaw now rejects duplicate exact paths +Give each enabled account a distinct `webhookPath`. OpenClaw rejects duplicate exact paths and refuses to start named accounts that only inherit a shared webhook path in multi-account setups. If you intentionally need legacy inheritance for a named account, set `dangerouslyAllowInheritedWebhookPath: true` on that account or at `channels.synology-chat`, @@ -155,8 +146,9 @@ but duplicate exact paths are still rejected fail-closed. Prefer explicit per-ac - Keep `token` secret and rotate it if leaked. - Keep `allowInsecureSsl: false` unless you explicitly trust a self-signed local NAS cert. -- Inbound webhook requests are token-verified and rate-limited per sender. -- Invalid token checks use constant-time secret comparison and fail closed. +- Inbound webhook requests are token-verified and rate-limited per sender (`rateLimitPerMinute`, default 30). +- Invalid token checks use constant-time secret comparison and fail closed; repeated invalid-token attempts temporarily lock out the source IP. +- Inbound message text is sanitized against known prompt-injection patterns and truncated at 4000 characters. - Prefer `dmPolicy: "allowlist"` for production. - Keep `dangerouslyAllowNameMatching` off unless you explicitly need legacy username-based reply delivery. - Keep `dangerouslyAllowInheritedWebhookPath` off unless you explicitly accept shared-path routing risk in a multi-account setup. @@ -181,7 +173,6 @@ but duplicate exact paths are still rejected fail-closed. Prefer explicit per-ac ## Related - [Channels Overview](/channels) — all supported channels -- [Pairing](/channels/pairing) — DM authentication and pairing flow - [Groups](/channels/groups) — group chat behavior and mention gating - [Channel Routing](/channels/channel-routing) — session routing for messages - [Security](/gateway/security) — access model and hardening diff --git a/docs/channels/telegram.md b/docs/channels/telegram.md index ecf9e0a5b16e..050a7f949a70 100644 --- a/docs/channels/telegram.md +++ b/docs/channels/telegram.md @@ -5,7 +5,7 @@ read_when: title: "Telegram" --- -Production-ready for bot DMs and groups via grammY. Long polling is the default mode; webhook mode is optional. +Production-ready for bot DMs and groups via grammY. Long polling is the default transport; webhook mode is optional. @@ -23,10 +23,7 @@ Production-ready for bot DMs and groups via grammY. Long polling is the default - Open Telegram and chat with **@BotFather** (confirm the handle is exactly `@BotFather`). - - Run `/newbot`, follow prompts, and save the token. - + Open Telegram, chat with **@BotFather** (confirm the handle is exactly `@BotFather`), run `/newbot`, follow the prompts, and save the token. @@ -44,8 +41,8 @@ Production-ready for bot DMs and groups via grammY. Long polling is the default } ``` - Env fallback: `TELEGRAM_BOT_TOKEN=...` (default account only). - Telegram does **not** use `openclaw channels login telegram`; configure token in config/env, then start gateway. + Env fallback: `TELEGRAM_BOT_TOKEN` (default account only; named accounts must use `botToken` or `tokenFile`). + Telegram does **not** use `openclaw channels login telegram`; set the token in config/env, then start the gateway. @@ -62,49 +59,45 @@ openclaw pairing approve telegram - Add the bot to your group, then get both IDs that group access needs: + Add the bot to your group, then get the two IDs group access needs: - - your Telegram user ID, used in `allowFrom` / `groupAllowFrom` - - the Telegram group chat ID, used as the key under `channels.telegram.groups` + - your Telegram user ID, for `allowFrom` / `groupAllowFrom` + - the Telegram group chat ID, as the key under `channels.telegram.groups` - For first-time setup, get the group chat ID from `openclaw logs --follow`, a forwarded-ID bot, or Bot API `getUpdates`. After the group is allowed, `/whoami@` can confirm the user and group IDs. + Get the group chat ID from `openclaw logs --follow`, a forwarded-ID bot, or Bot API `getUpdates`. After the group is allowed, `/whoami@` confirms the user and group IDs. - Negative Telegram supergroup IDs that start with `-100` are group chat IDs. Put them under `channels.telegram.groups`, not under `groupAllowFrom`. + Negative supergroup IDs starting with `-100` are group chat IDs. They go under `channels.telegram.groups`, not `groupAllowFrom`. -Token resolution order is account-aware. In practice, config values win over env fallback, and `TELEGRAM_BOT_TOKEN` only applies to the default account. -After a successful startup, OpenClaw caches the bot identity in the state directory for up to 24 hours so restarts can avoid an extra Telegram `getMe` call; changing or removing the token clears that cache. +Token resolution is account-aware: `tokenFile` beats `botToken` beats env, and config always wins over `TELEGRAM_BOT_TOKEN` (which only resolves for the default account). After a successful startup, OpenClaw caches the bot identity for up to 24 hours so restarts skip an extra `getMe` call; changing or removing the token clears that cache. ## Telegram side settings - Telegram bots default to **Privacy Mode**, which limits what group messages they receive. + Telegram bots default to **Privacy Mode**, which limits which group messages they receive. - If the bot must see all group messages, either: + To see all group messages, either: - disable privacy mode via `/setprivacy`, or - make the bot a group admin. - When toggling privacy mode, remove + re-add the bot in each group so Telegram applies the change. + After toggling privacy mode, remove and re-add the bot in each group so Telegram applies the change. - Admin status is controlled in Telegram group settings. - - Admin bots receive all group messages, which is useful for always-on group behavior. - + Admin status is controlled in Telegram group settings. Admin bots receive all group messages, useful for always-on group behavior. - - `/setjoingroups` to allow/deny group adds - - `/setprivacy` for group visibility behavior + - `/setjoingroups` — allow/deny group adds + - `/setprivacy` — group visibility behavior @@ -113,7 +106,7 @@ After a successful startup, OpenClaw caches the bot identity in the state direct ### Group bot identity -In Telegram groups and forum topics, an explicit mention of the configured bot handle (for example `@my_bot`) is treated as addressing the selected OpenClaw agent, even when the agent persona name differs from the Telegram username. The group silence policy still applies to unrelated group traffic, but the bot handle itself is not considered "someone else." +In groups and forum topics, an explicit mention of the configured bot handle (for example `@my_bot`) addresses the selected OpenClaw agent, even when the agent persona name differs from the Telegram username. Group silence policy still applies to unrelated traffic, but the bot handle itself is never "someone else." @@ -127,26 +120,19 @@ In Telegram groups and forum topics, an explicit mention of the configured bot h `dmPolicy: "open"` with `allowFrom: ["*"]` lets any Telegram account that finds or guesses the bot username command the bot. Use it only for intentionally public bots with tightly restricted tools; one-owner bots should use `allowlist` with numeric user IDs. `channels.telegram.allowFrom` accepts numeric Telegram user IDs. `telegram:` / `tg:` prefixes are accepted and normalized. - In multi-account configs, a restrictive top-level `channels.telegram.allowFrom` is treated as a safety boundary: account-level `allowFrom: ["*"]` entries do not make that account public unless the effective account allowlist still contains an explicit wildcard after merging. + In multi-account configs, a restrictive top-level `channels.telegram.allowFrom` is a safety boundary: an account-level `allowFrom: ["*"]` does not make that account public unless the merged effective allowlist still contains an explicit wildcard. `dmPolicy: "allowlist"` with empty `allowFrom` blocks all DMs and is rejected by config validation. - Setup asks for numeric user IDs only. - If you upgraded and your config contains `@username` allowlist entries, run `openclaw doctor --fix` to resolve them (best-effort; requires a Telegram bot token). - If you previously relied on pairing-store allowlist files, `openclaw doctor --fix` can recover entries into `channels.telegram.allowFrom` in allowlist flows (for example when `dmPolicy: "allowlist"` has no explicit IDs yet). + Setup asks for numeric user IDs only. If your config has `@username` allowlist entries from an older setup, run `openclaw doctor --fix` to resolve them to numeric IDs (best-effort; requires a Telegram bot token). + If you previously relied on pairing-store allowlist files, `openclaw doctor --fix` can recover entries into `channels.telegram.allowFrom` for allowlist flows (for example when `dmPolicy: "allowlist"` has no explicit IDs yet). - For one-owner bots, prefer `dmPolicy: "allowlist"` with explicit numeric `allowFrom` IDs to keep access policy durable in config (instead of depending on previous pairing approvals). + For one-owner bots, prefer `dmPolicy: "allowlist"` with explicit numeric `allowFrom` IDs over depending on previous pairing approvals. - Common confusion: DM pairing approval does not mean "this sender is authorized everywhere". - Pairing grants DM access. If no command owner exists yet, the first approved pairing also sets `commands.ownerAllowFrom` so owner-only commands and exec approvals have an explicit operator account. - Group sender authorization still comes from explicit config allowlists. - If you want "I am authorized once and both DMs and group commands work", put your numeric Telegram user ID in `channels.telegram.allowFrom`; for owner-only commands, make sure `commands.ownerAllowFrom` contains `telegram:`. + Common confusion: DM pairing approval does not mean "this sender is authorized everywhere." Pairing grants DM access only. If no command owner exists yet, the first approved pairing also sets `commands.ownerAllowFrom`, giving owner-only commands and exec approvals an explicit operator account. Group sender authorization still comes from explicit config allowlists. + To be authorized for both DMs and group commands with one identity: put your numeric Telegram user ID in `channels.telegram.allowFrom`, and for owner-only commands make sure `commands.ownerAllowFrom` contains `telegram:`. ### Finding your Telegram user ID - Safer (no third-party bot): - - 1. DM your bot. - 2. Run `openclaw logs --follow`. - 3. Read `from.id`. + Safer (no third-party bot): DM your bot, run `openclaw logs --follow`, read `from.id`. Official Bot API method: @@ -154,7 +140,7 @@ In Telegram groups and forum topics, an explicit mention of the configured bot h curl "https://api.telegram.org/bot/getUpdates" ``` - Third-party method (less private): `@userinfobot` or `@getidsbot`. + Third-party (less private): `@userinfobot` or `@getidsbot`. @@ -162,25 +148,17 @@ curl "https://api.telegram.org/bot/getUpdates" Two controls apply together: 1. **Which groups are allowed** (`channels.telegram.groups`) - - no `groups` config: - - with `groupPolicy: "open"`: any group can pass group-ID checks - - with `groupPolicy: "allowlist"` (default): groups are blocked until you add `groups` entries (or `"*"`) - - `groups` configured: acts as allowlist (explicit IDs or `"*"`) + - no `groups` config, `groupPolicy: "open"`: any group passes group-ID checks + - no `groups` config, `groupPolicy: "allowlist"` (default): all groups blocked until you add `groups` entries (or `"*"`) + - `groups` configured: acts as an allowlist (explicit IDs or `"*"`) 2. **Which senders are allowed in groups** (`channels.telegram.groupPolicy`) - - `open` - - `allowlist` (default) - - `disabled` + - `open` / `allowlist` (default) / `disabled` - `groupAllowFrom` is used for group sender filtering. If not set, Telegram falls back to `allowFrom`. - `groupAllowFrom` entries should be numeric Telegram user IDs (`telegram:` / `tg:` prefixes are normalized). - Do not put Telegram group or supergroup chat IDs in `groupAllowFrom`. Negative chat IDs belong under `channels.telegram.groups`. - Non-numeric entries are ignored for sender authorization. - Security boundary (`2026.2.25+`): group sender auth does **not** inherit DM pairing-store approvals. - Pairing stays DM-only. For groups, set `groupAllowFrom` or per-group/per-topic `allowFrom`. - If `groupAllowFrom` is unset, Telegram falls back to config `allowFrom`, not the pairing store. + `groupAllowFrom` filters group senders; if unset, Telegram falls back to `allowFrom` (not the pairing store — group sender auth never inherits DM pairing-store approvals, a security boundary since `2026.2.25`). + `groupAllowFrom` entries should be numeric Telegram user IDs (`telegram:` / `tg:` prefixes are normalized); non-numeric entries are ignored. Do not put group or supergroup chat IDs here — negative chat IDs belong under `channels.telegram.groups`. Practical pattern for one-owner bots: set your user ID in `channels.telegram.allowFrom`, leave `groupAllowFrom` unset, and allow the target groups under `channels.telegram.groups`. - Runtime note: if `channels.telegram` is completely missing, runtime defaults to fail-closed `groupPolicy="allowlist"` unless `channels.defaults.groupPolicy` is explicitly set. + If `channels.telegram` is entirely missing from config, runtime defaults to fail-closed `groupPolicy="allowlist"` unless `channels.defaults.groupPolicy` is explicitly set. Owner-only group setup: @@ -202,9 +180,9 @@ curl "https://api.telegram.org/bot/getUpdates" } ``` - Test it from the group with `@ ping`. Plain group messages do not trigger the bot while `requireMention: true`. + Test from the group with `@ ping`. Plain group messages do not trigger the bot while `requireMention: true`. - Example: allow any member in one specific group: + Allow any member in one specific group: ```json5 { @@ -221,7 +199,7 @@ curl "https://api.telegram.org/bot/getUpdates" } ``` - Example: allow only specific users inside one specific group: + Allow only specific users inside one specific group: ```json5 { @@ -239,34 +217,23 @@ curl "https://api.telegram.org/bot/getUpdates" ``` - Common mistake: `groupAllowFrom` is not a Telegram group allowlist. + Common mistake: `groupAllowFrom` is not a group allowlist. - - Put negative Telegram group or supergroup chat IDs like `-1001234567890` under `channels.telegram.groups`. - - Put Telegram user IDs like `8734062810` under `groupAllowFrom` when you want to limit which people inside an allowed group can trigger the bot. - - Use `groupAllowFrom: ["*"]` only when you want any member of an allowed group to be able to talk to the bot. + - Negative Telegram group/supergroup chat IDs (`-1001234567890`) go under `channels.telegram.groups`. + - Telegram user IDs (`8734062810`) go under `groupAllowFrom` to limit which people inside an allowed group can trigger the bot. + - Use `groupAllowFrom: ["*"]` only to let any member of an allowed group talk to the bot. - Group replies require mention by default. + Group replies require mention by default. A mention can come from: - Mention can come from: + - a native `@botusername` mention, or + - a mention pattern in `agents.list[].groupChat.mentionPatterns` or `messages.groupChat.mentionPatterns` - - native `@botusername` mention, or - - mention patterns in: - - `agents.list[].groupChat.mentionPatterns` - - `messages.groupChat.mentionPatterns` - - Session-level command toggles: - - - `/activation always` - - `/activation mention` - - These update session state only. Use config for persistence. - - Persistent config example: + Session-level toggles (state only, not persisted): `/activation always`, `/activation mention`. Use config for persistence: ```json5 { @@ -280,60 +247,47 @@ curl "https://api.telegram.org/bot/getUpdates" } ``` - Group history context is always on for groups and bounded by - `historyLimit`. Set `channels.telegram.historyLimit: 0` to disable the - Telegram group history window. The retired `includeGroupHistoryContext` - key is removed by `openclaw doctor --fix`. + Group history context is always on and bounded by `historyLimit`. Set `channels.telegram.historyLimit: 0` to disable the group history window. `openclaw doctor --fix` removes the retired `includeGroupHistoryContext` key. - Getting the group chat ID: - - - forward a group message to `@userinfobot` / `@getidsbot` - - or read `chat.id` from `openclaw logs --follow` - - or inspect Bot API `getUpdates` - - after the group is allowed, run `/whoami@` if native commands are enabled + Getting the group chat ID: forward a group message to `@userinfobot` / `@getidsbot`, read `chat.id` from `openclaw logs --follow`, inspect Bot API `getUpdates`, or (once the group is allowed) run `/whoami@`. ## Runtime behavior -- Telegram is owned by the gateway process. +- Telegram runs inside the gateway process. - Routing is deterministic: Telegram inbound replies back to Telegram (the model does not pick channels). -- Inbound messages normalize into the shared channel envelope with reply metadata, media placeholders, and persisted reply-chain context for Telegram replies the gateway has observed. -- Group sessions are isolated by group ID. Forum topics append `:topic:` to keep topics isolated. +- Inbound messages normalize into the shared channel envelope with reply metadata, media placeholders, and persisted reply-chain context for replies the gateway has observed. +- Group sessions are isolated by group ID. Forum topics append `:topic:`. - DM messages can carry `message_thread_id`; OpenClaw preserves it for replies. DM topic sessions split only when Telegram `getMe` reports `has_topics_enabled: true` for the bot; otherwise DMs stay on the flat session. -- Long polling uses grammY runner with per-chat/per-thread sequencing. Overall runner sink concurrency uses `agents.defaults.maxConcurrent`. -- Multi-account startup bounds concurrent Telegram `getMe` probes so large bot fleets do not fan out every account probe at once. -- Long polling is guarded inside each gateway process so only one active poller can use a bot token at a time. If you still see `getUpdates` 409 conflicts, another OpenClaw gateway, script, or external poller is likely using the same token. -- Long-polling watchdog restarts trigger after 120 seconds without completed `getUpdates` liveness by default. Increase `channels.telegram.pollingStallThresholdMs` only if your deployment still sees false polling-stall restarts during long-running work. The value is in milliseconds and is allowed from `30000` to `600000`; per-account overrides are supported. +- Long polling uses the grammY runner with per-chat/per-thread sequencing. Runner sink concurrency uses `agents.defaults.maxConcurrent`. +- Multi-account startup bounds concurrent `getMe` probes so large bot fleets do not fan out every account probe at once. +- Each gateway process guards long polling so only one active poller can use a bot token at a time. Persistent `getUpdates` 409 conflicts point to another OpenClaw gateway, script, or external poller using the same token. +- The polling watchdog restarts after 120 seconds without completed `getUpdates` liveness by default. Raise `channels.telegram.pollingStallThresholdMs` (30000-600000, per-account overrides supported) only if your deployment sees false polling-stall restarts during long-running work. - Telegram Bot API has no read-receipt support (`sendReadReceipts` does not apply). - `channels.telegram.dm.threadReplies` and `channels.telegram.direct..threadReplies` were removed. Run `openclaw doctor --fix` after upgrading if your config still has those keys. DM topic routing now follows the bot capability from Telegram `getMe.has_topics_enabled`, which is controlled by BotFather threaded mode: topics-enabled bots use thread-scoped DM sessions when Telegram sends `message_thread_id`; other DMs stay on the flat session. + `channels.telegram.dm.threadReplies` and `channels.telegram.direct..threadReplies` were removed. Run `openclaw doctor --fix` after upgrading if your config still has those keys. DM topic routing now follows Telegram `getMe.has_topics_enabled` (controlled by BotFather threaded mode): topics-enabled bots use thread-scoped DM sessions when Telegram sends `message_thread_id`; other DMs stay on the flat session. ## Feature reference - OpenClaw can stream partial replies in real time: - - - direct chats: preview message + `editMessageText` - - groups/topics: preview message + `editMessageText` - - Requirement: + OpenClaw streams partial replies in real time in direct chats, groups, and topics: send a preview message, then `editMessageText` repeatedly, finalizing in place. - `channels.telegram.streaming` is `off | partial | block | progress` (default: `partial`) - short initial answer previews are debounced, then materialized after a bounded delay if the run is still active - `progress` keeps one editable status draft for tool progress, shows the stable status label when answer activity arrives before tool progress, clears it at completion, and sends the final answer as a normal message - `streaming.preview.toolProgress` controls whether tool/progress updates reuse the same edited preview message (default: `true` when preview streaming is active) - - `streaming.preview.commandText` controls command/exec detail inside those tool-progress lines: `raw` (default, preserves released behavior) or `status` (tool label only) + - `streaming.preview.commandText` controls command/exec detail inside those lines: `raw` (default) or `status` (tool label only) - `streaming.progress.commentary` (default: `false`) opts into assistant commentary/preamble text in the temporary progress draft - - legacy `channels.telegram.streamMode`, boolean `streaming` values, and retired native draft preview keys are detected; run `openclaw doctor --fix` to migrate them to current streaming config + - legacy `channels.telegram.streamMode`, boolean `streaming` values, and retired native draft preview keys are detected; run `openclaw doctor --fix` to migrate them - Tool-progress preview updates are the short status lines shown while tools run, for example command execution, file reads, planning updates, patch summaries, or Codex preamble/commentary text in Codex app-server mode. Telegram keeps these enabled by default to match released OpenClaw behavior from `v2026.4.22` and later. + Tool-progress lines are the short status updates shown while tools run (command execution, file reads, planning updates, patch summaries, Codex preamble/commentary in app-server mode). Telegram keeps these on by default (matches released behavior from `v2026.4.22`+). - To keep the edited preview for answer text but hide tool-progress lines, set: + Keep answer-preview edits but hide tool-progress lines: ```json { @@ -341,16 +295,14 @@ curl "https://api.telegram.org/bot/getUpdates" "telegram": { "streaming": { "mode": "partial", - "preview": { - "toolProgress": false - } + "preview": { "toolProgress": false } } } } } ``` - To keep tool-progress visible but hide command/exec text, set: + Keep tool-progress visible but hide command/exec text: ```json { @@ -358,16 +310,14 @@ curl "https://api.telegram.org/bot/getUpdates" "telegram": { "streaming": { "mode": "partial", - "preview": { - "commandText": "status" - } + "preview": { "commandText": "status" } } } } } ``` - Use `progress` mode when you want visible tool progress without editing the final answer into that same message. Put the command-text policy under `streaming.progress`: + `progress` mode shows tool progress without editing the final answer into that message. Put the command-text policy under `streaming.progress`: ```json { @@ -385,35 +335,24 @@ curl "https://api.telegram.org/bot/getUpdates" } ``` - Use `streaming.mode: "off"` only when you want final-only delivery: Telegram preview edits are disabled and generic tool/progress chatter is suppressed instead of being sent as standalone status messages. Approval prompts, media payloads, and errors still route through normal final delivery. Use `streaming.preview.toolProgress: false` when you only want to keep answer preview edits while hiding the tool-progress status lines. + `streaming.mode: "off"` disables preview edits and suppresses generic tool/progress chatter instead of sending it as standalone status messages; approval prompts, media, and errors still route through normal final delivery. `streaming.preview.toolProgress: false` keeps only answer-preview edits. - Telegram selected quote replies are the exception. When `replyToMode` is `"first"`, `"all"`, or `"batched"` and the inbound message includes selected quote text, OpenClaw sends the final answer through Telegram's native quote-reply path instead of editing the answer preview, so `streaming.preview.toolProgress` cannot show the short status lines for that turn. Current-message replies without selected quote text still keep preview streaming. Set `replyToMode: "off"` when tool-progress visibility matters more than native quote replies, or set `streaming.preview.toolProgress: false` to acknowledge the trade-off. + Selected quote replies are the exception. When `replyToMode` is `first`, `all`, or `batched` and the inbound message has selected quote text, OpenClaw sends the final answer through Telegram's native quote-reply path instead of editing the answer preview, so `streaming.preview.toolProgress` cannot show status lines that turn. Current-message replies without selected quote text still stream. Set `replyToMode: "off"` when tool-progress visibility matters more than native quote replies, or `streaming.preview.toolProgress: false` to accept that trade-off. - For text-only replies: + For text-only replies: short previews get the final edit in place; long finals that split into multiple messages reuse the preview as the first chunk, then send only the remainder; progress-mode finals clear the status draft and use normal final delivery; if the final edit fails before completion is confirmed, OpenClaw falls back to normal final delivery and cleans up the stale preview. For complex replies (media payloads), OpenClaw always falls back to normal final delivery and cleans up the preview. - - short DM/group/topic previews: OpenClaw keeps the same preview message and performs the final edit in place - - long text finals that split into multiple Telegram messages reuse the existing preview as the first final chunk when possible, then send only the remaining chunks - - progress-mode finals clear the status draft and use normal final delivery instead of editing the draft into the answer - - if the final edit fails before the completed text is confirmed, OpenClaw uses normal final delivery and cleans up the stale preview + Preview streaming and block streaming are mutually exclusive — when block streaming is explicitly enabled, OpenClaw skips the preview stream to avoid double-streaming. - For complex replies (for example media payloads), OpenClaw falls back to normal final delivery and then cleans up the preview message. - - Preview streaming is separate from block streaming. When block streaming is explicitly enabled for Telegram, OpenClaw skips the preview stream to avoid double-streaming. - - Reasoning stream behavior: - - - `/reasoning stream` uses a supported channel's reasoning-preview path; on Telegram, it streams reasoning into the live preview while generating - - the reasoning preview is deleted after final delivery; use `/reasoning on` when reasoning should remain visible - - final answer is sent without reasoning text + Reasoning: `/reasoning stream` streams reasoning into the live preview while generating, then deletes the reasoning preview after final delivery (use `/reasoning on` to keep it visible). The final answer is sent without reasoning text. - Outbound text uses standard Telegram HTML messages by default so replies remain readable across current Telegram clients. This compatibility mode supports normal bold, italic, links, code, spoilers, and quotes, but not Bot API 10.1 rich-only blocks such as native tables, details, rich media, and formulas. + Outbound text uses standard Telegram HTML messages by default, readable across current clients: bold, italic, links, code, spoilers, quotes — not Bot API 10.1 rich-only blocks (native tables, details, rich media, formulas). - Set `channels.telegram.richMessages: true` to opt into Bot API 10.1 rich messages: + Opt into Bot API 10.1 rich messages: ```json5 { @@ -425,27 +364,18 @@ curl "https://api.telegram.org/bot/getUpdates" } ``` - When enabled: + When enabled: the agent is told rich messages are available for this bot/account; Markdown text renders through OpenClaw's Markdown IR as Telegram rich HTML; explicit rich HTML payloads preserve supported Bot API 10.1 tags (headings, tables, details, rich media, formulas); media captions still use Telegram HTML captions (rich messages do not replace captions, and captions cap at 1024 characters). - - The agent is told that Telegram rich messages are available for this bot/account. - - Markdown text is rendered through OpenClaw's Markdown IR and sent as Telegram rich HTML. - - Explicit rich HTML payloads preserve supported Bot API 10.1 tags such as headings, tables, details, rich media, and formulas. - - Media captions still use Telegram HTML captions because rich messages do not replace captions. + This keeps model text away from Telegram's rich-Markdown sigils, so currency like `$400-600K` is not parsed as math. Long rich text splits automatically across Telegram's limits. Tables over the 20-column limit fall back to a code block. - This keeps model text away from Telegram Rich Markdown sigils, so currency like `$400-600K` is not parsed as math. Long rich text is split automatically across Telegram's rich text and rich block limits. Tables over Telegram's column limit are sent as code blocks. + Default: off, for client compatibility — some current Desktop, Web, Android, and third-party clients render accepted rich messages as unsupported. Keep this off unless every client used with the bot can render them. `/status` shows whether the current session has rich messages on or off. - Default: off for client compatibility. Rich messages require compatible Telegram clients; some current Desktop, Web, Android, and third-party clients display accepted rich messages as unsupported. Keep this option disabled unless every client used with the bot can render them. `/status` shows whether the current Telegram session has rich messages on or off. - - Link previews are enabled by default. `channels.telegram.linkPreview: false` skips automatic entity detection for rich text. + Link previews are on by default. `channels.telegram.linkPreview: false` disables automatic entity detection for rich text. - Telegram command menu registration is handled at startup with `setMyCommands`. - - Native command defaults: - - - `commands.native: "auto"` enables native commands for Telegram + Telegram's command menu is registered at startup with `setMyCommands`. `commands.native: "auto"` enables native commands for Telegram. Add custom command menu entries: @@ -462,44 +392,29 @@ curl "https://api.telegram.org/bot/getUpdates" } ``` - Rules: + Rules: names are normalized (strip leading `/`, lowercase); valid pattern `a-z`, `0-9`, `_`, length 1-32; custom commands cannot override native commands; conflicts/duplicates are skipped and logged. - - names are normalized (strip leading `/`, lowercase) - - valid pattern: `a-z`, `0-9`, `_`, length `1..32` - - custom commands cannot override native commands - - conflicts/duplicates are skipped and logged - - Notes: - - - custom commands are menu entries only; they do not auto-implement behavior - - plugin/skill commands can still work when typed even if not shown in Telegram menu - - If native commands are disabled, built-ins are removed. Custom/plugin commands may still register if configured. + Custom commands are menu entries only — they do not auto-implement behavior. Plugin/skill commands can still work when typed even if not shown in the Telegram menu. If native commands are disabled, built-ins are removed; custom/plugin commands may still register if configured. Common setup failures: - - `setMyCommands failed` with `BOT_COMMANDS_TOO_MUCH` means the Telegram menu still overflowed after trimming; reduce plugin/skill/custom commands or disable `channels.telegram.commands.native`. - - `deleteWebhook`, `deleteMyCommands`, or `setMyCommands` failing with `404: Not Found` while direct Bot API curl commands work can mean `channels.telegram.apiRoot` was set to the full `/bot` endpoint. `apiRoot` must be only the Bot API root, and `openclaw doctor --fix` removes an accidental trailing `/bot`. - - `getMe returned 401` means Telegram rejected the configured bot token. Update `botToken`, `tokenFile`, or `TELEGRAM_BOT_TOKEN` with the current BotFather token; OpenClaw stops before polling so this is not reported as a webhook cleanup failure. + - `setMyCommands failed` with `BOT_COMMANDS_TOO_MUCH` after a trim retry means the menu still overflows; reduce plugin/skill/custom commands or disable `channels.telegram.commands.native`. + - `deleteWebhook`, `deleteMyCommands`, or `setMyCommands` failing with `404: Not Found` while direct Bot API curl commands work usually means `channels.telegram.apiRoot` was set to the full `/bot` endpoint. `apiRoot` must be the Bot API root only; `openclaw doctor --fix` removes an accidental trailing `/bot`. + - `getMe returned 401` means Telegram rejected the configured bot token. Update `botToken`, `tokenFile`, or `TELEGRAM_BOT_TOKEN` (default account) with the current BotFather token; OpenClaw stops before polling so this is not reported as a webhook cleanup failure. - `setMyCommands failed` with network/fetch errors usually means outbound DNS/HTTPS to `api.telegram.org` is blocked. ### Device pairing commands (`device-pair` plugin) - When the `device-pair` plugin is installed: + When installed: - 1. `/pair` generates setup code - 2. paste code in iOS app + 1. `/pair` generates a setup code + 2. paste the code in the iOS app 3. `/pair pending` lists pending requests (including role/scopes) - 4. approve the request: - - `/pair approve ` for explicit approval - - `/pair approve` when there is only one pending request - - `/pair approve latest` for most recent + 4. approve: `/pair approve `, `/pair approve` (only pending request), or `/pair approve latest` - The setup code carries a short-lived bootstrap token. Built-in setup-code bootstrap returns a durable node token with `scopes: []` plus a bounded operator handoff token for trusted mobile onboarding. That operator token can read setup-time native configuration, but it does not grant pairing mutation scopes or `operator.admin`. + If a device retries with changed auth details (role, scopes, public key), the previous pending request is superseded with a new `requestId`; re-run `/pair pending` before approving. - If a device retries with changed auth details (for example role/scopes/public key), the previous pending request is superseded and the new request uses a different `requestId`. Re-run `/pair pending` before approving. - - More details: [Pairing](/channels/pairing#pair-via-telegram-recommended-for-ios). + More detail: [Pairing](/channels/pairing#pair-via-telegram). @@ -536,15 +451,7 @@ curl "https://api.telegram.org/bot/getUpdates" } ``` - Scopes: - - - `off` - - `dm` - - `group` - - `all` - - `allowlist` (default) - - Legacy `capabilities: ["inlineButtons"]` maps to `inlineButtons: "all"`. + Scopes: `off`, `dm`, `group`, `all`, `allowlist` (default). Legacy `capabilities: ["inlineButtons"]` maps to `"all"`. Message action example: @@ -583,17 +490,14 @@ curl "https://api.telegram.org/bot/getUpdates" } ``` - Telegram `web_app` buttons work only in private chats between a user and the - bot. + `web_app` buttons only work in private chats between a user and the bot. - Callback clicks that are not claimed by a registered plugin interactive - handler are passed to the agent as text: - `callback_data: ` + Callback clicks not claimed by a registered plugin interactive handler are passed to the agent as text: `callback_data: `. - Telegram tool actions include: + Actions: - `sendMessage` (`to`, `content`, optional `mediaUrl`, `replyToMessageId`, `messageThreadId`) - `react` (`chatId`, `messageId`, `emoji`) @@ -601,58 +505,37 @@ curl "https://api.telegram.org/bot/getUpdates" - `editMessage` (`chatId`, `messageId`, `content` or `caption`, optional `presentation` inline buttons; button-only edits update reply markup) - `createForumTopic` (`chatId`, `name`, optional `iconColor`, `iconCustomEmojiId`) - Channel message actions expose ergonomic aliases (`send`, `react`, `delete`, `edit`, `sticker`, `sticker-search`, `topic-create`). + Ergonomic aliases: `send`, `react`, `delete`, `edit`, `sticker`, `sticker-search`, `topic-create`. - Gating controls: + Gating: `channels.telegram.actions.sendMessage`, `deleteMessage`, `reactions`, `sticker` (default: disabled). `edit`, `createForumTopic`, and `editForumTopic` are enabled by default with no dedicated toggle. + Runtime sends use the active config/secrets snapshot from startup/reload, so action paths do not re-resolve `SecretRef` values per send. - - `channels.telegram.actions.sendMessage` - - `channels.telegram.actions.deleteMessage` - - `channels.telegram.actions.reactions` - - `channels.telegram.actions.sticker` (default: disabled) - - Note: `edit` and `topic-create` are currently enabled by default and do not have separate `channels.telegram.actions.*` toggles. - Runtime sends use the active config/secrets snapshot (startup/reload), so action paths do not perform ad-hoc SecretRef re-resolution per send. - - Reaction removal semantics: [/tools/reactions](/tools/reactions) + Reaction removal semantics: [/tools/reactions](/tools/reactions). - Telegram supports explicit reply threading tags in generated output: + Explicit reply threading tags in generated output: - - `[[reply_to_current]]` replies to the triggering message - - `[[reply_to:]]` replies to a specific Telegram message ID + - `[[reply_to_current]]` — replies to the triggering message + - `[[reply_to:]]` — replies to a specific message ID - `channels.telegram.replyToMode` controls handling: + `channels.telegram.replyToMode`: `off` (default), `first`, `all`. - - `off` (default) - - `first` - - `all` + When reply threading is enabled and the original text/caption is available, OpenClaw adds a native quote excerpt automatically. Telegram caps native quote text at 1024 UTF-16 code units; longer messages are quoted from the start and fall back to a plain reply if Telegram rejects the quote. - When reply threading is enabled and the original Telegram text or caption is available, OpenClaw includes a native Telegram quote excerpt automatically. Telegram caps native quote text at 1024 UTF-16 code units, so longer messages are quoted from the start and fall back to a plain reply if Telegram rejects the quote. - - Note: `off` disables implicit reply threading. Explicit `[[reply_to_*]]` tags are still honored. + `off` disables implicit reply threading only; explicit `[[reply_to_*]]` tags are still honored. - Forum supergroups: + Forum supergroups: topic session keys append `:topic:`; replies and typing target the topic thread; topic config path is `channels.telegram.groups..topics.`. - - topic session keys append `:topic:` - - replies and typing target the topic thread - - topic config path: - `channels.telegram.groups..topics.` + General topic (`threadId=1`) is a special case: message sends omit `message_thread_id` (Telegram rejects `sendMessage(...thread_id=1)` with "thread not found"), but typing actions still include `message_thread_id` (empirically required for the typing indicator to appear). - General topic (`threadId=1`) special-case: + Topic entries inherit group settings unless overridden (`requireMention`, `allowFrom`, `skills`, `systemPrompt`, `enabled`, `groupPolicy`). `agentId` is topic-only and does not inherit from group defaults. `topics."*"` sets defaults for every topic in that group; exact topic IDs still win over `"*"`. - - message sends omit `message_thread_id` (Telegram rejects `sendMessage(...thread_id=1)`) - - typing actions still include `message_thread_id` - - Topic inheritance: topic entries inherit group settings unless overridden (`requireMention`, `allowFrom`, `skills`, `systemPrompt`, `enabled`, `groupPolicy`). - `agentId` is topic-only and does not inherit from group defaults. - `topics."*"` sets defaults for every topic in that group; exact topic IDs still win over `"*"`. - - **Per-topic agent routing**: Each topic can route to a different agent by setting `agentId` in the topic config. This gives each topic its own isolated workspace, memory, and session. Example: + **Per-topic agent routing**: each topic can route to a different agent via `agentId` in the topic config, giving it its own workspace, memory, and session: ```json5 { @@ -661,9 +544,9 @@ curl "https://api.telegram.org/bot/getUpdates" groups: { "-1001234567890": { topics: { - "1": { agentId: "main" }, // General topic → main agent - "3": { agentId: "zu" }, // Dev topic → zu agent - "5": { agentId: "coder" } // Code review → coder agent + "1": { agentId: "main" }, // General topic -> main agent + "3": { agentId: "zu" }, // Dev topic -> zu agent + "5": { agentId: "coder" } // Code review -> coder agent } } } @@ -672,29 +555,21 @@ curl "https://api.telegram.org/bot/getUpdates" } ``` - Each topic then has its own session key: `agent:zu:telegram:group:-1001234567890:topic:3` + Each topic then has its own session key, for example `agent:zu:telegram:group:-1001234567890:topic:3`. - **Persistent ACP topic binding**: Forum topics can pin ACP harness sessions through top-level typed ACP bindings (`bindings[]` with `type: "acp"` and `match.channel: "telegram"`, `peer.kind: "group"`, and a topic-qualified id like `-1001234567890:topic:42`). Currently scoped to forum topics in groups/supergroups. See [ACP Agents](/tools/acp-agents). + **Persistent ACP topic binding**: forum topics can pin ACP harness sessions through top-level typed bindings (`bindings[]` with `type: "acp"`, `match.channel: "telegram"`, `peer.kind: "group"`, and a topic-qualified id like `-1001234567890:topic:42`). Currently scoped to forum topics in groups/supergroups. See [ACP Agents](/tools/acp-agents). - **Thread-bound ACP spawn from chat**: `/acp spawn --thread here|auto` binds the current topic to a new ACP session; follow-ups route there directly. OpenClaw pins the spawn confirmation in-topic. Requires `channels.telegram.threadBindings.spawnSessions` to remain enabled (default: `true`). + **Thread-bound ACP spawn from chat**: `/acp spawn --thread here|auto` binds the current topic to a new ACP session; follow-ups route there directly, and OpenClaw pins the spawn confirmation in-topic. Requires `channels.telegram.threadBindings.spawnSessions` (default: `true`). - Template context exposes `MessageThreadId` and `IsForum`. DM chats with `message_thread_id` keep reply metadata; they use thread-aware session keys only when Telegram `getMe` reports `has_topics_enabled: true` for the bot. - The former `dm.threadReplies` and `direct.*.threadReplies` overrides are intentionally retired; use BotFather threaded mode as the single source of truth and run `openclaw doctor --fix` to remove stale config keys. + Template context exposes `MessageThreadId` and `IsForum`. DM chats with `message_thread_id` keep reply metadata but only use thread-aware session keys when Telegram `getMe` reports `has_topics_enabled: true`. + The retired `dm.threadReplies` and `direct.*.threadReplies` overrides are gone; BotFather threaded mode is the single source of truth. Run `openclaw doctor --fix` to remove stale config keys. ### Audio messages - Telegram distinguishes voice notes vs audio files. - - - default: audio file behavior - - tag `[[audio_as_voice]]` in agent reply to force voice-note send - - inbound voice-note transcripts are framed as machine-generated, - untrusted text in the agent context; mention detection still uses the raw - transcript so mention-gated voice messages continue to work. - - Message action example: + Telegram distinguishes voice notes from audio files. Default: audio-file behavior; tag `[[audio_as_voice]]` in the agent reply to force a voice-note send. Inbound voice-note transcripts are framed as machine-generated, untrusted text in agent context, but mention detection still uses the raw transcript so mention-gated voice messages keep working. ```json5 { @@ -708,9 +583,7 @@ curl "https://api.telegram.org/bot/getUpdates" ### Video messages - Telegram distinguishes video files vs video notes. - - Message action example: + Telegram distinguishes video files from video notes. Video notes do not support captions; provided message text sends separately. ```json5 { @@ -722,25 +595,11 @@ curl "https://api.telegram.org/bot/getUpdates" } ``` - Video notes do not support captions; provided message text is sent separately. - ### Stickers - Inbound sticker handling: + Inbound: static WEBP is downloaded and processed (placeholder ``); animated TGS and video WEBM are skipped. - - static WEBP: downloaded and processed (placeholder ``) - - animated TGS: skipped - - video WEBM: skipped - - Sticker context fields: - - - `Sticker.emoji` - - `Sticker.setName` - - `Sticker.fileId` - - `Sticker.fileUniqueId` - - `Sticker.cachedDescription` - - Sticker descriptions are cached in OpenClaw SQLite plugin state to reduce repeated vision calls. + Sticker context fields: `Sticker.emoji`, `Sticker.setName`, `Sticker.fileId`, `Sticker.fileUniqueId`, `Sticker.cachedDescription`. Descriptions are cached in OpenClaw SQLite plugin state to reduce repeated vision calls. Enable sticker actions: @@ -756,7 +615,7 @@ curl "https://api.telegram.org/bot/getUpdates" } ``` - Send sticker action: + Send: ```json5 { @@ -781,63 +640,43 @@ curl "https://api.telegram.org/bot/getUpdates" - Telegram reactions arrive as `message_reaction` updates (separate from message payloads). - - When enabled, OpenClaw enqueues system events like: - - - `Telegram reaction added: 👍 by Alice (@alice) on msg 42` - - Config: + Telegram reactions arrive as `message_reaction` updates, separate from message payloads. When enabled, OpenClaw enqueues system events like `Telegram reaction added: 👍 by Alice (@alice) on msg 42`. - `channels.telegram.reactionNotifications`: `off | own | all` (default: `own`) - `channels.telegram.reactionLevel`: `off | ack | minimal | extensive` (default: `minimal`) - Notes: + `own` means user reactions to bot-sent messages only (best-effort via a sent-message cache). Reaction events still respect Telegram access controls (`dmPolicy`, `allowFrom`, `groupPolicy`, `groupAllowFrom`); unauthorized senders are dropped. - - `own` means user reactions to bot-sent messages only (best-effort via sent-message cache). - - Reaction events still respect Telegram access controls (`dmPolicy`, `allowFrom`, `groupPolicy`, `groupAllowFrom`); unauthorized senders are dropped. - - Telegram does not provide thread IDs in reaction updates. - - non-forum groups route to group chat session - - forum groups route to the group general-topic session (`:topic:1`), not the exact originating topic + Telegram does not provide thread IDs in reaction updates: non-forum groups route to the group chat session; forum groups route to the general-topic session (`:topic:1`), not the exact originating topic. `allowed_updates` for polling/webhook include `message_reaction` automatically. - `ackReaction` sends an acknowledgement emoji while OpenClaw is processing an inbound message. `ackReactionScope` decides *when* that emoji is actually sent. + `ackReaction` sends an acknowledgement emoji while OpenClaw processes an inbound message. `messages.ackReactionScope` decides *when* it is sent. - **Emoji (`ackReaction`) resolution order:** + **Emoji resolution order:** - `channels.telegram.accounts..ackReaction` - `channels.telegram.ackReaction` - `messages.ackReaction` - agent identity emoji fallback (`agents.list[].identity.emoji`, else "👀") - Notes: + Telegram expects a unicode emoji (for example "👀"); use `""` to disable the reaction for a channel or account. - - Telegram expects unicode emoji (for example "👀"). - - Use `""` to disable the reaction for a channel or account. + **Scope (`messages.ackReactionScope`, default `"group-mentions"`; no Telegram-account or Telegram-channel override today):** - **Scope (`messages.ackReactionScope`):** - - The Telegram provider reads scope from `messages.ackReactionScope` (default `"group-mentions"`). There is no Telegram-account or Telegram-channel-level override today. - - Values: `"all"` (DMs + groups), `"direct"` (DMs only), `"group-all"` (every group message, no DMs), `"group-mentions"` (groups when the bot is mentioned; **no DMs** — this is the default), `"off"` / `"none"` (disabled). + `all` (DMs + groups), `direct` (DMs only), `group-all` (every group message, no DMs), `group-mentions` (groups when the bot is mentioned; **no DMs** — default), `off` / `none` (disabled). - The default scope (`"group-mentions"`) does not fire ack reactions in direct messages. To get an ack reaction on inbound Telegram DMs, set `messages.ackReactionScope` to `"direct"` or `"all"`. The value is read at Telegram provider startup, so a gateway restart is needed for the change to take effect. + The default scope (`group-mentions`) does not fire ack reactions in DMs. Set `messages.ackReactionScope` to `direct` or `all` for that. This value is read at Telegram provider startup, so a gateway restart is needed for the change to take effect. - Channel config writes are enabled by default (`configWrites !== false`). - - Telegram-triggered writes include: - - - group migration events (`migrate_to_chat_id`) to update `channels.telegram.groups` - - `/config set` and `/config unset` (requires command enablement) + Channel config writes are enabled by default (`configWrites !== false`). Telegram-triggered writes include group migration events (`migrate_to_chat_id`, updates `channels.telegram.groups`) and `/config set` / `/config unset` (requires command enablement). Disable: @@ -854,33 +693,29 @@ curl "https://api.telegram.org/bot/getUpdates" - Default is long polling. For webhook mode set `channels.telegram.webhookUrl` and `channels.telegram.webhookSecret`; optional `webhookPath`, `webhookHost`, `webhookPort` (defaults `/telegram-webhook`, `127.0.0.1`, `8787`). + Default is long polling. For webhook mode, set `channels.telegram.webhookUrl` and `channels.telegram.webhookSecret`; optional `webhookPath` (default `/telegram-webhook`), `webhookHost` (default `127.0.0.1`), `webhookPort` (default `8787`), `webhookCertPath` (self-signed cert PEM for direct-IP or no-domain setups). - In long-polling mode OpenClaw persists its restart watermark only after an update dispatches successfully. If a handler fails, that update remains retryable in the same process and is not written as completed for restart dedupe. + In long-polling mode, OpenClaw persists its restart watermark only after an update dispatches successfully; a failed handler leaves that update retryable in the same process instead of marking it completed. - The local listener binds to `127.0.0.1:8787`. For public ingress, either put a reverse proxy in front of the local port or set `webhookHost: "0.0.0.0"` intentionally. + The local listener binds to `127.0.0.1:8787` by default. For public ingress, put a reverse proxy in front of the local port, or set `webhookHost: "0.0.0.0"` intentionally. - Webhook mode validates request guards, the Telegram secret token, and the JSON body before returning `200` to Telegram. - OpenClaw then processes the update asynchronously through the same per-chat/per-topic bot lanes used by long polling, so slow agent turns do not hold Telegram's delivery ACK. + Webhook mode validates request guards, the Telegram secret token, and the JSON body before returning `200`. OpenClaw then processes the update asynchronously through the same per-chat/per-topic bot lanes used by long polling, so slow agent turns do not hold Telegram's delivery ACK. - - `channels.telegram.textChunkLimit` default is 4000. - - `channels.telegram.chunkMode="newline"` prefers paragraph boundaries (blank lines) before length splitting. - - `channels.telegram.mediaMaxMb` (default 100) caps inbound and outbound Telegram media size. - - `channels.telegram.mediaGroupFlushMs` (default 500) controls how long Telegram albums/media groups are buffered before OpenClaw dispatches them as one inbound message. Increase it if album parts arrive late; decrease it to reduce album reply latency. - - `channels.telegram.timeoutSeconds` overrides Telegram API client timeout (if unset, grammY default applies). Bot clients clamp configured values below the 60-second outbound text/typing request guard so grammY does not abort visible reply delivery before OpenClaw's transport guard and fallback can run. Long polling still uses a 45-second `getUpdates` request guard so idle polls are not abandoned indefinitely. - - `channels.telegram.pollingStallThresholdMs` defaults to `120000`; tune between `30000` and `600000` only for false-positive polling-stall restarts. + - `channels.telegram.textChunkLimit` default 4000; `chunkMode="newline"` prefers paragraph boundaries (blank lines) before length splitting. + - `channels.telegram.mediaMaxMb` (default 100) caps inbound and outbound media size. + - `channels.telegram.mediaGroupFlushMs` (default 500, range 10-60000) controls how long albums/media groups are buffered before OpenClaw dispatches them as one inbound message. Increase it if album parts arrive late; decrease it to reduce album reply latency. + - `channels.telegram.timeoutSeconds` overrides the API client timeout (grammY default applies if unset). Bot clients clamp configured values below the 60-second outbound text/typing request guard so grammY does not abort visible reply delivery before OpenClaw's transport guard and fallback can run. Long polling still uses a 45-second `getUpdates` request guard so idle polls are not abandoned indefinitely. + - `channels.telegram.pollingStallThresholdMs` defaults to 120000; tune between 30000 and 600000 only for false-positive polling-stall restarts. - group context history uses `channels.telegram.historyLimit` or `messages.groupChat.historyLimit` (default 50); `0` disables. - - reply/quote/forward supplemental context is normalized into one selected conversation context window when the gateway has observed the parent messages; the observed-message cache lives in OpenClaw SQLite plugin state, and `openclaw doctor --fix` imports legacy sidecars. Telegram only includes one shallow `reply_to_message` in updates, so chains older than the cache are limited to Telegram's current update payload. + - reply/quote/forward supplemental context normalizes into one selected conversation context window when the gateway has observed the parent messages; the observed-message cache lives in OpenClaw SQLite plugin state, and `openclaw doctor --fix` imports legacy sidecars. Telegram only includes one shallow `reply_to_message` per update, so chains older than the cache are limited to that payload. - Telegram allowlists primarily gate who can trigger the agent, not a full supplemental-context redaction boundary. - - DM history controls: - - `channels.telegram.dmHistoryLimit` - - `channels.telegram.dms[""].historyLimit` - - `channels.telegram.retry` config applies to Telegram send helpers (CLI/tools/actions) for recoverable outbound API errors. Inbound final-reply delivery also uses a bounded safe-send retry for Telegram pre-connect failures, but it does not retry ambiguous post-send network envelopes that could duplicate visible messages. + - DM history: `channels.telegram.dmHistoryLimit`, `channels.telegram.dms[""].historyLimit`. + - `channels.telegram.retry` applies to Telegram send helpers (CLI/tools/actions) for recoverable outbound API errors. Inbound final-reply delivery uses a bounded safe-send retry for pre-connect failures, but does not retry ambiguous post-send network envelopes that could duplicate visible messages. - CLI and message-tool send targets can be numeric chat ID, username, or a forum topic target: + CLI and message-tool send targets accept a numeric chat ID, username, or forum topic target: ```bash openclaw message send --channel telegram --target 123456789 --message "hi" @@ -888,7 +723,7 @@ openclaw message send --channel telegram --target @name --message "hi" openclaw message send --channel telegram --target -1001234567890:topic:42 --message "hi topic" ``` - Telegram polls use `openclaw message poll` and support forum topics: + Polls use `openclaw message poll` and support forum topics: ```bash openclaw message poll --channel telegram --target 123456789 \ @@ -898,39 +733,25 @@ openclaw message poll --channel telegram --target -1001234567890:topic:42 \ --poll-duration-seconds 300 --poll-public ``` - Telegram-only poll flags: + Telegram-only poll flags: `--poll-duration-seconds` (5-600), `--poll-anonymous`, `--poll-public`, `--thread-id` (or a `:topic:` target). `--poll-option` repeats 2-12 times (Telegram's option cap). - - `--poll-duration-seconds` (5-600) - - `--poll-anonymous` - - `--poll-public` - - `--thread-id` for forum topics (or use a `:topic:` target) + Telegram send also supports `--presentation` with `buttons` blocks for inline keyboards (when `channels.telegram.capabilities.inlineButtons` allows it), `--pin` or `--delivery '{"pin":true}'` to request pinned delivery when the bot can pin in that chat, and `--force-document` to send outbound images, GIFs, and videos as documents instead of compressed/animated/video uploads. - Telegram send also supports: - - - `--presentation` with `buttons` blocks for inline keyboards when `channels.telegram.capabilities.inlineButtons` allows it - - `--pin` or `--delivery '{"pin":true}'` to request pinned delivery when the bot can pin in that chat - - `--force-document` to send outbound images, GIFs, and videos as documents instead of compressed photo, animated-media, or video uploads - - Action gating: - - - `channels.telegram.actions.sendMessage=false` disables outbound Telegram messages, including polls - - `channels.telegram.actions.poll=false` disables Telegram poll creation while leaving regular sends enabled + Action gating: `channels.telegram.actions.sendMessage=false` disables all outbound messages including polls; `channels.telegram.actions.poll=false` disables poll creation while leaving regular sends enabled. Telegram supports exec approvals in approver DMs and can optionally post prompts in the originating chat or topic. Approvers must be numeric Telegram user IDs. - Config path: - - - `channels.telegram.execApprovals.enabled` (auto-enables when at least one approver is resolvable) + - `channels.telegram.execApprovals.enabled` (`"auto"` enables when at least one approver is resolvable) - `channels.telegram.execApprovals.approvers` (falls back to numeric owner IDs from `commands.ownerAllowFrom`) - `channels.telegram.execApprovals.target`: `dm` (default) | `channel` | `both` - `agentFilter`, `sessionFilter` - `channels.telegram.allowFrom`, `groupAllowFrom`, and `defaultTo` control who can talk to the bot and where it sends normal replies. They do not make someone an exec approver. The first approved DM pairing bootstraps `commands.ownerAllowFrom` when no command owner exists yet, so the one-owner setup still works without duplicating IDs under `execApprovals.approvers`. + `channels.telegram.allowFrom`, `groupAllowFrom`, and `defaultTo` control who can talk to the bot and where it sends normal replies — they do not make someone an exec approver. The first approved DM pairing bootstraps `commands.ownerAllowFrom` when no command owner exists yet, so one-owner setups work without duplicating IDs under `execApprovals.approvers`. - Channel delivery shows the command text in the chat; only enable `channel` or `both` in trusted groups/topics. When the prompt lands in a forum topic, OpenClaw preserves the topic for the approval prompt and the follow-up. Exec approvals expire after 30 minutes by default. + Channel delivery shows the command text in the chat; only enable `channel` or `both` in trusted groups/topics. When the prompt lands in a forum topic, OpenClaw preserves the topic for the approval prompt and follow-up. Exec approvals expire after 30 minutes by default. Inline approval buttons also require `channels.telegram.capabilities.inlineButtons` to allow the target surface (`dm`, `group`, or `all`). Approval IDs prefixed with `plugin:` resolve through plugin approvals; others resolve through exec approvals first. @@ -941,12 +762,12 @@ openclaw message poll --channel telegram --target -1001234567890:topic:42 \ ## Error reply controls -When the agent encounters a delivery or provider error, the error policy controls whether error messages are sent to the Telegram chat: +When the agent hits a delivery or provider error, the error policy controls whether error messages reach the Telegram chat: -| Key | Values | Default | Description | -| ----------------------------------- | -------------------------- | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `channels.telegram.errorPolicy` | `always`, `once`, `silent` | `always` | `always` — send every error message to the chat. `once` — send each unique error message once per cooldown window (suppress repeated identical errors). `silent` — never send error messages to the chat. | -| `channels.telegram.errorCooldownMs` | number (ms) | `14400000` (4h) | Cooldown window for the `once` policy. After an error is sent, the same error message is suppressed until this interval elapses. Prevents error spam during outages. | +| Key | Values | Default | Description | +| ----------------------------------- | -------------------------- | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `channels.telegram.errorPolicy` | `always`, `once`, `silent` | `always` | `always` sends every error message to the chat. `once` sends each unique error message once per cooldown window (suppresses repeated identical errors). `silent` never sends error messages to the chat. | +| `channels.telegram.errorCooldownMs` | number (ms) | `14400000` (4h) | Cooldown window for the `once` policy. After an error is sent, the same message is suppressed until this interval elapses. Prevents error spam during outages. | Per-account, per-group, and per-topic overrides are supported (same inheritance as other Telegram config keys). @@ -971,54 +792,50 @@ Per-account, per-group, and per-topic overrides are supported (same inheritance - - If `requireMention=false`, Telegram privacy mode must allow full visibility. - - BotFather: `/setprivacy` -> Disable - - then remove + re-add bot to group + - If `requireMention=false`, Telegram privacy mode must allow full visibility: BotFather `/setprivacy` -> Disable, then remove + re-add the bot to the group. - `openclaw channels status` warns when config expects unmentioned group messages. - - `openclaw channels status --probe` can check explicit numeric group IDs; wildcard `"*"` cannot be membership-probed. - - quick session test: `/activation always`. + - `openclaw channels status --probe` checks explicit numeric group IDs; wildcard `"*"` cannot be membership-probed. + - Quick session test: `/activation always`. - - when `channels.telegram.groups` exists, group must be listed (or include `"*"`) - - verify bot membership in group - - review logs: `openclaw logs --follow` for skip reasons + - When `channels.telegram.groups` exists, the group must be listed (or include `"*"`). + - Verify bot membership in the group. + - Review `openclaw logs --follow` for skip reasons. - - authorize your sender identity (pairing and/or numeric `allowFrom`) - - command authorization still applies even when group policy is `open` - - `setMyCommands failed` with `BOT_COMMANDS_TOO_MUCH` means the native menu has too many entries; reduce plugin/skill/custom commands or disable native menus - - `deleteMyCommands` / `setMyCommands` startup calls and `sendChatAction` typing calls are bounded and retry once through Telegram's transport fallback on request timeout. Persistent network/fetch errors usually indicate DNS/HTTPS reachability issues to `api.telegram.org` + - Authorize your sender identity (pairing and/or numeric `allowFrom`); command authorization still applies even when group policy is `open`. + - `setMyCommands failed` with `BOT_COMMANDS_TOO_MUCH` means the native menu has too many entries; reduce plugin/skill/custom commands or disable native menus. + - `deleteMyCommands` / `setMyCommands` startup calls and `sendChatAction` typing calls are bounded and retry once through Telegram's transport fallback on request timeout. Persistent network/fetch errors usually mean DNS/HTTPS to `api.telegram.org` is unreachable. - - `getMe returned 401` is a Telegram authentication failure for the configured bot token. - - Re-copy or regenerate the bot token in BotFather, then update `channels.telegram.botToken`, `channels.telegram.tokenFile`, `channels.telegram.accounts..botToken`, or `TELEGRAM_BOT_TOKEN` for the default account. - - `deleteWebhook 401 Unauthorized` during startup is also an auth failure; treating it as "no webhook exists" would only defer the same bad-token failure to later API calls. + - `getMe returned 401` is a Telegram auth failure for the configured bot token. Re-copy or regenerate the token in BotFather, then update `channels.telegram.botToken`, `tokenFile`, `accounts..botToken`, or `TELEGRAM_BOT_TOKEN` (default account). + - `deleteWebhook 401 Unauthorized` during startup is also an auth failure; treating it as "no webhook exists" would only defer the same bad-token failure to a later API call. - - Node 22+ + custom fetch/proxy can trigger immediate abort behavior if AbortSignal types mismatch. - - Some hosts resolve `api.telegram.org` to IPv6 first; broken IPv6 egress can cause intermittent Telegram API failures. - - If logs include `TypeError: fetch failed` or `Network request for 'getUpdates' failed!`, OpenClaw now retries these as recoverable network errors. + - Node 22+ with a custom fetch/proxy can trigger immediate abort behavior if `AbortSignal` types mismatch. + - Some hosts resolve `api.telegram.org` to IPv6 first; broken IPv6 egress causes intermittent API failures. + - Logs with `TypeError: fetch failed` or `Network request for 'getUpdates' failed!` are retried as recoverable network errors. - During polling startup, OpenClaw reuses the successful startup `getMe` probe for grammY so the runner does not need a second `getMe` before the first `getUpdates`. - - If `deleteWebhook` fails with a transient network error during polling startup, OpenClaw continues into long polling instead of making another pre-poll control-plane call. A still-active webhook surfaces as a `getUpdates` conflict; OpenClaw then rebuilds the Telegram transport and retries webhook cleanup. - - If Telegram sockets recycle on a short fixed cadence, check for a low `channels.telegram.timeoutSeconds`; bot clients clamp configured values below the outbound and `getUpdates` request guards, but older releases could abort every poll or reply when this was set below those guards. - - If logs include `Polling stall detected`, OpenClaw restarts polling and rebuilds the Telegram transport after 120 seconds without completed long-poll liveness by default. - - `openclaw channels status --probe` and `openclaw doctor` warn when a running polling account has not completed `getUpdates` after startup grace, when a running webhook account has not completed `setWebhook` after startup grace, or when the last successful polling transport activity is stale. - - Increase `channels.telegram.pollingStallThresholdMs` only when long-running `getUpdates` calls are healthy but your host still reports false polling-stall restarts. Persistent stalls usually point to proxy, DNS, IPv6, or TLS egress issues between the host and `api.telegram.org`. - - Telegram also honors process proxy env for Bot API transport, including `HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`, and their lowercase variants. `NO_PROXY` / `no_proxy` can still bypass `api.telegram.org`. - - If the OpenClaw managed proxy is configured through `OPENCLAW_PROXY_URL` for a service environment and no standard proxy env is present, Telegram uses that URL for Bot API transport too. - - On VPS hosts with unstable direct egress/TLS, route Telegram API calls through `channels.telegram.proxy`: + - If `deleteWebhook` fails with a transient network error during polling startup, OpenClaw continues into long polling instead of making another pre-poll control-plane call. A still-active webhook then surfaces as a `getUpdates` conflict; OpenClaw rebuilds the transport and retries webhook cleanup. + - If Telegram sockets recycle on a short fixed cadence, check for a low `channels.telegram.timeoutSeconds` — bot clients clamp configured values below the outbound and `getUpdates` request guards, but older releases could abort every poll or reply when this was set below those guards. + - `Polling stall detected` in logs means OpenClaw restarts polling and rebuilds the transport after 120 seconds without completed long-poll liveness by default. + - `openclaw channels status --probe` and `openclaw doctor` warn when a running polling account has not completed `getUpdates` after startup grace, a running webhook account has not completed `setWebhook` after startup grace, or the last successful polling transport activity is stale. + - Raise `channels.telegram.pollingStallThresholdMs` only when long-running `getUpdates` calls are healthy but your host still reports false polling-stall restarts. Persistent stalls usually point to proxy, DNS, IPv6, or TLS egress issues to `api.telegram.org`. + - Telegram honors process proxy env for Bot API transport: `HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`, and lowercase variants. `NO_PROXY` / `no_proxy` can still bypass `api.telegram.org`. + - If `OPENCLAW_PROXY_URL` is set for a service environment and no standard proxy env is present, Telegram uses that URL for Bot API transport too. + - On VPS hosts with unstable direct egress/TLS, route Telegram API calls through a proxy: ```yaml channels: @@ -1026,8 +843,8 @@ channels: proxy: socks5://:@proxy-host:1080 ``` - - Node 22+ defaults to `autoSelectFamily=true` (except WSL2). Telegram DNS result order honors `OPENCLAW_TELEGRAM_DNS_RESULT_ORDER`, then `channels.telegram.network.dnsResultOrder`, then the process default such as `NODE_OPTIONS=--dns-result-order=ipv4first`; if none applies, Node 22+ falls back to `ipv4first`. - - If your host is WSL2 or explicitly works better with IPv4-only behavior, force family selection: + - Node 22+ defaults to `autoSelectFamily=true` (except WSL2). Telegram DNS result order honors `OPENCLAW_TELEGRAM_DNS_RESULT_ORDER`, then `channels.telegram.network.dnsResultOrder`, then the process default (for example `NODE_OPTIONS=--dns-result-order=ipv4first`), falling back to `ipv4first` on Node 22+ if none applies. + - On WSL2, or when IPv4-only behavior works better, force family selection: ```yaml channels: @@ -1036,11 +853,7 @@ channels: autoSelectFamily: false ``` - - RFC 2544 benchmark-range answers (`198.18.0.0/15`) are already allowed - for Telegram media downloads by default. If a trusted fake-IP or - transparent proxy rewrites `api.telegram.org` to some other - private/internal/special-use address during media downloads, you can opt - in to the Telegram-only bypass: + - RFC 2544 benchmark-range answers (`198.18.0.0/15`) are already allowed for Telegram media downloads by default. If a trusted fake-IP or transparent proxy rewrites `api.telegram.org` to some other private/internal/special-use address during media downloads, opt in to the Telegram-only bypass: ```yaml channels: @@ -1049,24 +862,14 @@ channels: dangerouslyAllowPrivateNetwork: true ``` - - The same opt-in is available per account at - `channels.telegram.accounts..network.dangerouslyAllowPrivateNetwork`. - - If your proxy resolves Telegram media hosts into `198.18.x.x`, leave the - dangerous flag off first. Telegram media already allows the RFC 2544 - benchmark range by default. + - The same opt-in is available per account at `channels.telegram.accounts..network.dangerouslyAllowPrivateNetwork`. + - If your proxy resolves Telegram media hosts into `198.18.x.x`, leave the dangerous flag off first — that range is already allowed by default. - `channels.telegram.network.dangerouslyAllowPrivateNetwork` weakens Telegram - media SSRF protections. Use it only for trusted operator-controlled proxy - environments such as Clash, Mihomo, or Surge fake-IP routing when they - synthesize private or special-use answers outside the RFC 2544 benchmark - range. Leave it off for normal public internet Telegram access. + `channels.telegram.network.dangerouslyAllowPrivateNetwork` weakens Telegram media SSRF protections. Use it only for trusted operator-controlled proxy environments (Clash, Mihomo, Surge fake-IP routing) that synthesize private or special-use answers outside the RFC 2544 benchmark range. Leave it off for normal public internet Telegram access. - - Environment overrides (temporary): - - `OPENCLAW_TELEGRAM_DISABLE_AUTO_SELECT_FAMILY=1` - - `OPENCLAW_TELEGRAM_ENABLE_AUTO_SELECT_FAMILY=1` - - `OPENCLAW_TELEGRAM_DNS_RESULT_ORDER=ipv4first` + - Temporary environment overrides: `OPENCLAW_TELEGRAM_DISABLE_AUTO_SELECT_FAMILY=1`, `OPENCLAW_TELEGRAM_ENABLE_AUTO_SELECT_FAMILY=1`, `OPENCLAW_TELEGRAM_DNS_RESULT_ORDER=ipv4first`. - Validate DNS answers: ```bash @@ -1085,26 +888,26 @@ Primary reference: [Configuration reference - Telegram](/gateway/config-channels -- startup/auth: `enabled`, `botToken`, `tokenFile`, `accounts.*` (`tokenFile` must point to a regular file; symlinks are rejected) +- startup/auth: `enabled`, `botToken`, `tokenFile` (must be a regular file; symlinks are rejected), `accounts.*` - access control: `dmPolicy`, `allowFrom`, `groupPolicy`, `groupAllowFrom`, `groups`, `groups.*.topics.*`, top-level `bindings[]` (`type: "acp"`) - topic defaults: `groups..topics."*"` applies to unmatched forum topics; exact topic IDs override it - exec approvals: `execApprovals`, `accounts.*.execApprovals` - command/menu: `commands.native`, `commands.nativeSkills`, `customCommands` -- threading/replies: `replyToMode` -- streaming: `streaming` (preview), `streaming.preview.toolProgress`, `blockStreaming` -- formatting/delivery: `textChunkLimit`, `chunkMode`, `richMessages`, `linkPreview`, `responsePrefix` -- media/network: `mediaMaxMb`, `mediaGroupFlushMs`, `timeoutSeconds`, `pollingStallThresholdMs`, `retry`, `network.autoSelectFamily`, `network.dangerouslyAllowPrivateNetwork`, `proxy`, `trustedLocalFileRoots` -- custom API root: `apiRoot` (Bot API root only; do not include `/bot`) -- webhook: `webhookUrl`, `webhookSecret`, `webhookPath`, `webhookHost` -- actions/capabilities: `capabilities.inlineButtons`, `actions.sendMessage|editMessage|deleteMessage|reactions|sticker` +- threading/replies: `replyToMode`, `threadBindings` +- streaming: `streaming` (modes `off | partial | block | progress`), `streaming.preview.toolProgress` +- formatting/delivery: `textChunkLimit`, `chunkMode`, `richMessages`, `markdown.tables` (`off | bullets | code | block`), `linkPreview`, `responsePrefix` +- media/network: `mediaMaxMb`, `mediaGroupFlushMs`, `timeoutSeconds`, `pollingStallThresholdMs`, `retry`, `network.autoSelectFamily`, `network.dangerouslyAllowPrivateNetwork`, `proxy` +- custom API root: `apiRoot` (Bot API root only; do not include `/bot`), `trustedLocalFileRoots` (self-hosted Bot API absolute `file_path` roots) +- webhook: `webhookUrl`, `webhookSecret`, `webhookPath`, `webhookHost`, `webhookPort`, `webhookCertPath` +- actions/capabilities: `capabilities.inlineButtons`, `actions.sendMessage|editMessage|deleteMessage|reactions|sticker|createForumTopic|editForumTopic` - reactions: `reactionNotifications`, `reactionLevel` -- errors: `errorPolicy`, `errorCooldownMs` +- errors: `errorPolicy`, `errorCooldownMs`, `silentErrorReplies` - writes/history: `configWrites`, `historyLimit`, `dmHistoryLimit`, `dms.*.historyLimit` -Multi-account precedence: when two or more account IDs are configured, set `channels.telegram.defaultAccount` (or include `channels.telegram.accounts.default`) to make default routing explicit. Otherwise OpenClaw falls back to the first normalized account ID and `openclaw doctor` warns. Named accounts inherit `channels.telegram.allowFrom` / `groupAllowFrom`, but not `accounts.default.*` values. +Multi-account precedence: with two or more account IDs configured, set `channels.telegram.defaultAccount` (or include `channels.telegram.accounts.default`) to make default routing explicit. Otherwise OpenClaw falls back to the first normalized account ID and `openclaw doctor` warns. Named accounts inherit `channels.telegram.allowFrom` / `groupAllowFrom`, but not `accounts.default.*` values. ## Related diff --git a/docs/channels/tlon.md b/docs/channels/tlon.md index 2301b17de358..8c49504371ff 100644 --- a/docs/channels/tlon.md +++ b/docs/channels/tlon.md @@ -5,31 +5,27 @@ read_when: title: "Tlon" --- -Tlon is a decentralized messenger built on Urbit. OpenClaw connects to your Urbit ship and can -respond to DMs and group chat messages. Group replies require an @ mention by default and can -be further restricted via allowlists. +Tlon is a decentralized messenger built on Urbit. OpenClaw connects to your Urbit ship and +responds to DMs and group chat messages. Group replies require an @ mention by default, with +authorization rules and an owner-approval flow layered on top. -Status: bundled plugin. DMs, group mentions, thread replies, rich text formatting, and -image uploads are supported. Reactions and polls are not yet supported. +Status: bundled plugin. DMs, group mentions, threads, rich text, image upload/download, and an +owner approval system are supported. Reactions and polls are not. ## Bundled plugin -Tlon ships as a bundled plugin in current OpenClaw releases, so normal packaged -builds do not need a separate install. +Tlon ships bundled in current OpenClaw releases; packaged builds do not need a separate install. -If you are on an older build or a custom install that excludes Tlon, install a -current npm package: - -Install via CLI (npm registry): +On an older build or custom install that excludes it, install from npm: ```bash openclaw plugins install @openclaw/tlon ``` -Use the bare package to follow the current official release tag. Pin an exact -version only when you need a reproducible install. +Use the bare package name to track the current release tag. Pin a version (`@openclaw/tlon@x.y.z`) +only for reproducible installs. -Local checkout (when running from a git repo): +From a local checkout: ```bash openclaw plugins install ./path/to/local/tlon-plugin @@ -39,15 +35,11 @@ Details: [Plugins](/tools/plugin) ## Setup -1. Ensure the Tlon plugin is available. - - Current packaged OpenClaw releases already bundle it. - - Older/custom installs can add it manually with the commands above. -2. Gather your ship URL and login code. -3. Configure `channels.tlon`. -4. Restart the gateway. -5. DM the bot or mention it in a group channel. +```bash +openclaw channels add --channel tlon --ship ~sampel-palnet --url https://your-ship-host --code lidlut-tabwed-pillex-ridrup +``` -Minimal config (single account): +Or edit config directly: ```json5 { @@ -57,67 +49,64 @@ Minimal config (single account): ship: "~sampel-palnet", url: "https://your-ship-host", code: "lidlut-tabwed-pillex-ridrup", - ownerShip: "~your-main-ship", // recommended: your ship, always allowed + ownerShip: "~your-main-ship", // recommended: your ship, always authorized }, }, } ``` +Restart the gateway after editing config directly. Then DM the bot or @ mention it in a group +channel. + ## Private/LAN ships -By default, OpenClaw blocks private/internal hostnames and IP ranges for SSRF protection. -If your ship is running on a private network (localhost, LAN IP, or internal hostname), -you must explicitly opt in: +OpenClaw blocks private/internal hostnames and IP ranges for SSRF protection by default. If your +ship runs on a private network (localhost, LAN IP, internal hostname), opt in explicitly: ```json5 { channels: { tlon: { url: "http://localhost:8080", - allowPrivateNetwork: true, + network: { + dangerouslyAllowPrivateNetwork: true, + }, }, }, } ``` -This applies to URLs like: +Applies to targets like `http://localhost:8080`, `http://192.168.x.x:8080`, and +`http://my-ship.local:8080`. Only enable this for a ship URL you trust; it disables SSRF +protection for that account's HTTP requests. -- `http://localhost:8080` -- `http://192.168.x.x:8080` -- `http://my-ship.local:8080` - -⚠️ Only enable this if you trust your local network. This setting disables SSRF protections -for requests to your ship URL. + +`channels.tlon.allowPrivateNetwork` (flat key) is retired. `openclaw doctor --fix` moves it to +`channels.tlon.network.dangerouslyAllowPrivateNetwork` automatically. + ## Group channels -Auto-discovery is enabled by default. You can also pin channels manually: +Pin channels manually, or turn on auto-discovery: ```json5 { channels: { tlon: { groupChannels: ["chat/~host-ship/general", "chat/~host-ship/support"], + autoDiscoverChannels: true, }, }, } ``` -Disable auto-discovery: - -```json5 -{ - channels: { - tlon: { - autoDiscoverChannels: false, - }, - }, -} -``` +`autoDiscoverChannels` defaults to `false` when unset in config; the setup wizard defaults the +prompt to yes and writes `true` explicitly. With it on, OpenClaw scries joined groups on startup, +watches new channels as group invites are accepted, and rechecks every 2 minutes. ## Access control -DM allowlist (empty = no DMs allowed, use `ownerShip` for approval flow): +DM allowlist (empty = no DMs allowed unless the sender is `ownerShip`): ```json5 { @@ -129,7 +118,8 @@ DM allowlist (empty = no DMs allowed, use `ownerShip` for approval flow): } ``` -Group authorization (restricted by default): +Group authorization defaults to `restricted` per channel. Set `defaultAuthorizedShips` for a +baseline, and override per channel nest: ```json5 { @@ -152,9 +142,10 @@ Group authorization (restricted by default): } ``` -## Owner and approval system +Once the bot has replied inside a thread, it keeps responding to later messages in that thread +without requiring another mention. -Set an owner ship to receive approval requests when unauthorized users try to interact: +## Owner and approval system ```json5 { @@ -166,19 +157,36 @@ Set an owner ship to receive approval requests when unauthorized users try to in } ``` -The owner ship is **automatically authorized everywhere** — DM invites are auto-accepted and -channel messages are always allowed. You don't need to add the owner to `dmAllowlist` or -`defaultAuthorizedShips`. +The owner ship is authorized everywhere: DM invites are always auto-accepted, group invites are +always auto-accepted, and channel messages always pass authorization. The owner does not need to +be in `dmAllowlist`, `defaultAuthorizedShips`, or `groupInviteAllowlist`. -When set, the owner receives DM notifications for: +When `ownerShip` is set, unauthorized requests do not just get dropped — they queue a pending +approval and DM the owner: -- DM requests from ships not in the allowlist -- Mentions in channels without authorization -- Group invite requests +- DM requests from ships not on `dmAllowlist` +- Mentions in channels where the sender fails authorization +- Group invites from ships not on `groupInviteAllowlist` (when auto-accept is off, or on but the + inviter is not allowlisted) + +The owner replies in DM to act on a request: + +| Owner reply | Effect | +| ---------------------------- | ---------------------------------------------------- | +| `approve` / `deny` / `block` | Acts on the most recent pending approval | +| `approve ` / `deny ` | Acts on a specific approval by id | +| `block` | Also blocks the ship natively so it cannot reconnect | +| `unblock ~ship` | Reverses a native block | +| `blocked` | Lists currently blocked ships | +| `pending` | Lists pending approval requests | + +Without `ownerShip` configured, unauthorized DMs and channel mentions are just dropped and logged; +there is no approval prompt. ## Auto-accept settings -Auto-accept DM invites (for ships in dmAllowlist): +Auto-accept DM invites from ships already on `dmAllowlist` (the owner is always auto-accepted +regardless of this flag): ```json5 { @@ -190,7 +198,8 @@ Auto-accept DM invites (for ships in dmAllowlist): } ``` -Auto-accept group invites from trusted ships: +Auto-accept group invites from an allowlist (fails closed: with `autoAcceptGroupInvites: true` and +an empty `groupInviteAllowlist`, no non-owner invite is accepted): ```json5 { @@ -203,47 +212,54 @@ Auto-accept group invites from trusted ships: } ``` -`autoAcceptGroupInvites` fails closed when `groupInviteAllowlist` is empty. Set the -allowlist to the ships whose group invites should be accepted automatically. +## Hot-reload via Urbit settings store + +Most of the settings above (`dmAllowlist`, `groupInviteAllowlist`, `groupChannels`, +`defaultAuthorizedShips`, `autoDiscoverChannels`, `autoAcceptDmInvites`, +`autoAcceptGroupInvites`, `ownerShip`, `showModelSignature`) are mirrored into the ship's +`%settings` agent (desk `moltbot`, bucket `tlon`) on first run and then read live from there, +so changes made via a Landscape client or the bundled skill's settings commands apply without a +gateway restart. `channelRules` and pending approvals are also persisted there as JSON. File +config stays the source of truth for values never written to the settings store. ## Delivery targets (CLI/cron) -Use these with `openclaw message send` or cron delivery: +Use with `openclaw message send` or cron delivery: - DM: `~sampel-palnet` or `dm/~sampel-palnet` - Group: `chat/~host-ship/channel` or `group:~host-ship/channel` ## Bundled skill -The Tlon plugin includes a bundled skill ([`@tloncorp/tlon-skill`](https://github.com/tloncorp/tlon-skill)) -that provides CLI access to Tlon operations: +The plugin bundles [`@tloncorp/tlon-skill`](https://github.com/tloncorp/tlon-skill), a CLI for +direct Urbit operations, available automatically once the plugin is installed: -- **Contacts**: get/update profiles, list contacts -- **Channels**: list, create, post messages, fetch history -- **Groups**: list, create, manage members -- **DMs**: send messages, react to messages -- **Reactions**: add/remove emoji reactions to posts and DMs -- **Settings**: manage plugin permissions via slash commands - -The skill is automatically available when the plugin is installed. +- **Activity**: mentions, replies, unreads +- **Channels**: list, create, rename +- **Contacts**: list/get/update profiles +- **Groups**: create, join, invite/request flows, roles +- **Hooks**: manage channel hooks +- **Messages**: history, search +- **DMs**: send, react, accept/decline +- **Posts**: react, delete +- **Notebook**: post to diary channels +- **Settings**: hot-reload plugin config via the settings store above ## Capabilities -| Feature | Status | -| --------------- | --------------------------------------- | -| Direct messages | ✅ Supported | -| Groups/channels | ✅ Supported (mention-gated by default) | -| Threads | ✅ Supported (auto-replies in thread) | -| Rich text | ✅ Markdown converted to Tlon format | -| Images | ✅ Uploaded to Tlon storage | -| Reactions | ✅ Via [bundled skill](#bundled-skill) | -| Polls | ❌ Not yet supported | -| Native commands | ✅ Supported (owner-only by default) | +| Feature | Status | +| --------------- | --------------------------------------------- | +| Direct messages | Supported | +| Groups/channels | Supported (mention-gated by default) | +| Threads | Supported (keeps replying once it has joined) | +| Rich text | Markdown converted to Tlon's native format | +| Images | Downloaded inbound, uploaded outbound | +| Reactions | Only via the [bundled skill](#bundled-skill) | +| Polls | Not supported | +| Native commands | Owner-only by default | ## Troubleshooting -Run this ladder first: - ```bash openclaw status openclaw gateway status @@ -253,39 +269,45 @@ openclaw doctor Common failures: -- **DMs ignored**: sender not in `dmAllowlist` and no `ownerShip` configured for approval flow. -- **Group messages ignored**: channel not discovered or sender not authorized. -- **Connection errors**: check ship URL is reachable; enable `allowPrivateNetwork` for local ships. -- **Auth errors**: verify login code is current (codes rotate). +- **DMs ignored**: sender not in `dmAllowlist` and no `ownerShip` configured for the approval flow. +- **Group messages ignored**: channel not discovered/pinned, or sender fails authorization with no + `ownerShip` to queue an approval. +- **Connection errors**: check the ship URL is reachable; set + `network.dangerouslyAllowPrivateNetwork` for local ships. +- **Auth errors**: login codes rotate — copy the current code from your ship. ## Configuration reference Full configuration: [Configuration](/gateway/configuration) -Provider options: - -- `channels.tlon.enabled`: enable/disable channel startup. -- `channels.tlon.ship`: bot's Urbit ship name (e.g. `~sampel-palnet`). -- `channels.tlon.url`: ship URL (e.g. `https://sampel-palnet.tlon.network`). -- `channels.tlon.code`: ship login code. -- `channels.tlon.allowPrivateNetwork`: allow localhost/LAN URLs (SSRF bypass). -- `channels.tlon.ownerShip`: owner ship for approval system (always authorized). -- `channels.tlon.dmAllowlist`: ships allowed to DM (empty = none). -- `channels.tlon.autoAcceptDmInvites`: auto-accept DMs from allowlisted ships. -- `channels.tlon.autoAcceptGroupInvites`: auto-accept group invites from allowlisted ships. -- `channels.tlon.groupInviteAllowlist`: ships whose group invites may be auto-accepted. -- `channels.tlon.autoDiscoverChannels`: auto-discover group channels (default: true). -- `channels.tlon.groupChannels`: manually pinned channel nests. -- `channels.tlon.defaultAuthorizedShips`: ships authorized for all channels. -- `channels.tlon.authorization.channelRules`: per-channel auth rules. -- `channels.tlon.showModelSignature`: append model name to messages. +| Key | Meaning | +| ------------------------------------------------------ | -------------------------------------------------------------- | +| `channels.tlon.enabled` | Enable/disable channel startup. | +| `channels.tlon.ship` | Bot's Urbit ship name (e.g. `~sampel-palnet`). | +| `channels.tlon.url` | Ship URL (e.g. `https://sampel-palnet.tlon.network`). | +| `channels.tlon.code` | Ship login code. | +| `channels.tlon.network.dangerouslyAllowPrivateNetwork` | Allow localhost/LAN ship URLs (SSRF opt-in). | +| `channels.tlon.ownerShip` | Owner ship: always authorized, receives approval requests. | +| `channels.tlon.dmAllowlist` | Ships allowed to DM (empty = none besides owner). | +| `channels.tlon.autoAcceptDmInvites` | Auto-accept DMs from ships in `dmAllowlist`. | +| `channels.tlon.autoAcceptGroupInvites` | Auto-accept group invites from `groupInviteAllowlist`. | +| `channels.tlon.groupInviteAllowlist` | Ships whose group invites are auto-accepted. | +| `channels.tlon.autoDiscoverChannels` | Auto-discover joined group channels (default: `false`). | +| `channels.tlon.groupChannels` | Manually pinned channel nests. | +| `channels.tlon.defaultAuthorizedShips` | Ships authorized for all channels (used when no rule matches). | +| `channels.tlon.authorization.channelRules` | Per-channel-nest auth mode + allowlist. | +| `channels.tlon.showModelSignature` | Append `_[Generated by ]_` to replies. | +| `channels.tlon.responsePrefix` | Static prefix prepended to outbound replies. | +| `channels.tlon.accounts.` | Additional named accounts (multi-ship setups). | ## Notes -- Group replies require a mention (e.g. `~your-bot-ship`) to respond. -- Thread replies: if the inbound message is in a thread, OpenClaw replies in-thread. -- Rich text: Markdown formatting (bold, italic, code, headers, lists) is converted to Tlon's native format. -- Images: URLs are uploaded to Tlon storage and embedded as image blocks. +- Group replies need an @ mention (e.g. `~your-bot-ship`) unless the bot already joined that thread. +- Thread replies land in-thread; the bot also gets the last 10 messages of thread context prepended + for the agent. +- Rich text (bold, italic, code, headers, lists) converts to Tlon's native format. +- Sending an inbound message that asks for a channel summary (for example "summarize this + channel") triggers a built-in history summarization instead of the normal reply flow. ## Related diff --git a/docs/channels/troubleshooting.md b/docs/channels/troubleshooting.md index f9786fde9879..875751d30969 100644 --- a/docs/channels/troubleshooting.md +++ b/docs/channels/troubleshooting.md @@ -29,8 +29,8 @@ Healthy baseline: ## After an update -Use this when Telegram, iMessage, BlueBubbles-era configs, or another plugin -channel disappears after updating. +Use this when Telegram, iMessage, BlueBubbles-era configs, or another plugin channel disappears +after updating. ```bash openclaw status --all @@ -39,11 +39,10 @@ openclaw gateway restart openclaw status --all ``` -Look for `plugin load failed: dependency tree corrupted; run openclaw doctor ---fix` in `openclaw status --all`. That means the channel is configured, but -the plugin setup/load path hit a corrupt dependency tree instead of registering -the channel. `openclaw doctor --fix` removes stale plugin dependency staging -directories and stale auth shadows, then `openclaw gateway restart` reloads the +Look for `plugin load failed: dependency tree corrupted; run openclaw doctor --fix` in `openclaw +status --all`. That means the channel is configured, but plugin setup/load hit a corrupted +dependency tree instead of registering the channel. `openclaw doctor --fix` clears stale +plugin-runtime dependency symlinks and stale auth shadows, then `openclaw gateway restart` reloads clean state. ## WhatsApp @@ -112,9 +111,7 @@ Full troubleshooting: [Slack troubleshooting](/channels/slack#troubleshooting) | Can send but no receive on macOS | Check macOS privacy permissions for Messages automation | Re-grant TCC permissions and restart channel process. | | DM sender blocked | `openclaw pairing list imessage` | Approve pairing or update allowlist. | -Full troubleshooting: - -- [iMessage troubleshooting](/channels/imessage#troubleshooting) +Full troubleshooting: [iMessage troubleshooting](/channels/imessage#troubleshooting) ## Signal diff --git a/docs/channels/twitch.md b/docs/channels/twitch.md index 8934bfd47563..6047cb0d3f41 100644 --- a/docs/channels/twitch.md +++ b/docs/channels/twitch.md @@ -1,20 +1,16 @@ --- -summary: "Twitch chat bot configuration and setup" +summary: "Twitch chat bot: install, credentials, access control, token refresh" read_when: - Setting up Twitch chat integration for OpenClaw title: "Twitch" sidebarTitle: "Twitch" --- -Twitch chat support via IRC connection. OpenClaw connects as a Twitch user (bot account) to receive and send messages in channels. +Twitch chat support over Twitch's chat (IRC) interface via the Twurple client. OpenClaw signs in as a Twitch bot account, joins one channel per configured account, and replies in that channel. -## Bundled plugin +## Install - -Twitch ships as a bundled plugin in current OpenClaw releases, so normal packaged builds do not need a separate install. - - -If you are on an older build or a custom install that excludes Twitch, install the npm package directly: +Twitch ships as an official plugin; it is not part of the core install. @@ -29,16 +25,15 @@ If you are on an older build or a custom install that excludes Twitch, install t -Use the bare package to follow the current official release tag. Pin an exact -version only when you need a reproducible install. +`plugins install` registers and enables the plugin. Picking Twitch during `openclaw onboard` or `openclaw channels add` installs it on demand. Use the bare package name to follow the current release; pin an exact version only for reproducible installs. Requires OpenClaw 2026.4.10 or newer. Details: [Plugins](/tools/plugin) -## Quick setup (beginner) +## Quick setup - - Current packaged OpenClaw releases already bundle it. Older/custom installs can add it manually with the commands above. + + See [Install](#install) above. Create a dedicated Twitch account for the bot (or use an existing account). @@ -58,11 +53,13 @@ Details: [Plugins](/tools/plugin) - Env: `OPENCLAW_TWITCH_ACCESS_TOKEN=...` (default account only) - Or config: `channels.twitch.accessToken` - If both are set, config takes precedence (env fallback is default-account only). + If both are set, config takes precedence (the env var is only a fallback for the default account). - Start the gateway with the configured channel. + ```bash + openclaw gateway run + ``` @@ -77,11 +74,11 @@ Minimal config: channels: { twitch: { enabled: true, - username: "openclaw", // Bot's Twitch account - accessToken: "oauth:abc123...", // OAuth Access Token (or use OPENCLAW_TWITCH_ACCESS_TOKEN env var) + username: "openclaw", // Bot's Twitch account (authenticates) + accessToken: "oauth:abc123...", // OAuth access token (or use OPENCLAW_TWITCH_ACCESS_TOKEN env var) clientId: "xyz789...", // Client ID from Token Generator - channel: "vevisk", // Which Twitch channel's chat to join (required) - allowFrom: ["123456789"], // (recommended) Your Twitch user ID only - get it from https://www.streamweasels.com/tools/convert-twitch-username-to-user-id/ + channel: "yourchannel", // Which Twitch channel's chat to join (required) + allowFrom: ["123456789"], // (recommended) Your Twitch user ID only }, }, } @@ -90,78 +87,16 @@ Minimal config: ## What it is - A Twitch channel owned by the Gateway. -- Deterministic routing: replies always go back to Twitch. -- Each account maps to an isolated session key `agent::twitch:`. -- `username` is the bot's account (who authenticates), `channel` is which chat room to join. - -## Setup (detailed) - -### Generate credentials - -Use [Twitch Token Generator](https://twitchtokengenerator.com/): - -- Select **Bot Token** -- Verify scopes `chat:read` and `chat:write` are selected -- Copy the **Client ID** and **Access Token** - - -No manual app registration needed. Tokens expire after several hours. - - -### Configure the bot - - - - ```bash - OPENCLAW_TWITCH_ACCESS_TOKEN=oauth:abc123... - ``` - - - ```json5 - { - channels: { - twitch: { - enabled: true, - username: "openclaw", - accessToken: "oauth:abc123...", - clientId: "xyz789...", - channel: "vevisk", - }, - }, - } - ``` - - - -If both env and config are set, config takes precedence. - -### Access control (recommended) - -```json5 -{ - channels: { - twitch: { - allowFrom: ["123456789"], // (recommended) Your Twitch user ID only - }, - }, -} -``` - -Prefer `allowFrom` for a hard allowlist. Use `allowedRoles` instead if you want role-based access. - -**Available roles:** `"moderator"`, `"owner"`, `"vip"`, `"subscriber"`, `"all"`. - - -**Why user IDs?** Usernames can change, allowing impersonation. User IDs are permanent. - -Find your Twitch user ID: [https://www.streamweasels.com/tools/convert-twitch-username-to-user-id/](https://www.streamweasels.com/tools/convert-twitch-username-to-user-id/) (Convert your Twitch username to ID) - +- Deterministic routing: replies always go back to the Twitch channel the message came from. +- Each joined channel maps to an isolated group session key `agent::twitch:group:`. +- `username` is the bot's account (who authenticates), `channel` is which chat room to join. One account entry joins exactly one channel. +- Tokens work with or without the `oauth:` prefix; OpenClaw normalizes both ways (the setup wizard expects the `oauth:` form). ## Token refresh (optional) -Tokens from [Twitch Token Generator](https://twitchtokengenerator.com/) cannot be automatically refreshed - regenerate when expired. +Tokens from [Twitch Token Generator](https://twitchtokengenerator.com/) cannot be refreshed by OpenClaw - regenerate when expired (they last a few hours; no app registration needed). -For automatic token refresh, create your own Twitch application at [Twitch Developer Console](https://dev.twitch.tv/console) and add to config: +For automatic refresh, create your own app at the [Twitch Developer Console](https://dev.twitch.tv/console) and add: ```json5 { @@ -174,11 +109,11 @@ For automatic token refresh, create your own Twitch application at [Twitch Devel } ``` -The bot automatically refreshes tokens before expiration and logs refresh events. +With both set, the plugin uses a refreshing auth provider that renews tokens before expiration and logs each refresh. Without `refreshToken` it logs `token refresh disabled (no refresh token)`; without `clientSecret` it falls back to a static (non-refreshing) token. ## Multi-account support -Use `channels.twitch.accounts` with per-account tokens. See [Configuration](/gateway/configuration) for the shared pattern. +Use `channels.twitch.accounts` with per-account credentials. See [Configuration](/gateway/configuration) for the shared pattern. Example (one bot account in two channels): @@ -191,7 +126,7 @@ Example (one bot account in two channels): username: "openclaw", accessToken: "oauth:abc123...", clientId: "xyz789...", - channel: "vevisk", + channel: "yourchannel", }, channel2: { username: "openclaw", @@ -206,11 +141,15 @@ Example (one bot account in two channels): ``` -Each account needs its own token (one token per channel). +Every account entry needs its own `accessToken` (the env var covers only the default account). An account joins exactly one channel, so joining two channels means two accounts. `channels.twitch.defaultAccount` picks which account is the default. ## Access control +`allowFrom` is a hard allowlist of Twitch user IDs. When it is set, `allowedRoles` is ignored; leave `allowFrom` unset to use role-based access instead. + +**Available roles:** `"moderator"`, `"owner"`, `"vip"`, `"subscriber"`, `"all"`. + ```json5 @@ -241,12 +180,9 @@ Each account needs its own token (one token per channel). }, } ``` - - `allowFrom` is a hard allowlist. When set, only those user IDs are allowed. If you want role-based access, leave `allowFrom` unset and configure `allowedRoles` instead. - - By default, `requireMention` is `true`. To disable and respond to all messages: + By default, `requireMention` is `true`. To respond to all allowed messages: ```json5 { @@ -265,6 +201,12 @@ Each account needs its own token (one token per channel). + +**Why user IDs?** Usernames can change, allowing impersonation. User IDs are permanent. + +Find yours with the [username to ID converter](https://www.streamweasels.com/tools/convert-twitch-username-to-user-id/). + + ## Troubleshooting First, run diagnostic commands: @@ -277,26 +219,27 @@ openclaw channels status --probe - **Check access control:** Ensure your user ID is in `allowFrom`, or temporarily remove `allowFrom` and set `allowedRoles: ["all"]` to test. - - **Check the bot is in the channel:** The bot must join the channel specified in `channel`. + - **Check the mention gate:** With `requireMention: true` (default), messages must @mention the bot username. + - **Check the bot is in the channel:** The bot only joins the channel named in `channel`. "Failed to connect" or authentication errors: - - Verify `accessToken` is the OAuth access token value (typically starts with `oauth:` prefix) - - Check token has `chat:read` and `chat:write` scopes + - Verify `accessToken` is the OAuth access token value (the `oauth:` prefix is optional) + - Check the token has `chat:read` and `chat:write` scopes - If using token refresh, verify `clientSecret` and `refreshToken` are set Check logs for refresh events: - ``` + ```text Using env token source for mybot Access token refreshed for user 123456 (expires in 14400s) ``` - If you see "token refresh disabled (no refresh token)": + If you see `token refresh disabled (no refresh token)`: - Ensure `clientSecret` is provided - Ensure `refreshToken` is provided @@ -308,14 +251,14 @@ openclaw channels status --probe ### Account config - - Bot username. + + Bot username (the authenticating account). - - OAuth access token with `chat:read` and `chat:write`. + + OAuth access token with `chat:read` and `chat:write` (config or env for the default account). - - Twitch Client ID (from Token Generator or your app). + + Twitch Client ID (from Token Generator or your app). Optional in the schema but required to connect. Channel to join. @@ -330,29 +273,31 @@ openclaw channels status --probe Optional: for automatic token refresh. - Token expiry in seconds. + Token expiry in seconds (refresh tracking). - Token obtained timestamp. + Timestamp when the token was obtained (refresh tracking). - User ID allowlist. + User ID allowlist. When set, roles are ignored. Role-based access control. - Require @mention. + Require @mention to trigger the bot. + + + Outbound response prefix override for this account. ### Provider options - `channels.twitch.enabled` - Enable/disable channel startup -- `channels.twitch.username` - Bot username (simplified single-account config) -- `channels.twitch.accessToken` - OAuth access token (simplified single-account config) -- `channels.twitch.clientId` - Twitch Client ID (simplified single-account config) -- `channels.twitch.channel` - Channel to join (simplified single-account config) +- `channels.twitch.username` / `accessToken` / `clientId` / `channel` - Simplified single-account config (implicit `default` account; takes precedence over `accounts.default`) - `channels.twitch.accounts.` - Multi-account config (all account fields above) +- `channels.twitch.defaultAccount` - Which account name is the default +- `channels.twitch.markdown.tables` - Markdown table rendering mode (`off` | `bullets` | `code` | `block`) Full example: @@ -364,23 +309,19 @@ Full example: username: "openclaw", accessToken: "oauth:abc123...", clientId: "xyz789...", - channel: "vevisk", + channel: "yourchannel", clientSecret: "secret123...", refreshToken: "refresh456...", allowFrom: ["123456789"], - allowedRoles: ["moderator", "vip"], accounts: { - default: { + second: { username: "mybot", - accessToken: "oauth:abc123...", - clientId: "xyz789...", + accessToken: "oauth:def456...", + clientId: "uvw012...", channel: "your_channel", enabled: true, - clientSecret: "secret123...", - refreshToken: "refresh456...", expiresIn: 14400, obtainmentTimestamp: 1706092800000, - allowFrom: ["123456789", "987654321"], allowedRoles: ["moderator"], }, }, @@ -391,36 +332,33 @@ Full example: ## Tool actions -The agent can call `twitch` with action: - -- `send` - Send a message to a channel - -Example: +The agent can send Twitch messages through the message tool `send` action: ```json5 { - action: "twitch", - params: { - message: "Hello Twitch!", - to: "#mychannel", - }, + channel: "twitch", + action: "send", + to: "#mychannel", + message: "Hello Twitch!", } ``` +`to` is optional and defaults to the account's configured `channel`. + ## Safety and ops -- **Treat tokens like passwords** — Never commit tokens to git. +- **Treat tokens like passwords** - never commit tokens to git. - **Use automatic token refresh** for long-running bots. - **Use user ID allowlists** instead of usernames for access control. - **Monitor logs** for token refresh events and connection status. -- **Scope tokens minimally** — Only request `chat:read` and `chat:write`. -- **If stuck**: Restart the gateway after confirming no other process owns the session. +- **Scope tokens minimally** - only request `chat:read` and `chat:write`. +- **If stuck**: restart the gateway after confirming no other process owns the session. ## Limits -- **500 characters** per message (auto-chunked at word boundaries). -- Markdown is stripped before chunking. -- No rate limiting (uses Twitch's built-in rate limits). +- **500 characters** per message; longer replies are chunked at word boundaries. +- Markdown is stripped before sending (Twitch chat is plain text; newlines become spaces). +- OpenClaw adds no rate limiting of its own; the Twurple chat client handles Twitch rate limits. ## Related diff --git a/docs/channels/wechat.md b/docs/channels/wechat.md index 60269fc864fe..fff4bf648f7f 100644 --- a/docs/channels/wechat.md +++ b/docs/channels/wechat.md @@ -10,14 +10,15 @@ title: "WeChat" OpenClaw connects to WeChat through Tencent's external `@tencent-weixin/openclaw-weixin` channel plugin. -Status: external plugin. Direct chats and media are supported. Group chats are not -advertised by the current plugin capability metadata. +Status: external plugin, maintained by the Tencent Weixin team. Direct chats and +media are supported. Group chats are not advertised by the plugin capability +metadata (it declares direct chats only). ## Naming - **WeChat** is the user-facing name in these docs. - **Weixin** is the name used by Tencent's package and by the plugin id. -- `openclaw-weixin` is the OpenClaw channel id. +- `openclaw-weixin` is the OpenClaw channel id (`weixin` and `wechat` work as aliases). - `@tencent-weixin/openclaw-weixin` is the npm package. Use `openclaw-weixin` in CLI commands and config paths. @@ -32,13 +33,14 @@ WeChat-specific runtime: 2. The Gateway discovers the plugin manifest and loads the plugin entrypoint. 3. The plugin registers channel id `openclaw-weixin`. 4. `openclaw channels login --channel openclaw-weixin` starts QR login. -5. The plugin stores account credentials under the OpenClaw state directory. +5. The plugin stores account credentials under the OpenClaw state directory + (`~/.openclaw` by default). 6. When the Gateway starts, the plugin starts its Weixin monitor for each configured account. 7. Inbound WeChat messages are normalized through the channel contract, routed to the selected OpenClaw agent, and sent back through the plugin outbound path. -That separation matters: OpenClaw core should stay channel-agnostic. WeChat login, +That separation matters: OpenClaw core stays channel-agnostic. WeChat login, Tencent iLink API calls, media upload/download, context tokens, and account monitoring are owned by the external plugin. @@ -99,10 +101,10 @@ For the full access-control model, see [Pairing](/channels/pairing). The plugin checks the host OpenClaw version at startup. -| Plugin line | OpenClaw version | npm tag | -| ----------- | ----------------------- | -------- | -| `2.x` | `>=2026.3.22` | `latest` | -| `1.x` | `>=2026.1.0 <2026.3.22` | `legacy` | +| Plugin line | OpenClaw version | npm tag | +| ----------- | --------------------------------------------------------------- | -------- | +| `2.x` | `>=2026.5.12` (current 2.4.6; early 2.x accepted `>=2026.3.22`) | `latest` | +| `1.x` | `>=2026.1.0 <2026.3.22` | `legacy` | If the plugin reports that your OpenClaw version is too old, either update OpenClaw or install the legacy plugin line: @@ -119,7 +121,7 @@ generic stale-Gateway cleanup: a child process could try to clean up the parent Gateway process, causing restart loops under process managers such as systemd. Current OpenClaw startup cleanup excludes the current process and its ancestors, -so a channel helper must not kill the Gateway that launched it. This fix is +so a channel helper cannot kill the Gateway that launched it. This fix is generic; it is not a WeChat-specific path in core. ## Troubleshooting diff --git a/docs/channels/whatsapp.md b/docs/channels/whatsapp.md index 05cf485fd0ba..fe7a54b645e7 100644 --- a/docs/channels/whatsapp.md +++ b/docs/channels/whatsapp.md @@ -5,28 +5,17 @@ read_when: title: "WhatsApp" --- -Status: production-ready via WhatsApp Web (Baileys). Gateway owns linked session(s). +Status: production-ready via WhatsApp Web (Baileys). The gateway owns the linked session(s); there is no separate Twilio WhatsApp channel. -## Install (on demand) +## Install -- Onboarding (`openclaw onboard`) and `openclaw channels add --channel whatsapp` - prompt to install the WhatsApp plugin the first time you select it. -- `openclaw channels login --channel whatsapp` also offers the install flow when - the plugin is not present yet. -- Dev channel + git checkout: defaults to the local plugin path. -- Stable/Beta: installs the official `@openclaw/whatsapp` plugin from ClawHub - first, with npm as the fallback. -- The WhatsApp runtime is distributed outside the core OpenClaw npm package so - WhatsApp-specific runtime dependencies stay with the external plugin. - -Manual install stays available: +`openclaw onboard` and `openclaw channels add --channel whatsapp` prompt to install the plugin the first time you select it; `openclaw channels login --channel whatsapp` offers the same install flow if the plugin is missing. Dev checkouts use the local plugin path; stable/beta installs `@openclaw/whatsapp` from ClawHub first, falling back to npm. The WhatsApp runtime ships outside the core OpenClaw npm package, so its runtime dependencies stay with the external plugin. Manual install: ```bash openclaw plugins install clawhub:@openclaw/whatsapp ``` -Use the bare npm package (`@openclaw/whatsapp`) only when you need the registry -fallback. Pin an exact version only when you need a reproducible install. +Use the bare npm package (`@openclaw/whatsapp`) only for the registry fallback; pin an exact version only for a reproducible install. @@ -43,7 +32,7 @@ fallback. Pin an exact version only when you need a reproducible install. ## Quick setup - + ```json5 { @@ -66,9 +55,7 @@ fallback. Pin an exact version only when you need a reproducible install. openclaw channels login --channel whatsapp ``` - Current login is QR-based. In remote or headless environments, make sure you - have a reliable path to deliver the live QR code to the phone that will scan - it before starting login. + Login is QR-only. On remote or headless hosts, have a reliable path to deliver the live QR to the phone before starting login; terminal-rendered QRs, screenshots, or chat attachments can expire in transit. For a specific account: @@ -76,7 +63,7 @@ openclaw channels login --channel whatsapp openclaw channels login --channel whatsapp --account work ``` - To attach an existing/custom WhatsApp Web auth directory before login: + To attach an existing/custom auth directory before login: ```bash openclaw channels add --channel whatsapp --account work --auth-dir /path/to/wa-auth @@ -93,143 +80,30 @@ openclaw gateway - + ```bash openclaw pairing list whatsapp openclaw pairing approve whatsapp ``` - Pairing requests expire after 1 hour. Pending requests are capped at 3 per channel. + Pairing requests expire after 1 hour; pending requests are capped at 3 per account. -OpenClaw recommends running WhatsApp on a separate number when possible. (The channel metadata and setup flow are optimized for that setup, but personal-number setups are also supported.) +A separate WhatsApp number is recommended (setup and metadata are optimized for it), but personal-number/self-chat setups are fully supported. - -The current WhatsApp setup flow is QR-only. Terminal-rendered QRs, screenshots, -PDFs, or chat attachments can expire or become unreadable while being relayed -from a remote machine. For remote/headless hosts, prefer a direct QR image -handoff path over manual terminal capture. - - -## Call the current requester with MeowCaller (experimental) - -The WhatsApp plugin can expose `whatsapp_call` in WhatsApp-originated agent turns. The tool -uses [MeowCaller](https://github.com/purpshell/meowcaller) to place a WhatsApp voice call to -the current authorized requester and plays an OpenClaw TTS message after they answer. The tool -does not accept a destination number, so a prompt cannot redirect the call to a third party. -This experimental capability is disabled by default. - - -MeowCaller is experimental, has no tagged release, and uses a separately paired whatsmeow -linked-device session. It cannot reuse the WhatsApp plugin's Baileys credentials. Pairing adds -another linked device to the same WhatsApp account. Scan with the WhatsApp identity used by -OpenClaw. Personal-number/self-chat mode cannot call itself; use a dedicated OpenClaw number -to call your personal number. - - - - - - Add `actions.calls: true` to the WhatsApp channel in `openclaw.json`: - -```json -{ - "channels": { - "whatsapp": { - "actions": { - "calls": true - } - } - } -} -``` - - Merge this into your existing WhatsApp configuration, then restart the gateway. When the - setting is absent or `false`, OpenClaw does not expose the `whatsapp_call` tool to the agent. - - - - - - The adapter expects an executable named `meowcaller` on the gateway host's `PATH`. - Until [MeowCaller PR #7](https://github.com/purpshell/meowcaller/pull/7) merges, build - the reviewed branch at commit `752050471fc2bf7a8cdfbf7dbd3cd4e865d85d3f`: - -```bash -git clone --branch feat/send-only-notify https://github.com/steipete/meowcaller.git -cd meowcaller -git checkout 752050471fc2bf7a8cdfbf7dbd3cd4e865d85d3f -mkdir -p "$HOME/.local/bin" -go build -o "$HOME/.local/bin/meowcaller" ./cmd/meowcaller -``` - - Ensure `$HOME/.local/bin` is also on the gateway service's `PATH`. This revision provides - explicit `pair` and send-only `notify` commands. `notify` opens no microphone, speaker, - video device, inbound audio sink, or diagnostic capture. Do not substitute the example - CLI's `play` command. - - - - - - Ask the WhatsApp agent to check call setup. The `whatsapp_call` status action reports the - account-specific state directory and pairing command. For the default account: - -```bash -state_dir="$HOME/.openclaw/credentials/whatsapp-calls/default" -mkdir -p "$state_dir" -chmod 700 "$state_dir" -meowcaller pair --store "$state_dir/wa-voip.db" -``` - - Run the command in an interactive terminal. Scan its QR from **WhatsApp > Linked devices** - and wait for `MeowCaller linked device ready`. The command then exits. Keep `wa-voip.db` - private; it is the MeowCaller linked-device session. The `whatsapp_call` status action - returns the account-specific command and shell when you use a non-default account. On - Windows, run its PowerShell command; MeowCaller creates the store directory. - - - - - - Configure a telephony-capable [TTS provider](/tools/tts), restart the gateway, then send a - WhatsApp request such as `Call me and say the build finished.` The tool resolves the sender - from trusted inbound context, synthesizes a temporary private WAV file, runs MeowCaller for a - bounded call window, and deletes the audio file afterward. OpenClaw passes the account's - store explicitly, waits for a zero exit status after answer, playback, and hangup, and treats - a timeout or nonzero exit as a failed tool call. - - - - -Current limits: - -- one-to-one outbound audio calls only -- no arbitrary destination numbers -- no shared auth with the chat connection -- no self-calls from personal-number/self-chat mode -- synthesized audio is limited to 60 seconds -- no handset-side audibility receipt beyond MeowCaller's answer/playback/hangup completion -- OpenClaw stops the companion process after a bounded 115–175 second window, including - MeowCaller's connection, answer, playback, and shutdown phases - ## Deployment patterns - This is the cleanest operational mode: - - separate WhatsApp identity for OpenClaw - clearer DM allowlists and routing boundaries - lower chance of self-chat confusion - Minimal policy pattern: - ```json5 { channels: { @@ -244,43 +118,95 @@ Current limits: - Onboarding supports personal-number mode and writes a self-chat-friendly baseline: - - - `dmPolicy: "allowlist"` - - `allowFrom` includes your personal number - - `selfChatMode: true` - - In runtime, self-chat protections key off the linked self number and `allowFrom`. - - - - - The messaging platform channel is WhatsApp Web-based (`Baileys`) in current OpenClaw channel architecture. - - There is no separate Twilio WhatsApp messaging channel in the built-in chat-channel registry. - + Onboarding supports personal-number mode and writes a self-chat-friendly baseline: `dmPolicy: "allowlist"`, `allowFrom` including your own number, `selfChatMode: true`. Runtime self-chat protections key off the linked self number plus `allowFrom`. ## Runtime model -- Gateway owns the WhatsApp socket and reconnect loop. -- The reconnect watchdog uses WhatsApp Web transport activity, not only inbound app-message volume, so a quiet linked-device session is not restarted solely because nobody has sent a message recently. A longer application-silence cap still forces a reconnect if transport frames keep arriving but no application messages are handled for the watchdog window; after a transient reconnect for a recently active session, that application-silence check uses the normal message timeout for the first recovery window. -- Baileys socket timings are explicit under `web.whatsapp.*`: `keepAliveIntervalMs` controls WhatsApp Web application pings, `connectTimeoutMs` controls the opening handshake timeout, and `defaultQueryTimeoutMs` controls Baileys query waits plus OpenClaw's local outbound send/presence and inbound read-receipt operation bounds. -- Outbound sends require an active WhatsApp listener for the target account. -- Group sends attach native mention metadata for `@+` and `@` tokens in text and media captions when the token matches current WhatsApp participant metadata, including LID-backed groups. -- Status and broadcast chats are ignored (`@status`, `@broadcast`). -- The reconnect watchdog follows WhatsApp Web transport activity, not only inbound app-message volume: quiet linked-device sessions stay up while transport frames continue, but a transport stall forces reconnect well before the later remote disconnect path. -- Direct chats use DM session rules (`session.dmScope`; default `main` collapses DMs to the agent main session). -- Group sessions are isolated (`agent::whatsapp:group:`). -- WhatsApp Channels/Newsletters can be explicit outbound targets with their native `@newsletter` JID. Outbound newsletter sends use channel session metadata (`agent::whatsapp:channel:`) rather than DM session semantics. -- WhatsApp Web transport honors standard proxy environment variables on the gateway host (`HTTPS_PROXY`, `HTTP_PROXY`, `NO_PROXY` / lowercase variants). Prefer host-level proxy config over channel-specific WhatsApp proxy settings. -- When `messages.removeAckAfterReply` is enabled, OpenClaw clears the WhatsApp ack reaction after a visible reply is delivered. +- The gateway owns the WhatsApp socket and reconnect loop. +- A watchdog tracks two signals independently: raw WhatsApp Web transport activity and application-message activity. A quiet-but-connected session is not restarted just because no message arrived recently; it forces reconnect only when transport frames stop arriving for a fixed internal window (not user-configurable) or application messages stay silent past 4x the normal message timeout. Right after a reconnect for a recently active session, that first window uses the shorter normal message timeout instead of the 4x window. +- Baileys socket timings are explicit under `web.whatsapp.*`: `keepAliveIntervalMs` (application ping interval), `connectTimeoutMs` (opening handshake timeout), `defaultQueryTimeoutMs` (Baileys query waits, plus OpenClaw's outbound send/presence and inbound read-receipt timeouts). +- Outbound sends require an active WhatsApp listener for the target account; sends fail fast otherwise. +- Group sends attach native mention metadata for `@+` and `@` tokens (in text and media captions) when the token matches current participant metadata, including LID-backed groups. +- Status and broadcast chats (`@status`, `@broadcast`) are ignored. +- Direct chats use DM session rules (`session.dmScope`; default `main` collapses DMs into the agent main session). Group sessions are isolated per JID (`agent::whatsapp:group:`). +- WhatsApp Channels/Newsletters can be explicit outbound targets via their native `@newsletter` JID, using channel session metadata (`agent::whatsapp:channel:`) rather than DM semantics. +- WhatsApp Web transport honors standard proxy environment variables on the gateway host (`HTTPS_PROXY`, `HTTP_PROXY`, `NO_PROXY`, lowercase variants). Prefer host-level proxy config over per-channel settings. +- With `messages.removeAckAfterReply` enabled, OpenClaw clears the ack reaction once a visible reply is delivered. + +## Call the current requester with MeowCaller (experimental) + +The plugin can expose `whatsapp_call` in WhatsApp-originated agent turns. It uses [MeowCaller](https://github.com/purpshell/meowcaller) to place a WhatsApp voice call to the current authorized requester and play an OpenClaw TTS message after they answer. The tool has no destination-number parameter, so a prompt cannot redirect the call. Disabled by default. + + +MeowCaller is experimental, has no tagged release, and uses a separately paired whatsmeow linked-device session — it cannot reuse the plugin's Baileys credentials. Pairing adds another linked device to the same WhatsApp account; scan with the identity used by OpenClaw. Personal-number/self-chat mode cannot call itself; use a dedicated OpenClaw number to call your personal number. + + + + + + Add `actions.calls: true` to the WhatsApp channel config and restart the gateway: + +```json +{ + "channels": { + "whatsapp": { + "actions": { + "calls": true + } + } + } +} +``` + + When absent or `false`, OpenClaw does not expose the `whatsapp_call` tool. + + + + + + The adapter expects a `meowcaller` executable on the gateway host's `PATH`. Until [MeowCaller PR #7](https://github.com/purpshell/meowcaller/pull/7) merges, build the reviewed branch: + +```bash +git clone --branch feat/send-only-notify https://github.com/steipete/meowcaller.git +cd meowcaller +git checkout 752050471fc2bf7a8cdfbf7dbd3cd4e865d85d3f +mkdir -p "$HOME/.local/bin" +go build -o "$HOME/.local/bin/meowcaller" ./cmd/meowcaller +``` + + Ensure `$HOME/.local/bin` is on the gateway service's `PATH`. This revision has explicit `pair` and send-only `notify` commands; `notify` opens no microphone, speaker, video device, or diagnostic capture. Do not substitute the upstream example CLI's `play` command. + + + + + + Ask the WhatsApp agent to check call setup (`whatsapp_call` status action reports the account-specific state directory and pairing command). For the default account: + +```bash +state_dir="$HOME/.openclaw/credentials/whatsapp-calls/default" +mkdir -p "$state_dir" +chmod 700 "$state_dir" +meowcaller pair --store "$state_dir/wa-voip.db" +``` + + Run this interactively, scan the QR from **WhatsApp > Linked devices**, and wait for `MeowCaller linked device ready`. Keep `wa-voip.db` private — it is the MeowCaller session. Non-default accounts get their own store path from the status action; on Windows, run its PowerShell command. + + + + + + Configure a telephony-capable [TTS provider](/tools/tts), restart the gateway, then send a request such as `Call me and say the build finished.` The tool resolves the sender from trusted inbound context, synthesizes a temporary private WAV file, runs MeowCaller for a bounded call window, and deletes the audio file afterward. OpenClaw passes the account's store explicitly, waits for a zero exit status after answer/playback/hangup, and treats a timeout or nonzero exit as a failed tool call. + + + + +Limits: one-to-one outbound audio calls only, no arbitrary destination numbers, no shared auth with the chat connection, no self-calls from personal-number/self-chat mode, synthesized audio capped at 60 seconds, no handset-side audibility receipt beyond MeowCaller's answer/playback/hangup completion, and OpenClaw stops the companion process after a bounded 115-175 second window (covering MeowCaller's connection, answer, playback, and shutdown phases). ## Approval prompts -WhatsApp can render exec and plugin approval prompts with `👍` / `👎` reactions. Delivery is -controlled by the top-level approval forwarding config: +WhatsApp can render exec and plugin approval prompts as `👍`/`👎` reactions, controlled by the top-level approval forwarding config: ```json5 { @@ -298,23 +224,13 @@ controlled by the top-level approval forwarding config: } ``` -`approvals.exec` and `approvals.plugin` are independent. Enabling WhatsApp as a channel only links -the transport; it does not send approval prompts unless the matching approval family is enabled -and routes to WhatsApp. Session mode delivers native emoji approvals only for approvals that -originate from WhatsApp. Target mode uses the shared forwarding pipeline for explicit WhatsApp -targets and does not create separate approver-DM fanout. +`approvals.exec` and `approvals.plugin` are independent; enabling WhatsApp as a channel only links the transport and sends nothing unless the matching approval family is enabled and routed there. Session mode delivers native emoji approvals only for approvals that originate from WhatsApp. Target mode uses the shared forwarding pipeline for explicit targets and does not create separate approver-DM fanout. -WhatsApp approval reactions require explicit WhatsApp approvers from `allowFrom` or `"*"`. -`defaultTo` controls ordinary default message targets; it is not an approval approver. Manual -`/approve` commands still pass through the normal WhatsApp sender authorization path before -approval resolution. +WhatsApp approval reactions require explicit approvers in `allowFrom` (or `"*"`). `defaultTo` sets ordinary default message targets, not an approver list. Manual `/approve` commands still pass the normal WhatsApp sender-authorization path before approval resolution. ## Plugin hooks and privacy -WhatsApp inbound messages can contain personal message content, phone numbers, -group identifiers, sender names, and session correlation fields. For that reason, -WhatsApp does not broadcast inbound `message_received` hook payloads to plugins -unless you explicitly opt in: +Inbound WhatsApp messages can carry personal content, phone numbers, group identifiers, sender names, and session correlation fields. WhatsApp does not broadcast inbound `message_received` hook payloads to plugins unless you opt in: ```json5 { @@ -328,102 +244,68 @@ unless you explicitly opt in: } ``` -You can scope the opt-in to one account: - -```json5 -{ - channels: { - whatsapp: { - accounts: { - work: { - pluginHooks: { - messageReceived: true, - }, - }, - }, - }, - }, -} -``` - -Only enable this for plugins you trust to receive inbound WhatsApp message -content and identifiers. +Scope the opt-in to one account under `channels.whatsapp.accounts..pluginHooks.messageReceived`. Only enable this for plugins you trust with inbound WhatsApp content and identifiers. ## Access control and activation - `channels.whatsapp.dmPolicy` controls direct chat access: + `channels.whatsapp.dmPolicy`: - - `pairing` (default) - - `allowlist` - - `open` (requires `allowFrom` to include `"*"`) - - `disabled` + | Value | Behavior | + | --- | --- | + | `pairing` (default) | Unknown senders request pairing; owner approves | + | `allowlist` | Only `allowFrom` senders admitted | + | `open` | Requires `allowFrom` to include `"*"` | + | `disabled` | Block all DMs | - `allowFrom` accepts E.164-style numbers (normalized internally). + `allowFrom` accepts E.164-style numbers (normalized internally). It is a DM sender access-control list only — it does not gate explicit outbound sends to group JIDs or `@newsletter` channel JIDs. - `allowFrom` is a DM sender access-control list. It does not gate explicit outbound sends to WhatsApp group JIDs or `@newsletter` channel JIDs. + Multi-account override: `channels.whatsapp.accounts..dmPolicy` (and `.allowFrom`) take precedence over channel-level defaults for that account. - Multi-account override: `channels.whatsapp.accounts..dmPolicy` (and `allowFrom`) take precedence over channel-level defaults for that account. + Runtime notes: - Runtime behavior details: - - - pairings are persisted in channel allow-store and merged with configured `allowFrom` - - scheduled automation and heartbeat recipient fallback use explicit delivery targets or configured `allowFrom`; DM pairing approvals are not implicit cron or heartbeat recipients + - pairings persist in the channel allow-store and merge with configured `allowFrom` + - scheduled automation and heartbeat recipient fallback use explicit delivery targets or configured `allowFrom`; DM pairing approvals are not implicit cron/heartbeat recipients - if no allowlist is configured, the linked self number is allowed by default - - OpenClaw never auto-pairs outbound `fromMe` DMs (messages you send to yourself from the linked device) + - OpenClaw never auto-pairs outbound `fromMe` DMs (messages you send yourself from the linked device) - + Group access has two layers: - 1. **Group membership allowlist** (`channels.whatsapp.groups`) - - if `groups` is omitted, all groups are eligible - - if `groups` is present, it acts as a group allowlist (`"*"` allowed) + 1. **Group membership allowlist** (`channels.whatsapp.groups`): if `groups` is omitted, all groups are eligible; if present, it acts as a group allowlist (`"*"` admits all). + 2. **Group sender policy** (`channels.whatsapp.groupPolicy` + `groupAllowFrom`): `open` bypasses the sender allowlist, `allowlist` requires a `groupAllowFrom` (or `*`) match, `disabled` blocks all group inbound. - 2. **Group sender policy** (`channels.whatsapp.groupPolicy` + `groupAllowFrom`) - - `open`: sender allowlist bypassed - - `allowlist`: sender must match `groupAllowFrom` (or `*`) - - `disabled`: block all group inbound + If `groupAllowFrom` is unset, sender checks fall back to `allowFrom` when it has entries. Sender allowlists are evaluated before mention/reply activation. - Sender allowlist fallback: + If no `channels.whatsapp` block exists at all, runtime falls back to `groupPolicy: "allowlist"` (with a warning log), even if `channels.defaults.groupPolicy` is set to something else. - - if `groupAllowFrom` is unset, runtime falls back to `allowFrom` when available - - sender allowlists are evaluated before mention/reply activation - - Note: if no `channels.whatsapp` block exists at all, runtime group-policy fallback is `allowlist` (with a warning log), even if `channels.defaults.groupPolicy` is set. + + Group-membership resolution has a single-account safety net: if only one WhatsApp account is configured and its `accounts..groups` is an explicit empty object (`{}`), that is treated as "not set" and falls back to the root `channels.whatsapp.groups` map, instead of silently blocking every group. With 2+ accounts configured, an explicit empty account map stays empty and does not fall back — this lets one account intentionally disable all groups without affecting siblings. + - - Group replies require mention by default. - - Mention detection includes: + + Group replies require a mention by default. Mention detection includes: - explicit WhatsApp mentions of the bot identity - configured mention regex patterns (`agents.list[].groupChat.mentionPatterns`, fallback `messages.groupChat.mentionPatterns`) - inbound voice-note transcripts for authorized group messages - implicit reply-to-bot detection (reply sender matches bot identity) - Security note: + Security: quote/reply only satisfies mention gating — it does **not** grant sender authorization. With `groupPolicy: "allowlist"`, non-allowlisted senders stay blocked even replying to an allowlisted user's message. - - quote/reply only satisfies mention gating; it does **not** grant sender authorization - - with `groupPolicy: "allowlist"`, non-allowlisted senders are still blocked even if they reply to an allowlisted user's message - - Session-level activation command: - - - `/activation mention` - - `/activation always` - - `activation` updates session state (not global config). It is owner-gated. + Session-level activation command: `/activation mention` or `/activation always`. This updates session state (not global config) and is owner-gated. ## Configured ACP bindings -WhatsApp supports persistent ACP bindings with top-level `bindings[]` entries: +WhatsApp supports persistent ACP bindings via top-level `bindings[]`: ```json5 { @@ -450,26 +332,17 @@ WhatsApp supports persistent ACP bindings with top-level `bindings[]` entries: } ``` -- Direct chats match E.164 numbers such as `+15555550123`. -- Groups match WhatsApp group JIDs such as `120363424282127706@g.us`. -- Group allowlists, sender policy, and mention or activation gating run before OpenClaw ensures the configured ACP session exists. -- A matched configured ACP binding owns the route. WhatsApp broadcast groups do not fan out that turn to ordinary WhatsApp sessions. +Direct chats match E.164 numbers; groups match WhatsApp group JIDs. Group allowlists, sender policy, and mention/activation gating run before OpenClaw ensures the bound ACP session exists. A matched binding owns the route — broadcast groups do not fan that turn out to ordinary WhatsApp sessions. ## Personal-number and self-chat behavior -When the linked self number is also present in `allowFrom`, WhatsApp self-chat safeguards activate: - -- skip read receipts for self-chat turns -- ignore mention-JID auto-trigger behavior that would otherwise ping yourself -- if `messages.responsePrefix` is unset, self-chat replies default to `[{identity.name}]` or `[openclaw]` +When the linked self number is also present in `allowFrom`, self-chat safeguards activate: skip read receipts for self-chat turns, ignore mention-JID auto-trigger behavior that would ping yourself, and default replies to `[{identity.name}]` (or `[openclaw]`) when `messages.responsePrefix` is unset. ## Message normalization and context - - Incoming WhatsApp messages are wrapped in the shared inbound envelope. - - If a quoted reply exists, context is appended in this form: + + Incoming messages are wrapped in the shared inbound envelope. A quoted reply appends context in this form: ```text [Replying to id:] @@ -477,79 +350,38 @@ When the linked self number is also present in `allowFrom`, WhatsApp self-chat s [/Replying] ``` - Reply metadata fields are also populated when available (`ReplyToId`, `ReplyToBody`, `ReplyToSender`, sender JID/E.164). - When the quoted reply target is downloadable media, OpenClaw saves it through - the normal inbound media store and exposes it as `MediaPath`/`MediaType` so - the agent can inspect the referenced image instead of only seeing - ``. + Reply metadata (`ReplyToId`, `ReplyToBody`, `ReplyToSender`, sender JID/E.164) is populated when available. If the quoted target is downloadable media, OpenClaw saves it through the normal inbound media store and exposes `MediaPath`/`MediaType` so the agent can inspect it directly instead of seeing only ``. - Media-only inbound messages are normalized with placeholders such as: + Media-only messages normalize to placeholders: ``, ``, ``, ``, ``. - - `` - - `` - - `` - - `` - - `` + Authorized group voice notes are transcribed before mention gating when the body is only ``, so saying the bot mention in the voice note can trigger the reply. If the transcript still does not mention the bot, it stays in pending group history instead of the raw placeholder. - Authorized group voice notes are transcribed before mention gating when the - body is only ``, so saying the bot mention in the voice note can - trigger the reply. If the transcript still does not mention the bot, the - transcript is kept in pending group history instead of the raw placeholder. - - Location bodies use terse coordinate text. Location labels/comments and contact/vCard details are rendered as fenced untrusted metadata, not inline prompt text. + Location bodies render as terse coordinate text. Location labels/comments and contact/vCard details render as fenced untrusted metadata, not inline prompt text. - For groups, unprocessed messages can be buffered and injected as context when the bot is finally triggered. + Unprocessed group messages buffer and inject as context when the bot is finally triggered. - default limit: `50` - - config: `channels.whatsapp.historyLimit` - - fallback: `messages.groupChat.historyLimit` + - config: `channels.whatsapp.historyLimit`, fallback `messages.groupChat.historyLimit` - `0` disables - Injection markers: - - - `[Chat messages since your last reply - for context]` - - `[Current message - respond to this]` + Injection markers: `[Chat messages since your last reply - for context]` and `[Current message - respond to this]`. - Read receipts are enabled by default for accepted inbound WhatsApp messages. - - Disable globally: + Enabled by default for accepted inbound messages. Disable globally: ```json5 - { - channels: { - whatsapp: { - sendReadReceipts: false, - }, - }, - } + { channels: { whatsapp: { sendReadReceipts: false } } } ``` - Per-account override: - - ```json5 - { - channels: { - whatsapp: { - accounts: { - work: { - sendReadReceipts: false, - }, - }, - }, - }, - } - ``` - - Self-chat turns skip read receipts even when globally enabled. + Per-account override: `channels.whatsapp.accounts..sendReadReceipts`. Self-chat turns skip read receipts even when globally enabled. @@ -559,87 +391,68 @@ When the linked self number is also present in `allowFrom`, WhatsApp self-chat s - default chunk limit: `channels.whatsapp.textChunkLimit = 4000` - - `channels.whatsapp.chunkMode = "length" | "newline"` - - `newline` mode prefers paragraph boundaries (blank lines), then falls back to length-safe chunking + - `channels.whatsapp.chunkMode = "length" | "newline"`; `newline` prefers paragraph boundaries (blank lines), then falls back to length-safe chunking - supports image, video, audio (PTT voice-note), and document payloads - - audio media is sent through the Baileys `audio` payload with `ptt: true`, so WhatsApp clients render it as a push-to-talk voice note - - reply payloads preserve `audioAsVoice`; TTS voice-note output for WhatsApp stays on this PTT path even when the provider returns MP3 or WebM - - native Ogg/Opus audio is sent as `audio/ogg; codecs=opus` for voice-note compatibility - - non-Ogg audio, including Microsoft Edge TTS MP3/WebM output, is transcoded with `ffmpeg` to 48 kHz mono Ogg/Opus before PTT delivery - - `/tts latest` sends the latest assistant reply as one voice note and suppresses repeat sends for the same reply; `/tts chat on|off|default` controls auto-TTS for the current WhatsApp chat - - animated GIF playback is supported via `gifPlayback: true` on video sends - - `forceDocument` / `asDocument` sends outbound images, GIFs, and videos through the Baileys document payload to avoid WhatsApp media compression while preserving the resolved filename and MIME type - - captions are applied to the first media item when sending multi-media reply payloads, except PTT voice notes send the audio first and visible text separately because WhatsApp clients do not render voice-note captions consistently - - media source can be HTTP(S), `file://`, or local paths + - audio is sent as the Baileys `audio` payload with `ptt: true`, rendering as a push-to-talk voice note; `audioAsVoice` is preserved on reply payloads so TTS voice-note output stays on this path regardless of the provider's source format + - native Ogg/Opus audio sends as `audio/ogg; codecs=opus`; anything else (including Microsoft Edge TTS MP3/WebM output) is transcoded with `ffmpeg` to 48 kHz mono Ogg/Opus before PTT delivery + - `/tts latest` sends the latest assistant reply as one voice note and suppresses repeat sends for the same reply; `/tts chat on|off|default` controls auto-TTS for the current chat + - `gifPlayback: true` on video sends enables animated GIF playback + - `forceDocument`/`asDocument` routes outbound images, GIFs, and videos through the Baileys document payload to avoid WhatsApp's media compression, preserving the resolved filename and MIME type + - captions apply to the first media item in a multi-media reply, except PTT voice notes: the audio sends first with no caption, then the caption sends as a separate text message (WhatsApp clients do not render voice-note captions consistently) + - media source can be HTTP(S), `file://`, or a local path - - inbound media save cap: `channels.whatsapp.mediaMaxMb` (default `50`) - - outbound media send cap: `channels.whatsapp.mediaMaxMb` (default `50`) - - per-account overrides use `channels.whatsapp.accounts..mediaMaxMb` - - images are auto-optimized (resize/quality sweep) to fit limits unless `forceDocument` / `asDocument` requests document delivery - - on media send failure, first-item fallback sends text warning instead of dropping the response silently + - inbound save cap and outbound send cap: `channels.whatsapp.mediaMaxMb` (default `50`) + - per-account override: `channels.whatsapp.accounts..mediaMaxMb` + - images auto-optimize (resize/quality sweep) to fit limits unless `forceDocument`/`asDocument` requests document delivery + - on media send failure, the first-item fallback sends a text warning instead of dropping the response silently ## Reply quoting -WhatsApp supports native reply quoting, where outbound replies visibly quote the inbound message. Control it with `channels.whatsapp.replyToMode`. +`channels.whatsapp.replyToMode` controls native reply quoting (outbound replies visibly quote the inbound message): -| Value | Behavior | -| ----------- | --------------------------------------------------------------------- | -| `"off"` | Never quote; send as a plain message | -| `"first"` | Quote only the first outbound reply chunk | -| `"all"` | Quote every outbound reply chunk | -| `"batched"` | Quote queued batched replies while leaving immediate replies unquoted | +| Value | Behavior | +| ----------------- | -------------------------------------------------------------- | +| `"off"` (default) | Never quote; send as a plain message | +| `"first"` | Quote only the first outbound reply chunk | +| `"all"` | Quote every outbound reply chunk | +| `"batched"` | Quote queued batched replies; leave immediate replies unquoted | -Default is `"off"`. Per-account overrides use `channels.whatsapp.accounts..replyToMode`. +Per-account override: `channels.whatsapp.accounts..replyToMode`. ```json5 -{ - channels: { - whatsapp: { - replyToMode: "first", - }, - }, -} +{ channels: { whatsapp: { replyToMode: "first" } } } ``` ## Reaction level -`channels.whatsapp.reactionLevel` controls how broadly the agent uses emoji reactions on WhatsApp: +`channels.whatsapp.reactionLevel` controls how broadly the agent uses emoji reactions: -| Level | Ack reactions | Agent-initiated reactions | Description | -| ------------- | ------------- | ------------------------- | ------------------------------------------------ | -| `"off"` | No | No | No reactions at all | -| `"ack"` | Yes | No | Ack reactions only (pre-reply receipt) | -| `"minimal"` | Yes | Yes (conservative) | Ack + agent reactions with conservative guidance | -| `"extensive"` | Yes | Yes (encouraged) | Ack + agent reactions with encouraged guidance | +| Level | Ack reactions | Agent-initiated reactions | +| --------------------- | ------------- | -------------------------- | +| `"off"` | No | No | +| `"ack"` | Yes | No | +| `"minimal"` (default) | Yes | Yes, conservative guidance | +| `"extensive"` | Yes | Yes, encouraged guidance | -Default: `"minimal"`. - -Per-account overrides use `channels.whatsapp.accounts..reactionLevel`. +Per-account override: `channels.whatsapp.accounts..reactionLevel`. ```json5 -{ - channels: { - whatsapp: { - reactionLevel: "ack", - }, - }, -} +{ channels: { whatsapp: { reactionLevel: "ack" } } } ``` ## Acknowledgment reactions -WhatsApp supports immediate ack reactions on inbound receipt via `channels.whatsapp.ackReaction`. -Ack reactions are gated by `reactionLevel` — they are suppressed when `reactionLevel` is `"off"`. +`channels.whatsapp.ackReaction` sends an immediate reaction on inbound receipt, gated by `reactionLevel` (suppressed when `"off"`): ```json5 { @@ -655,17 +468,11 @@ Ack reactions are gated by `reactionLevel` — they are suppressed when `reactio } ``` -Behavior notes: - -- sent immediately after inbound is accepted (pre-reply) -- if `ackReaction` is present without `emoji`, WhatsApp uses the routed agent's identity emoji, falling back to "👀"; omit `ackReaction` or set `emoji: ""` to send no ack reaction -- failures are logged but do not block normal reply delivery -- group mode `mentions` reacts on mention-triggered turns; group activation `always` acts as bypass for this check -- WhatsApp uses `channels.whatsapp.ackReaction` (legacy `messages.ackReaction` is not used here) +Notes: sent immediately after inbound is accepted (pre-reply); if `ackReaction` is present without `emoji`, WhatsApp uses the routed agent's identity emoji falling back to "👀" (omit `ackReaction` or set `emoji: ""` for no ack); failures are logged but do not block reply delivery; group mode `mentions` reacts only on mention-triggered turns, while group activation `always` bypasses that check; WhatsApp uses `channels.whatsapp.ackReaction` only (legacy `messages.ackReaction` does not apply here). ## Lifecycle status reactions -Set `messages.statusReactions.enabled: true` to let WhatsApp replace the ack reaction during a turn instead of leaving a static receipt emoji. When enabled, OpenClaw uses the same inbound message reaction slot for lifecycle states such as queued, thinking, tool activity, compaction, done, and error. +Set `messages.statusReactions.enabled: true` to let WhatsApp replace the ack reaction during a turn instead of leaving a static receipt emoji, cycling through states such as queued, thinking, tool activity, compaction, done, and error: ```json5 { @@ -682,35 +489,23 @@ Set `messages.statusReactions.enabled: true` to let WhatsApp replace the ack rea } ``` -Behavior notes: - -- `channels.whatsapp.ackReaction` still controls whether status reactions are eligible for direct messages and groups. -- The queued status reaction uses the same effective ack emoji as plain ack reactions. -- WhatsApp has one bot reaction slot per message, so lifecycle updates replace the current reaction in place. -- `messages.removeAckAfterReply: true` clears the final status reaction after the configured done/error hold. -- Tool emoji categories include `tool`, `coding`, `web`, `deploy`, `build`, and `concierge`. +Notes: `channels.whatsapp.ackReaction` still controls eligibility for direct messages and groups; the queued state uses the same effective emoji as plain ack reactions; WhatsApp has one bot reaction slot per message, so lifecycle updates replace the current reaction in place; `messages.removeAckAfterReply: true` clears the final status reaction after the configured done/error hold; tool emoji categories include `tool`, `coding`, `web`, `deploy`, `build`, and `concierge`. ## Multi-account and credentials - - account ids come from `channels.whatsapp.accounts` - - default account selection: `default` if present, otherwise first configured account id (sorted) - - account ids are normalized internally for lookup - + Account ids come from `channels.whatsapp.accounts`. Default account selection is `default` if present, otherwise the first configured account id (alphabetically sorted). Account ids are normalized internally for lookup. - - current auth path: `~/.openclaw/credentials/whatsapp//creds.json` - - backup file: `creds.json.bak` + - current auth path: `~/.openclaw/credentials/whatsapp//creds.json` (backup: `creds.json.bak`) - legacy default auth in `~/.openclaw/credentials/` is still recognized/migrated for default-account flows - `openclaw channels logout --channel whatsapp [--account ]` clears WhatsApp auth state for that account. - - When a Gateway is reachable, logout first stops the live WhatsApp listener for the selected account so the linked session does not keep receiving messages until the next restart. `openclaw channels remove --channel whatsapp` also stops the live listener before disabling or deleting account config. + `openclaw channels logout --channel whatsapp [--account ]` clears WhatsApp auth state for that account. When a gateway is reachable, logout stops the live listener for that account first, so the linked session stops receiving messages before the next restart. `openclaw channels remove --channel whatsapp` also stops the live listener before disabling or deleting account config. In legacy auth directories, `oauth.json` is preserved while Baileys auth files are removed. @@ -719,11 +514,9 @@ Behavior notes: ## Tools, actions, and config writes -- Agent tool support includes WhatsApp reaction action (`react`). -- Action gates: - - `channels.whatsapp.actions.reactions` - - `channels.whatsapp.actions.polls` -- Channel-initiated config writes are enabled by default (disable via `channels.whatsapp.configWrites=false`). +- Agent tool support includes the WhatsApp reaction action (`react`). +- Action gates: `channels.whatsapp.actions.reactions`, `channels.whatsapp.actions.polls` (existing actions default to `true`), `channels.whatsapp.actions.calls` (default `false`, see MeowCaller above). +- Channel-initiated config writes are enabled by default; disable via `channels.whatsapp.configWrites: false`. ## Troubleshooting @@ -731,26 +524,19 @@ Behavior notes: Symptom: channel status reports not linked. - Fix: - - ```bash - openclaw channels login --channel whatsapp - openclaw channels status - ``` +```bash +openclaw channels login --channel whatsapp +openclaw channels status +``` Symptom: linked account with repeated disconnects or reconnect attempts. - Quiet accounts can stay connected past the normal message timeout; the watchdog - restarts when WhatsApp Web transport activity stops, the socket closes, or - application-level activity stays silent beyond the longer safety window. + Quiet accounts can stay connected past the normal message timeout; the watchdog restarts only when WhatsApp Web transport activity stops, the socket closes, or application-level activity stays silent beyond the longer safety window (see Runtime model above). - If logs show repeated `status=408 Request Time-out Connection was lost`, tune - Baileys socket timings under `web.whatsapp`. Start by shortening - `keepAliveIntervalMs` below your network's idle timeout and increasing - `connectTimeoutMs` on slow or lossy links: + If logs show repeated `status=408 Request Time-out Connection was lost`, tune Baileys socket timings under `web.whatsapp`. Start by shortening `keepAliveIntervalMs` below your network's idle timeout and increasing `connectTimeoutMs` on slow or lossy links: ```json5 { @@ -773,8 +559,7 @@ Behavior notes: openclaw gateway status ``` - If the loop persists after host connectivity and timing are fixed, back up - the account auth directory and re-link that account: + If the loop persists after host connectivity and timing are fixed, back up the account auth directory and re-link: ```bash cp -a ~/.openclaw/credentials/whatsapp/ \ @@ -783,51 +568,32 @@ Behavior notes: openclaw channels login --channel whatsapp --account ``` - If `~/.openclaw/logs/whatsapp-health.log` says `Gateway inactive` but - `openclaw gateway status` and `openclaw channels status --probe` show the - gateway and WhatsApp are healthy, run `openclaw doctor`. On Linux, doctor - warns about legacy crontab entries that still invoke - `~/.openclaw/bin/ensure-whatsapp.sh`; remove those stale entries with - `crontab -e` because cron can lack the systemd user-bus environment and - make that old script misreport gateway health. - - If needed, re-link with `channels login`. + If `~/.openclaw/logs/whatsapp-health.log` says `Gateway inactive` but `openclaw gateway status` and `openclaw channels status --probe` both show healthy, run `openclaw doctor`. On Linux, doctor warns about legacy crontab entries invoking the retired `~/.openclaw/bin/ensure-whatsapp.sh` script; remove those entries with `crontab -e` — cron can lack the systemd user-bus environment and make that old script misreport gateway health. - Symptom: `openclaw channels login --channel whatsapp` fails before showing a usable QR code with `status=408 Request Time-out` or a TLS socket disconnect. + Symptom: `openclaw channels login --channel whatsapp` fails before showing a usable QR with `status=408 Request Time-out` or a TLS socket disconnect. - WhatsApp Web login uses the gateway host's standard proxy environment (`HTTPS_PROXY`, `HTTP_PROXY`, lowercase variants, and `NO_PROXY`). Verify the gateway process inherits the proxy env and that `NO_PROXY` does not match `mmg.whatsapp.net`. + WhatsApp Web login uses the gateway host's standard proxy environment (`HTTPS_PROXY`, `HTTP_PROXY`, lowercase variants, `NO_PROXY`). Verify the gateway process inherits the proxy env and that `NO_PROXY` does not match `mmg.whatsapp.net`. - Outbound sends fail fast when no active gateway listener exists for the target account. - - Make sure gateway is running and the account is linked. - + Outbound sends fail fast when no active gateway listener exists for the target account. Confirm the gateway is running and the account is linked. - Transcript rows record what the agent generated. WhatsApp delivery is checked separately: OpenClaw only treats an auto-reply as sent after Baileys returns an outbound message id for at least one visible text or media send. + Transcript rows record what the agent generated; WhatsApp delivery is checked separately. OpenClaw only treats an auto-reply as sent after Baileys returns an outbound message id for at least one visible text or media send. - Ack reactions are independent pre-reply receipts. A successful reaction does not prove that the later text or media reply was accepted by WhatsApp. - - Check gateway logs for `auto-reply delivery failed` or `auto-reply was not accepted by WhatsApp provider`. + Ack reactions are independent pre-reply receipts — a successful reaction does not prove the later text/media reply was accepted. Check gateway logs for `auto-reply delivery failed` or `auto-reply was not accepted by WhatsApp provider`. - Check in this order: + Check in this order: `groupPolicy`, `groupAllowFrom`/`allowFrom`, `groups` allowlist entries, mention gating (`requireMention` + mention patterns), and duplicate keys in `openclaw.json` (JSON5 later entries override earlier ones — keep a single `groupPolicy` per scope). - - `groupPolicy` - - `groupAllowFrom` / `allowFrom` - - `groups` allowlist entries - - mention gating (`requireMention` + mention patterns) - - duplicate keys in `openclaw.json` (JSON5): later entries override earlier ones, so keep a single `groupPolicy` per scope - - If `channels.whatsapp.groups` is present, WhatsApp can still observe messages from other groups, but OpenClaw drops them before session routing. Add the group JID to `channels.whatsapp.groups` or add `groups["*"]` to admit all groups while keeping sender authorization under `groupPolicy` and `groupAllowFrom`. + If `channels.whatsapp.groups` is present, WhatsApp can still observe messages from other groups, but OpenClaw drops them before session routing. Add the group JID to `channels.whatsapp.groups`, or add `groups["*"]` to admit all groups while keeping sender authorization under `groupPolicy`/`groupAllowFrom`. @@ -840,32 +606,29 @@ Behavior notes: WhatsApp supports Telegram-style system prompts for groups and direct chats via the `groups` and `direct` maps. -Resolution hierarchy for group messages: +Resolution for group messages: the effective `groups` map is determined first — if the account defines its own `groups` key at all, it fully replaces the root `groups` map (no deep merge). Prompt lookup then runs on that single resulting map: -The effective `groups` map is determined first: if the account defines its own `groups`, it fully replaces the root `groups` map (no deep merge). Prompt lookup then runs on the resulting single map: +1. **Group-specific prompt** (`groups[""].systemPrompt`): used when the group entry exists **and** its `systemPrompt` key is defined. An empty string (`""`) suppresses the wildcard and applies no prompt. +2. **Group wildcard prompt** (`groups["*"].systemPrompt`): used when the specific group entry is absent, or exists without a `systemPrompt` key. -1. **Group-specific system prompt** (`groups[""].systemPrompt`): used when the specific group entry exists in the map **and** its `systemPrompt` key is defined. If `systemPrompt` is an empty string (`""`), the wildcard is suppressed and no system prompt is applied. -2. **Group wildcard system prompt** (`groups["*"].systemPrompt`): used when the specific group entry is absent from the map entirely, or when it exists but defines no `systemPrompt` key. - -Resolution hierarchy for direct messages: - -The effective `direct` map is determined first: if the account defines its own `direct`, it fully replaces the root `direct` map (no deep merge). Prompt lookup then runs on the resulting single map: - -1. **Direct-specific system prompt** (`direct[""].systemPrompt`): used when the specific peer entry exists in the map **and** its `systemPrompt` key is defined. If `systemPrompt` is an empty string (`""`), the wildcard is suppressed and no system prompt is applied. -2. **Direct wildcard system prompt** (`direct["*"].systemPrompt`): used when the specific peer entry is absent from the map entirely, or when it exists but defines no `systemPrompt` key. +Resolution for direct messages follows the identical pattern against the `direct` map and `direct["*"]`. `dms` remains the lightweight per-DM history override bucket (`dms..historyLimit`). Prompt overrides live under `direct`. -**Difference from Telegram multi-account behavior:** In Telegram, root `groups` is intentionally suppressed for all accounts in a multi-account setup — even accounts that define no `groups` of their own — to prevent a bot from receiving group messages for groups it does not belong to. WhatsApp does not apply this guard: root `groups` and root `direct` are always inherited by accounts that define no account-level override, regardless of how many accounts are configured. In a multi-account WhatsApp setup, if you want per-account group or direct prompts, define the full map under each account explicitly rather than relying on root-level defaults. + +This account-replaces-root behavior for prompt resolution is a plain shallow override: any account `groups`/`direct` key, including an explicit empty object, replaces the root map. It differs from the group-membership allowlist check described above, which has a single-account safety net for an accidentally empty `groups: {}`. + + +**Difference from Telegram:** Telegram suppresses root `groups` for every account in a multi-account setup (even accounts with no `groups` of their own) to stop a bot receiving group messages for groups it does not belong to. WhatsApp does not apply that guard — root `groups`/`direct` are inherited by any account without its own override, regardless of account count. In a multi-account WhatsApp setup, define the full map under each account explicitly if you want per-account prompts. Important behavior: -- `channels.whatsapp.groups` is both a per-group config map and the chat-level group allowlist. At either the root or account scope, `groups["*"]` means "all groups are admitted" for that scope. -- Only add a wildcard group `systemPrompt` when you already want that scope to admit all groups. If you still want only a fixed set of group IDs to be eligible, do not use `groups["*"]` for the prompt default. Instead, repeat the prompt on each explicitly allowlisted group entry. -- Group admission and sender authorization are separate checks. `groups["*"]` widens the set of groups that can reach group handling, but it does not by itself authorize every sender in those groups. Sender access is still controlled separately by `channels.whatsapp.groupPolicy` and `channels.whatsapp.groupAllowFrom`. -- `channels.whatsapp.direct` does not have the same side effect for DMs. `direct["*"]` only provides a default direct-chat config after a DM is already admitted by `dmPolicy` plus `allowFrom` or pairing-store rules. +- `channels.whatsapp.groups` is both a per-group config map and the chat-level group allowlist. At either root or account scope, `groups["*"]` means "all groups are admitted" for that scope. +- Only add a wildcard `systemPrompt` when you already want that scope to admit all groups. To keep only a fixed set of group IDs eligible, repeat the prompt on each explicitly allowlisted entry instead of using `groups["*"]`. +- Group admission and sender authorization are separate checks. `groups["*"]` widens which groups reach group handling; it does not authorize every sender in those groups — that stays controlled by `groupPolicy`/`groupAllowFrom`. +- `channels.whatsapp.direct` has no equivalent side effect for DMs: `direct["*"]` only supplies a default config after a DM is already admitted by `dmPolicy` plus `allowFrom` or pairing-store rules. Example: @@ -909,18 +672,16 @@ Example: ## Configuration reference pointers -Primary reference: +Primary reference: [Configuration reference - WhatsApp](/gateway/config-channels#whatsapp) -- [Configuration reference - WhatsApp](/gateway/config-channels#whatsapp) - -High-signal WhatsApp fields: - -- access: `dmPolicy`, `allowFrom`, `groupPolicy`, `groupAllowFrom`, `groups` -- delivery: `textChunkLimit`, `chunkMode`, `mediaMaxMb`, `sendReadReceipts`, `ackReaction`, `reactionLevel` -- multi-account: `accounts..enabled`, `accounts..authDir`, account-level overrides -- operations: `configWrites`, `debounceMs`, `web.enabled`, `web.heartbeatSeconds`, `web.reconnect.*`, `web.whatsapp.*` -- session behavior: `session.dmScope`, `historyLimit`, `dmHistoryLimit`, `dms..historyLimit` -- prompts: `groups..systemPrompt`, `groups["*"].systemPrompt`, `direct..systemPrompt`, `direct["*"].systemPrompt` +| Area | Fields | +| ---------------- | -------------------------------------------------------------------------------------------------------------- | +| Access | `dmPolicy`, `allowFrom`, `groupPolicy`, `groupAllowFrom`, `groups` | +| Delivery | `textChunkLimit`, `chunkMode`, `mediaMaxMb`, `sendReadReceipts`, `ackReaction`, `reactionLevel` | +| Multi-account | `accounts..enabled`, `accounts..authDir`, and other per-account overrides | +| Operations | `configWrites`, `debounceMs`, `web.enabled`, `web.heartbeatSeconds`, `web.reconnect.*`, `web.whatsapp.*` | +| Session behavior | `session.dmScope`, `historyLimit`, `dmHistoryLimit`, `dms..historyLimit` | +| Prompts | `groups..systemPrompt`, `groups["*"].systemPrompt`, `direct..systemPrompt`, `direct["*"].systemPrompt` | ## Related diff --git a/docs/channels/yuanbao.md b/docs/channels/yuanbao.md index 9c493a5d0651..623883af8daf 100644 --- a/docs/channels/yuanbao.md +++ b/docs/channels/yuanbao.md @@ -6,27 +6,23 @@ read_when: title: Yuanbao --- -Tencent Yuanbao is Tencent's AI assistant platform. The OpenClaw channel plugin -connects Yuanbao bots to OpenClaw over WebSocket so they can interact with users -through direct messages and group chats. +Tencent Yuanbao is Tencent's AI assistant platform. The community-maintained `openclaw-plugin-yuanbao` plugin connects Yuanbao bots to OpenClaw over WebSocket for direct messages and group chats. -**Status:** production-ready for bot DMs + group chats. WebSocket is the only supported connection mode. - ---- +**Status:** production-ready for bot DMs and group chats. WebSocket is the only supported connection mode. This plugin is maintained by the Tencent Yuanbao team as an external catalog entry, not by core OpenClaw; the config/behavior details below (beyond install and the generic CLI surface) come from the plugin's own docs and are not verified against OpenClaw core source. ## Quick start -> **Requires OpenClaw 2026.4.10 or above.** Run `openclaw --version` to check. Upgrade with `openclaw update`. +Requires OpenClaw 2026.4.10 or above. Check with `openclaw --version`; upgrade with `openclaw update`. ```bash openclaw channels add --channel yuanbao --token "appKey:appSecret" ``` - The `--token` value uses colon-separated `appKey:appSecret` format. You can obtain these from the Yuanbao app by creating a robot in your application settings. + `--token` uses colon-separated `appKey:appSecret`. Get these from the Yuanbao app by creating a bot in your application settings. - + ```bash openclaw gateway restart ``` @@ -35,28 +31,26 @@ through direct messages and group chats. ### Interactive setup (alternative) -You can also use the interactive wizard: - ```bash openclaw channels login --channel yuanbao ``` Follow the prompts to enter your App ID and App Secret. ---- - ## Access control ### Direct messages -Configure `dmPolicy` to control who can DM the bot: +`channels.yuanbao.dm.policy`: -- `"pairing"` - unknown users receive a pairing code; approve via CLI -- `"allowlist"` - only users listed in `allowFrom` can chat -- `"open"` - allow all users (default) -- `"disabled"` - disable all DMs +| Value | Behavior | +| ---------------- | ------------------------------------------------- | +| `open` (default) | Allow all users | +| `pairing` | Unknown users get a pairing code; approve via CLI | +| `allowlist` | Only users in `allowFrom` can chat | +| `disabled` | Disable all DMs | -**Approve a pairing request:** +Approve a pairing request: ```bash openclaw pairing list yuanbao @@ -65,18 +59,11 @@ openclaw pairing approve yuanbao ### Group chats -**Mention requirement** (`channels.yuanbao.requireMention`): - -- `true` - require @mention (default) -- `false` - respond without @mention - -Replying to the bot's message in a group chat is treated as an implicit mention. - ---- +`channels.yuanbao.requireMention` (default `true`): require an @mention before the bot responds in a group. Replying to the bot's own message is treated as an implicit mention. ## Configuration examples -### Basic setup with open DM policy +Basic setup, open DM policy: ```json5 { @@ -92,7 +79,7 @@ Replying to the bot's message in a group chat is treated as an implicit mention. } ``` -### Restrict DMs to specific users +Restrict DMs to specific users: ```json5 { @@ -109,7 +96,7 @@ Replying to the bot's message in a group chat is treated as an implicit mention. } ``` -### Disable @mention requirement in groups +Disable the @mention requirement in groups: ```json5 { @@ -121,20 +108,7 @@ Replying to the bot's message in a group chat is treated as an implicit mention. } ``` -### Optimize outbound message delivery - -```json5 -{ - channels: { - yuanbao: { - // Send each chunk immediately without buffering - outboundQueueStrategy: "immediate", - }, - }, -} -``` - -### Tune merge-text strategy +Outbound delivery tuning: ```json5 { @@ -149,7 +123,7 @@ Replying to the bot's message in a group chat is treated as an implicit mention. } ``` ---- +Set `outboundQueueStrategy: "immediate"` to send each chunk without buffering. ## Common commands @@ -162,39 +136,35 @@ Replying to the bot's message in a group chat is treated as an implicit mention. | `/restart` | Restart OpenClaw | | `/compact` | Compact the session context | -> Yuanbao supports native slash-command menus. Commands are synced to the platform automatically when the gateway starts. - ---- +Yuanbao supports native slash-command menus; commands sync to the platform automatically when the gateway starts. ## Troubleshooting -### Bot does not respond in group chats +**Bot does not respond in group chats:** -1. Ensure the bot is added to the group -2. Ensure you @mention the bot (required by default) +1. Confirm the bot is added to the group +2. Confirm you @mention the bot (required by default) 3. Check logs: `openclaw logs --follow` -### Bot does not receive messages +**Bot does not receive messages:** -1. Ensure the bot is created and approved in the Yuanbao app -2. Ensure `appKey` and `appSecret` are correctly configured -3. Ensure the gateway is running: `openclaw gateway status` +1. Confirm the bot is created and approved in the Yuanbao app +2. Confirm `appKey` and `appSecret` are correctly configured +3. Confirm the gateway is running: `openclaw gateway status` 4. Check logs: `openclaw logs --follow` -### Bot sends empty or fallback replies +**Bot sends empty or fallback replies:** -1. Check if the AI model is returning valid content -2. The default fallback reply is: "暂时无法解答,你可以换个问题问问我哦" -3. Customize it via `channels.yuanbao.fallbackReply` +1. Check whether the AI model is returning valid content +2. Default fallback reply: "暂时无法解答,你可以换个问题问问我哦" +3. Customize with `channels.yuanbao.fallbackReply` -### App Secret leaked +**App Secret leaked:** -1. Reset the App Secret in YuanBao APP +1. Reset the App Secret in the Yuanbao app 2. Update the value in your config 3. Restart the gateway: `openclaw gateway restart` ---- - ## Advanced configuration ### Multiple accounts @@ -226,13 +196,13 @@ Replying to the bot's message in a group chat is treated as an implicit mention. ### Message limits -- `maxChars` - single message max character count (default: `3000` chars) -- `mediaMaxMb` - media upload/download limit (default: `20` MB) -- `overflowPolicy` - behavior when message exceeds limit: `"split"` (default) or `"stop"` +- `maxChars`: single message max character count (default `3000`) +- `mediaMaxMb`: media upload/download limit (default `20` MB) +- `overflowPolicy`: behavior when a message exceeds the limit, `"split"` (default) or `"stop"` ### Streaming -Yuanbao supports block-level streaming output. When enabled, the bot sends text in chunks as it generates. +Yuanbao supports block-level streaming output; the bot sends text in chunks as it generates. ```json5 { @@ -248,8 +218,6 @@ Set `disableBlockStreaming: true` to send the complete reply in one message. ### Group chat history context -Control how many historical messages are included in the AI context for group chats: - ```json5 { channels: { @@ -260,9 +228,9 @@ Control how many historical messages are included in the AI context for group ch } ``` -### Reply-to mode +Controls how many historical messages are included in the AI context for group chats. -Control how the bot quotes messages when replying in group chats: +### Reply-to mode ```json5 { @@ -274,15 +242,15 @@ Control how the bot quotes messages when replying in group chats: } ``` -| Value | Behavior | -| --------- | -------------------------------------------------------- | -| `"off"` | No quote reply | -| `"first"` | Quote only the first reply per inbound message (default) | -| `"all"` | Quote every reply | +| Value | Behavior | +| ------- | -------------------------------------------------------- | +| `off` | No quote reply | +| `first` | Quote only the first reply per inbound message (default) | +| `all` | Quote every reply | ### Markdown hint injection -By default, the bot injects instructions in the system prompt to prevent the AI model from wrapping the entire reply in markdown code blocks. +By default, the bot injects a system-prompt instruction to prevent the model from wrapping the entire reply in a markdown code block. ```json5 { @@ -296,8 +264,6 @@ By default, the bot injects instructions in the system prompt to prevent the AI ### Debug mode -Enable unsanitized log output for specific bot IDs: - ```json5 { channels: { @@ -308,9 +274,11 @@ Enable unsanitized log output for specific bot IDs: } ``` +Enables unsanitized log output for the listed bot IDs. + ### Multi-agent routing -Use `bindings` to route Yuanbao DMs or groups to different agents. +Use `bindings` to route Yuanbao DMs or groups to different agents: ```json5 { @@ -340,14 +308,10 @@ Use `bindings` to route Yuanbao DMs or groups to different agents. } ``` -Routing fields: - - `match.channel`: `"yuanbao"` - `match.peer.kind`: `"direct"` (DM) or `"group"` (group chat) - `match.peer.id`: user ID or group code ---- - ## Configuration reference Full configuration: [Gateway configuration](/gateway/configuration) @@ -356,8 +320,8 @@ Full configuration: [Gateway configuration](/gateway/configuration) | ------------------------------------------ | ------------------------------------------------- | -------------------------------------- | | `channels.yuanbao.enabled` | Enable/disable the channel | `true` | | `channels.yuanbao.defaultAccount` | Default account for outbound routing | `default` | -| `channels.yuanbao.accounts..appKey` | App Key (used for signing and ticket generation) | - | -| `channels.yuanbao.accounts..appSecret` | App Secret (used for signing) | - | +| `channels.yuanbao.accounts..appKey` | App Key (signing + ticket generation) | - | +| `channels.yuanbao.accounts..appSecret` | App Secret (signing) | - | | `channels.yuanbao.accounts..token` | Pre-signed token (skips automatic ticket signing) | - | | `channels.yuanbao.accounts..name` | Account display name | - | | `channels.yuanbao.accounts..enabled` | Enable/disable a specific account | `true` | @@ -373,39 +337,17 @@ Full configuration: [Gateway configuration](/gateway/configuration) | `channels.yuanbao.mediaMaxMb` | Media size limit (MB) | `20` | | `channels.yuanbao.historyLimit` | Group chat history context entries | `100` | | `channels.yuanbao.disableBlockStreaming` | Disable block-level streaming output | `false` | -| `channels.yuanbao.fallbackReply` | Fallback reply when AI returns no content | `暂时无法解答,你可以换个问题问问我哦` | +| `channels.yuanbao.fallbackReply` | Fallback reply when the model returns no content | `暂时无法解答,你可以换个问题问问我哦` | | `channels.yuanbao.markdownHintEnabled` | Inject markdown anti-wrapping instructions | `true` | -| `channels.yuanbao.debugBotIds` | Debug whitelist bot IDs (unsanitized logs) | `[]` | - ---- +| `channels.yuanbao.debugBotIds` | Debug allowlist bot IDs (unsanitized logs) | `[]` | ## Supported message types -### Receive +**Receive:** text, images, files, audio/voice, video, stickers/custom emoji, custom elements (link cards). -- ✅ Text -- ✅ Images -- ✅ Files -- ✅ Audio / Voice -- ✅ Video -- ✅ Stickers / Custom emoji -- ✅ Custom elements (link cards, etc.) +**Send:** text (markdown), images, files, audio, video, stickers. -### Send - -- ✅ Text (with markdown support) -- ✅ Images -- ✅ Files -- ✅ Audio -- ✅ Video -- ✅ Stickers - -### Threads and replies - -- ✅ Quote replies (configurable via `replyToMode`) -- ❌ Thread replies (not supported by platform) - ---- +**Threads and replies:** quote replies (configurable via `replyToMode`); thread replies are not supported by the platform. ## Related diff --git a/docs/channels/zalo.md b/docs/channels/zalo.md index 3eff95dad56d..57a1d10f21bb 100644 --- a/docs/channels/zalo.md +++ b/docs/channels/zalo.md @@ -5,31 +5,25 @@ read_when: title: "Zalo" --- -Status: experimental. DMs are supported. The [Capabilities](#capabilities) section below reflects current Marketplace-bot behavior. +Status: experimental. Direct messages and group chats are both implemented; the [Capabilities](#capabilities) table below reflects verified behavior on Zalo Bot Creator / Marketplace bots. ## Bundled plugin -Zalo ships as a bundled plugin in current OpenClaw releases, so normal packaged -builds do not need a separate install. +Zalo ships as a bundled plugin in current OpenClaw releases, so packaged builds do not need a separate install. -If you are on an older build or a custom install that excludes Zalo, install the -npm package directly: +On an older build or a custom install that excludes Zalo, install the npm package directly: -- Install via CLI: `openclaw plugins install @openclaw/zalo` -- Pinned version: `openclaw plugins install @openclaw/zalo@2026.5.2` -- Or from a source checkout: `openclaw plugins install ./path/to/local/zalo-plugin` +- Install: `openclaw plugins install @openclaw/zalo` +- Pinned version: `openclaw plugins install @openclaw/zalo@2026.6.11` +- From a local checkout: `openclaw plugins install ./path/to/local/zalo-plugin` - Details: [Plugins](/tools/plugin) -## Quick setup (beginner) +## Quick setup -1. Ensure the Zalo plugin is available. - - Current packaged OpenClaw releases already bundle it. - - Older/custom installs can add it manually with the commands above. -2. Set the token: - - Env: `ZALO_BOT_TOKEN=...` - - Or config: `channels.zalo.accounts.default.botToken: "..."`. -3. Restart the gateway (or finish setup). -4. DM access is pairing by default; approve the pairing code on first contact. +1. Create a bot token at [https://bot.zaloplatforms.com](https://bot.zaloplatforms.com) (sign in, create a bot, configure settings). The token is `numeric_id:secret`; for Marketplace bots the usable runtime token may appear in the bot's welcome message. +2. Set the token, either as env `ZALO_BOT_TOKEN=...` (default account only) or in config. +3. Restart the gateway. +4. Approve the pairing code on first DM contact (default DM policy is pairing). Minimal config: @@ -49,205 +43,137 @@ Minimal config: } ``` +Multi-account: add more entries under `channels.zalo.accounts.`, each with its own `botToken`/`name`. `channels.zalo.botToken` (flat, no `accounts`) is a legacy single-account shorthand; prefer `accounts..*` for new configs. + ## What it is -Zalo is a Vietnam-focused messaging app; its Bot API lets the Gateway run a bot for 1:1 conversations. -It is a good fit for support or notifications where you want deterministic routing back to Zalo. +Zalo is a Vietnam-focused messaging app. Its Bot API lets the Gateway run a bot for both 1:1 conversations and group chats, with deterministic routing back to Zalo (the model never chooses channels). -This page reflects current OpenClaw behavior for **Zalo Bot Creator / Marketplace bots**. -**Zalo Official Account (OA) bots** are a different Zalo product surface and may behave differently. +This page covers **Zalo Bot Creator / Marketplace bots**. **Zalo Official Account (OA) bots** are a different product surface and may behave differently; this page does not cover them. -- A Zalo Bot API channel owned by the Gateway. -- Deterministic routing: replies go back to Zalo; the model never chooses channels. -- DMs share the agent's main session. -- The [Capabilities](#capabilities) section below shows current Marketplace-bot support. - -## Setup (fast path) - -### 1) Create a bot token (Zalo Bot Platform) - -1. Go to [https://bot.zaloplatforms.com](https://bot.zaloplatforms.com) and sign in. -2. Create a new bot and configure its settings. -3. Copy the full bot token (typically `numeric_id:secret`). For Marketplace bots, the usable runtime token may appear in the bot's welcome message after creation. - -### 2) Configure the token (env or config) - -Example: - -```json5 -{ - channels: { - zalo: { - enabled: true, - accounts: { - default: { - botToken: "12345689:abc-xyz", - dmPolicy: "pairing", - }, - }, - }, - }, -} -``` - -If you later move to a Zalo bot surface where groups are available, you can add group-specific config such as `groupPolicy` and `groupAllowFrom` explicitly. For current Marketplace-bot behavior, see [Capabilities](#capabilities). - -Env option: `ZALO_BOT_TOKEN=...` (works for the default account only). - -Multi-account support: use `channels.zalo.accounts` with per-account tokens and optional `name`. - -3. Restart the gateway. Zalo starts when a token is resolved (env or config). -4. DM access defaults to pairing. Approve the code when the bot is first contacted. - -## How it works (behavior) +## How it works - Inbound messages are normalized into the shared channel envelope with media placeholders. -- Replies always route back to the same Zalo chat. -- Long-polling by default; webhook mode available with `channels.zalo.webhookUrl`. +- Replies always route back to the same Zalo chat; quote-reply is not used (`replyToMode` is fixed off). +- Long-polling (`getUpdates`) by default; webhook mode available via `channels.zalo.webhookUrl`. +- Groups require an @mention to trigger the bot; this is not configurable per channel. ## Limits -- Outbound text is chunked to 2000 characters (Zalo API limit). -- Media downloads/uploads are capped by `channels.zalo.mediaMaxMb` (default 5). -- Streaming is blocked by default due to the 2000 char limit making streaming less useful. +| Limit | Value | +| ------------------------------ | ----------------------------------------------------------------------------- | +| Outbound text chunk size | 2000 characters (Zalo API limit) | +| Media size (inbound/outbound) | `channels.zalo.mediaMaxMb`, default `5` MB | +| Webhook request body | 1 MB, 30s read timeout | +| Webhook rate limit | 120 requests / 60s per path+client IP, then HTTP 429 | +| Webhook duplicate-event window | 5 minutes (keyed on path + account + event name + chat + sender + message id) | -## Access control (DMs) +## Access control -### DM access +### Direct messages -- Default: `channels.zalo.dmPolicy = "pairing"`. Unknown senders receive a pairing code; messages are ignored until approved (codes expire after 1 hour). -- Approve via: +- `channels.zalo.dmPolicy`: `pairing` (default) | `allowlist` | `open` | `disabled`. +- Pairing: unknown senders get a pairing code; messages are ignored until approved. Codes expire after 1 hour. - `openclaw pairing list zalo` - `openclaw pairing approve zalo ` -- Pairing is the default token exchange. Details: [Pairing](/channels/pairing) -- `channels.zalo.allowFrom` accepts numeric user IDs (no username lookup available). + - Details: [Pairing](/channels/pairing) +- `channels.zalo.allowFrom` accepts numeric Zalo user IDs (no username lookup). `open` requires `"*"`. -## Access control (Groups) +### Groups -For **Zalo Bot Creator / Marketplace bots**, group support was not available in practice because the bot could not be added to a group at all. +Group chats are supported by the plugin (`chatTypes: ["direct", "group"]`) and gated by mention plus group policy: -That means the group-related config keys below exist in the schema, but were not usable for Marketplace bots: - -- `channels.zalo.groupPolicy` controls group inbound handling: `open | allowlist | disabled`. -- `channels.zalo.groupAllowFrom` restricts which sender IDs can trigger the bot in groups. -- If `groupAllowFrom` is unset, Zalo falls back to `allowFrom` for sender checks. -- Runtime note: if `channels.zalo` is missing entirely, runtime still falls back to `groupPolicy="allowlist"` for safety. - -The group policy values (when group access is available on your bot surface) are: - -- `groupPolicy: "disabled"` — blocks all group messages. -- `groupPolicy: "open"` — allows any group member (mention-gated). -- `groupPolicy: "allowlist"` — fail-closed default; only allowed senders are accepted. - -If you are using a different Zalo bot product surface and have verified working group behavior, document that separately rather than assuming it matches the Marketplace-bot flow. +- `channels.zalo.groupPolicy`: `open` | `allowlist` | `disabled`. +- `channels.zalo.groupAllowFrom` restricts which sender IDs can trigger the bot in groups; falls back to `allowFrom` when unset. +- Default resolution: when `channels.zalo` is configured, an unset `groupPolicy` resolves to `open`. When `channels.zalo` is missing entirely, runtime fails closed to `allowlist`. +- Reported real-world caveat: on some Marketplace-bot setups the bot could not be added to a group at all. If you hit that, verify with your bot's Zalo Bot Platform settings; it is a platform-side constraint, not an OpenClaw policy. ## Long-polling vs webhook - Default: long-polling (no public URL required). - Webhook mode: set `channels.zalo.webhookUrl` and `channels.zalo.webhookSecret`. - - The webhook secret must be 8-256 characters. - Webhook URL must use HTTPS. - - Zalo sends events with `X-Bot-Api-Secret-Token` header for verification. - - Gateway HTTP handles webhook requests at `channels.zalo.webhookPath` (defaults to the webhook URL path). - - Requests must use `Content-Type: application/json` (or `+json` media types). - - Duplicate events (`event_name + message_id`) are ignored for a short replay window. - - Burst traffic is rate-limited per path/source and may return HTTP 429. - -**Note:** getUpdates (polling) and webhook are mutually exclusive per Zalo API docs. + - Webhook secret must be 8-256 characters. + - Zalo sends events with an `X-Bot-Api-Secret-Token` header, checked with a constant-time comparison. + - Gateway HTTP handles webhook requests at `channels.zalo.webhookPath` (defaults to the webhook URL's path). + - Requests must use `Content-Type: application/json` (or a `+json` media type). + - getUpdates polling and webhook are mutually exclusive per Zalo API docs. ## Supported message types -For a quick support snapshot, see [Capabilities](#capabilities). The notes below add detail where the behavior needs extra context. - -- **Text messages**: Full support with 2000 character chunking. -- **Plain URLs in text**: Behave like normal text input. -- **Link previews / rich link cards**: See the Marketplace-bot status in [Capabilities](#capabilities); they did not reliably trigger a reply. -- **Image messages**: See the Marketplace-bot status in [Capabilities](#capabilities); inbound image handling was unreliable (typing indicator without a final reply). -- **Stickers**: See the Marketplace-bot status in [Capabilities](#capabilities). -- **Voice notes / audio files / video / generic file attachments**: See the Marketplace-bot status in [Capabilities](#capabilities). -- **Unsupported types**: Logged (for example, messages from protected users). +- Text: full support, chunked to 2000 characters. +- Media: inbound/outbound, capped by `mediaMaxMb`. +- Reactions, threads, polls, native commands: not supported by the plugin. +- Streaming: the plugin declares block-streaming capability, but Zalo has no dedicated outbound queue/merge-text tuning knobs (unlike some other regional channels); verify current behavior in your environment if this matters for your use case. ## Capabilities -This table summarizes current **Zalo Bot Creator / Marketplace bot** behavior in OpenClaw. - -| Feature | Status | -| --------------------------- | --------------------------------------- | -| Direct messages | ✅ Supported | -| Groups | ❌ Not available for Marketplace bots | -| Media (inbound images) | ⚠️ Limited / verify in your environment | -| Media (outbound images) | ⚠️ Not re-tested for Marketplace bots | -| Plain URLs in text | ✅ Supported | -| Link previews | ⚠️ Unreliable for Marketplace bots | -| Reactions | ❌ Not supported | -| Stickers | ⚠️ No agent reply for Marketplace bots | -| Voice notes / audio / video | ⚠️ No agent reply for Marketplace bots | -| File attachments | ⚠️ No agent reply for Marketplace bots | -| Threads | ❌ Not supported | -| Polls | ❌ Not supported | -| Native commands | ❌ Not supported | -| Streaming | ⚠️ Blocked (2000 char limit) | +| Feature | Status | +| ------------------------ | --------------------------------- | +| Direct messages | Supported | +| Groups | Supported (mention-gated) | +| Media (inbound/outbound) | Supported, capped by `mediaMaxMb` | +| Reactions | Not supported | +| Threads | Not supported | +| Polls | Not supported | +| Native commands | Not supported | +| Reply-to / quote | Not used (fixed off) | ## Delivery targets (CLI/cron) -- Use a chat id as the target. -- Example: `openclaw message send --channel zalo --target 123456789 --message "hi"`. +Use a chat ID as the target: + +```bash +openclaw message send --channel zalo --target 123456789 --message "hi" +``` ## Troubleshooting -**Bot doesn't respond:** +**Bot does not respond:** -- Check that the token is valid: `openclaw channels status --probe` -- Verify the sender is approved (pairing or allowFrom) +- Check the token: `openclaw channels status --probe` +- Verify the sender is approved (pairing or `allowFrom`) - Check gateway logs: `openclaw logs --follow` **Webhook not receiving events:** -- Ensure webhook URL uses HTTPS -- Verify secret token is 8-256 characters +- Confirm the webhook URL uses HTTPS +- Confirm the secret is 8-256 characters - Confirm the gateway HTTP endpoint is reachable on the configured path -- Check that getUpdates polling is not running (they're mutually exclusive) +- Confirm getUpdates polling is not also running (they are mutually exclusive) +- A burst of requests can return HTTP 429 (120 requests / 60s per path+IP); back off and retry -## Configuration reference (Zalo) +## Configuration reference Full configuration: [Configuration](/gateway/configuration) -The flat top-level keys (`channels.zalo.botToken`, `channels.zalo.dmPolicy`, and similar) are a legacy single-account shorthand. Prefer `channels.zalo.accounts..*` for new configs. Both forms are still documented here because they exist in the schema. +| Setting | Description | Default | +| -------------------------------------------- | ------------------------------------------------- | --------------------- | +| `channels.zalo.enabled` | Enable/disable channel startup | `true` | +| `channels.zalo.accounts..botToken` | Bot token from Zalo Bot Platform | - | +| `channels.zalo.accounts..tokenFile` | Read token from a file (symlinks rejected) | - | +| `channels.zalo.accounts..name` | Display name | - | +| `channels.zalo.accounts..enabled` | Enable/disable this account | `true` | +| `channels.zalo.accounts..dmPolicy` | Per-account DM policy | `pairing` | +| `channels.zalo.accounts..allowFrom` | DM allowlist (user IDs) | - | +| `channels.zalo.accounts..groupPolicy` | Per-account group policy | see [Groups](#groups) | +| `channels.zalo.accounts..groupAllowFrom` | Group sender allowlist; falls back to `allowFrom` | - | +| `channels.zalo.accounts..mediaMaxMb` | Inbound/outbound media cap (MB) | `5` | +| `channels.zalo.accounts..webhookUrl` | Enable webhook mode (HTTPS required) | - | +| `channels.zalo.accounts..webhookSecret` | Webhook secret (8-256 chars) | - | +| `channels.zalo.accounts..webhookPath` | Webhook path on the gateway HTTP server | webhook URL path | +| `channels.zalo.accounts..proxy` | Proxy URL for API requests | - | +| `channels.zalo.accounts..responsePrefix` | Outbound response prefix override | - | +| `channels.zalo.defaultAccount` | Default account when multiple are configured | `default` | -Provider options: +`channels.zalo.botToken`, `channels.zalo.dmPolicy`, and other flat top-level keys are the legacy single-account shorthand for the fields above; both forms are supported. -- `channels.zalo.enabled`: enable/disable channel startup. -- `channels.zalo.botToken`: bot token from Zalo Bot Platform. -- `channels.zalo.tokenFile`: read token from a regular file path. Symlinks are rejected. -- `channels.zalo.dmPolicy`: `pairing | allowlist | open | disabled` (default: pairing). -- `channels.zalo.allowFrom`: DM allowlist (user IDs). `open` requires `"*"`. The wizard will ask for numeric IDs. -- `channels.zalo.groupPolicy`: `open | allowlist | disabled` (default: allowlist). Present in config; see [Capabilities](#capabilities) and [Access control (Groups)](#access-control-groups) for current Marketplace-bot behavior. -- `channels.zalo.groupAllowFrom`: group sender allowlist (user IDs). Falls back to `allowFrom` when unset. -- `channels.zalo.mediaMaxMb`: inbound/outbound media cap (MB, default 5). -- `channels.zalo.webhookUrl`: enable webhook mode (HTTPS required). -- `channels.zalo.webhookSecret`: webhook secret (8-256 chars). -- `channels.zalo.webhookPath`: webhook path on the gateway HTTP server. -- `channels.zalo.proxy`: proxy URL for API requests. - -Multi-account options: - -- `channels.zalo.accounts..botToken`: per-account token. -- `channels.zalo.accounts..tokenFile`: per-account regular token file. Symlinks are rejected. -- `channels.zalo.accounts..name`: display name. -- `channels.zalo.accounts..enabled`: enable/disable account. -- `channels.zalo.accounts..dmPolicy`: per-account DM policy. -- `channels.zalo.accounts..allowFrom`: per-account allowlist. -- `channels.zalo.accounts..groupPolicy`: per-account group policy. Present in config; see [Capabilities](#capabilities) and [Access control (Groups)](#access-control-groups) for current Marketplace-bot behavior. -- `channels.zalo.accounts..groupAllowFrom`: per-account group sender allowlist. -- `channels.zalo.accounts..webhookUrl`: per-account webhook URL. -- `channels.zalo.accounts..webhookSecret`: per-account webhook secret. -- `channels.zalo.accounts..webhookPath`: per-account webhook path. -- `channels.zalo.accounts..proxy`: per-account proxy URL. +Env option: `ZALO_BOT_TOKEN=...` resolves the default account's token only. ## Related -- [Channels Overview](/channels) — all supported channels -- [Pairing](/channels/pairing) — DM authentication and pairing flow -- [Groups](/channels/groups) — group chat behavior and mention gating -- [Channel Routing](/channels/channel-routing) — session routing for messages -- [Security](/gateway/security) — access model and hardening +- [Channels Overview](/channels) - all supported channels +- [Pairing](/channels/pairing) - DM authentication and pairing flow +- [Groups](/channels/groups) - group chat behavior and mention gating +- [Channel Routing](/channels/channel-routing) - session routing for messages +- [Security](/gateway/security) - access model and hardening diff --git a/docs/channels/zaloclawbot.md b/docs/channels/zaloclawbot.md index ff5769f690e2..d5cdc78bb963 100644 --- a/docs/channels/zaloclawbot.md +++ b/docs/channels/zaloclawbot.md @@ -6,35 +6,31 @@ read_when: title: "Zalo ClawBot" --- -OpenClaw connects to Zalo ClawBot through the catalog-listed external -`@zalo-platforms/openclaw-zaloclawbot` plugin. Login uses a Zalo Mini App QR -code. +OpenClaw connects to Zalo ClawBot through the catalog-listed external `@zalo-platforms/openclaw-zaloclawbot` plugin. Login uses a Zalo Mini App QR code; the plugin id in config is `openclaw-zaloclawbot`. ## Compatibility | Plugin Version | OpenClaw Version | npm dist-tag | Status | | -------------- | ---------------- | ------------ | ------------- | -| 0.1.x | >=2026.4.10 | `latest` | Active / Beta | +| 0.1.4 | >=2026.4.10 | `latest` | Active / Beta | ## Prerequisites -- Node.js **>= 22** -- [OpenClaw](https://docs.openclaw.ai/install) must be installed (`openclaw` CLI available). -- A Zalo account on a mobile device to scan the login QR code. +- Node.js >= 22 +- [OpenClaw](https://docs.openclaw.ai/install) installed (`openclaw` CLI available) +- A Zalo account on a mobile device to scan the login QR code ## Install with onboard (recommended) -Run the OpenClaw onboarding wizard and pick **Zalo ClawBot** from the channel menu: - ```bash openclaw onboard ``` -The wizard installs the plugin from the official catalog (integrity-verified), renders the login QR right in the terminal, and finishes the channel once you scan it with the Zalo app. No extra commands are needed. +Pick **Zalo ClawBot** from the channel menu. The wizard installs the plugin from the official catalog (integrity-verified), renders the login QR in the terminal, and finishes the channel once you scan it with the Zalo app. -## Manual Installation +## Manual installation -To add the channel to an already-onboarded gateway, follow these steps: +To add the channel to an already-onboarded gateway: ### 1. Install the plugin @@ -42,7 +38,7 @@ To add the channel to an already-onboarded gateway, follow these steps: openclaw plugins install "@zalo-platforms/openclaw-zaloclawbot@0.1.4" ``` -Use the exact pinned version shown above (it matches the official catalog entry), so OpenClaw verifies the package against the catalog integrity hash during install. +Use the exact pinned version so OpenClaw verifies the package against the catalog integrity hash during install. ### 2. Enable the plugin in config @@ -50,13 +46,13 @@ Use the exact pinned version shown above (it matches the official catalog entry) openclaw config set plugins.entries.openclaw-zaloclawbot.enabled true ``` -### 3. Generate QR code and log in +### 3. Generate a QR code and log in ```bash openclaw channels login --channel openclaw-zaloclawbot ``` -Scan the terminal-rendered QR code using the Zalo mobile app, accept the Terms of Use inside the Zalo Mini App, and authorize the session. +Scan the terminal-rendered QR code with the Zalo mobile app, accept the Terms of Use inside the Zalo Mini App, and authorize the session. ### 4. Restart the gateway @@ -64,32 +60,30 @@ Scan the terminal-rendered QR code using the Zalo mobile app, accept the Terms o openclaw gateway restart ``` ---- +## How it works -## How It Works +Unlike the standard Zalo channel, which requires registering your own Zalo Official Account (OA) and configuring static developer credentials, Zalo ClawBot is an **owner-bound personal assistant** on shared official infrastructure: -Unlike the standard developer Zalo channel which requires you to register your own Zalo Official Account (OA) and paste static developer credentials, Zalo ClawBot operates as an **owner-bound personal assistant** using a shared, official infrastructure: +1. **Onboarding:** the QR code resolves to a Zalo Mini App that binds a newly provisioned, private bot under a shared official OA directly to your Zalo user ID. +2. **Owner-bound privacy:** the bot only communicates with its owner. Messages from other users are dropped at the platform level. +3. **Official API path:** the plugin uses Zalo Bot Platform APIs, not browser or web-session automation. -1. **Secure Onboarding:** The QR code resolves to a secure Zalo Mini App that binds a newly-provisioned, private bot under a shared official OA directly to your Zalo User ID. -2. **Owner-Bound Privacy:** By design, the bot is restricted to communicating _only_ with its owner. Messages from other users are dropped at the platform level, making the connection private and secure. -3. **Official API path:** The plugin uses Zalo Bot Platform APIs instead of - browser or web-session automation. +## Under the hood -## Under the Hood +The plugin communicates with Zalo via a persistent long-polling loop (`getUpdates`). Webhooks are disabled by default for local desktop/terminal gateway runs. Messages are processed client-side and mapped to your local agent runtime. -The Zalo ClawBot plugin communicates with Zalo APIs via a persistent long-polling message loop. To maintain a clean and lightweight runtime: +The plugin manages bot credentials under the OpenClaw state directory. Treat that directory as sensitive and cover it under the same access-control and backup policy as the rest of OpenClaw state. -- Long-poll connections utilize the `getUpdates` endpoint. -- Webhooks are disabled by default for local desktop/terminal gateway runs. -- Messages are processed client-side and mapped directly to your local agent runtime. - -The external plugin manages bot credentials under the OpenClaw state directory. -Treat that directory as sensitive and include it in the same access-control and -backup policy as the rest of your OpenClaw state. - ---- +This plugin's runtime lives entirely in the external `@zalo-platforms/openclaw-zaloclawbot` package; behavior details below beyond install/config are as reported by the plugin's maintainers and are not verified against OpenClaw core source. ## Troubleshooting -- **QR Login Timeout:** The login token (`zbsk`) expires after 5 minutes for security reasons. If the QR code expires before you scan it, simply rerun the login command to generate a new one. -- **Gateway Fails to Load:** Ensure your OpenClaw host version is `2026.4.10` or higher. Older versions do not support the external npm-plugin installation ledger. +- **QR login timeout:** the login token (`zbsk`) expires after 5 minutes for security. If the QR code expires before you scan it, rerun the login command to generate a new one. +- **Gateway fails to load:** confirm your OpenClaw host version is `2026.4.10` or higher. Older versions do not support the external npm-plugin installation ledger this ID requires. + +## Related + +- [Channels Overview](/channels) - all supported channels +- [Zalo](/channels/zalo) - the bundled Zalo Bot Creator / Marketplace channel +- [Pairing](/channels/pairing) - DM authentication and pairing flow +- [Plugins](/tools/plugin) - installing and managing plugins diff --git a/docs/channels/zalouser.md b/docs/channels/zalouser.md index 25ac94c2eac5..b24ab0f4b1d3 100644 --- a/docs/channels/zalouser.md +++ b/docs/channels/zalouser.md @@ -6,32 +6,27 @@ read_when: title: "Zalo personal" --- -Status: experimental. This integration automates a **personal Zalo account** via native `zca-js` inside OpenClaw. +Status: experimental. This integration automates a **personal Zalo account** via native `zca-js`, in-process, with no external CLI binary. This is an unofficial integration and may result in account suspension or ban. Use at your own risk. -## Bundled plugin +## Install -Zalo Personal ships as a bundled plugin in current OpenClaw releases, so normal -packaged builds do not need a separate install. +Zalo Personal is an official external plugin, not bundled in core. Install it before use: -If you are on an older build or a custom install that excludes Zalo Personal, -install the npm package directly: +```bash +openclaw plugins install @openclaw/zalouser +``` -- Install via CLI: `openclaw plugins install @openclaw/zalouser` -- Pinned version: `openclaw plugins install @openclaw/zalouser@2026.5.2` -- Or from a source checkout: `openclaw plugins install ./path/to/local/zalouser-plugin` +- Pin a version: `openclaw plugins install @openclaw/zalouser@` +- From a source checkout: `openclaw plugins install ./path/to/local/zalouser-plugin` - Details: [Plugins](/tools/plugin) -No external `zca`/`openzca` CLI binary is required. +## Quick setup -## Quick setup (beginner) - -1. Ensure the Zalo Personal plugin is available. - - Current packaged OpenClaw releases already bundle it. - - Older/custom installs can add it manually with the commands above. +1. Install the plugin (above). 2. Login (QR, on the Gateway machine): - `openclaw channels login --channel zalouser` - Scan the QR code with the Zalo mobile app. @@ -53,19 +48,17 @@ No external `zca`/`openzca` CLI binary is required. ## What it is -- Runs entirely in-process via `zca-js`. -- Uses native event listeners to receive inbound messages. +- Runs entirely in-process via the `zca-js` library (no external `zca`/`openzca` binary). +- Uses native event listeners (`message`, `error`) to receive inbound messages. - Sends replies directly through the JS API (text/media/link). -- Designed for "personal account" use cases where Zalo Bot API is not available. +- Designed for "personal account" use cases where the Zalo Bot API is not available. ## Naming -Channel id is `zalouser` to make it explicit this automates a **personal Zalo user account** (unofficial). We keep `zalo` reserved for a potential future official Zalo API integration. +Channel id is `zalouser` to make it explicit this automates a **personal Zalo user account** (unofficial). `zalo` is reserved for a potential future official Zalo API integration. ## Finding IDs (directory) -Use the directory CLI to discover peers/groups and their IDs: - ```bash openclaw directory self --channel zalouser openclaw directory peers list --channel zalouser --query "name" @@ -74,12 +67,12 @@ openclaw directory groups list --channel zalouser --query "work" ## Limits -- Outbound text is chunked to ~2000 characters (Zalo client limits). -- Streaming is blocked by default. +- Outbound text is chunked to 2000 characters (Zalo client limit). +- Streaming is not supported. ## Access control (DMs) -`channels.zalouser.dmPolicy` supports: `pairing | allowlist | open | disabled` (default: `pairing`). +`channels.zalouser.dmPolicy`: `pairing | allowlist | open | disabled` (default: `pairing`). `channels.zalouser.allowFrom` should use stable Zalo user IDs. It can also reference static sender access groups (`accessGroup:`). During interactive setup, entered names can be resolved to IDs using the plugin's in-process contact lookup. @@ -92,18 +85,16 @@ Approve via: ## Group access (optional) -- Default: `channels.zalouser.groupPolicy = "open"` (groups allowed). Use `channels.defaults.groupPolicy` to override the default when unset. -- Restrict to an allowlist with: - - `channels.zalouser.groupPolicy = "allowlist"` - - `channels.zalouser.groups` (keys should be stable group IDs; names are resolved to IDs on startup only when `channels.zalouser.dangerouslyAllowNameMatching: true` is enabled) - - `channels.zalouser.groupAllowFrom` (controls which senders in allowed groups can trigger the bot; static sender access groups can be referenced with `accessGroup:`) +- Default: `channels.zalouser.groupPolicy = "allowlist"` (groups require an explicit allowlist entry). +- Open all groups: `channels.zalouser.groupPolicy = "open"`. - Block all groups: `channels.zalouser.groupPolicy = "disabled"`. +- With `groupPolicy = "allowlist"`: + - `channels.zalouser.groups` keys should be stable group IDs; names resolve to IDs on startup only when `channels.zalouser.dangerouslyAllowNameMatching: true` is enabled. + - `channels.zalouser.groupAllowFrom` controls which senders in allowed groups can trigger the bot; static sender access groups can be referenced with `accessGroup:`. - The configure wizard can prompt for group allowlists. -- On startup, OpenClaw resolves group/user names in allowlists to IDs and logs the mapping only when `channels.zalouser.dangerouslyAllowNameMatching: true` is enabled. - Group allowlist matching is ID-only by default. Unresolved names are ignored for auth unless `channels.zalouser.dangerouslyAllowNameMatching: true` is enabled. - `channels.zalouser.dangerouslyAllowNameMatching: true` is a break-glass compatibility mode that re-enables mutable startup name resolution and runtime group-name matching. -- If `groupAllowFrom` is unset, runtime falls back to `allowFrom` for group sender checks. -- Sender checks apply to both normal group messages and control commands (for example `/new`, `/reset`). +- `groupAllowFrom` does **not** fall back to `allowFrom` for normal group messages: leaving it empty on an allowlisted group opens that group to any sender. Authorized control commands (for example `/new`) are the exception; command sender checks fall back to `allowFrom` when `groupAllowFrom` is empty. Example: @@ -114,23 +105,27 @@ Example: groupPolicy: "allowlist", groupAllowFrom: ["1471383327500481391"], groups: { - "123456789": { allow: true }, - "Work Chat": { allow: true }, + "123456789": { enabled: true }, + "Work Chat": { enabled: true }, }, }, }, } ``` + +`channels.zalouser.groups..allow` is a legacy field name; current config uses `enabled`. `openclaw doctor --fix` migrates `allow` to `enabled` automatically. + + ### Group mention gating - `channels.zalouser.groups..requireMention` controls whether group replies require a mention. -- Resolution order: exact group id/name -> normalized group slug -> `*` -> default (`true`). -- This applies both to allowlisted groups and open group mode. +- Resolution order: group id -> `group:` alias -> group name/slug (name-based candidates only apply when `dangerouslyAllowNameMatching: true`) -> `*` -> default (`true`). +- Applies both to allowlisted groups and open group mode. - Quoting a bot message counts as an implicit mention for group activation. - Authorized control commands (for example `/new`) can bypass mention gating. -- When a group message is skipped because mention is required, OpenClaw stores it as pending group history and includes it on the next processed group message. -- Group history limit defaults to `messages.groupChat.historyLimit` (fallback `50`). You can override per account with `channels.zalouser.historyLimit`. +- When a group message is skipped because a mention is required, OpenClaw stores it as pending group history and includes it on the next processed group message. +- Group history limit: `channels.zalouser.historyLimit`, then `messages.groupChat.historyLimit`, then a fallback of `50`. Example: @@ -140,8 +135,8 @@ Example: zalouser: { groupPolicy: "allowlist", groups: { - "*": { allow: true, requireMention: true }, - "Work Chat": { allow: true, requireMention: false }, + "*": { enabled: true, requireMention: true }, + "Work Chat": { enabled: true, requireMention: false }, }, }, }, @@ -168,21 +163,21 @@ Accounts map to `zalouser` profiles in OpenClaw state. Example: ## Environment variables -The Zalo Personal plugin can also read profile selection from environment variables: +Profile selection can also come from environment variables: -- `ZALOUSER_PROFILE`: profile name to use when no `profile` is set in channel or account config. -- `ZCA_PROFILE`: legacy fallback profile name, used only when `ZALOUSER_PROFILE` is not set. +| Var | Purpose | +| ------------------ | -------------------------------------------------------------------------- | +| `ZALOUSER_PROFILE` | Profile name to use when no `profile` is set in channel or account config. | +| `ZCA_PROFILE` | Legacy fallback, used only when `ZALOUSER_PROFILE` is not set. | -Profile names select the saved Zalo login credentials in OpenClaw state. Resolution order is: +Profile names select the saved Zalo login credentials in OpenClaw state. Resolution order: 1. Explicit `profile` in config. 2. `ZALOUSER_PROFILE`. 3. `ZCA_PROFILE`. 4. The account id for non-default accounts, or `default` for the default account. -For multi-account setups, prefer setting `profile` on each account in config so -one environment variable does not make multiple accounts share the same login -session. +For multi-account setups, prefer setting `profile` on each account in config so one environment variable does not make multiple accounts share the same login session. ## Typing, reactions, and delivery acknowledgements @@ -203,15 +198,14 @@ session. - Use numeric IDs in `allowFrom`/`groupAllowFrom` and stable group IDs in `groups`. If you intentionally need exact friend/group names, enable `channels.zalouser.dangerouslyAllowNameMatching: true`. -**Upgraded from old CLI-based setup:** +**Upgraded from an old external `zca`/CLI-based setup:** -- Remove any old external `zca` process assumptions. -- The channel now runs fully in OpenClaw without external CLI binaries. +- Remove any external `zca` process assumptions; the channel now runs fully in-process via `zca-js`, with no external CLI binary. ## Related -- [Channels Overview](/channels) — all supported channels -- [Pairing](/channels/pairing) — DM authentication and pairing flow -- [Groups](/channels/groups) — group chat behavior and mention gating -- [Channel Routing](/channels/channel-routing) — session routing for messages -- [Security](/gateway/security) — access model and hardening +- [Channels Overview](/channels) - all supported channels +- [Pairing](/channels/pairing) - DM authentication and pairing flow +- [Groups](/channels/groups) - group chat behavior and mention gating +- [Channel Routing](/channels/channel-routing) - session routing for messages +- [Security](/gateway/security) - access model and hardening diff --git a/docs/ci.md b/docs/ci.md index 0b0abde8721f..a8710dccd18a 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -8,44 +8,47 @@ read_when: - You are changing ClawSweeper dispatch or GitHub activity forwarding --- -OpenClaw CI runs on every push to `main` and every pull request. Canonical -`main` pushes first pass through a 90-second hosted-runner admission window. -The existing `CI` concurrency group cancels that waiting run when a newer -commit lands, so sequential merges do not each register a full Blacksmith -matrix. Pull requests and manual dispatches skip the wait. The `preflight` job -then classifies the diff and turns expensive lanes off when only unrelated -areas changed. Manual `workflow_dispatch` runs intentionally bypass smart -scoping and fan out the full graph for release candidates and broad -validation. Android lanes stay opt-in through `include_android`. Release-only -plugin coverage lives in the separate [`Plugin Prerelease`](#plugin-prerelease) -workflow and only runs from [`Full Release Validation`](#full-release-validation) -or an explicit manual dispatch. +OpenClaw CI runs on pushes to `main` (Markdown and `docs/**` paths are ignored +at the trigger), on non-draft pull requests (CHANGELOG-only diffs are ignored), +and on manual dispatch. Canonical `main` pushes first pass through a 90-second +hosted-runner admission window; the `CI` concurrency group cancels that waiting +run when a newer commit lands, so sequential merges do not each register a full +Blacksmith matrix. Pull requests and manual dispatches skip the wait. The +`preflight` job then classifies the diff and turns expensive lanes off when +only unrelated areas changed. Manual `workflow_dispatch` runs intentionally +bypass smart scoping and fan out the full graph for release candidates and +broad validation. Android lanes stay opt-in through `include_android` (or the +`release_gate` input). Release-only plugin coverage lives in the separate +[`Plugin Prerelease`](#plugin-prerelease) workflow and only runs from +[`Full Release Validation`](#full-release-validation) or an explicit manual +dispatch. ## Pipeline overview -| Job | Purpose | When it runs | -| ---------------------------------- | --------------------------------------------------------------------------------------------------------- | --------------------------------------------------- | -| `preflight` | Detect docs-only changes, changed scopes, changed extensions, and build the CI manifest | Always on non-draft pushes and PRs | -| `runner-admission` | Hosted 90-second debounce for canonical `main` pushes before Blacksmith work is registered | Every CI run; sleep only on canonical `main` pushes | -| `security-fast` | Private key detection, changed-workflow audit via `zizmor`, and production lockfile audit | Always on non-draft pushes and PRs | -| `check-dependencies` | Production Knip dependency-only pass plus the unused-file allowlist guard | Node-relevant changes | -| `build-artifacts` | Build `dist/`, Control UI, built-CLI smoke checks, embedded built-artifact checks, and reusable artifacts | Node-relevant changes | -| `checks-fast-core` | Fast Linux correctness lanes such as bundled, protocol, QA Smoke CI, and CI-routing checks | Node-relevant changes | -| `checks-fast-contracts-plugins-*` | Two sharded plugin contract checks | Node-relevant changes | -| `checks-fast-contracts-channels-*` | Two sharded channel contract checks | Node-relevant changes | -| `checks-node-core-*` | Core Node test shards, excluding channel, bundled, contract, and extension lanes | Node-relevant changes | -| `check-*` | Sharded main local gate equivalent: prod types, lint, guards, test types, and strict smoke | Node-relevant changes | -| `check-additional-*` | Architecture, sharded boundary/prompt drift, extension guards, package boundary, and runtime topology | Node-relevant changes | -| `checks-node-compat-node22` | Node 22 compatibility build and smoke lane | Manual CI dispatch for releases | -| `check-docs` | Docs formatting, lint, and broken-link checks | Docs changed | -| `skills-python` | Ruff + pytest for Python-backed skills | Python-skill-relevant changes | -| `checks-windows` | Windows-specific process/path tests plus shared runtime import specifier regressions | Windows-relevant changes | -| `macos-node` | macOS TypeScript test lane using the shared built artifacts | macOS-relevant changes | -| `macos-swift` | Swift lint, build, and tests for the macOS app | macOS-relevant changes | -| `ios-build` | Xcode project generation plus the iOS app simulator build | iOS app, shared app kit, or Swabble changes | -| `android` | Android unit tests for both flavors plus one debug APK build | Android-relevant changes | -| `test-performance-agent` | Daily Codex slow-test optimization after trusted activity | Main CI success or manual dispatch | -| `openclaw-performance` | Daily/on-demand Kova runtime performance reports with mock-provider, deep-profile, and GPT 5.5 live lanes | Scheduled and manual dispatch | +| Job | Purpose | When it runs | +| ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- | +| `preflight` | Detect docs-only changes, changed scopes, changed extensions, and build the CI manifest | Always on non-draft pushes and PRs | +| `runner-admission` | Hosted 90-second debounce for canonical `main` pushes before Blacksmith work is registered | Every CI run; sleep only on canonical `main` pushes | +| `security-fast` | Private key detection, changed-workflow audit via `zizmor`, and production lockfile audit | Always on non-draft pushes and PRs | +| `pnpm-store-warmup` | Warm the lockfile-pinned pnpm store cache without blocking Linux Node shards | Node or docs-check lanes selected | +| `build-artifacts` | Build `dist/`, Control UI, built-CLI smoke checks, startup memory, and embedded built-artifact checks | Node-relevant changes | +| `checks-fast-core` | Fast Linux correctness lanes: bundled + protocol, QA Smoke CI, Bun launcher, and the CI-routing fast task | Node-relevant changes | +| `checks-fast-contracts-plugins-*` | Two weighted plugin contract shards | Node-relevant changes | +| `checks-fast-contracts-channels-*` | Two weighted channel contract shards | Node-relevant changes | +| `checks-node-*` | Core Node test shards, excluding channel, bundled, contract, and extension lanes | Node-relevant changes | +| `check-*` | Sharded main local gate equivalent: guards, shrinkwrap, bundled-channel config metadata, prod types, lint, dependencies, test types | Node-relevant changes | +| `check-additional-*` | Boundary check stripes (including prompt snapshot drift), session accessor/transcript reader boundaries, extension lint groups, package boundary compile/canary, and runtime topology architecture | Node-relevant changes | +| `checks-node-compat-node22` | Node 22 compatibility build and smoke lane | Manual CI dispatch for releases | +| `check-docs` | Docs formatting, lint, and broken-link checks | Docs changed (PRs and manual dispatch) | +| `native-i18n` | Native app, Android, and Apple i18n inventory checks | Native i18n-relevant changes | +| `skills-python` | Ruff + pytest for Python-backed skills | Python-skill-relevant changes | +| `checks-windows` | Windows-specific process/path tests plus shared runtime import specifier regressions | Windows-relevant changes | +| `macos-node` | Focused macOS TypeScript tests: launchd, Homebrew, runtime paths, packaging scripts, process-group wrapper | macOS-relevant changes | +| `macos-swift` | Swift lint, build, and tests for the macOS app | macOS-relevant changes | +| `ios-build` | Xcode project generation plus the iOS app simulator build | iOS app, shared app kit, or Swabble changes | +| `android` | Android unit tests for both flavors plus one debug APK build | Android-relevant changes | +| `test-performance-agent` | Separate workflow: daily Codex slow-test optimization after trusted activity | Main CI success or manual dispatch | +| `openclaw-performance` | Separate workflow: daily/on-demand Kova runtime performance reports with mock-provider, deep-profile, and GPT 5.5 live lanes | Scheduled and manual dispatch | ## Fail-fast order @@ -53,20 +56,18 @@ or an explicit manual dispatch. 2. `preflight` decides which lanes exist at all. The `docs-scope` and `changed-scope` logic are steps inside this job, not standalone jobs. 3. `security-fast`, `check-*`, `check-additional-*`, `check-docs`, and `skills-python` fail quickly without waiting on the heavier artifact and platform matrix jobs. 4. `build-artifacts` overlaps with the fast Linux lanes so downstream consumers can start as soon as the shared build is ready. -5. Heavier platform and runtime lanes fan out after that: `checks-fast-core`, `checks-fast-contracts-plugins-*`, `checks-fast-contracts-channels-*`, `checks-node-core-*`, `checks-windows`, `macos-node`, `macos-swift`, `ios-build`, and `android`. +5. Heavier platform and runtime lanes fan out after that: `checks-fast-core`, `checks-fast-contracts-plugins-*`, `checks-fast-contracts-channels-*`, `checks-node-*`, `checks-windows`, `macos-node`, `macos-swift`, `ios-build`, and `android`. GitHub may mark superseded jobs as `cancelled` when a newer push lands on the same PR or `main` ref. Treat that as CI noise unless the newest run for the same ref is also failing. Matrix jobs use `fail-fast: false`, and `build-artifacts` reports embedded channel, core-support-boundary, and gateway-watch failures directly instead of queuing tiny verifier jobs. The automatic CI concurrency key is versioned (`CI-v7-*`) so a GitHub-side zombie in an old queue group cannot indefinitely block newer main runs. Manual full-suite runs use `CI-manual-v1-*` and do not cancel in-progress runs. -Use `pnpm ci:timings`, `pnpm ci:timings:recent`, or `node scripts/ci-run-timings.mjs ` to summarize wall time, queue time, slowest jobs, failures, and the `pnpm-store-warmup` fanout barrier from GitHub Actions. CI also uploads the same run summary as a `ci-timings-summary` artifact. For build timing, check the `build-artifacts` job's `Build dist` step: `pnpm build:ci-artifacts` prints `[build-all] phase timings:` and includes `ui:build`; the job also uploads the `startup-memory` artifact. - -For pull request runs, the terminal timing-summary job runs the helper from the trusted base revision before passing `GH_TOKEN` to `gh run view`. That keeps the tokened query out of branch-controlled code while still summarizing the pull request's current CI run. +Use `pnpm ci:timings`, `pnpm ci:timings:recent`, or `node scripts/ci-run-timings.mjs ` to summarize wall time, queue time, slowest jobs, failures, and the `pnpm-store-warmup` fanout barrier from GitHub Actions. The in-workflow `ci-timings-summary` job exists in `ci.yml` but is currently disabled (`if: false`); run the timing helper locally instead. For build timing, check the `build-artifacts` job's `Build dist` step: `pnpm build:ci-artifacts` prints `[build-all] phase timings:` and includes `ui:build`; the job also uploads the `startup-memory` artifact. ## PR context and evidence External contributor PRs run a PR context and evidence gate from -`.github/workflows/real-behavior-proof.yml`. The workflow checks out the trusted -base commit and evaluates the PR body only; it does not execute code from the -contributor branch. +`.github/workflows/real-behavior-proof.yml`. The workflow checks out the +trusted workflow revision (`github.workflow_sha`) and evaluates the PR body +only; it does not execute code from the contributor branch. The gate applies to PR authors who are not repository owners, members, collaborators, or bots. It passes when the PR body contains authored @@ -81,26 +82,34 @@ When the check fails, update the PR body instead of pushing another code commit. Scope logic lives in `scripts/ci-changed-scope.mjs` and is covered by unit tests in `src/scripts/ci-changed-scope.test.ts`. Manual dispatch skips changed-scope detection and makes the preflight manifest act as if every scoped area changed. -- **CI workflow edits** validate the Node CI graph plus workflow linting, but do not force Windows, iOS, Android, or macOS native builds by themselves; those platform lanes stay scoped to platform source changes. +- **CI workflow edits** validate the Node CI graph, workflow linting, and the Windows lane (`ci.yml` executes it), but do not force iOS, Android, or macOS native builds by themselves; those platform lanes stay scoped to platform source changes. - **Workflow Sanity** runs `actionlint`, `zizmor` over all workflow YAML files, the composite-action interpolation guard, and the conflict-marker guard. The PR-scoped `security-fast` job also runs `zizmor` over changed workflow files so workflow security findings fail early in the main CI graph. - **Docs on `main` pushes** are checked by the standalone `Docs` workflow with the same ClawHub docs mirror used by CI, so mixed code+docs pushes do not also queue the CI `check-docs` shard. Pull requests and manual CI still run `check-docs` from CI when docs changed. - **TUI PTY** runs in the `checks-node-core-runtime-tui-pty` Linux Node shard for TUI changes. The shard runs `test/vitest/vitest.tui-pty.config.ts` with `OPENCLAW_TUI_PTY_INCLUDE_LOCAL=1`, so it covers both the deterministic `TuiBackend` fixture lane and the slower `tui --local` smoke that mocks only the external model endpoint. -- **CI routing-only edits, selected cheap core-test fixture edits, and narrow plugin contract helper/test-routing edits** use a fast Node-only manifest path: `preflight`, security, and a single `checks-fast-core` task. That path skips build artifacts, Node 22 compatibility, channel contracts, full core shards, bundled-plugin shards, and additional guard matrices when the change is limited to the routing or helper surfaces the fast task exercises directly. +- **CI routing-only edits, the small set of core-test fixtures the fast task runs directly, and narrow plugin contract helper edits** use a fast Node-only manifest path: `preflight`, `security-fast`, and only the fast lanes the change touches — a single `checks-fast-core` CI-routing task, the two plugin contract shards, or both. That path skips build artifacts, Node 22 compatibility, channel contracts, full core shards, bundled-plugin shards, and additional guard matrices. - **Windows Node checks** are scoped to Windows-specific process/path wrappers, npm/pnpm/UI runner helpers, package manager config, and the CI workflow surfaces that execute that lane; unrelated source, plugin, install-smoke, and test-only changes stay on the Linux Node lanes. -The slowest Node test families are split or balanced so each job stays small without over-reserving runners: plugin contracts and channel contracts each run as two weighted Blacksmith-backed shards with the standard GitHub runner fallback, core unit fast/support lanes run separately, core runtime infra is split between state, process/config, shared, and three cron domain shards, auto-reply runs as balanced workers (with the reply subtree split into agent-runner, dispatch, and commands/state-routing shards), and agentic gateway/server configs are split across chat/auth/model/http-plugin/runtime/startup lanes instead of waiting on built artifacts. Normal CI then packs only isolated infra include-pattern shards into deterministic bundles of at most 64 test files, reducing the Node matrix without merging non-isolated command/cron, stateful agents-core, or gateway/server suites; heavy fixed suites stay on 8 vCPU while the bundled and lower-weight lanes use 4 vCPU. Pull requests on the canonical repository use an additional compact admission plan: the same per-config groups run in isolated subprocesses inside the current 34-job Linux Node plan, so a single PR does not register the full 70-plus-job Node matrix. `main` pushes, manual dispatches, and release gates retain the full matrix. Broad browser, QA, media, and miscellaneous plugin tests use their dedicated Vitest configs instead of the shared plugin catch-all. Include-pattern shards record timing entries using the CI shard name, so `.artifacts/vitest-shard-timings.json` can distinguish a whole config from a filtered shard. `check-additional-*` keeps package-boundary compile/canary work together and separates runtime topology architecture from gateway watch coverage; the boundary guard list is striped into one prompt-heavy shard and one combined shard for the remaining guard stripes, each running selected independent guards concurrently and printing per-check timings. The expensive Codex happy-path prompt snapshot drift check runs as its own additional job for manual CI and for prompt-affecting changes only, so normal unrelated Node changes do not wait behind cold prompt snapshot generation and the boundary shards stay balanced while prompt drift is still pinned to the PR that caused it; the same flag skips prompt snapshot Vitest generation inside the built-artifact core support-boundary shard. Gateway watch, channel tests, and the core support-boundary shard run concurrently inside `build-artifacts` after `dist/` and `dist-runtime/` are already built. +The slowest Node test families are split or balanced so each job stays small without over-reserving runners: + +- Plugin contracts and channel contracts each run as two weighted Blacksmith-backed shards with the standard GitHub runner fallback. +- Core unit fast/support lanes run separately; core runtime infra splits into process, shared, hooks, secrets, and three cron domain shards. +- Auto-reply runs as balanced workers, with the reply subtree split into agent-runner, commands, dispatch, session, and state-routing shards. +- Agentic gateway/server (control-plane) configs split across chat, auth, model, HTTP/plugin, runtime, and startup lanes instead of waiting on built artifacts. +- Normal CI packs only isolated infra include-pattern shards into deterministic bundles of at most 64 test files, reducing the Node matrix without merging non-isolated command/cron, stateful agents-core, or gateway/server suites. Heavy fixed suites stay on 8 vCPU while the bundled and lower-weight lanes use 4 vCPU. +- Pull requests on the canonical repository use a compact admission plan: the same per-config groups run in isolated subprocesses, currently 18 Node test jobs instead of the 74-job full matrix. `main` pushes, manual dispatches, and release gates retain the full matrix. +- Broad browser, QA, media, and miscellaneous plugin tests use their dedicated Vitest configs instead of the shared plugin catch-all. Include-pattern shards record timing entries using the CI shard name, so `.artifacts/vitest-shard-timings.json` can distinguish a whole config from a filtered shard. +- `check-additional-*` stripes the supplemental boundary guard list (`scripts/run-additional-boundary-checks.mjs`) into one prompt-heavy shard (`check-additional-boundaries-a`, which includes the Codex prompt snapshot drift check) and one combined shard for the remaining stripes (`check-additional-boundaries-bcd`), each running independent guards concurrently and printing per-check timings. Package-boundary compile/canary work stays together, and runtime topology architecture runs separately from the gateway watch coverage embedded in `build-artifacts`. +- Gateway watch, channel tests, and the core support-boundary shard run concurrently inside `build-artifacts` after `dist/` and `dist-runtime/` are already built. Once admitted, canonical Linux CI permits up to 24 concurrent Node test jobs and 12 for the smaller fast/check lanes; Windows and Android stay at two because -those runner pools are narrower. - -The compact PR plan emits 18 Node jobs for the current suite: whole-config -groups are batched in isolated subprocesses with a 120-minute batch timeout, -while include-pattern groups share the same bounded job budget. +those runner pools are narrower. Compact whole-config batches run with a +120-minute batch timeout, while include-pattern groups share the same bounded +job budget. Android CI runs both `testPlayDebugUnitTest` and `testThirdPartyDebugUnitTest` and then builds the Play debug APK. The third-party flavor has no separate source set or manifest; its unit-test lane still compiles the flavor with the SMS/call-log BuildConfig flags, while avoiding a duplicate debug APK packaging job on every Android-relevant push. -The `check-dependencies` shard runs `pnpm deadcode:dependencies` (a production Knip dependency-only pass pinned to the latest Knip version, with pnpm's minimum release age disabled for the `dlx` install) and `pnpm deadcode:unused-files`, which compares Knip's production unused-file findings against `scripts/deadcode-unused-files.allowlist.mjs`. The unused-file guard fails when a PR adds a new unreviewed unused file or leaves a stale allowlist entry, while preserving intentional dynamic plugin, generated, build, live-test, and package bridge surfaces that Knip cannot resolve statically. +The `check-dependencies` shard runs `pnpm deadcode:dependencies` (a production Knip dependency-only pass pinned to an exact Knip version, with pnpm's minimum release age disabled for the `dlx` install) and `pnpm deadcode:unused-files`, which compares Knip's production unused-file findings against `scripts/deadcode-unused-files.allowlist.mjs`, plus an advisory `pnpm deadcode:report:ci:ts-unused` report uploaded as the `deadcode-reports` artifact. The unused-file guard fails when a PR adds a new unreviewed unused file or leaves a stale allowlist entry, while preserving intentional dynamic plugin, generated, build, live-test, and package bridge surfaces that Knip cannot resolve statically. ## ClawSweeper activity forwarding @@ -121,9 +130,9 @@ Treat GitHub titles, comments, bodies, review text, branch names, and commit mes ## Manual dispatches -Manual CI dispatches run the same job graph as normal CI but force every non-Android scoped lane on: Linux Node shards, bundled-plugin shards, plugin and channel contract shards, Node 22 compatibility, `check-*`, `check-additional-*`, built-artifact smoke checks, docs checks, Python skills, Windows, macOS, iOS build, and Control UI i18n. Standalone manual CI dispatches run Android only with `include_android=true`; the full release umbrella enables Android by passing `include_android=true`. Plugin prerelease static checks, the release-only `agentic-plugins` shard, the full extension batch sweep, and plugin prerelease Docker lanes are excluded from CI. The Docker prerelease suite runs only when `Full Release Validation` dispatches the separate `Plugin Prerelease` workflow with the release-validation gate enabled. +Manual CI dispatches run the same job graph as normal CI but force every non-Android scoped lane on: Linux Node shards, bundled-plugin shards, plugin and channel contract shards, Node 22 compatibility, `check-*`, `check-additional-*`, built-artifact smoke checks, docs checks, Python skills, Windows, macOS, iOS build, and Control UI i18n. Standalone manual CI dispatches run Android only with `include_android=true` (the `release_gate` input also forces Android); the full release umbrella enables Android by passing `include_android=true`. Plugin prerelease static checks, the release-only `agentic-plugins` shard, the full extension batch sweep, and plugin prerelease Docker lanes are excluded from CI. The Docker prerelease suite runs only when `Full Release Validation` dispatches the separate `Plugin Prerelease` workflow with the release-validation gate enabled. -Manual runs use a unique concurrency group so a release-candidate full suite is not cancelled by another push or PR run on the same ref. The optional `target_ref` input lets a trusted caller run that graph against a branch, tag, or full commit SHA while using the workflow file from the selected dispatch ref. +Manual runs use a unique concurrency group so a release-candidate full suite is not cancelled by another push or PR run on the same ref. The optional `target_ref` input lets a trusted caller run that graph against a branch, tag, or full commit SHA while using the workflow file from the selected dispatch ref. The `release_gate` input is an exact-SHA maintainer fallback for capacity-stalled PR CI: it requires `target_ref` to be a full commit SHA that matches the dispatched branch head. ```bash gh workflow run ci.yml --ref release/YYYY.M.PATCH @@ -142,15 +151,15 @@ Release, private dist-tag, or other platform publication. ## Runners -| Runner | Jobs | -| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `ubuntu-24.04` | Manual CI dispatch and non-canonical repository fallbacks, CodeQL JavaScript/actions quality scans, workflow-sanity, labeler, auto-response, docs workflows outside CI, and install-smoke preflight so the Blacksmith matrix can queue earlier | -| `blacksmith-4vcpu-ubuntu-2404` | `preflight`, `security-fast`, lower-weight extension shards, `checks-fast-core` except QA Smoke CI, plugin/channel contract shards, most bundled/lower-weight Linux Node shards, `check-guards`, `check-prod-types`, `check-test-types`, selected `check-additional-*` shards, and `check-dependencies` | -| `blacksmith-8vcpu-ubuntu-2404` | Retained heavy Linux Node suites, boundary/extension-heavy `check-additional-*` shards, and `android` | -| `blacksmith-16vcpu-ubuntu-2404` | QA Smoke CI, `build-artifacts` in CI and Testbox, `check-lint` (CPU-sensitive enough that 8 vCPU cost more than they saved); install-smoke Docker builds (32-vCPU queue time cost more than it saved) | -| `blacksmith-8vcpu-windows-2025` | `checks-windows` | -| `blacksmith-6vcpu-macos-15` | `macos-node` on `openclaw/openclaw`; forks fall back to `macos-15` | -| `blacksmith-12vcpu-macos-26` | `macos-swift` and `ios-build` on `openclaw/openclaw`; forks fall back to `macos-26` | +| Runner | Jobs | +| ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `ubuntu-24.04` | Manual CI dispatch and non-canonical repository fallbacks, CodeQL security and quality scans, workflow-sanity, labeler, auto-response, the standalone Docs workflow, and the whole Install Smoke workflow | +| `blacksmith-4vcpu-ubuntu-2404` | `preflight`, `security-fast`, `pnpm-store-warmup`, `native-i18n`, `checks-fast-core` except QA Smoke CI, plugin/channel contract shards, most bundled/lower-weight Linux Node shards, `check-*` lanes except `check-lint`, selected `check-additional-*` shards, `check-docs`, and `skills-python` | +| `blacksmith-8vcpu-ubuntu-2404` | Retained heavy Linux Node suites, boundary/extension-heavy `check-additional-*` shards, and `android` | +| `blacksmith-16vcpu-ubuntu-2404` | QA Smoke CI, `build-artifacts` in CI and Testbox, and `check-lint` (CPU-sensitive enough that 8 vCPU cost more than they saved) | +| `blacksmith-8vcpu-windows-2025` | `checks-windows` | +| `blacksmith-6vcpu-macos-15` | `macos-node` on `openclaw/openclaw`; forks fall back to `macos-15` | +| `blacksmith-12vcpu-macos-26` | `macos-swift` and `ios-build` on `openclaw/openclaw`; forks fall back to `macos-26` | ## Runner registration budget @@ -218,16 +227,16 @@ Manual dispatch normally benchmarks the workflow ref. Set `target_ref` to benchm The workflow installs OCM from a pinned release and Kova from `openclaw/Kova` at the pinned `kova_ref` input, then runs three lanes: - `mock-provider`: Kova diagnostic scenarios against a local-build runtime with deterministic fake OpenAI-compatible auth. -- `mock-deep-profile`: CPU/heap/trace profiling for startup, gateway, and agent-turn hotspots. -- `live-openai-candidate`: a real OpenAI `openai/gpt-5.5` agent turn, skipped when `OPENAI_API_KEY` is unavailable. +- `mock-deep-profile`: CPU/heap/trace profiling for startup, gateway, and agent-turn hotspots. Runs on schedule, or on dispatch with `deep_profile=true`. +- `live-openai-candidate`: a real OpenAI `openai/gpt-5.5` agent turn, skipped when `OPENAI_API_KEY` is unavailable. Runs on schedule, or on dispatch with `live_openai_candidate=true`. -The mock-provider lane also runs OpenClaw-native source probes after the Kova pass: gateway boot timing and memory across default, hook, and 50-plugin startup cases; bundled plugin import RSS, repeated mock-OpenAI `channel-chat-baseline` hello loops, CLI startup commands against the booted gateway, and the SQLite state smoke performance probe. When the previous published mock-provider source report is available for the tested ref, the source summary compares current RSS and heap values against that baseline and marks large RSS increases as `watch`. The source probe Markdown summary lives at `source/index.md` in the report bundle, with raw JSON beside it. +The mock-provider lane also runs OpenClaw-native source probes after the Kova pass: gateway boot timing and memory across default, skipped-channel, internal-hook, and fifty-plugin startup cases; bundled plugin import RSS, repeated mock-OpenAI `channel-chat-baseline` hello loops, CLI startup commands against the booted gateway, and the SQLite state smoke performance probe. When the previous published mock-provider source report is available for the tested ref, the source summary compares current RSS and heap values against that baseline and marks large RSS increases as `watch`. The source probe Markdown summary lives at `source/index.md` in the report bundle, with raw JSON beside it. Every lane uploads GitHub artifacts. When `CLAWGRIT_REPORTS_TOKEN` is configured, the workflow also commits `report.json`, `report.md`, bundles, `index.md`, and source-probe artifacts into `openclaw/clawgrit-reports` under `openclaw-performance//-//`. The current tested-ref pointer is written as `openclaw-performance//latest-.json`. ## Full Release Validation -`Full Release Validation` is the manual umbrella workflow for "run everything before release." It accepts a branch, tag, or full commit SHA, dispatches the manual `CI` workflow with that target, dispatches `Plugin Prerelease` for release-only plugin/package/static/Docker proof, and dispatches `OpenClaw Release Checks` for install smoke, package acceptance, cross-OS package checks, maturity scorecard rendering from QA profile evidence, QA Lab parity, Matrix, and Telegram lanes. Stable and full profiles always include exhaustive live/E2E and Docker release-path soak coverage; the beta profile can opt in with `run_release_soak=true`. The canonical package Telegram E2E runs inside Package Acceptance, so a full candidate does not start a duplicate live poller. After publishing, pass `release_package_spec` to reuse the shipped npm package across release checks, Package Acceptance, Docker, cross-OS, and Telegram without rebuilding. Use `npm_telegram_package_spec` only for a focused published-package Telegram rerun. The Codex plugin live package lane uses the same selected state by default: published `release_package_spec=openclaw@` derives `codex_plugin_spec=npm:@openclaw/codex@`, while SHA/artifact runs pack `extensions/codex` from the selected ref. Set `codex_plugin_spec` explicitly for custom plugin sources such as `npm:`, `npm-pack:`, or `git:` specs. +`Full Release Validation` is the manual umbrella workflow for "run everything before release." It accepts a branch, tag, or full commit SHA, dispatches the manual `CI` workflow with that target (including Android), dispatches `Plugin Prerelease` for release-only plugin/package/static/Docker proof, dispatches `OpenClaw Performance` against the target SHA, and dispatches `OpenClaw Release Checks` for install smoke, package acceptance, cross-OS package checks, QA Lab parity, Matrix, and Telegram lanes (advisory maturity scorecard rendering is opt-in via `run_maturity_scorecard`). Stable and full profiles always include exhaustive live/E2E and Docker release-path soak coverage; the beta profile can opt in with `run_release_soak=true`. The canonical package Telegram E2E runs inside Package Acceptance, so a full candidate does not start a duplicate live poller. After publishing, pass `release_package_spec` to reuse the shipped npm package across release checks, Package Acceptance, Docker, cross-OS, and Telegram without rebuilding. Use `npm_telegram_package_spec` only for a focused published-package Telegram rerun. The Codex plugin live package lane uses the same selected state by default: published `release_package_spec=openclaw@` derives `codex_plugin_spec=npm:@openclaw/codex@`, while SHA/artifact runs pack `extensions/codex` from the selected ref. Set `codex_plugin_spec` explicitly for custom plugin sources such as `npm:`, `npm-pack:`, or `git:` specs. See [Full release validation](/reference/full-release-validation) for the stage matrix, exact workflow job names, profile differences, artifacts, and @@ -235,10 +244,11 @@ focused rerun handles. `OpenClaw Release Publish` is the manual mutating release workflow. Dispatch it from `release/YYYY.M.PATCH` or `main` after the release tag exists and after the -OpenClaw npm preflight has succeeded. It verifies `pnpm plugins:sync:check`, -dispatches `Plugin NPM Release` for all publishable plugin packages, dispatches -`Plugin ClawHub Release` for the same release SHA, and only then dispatches -`OpenClaw NPM Release` with the saved `preflight_run_id`. Stable publish also +OpenClaw npm preflight has succeeded (the preflight runs `pnpm plugins:sync:check` +among its checks). It requires the saved `preflight_run_id` and a successful +`full_release_validation_run_id`, dispatches `Plugin NPM Release` for all +publishable plugin packages, dispatches `Plugin ClawHub Release` for the same +release SHA, and only then dispatches `OpenClaw NPM Release`. Stable publish also requires an exact `windows_node_tag`; the workflow verifies the Windows source release and compares its x64/ARM64 installers with the candidate-approved `windows_node_installer_digests` input before any publish child, then promotes @@ -280,7 +290,7 @@ the beta profile can opt in with `run_release_soak=true`. The umbrella records the dispatched child run ids, and the final `Verify full validation` job re-checks current child run conclusions and appends slowest-job tables for each child run. If a child workflow is rerun and turns green, rerun only the parent verifier job to refresh the umbrella result and timing summary. -For recovery, both `Full Release Validation` and `OpenClaw Release Checks` accept `rerun_group`. Use `all` for a release candidate, `ci` for only the normal full CI child, `plugin-prerelease` for only the plugin prerelease child, `release-checks` for every release child, or a narrower group: `install-smoke`, `cross-os`, `live-e2e`, `package`, `qa`, `qa-parity`, `qa-live`, or `npm-telegram` on the umbrella. This keeps a failed release box rerun bounded after a focused fix. For one failed cross-OS lane, combine `rerun_group=cross-os` with `cross_os_suite_filter`, for example `windows/packaged-upgrade`; long cross-OS commands emit heartbeat lines and packaged-upgrade summaries include per-phase timings. QA release-check lanes are advisory except the standard runtime tool coverage gate, which blocks when required OpenClaw dynamic tools drift or disappear from the standard tier summary. +For recovery, both `Full Release Validation` and `OpenClaw Release Checks` accept `rerun_group`. Use `all` for a release candidate, `ci` for only the normal full CI child, `plugin-prerelease` for only the plugin prerelease child, `performance` for only the OpenClaw Performance child, `release-checks` for every release child, or a narrower group: `install-smoke`, `cross-os`, `live-e2e`, `package`, `qa`, `qa-parity`, `qa-live`, or `npm-telegram` on the umbrella. This keeps a failed release box rerun bounded after a focused fix. For one failed cross-OS lane, combine `rerun_group=cross-os` with `cross_os_suite_filter`, for example `windows/packaged-upgrade`; long cross-OS commands emit heartbeat lines and packaged-upgrade summaries include per-phase timings. QA release-check lanes are advisory except the standard runtime tool coverage gate, which blocks when required OpenClaw dynamic tools drift or disappear from the standard tier summary. `OpenClaw Release Checks` uses the trusted workflow ref to resolve the selected ref once into a `release-package-under-test` tarball, then passes that artifact to cross-OS checks and Package Acceptance, plus the live/E2E release-path Docker workflow when soak coverage runs. That keeps the package bytes consistent across release boxes and avoids repacking the same candidate in multiple child jobs. For the Codex npm-plugin live lane, release checks either pass a matching published plugin spec derived from `release_package_spec`, pass the operator-supplied `codex_plugin_spec`, or leave the input blank so the Docker script packs the selected checkout's Codex plugin. @@ -294,23 +304,25 @@ validation and focused rerun groups keep `cancel-in-progress: false`. The release live/E2E child keeps broad native `pnpm test:live` coverage, but it runs it as named shards through `scripts/test-live-shard.mjs` instead of one serial job: -- `native-live-src-agents` +- `native-live-src-agents` and `native-live-src-agents-zai-coding` - `native-live-src-gateway-core` - provider-filtered `native-live-src-gateway-profiles` jobs - `native-live-src-gateway-backends` +- `native-live-src-infra` - `native-live-test` - `native-live-extensions-a-k` - `native-live-extensions-l-n` +- `native-live-extensions-moonshot` - `native-live-extensions-openai` - `native-live-extensions-o-z-other` - `native-live-extensions-xai` - split media audio/video shards and provider-filtered music shards -That keeps the same file coverage while making slow live provider failures easier to rerun and diagnose. The aggregate `native-live-extensions-o-z`, `native-live-extensions-media`, and `native-live-extensions-media-music` shard names remain valid for manual one-shot reruns. +That keeps the same file coverage while making slow live provider failures easier to rerun and diagnose. The aggregate `native-live-src-gateway`, `native-live-extensions-o-z`, `native-live-extensions-media`, and `native-live-extensions-media-music` shard names remain valid for manual one-shot reruns. The native live media shards run in `ghcr.io/openclaw/openclaw-live-media-runner:ubuntu-24.04`, built by the `Live Media Runner Image` workflow. That image preinstalls `ffmpeg` and `ffprobe`; media jobs only verify the binaries before setup. Keep Docker-backed live suites on normal Blacksmith runners — container jobs are the wrong place to launch nested Docker tests. -Docker-backed live model/backend shards use a separate shared `ghcr.io/openclaw/openclaw-live-test:` image per selected commit. The live release workflow builds and pushes that image once, then the Docker live model, provider-sharded gateway, CLI backend, ACP bind, and Codex harness shards run with `OPENCLAW_SKIP_DOCKER_BUILD=1`. Gateway Docker shards carry explicit script-level `timeout` caps below the workflow job timeout so a stuck container or cleanup path fails fast instead of consuming the whole release-check budget. If those shards rebuild the full source Docker target independently, the release run is misconfigured and will waste wall clock on duplicate image builds. +Docker-backed live model/backend shards use a separate shared `ghcr.io/openclaw/openclaw-live-test:-` image per selected commit. The live release workflow builds and pushes that image once, then the Docker live model, provider-sharded gateway, CLI backend, ACP bind, and Codex harness shards run with `OPENCLAW_SKIP_DOCKER_BUILD=1`. Gateway Docker shards carry explicit script-level `timeout` caps below the workflow job timeout so a stuck container or cleanup path fails fast instead of consuming the whole release-check budget. If those shards rebuild the full source Docker target independently, the release run is misconfigured and will waste wall clock on duplicate image builds. ## Package Acceptance @@ -319,9 +331,10 @@ Use `Package Acceptance` when the question is "does this installable OpenClaw pa ### Jobs 1. `resolve_package` checks out `workflow_ref`, resolves one package candidate, writes `.artifacts/docker-e2e-package/openclaw-current.tgz`, writes `.artifacts/docker-e2e-package/package-candidate.json`, uploads both as the `package-under-test` artifact, and prints the source, workflow ref, package ref, version, SHA-256, and profile in the GitHub step summary. -2. `docker_acceptance` calls `openclaw-live-and-e2e-checks-reusable.yml` with `ref=workflow_ref` and `package_artifact_name=package-under-test`. The reusable workflow downloads that artifact, validates the tarball inventory, prepares package-digest Docker images when needed, and runs the selected Docker lanes against that package instead of packing the workflow checkout. When a profile selects multiple targeted `docker_lanes`, the reusable workflow prepares the package and shared images once, then fans those lanes out as parallel targeted Docker jobs with unique artifacts. -3. `package_telegram` optionally calls `NPM Telegram Beta E2E`. It runs when `telegram_mode` is not `none` and installs the same `package-under-test` artifact when Package Acceptance resolved one; standalone Telegram dispatch can still install a published npm spec. -4. `summary` fails the workflow if package resolution, Docker acceptance, or the optional Telegram lane failed. +2. `package_integrity` downloads the `package-under-test` artifact and enforces the public package tarball contract with `scripts/check-openclaw-package-tarball.mjs`. +3. `docker_acceptance` calls `openclaw-live-and-e2e-checks-reusable.yml` with the resolved package source SHA (falling back to `workflow_ref`) and `package_artifact_name=package-under-test`. The reusable workflow downloads that artifact, validates the tarball inventory, prepares package-digest Docker images when needed, and runs the selected Docker lanes against that package instead of packing the workflow checkout. When a profile selects multiple targeted `docker_lanes`, the reusable workflow prepares the package and shared images once, then fans those lanes out as parallel targeted Docker jobs with unique artifacts. +4. `package_telegram` optionally calls `NPM Telegram Beta E2E`. It runs when `telegram_mode` is not `none` and installs the same `package-under-test` artifact when Package Acceptance resolved one; standalone Telegram dispatch can still install a published npm spec. +5. `summary` fails the workflow if package resolution, integrity, Docker acceptance, or the optional Telegram lane failed. The `advisory` input downgrades acceptance failures to warnings for advisory callers. ### Candidate sources @@ -336,8 +349,8 @@ Keep `workflow_ref` and `package_ref` separate. `workflow_ref` is the trusted wo ### Suite profiles - `smoke` — `npm-onboard-channel-agent`, `gateway-network`, `config-reload` -- `package` — `npm-onboard-channel-agent`, `doctor-switch`, `update-channel-switch`, `skill-install`, `update-corrupt-plugin`, `upgrade-survivor`, `published-upgrade-survivor`, `update-restart-auth`, `plugins-offline`, `plugin-update` -- `product` — `package` plus `mcp-channels`, `cron-mcp-cleanup`, `openai-web-search-minimal`, `openwebui` +- `package` — `npm-onboard-channel-agent`, `doctor-switch`, `update-channel-switch`, `skill-install`, `update-corrupt-plugin`, `upgrade-survivor`, `published-upgrade-survivor`, `root-managed-vps-upgrade`, `update-restart-auth`, `plugins-offline`, `plugin-update` +- `product` — the `package` set with live `plugins` coverage instead of `plugins-offline`, plus `mcp-channels`, `cron-mcp-cleanup`, `openai-web-search-minimal`, `openwebui` - `full` — full Docker release-path chunks with OpenWebUI - `custom` — exact `docker_lanes`; required when `suite_profile=custom` @@ -347,7 +360,9 @@ For the dedicated update and plugin testing policy, including local commands, Docker lanes, Package Acceptance inputs, release defaults, and failure triage, see [Testing updates and plugins](/help/testing-updates-plugins). -Release checks call Package Acceptance with `source=artifact`, the prepared release package artifact, `suite_profile=custom`, `docker_lanes='doctor-switch update-channel-switch skill-install update-corrupt-plugin upgrade-survivor published-upgrade-survivor update-restart-auth plugins-offline plugin-update'`, and `telegram_mode=mock-openai`. This keeps package migration, update, live ClawHub skill install, stale-plugin-dependency cleanup, configured-plugin install repair, offline plugin, plugin-update, and Telegram proof on the same resolved package tarball. Set `release_package_spec` on Full Release Validation or OpenClaw Release Checks after publishing a beta to run the same matrix against the shipped npm package without rebuilding; set `package_acceptance_package_spec` only when Package Acceptance needs a different package from the rest of release validation. Cross-OS release checks still cover OS-specific onboarding, installer, and platform behavior; package/update product validation should start with Package Acceptance. The `published-upgrade-survivor` Docker lane validates one published package baseline per run in the blocking release path. In Package Acceptance, the resolved `package-under-test` tarball is always the candidate and `published_upgrade_survivor_baseline` selects the fallback published baseline, defaulting to `openclaw@latest`; failed-lane rerun commands preserve that baseline. Full Release Validation with `run_release_soak=true` or `release_profile=full` sets `published_upgrade_survivor_baselines='last-stable-4 2026.4.23 2026.5.2 2026.4.15'` and `published_upgrade_survivor_scenarios=reported-issues` to expand across the four latest stable npm releases plus pinned plugin-compatibility boundary releases and issue-shaped fixtures for Feishu config, preserved bootstrap/persona files, configured OpenClaw plugin installs, tilde log paths, and stale legacy plugin dependency roots. Multi-baseline published-upgrade survivor selections are sharded by baseline into separate targeted Docker runner jobs. The separate `Update Migration` workflow uses the `update-migration` Docker lane with `all-since-2026.4.23` and `plugin-deps-cleanup` when the question is exhaustive published update cleanup, not normal Full Release CI breadth. Local aggregate runs can pass exact package specs with `OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPECS`, keep a single lane with `OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC` such as `openclaw@2026.4.15`, or set `OPENCLAW_UPGRADE_SURVIVOR_SCENARIOS` for the scenario matrix. The published lane configures the baseline with a baked `openclaw config set` command recipe, records recipe steps in `summary.json`, and probes `/healthz`, `/readyz`, plus RPC status after Gateway start. The Windows packaged and installer fresh lanes also verify that an installed package can import a browser-control override from a raw absolute Windows path. The OpenAI cross-OS agent-turn smoke defaults to `OPENCLAW_CROSS_OS_OPENAI_MODEL` when set, otherwise `openai/gpt-5.5`, so the install and gateway proof stays on a GPT-5 test model while avoiding GPT-4.x defaults. +Release checks call Package Acceptance with `source=artifact`, the prepared release package artifact, `suite_profile=custom`, `docker_lanes='doctor-switch update-channel-switch skill-install update-corrupt-plugin upgrade-survivor published-upgrade-survivor root-managed-vps-upgrade update-restart-auth plugins-offline plugin-update plugin-binding-command-escape'`, and `telegram_mode=mock-openai`. This keeps package migration, update, live ClawHub skill install, stale-plugin-dependency cleanup, configured-plugin install repair, offline plugin, plugin-update, and Telegram proof on the same resolved package tarball. Set `release_package_spec` on Full Release Validation or OpenClaw Release Checks after publishing a beta to run the same matrix against the shipped npm package without rebuilding; set `package_acceptance_package_spec` only when Package Acceptance needs a different package from the rest of release validation. Cross-OS release checks still cover OS-specific onboarding, installer, and platform behavior; package/update product validation should start with Package Acceptance. + +The `published-upgrade-survivor` Docker lane validates one published package baseline per run in the blocking release path. In Package Acceptance, the resolved `package-under-test` tarball is always the candidate and `published_upgrade_survivor_baseline` selects the fallback published baseline, defaulting to `openclaw@latest`; failed-lane rerun commands preserve that baseline. Full Release Validation with `run_release_soak=true` or `release_profile=full` sets `published_upgrade_survivor_baselines='last-stable-4 2026.4.23 2026.5.2 2026.4.15'` and `published_upgrade_survivor_scenarios=reported-issues` to expand across the four latest stable npm releases plus pinned plugin-compatibility boundary releases and issue-shaped fixtures for Feishu config, preserved bootstrap/persona files, configured OpenClaw plugin installs, tilde log paths, and stale legacy plugin dependency roots. Multi-baseline published-upgrade survivor selections are sharded by baseline into separate targeted Docker runner jobs. The separate `Update Migration` workflow uses the `update-migration` Docker lane with `all-since-2026.4.23` baselines and `plugin-deps-cleanup` scenarios when the question is exhaustive published update cleanup, not normal Full Release CI breadth. Local aggregate runs can pass exact package specs with `OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPECS`, keep a single lane with `OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC` such as `openclaw@2026.4.15`, or set `OPENCLAW_UPGRADE_SURVIVOR_SCENARIOS` for the scenario matrix. The published lane configures the baseline with a baked `openclaw config set` command recipe, records recipe steps in `summary.json`, and probes `/healthz`, `/readyz`, plus RPC status after Gateway start. The Windows packaged and installer fresh lanes also verify that an installed package can import a browser-control override from a raw absolute Windows path. The OpenAI cross-OS agent-turn smoke defaults to `OPENCLAW_CROSS_OS_OPENAI_MODEL` when set, otherwise `openai/gpt-5.5`, so the install and gateway proof stays on a GPT-5 test model while avoiding GPT-4.x defaults. ### Legacy compatibility windows @@ -359,7 +374,7 @@ Package Acceptance has bounded legacy-compatibility windows for already-publishe - plugin smokes may read legacy install-record locations or accept missing marketplace install-record persistence; - `plugin-update` may allow config metadata migration while still requiring the install record and no-reinstall behavior to stay unchanged. -The published `2026.4.26` package may also warn for local build metadata stamp files that were already shipped. Later packages must satisfy the modern contracts; the same conditions fail instead of warn or skip. +The published `2026.4.26` package may also warn for local build metadata stamp files that were already shipped, and packages through `2026.5.20` may warn instead of fail when `npm-shrinkwrap.json` is missing. Later packages must satisfy the modern contracts; the same conditions fail instead of warn or skip. ### Examples @@ -425,14 +440,12 @@ When debugging a failed package acceptance run, start at the `resolve_package` s ## Install smoke -The separate `Install Smoke` workflow reuses the same scope script through its own `preflight` job. It splits smoke coverage into `run_fast_install_smoke` and `run_full_install_smoke`. +The separate `Install Smoke` workflow no longer runs on pull requests or `main` pushes. It runs on a nightly schedule, on manual dispatch, and as a workflow call from release validation, and every run takes the full install-smoke path on GitHub-hosted runners: -- **Fast path** runs for pull requests touching Docker/package surfaces, bundled plugin package/manifest changes, or core plugin/channel/gateway/Plugin SDK surfaces that the Docker smoke jobs exercise. Source-only bundled plugin changes, test-only edits, and docs-only edits do not reserve Docker workers. The fast path builds the root Dockerfile image once, checks the CLI, runs the agents delete shared-workspace CLI smoke, runs the container gateway-network e2e, verifies a bundled extension build arg, and runs the bounded bundled-plugin Docker profile under a 240-second aggregate command timeout (each scenario's Docker run capped separately). -- **Full path** keeps QR package install and installer Docker/update coverage for nightly scheduled runs, manual dispatches, workflow-call release checks, and pull requests that truly touch installer/package/Docker surfaces. In full mode, install-smoke prepares or reuses one target-SHA GHCR root Dockerfile smoke image, then runs QR package install, root Dockerfile/gateway smokes, installer/update smokes, and the fast bundled-plugin Docker E2E as separate jobs so installer work does not wait behind the root image smokes. +- The root Dockerfile smoke image is built once per target SHA (or reused from GHCR as `ghcr.io/openclaw/openclaw-dockerfile-smoke:`), then the CLI smoke, the agents delete shared-workspace CLI smoke, the container gateway-network E2E, and the bundled `matrix` plugin build-arg smoke run against it. The plugin smoke verifies runtime dependency install mirroring and that the plugin loads without entry-escape diagnostics. +- QR package install and the installer/update Docker smokes (including Rocky Linux installer lanes and an update lane against a configurable `update_baseline_version` npm baseline) run as separate jobs so installer work does not wait behind the root image smokes. -`main` pushes (including merge commits) do not force the full path; when changed-scope logic would request full coverage on a push, the workflow keeps the fast Docker smoke and leaves the full install smoke to nightly or release validation. - -The slow Bun global install image-provider smoke is separately gated by `run_bun_global_install_smoke`. It runs on the nightly schedule and from the release checks workflow, and manual `Install Smoke` dispatches can opt into it, but pull requests and `main` pushes do not. Normal PR CI still runs the fast Bun launcher regression lane for Node-relevant changes. QR and installer Docker tests keep their own install-focused Dockerfiles. +The slow Bun global install image-provider smoke is separately gated by `run_bun_global_install_smoke`. It runs on the nightly schedule, defaults on for workflow calls from release checks, and manual `Install Smoke` dispatches can opt into it. Normal PR CI still runs the fast Bun launcher regression lane for Node-relevant changes. QR and installer Docker tests keep their own install-focused Dockerfiles. ## Local Docker E2E @@ -485,7 +498,7 @@ The scheduled live/E2E workflow runs the full release-path Docker suite daily. ## Plugin Prerelease -`Plugin Prerelease` is more expensive product/package coverage, so it is a separate workflow dispatched by `Full Release Validation` or by an explicit operator. Normal pull requests, `main` pushes, and standalone manual CI dispatches keep that suite off. It balances bundled plugin tests across eight extension workers; those extension shard jobs run up to two plugin config groups at a time with one Vitest worker per group and a larger Node heap so import-heavy plugin batches do not create extra CI jobs. The release-only Docker prerelease path batches targeted Docker lanes in small groups to avoid reserving dozens of runners for one-to-three-minute jobs. The workflow also uploads an informational `plugin-inspector-advisory` artifact from `@openclaw/plugin-inspector`; inspector findings are triage input and do not change the blocking Plugin Prerelease gate. +`Plugin Prerelease` is more expensive product/package coverage, so it is a separate workflow dispatched by `Full Release Validation` or by an explicit operator. Normal pull requests, `main` pushes, and standalone manual CI dispatches keep that suite off. It balances bundled plugin tests across eight extension workers; those extension shard jobs run up to two plugin config groups at a time with one Vitest worker per group and a larger Node heap so import-heavy plugin batches do not create extra CI jobs. The release-only Docker prerelease path (enabled by the `full_release_validation` input) batches targeted Docker lanes in groups of four to avoid reserving dozens of runners for one-to-three-minute jobs. The workflow also uploads an informational `plugin-inspector-advisory` artifact from `@openclaw/plugin-inspector`; inspector findings are triage input and do not change the blocking Plugin Prerelease gate. ## QA Lab @@ -503,7 +516,7 @@ For normal PRs, follow scoped CI/check evidence instead of treating parity as a ## CodeQL -The `CodeQL` workflow is intentionally a narrow first-pass security scanner, not the full repository sweep. Daily, manual, and non-draft pull request guard runs scan Actions workflow code plus the highest-risk JavaScript/TypeScript surfaces with high-confidence security queries filtered to high/critical `security-severity`. +The `CodeQL` workflow is intentionally a narrow first-pass security scanner, not the full repository sweep. Daily, manual, `main` push, and non-draft pull request guard runs scan Actions workflow code plus the highest-risk JavaScript/TypeScript surfaces with high-confidence security queries filtered to high/critical `security-severity`. The pull request guard stays light: it only starts for changes under `.github/actions`, `.github/codeql`, `.github/workflows`, `packages`, `scripts`, `src`, or process-owning bundled plugin runtime paths, and it runs the same high-confidence security matrix as the scheduled workflow. Android and macOS CodeQL stay out of PR defaults. @@ -525,12 +538,12 @@ The pull request guard stays light: it only starts for changes under `.github/ac ### Critical Quality categories -`CodeQL Critical Quality` is the matching non-security shard. It runs only error-severity, non-security JavaScript/TypeScript quality queries over narrow high-value surfaces on GitHub-hosted Linux runners so quality scans do not spend Blacksmith runner-registration budget. Its pull request guard is intentionally smaller than the scheduled profile: non-draft PRs only run the matching `agent-runtime-boundary`, `config-boundary`, `core-auth-secrets`, `channel-runtime-boundary`, `gateway-runtime-boundary`, `memory-runtime-boundary`, `mcp-process-runtime-boundary`, `provider-runtime-boundary`, `session-diagnostics-boundary`, `plugin-boundary`, `plugin-sdk-package-contract`, and `plugin-sdk-reply-runtime` shards for agent command/model/tool execution and reply dispatch code, config schema/migration/IO code, auth/secrets/sandbox/security code, core channel and bundled channel plugin runtime, gateway protocol/server-method, memory runtime/SDK glue, MCP/process/outbound delivery, provider runtime/model catalog, session diagnostics/delivery queues, plugin loader, Plugin SDK/package-contract, or Plugin SDK reply runtime changes. CodeQL config and quality workflow changes run all twelve PR quality shards. +`CodeQL Critical Quality` is the matching non-security shard. It runs only error-severity, non-security JavaScript/TypeScript quality queries over narrow high-value surfaces on GitHub-hosted Linux runners so quality scans do not spend Blacksmith runner-registration budget. Its pull request guard is intentionally smaller than the scheduled profile: non-draft PRs run only the matching shards for the surfaces they touch, from thirteen PR-routable shards — `agent-runtime-boundary`, `channel-runtime-boundary`, `config-boundary`, `core-auth-secrets`, `gateway-runtime-boundary`, `mcp-process-runtime-boundary`, `memory-runtime-boundary`, `network-runtime-boundary`, `plugin-boundary`, `plugin-sdk-package-contract`, `plugin-sdk-reply-runtime`, `provider-runtime-boundary`, and `session-diagnostics-boundary`. `ui-control-plane` and `web-media-runtime-boundary` stay out of PR runs. CodeQL config and quality workflow changes run the full PR shard set (the network runtime shard keys off its own CodeQL config files and network-owning source paths). Manual dispatch accepts: -``` -profile=all|agent-runtime-boundary|config-boundary|core-auth-secrets|channel-runtime-boundary|gateway-runtime-boundary|memory-runtime-boundary|mcp-process-runtime-boundary|plugin-boundary|plugin-sdk-package-contract|plugin-sdk-reply-runtime|provider-runtime-boundary|session-diagnostics-boundary +```text +profile=all|agent-runtime-boundary|config-boundary|core-auth-secrets|channel-runtime-boundary|gateway-runtime-boundary|memory-runtime-boundary|mcp-process-runtime-boundary|network-runtime-boundary|plugin-boundary|plugin-sdk-package-contract|plugin-sdk-reply-runtime|provider-runtime-boundary|session-diagnostics-boundary ``` The narrow profiles are teaching/iteration hooks for running one quality shard in isolation. @@ -544,6 +557,7 @@ The narrow profiles are teaching/iteration hooks for running one quality shard i | `/codeql-critical-quality/agent-runtime-boundary` | Command execution, model/provider dispatch, auto-reply dispatch and queues, and ACP control-plane runtime contracts | | `/codeql-critical-quality/mcp-process-runtime-boundary` | MCP servers and tool bridges, process supervision helpers, and outbound delivery contracts | | `/codeql-critical-quality/memory-runtime-boundary` | Memory host SDK, memory runtime facades, memory Plugin SDK aliases, memory runtime activation glue, and memory doctor commands | +| `/codeql-critical-quality/network-runtime-boundary` | Network policy package, raw socket and proxy-capture runtime, SSH tunnel, gateway lock, JSONL socket, and push transport surfaces | | `/codeql-critical-quality/session-diagnostics-boundary` | Reply queue internals, session delivery queues, outbound session binding/delivery helpers, diagnostic event/log bundle surfaces, and session doctor CLI contracts | | `/codeql-critical-quality/plugin-sdk-reply-runtime` | Plugin SDK inbound reply dispatch, reply payload/chunking/runtime helpers, channel reply options, delivery queues, and session/thread binding helpers | | `/codeql-critical-quality/provider-runtime-boundary` | Model catalog normalization, provider auth and discovery, provider runtime registration, provider defaults/catalogs, and web/search/fetch/embedding registries | @@ -595,14 +609,15 @@ Crabbox is the repo-owned remote-box wrapper for maintainer Linux proof. Use it from the repo root when a check is too broad for a local edit loop, when CI parity matters, or when the proof needs secrets, Docker, package lanes, reusable boxes, or remote logs. The normal OpenClaw backend is -`blacksmith-testbox`; owned AWS/Hetzner capacity is a fallback for Blacksmith -outages, quota issues, or explicit owned-capacity testing. +`blacksmith-testbox`, and `.crabbox.yaml` now defaults to it; owned AWS/Hetzner +capacity is a fallback for Blacksmith outages, quota issues, or explicit +owned-capacity testing. Crabbox-backed Blacksmith runs warm, claim, sync, run, report, and clean up -one-shot Testboxes. The built-in sync sanity check fails fast when required -root files such as `pnpm-lock.yaml` disappear or when `git status --short` -shows at least 200 tracked deletions. For intentional large-deletion PRs, set -`OPENCLAW_TESTBOX_ALLOW_MASS_DELETIONS=1` for the remote command. +one-shot Testboxes. The built-in sync sanity check fails fast when +`git status --short` on the synced box shows at least 200 tracked deletions, +which catches disappearing root files such as `pnpm-lock.yaml`. For intentional +large-deletion PRs, set `CRABBOX_ALLOW_MASS_DELETIONS=1` for the remote command. Crabbox also terminates a local Blacksmith CLI invocation that stays in the sync phase for more than five minutes without post-sync output. Set @@ -615,20 +630,20 @@ Before a first run, check the wrapper from the repo root: pnpm crabbox:run -- --help | sed -n '1,120p' ``` -The repo wrapper refuses a stale Crabbox binary that does not advertise `blacksmith-testbox`. Pass the provider explicitly even though `.crabbox.yaml` has owned-cloud defaults. In Codex worktrees or linked/sparse checkouts, avoid the local `pnpm crabbox:run` script because pnpm may reconcile dependencies before Crabbox starts; invoke the node wrapper directly instead: +The repo wrapper refuses a stale Crabbox binary that does not advertise the selected provider, and Blacksmith-backed runs require Crabbox 0.22.0 or newer so the wrapper gets the current Testbox sync, queue, and cleanup behavior. In Codex worktrees or linked/sparse checkouts, avoid the local `pnpm crabbox:run` script because pnpm may reconcile dependencies before Crabbox starts; invoke the node wrapper directly instead: ```bash node scripts/crabbox-wrapper.mjs run --provider blacksmith-testbox --timing-json --shell -- "pnpm test " ``` -Blacksmith-backed runs require Crabbox 0.22.0 or newer so the wrapper gets the current Testbox sync, queue, and cleanup behavior. When using the sibling checkout, rebuild the ignored local binary before timing or proof work: +When using the sibling checkout, rebuild the ignored local binary before timing or proof work: ```bash version="$(git -C ../crabbox describe --tags --always --dirty | sed 's/^v//')" \ && go build -C ../crabbox -trimpath -ldflags "-s -w -X github.com/openclaw/crabbox/internal/cli.version=${version}" -o bin/crabbox ./cmd/crabbox ``` -Changed gate: +The `blacksmith:` block in `.crabbox.yaml` already pins the org, workflow, job, and ref defaults, so the explicit flags below are optional. Changed gate: ```bash pnpm crabbox:run -- --provider blacksmith-testbox \ @@ -647,10 +662,6 @@ Focused test rerun: ```bash pnpm crabbox:run -- --provider blacksmith-testbox \ - --blacksmith-org openclaw \ - --blacksmith-workflow .github/workflows/ci-check-testbox.yml \ - --blacksmith-job check \ - --blacksmith-ref main \ --idle-timeout 90m \ --ttl 240m \ --timing-json \ @@ -662,10 +673,6 @@ Full suite: ```bash pnpm crabbox:run -- --provider blacksmith-testbox \ - --blacksmith-org openclaw \ - --blacksmith-workflow .github/workflows/ci-check-testbox.yml \ - --blacksmith-job check \ - --blacksmith-ref main \ --idle-timeout 90m \ --ttl 240m \ --timing-json \ @@ -718,9 +725,9 @@ pnpm crabbox:run -- --id --timing-json --shell -- "pnpm check:c pnpm crabbox:stop -- ``` -Under AWS pressure, avoid `class=beast` unless the task really needs 48xlarge-class CPU. A `beast` request starts at 192 vCPUs and is the easiest way to trip regional EC2 Spot or On-Demand Standard quota. The repo-owned `.crabbox.yaml` defaults to `standard`, multiple capacity regions, and `capacity.hints: true` so brokered AWS leases print selected region/market, quota pressure, Spot fallback, and high-pressure class warnings. Use `fast` for heavier broad checks, `large` only after standard/fast are not enough, and `beast` only for exceptional CPU-bound lanes such as full-suite or all-plugin Docker matrices, explicit release/blocker validation, or high-core performance profiling. Do not use `beast` for `pnpm check:changed`, focused tests, docs-only work, ordinary lint/typecheck, small E2E repros, or Blacksmith outage triage. Use `--market on-demand` for capacity diagnosis so Spot market churn is not mixed into the signal. +Under AWS pressure, avoid `class=beast` unless the task really needs 48xlarge-class CPU. A `beast` request starts at 192 vCPUs and is the easiest way to trip regional EC2 Spot or On-Demand Standard quota. The repo-owned `.crabbox.yaml` defaults to `class: standard`, on-demand market, and `capacity.hints: true` so brokered AWS leases print selected region/market, quota pressure, Spot fallback, and high-pressure class warnings. Use `fast` for heavier broad checks, `large` only after standard/fast are not enough, and `beast` only for exceptional CPU-bound lanes such as full-suite or all-plugin Docker matrices, explicit release/blocker validation, or high-core performance profiling. Do not use `beast` for `pnpm check:changed`, focused tests, docs-only work, ordinary lint/typecheck, small E2E repros, or Blacksmith outage triage. Use `--market on-demand` for capacity diagnosis so Spot market churn is not mixed into the signal. -`.crabbox.yaml` owns provider, sync, and GitHub Actions hydration defaults for owned-cloud lanes. It excludes local `.git` so the hydrated Actions checkout keeps its own remote Git metadata instead of syncing maintainer-local remotes and object stores, and it excludes local runtime/build artifacts that should never be transferred. `.github/workflows/crabbox-hydrate.yml` owns checkout, Node/pnpm setup, `origin/main` fetch, and the non-secret environment handoff for owned-cloud `crabbox run --id ` commands. +`.crabbox.yaml` owns provider, sync, and GitHub Actions hydration defaults. Crabbox sync never transfers `.git`, so the hydrated Actions checkout keeps its own remote Git metadata instead of syncing maintainer-local remotes and object stores, and the repo config additionally excludes local runtime/build artifacts (such as `.artifacts` and test reports) that should never be transferred. `.github/workflows/crabbox-hydrate.yml` owns checkout, Node/pnpm setup, `origin/main` fetch, and the non-secret environment handoff for owned-cloud `crabbox run --id ` commands. ## Related diff --git a/docs/clawhub/cli.md b/docs/clawhub/cli.md index d6180ba5f0c6..443224c68dcf 100644 --- a/docs/clawhub/cli.md +++ b/docs/clawhub/cli.md @@ -9,79 +9,82 @@ title: "ClawHub CLI" # ClawHub CLI -OpenClaw has two command-line entry points for ClawHub: +Two command-line surfaces talk to ClawHub: -- `openclaw skills` and `openclaw plugins` install and manage ClawHub packages - inside OpenClaw. -- The standalone `clawhub` CLI handles publisher workflows such as login, - publish, transfer, and sync. +- `openclaw skills` / `openclaw plugins` - discover, install, and update + packages for a local OpenClaw agent or Gateway. +- The standalone `clawhub` CLI - publisher workflows: login, publish, sync, + and transfer. ## Discover and install -Use OpenClaw commands when you want to install or update packages for a local -OpenClaw agent or Gateway. - ```bash openclaw skills search "calendar" openclaw skills install @owner/ -openclaw skills install @owner/ --acknowledge-clawhub-risk +openclaw skills install @owner/ --version --global openclaw skills update @owner/ -openclaw skills update @owner/ --acknowledge-clawhub-risk -openclaw skills verify @owner/ +openclaw skills update --all --acknowledge-clawhub-risk +openclaw skills verify @owner/ --card openclaw plugins search "calendar" openclaw plugins install clawhub: openclaw plugins install clawhub: --acknowledge-clawhub-risk openclaw plugins update +openclaw plugins update --all ``` -Skill installs target the active workspace `skills/` directory by default. Add -`--global` to install into the shared managed skills directory. +Skill installs target the active workspace `skills/` directory by default; add +`--global` for the shared managed skills directory. Plugin installs need the +explicit `clawhub:` prefix to force ClawHub resolution over npm, git, or a +local path. Full flag reference: [`openclaw skills`](/cli/skills) and +[`openclaw plugins`](/cli/plugins). -OpenClaw checks the selected community ClawHub skill or plugin trust state -before downloading it. Versioned community skill and plugin releases use -exact-release trust metadata; resolver-backed GitHub skills rely on ClawHub's -install resolver to enforce scan and force-install policy before it returns a -pinned commit. Malicious or blocked community releases are refused. Risky -community releases require review and `--acknowledge-clawhub-risk` when a -non-interactive command should continue after that review. +### Release trust -Official ClawHub publishers/packages and bundled OpenClaw sources bypass this -release-trust prompt and security-verdict fetch during install and update. +OpenClaw checks a release's ClawHub trust state before downloading it, for +both skills and plugins. Versioned releases use exact-release trust metadata; +resolver-backed GitHub skills go through ClawHub's install resolver, which +enforces scan and force-install policy before returning a pinned commit. -Plugin installs use the `clawhub:` prefix when you want ClawHub resolution -instead of npm or another install source. +- **Malicious or blocked** releases are refused outright. +- **Risky** releases (non-clean scan, non-blocking moderation state) print a + warning and require `--acknowledge-clawhub-risk` to continue + non-interactively. +- **Official ClawHub publishers/packages and bundled OpenClaw sources** skip + the trust prompt and security-verdict fetch entirely. ## Publish and maintain -Install the standalone ClawHub CLI for publisher workflows: +Install the standalone CLI once, then log in: ```bash npm i -g clawhub clawhub login ``` -Publish plugin packages with `clawhub package publish`: +Publish a plugin package (folder path, GitHub repo `owner/repo[@ref]`, or +tarball URL) with `clawhub package publish`: ```bash -clawhub package publish your-org/your-plugin --dry-run -clawhub package publish your-org/your-plugin +clawhub package publish ./my-plugin --dry-run +clawhub package publish ./my-plugin clawhub package publish your-org/your-plugin@v1.0.0 ``` -Publish skill folders with `clawhub skill publish`: +Publish a skill folder with `clawhub skill publish`: ```bash clawhub skill publish ./skills/review-helper -clawhub skill publish ./skills/review-helper --version 1.0.0 +clawhub skill publish ./skills/review-helper --version 1.0.0 --owner your-org ``` -When local skill scan state or package ownership needs maintenance, use the -relevant standalone command: +Other maintenance commands: ```bash -clawhub sync --all -clawhub package transfer @old-owner/package --to new-owner +clawhub sync --all # scan local skills, publish new/updated ones +clawhub package transfer @old-owner/package --to new-owner # move a plugin package to another publisher +clawhub skill rename old-slug new-slug # rename a published skill, redirect the old slug +clawhub explore --sort trending # browse the registry, sorted by trending ``` ## Related diff --git a/docs/clawhub/publishing.md b/docs/clawhub/publishing.md index 82a66b1b8147..77a2f9a93920 100644 --- a/docs/clawhub/publishing.md +++ b/docs/clawhub/publishing.md @@ -9,61 +9,62 @@ read_when: # Publishing on ClawHub ClawHub publishing is owner-scoped: every publish targets a publisher, and the -server decides whether the signed-in user is allowed to publish there. +server decides whether the signed-in user can publish there. ## Owners An owner is a ClawHub publisher handle, such as `@alice` or `@openclaw`. -Personal owners are created for users. Org owners can have multiple members. +Every user gets a personal owner; org owners can have multiple members with +`owner`, `admin`, or `publisher` roles. -When you publish, you either use your personal owner or choose an org owner -where you have publisher access. +When you publish, you use your personal owner or an org owner where you have +publisher access. ## Skills -Skills are published from a skill folder. The public page is: +Skills publish from a skill folder (`clawhub skill publish `). The +public page is: ```text -https://clawhub.ai//skills/ +https://clawhub.ai// ``` Example: ```text -https://clawhub.ai/alice/skills/review-helper +https://clawhub.ai/alice/review-helper ``` The publish request includes the selected owner, slug, version, changelog, and -files. The server verifies that the actor can publish as that owner before it -creates the release. +files. The server verifies the actor can publish as that owner before creating +the release. ## Plugins -Plugins use npm-style package names. Scoped package names include the owner in -the first part of the name: +Plugins use npm-style package names (`clawhub package publish `). +Scoped names include the owner in the first path segment: ```text @owner/package-name ``` -The scope must match the selected publish owner. If your package is named -`@openclaw/dronzer`, it can only be published as `@openclaw`. If you publish as +The scope must match the selected publish owner. A package named +`@openclaw/dronzer` can only be published as `@openclaw`. To publish as `@vintageayu`, rename the package to `@vintageayu/dronzer`. -This prevents a package from claiming an org namespace that the publisher does -not control. +This stops a package from claiming an org namespace the publisher does not +control. -## Release Flow +## Release flow 1. The UI, CLI, or GitHub workflow gathers package metadata and files. -2. The publish request is sent to ClawHub with the selected owner. -3. The server validates owner permissions, package scope, package name, version, - file limits, and source metadata. +2. The publish request goes to ClawHub with the selected owner. +3. The server validates owner permissions, package scope, package name, + version, file limits, and source metadata. Validation failure means no + release is created. 4. ClawHub stores the release and starts automated security checks. -5. New releases are hidden from normal install/download surfaces until review - and verification finish. - -If validation fails, the release is not created. +5. The release stays hidden from normal install/download surfaces until + review and verification finish. ## FAQ @@ -77,20 +78,18 @@ Package scope "@openclaw" must match selected owner "@vintageayu". Publish as "@openclaw" or rename this package to "@vintageayu/dronzer". ``` -To fix it, either choose the owner named by the package scope, or rename the -package so the scope matches the owner you can publish as. +Fix it by either publishing as the owner named in the scope, or renaming the +package so its scope matches the owner you can publish as. -If the package name already has the right scope but the package is owned by the -wrong publisher, transfer ownership instead: +If the package already has the right scope but the wrong publisher owns it, +transfer it instead: ```sh clawhub package transfer @opik/opik-openclaw --to opik ``` -Use package transfer only when you have admin access to both the current package -owner and the destination publisher. It does not let you publish into a scope you -cannot manage. - -This protects org namespaces. A package named `@openclaw/dronzer` claims the -`@openclaw` namespace, so only publishers with access to the `@openclaw` owner -can publish it. +Package transfer needs admin access to both the current owner and the +destination publisher; it does not let you publish into a scope you do not +control. This is the same namespace protection: a package named +`@openclaw/dronzer` claims the `@openclaw` namespace, so only publishers with +access to `@openclaw` can publish or transfer into it. diff --git a/docs/cli/acp.md b/docs/cli/acp.md index 6aeda4fdeb20..3843a961b0b1 100644 --- a/docs/cli/acp.md +++ b/docs/cli/acp.md @@ -8,78 +8,49 @@ title: "ACP" Run the [Agent Client Protocol (ACP)](https://agentclientprotocol.com/) bridge that talks to an OpenClaw Gateway. -This command speaks ACP over stdio for IDEs and forwards prompts to the Gateway -over WebSocket. It keeps ACP sessions mapped to Gateway session keys. +`openclaw acp` speaks ACP over stdio for IDEs and forwards prompts to the Gateway over WebSocket, keeping ACP sessions mapped to Gateway session keys. It is a Gateway-backed ACP bridge, not a full ACP-native editor runtime: it focuses on session routing, prompt delivery, and streaming updates. -`openclaw acp` is a Gateway-backed ACP bridge, not a full ACP-native editor -runtime. It focuses on session routing, prompt delivery, and basic streaming -updates. - -If you want an external MCP client to talk directly to OpenClaw channel -conversations instead of hosting an ACP harness session, use -[`openclaw mcp serve`](/cli/mcp) instead. +If you want an external MCP client to talk directly to OpenClaw channel conversations instead of hosting an ACP harness session, use [`openclaw mcp serve`](/cli/mcp) instead. ## What this is not -This page is often confused with ACP harness sessions. +`openclaw acp` means OpenClaw acts as an ACP server: an IDE or ACP client connects to OpenClaw, and OpenClaw forwards that work into a Gateway session. -`openclaw acp` means: - -- OpenClaw acts as an ACP server -- an IDE or ACP client connects to OpenClaw -- OpenClaw forwards that work into a Gateway session - -This is different from [ACP Agents](/tools/acp-agents), where OpenClaw runs an -external harness such as Codex or Claude Code through `acpx`. +This is different from [ACP Agents](/tools/acp-agents), where OpenClaw runs an external harness such as Codex or Claude Code through `acpx`. Quick rule: - editor/client wants to talk ACP to OpenClaw: use `openclaw acp` - OpenClaw should launch Codex/Claude/Gemini as an ACP harness: use `/acp spawn` and [ACP Agents](/tools/acp-agents) -## Compatibility Matrix +## Compatibility matrix -| ACP area | Status | Notes | -| --------------------------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `initialize`, `newSession`, `prompt`, `cancel` | Implemented | Core bridge flow over stdio to Gateway chat/send + abort. | -| `listSessions`, slash commands | Implemented | Session list works against Gateway session state with bounded cursor pagination and `cwd` filtering where Gateway session rows carry workspace metadata; commands are advertised via `available_commands_update`. | -| Session lineage metadata | Implemented | Session listings and session info snapshots include OpenClaw parent and child lineage in `_meta` so ACP clients can render subagent graphs without private Gateway side channels. | -| `resumeSession`, `closeSession` | Implemented | Resume rebinds an ACP session to an existing Gateway session without replaying history. Close cancels active bridge work, resolves pending prompts as cancelled, and releases bridge session state. | -| `loadSession` | Partial | Rebinds the ACP session to a Gateway session key and replays ACP event-ledger history for bridge-created sessions. Older/no-ledger sessions fall back to stored user/assistant text. | -| Prompt content (`text`, embedded `resource`, images) | Partial | Text/resources are flattened into chat input; images become Gateway attachments. | -| Session modes | Partial | `session/set_mode` is supported and the bridge exposes initial Gateway-backed session controls for thought level, tool verbosity, reasoning, usage detail, and elevated actions. Broader ACP-native mode/config surfaces are still out of scope. | -| Session info and usage updates | Partial | The bridge emits `session_info_update` and best-effort `usage_update` notifications from cached Gateway session snapshots. Usage is approximate and only sent when Gateway token totals are marked fresh. | -| Tool streaming | Partial | `tool_call` / `tool_call_update` events include raw I/O, text content, and best-effort file locations when Gateway tool args/results expose them. Embedded terminals and richer diff-native output are still not exposed. | -| Exec approvals | Partial | Gateway exec approval prompts during active ACP prompt turns are relayed to the ACP client with `session/request_permission`. | -| Per-session MCP servers (`mcpServers`) | Unsupported | Bridge mode rejects per-session MCP server requests. Configure MCP on the OpenClaw gateway or agent instead. | -| Client filesystem methods (`fs/read_text_file`, `fs/write_text_file`) | Unsupported | The bridge does not call ACP client filesystem methods. | -| Client terminal methods (`terminal/*`) | Unsupported | The bridge does not create ACP client terminals or stream terminal ids through tool calls. | -| Session plans / thought streaming | Unsupported | The bridge currently emits output text and tool status, not ACP plan or thought updates. | +| ACP area | Status | Notes | +| --------------------------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `initialize`, `newSession`, `prompt`, `cancel` | Implemented | Core bridge flow over stdio to Gateway chat/send + abort. | +| `listSessions`, slash commands | Implemented | Session list works against Gateway session state with bounded cursor pagination and `cwd` filtering where Gateway session rows carry workspace metadata; commands are advertised via `available_commands_update`. | +| Session lineage metadata | Implemented | Session listings and session info snapshots include OpenClaw parent and child lineage in `_meta` so ACP clients can render subagent graphs without private Gateway side channels. | +| `resumeSession`, `closeSession` | Implemented | Resume rebinds an ACP session to an existing Gateway session without replaying history. Close cancels active bridge work, resolves pending prompts as cancelled, and releases bridge session state. | +| `loadSession` | Partial | Rebinds the ACP session to a Gateway session key and replays ACP event-ledger history for bridge-created sessions. Older/no-ledger sessions fall back to stored user/assistant text. | +| Prompt content (`text`, embedded `resource`, images) | Partial | Text/resources flatten into chat input; images become Gateway attachments. | +| Session modes | Partial | `session/set_mode` is supported; the bridge exposes Gateway-backed session controls for thought level, tool verbosity, reasoning, usage detail, and elevated actions. Broader ACP-native mode/config surfaces are still out of scope. | +| Thought streaming | Implemented | Model thinking content streams as `agent_thought_chunk` session updates. ACP-native session plans are not emitted. | +| Session info and usage updates | Partial | The bridge emits `session_info_update` and best-effort `usage_update` notifications from cached Gateway session snapshots. Usage is approximate and only sent when Gateway token totals are marked fresh. | +| Tool streaming | Partial | `tool_call`/`tool_call_update` events include raw I/O, text content, and best-effort file locations when Gateway tool args/results expose them. Embedded terminals and richer diff-native output are not exposed. | +| Exec approvals | Partial | Gateway exec approval prompts during active ACP prompt turns relay to the ACP client with `session/request_permission`. | +| Per-session MCP servers (`mcpServers`) | Unsupported | Bridge mode rejects per-session MCP server requests. Configure MCP on the OpenClaw Gateway or agent instead. | +| Client filesystem methods (`fs/read_text_file`, `fs/write_text_file`) | Unsupported | The bridge does not call ACP client filesystem methods. | +| Client terminal methods (`terminal/*`) | Unsupported | The bridge does not create ACP client terminals or stream terminal ids through tool calls. | -## Known Limitations +## Known limitations -- `loadSession` can replay complete ACP event-ledger history only for - bridge-created sessions. Older/no-ledger sessions still use transcript - fallback and do not reconstruct historic tool calls or system notices. -- If multiple ACP clients share the same Gateway session key, event and cancel - routing are best-effort rather than strictly isolated per client. Prefer the - default isolated `acp-bridge:` sessions when you need clean editor-local - turns. -- Gateway stop states are translated into ACP stop reasons, but that mapping is - less expressive than a fully ACP-native runtime. -- Initial session controls currently surface a focused subset of Gateway knobs: - thought level, tool verbosity, reasoning, usage detail, and elevated - actions. Model selection and exec-host controls are not yet exposed as ACP - config options. -- `session_info_update` and `usage_update` are derived from Gateway session - snapshots, not live ACP-native runtime accounting. Usage is approximate, - carries no cost data, and is only emitted when the Gateway marks total token - data as fresh. -- Tool follow-along data is best-effort. The bridge can surface file paths that - appear in known tool args/results, but it does not yet emit ACP terminals or - structured file diffs. -- Exec approval relay is scoped to the active ACP prompt turn; approvals from - other Gateway sessions are ignored. +- `loadSession` replays complete ACP event-ledger history only for bridge-created sessions. Older/no-ledger sessions use transcript fallback and do not reconstruct historic tool calls or system notices. +- If multiple ACP clients share the same Gateway session key, event and cancel routing are best-effort rather than strictly isolated per client. Prefer the default isolated `acp-bridge:` sessions when you need clean editor-local turns. +- Gateway stop states translate into ACP stop reasons, but that mapping is less expressive than a fully ACP-native runtime. +- Session controls surface a focused subset of Gateway knobs: thought level, tool verbosity, reasoning, usage detail, and elevated actions. Model selection and exec-host controls are not exposed as ACP config options. +- `session_info_update` and `usage_update` derive from Gateway session snapshots, not live ACP-native runtime accounting. Usage is approximate, carries no cost data, and is only emitted when the Gateway marks total token data as fresh. +- Tool follow-along data is best-effort: the bridge surfaces file paths that appear in known tool args/results, but does not emit ACP terminals or structured file diffs. +- Exec approval relay is scoped to the active ACP prompt turn; approvals from other Gateway sessions are ignored. ## Usage @@ -104,8 +75,7 @@ openclaw acp --session agent:main:main --reset-session ## ACP client (debug) -Use the built-in ACP client to sanity-check the bridge without an IDE. -It spawns the ACP bridge and lets you type prompts interactively. +Use the built-in ACP client to sanity-check the bridge without an IDE. It spawns the ACP bridge and lets you type prompts interactively. ```bash openclaw acp client @@ -119,21 +89,17 @@ openclaw acp client --server "node" --server-args openclaw.mjs acp --url ws://12 Permission model (client debug mode): -- Auto-approval is allowlist-based and only applies to trusted core tool IDs. +- Auto-approval is allowlist-based and applies only to trusted core tool IDs. - `read` auto-approval is scoped to the current working directory (`--cwd` when set). -- ACP only auto-approves narrow readonly classes: scoped `read` calls under the active cwd plus readonly search tools (`search`, `web_search`, `memory_search`). Unknown/non-core tools, out-of-scope reads, exec-capable tools, control-plane tools, mutating tools, and interactive flows always require explicit prompt approval. -- Server-provided `toolCall.kind` is treated as untrusted metadata (not an authorization source). +- ACP only auto-approves narrow readonly classes: scoped `read` calls under the active cwd, plus readonly search tools (`search`, `web_search`, `memory_search`). Unknown/non-core tools, out-of-scope reads, exec-capable tools, control-plane tools, mutating tools, and interactive flows always require explicit prompt approval. +- Server-provided `toolCall.kind` is treated as untrusted metadata, not an authorization source. - This ACP bridge policy is separate from ACPX harness permissions. If you run OpenClaw through the `acpx` backend, `plugins.entries.acpx.config.permissionMode=approve-all` is the break-glass "yolo" switch for that harness session. ## Protocol smoke testing -For protocol-level debugging, start a Gateway with isolated state and drive -`openclaw acp` over stdio with an ACP JSON-RPC client. Cover `initialize`, -`session/new`, `session/list` with an absolute `cwd`, `session/resume`, -`session/close`, duplicate close, and missing resume. +For protocol-level debugging, start a Gateway with isolated state and drive `openclaw acp` over stdio with an ACP JSON-RPC client. Cover `initialize`, `session/new`, `session/list` with an absolute `cwd`, `session/resume`, `session/close`, duplicate close, and missing resume. -The proof should include the advertised lifecycle capabilities, a Gateway-backed -session row, update notifications, and the Gateway `sessions.list` log: +The proof should include the advertised lifecycle capabilities, a Gateway-backed session row, update notifications, and the Gateway `sessions.list` log: ```json { @@ -165,14 +131,11 @@ session row, update notifications, and the Gateway `sessions.list` log: } ``` -Avoid using `openclaw gateway call sessions.list` as the only ACP proof. That -CLI path may request a fresh-token operator scope upgrade; ACP bridge -correctness is proven by ACP stdio frames plus the Gateway `sessions.list` log. +Avoid using `openclaw gateway call sessions.list` as the only ACP proof. That CLI path may request a fresh-token operator scope upgrade; ACP bridge correctness is proven by ACP stdio frames plus the Gateway `sessions.list` log. ## How to use this -Use ACP when an IDE (or other client) speaks Agent Client Protocol and you want -it to drive an OpenClaw Gateway session. +Use ACP when an IDE (or other client) speaks Agent Client Protocol and you want it to drive an OpenClaw Gateway session. 1. Ensure the Gateway is running (local or remote). 2. Configure the Gateway target (config or flags). @@ -195,9 +158,7 @@ openclaw acp --url wss://gateway-host:18789 --token-file ~/.openclaw/gateway.tok ## Selecting agents -ACP does not pick agents directly. It routes by the Gateway session key. - -Use agent-scoped session keys to target a specific agent: +ACP does not pick agents directly. It routes by the Gateway session key. Use agent-scoped session keys to target a specific agent: ```bash openclaw acp --session agent:main:main @@ -205,24 +166,15 @@ openclaw acp --session agent:design:main openclaw acp --session agent:qa:bug-123 ``` -Each ACP session maps to a single Gateway session key. One agent can have many -sessions; ACP defaults to an isolated `acp-bridge:` session unless you override -the key or label. +Each ACP session maps to a single Gateway session key. One agent can have many sessions; ACP defaults to an isolated `acp-bridge:` session unless you override the key or label. -Per-session `mcpServers` are not supported in bridge mode. If an ACP client -sends them during `newSession` or `loadSession`, the bridge returns a clear -error instead of silently ignoring them. +Per-session `mcpServers` are not supported in bridge mode. If an ACP client sends them during `newSession` or `loadSession`, the bridge returns a clear error instead of silently ignoring them. -If you want ACPX-backed sessions to see OpenClaw plugin tools or selected -built-in tools such as `cron`, enable the gateway-side ACPX MCP bridges instead -of trying to pass per-session `mcpServers`. See -[ACP Agents](/tools/acp-agents-setup#plugin-tools-mcp-bridge) and -[OpenClaw tools MCP bridge](/tools/acp-agents-setup#openclaw-tools-mcp-bridge). +If you want ACPX-backed sessions to see OpenClaw plugin tools or selected built-in tools such as `cron`, enable the gateway-side ACPX MCP bridges instead of trying to pass per-session `mcpServers`. See [ACP Agents](/tools/acp-agents-setup#plugin-tools-mcp-bridge) and [OpenClaw tools MCP bridge](/tools/acp-agents-setup#openclaw-tools-mcp-bridge). ## Use from `acpx` (Codex, Claude, other ACP clients) -If you want a coding agent such as Codex or Claude Code to talk to your -OpenClaw bot over ACP, use `acpx` with its built-in `openclaw` target. +If you want a coding agent such as Codex or Claude Code to talk to your OpenClaw bot over ACP, use `acpx` with its built-in `openclaw` target. Typical flow: @@ -242,8 +194,7 @@ acpx openclaw -s codex-bridge --cwd /path/to/repo \ "Ask my OpenClaw work agent for recent context relevant to this repo." ``` -If you want `acpx openclaw` to target a specific Gateway and session key every -time, override the `openclaw` agent command in `~/.acpx/config.json`: +If you want `acpx openclaw` to target a specific Gateway and session key every time, override the `openclaw` agent command in `~/.acpx/config.json`: ```json { @@ -255,15 +206,13 @@ time, override the `openclaw` agent command in `~/.acpx/config.json`: } ``` -For a repo-local OpenClaw checkout, use the direct CLI entrypoint instead of the -dev runner so the ACP stream stays clean. For example: +For a repo-local OpenClaw checkout, use the direct CLI entrypoint instead of the dev runner so the ACP stream stays clean: ```bash env OPENCLAW_HIDE_BANNER=1 OPENCLAW_SUPPRESS_NOTES=1 node openclaw.mjs acp ... ``` -This is the easiest way to let Codex, Claude Code, or another ACP-aware client -pull contextual information from an OpenClaw agent without scraping a terminal. +This is the easiest way to let Codex, Claude Code, or another ACP-aware client pull contextual information from an OpenClaw agent without scraping a terminal. ## Zed editor setup @@ -309,10 +258,7 @@ In Zed, open the Agent panel and select "OpenClaw ACP" to start a thread. ## Session mapping -By default, ACP bridge sessions get an isolated Gateway session key with an -`acp-bridge:` prefix. These normal-model bridge sessions are synthetic and -subject to stale-entry pruning and entry-count caps. To reuse a known session, -pass a session key or label: +By default, ACP bridge sessions get an isolated Gateway session key with an `acp-bridge:` prefix. These normal-model bridge sessions are synthetic and disposable: they are subject to stale-entry pruning and are not treated as protected human conversation surfaces. To reuse a known session, pass a session key or label: - `--session `: use a specific Gateway session key. - `--session-label