Compare commits

..

1 Commits

Author SHA1 Message Date
Peter Steinberger
751263d789 fix: improve parallels windows smoke logging 2026-04-02 15:11:08 +01:00
275 changed files with 3153 additions and 13152 deletions

0
.codex
View File

View File

@@ -4,94 +4,82 @@ Docs: https://docs.openclaw.ai
## Unreleased
### Changes
- Android/assistant: auto-send Google Assistant App Actions prompts once chat is healthy and idle, while keeping bare assistant launches as open-only. (#59721) Thanks @obviyus.
### Fixes
- Agents/tools: include value-shape hints in missing-parameter tool errors so dropped, empty-string, and wrong-type write payloads are easier to diagnose from logs. (#55317) Thanks @priyansh19.
- Plugins/browser: block SSRF redirect bypass by installing a real-time Playwright route handler before `page.goto()` so navigation to private/internal IPs is intercepted and aborted mid-redirect instead of checked post-hoc. (#58771) Thanks @pgondhi987.
- Android/assistant: keep queued App Actions prompts pending when auto-send enqueue is rejected, so transient chat-health drops do not silently lose the assistant request. Thanks @obviyus.
- Zalo/webhook: scope replay-dedupe cache key to path and account using `JSON.stringify` so multi-account deployments do not silently drop events due to cross-account cache poisoning. (#59387) Thanks @pgondhi987.
- Plugins/Google: separate OAuth CSRF state from PKCE code verifier during Gemini browser sign-in so state validation and token exchange use independent values. (#59116) Thanks @eleqtrizit.
- Exec/Windows: reject malformed drive-less rooted executable paths like `:\Users\...` so approval and allowlist candidate resolution no longer treat them as cwd-relative commands. (#58040) Thanks @SnowSky1.
- Exec/preflight: fail closed on complex interpreter invocations that would otherwise skip script-content validation, and correctly inspect quoted script paths before host execution. Thanks @pgondhi987.
## 2026.4.2-beta.1
### Breaking
- Plugins/xAI: move `x_search` settings from the legacy core `tools.web.x_search.*` path to the plugin-owned `plugins.entries.xai.config.xSearch.*` path, standardize `x_search` auth on `plugins.entries.xai.config.webSearch.apiKey` / `XAI_API_KEY`, and migrate legacy config with `openclaw doctor --fix`. (#59674) Thanks @vincentkoc.
- Plugins/web fetch: move Firecrawl `web_fetch` config from the legacy core `tools.web.fetch.firecrawl.*` path to the plugin-owned `plugins.entries.firecrawl.config.webFetch.*` path, route `web_fetch` fallback through the new fetch-provider boundary instead of a Firecrawl-only core branch, and migrate legacy config with `openclaw doctor --fix`. (#59465) Thanks @vincentkoc.
- Plugins/web fetch: move Firecrawl `web_fetch` config from the legacy core `tools.web.fetch.firecrawl.*` path to the plugin-owned `plugins.entries.firecrawl.config.webFetch.*` path, route `web_fetch` fallback through the new fetch-provider boundary instead of a Firecrawl-only core branch, and migrate legacy config with `openclaw doctor --fix`. Thanks @vincentkoc.
- Plugins/xAI: move `x_search` settings from the legacy core `tools.web.x_search.*` path to the plugin-owned `plugins.entries.xai.config.xSearch.*` path, standardize `x_search` auth on `plugins.entries.xai.config.webSearch.apiKey` / `XAI_API_KEY`, and migrate legacy config with `openclaw doctor --fix`. Thanks @vincentkoc.
### Changes
- Matrix/plugin: emit spec-compliant `m.mentions` metadata across text sends, media captions, edits, poll fallback text, and action-driven edits so Matrix mentions notify reliably in clients like Element. (#59323) Thanks @gumadeiras.
- Feishu/comments: add a dedicated Drive comment-event flow with comment-thread context resolution, in-thread replies, and `feishu_drive` comment actions for document collaboration workflows. (#58497) thanks @wittam-01.
- WhatsApp/reactions: add `reactionLevel` guidance for agent reactions. Thanks @mcaxtr.
- Diffs: add plugin-owned `viewerBaseUrl` so viewer links can use a stable proxy/public origin without passing `baseUrl` on every tool call. (#59341) Related #59227. Thanks @gumadeiras.
- Agents/compaction: add `agents.defaults.compaction.notifyUser` so the `🧹 Compacting context...` start notice is opt-in instead of always being shown. (#54251) Thanks @oguricap0327.
- Agents/compaction: resolve `agents.defaults.compaction.model` consistently for manual `/compact` and other context-engine compaction paths, so engine-owned compaction uses the configured override model across runtime entrypoints. (#56710) Thanks @oliviareid-svg
- Channels/session routing: move provider-specific session conversation grammar into plugin-owned session-key surfaces, preserving Telegram topic routing and Feishu scoped inheritance across bootstrap, model override, restart, and tool-policy paths.
- Plugins/hooks: add `before_agent_reply` so plugins can short-circuit the LLM with synthetic replies after inline actions. (#20067) Thanks @JoshuaLelon
- Providers/runtime: add provider-owned replay hook surfaces for transcript policy, replay cleanup, and reasoning-mode dispatch. (#59143) Thanks @jalehman.
- Tasks/TaskFlow: restore the core TaskFlow substrate with managed-vs-mirrored sync modes, durable flow state/revision tracking, and `openclaw flows` inspection/recovery primitives so background orchestration can persist and be operated separately from plugin authoring layers. (#58930) Thanks @mbelinky.
- Tasks/TaskFlow: add managed child task spawning plus sticky cancel intent, so external orchestrators can stop scheduling immediately and let parent TaskFlows settle to `cancelled` once active child tasks finish. (#59610) Thanks @mbelinky.
- Plugins/TaskFlow: add a bound `api.runtime.taskFlow` seam so plugins and trusted authoring layers can create and drive managed TaskFlows from host-resolved OpenClaw context without passing owner identifiers on each call. (#59622) Thanks @mbelinky.
- Android/assistant: add assistant-role entrypoints plus Google Assistant App Actions metadata so Android can launch OpenClaw from the assistant trigger and hand prompts into the chat composer. (#59596) Thanks @obviyus.
- Exec defaults: make gateway/node host exec default to YOLO mode by requesting `security=full` with `ask=off`, and align host approval-file fallbacks plus docs/doctor reporting with that no-prompt default.
- Providers/runtime: add provider-owned replay hook surfaces for transcript policy, replay cleanup, and reasoning-mode dispatch. (#59143) Thanks @jalehman.
- Plugins/hooks: add `before_agent_reply` so plugins can short-circuit the LLM with synthetic replies after inline actions. (#20067) Thanks @JoshuaLelon.
- Channels/session routing: move provider-specific session conversation grammar into plugin-owned session-key surfaces, preserving Telegram topic routing and Feishu scoped inheritance across bootstrap, model override, restart, and tool-policy paths.
- Feishu/comments: add a dedicated Drive comment-event flow with comment-thread context resolution, in-thread replies, and `feishu_drive` comment actions for document collaboration workflows. (#58497) Thanks @wittam-01.
- Matrix/plugin: emit spec-compliant `m.mentions` metadata across text sends, media captions, edits, poll fallback text, and action-driven edits so Matrix mentions notify reliably in clients like Element. (#59323) Thanks @gumadeiras.
- Diffs: add plugin-owned `viewerBaseUrl` so viewer links can use a stable proxy/public origin without passing `baseUrl` on every tool call. (#59341) Related #59227. Thanks @gumadeiras.
- Agents/compaction: resolve `agents.defaults.compaction.model` consistently for manual `/compact` and other context-engine compaction paths, so engine-owned compaction uses the configured override model across runtime entrypoints. (#56710) Thanks @oliviareid-svg.
- Agents/compaction: add `agents.defaults.compaction.notifyUser` so the `🧹 Compacting context...` start notice is opt-in instead of always being shown. (#54251) Thanks @oguricap0327.
- WhatsApp/reactions: add `reactionLevel` guidance for agent reactions. Thanks @mcaxtr.
- Exec approvals/channels: auto-enable DM-first native chat approvals when supported channels can infer approvers from existing owner config, while keeping channel fanout explicit and clarifying forwarding versus native approval client config.
### Fixes
- Providers/transport policy: centralize request auth, proxy, TLS, and header shaping across shared HTTP, stream, and websocket paths, block insecure TLS/runtime transport overrides, and keep proxy-hop TLS separate from target mTLS settings. (#59682) Thanks @vincentkoc.
- Providers/Copilot: classify native GitHub Copilot API hosts in the shared provider endpoint resolver and harden token-derived proxy endpoint parsing so Copilot base URL routing stays centralized and fails closed on malformed hints. (#59644) Thanks @vincentkoc.
- Providers/streaming headers: centralize default and attribution header merging across OpenAI websocket, embedded-runner, and proxy stream paths so provider-specific headers stay consistent and caller overrides only win where intended. (#59542) Thanks @vincentkoc.
- Providers/media HTTP: centralize base URL normalization, default auth/header injection, and explicit header override handling across shared OpenAI-compatible audio, Deepgram audio, Gemini media/image, and Moonshot video request paths. (#59469) Thanks @vincentkoc.
- Providers/OpenAI-compatible routing: centralize native-vs-proxy request policy so hidden attribution and related OpenAI-family defaults only apply on verified native endpoints across stream, websocket, and shared audio HTTP paths. (#59433) Thanks @vincentkoc.
- Providers/Anthropic routing: centralize native-vs-proxy endpoint classification for direct Anthropic `service_tier` handling so spoofed or proxied hosts do not inherit native Anthropic defaults. (#59608) Thanks @vincentkoc.
- Gateway/exec loopback: restore legacy-role fallback for empty paired-device token maps and allow silent local role upgrades so local exec and node clients stop failing with pairing-required errors after `2026.3.31`. (#59092) Thanks @openperf.
- Agents/subagents: pin admin-only subagent gateway calls to `operator.admin` while keeping `agent` at least privilege, so `sessions_spawn` no longer dies on loopback scope-upgrade pairing with `close(1008) "pairing required"`. (#59555) Thanks @openperf.
- Exec approvals/config: strip invalid `security`, `ask`, and `askFallback` values from `~/.openclaw/exec-approvals.json` during normalization so malformed policy enums fall back cleanly to the documented defaults instead of corrupting runtime policy resolution. (#59112) Thanks @openperf.
- Exec approvals/doctor: report host policy sources from the real approvals file path and ignore malformed host override values when attributing effective policy conflicts. (#59367) Thanks @gumadeiras.
- Slack/mrkdwn formatting: add built-in Slack mrkdwn guidance in inbound context so Slack replies stop falling back to generic Markdown patterns that render poorly in Slack. (#59100) Thanks @jadewon.
- WhatsApp/presence: send `unavailable` presence on connect in self-chat mode so personal-phone users stop losing all push notifications while the gateway is running. (#59410) Thanks @mcaxtr.
- WhatsApp/media: add HTML, XML, and CSS to the MIME map and fall back gracefully for unknown media types instead of dropping the attachment. (#51562) Thanks @bobbyt74.
- Matrix/onboarding: restore guided setup in `openclaw channels add` and `openclaw configure --section channels`, while keeping custom plugin wizards on the shared `setupWizard` seam. (#59462) Thanks @gumadeiras.
- Matrix/streaming: keep live partial previews for the current assistant block while preserving completed block updates as separate messages when `channels.matrix.blockStreaming` is enabled. (#59384) Thanks @gumadeiras.
- Matrix/streaming: keep live partial previews for the current assistant block while preserving completed block updates as separate messages when `channels.matrix.blockStreaming` is enabled. (#59384) thanks @gumadeiras
- Feishu/comment threads: harden document comment-thread delivery so whole-document comments fall back to `add_comment`, delayed reply lookups retry more reliably, and user-visible replies avoid reasoning/planning spillover. (#59129) Thanks @wittam-01.
- MS Teams/streaming: strip already-streamed text from fallback block delivery when replies exceed the 4000-character streaming limit so long responses stop duplicating content. (#59297) Thanks @bradgroux.
- Slack/thread context: filter thread starter and history by the effective conversation allowlist without dropping valid open-room, DM, or group DM context. (#58380) Thanks @jacobtomlinson.
- Mattermost/probes: route status probes through the SSRF guard and honor `allowPrivateNetwork` so connectivity checks stay safe for self-hosted Mattermost deployments. (#58529) Thanks @mappel-nv.
- Zalo/webhook replay: scope replay dedupe key by chat and sender so reused message IDs across different chats or senders no longer collide, and harden metadata reads for partially missing payloads. (#58444)
- QQBot/structured payloads: restrict local file paths to QQ Bot-owned media storage, block traversal outside that root, reduce path leakage in logs, and keep inline image data URLs working. (#58453) Thanks @jacobtomlinson.
- Image generation/providers: route OpenAI, MiniMax, and fal image requests through the shared provider HTTP transport path so custom base URLs, guarded private-network routing, and provider request defaults stay aligned with the rest of provider HTTP. Thanks @vincentkoc.
- Image generation/providers: stop inferring private-network access from configured OpenAI, MiniMax, and fal image base URLs, and cap shared HTTP error-body reads so hostile or misconfigured endpoints fail closed without relaxing SSRF policy or buffering unbounded error payloads. Thanks @vincentkoc.
- Browser/host inspection: keep static Chrome inspection helpers out of the activated browser runtime so `openclaw doctor browser` and related checks do not eagerly load the bundled browser plugin. (#59471) Thanks @vincentkoc.
- Browser/CDP: normalize trailing-dot localhost absolute-form hosts before loopback checks so remote CDP websocket URLs like `ws://localhost.:...` rewrite back to the configured remote host. (#59236) Thanks @mappel-nv.
- Gateway/exec loopback: restore legacy-role fallback for empty paired-device token maps and allow silent local role upgrades so local exec and node clients stop failing with pairing-required errors after `2026.3.31`. (#59092) Thanks @openperf.
- Agents/output sanitization: strip namespaced `antml:thinking` blocks from user-visible text so Anthropic-style internal monologue tags do not leak into replies. (#59550) Thanks @obviyus.
- Kimi Coding/tools: normalize Anthropic tool payloads into the OpenAI-compatible function shape Kimi Coding expects so tool calls stop losing required arguments. (#59440) Thanks @obviyus.
- Image tool/paths: resolve relative local media paths against the agent `workspaceDir` instead of `process.cwd()` so inputs like `inbox/receipt.png` pass the local-path allowlist reliably. (#57222) Thanks Priyansh Gupta.
- Browser/CDP: normalize trailing-dot localhost absolute-form hosts before loopback checks so remote CDP websocket URLs like `ws://localhost.:...` rewrite back to the configured remote host. (#59236) Thanks @mappel-nv.
- Browser/host inspection: keep static Chrome inspection helpers out of the activated browser runtime so `openclaw doctor browser` and related checks do not eagerly load the bundled browser plugin. (#59471) Thanks @vincentkoc.
- Podman/launch: remove noisy container output from `scripts/run-openclaw-podman.sh` and align the Podman install guidance with the quieter startup flow. (#59368) Thanks @sallyom.
- Plugins/runtime: keep LINE reply directives and browser-backed cleanup/reset flows working even when those plugins are disabled while tightening bundled plugin activation guards. (#59412) Thanks @vincentkoc.
- Providers/OpenAI-compatible routing: centralize native-vs-proxy request policy so hidden attribution and related OpenAI-family defaults only apply on verified native endpoints across stream, websocket, and shared audio HTTP paths. (#59433) Thanks @vincentkoc.
- Providers/media HTTP: centralize base URL normalization, default auth/header injection, and explicit header override handling across shared OpenAI-compatible audio, Deepgram audio, Gemini media/image, and Moonshot video request paths. (#59469) Thanks @vincentkoc.
- Providers/streaming headers: centralize default and attribution header merging across OpenAI websocket, embedded-runner, and proxy stream paths so provider-specific headers stay consistent and caller overrides only win where intended. (#59542) Thanks @vincentkoc.
- Providers/Anthropic routing: centralize native-vs-proxy endpoint classification for direct Anthropic `service_tier` handling so spoofed or proxied hosts do not inherit native Anthropic defaults. (#59608) Thanks @vincentkoc.
- Providers/Copilot: classify native GitHub Copilot API hosts in the shared provider endpoint resolver and harden token-derived proxy endpoint parsing so Copilot base URL routing stays centralized and fails closed on malformed hints. Thanks @vincentkoc.
- ACP/gateway reconnects: keep ACP prompts alive across transient websocket drops while still failing boundedly when reconnect recovery does not complete. (#59473) Thanks @obviyus.
- ACP/gateway reconnects: reject stale pre-ack ACP prompts after reconnect grace expiry so callers fail cleanly instead of hanging indefinitely when the gateway never confirms the run.
- Exec approvals/doctor: report host policy sources from the real approvals file path and ignore malformed host override values when attributing effective policy conflicts. (#59367) Thanks @gumadeiras.
- Exec approvals/config: strip invalid `security`, `ask`, and `askFallback` values from `~/.openclaw/exec-approvals.json` during normalization so malformed policy enums fall back cleanly to the documented defaults instead of corrupting runtime policy resolution. (#59112) Thanks @openperf.
- Gateway/session kill: enforce HTTP operator scopes on session kill requests and gate authorization before session lookup so unauthenticated callers cannot probe session existence. (#59128) Thanks @jacobtomlinson.
- MS Teams/logging: format non-`Error` failures with the shared unknown-error helper so logs stop collapsing caught SDK or Axios objects into `[object Object]`. (#59321) Thanks @bradgroux.
- Channels/setup: ignore untrusted workspace channel plugins during setup resolution so a shadowing workspace plugin cannot override built-in channel setup/login flows unless explicitly trusted in config. (#59158) Thanks @mappel-nv.
- Exec/Windows: restore allowlist enforcement with quote-aware `argPattern` matching across gateway and node exec, and surface accurate dynamic pre-approved executable hints in the exec tool description. (#56285) Thanks @kpngr.
- Gateway: prune empty `node-pending-work` state entries after explicit acknowledgments and natural expiry so the per-node state map no longer grows indefinitely. (#58179) Thanks @gavyngong.
- Webhooks/secret comparison: replace ad-hoc timing-safe secret comparisons across BlueBubbles, Feishu, Mattermost, Telegram, Twilio, and Zalo webhook handlers with the shared `safeEqualSecret` helper and reject empty auth tokens in BlueBubbles. (#58432) Thanks @eleqtrizit.
- OpenShell/mirror: constrain `remoteWorkspaceDir` and `remoteAgentWorkspaceDir` to the managed `/sandbox` and `/agent` roots, and keep mirror sync from overwriting or removing user-added shell roots during config synchronization. (#58515) Thanks @eleqtrizit.
- Plugins/activation: preserve explicit, auto-enabled, and default activation provenance plus reason metadata across CLI, gateway bootstrap, and status surfaces so plugin enablement state stays accurate after auto-enable resolution. (#59641) Thanks @vincentkoc.
- Exec/env: block additional host environment override pivots for package roots, language runtimes, compiler include paths, and credential/config locations so request-scoped exec cannot redirect trusted toolchains or config lookups. (#59233) Thanks @drobison00.
- Dotenv/workspace overrides: block workspace `.env` files from overriding `OPENCLAW_PINNED_PYTHON` and `OPENCLAW_PINNED_WRITE_PYTHON` so trusted helper interpreters cannot be redirected by repo-local env injection. (#58473) Thanks @eleqtrizit.
- Plugins/install: accept JSON5 syntax in `openclaw.plugin.json` and bundle `plugin.json` manifests during install/validation, so third-party plugins with trailing commas, comments, or unquoted keys no longer fail to install. (#59084) Thanks @singleGanghood.
- Telegram/exec approvals: rewrite shared `/approve … allow-always` callback payloads to `/approve … always` before Telegram button rendering so plugin approval IDs still fit Telegram's `callback_data` limit and keep the Allow Always action visible. (#59217) Thanks @jameslcowan.
- Cron/exec timeouts: surface timed-out `exec` and `bash` failures in isolated cron runs even when `verbose: off`, including custom session-target cron jobs, so scheduled runs stop failing silently. (#58247) Thanks @skainguyen1412.
- Telegram/exec approvals: fall back to the origin session key for async approval followups and keep resume-failure status delivery sanitized so Telegram followups still land without leaking raw exec metadata. (#59351) Thanks @seonang.
- Node-host/exec approvals: bind `pnpm dlx` invocations through the approval planner's mutable-script path so the effective runtime command is resolved for approval instead of being left unbound. (#58374)
- Webhooks/secret comparison: replace ad-hoc timing-safe secret comparisons across BlueBubbles, Feishu, Mattermost, Telegram, Twilio, and Zalo webhook handlers with the shared `safeEqualSecret` helper and reject empty auth tokens in BlueBubbles. Thanks @eleqtrizit.
- OpenShell/mirror sync: constrain mirror sync to managed roots only so user-added shell roots are no longer overwritten or removed during config synchronization. (#58515) Thanks @eleqtrizit.
## 2026.4.1-beta.1
- Plugins/runtime: stop ambient core helper and setup paths from loading non-selected bundled plugins, keep channel-setup snapshot scoping safe for custom channel plugins, and honor env-scoped plugin auth paths. (#59136) Thanks @vincentkoc.
- Matrix/multi-account: keep room-level `account` scoping, inherited room overrides, and implicit account selection consistent across top-level default auth, named accounts, and cached-credential env setups. (#58449) thanks @Daanvdplas and @gumadeiras.
- Gateway/pairing: prefer explicit QR bootstrap auth over earlier Tailscale auth classification so iOS `/pair qr` silent bootstrap pairing does not fall through to `pairing required`. (#59232) Thanks @ngutman.
- Config/Discord: coerce safe integer numeric Discord IDs to strings during config validation, keep unsafe or precision-losing numeric snowflakes rejected, and align `openclaw doctor` repair guidance with the same fail-closed behavior. (#45125) Thanks @moliendocode.
- Gateway/sessions: scope bare `sessions.create` aliases like `main` to the requested agent while preserving the canonical `global` and `unknown` sentinel keys. (#58207) thanks @jalehman.
- `/context detail` now compares the tracked prompt estimate with cached context usage and surfaces untracked provider/runtime overhead when present. (#28391) thanks @ImLukeF.
- Gateway/session reset: emit the typed `before_reset` hook for gateway `/new` and `/reset`, preserving reset-hook behavior even when the previous transcript has already been archived. (#53872) thanks @VACInc
- Plugins/commands: pass the active host `sessionKey` into plugin command contexts, and include `sessionId` when it is already available from the active session entry, so bundled and third-party commands can resolve the current conversation reliably. (#59044) Thanks @jalehman.
- Agents/auth: honor `models.providers.*.authHeader` for pi embedded runner model requests by injecting `Authorization: Bearer <apiKey>` when requested. (#54390) Thanks @lndyzwdxhs.
- UI/compaction: keep the compaction indicator in a retry-pending state until the run actually finishes, so the UI does not show `Context compacted` before compaction actually finishes. (#55132) Thanks @mpz4life.
- Cron/tool schemas: keep cron tool schemas strict-model-friendly while still preserving `failureAlert=false`, nullable `agentId`/`sessionKey`, and flattened add/update recovery for the newly exposed cron job fields. (#55043) Thanks @brunolorente.
- BlueBubbles/config: accept `enrichGroupParticipantsFromContacts` in the core strict config schema so gateways no longer fail validation or startup when the BlueBubbles plugin writes that field. (#56889) Thanks @zqchris.
- Agents/failover: classify AbortError and stream-abort messages as timeout so Ollama NDJSON stream aborts stop showing `reason=unknown` in model fallback logs. (#58324) Thanks @yelog
- Exec approvals: route Slack, Discord, and Telegram approvals through the shared channel approval-capability path so native approval auth, delivery, and `/approve` handling stay aligned across channels while preserving Telegram session-key agent filtering. (#58634) thanks @gumadeiras
- Matrix/runtime: resolve the verification/bootstrap runtime from a distinct packaged Matrix entry so global npm installs stop failing on crypto bootstrap with missing-module or recursive runtime alias errors. (#59249) Thanks @gumadeiras.
- Matrix/streaming: preserve ordered block flushes before tool, message, and agent boundaries, add explicit `channels.matrix.blockStreaming` opt-in so Matrix `streaming: "off"` stays final-only by default, and move MiniMax plain-text final handling into the MiniMax provider runtime instead of the shared core heuristic. (#59266) thanks @gumadeiras
- Agents/compaction: resolve compaction wait before final reply/channel flush completion so slow end-of-run delivery drains no longer delay compaction completion. (#59308) thanks @gumadeiras
- Exec approvals: align approval UX, effective-policy reporting, and `allow-always` availability with the host policy so CLI, doctor, and approval surfaces explain the real host-effective decision path. (#59283) Thanks @gumadeiras.
## 2026.4.2
@@ -123,27 +111,6 @@ Docs: https://docs.openclaw.ai
- Telegram/local Bot API: preserve media MIME types for absolute-path downloads so local audio files still trigger transcription and other MIME-based handling. (#54603) Thanks @jzakirov
- Channels/WhatsApp: pass inbound message timestamp to model context so the AI can see when WhatsApp messages were sent. (#58590) Thanks @Maninae
- QQBot/voice: lazy-load `silk-wasm` in `audio-convert.ts` so qqbot still starts when the optional voice dependency is missing, while voice encode/decode degrades gracefully instead of crashing at module load time. (#58829) Thanks @WideLee.
## 2026.4.1
- Plugins/runtime: stop ambient core helper and setup paths from loading non-selected bundled plugins, keep channel-setup snapshot scoping safe for custom channel plugins, and honor env-scoped plugin auth paths. (#59136) Thanks @vincentkoc.
- Matrix/multi-account: keep room-level `account` scoping, inherited room overrides, and implicit account selection consistent across top-level default auth, named accounts, and cached-credential env setups. (#58449) thanks @Daanvdplas and @gumadeiras.
- Gateway/pairing: prefer explicit QR bootstrap auth over earlier Tailscale auth classification so iOS `/pair qr` silent bootstrap pairing does not fall through to `pairing required`. (#59232) Thanks @ngutman.
- Config/Discord: coerce safe integer numeric Discord IDs to strings during config validation, keep unsafe or precision-losing numeric snowflakes rejected, and align `openclaw doctor` repair guidance with the same fail-closed behavior. (#45125) Thanks @moliendocode.
- Gateway/sessions: scope bare `sessions.create` aliases like `main` to the requested agent while preserving the canonical `global` and `unknown` sentinel keys. (#58207) thanks @jalehman.
- `/context detail` now compares the tracked prompt estimate with cached context usage and surfaces untracked provider/runtime overhead when present. (#28391) thanks @ImLukeF.
- Gateway/session reset: emit the typed `before_reset` hook for gateway `/new` and `/reset`, preserving reset-hook behavior even when the previous transcript has already been archived. (#53872) thanks @VACInc
- Plugins/commands: pass the active host `sessionKey` into plugin command contexts, and include `sessionId` when it is already available from the active session entry, so bundled and third-party commands can resolve the current conversation reliably. (#59044) Thanks @jalehman.
- Agents/auth: honor `models.providers.*.authHeader` for pi embedded runner model requests by injecting `Authorization: Bearer <apiKey>` when requested. (#54390) Thanks @lndyzwdxhs.
- UI/compaction: keep the compaction indicator in a retry-pending state until the run actually finishes, so the UI does not show `Context compacted` before compaction actually finishes. (#55132) Thanks @mpz4life.
- Cron/tool schemas: keep cron tool schemas strict-model-friendly while still preserving `failureAlert=false`, nullable `agentId`/`sessionKey`, and flattened add/update recovery for the newly exposed cron job fields. (#55043) Thanks @brunolorente.
- BlueBubbles/config: accept `enrichGroupParticipantsFromContacts` in the core strict config schema so gateways no longer fail validation or startup when the BlueBubbles plugin writes that field. (#56889) Thanks @zqchris.
- Agents/failover: classify AbortError and stream-abort messages as timeout so Ollama NDJSON stream aborts stop showing `reason=unknown` in model fallback logs. (#58324) Thanks @yelog
- Exec approvals: route Slack, Discord, and Telegram approvals through the shared channel approval-capability path so native approval auth, delivery, and `/approve` handling stay aligned across channels while preserving Telegram session-key agent filtering. (#58634) thanks @gumadeiras
- Matrix/runtime: resolve the verification/bootstrap runtime from a distinct packaged Matrix entry so global npm installs stop failing on crypto bootstrap with missing-module or recursive runtime alias errors. (#59249) Thanks @gumadeiras.
- Matrix/streaming: preserve ordered block flushes before tool, message, and agent boundaries, add explicit `channels.matrix.blockStreaming` opt-in so Matrix `streaming: "off"` stays final-only by default, and move MiniMax plain-text final handling into the MiniMax provider runtime instead of the shared core heuristic. (#59266) thanks @gumadeiras
- Agents/compaction: resolve compaction wait before final reply/channel flush completion so slow end-of-run delivery drains no longer delay compaction completion. (#59308) thanks @gumadeiras
- Exec approvals: align approval UX, effective-policy reporting, and `allow-always` availability with the host policy so CLI, doctor, and approval surfaces explain the real host-effective decision path. (#59283) Thanks @gumadeiras.
- Config/Telegram: migrate removed `channels.telegram.groupMentionsOnly` into `channels.telegram.groups["*"].requireMention` on load so legacy configs no longer crash at startup. (#55336) thanks @jameslcowan.
- Ollama/model picker: show only Ollama models after provider selection in the CLI picker. (#55290) Thanks @Luckymingxuan.
- MiniMax/plugins: auto-enable the bundled MiniMax plugin for API-key auth/config so MiniMax image generation and other plugin-owned capabilities load without manual plugin allowlisting. (#57127) Thanks @tars90percent.
@@ -229,7 +196,6 @@ Docs: https://docs.openclaw.ai
- Slack: stop retry-driven duplicate replies when draft-finalization edits fail ambiguously, and log configured allowlisted users/channels by readable name instead of raw IDs.
- Agents/OpenAI Responses: normalize raw bundled MCP tool schemas on the WebSocket/Responses path so bare-object, object-ish, and top-level union MCP tools no longer get rejected by OpenAI during tool registration. (#58299) Thanks @yelog.
- ACP/security: replace ACP's dangerous-tool name override with semantic approval classes, so only narrow readonly reads/searches can auto-approve while indirect exec-capable and control-plane tools always require explicit prompt approval. Thanks @vincentkoc.
- ACP: derive owner-only approval classes from the shared tool-policy fallback map so `cron`, `nodes`, and `whatsapp_login` cannot drift out of prompt-required coverage.
- ACP/sessions_spawn: register ACP child runs for completion tracking and lifecycle cleanup, and make registration-failure cleanup explicitly best-effort so callers do not assume an already-started ACP turn was fully aborted. (#40885) Thanks @xaeon2026 and @vincentkoc.
- ACP/tasks: mark cleanly exited ACP runs as blocked when they end on deterministic write or authorization blockers, and wake the parent session with a follow-up instead of falsely reporting success.
- ACPX/runtime: derive the bundled ACPX expected version from the extension package metadata instead of hardcoding a separate literal, so plugin-local ACPX installs stop drifting out of health-check parity after version bumps. (#49089) Thanks @jiejiesks and @vincentkoc.

View File

@@ -65,8 +65,8 @@ android {
applicationId = "ai.openclaw.app"
minSdk = 31
targetSdk = 36
versionCode = 2026040201
versionName = "2026.4.2-beta.1"
versionCode = 2026040101
versionName = "2026.4.2"
ndk {
// Support all major ABIs — native libs are tiny (~47 KB per ABI)
abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")

View File

@@ -76,17 +76,10 @@
android:exported="true"
android:windowSoftInputMode="adjustResize"
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|uiMode|density|keyboard|keyboardHidden|navigation">
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.ASSIST" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -1,43 +0,0 @@
package ai.openclaw.app
import android.content.Intent
const val actionAskOpenClaw = "ai.openclaw.app.action.ASK_OPENCLAW"
const val extraAssistantPrompt = "prompt"
enum class HomeDestination {
Connect,
Chat,
Voice,
Screen,
Settings,
}
data class AssistantLaunchRequest(
val source: String,
val prompt: String?,
val autoSend: Boolean,
)
fun parseAssistantLaunchIntent(intent: Intent?): AssistantLaunchRequest? {
val action = intent?.action ?: return null
return when (action) {
Intent.ACTION_ASSIST ->
AssistantLaunchRequest(
source = "assist",
prompt = null,
autoSend = false,
)
actionAskOpenClaw -> {
val prompt = intent.getStringExtra(extraAssistantPrompt)?.trim()?.ifEmpty { null }
AssistantLaunchRequest(
source = "app_action",
prompt = prompt,
autoSend = prompt != null,
)
}
else -> null
}
}

View File

@@ -23,7 +23,6 @@ class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
handleAssistantIntent(intent)
WindowCompat.setDecorFitsSystemWindows(window, false)
permissionRequester = PermissionRequester(this)
@@ -71,15 +70,4 @@ class MainActivity : ComponentActivity() {
viewModel.setForeground(false)
super.onStop()
}
override fun onNewIntent(intent: android.content.Intent) {
super.onNewIntent(intent)
setIntent(intent)
handleAssistantIntent(intent)
}
private fun handleAssistantIntent(intent: android.content.Intent?) {
val request = parseAssistantLaunchIntent(intent) ?: return
viewModel.handleAssistantLaunch(request)
}
}

View File

@@ -27,12 +27,6 @@ class MainViewModel(app: Application) : AndroidViewModel(app) {
private val prefs = nodeApp.prefs
private val runtimeRef = MutableStateFlow<NodeRuntime?>(null)
private var foreground = true
private val _requestedHomeDestination = MutableStateFlow<HomeDestination?>(null)
val requestedHomeDestination: StateFlow<HomeDestination?> = _requestedHomeDestination
private val _chatDraft = MutableStateFlow<String?>(null)
val chatDraft: StateFlow<String?> = _chatDraft
private val _pendingAssistantAutoSend = MutableStateFlow<String?>(null)
val pendingAssistantAutoSend: StateFlow<String?> = _pendingAssistantAutoSend
private fun ensureRuntime(): NodeRuntime {
runtimeRef.value?.let { return it }
@@ -252,29 +246,6 @@ class MainViewModel(app: Application) : AndroidViewModel(app) {
ensureRuntime().setVoiceScreenActive(active)
}
fun handleAssistantLaunch(request: AssistantLaunchRequest) {
_requestedHomeDestination.value = HomeDestination.Chat
if (request.autoSend) {
_pendingAssistantAutoSend.value = request.prompt
_chatDraft.value = null
return
}
_pendingAssistantAutoSend.value = null
_chatDraft.value = request.prompt
}
fun clearRequestedHomeDestination() {
_requestedHomeDestination.value = null
}
fun clearChatDraft() {
_chatDraft.value = null
}
fun clearPendingAssistantAutoSend() {
_pendingAssistantAutoSend.value = null
}
fun setMicEnabled(enabled: Boolean) {
ensureRuntime().setMicEnabled(enabled)
}
@@ -366,16 +337,4 @@ class MainViewModel(app: Application) : AndroidViewModel(app) {
fun sendChat(message: String, thinking: String, attachments: List<OutgoingAttachment>) {
ensureRuntime().sendChat(message = message, thinking = thinking, attachments = attachments)
}
suspend fun sendChatAwaitAcceptance(
message: String,
thinking: String,
attachments: List<OutgoingAttachment>,
): Boolean {
return ensureRuntime().sendChatAwaitAcceptance(
message = message,
thinking = thinking,
attachments = attachments,
)
}
}

View File

@@ -1016,14 +1016,6 @@ class NodeRuntime(
chat.sendMessage(message = message, thinkingLevel = thinking, attachments = attachments)
}
suspend fun sendChatAwaitAcceptance(
message: String,
thinking: String,
attachments: List<OutgoingAttachment>,
): Boolean {
return chat.sendMessageAwaitAcceptance(message = message, thinkingLevel = thinking, attachments = attachments)
}
private fun handleGatewayEvent(event: String, payloadJson: String?) {
micCapture.handleGatewayEvent(event, payloadJson)
talkMode.handleGatewayEvent(event, payloadJson)

View File

@@ -130,25 +130,11 @@ class ChatController(
thinkingLevel: String,
attachments: List<OutgoingAttachment>,
) {
scope.launch {
sendMessageAwaitAcceptance(
message = message,
thinkingLevel = thinkingLevel,
attachments = attachments,
)
}
}
suspend fun sendMessageAwaitAcceptance(
message: String,
thinkingLevel: String,
attachments: List<OutgoingAttachment>,
): Boolean {
val trimmed = message.trim()
if (trimmed.isEmpty() && attachments.isEmpty()) return false
if (trimmed.isEmpty() && attachments.isEmpty()) return
if (!_healthOk.value) {
_errorText.value = "Gateway health not OK; cannot send"
return false
return
}
val runId = UUID.randomUUID().toString()
@@ -191,45 +177,45 @@ class ChatController(
pendingToolCallsById.clear()
publishPendingToolCalls()
return try {
val params =
buildJsonObject {
put("sessionKey", JsonPrimitive(sessionKey))
put("message", JsonPrimitive(text))
put("thinking", JsonPrimitive(thinking))
put("timeoutMs", JsonPrimitive(30_000))
put("idempotencyKey", JsonPrimitive(runId))
if (attachments.isNotEmpty()) {
put(
"attachments",
JsonArray(
attachments.map { att ->
buildJsonObject {
put("type", JsonPrimitive(att.type))
put("mimeType", JsonPrimitive(att.mimeType))
put("fileName", JsonPrimitive(att.fileName))
put("content", JsonPrimitive(att.base64))
}
},
),
)
scope.launch {
try {
val params =
buildJsonObject {
put("sessionKey", JsonPrimitive(sessionKey))
put("message", JsonPrimitive(text))
put("thinking", JsonPrimitive(thinking))
put("timeoutMs", JsonPrimitive(30_000))
put("idempotencyKey", JsonPrimitive(runId))
if (attachments.isNotEmpty()) {
put(
"attachments",
JsonArray(
attachments.map { att ->
buildJsonObject {
put("type", JsonPrimitive(att.type))
put("mimeType", JsonPrimitive(att.mimeType))
put("fileName", JsonPrimitive(att.fileName))
put("content", JsonPrimitive(att.base64))
}
},
),
)
}
}
val res = session.request("chat.send", params.toString())
val actualRunId = parseRunId(res) ?: runId
if (actualRunId != runId) {
clearPendingRun(runId)
armPendingRunTimeout(actualRunId)
synchronized(pendingRuns) {
pendingRuns.add(actualRunId)
_pendingRunCount.value = pendingRuns.size
}
}
val res = session.request("chat.send", params.toString())
val actualRunId = parseRunId(res) ?: runId
if (actualRunId != runId) {
} catch (err: Throwable) {
clearPendingRun(runId)
armPendingRunTimeout(actualRunId)
synchronized(pendingRuns) {
pendingRuns.add(actualRunId)
_pendingRunCount.value = pendingRuns.size
}
_errorText.value = err.message
}
true
} catch (err: Throwable) {
clearPendingRun(runId)
_errorText.value = err.message
false
}
}

View File

@@ -46,7 +46,6 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import ai.openclaw.app.HomeDestination
import ai.openclaw.app.MainViewModel
private enum class HomeTab(
@@ -73,20 +72,6 @@ fun PostOnboardingTabs(viewModel: MainViewModel, modifier: Modifier = Modifier)
var activeTab by rememberSaveable { mutableStateOf(HomeTab.Connect) }
var chatTabStarted by rememberSaveable { mutableStateOf(false) }
var screenTabStarted by rememberSaveable { mutableStateOf(false) }
val requestedHomeDestination by viewModel.requestedHomeDestination.collectAsState()
LaunchedEffect(requestedHomeDestination) {
val destination = requestedHomeDestination ?: return@LaunchedEffect
activeTab =
when (destination) {
HomeDestination.Connect -> HomeTab.Connect
HomeDestination.Chat -> HomeTab.Chat
HomeDestination.Voice -> HomeTab.Voice
HomeDestination.Screen -> HomeTab.Screen
HomeDestination.Settings -> HomeTab.Settings
}
viewModel.clearRequestedHomeDestination()
}
// Stop TTS when user navigates away from voice tab, and lazily keep the Chat/Screen tabs
// alive after the first visit so repeated tab switches do not rebuild their UI trees.

View File

@@ -9,7 +9,6 @@ import android.hardware.SensorManager
import android.net.Uri
import android.os.Build
import android.provider.Settings
import android.app.role.RoleManager
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.background
@@ -151,8 +150,6 @@ fun SettingsSheet(viewModel: MainViewModel) {
versionName
}
}
var assistantRoleAvailable by remember(context) { mutableStateOf(isAssistantRoleAvailable(context)) }
var assistantRoleHeld by remember(context) { mutableStateOf(isAssistantRoleHeld(context)) }
val listItemColors =
ListItemDefaults.colors(
containerColor = Color.Transparent,
@@ -329,12 +326,6 @@ fun SettingsSheet(viewModel: MainViewModel) {
viewModel.refreshGatewayConnection()
}
val assistantRoleLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
assistantRoleAvailable = isAssistantRoleAvailable(context)
assistantRoleHeld = isAssistantRoleHeld(context)
}
DisposableEffect(lifecycleOwner, context) {
val observer =
LifecycleEventObserver { _, event ->
@@ -371,8 +362,6 @@ fun SettingsSheet(viewModel: MainViewModel) {
||
ContextCompat.checkSelfPermission(context, Manifest.permission.READ_SMS) ==
PackageManager.PERMISSION_GRANTED
assistantRoleAvailable = isAssistantRoleAvailable(context)
assistantRoleHeld = isAssistantRoleHeld(context)
}
}
lifecycleOwner.lifecycle.addObserver(observer)
@@ -489,42 +478,6 @@ fun SettingsSheet(viewModel: MainViewModel) {
color = mobileTextTertiary,
)
}
if (assistantRoleAvailable) {
HorizontalDivider(color = mobileBorder)
ListItem(
modifier = Modifier.fillMaxWidth(),
colors = listItemColors,
headlineContent = { Text("Default Assistant", style = mobileHeadline) },
supportingContent = {
Text(
if (assistantRoleHeld) {
"OpenClaw is registered as the device assistant."
} else {
"Let Android launch OpenClaw from the assistant gesture. Google Assistant App Actions still work separately."
},
style = mobileCallout,
)
},
trailingContent = {
Button(
onClick = {
assistantRoleLauncher.launch(
context
.getSystemService(RoleManager::class.java)
.createRequestRoleIntent(RoleManager.ROLE_ASSISTANT),
)
},
colors = settingsPrimaryButtonColors(),
shape = RoundedCornerShape(14.dp),
) {
Text(
if (assistantRoleHeld) "Manage" else "Enable",
style = mobileCallout.copy(fontWeight = FontWeight.Bold),
)
}
},
)
}
}
}
@@ -1341,11 +1294,3 @@ private fun hasMotionCapabilities(context: Context): Boolean {
return sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) != null ||
sensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER) != null
}
private fun isAssistantRoleAvailable(context: Context): Boolean {
return context.getSystemService(RoleManager::class.java).isRoleAvailable(RoleManager.ROLE_ASSISTANT)
}
private fun isAssistantRoleHeld(context: Context): Boolean {
return context.getSystemService(RoleManager::class.java).isRoleHeld(RoleManager.ROLE_ASSISTANT)
}

View File

@@ -33,7 +33,6 @@ import androidx.compose.material3.OutlinedTextFieldDefaults
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -60,45 +59,12 @@ import ai.openclaw.app.ui.mobileText
import ai.openclaw.app.ui.mobileTextSecondary
import ai.openclaw.app.ui.mobileTextTertiary
internal data class DraftApplication(
val input: String,
val lastAppliedDraft: String?,
val consumed: Boolean,
)
internal fun applyDraftText(
draftText: String?,
currentInput: String,
lastAppliedDraft: String?,
): DraftApplication {
val draft =
draftText?.trim()?.ifEmpty { null } ?: return DraftApplication(
input = currentInput,
lastAppliedDraft = null,
consumed = false,
)
if (draft == lastAppliedDraft) {
return DraftApplication(
input = currentInput,
lastAppliedDraft = lastAppliedDraft,
consumed = false,
)
}
return DraftApplication(
input = draft,
lastAppliedDraft = draft,
consumed = true,
)
}
@Composable
fun ChatComposer(
draftText: String?,
healthOk: Boolean,
thinkingLevel: String,
pendingRunCount: Int,
attachments: List<PendingImageAttachment>,
onDraftApplied: () -> Unit,
onPickImages: () -> Unit,
onRemoveAttachment: (id: String) -> Unit,
onSetThinkingLevel: (level: String) -> Unit,
@@ -107,18 +73,8 @@ fun ChatComposer(
onSend: (text: String) -> Unit,
) {
var input by rememberSaveable { mutableStateOf("") }
var lastAppliedDraft by rememberSaveable { mutableStateOf<String?>(null) }
var showThinkingMenu by remember { mutableStateOf(false) }
LaunchedEffect(draftText) {
val next = applyDraftText(draftText = draftText, currentInput = input, lastAppliedDraft = lastAppliedDraft)
input = next.input
lastAppliedDraft = next.lastAppliedDraft
if (next.consumed) {
onDraftApplied()
}
}
val canSend = pendingRunCount == 0 && (input.trim().isNotEmpty() || attachments.isNotEmpty()) && healthOk
val sendBusy = pendingRunCount > 0

View File

@@ -48,31 +48,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
internal fun resolvePendingAssistantAutoSend(
pendingPrompt: String?,
healthOk: Boolean,
pendingRunCount: Int,
): String? {
val prompt = pendingPrompt?.trim()?.ifEmpty { null } ?: return null
if (!healthOk || pendingRunCount > 0) return null
return prompt
}
internal suspend fun dispatchPendingAssistantAutoSend(
pendingPrompt: String?,
healthOk: Boolean,
pendingRunCount: Int,
dispatch: suspend (String) -> Boolean,
): Boolean {
val prompt =
resolvePendingAssistantAutoSend(
pendingPrompt = pendingPrompt,
healthOk = healthOk,
pendingRunCount = pendingRunCount,
) ?: return false
return dispatch(prompt)
}
@Composable
fun ChatSheetContent(viewModel: MainViewModel) {
val messages by viewModel.chatMessages.collectAsState()
@@ -85,30 +60,11 @@ fun ChatSheetContent(viewModel: MainViewModel) {
val streamingAssistantText by viewModel.chatStreamingAssistantText.collectAsState()
val pendingToolCalls by viewModel.chatPendingToolCalls.collectAsState()
val sessions by viewModel.chatSessions.collectAsState()
val chatDraft by viewModel.chatDraft.collectAsState()
val pendingAssistantAutoSend by viewModel.pendingAssistantAutoSend.collectAsState()
LaunchedEffect(Unit) {
viewModel.loadChat(mainSessionKey)
}
LaunchedEffect(pendingAssistantAutoSend, healthOk, pendingRunCount, thinkingLevel) {
val accepted =
dispatchPendingAssistantAutoSend(
pendingPrompt = pendingAssistantAutoSend,
healthOk = healthOk,
pendingRunCount = pendingRunCount,
) { prompt ->
viewModel.sendChatAwaitAcceptance(
message = prompt,
thinking = thinkingLevel,
attachments = emptyList(),
)
}
if (!accepted) return@LaunchedEffect
viewModel.clearPendingAssistantAutoSend()
}
val context = LocalContext.current
val resolver = context.contentResolver
val scope = rememberCoroutineScope()
@@ -162,12 +118,10 @@ fun ChatSheetContent(viewModel: MainViewModel) {
Row(modifier = Modifier.fillMaxWidth().imePadding()) {
ChatComposer(
draftText = chatDraft,
healthOk = healthOk,
thinkingLevel = thinkingLevel,
pendingRunCount = pendingRunCount,
attachments = attachments,
onDraftApplied = viewModel::clearChatDraft,
onPickImages = { pickImages.launch("image/*") },
onRemoveAttachment = { id -> attachments.removeAll { it.id == id } },
onSetThinkingLevel = { level -> viewModel.setChatThinkingLevel(level) },

View File

@@ -1,7 +0,0 @@
<resources>
<string-array name="ask_openclaw_query_patterns">
<item>ask OpenClaw $prompt</item>
<item>tell OpenClaw to $prompt</item>
<item>open OpenClaw and ask $prompt</item>
</string-array>
</resources>

View File

@@ -1,17 +0,0 @@
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<capability
android:name="custom.actions.intent.ASK_OPENCLAW"
app:queryPatterns="@array/ask_openclaw_query_patterns">
<intent
android:action="ai.openclaw.app.action.ASK_OPENCLAW"
android:targetPackage="ai.openclaw.app"
android:targetClass="ai.openclaw.app.MainActivity">
<parameter
android:name="prompt"
android:key="prompt"
android:mimeType="text/*"
android:required="true" />
</intent>
</capability>
</shortcuts>

View File

@@ -1,43 +0,0 @@
package ai.openclaw.app
import android.content.Intent
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [34])
class AssistantLaunchTest {
@Test
fun parsesAssistGestureIntent() {
val parsed = parseAssistantLaunchIntent(Intent(Intent.ACTION_ASSIST))
requireNotNull(parsed)
assertEquals("assist", parsed.source)
assertNull(parsed.prompt)
assertFalse(parsed.autoSend)
}
@Test
fun parsesAppActionPrompt() {
val parsed =
parseAssistantLaunchIntent(
Intent(actionAskOpenClaw).putExtra(extraAssistantPrompt, " summarize my unread texts "),
)
requireNotNull(parsed)
assertEquals("app_action", parsed.source)
assertEquals("summarize my unread texts", parsed.prompt)
assertTrue(parsed.autoSend)
}
@Test
fun ignoresUnrelatedIntents() {
assertNull(parseAssistantLaunchIntent(Intent(Intent.ACTION_VIEW)))
}
}

View File

@@ -1,44 +0,0 @@
package ai.openclaw.app.ui.chat
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
class ChatComposerDraftTest {
@Test
fun clearsLastAppliedDraftWhenViewModelDraftResets() {
val consumed =
applyDraftText(
draftText = "repeat this",
currentInput = "",
lastAppliedDraft = null,
)
assertTrue(consumed.consumed)
assertEquals("repeat this", consumed.input)
assertEquals("repeat this", consumed.lastAppliedDraft)
val cleared =
applyDraftText(
draftText = null,
currentInput = consumed.input,
lastAppliedDraft = consumed.lastAppliedDraft,
)
assertFalse(cleared.consumed)
assertEquals("repeat this", cleared.input)
assertEquals(null, cleared.lastAppliedDraft)
val repeated =
applyDraftText(
draftText = "repeat this",
currentInput = cleared.input,
lastAppliedDraft = cleared.lastAppliedDraft,
)
assertTrue(repeated.consumed)
assertEquals("repeat this", repeated.input)
assertEquals("repeat this", repeated.lastAppliedDraft)
}
}

View File

@@ -1,72 +0,0 @@
package ai.openclaw.app.ui.chat
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import kotlinx.coroutines.runBlocking
class ChatSheetContentTest {
@Test
fun resolvesPendingAssistantAutoSendOnlyWhenChatIsReady() {
assertNull(
resolvePendingAssistantAutoSend(
pendingPrompt = "summarize mail",
healthOk = false,
pendingRunCount = 0,
),
)
assertNull(
resolvePendingAssistantAutoSend(
pendingPrompt = "summarize mail",
healthOk = true,
pendingRunCount = 1,
),
)
assertEquals(
"summarize mail",
resolvePendingAssistantAutoSend(
pendingPrompt = " summarize mail ",
healthOk = true,
pendingRunCount = 0,
),
)
}
@Test
fun keepsPendingAssistantAutoSendWhenDispatchRejected() = runBlocking {
var dispatchedPrompt: String? = null
val consumed =
dispatchPendingAssistantAutoSend(
pendingPrompt = "summarize mail",
healthOk = true,
pendingRunCount = 0,
) { prompt ->
dispatchedPrompt = prompt
false
}
assertFalse(consumed)
assertEquals("summarize mail", dispatchedPrompt)
}
@Test
fun clearsPendingAssistantAutoSendOnlyAfterAcceptedDispatch() = runBlocking {
var dispatchedPrompt: String? = null
val consumed =
dispatchPendingAssistantAutoSend(
pendingPrompt = "summarize mail",
healthOk = true,
pendingRunCount = 0,
) { prompt ->
dispatchedPrompt = prompt
true
}
assertTrue(consumed)
assertEquals("summarize mail", dispatchedPrompt)
}
}

View File

@@ -1,8 +1,8 @@
// Shared iOS version defaults.
// Generated overrides live in build/Version.xcconfig (git-ignored).
OPENCLAW_GATEWAY_VERSION = 2026.4.2-beta.1
OPENCLAW_GATEWAY_VERSION = 2026.4.2
OPENCLAW_MARKETING_VERSION = 2026.4.2
OPENCLAW_BUILD_VERSION = 2026040201
OPENCLAW_BUILD_VERSION = 2026040101
#include? "../build/Version.xcconfig"

View File

@@ -15,9 +15,9 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>2026.4.2-beta.1</string>
<string>2026.4.2</string>
<key>CFBundleVersion</key>
<string>2026040201</string>
<string>2026040101</string>
<key>CFBundleIconFile</key>
<string>OpenClaw</string>
<key>CFBundleURLTypes</key>

View File

@@ -10213,7 +10213,10 @@
{
"path": "channels.discord.accounts.*.allowFrom.*",
"kind": "channel",
"type": "string",
"type": [
"number",
"string"
],
"required": false,
"deprecated": false,
"sensitive": false,
@@ -10463,7 +10466,10 @@
{
"path": "channels.discord.accounts.*.dm.allowFrom.*",
"kind": "channel",
"type": "string",
"type": [
"number",
"string"
],
"required": false,
"deprecated": false,
"sensitive": false,
@@ -10493,7 +10499,10 @@
{
"path": "channels.discord.accounts.*.dm.groupChannels.*",
"kind": "channel",
"type": "string",
"type": [
"number",
"string"
],
"required": false,
"deprecated": false,
"sensitive": false,
@@ -10715,7 +10724,10 @@
{
"path": "channels.discord.accounts.*.execApprovals.approvers.*",
"kind": "channel",
"type": "string",
"type": [
"number",
"string"
],
"required": false,
"deprecated": false,
"sensitive": false,
@@ -10939,7 +10951,10 @@
{
"path": "channels.discord.accounts.*.guilds.*.channels.*.roles.*",
"kind": "channel",
"type": "string",
"type": [
"number",
"string"
],
"required": false,
"deprecated": false,
"sensitive": false,
@@ -11139,7 +11154,10 @@
{
"path": "channels.discord.accounts.*.guilds.*.channels.*.users.*",
"kind": "channel",
"type": "string",
"type": [
"number",
"string"
],
"required": false,
"deprecated": false,
"sensitive": false,
@@ -11195,7 +11213,10 @@
{
"path": "channels.discord.accounts.*.guilds.*.roles.*",
"kind": "channel",
"type": "string",
"type": [
"number",
"string"
],
"required": false,
"deprecated": false,
"sensitive": false,
@@ -11375,7 +11396,10 @@
{
"path": "channels.discord.accounts.*.guilds.*.users.*",
"kind": "channel",
"type": "string",
"type": [
"number",
"string"
],
"required": false,
"deprecated": false,
"sensitive": false,
@@ -12615,7 +12639,10 @@
{
"path": "channels.discord.allowFrom.*",
"kind": "channel",
"type": "string",
"type": [
"number",
"string"
],
"required": false,
"deprecated": false,
"sensitive": false,
@@ -12923,7 +12950,10 @@
{
"path": "channels.discord.dm.allowFrom.*",
"kind": "channel",
"type": "string",
"type": [
"number",
"string"
],
"required": false,
"deprecated": false,
"sensitive": false,
@@ -12953,7 +12983,10 @@
{
"path": "channels.discord.dm.groupChannels.*",
"kind": "channel",
"type": "string",
"type": [
"number",
"string"
],
"required": false,
"deprecated": false,
"sensitive": false,
@@ -13221,7 +13254,10 @@
{
"path": "channels.discord.execApprovals.approvers.*",
"kind": "channel",
"type": "string",
"type": [
"number",
"string"
],
"required": false,
"deprecated": false,
"sensitive": false,
@@ -13445,7 +13481,10 @@
{
"path": "channels.discord.guilds.*.channels.*.roles.*",
"kind": "channel",
"type": "string",
"type": [
"number",
"string"
],
"required": false,
"deprecated": false,
"sensitive": false,
@@ -13645,7 +13684,10 @@
{
"path": "channels.discord.guilds.*.channels.*.users.*",
"kind": "channel",
"type": "string",
"type": [
"number",
"string"
],
"required": false,
"deprecated": false,
"sensitive": false,
@@ -13701,7 +13743,10 @@
{
"path": "channels.discord.guilds.*.roles.*",
"kind": "channel",
"type": "string",
"type": [
"number",
"string"
],
"required": false,
"deprecated": false,
"sensitive": false,
@@ -13881,7 +13926,10 @@
{
"path": "channels.discord.guilds.*.users.*",
"kind": "channel",
"type": "string",
"type": [
"number",
"string"
],
"required": false,
"deprecated": false,
"sensitive": false,
@@ -22230,16 +22278,6 @@
"tags": [],
"hasChildren": false
},
{
"path": "channels.matrix.blockStreaming",
"kind": "channel",
"type": "boolean",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [],
"hasChildren": false
},
{
"path": "channels.matrix.chunkMode",
"kind": "channel",
@@ -22436,16 +22474,6 @@
"tags": [],
"hasChildren": true
},
{
"path": "channels.matrix.groups.*.account",
"kind": "channel",
"type": "string",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [],
"hasChildren": false
},
{
"path": "channels.matrix.groups.*.allow",
"kind": "channel",
@@ -22815,16 +22843,6 @@
"tags": [],
"hasChildren": true
},
{
"path": "channels.matrix.rooms.*.account",
"kind": "channel",
"type": "string",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [],
"hasChildren": false
},
{
"path": "channels.matrix.rooms.*.allow",
"kind": "channel",
@@ -31477,7 +31495,7 @@
"network"
],
"label": "Slack Exec Approvals",
"help": "Slack-native exec approval routing and approver authorization. When unset, OpenClaw auto-enables DM-first native approvals if approvers can be resolved for this workspace account.",
"help": "Slack-native exec approval routing and approver authorization. Enable this only when Slack should act as an explicit exec-approval client for the selected workspace account.",
"hasChildren": true
},
{
@@ -31545,7 +31563,7 @@
"network"
],
"label": "Slack Exec Approvals Enabled",
"help": "Controls Slack native exec approvals for this account: unset or \"auto\" enables DM-first native approvals when approvers can be resolved, true forces native approvals on, and false disables them.",
"help": "Enable Slack exec approvals for this account. When false or unset, Slack messages/buttons cannot approve exec requests.",
"hasChildren": false
},
{
@@ -35444,7 +35462,7 @@
"network"
],
"label": "Telegram Exec Approvals",
"help": "Telegram-native exec approval routing and approver authorization. When unset, OpenClaw auto-enables DM-first native approvals if approvers can be resolved for the selected bot account.",
"help": "Telegram-native exec approval routing and approver authorization. Enable this only when Telegram should act as an explicit exec-approval client for the selected bot account.",
"hasChildren": true
},
{
@@ -35512,7 +35530,7 @@
"network"
],
"label": "Telegram Exec Approvals Enabled",
"help": "Controls Telegram native exec approvals for this account: unset or \"auto\" enables DM-first native approvals when approvers can be resolved, true forces native approvals on, and false disables them.",
"help": "Enable Telegram exec approvals for this account. When false or unset, Telegram messages/buttons cannot approve exec requests.",
"hasChildren": false
},
{
@@ -67320,6 +67338,55 @@
"tags": [],
"hasChildren": true
},
{
"path": "tools.web.x_search.apiKey",
"kind": "core",
"type": [
"object",
"string"
],
"required": false,
"deprecated": false,
"sensitive": true,
"tags": [
"auth",
"security",
"tools"
],
"label": "xAI API Key",
"help": "xAI API key for X search (fallback: XAI_API_KEY env var).",
"hasChildren": true
},
{
"path": "tools.web.x_search.apiKey.id",
"kind": "core",
"type": "string",
"required": true,
"deprecated": false,
"sensitive": false,
"tags": [],
"hasChildren": false
},
{
"path": "tools.web.x_search.apiKey.provider",
"kind": "core",
"type": "string",
"required": true,
"deprecated": false,
"sensitive": false,
"tags": [],
"hasChildren": false
},
{
"path": "tools.web.x_search.apiKey.source",
"kind": "core",
"type": "string",
"required": true,
"deprecated": false,
"sensitive": false,
"tags": [],
"hasChildren": false
},
{
"path": "tools.web.x_search.cacheTtlMinutes",
"kind": "core",
@@ -67327,7 +67394,13 @@
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [],
"tags": [
"performance",
"storage",
"tools"
],
"label": "X Search Cache TTL (min)",
"help": "Cache TTL in minutes for x_search results.",
"hasChildren": false
},
{
@@ -67337,7 +67410,11 @@
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [],
"tags": [
"tools"
],
"label": "Enable X Search Tool",
"help": "Enable the x_search tool (requires XAI_API_KEY or tools.web.x_search.apiKey).",
"hasChildren": false
},
{
@@ -67347,7 +67424,11 @@
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [],
"tags": [
"tools"
],
"label": "X Search Inline Citations",
"help": "Keep inline citations from xAI in x_search responses when available (default: false).",
"hasChildren": false
},
{
@@ -67357,7 +67438,12 @@
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [],
"tags": [
"performance",
"tools"
],
"label": "X Search Max Turns",
"help": "Optional max internal search/tool turns xAI may use per x_search request. Omit to let xAI choose.",
"hasChildren": false
},
{
@@ -67367,7 +67453,12 @@
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [],
"tags": [
"models",
"tools"
],
"label": "X Search Model",
"help": "Model to use for X search (default: \"grok-4-1-fast-non-reasoning\").",
"hasChildren": false
},
{
@@ -67377,7 +67468,12 @@
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [],
"tags": [
"performance",
"tools"
],
"label": "X Search Timeout (sec)",
"help": "Timeout in seconds for x_search requests.",
"hasChildren": false
},
{

View File

@@ -1,4 +1,4 @@
{"generatedBy":"scripts/generate-config-doc-baseline.ts","recordType":"meta","totalPaths":5780}
{"generatedBy":"scripts/generate-config-doc-baseline.ts","recordType":"meta","totalPaths":5781}
{"recordType":"path","path":"acp","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"ACP","help":"ACP runtime controls for enabling dispatch, selecting backends, constraining allowed agent targets, and tuning streamed turn projection behavior.","hasChildren":true}
{"recordType":"path","path":"acp.allowedAgents","kind":"core","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"ACP Allowed Agents","help":"Allowlist of ACP target agent ids permitted for ACP runtime sessions. Empty means no additional allowlist restriction.","hasChildren":true}
{"recordType":"path","path":"acp.allowedAgents.*","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
@@ -896,7 +896,7 @@
{"recordType":"path","path":"channels.discord.accounts.*.agentComponents.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.allowBots","kind":"channel","type":["boolean","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.allowFrom","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.accounts.*.allowFrom.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.allowFrom.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.autoPresence","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.accounts.*.autoPresence.degradedText","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.autoPresence.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
@@ -920,10 +920,10 @@
{"recordType":"path","path":"channels.discord.accounts.*.defaultTo","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.dm","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.accounts.*.dm.allowFrom","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.accounts.*.dm.allowFrom.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.dm.allowFrom.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.dm.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.dm.groupChannels","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.accounts.*.dm.groupChannels.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.dm.groupChannels.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.dm.groupEnabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.dm.policy","kind":"channel","type":"string","required":false,"enumValues":["pairing","allowlist","open","disabled"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.dmHistoryLimit","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
@@ -944,7 +944,7 @@
{"recordType":"path","path":"channels.discord.accounts.*.execApprovals.agentFilter","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.accounts.*.execApprovals.agentFilter.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.execApprovals.approvers","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.accounts.*.execApprovals.approvers.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.execApprovals.approvers.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.execApprovals.cleanupAfterResolve","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.execApprovals.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.execApprovals.sessionFilter","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
@@ -964,7 +964,7 @@
{"recordType":"path","path":"channels.discord.accounts.*.guilds.*.channels.*.includeThreadStarter","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.guilds.*.channels.*.requireMention","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.guilds.*.channels.*.roles","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.accounts.*.guilds.*.channels.*.roles.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.guilds.*.channels.*.roles.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.guilds.*.channels.*.skills","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.accounts.*.guilds.*.channels.*.skills.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.guilds.*.channels.*.systemPrompt","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
@@ -984,12 +984,12 @@
{"recordType":"path","path":"channels.discord.accounts.*.guilds.*.channels.*.toolsBySender.*.deny","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.accounts.*.guilds.*.channels.*.toolsBySender.*.deny.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.guilds.*.channels.*.users","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.accounts.*.guilds.*.channels.*.users.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.guilds.*.channels.*.users.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.guilds.*.ignoreOtherMentions","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.guilds.*.reactionNotifications","kind":"channel","type":"string","required":false,"enumValues":["off","own","all","allowlist"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.guilds.*.requireMention","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.guilds.*.roles","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.accounts.*.guilds.*.roles.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.guilds.*.roles.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.guilds.*.slug","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.guilds.*.tools","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.accounts.*.guilds.*.tools.allow","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
@@ -1007,7 +1007,7 @@
{"recordType":"path","path":"channels.discord.accounts.*.guilds.*.toolsBySender.*.deny","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.accounts.*.guilds.*.toolsBySender.*.deny.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.guilds.*.users","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.accounts.*.guilds.*.users.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.guilds.*.users.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.healthMonitor","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.accounts.*.healthMonitor.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.heartbeat","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
@@ -1121,7 +1121,7 @@
{"recordType":"path","path":"channels.discord.agentComponents.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.allowBots","kind":"channel","type":["boolean","string"],"required":false,"deprecated":false,"sensitive":false,"tags":["access","channels","network"],"label":"Discord Allow Bot Messages","help":"Allow bot-authored messages to trigger Discord replies (default: false). Set \"mentions\" to only accept bot messages that mention the bot.","hasChildren":false}
{"recordType":"path","path":"channels.discord.allowFrom","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.allowFrom.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.allowFrom.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.autoPresence","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.autoPresence.degradedText","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"Discord Auto Presence Degraded Text","help":"Optional custom status text while runtime/model availability is degraded or unknown (idle).","hasChildren":false}
{"recordType":"path","path":"channels.discord.autoPresence.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"Discord Auto Presence Enabled","help":"Enable automatic Discord bot presence updates based on runtime/model availability signals. When enabled: healthy=>online, degraded/unknown=>idle, exhausted/unavailable=>dnd.","hasChildren":false}
@@ -1146,10 +1146,10 @@
{"recordType":"path","path":"channels.discord.defaultTo","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.dm","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.dm.allowFrom","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.dm.allowFrom.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.dm.allowFrom.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.dm.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.dm.groupChannels","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.dm.groupChannels.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.dm.groupChannels.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.dm.groupEnabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.dm.policy","kind":"channel","type":"string","required":false,"enumValues":["pairing","allowlist","open","disabled"],"deprecated":false,"sensitive":false,"tags":["access","channels","network"],"label":"Discord DM Policy","help":"Direct message access control (\"pairing\" recommended). \"open\" requires channels.discord.allowFrom=[\"*\"] (legacy: channels.discord.dm.allowFrom).","hasChildren":false}
{"recordType":"path","path":"channels.discord.dmHistoryLimit","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
@@ -1170,7 +1170,7 @@
{"recordType":"path","path":"channels.discord.execApprovals.agentFilter","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.execApprovals.agentFilter.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.execApprovals.approvers","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.execApprovals.approvers.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.execApprovals.approvers.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.execApprovals.cleanupAfterResolve","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.execApprovals.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.execApprovals.sessionFilter","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
@@ -1190,7 +1190,7 @@
{"recordType":"path","path":"channels.discord.guilds.*.channels.*.includeThreadStarter","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.guilds.*.channels.*.requireMention","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.guilds.*.channels.*.roles","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.guilds.*.channels.*.roles.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.guilds.*.channels.*.roles.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.guilds.*.channels.*.skills","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.guilds.*.channels.*.skills.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.guilds.*.channels.*.systemPrompt","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
@@ -1210,12 +1210,12 @@
{"recordType":"path","path":"channels.discord.guilds.*.channels.*.toolsBySender.*.deny","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.guilds.*.channels.*.toolsBySender.*.deny.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.guilds.*.channels.*.users","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.guilds.*.channels.*.users.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.guilds.*.channels.*.users.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.guilds.*.ignoreOtherMentions","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.guilds.*.reactionNotifications","kind":"channel","type":"string","required":false,"enumValues":["off","own","all","allowlist"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.guilds.*.requireMention","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.guilds.*.roles","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.guilds.*.roles.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.guilds.*.roles.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.guilds.*.slug","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.guilds.*.tools","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.guilds.*.tools.allow","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
@@ -1233,7 +1233,7 @@
{"recordType":"path","path":"channels.discord.guilds.*.toolsBySender.*.deny","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.guilds.*.toolsBySender.*.deny.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.guilds.*.users","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.guilds.*.users.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.guilds.*.users.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.healthMonitor","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.healthMonitor.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.heartbeat","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
@@ -1982,7 +1982,6 @@
{"recordType":"path","path":"channels.matrix.autoJoinAllowlist","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.matrix.autoJoinAllowlist.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.matrix.avatarUrl","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.matrix.blockStreaming","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.matrix.chunkMode","kind":"channel","type":"string","required":false,"enumValues":["length","newline"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.matrix.defaultAccount","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.matrix.deviceId","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
@@ -2000,7 +1999,6 @@
{"recordType":"path","path":"channels.matrix.groupPolicy","kind":"channel","type":"string","required":false,"enumValues":["open","disabled","allowlist"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.matrix.groups","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.matrix.groups.*","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.matrix.groups.*.account","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.matrix.groups.*.allow","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.matrix.groups.*.allowBots","kind":"channel","type":["boolean","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.matrix.groups.*.autoReply","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
@@ -2035,7 +2033,6 @@
{"recordType":"path","path":"channels.matrix.responsePrefix","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.matrix.rooms","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.matrix.rooms.*","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.matrix.rooms.*.account","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.matrix.rooms.*.allow","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.matrix.rooms.*.allowBots","kind":"channel","type":["boolean","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.matrix.rooms.*.autoReply","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
@@ -2830,12 +2827,12 @@
{"recordType":"path","path":"channels.slack.dms.*","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.slack.dms.*.historyLimit","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.slack.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.slack.execApprovals","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"Slack Exec Approvals","help":"Slack-native exec approval routing and approver authorization. When unset, OpenClaw auto-enables DM-first native approvals if approvers can be resolved for this workspace account.","hasChildren":true}
{"recordType":"path","path":"channels.slack.execApprovals","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"Slack Exec Approvals","help":"Slack-native exec approval routing and approver authorization. Enable this only when Slack should act as an explicit exec-approval client for the selected workspace account.","hasChildren":true}
{"recordType":"path","path":"channels.slack.execApprovals.agentFilter","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"Slack Exec Approval Agent Filter","help":"Optional allowlist of agent IDs eligible for Slack exec approvals, for example `[\"main\", \"ops-agent\"]`. Use this to keep approval prompts scoped to the agents you actually operate from Slack.","hasChildren":true}
{"recordType":"path","path":"channels.slack.execApprovals.agentFilter.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.slack.execApprovals.approvers","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"Slack Exec Approval Approvers","help":"Slack user IDs allowed to approve exec requests for this workspace account. Use Slack user IDs or user targets such as `U123`, `user:U123`, or `<@U123>`. If you leave this unset, OpenClaw falls back to commands.ownerAllowFrom when possible.","hasChildren":true}
{"recordType":"path","path":"channels.slack.execApprovals.approvers.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.slack.execApprovals.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"Slack Exec Approvals Enabled","help":"Controls Slack native exec approvals for this account: unset or \"auto\" enables DM-first native approvals when approvers can be resolved, true forces native approvals on, and false disables them.","hasChildren":false}
{"recordType":"path","path":"channels.slack.execApprovals.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"Slack Exec Approvals Enabled","help":"Enable Slack exec approvals for this account. When false or unset, Slack messages/buttons cannot approve exec requests.","hasChildren":false}
{"recordType":"path","path":"channels.slack.execApprovals.sessionFilter","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network","storage"],"label":"Slack Exec Approval Session Filter","help":"Optional session-key filters matched as substring or regex-style patterns before Slack approval routing is used. Use narrow patterns so Slack approvals only appear for intended sessions.","hasChildren":true}
{"recordType":"path","path":"channels.slack.execApprovals.sessionFilter.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.slack.execApprovals.target","kind":"channel","type":"string","required":false,"enumValues":["dm","channel","both"],"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"Slack Exec Approval Target","help":"Controls where Slack approval prompts are sent: \"dm\" sends to approver DMs (default), \"channel\" sends to the originating Slack chat/thread, and \"both\" sends to both. Channel delivery exposes the command text to the chat, so only use it in trusted channels.","hasChildren":false}
@@ -3182,12 +3179,12 @@
{"recordType":"path","path":"channels.telegram.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.telegram.errorCooldownMs","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.telegram.errorPolicy","kind":"channel","type":"string","required":false,"enumValues":["always","once","silent"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.telegram.execApprovals","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"Telegram Exec Approvals","help":"Telegram-native exec approval routing and approver authorization. When unset, OpenClaw auto-enables DM-first native approvals if approvers can be resolved for the selected bot account.","hasChildren":true}
{"recordType":"path","path":"channels.telegram.execApprovals","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"Telegram Exec Approvals","help":"Telegram-native exec approval routing and approver authorization. Enable this only when Telegram should act as an explicit exec-approval client for the selected bot account.","hasChildren":true}
{"recordType":"path","path":"channels.telegram.execApprovals.agentFilter","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"Telegram Exec Approval Agent Filter","help":"Optional allowlist of agent IDs eligible for Telegram exec approvals, for example `[\"main\", \"ops-agent\"]`. Use this to keep approval prompts scoped to the agents you actually operate from Telegram.","hasChildren":true}
{"recordType":"path","path":"channels.telegram.execApprovals.agentFilter.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.telegram.execApprovals.approvers","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"Telegram Exec Approval Approvers","help":"Telegram user IDs allowed to approve exec requests for this bot account. Use numeric Telegram user IDs. If you leave this unset, OpenClaw falls back to numeric owner IDs inferred from channels.telegram.allowFrom and direct-message defaultTo when possible.","hasChildren":true}
{"recordType":"path","path":"channels.telegram.execApprovals.approvers.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.telegram.execApprovals.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"Telegram Exec Approvals Enabled","help":"Controls Telegram native exec approvals for this account: unset or \"auto\" enables DM-first native approvals when approvers can be resolved, true forces native approvals on, and false disables them.","hasChildren":false}
{"recordType":"path","path":"channels.telegram.execApprovals.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"Telegram Exec Approvals Enabled","help":"Enable Telegram exec approvals for this account. When false or unset, Telegram messages/buttons cannot approve exec requests.","hasChildren":false}
{"recordType":"path","path":"channels.telegram.execApprovals.sessionFilter","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network","storage"],"label":"Telegram Exec Approval Session Filter","help":"Optional session-key filters matched as substring or regex-style patterns before Telegram approval routing is used. Use narrow patterns so Telegram approvals only appear for intended sessions.","hasChildren":true}
{"recordType":"path","path":"channels.telegram.execApprovals.sessionFilter.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.telegram.execApprovals.target","kind":"channel","type":"string","required":false,"enumValues":["dm","channel","both"],"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"Telegram Exec Approval Target","help":"Controls where Telegram approval prompts are sent: \"dm\" sends to approver DMs (default), \"channel\" sends to the originating Telegram chat/topic, and \"both\" sends to both. Channel delivery exposes the command text to the chat, so only use it in trusted groups/topics.","hasChildren":false}
@@ -5745,12 +5742,16 @@
{"recordType":"path","path":"tools.web.search.provider","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["tools"],"label":"Web Search Provider","help":"Search provider id. Auto-detected from available API keys if omitted.","hasChildren":false}
{"recordType":"path","path":"tools.web.search.timeoutSeconds","kind":"core","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":["performance","tools"],"label":"Web Search Timeout (sec)","help":"Timeout in seconds for web_search requests.","hasChildren":false}
{"recordType":"path","path":"tools.web.x_search","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"tools.web.x_search.cacheTtlMinutes","kind":"core","type":"number","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"tools.web.x_search.enabled","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"tools.web.x_search.inlineCitations","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"tools.web.x_search.maxTurns","kind":"core","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"tools.web.x_search.model","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"tools.web.x_search.timeoutSeconds","kind":"core","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"tools.web.x_search.apiKey","kind":"core","type":["object","string"],"required":false,"deprecated":false,"sensitive":true,"tags":["auth","security","tools"],"label":"xAI API Key","help":"xAI API key for X search (fallback: XAI_API_KEY env var).","hasChildren":true}
{"recordType":"path","path":"tools.web.x_search.apiKey.id","kind":"core","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"tools.web.x_search.apiKey.provider","kind":"core","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"tools.web.x_search.apiKey.source","kind":"core","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"tools.web.x_search.cacheTtlMinutes","kind":"core","type":"number","required":false,"deprecated":false,"sensitive":false,"tags":["performance","storage","tools"],"label":"X Search Cache TTL (min)","help":"Cache TTL in minutes for x_search results.","hasChildren":false}
{"recordType":"path","path":"tools.web.x_search.enabled","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["tools"],"label":"Enable X Search Tool","help":"Enable the x_search tool (requires XAI_API_KEY or tools.web.x_search.apiKey).","hasChildren":false}
{"recordType":"path","path":"tools.web.x_search.inlineCitations","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["tools"],"label":"X Search Inline Citations","help":"Keep inline citations from xAI in x_search responses when available (default: false).","hasChildren":false}
{"recordType":"path","path":"tools.web.x_search.maxTurns","kind":"core","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":["performance","tools"],"label":"X Search Max Turns","help":"Optional max internal search/tool turns xAI may use per x_search request. Omit to let xAI choose.","hasChildren":false}
{"recordType":"path","path":"tools.web.x_search.model","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["models","tools"],"label":"X Search Model","help":"Model to use for X search (default: \"grok-4-1-fast-non-reasoning\").","hasChildren":false}
{"recordType":"path","path":"tools.web.x_search.timeoutSeconds","kind":"core","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":["performance","tools"],"label":"X Search Timeout (sec)","help":"Timeout in seconds for x_search requests.","hasChildren":false}
{"recordType":"path","path":"ui","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"UI","help":"UI presentation settings for accenting and assistant identity shown in control surfaces. Use this for branding and readability customization without changing runtime behavior.","hasChildren":true}
{"recordType":"path","path":"ui.assistant","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Assistant Appearance","help":"Assistant display identity settings for name and avatar shown in UI surfaces. Keep these values aligned with your operator-facing persona and support expectations.","hasChildren":true}
{"recordType":"path","path":"ui.assistant.avatar","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Assistant Avatar","help":"Assistant avatar image source used in UI surfaces (URL, path, or data URI depending on runtime support). Use trusted assets and consistent branding dimensions for clean rendering.","hasChildren":false}

View File

@@ -253,7 +253,7 @@
"exportName": "CliBackendPlugin",
"kind": "type",
"source": {
"line": 1837,
"line": 1828,
"path": "src/plugins/types.ts"
}
},
@@ -397,7 +397,7 @@
"exportName": "MediaUnderstandingProviderPlugin",
"kind": "type",
"source": {
"line": 1476,
"line": 1467,
"path": "src/plugins/types.ts"
}
},
@@ -415,7 +415,7 @@
"exportName": "OpenClawPluginApi",
"kind": "type",
"source": {
"line": 1881,
"line": 1872,
"path": "src/plugins/types.ts"
}
},
@@ -424,7 +424,7 @@
"exportName": "OpenClawPluginConfigSchema",
"kind": "type",
"source": {
"line": 105,
"line": 104,
"path": "src/plugins/types.ts"
}
},
@@ -433,7 +433,7 @@
"exportName": "PluginLogger",
"kind": "type",
"source": {
"line": 76,
"line": 75,
"path": "src/plugins/types.ts"
}
},
@@ -451,7 +451,7 @@
"exportName": "ProviderAuthContext",
"kind": "type",
"source": {
"line": 180,
"line": 179,
"path": "src/plugins/types.ts"
}
},
@@ -460,7 +460,7 @@
"exportName": "ProviderAuthResult",
"kind": "type",
"source": {
"line": 165,
"line": 164,
"path": "src/plugins/types.ts"
}
},
@@ -469,7 +469,7 @@
"exportName": "ProviderRuntimeModel",
"kind": "type",
"source": {
"line": 321,
"line": 320,
"path": "src/plugins/types.ts"
}
},
@@ -523,7 +523,7 @@
"exportName": "SpeechProviderPlugin",
"kind": "type",
"source": {
"line": 1451,
"line": 1442,
"path": "src/plugins/types.ts"
}
},
@@ -3828,7 +3828,7 @@
"exportName": "MediaUnderstandingProviderPlugin",
"kind": "type",
"source": {
"line": 1476,
"line": 1467,
"path": "src/plugins/types.ts"
}
},
@@ -3846,7 +3846,7 @@
"exportName": "OpenClawPluginApi",
"kind": "type",
"source": {
"line": 1881,
"line": 1872,
"path": "src/plugins/types.ts"
}
},
@@ -3855,7 +3855,7 @@
"exportName": "OpenClawPluginCommandDefinition",
"kind": "type",
"source": {
"line": 1599,
"line": 1590,
"path": "src/plugins/types.ts"
}
},
@@ -3864,7 +3864,7 @@
"exportName": "OpenClawPluginConfigSchema",
"kind": "type",
"source": {
"line": 105,
"line": 104,
"path": "src/plugins/types.ts"
}
},
@@ -3873,7 +3873,7 @@
"exportName": "OpenClawPluginDefinition",
"kind": "type",
"source": {
"line": 1863,
"line": 1854,
"path": "src/plugins/types.ts"
}
},
@@ -3882,7 +3882,7 @@
"exportName": "OpenClawPluginService",
"kind": "type",
"source": {
"line": 1830,
"line": 1821,
"path": "src/plugins/types.ts"
}
},
@@ -3891,7 +3891,7 @@
"exportName": "OpenClawPluginServiceContext",
"kind": "type",
"source": {
"line": 1822,
"line": 1813,
"path": "src/plugins/types.ts"
}
},
@@ -3900,7 +3900,7 @@
"exportName": "OpenClawPluginToolContext",
"kind": "type",
"source": {
"line": 120,
"line": 119,
"path": "src/plugins/types.ts"
}
},
@@ -3909,7 +3909,7 @@
"exportName": "OpenClawPluginToolFactory",
"kind": "type",
"source": {
"line": 145,
"line": 144,
"path": "src/plugins/types.ts"
}
},
@@ -3918,7 +3918,7 @@
"exportName": "PluginCommandContext",
"kind": "type",
"source": {
"line": 1491,
"line": 1482,
"path": "src/plugins/types.ts"
}
},
@@ -3927,7 +3927,7 @@
"exportName": "PluginInteractiveTelegramHandlerContext",
"kind": "type",
"source": {
"line": 1628,
"line": 1619,
"path": "src/plugins/types.ts"
}
},
@@ -3936,7 +3936,7 @@
"exportName": "PluginLogger",
"kind": "type",
"source": {
"line": 76,
"line": 75,
"path": "src/plugins/types.ts"
}
},
@@ -3954,7 +3954,7 @@
"exportName": "ProviderAugmentModelCatalogContext",
"kind": "type",
"source": {
"line": 801,
"line": 799,
"path": "src/plugins/types.ts"
}
},
@@ -3963,7 +3963,7 @@
"exportName": "ProviderAuthContext",
"kind": "type",
"source": {
"line": 180,
"line": 179,
"path": "src/plugins/types.ts"
}
},
@@ -3972,7 +3972,7 @@
"exportName": "ProviderAuthDoctorHintContext",
"kind": "type",
"source": {
"line": 519,
"line": 517,
"path": "src/plugins/types.ts"
}
},
@@ -3981,7 +3981,7 @@
"exportName": "ProviderAuthMethod",
"kind": "type",
"source": {
"line": 259,
"line": 258,
"path": "src/plugins/types.ts"
}
},
@@ -3990,7 +3990,7 @@
"exportName": "ProviderAuthMethodNonInteractiveContext",
"kind": "type",
"source": {
"line": 243,
"line": 242,
"path": "src/plugins/types.ts"
}
},
@@ -3999,7 +3999,7 @@
"exportName": "ProviderAuthResult",
"kind": "type",
"source": {
"line": 165,
"line": 164,
"path": "src/plugins/types.ts"
}
},
@@ -4008,7 +4008,7 @@
"exportName": "ProviderBuildMissingAuthMessageContext",
"kind": "type",
"source": {
"line": 713,
"line": 711,
"path": "src/plugins/types.ts"
}
},
@@ -4017,7 +4017,7 @@
"exportName": "ProviderBuildUnknownModelHintContext",
"kind": "type",
"source": {
"line": 729,
"line": 727,
"path": "src/plugins/types.ts"
}
},
@@ -4026,7 +4026,7 @@
"exportName": "ProviderBuiltInModelSuppressionContext",
"kind": "type",
"source": {
"line": 745,
"line": 743,
"path": "src/plugins/types.ts"
}
},
@@ -4035,7 +4035,7 @@
"exportName": "ProviderBuiltInModelSuppressionResult",
"kind": "type",
"source": {
"line": 754,
"line": 752,
"path": "src/plugins/types.ts"
}
},
@@ -4044,7 +4044,7 @@
"exportName": "ProviderCacheTtlEligibilityContext",
"kind": "type",
"source": {
"line": 701,
"line": 699,
"path": "src/plugins/types.ts"
}
},
@@ -4053,7 +4053,7 @@
"exportName": "ProviderCatalogContext",
"kind": "type",
"source": {
"line": 280,
"line": 279,
"path": "src/plugins/types.ts"
}
},
@@ -4062,7 +4062,7 @@
"exportName": "ProviderCatalogResult",
"kind": "type",
"source": {
"line": 303,
"line": 302,
"path": "src/plugins/types.ts"
}
},
@@ -4071,7 +4071,7 @@
"exportName": "ProviderDefaultThinkingPolicyContext",
"kind": "type",
"source": {
"line": 778,
"line": 776,
"path": "src/plugins/types.ts"
}
},
@@ -4080,7 +4080,7 @@
"exportName": "ProviderDiscoveryContext",
"kind": "type",
"source": {
"line": 817,
"line": 815,
"path": "src/plugins/types.ts"
}
},
@@ -4089,7 +4089,7 @@
"exportName": "ProviderFetchUsageSnapshotContext",
"kind": "type",
"source": {
"line": 500,
"line": 498,
"path": "src/plugins/types.ts"
}
},
@@ -4098,7 +4098,7 @@
"exportName": "ProviderModernModelPolicyContext",
"kind": "type",
"source": {
"line": 788,
"line": 786,
"path": "src/plugins/types.ts"
}
},
@@ -4107,7 +4107,7 @@
"exportName": "ProviderNormalizeResolvedModelContext",
"kind": "type",
"source": {
"line": 364,
"line": 363,
"path": "src/plugins/types.ts"
}
},
@@ -4116,7 +4116,7 @@
"exportName": "ProviderNormalizeToolSchemasContext",
"kind": "type",
"source": {
"line": 617,
"line": 615,
"path": "src/plugins/types.ts"
}
},
@@ -4125,7 +4125,7 @@
"exportName": "ProviderPreparedRuntimeAuth",
"kind": "type",
"source": {
"line": 446,
"line": 445,
"path": "src/plugins/types.ts"
}
},
@@ -4134,7 +4134,7 @@
"exportName": "ProviderPrepareDynamicModelContext",
"kind": "type",
"source": {
"line": 355,
"line": 354,
"path": "src/plugins/types.ts"
}
},
@@ -4143,7 +4143,7 @@
"exportName": "ProviderPrepareExtraParamsContext",
"kind": "type",
"source": {
"line": 533,
"line": 531,
"path": "src/plugins/types.ts"
}
},
@@ -4152,7 +4152,7 @@
"exportName": "ProviderPrepareRuntimeAuthContext",
"kind": "type",
"source": {
"line": 425,
"line": 424,
"path": "src/plugins/types.ts"
}
},
@@ -4161,7 +4161,7 @@
"exportName": "ProviderReasoningOutputMode",
"kind": "type",
"source": {
"line": 547,
"line": 545,
"path": "src/plugins/types.ts"
}
},
@@ -4170,7 +4170,7 @@
"exportName": "ProviderReasoningOutputModeContext",
"kind": "type",
"source": {
"line": 627,
"line": 625,
"path": "src/plugins/types.ts"
}
},
@@ -4179,7 +4179,7 @@
"exportName": "ProviderReplayPolicy",
"kind": "type",
"source": {
"line": 556,
"line": 554,
"path": "src/plugins/types.ts"
}
},
@@ -4188,7 +4188,7 @@
"exportName": "ProviderReplayPolicyContext",
"kind": "type",
"source": {
"line": 577,
"line": 575,
"path": "src/plugins/types.ts"
}
},
@@ -4197,7 +4197,7 @@
"exportName": "ProviderResolvedUsageAuth",
"kind": "type",
"source": {
"line": 487,
"line": 485,
"path": "src/plugins/types.ts"
}
},
@@ -4206,7 +4206,7 @@
"exportName": "ProviderResolveDynamicModelContext",
"kind": "type",
"source": {
"line": 338,
"line": 337,
"path": "src/plugins/types.ts"
}
},
@@ -4215,7 +4215,7 @@
"exportName": "ProviderResolveUsageAuthContext",
"kind": "type",
"source": {
"line": 468,
"line": 466,
"path": "src/plugins/types.ts"
}
},
@@ -4224,7 +4224,7 @@
"exportName": "ProviderRuntimeModel",
"kind": "type",
"source": {
"line": 321,
"line": 320,
"path": "src/plugins/types.ts"
}
},
@@ -4233,7 +4233,7 @@
"exportName": "ProviderSanitizeReplayHistoryContext",
"kind": "type",
"source": {
"line": 594,
"line": 592,
"path": "src/plugins/types.ts"
}
},
@@ -4242,7 +4242,7 @@
"exportName": "ProviderThinkingPolicyContext",
"kind": "type",
"source": {
"line": 766,
"line": 764,
"path": "src/plugins/types.ts"
}
},
@@ -4260,7 +4260,7 @@
"exportName": "ProviderValidateReplayTurnsContext",
"kind": "type",
"source": {
"line": 606,
"line": 604,
"path": "src/plugins/types.ts"
}
},
@@ -4269,7 +4269,7 @@
"exportName": "ProviderWrapStreamFnContext",
"kind": "type",
"source": {
"line": 652,
"line": 650,
"path": "src/plugins/types.ts"
}
},
@@ -4314,7 +4314,7 @@
"exportName": "SpeechProviderPlugin",
"kind": "type",
"source": {
"line": 1451,
"line": 1442,
"path": "src/plugins/types.ts"
}
},
@@ -4406,7 +4406,7 @@
"exportName": "MediaUnderstandingProviderPlugin",
"kind": "type",
"source": {
"line": 1476,
"line": 1467,
"path": "src/plugins/types.ts"
}
},
@@ -4424,7 +4424,7 @@
"exportName": "OpenClawPluginApi",
"kind": "type",
"source": {
"line": 1881,
"line": 1872,
"path": "src/plugins/types.ts"
}
},
@@ -4433,7 +4433,7 @@
"exportName": "OpenClawPluginCommandDefinition",
"kind": "type",
"source": {
"line": 1599,
"line": 1590,
"path": "src/plugins/types.ts"
}
},
@@ -4442,7 +4442,7 @@
"exportName": "OpenClawPluginConfigSchema",
"kind": "type",
"source": {
"line": 105,
"line": 104,
"path": "src/plugins/types.ts"
}
},
@@ -4451,7 +4451,7 @@
"exportName": "OpenClawPluginDefinition",
"kind": "type",
"source": {
"line": 1863,
"line": 1854,
"path": "src/plugins/types.ts"
}
},
@@ -4460,7 +4460,7 @@
"exportName": "OpenClawPluginService",
"kind": "type",
"source": {
"line": 1830,
"line": 1821,
"path": "src/plugins/types.ts"
}
},
@@ -4469,7 +4469,7 @@
"exportName": "OpenClawPluginServiceContext",
"kind": "type",
"source": {
"line": 1822,
"line": 1813,
"path": "src/plugins/types.ts"
}
},
@@ -4478,7 +4478,7 @@
"exportName": "OpenClawPluginToolContext",
"kind": "type",
"source": {
"line": 120,
"line": 119,
"path": "src/plugins/types.ts"
}
},
@@ -4487,7 +4487,7 @@
"exportName": "OpenClawPluginToolFactory",
"kind": "type",
"source": {
"line": 145,
"line": 144,
"path": "src/plugins/types.ts"
}
},
@@ -4496,7 +4496,7 @@
"exportName": "PluginCommandContext",
"kind": "type",
"source": {
"line": 1491,
"line": 1482,
"path": "src/plugins/types.ts"
}
},
@@ -4505,7 +4505,7 @@
"exportName": "PluginInteractiveTelegramHandlerContext",
"kind": "type",
"source": {
"line": 1628,
"line": 1619,
"path": "src/plugins/types.ts"
}
},
@@ -4514,7 +4514,7 @@
"exportName": "PluginLogger",
"kind": "type",
"source": {
"line": 76,
"line": 75,
"path": "src/plugins/types.ts"
}
},
@@ -4523,7 +4523,7 @@
"exportName": "ProviderAugmentModelCatalogContext",
"kind": "type",
"source": {
"line": 801,
"line": 799,
"path": "src/plugins/types.ts"
}
},
@@ -4532,7 +4532,7 @@
"exportName": "ProviderAuthContext",
"kind": "type",
"source": {
"line": 180,
"line": 179,
"path": "src/plugins/types.ts"
}
},
@@ -4541,7 +4541,7 @@
"exportName": "ProviderAuthDoctorHintContext",
"kind": "type",
"source": {
"line": 519,
"line": 517,
"path": "src/plugins/types.ts"
}
},
@@ -4550,7 +4550,7 @@
"exportName": "ProviderAuthMethod",
"kind": "type",
"source": {
"line": 259,
"line": 258,
"path": "src/plugins/types.ts"
}
},
@@ -4559,7 +4559,7 @@
"exportName": "ProviderAuthMethodNonInteractiveContext",
"kind": "type",
"source": {
"line": 243,
"line": 242,
"path": "src/plugins/types.ts"
}
},
@@ -4568,7 +4568,7 @@
"exportName": "ProviderAuthResult",
"kind": "type",
"source": {
"line": 165,
"line": 164,
"path": "src/plugins/types.ts"
}
},
@@ -4577,7 +4577,7 @@
"exportName": "ProviderBuildMissingAuthMessageContext",
"kind": "type",
"source": {
"line": 713,
"line": 711,
"path": "src/plugins/types.ts"
}
},
@@ -4586,7 +4586,7 @@
"exportName": "ProviderBuildUnknownModelHintContext",
"kind": "type",
"source": {
"line": 729,
"line": 727,
"path": "src/plugins/types.ts"
}
},
@@ -4595,7 +4595,7 @@
"exportName": "ProviderBuiltInModelSuppressionContext",
"kind": "type",
"source": {
"line": 745,
"line": 743,
"path": "src/plugins/types.ts"
}
},
@@ -4604,7 +4604,7 @@
"exportName": "ProviderBuiltInModelSuppressionResult",
"kind": "type",
"source": {
"line": 754,
"line": 752,
"path": "src/plugins/types.ts"
}
},
@@ -4613,7 +4613,7 @@
"exportName": "ProviderCacheTtlEligibilityContext",
"kind": "type",
"source": {
"line": 701,
"line": 699,
"path": "src/plugins/types.ts"
}
},
@@ -4622,7 +4622,7 @@
"exportName": "ProviderCatalogContext",
"kind": "type",
"source": {
"line": 280,
"line": 279,
"path": "src/plugins/types.ts"
}
},
@@ -4631,7 +4631,7 @@
"exportName": "ProviderCatalogResult",
"kind": "type",
"source": {
"line": 303,
"line": 302,
"path": "src/plugins/types.ts"
}
},
@@ -4640,7 +4640,7 @@
"exportName": "ProviderDefaultThinkingPolicyContext",
"kind": "type",
"source": {
"line": 778,
"line": 776,
"path": "src/plugins/types.ts"
}
},
@@ -4649,7 +4649,7 @@
"exportName": "ProviderDiscoveryContext",
"kind": "type",
"source": {
"line": 817,
"line": 815,
"path": "src/plugins/types.ts"
}
},
@@ -4658,7 +4658,7 @@
"exportName": "ProviderFetchUsageSnapshotContext",
"kind": "type",
"source": {
"line": 500,
"line": 498,
"path": "src/plugins/types.ts"
}
},
@@ -4667,7 +4667,7 @@
"exportName": "ProviderModernModelPolicyContext",
"kind": "type",
"source": {
"line": 788,
"line": 786,
"path": "src/plugins/types.ts"
}
},
@@ -4676,7 +4676,7 @@
"exportName": "ProviderNormalizeConfigContext",
"kind": "type",
"source": {
"line": 390,
"line": 389,
"path": "src/plugins/types.ts"
}
},
@@ -4685,7 +4685,7 @@
"exportName": "ProviderNormalizeModelIdContext",
"kind": "type",
"source": {
"line": 379,
"line": 378,
"path": "src/plugins/types.ts"
}
},
@@ -4694,7 +4694,7 @@
"exportName": "ProviderNormalizeResolvedModelContext",
"kind": "type",
"source": {
"line": 364,
"line": 363,
"path": "src/plugins/types.ts"
}
},
@@ -4703,7 +4703,7 @@
"exportName": "ProviderNormalizeToolSchemasContext",
"kind": "type",
"source": {
"line": 617,
"line": 615,
"path": "src/plugins/types.ts"
}
},
@@ -4712,7 +4712,7 @@
"exportName": "ProviderNormalizeTransportContext",
"kind": "type",
"source": {
"line": 402,
"line": 401,
"path": "src/plugins/types.ts"
}
},
@@ -4721,7 +4721,7 @@
"exportName": "ProviderPreparedRuntimeAuth",
"kind": "type",
"source": {
"line": 446,
"line": 445,
"path": "src/plugins/types.ts"
}
},
@@ -4730,7 +4730,7 @@
"exportName": "ProviderPrepareDynamicModelContext",
"kind": "type",
"source": {
"line": 355,
"line": 354,
"path": "src/plugins/types.ts"
}
},
@@ -4739,7 +4739,7 @@
"exportName": "ProviderPrepareExtraParamsContext",
"kind": "type",
"source": {
"line": 533,
"line": 531,
"path": "src/plugins/types.ts"
}
},
@@ -4748,7 +4748,7 @@
"exportName": "ProviderPrepareRuntimeAuthContext",
"kind": "type",
"source": {
"line": 425,
"line": 424,
"path": "src/plugins/types.ts"
}
},
@@ -4757,7 +4757,7 @@
"exportName": "ProviderReasoningOutputMode",
"kind": "type",
"source": {
"line": 547,
"line": 545,
"path": "src/plugins/types.ts"
}
},
@@ -4766,7 +4766,7 @@
"exportName": "ProviderReasoningOutputModeContext",
"kind": "type",
"source": {
"line": 627,
"line": 625,
"path": "src/plugins/types.ts"
}
},
@@ -4775,7 +4775,7 @@
"exportName": "ProviderReplayPolicy",
"kind": "type",
"source": {
"line": 556,
"line": 554,
"path": "src/plugins/types.ts"
}
},
@@ -4784,7 +4784,7 @@
"exportName": "ProviderReplayPolicyContext",
"kind": "type",
"source": {
"line": 577,
"line": 575,
"path": "src/plugins/types.ts"
}
},
@@ -4793,7 +4793,7 @@
"exportName": "ProviderResolveConfigApiKeyContext",
"kind": "type",
"source": {
"line": 414,
"line": 413,
"path": "src/plugins/types.ts"
}
},
@@ -4802,7 +4802,7 @@
"exportName": "ProviderResolvedUsageAuth",
"kind": "type",
"source": {
"line": 487,
"line": 485,
"path": "src/plugins/types.ts"
}
},
@@ -4811,7 +4811,7 @@
"exportName": "ProviderResolveDynamicModelContext",
"kind": "type",
"source": {
"line": 338,
"line": 337,
"path": "src/plugins/types.ts"
}
},
@@ -4820,7 +4820,7 @@
"exportName": "ProviderResolveUsageAuthContext",
"kind": "type",
"source": {
"line": 468,
"line": 466,
"path": "src/plugins/types.ts"
}
},
@@ -4829,7 +4829,7 @@
"exportName": "ProviderRuntimeModel",
"kind": "type",
"source": {
"line": 321,
"line": 320,
"path": "src/plugins/types.ts"
}
},
@@ -4838,7 +4838,7 @@
"exportName": "ProviderSanitizeReplayHistoryContext",
"kind": "type",
"source": {
"line": 594,
"line": 592,
"path": "src/plugins/types.ts"
}
},
@@ -4847,7 +4847,7 @@
"exportName": "ProviderThinkingPolicyContext",
"kind": "type",
"source": {
"line": 766,
"line": 764,
"path": "src/plugins/types.ts"
}
},
@@ -4856,7 +4856,7 @@
"exportName": "ProviderValidateReplayTurnsContext",
"kind": "type",
"source": {
"line": 606,
"line": 604,
"path": "src/plugins/types.ts"
}
},
@@ -4865,7 +4865,7 @@
"exportName": "ProviderWrapStreamFnContext",
"kind": "type",
"source": {
"line": 652,
"line": 650,
"path": "src/plugins/types.ts"
}
},
@@ -4874,7 +4874,7 @@
"exportName": "SpeechProviderPlugin",
"kind": "type",
"source": {
"line": 1451,
"line": 1442,
"path": "src/plugins/types.ts"
}
}
@@ -5577,7 +5577,7 @@
"exportName": "getActivePluginRegistry",
"kind": "function",
"source": {
"line": 95,
"line": 89,
"path": "src/plugins/runtime.ts"
}
},
@@ -5676,7 +5676,7 @@
"exportName": "resetPluginRuntimeStateForTest",
"kind": "function",
"source": {
"line": 232,
"line": 193,
"path": "src/plugins/runtime.ts"
}
},
@@ -5721,7 +5721,7 @@
"exportName": "setActivePluginRegistry",
"kind": "function",
"source": {
"line": 82,
"line": 76,
"path": "src/plugins/runtime.ts"
}
},

View File

@@ -26,7 +26,7 @@
{"declaration":"export type ChannelStatusIssue = ChannelStatusIssue;","entrypoint":"index","exportName":"ChannelStatusIssue","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":102,"sourcePath":"src/channels/plugins/types.core.ts"}
{"declaration":"export type OpenClawConfig = OpenClawConfig;","entrypoint":"index","exportName":"ClawdbotConfig","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":32,"sourcePath":"src/config/types.openclaw.ts"}
{"declaration":"export type CliBackendConfig = CliBackendConfig;","entrypoint":"index","exportName":"CliBackendConfig","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":47,"sourcePath":"src/config/types.agent-defaults.ts"}
{"declaration":"export type CliBackendPlugin = CliBackendPlugin;","entrypoint":"index","exportName":"CliBackendPlugin","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":1837,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type CliBackendPlugin = CliBackendPlugin;","entrypoint":"index","exportName":"CliBackendPlugin","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":1828,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type CompiledConfiguredBinding = CompiledConfiguredBinding;","entrypoint":"index","exportName":"CompiledConfiguredBinding","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":38,"sourcePath":"src/channels/plugins/binding-types.ts"}
{"declaration":"export type ConfiguredBindingConversation = ConversationRef;","entrypoint":"index","exportName":"ConfiguredBindingConversation","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":13,"sourcePath":"src/channels/plugins/binding-types.ts"}
{"declaration":"export type ConfiguredBindingResolution = ConfiguredBindingResolution;","entrypoint":"index","exportName":"ConfiguredBindingResolution","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":49,"sourcePath":"src/channels/plugins/binding-types.ts"}
@@ -42,21 +42,21 @@
{"declaration":"export type ImageGenerationResolution = ImageGenerationResolution;","entrypoint":"index","exportName":"ImageGenerationResolution","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":12,"sourcePath":"src/image-generation/types.ts"}
{"declaration":"export type ImageGenerationResult = ImageGenerationResult;","entrypoint":"index","exportName":"ImageGenerationResult","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":36,"sourcePath":"src/image-generation/types.ts"}
{"declaration":"export type ImageGenerationSourceImage = ImageGenerationSourceImage;","entrypoint":"index","exportName":"ImageGenerationSourceImage","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":14,"sourcePath":"src/image-generation/types.ts"}
{"declaration":"export type MediaUnderstandingProviderPlugin = MediaUnderstandingProvider;","entrypoint":"index","exportName":"MediaUnderstandingProviderPlugin","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":1476,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type MediaUnderstandingProviderPlugin = MediaUnderstandingProvider;","entrypoint":"index","exportName":"MediaUnderstandingProviderPlugin","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":1467,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawConfig = OpenClawConfig;","entrypoint":"index","exportName":"OpenClawConfig","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":32,"sourcePath":"src/config/types.openclaw.ts"}
{"declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"index","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":1881,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginConfigSchema = OpenClawPluginConfigSchema;","entrypoint":"index","exportName":"OpenClawPluginConfigSchema","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":105,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type PluginLogger = PluginLogger;","entrypoint":"index","exportName":"PluginLogger","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":76,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"index","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":1872,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginConfigSchema = OpenClawPluginConfigSchema;","entrypoint":"index","exportName":"OpenClawPluginConfigSchema","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":104,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type PluginLogger = PluginLogger;","entrypoint":"index","exportName":"PluginLogger","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":75,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type PluginRuntime = PluginRuntime;","entrypoint":"index","exportName":"PluginRuntime","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":54,"sourcePath":"src/plugins/runtime/types.ts"}
{"declaration":"export type ProviderAuthContext = ProviderAuthContext;","entrypoint":"index","exportName":"ProviderAuthContext","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":180,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthResult = ProviderAuthResult;","entrypoint":"index","exportName":"ProviderAuthResult","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":165,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderRuntimeModel = ProviderRuntimeModel;","entrypoint":"index","exportName":"ProviderRuntimeModel","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":321,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthContext = ProviderAuthContext;","entrypoint":"index","exportName":"ProviderAuthContext","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":179,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthResult = ProviderAuthResult;","entrypoint":"index","exportName":"ProviderAuthResult","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":164,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderRuntimeModel = ProviderRuntimeModel;","entrypoint":"index","exportName":"ProviderRuntimeModel","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":320,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ReplyPayload = ReplyPayload;","entrypoint":"index","exportName":"ReplyPayload","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":85,"sourcePath":"src/auto-reply/types.ts"}
{"declaration":"export type RuntimeEnv = RuntimeEnv;","entrypoint":"index","exportName":"RuntimeEnv","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":4,"sourcePath":"src/runtime.ts"}
{"declaration":"export type RuntimeLogger = RuntimeLogger;","entrypoint":"index","exportName":"RuntimeLogger","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":7,"sourcePath":"src/plugins/runtime/types-core.ts"}
{"declaration":"export type SecretInput = SecretInput;","entrypoint":"index","exportName":"SecretInput","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":16,"sourcePath":"src/config/types.secrets.ts"}
{"declaration":"export type SecretRef = SecretRef;","entrypoint":"index","exportName":"SecretRef","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":10,"sourcePath":"src/config/types.secrets.ts"}
{"declaration":"export type SpeechProviderPlugin = SpeechProviderPlugin;","entrypoint":"index","exportName":"SpeechProviderPlugin","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":1451,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type SpeechProviderPlugin = SpeechProviderPlugin;","entrypoint":"index","exportName":"SpeechProviderPlugin","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":1442,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type StatefulBindingTargetDescriptor = StatefulBindingTargetDescriptor;","entrypoint":"index","exportName":"StatefulBindingTargetDescriptor","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":17,"sourcePath":"src/channels/plugins/binding-types.ts"}
{"declaration":"export type StatefulBindingTargetDriver = StatefulBindingTargetDriver;","entrypoint":"index","exportName":"StatefulBindingTargetDriver","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":15,"sourcePath":"src/channels/plugins/stateful-target-drivers.ts"}
{"declaration":"export type StatefulBindingTargetReadyResult = StatefulBindingTargetReadyResult;","entrypoint":"index","exportName":"StatefulBindingTargetReadyResult","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":7,"sourcePath":"src/channels/plugins/stateful-target-drivers.ts"}
@@ -421,61 +421,61 @@
{"declaration":"export type ChannelPlugin = ChannelPlugin<ResolvedAccount, Probe, Audit>;","entrypoint":"core","exportName":"ChannelPlugin","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":81,"sourcePath":"src/channels/plugins/types.plugin.ts"}
{"declaration":"export type GatewayBindUrlResult = GatewayBindUrlResult;","entrypoint":"core","exportName":"GatewayBindUrlResult","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1,"sourcePath":"src/shared/gateway-bind-url.ts"}
{"declaration":"export type GatewayRequestHandlerOptions = GatewayRequestHandlerOptions;","entrypoint":"core","exportName":"GatewayRequestHandlerOptions","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":115,"sourcePath":"src/gateway/server-methods/types.ts"}
{"declaration":"export type MediaUnderstandingProviderPlugin = MediaUnderstandingProvider;","entrypoint":"core","exportName":"MediaUnderstandingProviderPlugin","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1476,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type MediaUnderstandingProviderPlugin = MediaUnderstandingProvider;","entrypoint":"core","exportName":"MediaUnderstandingProviderPlugin","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1467,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawConfig = OpenClawConfig;","entrypoint":"core","exportName":"OpenClawConfig","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":32,"sourcePath":"src/config/types.openclaw.ts"}
{"declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"core","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1881,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginCommandDefinition = OpenClawPluginCommandDefinition;","entrypoint":"core","exportName":"OpenClawPluginCommandDefinition","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1599,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginConfigSchema = OpenClawPluginConfigSchema;","entrypoint":"core","exportName":"OpenClawPluginConfigSchema","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":105,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginDefinition = OpenClawPluginDefinition;","entrypoint":"core","exportName":"OpenClawPluginDefinition","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1863,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginService = OpenClawPluginService;","entrypoint":"core","exportName":"OpenClawPluginService","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1830,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginServiceContext = OpenClawPluginServiceContext;","entrypoint":"core","exportName":"OpenClawPluginServiceContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1822,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginToolContext = OpenClawPluginToolContext;","entrypoint":"core","exportName":"OpenClawPluginToolContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":120,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginToolFactory = OpenClawPluginToolFactory;","entrypoint":"core","exportName":"OpenClawPluginToolFactory","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":145,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type PluginCommandContext = PluginCommandContext;","entrypoint":"core","exportName":"PluginCommandContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1491,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type PluginInteractiveTelegramHandlerContext = PluginInteractiveTelegramHandlerContext;","entrypoint":"core","exportName":"PluginInteractiveTelegramHandlerContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1628,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type PluginLogger = PluginLogger;","entrypoint":"core","exportName":"PluginLogger","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":76,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"core","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1872,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginCommandDefinition = OpenClawPluginCommandDefinition;","entrypoint":"core","exportName":"OpenClawPluginCommandDefinition","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1590,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginConfigSchema = OpenClawPluginConfigSchema;","entrypoint":"core","exportName":"OpenClawPluginConfigSchema","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":104,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginDefinition = OpenClawPluginDefinition;","entrypoint":"core","exportName":"OpenClawPluginDefinition","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1854,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginService = OpenClawPluginService;","entrypoint":"core","exportName":"OpenClawPluginService","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1821,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginServiceContext = OpenClawPluginServiceContext;","entrypoint":"core","exportName":"OpenClawPluginServiceContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1813,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginToolContext = OpenClawPluginToolContext;","entrypoint":"core","exportName":"OpenClawPluginToolContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":119,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginToolFactory = OpenClawPluginToolFactory;","entrypoint":"core","exportName":"OpenClawPluginToolFactory","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":144,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type PluginCommandContext = PluginCommandContext;","entrypoint":"core","exportName":"PluginCommandContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1482,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type PluginInteractiveTelegramHandlerContext = PluginInteractiveTelegramHandlerContext;","entrypoint":"core","exportName":"PluginInteractiveTelegramHandlerContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1619,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type PluginLogger = PluginLogger;","entrypoint":"core","exportName":"PluginLogger","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":75,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type PluginRuntime = PluginRuntime;","entrypoint":"core","exportName":"PluginRuntime","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":54,"sourcePath":"src/plugins/runtime/types.ts"}
{"declaration":"export type ProviderAugmentModelCatalogContext = ProviderAugmentModelCatalogContext;","entrypoint":"core","exportName":"ProviderAugmentModelCatalogContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":801,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthContext = ProviderAuthContext;","entrypoint":"core","exportName":"ProviderAuthContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":180,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthDoctorHintContext = ProviderAuthDoctorHintContext;","entrypoint":"core","exportName":"ProviderAuthDoctorHintContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":519,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthMethod = ProviderAuthMethod;","entrypoint":"core","exportName":"ProviderAuthMethod","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":259,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthMethodNonInteractiveContext = ProviderAuthMethodNonInteractiveContext;","entrypoint":"core","exportName":"ProviderAuthMethodNonInteractiveContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":243,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthResult = ProviderAuthResult;","entrypoint":"core","exportName":"ProviderAuthResult","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":165,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderBuildMissingAuthMessageContext = ProviderBuildMissingAuthMessageContext;","entrypoint":"core","exportName":"ProviderBuildMissingAuthMessageContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":713,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderBuildUnknownModelHintContext = ProviderBuildUnknownModelHintContext;","entrypoint":"core","exportName":"ProviderBuildUnknownModelHintContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":729,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderBuiltInModelSuppressionContext = ProviderBuiltInModelSuppressionContext;","entrypoint":"core","exportName":"ProviderBuiltInModelSuppressionContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":745,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderBuiltInModelSuppressionResult = ProviderBuiltInModelSuppressionResult;","entrypoint":"core","exportName":"ProviderBuiltInModelSuppressionResult","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":754,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderCacheTtlEligibilityContext = ProviderCacheTtlEligibilityContext;","entrypoint":"core","exportName":"ProviderCacheTtlEligibilityContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":701,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderCatalogContext = ProviderCatalogContext;","entrypoint":"core","exportName":"ProviderCatalogContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":280,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderCatalogResult = ProviderCatalogResult;","entrypoint":"core","exportName":"ProviderCatalogResult","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":303,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderDefaultThinkingPolicyContext = ProviderDefaultThinkingPolicyContext;","entrypoint":"core","exportName":"ProviderDefaultThinkingPolicyContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":778,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderDiscoveryContext = ProviderCatalogContext;","entrypoint":"core","exportName":"ProviderDiscoveryContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":817,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderFetchUsageSnapshotContext = ProviderFetchUsageSnapshotContext;","entrypoint":"core","exportName":"ProviderFetchUsageSnapshotContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":500,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderModernModelPolicyContext = ProviderModernModelPolicyContext;","entrypoint":"core","exportName":"ProviderModernModelPolicyContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":788,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderNormalizeResolvedModelContext = ProviderNormalizeResolvedModelContext;","entrypoint":"core","exportName":"ProviderNormalizeResolvedModelContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":364,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderNormalizeToolSchemasContext = ProviderNormalizeToolSchemasContext;","entrypoint":"core","exportName":"ProviderNormalizeToolSchemasContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":617,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderPreparedRuntimeAuth = ProviderPreparedRuntimeAuth;","entrypoint":"core","exportName":"ProviderPreparedRuntimeAuth","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":446,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderPrepareDynamicModelContext = ProviderResolveDynamicModelContext;","entrypoint":"core","exportName":"ProviderPrepareDynamicModelContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":355,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderPrepareExtraParamsContext = ProviderPrepareExtraParamsContext;","entrypoint":"core","exportName":"ProviderPrepareExtraParamsContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":533,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderPrepareRuntimeAuthContext = ProviderPrepareRuntimeAuthContext;","entrypoint":"core","exportName":"ProviderPrepareRuntimeAuthContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":425,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderReasoningOutputMode = ProviderReasoningOutputMode;","entrypoint":"core","exportName":"ProviderReasoningOutputMode","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":547,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderReasoningOutputModeContext = ProviderReplayPolicyContext;","entrypoint":"core","exportName":"ProviderReasoningOutputModeContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":627,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderReplayPolicy = ProviderReplayPolicy;","entrypoint":"core","exportName":"ProviderReplayPolicy","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":556,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderReplayPolicyContext = ProviderReplayPolicyContext;","entrypoint":"core","exportName":"ProviderReplayPolicyContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":577,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderResolvedUsageAuth = ProviderResolvedUsageAuth;","entrypoint":"core","exportName":"ProviderResolvedUsageAuth","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":487,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderResolveDynamicModelContext = ProviderResolveDynamicModelContext;","entrypoint":"core","exportName":"ProviderResolveDynamicModelContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":338,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderResolveUsageAuthContext = ProviderResolveUsageAuthContext;","entrypoint":"core","exportName":"ProviderResolveUsageAuthContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":468,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderRuntimeModel = ProviderRuntimeModel;","entrypoint":"core","exportName":"ProviderRuntimeModel","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":321,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderSanitizeReplayHistoryContext = ProviderSanitizeReplayHistoryContext;","entrypoint":"core","exportName":"ProviderSanitizeReplayHistoryContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":594,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderThinkingPolicyContext = ProviderThinkingPolicyContext;","entrypoint":"core","exportName":"ProviderThinkingPolicyContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":766,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAugmentModelCatalogContext = ProviderAugmentModelCatalogContext;","entrypoint":"core","exportName":"ProviderAugmentModelCatalogContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":799,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthContext = ProviderAuthContext;","entrypoint":"core","exportName":"ProviderAuthContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":179,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthDoctorHintContext = ProviderAuthDoctorHintContext;","entrypoint":"core","exportName":"ProviderAuthDoctorHintContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":517,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthMethod = ProviderAuthMethod;","entrypoint":"core","exportName":"ProviderAuthMethod","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":258,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthMethodNonInteractiveContext = ProviderAuthMethodNonInteractiveContext;","entrypoint":"core","exportName":"ProviderAuthMethodNonInteractiveContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":242,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthResult = ProviderAuthResult;","entrypoint":"core","exportName":"ProviderAuthResult","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":164,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderBuildMissingAuthMessageContext = ProviderBuildMissingAuthMessageContext;","entrypoint":"core","exportName":"ProviderBuildMissingAuthMessageContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":711,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderBuildUnknownModelHintContext = ProviderBuildUnknownModelHintContext;","entrypoint":"core","exportName":"ProviderBuildUnknownModelHintContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":727,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderBuiltInModelSuppressionContext = ProviderBuiltInModelSuppressionContext;","entrypoint":"core","exportName":"ProviderBuiltInModelSuppressionContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":743,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderBuiltInModelSuppressionResult = ProviderBuiltInModelSuppressionResult;","entrypoint":"core","exportName":"ProviderBuiltInModelSuppressionResult","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":752,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderCacheTtlEligibilityContext = ProviderCacheTtlEligibilityContext;","entrypoint":"core","exportName":"ProviderCacheTtlEligibilityContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":699,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderCatalogContext = ProviderCatalogContext;","entrypoint":"core","exportName":"ProviderCatalogContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":279,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderCatalogResult = ProviderCatalogResult;","entrypoint":"core","exportName":"ProviderCatalogResult","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":302,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderDefaultThinkingPolicyContext = ProviderDefaultThinkingPolicyContext;","entrypoint":"core","exportName":"ProviderDefaultThinkingPolicyContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":776,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderDiscoveryContext = ProviderCatalogContext;","entrypoint":"core","exportName":"ProviderDiscoveryContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":815,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderFetchUsageSnapshotContext = ProviderFetchUsageSnapshotContext;","entrypoint":"core","exportName":"ProviderFetchUsageSnapshotContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":498,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderModernModelPolicyContext = ProviderModernModelPolicyContext;","entrypoint":"core","exportName":"ProviderModernModelPolicyContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":786,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderNormalizeResolvedModelContext = ProviderNormalizeResolvedModelContext;","entrypoint":"core","exportName":"ProviderNormalizeResolvedModelContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":363,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderNormalizeToolSchemasContext = ProviderNormalizeToolSchemasContext;","entrypoint":"core","exportName":"ProviderNormalizeToolSchemasContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":615,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderPreparedRuntimeAuth = ProviderPreparedRuntimeAuth;","entrypoint":"core","exportName":"ProviderPreparedRuntimeAuth","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":445,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderPrepareDynamicModelContext = ProviderResolveDynamicModelContext;","entrypoint":"core","exportName":"ProviderPrepareDynamicModelContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":354,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderPrepareExtraParamsContext = ProviderPrepareExtraParamsContext;","entrypoint":"core","exportName":"ProviderPrepareExtraParamsContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":531,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderPrepareRuntimeAuthContext = ProviderPrepareRuntimeAuthContext;","entrypoint":"core","exportName":"ProviderPrepareRuntimeAuthContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":424,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderReasoningOutputMode = ProviderReasoningOutputMode;","entrypoint":"core","exportName":"ProviderReasoningOutputMode","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":545,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderReasoningOutputModeContext = ProviderReplayPolicyContext;","entrypoint":"core","exportName":"ProviderReasoningOutputModeContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":625,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderReplayPolicy = ProviderReplayPolicy;","entrypoint":"core","exportName":"ProviderReplayPolicy","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":554,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderReplayPolicyContext = ProviderReplayPolicyContext;","entrypoint":"core","exportName":"ProviderReplayPolicyContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":575,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderResolvedUsageAuth = ProviderResolvedUsageAuth;","entrypoint":"core","exportName":"ProviderResolvedUsageAuth","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":485,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderResolveDynamicModelContext = ProviderResolveDynamicModelContext;","entrypoint":"core","exportName":"ProviderResolveDynamicModelContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":337,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderResolveUsageAuthContext = ProviderResolveUsageAuthContext;","entrypoint":"core","exportName":"ProviderResolveUsageAuthContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":466,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderRuntimeModel = ProviderRuntimeModel;","entrypoint":"core","exportName":"ProviderRuntimeModel","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":320,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderSanitizeReplayHistoryContext = ProviderSanitizeReplayHistoryContext;","entrypoint":"core","exportName":"ProviderSanitizeReplayHistoryContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":592,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderThinkingPolicyContext = ProviderThinkingPolicyContext;","entrypoint":"core","exportName":"ProviderThinkingPolicyContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":764,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderUsageSnapshot = ProviderUsageSnapshot;","entrypoint":"core","exportName":"ProviderUsageSnapshot","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":7,"sourcePath":"src/infra/provider-usage.types.ts"}
{"declaration":"export type ProviderValidateReplayTurnsContext = ProviderValidateReplayTurnsContext;","entrypoint":"core","exportName":"ProviderValidateReplayTurnsContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":606,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderWrapStreamFnContext = ProviderWrapStreamFnContext;","entrypoint":"core","exportName":"ProviderWrapStreamFnContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":652,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderValidateReplayTurnsContext = ProviderValidateReplayTurnsContext;","entrypoint":"core","exportName":"ProviderValidateReplayTurnsContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":604,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderWrapStreamFnContext = ProviderWrapStreamFnContext;","entrypoint":"core","exportName":"ProviderWrapStreamFnContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":650,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type RoutePeer = RoutePeer;","entrypoint":"core","exportName":"RoutePeer","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":21,"sourcePath":"src/routing/resolve-route.ts"}
{"declaration":"export type RoutePeerKind = ChatType;","entrypoint":"core","exportName":"RoutePeerKind","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":19,"sourcePath":"src/routing/resolve-route.ts"}
{"declaration":"export type SecretFileReadOptions = SecretFileReadOptions;","entrypoint":"core","exportName":"SecretFileReadOptions","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":7,"sourcePath":"src/infra/secret-file.ts"}
{"declaration":"export type SecretFileReadResult = SecretFileReadResult;","entrypoint":"core","exportName":"SecretFileReadResult","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":12,"sourcePath":"src/infra/secret-file.ts"}
{"declaration":"export type SpeechProviderPlugin = SpeechProviderPlugin;","entrypoint":"core","exportName":"SpeechProviderPlugin","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1451,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type SpeechProviderPlugin = SpeechProviderPlugin;","entrypoint":"core","exportName":"SpeechProviderPlugin","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1442,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type TailscaleStatusCommandResult = TailscaleStatusCommandResult;","entrypoint":"core","exportName":"TailscaleStatusCommandResult","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":4,"sourcePath":"src/shared/tailscale-status.ts"}
{"declaration":"export type TailscaleStatusCommandRunner = TailscaleStatusCommandRunner;","entrypoint":"core","exportName":"TailscaleStatusCommandRunner","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":9,"sourcePath":"src/shared/tailscale-status.ts"}
{"declaration":"export type UsageProviderId = UsageProviderId;","entrypoint":"core","exportName":"UsageProviderId","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":20,"sourcePath":"src/infra/provider-usage.types.ts"}
@@ -485,59 +485,59 @@
{"declaration":"export function definePluginEntry({ id, name, description, kind, configSchema, register, }: DefinePluginEntryOptions): DefinedPluginEntry;","entrypoint":"plugin-entry","exportName":"definePluginEntry","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"function","recordType":"export","sourceLine":151,"sourcePath":"src/plugin-sdk/plugin-entry.ts"}
{"declaration":"export function emptyPluginConfigSchema(): OpenClawPluginConfigSchema;","entrypoint":"plugin-entry","exportName":"emptyPluginConfigSchema","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"function","recordType":"export","sourceLine":108,"sourcePath":"src/plugins/config-schema.ts"}
{"declaration":"export type AnyAgentTool = AnyAgentTool;","entrypoint":"plugin-entry","exportName":"AnyAgentTool","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":9,"sourcePath":"src/agents/tools/common.ts"}
{"declaration":"export type MediaUnderstandingProviderPlugin = MediaUnderstandingProvider;","entrypoint":"plugin-entry","exportName":"MediaUnderstandingProviderPlugin","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":1476,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type MediaUnderstandingProviderPlugin = MediaUnderstandingProvider;","entrypoint":"plugin-entry","exportName":"MediaUnderstandingProviderPlugin","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":1467,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawConfig = OpenClawConfig;","entrypoint":"plugin-entry","exportName":"OpenClawConfig","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":32,"sourcePath":"src/config/types.openclaw.ts"}
{"declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"plugin-entry","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":1881,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginCommandDefinition = OpenClawPluginCommandDefinition;","entrypoint":"plugin-entry","exportName":"OpenClawPluginCommandDefinition","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":1599,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginConfigSchema = OpenClawPluginConfigSchema;","entrypoint":"plugin-entry","exportName":"OpenClawPluginConfigSchema","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":105,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginDefinition = OpenClawPluginDefinition;","entrypoint":"plugin-entry","exportName":"OpenClawPluginDefinition","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":1863,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginService = OpenClawPluginService;","entrypoint":"plugin-entry","exportName":"OpenClawPluginService","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":1830,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginServiceContext = OpenClawPluginServiceContext;","entrypoint":"plugin-entry","exportName":"OpenClawPluginServiceContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":1822,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginToolContext = OpenClawPluginToolContext;","entrypoint":"plugin-entry","exportName":"OpenClawPluginToolContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":120,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginToolFactory = OpenClawPluginToolFactory;","entrypoint":"plugin-entry","exportName":"OpenClawPluginToolFactory","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":145,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type PluginCommandContext = PluginCommandContext;","entrypoint":"plugin-entry","exportName":"PluginCommandContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":1491,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type PluginInteractiveTelegramHandlerContext = PluginInteractiveTelegramHandlerContext;","entrypoint":"plugin-entry","exportName":"PluginInteractiveTelegramHandlerContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":1628,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type PluginLogger = PluginLogger;","entrypoint":"plugin-entry","exportName":"PluginLogger","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":76,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAugmentModelCatalogContext = ProviderAugmentModelCatalogContext;","entrypoint":"plugin-entry","exportName":"ProviderAugmentModelCatalogContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":801,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthContext = ProviderAuthContext;","entrypoint":"plugin-entry","exportName":"ProviderAuthContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":180,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthDoctorHintContext = ProviderAuthDoctorHintContext;","entrypoint":"plugin-entry","exportName":"ProviderAuthDoctorHintContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":519,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthMethod = ProviderAuthMethod;","entrypoint":"plugin-entry","exportName":"ProviderAuthMethod","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":259,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthMethodNonInteractiveContext = ProviderAuthMethodNonInteractiveContext;","entrypoint":"plugin-entry","exportName":"ProviderAuthMethodNonInteractiveContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":243,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthResult = ProviderAuthResult;","entrypoint":"plugin-entry","exportName":"ProviderAuthResult","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":165,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderBuildMissingAuthMessageContext = ProviderBuildMissingAuthMessageContext;","entrypoint":"plugin-entry","exportName":"ProviderBuildMissingAuthMessageContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":713,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderBuildUnknownModelHintContext = ProviderBuildUnknownModelHintContext;","entrypoint":"plugin-entry","exportName":"ProviderBuildUnknownModelHintContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":729,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderBuiltInModelSuppressionContext = ProviderBuiltInModelSuppressionContext;","entrypoint":"plugin-entry","exportName":"ProviderBuiltInModelSuppressionContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":745,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderBuiltInModelSuppressionResult = ProviderBuiltInModelSuppressionResult;","entrypoint":"plugin-entry","exportName":"ProviderBuiltInModelSuppressionResult","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":754,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderCacheTtlEligibilityContext = ProviderCacheTtlEligibilityContext;","entrypoint":"plugin-entry","exportName":"ProviderCacheTtlEligibilityContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":701,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderCatalogContext = ProviderCatalogContext;","entrypoint":"plugin-entry","exportName":"ProviderCatalogContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":280,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderCatalogResult = ProviderCatalogResult;","entrypoint":"plugin-entry","exportName":"ProviderCatalogResult","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":303,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderDefaultThinkingPolicyContext = ProviderDefaultThinkingPolicyContext;","entrypoint":"plugin-entry","exportName":"ProviderDefaultThinkingPolicyContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":778,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderDiscoveryContext = ProviderCatalogContext;","entrypoint":"plugin-entry","exportName":"ProviderDiscoveryContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":817,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderFetchUsageSnapshotContext = ProviderFetchUsageSnapshotContext;","entrypoint":"plugin-entry","exportName":"ProviderFetchUsageSnapshotContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":500,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderModernModelPolicyContext = ProviderModernModelPolicyContext;","entrypoint":"plugin-entry","exportName":"ProviderModernModelPolicyContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":788,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderNormalizeConfigContext = ProviderNormalizeConfigContext;","entrypoint":"plugin-entry","exportName":"ProviderNormalizeConfigContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":390,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderNormalizeModelIdContext = ProviderNormalizeModelIdContext;","entrypoint":"plugin-entry","exportName":"ProviderNormalizeModelIdContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":379,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderNormalizeResolvedModelContext = ProviderNormalizeResolvedModelContext;","entrypoint":"plugin-entry","exportName":"ProviderNormalizeResolvedModelContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":364,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderNormalizeToolSchemasContext = ProviderNormalizeToolSchemasContext;","entrypoint":"plugin-entry","exportName":"ProviderNormalizeToolSchemasContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":617,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderNormalizeTransportContext = ProviderNormalizeTransportContext;","entrypoint":"plugin-entry","exportName":"ProviderNormalizeTransportContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":402,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderPreparedRuntimeAuth = ProviderPreparedRuntimeAuth;","entrypoint":"plugin-entry","exportName":"ProviderPreparedRuntimeAuth","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":446,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderPrepareDynamicModelContext = ProviderResolveDynamicModelContext;","entrypoint":"plugin-entry","exportName":"ProviderPrepareDynamicModelContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":355,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderPrepareExtraParamsContext = ProviderPrepareExtraParamsContext;","entrypoint":"plugin-entry","exportName":"ProviderPrepareExtraParamsContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":533,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderPrepareRuntimeAuthContext = ProviderPrepareRuntimeAuthContext;","entrypoint":"plugin-entry","exportName":"ProviderPrepareRuntimeAuthContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":425,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderReasoningOutputMode = ProviderReasoningOutputMode;","entrypoint":"plugin-entry","exportName":"ProviderReasoningOutputMode","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":547,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderReasoningOutputModeContext = ProviderReplayPolicyContext;","entrypoint":"plugin-entry","exportName":"ProviderReasoningOutputModeContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":627,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderReplayPolicy = ProviderReplayPolicy;","entrypoint":"plugin-entry","exportName":"ProviderReplayPolicy","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":556,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderReplayPolicyContext = ProviderReplayPolicyContext;","entrypoint":"plugin-entry","exportName":"ProviderReplayPolicyContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":577,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderResolveConfigApiKeyContext = ProviderResolveConfigApiKeyContext;","entrypoint":"plugin-entry","exportName":"ProviderResolveConfigApiKeyContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":414,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderResolvedUsageAuth = ProviderResolvedUsageAuth;","entrypoint":"plugin-entry","exportName":"ProviderResolvedUsageAuth","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":487,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderResolveDynamicModelContext = ProviderResolveDynamicModelContext;","entrypoint":"plugin-entry","exportName":"ProviderResolveDynamicModelContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":338,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderResolveUsageAuthContext = ProviderResolveUsageAuthContext;","entrypoint":"plugin-entry","exportName":"ProviderResolveUsageAuthContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":468,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderRuntimeModel = ProviderRuntimeModel;","entrypoint":"plugin-entry","exportName":"ProviderRuntimeModel","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":321,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderSanitizeReplayHistoryContext = ProviderSanitizeReplayHistoryContext;","entrypoint":"plugin-entry","exportName":"ProviderSanitizeReplayHistoryContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":594,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderThinkingPolicyContext = ProviderThinkingPolicyContext;","entrypoint":"plugin-entry","exportName":"ProviderThinkingPolicyContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":766,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderValidateReplayTurnsContext = ProviderValidateReplayTurnsContext;","entrypoint":"plugin-entry","exportName":"ProviderValidateReplayTurnsContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":606,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderWrapStreamFnContext = ProviderWrapStreamFnContext;","entrypoint":"plugin-entry","exportName":"ProviderWrapStreamFnContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":652,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type SpeechProviderPlugin = SpeechProviderPlugin;","entrypoint":"plugin-entry","exportName":"SpeechProviderPlugin","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":1451,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"plugin-entry","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":1872,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginCommandDefinition = OpenClawPluginCommandDefinition;","entrypoint":"plugin-entry","exportName":"OpenClawPluginCommandDefinition","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":1590,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginConfigSchema = OpenClawPluginConfigSchema;","entrypoint":"plugin-entry","exportName":"OpenClawPluginConfigSchema","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":104,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginDefinition = OpenClawPluginDefinition;","entrypoint":"plugin-entry","exportName":"OpenClawPluginDefinition","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":1854,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginService = OpenClawPluginService;","entrypoint":"plugin-entry","exportName":"OpenClawPluginService","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":1821,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginServiceContext = OpenClawPluginServiceContext;","entrypoint":"plugin-entry","exportName":"OpenClawPluginServiceContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":1813,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginToolContext = OpenClawPluginToolContext;","entrypoint":"plugin-entry","exportName":"OpenClawPluginToolContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":119,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginToolFactory = OpenClawPluginToolFactory;","entrypoint":"plugin-entry","exportName":"OpenClawPluginToolFactory","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":144,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type PluginCommandContext = PluginCommandContext;","entrypoint":"plugin-entry","exportName":"PluginCommandContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":1482,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type PluginInteractiveTelegramHandlerContext = PluginInteractiveTelegramHandlerContext;","entrypoint":"plugin-entry","exportName":"PluginInteractiveTelegramHandlerContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":1619,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type PluginLogger = PluginLogger;","entrypoint":"plugin-entry","exportName":"PluginLogger","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":75,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAugmentModelCatalogContext = ProviderAugmentModelCatalogContext;","entrypoint":"plugin-entry","exportName":"ProviderAugmentModelCatalogContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":799,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthContext = ProviderAuthContext;","entrypoint":"plugin-entry","exportName":"ProviderAuthContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":179,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthDoctorHintContext = ProviderAuthDoctorHintContext;","entrypoint":"plugin-entry","exportName":"ProviderAuthDoctorHintContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":517,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthMethod = ProviderAuthMethod;","entrypoint":"plugin-entry","exportName":"ProviderAuthMethod","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":258,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthMethodNonInteractiveContext = ProviderAuthMethodNonInteractiveContext;","entrypoint":"plugin-entry","exportName":"ProviderAuthMethodNonInteractiveContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":242,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthResult = ProviderAuthResult;","entrypoint":"plugin-entry","exportName":"ProviderAuthResult","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":164,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderBuildMissingAuthMessageContext = ProviderBuildMissingAuthMessageContext;","entrypoint":"plugin-entry","exportName":"ProviderBuildMissingAuthMessageContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":711,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderBuildUnknownModelHintContext = ProviderBuildUnknownModelHintContext;","entrypoint":"plugin-entry","exportName":"ProviderBuildUnknownModelHintContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":727,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderBuiltInModelSuppressionContext = ProviderBuiltInModelSuppressionContext;","entrypoint":"plugin-entry","exportName":"ProviderBuiltInModelSuppressionContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":743,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderBuiltInModelSuppressionResult = ProviderBuiltInModelSuppressionResult;","entrypoint":"plugin-entry","exportName":"ProviderBuiltInModelSuppressionResult","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":752,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderCacheTtlEligibilityContext = ProviderCacheTtlEligibilityContext;","entrypoint":"plugin-entry","exportName":"ProviderCacheTtlEligibilityContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":699,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderCatalogContext = ProviderCatalogContext;","entrypoint":"plugin-entry","exportName":"ProviderCatalogContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":279,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderCatalogResult = ProviderCatalogResult;","entrypoint":"plugin-entry","exportName":"ProviderCatalogResult","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":302,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderDefaultThinkingPolicyContext = ProviderDefaultThinkingPolicyContext;","entrypoint":"plugin-entry","exportName":"ProviderDefaultThinkingPolicyContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":776,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderDiscoveryContext = ProviderCatalogContext;","entrypoint":"plugin-entry","exportName":"ProviderDiscoveryContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":815,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderFetchUsageSnapshotContext = ProviderFetchUsageSnapshotContext;","entrypoint":"plugin-entry","exportName":"ProviderFetchUsageSnapshotContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":498,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderModernModelPolicyContext = ProviderModernModelPolicyContext;","entrypoint":"plugin-entry","exportName":"ProviderModernModelPolicyContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":786,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderNormalizeConfigContext = ProviderNormalizeConfigContext;","entrypoint":"plugin-entry","exportName":"ProviderNormalizeConfigContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":389,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderNormalizeModelIdContext = ProviderNormalizeModelIdContext;","entrypoint":"plugin-entry","exportName":"ProviderNormalizeModelIdContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":378,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderNormalizeResolvedModelContext = ProviderNormalizeResolvedModelContext;","entrypoint":"plugin-entry","exportName":"ProviderNormalizeResolvedModelContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":363,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderNormalizeToolSchemasContext = ProviderNormalizeToolSchemasContext;","entrypoint":"plugin-entry","exportName":"ProviderNormalizeToolSchemasContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":615,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderNormalizeTransportContext = ProviderNormalizeTransportContext;","entrypoint":"plugin-entry","exportName":"ProviderNormalizeTransportContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":401,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderPreparedRuntimeAuth = ProviderPreparedRuntimeAuth;","entrypoint":"plugin-entry","exportName":"ProviderPreparedRuntimeAuth","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":445,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderPrepareDynamicModelContext = ProviderResolveDynamicModelContext;","entrypoint":"plugin-entry","exportName":"ProviderPrepareDynamicModelContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":354,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderPrepareExtraParamsContext = ProviderPrepareExtraParamsContext;","entrypoint":"plugin-entry","exportName":"ProviderPrepareExtraParamsContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":531,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderPrepareRuntimeAuthContext = ProviderPrepareRuntimeAuthContext;","entrypoint":"plugin-entry","exportName":"ProviderPrepareRuntimeAuthContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":424,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderReasoningOutputMode = ProviderReasoningOutputMode;","entrypoint":"plugin-entry","exportName":"ProviderReasoningOutputMode","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":545,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderReasoningOutputModeContext = ProviderReplayPolicyContext;","entrypoint":"plugin-entry","exportName":"ProviderReasoningOutputModeContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":625,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderReplayPolicy = ProviderReplayPolicy;","entrypoint":"plugin-entry","exportName":"ProviderReplayPolicy","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":554,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderReplayPolicyContext = ProviderReplayPolicyContext;","entrypoint":"plugin-entry","exportName":"ProviderReplayPolicyContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":575,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderResolveConfigApiKeyContext = ProviderResolveConfigApiKeyContext;","entrypoint":"plugin-entry","exportName":"ProviderResolveConfigApiKeyContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":413,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderResolvedUsageAuth = ProviderResolvedUsageAuth;","entrypoint":"plugin-entry","exportName":"ProviderResolvedUsageAuth","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":485,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderResolveDynamicModelContext = ProviderResolveDynamicModelContext;","entrypoint":"plugin-entry","exportName":"ProviderResolveDynamicModelContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":337,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderResolveUsageAuthContext = ProviderResolveUsageAuthContext;","entrypoint":"plugin-entry","exportName":"ProviderResolveUsageAuthContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":466,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderRuntimeModel = ProviderRuntimeModel;","entrypoint":"plugin-entry","exportName":"ProviderRuntimeModel","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":320,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderSanitizeReplayHistoryContext = ProviderSanitizeReplayHistoryContext;","entrypoint":"plugin-entry","exportName":"ProviderSanitizeReplayHistoryContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":592,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderThinkingPolicyContext = ProviderThinkingPolicyContext;","entrypoint":"plugin-entry","exportName":"ProviderThinkingPolicyContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":764,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderValidateReplayTurnsContext = ProviderValidateReplayTurnsContext;","entrypoint":"plugin-entry","exportName":"ProviderValidateReplayTurnsContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":604,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderWrapStreamFnContext = ProviderWrapStreamFnContext;","entrypoint":"plugin-entry","exportName":"ProviderWrapStreamFnContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":650,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type SpeechProviderPlugin = SpeechProviderPlugin;","entrypoint":"plugin-entry","exportName":"SpeechProviderPlugin","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":1442,"sourcePath":"src/plugins/types.ts"}
{"category":"provider","entrypoint":"provider-onboard","importSpecifier":"openclaw/plugin-sdk/provider-onboard","recordType":"module","sourceLine":1,"sourcePath":"src/plugin-sdk/provider-onboard.ts"}
{"declaration":"export function applyAgentDefaultModelPrimary(cfg: OpenClawConfig, primary: string): OpenClawConfig;","entrypoint":"provider-onboard","exportName":"applyAgentDefaultModelPrimary","importSpecifier":"openclaw/plugin-sdk/provider-onboard","kind":"function","recordType":"export","sourceLine":271,"sourcePath":"src/plugin-sdk/provider-onboard.ts"}
{"declaration":"export function applyOnboardAuthAgentModelsAndProviders(cfg: OpenClawConfig, params: { agentModels: Record<string, AgentModelEntryConfig>; providers: Record<string, ModelProviderConfig>; }): OpenClawConfig;","entrypoint":"provider-onboard","exportName":"applyOnboardAuthAgentModelsAndProviders","importSpecifier":"openclaw/plugin-sdk/provider-onboard","kind":"function","recordType":"export","sourceLine":248,"sourcePath":"src/plugin-sdk/provider-onboard.ts"}
@@ -614,7 +614,7 @@
{"declaration":"export function expectChannelInboundContextContract(ctx: MsgContext): void;","entrypoint":"testing","exportName":"expectChannelInboundContextContract","importSpecifier":"openclaw/plugin-sdk/testing","kind":"function","recordType":"export","sourceLine":856,"sourcePath":"src/channels/plugins/contracts/suites.ts"}
{"declaration":"export function expectWhatsAppPollSent(sendPollWhatsApp: MockInstance<Procedure>, params: { cfg: OpenClawConfig; poll: { question: string; options: string[]; maxSelections: number; }; to?: string | undefined; accountId?: string | undefined; }): void;","entrypoint":"testing","exportName":"expectWhatsAppPollSent","importSpecifier":"openclaw/plugin-sdk/testing","kind":"function","recordType":"export","sourceLine":19,"sourcePath":"src/test-helpers/whatsapp-outbound.ts"}
{"declaration":"export function firstWrittenJsonArg<T>(writeJson: MockCallsWithFirstArg): T | null;","entrypoint":"testing","exportName":"firstWrittenJsonArg","importSpecifier":"openclaw/plugin-sdk/testing","kind":"function","recordType":"export","sourceLine":92,"sourcePath":"src/cli/test-runtime-capture.ts"}
{"declaration":"export function getActivePluginRegistry(): PluginRegistry | null;","entrypoint":"testing","exportName":"getActivePluginRegistry","importSpecifier":"openclaw/plugin-sdk/testing","kind":"function","recordType":"export","sourceLine":95,"sourcePath":"src/plugins/runtime.ts"}
{"declaration":"export function getActivePluginRegistry(): PluginRegistry | null;","entrypoint":"testing","exportName":"getActivePluginRegistry","importSpecifier":"openclaw/plugin-sdk/testing","kind":"function","recordType":"export","sourceLine":89,"sourcePath":"src/plugins/runtime.ts"}
{"declaration":"export function installCommonResolveTargetErrorCases(params: { resolveTarget: ResolveTargetFn; implicitAllowFrom: string[]; }): void;","entrypoint":"testing","exportName":"installCommonResolveTargetErrorCases","importSpecifier":"openclaw/plugin-sdk/testing","kind":"function","recordType":"export","sourceLine":88,"sourcePath":"src/plugin-sdk/testing.ts"}
{"declaration":"export function installPinnedHostnameTestHooks(): void;","entrypoint":"testing","exportName":"installPinnedHostnameTestHooks","importSpecifier":"openclaw/plugin-sdk/testing","kind":"function","recordType":"export","sourceLine":16,"sourcePath":"src/media-understanding/audio.test-helpers.ts"}
{"declaration":"export function isLiveTestEnabled(extraEnvVars?: readonly string[], env?: ProcessEnv): boolean;","entrypoint":"testing","exportName":"isLiveTestEnabled","importSpecifier":"openclaw/plugin-sdk/testing","kind":"function","recordType":"export","sourceLine":5,"sourcePath":"src/agents/live-test-helpers.ts"}
@@ -625,12 +625,12 @@
{"declaration":"export function removeAckReactionAfterReply(params: { removeAfterReply: boolean; ackReactionPromise: Promise<boolean> | null; ackReactionValue: string | null; remove: () => Promise<void>; onError?: ((err: unknown) => void) | undefined; }): void;","entrypoint":"testing","exportName":"removeAckReactionAfterReply","importSpecifier":"openclaw/plugin-sdk/testing","kind":"function","recordType":"export","sourceLine":81,"sourcePath":"src/channels/ack-reactions.ts"}
{"declaration":"export function requestBodyText(body: BodyInit | null | undefined): string;","entrypoint":"testing","exportName":"requestBodyText","importSpecifier":"openclaw/plugin-sdk/testing","kind":"function","recordType":"export","sourceLine":18,"sourcePath":"src/test-helpers/http.ts"}
{"declaration":"export function requestUrl(input: string | Request | URL): string;","entrypoint":"testing","exportName":"requestUrl","importSpecifier":"openclaw/plugin-sdk/testing","kind":"function","recordType":"export","sourceLine":8,"sourcePath":"src/test-helpers/http.ts"}
{"declaration":"export function resetPluginRuntimeStateForTest(): void;","entrypoint":"testing","exportName":"resetPluginRuntimeStateForTest","importSpecifier":"openclaw/plugin-sdk/testing","kind":"function","recordType":"export","sourceLine":232,"sourcePath":"src/plugins/runtime.ts"}
{"declaration":"export function resetPluginRuntimeStateForTest(): void;","entrypoint":"testing","exportName":"resetPluginRuntimeStateForTest","importSpecifier":"openclaw/plugin-sdk/testing","kind":"function","recordType":"export","sourceLine":193,"sourcePath":"src/plugins/runtime.ts"}
{"declaration":"export function resetSystemEventsForTest(): void;","entrypoint":"testing","exportName":"resetSystemEventsForTest","importSpecifier":"openclaw/plugin-sdk/testing","kind":"function","recordType":"export","sourceLine":160,"sourcePath":"src/infra/system-events.ts"}
{"declaration":"export function resolveProviderPluginChoice(params: { providers: ProviderPlugin[]; choice: string; }): { provider: ProviderPlugin; method: ProviderAuthMethod; wizard?: ProviderPluginWizardSetup | undefined; } | null;","entrypoint":"testing","exportName":"resolveProviderPluginChoice","importSpecifier":"openclaw/plugin-sdk/testing","kind":"function","recordType":"export","sourceLine":13,"sourcePath":"src/plugins/provider-auth-choice.runtime.ts"}
{"declaration":"export function runAcpRuntimeAdapterContract(params: AcpRuntimeAdapterContractParams): Promise<void>;","entrypoint":"testing","exportName":"runAcpRuntimeAdapterContract","importSpecifier":"openclaw/plugin-sdk/testing","kind":"function","recordType":"export","sourceLine":19,"sourcePath":"src/acp/runtime/adapter-contract.testkit.ts"}
{"declaration":"export function sanitizeTerminalText(input: string): string;","entrypoint":"testing","exportName":"sanitizeTerminalText","importSpecifier":"openclaw/plugin-sdk/testing","kind":"function","recordType":"export","sourceLine":6,"sourcePath":"src/terminal/safe-text.ts"}
{"declaration":"export function setActivePluginRegistry(registry: PluginRegistry, cacheKey?: string | undefined, runtimeSubagentMode?: \"default\" | \"explicit\" | \"gateway-bindable\"): void;","entrypoint":"testing","exportName":"setActivePluginRegistry","importSpecifier":"openclaw/plugin-sdk/testing","kind":"function","recordType":"export","sourceLine":82,"sourcePath":"src/plugins/runtime.ts"}
{"declaration":"export function setActivePluginRegistry(registry: PluginRegistry, cacheKey?: string | undefined, runtimeSubagentMode?: \"default\" | \"explicit\" | \"gateway-bindable\"): void;","entrypoint":"testing","exportName":"setActivePluginRegistry","importSpecifier":"openclaw/plugin-sdk/testing","kind":"function","recordType":"export","sourceLine":76,"sourcePath":"src/plugins/runtime.ts"}
{"declaration":"export function setDefaultChannelPluginRegistryForTests(): void;","entrypoint":"testing","exportName":"setDefaultChannelPluginRegistryForTests","importSpecifier":"openclaw/plugin-sdk/testing","kind":"function","recordType":"export","sourceLine":50,"sourcePath":"src/commands/channel-test-helpers.ts"}
{"declaration":"export function shouldAckReaction(params: AckReactionGateParams): boolean;","entrypoint":"testing","exportName":"shouldAckReaction","importSpecifier":"openclaw/plugin-sdk/testing","kind":"function","recordType":"export","sourceLine":16,"sourcePath":"src/channels/ack-reactions.ts"}
{"declaration":"export function spyRuntimeErrors(runtime: Pick<OutputRuntimeEnv, \"error\">): Mock<(...args: unknown[]) => void>;","entrypoint":"testing","exportName":"spyRuntimeErrors","importSpecifier":"openclaw/plugin-sdk/testing","kind":"function","recordType":"export","sourceLine":84,"sourcePath":"src/cli/test-runtime-capture.ts"}

View File

@@ -952,7 +952,7 @@ Default slash command settings:
- `channels.discord.execApprovals.target` (`dm` | `channel` | `both`, default: `dm`)
- `agentFilter`, `sessionFilter`, `cleanupAfterResolve`
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 the account's existing owner config (`allowFrom`, legacy `dm.allowFrom`, or explicit DM `defaultTo`). Set `enabled: false` to disable Discord as a native approval client explicitly.
Discord becomes an approval client when `enabled: true` and at least one approver can be resolved, either from `execApprovals.approvers` or from the account's existing owner config (`allowFrom`, legacy `dm.allowFrom`, or explicit DM `defaultTo`).
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.

View File

@@ -487,47 +487,19 @@ Exec approval prompts can route natively through Slack using interactive buttons
This uses the same shared approval button surface as other channels. When `interactivity` is enabled in your Slack app settings, approval prompts render as Block Kit buttons directly in the conversation.
Config path:
- `channels.slack.execApprovals.enabled`
- `channels.slack.execApprovals.approvers` (optional; falls back to `commands.ownerAllowFrom` when possible)
- `channels.slack.execApprovals.target` (`dm` | `channel` | `both`, default: `dm`)
- `agentFilter`, `sessionFilter`
Slack auto-enables native exec approvals when `enabled` is unset or `"auto"` and at least one
approver resolves. Set `enabled: false` to disable Slack as a native approval client explicitly.
Set `enabled: true` to force native approvals on when approvers resolve.
Default behavior with no explicit Slack exec approval config:
Configuration uses the shared `approvals.exec` config with Slack targets:
```json5
{
commands: {
ownerAllowFrom: ["slack:U12345678"],
},
}
```
Explicit Slack-native config is only needed when you want to override approvers, add filters, or
opt into origin-chat delivery:
```json5
{
channels: {
slack: {
execApprovals: {
enabled: true,
approvers: ["U12345678"],
target: "both",
},
approvals: {
exec: {
enabled: true,
targets: [{ channel: "slack", to: "U12345678" }],
},
},
}
```
Shared `approvals.exec` forwarding is separate. Use it only when approval prompts must also route
to other chats or explicit out-of-band targets.
Same-chat `/approve` also works in Slack channels and DMs that already support commands. See [Exec approvals](/tools/exec-approvals) for the full approval forwarding model.
## Troubleshooting

View File

@@ -810,7 +810,7 @@ openclaw message poll --channel telegram --target -1001234567890:topic:42 \
- `channels.telegram.execApprovals.target` (`dm` | `channel` | `both`, default: `dm`)
- `agentFilter`, `sessionFilter`
Approvers must be numeric Telegram user IDs. Telegram 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 the account's numeric owner config (`allowFrom` and direct-message `defaultTo`). Set `enabled: false` to disable Telegram as a native approval client explicitly. Approval requests otherwise fall back to other configured approval routes or the exec approval fallback policy.
Approvers must be numeric Telegram user IDs. Telegram becomes an exec approval client when `enabled` is true and at least one approver can be resolved, either from `execApprovals.approvers` or from the account's numeric owner config (`allowFrom` and direct-message `defaultTo`). Approval requests otherwise fall back to other configured approval routes or the exec approval fallback policy.
Telegram also renders the shared approval buttons used by other chat channels. The native Telegram adapter mainly adds approver DM routing, channel/topic fanout, and typing hints before delivery.

View File

@@ -45,48 +45,6 @@ openclaw approvals set --node <id|name|ip> --file ./exec-approvals.json
openclaw approvals set --gateway --file ./exec-approvals.json
```
## "Never prompt" / YOLO example
For a host that should never stop on exec approvals, set the host approvals defaults to `full` + `off`:
```bash
openclaw approvals set --stdin <<'EOF'
{
version: 1,
defaults: {
security: "full",
ask: "off",
askFallback: "full"
}
}
EOF
```
Node variant:
```bash
openclaw approvals set --node <id|name|ip> --stdin <<'EOF'
{
version: 1,
defaults: {
security: "full",
ask: "off",
askFallback: "full"
}
}
EOF
```
This changes the **host approvals file** only. To keep the requested OpenClaw policy aligned, also set:
```bash
openclaw config set tools.exec.host gateway
openclaw config set tools.exec.security full
openclaw config set tools.exec.ask off
```
This matches the current host-default YOLO behavior. Tighten it if you want approvals.
## Allowlist helpers
```bash

View File

@@ -90,7 +90,6 @@ Treat Gateway and node as one operator trust domain, with different roles:
- A caller authenticated to the Gateway is trusted at Gateway scope. After pairing, node actions are trusted operator actions on that node.
- `sessionKey` is routing/context selection, not per-user auth.
- Exec approvals (allowlist + ask) are guardrails for operator intent, not hostile multi-tenant isolation.
- OpenClaw's product default for trusted single-operator setups is that host exec on `gateway`/`node` is allowed without approval prompts (`security="full"`, `ask="off"` unless you tighten it). That default is intentional UX, not a vulnerability by itself.
- Exec approvals bind exact request context and best-effort direct local file operands; they do not semantically model every runtime/interpreter loader path. Use sandboxing and host isolation for strong boundaries.
If you need hostile-user isolation, split trust boundaries by OS user/host and run separate gateways.
@@ -174,7 +173,6 @@ If more than one person can DM your bot:
- **Inbound access** (DM policies, group policies, allowlists): can strangers trigger the bot?
- **Tool blast radius** (elevated tools + open rooms): could prompt injection turn into shell/file/network actions?
- **Exec approval drift** (`security=full`, `autoAllowSkills`, interpreter allowlists without `strictInlineEval`): are host-exec guardrails still doing what you think they are?
- `security="full"` is a broad posture warning, not proof of a bug. It is the chosen default for trusted personal-assistant setups; tighten it only when your threat model needs approval or allowlist guardrails.
- **Network exposure** (Gateway bind/auth, Tailscale Serve/Funnel, weak/short auth tokens).
- **Browser control exposure** (remote nodes, relay ports, remote CDP endpoints).
- **Local disk hygiene** (permissions, symlinks, config includes, “synced folder” paths).
@@ -377,7 +375,6 @@ If a macOS node is paired, the Gateway can invoke `system.run` on that node. Thi
- The Gateway applies a coarse global node command policy via `gateway.nodes.allowCommands` / `denyCommands`.
- Controlled on the Mac via **Settings → Exec approvals** (security + ask + allowlist).
- The per-node `system.run` policy is the node's own exec approvals file (`exec.approvals.node.*`), which can be stricter or looser than the gateway's global command-ID policy.
- A node running with `security="full"` and `ask="off"` is following the default trusted-operator model. Treat that as expected behavior unless your deployment explicitly requires a tighter approval or allowlist stance.
- Approval mode binds exact request context and, when possible, one concrete local script/file operand. If OpenClaw cannot identify exactly one direct local file for an interpreter/runtime command, approval-backed execution is denied rather than promising full semantic coverage.
- If you dont want remote execution, set security to **deny** and remove node pairing for that Mac.

View File

@@ -173,27 +173,6 @@ Quick answers plus deeper troubleshooting for real-world setups (local dev, VPS,
</Accordion>
<Accordion title="Why are there two exec approval configs for chat approvals?">
They control different layers:
- `approvals.exec`: forwards approval prompts to chat destinations
- `channels.<channel>.execApprovals`: makes that channel act as a native approval client
The host exec policy is still the real approval gate. Chat config only controls where approval
prompts appear and how people can answer them.
In most setups you do **not** need both:
- If the chat already supports commands and replies, same-chat `/approve` works through the shared path.
- If a supported native channel can infer approvers safely, OpenClaw now auto-enables DM-first native approvals when `channels.<channel>.execApprovals.enabled` is unset or `"auto"`.
- Use `approvals.exec` only when prompts must also be forwarded to other chats or explicit ops rooms.
- Use `channels.<channel>.execApprovals.target: "channel"` or `"both"` only when you explicitly want approval prompts posted back into the originating room/topic.
Short version: forwarding is for routing, native client config is for richer channel-specific UX.
See [Exec Approvals](/tools/exec-approvals).
</Accordion>
<Accordion title="What runtime do I need?">
Node **>= 22** is required. `pnpm` is recommended. Bun is **not recommended** for the Gateway.
</Accordion>

View File

@@ -498,15 +498,7 @@ If you want to rely on env keys (e.g. exported in your `~/.profile`), run local
These Docker runners split into two buckets:
- Live-model runners: `test:docker:live-models` and `test:docker:live-gateway` run only their matching profile-key live file inside the repo Docker image (`src/agents/models.profiles.live.test.ts` and `src/gateway/gateway-models.profiles.live.test.ts`), mounting your local config dir and workspace (and sourcing `~/.profile` if mounted). The matching local entrypoints are `test:live:models-profiles` and `test:live:gateway-profiles`.
- Docker live runners default to a smaller smoke cap so a full Docker sweep stays practical:
`test:docker:live-models` defaults to `OPENCLAW_LIVE_MAX_MODELS=12`, and
`test:docker:live-gateway` defaults to `OPENCLAW_LIVE_GATEWAY_SMOKE=1`,
`OPENCLAW_LIVE_GATEWAY_MAX_MODELS=8`,
`OPENCLAW_LIVE_GATEWAY_STEP_TIMEOUT_MS=45000`, and
`OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=90000`. Override those env vars when you
explicitly want the larger exhaustive scan.
- `test:docker:all` builds the live Docker image once via `test:docker:live-build`, then reuses it for the two live Docker lanes.
- Live-model runners: `test:docker:live-models` and `test:docker:live-gateway` run `pnpm test:live` inside the repo Docker image, mounting your local config dir and workspace (and sourcing `~/.profile` if mounted).
- Container smoke runners: `test:docker:openwebui`, `test:docker:onboard`, `test:docker:gateway-network`, `test:docker:mcp-channels`, and `test:docker:plugins` boot one or more real containers and verify higher-level integration paths.
The live-model Docker runners also bind-mount only the needed CLI auth homes (or all supported ones when the run is not narrowed), then copy them into the container home before the run so external-CLI OAuth can refresh tokens without mutating the host auth store:

View File

@@ -278,11 +278,11 @@ flowchart TD
- If `tools.exec.host` is unset, the default is `auto`.
- `host=auto` resolves to `sandbox` when a sandbox runtime is active, `gateway` otherwise.
- On `gateway` and `node`, unset `tools.exec.security` defaults to `full`.
- Unset `tools.exec.ask` defaults to `off`.
- Result: if you are seeing approvals, some host-local or per-session policy tightened exec away from the current defaults.
- On `gateway` and `node`, unset `tools.exec.security` defaults to `allowlist`.
- Unset `tools.exec.ask` defaults to `on-miss`.
- Result: ordinary host commands can now pause with `Approval required` instead of running immediately.
Restore current default no-approval behavior:
Restore the old gateway no-approval behavior:
```bash
openclaw config set tools.exec.host gateway
@@ -293,8 +293,8 @@ flowchart TD
Safer alternatives:
- Set only `tools.exec.host=gateway` if you just want stable host routing.
- Use `security=allowlist` with `ask=on-miss` if you want host exec but still want review on allowlist misses.
- Set only `tools.exec.host=gateway` if you just want stable host routing and still want approvals.
- Keep `security=allowlist` with `ask=on-miss` if you want host exec but still want review on allowlist misses.
- Enable sandbox mode if you want `host=auto` to resolve back to `sandbox`.
Common log signatures:

View File

@@ -167,21 +167,6 @@ See [Camera node](/nodes/camera) for parameters and CLI helpers.
- `sms.search`
- `motion.activity`, `motion.pedometer`
## Assistant entrypoints
Android supports launching OpenClaw from the system assistant trigger (Google
Assistant). When configured, holding the home button or saying "Hey Google, ask
OpenClaw..." opens the app and hands the prompt into the chat composer.
This uses Android **App Actions** metadata declared in the app manifest. No
extra configuration is needed on the gateway side -- the assistant intent is
handled entirely by the Android app and forwarded as a normal chat message.
<Note>
App Actions availability depends on the device, Google Play Services version,
and whether the user has set OpenClaw as the default assistant app.
</Note>
## Notification forwarding
Android can forward device notifications to the gateway as events. Several controls let you scope which notifications are forwarded and when.

View File

@@ -39,6 +39,7 @@ Scope intent:
- `plugins.entries.firecrawl.config.webSearch.apiKey`
- `plugins.entries.tavily.config.webSearch.apiKey`
- `tools.web.search.apiKey`
- `tools.web.x_search.apiKey`
- `gateway.auth.password`
- `gateway.auth.token`
- `gateway.remote.token`

View File

@@ -523,6 +523,13 @@
"path": "tools.web.search.apiKey",
"secretShape": "secret_input",
"optIn": true
},
{
"id": "tools.web.x_search.apiKey",
"configFile": "openclaw.json",
"path": "tools.web.x_search.apiKey",
"secretShape": "secret_input",
"optIn": true
}
]
}

View File

@@ -91,68 +91,6 @@ Example schema:
}
```
## No-approval "YOLO" mode
If you want host exec to run without approval prompts, you must open **both** policy layers:
- requested exec policy in OpenClaw config (`tools.exec.*`)
- host-local approvals policy in `~/.openclaw/exec-approvals.json`
This is now the default host behavior unless you tighten it explicitly:
- `tools.exec.security`: `full` on `gateway`/`node`
- `tools.exec.ask`: `off`
- host `askFallback`: `full`
If you want a more conservative setup, tighten either layer back to `allowlist` / `on-miss`
or `deny`.
Persistent gateway-host "never prompt" setup:
```bash
openclaw config set tools.exec.host gateway
openclaw config set tools.exec.security full
openclaw config set tools.exec.ask off
openclaw gateway restart
```
Then set the host approvals file to match:
```bash
openclaw approvals set --stdin <<'EOF'
{
version: 1,
defaults: {
security: "full",
ask: "off",
askFallback: "full"
}
}
EOF
```
For a node host, apply the same approvals file on that node instead:
```bash
openclaw approvals set --node <id|name|ip> --stdin <<'EOF'
{
version: 1,
defaults: {
security: "full",
ask: "off",
askFallback: "full"
}
}
EOF
```
Session-only shortcut:
- `/exec security=full ask=off` changes only the current session.
- `/elevated full` is a break-glass shortcut that also skips exec approvals for that session.
If the host approvals file stays stricter than config, the stricter host policy still wins.
## Policy knobs
### Security (`exec.security`)
@@ -485,46 +423,24 @@ resolved approver list for authorization even when native approval delivery is d
### Native approval delivery
Some channels can also act as native approval clients. Native clients add approver DMs, origin-chat
fanout, and channel-specific interactive approval UX on top of the shared same-chat `/approve`
flow.
Generic model:
- host exec policy still decides whether exec approval is required
- `approvals.exec` controls forwarding approval prompts to other chat destinations
- `channels.<channel>.execApprovals` controls whether that channel acts as a native approval client
Native approval clients auto-enable DM-first delivery when all of these are true:
- the channel supports native approval delivery
- approvers can be resolved from explicit `execApprovals.approvers` or existing owner config
- `channels.<channel>.execApprovals.enabled` is unset or `"auto"`
Set `enabled: false` to disable a native approval client explicitly. Set `enabled: true` to force
it on when approvers resolve. Public origin-chat delivery stays explicit through
`channels.<channel>.execApprovals.target`.
FAQ: [Why are there two exec approval configs for chat approvals?](/help/faq#why-are-there-two-exec-approval-configs-for-chat-approvals)
Discord, Slack, and Telegram can also act as native approval-delivery adapters with channel-specific config.
- Discord: `channels.discord.execApprovals.*`
- Slack: `channels.slack.execApprovals.*`
- Slack: uses shared `approvals.exec.targets` with `channel: "slack"` and renders Block Kit approval buttons when interactivity is enabled
- Telegram: `channels.telegram.execApprovals.*`
These native approval clients add DM routing and optional channel fanout on top of the shared
same-chat `/approve` flow and shared approval buttons.
These native delivery adapters are opt-in. They add DM routing and channel fanout on top of the
shared same-chat `/approve` flow and the shared approval buttons.
Shared behavior:
- Slack, Matrix, Microsoft Teams, and similar deliverable chats use the normal channel auth model
for same-chat `/approve`
- when a native approval client auto-enables, the default native delivery target is approver DMs
- for Discord and Telegram, only resolved approvers can approve or deny
- Discord and Telegram approvers can be explicit (`execApprovals.approvers`) or inferred from existing owner config (`allowFrom`, plus direct-message `defaultTo` where supported)
- Slack approvers can be explicit (`execApprovals.approvers`) or inferred from `commands.ownerAllowFrom`
- the requester does not need to be an approver
- the originating chat can approve directly with `/approve` when that chat already supports commands and replies
- when native `target` enables origin-chat delivery, approval prompts include the command text
- when channel delivery is enabled, approval prompts include the command text
- pending exec approvals expire after 30 minutes by default
- if no operator UI or configured approval client can accept the request, the prompt falls back to `askFallback`

View File

@@ -54,9 +54,8 @@ Notes:
- `tools.exec.notifyOnExit` (default: true): when true, backgrounded exec sessions enqueue a system event and request a heartbeat on exit.
- `tools.exec.approvalRunningNoticeMs` (default: 10000): emit a single “running” notice when an approval-gated exec runs longer than this (0 disables).
- `tools.exec.host` (default: `auto`; resolves to `sandbox` when sandbox runtime is active, `gateway` otherwise)
- `tools.exec.security` (default: `deny` for sandbox, `full` for gateway + node when unset)
- `tools.exec.ask` (default: `off`)
- No-approval host exec is the default for gateway + node. If you want approvals/allowlist behavior, tighten both `tools.exec.*` and the host `~/.openclaw/exec-approvals.json`; see [Exec approvals](/tools/exec-approvals#no-approval-yolo-mode).
- `tools.exec.security` (default: `deny` for sandbox, `allowlist` for gateway + node when unset)
- `tools.exec.ask` (default: `on-miss`)
- `tools.exec.node` (default: unset)
- `tools.exec.strictInlineEval` (default: false): when true, inline interpreter eval forms such as `python -c`, `node -e`, `ruby -e`, `perl -e`, `php -r`, `lua -e`, and `osascript -e` always require explicit approval. `allow-always` can still persist benign interpreter/script invocations, but inline-eval forms still prompt each time.
- `tools.exec.pathPrepend`: list of directories to prepend to `PATH` for exec runs (gateway + sandbox only).

View File

@@ -65,14 +65,18 @@ Notes:
entries: {
firecrawl: {
enabled: true,
config: {
webFetch: {
apiKey: "FIRECRAWL_API_KEY_HERE",
baseUrl: "https://api.firecrawl.dev",
onlyMainContent: true,
maxAgeMs: 172800000,
timeoutSeconds: 60,
},
},
},
},
tools: {
web: {
fetch: {
firecrawl: {
apiKey: "FIRECRAWL_API_KEY_HERE",
baseUrl: "https://api.firecrawl.dev",
onlyMainContent: true,
maxAgeMs: 172800000,
timeoutSeconds: 60,
},
},
},
@@ -83,11 +87,10 @@ Notes:
Notes:
- `firecrawl.enabled` defaults to `true` unless explicitly set to `false`.
- Firecrawl fallback attempts run only when an API key is available (`plugins.entries.firecrawl.config.webFetch.apiKey` or `FIRECRAWL_API_KEY`).
- Firecrawl fallback attempts run only when an API key is available (`tools.web.fetch.firecrawl.apiKey` or `FIRECRAWL_API_KEY`).
- `maxAgeMs` controls how old cached results can be (ms). Default is 2 days.
- Legacy `tools.web.fetch.firecrawl.*` config is auto-migrated by `openclaw doctor --fix`.
`firecrawl_scrape` reuses the same `plugins.entries.firecrawl.config.webFetch.*` settings and env vars.
`firecrawl_scrape` reuses the same `tools.web.fetch.firecrawl.*` settings and env vars.
## Firecrawl plugin tools

View File

@@ -82,18 +82,16 @@ If Readability extraction fails, `web_fetch` can fall back to
```json5
{
plugins: {
entries: {
firecrawl: {
enabled: true,
config: {
webFetch: {
apiKey: "fc-...", // optional if FIRECRAWL_API_KEY is set
baseUrl: "https://api.firecrawl.dev",
onlyMainContent: true,
maxAgeMs: 86400000, // cache duration (1 day)
timeoutSeconds: 60,
},
tools: {
web: {
fetch: {
firecrawl: {
enabled: true,
apiKey: "fc-...", // optional if FIRECRAWL_API_KEY is set
baseUrl: "https://api.firecrawl.dev",
onlyMainContent: true,
maxAgeMs: 86400000, // cache duration (1 day)
timeoutSeconds: 60,
},
},
},
@@ -101,8 +99,7 @@ If Readability extraction fails, `web_fetch` can fall back to
}
```
`plugins.entries.firecrawl.config.webFetch.apiKey` supports SecretRef objects.
Legacy `tools.web.fetch.firecrawl.*` config is auto-migrated by `openclaw doctor --fix`.
`tools.web.fetch.firecrawl.apiKey` supports SecretRef objects.
<Note>
If Firecrawl is enabled and its SecretRef is unresolved with no

View File

@@ -193,9 +193,8 @@ Provider-specific config (API keys, base URLs, modes) lives under
`plugins.entries.<plugin>.config.webSearch.*`. See the provider pages for
examples.
For `x_search`, configure `plugins.entries.xai.config.xSearch.*`. It uses the
same `XAI_API_KEY` fallback as Grok web search.
Legacy `tools.web.x_search.*` config is auto-migrated by `openclaw doctor --fix`.
For `x_search`, configure `tools.web.x_search.*` directly. It uses the same
`XAI_API_KEY` fallback as Grok web search.
When you choose Grok during `openclaw onboard` or `openclaw configure --section web`,
OpenClaw can also offer optional `x_search` setup with the same key.
This is a separate follow-up step inside the Grok path, not a separate top-level
@@ -281,22 +280,16 @@ tool on the request that serves this tool call.
```json5
{
plugins: {
entries: {
xai: {
config: {
xSearch: {
enabled: true,
model: "grok-4-1-fast-non-reasoning",
inlineCitations: false,
maxTurns: 2,
timeoutSeconds: 30,
cacheTtlMinutes: 15,
},
webSearch: {
apiKey: "xai-...", // optional if XAI_API_KEY is set
},
},
tools: {
web: {
x_search: {
enabled: true,
apiKey: "xai-...", // optional if XAI_API_KEY is set
model: "grok-4-1-fast-non-reasoning",
inlineCitations: false,
maxTurns: 2,
timeoutSeconds: 30,
cacheTtlMinutes: 15,
},
},
},

View File

@@ -1,23 +0,0 @@
import { describe, expect, it } from "vitest";
import { BrowserValidationError, toBrowserErrorResponse } from "./errors.js";
describe("browser error mapping", () => {
it("maps blocked browser targets to conflict responses", () => {
const err = new Error(
"Browser target is unavailable after SSRF policy blocked its navigation.",
);
err.name = "BlockedBrowserTargetError";
expect(toBrowserErrorResponse(err)).toEqual({
status: 409,
message: "Browser target is unavailable after SSRF policy blocked its navigation.",
});
});
it("preserves BrowserError mappings", () => {
expect(toBrowserErrorResponse(new BrowserValidationError("bad input"))).toEqual({
status: 400,
message: "bad input",
});
});
});

View File

@@ -72,9 +72,6 @@ export function toBrowserErrorResponse(err: unknown): {
if (err instanceof BrowserError) {
return { status: err.status, message: err.message };
}
if (err instanceof Error && err.name === "BlockedBrowserTargetError") {
return { status: 409, message: err.message };
}
if (err instanceof SsrFBlockedError) {
return { status: 400, message: err.message };
}

View File

@@ -2,49 +2,19 @@ import { chromium } from "playwright-core";
import { afterEach, describe, expect, it, vi } from "vitest";
import { SsrFBlockedError } from "../infra/net/ssrf.js";
import * as chromeModule from "./chrome.js";
import { BrowserTabNotFoundError } from "./errors.js";
import { InvalidBrowserNavigationUrlError } from "./navigation-guard.js";
import * as navigationGuardModule from "./navigation-guard.js";
import {
BlockedBrowserTargetError,
closePlaywrightBrowserConnection,
createPageViaPlaywright,
forceDisconnectPlaywrightForTarget,
getPageForTargetId,
gotoPageWithNavigationGuard,
listPagesViaPlaywright,
} from "./pw-session.js";
import { closePlaywrightBrowserConnection, createPageViaPlaywright } from "./pw-session.js";
const connectOverCdpSpy = vi.spyOn(chromium, "connectOverCDP");
const getChromeWebSocketUrlSpy = vi.spyOn(chromeModule, "getChromeWebSocketUrl");
function installBrowserMocks() {
const pageOn = vi.fn();
let routeHandler:
| ((
route: { continue: () => Promise<void>; abort: () => Promise<void> },
request: unknown,
) => Promise<void>)
| null = null;
const pageGoto = vi.fn<
(...args: unknown[]) => Promise<null | { request: () => Record<string, unknown> }>
>(async () => null);
const pageTitle = vi.fn(async () => "");
const pageUrl = vi.fn(() => "about:blank");
const pageRoute = vi.fn(async (_pattern: string, handler: typeof routeHandler) => {
routeHandler = handler;
});
const pageUnroute = vi.fn(async () => {
routeHandler = null;
});
const openPages: import("playwright-core").Page[] = [];
const pageClose = vi.fn(async () => {
const index = openPages.indexOf(page);
if (index >= 0) {
openPages.splice(index, 1);
}
});
const mainFrame = {};
const contextOn = vi.fn();
const browserOn = vi.fn();
const browserClose = vi.fn(async () => {});
@@ -57,12 +27,9 @@ function installBrowserMocks() {
const sessionDetach = vi.fn(async () => {});
const context = {
pages: () => openPages,
pages: () => [],
on: contextOn,
newPage: vi.fn(async () => {
openPages.push(page);
return page;
}),
newPage: vi.fn(async () => page),
newCDPSession: vi.fn(async () => ({
send: sessionSend,
detach: sessionDetach,
@@ -75,10 +42,6 @@ function installBrowserMocks() {
goto: pageGoto,
title: pageTitle,
url: pageUrl,
route: pageRoute,
unroute: pageUnroute,
close: pageClose,
mainFrame: () => mainFrame,
} as unknown as import("playwright-core").Page;
const browser = {
@@ -90,24 +53,7 @@ function installBrowserMocks() {
connectOverCdpSpy.mockResolvedValue(browser);
getChromeWebSocketUrlSpy.mockResolvedValue(null);
const getBrowserDisconnectedHandler = () =>
browserOn.mock.calls.find((call) => call[0] === "disconnected")?.[1] as
| (() => void)
| undefined;
return {
pageGoto,
browserClose,
pageClose,
sessionSend,
getBrowserDisconnectedHandler,
getRouteHandler: () => routeHandler,
mainFrame,
pushOpenPage: () => {
openPages.push(page);
return page;
},
};
return { pageGoto, browserClose };
}
afterEach(async () => {
@@ -143,252 +89,18 @@ describe("pw-session createPageViaPlaywright navigation guard", () => {
});
it("blocks private intermediate redirect hops", async () => {
const { pageGoto, pageClose, getRouteHandler, mainFrame } = installBrowserMocks();
pageGoto.mockImplementationOnce(async () => {
const handler = getRouteHandler();
if (!handler) {
throw new Error("missing route handler");
}
await handler(
{ continue: vi.fn(async () => {}), abort: vi.fn(async () => {}) },
{
isNavigationRequest: () => true,
frame: () => mainFrame,
url: () => "https://93.184.216.34/start",
},
);
await handler(
{ continue: vi.fn(async () => {}), abort: vi.fn(async () => {}) },
{
isNavigationRequest: () => true,
frame: () => mainFrame,
const { pageGoto } = installBrowserMocks();
pageGoto.mockResolvedValueOnce({
request: () => ({
url: () => "https://93.184.216.34/final",
redirectedFrom: () => ({
url: () => "http://127.0.0.1:18080/internal-hop",
},
);
throw new Error("Navigation aborted");
});
await expect(
createPageViaPlaywright({
cdpUrl: "http://127.0.0.1:18792",
url: "https://93.184.216.34/start",
}),
).rejects.toBeInstanceOf(SsrFBlockedError);
expect(pageGoto).toHaveBeenCalledTimes(1);
expect(pageClose).toHaveBeenCalledTimes(1);
});
it("preserves the created tab on ordinary navigation failure", async () => {
const { pageGoto, pageClose } = installBrowserMocks();
pageGoto.mockRejectedValueOnce(new Error("page.goto: net::ERR_NAME_NOT_RESOLVED"));
const created = await createPageViaPlaywright({
cdpUrl: "http://127.0.0.1:18792",
url: "https://93.184.216.34/start",
});
expect(created.targetId).toBe("TARGET_1");
expect(created.url).toBe("about:blank");
expect(pageGoto).toHaveBeenCalledTimes(1);
expect(pageClose).not.toHaveBeenCalled();
});
it("does not quarantine a tab when route.continue fails", async () => {
const { pageGoto, pageClose, getRouteHandler, mainFrame } = installBrowserMocks();
pageGoto.mockImplementationOnce(async () => {
const handler = getRouteHandler();
if (!handler) {
throw new Error("missing route handler");
}
await handler(
{
continue: vi.fn(async () => {
throw new Error("page.goto: Frame has been detached");
}),
abort: vi.fn(async () => {}),
},
{
isNavigationRequest: () => true,
frame: () => mainFrame,
url: () => "https://example.com",
},
);
throw new Error("page.goto: Frame has been detached");
});
const created = await createPageViaPlaywright({
cdpUrl: "http://127.0.0.1:18792",
url: "https://example.com",
});
expect(created.targetId).toBe("TARGET_1");
expect(pageGoto).toHaveBeenCalledTimes(1);
expect(pageClose).not.toHaveBeenCalled();
});
it("propagates unsupported redirect protocols as navigation errors", async () => {
const { pageGoto, pageClose, getRouteHandler, mainFrame } = installBrowserMocks();
pageGoto.mockImplementationOnce(async () => {
const handler = getRouteHandler();
if (!handler) {
throw new Error("missing route handler");
}
await handler(
{ continue: vi.fn(async () => {}), abort: vi.fn(async () => {}) },
{
isNavigationRequest: () => true,
frame: () => mainFrame,
url: () => "https://93.184.216.34/start",
},
);
await handler(
{ continue: vi.fn(async () => {}), abort: vi.fn(async () => {}) },
{
isNavigationRequest: () => true,
frame: () => mainFrame,
url: () => "file:///etc/passwd",
},
);
throw new Error("Navigation aborted");
});
await expect(
createPageViaPlaywright({
cdpUrl: "http://127.0.0.1:18792",
url: "https://93.184.216.34/start",
}),
).rejects.toBeInstanceOf(InvalidBrowserNavigationUrlError);
expect(pageGoto).toHaveBeenCalledTimes(1);
expect(pageClose).toHaveBeenCalledTimes(1);
});
it("does not quarantine a tab on transient redirect lookup errors", async () => {
const { pageGoto, pageClose, getRouteHandler, mainFrame } = installBrowserMocks();
const assertNavigationAllowedSpy = vi.spyOn(
navigationGuardModule,
"assertBrowserNavigationAllowed",
);
assertNavigationAllowedSpy.mockImplementation(async (opts: { url: string }) => {
if (opts.url === "http://127.0.0.1:18080/internal-hop") {
throw new Error("getaddrinfo EAI_AGAIN internal-hop");
}
});
pageGoto.mockImplementationOnce(async () => {
const handler = getRouteHandler();
if (!handler) {
throw new Error("missing route handler");
}
await handler(
{ continue: vi.fn(async () => {}), abort: vi.fn(async () => {}) },
{
isNavigationRequest: () => true,
frame: () => mainFrame,
url: () => "https://93.184.216.34/start",
},
);
await handler(
{ continue: vi.fn(async () => {}), abort: vi.fn(async () => {}) },
{
isNavigationRequest: () => true,
frame: () => mainFrame,
url: () => "http://127.0.0.1:18080/internal-hop",
},
);
throw new Error("Navigation aborted");
});
try {
const created = await createPageViaPlaywright({
cdpUrl: "http://127.0.0.1:18792",
url: "https://93.184.216.34/start",
});
const pages = await listPagesViaPlaywright({ cdpUrl: "http://127.0.0.1:18792" });
expect(created.targetId).toBe("TARGET_1");
expect(pages).toHaveLength(1);
expect(pageClose).not.toHaveBeenCalled();
} finally {
assertNavigationAllowedSpy.mockRestore();
}
});
it("does not quarantine a tab on transient post-navigation check errors", async () => {
const { pageGoto, pageClose, getRouteHandler, mainFrame } = installBrowserMocks();
const assertRedirectChainAllowedSpy = vi.spyOn(
navigationGuardModule,
"assertBrowserNavigationRedirectChainAllowed",
);
assertRedirectChainAllowedSpy.mockRejectedValueOnce(
new Error("getaddrinfo EAI_AGAIN postcheck.example"),
);
pageGoto.mockImplementationOnce(async () => {
const handler = getRouteHandler();
if (!handler) {
throw new Error("missing route handler");
}
await handler(
{ continue: vi.fn(async () => {}), abort: vi.fn(async () => {}) },
{
isNavigationRequest: () => true,
frame: () => mainFrame,
url: () => "https://93.184.216.34/start",
},
);
return {
request: () => ({
url: () => "https://93.184.216.34/final",
redirectedFrom: () => ({
url: () => "https://postcheck.example/hop",
url: () => "https://93.184.216.34/start",
redirectedFrom: () => null,
}),
}),
};
});
try {
await expect(
createPageViaPlaywright({
cdpUrl: "http://127.0.0.1:18792",
url: "https://93.184.216.34/start",
}),
).rejects.toThrow(/getaddrinfo .*postcheck\.example/);
const pages = await listPagesViaPlaywright({ cdpUrl: "http://127.0.0.1:18792" });
expect(pages).toHaveLength(1);
expect(pages[0]?.targetId).toBe("TARGET_1");
expect(pageClose).not.toHaveBeenCalled();
} finally {
assertRedirectChainAllowedSpy.mockRestore();
}
});
it("keeps blocked tab quarantined if close fails", async () => {
const { pageGoto, pageClose, getRouteHandler, mainFrame } = installBrowserMocks();
pageClose.mockRejectedValueOnce(new Error("close failed"));
pageGoto.mockImplementationOnce(async () => {
const handler = getRouteHandler();
if (!handler) {
throw new Error("missing route handler");
}
await handler(
{ continue: vi.fn(async () => {}), abort: vi.fn(async () => {}) },
{
isNavigationRequest: () => true,
frame: () => mainFrame,
url: () => "https://93.184.216.34/start",
},
);
await handler(
{ continue: vi.fn(async () => {}), abort: vi.fn(async () => {}) },
{
isNavigationRequest: () => true,
frame: () => mainFrame,
url: () => "http://127.0.0.1:18080/internal-hop",
},
);
throw new Error("Navigation aborted");
}),
});
await expect(
@@ -397,326 +109,5 @@ describe("pw-session createPageViaPlaywright navigation guard", () => {
url: "https://93.184.216.34/start",
}),
).rejects.toBeInstanceOf(SsrFBlockedError);
const pages = await listPagesViaPlaywright({ cdpUrl: "http://127.0.0.1:18792" });
expect(pages).toHaveLength(0);
await expect(
getPageForTargetId({
cdpUrl: "http://127.0.0.1:18792",
targetId: "TARGET_1",
}),
).rejects.toBeInstanceOf(BlockedBrowserTargetError);
await expect(
getPageForTargetId({
cdpUrl: "http://127.0.0.1:18792",
}),
).rejects.toBeInstanceOf(BlockedBrowserTargetError);
expect(pageClose).toHaveBeenCalledTimes(1);
});
it("preserves blocked-target quarantine across forced reconnects", async () => {
const { pageGoto, pageClose, getRouteHandler, mainFrame } = installBrowserMocks();
pageClose.mockRejectedValueOnce(new Error("close failed"));
pageGoto.mockImplementationOnce(async () => {
const handler = getRouteHandler();
if (!handler) {
throw new Error("missing route handler");
}
await handler(
{ continue: vi.fn(async () => {}), abort: vi.fn(async () => {}) },
{
isNavigationRequest: () => true,
frame: () => mainFrame,
url: () => "https://93.184.216.34/start",
},
);
await handler(
{ continue: vi.fn(async () => {}), abort: vi.fn(async () => {}) },
{
isNavigationRequest: () => true,
frame: () => mainFrame,
url: () => "http://127.0.0.1:18080/internal-hop",
},
);
throw new Error("Navigation aborted");
});
await expect(
createPageViaPlaywright({
cdpUrl: "http://127.0.0.1:18792",
url: "https://93.184.216.34/start",
}),
).rejects.toBeInstanceOf(SsrFBlockedError);
await forceDisconnectPlaywrightForTarget({
cdpUrl: "http://127.0.0.1:18792",
reason: "test forced reconnect",
});
await expect(
getPageForTargetId({
cdpUrl: "http://127.0.0.1:18792",
targetId: "TARGET_1",
}),
).rejects.toBeInstanceOf(BlockedBrowserTargetError);
});
it("preserves blocked-target quarantine across transport disconnects", async () => {
const { pageGoto, pageClose, getBrowserDisconnectedHandler, getRouteHandler, mainFrame } =
installBrowserMocks();
pageClose.mockRejectedValueOnce(new Error("close failed"));
pageGoto.mockImplementationOnce(async () => {
const handler = getRouteHandler();
if (!handler) {
throw new Error("missing route handler");
}
await handler(
{ continue: vi.fn(async () => {}), abort: vi.fn(async () => {}) },
{
isNavigationRequest: () => true,
frame: () => mainFrame,
url: () => "https://93.184.216.34/start",
},
);
await handler(
{ continue: vi.fn(async () => {}), abort: vi.fn(async () => {}) },
{
isNavigationRequest: () => true,
frame: () => mainFrame,
url: () => "http://127.0.0.1:18080/internal-hop",
},
);
throw new Error("Navigation aborted");
});
await expect(
createPageViaPlaywright({
cdpUrl: "http://127.0.0.1:18792",
url: "https://93.184.216.34/start",
}),
).rejects.toBeInstanceOf(SsrFBlockedError);
const disconnectedHandler = getBrowserDisconnectedHandler();
expect(disconnectedHandler).toBeTypeOf("function");
disconnectedHandler?.();
await expect(
getPageForTargetId({
cdpUrl: "http://127.0.0.1:18792",
targetId: "TARGET_1",
}),
).rejects.toBeInstanceOf(BlockedBrowserTargetError);
});
it("keeps blocked tabs inaccessible when target lookup fails", async () => {
const { pageGoto, pageClose, sessionSend, getRouteHandler, mainFrame } = installBrowserMocks();
pageClose.mockRejectedValueOnce(new Error("close failed"));
pageGoto.mockImplementationOnce(async () => {
const handler = getRouteHandler();
if (!handler) {
throw new Error("missing route handler");
}
await handler(
{ continue: vi.fn(async () => {}), abort: vi.fn(async () => {}) },
{
isNavigationRequest: () => true,
frame: () => mainFrame,
url: () => "https://93.184.216.34/start",
},
);
await handler(
{ continue: vi.fn(async () => {}), abort: vi.fn(async () => {}) },
{
isNavigationRequest: () => true,
frame: () => mainFrame,
url: () => "http://127.0.0.1:18080/internal-hop",
},
);
throw new Error("Navigation aborted");
});
await expect(
createPageViaPlaywright({
cdpUrl: "http://127.0.0.1:18792",
url: "https://93.184.216.34/start",
}),
).rejects.toBeInstanceOf(SsrFBlockedError);
sessionSend.mockRejectedValueOnce(new Error("Target lookup failed"));
await expect(
getPageForTargetId({
cdpUrl: "http://127.0.0.1:18792",
}),
).rejects.toBeInstanceOf(BlockedBrowserTargetError);
});
it("does not fall back to another tab when explicit target lookup misses", async () => {
const { pageGoto, pageClose, sessionSend, getRouteHandler, mainFrame } = installBrowserMocks();
pageClose.mockRejectedValueOnce(new Error("close failed"));
pageGoto.mockImplementationOnce(async () => {
const handler = getRouteHandler();
if (!handler) {
throw new Error("missing route handler");
}
await handler(
{ continue: vi.fn(async () => {}), abort: vi.fn(async () => {}) },
{
isNavigationRequest: () => true,
frame: () => mainFrame,
url: () => "https://93.184.216.34/start",
},
);
await handler(
{ continue: vi.fn(async () => {}), abort: vi.fn(async () => {}) },
{
isNavigationRequest: () => true,
frame: () => mainFrame,
url: () => "http://127.0.0.1:18080/internal-hop",
},
);
throw new Error("Navigation aborted");
});
await expect(
createPageViaPlaywright({
cdpUrl: "http://127.0.0.1:18792",
url: "https://93.184.216.34/start",
}),
).rejects.toBeInstanceOf(SsrFBlockedError);
sessionSend.mockImplementationOnce(async (method: string) => {
if (method === "Target.getTargetInfo") {
return { targetInfo: { targetId: "TARGET_2" } };
}
return {};
});
await createPageViaPlaywright({
cdpUrl: "http://127.0.0.1:18792",
url: "https://example.com",
});
let targetInfoLookups = 0;
sessionSend.mockImplementation(async (method: string) => {
if (method === "Target.getTargetInfo") {
targetInfoLookups += 1;
return {
targetInfo: { targetId: targetInfoLookups % 2 === 1 ? "TARGET_1" : "TARGET_2" },
};
}
return {};
});
await expect(
getPageForTargetId({
cdpUrl: "http://127.0.0.1:18792",
targetId: "MISSING_TARGET",
}),
).rejects.toBeInstanceOf(BrowserTabNotFoundError);
});
it("quarantines the actual page when blocked navigation receives a stale target id", async () => {
const { pageGoto, pageClose, sessionSend, getRouteHandler, mainFrame } = installBrowserMocks();
pageClose.mockRejectedValueOnce(new Error("close failed"));
await createPageViaPlaywright({
cdpUrl: "http://127.0.0.1:18792",
url: "about:blank",
});
const page = await getPageForTargetId({
cdpUrl: "http://127.0.0.1:18792",
targetId: "MISSING_TARGET",
});
pageGoto.mockImplementationOnce(async () => {
const handler = getRouteHandler();
if (!handler) {
throw new Error("missing route handler");
}
await handler(
{ continue: vi.fn(async () => {}), abort: vi.fn(async () => {}) },
{
isNavigationRequest: () => true,
frame: () => mainFrame,
url: () => "http://127.0.0.1:18080/internal-hop",
},
);
throw new Error("Navigation aborted");
});
// Simulate target-info churn while quarantining so caller target id cannot be trusted.
sessionSend.mockRejectedValueOnce(new Error("Target lookup failed"));
await expect(
gotoPageWithNavigationGuard({
cdpUrl: "http://127.0.0.1:18792",
page,
url: "https://93.184.216.34/start",
timeoutMs: 1000,
targetId: "MISSING_TARGET",
}),
).rejects.toBeInstanceOf(SsrFBlockedError);
await expect(
getPageForTargetId({
cdpUrl: "http://127.0.0.1:18792",
}),
).rejects.toBeInstanceOf(BlockedBrowserTargetError);
});
it("falls back to caller targetId quarantine when target lookup fails", async () => {
const first = installBrowserMocks();
first.pageClose.mockRejectedValueOnce(new Error("close failed"));
await createPageViaPlaywright({
cdpUrl: "http://127.0.0.1:18792",
url: "about:blank",
});
const page = await getPageForTargetId({
cdpUrl: "http://127.0.0.1:18792",
targetId: "TARGET_1",
});
first.pageGoto.mockImplementationOnce(async () => {
const handler = first.getRouteHandler();
if (!handler) {
throw new Error("missing route handler");
}
await handler(
{ continue: vi.fn(async () => {}), abort: vi.fn(async () => {}) },
{
isNavigationRequest: () => true,
frame: () => first.mainFrame,
url: () => "http://127.0.0.1:18080/internal-hop",
},
);
throw new Error("Navigation aborted");
});
first.sessionSend.mockRejectedValueOnce(new Error("Target lookup failed"));
await expect(
gotoPageWithNavigationGuard({
cdpUrl: "http://127.0.0.1:18792",
page,
url: "https://93.184.216.34/start",
timeoutMs: 1000,
targetId: "TARGET_1",
}),
).rejects.toBeInstanceOf(SsrFBlockedError);
await forceDisconnectPlaywrightForTarget({
cdpUrl: "http://127.0.0.1:18792",
reason: "test reconnect after blocked navigation",
});
const second = installBrowserMocks();
second.pushOpenPage();
await expect(
getPageForTargetId({
cdpUrl: "http://127.0.0.1:18792",
targetId: "TARGET_1",
}),
).rejects.toBeInstanceOf(BlockedBrowserTargetError);
});
});

View File

@@ -5,11 +5,10 @@ import type {
Page,
Request,
Response,
Route,
} from "playwright-core";
import { chromium } from "playwright-core";
import { formatErrorMessage } from "../infra/errors.js";
import { SsrFBlockedError, type SsrFPolicy } from "../infra/net/ssrf.js";
import type { SsrFPolicy } from "../infra/net/ssrf.js";
import { withNoProxyForCdpUrl } from "./cdp-proxy-bypass.js";
import {
appendCdpPath,
@@ -25,7 +24,6 @@ import {
assertBrowserNavigationAllowed,
assertBrowserNavigationRedirectChainAllowed,
assertBrowserNavigationResultAllowed,
InvalidBrowserNavigationUrlError,
withBrowserNavigationPolicy,
} from "./navigation-guard.js";
import { withPageScopedCdpClient } from "./pw-session.page-cdp.js";
@@ -120,8 +118,6 @@ const MAX_NETWORK_REQUESTS = 500;
const cachedByCdpUrl = new Map<string, ConnectedBrowser>();
const connectingByCdpUrl = new Map<string, Promise<ConnectedBrowser>>();
const blockedTargetsByCdpUrl = new Set<string>();
const blockedPageRefsByCdpUrl = new Map<string, WeakSet<Page>>();
function normalizeCdpUrl(raw: string) {
return raw.replace(/\/$/, "");
@@ -137,97 +133,8 @@ function findNetworkRequestById(state: PageState, id: string): BrowserNetworkReq
return undefined;
}
function targetKey(cdpUrl: string, targetId: string) {
return `${normalizeCdpUrl(cdpUrl)}::${targetId}`;
}
function roleRefsKey(cdpUrl: string, targetId: string) {
return targetKey(cdpUrl, targetId);
}
function isBlockedTarget(cdpUrl: string, targetId?: string): boolean {
const normalizedTargetId = targetId?.trim() || "";
if (!normalizedTargetId) {
return false;
}
return blockedTargetsByCdpUrl.has(targetKey(cdpUrl, normalizedTargetId));
}
function markTargetBlocked(cdpUrl: string, targetId?: string): void {
const normalizedTargetId = targetId?.trim() || "";
if (!normalizedTargetId) {
return;
}
blockedTargetsByCdpUrl.add(targetKey(cdpUrl, normalizedTargetId));
}
function clearBlockedTarget(cdpUrl: string, targetId?: string): void {
const normalizedTargetId = targetId?.trim() || "";
if (!normalizedTargetId) {
return;
}
blockedTargetsByCdpUrl.delete(targetKey(cdpUrl, normalizedTargetId));
}
function clearBlockedTargetsForCdpUrl(cdpUrl?: string): void {
if (!cdpUrl) {
blockedTargetsByCdpUrl.clear();
return;
}
const prefix = `${normalizeCdpUrl(cdpUrl)}::`;
for (const key of blockedTargetsByCdpUrl) {
if (key.startsWith(prefix)) {
blockedTargetsByCdpUrl.delete(key);
}
}
}
function blockedPageRefsForCdpUrl(cdpUrl: string): WeakSet<Page> {
const normalized = normalizeCdpUrl(cdpUrl);
const existing = blockedPageRefsByCdpUrl.get(normalized);
if (existing) {
return existing;
}
const created = new WeakSet<Page>();
blockedPageRefsByCdpUrl.set(normalized, created);
return created;
}
function isBlockedPageRef(cdpUrl: string, page: Page): boolean {
return blockedPageRefsByCdpUrl.get(normalizeCdpUrl(cdpUrl))?.has(page) ?? false;
}
function markPageRefBlocked(cdpUrl: string, page: Page): void {
blockedPageRefsForCdpUrl(cdpUrl).add(page);
}
function clearBlockedPageRefsForCdpUrl(cdpUrl?: string): void {
if (!cdpUrl) {
blockedPageRefsByCdpUrl.clear();
return;
}
blockedPageRefsByCdpUrl.delete(normalizeCdpUrl(cdpUrl));
}
function clearBlockedPageRef(cdpUrl: string, page: Page): void {
blockedPageRefsByCdpUrl.get(normalizeCdpUrl(cdpUrl))?.delete(page);
}
function hasBlockedTargetsForCdpUrl(cdpUrl: string): boolean {
const prefix = `${normalizeCdpUrl(cdpUrl)}::`;
for (const key of blockedTargetsByCdpUrl) {
if (key.startsWith(prefix)) {
return true;
}
}
return false;
}
export class BlockedBrowserTargetError extends Error {
constructor() {
super("Browser target is unavailable after SSRF policy blocked its navigation.");
this.name = "BlockedBrowserTargetError";
}
return `${normalizeCdpUrl(cdpUrl)}::${targetId}`;
}
export function rememberRoleRefsForTarget(opts: {
@@ -488,37 +395,6 @@ async function getAllPages(browser: Browser): Promise<Page[]> {
return pages;
}
async function partitionAccessiblePages(opts: {
cdpUrl: string;
pages: Page[];
}): Promise<{ accessible: Page[]; blockedCount: number }> {
const accessible: Page[] = [];
let blockedCount = 0;
for (const page of opts.pages) {
if (isBlockedPageRef(opts.cdpUrl, page)) {
blockedCount += 1;
continue;
}
const targetId = await pageTargetId(page).catch(() => null);
// Fail closed when we cannot resolve a target id while this session has
// quarantined targets; otherwise a blocked tab can become selectable.
if (!targetId) {
if (hasBlockedTargetsForCdpUrl(opts.cdpUrl)) {
blockedCount += 1;
continue;
}
accessible.push(page);
continue;
}
if (isBlockedTarget(opts.cdpUrl, targetId)) {
blockedCount += 1;
continue;
}
accessible.push(page);
}
return { accessible, blockedCount };
}
async function pageTargetId(page: Page): Promise<string | null> {
const session = await page.context().newCDPSession(page);
try {
@@ -608,9 +484,6 @@ async function resolvePageByTargetIdOrThrow(opts: {
cdpUrl: string;
targetId: string;
}): Promise<Page> {
if (isBlockedTarget(opts.cdpUrl, opts.targetId)) {
throw new BlockedBrowserTargetError();
}
const { browser } = await connectBrowser(opts.cdpUrl);
const page = await findPageByTargetId(browser, opts.targetId, opts.cdpUrl);
if (!page) {
@@ -623,165 +496,24 @@ export async function getPageForTargetId(opts: {
cdpUrl: string;
targetId?: string;
}): Promise<Page> {
if (opts.targetId && isBlockedTarget(opts.cdpUrl, opts.targetId)) {
throw new BlockedBrowserTargetError();
}
const { browser } = await connectBrowser(opts.cdpUrl);
const pages = await getAllPages(browser);
if (!pages.length) {
throw new Error("No pages available in the connected browser.");
}
const { accessible, blockedCount } = await partitionAccessiblePages({
cdpUrl: opts.cdpUrl,
pages,
});
if (!accessible.length) {
if (blockedCount > 0) {
throw new BlockedBrowserTargetError();
}
throw new Error("No pages available in the connected browser.");
}
const first = accessible[0];
const first = pages[0];
if (!opts.targetId) {
return first;
}
const found = await findPageByTargetId(browser, opts.targetId, opts.cdpUrl);
if (found) {
if (isBlockedPageRef(opts.cdpUrl, found)) {
throw new BlockedBrowserTargetError();
}
const foundTargetId = await pageTargetId(found).catch(() => null);
if (foundTargetId && isBlockedTarget(opts.cdpUrl, foundTargetId)) {
throw new BlockedBrowserTargetError();
}
return found;
}
// If Playwright only exposes a single Page total, use it as a best-effort fallback.
if (pages.length === 1) {
return first;
}
throw new BrowserTabNotFoundError();
}
function isTopLevelNavigationRequest(page: Page, request: Request): boolean {
if (!request.isNavigationRequest()) {
return false;
}
try {
return request.frame() === page.mainFrame();
} catch {
return true;
}
}
function isPolicyDenyNavigationError(err: unknown): boolean {
return err instanceof SsrFBlockedError || err instanceof InvalidBrowserNavigationUrlError;
}
async function closeBlockedNavigationTarget(opts: {
cdpUrl: string;
page: Page;
targetId?: string;
}): Promise<void> {
// Quarantine the concrete page first; then persist by target id when available.
markPageRefBlocked(opts.cdpUrl, opts.page);
const resolvedTargetId = await pageTargetId(opts.page).catch(() => null);
const fallbackTargetId = opts.targetId?.trim() || "";
const targetIdToBlock = resolvedTargetId || fallbackTargetId;
if (targetIdToBlock) {
markTargetBlocked(opts.cdpUrl, targetIdToBlock);
}
await opts.page.close().catch(() => {});
}
export async function assertPageNavigationCompletedSafely(opts: {
cdpUrl: string;
page: Page;
response: Response | null;
ssrfPolicy?: SsrFPolicy;
targetId?: string;
}): Promise<void> {
const navigationPolicy = withBrowserNavigationPolicy(opts.ssrfPolicy);
try {
await assertBrowserNavigationRedirectChainAllowed({
request: opts.response?.request(),
...navigationPolicy,
});
await assertBrowserNavigationResultAllowed({
url: opts.page.url(),
...navigationPolicy,
});
} catch (err) {
if (isPolicyDenyNavigationError(err)) {
await closeBlockedNavigationTarget({
cdpUrl: opts.cdpUrl,
page: opts.page,
targetId: opts.targetId,
});
}
throw err;
}
}
export async function gotoPageWithNavigationGuard(opts: {
cdpUrl: string;
page: Page;
url: string;
timeoutMs: number;
ssrfPolicy?: SsrFPolicy;
targetId?: string;
}): Promise<Response | null> {
const navigationPolicy = withBrowserNavigationPolicy(opts.ssrfPolicy);
let blockedError: unknown = null;
const handler = async (route: Route, request: Request) => {
if (blockedError) {
await route.abort().catch(() => {});
return;
}
if (!isTopLevelNavigationRequest(opts.page, request)) {
await route.continue();
return;
}
try {
await assertBrowserNavigationAllowed({
url: request.url(),
...navigationPolicy,
});
} catch (err) {
if (isPolicyDenyNavigationError(err)) {
blockedError = err;
await route.abort().catch(() => {});
return;
}
throw err;
}
await route.continue();
};
await opts.page.route("**", handler);
try {
const response = await opts.page.goto(opts.url, { timeout: opts.timeoutMs });
if (blockedError) {
throw blockedError;
}
return response;
} catch (err) {
if (blockedError) {
throw blockedError;
}
throw err;
} finally {
await opts.page.unroute("**", handler).catch(() => {});
if (blockedError) {
await closeBlockedNavigationTarget({
cdpUrl: opts.cdpUrl,
page: opts.page,
targetId: opts.targetId,
});
if (!found) {
// If Playwright only exposes a single Page, use it as a best-effort fallback.
if (pages.length === 1) {
return first;
}
throw new BrowserTabNotFoundError();
}
return found;
}
export function refLocator(page: Page, ref: string) {
@@ -827,8 +559,6 @@ export async function closePlaywrightBrowserConnection(opts?: { cdpUrl?: string
const normalized = opts?.cdpUrl ? normalizeCdpUrl(opts.cdpUrl) : null;
if (normalized) {
clearBlockedTargetsForCdpUrl(normalized);
clearBlockedPageRefsForCdpUrl(normalized);
const cur = cachedByCdpUrl.get(normalized);
cachedByCdpUrl.delete(normalized);
connectingByCdpUrl.delete(normalized);
@@ -843,8 +573,6 @@ export async function closePlaywrightBrowserConnection(opts?: { cdpUrl?: string
}
const connections = Array.from(cachedByCdpUrl.values());
clearBlockedTargetsForCdpUrl();
clearBlockedPageRefsForCdpUrl();
cachedByCdpUrl.clear();
connectingByCdpUrl.clear();
for (const cur of connections) {
@@ -1005,11 +733,8 @@ export async function listPagesViaPlaywright(opts: { cdpUrl: string }): Promise<
}> = [];
for (const page of pages) {
if (isBlockedPageRef(opts.cdpUrl, page)) {
continue;
}
const tid = await pageTargetId(page).catch(() => null);
if (tid && !isBlockedTarget(opts.cdpUrl, tid)) {
if (tid) {
results.push({
targetId: tid,
title: await page.title().catch(() => ""),
@@ -1042,9 +767,6 @@ export async function createPageViaPlaywright(opts: {
const page = await context.newPage();
ensurePageState(page);
clearBlockedPageRef(opts.cdpUrl, page);
const createdTargetId = await pageTargetId(page).catch(() => null);
clearBlockedTarget(opts.cdpUrl, createdTargetId ?? undefined);
// Navigate to the URL
const targetUrl = opts.url.trim() || "about:blank";
@@ -1054,32 +776,22 @@ export async function createPageViaPlaywright(opts: {
url: targetUrl,
...navigationPolicy,
});
let response: Response | null = null;
try {
response = await gotoPageWithNavigationGuard({
cdpUrl: opts.cdpUrl,
page,
url: targetUrl,
timeoutMs: 30_000,
ssrfPolicy: opts.ssrfPolicy,
targetId: createdTargetId ?? undefined,
});
} catch (err) {
if (isPolicyDenyNavigationError(err) || err instanceof BlockedBrowserTargetError) {
throw err;
}
}
await assertPageNavigationCompletedSafely({
cdpUrl: opts.cdpUrl,
page,
response,
ssrfPolicy: opts.ssrfPolicy,
targetId: createdTargetId ?? undefined,
const response = await page.goto(targetUrl, { timeout: 30_000 }).catch(() => {
// Navigation might fail for some URLs, but page is still created
return null;
});
await assertBrowserNavigationRedirectChainAllowed({
request: response?.request(),
...navigationPolicy,
});
await assertBrowserNavigationResultAllowed({
url: page.url(),
...navigationPolicy,
});
}
// Get the targetId for this page
const tid = createdTargetId || (await pageTargetId(page).catch(() => null));
const tid = await pageTargetId(page).catch(() => null);
if (!tid) {
throw new Error("Failed to get targetId for new page");
}

View File

@@ -63,21 +63,6 @@ describe("pw-tools-core.snapshot navigate guard", () => {
});
expect(goto).toHaveBeenCalledWith("https://example.com", { timeout: 1000 });
expect(getPwToolsCoreSessionMocks().gotoPageWithNavigationGuard).toHaveBeenCalledWith({
cdpUrl: "http://127.0.0.1:18792",
page: expect.anything(),
ssrfPolicy: { allowPrivateNetwork: true },
targetId: undefined,
timeoutMs: 1000,
url: "https://example.com",
});
expect(getPwToolsCoreSessionMocks().assertPageNavigationCompletedSafely).toHaveBeenCalledWith({
cdpUrl: "http://127.0.0.1:18792",
page: expect.anything(),
response: null,
ssrfPolicy: { allowPrivateNetwork: true },
targetId: undefined,
});
expect(result.url).toBe("https://example.com");
});
@@ -107,7 +92,7 @@ describe("pw-tools-core.snapshot navigate guard", () => {
targetId: "tab-1",
reason: "retry navigate after detached frame",
});
expect(getPwToolsCoreSessionMocks().gotoPageWithNavigationGuard).toHaveBeenCalledTimes(2);
expect(goto).toHaveBeenCalledTimes(2);
expect(result.url).toBe("https://example.com/recovered");
});
@@ -128,9 +113,6 @@ describe("pw-tools-core.snapshot navigate guard", () => {
goto,
url: vi.fn(() => "https://93.184.216.34/final"),
});
getPwToolsCoreSessionMocks().assertPageNavigationCompletedSafely.mockRejectedValueOnce(
new SsrFBlockedError("Blocked hostname or private/internal/special-use IP address"),
);
await expect(
mod.navigateViaPlaywright({
@@ -139,9 +121,6 @@ describe("pw-tools-core.snapshot navigate guard", () => {
}),
).rejects.toBeInstanceOf(SsrFBlockedError);
expect(getPwToolsCoreSessionMocks().gotoPageWithNavigationGuard).toHaveBeenCalledTimes(1);
expect(getPwToolsCoreSessionMocks().assertPageNavigationCompletedSafely).toHaveBeenCalledTimes(
1,
);
expect(goto).toHaveBeenCalledTimes(1);
});
});

View File

@@ -1,6 +1,11 @@
import type { SsrFPolicy } from "../infra/net/ssrf.js";
import { type AriaSnapshotNode, formatAriaSnapshot, type RawAXNode } from "./cdp.js";
import { assertBrowserNavigationAllowed, withBrowserNavigationPolicy } from "./navigation-guard.js";
import {
assertBrowserNavigationAllowed,
assertBrowserNavigationRedirectChainAllowed,
assertBrowserNavigationResultAllowed,
withBrowserNavigationPolicy,
} from "./navigation-guard.js";
import {
buildRoleSnapshotFromAiSnapshot,
buildRoleSnapshotFromAriaSnapshot,
@@ -9,11 +14,9 @@ import {
type RoleRefMap,
} from "./pw-role-snapshot.js";
import {
assertPageNavigationCompletedSafely,
ensurePageState,
forceDisconnectPlaywrightForTarget,
getPageForTargetId,
gotoPageWithNavigationGuard,
storeRoleRefsForTarget,
type WithSnapshotForAI,
} from "./pw-session.js";
@@ -194,15 +197,7 @@ export async function navigateViaPlaywright(opts: {
const timeout = Math.max(1000, Math.min(120_000, opts.timeoutMs ?? 20_000));
let page = await getPageForTargetId(opts);
ensurePageState(page);
const navigate = async () =>
await gotoPageWithNavigationGuard({
cdpUrl: opts.cdpUrl,
page,
url,
timeoutMs: timeout,
ssrfPolicy: opts.ssrfPolicy,
targetId: opts.targetId,
});
const navigate = async () => await page.goto(url, { timeout });
let response;
try {
response = await navigate();
@@ -221,14 +216,15 @@ export async function navigateViaPlaywright(opts: {
ensurePageState(page);
response = await navigate();
}
await assertPageNavigationCompletedSafely({
cdpUrl: opts.cdpUrl,
page,
response,
ssrfPolicy: opts.ssrfPolicy,
targetId: opts.targetId,
await assertBrowserNavigationRedirectChainAllowed({
request: response?.request(),
...withBrowserNavigationPolicy(opts.ssrfPolicy),
});
const finalUrl = page.url();
await assertBrowserNavigationResultAllowed({
url: finalUrl,
...withBrowserNavigationPolicy(opts.ssrfPolicy),
});
return { url: finalUrl };
}

View File

@@ -15,7 +15,6 @@ let pageState: {
};
const sessionMocks = vi.hoisted(() => ({
assertPageNavigationCompletedSafely: vi.fn(async () => {}),
getPageForTargetId: vi.fn(async () => {
if (!currentPage) {
throw new Error("missing page");
@@ -24,13 +23,6 @@ const sessionMocks = vi.hoisted(() => ({
}),
ensurePageState: vi.fn(() => pageState),
forceDisconnectPlaywrightForTarget: vi.fn(async () => {}),
gotoPageWithNavigationGuard: vi.fn(
async (opts: {
url: string;
timeoutMs: number;
page: { goto: (url: string, init: { timeout: number }) => Promise<unknown> };
}) => (await opts.page.goto(opts.url, { timeout: opts.timeoutMs })) ?? null,
),
restoreRoleRefsForTarget: vi.fn(() => {}),
storeRoleRefsForTarget: vi.fn(() => {}),
refLocator: vi.fn(() => {

View File

@@ -32,20 +32,18 @@ export async function transcribeDeepgramAudio(
): Promise<AudioTranscriptionResult> {
const fetchFn = params.fetchFn ?? fetch;
const model = resolveModel(params.model);
const { baseUrl, allowPrivateNetwork, headers, dispatcherPolicy } =
resolveProviderHttpRequestConfig({
baseUrl: params.baseUrl,
defaultBaseUrl: DEFAULT_DEEPGRAM_AUDIO_BASE_URL,
headers: params.headers,
request: params.request,
defaultHeaders: {
authorization: `Token ${params.apiKey}`,
"content-type": params.mime ?? "application/octet-stream",
},
provider: "deepgram",
capability: "audio",
transport: "media-understanding",
});
const { baseUrl, allowPrivateNetwork, headers } = resolveProviderHttpRequestConfig({
baseUrl: params.baseUrl,
defaultBaseUrl: DEFAULT_DEEPGRAM_AUDIO_BASE_URL,
headers: params.headers,
defaultHeaders: {
authorization: `Token ${params.apiKey}`,
"content-type": params.mime ?? "application/octet-stream",
},
provider: "deepgram",
capability: "audio",
transport: "media-understanding",
});
const url = new URL(`${baseUrl}/listen`);
url.searchParams.set("model", model);
@@ -69,7 +67,6 @@ export async function transcribeDeepgramAudio(
timeoutMs: params.timeoutMs,
fetchFn,
allowPrivateNetwork,
dispatcherPolicy,
});
try {

View File

@@ -74,12 +74,7 @@
"additionalProperties": false,
"properties": {
"viewerBaseUrl": {
"type": "string",
"format": "uri",
"pattern": "^[Hh][Tt][Tt][Pp][Ss]?://",
"not": {
"pattern": "[?#]"
}
"type": "string"
},
"defaults": {
"type": "object",

View File

@@ -43,15 +43,6 @@ const FULL_DEFAULTS = {
mode: "file",
} as const;
function compileManifestConfigSchema() {
const manifest = JSON.parse(
fs.readFileSync(new URL("../openclaw.plugin.json", import.meta.url), "utf8"),
) as { configSchema: Record<string, unknown> };
const Ajv = AjvPkg as unknown as new (opts?: object) => import("ajv").default;
const ajv = new Ajv({ allErrors: true, strict: false, useDefaults: true });
return ajv.compile(manifest.configSchema);
}
describe("resolveDiffsPluginDefaults", () => {
it("returns built-in defaults when config is missing", () => {
expect(resolveDiffsPluginDefaults(undefined)).toEqual(DEFAULT_DIFFS_TOOL_DEFAULTS);
@@ -182,7 +173,12 @@ describe("resolveDiffsPluginDefaults", () => {
});
it("keeps loader-applied schema defaults from shadowing aliases and quality-derived defaults", () => {
const validate = compileManifestConfigSchema();
const manifest = JSON.parse(
fs.readFileSync(new URL("../openclaw.plugin.json", import.meta.url), "utf8"),
) as { configSchema: Record<string, unknown> };
const Ajv = AjvPkg as unknown as new (opts?: object) => import("ajv").default;
const ajv = new Ajv({ allErrors: true, strict: false, useDefaults: true });
const validate = ajv.compile(manifest.configSchema);
const aliasOnly = {
defaults: {
@@ -239,15 +235,6 @@ describe("resolveDiffsPluginViewerBaseUrl", () => {
});
describe("diffs plugin schema surfaces", () => {
it("rejects invalid viewerBaseUrl values at manifest-validation time too", () => {
const validate = compileManifestConfigSchema();
expect(validate({ viewerBaseUrl: "javascript:alert(1)" })).toBe(false);
expect(validate({ viewerBaseUrl: "https://example.com/openclaw?x=1" })).toBe(false);
expect(validate({ viewerBaseUrl: "https://example.com/openclaw#frag" })).toBe(false);
expect(validate({ viewerBaseUrl: "https://example.com/openclaw/" })).toBe(true);
});
it("preserves defaults and security for direct safeParse callers", () => {
expect(
diffsPluginConfigSchema.safeParse?.({

View File

@@ -95,15 +95,6 @@ export const DEFAULT_DIFFS_PLUGIN_SECURITY: DiffsPluginSecurityConfig = {
allowRemoteViewer: false,
};
const VIEWER_BASE_URL_JSON_SCHEMA = {
type: "string",
format: "uri",
pattern: "^[Hh][Tt][Tt][Pp][Ss]?://",
not: {
pattern: "[?#]",
},
} as const satisfies Record<string, unknown>;
const DiffsPluginJsonSchemaSource = z.strictObject({
viewerBaseUrl: z
.string()
@@ -165,44 +156,35 @@ const DiffsPluginJsonSchemaSource = z.strictObject({
.optional(),
});
const diffsPluginConfigSchemaBase = buildPluginConfigSchema(DiffsPluginJsonSchemaSource, {
safeParse(value: unknown) {
if (value === undefined) {
return { success: true, data: undefined };
}
const result = DiffsPluginJsonSchemaSource.safeParse(value);
if (result.success) {
export const diffsPluginConfigSchema: OpenClawPluginConfigSchema = buildPluginConfigSchema(
DiffsPluginJsonSchemaSource,
{
safeParse(value: unknown) {
if (value === undefined) {
return { success: true, data: undefined };
}
const result = DiffsPluginJsonSchemaSource.safeParse(value);
if (result.success) {
return {
success: true,
data: buildDiffsPluginConfigShape(result.data as DiffsPluginConfig),
};
}
return {
success: true,
data: buildDiffsPluginConfigShape(result.data as DiffsPluginConfig),
success: false,
error: {
issues: result.error.issues.map((issue) => ({
path: issue.path.filter((segment): segment is string | number => {
const kind = typeof segment;
return kind === "string" || kind === "number";
}),
message: issue.message,
})),
},
};
}
return {
success: false,
error: {
issues: result.error.issues.map((issue) => ({
path: issue.path.filter((segment): segment is string | number => {
const kind = typeof segment;
return kind === "string" || kind === "number";
}),
message: issue.message,
})),
},
};
},
});
export const diffsPluginConfigSchema: OpenClawPluginConfigSchema = {
...diffsPluginConfigSchemaBase,
jsonSchema: {
...diffsPluginConfigSchemaBase.jsonSchema,
properties: {
...(diffsPluginConfigSchemaBase.jsonSchema as { properties?: Record<string, unknown> })
.properties,
viewerBaseUrl: VIEWER_BASE_URL_JSON_SCHEMA,
},
},
};
);
function resolveConfiguredValue<T>(options: {
primary: T | undefined;

View File

@@ -8,7 +8,7 @@
"@discordjs/voice": "^0.19.2",
"discord-api-types": "^0.38.43",
"https-proxy-agent": "^8.0.0",
"opusscript": "^0.0.8"
"opusscript": "^0.1.1"
},
"devDependencies": {
"openclaw": "workspace:*"

View File

@@ -9,11 +9,6 @@ import {
} from "./approval-native.js";
const STORE_PATH = path.join(os.tmpdir(), "openclaw-discord-approval-native-test.json");
const NATIVE_APPROVAL_CFG = {
commands: {
ownerAllowFrom: ["discord:555555555"],
},
} as const;
function writeStore(store: Record<string, unknown>) {
fs.writeFileSync(STORE_PATH, `${JSON.stringify(store, null, 2)}\n`, "utf8");
@@ -50,7 +45,7 @@ describe("createDiscordNativeApprovalAdapter", () => {
const adapter = createDiscordNativeApprovalAdapter();
const target = await adapter.native?.resolveOriginTarget?.({
cfg: NATIVE_APPROVAL_CFG as never,
cfg: {} as never,
accountId: "main",
approvalKind: "plugin",
request: {
@@ -74,7 +69,7 @@ describe("createDiscordNativeApprovalAdapter", () => {
const adapter = createDiscordNativeApprovalAdapter();
const target = await adapter.native?.resolveOriginTarget?.({
cfg: NATIVE_APPROVAL_CFG as never,
cfg: {} as never,
accountId: "main",
approvalKind: "plugin",
request: {
@@ -109,10 +104,7 @@ describe("createDiscordNativeApprovalAdapter", () => {
const adapter = createDiscordNativeApprovalAdapter();
const target = await adapter.native?.resolveOriginTarget?.({
cfg: {
...NATIVE_APPROVAL_CFG,
session: { store: STORE_PATH },
} as never,
cfg: { session: { store: STORE_PATH } } as never,
accountId: "main",
approvalKind: "plugin",
request: {
@@ -137,7 +129,7 @@ describe("createDiscordNativeApprovalAdapter", () => {
const adapter = createDiscordNativeApprovalAdapter();
const target = await adapter.native?.resolveOriginTarget?.({
cfg: NATIVE_APPROVAL_CFG as never,
cfg: {} as never,
accountId: "main",
approvalKind: "plugin",
request: {
@@ -162,7 +154,7 @@ describe("createDiscordNativeApprovalAdapter", () => {
const adapter = createDiscordNativeApprovalAdapter();
const target = await adapter.native?.resolveOriginTarget?.({
cfg: NATIVE_APPROVAL_CFG as never,
cfg: {} as never,
accountId: "main",
approvalKind: "plugin",
request: {
@@ -184,7 +176,7 @@ describe("createDiscordNativeApprovalAdapter", () => {
const adapter = createDiscordNativeApprovalAdapter();
const target = await adapter.native?.resolveOriginTarget?.({
cfg: NATIVE_APPROVAL_CFG as never,
cfg: {} as never,
accountId: "main",
approvalKind: "plugin",
request: {

View File

@@ -4,7 +4,6 @@ import {
createApproverRestrictedNativeApprovalCapability,
splitChannelApprovalCapability,
doesApprovalRequestMatchChannelAccount,
isChannelExecApprovalClientEnabledFromConfig,
matchesApprovalRequestFilters,
} from "openclaw/plugin-sdk/approval-runtime";
import type { DiscordExecApprovalConfig, OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
@@ -76,18 +75,16 @@ export function shouldHandleDiscordApprovalRequest(params: {
) {
return false;
}
if (
!isChannelExecApprovalClientEnabledFromConfig({
enabled: config?.enabled,
approverCount: approvers.length,
})
) {
if (!config) {
return true;
}
if (!config.enabled || approvers.length === 0) {
return false;
}
return matchesApprovalRequestFilters({
request: params.request.request,
agentFilter: config?.agentFilter,
sessionFilter: config?.sessionFilter,
agentFilter: config.agentFilter,
sessionFilter: config.sessionFilter,
});
}

View File

@@ -28,6 +28,7 @@ import {
buildDiscordModalCustomId as buildDiscordModalCustomIdImpl,
parseDiscordModalCustomIdForCarbon as parseDiscordModalCustomIdForCarbonImpl,
} from "./component-custom-id.js";
// Some test-only module graphs partially mock `@buape/carbon` and can drop `Modal`.
// Keep dynamic form definitions loadable instead of crashing unrelated suites.
const ModalBase: typeof Modal = (Modal ?? class {}) as typeof Modal;

View File

@@ -22,35 +22,34 @@ function buildConfig(
}
describe("discord exec approvals", () => {
it("auto-enables when owner approvers resolve and disables only when forced off", () => {
it("requires enablement and explicit or owner approvers", () => {
expect(isDiscordExecApprovalClientEnabled({ cfg: buildConfig() })).toBe(false);
expect(isDiscordExecApprovalClientEnabled({ cfg: buildConfig({ enabled: true }) })).toBe(false);
expect(
isDiscordExecApprovalClientEnabled({
cfg: buildConfig({ enabled: true }),
cfg: buildConfig({ enabled: true }, { allowFrom: ["123"] }),
}),
).toBe(false);
expect(
isDiscordExecApprovalClientEnabled({
cfg: buildConfig({ approvers: ["123"] }),
cfg: buildConfig({ enabled: true, approvers: ["123"] }),
}),
).toBe(true);
expect(
isDiscordExecApprovalClientEnabled({
cfg: {
...buildConfig(),
...buildConfig({ enabled: true }),
commands: { ownerAllowFrom: ["discord:789"] },
} as OpenClawConfig,
}),
).toBe(true);
expect(
isDiscordExecApprovalClientEnabled({
cfg: buildConfig({ enabled: false, approvers: ["123"] }),
}),
).toBe(false);
});
it("prefers explicit approvers when configured", () => {
const cfg = buildConfig({ approvers: ["456"] }, { allowFrom: ["123"], defaultTo: "user:789" });
const cfg = buildConfig(
{ enabled: true, approvers: ["456"] },
{ allowFrom: ["123"], defaultTo: "user:789" },
);
expect(getDiscordExecApprovalApprovers({ cfg })).toEqual(["456"]);
expect(isDiscordExecApprovalApprover({ cfg, senderId: "456" })).toBe(true);
@@ -73,7 +72,7 @@ describe("discord exec approvals", () => {
it("falls back to commands.ownerAllowFrom for exec approvers", () => {
const cfg = {
...buildConfig(),
...buildConfig({ enabled: true }),
commands: { ownerAllowFrom: ["discord:123", "user:456", "789"] },
} as OpenClawConfig;

View File

@@ -1,5 +1,4 @@
import { getExecApprovalReplyMetadata } from "openclaw/plugin-sdk/approval-runtime";
import { isChannelExecApprovalClientEnabledFromConfig } from "openclaw/plugin-sdk/approval-runtime";
import { resolveApprovalApprovers } from "openclaw/plugin-sdk/approval-runtime";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
import type { DiscordExecApprovalConfig } from "openclaw/plugin-sdk/config-runtime";
@@ -54,14 +53,14 @@ export function isDiscordExecApprovalClientEnabled(params: {
configOverride?: DiscordExecApprovalConfig | null;
}): boolean {
const config = params.configOverride ?? resolveDiscordAccount(params).config.execApprovals;
return isChannelExecApprovalClientEnabledFromConfig({
enabled: config?.enabled,
approverCount: getDiscordExecApprovalApprovers({
return Boolean(
config?.enabled &&
getDiscordExecApprovalApprovers({
cfg: params.cfg,
accountId: params.accountId,
configOverride: params.configOverride,
}).length,
});
}).length > 0,
);
}
export function isDiscordExecApprovalApprover(params: {

View File

@@ -1,7 +1,7 @@
import { beforeAll, describe, expect, it } from "vitest";
import { beforeAll, describe, expect, it, vi } from "vitest";
let buildDiscordComponentCustomId: typeof import("../component-custom-id.js").buildDiscordComponentCustomId;
let buildDiscordModalCustomId: typeof import("../component-custom-id.js").buildDiscordModalCustomId;
let buildDiscordComponentCustomId: typeof import("../components.js").buildDiscordComponentCustomId;
let buildDiscordModalCustomId: typeof import("../components.js").buildDiscordModalCustomId;
let createDiscordComponentButton: typeof import("./agent-components.js").createDiscordComponentButton;
let createDiscordComponentChannelSelect: typeof import("./agent-components.js").createDiscordComponentChannelSelect;
let createDiscordComponentMentionableSelect: typeof import("./agent-components.js").createDiscordComponentMentionableSelect;
@@ -11,8 +11,7 @@ let createDiscordComponentStringSelect: typeof import("./agent-components.js").c
let createDiscordComponentUserSelect: typeof import("./agent-components.js").createDiscordComponentUserSelect;
beforeAll(async () => {
({ buildDiscordComponentCustomId, buildDiscordModalCustomId } =
await import("../component-custom-id.js"));
({ buildDiscordComponentCustomId, buildDiscordModalCustomId } = await import("../components.js"));
({
createDiscordComponentButton,
createDiscordComponentChannelSelect,

View File

@@ -34,10 +34,7 @@ import {
createDiscordApprovalCapability,
shouldHandleDiscordApprovalRequest,
} from "../approval-native.js";
import {
getDiscordExecApprovalApprovers,
isDiscordExecApprovalClientEnabled,
} from "../exec-approvals.js";
import { getDiscordExecApprovalApprovers } from "../exec-approvals.js";
import { createDiscordClient, stripUndefinedFields } from "../send.shared.js";
import { DiscordUiContainer } from "../ui.js";
@@ -487,12 +484,7 @@ export class DiscordExecApprovalHandler {
gatewayUrl: this.opts.gatewayUrl,
eventKinds: ["exec", "plugin"],
nativeAdapter: createDiscordApprovalCapability(this.opts.config).native,
isConfigured: () =>
isDiscordExecApprovalClientEnabled({
cfg: this.opts.cfg,
accountId: this.opts.accountId,
configOverride: this.opts.config,
}),
isConfigured: () => Boolean(this.opts.config.enabled && this.getApprovers().length > 0),
shouldHandle: (request) => this.shouldHandle(request),
buildPendingContent: ({ request }) => {
const actionRow = createApprovalActionRow(request);

View File

@@ -20,6 +20,12 @@ import {
} from "openclaw/plugin-sdk/config-runtime";
import type { OpenClawConfig, ReplyToMode } from "openclaw/plugin-sdk/config-runtime";
import { loadConfig } from "openclaw/plugin-sdk/config-runtime";
import {
GROUP_POLICY_BLOCKED_LABEL,
resolveOpenProviderRuntimeGroupPolicy,
resolveDefaultGroupPolicy,
warnMissingProviderGroupPolicyFallbackOnce,
} from "openclaw/plugin-sdk/runtime-group-policy";
import { createConnectedChannelStatusPatch } from "openclaw/plugin-sdk/gateway-runtime";
import { getPluginCommandSpecs } from "openclaw/plugin-sdk/plugin-runtime";
import { resolveTextChunkLimit } from "openclaw/plugin-sdk/reply-runtime";
@@ -32,16 +38,9 @@ import {
} from "openclaw/plugin-sdk/runtime-env";
import { createSubsystemLogger } from "openclaw/plugin-sdk/runtime-env";
import { createNonExitingRuntime, type RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
import {
GROUP_POLICY_BLOCKED_LABEL,
resolveOpenProviderRuntimeGroupPolicy,
resolveDefaultGroupPolicy,
warnMissingProviderGroupPolicyFallbackOnce,
} from "openclaw/plugin-sdk/runtime-group-policy";
import { formatErrorMessage } from "openclaw/plugin-sdk/ssrf-runtime";
import { summarizeStringEntries } from "openclaw/plugin-sdk/text-runtime";
import { resolveDiscordAccount } from "../accounts.js";
import { isDiscordExecApprovalClientEnabled } from "../exec-approvals.js";
import { fetchDiscordApplicationId } from "../probe.js";
import { normalizeDiscordToken } from "../token.js";
import { createDiscordVoiceCommand } from "../voice/command.js";
@@ -825,11 +824,7 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
// Initialize exec approvals handler if enabled
const execApprovalsConfig = discordCfg.execApprovals ?? {};
const execApprovalsHandler = isDiscordExecApprovalClientEnabled({
cfg,
accountId: account.accountId,
configOverride: execApprovalsConfig,
})
const execApprovalsHandler = execApprovalsConfig.enabled
? new DiscordExecApprovalHandler({
token,
accountId: account.accountId,

View File

@@ -11,14 +11,23 @@ import {
} from "./image-generation-provider.js";
function expectFalJsonPost(params: { call: number; url: string; body: Record<string, unknown> }) {
expect(fetchWithSsrFGuardMock).toHaveBeenNthCalledWith(
params.call,
expect.objectContaining({
url: params.url,
init: expect.objectContaining({
method: "POST",
headers: expect.objectContaining({
Authorization: "Key fal-test-key",
"Content-Type": "application/json",
}),
}),
auditContext: "fal-image-generate",
}),
);
const request = fetchWithSsrFGuardMock.mock.calls[params.call - 1]?.[0];
expect(request).toBeTruthy();
expect(request?.url).toBe(params.url);
expect(request?.auditContext).toBe("fal-image-generate");
expect(request?.init?.method).toBe("POST");
const headers = new Headers(request?.init?.headers);
expect(headers.get("authorization")).toBe("Key fal-test-key");
expect(headers.get("content-type")).toBe("application/json");
expect(JSON.parse(String(request?.init?.body))).toEqual(params.body);
}
@@ -352,13 +361,17 @@ describe("fal image-generation provider", () => {
);
});
it("does not auto-whitelist trusted private relay hosts from a configured baseUrl", async () => {
it("allows trusted private relay hosts derived from configured baseUrl", async () => {
vi.spyOn(providerAuth, "resolveApiKeyForProvider").mockResolvedValue({
apiKey: "fal-test-key",
source: "env",
mode: "api-key",
});
_setFalFetchGuardForTesting(fetchWithSsrFGuardMock);
const relayPolicy = {
allowPrivateNetwork: true,
hostnameAllowlist: ["relay.internal", "*.relay.internal"],
};
fetchWithSsrFGuardMock
.mockResolvedValueOnce({
response: new Response(
@@ -402,7 +415,7 @@ describe("fal image-generation provider", () => {
expect.objectContaining({
url: "http://relay.internal:8080/fal-ai/flux/dev",
auditContext: "fal-image-generate",
policy: undefined,
policy: relayPolicy,
}),
);
expect(fetchWithSsrFGuardMock).toHaveBeenNthCalledWith(
@@ -410,7 +423,7 @@ describe("fal image-generation provider", () => {
expect.objectContaining({
url: "http://media.relay.internal/files/generated.png",
auditContext: "fal-image-download",
policy: undefined,
policy: relayPolicy,
}),
);
});

View File

@@ -3,10 +3,6 @@ import type {
ImageGenerationProvider,
} from "openclaw/plugin-sdk/image-generation";
import { resolveApiKeyForProvider } from "openclaw/plugin-sdk/provider-auth-runtime";
import {
assertOkOrThrowHttpError,
resolveProviderHttpRequestConfig,
} from "openclaw/plugin-sdk/provider-http";
import {
buildHostnameAllowlistPolicyFromSuffixAllowlist,
fetchWithSsrFGuard,
@@ -85,32 +81,40 @@ function matchesTrustedHostSuffix(hostname: string, trustedSuffix: string): bool
return normalizedHost === normalizedSuffix || normalizedHost.endsWith(`.${normalizedSuffix}`);
}
function resolveFalNetworkPolicy(params: {
baseUrl: string;
allowPrivateNetwork: boolean;
}): FalNetworkPolicy {
function resolveFalNetworkPolicy(
cfg: Parameters<typeof resolveApiKeyForProvider>[0]["cfg"],
): FalNetworkPolicy {
const baseUrl = resolveFalBaseUrl(cfg);
const explicitBaseUrl = cfg?.models?.providers?.fal?.baseUrl?.trim();
let parsedBaseUrl: URL;
try {
parsedBaseUrl = new URL(params.baseUrl);
parsedBaseUrl = new URL(baseUrl);
} catch {
return {};
}
const hostSuffix = parsedBaseUrl.hostname.trim().toLowerCase();
if (!hostSuffix || !params.allowPrivateNetwork) {
if (!hostSuffix) {
return {};
}
const hostPolicy = buildHostnameAllowlistPolicyFromSuffixAllowlist([hostSuffix]);
const privateNetworkPolicy = ssrfPolicyFromAllowPrivateNetwork(true);
const privateNetworkPolicy = explicitBaseUrl
? ssrfPolicyFromAllowPrivateNetwork(true)
: undefined;
const trustedHostPolicy = mergeSsrFPolicies(hostPolicy, privateNetworkPolicy);
return {
apiPolicy: trustedHostPolicy,
trustedDownloadHostSuffix: hostSuffix,
trustedDownloadPolicy: trustedHostPolicy,
trustedDownloadHostSuffix: explicitBaseUrl ? hostSuffix : undefined,
trustedDownloadPolicy: explicitBaseUrl ? trustedHostPolicy : undefined,
};
}
function resolveFalBaseUrl(cfg: Parameters<typeof resolveApiKeyForProvider>[0]["cfg"]): string {
const direct = cfg?.models?.providers?.fal?.baseUrl?.trim();
return (direct || DEFAULT_FAL_BASE_URL).replace(/\/+$/u, "");
}
function ensureFalModelPath(model: string | undefined, hasInputImages: boolean): string {
const trimmed = model?.trim() || DEFAULT_FAL_IMAGE_MODEL;
if (!hasInputImages) {
@@ -337,21 +341,7 @@ export function buildFalImageGenerationProvider(): ImageGenerationProvider {
hasInputImages,
});
const model = ensureFalModelPath(req.model, hasInputImages);
const explicitBaseUrl = req.cfg?.models?.providers?.fal?.baseUrl?.trim();
const { baseUrl, allowPrivateNetwork, headers, dispatcherPolicy } =
resolveProviderHttpRequestConfig({
baseUrl: explicitBaseUrl,
defaultBaseUrl: DEFAULT_FAL_BASE_URL,
allowPrivateNetwork: false,
defaultHeaders: {
Authorization: `Key ${auth.apiKey}`,
"Content-Type": "application/json",
},
provider: "fal",
capability: "image",
transport: "http",
});
const networkPolicy = resolveFalNetworkPolicy({ baseUrl, allowPrivateNetwork });
const networkPolicy = resolveFalNetworkPolicy(req.cfg);
const requestBody: Record<string, unknown> = {
prompt: req.prompt,
num_images: req.count ?? 1,
@@ -368,20 +358,27 @@ export function buildFalImageGenerationProvider(): ImageGenerationProvider {
}
requestBody.image_url = toDataUri(input.buffer, input.mimeType);
}
const { response, release } = await falFetchGuard({
url: `${baseUrl}/${model}`,
url: `${resolveFalBaseUrl(req.cfg)}/${model}`,
init: {
method: "POST",
headers,
headers: {
Authorization: `Key ${auth.apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify(requestBody),
},
timeoutMs: req.timeoutMs,
policy: networkPolicy.apiPolicy,
dispatcherPolicy,
auditContext: "fal-image-generate",
});
try {
await assertOkOrThrowHttpError(response, "fal image generation failed");
if (!response.ok) {
const text = await response.text().catch(() => "");
throw new Error(
`fal image generation failed (${response.status}): ${text || response.statusText}`,
);
}
const payload = (await response.json()) as FalImageGenerationResponse;
const images: GeneratedImageAsset[] = [];

View File

@@ -134,17 +134,16 @@ export function buildGoogleImageGenerationProvider(): ImageGenerationProvider {
}
const model = normalizeGoogleImageModel(req.model);
const { baseUrl, allowPrivateNetwork, headers, dispatcherPolicy } =
resolveProviderHttpRequestConfig({
baseUrl: resolveGoogleBaseUrl(req.cfg),
defaultBaseUrl: DEFAULT_GOOGLE_API_BASE_URL,
allowPrivateNetwork: Boolean(req.cfg?.models?.providers?.google?.baseUrl?.trim()),
defaultHeaders: parseGeminiAuth(auth.apiKey).headers,
provider: "google",
api: "google-generative-ai",
capability: "image",
transport: "http",
});
const { baseUrl, allowPrivateNetwork, headers } = resolveProviderHttpRequestConfig({
baseUrl: resolveGoogleBaseUrl(req.cfg),
defaultBaseUrl: DEFAULT_GOOGLE_API_BASE_URL,
allowPrivateNetwork: Boolean(req.cfg?.models?.providers?.google?.baseUrl?.trim()),
defaultHeaders: parseGeminiAuth(auth.apiKey).headers,
provider: "google",
api: "google-generative-ai",
capability: "image",
transport: "http",
});
const imageConfig = mapSizeToImageConfig(req.size);
const inputParts = (req.inputImages ?? []).map((image) => ({
inlineData: {
@@ -178,7 +177,6 @@ export function buildGoogleImageGenerationProvider(): ImageGenerationProvider {
timeoutMs: 60_000,
fetchFn: fetch,
allowPrivateNetwork,
dispatcherPolicy,
});
try {

View File

@@ -11,7 +11,6 @@ import {
assertOkOrThrowHttpError,
postJsonRequest,
resolveProviderHttpRequestConfig,
type ProviderRequestTransportOverrides,
} from "openclaw/plugin-sdk/provider-http";
import {
DEFAULT_GOOGLE_API_BASE_URL,
@@ -33,7 +32,6 @@ async function generateGeminiInlineDataText(params: {
apiKey: string;
baseUrl?: string;
headers?: Record<string, string>;
request?: ProviderRequestTransportOverrides;
model?: string;
prompt?: string;
timeoutMs: number;
@@ -53,19 +51,17 @@ async function generateGeminiInlineDataText(params: {
}
return normalizeGoogleModelId(trimmed);
})();
const { baseUrl, allowPrivateNetwork, headers, dispatcherPolicy } =
resolveProviderHttpRequestConfig({
baseUrl: normalizeGoogleApiBaseUrl(params.baseUrl ?? params.defaultBaseUrl),
defaultBaseUrl: DEFAULT_GOOGLE_API_BASE_URL,
allowPrivateNetwork: Boolean(params.baseUrl?.trim()),
headers: params.headers,
request: params.request,
defaultHeaders: parseGeminiAuth(params.apiKey).headers,
provider: "google",
api: "google-generative-ai",
capability: params.defaultMime.startsWith("audio/") ? "audio" : "video",
transport: "media-understanding",
});
const { baseUrl, allowPrivateNetwork, headers } = resolveProviderHttpRequestConfig({
baseUrl: normalizeGoogleApiBaseUrl(params.baseUrl ?? params.defaultBaseUrl),
defaultBaseUrl: DEFAULT_GOOGLE_API_BASE_URL,
allowPrivateNetwork: Boolean(params.baseUrl?.trim()),
headers: params.headers,
defaultHeaders: parseGeminiAuth(params.apiKey).headers,
provider: "google",
api: "google-generative-ai",
capability: params.defaultMime.startsWith("audio/") ? "audio" : "video",
transport: "media-understanding",
});
const url = `${baseUrl}/models/${model}:generateContent`;
const prompt = (() => {
@@ -97,7 +93,6 @@ async function generateGeminiInlineDataText(params: {
timeoutMs: params.timeoutMs,
fetchFn,
allowPrivateNetwork,
dispatcherPolicy,
});
try {

View File

@@ -14,11 +14,7 @@ export function generatePkce(): { verifier: string; challenge: string } {
return { verifier, challenge };
}
export function generateOAuthState(): string {
return randomBytes(32).toString("hex");
}
export function buildAuthUrl(challenge: string, state: string): string {
export function buildAuthUrl(challenge: string, verifier: string): string {
const { clientId } = resolveOAuthClientConfig();
const params = new URLSearchParams({
client_id: clientId,
@@ -27,7 +23,7 @@ export function buildAuthUrl(challenge: string, state: string): string {
scope: SCOPES.join(" "),
code_challenge: challenge,
code_challenge_method: "S256",
state,
state: verifier,
access_type: "offline",
prompt: "consent",
});
@@ -36,6 +32,7 @@ export function buildAuthUrl(challenge: string, state: string): string {
export function parseCallbackInput(
input: string,
expectedState: string,
): { code: string; state: string } | { error: string } {
const trimmed = input.trim();
if (!trimmed) {
@@ -45,7 +42,7 @@ export function parseCallbackInput(
try {
const url = new URL(trimmed);
const code = url.searchParams.get("code");
const state = url.searchParams.get("state");
const state = url.searchParams.get("state") ?? expectedState;
if (!code) {
return { error: "Missing 'code' parameter in URL" };
}
@@ -54,7 +51,10 @@ export function parseCallbackInput(
}
return { code, state };
} catch {
return { error: "Paste the full redirect URL, not just the code." };
if (!expectedState) {
return { error: "Paste the full redirect URL, not just the code." };
}
return { code: trimmed, state: expectedState };
}
}

View File

@@ -279,13 +279,6 @@ describe("loginGeminiCliOAuth", () => {
});
}
function getFormField(body: RequestInit["body"], name: string): string | null {
if (!(body instanceof URLSearchParams)) {
throw new Error("Expected URLSearchParams body");
}
return body.get(name);
}
type LoginGeminiCliOAuthFn = (options: {
isRemote: boolean;
openUrl: () => Promise<void>;
@@ -406,61 +399,6 @@ describe("loginGeminiCliOAuth", () => {
});
});
it("keeps OAuth state separate from the PKCE verifier during manual login", async () => {
const requests: Array<{ url: string; init?: RequestInit }> = [];
const fetchMock = vi.fn(async (input: string | URL | Request, init?: RequestInit) => {
const url = getRequestUrl(input);
requests.push({ url, init });
if (url === TOKEN_URL) {
return responseJson({
access_token: "access-token",
refresh_token: "refresh-token",
expires_in: 3600,
});
}
if (url === USERINFO_URL) {
return responseJson({ email: "lobster@openclaw.ai" });
}
if (url === LOAD_PROD) {
return responseJson({
currentTier: { id: "standard-tier" },
cloudaicompanionProject: { id: "prod-project" },
});
}
throw new Error(`Unexpected request: ${url}`);
});
vi.stubGlobal("fetch", fetchMock);
const { loginGeminiCliOAuth } = await import("./oauth.js");
const { authUrl } = await runRemoteLoginWithCapturedAuthUrl(loginGeminiCliOAuth);
const authState = new URL(authUrl).searchParams.get("state");
expect(authState).toBeTruthy();
const tokenRequest = requests.find((request) => request.url === TOKEN_URL);
expect(tokenRequest).toBeDefined();
const codeVerifier = getFormField(tokenRequest?.init?.body, "code_verifier");
expect(codeVerifier).toBeTruthy();
expect(codeVerifier).not.toBe(authState);
});
it("rejects manual callback input when the returned state does not match", async () => {
const { loginGeminiCliOAuth } = await import("./oauth.js");
await expect(
loginGeminiCliOAuth({
isRemote: true,
openUrl: async () => {},
log: () => {},
note: async () => {},
prompt: async () =>
"http://localhost:8085/oauth2callback?code=oauth-code&state=wrong-state",
progress: { update: () => {}, stop: () => {} },
}),
).rejects.toThrow("OAuth state mismatch - please try again");
});
it("falls back to GOOGLE_CLOUD_PROJECT when all loadCodeAssist endpoints fail", async () => {
process.env.GOOGLE_CLOUD_PROJECT = "env-project";

View File

@@ -1,7 +1,6 @@
import { clearCredentialsCache, extractGeminiCliCredentials } from "./oauth.credentials.js";
import {
buildAuthUrl,
generateOAuthState,
generatePkce,
parseCallbackInput,
shouldUseManualOAuthFlow,
@@ -33,19 +32,18 @@ export async function loginGeminiCliOAuth(
);
const { verifier, challenge } = generatePkce();
const state = generateOAuthState();
const authUrl = buildAuthUrl(challenge, state);
const authUrl = buildAuthUrl(challenge, verifier);
if (needsManual) {
ctx.progress.update("OAuth URL ready");
ctx.log(`\nOpen this URL in your LOCAL browser:\n\n${authUrl}\n`);
ctx.progress.update("Waiting for you to paste the callback URL...");
const callbackInput = await ctx.prompt("Paste the redirect URL here: ");
const parsed = parseCallbackInput(callbackInput);
const parsed = parseCallbackInput(callbackInput, verifier);
if ("error" in parsed) {
throw new Error(parsed.error);
}
if (parsed.state !== state) {
if (parsed.state !== verifier) {
throw new Error("OAuth state mismatch - please try again");
}
ctx.progress.update("Exchanging authorization code for tokens...");
@@ -61,7 +59,7 @@ export async function loginGeminiCliOAuth(
try {
const { code } = await waitForLocalCallback({
expectedState: state,
expectedState: verifier,
timeoutMs: 5 * 60 * 1000,
onProgress: (msg) => ctx.progress.update(msg),
});
@@ -77,11 +75,11 @@ export async function loginGeminiCliOAuth(
ctx.progress.update("Local callback server failed. Switching to manual mode...");
ctx.log(`\nOpen this URL in your LOCAL browser:\n\n${authUrl}\n`);
const callbackInput = await ctx.prompt("Paste the redirect URL here: ");
const parsed = parseCallbackInput(callbackInput);
const parsed = parseCallbackInput(callbackInput, verifier);
if ("error" in parsed) {
throw new Error(parsed.error, { cause: err });
}
if (parsed.state !== state) {
if (parsed.state !== verifier) {
throw new Error("OAuth state mismatch - please try again", { cause: err });
}
ctx.progress.update("Exchanging authorization code for tokens...");

View File

@@ -3,7 +3,7 @@ export {
normalizeAccountId,
normalizeOptionalAccountId,
} from "openclaw/plugin-sdk/account-id";
export { isPrivateOrLoopbackHost } from "./private-network-host.js";
export { isPrivateOrLoopbackHost } from "../../runtime-api.js";
export {
assertHttpUrlTargetsPrivateNetwork,
ssrfPolicyFromAllowPrivateNetwork,

View File

@@ -1,55 +0,0 @@
import net from "node:net";
function normalizeHost(host: string): string {
const normalized = host.trim().toLowerCase().replace(/\.+$/, "");
return normalized.startsWith("[") && normalized.endsWith("]")
? normalized.slice(1, -1)
: normalized;
}
function isPrivateIpv4(host: string): boolean {
const parts = host.split(".").map((part) => Number(part));
if (
parts.length !== 4 ||
parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)
) {
return false;
}
const [a, b] = parts;
return (
a === 10 ||
a === 127 ||
(a === 172 && b >= 16 && b <= 31) ||
(a === 192 && b === 168) ||
(a === 169 && b === 254) ||
(a === 100 && b >= 64 && b <= 127)
);
}
function isPrivateIpv6(host: string): boolean {
if (host === "::1") {
return true;
}
if (host === "::" || host.startsWith("ff")) {
return false;
}
return host.startsWith("fc") || host.startsWith("fd") || host.startsWith("fe80:");
}
export function isPrivateOrLoopbackHost(host: string): boolean {
const normalized = normalizeHost(host);
if (!normalized) {
return false;
}
if (normalized === "localhost") {
return true;
}
const family = net.isIP(normalized);
if (family === 4) {
return isPrivateIpv4(normalized);
}
if (family === 6) {
return isPrivateIpv6(normalized);
}
return false;
}

View File

@@ -1,147 +0,0 @@
import * as providerAuth from "openclaw/plugin-sdk/provider-auth-runtime";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { buildMinimaxImageGenerationProvider } from "./image-generation-provider.js";
describe("minimax image-generation provider", () => {
beforeEach(() => {
vi.clearAllMocks();
});
afterEach(() => {
vi.restoreAllMocks();
});
it("generates PNG buffers through the shared provider HTTP path", async () => {
vi.spyOn(providerAuth, "resolveApiKeyForProvider").mockResolvedValue({
apiKey: "minimax-test-key",
source: "env",
mode: "api-key",
});
const fetchMock = vi.fn().mockResolvedValue(
new Response(
JSON.stringify({
data: {
image_base64: [Buffer.from("png-data").toString("base64")],
},
base_resp: { status_code: 0 },
}),
{
status: 200,
headers: { "Content-Type": "application/json" },
},
),
);
vi.stubGlobal("fetch", fetchMock);
const provider = buildMinimaxImageGenerationProvider();
const result = await provider.generateImage({
provider: "minimax",
model: "image-01",
prompt: "draw a cat",
cfg: {},
});
expect(fetchMock).toHaveBeenCalledWith(
"https://api.minimax.io/v1/image_generation",
expect.objectContaining({
method: "POST",
body: JSON.stringify({
model: "image-01",
prompt: "draw a cat",
response_format: "base64",
n: 1,
}),
}),
);
const [, init] = fetchMock.mock.calls[0] as [string, RequestInit];
const headers = new Headers(init.headers);
expect(headers.get("authorization")).toBe("Bearer minimax-test-key");
expect(headers.get("content-type")).toBe("application/json");
expect(result).toEqual({
images: [
{
buffer: Buffer.from("png-data"),
mimeType: "image/png",
fileName: "image-1.png",
},
],
model: "image-01",
});
});
it("uses the configured provider base URL origin", async () => {
vi.spyOn(providerAuth, "resolveApiKeyForProvider").mockResolvedValue({
apiKey: "minimax-test-key",
source: "env",
mode: "api-key",
});
const fetchMock = vi.fn().mockResolvedValue(
new Response(
JSON.stringify({
data: {
image_base64: [Buffer.from("png-data").toString("base64")],
},
base_resp: { status_code: 0 },
}),
{
status: 200,
headers: { "Content-Type": "application/json" },
},
),
);
vi.stubGlobal("fetch", fetchMock);
const provider = buildMinimaxImageGenerationProvider();
await provider.generateImage({
provider: "minimax",
model: "image-01",
prompt: "draw a cat",
cfg: {
models: {
providers: {
minimax: {
baseUrl: "https://api.minimax.io/anthropic",
models: [],
},
},
},
},
});
expect(fetchMock).toHaveBeenCalledWith(
"https://api.minimax.io/v1/image_generation",
expect.any(Object),
);
});
it("does not allow private-network routing just because a custom base URL is configured", async () => {
vi.spyOn(providerAuth, "resolveApiKeyForProvider").mockResolvedValue({
apiKey: "minimax-test-key",
source: "env",
mode: "api-key",
});
const fetchMock = vi.fn();
vi.stubGlobal("fetch", fetchMock);
const provider = buildMinimaxImageGenerationProvider();
await expect(
provider.generateImage({
provider: "minimax",
model: "image-01",
prompt: "draw a cat",
cfg: {
models: {
providers: {
minimax: {
baseUrl: "http://127.0.0.1:8080/anthropic",
models: [],
},
},
},
},
}),
).rejects.toThrow("Blocked hostname or private/internal/special-use IP address");
expect(fetchMock).not.toHaveBeenCalled();
});
});

View File

@@ -1,10 +1,5 @@
import type { ImageGenerationProvider } from "openclaw/plugin-sdk/image-generation";
import { resolveApiKeyForProvider } from "openclaw/plugin-sdk/provider-auth-runtime";
import {
assertOkOrThrowHttpError,
postJsonRequest,
resolveProviderHttpRequestConfig,
} from "openclaw/plugin-sdk/provider-http";
const DEFAULT_MINIMAX_IMAGE_BASE_URL = "https://api.minimax.io";
const DEFAULT_MODEL = "image-01";
@@ -88,23 +83,6 @@ function buildMinimaxImageProvider(providerId: string): ImageGenerationProvider
}
const baseUrl = resolveMinimaxImageBaseUrl(req.cfg, providerId);
const {
baseUrl: resolvedBaseUrl,
allowPrivateNetwork,
headers,
dispatcherPolicy,
} = resolveProviderHttpRequestConfig({
baseUrl,
defaultBaseUrl: DEFAULT_MINIMAX_IMAGE_BASE_URL,
allowPrivateNetwork: false,
defaultHeaders: {
Authorization: `Bearer ${auth.apiKey}`,
"Content-Type": "application/json",
},
provider: providerId,
capability: "image",
transport: "http",
});
const body: Record<string, unknown> = {
model: req.model || DEFAULT_MODEL,
@@ -124,55 +102,67 @@ function buildMinimaxImageProvider(providerId: string): ImageGenerationProvider
const dataUrl = `data:${mime};base64,${ref.buffer.toString("base64")}`;
body.subject_reference = [{ type: "character", image_file: dataUrl }];
}
const { response, release } = await postJsonRequest({
url: `${resolvedBaseUrl}/v1/image_generation`,
headers,
body,
timeoutMs: req.timeoutMs,
fetchFn: fetch,
allowPrivateNetwork,
dispatcherPolicy,
const controller = new AbortController();
const timeoutMs = req.timeoutMs;
const timeout =
typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs > 0
? setTimeout(() => controller.abort(), timeoutMs)
: undefined;
const response = await fetch(`${baseUrl}/v1/image_generation`, {
method: "POST",
headers: {
Authorization: `Bearer ${auth.apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
signal: controller.signal,
}).finally(() => {
clearTimeout(timeout);
});
try {
await assertOkOrThrowHttpError(response, "MiniMax image generation failed");
const data = (await response.json()) as MinimaxImageApiResponse;
const baseResp = data.base_resp;
if (baseResp && typeof baseResp.status_code === "number" && baseResp.status_code !== 0) {
const msg = baseResp.status_msg ?? "";
throw new Error(`MiniMax image generation API error (${baseResp.status_code}): ${msg}`);
}
const base64Images = data.data?.image_base64 ?? [];
const failedCount = data.metadata?.failed_count ?? 0;
if (base64Images.length === 0) {
const reason =
failedCount > 0 ? `${failedCount} image(s) failed to generate` : "no images returned";
throw new Error(`MiniMax image generation returned no images: ${reason}`);
}
const images = base64Images
.map((b64, index) => {
if (!b64) {
return null;
}
return {
buffer: Buffer.from(b64, "base64"),
mimeType: DEFAULT_OUTPUT_MIME,
fileName: `image-${index + 1}.png`,
};
})
.filter((entry): entry is NonNullable<typeof entry> => entry !== null);
return {
images,
model: req.model || DEFAULT_MODEL,
};
} finally {
await release();
if (!response.ok) {
const text = await response.text().catch(() => "");
throw new Error(
`MiniMax image generation failed (${response.status}): ${text || response.statusText}`,
);
}
const data = (await response.json()) as MinimaxImageApiResponse;
const baseResp = data.base_resp;
if (baseResp && typeof baseResp.status_code === "number" && baseResp.status_code !== 0) {
const msg = baseResp.status_msg ?? "";
throw new Error(`MiniMax image generation API error (${baseResp.status_code}): ${msg}`);
}
const base64Images = data.data?.image_base64 ?? [];
const failedCount = data.metadata?.failed_count ?? 0;
if (base64Images.length === 0) {
const reason =
failedCount > 0 ? `${failedCount} image(s) failed to generate` : "no images returned";
throw new Error(`MiniMax image generation returned no images: ${reason}`);
}
const images = base64Images
.map((b64, index) => {
if (!b64) {
return null;
}
return {
buffer: Buffer.from(b64, "base64"),
mimeType: DEFAULT_OUTPUT_MIME,
fileName: `image-${index + 1}.png`,
};
})
.filter((entry): entry is NonNullable<typeof entry> => entry !== null);
return {
images,
model: req.model || DEFAULT_MODEL,
};
},
};
}

View File

@@ -65,21 +65,19 @@ export async function describeMoonshotVideo(
const model = resolveModel(params.model);
const mime = params.mime ?? "video/mp4";
const prompt = resolvePrompt(params.prompt);
const { baseUrl, allowPrivateNetwork, headers, dispatcherPolicy } =
resolveProviderHttpRequestConfig({
baseUrl: params.baseUrl,
defaultBaseUrl: DEFAULT_MOONSHOT_VIDEO_BASE_URL,
headers: params.headers,
request: params.request,
defaultHeaders: {
"content-type": "application/json",
authorization: `Bearer ${params.apiKey}`,
},
provider: "moonshot",
api: "openai-completions",
capability: "video",
transport: "media-understanding",
});
const { baseUrl, allowPrivateNetwork, headers } = resolveProviderHttpRequestConfig({
baseUrl: params.baseUrl,
defaultBaseUrl: DEFAULT_MOONSHOT_VIDEO_BASE_URL,
headers: params.headers,
defaultHeaders: {
"content-type": "application/json",
authorization: `Bearer ${params.apiKey}`,
},
provider: "moonshot",
api: "openai-completions",
capability: "video",
transport: "media-understanding",
});
const url = `${baseUrl}/chat/completions`;
const body = {
@@ -107,7 +105,6 @@ export async function describeMoonshotVideo(
timeoutMs: params.timeoutMs,
fetchFn,
allowPrivateNetwork,
dispatcherPolicy,
});
try {

View File

@@ -1,10 +1,5 @@
import type { ImageGenerationProvider } from "openclaw/plugin-sdk/image-generation";
import { resolveApiKeyForProvider } from "openclaw/plugin-sdk/provider-auth-runtime";
import {
assertOkOrThrowHttpError,
postJsonRequest,
resolveProviderHttpRequestConfig,
} from "openclaw/plugin-sdk/provider-http";
import { OPENAI_DEFAULT_IMAGE_MODEL as DEFAULT_OPENAI_IMAGE_MODEL } from "./default-models.js";
const DEFAULT_OPENAI_IMAGE_BASE_URL = "https://api.openai.com/v1";
@@ -62,59 +57,57 @@ export function buildOpenAIImageGenerationProvider(): ImageGenerationProvider {
if (!auth.apiKey) {
throw new Error("OpenAI API key missing");
}
const { baseUrl, allowPrivateNetwork, headers, dispatcherPolicy } =
resolveProviderHttpRequestConfig({
baseUrl: resolveOpenAIBaseUrl(req.cfg),
defaultBaseUrl: DEFAULT_OPENAI_IMAGE_BASE_URL,
allowPrivateNetwork: false,
defaultHeaders: {
Authorization: `Bearer ${auth.apiKey}`,
"Content-Type": "application/json",
},
provider: "openai",
capability: "image",
transport: "http",
});
const { response, release } = await postJsonRequest({
url: `${baseUrl}/images/generations`,
headers,
body: {
const controller = new AbortController();
const timeoutMs = req.timeoutMs;
const timeout =
typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs > 0
? setTimeout(() => controller.abort(), timeoutMs)
: undefined;
const response = await fetch(`${resolveOpenAIBaseUrl(req.cfg)}/images/generations`, {
method: "POST",
headers: {
Authorization: `Bearer ${auth.apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: req.model || DEFAULT_OPENAI_IMAGE_MODEL,
prompt: req.prompt,
n: req.count ?? 1,
size: req.size ?? DEFAULT_SIZE,
},
timeoutMs: req.timeoutMs,
fetchFn: fetch,
allowPrivateNetwork,
dispatcherPolicy,
}),
signal: controller.signal,
}).finally(() => {
clearTimeout(timeout);
});
try {
await assertOkOrThrowHttpError(response, "OpenAI image generation failed");
const data = (await response.json()) as OpenAIImageApiResponse;
const images = (data.data ?? [])
.map((entry, index) => {
if (!entry.b64_json) {
return null;
}
return {
buffer: Buffer.from(entry.b64_json, "base64"),
mimeType: DEFAULT_OUTPUT_MIME,
fileName: `image-${index + 1}.png`,
...(entry.revised_prompt ? { revisedPrompt: entry.revised_prompt } : {}),
};
})
.filter((entry): entry is NonNullable<typeof entry> => entry !== null);
return {
images,
model: req.model || DEFAULT_OPENAI_IMAGE_MODEL,
};
} finally {
await release();
if (!response.ok) {
const text = await response.text().catch(() => "");
throw new Error(
`OpenAI image generation failed (${response.status}): ${text || response.statusText}`,
);
}
const data = (await response.json()) as OpenAIImageApiResponse;
const images = (data.data ?? [])
.map((entry, index) => {
if (!entry.b64_json) {
return null;
}
return {
buffer: Buffer.from(entry.b64_json, "base64"),
mimeType: DEFAULT_OUTPUT_MIME,
fileName: `image-${index + 1}.png`,
...(entry.revised_prompt ? { revisedPrompt: entry.revised_prompt } : {}),
};
})
.filter((entry): entry is NonNullable<typeof entry> => entry !== null);
return {
images,
model: req.model || DEFAULT_OPENAI_IMAGE_MODEL,
};
},
};
}

View File

@@ -117,37 +117,6 @@ describe("openai plugin", () => {
).rejects.toThrow("does not support reference-image edits");
});
it("does not allow private-network routing just because a custom base URL is configured", async () => {
vi.spyOn(providerAuth, "resolveApiKeyForProvider").mockResolvedValue({
apiKey: "sk-test",
source: "env",
mode: "api-key",
});
const fetchMock = vi.fn();
vi.stubGlobal("fetch", fetchMock);
const provider = buildOpenAIImageGenerationProvider();
await expect(
provider.generateImage({
provider: "openai",
model: "gpt-image-1",
prompt: "draw a cat",
cfg: {
models: {
providers: {
openai: {
baseUrl: "http://127.0.0.1:8080/v1",
models: [],
},
},
},
} satisfies OpenClawConfig,
}),
).rejects.toThrow("Blocked hostname or private/internal/special-use IP address");
expect(fetchMock).not.toHaveBeenCalled();
});
it("bootstraps the env proxy dispatcher before refreshing codex oauth credentials", async () => {
const refreshed = {
access: "next-access",

View File

@@ -51,11 +51,11 @@ export const slackChannelConfigUiHints = {
},
execApprovals: {
label: "Slack Exec Approvals",
help: "Slack-native exec approval routing and approver authorization. When unset, OpenClaw auto-enables DM-first native approvals if approvers can be resolved for this workspace account.",
help: "Slack-native exec approval routing and approver authorization. Enable this only when Slack should act as an explicit exec-approval client for the selected workspace account.",
},
"execApprovals.enabled": {
label: "Slack Exec Approvals Enabled",
help: 'Controls Slack native exec approvals for this account: unset or "auto" enables DM-first native approvals when approvers can be resolved, true forces native approvals on, and false disables them.',
help: "Enable Slack exec approvals for this account. When false or unset, Slack messages/buttons cannot approve exec requests.",
},
"execApprovals.approvers": {
label: "Slack Exec Approval Approvers",

View File

@@ -29,36 +29,32 @@ function buildConfig(
}
describe("slack exec approvals", () => {
it("auto-enables when owner approvers resolve and disables only when forced off", () => {
it("requires enablement and explicit or owner approvers", () => {
expect(isSlackExecApprovalClientEnabled({ cfg: buildConfig() })).toBe(false);
expect(isSlackExecApprovalClientEnabled({ cfg: buildConfig({ enabled: true }) })).toBe(false);
expect(
isSlackExecApprovalClientEnabled({
cfg: buildConfig({ enabled: true }),
cfg: buildConfig({ enabled: true }, { allowFrom: ["U123"] }),
}),
).toBe(false);
expect(
isSlackExecApprovalClientEnabled({
cfg: buildConfig({ approvers: ["U123"] }),
cfg: buildConfig({ enabled: true, approvers: ["U123"] }),
}),
).toBe(true);
expect(
isSlackExecApprovalClientEnabled({
cfg: {
...buildConfig(),
...buildConfig({ enabled: true }),
commands: { ownerAllowFrom: ["slack:U123OWNER"] },
} as OpenClawConfig,
}),
).toBe(true);
expect(
isSlackExecApprovalClientEnabled({
cfg: buildConfig({ enabled: false, approvers: ["U123"] }),
}),
).toBe(false);
});
it("prefers explicit approvers when configured", () => {
const cfg = buildConfig(
{ approvers: ["U456"] },
{ enabled: true, approvers: ["U456"] },
{ allowFrom: ["U123"], defaultTo: "user:U789" },
);

View File

@@ -16,7 +16,6 @@ import { logError } from "openclaw/plugin-sdk/text-runtime";
import { slackNativeApprovalAdapter } from "../approval-native.js";
import {
getSlackExecApprovalApprovers,
isSlackExecApprovalClientEnabled,
normalizeSlackApproverId,
shouldHandleSlackExecApprovalRequest,
} from "../exec-approvals.js";
@@ -240,10 +239,13 @@ export class SlackExecApprovalHandler {
gatewayUrl: opts.gatewayUrl,
nativeAdapter: slackNativeApprovalAdapter.native,
isConfigured: () =>
isSlackExecApprovalClientEnabled({
cfg: opts.cfg,
accountId: opts.accountId,
}),
Boolean(
opts.config.enabled &&
getSlackExecApprovalApprovers({
cfg: opts.cfg,
accountId: opts.accountId,
}).length > 0,
),
shouldHandle: (request) => this.shouldHandle(request),
buildPendingContent: ({ request }) => ({
text: buildSlackPendingApprovalText(request),

View File

@@ -31,7 +31,6 @@ import { normalizeStringEntries } from "openclaw/plugin-sdk/text-runtime";
import { installRequestBodyLimitGuard } from "openclaw/plugin-sdk/webhook-request-guards";
import { resolveSlackAccount } from "../accounts.js";
import { resolveSlackWebClientOptions } from "../client.js";
import { isSlackExecApprovalClientEnabled } from "../exec-approvals.js";
import { normalizeSlackWebhookPath, registerSlackHttpHandler } from "../http/index.js";
import { SLACK_TEXT_LIMIT } from "../limits.js";
import { resolveSlackChannelAllowlist, type SlackChannelResolution } from "../resolve-channels.js";
@@ -407,14 +406,11 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
: undefined;
const handleSlackMessage = createSlackMessageHandler({ ctx, account, trackEvent });
const execApprovalsHandler = isSlackExecApprovalClientEnabled({
cfg,
accountId: account.accountId,
})
const execApprovalsHandler = slackCfg.execApprovals?.enabled
? new SlackExecApprovalHandler({
app,
accountId: account.accountId,
config: slackCfg.execApprovals ?? {},
config: slackCfg.execApprovals,
cfg,
})
: null;

View File

@@ -867,28 +867,6 @@ describe("readTelegramButtons", () => {
}),
).toThrow(/style must be one of danger, success, primary/i);
});
it("rejects callback_data over Telegram's 64-byte limit", () => {
expect(() =>
readTelegramButtons({
buttons: [[{ text: "Option A", callback_data: "x".repeat(65) }]],
}),
).toThrow(/callback_data too long/i);
});
it("accepts multibyte callback_data at 64 bytes and rejects 68 bytes", () => {
expect(
readTelegramButtons({
buttons: [[{ text: "Option A", callback_data: "😀".repeat(16) }]],
}),
).toEqual([[{ text: "Option A", callback_data: "😀".repeat(16) }]]);
expect(() =>
readTelegramButtons({
buttons: [[{ text: "Option A", callback_data: "😀".repeat(17) }]],
}),
).toThrow(/callback_data too long/i);
});
});
describe("handleTelegramAction per-account gating", () => {

View File

@@ -13,10 +13,6 @@ import {
type TelegramActionConfig,
} from "openclaw/plugin-sdk/telegram-core";
import { createTelegramActionGate, resolveTelegramPollActionGateState } from "./accounts.js";
import {
fitsTelegramCallbackData,
TELEGRAM_CALLBACK_DATA_MAX_BYTES,
} from "./approval-callback-data.js";
import type { TelegramButtonStyle, TelegramInlineButtons } from "./button-types.js";
import { resolveTelegramInlineButtons } from "./button-types.js";
import {
@@ -135,9 +131,9 @@ export function readTelegramButtons(
if (!text || !callbackData) {
throw new Error(`buttons[${rowIndex}][${buttonIndex}] requires text and callback_data`);
}
if (!fitsTelegramCallbackData(callbackData)) {
if (callbackData.length > 64) {
throw new Error(
`buttons[${rowIndex}][${buttonIndex}] callback_data too long (max ${TELEGRAM_CALLBACK_DATA_MAX_BYTES} bytes)`,
`buttons[${rowIndex}][${buttonIndex}] callback_data too long (max 64 chars)`,
);
}
const styleRaw = rawButton.style;

View File

@@ -1,7 +1,12 @@
import type { ExecApprovalReplyDecision } from "openclaw/plugin-sdk/approval-runtime";
import { sanitizeTelegramCallbackData } from "./approval-callback-data.js";
import type { TelegramInlineButtons } from "./button-types.js";
const MAX_CALLBACK_DATA_BYTES = 64;
function fitsCallbackData(value: string): boolean {
return Buffer.byteLength(value, "utf8") <= MAX_CALLBACK_DATA_BYTES;
}
export function buildTelegramExecApprovalButtons(
approvalId: string,
): TelegramInlineButtons | undefined {
@@ -16,21 +21,23 @@ function buildTelegramExecApprovalButtonsForDecisions(
approvalId: string,
allowedDecisions: readonly ExecApprovalReplyDecision[],
): TelegramInlineButtons | undefined {
const allowOnce = sanitizeTelegramCallbackData(`/approve ${approvalId} allow-once`);
if (!allowedDecisions.includes("allow-once") || !allowOnce) {
const allowOnce = `/approve ${approvalId} allow-once`;
if (!allowedDecisions.includes("allow-once") || !fitsCallbackData(allowOnce)) {
return undefined;
}
const primaryRow: Array<{ text: string; callback_data: string }> = [
{ text: "Allow Once", callback_data: allowOnce },
];
const allowAlways = sanitizeTelegramCallbackData(`/approve ${approvalId} allow-always`);
if (allowedDecisions.includes("allow-always") && allowAlways) {
// Use a shorter decision alias so full plugin:<uuid> IDs still fit Telegram's
// 64-byte callback_data limit for the "Allow Always" action.
const allowAlways = `/approve ${approvalId} always`;
if (allowedDecisions.includes("allow-always") && fitsCallbackData(allowAlways)) {
primaryRow.push({ text: "Allow Always", callback_data: allowAlways });
}
const rows: Array<Array<{ text: string; callback_data: string }>> = [primaryRow];
const deny = sanitizeTelegramCallbackData(`/approve ${approvalId} deny`);
if (allowedDecisions.includes("deny") && deny) {
const deny = `/approve ${approvalId} deny`;
if (allowedDecisions.includes("deny") && fitsCallbackData(deny)) {
rows.push([{ text: "Deny", callback_data: deny }]);
}
return rows;

View File

@@ -1,33 +0,0 @@
import { describe, expect, it } from "vitest";
import {
fitsTelegramCallbackData,
rewriteTelegramApprovalDecisionAlias,
sanitizeTelegramCallbackData,
} from "./approval-callback-data.js";
describe("approval callback data", () => {
it("enforces Telegram callback byte boundaries", () => {
expect(fitsTelegramCallbackData("x".repeat(63))).toBe(true);
expect(fitsTelegramCallbackData("x".repeat(64))).toBe(true);
expect(fitsTelegramCallbackData("x".repeat(65))).toBe(false);
});
it("rewrites /approve allow-always callbacks to always", () => {
const approvalId = `plugin:${"a".repeat(36)}`;
expect(rewriteTelegramApprovalDecisionAlias(`/approve ${approvalId} allow-always`)).toBe(
`/approve ${approvalId} always`,
);
});
it("keeps rewritten allow-always callbacks when canonical form would overflow", () => {
const approvalId = `plugin:${"a".repeat(36)}`;
expect(sanitizeTelegramCallbackData(`/approve ${approvalId} allow-always`)).toBe(
`/approve ${approvalId} always`,
);
});
it("keeps 64-byte callbacks and drops 65-byte callbacks through sanitize", () => {
expect(sanitizeTelegramCallbackData("x".repeat(64))).toBe("x".repeat(64));
expect(sanitizeTelegramCallbackData("x".repeat(65))).toBeUndefined();
});
});

View File

@@ -1,23 +0,0 @@
export const TELEGRAM_CALLBACK_DATA_MAX_BYTES = 64;
const TELEGRAM_APPROVE_ALLOW_ALWAYS_PATTERN =
/^\/approve(?:@[^\s]+)?\s+[A-Za-z0-9][A-Za-z0-9._:-]*\s+allow-always$/i;
export function fitsTelegramCallbackData(value: string): boolean {
return Buffer.byteLength(value, "utf8") <= TELEGRAM_CALLBACK_DATA_MAX_BYTES;
}
export function rewriteTelegramApprovalDecisionAlias(value: string): string {
if (!value.endsWith(" allow-always")) {
return value;
}
if (!TELEGRAM_APPROVE_ALLOW_ALWAYS_PATTERN.test(value)) {
return value;
}
return value.slice(0, -"allow-always".length) + "always";
}
export function sanitizeTelegramCallbackData(value: string): string | undefined {
const rewritten = rewriteTelegramApprovalDecisionAlias(value);
return fitsTelegramCallbackData(rewritten) ? rewritten : undefined;
}

View File

@@ -4,7 +4,6 @@ import {
type InteractiveReply,
type InteractiveReplyButton,
} from "openclaw/plugin-sdk/interactive-runtime";
import { sanitizeTelegramCallbackData } from "./approval-callback-data.js";
export type TelegramButtonStyle = "danger" | "success" | "primary";
@@ -17,6 +16,11 @@ export type TelegramInlineButton = {
export type TelegramInlineButtons = ReadonlyArray<ReadonlyArray<TelegramInlineButton>>;
const TELEGRAM_INTERACTIVE_ROW_SIZE = 3;
const MAX_CALLBACK_DATA_BYTES = 64;
function fitsTelegramCallbackData(value: string): boolean {
return Buffer.byteLength(value, "utf8") <= MAX_CALLBACK_DATA_BYTES;
}
function toTelegramButtonStyle(
style?: InteractiveReplyButton["style"],
@@ -29,19 +33,14 @@ function chunkInteractiveButtons(
rows: TelegramInlineButton[][],
) {
for (let i = 0; i < buttons.length; i += TELEGRAM_INTERACTIVE_ROW_SIZE) {
const row = buttons.slice(i, i + TELEGRAM_INTERACTIVE_ROW_SIZE).flatMap((button) => {
const callbackData = sanitizeTelegramCallbackData(button.value);
if (!callbackData) {
return [];
}
return [
{
text: button.label,
callback_data: callbackData,
style: toTelegramButtonStyle(button.style),
},
];
});
const row = buttons
.slice(i, i + TELEGRAM_INTERACTIVE_ROW_SIZE)
.filter((button) => fitsTelegramCallbackData(button.value))
.map((button) => ({
text: button.label,
callback_data: button.value,
style: toTelegramButtonStyle(button.style),
}));
if (row.length > 0) {
rows.push(row);
}

View File

@@ -40,6 +40,7 @@ import {
} from "./accounts.js";
import { resolveTelegramAutoThreadId } from "./action-threading.js";
import { lookupTelegramChatId } from "./api-fetch.js";
import { buildTelegramExecApprovalButtons } from "./approval-buttons.js";
import { telegramApprovalCapability } from "./approval-native.js";
import * as auditModule from "./audit.js";
import { buildTelegramGroupPeerId } from "./bot/helpers.js";

View File

@@ -83,11 +83,11 @@ export const telegramChannelConfigUiHints = {
},
execApprovals: {
label: "Telegram Exec Approvals",
help: "Telegram-native exec approval routing and approver authorization. When unset, OpenClaw auto-enables DM-first native approvals if approvers can be resolved for the selected bot account.",
help: "Telegram-native exec approval routing and approver authorization. Enable this only when Telegram should act as an explicit exec-approval client for the selected bot account.",
},
"execApprovals.enabled": {
label: "Telegram Exec Approvals Enabled",
help: 'Controls Telegram native exec approvals for this account: unset or "auto" enables DM-first native approvals when approvers can be resolved, true forces native approvals on, and false disables them.',
help: "Enable Telegram exec approvals for this account. When false or unset, Telegram messages/buttons cannot approve exec requests.",
},
"execApprovals.approvers": {
label: "Telegram Exec Approval Approvers",

View File

@@ -21,7 +21,8 @@ import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
import { telegramNativeApprovalAdapter } from "./approval-native.js";
import { resolveTelegramInlineButtons } from "./button-types.js";
import {
isTelegramExecApprovalHandlerConfigured,
getTelegramExecApprovalApprovers,
resolveTelegramExecApprovalConfig,
shouldHandleTelegramExecApprovalRequest,
} from "./exec-approvals.js";
import { editMessageReplyMarkupTelegram, sendMessageTelegram, sendTypingTelegram } from "./send.js";
@@ -55,7 +56,19 @@ export type TelegramExecApprovalHandlerDeps = {
};
function isHandlerConfigured(params: { cfg: OpenClawConfig; accountId: string }): boolean {
return isTelegramExecApprovalHandlerConfigured(params);
const config = resolveTelegramExecApprovalConfig({
cfg: params.cfg,
accountId: params.accountId,
});
if (!config?.enabled) {
return false;
}
return (
getTelegramExecApprovalApprovers({
cfg: params.cfg,
accountId: params.accountId,
}).length > 0
);
}
export class TelegramExecApprovalHandler {

View File

@@ -28,7 +28,7 @@ function buildConfig(
}
describe("telegram exec approvals", () => {
it("auto-enables when approvers resolve and disables only when forced off", () => {
it("requires enablement and an explicit or inferred approver", () => {
expect(isTelegramExecApprovalClientEnabled({ cfg: buildConfig() })).toBe(false);
expect(
isTelegramExecApprovalClientEnabled({
@@ -37,23 +37,18 @@ describe("telegram exec approvals", () => {
).toBe(false);
expect(
isTelegramExecApprovalClientEnabled({
cfg: buildConfig(undefined, { allowFrom: ["123"] }),
cfg: buildConfig({ enabled: true }, { allowFrom: ["123"] }),
}),
).toBe(true);
expect(
isTelegramExecApprovalClientEnabled({
cfg: buildConfig({ approvers: ["123"] }),
cfg: buildConfig({ enabled: true, approvers: ["123"] }),
}),
).toBe(true);
expect(
isTelegramExecApprovalClientEnabled({
cfg: buildConfig({ enabled: false, approvers: ["123"] }),
}),
).toBe(false);
});
it("matches approvers by normalized sender id", () => {
const cfg = buildConfig({ approvers: [123, "456"] });
const cfg = buildConfig({ enabled: true, approvers: [123, "456"] });
expect(isTelegramExecApprovalApprover({ cfg, senderId: "123" })).toBe(true);
expect(isTelegramExecApprovalApprover({ cfg, senderId: "456" })).toBe(true);
expect(isTelegramExecApprovalApprover({ cfg, senderId: "789" })).toBe(false);

View File

@@ -1,6 +1,5 @@
import {
createChannelExecApprovalProfile,
isChannelExecApprovalClientEnabledFromConfig,
isChannelExecApprovalTargetRecipient,
resolveApprovalRequestAccountId,
resolveApprovalApprovers,
@@ -139,13 +138,3 @@ export function shouldSuppressLocalTelegramExecApprovalPrompt(params: {
}): boolean {
return telegramExecApprovalProfile.shouldSuppressLocalPrompt(params);
}
export function isTelegramExecApprovalHandlerConfigured(params: {
cfg: OpenClawConfig;
accountId?: string | null;
}): boolean {
return isChannelExecApprovalClientEnabledFromConfig({
enabled: resolveTelegramExecApprovalConfig(params)?.enabled,
approverCount: getTelegramExecApprovalApprovers(params).length,
});
}

View File

@@ -206,13 +206,11 @@ function resolveTelegramDispatcherPolicy(params: {
? {
mode: "explicit-proxy",
proxyUrl: explicitProxyUrl,
allowPrivateProxy: true,
proxyTls: { ...connect },
}
: {
mode: "explicit-proxy",
proxyUrl: explicitProxyUrl,
allowPrivateProxy: true,
},
mode: "explicit-proxy",
};

View File

@@ -100,34 +100,6 @@ describe("buildTelegramInteractiveButtons", () => {
}),
).toEqual([[{ text: "Keep", callback_data: "keep", style: undefined }]]);
});
it("rewrites /approve allow-always callbacks to always so plugin IDs fit Telegram limits", () => {
const pluginApprovalId = `plugin:${"a".repeat(36)}`;
expect(
buildTelegramInteractiveButtons({
blocks: [
{
type: "buttons",
buttons: [
{
label: "Allow Always",
value: `/approve ${pluginApprovalId} allow-always`,
style: "primary",
},
],
},
],
}),
).toEqual([
[
{
text: "Allow Always",
callback_data: `/approve ${pluginApprovalId} always`,
style: "primary",
},
],
]);
});
});
describe("resolveTelegramInlineButtons", () => {

View File

@@ -8,7 +8,6 @@
* - mdl_sel/{model} - select model (compact fallback when standard is >64 bytes)
* - mdl_back - back to providers list
*/
import { fitsTelegramCallbackData } from "./approval-callback-data.js";
export type ButtonRow = Array<{ text: string; callback_data: string }>;
@@ -40,6 +39,7 @@ export type ModelsKeyboardParams = {
};
const MODELS_PAGE_SIZE = 8;
const MAX_CALLBACK_DATA_BYTES = 64;
const CALLBACK_PREFIX = {
providers: "mdl_prov",
back: "mdl_back",
@@ -108,11 +108,13 @@ export function buildModelSelectionCallbackData(params: {
model: string;
}): string | null {
const fullCallbackData = `${CALLBACK_PREFIX.selectStandard}${params.provider}/${params.model}`;
if (fitsTelegramCallbackData(fullCallbackData)) {
if (Buffer.byteLength(fullCallbackData, "utf8") <= MAX_CALLBACK_DATA_BYTES) {
return fullCallbackData;
}
const compactCallbackData = `${CALLBACK_PREFIX.selectCompact}${params.model}`;
return fitsTelegramCallbackData(compactCallbackData) ? compactCallbackData : null;
return Buffer.byteLength(compactCallbackData, "utf8") <= MAX_CALLBACK_DATA_BYTES
? compactCallbackData
: null;
}
export function resolveModelSelection(params: {

View File

@@ -8,7 +8,6 @@ import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
import { formatErrorMessage } from "openclaw/plugin-sdk/ssrf-runtime";
import { resolveTelegramAccount } from "./accounts.js";
import { resolveTelegramAllowedUpdates } from "./allowed-updates.js";
import { isTelegramExecApprovalHandlerConfigured } from "./exec-approvals.js";
import { resolveTelegramTransport } from "./fetch.js";
import {
isRecoverableTelegramNetworkError,
@@ -146,15 +145,13 @@ export async function monitorTelegramProvider(opts: MonitorTelegramOpts = {}) {
if (opts.useWebhook) {
const { TelegramExecApprovalHandler, startTelegramWebhook } =
await loadTelegramMonitorWebhookRuntime();
if (isTelegramExecApprovalHandlerConfigured({ cfg, accountId: account.accountId })) {
execApprovalsHandler = new TelegramExecApprovalHandler({
token,
accountId: account.accountId,
cfg,
runtime: opts.runtime,
});
await execApprovalsHandler.start();
}
execApprovalsHandler = new TelegramExecApprovalHandler({
token,
accountId: account.accountId,
cfg,
runtime: opts.runtime,
});
await execApprovalsHandler.start();
await startTelegramWebhook({
token,
accountId: account.accountId,
@@ -180,15 +177,13 @@ export async function monitorTelegramProvider(opts: MonitorTelegramOpts = {}) {
writeTelegramUpdateOffset,
} = await loadTelegramMonitorPollingRuntime();
if (isTelegramExecApprovalHandlerConfigured({ cfg, accountId: account.accountId })) {
execApprovalsHandler = new TelegramExecApprovalHandler({
token,
accountId: account.accountId,
cfg,
runtime: opts.runtime,
});
await execApprovalsHandler.start();
}
execApprovalsHandler = new TelegramExecApprovalHandler({
token,
accountId: account.accountId,
cfg,
runtime: opts.runtime,
});
await execApprovalsHandler.start();
const persistedOffsetRaw = await readTelegramUpdateOffset({
accountId: account.accountId,

View File

@@ -231,44 +231,6 @@ describe("xai x_search tool", () => {
);
});
it("uses migrated runtime auth when the source config still carries legacy x_search apiKey", async () => {
const mockFetch = installXSearchFetch();
const tool = createXSearchTool({
config: {
tools: {
web: {
x_search: {
apiKey: "legacy-x-search-key", // pragma: allowlist secret
enabled: true,
} as Record<string, unknown>,
},
},
},
runtimeConfig: {
plugins: {
entries: {
xai: {
config: {
webSearch: {
apiKey: "migrated-runtime-key", // pragma: allowlist secret
},
},
},
},
},
},
});
await tool?.execute?.("x-search:migrated-runtime-key", {
query: "migrated runtime auth",
});
const request = mockFetch.mock.calls[0]?.[1] as RequestInit | undefined;
expect((request?.headers as Record<string, string> | undefined)?.Authorization).toBe(
"Bearer migrated-runtime-key",
);
});
it("rejects invalid date ordering before calling xAI", async () => {
const mockFetch = installXSearchFetch();
const tool = createXSearchTool({

View File

@@ -77,6 +77,11 @@ function resolveFallbackXaiApiKey(cfg?: OpenClawConfig): string | undefined {
return readPluginXaiWebSearchApiKey(cfg) ?? readLegacyGrokApiKey(cfg);
}
function readLegacyXSearchApiKey(cfg?: OpenClawConfig): string | undefined {
const legacyConfig = resolveLegacyXSearchConfig(cfg);
return readConfiguredSecretString(legacyConfig?.apiKey, "tools.web.x_search.apiKey");
}
function resolveXSearchConfig(cfg?: OpenClawConfig): Record<string, unknown> | undefined {
return resolveEffectiveXSearchConfig(cfg);
}
@@ -89,19 +94,33 @@ function resolveXSearchEnabled(params: {
if (params.config?.enabled === false) {
return false;
}
if (resolveFallbackXaiApiKey(params.runtimeConfig)) {
if (
resolveFallbackXaiApiKey(params.runtimeConfig) ||
readLegacyXSearchApiKey(params.runtimeConfig)
) {
return true;
}
return Boolean(resolveFallbackXaiApiKey(params.cfg) || readProviderEnvValue(["XAI_API_KEY"]));
return Boolean(
resolveFallbackXaiApiKey(params.cfg) ||
readLegacyXSearchApiKey(params.cfg) ||
readProviderEnvValue(["XAI_API_KEY"]),
);
}
function resolveXSearchApiKey(params: {
sourceConfig?: OpenClawConfig;
runtimeConfig?: OpenClawConfig;
}): string | undefined {
const sourceXSearchConfig = resolveXSearchConfig(params.sourceConfig);
const runtimeXSearchConfig =
params.runtimeConfig && params.runtimeConfig !== params.sourceConfig
? resolveXSearchConfig(params.runtimeConfig)
: undefined;
return (
resolveFallbackXaiApiKey(params.runtimeConfig) ??
resolveFallbackXaiApiKey(params.sourceConfig) ??
readLegacyXSearchApiKey(params.runtimeConfig) ??
readLegacyXSearchApiKey(params.sourceConfig) ??
readProviderEnvValue(["XAI_API_KEY"])
);
}

View File

@@ -397,120 +397,6 @@ describe("handleZaloWebhookRequest", () => {
}
});
it("keeps replay dedupe isolated when path/account values collide under colon-joined keys", async () => {
const sinkA = vi.fn();
const sinkB = vi.fn();
// Old key format `${path}:${accountId}:${event_name}:${messageId}` would collide for these two targets.
const unregisterA = registerTarget({
path: "/hook-replay-collision:a",
secret: "secret-a",
statusSink: sinkA,
account: {
...DEFAULT_ACCOUNT,
accountId: "team",
},
});
const unregisterB = registerTarget({
path: "/hook-replay-collision",
secret: "secret-b",
statusSink: sinkB,
account: {
...DEFAULT_ACCOUNT,
accountId: "a:team",
},
});
const payload = createTextUpdate({
messageId: "msg-replay-collision-1",
userId: "123",
userName: "",
chatId: "123",
text: "hello",
});
try {
await withServer(webhookRequestHandler, async (baseUrl) => {
const first = await fetch(`${baseUrl}/hook-replay-collision:a`, {
method: "POST",
headers: {
"x-bot-api-secret-token": "secret-a",
"content-type": "application/json",
},
body: JSON.stringify(payload),
});
const second = await fetch(`${baseUrl}/hook-replay-collision`, {
method: "POST",
headers: {
"x-bot-api-secret-token": "secret-b",
"content-type": "application/json",
},
body: JSON.stringify(payload),
});
expect(first.status).toBe(200);
expect(second.status).toBe(200);
});
expect(sinkA).toHaveBeenCalledTimes(1);
expect(sinkB).toHaveBeenCalledTimes(1);
} finally {
unregisterA();
unregisterB();
}
});
it("keeps replay dedupe isolated across different webhook paths", async () => {
const sinkA = vi.fn();
const sinkB = vi.fn();
const sharedSecret = "secret";
const unregisterA = registerTarget({
path: "/hook-replay-scope-a",
secret: sharedSecret,
statusSink: sinkA,
});
const unregisterB = registerTarget({
path: "/hook-replay-scope-b",
secret: sharedSecret,
statusSink: sinkB,
});
const payload = createTextUpdate({
messageId: "msg-replay-cross-path-1",
userId: "123",
userName: "",
chatId: "123",
text: "hello",
});
try {
await withServer(webhookRequestHandler, async (baseUrl) => {
const first = await fetch(`${baseUrl}/hook-replay-scope-a`, {
method: "POST",
headers: {
"x-bot-api-secret-token": sharedSecret,
"content-type": "application/json",
},
body: JSON.stringify(payload),
});
const second = await fetch(`${baseUrl}/hook-replay-scope-b`, {
method: "POST",
headers: {
"x-bot-api-secret-token": sharedSecret,
"content-type": "application/json",
},
body: JSON.stringify(payload),
});
expect(first.status).toBe(200);
expect(second.status).toBe(200);
});
expect(sinkA).toHaveBeenCalledTimes(1);
expect(sinkB).toHaveBeenCalledTimes(1);
} finally {
unregisterA();
unregisterB();
}
});
it("downloads inbound image media from webhook photo_url and preserves display_name", async () => {
const {
core,

View File

@@ -75,22 +75,23 @@ function timingSafeEquals(left: string, right: string): boolean {
return safeEqualSecret(left, right);
}
function buildReplayEventCacheKey(
target: ZaloWebhookTarget,
update: ZaloUpdate,
messageId: string,
): string {
const chatId = update.message?.chat?.id ?? "";
const senderId = update.message?.from?.id ?? "";
return JSON.stringify([target.path, target.account.accountId, update.event_name, chatId, senderId, messageId]);
}
function isReplayEvent(target: ZaloWebhookTarget, update: ZaloUpdate, nowMs: number): boolean {
const messageId = update.message?.message_id;
if (!messageId) {
return false;
}
const key = buildReplayEventCacheKey(target, update, messageId);
const chatId = update.message?.chat?.id ?? "";
const senderId = update.message?.from?.id ?? "";
// Scope replay dedupe to the authenticated target and the message origin so
// reused message ids in other chats or from other senders do not collide.
const key = [
target.path,
target.account.accountId,
update.event_name,
chatId,
senderId,
messageId,
].join(":");
return recentWebhookEvents.check(key, nowMs);
}

View File

@@ -1,6 +1,6 @@
{
"name": "openclaw",
"version": "2026.4.2-beta.1",
"version": "2026.4.2",
"description": "Multi-channel AI gateway with extensible messaging integrations",
"keywords": [],
"homepage": "https://github.com/openclaw/openclaw#readme",
@@ -1155,12 +1155,11 @@
"test:contracts:plugins": "OPENCLAW_TEST_PROFILE=serial pnpm exec vitest run --config vitest.contracts.config.ts src/plugins/contracts",
"test:coverage": "vitest run --config vitest.unit.config.ts --coverage",
"test:coverage:changed": "vitest run --config vitest.unit.config.ts --coverage --changed origin/main",
"test:docker:all": "pnpm test:docker:live-build && OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:live-models && OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:live-gateway && pnpm test:docker:openwebui && pnpm test:docker:onboard && pnpm test:docker:gateway-network && pnpm test:docker:mcp-channels && pnpm test:docker:qr && pnpm test:docker:doctor-switch && pnpm test:docker:plugins && pnpm test:docker:cleanup",
"test:docker:all": "pnpm test:docker:live-models && pnpm test:docker:live-gateway && pnpm test:docker:openwebui && pnpm test:docker:onboard && pnpm test:docker:gateway-network && pnpm test:docker:mcp-channels && pnpm test:docker:qr && pnpm test:docker:doctor-switch && pnpm test:docker:plugins && pnpm test:docker:cleanup",
"test:docker:cleanup": "bash scripts/test-cleanup-docker.sh",
"test:docker:doctor-switch": "bash scripts/e2e/doctor-install-switch-docker.sh",
"test:docker:gateway-network": "bash scripts/e2e/gateway-network-docker.sh",
"test:docker:live-acp-bind": "bash scripts/test-live-acp-bind-docker.sh",
"test:docker:live-build": "bash scripts/test-live-build-docker.sh",
"test:docker:live-cli-backend": "bash scripts/test-live-cli-backend-docker.sh",
"test:docker:live-gateway": "bash scripts/test-live-gateway-models-docker.sh",
"test:docker:live-models": "bash scripts/test-live-models-docker.sh",
@@ -1183,8 +1182,6 @@
"test:install:e2e:openai": "OPENCLAW_E2E_MODELS=openai bash scripts/test-install-sh-e2e-docker.sh",
"test:install:smoke": "bash scripts/test-install-sh-docker.sh",
"test:live": "node scripts/test-live.mjs",
"test:live:gateway-profiles": "node scripts/test-live.mjs -- src/gateway/gateway-models.profiles.live.test.ts",
"test:live:models-profiles": "node scripts/test-live.mjs -- src/agents/models.profiles.live.test.ts",
"test:max": "node scripts/test-parallel.mjs --profile max",
"test:parallels:linux": "bash scripts/e2e/parallels-linux-smoke.sh",
"test:parallels:macos": "bash scripts/e2e/parallels-macos-smoke.sh",
@@ -1276,7 +1273,7 @@
"@types/node": "^25.5.0",
"@types/qrcode-terminal": "^0.12.2",
"@types/ws": "^8.18.1",
"@typescript/native-preview": "7.0.0-dev.20260401.1",
"@typescript/native-preview": "7.0.0-dev.20260331.1",
"@vitest/coverage-v8": "^4.1.2",
"jscpd": "4.0.8",
"jsdom": "^29.0.1",

106
pnpm-lock.yaml generated
View File

@@ -201,8 +201,8 @@ importers:
specifier: ^8.18.1
version: 8.18.1
'@typescript/native-preview':
specifier: 7.0.0-dev.20260401.1
version: 7.0.0-dev.20260401.1
specifier: 7.0.0-dev.20260331.1
version: 7.0.0-dev.20260331.1
'@vitest/coverage-v8':
specifier: ^4.1.2
version: 4.1.2(@vitest/browser@4.1.2(vite@8.0.3(@emnapi/core@1.8.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2))(vitest@4.1.2)
@@ -232,7 +232,7 @@ importers:
version: 0.21.1(signal-polyfill@0.2.2)
tsdown:
specifier: 0.21.7
version: 0.21.7(@emnapi/core@1.8.1)(@emnapi/runtime@1.9.1)(@typescript/native-preview@7.0.0-dev.20260401.1)(typescript@6.0.2)
version: 0.21.7(@emnapi/core@1.8.1)(@emnapi/runtime@1.9.1)(@typescript/native-preview@7.0.0-dev.20260331.1)(typescript@6.0.2)
tsx:
specifier: ^4.21.0
version: 4.21.0
@@ -343,10 +343,10 @@ importers:
dependencies:
'@buape/carbon':
specifier: 0.0.0-beta-20260327000044
version: 0.0.0-beta-20260327000044(@discordjs/opus@0.10.0)(hono@4.12.9)(opusscript@0.0.8)
version: 0.0.0-beta-20260327000044(@discordjs/opus@0.10.0)(hono@4.12.9)(opusscript@0.1.1)
'@discordjs/voice':
specifier: ^0.19.2
version: 0.19.2(@discordjs/opus@0.10.0)(@emnapi/core@1.8.1)(@emnapi/runtime@1.9.1)(opusscript@0.0.8)
version: 0.19.2(@discordjs/opus@0.10.0)(@emnapi/core@1.8.1)(@emnapi/runtime@1.9.1)(opusscript@0.1.1)
discord-api-types:
specifier: ^0.38.43
version: 0.38.43
@@ -354,8 +354,8 @@ importers:
specifier: ^8.0.0
version: 8.0.0
opusscript:
specifier: ^0.0.8
version: 0.0.8
specifier: ^0.1.1
version: 0.1.1
devDependencies:
openclaw:
specifier: workspace:*
@@ -3459,43 +3459,43 @@ packages:
'@types/yauzl@2.10.3':
resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==}
'@typescript/native-preview-darwin-arm64@7.0.0-dev.20260401.1':
resolution: {integrity: sha512-9PCc1D4/zLic30g1upOw6ZmUl98fnrXYRv5wIZ6fxm1zZAObieRKUX3Jbr8M9N8iQQFxPIZPniIScsxAbmbJvw==}
'@typescript/native-preview-darwin-arm64@7.0.0-dev.20260331.1':
resolution: {integrity: sha512-1PRnBCN2csiCzj76YaSBtP4jPLEGBUmVhXHplC+yHOKaxx9nf3HFiFCg/19raInvN/lJ8+Bp1fZ/qIsWAAHiBw==}
cpu: [arm64]
os: [darwin]
'@typescript/native-preview-darwin-x64@7.0.0-dev.20260401.1':
resolution: {integrity: sha512-wwzca1KrjSVC6ApXfITsg/wF4GGbhVYebc7zChpuyi+phrHfw6ThTPB5XFUH4nA32vqw0Hn/6KACipMgzg8GPA==}
'@typescript/native-preview-darwin-x64@7.0.0-dev.20260331.1':
resolution: {integrity: sha512-llXnfLGjl+gXsANLD7UI/gSb3lj7aZW13Rf8sVXQnHJ3/dkJRAm/MgLqdjuuyvYq3pFaleiep+zoLd96rLRqUw==}
cpu: [x64]
os: [darwin]
'@typescript/native-preview-linux-arm64@7.0.0-dev.20260401.1':
resolution: {integrity: sha512-1hgKibGi4QZF1J0hKltgY4nj4yKDmI4Ang5ar80I+YeUdGxV/fP2kU3bJang7QtHuSso6W+a52SF62zgqbzdow==}
'@typescript/native-preview-linux-arm64@7.0.0-dev.20260331.1':
resolution: {integrity: sha512-sH5gALi89jl5ZjAL/UsLDPsjT/nCLRfHl/pw86ablRX10tYsJhJ/RD6J/cl3g39kJ18tIISSbsuIBn+ncanfSA==}
cpu: [arm64]
os: [linux]
'@typescript/native-preview-linux-arm@7.0.0-dev.20260401.1':
resolution: {integrity: sha512-bbIkRZYjtyoyCJ3wFES7qn3EwYO5Go1hxArL5X5oWiBmUHq5gMIxTZDv5mpWxopVf9Eyh4ErHefXjf1s4J+6Ag==}
'@typescript/native-preview-linux-arm@7.0.0-dev.20260331.1':
resolution: {integrity: sha512-+8AZzA0BRjMkLDvdQKZOMuheRxNGpSWn7sOtoKqo70R915D0TyEynEXX6B7/aw3+Jfn1H5hLRiBjxoVsmdKENw==}
cpu: [arm]
os: [linux]
'@typescript/native-preview-linux-x64@7.0.0-dev.20260401.1':
resolution: {integrity: sha512-1ysZ4c/Wa3RYIlrwVceYlhb1m1hxQ4P2x92valZXH0bNWEPb+oiQ4Yf35O/vi5h8zDdX/ZQ59vivYl27cF1VVA==}
'@typescript/native-preview-linux-x64@7.0.0-dev.20260331.1':
resolution: {integrity: sha512-Yic6MYfX7Uit5jLLENzWFIi6tjp4LTLF37KBiVaHZSvEFyX1kqVwu4j9WNeaz81O6fcB/1dZ1MrILgfcqalNBg==}
cpu: [x64]
os: [linux]
'@typescript/native-preview-win32-arm64@7.0.0-dev.20260401.1':
resolution: {integrity: sha512-fZYLCRe36y1BuzRFFpU2/RQ212l6Y1dccRMh8oTB8HlAVAAwtbkb6cjEn0Ablj4Dy16+Ih8R9uHsxPLNhtKw1Q==}
'@typescript/native-preview-win32-arm64@7.0.0-dev.20260331.1':
resolution: {integrity: sha512-vGxK6gtGF97zSx9wOpiVME3h9v0tbZbrHHdKA+fLFNvDV0Df8ud89DEePL7l2yKnVVmf0OnjJy6sYoVyj+LIPA==}
cpu: [arm64]
os: [win32]
'@typescript/native-preview-win32-x64@7.0.0-dev.20260401.1':
resolution: {integrity: sha512-I6ses4SjWvpbvSpm1BPFRrDeqrzu7JTchJG/a26iwwmTLv4fAGLc5/o6Kv9Naygozop1W3KOcVM5i3A9oxiIjQ==}
'@typescript/native-preview-win32-x64@7.0.0-dev.20260331.1':
resolution: {integrity: sha512-oJnNiU9UTDPJp6dOmOUW+/Wzt3MQZXIHsDaU4qM0RiAjFE6S+PIX8s5z/ID0orr4MMroUMiLdolL4OVZolNDSw==}
cpu: [x64]
os: [win32]
'@typescript/native-preview@7.0.0-dev.20260401.1':
resolution: {integrity: sha512-xJcN9WlY/P6xKjCMH4+DTzZSj/EKR6H9avuqUKs4eKyPEvaQ4bX+9Ys3Vl2qhlUaD7IRWY7HN7db0LHAGlWRSA==}
'@typescript/native-preview@7.0.0-dev.20260331.1':
resolution: {integrity: sha512-Gfy2J/LhydkOHOw+ZWRw0M8Xl3O2bzQXLXIYITdMz2N4GpMm8misAvvCzhqMacOGvazKr1FsL9LIIW2kxk6kzw==}
hasBin: true
'@ungap/structured-clone@1.3.0':
@@ -5399,8 +5399,8 @@ packages:
opus-decoder@0.7.11:
resolution: {integrity: sha512-+e+Jz3vGQLxRTBHs8YJQPRPc1Tr+/aC6coV/DlZylriA29BdHQAYXhvNRKtjftof17OFng0+P4wsFIqQu3a48A==}
opusscript@0.0.8:
resolution: {integrity: sha512-VSTi1aWFuCkRCVq+tx/BQ5q9fMnQ9pVZ3JU4UHKqTkf0ED3fKEPdr+gKAAl3IA2hj9rrP6iyq3hlcJq3HELtNQ==}
opusscript@0.1.1:
resolution: {integrity: sha512-mL0fZZOUnXdZ78woRXp18lApwpp0lF5tozJOD1Wut0dgrA9WuQTgSels/CSmFleaAZrJi/nci5KOVtbuxeWoQA==}
ora@9.3.0:
resolution: {integrity: sha512-lBX72MWFduWEf7v7uWf5DHp9Jn5BI8bNPGuFgtXMmr2uDz2Gz2749y3am3agSDdkhHPHYmmxEGSKH85ZLGzgXw==}
@@ -7349,13 +7349,13 @@ snapshots:
dependencies:
css-tree: 3.2.1
'@buape/carbon@0.0.0-beta-20260327000044(@discordjs/opus@0.10.0)(hono@4.12.9)(opusscript@0.0.8)':
'@buape/carbon@0.0.0-beta-20260327000044(@discordjs/opus@0.10.0)(hono@4.12.9)(opusscript@0.1.1)':
dependencies:
'@types/node': 25.5.0
discord-api-types: 0.38.37
optionalDependencies:
'@cloudflare/workers-types': 4.20260120.0
'@discordjs/voice': 0.19.0(@discordjs/opus@0.10.0)(opusscript@0.0.8)
'@discordjs/voice': 0.19.0(@discordjs/opus@0.10.0)(opusscript@0.1.1)
'@hono/node-server': 1.19.10(hono@4.12.9)
'@types/bun': 1.3.9
'@types/ws': 8.18.1
@@ -7507,11 +7507,11 @@ snapshots:
- supports-color
optional: true
'@discordjs/voice@0.19.0(@discordjs/opus@0.10.0)(opusscript@0.0.8)':
'@discordjs/voice@0.19.0(@discordjs/opus@0.10.0)(opusscript@0.1.1)':
dependencies:
'@types/ws': 8.18.1
discord-api-types: 0.38.43
prism-media: 1.3.5(@discordjs/opus@0.10.0)(opusscript@0.0.8)
prism-media: 1.3.5(@discordjs/opus@0.10.0)(opusscript@0.1.1)
tslib: 2.8.1
ws: 8.20.0
transitivePeerDependencies:
@@ -7523,12 +7523,12 @@ snapshots:
- utf-8-validate
optional: true
'@discordjs/voice@0.19.2(@discordjs/opus@0.10.0)(@emnapi/core@1.8.1)(@emnapi/runtime@1.9.1)(opusscript@0.0.8)':
'@discordjs/voice@0.19.2(@discordjs/opus@0.10.0)(@emnapi/core@1.8.1)(@emnapi/runtime@1.9.1)(opusscript@0.1.1)':
dependencies:
'@snazzah/davey': 0.1.11(@emnapi/core@1.8.1)(@emnapi/runtime@1.9.1)
'@types/ws': 8.18.1
discord-api-types: 0.38.43
prism-media: 1.3.5(@discordjs/opus@0.10.0)(opusscript@0.0.8)
prism-media: 1.3.5(@discordjs/opus@0.10.0)(opusscript@0.1.1)
tslib: 2.8.1
ws: 8.20.0
transitivePeerDependencies:
@@ -9814,36 +9814,36 @@ snapshots:
'@types/node': 25.5.0
optional: true
'@typescript/native-preview-darwin-arm64@7.0.0-dev.20260401.1':
'@typescript/native-preview-darwin-arm64@7.0.0-dev.20260331.1':
optional: true
'@typescript/native-preview-darwin-x64@7.0.0-dev.20260401.1':
'@typescript/native-preview-darwin-x64@7.0.0-dev.20260331.1':
optional: true
'@typescript/native-preview-linux-arm64@7.0.0-dev.20260401.1':
'@typescript/native-preview-linux-arm64@7.0.0-dev.20260331.1':
optional: true
'@typescript/native-preview-linux-arm@7.0.0-dev.20260401.1':
'@typescript/native-preview-linux-arm@7.0.0-dev.20260331.1':
optional: true
'@typescript/native-preview-linux-x64@7.0.0-dev.20260401.1':
'@typescript/native-preview-linux-x64@7.0.0-dev.20260331.1':
optional: true
'@typescript/native-preview-win32-arm64@7.0.0-dev.20260401.1':
'@typescript/native-preview-win32-arm64@7.0.0-dev.20260331.1':
optional: true
'@typescript/native-preview-win32-x64@7.0.0-dev.20260401.1':
'@typescript/native-preview-win32-x64@7.0.0-dev.20260331.1':
optional: true
'@typescript/native-preview@7.0.0-dev.20260401.1':
'@typescript/native-preview@7.0.0-dev.20260331.1':
optionalDependencies:
'@typescript/native-preview-darwin-arm64': 7.0.0-dev.20260401.1
'@typescript/native-preview-darwin-x64': 7.0.0-dev.20260401.1
'@typescript/native-preview-linux-arm': 7.0.0-dev.20260401.1
'@typescript/native-preview-linux-arm64': 7.0.0-dev.20260401.1
'@typescript/native-preview-linux-x64': 7.0.0-dev.20260401.1
'@typescript/native-preview-win32-arm64': 7.0.0-dev.20260401.1
'@typescript/native-preview-win32-x64': 7.0.0-dev.20260401.1
'@typescript/native-preview-darwin-arm64': 7.0.0-dev.20260331.1
'@typescript/native-preview-darwin-x64': 7.0.0-dev.20260331.1
'@typescript/native-preview-linux-arm': 7.0.0-dev.20260331.1
'@typescript/native-preview-linux-arm64': 7.0.0-dev.20260331.1
'@typescript/native-preview-linux-x64': 7.0.0-dev.20260331.1
'@typescript/native-preview-win32-arm64': 7.0.0-dev.20260331.1
'@typescript/native-preview-win32-x64': 7.0.0-dev.20260331.1
'@ungap/structured-clone@1.3.0': {}
@@ -11947,7 +11947,7 @@ snapshots:
'@wasm-audio-decoders/common': 9.0.7
optional: true
opusscript@0.0.8: {}
opusscript@0.1.1: {}
ora@9.3.0:
dependencies:
@@ -12184,10 +12184,10 @@ snapshots:
dependencies:
parse-ms: 4.0.0
prism-media@1.3.5(@discordjs/opus@0.10.0)(opusscript@0.0.8):
prism-media@1.3.5(@discordjs/opus@0.10.0)(opusscript@0.1.1):
optionalDependencies:
'@discordjs/opus': 0.10.0
opusscript: 0.0.8
opusscript: 0.1.1
process-nextick-args@2.0.1: {}
@@ -12456,7 +12456,7 @@ snapshots:
glob: 7.2.3
optional: true
rolldown-plugin-dts@0.23.2(@typescript/native-preview@7.0.0-dev.20260401.1)(rolldown@1.0.0-rc.12(@emnapi/core@1.8.1)(@emnapi/runtime@1.9.1))(typescript@6.0.2):
rolldown-plugin-dts@0.23.2(@typescript/native-preview@7.0.0-dev.20260331.1)(rolldown@1.0.0-rc.12(@emnapi/core@1.8.1)(@emnapi/runtime@1.9.1))(typescript@6.0.2):
dependencies:
'@babel/generator': 8.0.0-rc.3
'@babel/helper-validator-identifier': 8.0.0-rc.3
@@ -12470,7 +12470,7 @@ snapshots:
picomatch: 4.0.4
rolldown: 1.0.0-rc.12(@emnapi/core@1.8.1)(@emnapi/runtime@1.9.1)
optionalDependencies:
'@typescript/native-preview': 7.0.0-dev.20260401.1
'@typescript/native-preview': 7.0.0-dev.20260331.1
typescript: 6.0.2
transitivePeerDependencies:
- oxc-resolver
@@ -12963,7 +12963,7 @@ snapshots:
ts-algebra@2.0.0: {}
tsdown@0.21.7(@emnapi/core@1.8.1)(@emnapi/runtime@1.9.1)(@typescript/native-preview@7.0.0-dev.20260401.1)(typescript@6.0.2):
tsdown@0.21.7(@emnapi/core@1.8.1)(@emnapi/runtime@1.9.1)(@typescript/native-preview@7.0.0-dev.20260331.1)(typescript@6.0.2):
dependencies:
ansis: 4.2.0
cac: 7.0.0
@@ -12974,7 +12974,7 @@ snapshots:
obug: 2.1.1
picomatch: 4.0.4
rolldown: 1.0.0-rc.12(@emnapi/core@1.8.1)(@emnapi/runtime@1.9.1)
rolldown-plugin-dts: 0.23.2(@typescript/native-preview@7.0.0-dev.20260401.1)(rolldown@1.0.0-rc.12(@emnapi/core@1.8.1)(@emnapi/runtime@1.9.1))(typescript@6.0.2)
rolldown-plugin-dts: 0.23.2(@typescript/native-preview@7.0.0-dev.20260331.1)(rolldown@1.0.0-rc.12(@emnapi/core@1.8.1)(@emnapi/runtime@1.9.1))(typescript@6.0.2)
semver: 7.7.4
tinyexec: 1.0.4
tinyglobby: 0.2.15

View File

@@ -281,23 +281,35 @@ function Invoke-CaptureLogged {
}
try {
function Write-Phase {
param([Parameter(Mandatory = $true)][string]$Message)
('==> ' + $Message) | Tee-Object -FilePath $LogPath -Append | Out-Null
}
$env:PATH = "$env:LOCALAPPDATA\OpenClaw\deps\portable-git\cmd;$env:LOCALAPPDATA\OpenClaw\deps\portable-git\mingw64\bin;$env:LOCALAPPDATA\OpenClaw\deps\portable-git\usr\bin;$env:PATH"
$tgz = Join-Path $env:TEMP 'openclaw-main-update.tgz'
Remove-Item $tgz, $LogPath, $DonePath -Force -ErrorAction SilentlyContinue
Set-Item -Path ('Env:' + $ProviderKeyEnv) -Value $ProviderKey
Write-Phase 'windows update.download-current-tgz'
Invoke-Logged 'download current tgz' { curl.exe -fsSL $TgzUrl -o $tgz }
Write-Phase 'windows update.install-current-tgz'
Invoke-Logged 'npm install current tgz' { npm.cmd install -g $tgz --no-fund --no-audit }
$openclaw = Join-Path $env:APPDATA 'npm\openclaw.cmd'
Write-Phase 'windows update.verify-version'
$version = Invoke-CaptureLogged 'openclaw --version' { & $openclaw --version }
if ($version -notmatch [regex]::Escape($HeadShort)) {
throw "version mismatch: expected substring $HeadShort"
}
Write-Phase 'windows update.set-model'
Invoke-Logged 'openclaw models set' { & $openclaw models set $ModelId }
# Windows can keep the old hashed dist modules alive across in-place global npm upgrades.
# Restart the gateway/service before verifying status or the next agent turn.
Write-Phase 'windows update.gateway-restart'
Invoke-Logged 'openclaw gateway restart' { & $openclaw gateway restart }
Start-Sleep -Seconds 5
Write-Phase 'windows update.gateway-status'
Invoke-Logged 'openclaw gateway status' { & $openclaw gateway status --deep --require-rpc }
Write-Phase 'windows update.agent-turn'
Invoke-CaptureLogged 'openclaw agent' { & $openclaw agent --agent main --session-id $SessionId --message 'Reply with exact ASCII text OK only.' --json } | Out-Null
$exitCode = $LASTEXITCODE
if ($null -eq $exitCode) {
@@ -502,6 +514,67 @@ PY
host_timeout_exec "$timeout_s" prlctl exec "$WINDOWS_VM" --current-user powershell.exe -NoProfile -ExecutionPolicy Bypass -EncodedCommand "$encoded"
}
launch_guest_helper() {
local label="$1"
local script="$2"
local attempt rc
for attempt in 1 2; do
say "$label launch attempt $attempt/2"
set +e
guest_powershell "$script"
rc=$?
set -e
if [[ $rc -eq 0 ]]; then
return 0
fi
warn "$label launch failed (rc=$rc)"
sleep 2
done
return 1
}
drain_guest_log_incremental() {
local log_name="$1"
local count_var="$2"
local current_count="${!count_var:-0}"
local count_output count_line count_rc new_output new_rc
set +e
count_output="$(
guest_powershell_poll 20 "\$log = Join-Path \$env:TEMP '$log_name'; if (Test-Path \$log) { (Get-Content \$log).Count } else { 0 }"
)"
count_rc=$?
set -e
if [[ $count_rc -ne 0 ]]; then
return "$count_rc"
fi
count_output="${count_output//$'\r'/}"
count_line="${count_output##*$'\n'}"
[[ "$count_line" =~ ^[0-9]+$ ]] || return 0
if (( count_line <= current_count )); then
return 0
fi
set +e
new_output="$(
guest_powershell_poll 20 "\$log = Join-Path \$env:TEMP '$log_name'; if (Test-Path \$log) { Get-Content \$log | Select-Object -Skip $current_count }"
)"
new_rc=$?
set -e
if [[ $new_rc -ne 0 ]]; then
return "$new_rc"
fi
new_output="${new_output//$'\r'/}"
if [[ -n "$new_output" ]]; then
printf '%s\n' "$new_output"
fi
printf -v "$count_var" '%s' "$count_line"
}
run_windows_script_via_log() {
local script_url="$1"
local tgz_url="$2"
@@ -511,15 +584,16 @@ run_windows_script_via_log() {
local provider_key_env="$6"
local provider_key="$7"
local runner_name log_name done_name done_status launcher_state
local start_seconds poll_deadline startup_checked poll_rc state_rc log_rc
local start_seconds poll_deadline startup_checked poll_rc state_rc log_rc log_line_count
runner_name="openclaw-update-$RANDOM-$RANDOM.ps1"
log_name="openclaw-update-$RANDOM-$RANDOM.log"
done_name="openclaw-update-$RANDOM-$RANDOM.done"
start_seconds="$SECONDS"
poll_deadline=$((SECONDS + 900))
startup_checked=0
log_line_count=0
guest_powershell "$(cat <<EOF
launch_guest_helper "windows update helper" "$(cat <<EOF
\$runner = Join-Path \$env:TEMP '$runner_name'
\$log = Join-Path \$env:TEMP '$log_name'
\$done = Join-Path \$env:TEMP '$done_name'
@@ -539,9 +613,17 @@ Start-Process powershell.exe -ArgumentList @(
'-DonePath', \$done
) -WindowStyle Hidden | Out-Null
EOF
)"
)" || return 1
while :; do
set +e
drain_guest_log_incremental "$log_name" log_line_count
log_rc=$?
set -e
if [[ $log_rc -ne 0 ]]; then
warn "windows update helper live log poll failed; retrying"
fi
set +e
done_status="$(
guest_powershell_poll 20 "\$done = Join-Path \$env:TEMP '$done_name'; if (Test-Path \$done) { (Get-Content \$done -Raw).Trim() }"
@@ -560,7 +642,7 @@ EOF
fi
if [[ -n "$done_status" ]]; then
set +e
guest_powershell_poll 20 "\$log = Join-Path \$env:TEMP '$log_name'; if (Test-Path \$log) { Get-Content \$log }"
drain_guest_log_incremental "$log_name" log_line_count
log_rc=$?
set -e
if [[ $log_rc -ne 0 ]]; then
@@ -585,7 +667,7 @@ EOF
fi
if (( SECONDS >= poll_deadline )); then
set +e
guest_powershell_poll 20 "\$log = Join-Path \$env:TEMP '$log_name'; if (Test-Path \$log) { Get-Content \$log }"
drain_guest_log_incremental "$log_name" log_line_count
log_rc=$?
set -e
if [[ $log_rc -ne 0 ]]; then

Some files were not shown because too many files have changed in this diff Show More