diff --git a/.github/labeler.yml b/.github/labeler.yml index b6422060fea..7dcc038de4c 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -314,3 +314,7 @@ - changed-files: - any-glob-to-any-file: - "extensions/xiaomi/**" +"extensions: fal": + - changed-files: + - any-glob-to-any-file: + - "extensions/fal/**" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bb1232d489d..0c6ade156ca 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -274,8 +274,8 @@ jobs: - name: Run changed extension tests env: - EXTENSION_ID: ${{ matrix.extension }} - run: pnpm test:extension "$EXTENSION_ID" + OPENCLAW_CHANGED_EXTENSION: ${{ matrix.extension }} + run: pnpm test:extension "$OPENCLAW_CHANGED_EXTENSION" # Types, lint, and format check. check: @@ -304,6 +304,194 @@ jobs: - name: Enforce safe external URL opening policy run: pnpm lint:ui:no-raw-window-open + plugin-extension-boundary: + name: "plugin-extension-boundary" + needs: [docs-scope, changed-scope] + if: needs.docs-scope.outputs.docs_only != 'true' && needs.changed-scope.outputs.run_node == 'true' + runs-on: blacksmith-16vcpu-ubuntu-2404 + env: + PLUGIN_EXTENSION_BOUNDARY_ENFORCE_AFTER: "2026-03-24T05:00:00Z" + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + submodules: false + + - name: Setup Node environment + uses: ./.github/actions/setup-node-env + with: + install-bun: "false" + use-sticky-disk: "false" + + - name: Run plugin extension boundary guard with grace period + shell: bash + run: | + set -euo pipefail + + tmp_output="$(mktemp)" + if pnpm run lint:plugins:no-extension-imports >"$tmp_output" 2>&1; then + cat "$tmp_output" + rm -f "$tmp_output" + exit 0 + fi + + status=$? + cat "$tmp_output" + rm -f "$tmp_output" + + now_epoch="$(date -u +%s)" + enforce_epoch="$(date -u -d "$PLUGIN_EXTENSION_BOUNDARY_ENFORCE_AFTER" +%s)" + fix_instructions="If you are an LLM agent fixing this: run 'pnpm run lint:plugins:no-extension-imports', remove src/plugins/** -> extensions/** imports where possible, and if the remaining inventory is intentional for now update test/fixtures/plugin-extension-import-boundary-inventory.json in the same PR." + + if [ "$now_epoch" -lt "$enforce_epoch" ]; then + echo "::warning::Plugin extension import boundary violations are temporarily allowed until ${PLUGIN_EXTENSION_BOUNDARY_ENFORCE_AFTER}. This grace period ends in one week from the rollout date. After that timestamp this job will fail unless the inventory is reduced or the baseline is intentionally updated. ${fix_instructions}" + exit 0 + fi + + echo "::error::Plugin extension import boundary grace period ended at ${PLUGIN_EXTENSION_BOUNDARY_ENFORCE_AFTER}. ${fix_instructions}" + exit "$status" + + web-search-provider-boundary: + name: "web-search-provider-boundary" + needs: [docs-scope, changed-scope] + if: needs.docs-scope.outputs.docs_only != 'true' && needs.changed-scope.outputs.run_node == 'true' + runs-on: blacksmith-16vcpu-ubuntu-2404 + env: + WEB_SEARCH_PROVIDER_BOUNDARY_ENFORCE_AFTER: "2026-03-24T05:00:00Z" + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + submodules: false + + - name: Setup Node environment + uses: ./.github/actions/setup-node-env + with: + install-bun: "false" + use-sticky-disk: "false" + + - name: Run web search provider boundary guard with grace period + shell: bash + run: | + set -euo pipefail + + tmp_output="$(mktemp)" + if pnpm run lint:web-search-provider-boundaries >"$tmp_output" 2>&1; then + cat "$tmp_output" + rm -f "$tmp_output" + exit 0 + fi + + status=$? + cat "$tmp_output" + rm -f "$tmp_output" + + now_epoch="$(date -u +%s)" + enforce_epoch="$(date -u -d "$WEB_SEARCH_PROVIDER_BOUNDARY_ENFORCE_AFTER" +%s)" + fix_instructions="If you are an LLM agent fixing this: run 'pnpm run lint:web-search-provider-boundaries', move provider-specific web-search logic out of core, and if the remaining inventory is intentional for now update test/fixtures/web-search-provider-boundary-inventory.json in the same PR." + + if [ "$now_epoch" -lt "$enforce_epoch" ]; then + echo "::warning::Web search provider boundary violations are temporarily allowed until ${WEB_SEARCH_PROVIDER_BOUNDARY_ENFORCE_AFTER}. This grace period ends in one week from the rollout date. After that timestamp this job will fail unless the inventory is reduced or the baseline is intentionally updated. ${fix_instructions}" + exit 0 + fi + + echo "::error::Web search provider boundary grace period ended at ${WEB_SEARCH_PROVIDER_BOUNDARY_ENFORCE_AFTER}. ${fix_instructions}" + exit "$status" + + extension-src-outside-plugin-sdk-boundary: + name: "extension-src-outside-plugin-sdk-boundary" + needs: [docs-scope, changed-scope] + if: needs.docs-scope.outputs.docs_only != 'true' && needs.changed-scope.outputs.run_node == 'true' + runs-on: blacksmith-16vcpu-ubuntu-2404 + env: + EXTENSION_PLUGIN_SDK_BOUNDARY_ENFORCE_AFTER: "2026-03-24T05:00:00Z" + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + submodules: false + + - name: Setup Node environment + uses: ./.github/actions/setup-node-env + with: + install-bun: "false" + use-sticky-disk: "false" + + - name: Run extension src boundary guard with grace period + shell: bash + run: | + set -euo pipefail + + tmp_output="$(mktemp)" + if pnpm run lint:extensions:no-src-outside-plugin-sdk >"$tmp_output" 2>&1; then + cat "$tmp_output" + rm -f "$tmp_output" + exit 0 + fi + + status=$? + cat "$tmp_output" + rm -f "$tmp_output" + + now_epoch="$(date -u +%s)" + enforce_epoch="$(date -u -d "$EXTENSION_PLUGIN_SDK_BOUNDARY_ENFORCE_AFTER" +%s)" + fix_instructions="If you are an LLM agent fixing this: run 'pnpm run lint:extensions:no-src-outside-plugin-sdk', move extension imports off core src paths and onto src/plugin-sdk/**, and if the remaining inventory is intentional for now update test/fixtures/extension-src-outside-plugin-sdk-inventory.json in the same PR." + + if [ "$now_epoch" -lt "$enforce_epoch" ]; then + echo "::warning::Extension src boundary violations are temporarily allowed until ${EXTENSION_PLUGIN_SDK_BOUNDARY_ENFORCE_AFTER}. This grace period ends in one week from the rollout date. After that timestamp this job will fail unless the inventory is reduced or the baseline is intentionally updated. ${fix_instructions}" + exit 0 + fi + + echo "::error::Extension src boundary grace period ended at ${EXTENSION_PLUGIN_SDK_BOUNDARY_ENFORCE_AFTER}. ${fix_instructions}" + exit "$status" + + extension-plugin-sdk-internal-boundary: + name: "extension-plugin-sdk-internal-boundary" + needs: [docs-scope, changed-scope] + if: needs.docs-scope.outputs.docs_only != 'true' && needs.changed-scope.outputs.run_node == 'true' + runs-on: blacksmith-16vcpu-ubuntu-2404 + env: + EXTENSION_PLUGIN_SDK_INTERNAL_ENFORCE_AFTER: "2026-03-24T05:00:00Z" + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + submodules: false + + - name: Setup Node environment + uses: ./.github/actions/setup-node-env + with: + install-bun: "false" + use-sticky-disk: "false" + + - name: Run extension plugin-sdk-internal guard with grace period + shell: bash + run: | + set -euo pipefail + + tmp_output="$(mktemp)" + if pnpm run lint:extensions:no-plugin-sdk-internal >"$tmp_output" 2>&1; then + cat "$tmp_output" + rm -f "$tmp_output" + exit 0 + fi + + status=$? + cat "$tmp_output" + rm -f "$tmp_output" + + now_epoch="$(date -u +%s)" + enforce_epoch="$(date -u -d "$EXTENSION_PLUGIN_SDK_INTERNAL_ENFORCE_AFTER" +%s)" + fix_instructions="If you are an LLM agent fixing this: run 'pnpm run lint:extensions:no-plugin-sdk-internal', remove extension imports of src/plugin-sdk-internal/** in favor of src/plugin-sdk/**, and if the remaining inventory is intentional for now update test/fixtures/extension-plugin-sdk-internal-inventory.json in the same PR." + + if [ "$now_epoch" -lt "$enforce_epoch" ]; then + echo "::warning::Extension plugin-sdk-internal boundary violations are temporarily allowed until ${EXTENSION_PLUGIN_SDK_INTERNAL_ENFORCE_AFTER}. This grace period ends in one week from the rollout date. After that timestamp this job will fail unless the inventory is reduced or the baseline is intentionally updated. ${fix_instructions}" + exit 0 + fi + + echo "::error::Extension plugin-sdk-internal boundary grace period ended at ${EXTENSION_PLUGIN_SDK_INTERNAL_ENFORCE_AFTER}. ${fix_instructions}" + exit "$status" + build-smoke: name: "build-smoke" needs: [docs-scope, changed-scope] @@ -330,6 +518,9 @@ jobs: - name: Smoke test CLI launcher status json run: node openclaw.mjs status --json --timeout 1 + - name: Smoke test built bundled plugin singleton + run: pnpm test:build:singleton + - name: Check CLI startup memory run: pnpm test:startup:memory diff --git a/AGENTS.md b/AGENTS.md index df72efbe720..57e7bd22100 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -114,6 +114,7 @@ - Never add `@ts-nocheck` and do not disable `no-explicit-any`; fix root causes and update Oxlint/Oxfmt config only when required. - Dynamic import guardrail: do not mix `await import("x")` and static `import ... from "x"` for the same module in production code paths. If you need lazy loading, create a dedicated `*.runtime.ts` boundary (that re-exports from `x`) and dynamically import that boundary from lazy callers only. - Dynamic import verification: after refactors that touch lazy-loading/module boundaries, run `pnpm build` and check for `[INEFFECTIVE_DYNAMIC_IMPORT]` warnings before submitting. +- Extension SDK self-import guardrail: inside an extension package, do not import that same extension via `openclaw/plugin-sdk/` from production files. Route internal imports through a local barrel such as `./api.ts` or `./runtime-api.ts`, and keep the `plugin-sdk/` path as the external contract only. - Never share class behavior via prototype mutation (`applyPrototypeMixins`, `Object.defineProperty` on `.prototype`, or exporting `Class.prototype` for merges). Use explicit inheritance/composition (`A extends B extends C`) or helper composition so TypeScript can typecheck. - If this pattern is needed, stop and get explicit approval before shipping; default behavior is to split/refactor into an explicit class hierarchy and keep members strongly typed. - In tests, prefer per-instance stubs over prototype mutation (`SomeClass.prototype.method = ...`) unless a test explicitly documents why prototype-level patching is required. diff --git a/CHANGELOG.md b/CHANGELOG.md index 1dd11adfede..e99959251ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,18 +32,21 @@ Docs: https://docs.openclaw.ai - secrets: harden read-only SecretRef command paths and diagnostics. (#47794) Thanks @joshavant. - Browser/existing-session: support `browser.profiles..userDataDir` so Chrome DevTools MCP can attach to Brave, Edge, and other Chromium-based browsers through their own user data directories. (#48170) Thanks @velvet-shark. - Skills/prompt budget: preserve all registered skills via a compact catalog fallback before dropping entries when the full prompt format exceeds `maxSkillsPromptChars`. (#47553) Thanks @snese. +- Models/OpenAI: add native forward-compat support for `gpt-5.4-mini` and `gpt-5.4-nano` in the OpenAI provider catalog, runtime resolution, and reasoning capability gates. Thanks @vincentkoc. - Plugins/bundles: make enabled bundle MCP servers expose runnable tools in embedded Pi, and default relative bundle MCP launches to the bundle root so marketplace bundles like Context7 work through Pi instead of stopping at config import. - Scope message SecretRef resolution and harden doctor/status paths. (#48728) Thanks @joshavant. - Plugins/testing: add a public `openclaw/plugin-sdk/testing` seam for plugin-author test helpers, and move bundled-extension-only test bridges out of `extensions/` into private repo test helpers. - -### Breaking - -- Browser/Chrome MCP: remove the legacy Chrome extension relay path, bundled extension assets, `driver: "extension"`, and `browser.relayBindHost`. Run `openclaw doctor --fix` to migrate host-local browser config to `existing-session` / `user`; Docker, headless, sandbox, and remote browser flows still use raw CDP. (#47893) Thanks @vincentkoc. -- Plugins/runtime: remove the public `openclaw/extension-api` surface with no compatibility shim. Bundled plugins must use injected runtime for host-side operations (for example `api.runtime.agent.runEmbeddedPiAgent`) and any remaining direct imports must come from narrow `openclaw/plugin-sdk/*` subpaths instead of the monolithic SDK root. -- Tools/image generation: standardize the stock image create/edit path on the core `image_generate` tool. The old `nano-banana-pro` docs/examples are gone; if you previously copied that sample-skill config, switch to `agents.defaults.imageGenerationModel` for built-in image generation or install a separate third-party skill explicitly. +- Plugins/Chutes: add a bundled Chutes provider with plugin-owned OAuth/API-key auth, dynamic model discovery, and default-on extension wiring. (#41416) Thanks @Veightor. +- Plugins/binding: add `onConversationBindingResolved(...)` so plugins can react immediately after bind approvals or denies without blocking channel interaction acknowledgements. (#48678) Thanks @huntharo. +- CLI/config: expand `config set` with SecretRef and provider builder modes, JSON/batch assignment support, and `--dry-run` validation with structured JSON output. (#49296) Thanks @joshavant. +- Control UI/appearance: unify theme border radii across Claw, Knot, and Dash, and add a Roundness slider to the Appearance settings so users can adjust corner radius from sharp to fully rounded. Thanks @BunsDev. +- Control UI/chat: add an expand-to-canvas button on assistant chat bubbles and in-app session navigation from Sessions and Cron views. Thanks @BunsDev. +- Plugins/context engines: expose `delegateCompactionToRuntime(...)` on the public plugin SDK, refactor the legacy engine to use the shared helper, and clarify `ownsCompaction` delegation semantics for non-owning engines. (#49061) Thanks @jalehman. ### Fixes +- Plugins/bundler TDZ: fix `RESERVED_COMMANDS` temporal dead zone error that prevented device-pair, phone-control, and talk-voice plugins from registering when the bundler placed the commands module after call sites in the same output chunk. Thanks @BunsDev. +- Plugins/imports: fix stale googlechat runtime-api import paths and signal SDK circular re-exports broken by recent plugin-sdk refactors. Thanks @BunsDev. - Google auth/Node 25: patch `gaxios` to use native fetch without injecting `globalThis.window`, while translating proxy and mTLS transport settings so Google Vertex and Google Chat auth keep working on Node 25. (#47914) Thanks @pdd-cli. - Gateway/startup: load bundled channel plugins from compiled `dist/extensions` entries in built installs, so gateway boot no longer recompiles bundled extension TypeScript on every startup and WhatsApp-class cold starts drop back to seconds instead of tens of seconds or worse. (#47560) Thanks @ngutman. - Plugins/context engines: enforce owner-aware context-engine registration on both loader and public SDK paths so plugins cannot spoof privileged ownership, claim the core `legacy` engine id, or overwrite an existing engine id through direct SDK imports. (#47595) Thanks @vincentkoc. @@ -101,6 +104,9 @@ Docs: https://docs.openclaw.ai - Docs/Mintlify: fix MDX marker syntax on Perplexity, Model Providers, Moonshot, and exec approvals pages so local docs preview no longer breaks rendering or leaves stale pages unpublished. (#46695) Thanks @velvet-shark. - Gateway/config validation: stop treating the implicit default memory slot as a required explicit plugin config, so startup no longer fails with `plugins.slots.memory: plugin not found: memory-core` when `memory-core` was only inferred. (#47494) Thanks @ngutman. - Tlon: honor explicit empty allowlists and defer cite expansion. (#46788) Thanks @zpbrent and @vincentkoc. +- Tlon/DM auth: defer cited-message expansion until after DM authorization and owner command handling, so unauthorized DMs and owner approval/admin commands no longer trigger cross-channel cite fetches before the deny or command path. +- Docs/security audit: spell out that `gateway.controlUi.allowedOrigins: ["*"]` is an explicit allow-all browser-origin policy and should be avoided outside tightly controlled local testing. +- Gateway/auth: clear self-declared scopes for device-less trusted-proxy Control UI sessions so proxy-authenticated connects cannot claim admin or secrets scopes without a bound device identity. - Nodes/pending actions: re-check queued foreground actions against the current node command policy before returning them to the node. (#46815) Thanks @zpbrent and @vincentkoc. - Node/startup: remove leftover debug `console.log("node host PATH: ...")` that printed the resolved PATH on every `openclaw node run` invocation. (#46515) Fixes #46411. Thanks @ademczuk. - CLI/completion: reduce recursive completion-script string churn and fix nested PowerShell command-path matching so generated nested completions resolve on PowerShell too. (#45537) Thanks @yiShanXin and @vincentkoc. @@ -111,13 +117,17 @@ Docs: https://docs.openclaw.ai - Control UI/model switching: preserve the selected provider prefix when switching models from the chat dropdown, so multi-provider setups no longer send `anthropic/gpt-5.2`-style mismatches when the user picked `openai/gpt-5.2`. (#47581) Thanks @chrishham. - Control UI/storage: scope persisted settings keys by gateway base path, with migration from the legacy shared key, so multiple gateways under one domain stop overwriting each other's dashboard preferences. (#47932) Thanks @bobBot-claw. - Agents/usage tracking: stop forcing `supportsUsageInStreaming: false` on non-native OpenAI-completions providers so compatible backends report token usage and cost again instead of showing all zeros. (#46500) Fixes #46142. Thanks @ademczuk. +- ACP/acpx: keep plugin-local backend installs under `extensions/acpx` in live repo checkouts so rebuilds no longer delete the runtime binary, and avoid package-lock churn during runtime repair. - Plugins/subagents: preserve gateway-owned plugin subagent access across runtime, tool, and embedded-runner load paths so gateway plugin tools and context engines can still spawn and manage subagents after the loader cache split. (#46648) Thanks @jalehman. - Control UI/overview: keep the language dropdown aligned with the persisted locale during dashboard startup so refreshing the page does not fall back to English before locale hydration completes. (#48019) Thanks @git-jxj. - Agents/compaction: rerun transcript repair after `session.compact()` so orphaned `tool_result` blocks cannot survive compaction and break later Anthropic requests. (#16095) thanks @claw-sylphx. - Agents/compaction: trigger overflow recovery from the tool-result guard once post-compaction context still exceeds the safe threshold, so long tool loops compact before the next model call hard-fails. (#29371) thanks @keshav55. - macOS/exec approvals: harden exec-host request HMAC verification to use a timing-safe compare and keep malformed or truncated signatures fail-closed in focused IPC auth coverage. - Gateway/exec approvals: surface requested env override keys in gateway-host approval prompts so operators can review surviving env context without inheriting noisy base host env. +- Telegram/network: preserve sticky IPv4 fallback state across polling restarts so hosts with unstable IPv6 to `api.telegram.org` stop re-triggering repeated Telegram timeouts after each restart. (#48282) Thanks @yassinebkr. - Plugins/subagents: forward per-run provider and model overrides through gateway plugin subagent dispatch so plugin-launched agent delegations honor explicit model selection again. (#48277) Thanks @jalehman. +- Agents/compaction: write minimal boundary summaries for empty preparations while keeping split-turn prefixes on the normal path, so no-summarizable-message sessions stop retriggering the safeguard loop. (#42215) thanks @lml2468. +- Models/chat commands: keep `/model ...@YYYYMMDD` version suffixes intact by default, but still honor matching stored numeric auth-profile overrides for the same provider. (#48896) Thanks @Alix-007. ### Fixes @@ -125,6 +135,29 @@ Docs: https://docs.openclaw.ai - Telegram/DM topic session keys: route named-account DM topics through the same per-account base session key across inbound messages, native commands, and session-state lookups so `/status` and thread recovery stop creating phantom `agent:main:main:thread:...` sessions. (#48204) Thanks @vincentkoc. - macOS/node service startup: use `openclaw node start/stop --json` from the Mac app instead of the removed `openclaw service node ...` command shape, so current CLI installs expose the full node exec surface again. (#46843) Fixes #43171. Thanks @Br1an67. - macOS/launch at login: stop emitting `KeepAlive` for the desktop app launch agent so OpenClaw no longer relaunches immediately after a manual quit while launch at login remains enabled. (#40213) Thanks @stablegenius49. +- ACP/gateway startup: use direct Telegram and Discord startup/status helpers instead of routing probes through the plugin runtime, and prepend the selected daemon Node bin dir to service PATH so plugin-local installs can still find `npm` and `pnpm`. +- ACP/configured bindings: reinitialize configured ACP sessions that are stuck in `error` state instead of reusing the failed runtime. +- Mattermost/DM send: retry transient direct-channel creation failures for DM deliveries, with configurable backoff and per-request timeout. (#42398) Thanks @JonathanJing. +- Telegram/network: unify API and media fetches under the same sticky IPv4 and pinned-IP fallback chain, and re-validate pinned override addresses against SSRF policy. (#49148) Thanks @obviyus. +- Agents/prompt composition: append bootstrap truncation warnings to the current-turn prompt and add regression coverage for stable system-prompt cache invariants. (#49237) Thanks @scoootscooob. +- Gateway/auth: add regression coverage that keeps device-less trusted-proxy Control UI sessions off privileged pairing approval RPCs. Thanks @vincentkoc. +- Plugins/runtime-api: pin extension runtime-api export seams with explicit guardrail coverage so future surface creep becomes a deliberate diff. Thanks @vincentkoc. +- Telegram/security: add regression coverage proving pinned fallback host overrides stay bound to Telegram and delegate non-matching hostnames back to the original lookup path. Thanks @vincentkoc. +- Secrets/exec refs: require explicit `--allow-exec` for `secrets apply` write plans that contain exec SecretRefs/providers, and align audit/configure/apply dry-run behavior to skip exec checks unless opted in to prevent unexpected command side effects. (#49417) Thanks @restriction and @joshavant. +- Tools/image generation: add bundled fal image generation support so `image_generate` can target `fal/*` models with `FAL_KEY`, including single-image edit flows via FLUX image-to-image. Thanks @vincentkoc. +- xAI/web search: add missing Grok credential metadata so the bundled provider registration type-checks again. (#49472) thanks @scoootscooob. +- Signal/runtime API: re-export `SignalAccountConfig` so Signal account resolution type-checks again. (#49470) Thanks @scoootscooob. +- Google Chat/runtime API: thin the private runtime barrel onto the curated public SDK surface while keeping public Google Chat exports intact. (#49504) Thanks @scoootscooob. + +### Breaking + +- Skills/image generation: remove the bundled `nano-banana-pro` skill wrapper. Use `agents.defaults.imageGenerationModel.primary: "google/gemini-3-pro-image-preview"` for the native Nano Banana-style path instead. + +- Browser/Chrome MCP: remove the legacy Chrome extension relay path, bundled extension assets, `driver: "extension"`, and `browser.relayBindHost`. Run `openclaw doctor --fix` to migrate host-local browser config to `existing-session` / `user`; Docker, headless, sandbox, and remote browser flows still use raw CDP. (#47893) Thanks @vincentkoc. +- Plugins/runtime: remove the public `openclaw/extension-api` surface with no compatibility shim. Bundled plugins must use injected runtime for host-side operations (for example `api.runtime.agent.runEmbeddedPiAgent`) and any remaining direct imports must come from narrow `openclaw/plugin-sdk/*` subpaths instead of the monolithic SDK root. +- Tools/image generation: standardize the stock image create/edit path on the core `image_generate` tool. The old `nano-banana-pro` docs/examples are gone; if you previously copied that sample-skill config, switch to `agents.defaults.imageGenerationModel` for built-in image generation or install a separate third-party skill explicitly. +- Skills/image generation: remove the bundled `nano-banana-pro` skill wrapper. Use `agents.defaults.imageGenerationModel.primary: "google/gemini-3-pro-image-preview"` for the native Nano Banana-style path instead. +- Plugins/message discovery: require `ChannelMessageActionAdapter.describeMessageTool(...)` for shared `message` tool discovery. The legacy `listActions`, `getCapabilities`, and `getToolSchema` adapter methods are removed. Plugin authors should migrate message discovery to `describeMessageTool(...)` and keep channel-specific action runtime code inside the owning plugin package. Thanks @gumadeiras. ## 2026.3.13 @@ -140,10 +173,6 @@ Docs: https://docs.openclaw.ai - Cron/sessions: add `sessionTarget: "current"` and `session:` support so cron jobs can bind to the creating session or a persistent named session instead of only `main` or `isolated`. Thanks @kkhomej33-netizen and @ImLukeF. - Telegram/message send: add `--force-document` so Telegram image and GIF sends can upload as documents without compression. (#45111) Thanks @thepagent. -### Breaking - -- **BREAKING:** Agents now load at most one root memory bootstrap file. `MEMORY.md` wins; `memory.md` is only used when `MEMORY.md` is absent. If you intentionally kept both files and depended on both being injected, merge them before upgrade. This also fixes duplicate memory injection on case-insensitive Docker mounts. (#26054) Thanks @Lanfei. - ### Fixes - Dashboard/chat UI: stop reloading full chat history on every live tool result in dashboard v2 so tool-heavy runs no longer trigger UI freeze/re-render storms while the final event still refreshes persisted history. (#45541) Thanks @BunsDev. @@ -203,6 +232,12 @@ Docs: https://docs.openclaw.ai - Auth/login lockout recovery: clear stale `auth_permanent` and `billing` disabled state for all profiles matching the target provider when `openclaw models auth login` is invoked, so users locked out by expired or revoked OAuth tokens can recover by re-authenticating instead of waiting for the cooldown timer to expire. (#43057) - Auto-reply/context-engine compaction: persist the exact embedded-run metadata compaction count for main and followup runner session accounting, so metadata-only auto-compactions no longer undercount multi-compaction runs. (#42629) thanks @uf-hy. - Auth/Codex CLI reuse: sync reused Codex CLI credentials into the supported `openai-codex:default` OAuth profile instead of reviving the deprecated `openai-codex:codex-cli` slot, so doctor cleanup no longer loops. (#45353) thanks @Gugu-sugar. +- Deps/audit: bump the pinned `fast-xml-parser` override to the first patched release so `pnpm audit --prod --audit-level=high` no longer fails on the AWS Bedrock XML builder path. Thanks @vincentkoc. +- Hooks/after_compaction: forward `sessionFile` for direct/manual compaction events and add `sessionFile` plus `sessionKey` to wired auto-compaction hook context so plugins receive the session metadata already declared in the hook types. (#40781) Thanks @jarimustonen. + +### Breaking + +- **BREAKING:** Agents now load at most one root memory bootstrap file. `MEMORY.md` wins; `memory.md` is only used when `MEMORY.md` is absent. If you intentionally kept both files and depended on both being injected, merge them before upgrade. This also fixes duplicate memory injection on case-insensitive Docker mounts. (#26054) Thanks @Lanfei. ## 2026.3.12 @@ -300,13 +335,10 @@ Docs: https://docs.openclaw.ai - Agents/Ollama overflow: rewrite Ollama `prompt too long` API payloads through the normal context-overflow sanitizer so embedded sessions keep the friendly overflow copy and auto-compaction trigger. (#34019) thanks @lishuaigit. - Control UI/auth: restore one-time legacy `?token=` imports for shared Control UI links while keeping `#token=` preferred, and carry pending query tokens through gateway URL confirmation so compatibility links still authenticate after confirmation. (#43979) Thanks @stim64045-spec. +- Plugins/context engines: retry legacy lifecycle calls once without `sessionKey` when older plugins reject that field, memoize legacy mode after the first strict-schema fallback, and preserve non-compat runtime errors without retry. (#44779) thanks @hhhhao28. ## 2026.3.11 -### Security - -- Gateway/WebSocket: enforce browser origin validation for all browser-originated connections regardless of whether proxy headers are present, closing a cross-site WebSocket hijacking path in `trusted-proxy` mode that could grant untrusted origins `operator.admin` access. (GHSA-5wcw-8jjv-m286) - ### Changes - OpenRouter/models: add temporary Hunter Alpha and Healer Alpha entries to the built-in catalog so OpenRouter users can try the new free stealth models during their roughly one-week availability window. (#43642) Thanks @ping-Toven. @@ -328,10 +360,6 @@ Docs: https://docs.openclaw.ai - Mattermost/reply threading: add `channels.mattermost.replyToMode` for channel and group messages so top-level posts can start thread-scoped sessions without the manual reply-then-thread workaround. (#29587) Thanks @teconomix. - iOS/push relay: add relay-backed official-build push delivery with App Attest + receipt verification, gateway-bound send delegation, and config-based relay URL setup on the gateway. (#43369) Thanks @ngutman. -### Breaking - -- Cron/doctor: tighten isolated cron delivery so cron jobs can no longer notify through ad hoc agent sends or fallback main-session summaries, and add `openclaw doctor --fix` migration for legacy cron storage and legacy notify/webhook delivery metadata. (#40998) Thanks @mbelinky. - ### Fixes - Windows/install: stop auto-installing `node-llama-cpp` during normal npm CLI installs so `openclaw@latest` no longer fails on Windows while building optional local-embedding dependencies. @@ -444,6 +472,14 @@ Docs: https://docs.openclaw.ai - Feishu/streaming recovery: clear stale `streamingStartPromise` when card creation fails (HTTP 400) so subsequent messages can retry streaming instead of silently dropping all future replies. Fixes #43322. - Exec/env sandbox: block JVM agent injection (`JAVA_TOOL_OPTIONS`, `_JAVA_OPTIONS`, `JDK_JAVA_OPTIONS`), Python breakpoint hijack (`PYTHONBREAKPOINT`), and .NET startup hooks (`DOTNET_STARTUP_HOOKS`) from the host exec environment. (#49025) +### Security + +- Gateway/WebSocket: enforce browser origin validation for all browser-originated connections regardless of whether proxy headers are present, closing a cross-site WebSocket hijacking path in `trusted-proxy` mode that could grant untrusted origins `operator.admin` access. (GHSA-5wcw-8jjv-m286) + +### Breaking + +- Cron/doctor: tighten isolated cron delivery so cron jobs can no longer notify through ad hoc agent sends or fallback main-session summaries, and add `openclaw doctor --fix` migration for legacy cron storage and legacy notify/webhook delivery metadata. (#40998) Thanks @mbelinky. + ## 2026.3.8 ### Changes @@ -556,10 +592,6 @@ Docs: https://docs.openclaw.ai - Google/Gemini 3.1 Flash-Lite: add first-class `google/gemini-3.1-flash-lite-preview` support across model-id normalization, default aliases, media-understanding image lookups, Google Gemini CLI forward-compat fallback, and docs. - Agents/compaction model override: allow `agents.defaults.compaction.model` to route compaction summarization through a different model than the main session, and document the override across config help/reference surfaces. (#38753) thanks @starbuck100. -### Breaking - -- **BREAKING:** Gateway auth now requires explicit `gateway.auth.mode` when both `gateway.auth.token` and `gateway.auth.password` are configured (including SecretRefs). Set `gateway.auth.mode` to `token` or `password` before upgrade to avoid startup/pairing/TUI failures. (#35094) Thanks @joshavant. - ### Fixes - Models/MiniMax: stop advertising removed `MiniMax-M2.5-Lightning` in built-in provider catalogs, onboarding metadata, and docs; keep the supported fast-tier model as `MiniMax-M2.5-highspeed`. @@ -630,6 +662,7 @@ Docs: https://docs.openclaw.ai - Control UI/markdown fallback regression coverage: add explicit regression assertions for parser-error fallback behavior so malformed markdown no longer risks reintroducing hard-crash rendering paths in future markdown/parser upgrades. (#36445) Thanks @BinHPdev. - Web UI/config form: treat `additionalProperties: true` object schemas as editable map entries instead of unsupported fields so Accounts-style maps stay editable in form mode. (#35380, supersedes #32072) Thanks @stakeswky and @liuxiaopai-ai. - Feishu/streaming card delivery synthesis: unify snapshot and delta streaming merge semantics, apply overlap-aware final merge, suppress duplicate final text delivery (including text+media final packets), prefer topic-thread `message.reply` routing when a reply target exists, and tune card print cadence to avoid duplicate incremental rendering. (from #33245, #32896, #33840) Thanks @rexl2018, @kcinzgg, and @aerelune. +- macOS/tray menu: keep injected sessions and device rows below the controls section so toggles and action buttons stay visible even when many sessions are active. (#38079) Thanks @bernesto. - Feishu/group mention detection: carry startup-probed bot display names through monitor dispatch so `requireMention` checks compare against current bot identity instead of stale config names, fixing missed `@bot` handling in groups while preserving multi-bot false-positive guards. (#36317, #34271) Thanks @liuxiaopai-ai. - Security/dependency audit: patch transitive Hono vulnerabilities by pinning `hono` to `4.12.5` and `@hono/node-server` to `1.19.10` in production resolution paths. Thanks @shakkernerd. - Security/dependency audit: bump `tar` to `7.5.10` (from `7.5.9`) to address the high-severity hardlink path traversal advisory (`GHSA-qffp-2rhf-9h96`). Thanks @shakkernerd. @@ -884,6 +917,10 @@ Docs: https://docs.openclaw.ai - Mattermost/DM media uploads: resolve bare 26-character Mattermost IDs user-first for direct messages so media sends no longer fail with `403 Forbidden` when targets are configured as unprefixed user IDs. (#29925) Thanks @teconomix. - Voice-call/OpenAI TTS config parity: add missing `speed`, `instructions`, and `baseUrl` fields to the OpenAI TTS config schema and gate `instructions` to supported models so voice-call overrides validate and route cleanly through core TTS. (#39226) Thanks @ademczuk. +### Breaking + +- **BREAKING:** Gateway auth now requires explicit `gateway.auth.mode` when both `gateway.auth.token` and `gateway.auth.password` are configured (including SecretRefs). Set `gateway.auth.mode` to `token` or `password` before upgrade to avoid startup/pairing/TUI failures. (#35094) Thanks @joshavant. + ## 2026.3.2 ### Changes @@ -912,13 +949,6 @@ Docs: https://docs.openclaw.ai - Gateway/input_image MIME validation: sniff uploaded image bytes before MIME allowlist enforcement again so declared image types cannot mask concrete non-image payloads, while keeping HEIC/HEIF normalization behavior scoped to actual HEIC inputs. Thanks @vincentkoc. - Zalo Personal plugin (`@openclaw/zalouser`): keep canonical DM routing while preserving legacy DM session continuity on upgrade, and preserve provider-native `g-`/`u-` target ids in outbound send and directory flows so #33992 lands without breaking existing sessions or stored targets. (#33992) Thanks @darkamenosa. -### Breaking - -- **BREAKING:** Onboarding now defaults `tools.profile` to `messaging` for new local installs (interactive + non-interactive). New setups no longer start with broad coding/system tools unless explicitly configured. -- **BREAKING:** ACP dispatch now defaults to enabled unless explicitly disabled (`acp.dispatch.enabled=false`). If you need to pause ACP turn routing while keeping `/acp` controls, set `acp.dispatch.enabled=false`. Docs: https://docs.openclaw.ai/tools/acp-agents -- **BREAKING:** Plugin SDK removed `api.registerHttpHandler(...)`. Plugins must register explicit HTTP routes via `api.registerHttpRoute({ path, auth, match, handler })`, and dynamic webhook lifecycles should use `registerPluginHttpRoute(...)`. -- **BREAKING:** Zalo Personal plugin (`@openclaw/zalouser`) no longer depends on external `zca`-compatible CLI binaries (`openzca`, `zca-cli`) for runtime send/listen/login; operators should use `openclaw channels login --channel zalouser` after upgrade to refresh sessions in the new JS-native path. - ### Fixes - Feishu/Outbound render mode: respect Feishu account `renderMode` in outbound sends so card mode (and auto-detected markdown tables/code blocks) uses markdown card delivery instead of always sending plain text. (#31562) Thanks @arkyu2077. @@ -1105,6 +1135,13 @@ Docs: https://docs.openclaw.ai - Tests/Subagent announce: set `OPENCLAW_TEST_FAST=1` before importing `subagent-announce` format suites so module-level fast-mode constants are captured deterministically on Windows CI, preventing timeout flakes in nested completion announce coverage. (#31370) Thanks @zwffff. - Control UI/markdown recursion fallback: catch markdown parser failures and safely render escaped plain-text fallback instead of crashing the Control UI on pathological markdown history payloads. (#36445, fixes #36213) Thanks @BinHPdev. +### Breaking + +- **BREAKING:** Onboarding now defaults `tools.profile` to `messaging` for new local installs (interactive + non-interactive). New setups no longer start with broad coding/system tools unless explicitly configured. +- **BREAKING:** ACP dispatch now defaults to enabled unless explicitly disabled (`acp.dispatch.enabled=false`). If you need to pause ACP turn routing while keeping `/acp` controls, set `acp.dispatch.enabled=false`. Docs: https://docs.openclaw.ai/tools/acp-agents +- **BREAKING:** Plugin SDK removed `api.registerHttpHandler(...)`. Plugins must register explicit HTTP routes via `api.registerHttpRoute({ path, auth, match, handler })`, and dynamic webhook lifecycles should use `registerPluginHttpRoute(...)`. +- **BREAKING:** Zalo Personal plugin (`@openclaw/zalouser`) no longer depends on external `zca`-compatible CLI binaries (`openzca`, `zca-cli`) for runtime send/listen/login; operators should use `openclaw channels login --channel zalouser` after upgrade to refresh sessions in the new JS-native path. + ## 2026.3.1 ### Changes @@ -1132,11 +1169,6 @@ Docs: https://docs.openclaw.ai - OpenAI/WebSocket warm-up: add optional OpenAI Responses WebSocket warm-up (`response.create` with `generate:false`), enable it by default for `openai/*`, and expose `params.openaiWsWarmup` for per-model enable/disable control. - Agents/Subagents runtime events: replace ad-hoc subagent completion system-message handoff with typed internal completion events (`task_completion`) that are rendered consistently across direct and queued announce paths, with gateway/CLI plumbing for structured `internalEvents`. -### Breaking - -- **BREAKING:** Node exec approval payloads now require `systemRunPlan`. `host=node` approval requests without that plan are rejected. -- **BREAKING:** Node `system.run` execution now pins path-token commands to the canonical executable path (`realpath`) in both allowlist and approval execution flows. Integrations/tests that asserted token-form argv (for example `tr`) must now accept canonical paths (for example `/usr/bin/tr`). - ### Fixes - Feishu/Streaming card text fidelity: merge throttled/fragmented partial updates without dropping content and avoid newline injection when stitching chunk-style deltas so card-stream output matches final reply text. (#29616) Thanks @HaoHuaqing. @@ -1231,6 +1263,11 @@ Docs: https://docs.openclaw.ai - Signal/Sync message null-handling: treat `syncMessage` presence (including `null`) as sync envelope traffic so replayed sentTranscript payloads cannot bypass loop guards after daemon restart. Landed from contributor PR #31138 by @Sid-Qin. Thanks @Sid-Qin. - Infra/fs-safe: sanitize directory-read failures so raw `EISDIR` text never leaks to messaging surfaces, with regression tests for both root-scoped and direct safe reads. Landed from contributor PR #31205 by @polooooo. Thanks @polooooo. +### Breaking + +- **BREAKING:** Node exec approval payloads now require `systemRunPlan`. `host=node` approval requests without that plan are rejected. +- **BREAKING:** Node `system.run` execution now pins path-token commands to the canonical executable path (`realpath`) in both allowlist and approval execution flows. Integrations/tests that asserted token-form argv (for example `tr`) must now accept canonical paths (for example `/usr/bin/tr`). + ## 2026.2.27 ### Changes @@ -1506,10 +1543,6 @@ Docs: https://docs.openclaw.ai - Agents/Config: remind agents to call `config.schema` before config edits or config-field questions to avoid guessing. Thanks @thewilloftheshadow. - Dependencies: update workspace dependency pins and lockfile (Bedrock SDK `3.998.0`, `@mariozechner/pi-*` `0.55.1`, TypeScript native preview `7.0.0-dev.20260225.1`) while keeping `@buape/carbon` pinned. -### Breaking - -- **BREAKING:** Heartbeat direct/DM delivery default is now `allow` again. To keep DM-blocked behavior from `2026.2.24`, set `agents.defaults.heartbeat.directPolicy: "block"` (or per-agent override). - ### Fixes - Slack/Identity: thread agent outbound identity (`chat:write.customize` overrides) through the channel reply delivery path so per-agent username, icon URL, and icon emoji are applied to all Slack replies including media messages. (#27134) Thanks @hou-rong. @@ -1573,6 +1606,10 @@ Docs: https://docs.openclaw.ai - Tests/Low-memory stability: disable Vitest `vmForks` by default on low-memory local hosts (`<64 GiB`), keep low-profile extension lane parallelism at 4 workers, and align cron isolated-agent tests with `setSessionRuntimeModel` usage to avoid deterministic suite failures. (#26324) Thanks @ngutman. - Feishu/WebSocket proxy: pass a proxy agent to Feishu WS clients from standard proxy environment variables and include plugin-local runtime dependency wiring so websocket mode works in proxy-constrained installs. (#26397) Thanks @colin719. +### Breaking + +- **BREAKING:** Heartbeat direct/DM delivery default is now `allow` again. To keep DM-blocked behavior from `2026.2.24`, set `agents.defaults.heartbeat.directPolicy: "block"` (or per-agent override). + ## 2026.2.24 ### Changes @@ -1583,11 +1620,6 @@ Docs: https://docs.openclaw.ai - Security/Audit: add `security.trust_model.multi_user_heuristic` to flag likely shared-user ingress and clarify the personal-assistant trust model, with hardening guidance for intentional multi-user setups (`sandbox.mode="all"`, workspace-scoped FS, reduced tool surface, no personal/private identities on shared runtimes). - Dependencies: refresh key runtime and tooling packages across the workspace (Bedrock SDK, pi runtime stack, OpenAI, Google auth, and oxlint/oxfmt), while intentionally keeping `@buape/carbon` pinned. -### Breaking - -- **BREAKING:** Heartbeat delivery now blocks direct/DM targets when destination parsing identifies a direct chat (for example `user:`, Telegram user chat IDs, or WhatsApp direct numbers/JIDs). Heartbeat runs still execute, but direct-message delivery is skipped and only non-DM destinations (for example channel/group targets) can receive outbound heartbeat messages. -- **BREAKING:** Security/Sandbox: block Docker `network: "container:"` namespace-join mode by default for sandbox and sandbox-browser containers. To keep that behavior intentionally, set `agents.defaults.sandbox.docker.dangerouslyAllowContainerNamespaceJoin: true` (break-glass). Thanks @tdjackey for reporting. - ### Fixes - Routing/Session isolation: harden followup routing so explicit cross-channel origin replies never fall back to the active dispatcher on route failure, preserve queued overflow summary routing metadata (`channel`/`to`/`thread`) across followup drain, and prefer originating channel context over internal provider tags for embedded followup runs. This prevents webchat/control-ui context from hijacking Discord-targeted replies in shared sessions. (#25864) Thanks @Gamedesigner. @@ -1667,6 +1699,11 @@ Docs: https://docs.openclaw.ai - Agents/Compaction: harden summarization prompts to preserve opaque identifiers verbatim (UUIDs, IDs, tokens, host/IP/port, URLs), reducing post-compaction identifier drift and hallucinated identifier reconstruction. - Security/Sandbox: canonicalize bind-mount source paths via existing-ancestor realpath so symlink-parent + non-existent-leaf paths cannot bypass allowed-source-roots or blocked-path checks. Thanks @tdjackey. +### Breaking + +- **BREAKING:** Heartbeat delivery now blocks direct/DM targets when destination parsing identifies a direct chat (for example `user:`, Telegram user chat IDs, or WhatsApp direct numbers/JIDs). Heartbeat runs still execute, but direct-message delivery is skipped and only non-DM destinations (for example channel/group targets) can receive outbound heartbeat messages. +- **BREAKING:** Security/Sandbox: block Docker `network: "container:"` namespace-join mode by default for sandbox and sandbox-browser containers. To keep that behavior intentionally, set `agents.defaults.sandbox.docker.dangerouslyAllowContainerNamespaceJoin: true` (break-glass). Thanks @tdjackey for reporting. + ## 2026.2.23 ### Changes @@ -1681,10 +1718,6 @@ Docs: https://docs.openclaw.ai - Agents/Config: support per-agent `params` overrides merged on top of model defaults (including `cacheRetention`) so mixed-traffic agents can tune cache behavior independently. (#17470, #17112) Thanks @rrenamed. - Agents/Bootstrap: cache bootstrap file snapshots per session key and clear them on session reset/delete, reducing prompt-cache invalidations from in-session `AGENTS.md`/`MEMORY.md` writes. (#22220) Thanks @anisoptera. -### Breaking - -- **BREAKING:** browser SSRF policy now defaults to trusted-network mode (`browser.ssrfPolicy.dangerouslyAllowPrivateNetwork=true` when unset), and canonical config uses `browser.ssrfPolicy.dangerouslyAllowPrivateNetwork` instead of `browser.ssrfPolicy.allowPrivateNetwork`. `openclaw doctor --fix` migrates the legacy key automatically. - ### Fixes - Security/Config: redact sensitive-looking dynamic catchall keys in `config.get` snapshots (for example `env.*` and `skills.entries.*.env.*`) and preserve round-trip restore behavior for those redacted sentinels. Thanks @merc1305. @@ -1730,6 +1763,10 @@ Docs: https://docs.openclaw.ai - Skills/Python: harden skill script packaging and validation edge cases (self-including `.skill` outputs, CRLF frontmatter parsing, strict `--days` validation, and safer image file loading), with expanded Python regression coverage. Thanks @vincentkoc. - Skills/Python: add CI + pre-commit linting (`ruff`) and pytest discovery coverage for Python scripts/tests under `skills/`, including package test execution from repo root. Thanks @vincentkoc. +### Breaking + +- **BREAKING:** browser SSRF policy now defaults to trusted-network mode (`browser.ssrfPolicy.dangerouslyAllowPrivateNetwork=true` when unset), and canonical config uses `browser.ssrfPolicy.dangerouslyAllowPrivateNetwork` instead of `browser.ssrfPolicy.allowPrivateNetwork`. `openclaw doctor --fix` migrates the legacy key automatically. + ## 2026.2.22 ### Changes @@ -1754,14 +1791,6 @@ Docs: https://docs.openclaw.ai - Skills: remove bundled `food-order` skill from this repo; manage/install it from ClawHub instead. - Docs/Subagents: make thread-bound session guidance channel-first instead of Discord-specific, and list thread-supporting channels explicitly. (#23589) Thanks @osolmaz. -### Breaking - -- **BREAKING:** removed Google Antigravity provider support and the bundled `google-antigravity-auth` plugin. Existing `google-antigravity/*` model/profile configs no longer work; migrate to `google-gemini-cli` or other supported providers. -- **BREAKING:** tool-failure replies now hide raw error details by default. OpenClaw still sends a failure summary, but detailed error suffixes (for example provider/runtime messages and local path fragments) now require `/verbose on` or `/verbose full`. -- **BREAKING:** CLI local onboarding now sets `session.dmScope` to `per-channel-peer` by default for new/implicit DM scope configuration. If you depend on shared DM continuity across senders, explicitly set `session.dmScope` to `main`. (#23468) Thanks @bmendonca3. -- **BREAKING:** unify channel preview-streaming config to `channels..streaming` with enum values `off | partial | block | progress`, and move Slack native stream toggle to `channels.slack.nativeStreaming`. Legacy keys (`streamMode`, Slack boolean `streaming`) are still read and migrated by `openclaw doctor --fix`, but canonical saved config/docs now use the unified names. -- **BREAKING:** remove legacy Gateway device-auth signature `v1`. Device-auth clients must now sign `v2` payloads with the per-connection `connect.challenge` nonce and send `device.nonce`; nonce-less connects are rejected. - ### Fixes - Sessions/Resilience: ignore invalid persisted `sessionFile` metadata and fall back to the derived safe transcript path instead of aborting session resolution for handlers and tooling. (#16061) Thanks @haoyifan and @vincentkoc. @@ -1988,6 +2017,14 @@ Docs: https://docs.openclaw.ai - Gateway/Daemon: verify gateway health after daemon restart. - Agents/UI text: stop rewriting normal assistant billing/payment language outside explicit error contexts. (#17834) Thanks @niceysam. +### Breaking + +- **BREAKING:** removed Google Antigravity provider support and the bundled `google-antigravity-auth` plugin. Existing `google-antigravity/*` model/profile configs no longer work; migrate to `google-gemini-cli` or other supported providers. +- **BREAKING:** tool-failure replies now hide raw error details by default. OpenClaw still sends a failure summary, but detailed error suffixes (for example provider/runtime messages and local path fragments) now require `/verbose on` or `/verbose full`. +- **BREAKING:** CLI local onboarding now sets `session.dmScope` to `per-channel-peer` by default for new/implicit DM scope configuration. If you depend on shared DM continuity across senders, explicitly set `session.dmScope` to `main`. (#23468) Thanks @bmendonca3. +- **BREAKING:** unify channel preview-streaming config to `channels..streaming` with enum values `off | partial | block | progress`, and move Slack native stream toggle to `channels.slack.nativeStreaming`. Legacy keys (`streamMode`, Slack boolean `streaming`) are still read and migrated by `openclaw doctor --fix`, but canonical saved config/docs now use the unified names. +- **BREAKING:** remove legacy Gateway device-auth signature `v1`. Device-auth clients must now sign `v2` payloads with the per-connection `connect.challenge` nonce and send `device.nonce`; nonce-less connects are rejected. + ## 2026.2.21 ### Changes @@ -2636,10 +2673,6 @@ Docs: https://docs.openclaw.ai - Onboarding/Providers: add first-class Hugging Face Inference provider support (provider wiring, onboarding auth choice/API key flow, and default-model selection), and preserve Hugging Face auth intent in auth-choice remapping (`tokenProvider=huggingface` with `authChoice=apiKey`) while skipping env-override prompts when an explicit token is provided. (#13472) Thanks @Josephrp. - Onboarding/Providers: add `minimax-api-key-cn` auth choice for the MiniMax China API endpoint. (#15191) Thanks @liuy. -### Breaking - -- Config/State: removed legacy `.moltbot` auto-detection/migration and `moltbot.json` config candidates. If you still have state/config under `~/.moltbot`, move it to `~/.openclaw` (recommended) or set `OPENCLAW_STATE_DIR` / `OPENCLAW_CONFIG_PATH` explicitly. - ### Fixes - Gateway/Auth: add trusted-proxy mode hardening follow-ups by keeping `OPENCLAW_GATEWAY_*` env compatibility, auto-normalizing invalid setup combinations in interactive `gateway configure` (trusted-proxy forces `bind=lan` and disables Tailscale serve/funnel), and suppressing shared-secret/rate-limit audit findings that do not apply to trusted-proxy deployments. (#15940) Thanks @nickytonline. @@ -2742,6 +2775,10 @@ Docs: https://docs.openclaw.ai - Docs/Mermaid: remove hardcoded Mermaid init theme blocks from four docs diagrams so dark mode inherits readable theme defaults. (#15157) Thanks @heytulsiprasad. - Security/Pairing: generate 256-bit base64url device and node pairing tokens and use byte-safe constant-time verification to avoid token-compare edge-case failures. (#16535) Thanks @FaizanKolega, @gumadeiras. +### Breaking + +- Config/State: removed legacy `.moltbot` auto-detection/migration and `moltbot.json` config candidates. If you still have state/config under `~/.moltbot`, move it to `~/.openclaw` (recommended) or set `OPENCLAW_STATE_DIR` / `OPENCLAW_CONFIG_PATH` explicitly. + ## 2026.2.12 ### Changes @@ -2753,10 +2790,6 @@ Docs: https://docs.openclaw.ai - Discord: add role-based allowlists and role-based agent routing. (#10650) Thanks @Minidoracat. - Config: avoid redacting `maxTokens`-like fields during config snapshot redaction, preventing round-trip validation failures in `/config`. (#14006) Thanks @constansino. -### Breaking - -- Hooks: `POST /hooks/agent` now rejects payload `sessionKey` overrides by default. To keep fixed hook context, set `hooks.defaultSessionKey` (recommended with `hooks.allowedSessionKeyPrefixes: ["hook:"]`). If you need legacy behavior, explicitly set `hooks.allowRequestSessionKey: true`. Thanks @alpernae for reporting. - ### Fixes - Gateway/OpenResponses: harden URL-based `input_file`/`input_image` handling with explicit SSRF deny policy, hostname allowlists (`files.urlAllowlist` / `images.urlAllowlist`), per-request URL input caps (`maxUrlParts`), blocked-fetch audit logging, and regression coverage/docs updates. @@ -2839,6 +2872,10 @@ Docs: https://docs.openclaw.ai - Tests: update thread ID handling in Slack message collection tests. (#14108) Thanks @swizzmagik. - Update/Daemon: fix post-update restart compatibility by generating `dist/cli/daemon-cli.js` with alias-aware exports from hashed daemon bundles, preventing `registerDaemonCli` import failures during `openclaw update`. +### Breaking + +- Hooks: `POST /hooks/agent` now rejects payload `sessionKey` overrides by default. To keep fixed hook context, set `hooks.defaultSessionKey` (recommended with `hooks.allowedSessionKeyPrefixes: ["hook:"]`). If you need legacy behavior, explicitly set `hooks.allowRequestSessionKey: true`. Thanks @alpernae for reporting. + ## 2026.2.9 ### Added @@ -2928,6 +2965,12 @@ Docs: https://docs.openclaw.ai ## 2026.2.6 +### Added + +- Cron: run history deep-links to session chat from the dashboard. (#10776) Thanks @tyler6204. +- Cron: per-run session keys in run log entries and default labels for cron sessions. (#10776) Thanks @tyler6204. +- Cron: legacy payload field compatibility (`deliver`, `channel`, `to`, `bestEffortDeliver`) in schema. (#10776) Thanks @tyler6204. + ### Changes - Cron: default `wakeMode` is now `"now"` for new jobs (was `"next-heartbeat"`). (#10776) Thanks @tyler6204. @@ -2943,12 +2986,6 @@ Docs: https://docs.openclaw.ai - CI: optimize pipeline throughput (macOS consolidation, Windows perf, workflow concurrency). (#10784) Thanks @mcaxtr. - Agents: bump pi-mono to 0.52.7; add embedded forward-compat fallback for Opus 4.6 model ids. -### Added - -- Cron: run history deep-links to session chat from the dashboard. (#10776) Thanks @tyler6204. -- Cron: per-run session keys in run log entries and default labels for cron sessions. (#10776) Thanks @tyler6204. -- Cron: legacy payload field compatibility (`deliver`, `channel`, `to`, `bestEffortDeliver`) in schema. (#10776) Thanks @tyler6204. - ### Fixes - TTS: add missing OpenAI voices (ballad, cedar, juniper, marin, verse) to the allowlist so they are recognized instead of silently falling back to Edge TTS. (#2393) @@ -3247,10 +3284,6 @@ Docs: https://docs.openclaw.ai - Docs: keep docs header sticky so navbar stays visible while scrolling. (#2445) Thanks @chenyuan99. - Docs: update exe.dev install instructions. (#https://github.com/openclaw/openclaw/pull/3047) Thanks @zackerthescar. -### Breaking - -- **BREAKING:** Gateway auth mode "none" is removed; gateway now requires token/password (Tailscale Serve identity still allowed). - ### Fixes - Skills: update session-logs paths to use ~/.openclaw. (#4502) Thanks @bonald. @@ -3303,6 +3336,10 @@ Docs: https://docs.openclaw.ai - Gateway: treat loopback + non-local Host connections as remote unless trusted proxy headers are present. - Onboarding: remove unsupported gateway auth "off" choice from onboarding/configure flows and CLI flags. +### Breaking + +- **BREAKING:** Gateway auth mode "none" is removed; gateway now requires token/password (Tailscale Serve identity still allowed). + ## 2026.1.24-3 ### Fixes @@ -3534,11 +3571,6 @@ Docs: https://docs.openclaw.ai - Docs: add /model allowlist troubleshooting note. (#1405) - Docs: add per-message Gmail search example for gog. (#1220) Thanks @mbelinky. -### Breaking - -- **BREAKING:** Control UI now rejects insecure HTTP without device identity by default. Use HTTPS (Tailscale Serve) or set `gateway.controlUi.allowInsecureAuth: true` to allow token-only auth. https://docs.openclaw.ai/web/control-ui#insecure-http -- **BREAKING:** Envelope and system event timestamps now default to host-local time (was UTC) so agents don’t have to constantly convert. - ### Fixes - Nodes/macOS: prompt on allowlist miss for node exec approvals, persist allowlist decisions, and flatten node invoke errors. (#1394) Thanks @ngutman. @@ -3561,6 +3593,11 @@ Docs: https://docs.openclaw.ai - macOS: default distribution packaging to universal binaries. (#1396) Thanks @JustYannicc. - Embedded runner: forward sender identity into attempt execution so Feishu doc auto-grant receives requester context again. (#32915) Thanks @cszhouwei. +### Breaking + +- **BREAKING:** Control UI now rejects insecure HTTP without device identity by default. Use HTTPS (Tailscale Serve) or set `gateway.controlUi.allowInsecureAuth: true` to allow token-only auth. https://docs.openclaw.ai/web/control-ui#insecure-http +- **BREAKING:** Envelope and system event timestamps now default to host-local time (was UTC) so agents don’t have to constantly convert. + ## 2026.1.20 ### Changes @@ -3642,10 +3679,6 @@ Docs: https://docs.openclaw.ai - macOS: stop syncing Peekaboo in postinstall. - Swabble: use the tagged Commander Swift package release. -### Breaking - -- **BREAKING:** Reject invalid/unknown config entries and refuse to start the gateway for safety. Run `openclaw doctor --fix` to repair, then update plugins (`openclaw plugins update`) if you use any. - ### Fixes - Discovery: shorten Bonjour DNS-SD service type to `_moltbot-gw._tcp` and update discovery clients/docs. @@ -3744,6 +3777,10 @@ Docs: https://docs.openclaw.ai Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @NicholaiVogel, @RyanLisse, @ThePickle31, @VACInc, @Whoaa512, @YuriNachos, @aaronveklabs, @abdaraxus, @alauppe, @ameno-, @artuskg, @austinm911, @bradleypriest, @cheeeee, @dougvk, @fogboots, @gnarco, @gumadeiras, @jdrhyne, @joelklabo, @longmaba, @mukhtharcm, @odysseus0, @oscargavin, @rhjoh, @sebslight, @sibbl, @sleontenko, @steipete, @suminhthanh, @thewilloftheshadow, @tyler6204, @vignesh07, @visionik, @ysqander, @zerone0x. +### Breaking + +- **BREAKING:** Reject invalid/unknown config entries and refuse to start the gateway for safety. Run `openclaw doctor --fix` to repair, then update plugins (`openclaw plugins update`) if you use any. + ## 2026.1.16-2 ### Changes @@ -3762,15 +3799,6 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic - Sessions: add `session.identityLinks` for cross-platform DM session li nking. (#1033) — thanks @thewilloftheshadow. https://docs.openclaw.ai/concepts/session - Web search: add `country`/`language` parameters (schema + Brave API) and docs. (#1046) — thanks @YuriNachos. https://docs.openclaw.ai/tools/web -### Breaking - -- **BREAKING:** `openclaw message` and message tool now require `target` (dropping `to`/`channelId` for destinations). (#1034) — thanks @tobalsan. -- **BREAKING:** Channel auth now prefers config over env for Discord/Telegram/Matrix (env is fallback only). (#1040) — thanks @thewilloftheshadow. -- **BREAKING:** Drop legacy `chatType: "room"` support; use `chatType: "channel"`. -- **BREAKING:** remove legacy provider-specific target resolution fallbacks; target resolution is centralized with plugin hints + directory lookups. -- **BREAKING:** `openclaw hooks` is now `openclaw webhooks`; hooks live under `openclaw hooks`. https://docs.openclaw.ai/cli/webhooks -- **BREAKING:** `openclaw plugins install ` now copies into `~/.openclaw/extensions` (use `--link` to keep path-based loading). - ### Changes - Plugins: ship bundled plugins disabled by default and allow overrides by installed versions. (#1066) — thanks @ItzR3NO. @@ -3862,6 +3890,15 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic - Discord: preserve whitespace when chunking long lines so message splits keep spacing intact. - Skills: fix skills watcher ignored list typing (tsc). +### Breaking + +- **BREAKING:** `openclaw message` and message tool now require `target` (dropping `to`/`channelId` for destinations). (#1034) — thanks @tobalsan. +- **BREAKING:** Channel auth now prefers config over env for Discord/Telegram/Matrix (env is fallback only). (#1040) — thanks @thewilloftheshadow. +- **BREAKING:** Drop legacy `chatType: "room"` support; use `chatType: "channel"`. +- **BREAKING:** remove legacy provider-specific target resolution fallbacks; target resolution is centralized with plugin hints + directory lookups. +- **BREAKING:** `openclaw hooks` is now `openclaw webhooks`; hooks live under `openclaw hooks`. https://docs.openclaw.ai/cli/webhooks +- **BREAKING:** `openclaw plugins install ` now copies into `~/.openclaw/extensions` (use `--link` to keep path-based loading). + ## 2026.1.15 ### Highlights @@ -3871,11 +3908,6 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic - Heartbeat: per-agent configuration + 24h duplicate suppression. (#980) — thanks @voidserf. - Security: audit warns on weak model tiers; app nodes store auth tokens encrypted (Keychain/SecurePrefs). -### Breaking - -- **BREAKING:** iOS minimum version is now 18.0 to support Textual markdown rendering in native chat. (#702) -- **BREAKING:** Microsoft Teams is now a plugin; install `@openclaw/msteams` via `openclaw plugins install @openclaw/msteams`. - ### Changes - UI/Apps: move channel/config settings to schema-driven forms and rename Connections → Channels. (#1040) — thanks @thewilloftheshadow. @@ -3948,6 +3980,11 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic - Fix: allow local Tailscale Serve hostnames without treating tailnet clients as direct. (#885) — thanks @oswalpalash. - Fix: reset sessions after role-ordering conflicts to recover from consecutive user turns. (#998) +### Breaking + +- **BREAKING:** iOS minimum version is now 18.0 to support Textual markdown rendering in native chat. (#702) +- **BREAKING:** Microsoft Teams is now a plugin; install `@openclaw/msteams` via `openclaw plugins install @openclaw/msteams`. + ## 2026.1.14-1 ### Highlights @@ -4084,10 +4121,6 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic - Gateway: allow Tailscale Serve identity headers to satisfy token auth; rebuild Control UI assets when protocol schema is newer. (#823) — thanks @roshanasingh4; (#786) — thanks @meaningfool. - Heartbeat: default `ackMaxChars` to 300 so short `HEARTBEAT_OK` replies stay internal. -### Installer - -- Install: run `openclaw doctor --non-interactive` after git installs/updates and nudge daemon restarts when detected. - ### Fixes - Doctor: warn on pnpm workspace mismatches, missing Control UI assets, and missing tsx binaries; offer UI rebuilds. @@ -4113,6 +4146,10 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic - Tools/UI: harden tool input schemas for strict providers; drop null-only union variants for Gemini schema cleanup; treat `maxChars: 0` as unlimited; keep TUI last streamed response instead of "(no output)". (#782) — thanks @AbhisekBasu1; (#796) — thanks @gabriel-trigo; (#747) — thanks @thewilloftheshadow. - Connections UI: polish multi-account account cards. (#816) — thanks @steipete. +### Installer + +- Install: run `openclaw doctor --non-interactive` after git installs/updates and nudge daemon restarts when detected. + ### Maintenance - Dependencies: bump Pi packages to 0.45.3 and refresh patched pi-ai. @@ -4164,15 +4201,6 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic - Gateway: require `client.id` in WebSocket connect params; use `client.instanceId` for presence de-dupe; update docs/tests. - macOS: remove the attach-only gateway setting; local mode now always manages launchd while still attaching to an existing gateway if present. -### Installer - -- Postinstall: replace `git apply` with builtin JS patcher (works npm/pnpm/bun; no git dependency) plus regression tests. -- Postinstall: skip pnpm patch fallback when the new patcher is active. -- Installer tests: add root+non-root docker smokes, CI workflow to fetch openclaw.ai scripts and run install sh/cli with onboarding skipped. -- Installer UX: support `CLAWDBOT_NO_ONBOARD=1` for non-interactive installs; fix npm prefix on Linux and auto-install git. -- Installer UX: add `install.sh --help` with flags/env and git install hint. -- Installer UX: add `--install-method git|npm` and auto-detect source checkouts (prompt to update git checkout vs migrate to npm). - ### Fixes - Models/Onboarding: configure MiniMax (minimax.io) via Anthropic-compatible `/anthropic` endpoint by default (keep `minimax-api` as a legacy alias). @@ -4211,6 +4239,15 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic - Sandbox/Gateway: treat `agent::main` as a main-session alias when `session.mainKey` is customized (backwards compatible). - Auto-reply: fast-path allowlisted slash commands (inline `/help`/`/commands`/`/status`/`/whoami` stripped before model). +### Installer + +- Postinstall: replace `git apply` with builtin JS patcher (works npm/pnpm/bun; no git dependency) plus regression tests. +- Postinstall: skip pnpm patch fallback when the new patcher is active. +- Installer tests: add root+non-root docker smokes, CI workflow to fetch openclaw.ai scripts and run install sh/cli with onboarding skipped. +- Installer UX: support `CLAWDBOT_NO_ONBOARD=1` for non-interactive installs; fix npm prefix on Linux and auto-install git. +- Installer UX: add `install.sh --help` with flags/env and git install hint. +- Installer UX: add `--install-method git|npm` and auto-detect source checkouts (prompt to update git checkout vs migrate to npm). + ## 2026.1.10 ### Highlights @@ -4319,11 +4356,6 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic - Auto-reply + status: block-streaming controls, reasoning handling, usage/cost reporting. - Control UI/TUI: queued messages, session links, reasoning view, mobile polish, logs UX. -### Breaking - -- CLI: `openclaw message` now subcommands (`message send|poll|...`) and requires `--provider` unless only one provider configured. -- Commands/Tools: `/restart` and gateway restart tool disabled by default; enable with `commands.restart=true`. - ### New Features and Changes - Models/Auth: OpenCode Zen onboarding (#623) — thanks @magimetal; MiniMax Anthropic-compatible API + hosted onboarding (#590, #495) — thanks @mneves75, @tobiasbischoff. @@ -4365,6 +4397,11 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic - Onboarding/Configure: QuickStart single-select provider picker; avoid Codex CLI false-expiry warnings; clarify WhatsApp owner prompt; fix Minimax hosted onboarding (agents.defaults + msteams heartbeat target); remove configure Control UI prompt; honor gateway --dev flag. - Agent loop: guard overflow compaction throws and restore compaction hooks for engine-owned context engines. (#41361) — thanks @davidrudduck +### Breaking + +- CLI: `openclaw message` now subcommands (`message send|poll|...`) and requires `--provider` unless only one provider configured. +- Commands/Tools: `/restart` and gateway restart tool disabled by default; enable with `commands.restart=true`. + ### Maintenance - Dependencies: bump pi-\* stack to 0.42.2. @@ -4384,6 +4421,18 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic - Control UI: logs tab, streaming stability, focus mode, and large-output rendering fixes. - CLI/Gateway/Doctor: daemon/logs/status, auth migration, and diagnostics significantly expanded. +### Fixes + +- **CLI/Gateway/Doctor:** daemon runtime selection + improved logs/status/health/errors; auth/password handling for local CLI; richer close/timeout details; auto-migrate legacy config/sessions/state; integrity checks + repair prompts; `--yes`/`--non-interactive`; `--deep` gateway scans; better restart/service hints. +- **Agent loop + compaction:** compaction/pruning tuning, overflow handling, safer bootstrap context, and per-provider threading/confirmations; opt-in tool-result pruning + compact tracking. +- **Sandbox + tools:** per-agent sandbox overrides, workspaceAccess controls, session tool visibility, tool policy overrides, process isolation, and tool schema/timeout/reaction unification. +- **Providers (Telegram/WhatsApp/Discord/Slack/Signal/iMessage):** retry/backoff, threading, reactions, media groups/attachments, mention gating, typing behavior, and error/log stability; long polling + forum topic isolation for Telegram. +- **Gateway/CLI UX:** `openclaw logs`, cron list colors/aliases, docs search, agents list/add/delete flows, status usage snapshots, runtime/auth source display, and `/status`/commands auth unification. +- **Control UI/Web:** logs tab, focus mode polish, config form resilience, streaming stability, tool output caps, windowed chat history, and reconnect/password URL auth. +- **macOS/Android/TUI/Build:** macOS gateway races, QR bundling, JSON5 config safety, Voice Wake hardening; Android EXIF rotation + APK naming/versioning; TUI key handling; tooling/bundling fixes. +- **Packaging/compat:** npm dist folder coverage, Node 25 qrcode-terminal import fixes, Bun/Playwright/WebSocket patches, and Docker Bun install. +- **Docs:** new FAQ/ClawHub/config examples/showcase entries and clarified auth, sandbox, and systemd docs. + ### Breaking - **SECURITY (update ASAP):** inbound DMs are now **locked down by default** on Telegram/WhatsApp/Signal/iMessage/Discord/Slack. @@ -4399,18 +4448,6 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic - Auto-reply: removed `autoReply` from Discord/Slack/Telegram channel configs; use `requireMention` instead (Telegram topics now support `requireMention` overrides). - CLI: remove `update`, `gateway-daemon`, `gateway {install|uninstall|start|stop|restart|daemon status|wake|send|agent}`, and `telegram` commands; move `login/logout` to `providers login/logout` (top-level aliases hidden); use `daemon` for service control, `send`/`agent`/`wake` for RPC, and `nodes canvas` for canvas ops. -### Fixes - -- **CLI/Gateway/Doctor:** daemon runtime selection + improved logs/status/health/errors; auth/password handling for local CLI; richer close/timeout details; auto-migrate legacy config/sessions/state; integrity checks + repair prompts; `--yes`/`--non-interactive`; `--deep` gateway scans; better restart/service hints. -- **Agent loop + compaction:** compaction/pruning tuning, overflow handling, safer bootstrap context, and per-provider threading/confirmations; opt-in tool-result pruning + compact tracking. -- **Sandbox + tools:** per-agent sandbox overrides, workspaceAccess controls, session tool visibility, tool policy overrides, process isolation, and tool schema/timeout/reaction unification. -- **Providers (Telegram/WhatsApp/Discord/Slack/Signal/iMessage):** retry/backoff, threading, reactions, media groups/attachments, mention gating, typing behavior, and error/log stability; long polling + forum topic isolation for Telegram. -- **Gateway/CLI UX:** `openclaw logs`, cron list colors/aliases, docs search, agents list/add/delete flows, status usage snapshots, runtime/auth source display, and `/status`/commands auth unification. -- **Control UI/Web:** logs tab, focus mode polish, config form resilience, streaming stability, tool output caps, windowed chat history, and reconnect/password URL auth. -- **macOS/Android/TUI/Build:** macOS gateway races, QR bundling, JSON5 config safety, Voice Wake hardening; Android EXIF rotation + APK naming/versioning; TUI key handling; tooling/bundling fixes. -- **Packaging/compat:** npm dist folder coverage, Node 25 qrcode-terminal import fixes, Bun/Playwright/WebSocket patches, and Docker Bun install. -- **Docs:** new FAQ/ClawHub/config examples/showcase entries and clarified auth, sandbox, and systemd docs. - ### Maintenance - Skills additions (Himalaya email, CodexBar, 1Password). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 14a9b3c8bcd..9e487f254cd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -47,7 +47,7 @@ Welcome to the lobster tank! 🦞 - **Christoph Nakazawa** - JS Infra - GitHub: [@cpojer](https://github.com/cpojer) · X: [@cnakazawa](https://x.com/cnakazawa) -- **Gustavo Madeira Santana** - Multi-agents, CLI, web UI +- **Gustavo Madeira Santana** - Multi-agents, CLI, Performance, Plugins, Matrix - GitHub: [@gumadeiras](https://github.com/gumadeiras) · X: [@gumadeiras](https://x.com/gumadeiras) - **Onur Solmaz** - Agents, dev workflows, ACP integrations, MS Teams diff --git a/README.md b/README.md index 418e2a070af..1c836da84ee 100644 --- a/README.md +++ b/README.md @@ -364,7 +364,7 @@ Details: [Security guide](https://docs.openclaw.ai/gateway/security) · [Docker ### [Discord](https://docs.openclaw.ai/channels/discord) -- Set `DISCORD_BOT_TOKEN` or `channels.discord.token` (env wins). +- Set `DISCORD_BOT_TOKEN` or `channels.discord.token`. - Optional: set `commands.native`, `commands.text`, or `commands.useAccessGroups`, plus `channels.discord.allowFrom`, `channels.discord.guilds`, or `channels.discord.mediaMaxMb` as needed. ```json5 diff --git a/apps/macos/Sources/OpenClaw/MenuSessionsInjector.swift b/apps/macos/Sources/OpenClaw/MenuSessionsInjector.swift index eb6271d0a8c..9f667cc6239 100644 --- a/apps/macos/Sources/OpenClaw/MenuSessionsInjector.swift +++ b/apps/macos/Sources/OpenClaw/MenuSessionsInjector.swift @@ -1099,38 +1099,33 @@ extension MenuSessionsInjector { // MARK: - Width + placement private func findInsertIndex(in menu: NSMenu) -> Int? { - // Insert right before the separator above "Send Heartbeats". - if let idx = menu.items.firstIndex(where: { $0.title == "Send Heartbeats" }) { - if let sepIdx = menu.items[..= 1 { return 1 } - return menu.items.count + self.findDynamicSectionInsertIndex(in: menu) } private func findNodesInsertIndex(in menu: NSMenu) -> Int? { - if let idx = menu.items.firstIndex(where: { $0.title == "Send Heartbeats" }) { - if let sepIdx = menu.items[.. Int? { + // Keep controls and action buttons visible by inserting dynamic rows at the + // built-in footer boundary, not by matching localized menu item titles. + if let footerSeparatorIndex = menu.items.lastIndex(where: { item in + item.isSeparatorItem && !self.isInjectedItem(item) + }) { + return footerSeparatorIndex } - if let sepIdx = menu.items.firstIndex(where: { $0.isSeparatorItem }) { - return sepIdx + if let firstBaseItemIndex = menu.items.firstIndex(where: { !self.isInjectedItem($0) }) { + return min(firstBaseItemIndex + 1, menu.items.count) } - if menu.items.count >= 1 { return 1 } return menu.items.count } + private func isInjectedItem(_ item: NSMenuItem) -> Bool { + item.tag == self.tag || item.tag == self.nodesTag + } + private func initialWidth(for menu: NSMenu) -> CGFloat { if let openWidth = self.menuOpenWidth { return max(300, openWidth) @@ -1236,5 +1231,13 @@ extension MenuSessionsInjector { func injectForTesting(into menu: NSMenu) { self.inject(into: menu) } + + func testingFindInsertIndex(in menu: NSMenu) -> Int? { + self.findInsertIndex(in: menu) + } + + func testingFindNodesInsertIndex(in menu: NSMenu) -> Int? { + self.findNodesInsertIndex(in: menu) + } } #endif diff --git a/apps/macos/Tests/OpenClawIPCTests/MenuSessionsInjectorTests.swift b/apps/macos/Tests/OpenClawIPCTests/MenuSessionsInjectorTests.swift index 186675f1eea..b1d01b9650e 100644 --- a/apps/macos/Tests/OpenClawIPCTests/MenuSessionsInjectorTests.swift +++ b/apps/macos/Tests/OpenClawIPCTests/MenuSessionsInjectorTests.swift @@ -5,7 +5,26 @@ import Testing @Suite(.serialized) @MainActor struct MenuSessionsInjectorTests { - @Test func `injects disconnected message`() { + @Test func anchorsDynamicRowsBelowControlsAndActions() throws { + let injector = MenuSessionsInjector() + + let menu = NSMenu() + menu.addItem(NSMenuItem(title: "Header", action: nil, keyEquivalent: "")) + menu.addItem(.separator()) + menu.addItem(NSMenuItem(title: "Send Heartbeats", action: nil, keyEquivalent: "")) + menu.addItem(NSMenuItem(title: "Browser Control", action: nil, keyEquivalent: "")) + menu.addItem(.separator()) + menu.addItem(NSMenuItem(title: "Open Dashboard", action: nil, keyEquivalent: "")) + menu.addItem(NSMenuItem(title: "Open Chat", action: nil, keyEquivalent: "")) + menu.addItem(.separator()) + menu.addItem(NSMenuItem(title: "Settings…", action: nil, keyEquivalent: "")) + + let footerSeparatorIndex = try #require(menu.items.lastIndex(where: { $0.isSeparatorItem })) + #expect(injector.testingFindInsertIndex(in: menu) == footerSeparatorIndex) + #expect(injector.testingFindNodesInsertIndex(in: menu) == footerSeparatorIndex) + } + + @Test func injectsDisconnectedMessage() { let injector = MenuSessionsInjector() injector.setTestingControlChannelConnected(false) injector.setTestingSnapshot(nil, errorText: nil) @@ -19,7 +38,7 @@ struct MenuSessionsInjectorTests { #expect(menu.items.contains { $0.tag == 9_415_557 }) } - @Test func `injects session rows`() { + @Test func injectsSessionRows() throws { let injector = MenuSessionsInjector() injector.setTestingControlChannelConnected(true) @@ -88,10 +107,22 @@ struct MenuSessionsInjectorTests { menu.addItem(NSMenuItem(title: "Header", action: nil, keyEquivalent: "")) menu.addItem(.separator()) menu.addItem(NSMenuItem(title: "Send Heartbeats", action: nil, keyEquivalent: "")) + menu.addItem(NSMenuItem(title: "Browser Control", action: nil, keyEquivalent: "")) + menu.addItem(.separator()) + menu.addItem(NSMenuItem(title: "Open Dashboard", action: nil, keyEquivalent: "")) + menu.addItem(.separator()) + menu.addItem(NSMenuItem(title: "Settings…", action: nil, keyEquivalent: "")) injector.injectForTesting(into: menu) #expect(menu.items.contains { $0.tag == 9_415_557 }) #expect(menu.items.contains { $0.tag == 9_415_557 && $0.isSeparatorItem }) + let sendHeartbeatsIndex = try #require(menu.items.firstIndex(where: { $0.title == "Send Heartbeats" })) + let openDashboardIndex = try #require(menu.items.firstIndex(where: { $0.title == "Open Dashboard" })) + let firstInjectedIndex = try #require(menu.items.firstIndex(where: { $0.tag == 9_415_557 })) + let settingsIndex = try #require(menu.items.firstIndex(where: { $0.title == "Settings…" })) + #expect(sendHeartbeatsIndex < firstInjectedIndex) + #expect(openDashboardIndex < firstInjectedIndex) + #expect(firstInjectedIndex < settingsIndex) } @Test func `cost usage submenu does not use injector delegate`() { diff --git a/docs/.generated/config-baseline.json b/docs/.generated/config-baseline.json index 1efe91f11a7..7229f7e07cc 100644 --- a/docs/.generated/config-baseline.json +++ b/docs/.generated/config-baseline.json @@ -23200,6 +23200,56 @@ "tags": [], "hasChildren": false }, + { + "path": "channels.mattermost.accounts.*.dmChannelRetry", + "kind": "channel", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": true + }, + { + "path": "channels.mattermost.accounts.*.dmChannelRetry.initialDelayMs", + "kind": "channel", + "type": "integer", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.mattermost.accounts.*.dmChannelRetry.maxDelayMs", + "kind": "channel", + "type": "integer", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.mattermost.accounts.*.dmChannelRetry.maxRetries", + "kind": "channel", + "type": "integer", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.mattermost.accounts.*.dmChannelRetry.timeoutMs", + "kind": "channel", + "type": "integer", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, { "path": "channels.mattermost.accounts.*.dmPolicy", "kind": "channel", @@ -23709,6 +23759,56 @@ "tags": [], "hasChildren": false }, + { + "path": "channels.mattermost.dmChannelRetry", + "kind": "channel", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": true + }, + { + "path": "channels.mattermost.dmChannelRetry.initialDelayMs", + "kind": "channel", + "type": "integer", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.mattermost.dmChannelRetry.maxDelayMs", + "kind": "channel", + "type": "integer", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.mattermost.dmChannelRetry.maxRetries", + "kind": "channel", + "type": "integer", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "channels.mattermost.dmChannelRetry.timeoutMs", + "kind": "channel", + "type": "integer", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, { "path": "channels.mattermost.dmPolicy", "kind": "channel", @@ -37601,12 +37701,13 @@ "path": "channels.zalouser.accounts.*.groupPolicy", "kind": "channel", "type": "string", - "required": false, + "required": true, "enumValues": [ "open", "disabled", "allowlist" ], + "defaultValue": "allowlist", "deprecated": false, "sensitive": false, "tags": [], @@ -37903,12 +38004,13 @@ "path": "channels.zalouser.groupPolicy", "kind": "channel", "type": "string", - "required": false, + "required": true, "enumValues": [ "open", "disabled", "allowlist" ], + "defaultValue": "allowlist", "deprecated": false, "sensitive": false, "tags": [], @@ -39699,7 +39801,7 @@ "network" ], "label": "Control UI Allowed Origins", - "help": "Allowed browser origins for Control UI/WebChat websocket connections (full origins only, e.g. https://control.example.com). Required for non-loopback Control UI deployments unless dangerous Host-header fallback is explicitly enabled.", + "help": "Allowed browser origins for Control UI/WebChat websocket connections (full origins only, e.g. https://control.example.com). Required for non-loopback Control UI deployments unless dangerous Host-header fallback is explicitly enabled. Setting [\"*\"] means allow any browser origin and should be avoided outside tightly controlled local testing.", "hasChildren": true }, { @@ -41038,7 +41140,7 @@ "access" ], "label": "Hooks Allowed Agent IDs", - "help": "Allowlist of agent IDs that hook mappings are allowed to target when selecting execution agents. Use this to constrain automation events to dedicated service agents.", + "help": "Allowlist of agent IDs that hook mappings are allowed to target when selecting execution agents. Use this to constrain automation events to dedicated service agents and reduce blast radius if a hook token is exposed.", "hasChildren": true }, { @@ -42156,7 +42258,7 @@ "security" ], "label": "Hooks Auth Token", - "help": "Shared bearer token checked by hooks ingress for request authentication before mappings run. Use environment substitution and rotate regularly when webhook endpoints are internet-accessible.", + "help": "Shared bearer token checked by hooks ingress for request authentication before mappings run. Treat holders as full-trust callers for the hook ingress surface, not as a separate non-owner role. Use environment substitution and rotate regularly when webhook endpoints are internet-accessible.", "hasChildren": false }, { @@ -45294,6 +45396,58 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.*.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.*.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.*.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.*.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, { "path": "plugins.entries.acpx", "kind": "plugin", @@ -45560,6 +45714,58 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.acpx.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.acpx.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.acpx.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.acpx.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, { "path": "plugins.entries.amazon-bedrock", "kind": "plugin", @@ -45629,6 +45835,58 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.amazon-bedrock.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.amazon-bedrock.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.amazon-bedrock.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.amazon-bedrock.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, { "path": "plugins.entries.anthropic", "kind": "plugin", @@ -45698,6 +45956,58 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.anthropic.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.anthropic.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.anthropic.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.anthropic.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, { "path": "plugins.entries.bluebubbles", "kind": "plugin", @@ -45767,6 +46077,58 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.bluebubbles.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.bluebubbles.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.bluebubbles.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.bluebubbles.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, { "path": "plugins.entries.brave", "kind": "plugin", @@ -45836,6 +46198,58 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.brave.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.brave.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.brave.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.brave.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, { "path": "plugins.entries.byteplus", "kind": "plugin", @@ -45905,6 +46319,179 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.byteplus.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.byteplus.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.byteplus.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.byteplus.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, + { + "path": "plugins.entries.chutes", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "@openclaw/chutes-provider", + "help": "OpenClaw Chutes.ai provider plugin (plugin: chutes)", + "hasChildren": true + }, + { + "path": "plugins.entries.chutes.config", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "@openclaw/chutes-provider Config", + "help": "Plugin-defined config payload for chutes.", + "hasChildren": false + }, + { + "path": "plugins.entries.chutes.enabled", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Enable @openclaw/chutes-provider", + "hasChildren": false + }, + { + "path": "plugins.entries.chutes.hooks", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Hook Policy", + "help": "Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.", + "hasChildren": true + }, + { + "path": "plugins.entries.chutes.hooks.allowPromptInjection", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Prompt Injection Hooks", + "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", + "hasChildren": false + }, + { + "path": "plugins.entries.chutes.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.chutes.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.chutes.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.chutes.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, { "path": "plugins.entries.cloudflare-ai-gateway", "kind": "plugin", @@ -45974,6 +46561,58 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.cloudflare-ai-gateway.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.cloudflare-ai-gateway.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.cloudflare-ai-gateway.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.cloudflare-ai-gateway.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, { "path": "plugins.entries.copilot-proxy", "kind": "plugin", @@ -46043,6 +46682,58 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.copilot-proxy.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.copilot-proxy.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.copilot-proxy.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.copilot-proxy.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, { "path": "plugins.entries.device-pair", "kind": "plugin", @@ -46126,6 +46817,58 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.device-pair.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.device-pair.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.device-pair.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.device-pair.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, { "path": "plugins.entries.diagnostics-otel", "kind": "plugin", @@ -46195,6 +46938,58 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.diagnostics-otel.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.diagnostics-otel.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.diagnostics-otel.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.diagnostics-otel.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, { "path": "plugins.entries.diffs", "kind": "plugin", @@ -46601,6 +47396,58 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.diffs.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.diffs.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.diffs.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.diffs.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, { "path": "plugins.entries.discord", "kind": "plugin", @@ -46670,6 +47517,58 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.discord.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.discord.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.discord.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.discord.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, { "path": "plugins.entries.elevenlabs", "kind": "plugin", @@ -46739,6 +47638,58 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.elevenlabs.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.elevenlabs.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.elevenlabs.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.elevenlabs.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, { "path": "plugins.entries.feishu", "kind": "plugin", @@ -46808,6 +47759,58 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.feishu.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.feishu.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.feishu.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.feishu.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, { "path": "plugins.entries.firecrawl", "kind": "plugin", @@ -46877,6 +47880,58 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.firecrawl.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.firecrawl.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.firecrawl.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.firecrawl.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, { "path": "plugins.entries.github-copilot", "kind": "plugin", @@ -46946,6 +48001,58 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.github-copilot.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.github-copilot.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.github-copilot.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.github-copilot.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, { "path": "plugins.entries.google", "kind": "plugin", @@ -47015,6 +48122,58 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.google.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.google.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.google.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.google.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, { "path": "plugins.entries.googlechat", "kind": "plugin", @@ -47084,6 +48243,58 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.googlechat.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.googlechat.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.googlechat.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.googlechat.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, { "path": "plugins.entries.huggingface", "kind": "plugin", @@ -47153,6 +48364,58 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.huggingface.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.huggingface.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.huggingface.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.huggingface.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, { "path": "plugins.entries.imessage", "kind": "plugin", @@ -47222,6 +48485,58 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.imessage.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.imessage.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.imessage.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.imessage.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, { "path": "plugins.entries.irc", "kind": "plugin", @@ -47291,6 +48606,58 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.irc.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.irc.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.irc.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.irc.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, { "path": "plugins.entries.kilocode", "kind": "plugin", @@ -47360,6 +48727,58 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.kilocode.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.kilocode.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.kilocode.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.kilocode.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, { "path": "plugins.entries.kimi", "kind": "plugin", @@ -47429,6 +48848,58 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.kimi.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.kimi.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.kimi.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.kimi.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, { "path": "plugins.entries.line", "kind": "plugin", @@ -47498,6 +48969,58 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.line.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.line.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.line.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.line.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, { "path": "plugins.entries.llm-task", "kind": "plugin", @@ -47637,6 +49160,58 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.llm-task.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.llm-task.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.llm-task.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.llm-task.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, { "path": "plugins.entries.lobster", "kind": "plugin", @@ -47706,6 +49281,58 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.lobster.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.lobster.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.lobster.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.lobster.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, { "path": "plugins.entries.matrix", "kind": "plugin", @@ -47775,6 +49402,58 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.matrix.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.matrix.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.matrix.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.matrix.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, { "path": "plugins.entries.mattermost", "kind": "plugin", @@ -47844,6 +49523,58 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.mattermost.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.mattermost.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.mattermost.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.mattermost.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, { "path": "plugins.entries.memory-core", "kind": "plugin", @@ -47913,6 +49644,58 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.memory-core.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.memory-core.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.memory-core.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.memory-core.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, { "path": "plugins.entries.memory-lancedb", "kind": "plugin", @@ -48111,6 +49894,58 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.memory-lancedb.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.memory-lancedb.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.memory-lancedb.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.memory-lancedb.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, { "path": "plugins.entries.microsoft", "kind": "plugin", @@ -48180,6 +50015,58 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.microsoft.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.microsoft.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.microsoft.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.microsoft.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, { "path": "plugins.entries.minimax", "kind": "plugin", @@ -48249,6 +50136,58 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.minimax.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.minimax.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.minimax.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.minimax.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, { "path": "plugins.entries.mistral", "kind": "plugin", @@ -48318,6 +50257,58 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.mistral.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.mistral.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.mistral.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.mistral.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, { "path": "plugins.entries.modelstudio", "kind": "plugin", @@ -48387,6 +50378,58 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.modelstudio.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.modelstudio.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.modelstudio.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.modelstudio.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, { "path": "plugins.entries.moonshot", "kind": "plugin", @@ -48456,6 +50499,58 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.moonshot.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.moonshot.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.moonshot.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.moonshot.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, { "path": "plugins.entries.msteams", "kind": "plugin", @@ -48525,6 +50620,58 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.msteams.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.msteams.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.msteams.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.msteams.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, { "path": "plugins.entries.nextcloud-talk", "kind": "plugin", @@ -48594,6 +50741,58 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.nextcloud-talk.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.nextcloud-talk.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.nextcloud-talk.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.nextcloud-talk.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, { "path": "plugins.entries.nostr", "kind": "plugin", @@ -48663,6 +50862,58 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.nostr.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.nostr.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.nostr.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.nostr.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, { "path": "plugins.entries.nvidia", "kind": "plugin", @@ -48732,6 +50983,58 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.nvidia.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.nvidia.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.nvidia.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.nvidia.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, { "path": "plugins.entries.ollama", "kind": "plugin", @@ -48801,6 +51104,58 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.ollama.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.ollama.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.ollama.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.ollama.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, { "path": "plugins.entries.open-prose", "kind": "plugin", @@ -48870,6 +51225,58 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.open-prose.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.open-prose.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.open-prose.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.open-prose.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, { "path": "plugins.entries.openai", "kind": "plugin", @@ -48939,6 +51346,58 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.openai.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.openai.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.openai.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.openai.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, { "path": "plugins.entries.opencode", "kind": "plugin", @@ -49022,6 +51481,58 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.opencode-go.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.opencode-go.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.opencode-go.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.opencode-go.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, { "path": "plugins.entries.opencode.config", "kind": "plugin", @@ -49077,6 +51588,58 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.opencode.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.opencode.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.opencode.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.opencode.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, { "path": "plugins.entries.openrouter", "kind": "plugin", @@ -49146,6 +51709,58 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.openrouter.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.openrouter.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.openrouter.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.openrouter.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, { "path": "plugins.entries.openshell", "kind": "plugin", @@ -49382,6 +51997,58 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.openshell.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.openshell.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.openshell.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.openshell.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, { "path": "plugins.entries.perplexity", "kind": "plugin", @@ -49451,6 +52118,58 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.perplexity.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.perplexity.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.perplexity.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.perplexity.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, { "path": "plugins.entries.phone-control", "kind": "plugin", @@ -49520,6 +52239,58 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.phone-control.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.phone-control.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.phone-control.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.phone-control.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, { "path": "plugins.entries.qianfan", "kind": "plugin", @@ -49589,6 +52360,58 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.qianfan.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.qianfan.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.qianfan.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.qianfan.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, { "path": "plugins.entries.qwen-portal-auth", "kind": "plugin", @@ -49658,6 +52481,58 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.qwen-portal-auth.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.qwen-portal-auth.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.qwen-portal-auth.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.qwen-portal-auth.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, { "path": "plugins.entries.sglang", "kind": "plugin", @@ -49727,6 +52602,58 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.sglang.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.sglang.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.sglang.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.sglang.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, { "path": "plugins.entries.signal", "kind": "plugin", @@ -49796,6 +52723,58 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.signal.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.signal.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.signal.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.signal.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, { "path": "plugins.entries.slack", "kind": "plugin", @@ -49865,6 +52844,58 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.slack.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.slack.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.slack.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.slack.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, { "path": "plugins.entries.synology-chat", "kind": "plugin", @@ -49934,6 +52965,58 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.synology-chat.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.synology-chat.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.synology-chat.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.synology-chat.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, { "path": "plugins.entries.synthetic", "kind": "plugin", @@ -50003,6 +53086,58 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.synthetic.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.synthetic.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.synthetic.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.synthetic.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, { "path": "plugins.entries.talk-voice", "kind": "plugin", @@ -50072,6 +53207,58 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.talk-voice.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.talk-voice.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.talk-voice.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.talk-voice.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, { "path": "plugins.entries.telegram", "kind": "plugin", @@ -50141,6 +53328,58 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.telegram.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.telegram.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.telegram.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.telegram.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, { "path": "plugins.entries.thread-ownership", "kind": "plugin", @@ -50248,6 +53487,58 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.thread-ownership.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.thread-ownership.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.thread-ownership.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.thread-ownership.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, { "path": "plugins.entries.tlon", "kind": "plugin", @@ -50317,6 +53608,58 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.tlon.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.tlon.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.tlon.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.tlon.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, { "path": "plugins.entries.together", "kind": "plugin", @@ -50386,6 +53729,58 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.together.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.together.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.together.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.together.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, { "path": "plugins.entries.twitch", "kind": "plugin", @@ -50455,6 +53850,58 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.twitch.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.twitch.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.twitch.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.twitch.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, { "path": "plugins.entries.venice", "kind": "plugin", @@ -50524,6 +53971,58 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.venice.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.venice.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.venice.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.venice.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, { "path": "plugins.entries.vercel-ai-gateway", "kind": "plugin", @@ -50593,6 +54092,58 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.vercel-ai-gateway.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.vercel-ai-gateway.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.vercel-ai-gateway.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.vercel-ai-gateway.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, { "path": "plugins.entries.vllm", "kind": "plugin", @@ -50662,6 +54213,58 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.vllm.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.vllm.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.vllm.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.vllm.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, { "path": "plugins.entries.voice-call", "kind": "plugin", @@ -52087,6 +55690,58 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.voice-call.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.voice-call.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.voice-call.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.voice-call.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, { "path": "plugins.entries.volcengine", "kind": "plugin", @@ -52156,6 +55811,58 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.volcengine.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.volcengine.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.volcengine.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.volcengine.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, { "path": "plugins.entries.whatsapp", "kind": "plugin", @@ -52225,6 +55932,58 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.whatsapp.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.whatsapp.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.whatsapp.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.whatsapp.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, { "path": "plugins.entries.xai", "kind": "plugin", @@ -52294,6 +56053,58 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.xai.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.xai.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.xai.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.xai.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, { "path": "plugins.entries.xiaomi", "kind": "plugin", @@ -52363,6 +56174,58 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.xiaomi.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.xiaomi.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.xiaomi.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.xiaomi.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, { "path": "plugins.entries.zai", "kind": "plugin", @@ -52432,6 +56295,58 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.zai.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.zai.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.zai.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.zai.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, { "path": "plugins.entries.zalo", "kind": "plugin", @@ -52501,6 +56416,58 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.zalo.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.zalo.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.zalo.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.zalo.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, { "path": "plugins.entries.zalouser", "kind": "plugin", @@ -52570,6 +56537,58 @@ "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", "hasChildren": false }, + { + "path": "plugins.entries.zalouser.subagent", + "kind": "plugin", + "type": "object", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "advanced" + ], + "label": "Plugin Subagent Policy", + "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", + "hasChildren": true + }, + { + "path": "plugins.entries.zalouser.subagent.allowedModels", + "kind": "plugin", + "type": "array", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Plugin Subagent Allowed Models", + "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", + "hasChildren": true + }, + { + "path": "plugins.entries.zalouser.subagent.allowedModels.*", + "kind": "plugin", + "type": "string", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [], + "hasChildren": false + }, + { + "path": "plugins.entries.zalouser.subagent.allowModelOverride", + "kind": "plugin", + "type": "boolean", + "required": false, + "deprecated": false, + "sensitive": false, + "tags": [ + "access" + ], + "label": "Allow Plugin Subagent Model Override", + "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", + "hasChildren": false + }, { "path": "plugins.installs", "kind": "core", diff --git a/docs/.generated/config-baseline.jsonl b/docs/.generated/config-baseline.jsonl index caf0e22623c..fb570a6e18a 100644 --- a/docs/.generated/config-baseline.jsonl +++ b/docs/.generated/config-baseline.jsonl @@ -1,4 +1,4 @@ -{"generatedBy":"scripts/generate-config-doc-baseline.ts","recordType":"meta","totalPaths":5165} +{"generatedBy":"scripts/generate-config-doc-baseline.ts","recordType":"meta","totalPaths":5476} {"recordType":"path","path":"acp","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"ACP","help":"ACP runtime controls for enabling dispatch, selecting backends, constraining allowed agent targets, and tuning streamed turn projection behavior.","hasChildren":true} {"recordType":"path","path":"acp.allowedAgents","kind":"core","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"ACP Allowed Agents","help":"Allowlist of ACP target agent ids permitted for ACP runtime sessions. Empty means no additional allowlist restriction.","hasChildren":true} {"recordType":"path","path":"acp.allowedAgents.*","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} @@ -2086,6 +2086,11 @@ {"recordType":"path","path":"channels.mattermost.accounts.*.commands.nativeSkills","kind":"channel","type":["boolean","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.mattermost.accounts.*.configWrites","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.mattermost.accounts.*.dangerouslyAllowNameMatching","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"channels.mattermost.accounts.*.dmChannelRetry","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true} +{"recordType":"path","path":"channels.mattermost.accounts.*.dmChannelRetry.initialDelayMs","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"channels.mattermost.accounts.*.dmChannelRetry.maxDelayMs","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"channels.mattermost.accounts.*.dmChannelRetry.maxRetries","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"channels.mattermost.accounts.*.dmChannelRetry.timeoutMs","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.mattermost.accounts.*.dmPolicy","kind":"channel","type":"string","required":true,"enumValues":["pairing","allowlist","open","disabled"],"defaultValue":"pairing","deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.mattermost.accounts.*.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.mattermost.accounts.*.groupAllowFrom","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true} @@ -2130,6 +2135,11 @@ {"recordType":"path","path":"channels.mattermost.configWrites","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"Mattermost Config Writes","help":"Allow Mattermost to write config in response to channel events/commands (default: true).","hasChildren":false} {"recordType":"path","path":"channels.mattermost.dangerouslyAllowNameMatching","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.mattermost.defaultAccount","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"channels.mattermost.dmChannelRetry","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true} +{"recordType":"path","path":"channels.mattermost.dmChannelRetry.initialDelayMs","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"channels.mattermost.dmChannelRetry.maxDelayMs","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"channels.mattermost.dmChannelRetry.maxRetries","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"channels.mattermost.dmChannelRetry.timeoutMs","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.mattermost.dmPolicy","kind":"channel","type":"string","required":true,"enumValues":["pairing","allowlist","open","disabled"],"defaultValue":"pairing","deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.mattermost.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.mattermost.groupAllowFrom","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true} @@ -3398,7 +3408,7 @@ {"recordType":"path","path":"channels.zalouser.accounts.*.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.zalouser.accounts.*.groupAllowFrom","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true} {"recordType":"path","path":"channels.zalouser.accounts.*.groupAllowFrom.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} -{"recordType":"path","path":"channels.zalouser.accounts.*.groupPolicy","kind":"channel","type":"string","required":false,"enumValues":["open","disabled","allowlist"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"channels.zalouser.accounts.*.groupPolicy","kind":"channel","type":"string","required":true,"enumValues":["open","disabled","allowlist"],"defaultValue":"allowlist","deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.zalouser.accounts.*.groups","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true} {"recordType":"path","path":"channels.zalouser.accounts.*.groups.*","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true} {"recordType":"path","path":"channels.zalouser.accounts.*.groups.*.allow","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} @@ -3426,7 +3436,7 @@ {"recordType":"path","path":"channels.zalouser.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.zalouser.groupAllowFrom","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true} {"recordType":"path","path":"channels.zalouser.groupAllowFrom.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} -{"recordType":"path","path":"channels.zalouser.groupPolicy","kind":"channel","type":"string","required":false,"enumValues":["open","disabled","allowlist"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"channels.zalouser.groupPolicy","kind":"channel","type":"string","required":true,"enumValues":["open","disabled","allowlist"],"defaultValue":"allowlist","deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.zalouser.groups","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true} {"recordType":"path","path":"channels.zalouser.groups.*","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true} {"recordType":"path","path":"channels.zalouser.groups.*.allow","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} @@ -3563,7 +3573,7 @@ {"recordType":"path","path":"gateway.channelMaxRestartsPerHour","kind":"core","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":["network","performance"],"label":"Gateway Channel Max Restarts Per Hour","help":"Maximum number of health-monitor-initiated channel restarts allowed within a rolling one-hour window. Once hit, further restarts are skipped until the window expires. Default: 10.","hasChildren":false} {"recordType":"path","path":"gateway.channelStaleEventThresholdMinutes","kind":"core","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":["network"],"label":"Gateway Channel Stale Event Threshold (min)","help":"How many minutes a connected channel can go without receiving any event before the health monitor treats it as a stale socket and triggers a restart. Default: 30.","hasChildren":false} {"recordType":"path","path":"gateway.controlUi","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["network"],"label":"Control UI","help":"Control UI hosting settings including enablement, pathing, and browser-origin/auth hardening behavior. Keep UI exposure minimal and pair with strong auth controls before internet-facing deployments.","hasChildren":true} -{"recordType":"path","path":"gateway.controlUi.allowedOrigins","kind":"core","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access","network"],"label":"Control UI Allowed Origins","help":"Allowed browser origins for Control UI/WebChat websocket connections (full origins only, e.g. https://control.example.com). Required for non-loopback Control UI deployments unless dangerous Host-header fallback is explicitly enabled.","hasChildren":true} +{"recordType":"path","path":"gateway.controlUi.allowedOrigins","kind":"core","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access","network"],"label":"Control UI Allowed Origins","help":"Allowed browser origins for Control UI/WebChat websocket connections (full origins only, e.g. https://control.example.com). Required for non-loopback Control UI deployments unless dangerous Host-header fallback is explicitly enabled. Setting [\"*\"] means allow any browser origin and should be avoided outside tightly controlled local testing.","hasChildren":true} {"recordType":"path","path":"gateway.controlUi.allowedOrigins.*","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"gateway.controlUi.allowInsecureAuth","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access","advanced","network","security"],"label":"Insecure Control UI Auth Toggle","help":"Loosens strict browser auth checks for Control UI when you must run a non-standard setup. Keep this off unless you trust your network and proxy path, because impersonation risk is higher.","hasChildren":false} {"recordType":"path","path":"gateway.controlUi.basePath","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["network","storage"],"label":"Control UI Base Path","help":"Optional URL prefix where the Control UI is served (e.g. /openclaw).","hasChildren":false} @@ -3667,7 +3677,7 @@ {"recordType":"path","path":"gateway.trustedProxies","kind":"core","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["network"],"label":"Gateway Trusted Proxy CIDRs","help":"CIDR/IP allowlist of upstream proxies permitted to provide forwarded client identity headers. Keep this list narrow so untrusted hops cannot impersonate users.","hasChildren":true} {"recordType":"path","path":"gateway.trustedProxies.*","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"hooks","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Hooks","help":"Inbound webhook automation surface for mapping external events into wake or agent actions in OpenClaw. Keep this locked down with explicit token/session/agent controls before exposing it beyond trusted networks.","hasChildren":true} -{"recordType":"path","path":"hooks.allowedAgentIds","kind":"core","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Hooks Allowed Agent IDs","help":"Allowlist of agent IDs that hook mappings are allowed to target when selecting execution agents. Use this to constrain automation events to dedicated service agents.","hasChildren":true} +{"recordType":"path","path":"hooks.allowedAgentIds","kind":"core","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Hooks Allowed Agent IDs","help":"Allowlist of agent IDs that hook mappings are allowed to target when selecting execution agents. Use this to constrain automation events to dedicated service agents and reduce blast radius if a hook token is exposed.","hasChildren":true} {"recordType":"path","path":"hooks.allowedAgentIds.*","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"hooks.allowedSessionKeyPrefixes","kind":"core","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access","storage"],"label":"Hooks Allowed Session Key Prefixes","help":"Allowlist of accepted session-key prefixes for inbound hook requests when caller-provided keys are enabled. Use narrow prefixes to prevent arbitrary session-key injection.","hasChildren":true} {"recordType":"path","path":"hooks.allowedSessionKeyPrefixes.*","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} @@ -3754,7 +3764,7 @@ {"recordType":"path","path":"hooks.path","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["storage"],"label":"Hooks Endpoint Path","help":"HTTP path used by the hooks endpoint (for example `/hooks`) on the gateway control server. Use a non-guessable path and combine it with token validation for defense in depth.","hasChildren":false} {"recordType":"path","path":"hooks.presets","kind":"core","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Hooks Presets","help":"Named hook preset bundles applied at load time to seed standard mappings and behavior defaults. Keep preset usage explicit so operators can audit which automations are active.","hasChildren":true} {"recordType":"path","path":"hooks.presets.*","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} -{"recordType":"path","path":"hooks.token","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":true,"tags":["auth","security"],"label":"Hooks Auth Token","help":"Shared bearer token checked by hooks ingress for request authentication before mappings run. Use environment substitution and rotate regularly when webhook endpoints are internet-accessible.","hasChildren":false} +{"recordType":"path","path":"hooks.token","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":true,"tags":["auth","security"],"label":"Hooks Auth Token","help":"Shared bearer token checked by hooks ingress for request authentication before mappings run. Treat holders as full-trust callers for the hook ingress surface, not as a separate non-owner role. Use environment substitution and rotate regularly when webhook endpoints are internet-accessible.","hasChildren":false} {"recordType":"path","path":"hooks.transformsDir","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["storage"],"label":"Hooks Transforms Directory","help":"Base directory for hook transform modules referenced by mapping transform.module paths. Use a controlled repo directory so dynamic imports remain reviewable and predictable.","hasChildren":false} {"recordType":"path","path":"logging","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Logging","help":"Logging behavior controls for severity, output destinations, formatting, and sensitive-data redaction. Keep levels and redaction strict enough for production while preserving useful diagnostics.","hasChildren":true} {"recordType":"path","path":"logging.consoleLevel","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["observability"],"label":"Console Log Level","help":"Console-specific log threshold: \"silent\", \"fatal\", \"error\", \"warn\", \"info\", \"debug\", or \"trace\" for terminal output control. Use this to keep local console quieter while retaining richer file logging if needed.","hasChildren":false} @@ -4020,6 +4030,10 @@ {"recordType":"path","path":"plugins.entries.*.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Enabled","help":"Per-plugin enablement override for a specific entry, applied on top of global plugin policy (restart required). Use this to stage plugin rollout gradually across environments.","hasChildren":false} {"recordType":"path","path":"plugins.entries.*.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.*.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.*.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.*.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.*.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.*.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.entries.acpx","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"ACPX Runtime","help":"ACP runtime backend powered by acpx with configurable command path and version policy. (plugin: acpx)","hasChildren":true} {"recordType":"path","path":"plugins.entries.acpx.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"ACPX Runtime Config","help":"Plugin-defined config payload for acpx.","hasChildren":true} {"recordType":"path","path":"plugins.entries.acpx.config.command","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"acpx Command","help":"Optional path/command override for acpx (for example /home/user/repos/acpx/dist/cli.js). Leave unset to use plugin-local bundled acpx.","hasChildren":false} @@ -4040,52 +4054,101 @@ {"recordType":"path","path":"plugins.entries.acpx.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable ACPX Runtime","hasChildren":false} {"recordType":"path","path":"plugins.entries.acpx.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.acpx.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.acpx.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.acpx.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.acpx.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.acpx.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.entries.amazon-bedrock","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/amazon-bedrock-provider","help":"OpenClaw Amazon Bedrock provider plugin (plugin: amazon-bedrock)","hasChildren":true} {"recordType":"path","path":"plugins.entries.amazon-bedrock.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/amazon-bedrock-provider Config","help":"Plugin-defined config payload for amazon-bedrock.","hasChildren":false} {"recordType":"path","path":"plugins.entries.amazon-bedrock.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/amazon-bedrock-provider","hasChildren":false} {"recordType":"path","path":"plugins.entries.amazon-bedrock.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.amazon-bedrock.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.amazon-bedrock.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.amazon-bedrock.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.amazon-bedrock.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.amazon-bedrock.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.entries.anthropic","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/anthropic-provider","help":"OpenClaw Anthropic provider plugin (plugin: anthropic)","hasChildren":true} {"recordType":"path","path":"plugins.entries.anthropic.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/anthropic-provider Config","help":"Plugin-defined config payload for anthropic.","hasChildren":false} {"recordType":"path","path":"plugins.entries.anthropic.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/anthropic-provider","hasChildren":false} {"recordType":"path","path":"plugins.entries.anthropic.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.anthropic.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.anthropic.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.anthropic.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.anthropic.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.anthropic.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.entries.bluebubbles","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/bluebubbles","help":"OpenClaw BlueBubbles channel plugin (plugin: bluebubbles)","hasChildren":true} {"recordType":"path","path":"plugins.entries.bluebubbles.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/bluebubbles Config","help":"Plugin-defined config payload for bluebubbles.","hasChildren":false} {"recordType":"path","path":"plugins.entries.bluebubbles.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/bluebubbles","hasChildren":false} {"recordType":"path","path":"plugins.entries.bluebubbles.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.bluebubbles.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.bluebubbles.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.bluebubbles.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.bluebubbles.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.bluebubbles.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.entries.brave","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/brave-plugin","help":"OpenClaw Brave plugin (plugin: brave)","hasChildren":true} {"recordType":"path","path":"plugins.entries.brave.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/brave-plugin Config","help":"Plugin-defined config payload for brave.","hasChildren":false} {"recordType":"path","path":"plugins.entries.brave.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/brave-plugin","hasChildren":false} {"recordType":"path","path":"plugins.entries.brave.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.brave.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.brave.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.brave.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.brave.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.brave.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.entries.byteplus","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/byteplus-provider","help":"OpenClaw BytePlus provider plugin (plugin: byteplus)","hasChildren":true} {"recordType":"path","path":"plugins.entries.byteplus.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/byteplus-provider Config","help":"Plugin-defined config payload for byteplus.","hasChildren":false} {"recordType":"path","path":"plugins.entries.byteplus.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/byteplus-provider","hasChildren":false} {"recordType":"path","path":"plugins.entries.byteplus.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.byteplus.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.byteplus.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.byteplus.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.byteplus.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.byteplus.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.chutes","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/chutes-provider","help":"OpenClaw Chutes.ai provider plugin (plugin: chutes)","hasChildren":true} +{"recordType":"path","path":"plugins.entries.chutes.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/chutes-provider Config","help":"Plugin-defined config payload for chutes.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.chutes.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/chutes-provider","hasChildren":false} +{"recordType":"path","path":"plugins.entries.chutes.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.chutes.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.chutes.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.chutes.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.chutes.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.chutes.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.entries.cloudflare-ai-gateway","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/cloudflare-ai-gateway-provider","help":"OpenClaw Cloudflare AI Gateway provider plugin (plugin: cloudflare-ai-gateway)","hasChildren":true} {"recordType":"path","path":"plugins.entries.cloudflare-ai-gateway.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/cloudflare-ai-gateway-provider Config","help":"Plugin-defined config payload for cloudflare-ai-gateway.","hasChildren":false} {"recordType":"path","path":"plugins.entries.cloudflare-ai-gateway.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/cloudflare-ai-gateway-provider","hasChildren":false} {"recordType":"path","path":"plugins.entries.cloudflare-ai-gateway.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.cloudflare-ai-gateway.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.cloudflare-ai-gateway.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.cloudflare-ai-gateway.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.cloudflare-ai-gateway.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.cloudflare-ai-gateway.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.entries.copilot-proxy","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/copilot-proxy","help":"OpenClaw Copilot Proxy provider plugin (plugin: copilot-proxy)","hasChildren":true} {"recordType":"path","path":"plugins.entries.copilot-proxy.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/copilot-proxy Config","help":"Plugin-defined config payload for copilot-proxy.","hasChildren":false} {"recordType":"path","path":"plugins.entries.copilot-proxy.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/copilot-proxy","hasChildren":false} {"recordType":"path","path":"plugins.entries.copilot-proxy.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.copilot-proxy.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.copilot-proxy.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.copilot-proxy.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.copilot-proxy.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.copilot-proxy.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.entries.device-pair","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Device Pairing","help":"Generate setup codes and approve device pairing requests. (plugin: device-pair)","hasChildren":true} {"recordType":"path","path":"plugins.entries.device-pair.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Device Pairing Config","help":"Plugin-defined config payload for device-pair.","hasChildren":true} {"recordType":"path","path":"plugins.entries.device-pair.config.publicUrl","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Gateway URL","help":"Public WebSocket URL used for /pair setup codes (ws/wss or http/https).","hasChildren":false} {"recordType":"path","path":"plugins.entries.device-pair.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable Device Pairing","hasChildren":false} {"recordType":"path","path":"plugins.entries.device-pair.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.device-pair.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.device-pair.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.device-pair.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.device-pair.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.device-pair.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.entries.diagnostics-otel","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["observability"],"label":"@openclaw/diagnostics-otel","help":"OpenClaw diagnostics OpenTelemetry exporter (plugin: diagnostics-otel)","hasChildren":true} {"recordType":"path","path":"plugins.entries.diagnostics-otel.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["observability"],"label":"@openclaw/diagnostics-otel Config","help":"Plugin-defined config payload for diagnostics-otel.","hasChildren":false} {"recordType":"path","path":"plugins.entries.diagnostics-otel.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["observability"],"label":"Enable @openclaw/diagnostics-otel","hasChildren":false} {"recordType":"path","path":"plugins.entries.diagnostics-otel.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.diagnostics-otel.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.diagnostics-otel.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.diagnostics-otel.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.diagnostics-otel.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.diagnostics-otel.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.entries.diffs","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Diffs","help":"Read-only diff viewer and file renderer for agents. (plugin: diffs)","hasChildren":true} {"recordType":"path","path":"plugins.entries.diffs.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Diffs Config","help":"Plugin-defined config payload for diffs.","hasChildren":true} {"recordType":"path","path":"plugins.entries.diffs.config.defaults","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true} @@ -4113,71 +4176,127 @@ {"recordType":"path","path":"plugins.entries.diffs.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable Diffs","hasChildren":false} {"recordType":"path","path":"plugins.entries.diffs.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.diffs.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.diffs.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.diffs.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.diffs.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.diffs.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.entries.discord","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/discord","help":"OpenClaw Discord channel plugin (plugin: discord)","hasChildren":true} {"recordType":"path","path":"plugins.entries.discord.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/discord Config","help":"Plugin-defined config payload for discord.","hasChildren":false} {"recordType":"path","path":"plugins.entries.discord.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/discord","hasChildren":false} {"recordType":"path","path":"plugins.entries.discord.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.discord.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.discord.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.discord.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.discord.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.discord.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.entries.elevenlabs","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/elevenlabs-speech","help":"OpenClaw ElevenLabs speech plugin (plugin: elevenlabs)","hasChildren":true} {"recordType":"path","path":"plugins.entries.elevenlabs.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/elevenlabs-speech Config","help":"Plugin-defined config payload for elevenlabs.","hasChildren":false} {"recordType":"path","path":"plugins.entries.elevenlabs.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/elevenlabs-speech","hasChildren":false} {"recordType":"path","path":"plugins.entries.elevenlabs.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.elevenlabs.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.elevenlabs.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.elevenlabs.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.elevenlabs.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.elevenlabs.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.entries.feishu","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/feishu","help":"OpenClaw Feishu/Lark channel plugin (community maintained by @m1heng) (plugin: feishu)","hasChildren":true} {"recordType":"path","path":"plugins.entries.feishu.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/feishu Config","help":"Plugin-defined config payload for feishu.","hasChildren":false} {"recordType":"path","path":"plugins.entries.feishu.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/feishu","hasChildren":false} {"recordType":"path","path":"plugins.entries.feishu.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.feishu.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.feishu.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.feishu.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.feishu.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.feishu.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.entries.firecrawl","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/firecrawl-plugin","help":"OpenClaw Firecrawl plugin (plugin: firecrawl)","hasChildren":true} {"recordType":"path","path":"plugins.entries.firecrawl.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/firecrawl-plugin Config","help":"Plugin-defined config payload for firecrawl.","hasChildren":false} {"recordType":"path","path":"plugins.entries.firecrawl.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/firecrawl-plugin","hasChildren":false} {"recordType":"path","path":"plugins.entries.firecrawl.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.firecrawl.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.firecrawl.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.firecrawl.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.firecrawl.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.firecrawl.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.entries.github-copilot","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/github-copilot-provider","help":"OpenClaw GitHub Copilot provider plugin (plugin: github-copilot)","hasChildren":true} {"recordType":"path","path":"plugins.entries.github-copilot.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/github-copilot-provider Config","help":"Plugin-defined config payload for github-copilot.","hasChildren":false} {"recordType":"path","path":"plugins.entries.github-copilot.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/github-copilot-provider","hasChildren":false} {"recordType":"path","path":"plugins.entries.github-copilot.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.github-copilot.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.github-copilot.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.github-copilot.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.github-copilot.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.github-copilot.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.entries.google","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/google-plugin","help":"OpenClaw Google plugin (plugin: google)","hasChildren":true} {"recordType":"path","path":"plugins.entries.google.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/google-plugin Config","help":"Plugin-defined config payload for google.","hasChildren":false} {"recordType":"path","path":"plugins.entries.google.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/google-plugin","hasChildren":false} {"recordType":"path","path":"plugins.entries.google.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.google.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.google.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.google.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.google.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.google.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.entries.googlechat","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/googlechat","help":"OpenClaw Google Chat channel plugin (plugin: googlechat)","hasChildren":true} {"recordType":"path","path":"plugins.entries.googlechat.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/googlechat Config","help":"Plugin-defined config payload for googlechat.","hasChildren":false} {"recordType":"path","path":"plugins.entries.googlechat.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/googlechat","hasChildren":false} {"recordType":"path","path":"plugins.entries.googlechat.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.googlechat.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.googlechat.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.googlechat.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.googlechat.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.googlechat.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.entries.huggingface","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/huggingface-provider","help":"OpenClaw Hugging Face provider plugin (plugin: huggingface)","hasChildren":true} {"recordType":"path","path":"plugins.entries.huggingface.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/huggingface-provider Config","help":"Plugin-defined config payload for huggingface.","hasChildren":false} {"recordType":"path","path":"plugins.entries.huggingface.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/huggingface-provider","hasChildren":false} {"recordType":"path","path":"plugins.entries.huggingface.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.huggingface.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.huggingface.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.huggingface.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.huggingface.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.huggingface.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.entries.imessage","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/imessage","help":"OpenClaw iMessage channel plugin (plugin: imessage)","hasChildren":true} {"recordType":"path","path":"plugins.entries.imessage.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/imessage Config","help":"Plugin-defined config payload for imessage.","hasChildren":false} {"recordType":"path","path":"plugins.entries.imessage.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/imessage","hasChildren":false} {"recordType":"path","path":"plugins.entries.imessage.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.imessage.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.imessage.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.imessage.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.imessage.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.imessage.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.entries.irc","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/irc","help":"OpenClaw IRC channel plugin (plugin: irc)","hasChildren":true} {"recordType":"path","path":"plugins.entries.irc.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/irc Config","help":"Plugin-defined config payload for irc.","hasChildren":false} {"recordType":"path","path":"plugins.entries.irc.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/irc","hasChildren":false} {"recordType":"path","path":"plugins.entries.irc.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.irc.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.irc.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.irc.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.irc.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.irc.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.entries.kilocode","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/kilocode-provider","help":"OpenClaw Kilo Gateway provider plugin (plugin: kilocode)","hasChildren":true} {"recordType":"path","path":"plugins.entries.kilocode.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/kilocode-provider Config","help":"Plugin-defined config payload for kilocode.","hasChildren":false} {"recordType":"path","path":"plugins.entries.kilocode.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/kilocode-provider","hasChildren":false} {"recordType":"path","path":"plugins.entries.kilocode.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.kilocode.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.kilocode.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.kilocode.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.kilocode.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.kilocode.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.entries.kimi","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/kimi-provider","help":"OpenClaw Kimi provider plugin (plugin: kimi)","hasChildren":true} {"recordType":"path","path":"plugins.entries.kimi.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/kimi-provider Config","help":"Plugin-defined config payload for kimi.","hasChildren":false} {"recordType":"path","path":"plugins.entries.kimi.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/kimi-provider","hasChildren":false} {"recordType":"path","path":"plugins.entries.kimi.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.kimi.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.kimi.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.kimi.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.kimi.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.kimi.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.entries.line","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/line","help":"OpenClaw LINE channel plugin (plugin: line)","hasChildren":true} {"recordType":"path","path":"plugins.entries.line.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/line Config","help":"Plugin-defined config payload for line.","hasChildren":false} {"recordType":"path","path":"plugins.entries.line.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/line","hasChildren":false} {"recordType":"path","path":"plugins.entries.line.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.line.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.line.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.line.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.line.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.line.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.entries.llm-task","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"LLM Task","help":"Generic JSON-only LLM tool for structured tasks callable from workflows. (plugin: llm-task)","hasChildren":true} {"recordType":"path","path":"plugins.entries.llm-task.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"LLM Task Config","help":"Plugin-defined config payload for llm-task.","hasChildren":true} {"recordType":"path","path":"plugins.entries.llm-task.config.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true} @@ -4190,26 +4309,46 @@ {"recordType":"path","path":"plugins.entries.llm-task.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable LLM Task","hasChildren":false} {"recordType":"path","path":"plugins.entries.llm-task.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.llm-task.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.llm-task.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.llm-task.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.llm-task.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.llm-task.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.entries.lobster","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Lobster","help":"Typed workflow tool with resumable approvals. (plugin: lobster)","hasChildren":true} {"recordType":"path","path":"plugins.entries.lobster.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Lobster Config","help":"Plugin-defined config payload for lobster.","hasChildren":false} {"recordType":"path","path":"plugins.entries.lobster.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable Lobster","hasChildren":false} {"recordType":"path","path":"plugins.entries.lobster.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.lobster.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.lobster.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.lobster.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.lobster.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.lobster.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.entries.matrix","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/matrix","help":"OpenClaw Matrix channel plugin (plugin: matrix)","hasChildren":true} {"recordType":"path","path":"plugins.entries.matrix.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/matrix Config","help":"Plugin-defined config payload for matrix.","hasChildren":false} {"recordType":"path","path":"plugins.entries.matrix.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/matrix","hasChildren":false} {"recordType":"path","path":"plugins.entries.matrix.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.matrix.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.matrix.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.matrix.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.matrix.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.matrix.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.entries.mattermost","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/mattermost","help":"OpenClaw Mattermost channel plugin (plugin: mattermost)","hasChildren":true} {"recordType":"path","path":"plugins.entries.mattermost.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/mattermost Config","help":"Plugin-defined config payload for mattermost.","hasChildren":false} {"recordType":"path","path":"plugins.entries.mattermost.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/mattermost","hasChildren":false} {"recordType":"path","path":"plugins.entries.mattermost.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.mattermost.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.mattermost.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.mattermost.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.mattermost.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.mattermost.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.entries.memory-core","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/memory-core","help":"OpenClaw core memory search plugin (plugin: memory-core)","hasChildren":true} {"recordType":"path","path":"plugins.entries.memory-core.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/memory-core Config","help":"Plugin-defined config payload for memory-core.","hasChildren":false} {"recordType":"path","path":"plugins.entries.memory-core.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/memory-core","hasChildren":false} {"recordType":"path","path":"plugins.entries.memory-core.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.memory-core.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.memory-core.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.memory-core.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.memory-core.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.memory-core.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.entries.memory-lancedb","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["storage"],"label":"@openclaw/memory-lancedb","help":"OpenClaw LanceDB-backed long-term memory plugin with auto-recall/capture (plugin: memory-lancedb)","hasChildren":true} {"recordType":"path","path":"plugins.entries.memory-lancedb.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["storage"],"label":"@openclaw/memory-lancedb Config","help":"Plugin-defined config payload for memory-lancedb.","hasChildren":true} {"recordType":"path","path":"plugins.entries.memory-lancedb.config.autoCapture","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["storage"],"label":"Auto-Capture","help":"Automatically capture important information from conversations","hasChildren":false} @@ -4224,81 +4363,145 @@ {"recordType":"path","path":"plugins.entries.memory-lancedb.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["storage"],"label":"Enable @openclaw/memory-lancedb","hasChildren":false} {"recordType":"path","path":"plugins.entries.memory-lancedb.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.memory-lancedb.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.memory-lancedb.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.memory-lancedb.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.memory-lancedb.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.memory-lancedb.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.entries.microsoft","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/microsoft-speech","help":"OpenClaw Microsoft speech plugin (plugin: microsoft)","hasChildren":true} {"recordType":"path","path":"plugins.entries.microsoft.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/microsoft-speech Config","help":"Plugin-defined config payload for microsoft.","hasChildren":false} {"recordType":"path","path":"plugins.entries.microsoft.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/microsoft-speech","hasChildren":false} {"recordType":"path","path":"plugins.entries.microsoft.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.microsoft.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.microsoft.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.microsoft.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.microsoft.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.microsoft.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.entries.minimax","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["performance"],"label":"@openclaw/minimax-provider","help":"OpenClaw MiniMax provider and OAuth plugin (plugin: minimax)","hasChildren":true} {"recordType":"path","path":"plugins.entries.minimax.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["performance"],"label":"@openclaw/minimax-provider Config","help":"Plugin-defined config payload for minimax.","hasChildren":false} {"recordType":"path","path":"plugins.entries.minimax.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["performance"],"label":"Enable @openclaw/minimax-provider","hasChildren":false} {"recordType":"path","path":"plugins.entries.minimax.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.minimax.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.minimax.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.minimax.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.minimax.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.minimax.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.entries.mistral","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/mistral-provider","help":"OpenClaw Mistral provider plugin (plugin: mistral)","hasChildren":true} {"recordType":"path","path":"plugins.entries.mistral.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/mistral-provider Config","help":"Plugin-defined config payload for mistral.","hasChildren":false} {"recordType":"path","path":"plugins.entries.mistral.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/mistral-provider","hasChildren":false} {"recordType":"path","path":"plugins.entries.mistral.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.mistral.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.mistral.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.mistral.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.mistral.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.mistral.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.entries.modelstudio","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/modelstudio-provider","help":"OpenClaw Model Studio provider plugin (plugin: modelstudio)","hasChildren":true} {"recordType":"path","path":"plugins.entries.modelstudio.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/modelstudio-provider Config","help":"Plugin-defined config payload for modelstudio.","hasChildren":false} {"recordType":"path","path":"plugins.entries.modelstudio.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/modelstudio-provider","hasChildren":false} {"recordType":"path","path":"plugins.entries.modelstudio.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.modelstudio.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.modelstudio.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.modelstudio.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.modelstudio.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.modelstudio.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.entries.moonshot","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/moonshot-provider","help":"OpenClaw Moonshot provider plugin (plugin: moonshot)","hasChildren":true} {"recordType":"path","path":"plugins.entries.moonshot.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/moonshot-provider Config","help":"Plugin-defined config payload for moonshot.","hasChildren":false} {"recordType":"path","path":"plugins.entries.moonshot.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/moonshot-provider","hasChildren":false} {"recordType":"path","path":"plugins.entries.moonshot.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.moonshot.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.moonshot.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.moonshot.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.moonshot.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.moonshot.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.entries.msteams","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/msteams","help":"OpenClaw Microsoft Teams channel plugin (plugin: msteams)","hasChildren":true} {"recordType":"path","path":"plugins.entries.msteams.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/msteams Config","help":"Plugin-defined config payload for msteams.","hasChildren":false} {"recordType":"path","path":"plugins.entries.msteams.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/msteams","hasChildren":false} {"recordType":"path","path":"plugins.entries.msteams.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.msteams.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.msteams.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.msteams.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.msteams.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.msteams.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.entries.nextcloud-talk","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/nextcloud-talk","help":"OpenClaw Nextcloud Talk channel plugin (plugin: nextcloud-talk)","hasChildren":true} {"recordType":"path","path":"plugins.entries.nextcloud-talk.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/nextcloud-talk Config","help":"Plugin-defined config payload for nextcloud-talk.","hasChildren":false} {"recordType":"path","path":"plugins.entries.nextcloud-talk.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/nextcloud-talk","hasChildren":false} {"recordType":"path","path":"plugins.entries.nextcloud-talk.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.nextcloud-talk.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.nextcloud-talk.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.nextcloud-talk.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.nextcloud-talk.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.nextcloud-talk.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.entries.nostr","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/nostr","help":"OpenClaw Nostr channel plugin for NIP-04 encrypted DMs (plugin: nostr)","hasChildren":true} {"recordType":"path","path":"plugins.entries.nostr.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/nostr Config","help":"Plugin-defined config payload for nostr.","hasChildren":false} {"recordType":"path","path":"plugins.entries.nostr.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/nostr","hasChildren":false} {"recordType":"path","path":"plugins.entries.nostr.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.nostr.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.nostr.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.nostr.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.nostr.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.nostr.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.entries.nvidia","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/nvidia-provider","help":"OpenClaw NVIDIA provider plugin (plugin: nvidia)","hasChildren":true} {"recordType":"path","path":"plugins.entries.nvidia.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/nvidia-provider Config","help":"Plugin-defined config payload for nvidia.","hasChildren":false} {"recordType":"path","path":"plugins.entries.nvidia.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/nvidia-provider","hasChildren":false} {"recordType":"path","path":"plugins.entries.nvidia.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.nvidia.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.nvidia.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.nvidia.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.nvidia.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.nvidia.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.entries.ollama","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/ollama-provider","help":"OpenClaw Ollama provider plugin (plugin: ollama)","hasChildren":true} {"recordType":"path","path":"plugins.entries.ollama.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/ollama-provider Config","help":"Plugin-defined config payload for ollama.","hasChildren":false} {"recordType":"path","path":"plugins.entries.ollama.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/ollama-provider","hasChildren":false} {"recordType":"path","path":"plugins.entries.ollama.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.ollama.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.ollama.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.ollama.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.ollama.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.ollama.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.entries.open-prose","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"OpenProse","help":"OpenProse VM skill pack with a /prose slash command. (plugin: open-prose)","hasChildren":true} {"recordType":"path","path":"plugins.entries.open-prose.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"OpenProse Config","help":"Plugin-defined config payload for open-prose.","hasChildren":false} {"recordType":"path","path":"plugins.entries.open-prose.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable OpenProse","hasChildren":false} {"recordType":"path","path":"plugins.entries.open-prose.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.open-prose.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.open-prose.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.open-prose.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.open-prose.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.open-prose.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.entries.openai","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/openai-provider","help":"OpenClaw OpenAI provider plugins (plugin: openai)","hasChildren":true} {"recordType":"path","path":"plugins.entries.openai.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/openai-provider Config","help":"Plugin-defined config payload for openai.","hasChildren":false} {"recordType":"path","path":"plugins.entries.openai.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/openai-provider","hasChildren":false} {"recordType":"path","path":"plugins.entries.openai.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.openai.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.openai.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.openai.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.openai.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.openai.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.entries.opencode","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/opencode-provider","help":"OpenClaw OpenCode Zen provider plugin (plugin: opencode)","hasChildren":true} {"recordType":"path","path":"plugins.entries.opencode-go","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/opencode-go-provider","help":"OpenClaw OpenCode Go provider plugin (plugin: opencode-go)","hasChildren":true} {"recordType":"path","path":"plugins.entries.opencode-go.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/opencode-go-provider Config","help":"Plugin-defined config payload for opencode-go.","hasChildren":false} {"recordType":"path","path":"plugins.entries.opencode-go.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/opencode-go-provider","hasChildren":false} {"recordType":"path","path":"plugins.entries.opencode-go.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.opencode-go.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.opencode-go.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.opencode-go.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.opencode-go.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.opencode-go.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.entries.opencode.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/opencode-provider Config","help":"Plugin-defined config payload for opencode.","hasChildren":false} {"recordType":"path","path":"plugins.entries.opencode.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/opencode-provider","hasChildren":false} {"recordType":"path","path":"plugins.entries.opencode.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.opencode.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.opencode.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.opencode.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.opencode.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.opencode.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.entries.openrouter","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/openrouter-provider","help":"OpenClaw OpenRouter provider plugin (plugin: openrouter)","hasChildren":true} {"recordType":"path","path":"plugins.entries.openrouter.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/openrouter-provider Config","help":"Plugin-defined config payload for openrouter.","hasChildren":false} {"recordType":"path","path":"plugins.entries.openrouter.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/openrouter-provider","hasChildren":false} {"recordType":"path","path":"plugins.entries.openrouter.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.openrouter.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.openrouter.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.openrouter.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.openrouter.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.openrouter.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.entries.openshell","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"OpenShell Sandbox","help":"Sandbox backend powered by OpenShell with mirrored local workspaces and SSH-based command execution. (plugin: openshell)","hasChildren":true} {"recordType":"path","path":"plugins.entries.openshell.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"OpenShell Sandbox Config","help":"Plugin-defined config payload for openshell.","hasChildren":true} {"recordType":"path","path":"plugins.entries.openshell.config.autoProviders","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Auto-create Providers","help":"When enabled, pass --auto-providers during sandbox create.","hasChildren":false} @@ -4316,61 +4519,109 @@ {"recordType":"path","path":"plugins.entries.openshell.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable OpenShell Sandbox","hasChildren":false} {"recordType":"path","path":"plugins.entries.openshell.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.openshell.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.openshell.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.openshell.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.openshell.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.openshell.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.entries.perplexity","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/perplexity-plugin","help":"OpenClaw Perplexity plugin (plugin: perplexity)","hasChildren":true} {"recordType":"path","path":"plugins.entries.perplexity.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/perplexity-plugin Config","help":"Plugin-defined config payload for perplexity.","hasChildren":false} {"recordType":"path","path":"plugins.entries.perplexity.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/perplexity-plugin","hasChildren":false} {"recordType":"path","path":"plugins.entries.perplexity.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.perplexity.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.perplexity.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.perplexity.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.perplexity.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.perplexity.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.entries.phone-control","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Phone Control","help":"Arm/disarm high-risk phone node commands (camera/screen/writes) with an optional auto-expiry. (plugin: phone-control)","hasChildren":true} {"recordType":"path","path":"plugins.entries.phone-control.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Phone Control Config","help":"Plugin-defined config payload for phone-control.","hasChildren":false} {"recordType":"path","path":"plugins.entries.phone-control.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable Phone Control","hasChildren":false} {"recordType":"path","path":"plugins.entries.phone-control.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.phone-control.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.phone-control.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.phone-control.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.phone-control.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.phone-control.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.entries.qianfan","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/qianfan-provider","help":"OpenClaw Qianfan provider plugin (plugin: qianfan)","hasChildren":true} {"recordType":"path","path":"plugins.entries.qianfan.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/qianfan-provider Config","help":"Plugin-defined config payload for qianfan.","hasChildren":false} {"recordType":"path","path":"plugins.entries.qianfan.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/qianfan-provider","hasChildren":false} {"recordType":"path","path":"plugins.entries.qianfan.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.qianfan.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.qianfan.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.qianfan.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.qianfan.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.qianfan.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.entries.qwen-portal-auth","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"qwen-portal-auth","help":"Plugin entry for qwen-portal-auth.","hasChildren":true} {"recordType":"path","path":"plugins.entries.qwen-portal-auth.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"qwen-portal-auth Config","help":"Plugin-defined config payload for qwen-portal-auth.","hasChildren":false} {"recordType":"path","path":"plugins.entries.qwen-portal-auth.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable qwen-portal-auth","hasChildren":false} {"recordType":"path","path":"plugins.entries.qwen-portal-auth.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.qwen-portal-auth.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.qwen-portal-auth.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.qwen-portal-auth.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.qwen-portal-auth.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.qwen-portal-auth.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.entries.sglang","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/sglang-provider","help":"OpenClaw SGLang provider plugin (plugin: sglang)","hasChildren":true} {"recordType":"path","path":"plugins.entries.sglang.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/sglang-provider Config","help":"Plugin-defined config payload for sglang.","hasChildren":false} {"recordType":"path","path":"plugins.entries.sglang.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/sglang-provider","hasChildren":false} {"recordType":"path","path":"plugins.entries.sglang.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.sglang.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.sglang.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.sglang.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.sglang.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.sglang.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.entries.signal","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/signal","help":"OpenClaw Signal channel plugin (plugin: signal)","hasChildren":true} {"recordType":"path","path":"plugins.entries.signal.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/signal Config","help":"Plugin-defined config payload for signal.","hasChildren":false} {"recordType":"path","path":"plugins.entries.signal.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/signal","hasChildren":false} {"recordType":"path","path":"plugins.entries.signal.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.signal.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.signal.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.signal.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.signal.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.signal.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.entries.slack","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/slack","help":"OpenClaw Slack channel plugin (plugin: slack)","hasChildren":true} {"recordType":"path","path":"plugins.entries.slack.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/slack Config","help":"Plugin-defined config payload for slack.","hasChildren":false} {"recordType":"path","path":"plugins.entries.slack.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/slack","hasChildren":false} {"recordType":"path","path":"plugins.entries.slack.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.slack.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.slack.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.slack.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.slack.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.slack.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.entries.synology-chat","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/synology-chat","help":"Synology Chat channel plugin for OpenClaw (plugin: synology-chat)","hasChildren":true} {"recordType":"path","path":"plugins.entries.synology-chat.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/synology-chat Config","help":"Plugin-defined config payload for synology-chat.","hasChildren":false} {"recordType":"path","path":"plugins.entries.synology-chat.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/synology-chat","hasChildren":false} {"recordType":"path","path":"plugins.entries.synology-chat.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.synology-chat.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.synology-chat.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.synology-chat.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.synology-chat.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.synology-chat.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.entries.synthetic","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/synthetic-provider","help":"OpenClaw Synthetic provider plugin (plugin: synthetic)","hasChildren":true} {"recordType":"path","path":"plugins.entries.synthetic.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/synthetic-provider Config","help":"Plugin-defined config payload for synthetic.","hasChildren":false} {"recordType":"path","path":"plugins.entries.synthetic.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/synthetic-provider","hasChildren":false} {"recordType":"path","path":"plugins.entries.synthetic.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.synthetic.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.synthetic.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.synthetic.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.synthetic.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.synthetic.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.entries.talk-voice","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Talk Voice","help":"Manage Talk voice selection (list/set). (plugin: talk-voice)","hasChildren":true} {"recordType":"path","path":"plugins.entries.talk-voice.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Talk Voice Config","help":"Plugin-defined config payload for talk-voice.","hasChildren":false} {"recordType":"path","path":"plugins.entries.talk-voice.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable Talk Voice","hasChildren":false} {"recordType":"path","path":"plugins.entries.talk-voice.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.talk-voice.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.talk-voice.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.talk-voice.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.talk-voice.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.talk-voice.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.entries.telegram","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/telegram","help":"OpenClaw Telegram channel plugin (plugin: telegram)","hasChildren":true} {"recordType":"path","path":"plugins.entries.telegram.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/telegram Config","help":"Plugin-defined config payload for telegram.","hasChildren":false} {"recordType":"path","path":"plugins.entries.telegram.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/telegram","hasChildren":false} {"recordType":"path","path":"plugins.entries.telegram.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.telegram.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.telegram.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.telegram.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.telegram.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.telegram.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.entries.thread-ownership","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Thread Ownership","help":"Prevents multiple agents from responding in the same Slack thread. Uses HTTP calls to the slack-forwarder ownership API. (plugin: thread-ownership)","hasChildren":true} {"recordType":"path","path":"plugins.entries.thread-ownership.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Thread Ownership Config","help":"Plugin-defined config payload for thread-ownership.","hasChildren":true} {"recordType":"path","path":"plugins.entries.thread-ownership.config.abTestChannels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"A/B Test Channels","help":"Slack channel IDs where thread ownership is enforced","hasChildren":true} @@ -4379,36 +4630,64 @@ {"recordType":"path","path":"plugins.entries.thread-ownership.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Enable Thread Ownership","hasChildren":false} {"recordType":"path","path":"plugins.entries.thread-ownership.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.thread-ownership.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.thread-ownership.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.thread-ownership.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.thread-ownership.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.thread-ownership.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.entries.tlon","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/tlon","help":"OpenClaw Tlon/Urbit channel plugin (plugin: tlon)","hasChildren":true} {"recordType":"path","path":"plugins.entries.tlon.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/tlon Config","help":"Plugin-defined config payload for tlon.","hasChildren":false} {"recordType":"path","path":"plugins.entries.tlon.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/tlon","hasChildren":false} {"recordType":"path","path":"plugins.entries.tlon.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.tlon.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.tlon.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.tlon.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.tlon.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.tlon.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.entries.together","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/together-provider","help":"OpenClaw Together provider plugin (plugin: together)","hasChildren":true} {"recordType":"path","path":"plugins.entries.together.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/together-provider Config","help":"Plugin-defined config payload for together.","hasChildren":false} {"recordType":"path","path":"plugins.entries.together.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/together-provider","hasChildren":false} {"recordType":"path","path":"plugins.entries.together.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.together.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.together.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.together.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.together.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.together.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.entries.twitch","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/twitch","help":"OpenClaw Twitch channel plugin (plugin: twitch)","hasChildren":true} {"recordType":"path","path":"plugins.entries.twitch.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/twitch Config","help":"Plugin-defined config payload for twitch.","hasChildren":false} {"recordType":"path","path":"plugins.entries.twitch.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/twitch","hasChildren":false} {"recordType":"path","path":"plugins.entries.twitch.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.twitch.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.twitch.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.twitch.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.twitch.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.twitch.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.entries.venice","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/venice-provider","help":"OpenClaw Venice provider plugin (plugin: venice)","hasChildren":true} {"recordType":"path","path":"plugins.entries.venice.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/venice-provider Config","help":"Plugin-defined config payload for venice.","hasChildren":false} {"recordType":"path","path":"plugins.entries.venice.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/venice-provider","hasChildren":false} {"recordType":"path","path":"plugins.entries.venice.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.venice.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.venice.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.venice.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.venice.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.venice.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.entries.vercel-ai-gateway","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/vercel-ai-gateway-provider","help":"OpenClaw Vercel AI Gateway provider plugin (plugin: vercel-ai-gateway)","hasChildren":true} {"recordType":"path","path":"plugins.entries.vercel-ai-gateway.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/vercel-ai-gateway-provider Config","help":"Plugin-defined config payload for vercel-ai-gateway.","hasChildren":false} {"recordType":"path","path":"plugins.entries.vercel-ai-gateway.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/vercel-ai-gateway-provider","hasChildren":false} {"recordType":"path","path":"plugins.entries.vercel-ai-gateway.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.vercel-ai-gateway.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.vercel-ai-gateway.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.vercel-ai-gateway.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.vercel-ai-gateway.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.vercel-ai-gateway.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.entries.vllm","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/vllm-provider","help":"OpenClaw vLLM provider plugin (plugin: vllm)","hasChildren":true} {"recordType":"path","path":"plugins.entries.vllm.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/vllm-provider Config","help":"Plugin-defined config payload for vllm.","hasChildren":false} {"recordType":"path","path":"plugins.entries.vllm.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/vllm-provider","hasChildren":false} {"recordType":"path","path":"plugins.entries.vllm.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.vllm.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.vllm.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.vllm.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.vllm.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.vllm.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.entries.voice-call","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/voice-call","help":"OpenClaw voice-call plugin (plugin: voice-call)","hasChildren":true} {"recordType":"path","path":"plugins.entries.voice-call.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/voice-call Config","help":"Plugin-defined config payload for voice-call.","hasChildren":true} {"recordType":"path","path":"plugins.entries.voice-call.config.allowFrom","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Inbound Allowlist","hasChildren":true} @@ -4530,41 +4809,73 @@ {"recordType":"path","path":"plugins.entries.voice-call.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/voice-call","hasChildren":false} {"recordType":"path","path":"plugins.entries.voice-call.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.voice-call.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.voice-call.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.voice-call.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.voice-call.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.voice-call.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.entries.volcengine","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/volcengine-provider","help":"OpenClaw Volcengine provider plugin (plugin: volcengine)","hasChildren":true} {"recordType":"path","path":"plugins.entries.volcengine.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/volcengine-provider Config","help":"Plugin-defined config payload for volcengine.","hasChildren":false} {"recordType":"path","path":"plugins.entries.volcengine.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/volcengine-provider","hasChildren":false} {"recordType":"path","path":"plugins.entries.volcengine.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.volcengine.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.volcengine.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.volcengine.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.volcengine.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.volcengine.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.entries.whatsapp","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/whatsapp","help":"OpenClaw WhatsApp channel plugin (plugin: whatsapp)","hasChildren":true} {"recordType":"path","path":"plugins.entries.whatsapp.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/whatsapp Config","help":"Plugin-defined config payload for whatsapp.","hasChildren":false} {"recordType":"path","path":"plugins.entries.whatsapp.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/whatsapp","hasChildren":false} {"recordType":"path","path":"plugins.entries.whatsapp.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.whatsapp.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.whatsapp.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.whatsapp.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.whatsapp.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.whatsapp.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.entries.xai","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/xai-plugin","help":"OpenClaw xAI plugin (plugin: xai)","hasChildren":true} {"recordType":"path","path":"plugins.entries.xai.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/xai-plugin Config","help":"Plugin-defined config payload for xai.","hasChildren":false} {"recordType":"path","path":"plugins.entries.xai.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/xai-plugin","hasChildren":false} {"recordType":"path","path":"plugins.entries.xai.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.xai.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.xai.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.xai.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.xai.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.xai.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.entries.xiaomi","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/xiaomi-provider","help":"OpenClaw Xiaomi provider plugin (plugin: xiaomi)","hasChildren":true} {"recordType":"path","path":"plugins.entries.xiaomi.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/xiaomi-provider Config","help":"Plugin-defined config payload for xiaomi.","hasChildren":false} {"recordType":"path","path":"plugins.entries.xiaomi.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/xiaomi-provider","hasChildren":false} {"recordType":"path","path":"plugins.entries.xiaomi.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.xiaomi.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.xiaomi.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.xiaomi.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.xiaomi.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.xiaomi.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.entries.zai","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/zai-provider","help":"OpenClaw Z.AI provider plugin (plugin: zai)","hasChildren":true} {"recordType":"path","path":"plugins.entries.zai.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/zai-provider Config","help":"Plugin-defined config payload for zai.","hasChildren":false} {"recordType":"path","path":"plugins.entries.zai.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/zai-provider","hasChildren":false} {"recordType":"path","path":"plugins.entries.zai.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.zai.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.zai.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.zai.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.zai.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.zai.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.entries.zalo","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/zalo","help":"OpenClaw Zalo channel plugin (plugin: zalo)","hasChildren":true} {"recordType":"path","path":"plugins.entries.zalo.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/zalo Config","help":"Plugin-defined config payload for zalo.","hasChildren":false} {"recordType":"path","path":"plugins.entries.zalo.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/zalo","hasChildren":false} {"recordType":"path","path":"plugins.entries.zalo.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.zalo.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.zalo.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.zalo.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.zalo.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.zalo.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.entries.zalouser","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/zalouser","help":"OpenClaw Zalo Personal Account plugin via native zca-js integration (plugin: zalouser)","hasChildren":true} {"recordType":"path","path":"plugins.entries.zalouser.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/zalouser Config","help":"Plugin-defined config payload for zalouser.","hasChildren":false} {"recordType":"path","path":"plugins.entries.zalouser.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/zalouser","hasChildren":false} {"recordType":"path","path":"plugins.entries.zalouser.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} {"recordType":"path","path":"plugins.entries.zalouser.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} +{"recordType":"path","path":"plugins.entries.zalouser.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.zalouser.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} +{"recordType":"path","path":"plugins.entries.zalouser.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"plugins.entries.zalouser.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.installs","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Install Records","help":"CLI-managed install metadata (used by `openclaw plugins update` to locate install sources).","hasChildren":true} {"recordType":"path","path":"plugins.installs.*","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true} {"recordType":"path","path":"plugins.installs.*.installedAt","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Install Time","help":"ISO timestamp of last install/update.","hasChildren":false} diff --git a/docs/auth-credential-semantics.md b/docs/auth-credential-semantics.md index 17adb38f9ae..8c5c643b333 100644 --- a/docs/auth-credential-semantics.md +++ b/docs/auth-credential-semantics.md @@ -1,3 +1,11 @@ +--- +title: "Auth Credential Semantics" +summary: "Canonical credential eligibility and resolution semantics for auth profiles" +read_when: + - Working on auth profile resolution or credential routing + - Debugging model auth failures or profile order +--- + # Auth Credential Semantics This document defines the canonical credential eligibility and resolution semantics used across: diff --git a/docs/automation/webhook.md b/docs/automation/webhook.md index b35ee9d4469..38676a8fdbe 100644 --- a/docs/automation/webhook.md +++ b/docs/automation/webhook.md @@ -38,6 +38,7 @@ Every request must include the hook token. Prefer headers: - `Authorization: Bearer ` (recommended) - `x-openclaw-token: ` - Query-string tokens are rejected (`?token=...` returns `400`). +- Treat `hooks.token` holders as full-trust callers for the hook ingress surface on that gateway. Hook payload content is still untrusted, but this is not a separate non-owner auth boundary. ## Endpoints @@ -205,6 +206,7 @@ curl -X POST http://127.0.0.1:18789/hooks/gmail \ - Keep hook endpoints behind loopback, tailnet, or trusted reverse proxy. - Use a dedicated hook token; do not reuse gateway auth tokens. +- Prefer a dedicated hook agent with strict `tools.profile` and sandboxing so hook ingress has a narrower blast radius. - Repeated auth failures are rate-limited per client address to slow brute-force attempts. - If you use multi-agent routing, set `hooks.allowedAgentIds` to limit explicit `agentId` selection. - Keep `hooks.allowRequestSessionKey=false` unless you require caller-selected sessions. diff --git a/docs/brave-search.md b/docs/brave-search.md index 4a541690431..12cd78c358f 100644 --- a/docs/brave-search.md +++ b/docs/brave-search.md @@ -20,11 +20,21 @@ OpenClaw supports Brave Search API as a `web_search` provider. ```json5 { + plugins: { + entries: { + brave: { + config: { + webSearch: { + apiKey: "BRAVE_API_KEY_HERE", + }, + }, + }, + }, + }, tools: { web: { search: { provider: "brave", - apiKey: "BRAVE_API_KEY_HERE", maxResults: 5, timeoutSeconds: 30, }, @@ -33,6 +43,9 @@ OpenClaw supports Brave Search API as a `web_search` provider. } ``` +Provider-specific Brave search settings now live under `plugins.entries.brave.config.webSearch.*`. +Legacy `tools.web.search.apiKey` still loads through the compatibility shim, but it is no longer the canonical config path. + ## Tool parameters | Parameter | Description | diff --git a/docs/channels/discord.md b/docs/channels/discord.md index 2b2266c4c83..0f7b6ac7074 100644 --- a/docs/channels/discord.md +++ b/docs/channels/discord.md @@ -96,8 +96,10 @@ You will need to create a new application with a bot, add the bot to your server Your Discord bot token is a secret (like a password). Set it on the machine running OpenClaw before messaging your agent. ```bash -openclaw config set channels.discord.token '"YOUR_BOT_TOKEN"' --json -openclaw config set channels.discord.enabled true --json +export DISCORD_BOT_TOKEN="YOUR_BOT_TOKEN" +openclaw config set channels.discord.token --ref-provider default --ref-source env --ref-id DISCORD_BOT_TOKEN --dry-run +openclaw config set channels.discord.token --ref-provider default --ref-source env --ref-id DISCORD_BOT_TOKEN +openclaw config set channels.discord.enabled true --strict-json openclaw gateway ``` @@ -121,7 +123,11 @@ openclaw gateway channels: { discord: { enabled: true, - token: "YOUR_BOT_TOKEN", + token: { + source: "env", + provider: "default", + id: "DISCORD_BOT_TOKEN", + }, }, }, } @@ -133,7 +139,7 @@ openclaw gateway DISCORD_BOT_TOKEN=... ``` - SecretRef values are also supported for `channels.discord.token` (env/file/exec providers). See [Secrets Management](/gateway/secrets). + Plaintext `token` values are supported. SecretRef values are also supported for `channels.discord.token` across env/file/exec providers. See [Secrets Management](/gateway/secrets). diff --git a/docs/channels/mattermost.md b/docs/channels/mattermost.md index 2ceb6c17626..41f6ffa19a0 100644 --- a/docs/channels/mattermost.md +++ b/docs/channels/mattermost.md @@ -191,6 +191,35 @@ OpenClaw resolves them **user-first**: If you need deterministic behavior, always use the explicit prefixes (`user:` / `channel:`). +## DM channel retry + +When OpenClaw sends to a Mattermost DM target and needs to resolve the direct channel first, it +retries transient direct-channel creation failures by default. + +Use `channels.mattermost.dmChannelRetry` to tune that behavior globally for the Mattermost plugin, +or `channels.mattermost.accounts..dmChannelRetry` for one account. + +```json5 +{ + channels: { + mattermost: { + dmChannelRetry: { + maxRetries: 3, + initialDelayMs: 1000, + maxDelayMs: 10000, + timeoutMs: 30000, + }, + }, + }, +} +``` + +Notes: + +- This applies only to DM channel creation (`/api/v4/channels/direct`), not every Mattermost API call. +- Retries apply to transient failures such as rate limits, 5xx responses, and network or timeout errors. +- 4xx client errors other than `429` are treated as permanent and are not retried. + ## Reactions (message tool) - Use `message action=react` with `channel=mattermost`. diff --git a/docs/channels/twitch.md b/docs/channels/twitch.md index 32670f31540..d184a2d8432 100644 --- a/docs/channels/twitch.md +++ b/docs/channels/twitch.md @@ -255,7 +255,7 @@ openclaw doctor openclaw channels status --probe ``` -### Bot doesn't respond to messages +### Bot does not respond to messages **Check access control:** Ensure your user ID is in `allowFrom`, or temporarily remove `allowFrom` and set `allowedRoles: ["all"]` to test. diff --git a/docs/cli/acp.md b/docs/cli/acp.md index 9e239fc8bdf..d30deb49eca 100644 --- a/docs/cli/acp.md +++ b/docs/cli/acp.md @@ -8,7 +8,7 @@ title: "acp" # acp -Run the [Agent Client Protocol (ACP)](https://agentclientprotocol.com/) bridge that talks to a OpenClaw Gateway. +Run the [Agent Client Protocol (ACP)](https://agentclientprotocol.com/) bridge that talks to an OpenClaw Gateway. This command speaks ACP over stdio for IDEs and forwards prompts to the Gateway over WebSocket. It keeps ACP sessions mapped to Gateway session keys. @@ -102,7 +102,7 @@ Permission model (client debug mode): ## How to use this Use ACP when an IDE (or other client) speaks Agent Client Protocol and you want -it to drive a OpenClaw Gateway session. +it to drive an OpenClaw Gateway session. 1. Ensure the Gateway is running (local or remote). 2. Configure the Gateway target (config or flags). diff --git a/docs/cli/config.md b/docs/cli/config.md index fa0d62e8511..1eb376f0fa0 100644 --- a/docs/cli/config.md +++ b/docs/cli/config.md @@ -7,9 +7,9 @@ title: "config" # `openclaw config` -Config helpers: get/set/unset/validate values by path and print the active -config file. Run without a subcommand to open -the configure wizard (same as `openclaw configure`). +Config helpers for non-interactive edits in `openclaw.json`: get/set/unset/validate +values by path and print the active config file. Run without a subcommand to +open the configure wizard (same as `openclaw configure`). ## Examples @@ -19,7 +19,10 @@ openclaw config get browser.executablePath openclaw config set browser.executablePath "/usr/bin/google-chrome" openclaw config set agents.defaults.heartbeat.every "2h" openclaw config set agents.list[0].tools.exec.node "node-id-or-name" -openclaw config unset tools.web.search.apiKey +openclaw config set channels.discord.token --ref-provider default --ref-source env --ref-id DISCORD_BOT_TOKEN +openclaw config set secrets.providers.vaultfile --provider-source file --provider-path /etc/openclaw/secrets.json --provider-mode json +openclaw config unset plugins.entries.brave.config.webSearch.apiKey +openclaw config set channels.discord.token --ref-provider default --ref-source env --ref-id DISCORD_BOT_TOKEN --dry-run openclaw config validate openclaw config validate --json ``` @@ -51,6 +54,230 @@ openclaw config set gateway.port 19001 --strict-json openclaw config set channels.whatsapp.groups '["*"]' --strict-json ``` +## `config set` modes + +`openclaw config set` supports four assignment styles: + +1. Value mode: `openclaw config set ` +2. SecretRef builder mode: + +```bash +openclaw config set channels.discord.token \ + --ref-provider default \ + --ref-source env \ + --ref-id DISCORD_BOT_TOKEN +``` + +3. Provider builder mode (`secrets.providers.` path only): + +```bash +openclaw config set secrets.providers.vault \ + --provider-source exec \ + --provider-command /usr/local/bin/openclaw-vault \ + --provider-arg read \ + --provider-arg openai/api-key \ + --provider-timeout-ms 5000 +``` + +4. Batch mode (`--batch-json` or `--batch-file`): + +```bash +openclaw config set --batch-json '[ + { + "path": "secrets.providers.default", + "provider": { "source": "env" } + }, + { + "path": "channels.discord.token", + "ref": { "source": "env", "provider": "default", "id": "DISCORD_BOT_TOKEN" } + } +]' +``` + +```bash +openclaw config set --batch-file ./config-set.batch.json --dry-run +``` + +Batch parsing always uses the batch payload (`--batch-json`/`--batch-file`) as the source of truth. +`--strict-json` / `--json` do not change batch parsing behavior. + +JSON path/value mode remains supported for both SecretRefs and providers: + +```bash +openclaw config set channels.discord.token \ + '{"source":"env","provider":"default","id":"DISCORD_BOT_TOKEN"}' \ + --strict-json + +openclaw config set secrets.providers.vaultfile \ + '{"source":"file","path":"/etc/openclaw/secrets.json","mode":"json"}' \ + --strict-json +``` + +## Provider Builder Flags + +Provider builder targets must use `secrets.providers.` as the path. + +Common flags: + +- `--provider-source ` +- `--provider-timeout-ms ` (`file`, `exec`) + +Env provider (`--provider-source env`): + +- `--provider-allowlist ` (repeatable) + +File provider (`--provider-source file`): + +- `--provider-path ` (required) +- `--provider-mode ` +- `--provider-max-bytes ` + +Exec provider (`--provider-source exec`): + +- `--provider-command ` (required) +- `--provider-arg ` (repeatable) +- `--provider-no-output-timeout-ms ` +- `--provider-max-output-bytes ` +- `--provider-json-only` +- `--provider-env ` (repeatable) +- `--provider-pass-env ` (repeatable) +- `--provider-trusted-dir ` (repeatable) +- `--provider-allow-insecure-path` +- `--provider-allow-symlink-command` + +Hardened exec provider example: + +```bash +openclaw config set secrets.providers.vault \ + --provider-source exec \ + --provider-command /usr/local/bin/openclaw-vault \ + --provider-arg read \ + --provider-arg openai/api-key \ + --provider-json-only \ + --provider-pass-env VAULT_TOKEN \ + --provider-trusted-dir /usr/local/bin \ + --provider-timeout-ms 5000 +``` + +## Dry run + +Use `--dry-run` to validate changes without writing `openclaw.json`. + +```bash +openclaw config set channels.discord.token \ + --ref-provider default \ + --ref-source env \ + --ref-id DISCORD_BOT_TOKEN \ + --dry-run + +openclaw config set channels.discord.token \ + --ref-provider default \ + --ref-source env \ + --ref-id DISCORD_BOT_TOKEN \ + --dry-run \ + --json + +openclaw config set channels.discord.token \ + --ref-provider vault \ + --ref-source exec \ + --ref-id discord/token \ + --dry-run \ + --allow-exec +``` + +Dry-run behavior: + +- Builder mode: runs SecretRef resolvability checks for changed refs/providers. +- JSON mode (`--strict-json`, `--json`, or batch mode): runs schema validation plus SecretRef resolvability checks. +- Exec SecretRef checks are skipped by default during dry-run to avoid command side effects. +- Use `--allow-exec` with `--dry-run` to opt in to exec SecretRef checks (this may execute provider commands). +- `--allow-exec` is dry-run only and errors if used without `--dry-run`. + +`--dry-run --json` prints a machine-readable report: + +- `ok`: whether dry-run passed +- `operations`: number of assignments evaluated +- `checks`: whether schema/resolvability checks ran +- `checks.resolvabilityComplete`: whether resolvability checks ran to completion (false when exec refs are skipped) +- `refsChecked`: number of refs actually resolved during dry-run +- `skippedExecRefs`: number of exec refs skipped because `--allow-exec` was not set +- `errors`: structured schema/resolvability failures when `ok=false` + +### JSON Output Shape + +```json5 +{ + ok: boolean, + operations: number, + configPath: string, + inputModes: ["value" | "json" | "builder", ...], + checks: { + schema: boolean, + resolvability: boolean, + resolvabilityComplete: boolean, + }, + refsChecked: number, + skippedExecRefs: number, + errors?: [ + { + kind: "schema" | "resolvability", + message: string, + ref?: string, // present for resolvability errors + }, + ], +} +``` + +Success example: + +```json +{ + "ok": true, + "operations": 1, + "configPath": "~/.openclaw/openclaw.json", + "inputModes": ["builder"], + "checks": { + "schema": false, + "resolvability": true, + "resolvabilityComplete": true + }, + "refsChecked": 1, + "skippedExecRefs": 0 +} +``` + +Failure example: + +```json +{ + "ok": false, + "operations": 1, + "configPath": "~/.openclaw/openclaw.json", + "inputModes": ["builder"], + "checks": { + "schema": false, + "resolvability": true, + "resolvabilityComplete": true + }, + "refsChecked": 1, + "skippedExecRefs": 0, + "errors": [ + { + "kind": "resolvability", + "message": "Error: Environment variable \"MISSING_TEST_SECRET\" is not set.", + "ref": "env:default:MISSING_TEST_SECRET" + } + ] +} +``` + +If dry-run fails: + +- `config schema validation failed`: your post-change config shape is invalid; fix path/value or provider/ref object shape. +- `SecretRef assignment(s) could not be resolved`: referenced provider/ref currently cannot resolve (missing env var, invalid file pointer, exec provider failure, or provider/source mismatch). +- `Dry run note: skipped exec SecretRef resolvability check(s)`: dry-run skipped exec refs; rerun with `--allow-exec` if you need exec resolvability validation. +- For batch mode, fix failing entries and rerun `--dry-run` before writing. + ## Subcommands - `config file`: Print the active config file path (resolved from `OPENCLAW_CONFIG_PATH` or default location). diff --git a/docs/cli/index.md b/docs/cli/index.md index 8700655c766..d9d50733632 100644 --- a/docs/cli/index.md +++ b/docs/cli/index.md @@ -101,6 +101,8 @@ openclaw [--dev] [--profile ] get set unset + file + validate completion doctor dashboard @@ -274,16 +276,16 @@ Note: plugins can add additional top-level commands (for example `openclaw voice ## Secrets - `openclaw secrets reload` — re-resolve refs and atomically swap the runtime snapshot. -- `openclaw secrets audit` — scan for plaintext residues, unresolved refs, and precedence drift. -- `openclaw secrets configure` — interactive helper for provider setup + SecretRef mapping + preflight/apply. -- `openclaw secrets apply --from ` — apply a previously generated plan (`--dry-run` supported). +- `openclaw secrets audit` — scan for plaintext residues, unresolved refs, and precedence drift (`--allow-exec` to execute exec providers during audit). +- `openclaw secrets configure` — interactive helper for provider setup + SecretRef mapping + preflight/apply (`--allow-exec` to execute exec providers during preflight and exec-containing apply flows). +- `openclaw secrets apply --from ` — apply a previously generated plan (`--dry-run` supported; use `--allow-exec` to permit exec providers in dry-run and exec-containing write plans). ## Plugins Manage extensions and their config: - `openclaw plugins list` — discover plugins (use `--json` for machine output). -- `openclaw plugins info ` — show details for a plugin. +- `openclaw plugins inspect ` — show details for a plugin (`info` is an alias). - `openclaw plugins install ` — install a plugin (or add a plugin path to `plugins.load.paths`). - `openclaw plugins marketplace list ` — list marketplace entries before install. - `openclaw plugins enable ` / `disable ` — toggle `plugins.entries..enabled`. @@ -393,7 +395,15 @@ subcommand launches the wizard. Subcommands: - `config get `: print a config value (dot/bracket path). -- `config set `: set a value (JSON5 or raw string). +- `config set`: supports four assignment modes: + - value mode: `config set ` (JSON5-or-string parsing) + - SecretRef builder mode: `config set --ref-provider --ref-source --ref-id ` + - provider builder mode: `config set secrets.providers. --provider-source ...` + - batch mode: `config set --batch-json ''` or `config set --batch-file ` +- `config set --dry-run`: validate assignments without writing `openclaw.json` (exec SecretRef checks are skipped by default). +- `config set --allow-exec --dry-run`: opt in to exec SecretRef dry-run checks (may execute provider commands). +- `config set --dry-run --json`: emit machine-readable dry-run output (checks + completeness signal, operations, refs checked/skipped, errors). +- `config set --strict-json`: require JSON5 parsing for path/value input. `--json` remains a legacy alias for strict parsing outside dry-run output mode. - `config unset `: remove a value. - `config file`: print the active config file path. - `config validate`: validate the current config against the schema without starting the gateway. diff --git a/docs/cli/plugins.md b/docs/cli/plugins.md index 5e551a9c64f..6d0fa0af76b 100644 --- a/docs/cli/plugins.md +++ b/docs/cli/plugins.md @@ -21,7 +21,8 @@ Related: ```bash openclaw plugins list -openclaw plugins info +openclaw plugins install +openclaw plugins inspect openclaw plugins enable openclaw plugins disable openclaw plugins uninstall @@ -148,3 +149,28 @@ marketplace installs. When a stored integrity hash exists and the fetched artifact hash changes, OpenClaw prints a warning and asks for confirmation before proceeding. Use global `--yes` to bypass prompts in CI/non-interactive runs. + +### Inspect + +```bash +openclaw plugins inspect +openclaw plugins inspect --json +``` + +Deep introspection for a single plugin. Shows identity, load status, source, +registered capabilities, hooks, tools, commands, services, gateway methods, +HTTP routes, policy flags, diagnostics, and install metadata. + +Each plugin is classified by what it actually registers at runtime: + +- **plain-capability** — one capability type (e.g. a provider-only plugin) +- **hybrid-capability** — multiple capability types (e.g. text + speech + images) +- **hook-only** — only hooks, no capabilities or surfaces +- **non-capability** — tools/commands/services but no capabilities + +See [Plugins](/tools/plugin#plugin-shapes) for more on the capability model. + +The `--json` flag outputs a machine-readable report suitable for scripting and +auditing. + +`info` is an alias for `inspect`. diff --git a/docs/cli/secrets.md b/docs/cli/secrets.md index f90a5de8ec0..baefdc91886 100644 --- a/docs/cli/secrets.md +++ b/docs/cli/secrets.md @@ -14,9 +14,9 @@ Use `openclaw secrets` to manage SecretRefs and keep the active runtime snapshot Command roles: - `reload`: gateway RPC (`secrets.reload`) that re-resolves refs and swaps runtime snapshot only on full success (no config writes). -- `audit`: read-only scan of configuration/auth/generated-model stores and legacy residues for plaintext, unresolved refs, and precedence drift. +- `audit`: read-only scan of configuration/auth/generated-model stores and legacy residues for plaintext, unresolved refs, and precedence drift (exec refs are skipped unless `--allow-exec` is set). - `configure`: interactive planner for provider setup, target mapping, and preflight (TTY required). -- `apply`: execute a saved plan (`--dry-run` for validation only), then scrub targeted plaintext residues. +- `apply`: execute a saved plan (`--dry-run` for validation only; dry-run skips exec checks by default, and write mode rejects exec-containing plans unless `--allow-exec` is set), then scrub targeted plaintext residues. Recommended operator loop: @@ -29,6 +29,8 @@ openclaw secrets audit --check openclaw secrets reload ``` +If your plan includes `exec` SecretRefs/providers, pass `--allow-exec` on both dry-run and write apply commands. + Exit code note for CI/gates: - `audit --check` returns `1` on findings. @@ -73,6 +75,7 @@ Header residue note: openclaw secrets audit openclaw secrets audit --check openclaw secrets audit --json +openclaw secrets audit --allow-exec ``` Exit behavior: @@ -83,6 +86,7 @@ Exit behavior: Report shape highlights: - `status`: `clean | findings | unresolved` +- `resolution`: `refsChecked`, `skippedExecRefs`, `resolvabilityComplete` - `summary`: `plaintextCount`, `unresolvedRefCount`, `shadowedRefCount`, `legacyResidueCount` - finding codes: - `PLAINTEXT_FOUND` @@ -115,6 +119,7 @@ Flags: - `--providers-only`: configure `secrets.providers` only, skip credential mapping. - `--skip-provider-setup`: skip provider setup and map credentials to existing providers. - `--agent `: scope `auth-profiles.json` target discovery and writes to one agent store. +- `--allow-exec`: allow exec SecretRef checks during preflight/apply (may execute provider commands). Notes: @@ -124,6 +129,7 @@ Notes: - `configure` supports creating new `auth-profiles.json` mappings directly in the picker flow. - Canonical supported surface: [SecretRef Credential Surface](/reference/secretref-credential-surface). - It performs preflight resolution before apply. +- If preflight/apply includes exec refs, keep `--allow-exec` set for both steps. - Generated plans default to scrub options (`scrubEnv`, `scrubAuthProfilesForProviderTargets`, `scrubLegacyAuthJson` all enabled). - Apply path is one-way for scrubbed plaintext values. - Without `--apply`, CLI still prompts `Apply this plan now?` after preflight. @@ -141,10 +147,19 @@ Apply or preflight a plan generated previously: ```bash openclaw secrets apply --from /tmp/openclaw-secrets-plan.json +openclaw secrets apply --from /tmp/openclaw-secrets-plan.json --allow-exec openclaw secrets apply --from /tmp/openclaw-secrets-plan.json --dry-run +openclaw secrets apply --from /tmp/openclaw-secrets-plan.json --dry-run --allow-exec openclaw secrets apply --from /tmp/openclaw-secrets-plan.json --json ``` +Exec behavior: + +- `--dry-run` validates preflight without writing files. +- exec SecretRef checks are skipped by default in dry-run. +- write mode rejects plans that contain exec SecretRefs/providers unless `--allow-exec` is set. +- Use `--allow-exec` to opt in to exec provider checks/execution in either mode. + Plan contract details (allowed target paths, validation rules, and failure semantics): - [Secrets Apply Plan Contract](/gateway/secrets-plan-contract) diff --git a/docs/cli/security.md b/docs/cli/security.md index 76a7ae75976..28b65f3629b 100644 --- a/docs/cli/security.md +++ b/docs/cli/security.md @@ -30,7 +30,7 @@ This is for cooperative/shared inbox hardening. A single Gateway shared by mutua It also emits `security.trust_model.multi_user_heuristic` when config suggests likely shared-user ingress (for example open DM/group policy, configured group targets, or wildcard sender rules), and reminds you that OpenClaw is a personal-assistant trust model by default. For intentional shared-user setups, the audit guidance is to sandbox all sessions, keep filesystem access workspace-scoped, and keep personal/private identities or credentials off that runtime. It also warns when small models (`<=300B`) are used without sandboxing and with web/browser tools enabled. -For webhook ingress, it warns when `hooks.defaultSessionKey` is unset, when request `sessionKey` overrides are enabled, and when overrides are enabled without `hooks.allowedSessionKeyPrefixes`. +For webhook ingress, it warns when `hooks.token` reuses the Gateway token, when `hooks.defaultSessionKey` is unset, when `hooks.allowedAgentIds` is unrestricted, when request `sessionKey` overrides are enabled, and when overrides are enabled without `hooks.allowedSessionKeyPrefixes`. It also warns when sandbox Docker settings are configured while sandbox mode is off, when `gateway.nodes.denyCommands` uses ineffective pattern-like/unknown entries (exact node command-name matching only, not shell-text filtering), when `gateway.nodes.allowCommands` explicitly enables dangerous node commands, when global `tools.profile="minimal"` is overridden by agent tool profiles, when open groups expose runtime/filesystem tools without sandbox/workspace guards, and when installed extension plugin tools may be reachable under permissive tool policy. It also flags `gateway.allowRealIpFallback=true` (header-spoofing risk if proxies are misconfigured) and `discovery.mdns.mode="full"` (metadata leakage via mDNS TXT records). It also warns when sandbox browser uses Docker `bridge` network without `sandbox.browser.cdpSourceRange`. diff --git a/docs/concepts/compaction.md b/docs/concepts/compaction.md index 5640fa51a35..550d3b385d4 100644 --- a/docs/concepts/compaction.md +++ b/docs/concepts/compaction.md @@ -108,6 +108,14 @@ summaries, vector retrieval, incremental condensation, etc. When a plugin engine sets `ownsCompaction: true`, OpenClaw delegates all compaction decisions to the engine and does not run built-in auto-compaction. +When `ownsCompaction` is `false` or unset, OpenClaw may still use Pi's +built-in in-attempt auto-compaction, but the active engine's `compact()` method +still handles `/compact` and overflow recovery. There is no automatic fallback +to the legacy engine's compaction path. + +If you are building a non-owning context engine, implement `compact()` by +calling `delegateCompactionToRuntime(...)` from `openclaw/plugin-sdk/core`. + ## Tips - Use `/compact` when sessions feel stale or context is bloated. diff --git a/docs/concepts/context-engine.md b/docs/concepts/context-engine.md index 87d5e87d85b..0b2ec1cd78b 100644 --- a/docs/concepts/context-engine.md +++ b/docs/concepts/context-engine.md @@ -14,7 +14,7 @@ It decides which messages to include, how to summarize older history, and how to manage context across subagent boundaries. OpenClaw ships with a built-in `legacy` engine. Plugins can register -alternative engines that replace the entire context pipeline. +alternative engines that replace the active context-engine lifecycle. ## Quick start @@ -194,13 +194,31 @@ Optional members: ### ownsCompaction -When `info.ownsCompaction` is `true`, the engine manages its own compaction -lifecycle. OpenClaw will not trigger the built-in auto-compaction; instead it -delegates entirely to the engine's `compact()` method. The engine may also -run compaction proactively in `afterTurn()`. +`ownsCompaction` controls whether Pi's built-in in-attempt auto-compaction stays +enabled for the run: -When `false` or unset, OpenClaw's built-in auto-compaction logic runs -alongside the engine. +- `true` — the engine owns compaction behavior. OpenClaw disables Pi's built-in + auto-compaction for that run, and the engine's `compact()` implementation is + responsible for `/compact`, overflow recovery compaction, and any proactive + compaction it wants to do in `afterTurn()`. +- `false` or unset — Pi's built-in auto-compaction may still run during prompt + execution, but the active engine's `compact()` method is still called for + `/compact` and overflow recovery. + +`ownsCompaction: false` does **not** mean OpenClaw automatically falls back to +the legacy engine's compaction path. + +That means there are two valid plugin patterns: + +- **Owning mode** — implement your own compaction algorithm and set + `ownsCompaction: true`. +- **Delegating mode** — set `ownsCompaction: false` and have `compact()` call + `delegateCompactionToRuntime(...)` from `openclaw/plugin-sdk/core` to use + OpenClaw's built-in compaction behavior. + +A no-op `compact()` is unsafe for an active non-owning engine because it +disables the normal `/compact` and overflow-recovery compaction path for that +engine slot. ## Configuration reference diff --git a/docs/concepts/context.md b/docs/concepts/context.md index d5316ea8bf8..356f8b810c3 100644 --- a/docs/concepts/context.md +++ b/docs/concepts/context.md @@ -157,7 +157,9 @@ By default, OpenClaw uses the built-in `legacy` context engine for assembly and compaction. If you install a plugin that provides `kind: "context-engine"` and select it with `plugins.slots.contextEngine`, OpenClaw delegates context assembly, `/compact`, and related subagent context lifecycle hooks to that -engine instead. See [Context Engine](/concepts/context-engine) for the full +engine instead. `ownsCompaction: false` does not auto-fallback to the legacy +engine; the active engine must still implement `compact()` correctly. See +[Context Engine](/concepts/context-engine) for the full pluggable interface, lifecycle hooks, and configuration. ## What `/context` actually reports diff --git a/docs/concepts/model-providers.md b/docs/concepts/model-providers.md index 6adbb5d0f26..f5a73d7256e 100644 --- a/docs/concepts/model-providers.md +++ b/docs/concepts/model-providers.md @@ -32,6 +32,10 @@ For model selection rules, see [/concepts/models](/concepts/models). `supportsXHighThinking`, `resolveDefaultThinkingLevel`, `isModernModelRef`, `prepareRuntimeAuth`, `resolveUsageAuth`, and `fetchUsageSnapshot`. +- Note: provider runtime `capabilities` is shared runner metadata (provider + family, transcript/tooling quirks, transport/cache hints). It is not the + same as the [public capability model](/tools/plugin#public-capability-model) + which describes what a plugin registers (text inference, speech, etc.). ## Plugin-owned provider behavior diff --git a/docs/concepts/models.md b/docs/concepts/models.md index 88cf928568e..6ed1d1de3ab 100644 --- a/docs/concepts/models.md +++ b/docs/concepts/models.md @@ -26,7 +26,7 @@ Related: - `agents.defaults.models` is the allowlist/catalog of models OpenClaw can use (plus aliases). - `agents.defaults.imageModel` is used **only when** the primary model can’t accept images. -- `agents.defaults.imageGenerationModel` is used by the shared image-generation capability. +- `agents.defaults.imageGenerationModel` is used by the shared image-generation capability. If omitted, `image_generate` can still infer a provider default from compatible auth-backed image-generation plugins. - Per-agent defaults can override `agents.defaults.model` via `agents.list[].model` plus bindings (see [/concepts/multi-agent](/concepts/multi-agent)). ## Quick model policy diff --git a/docs/design/kilo-gateway-integration.md b/docs/design/kilo-gateway-integration.md index 39088aaf5b2..e498ea36e89 100644 --- a/docs/design/kilo-gateway-integration.md +++ b/docs/design/kilo-gateway-integration.md @@ -1,3 +1,11 @@ +--- +title: "Kilo Gateway Integration Design" +summary: "Design doc for integrating Kilo Gateway as a first-class OpenClaw provider" +read_when: + - Working on the Kilo Gateway provider integration + - Understanding provider integration patterns +--- + # Kilo Gateway Provider Integration Design ## Overview diff --git a/docs/docs.json b/docs/docs.json index 31dfee49c2f..3a79d609100 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -1055,6 +1055,7 @@ "plugins/zalouser", "plugins/manifest", "plugins/agent-tools", + "tools/capability-cookbook", "prose" ] }, diff --git a/docs/gateway/configuration-reference.md b/docs/gateway/configuration-reference.md index dccd87da423..49c743db623 100644 --- a/docs/gateway/configuration-reference.md +++ b/docs/gateway/configuration-reference.md @@ -905,6 +905,9 @@ Time format in system prompt. Default: `auto` (OS preference). - Also used as fallback routing when the selected/default model cannot accept image input. - `imageGenerationModel`: accepts either a string (`"provider/model"`) or an object (`{ primary, fallbacks }`). - Used by the shared image-generation capability and any future tool/plugin surface that generates images. + - Typical values: `google/gemini-3-pro-image-preview` for the native Nano Banana-style flow, `fal/fal-ai/flux/dev` for fal, or `openai/gpt-image-1` for OpenAI Images. + - If omitted, `image_generate` can still infer a best-effort provider default from compatible auth-backed image-generation providers. + - Typical values: `google/gemini-3-pro-image-preview`, `fal/fal-ai/flux/dev`, `openai/gpt-image-1`. - `pdfModel`: accepts either a string (`"provider/model"`) or an object (`{ primary, fallbacks }`). - Used by the `pdf` tool for model routing. - If omitted, the PDF tool falls back to `imageModel`, then to best-effort provider defaults. diff --git a/docs/gateway/configuration.md b/docs/gateway/configuration.md index d15efb3384b..b8977ca10ac 100644 --- a/docs/gateway/configuration.md +++ b/docs/gateway/configuration.md @@ -46,7 +46,7 @@ See the [full reference](/gateway/configuration-reference) for every available f ```bash openclaw config get agents.defaults.workspace openclaw config set agents.defaults.heartbeat.every "2h" - openclaw config unset tools.web.search.apiKey + openclaw config unset plugins.entries.brave.config.webSearch.apiKey ``` diff --git a/docs/gateway/secrets-plan-contract.md b/docs/gateway/secrets-plan-contract.md index 83ed10b06dd..b27518bdb1e 100644 --- a/docs/gateway/secrets-plan-contract.md +++ b/docs/gateway/secrets-plan-contract.md @@ -81,6 +81,12 @@ Invalid plan target path for models.providers.apiKey: models.providers.openai.ba No writes are committed for an invalid plan. +## Exec provider consent behavior + +- `--dry-run` skips exec SecretRef checks by default. +- Plans containing exec SecretRefs/providers are rejected in write mode unless `--allow-exec` is set. +- When validating/applying exec-containing plans, pass `--allow-exec` in both dry-run and write commands. + ## Runtime and audit scope notes - Ref-only `auth-profiles.json` entries (`keyRef`/`tokenRef`) are included in runtime resolution and audit coverage. @@ -94,6 +100,10 @@ openclaw secrets apply --from /tmp/openclaw-secrets-plan.json --dry-run # Then apply for real openclaw secrets apply --from /tmp/openclaw-secrets-plan.json + +# For exec-containing plans, opt in explicitly in both modes +openclaw secrets apply --from /tmp/openclaw-secrets-plan.json --dry-run --allow-exec +openclaw secrets apply --from /tmp/openclaw-secrets-plan.json --allow-exec ``` If apply fails with an invalid target path message, regenerate the plan with `openclaw secrets configure` or fix the target path to a supported shape above. diff --git a/docs/gateway/secrets.md b/docs/gateway/secrets.md index 1379d8e0202..d404399ac65 100644 --- a/docs/gateway/secrets.md +++ b/docs/gateway/secrets.md @@ -414,6 +414,11 @@ Findings include: - precedence shadowing (`auth-profiles.json` taking priority over `openclaw.json` refs) - legacy residues (`auth.json`, OAuth reminders) +Exec note: + +- By default, audit skips exec SecretRef resolvability checks to avoid command side effects. +- Use `openclaw secrets audit --allow-exec` to execute exec providers during audit. + Header residue note: - Sensitive provider header detection is name-heuristic based (common auth/credential header names and fragments such as `authorization`, `x-api-key`, `token`, `secret`, `password`, and `credential`). @@ -429,6 +434,11 @@ Interactive helper that: - runs preflight resolution - can apply immediately +Exec note: + +- Preflight skips exec SecretRef checks unless `--allow-exec` is set. +- If you apply directly from `configure --apply` and the plan includes exec refs/providers, keep `--allow-exec` set for the apply step too. + Helpful modes: - `openclaw secrets configure --providers-only` @@ -447,9 +457,16 @@ Apply a saved plan: ```bash openclaw secrets apply --from /tmp/openclaw-secrets-plan.json +openclaw secrets apply --from /tmp/openclaw-secrets-plan.json --allow-exec openclaw secrets apply --from /tmp/openclaw-secrets-plan.json --dry-run +openclaw secrets apply --from /tmp/openclaw-secrets-plan.json --dry-run --allow-exec ``` +Exec note: + +- dry-run skips exec checks unless `--allow-exec` is set. +- write mode rejects plans containing exec SecretRefs/providers unless `--allow-exec` is set. + For strict target/path contract details and exact rejection rules, see: - [Secrets Apply Plan Contract](/gateway/secrets-plan-contract) diff --git a/docs/gateway/security/index.md b/docs/gateway/security/index.md index c3c1ee2eb1b..b9f37597b58 100644 --- a/docs/gateway/security/index.md +++ b/docs/gateway/security/index.md @@ -243,7 +243,10 @@ High-signal `checkId` values you will most likely see in real deployments (not e | `gateway.real_ip_fallback_enabled` | warn/critical | Trusting `X-Real-IP` fallback can enable source-IP spoofing via proxy misconfig | `gateway.allowRealIpFallback`, `gateway.trustedProxies` | no | | `discovery.mdns_full_mode` | warn/critical | mDNS full mode advertises `cliPath`/`sshPort` metadata on local network | `discovery.mdns.mode`, `gateway.bind` | no | | `config.insecure_or_dangerous_flags` | warn | Any insecure/dangerous debug flags enabled | multiple keys (see finding detail) | no | +| `hooks.token_reuse_gateway_token` | critical | Hook ingress token also unlocks Gateway auth | `hooks.token`, `gateway.auth.token` | no | | `hooks.token_too_short` | warn | Easier brute force on hook ingress | `hooks.token` | no | +| `hooks.default_session_key_unset` | warn | Hook agent runs fan out into generated per-request sessions | `hooks.defaultSessionKey` | no | +| `hooks.allowed_agent_ids_unrestricted` | warn/critical | Authenticated hook callers may route to any configured agent | `hooks.allowedAgentIds` | no | | `hooks.request_session_key_enabled` | warn/critical | External caller can choose sessionKey | `hooks.allowRequestSessionKey` | no | | `hooks.request_session_key_prefixes_missing` | warn/critical | No bound on external session key shapes | `hooks.allowedSessionKeyPrefixes` | no | | `logging.redact_off` | warn | Sensitive values leak to logs/status | `logging.redactSensitive` | yes | @@ -355,6 +358,7 @@ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - If the gateway itself terminates HTTPS, you can set `gateway.http.securityHeaders.strictTransportSecurity` to emit the HSTS header from OpenClaw responses. - Detailed deployment guidance is in [Trusted Proxy Auth](/gateway/trusted-proxy-auth#tls-termination-and-hsts). - For non-loopback Control UI deployments, `gateway.controlUi.allowedOrigins` is required by default. +- `gateway.controlUi.allowedOrigins: ["*"]` is an explicit allow-all browser-origin policy, not a hardened default. Avoid it outside tightly controlled local testing. - `gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback=true` enables Host-header origin fallback mode; treat it as a dangerous operator-selected policy. - Treat DNS rebinding and proxy-host header behavior as deployment hardening concerns; keep `trustedProxies` tight and avoid exposing the gateway directly to the public internet. @@ -495,7 +499,7 @@ Treat the snippet above as **secure DM mode**: If you run multiple accounts on the same channel, use `per-account-channel-peer` instead. If the same person contacts you on multiple channels, use `session.identityLinks` to collapse those DM sessions into one canonical identity. See [Session Management](/concepts/session) and [Configuration](/gateway/configuration). -## Allowlists (DM + groups) — terminology +## Allowlists (DM + groups) - terminology OpenClaw has two separate “who can trigger me?” layers: diff --git a/docs/help/faq.md b/docs/help/faq.md index cc52aafd604..5e892da6a7b 100644 --- a/docs/help/faq.md +++ b/docs/help/faq.md @@ -13,8 +13,8 @@ Quick answers plus deeper troubleshooting for real-world setups (local dev, VPS, ## Table of contents - [Quick start and first-run setup] - - [Im stuck what's the fastest way to get unstuck?](#im-stuck-whats-the-fastest-way-to-get-unstuck) - - [What's the recommended way to install and set up OpenClaw?](#whats-the-recommended-way-to-install-and-set-up-openclaw) + - [I am stuck - fastest way to get unstuck](#i-am-stuck---fastest-way-to-get-unstuck) + - [Recommended way to install and set up OpenClaw](#recommended-way-to-install-and-set-up-openclaw) - [How do I open the dashboard after onboarding?](#how-do-i-open-the-dashboard-after-onboarding) - [How do I authenticate the dashboard (token) on localhost vs remote?](#how-do-i-authenticate-the-dashboard-token-on-localhost-vs-remote) - [What runtime do I need?](#what-runtime-do-i-need) @@ -23,15 +23,15 @@ Quick answers plus deeper troubleshooting for real-world setups (local dev, VPS, - [It is stuck on "wake up my friend" / onboarding will not hatch. What now?](#it-is-stuck-on-wake-up-my-friend-onboarding-will-not-hatch-what-now) - [Can I migrate my setup to a new machine (Mac mini) without redoing onboarding?](#can-i-migrate-my-setup-to-a-new-machine-mac-mini-without-redoing-onboarding) - [Where do I see what is new in the latest version?](#where-do-i-see-what-is-new-in-the-latest-version) - - [I can't access docs.openclaw.ai (SSL error). What now?](#i-cant-access-docsopenclawai-ssl-error-what-now) - - [What's the difference between stable and beta?](#whats-the-difference-between-stable-and-beta) - - [How do I install the beta version, and what's the difference between beta and dev?](#how-do-i-install-the-beta-version-and-whats-the-difference-between-beta-and-dev) + - [Cannot access docs.openclaw.ai (SSL error)](#cannot-access-docsopenclawai-ssl-error) + - [Difference between stable and beta](#difference-between-stable-and-beta) + - [How do I install the beta version and what is the difference between beta and dev](#how-do-i-install-the-beta-version-and-what-is-the-difference-between-beta-and-dev) - [How do I try the latest bits?](#how-do-i-try-the-latest-bits) - [How long does install and onboarding usually take?](#how-long-does-install-and-onboarding-usually-take) - [Installer stuck? How do I get more feedback?](#installer-stuck-how-do-i-get-more-feedback) - [Windows install says git not found or openclaw not recognized](#windows-install-says-git-not-found-or-openclaw-not-recognized) - [Windows exec output shows garbled Chinese text what should I do](#windows-exec-output-shows-garbled-chinese-text-what-should-i-do) - - [The docs didn't answer my question - how do I get a better answer?](#the-docs-didnt-answer-my-question-how-do-i-get-a-better-answer) + - [The docs did not answer my question - how do I get a better answer](#the-docs-did-not-answer-my-question---how-do-i-get-a-better-answer) - [How do I install OpenClaw on Linux?](#how-do-i-install-openclaw-on-linux) - [How do I install OpenClaw on a VPS?](#how-do-i-install-openclaw-on-a-vps) - [Where are the cloud/VPS install guides?](#where-are-the-cloudvps-install-guides) @@ -57,7 +57,7 @@ Quick answers plus deeper troubleshooting for real-world setups (local dev, VPS, - [Can multiple people use one WhatsApp number with different OpenClaw instances?](#can-multiple-people-use-one-whatsapp-number-with-different-openclaw-instances) - [Can I run a "fast chat" agent and an "Opus for coding" agent?](#can-i-run-a-fast-chat-agent-and-an-opus-for-coding-agent) - [Does Homebrew work on Linux?](#does-homebrew-work-on-linux) - - [What's the difference between the hackable (git) install and npm install?](#whats-the-difference-between-the-hackable-git-install-and-npm-install) + - [Difference between the hackable git install and npm install](#difference-between-the-hackable-git-install-and-npm-install) - [Can I switch between npm and git installs later?](#can-i-switch-between-npm-and-git-installs-later) - [Should I run the Gateway on my laptop or a VPS?](#should-i-run-the-gateway-on-my-laptop-or-a-vps) - [How important is it to run OpenClaw on a dedicated machine?](#how-important-is-it-to-run-openclaw-on-a-dedicated-machine) @@ -65,7 +65,7 @@ Quick answers plus deeper troubleshooting for real-world setups (local dev, VPS, - [Can I run OpenClaw in a VM and what are the requirements](#can-i-run-openclaw-in-a-vm-and-what-are-the-requirements) - [What is OpenClaw?](#what-is-openclaw) - [What is OpenClaw, in one paragraph?](#what-is-openclaw-in-one-paragraph) - - [What's the value proposition?](#whats-the-value-proposition) + - [Value proposition](#value-proposition) - [I just set it up what should I do first](#i-just-set-it-up-what-should-i-do-first) - [What are the top five everyday use cases for OpenClaw](#what-are-the-top-five-everyday-use-cases-for-openclaw) - [Can OpenClaw help with lead gen outreach ads and blogs for a SaaS](#can-openclaw-help-with-lead-gen-outreach-ads-and-blogs-for-a-saas) @@ -92,7 +92,7 @@ Quick answers plus deeper troubleshooting for real-world setups (local dev, VPS, - [Is all data used with OpenClaw saved locally?](#is-all-data-used-with-openclaw-saved-locally) - [Where does OpenClaw store its data?](#where-does-openclaw-store-its-data) - [Where should AGENTS.md / SOUL.md / USER.md / MEMORY.md live?](#where-should-agentsmd-soulmd-usermd-memorymd-live) - - [What's the recommended backup strategy?](#whats-the-recommended-backup-strategy) + - [Recommended backup strategy](#recommended-backup-strategy) - [How do I completely uninstall OpenClaw?](#how-do-i-completely-uninstall-openclaw) - [Can agents work outside the workspace?](#can-agents-work-outside-the-workspace) - [I'm in remote mode - where is the session store?](#im-in-remote-mode-where-is-the-session-store) @@ -116,7 +116,7 @@ Quick answers plus deeper troubleshooting for real-world setups (local dev, VPS, - [Is there a benefit to using a node on my personal laptop instead of SSH from a VPS?](#is-there-a-benefit-to-using-a-node-on-my-personal-laptop-instead-of-ssh-from-a-vps) - [Do nodes run a gateway service?](#do-nodes-run-a-gateway-service) - [Is there an API / RPC way to apply config?](#is-there-an-api-rpc-way-to-apply-config) - - [What's a minimal "sane" config for a first install?](#whats-a-minimal-sane-config-for-a-first-install) + - [Minimal sane config for a first install](#minimal-sane-config-for-a-first-install) - [How do I set up Tailscale on a VPS and connect from my Mac?](#how-do-i-set-up-tailscale-on-a-vps-and-connect-from-my-mac) - [How do I connect a Mac node to a remote Gateway (Tailscale Serve)?](#how-do-i-connect-a-mac-node-to-a-remote-gateway-tailscale-serve) - [Should I install on a second laptop or just add a node?](#should-i-install-on-a-second-laptop-or-just-add-a-node) @@ -135,7 +135,7 @@ Quick answers plus deeper troubleshooting for real-world setups (local dev, VPS, - [Why am I getting heartbeat messages every 30 minutes?](#why-am-i-getting-heartbeat-messages-every-30-minutes) - [Do I need to add a "bot account" to a WhatsApp group?](#do-i-need-to-add-a-bot-account-to-a-whatsapp-group) - [How do I get the JID of a WhatsApp group?](#how-do-i-get-the-jid-of-a-whatsapp-group) - - [Why doesn't OpenClaw reply in a group?](#why-doesnt-openclaw-reply-in-a-group) + - [Why does OpenClaw not reply in a group](#why-does-openclaw-not-reply-in-a-group) - [Do groups/threads share context with DMs?](#do-groupsthreads-share-context-with-dms) - [How many workspaces and agents can I create?](#how-many-workspaces-and-agents-can-i-create) - [Can I run multiple bots or chats at the same time (Slack), and how should I set that up?](#can-i-run-multiple-bots-or-chats-at-the-same-time-slack-and-how-should-i-set-that-up) @@ -162,7 +162,7 @@ Quick answers plus deeper troubleshooting for real-world setups (local dev, VPS, - [What is an auth profile?](#what-is-an-auth-profile) - [What are typical profile IDs?](#what-are-typical-profile-ids) - [Can I control which auth profile is tried first?](#can-i-control-which-auth-profile-is-tried-first) - - [OAuth vs API key: what's the difference?](#oauth-vs-api-key-whats-the-difference) + - [OAuth vs API key - what is the difference](#oauth-vs-api-key---what-is-the-difference) - [Gateway: ports, "already running", and remote mode](#gateway-ports-already-running-and-remote-mode) - [What port does the Gateway use?](#what-port-does-the-gateway-use) - [Why does `openclaw gateway status` say `Runtime: running` but `RPC probe: failed`?](#why-does-openclaw-gateway-status-say-runtime-running-but-rpc-probe-failed) @@ -170,7 +170,7 @@ Quick answers plus deeper troubleshooting for real-world setups (local dev, VPS, - [What does "another gateway instance is already listening" mean?](#what-does-another-gateway-instance-is-already-listening-mean) - [How do I run OpenClaw in remote mode (client connects to a Gateway elsewhere)?](#how-do-i-run-openclaw-in-remote-mode-client-connects-to-a-gateway-elsewhere) - [The Control UI says "unauthorized" (or keeps reconnecting). What now?](#the-control-ui-says-unauthorized-or-keeps-reconnecting-what-now) - - [I set `gateway.bind: "tailnet"` but it can't bind / nothing listens](#i-set-gatewaybind-tailnet-but-it-cant-bind-nothing-listens) + - [I set gateway.bind tailnet but it cannot bind and nothing listens](#i-set-gatewaybind-tailnet-but-it-cannot-bind-and-nothing-listens) - [Can I run multiple Gateways on the same host?](#can-i-run-multiple-gateways-on-the-same-host) - [What does "invalid handshake" / code 1008 mean?](#what-does-invalid-handshake-code-1008-mean) - [Logging and debugging](#logging-and-debugging) @@ -183,7 +183,7 @@ Quick answers plus deeper troubleshooting for real-world setups (local dev, VPS, - [TUI shows no output. What should I check?](#tui-shows-no-output-what-should-i-check) - [How do I completely stop then start the Gateway?](#how-do-i-completely-stop-then-start-the-gateway) - [ELI5: `openclaw gateway restart` vs `openclaw gateway`](#eli5-openclaw-gateway-restart-vs-openclaw-gateway) - - [What's the fastest way to get more details when something fails?](#whats-the-fastest-way-to-get-more-details-when-something-fails) + - [Fastest way to get more details when something fails](#fastest-way-to-get-more-details-when-something-fails) - [Media and attachments](#media-and-attachments) - [My skill generated an image/PDF, but nothing was sent](#my-skill-generated-an-imagepdf-but-nothing-was-sent) - [Security and access control](#security-and-access-control) @@ -192,15 +192,15 @@ Quick answers plus deeper troubleshooting for real-world setups (local dev, VPS, - [Should my bot have its own email GitHub account or phone number](#should-my-bot-have-its-own-email-github-account-or-phone-number) - [Can I give it autonomy over my text messages and is that safe](#can-i-give-it-autonomy-over-my-text-messages-and-is-that-safe) - [Can I use cheaper models for personal assistant tasks?](#can-i-use-cheaper-models-for-personal-assistant-tasks) - - [I ran `/start` in Telegram but didn't get a pairing code](#i-ran-start-in-telegram-but-didnt-get-a-pairing-code) + - [I ran /start in Telegram but did not get a pairing code](#i-ran-start-in-telegram-but-did-not-get-a-pairing-code) - [WhatsApp: will it message my contacts? How does pairing work?](#whatsapp-will-it-message-my-contacts-how-does-pairing-work) -- [Chat commands, aborting tasks, and "it won't stop"](#chat-commands-aborting-tasks-and-it-wont-stop) +- [Chat commands, aborting tasks, and "it will not stop"](#chat-commands-aborting-tasks-and-it-will-not-stop) - [How do I stop internal system messages from showing in chat](#how-do-i-stop-internal-system-messages-from-showing-in-chat) - [How do I stop/cancel a running task?](#how-do-i-stopcancel-a-running-task) - [How do I send a Discord message from Telegram? ("Cross-context messaging denied")](#how-do-i-send-a-discord-message-from-telegram-crosscontext-messaging-denied) - [Why does it feel like the bot "ignores" rapid-fire messages?](#why-does-it-feel-like-the-bot-ignores-rapidfire-messages) -## First 60 seconds if something's broken +## First 60 seconds if something is broken 1. **Quick status (first check)** @@ -267,7 +267,7 @@ Quick answers plus deeper troubleshooting for real-world setups (local dev, VPS, ## Quick start and first-run setup -### Im stuck what's the fastest way to get unstuck +### I am stuck - fastest way to get unstuck Use a local AI agent that can **see your machine**. That is far more effective than asking in Discord, because most "I'm stuck" cases are **local config or environment issues** that @@ -312,10 +312,10 @@ What they do: Other useful CLI checks: `openclaw status --all`, `openclaw logs --follow`, `openclaw gateway status`, `openclaw health --verbose`. -Quick debug loop: [First 60 seconds if something's broken](#first-60-seconds-if-somethings-broken). +Quick debug loop: [First 60 seconds if something is broken](#first-60-seconds-if-something-is-broken). Install docs: [Install](/install), [Installer flags](/install/installer), [Updating](/install/updating). -### What's the recommended way to install and set up OpenClaw +### Recommended way to install and set up OpenClaw The repo recommends running from source and using onboarding: @@ -445,7 +445,7 @@ Newest entries are at the top. If the top section is marked **Unreleased**, the section is the latest shipped version. Entries are grouped by **Highlights**, **Changes**, and **Fixes** (plus docs/other sections when needed). -### I can't access docs.openclaw.ai SSL error What now +### Cannot access docs.openclaw.ai (SSL error) Some Comcast/Xfinity connections incorrectly block `docs.openclaw.ai` via Xfinity Advanced Security. Disable it or allowlist `docs.openclaw.ai`, then retry. More @@ -455,7 +455,7 @@ Please help us unblock it by reporting here: [https://spa.xfinity.com/check_url_ If you still can't reach the site, the docs are mirrored on GitHub: [https://github.com/openclaw/openclaw/tree/main/docs](https://github.com/openclaw/openclaw/tree/main/docs) -### What's the difference between stable and beta +### Difference between stable and beta **Stable** and **beta** are **npm dist-tags**, not separate code lines: @@ -469,7 +469,7 @@ that same version to `latest`**. That's why beta and stable can point at the See what changed: [https://github.com/openclaw/openclaw/blob/main/CHANGELOG.md](https://github.com/openclaw/openclaw/blob/main/CHANGELOG.md) -### How do I install the beta version and what's the difference between beta and dev +### How do I install the beta version and what is the difference between beta and dev **Beta** is the npm dist-tag `beta` (may match `latest`). **Dev** is the moving head of `main` (git); when published, it uses the npm dist-tag `dev`. @@ -497,7 +497,7 @@ Rough guide: - **Onboarding:** 5-15 minutes depending on how many channels/models you configure If it hangs, use [Installer stuck](/help/faq#installer-stuck-how-do-i-get-more-feedback) -and the fast debug loop in [Im stuck](/help/faq#im-stuck--whats-the-fastest-way-to-get-unstuck). +and the fast debug loop in [I am stuck](/help/faq#i-am-stuck---fastest-way-to-get-unstuck). ### How do I try the latest bits @@ -614,7 +614,7 @@ If you still reproduce this on latest OpenClaw, track/report it in: - [Issue #30640](https://github.com/openclaw/openclaw/issues/30640) -### The docs didn't answer my question how do I get a better answer +### The docs did not answer my question - how do I get a better answer Use the **hackable (git) install** so you have the full source and docs locally, then ask your bot (or Claude/Codex) _from that folder_ so it can read the repo and answer precisely. @@ -882,7 +882,7 @@ brew install If you run OpenClaw via systemd, ensure the service PATH includes `/home/linuxbrew/.linuxbrew/bin` (or your brew prefix) so `brew`-installed tools resolve in non-login shells. Recent builds also prepend common user bin dirs on Linux systemd services (for example `~/.local/bin`, `~/.npm-global/bin`, `~/.local/share/pnpm`, `~/.bun/bin`) and honor `PNPM_HOME`, `NPM_CONFIG_PREFIX`, `BUN_INSTALL`, `VOLTA_HOME`, `ASDF_DATA_DIR`, `NVM_DIR`, and `FNM_DIR` when set. -### What's the difference between the hackable git install and npm install +### Difference between the hackable git install and npm install - **Hackable (git) install:** full source checkout, editable, best for contributors. You run builds locally and can patch code/docs. @@ -918,7 +918,7 @@ openclaw gateway restart Doctor detects a gateway service entrypoint mismatch and offers to rewrite the service config to match the current install (use `--repair` in automation). -Backup tips: see [Backup strategy](/help/faq#whats-the-recommended-backup-strategy). +Backup tips: see [Backup strategy](/help/faq#recommended-backup-strategy). ### Should I run the Gateway on my laptop or a VPS @@ -981,7 +981,7 @@ If you are running macOS in a VM, see [macOS VM](/install/macos-vm). OpenClaw is a personal AI assistant you run on your own devices. It replies on the messaging surfaces you already use (WhatsApp, Telegram, Slack, Mattermost (plugin), Discord, Google Chat, Signal, iMessage, WebChat) and can also do voice + a live Canvas on supported platforms. The **Gateway** is the always-on control plane; the assistant is the product. -### What's the value proposition +### Value proposition OpenClaw is not "just a Claude wrapper." It's a **local-first control plane** that lets you run a capable assistant on **your own hardware**, reachable from the chat apps you already use, with @@ -1381,7 +1381,7 @@ AGENTS.md or MEMORY.md** rather than relying on chat history. See [Agent workspace](/concepts/agent-workspace) and [Memory](/concepts/memory). -### What's the recommended backup strategy +### Recommended backup strategy Put your **agent workspace** in a **private** git repo and back it up somewhere private (for example GitHub private). This captures memory + AGENTS/SOUL/USER @@ -1505,12 +1505,22 @@ Environment alternatives: ```json5 { + plugins: { + entries: { + brave: { + config: { + webSearch: { + apiKey: "BRAVE_API_KEY_HERE", + }, + }, + }, + }, + }, tools: { web: { search: { enabled: true, provider: "brave", - apiKey: "BRAVE_API_KEY_HERE", maxResults: 5, }, fetch: { @@ -1521,6 +1531,9 @@ Environment alternatives: } ``` +Provider-specific web-search config now lives under `plugins.entries..config.webSearch.*`. +Legacy `tools.web.search.*` provider paths still load temporarily for compatibility, but they should not be used for new configs. + Notes: - If you use allowlists, add `web_search`/`web_fetch` or `group:web`. @@ -1714,7 +1727,7 @@ Avoid it: Docs: [Config](/cli/config), [Configure](/cli/configure), [Doctor](/gateway/doctor). -### What's a minimal sane config for a first install +### Minimal sane config for a first install ```json5 { @@ -2006,7 +2019,7 @@ openclaw directory groups list --channel whatsapp Docs: [WhatsApp](/channels/whatsapp), [Directory](/cli/directory), [Logs](/cli/logs). -### Why doesn't OpenClaw reply in a group +### Why does OpenClaw not reply in a group Two common causes: @@ -2449,7 +2462,7 @@ To target a specific agent: openclaw models auth order set --provider anthropic --agent main anthropic:default ``` -### OAuth vs API key what's the difference +### OAuth vs API key - what is the difference OpenClaw supports both: @@ -2541,7 +2554,7 @@ Fix: - `openclaw devices rotate --device --role operator` - Still stuck? Run `openclaw status --all` and follow [Troubleshooting](/gateway/troubleshooting). See [Dashboard](/web/dashboard) for auth details. -### I set gatewaybind tailnet but it can't bind nothing listens +### I set gateway.bind tailnet but it cannot bind and nothing listens `tailnet` bind picks a Tailscale IP from your network interfaces (100.64.0.0/10). If the machine isn't on Tailscale (or the interface is down), there's nothing to bind to. @@ -2772,7 +2785,7 @@ Docs: [Gateway service runbook](/gateway). If you installed the service, use the gateway commands. Use `openclaw gateway` when you want a one-off, foreground run. -### What's the fastest way to get more details when something fails +### Fastest way to get more details when something fails Start the Gateway with `--verbose` to get more console detail. Then inspect the log file for channel auth, model routing, and RPC errors. @@ -2854,7 +2867,7 @@ more susceptible to instruction hijacking, so avoid them for tool-enabled agents or when reading untrusted content. If you must use a smaller model, lock down tools and run inside a sandbox. See [Security](/gateway/security). -### I ran start in Telegram but didn't get a pairing code +### I ran start in Telegram but did not get a pairing code Pairing codes are sent **only** when an unknown sender messages the bot and `dmPolicy: "pairing"` is enabled. `/start` by itself doesn't generate a code. @@ -2886,7 +2899,7 @@ openclaw pairing list whatsapp Wizard phone number prompt: it's used to set your **allowlist/owner** so your own DMs are permitted. It's not used for auto-sending. If you run on your personal WhatsApp number, use that number and enable `channels.whatsapp.selfChatMode`. -## Chat commands, aborting tasks, and "it won't stop" +## Chat commands, aborting tasks, and "it will not stop" ### How do I stop internal system messages from showing in chat diff --git a/docs/help/testing.md b/docs/help/testing.md index 2055db4373f..0d14f507bc9 100644 --- a/docs/help/testing.md +++ b/docs/help/testing.md @@ -52,6 +52,17 @@ Think of the suites as “increasing realism” (and increasing flakiness/cost): - Runs in CI - No real keys required - Should be fast and stable +- Embedded runner note: + - When you change message-tool discovery inputs or compaction runtime context, + keep both levels of coverage. + - Add focused helper regressions for pure routing/normalization seams. + - Also keep the embedded runner integration suites healthy: + `src/agents/pi-embedded-runner/compact.hooks.test.ts`, + `src/agents/pi-embedded-runner/run.overflow-compaction.test.ts`, and + `src/agents/pi-embedded-runner/run.overflow-compaction.loop.test.ts`. + - Those suites verify that scoped ids and compaction behavior still flow + through the real `run.ts` / `compact.ts` paths; helper-only tests are not a + sufficient substitute for those seams. - Pool note: - OpenClaw uses Vitest `vmForks` on Node 22, 23, and 24 for faster unit shards. - On Node 25+, OpenClaw automatically falls back to regular `forks` until the repo is re-validated there. diff --git a/docs/install/ansible.md b/docs/install/ansible.md index 63c18bec237..d19383398d6 100644 --- a/docs/install/ansible.md +++ b/docs/install/ansible.md @@ -154,7 +154,7 @@ If you're locked out: - SSH access (port 22) is always allowed - The gateway is **only** accessible via Tailscale by design -### Service won't start +### Service will not start ```bash # Check logs diff --git a/docs/install/macos-vm.md b/docs/install/macos-vm.md index f2eadfda113..2bbd8e65051 100644 --- a/docs/install/macos-vm.md +++ b/docs/install/macos-vm.md @@ -112,7 +112,7 @@ After setup completes, enable SSH: --- -## 4) Get the VM's IP address +## 4) Get the VM IP address ```bash lume get openclaw diff --git a/docs/install/migrating.md b/docs/install/migrating.md index f9e82fd9777..64c136be425 100644 --- a/docs/install/migrating.md +++ b/docs/install/migrating.md @@ -1,5 +1,5 @@ --- -summary: "Move (migrate) a OpenClaw install from one machine to another" +summary: "Move (migrate) an OpenClaw install from one machine to another" read_when: - You are moving OpenClaw to a new laptop/server - You want to preserve sessions, auth, and channel logins (WhatsApp, etc.) @@ -8,7 +8,7 @@ title: "Migration Guide" # Migrating OpenClaw to a new machine -This guide migrates a OpenClaw Gateway from one machine to another **without redoing onboarding**. +This guide migrates an OpenClaw Gateway from one machine to another **without redoing onboarding**. The migration is simple conceptually: @@ -67,7 +67,7 @@ Those live under `$OPENCLAW_STATE_DIR`. ## Migration steps (recommended) -### Step 0 — Make a backup (old machine) +### Step 0 - Make a backup (old machine) On the **old** machine, stop the gateway first so files aren’t changing mid-copy: @@ -87,7 +87,7 @@ tar -czf openclaw-workspace.tgz .openclaw/workspace If you have multiple profiles/state dirs (e.g. `~/.openclaw-main`, `~/.openclaw-work`), archive each. -### Step 1 — Install OpenClaw on the new machine +### Step 1 - Install OpenClaw on the new machine On the **new** machine, install the CLI (and Node if needed): @@ -95,7 +95,7 @@ On the **new** machine, install the CLI (and Node if needed): At this stage, it’s OK if onboarding creates a fresh `~/.openclaw/` — you will overwrite it in the next step. -### Step 2 — Copy the state dir + workspace to the new machine +### Step 2 - Copy the state dir + workspace to the new machine Copy **both**: @@ -113,7 +113,7 @@ After copying, ensure: - Hidden directories were included (e.g. `.openclaw/`) - File ownership is correct for the user running the gateway -### Step 3 — Run Doctor (migrations + service repair) +### Step 3 - Run Doctor (migrations + service repair) On the **new** machine: diff --git a/docs/install/render.mdx b/docs/install/render.mdx index 7e43bfca012..e7a8b26346d 100644 --- a/docs/install/render.mdx +++ b/docs/install/render.mdx @@ -135,7 +135,7 @@ This downloads a portable backup you can restore on any OpenClaw host. ## Troubleshooting -### Service won't start +### Service will not start Check the deploy logs in the Render Dashboard. Common issues: diff --git a/docs/ja-JP/start/wizard.md b/docs/ja-JP/start/wizard.md index 19f53125857..d7a9a77bb57 100644 --- a/docs/ja-JP/start/wizard.md +++ b/docs/ja-JP/start/wizard.md @@ -67,7 +67,7 @@ openclaw agents add -推奨:エージェントが `web_search` を使用できるように、Brave Search APIキーを設定してください(`web_fetch` はキーなしで動作します)。最も簡単な方法:`openclaw configure --section web` を実行すると `tools.web.search.apiKey` が保存されます。ドキュメント:[Webツール](/tools/web)。 +推奨:エージェントが `web_search` を使用できるように、Brave Search APIキーを設定してください(`web_fetch` はキーなしで動作します)。最も簡単な方法:`openclaw configure --section web` を実行すると `plugins.entries.brave.config.webSearch.apiKey` に保存されます。旧 `tools.web.search.apiKey` パスは互換用に引き続き読み込まれますが、新しい設定では使用しないでください。ドキュメント:[Webツール](/tools/web)。 ## 関連ドキュメント diff --git a/docs/nodes/audio.md b/docs/nodes/audio.md index 1be35610323..57e9ab14d8a 100644 --- a/docs/nodes/audio.md +++ b/docs/nodes/audio.md @@ -5,7 +5,7 @@ read_when: title: "Audio and Voice Notes" --- -# Audio / Voice Notes — 2026-01-17 +# Audio / Voice Notes (2026-01-17) ## What works diff --git a/docs/nodes/images.md b/docs/nodes/images.md index c5f7bade748..6236ad089ef 100644 --- a/docs/nodes/images.md +++ b/docs/nodes/images.md @@ -5,7 +5,7 @@ read_when: title: "Image and Media Support" --- -# Image & Media Support — 2025-12-05 +# Image & Media Support (2025-12-05) The WhatsApp channel runs via **Baileys Web**. This document captures the current media handling rules for send, gateway, and agent replies. diff --git a/docs/nodes/media-understanding.md b/docs/nodes/media-understanding.md index ab3701387be..3178854ccfb 100644 --- a/docs/nodes/media-understanding.md +++ b/docs/nodes/media-understanding.md @@ -6,7 +6,7 @@ read_when: title: "Media Understanding" --- -# Media Understanding (Inbound) — 2026-01-17 +# Media Understanding - Inbound (2026-01-17) OpenClaw can **summarize inbound media** (image/audio/video) before the reply pipeline runs. It auto‑detects when local tools or provider keys are available, and can be disabled or customized. If understanding is off, models still receive the original files/URLs as usual. diff --git a/docs/perplexity.md b/docs/perplexity.md index b71f34d666b..3ad4c50c3f7 100644 --- a/docs/perplexity.md +++ b/docs/perplexity.md @@ -12,7 +12,7 @@ OpenClaw supports Perplexity Search API as a `web_search` provider. It returns structured results with `title`, `url`, and `snippet` fields. For compatibility, OpenClaw also supports legacy Perplexity Sonar/OpenRouter setups. -If you use `OPENROUTER_API_KEY`, an `sk-or-...` key in `tools.web.search.perplexity.apiKey`, or set `tools.web.search.perplexity.baseUrl` / `model`, the provider switches to the chat-completions path and returns AI-synthesized answers with citations instead of structured Search API results. +If you use `OPENROUTER_API_KEY`, an `sk-or-...` key in `plugins.entries.perplexity.config.webSearch.apiKey`, or set `plugins.entries.perplexity.config.webSearch.baseUrl` / `model`, the provider switches to the chat-completions path and returns AI-synthesized answers with citations instead of structured Search API results. ## Getting a Perplexity API key @@ -22,12 +22,12 @@ If you use `OPENROUTER_API_KEY`, an `sk-or-...` key in `tools.web.search.perplex ## OpenRouter compatibility -If you were already using OpenRouter for Perplexity Sonar, keep `provider: "perplexity"` and set `OPENROUTER_API_KEY` in the Gateway environment, or store an `sk-or-...` key in `tools.web.search.perplexity.apiKey`. +If you were already using OpenRouter for Perplexity Sonar, keep `provider: "perplexity"` and set `OPENROUTER_API_KEY` in the Gateway environment, or store an `sk-or-...` key in `plugins.entries.perplexity.config.webSearch.apiKey`. -Optional legacy controls: +Optional compatibility controls: -- `tools.web.search.perplexity.baseUrl` -- `tools.web.search.perplexity.model` +- `plugins.entries.perplexity.config.webSearch.baseUrl` +- `plugins.entries.perplexity.config.webSearch.model` ## Config examples @@ -35,13 +35,21 @@ Optional legacy controls: ```json5 { + plugins: { + entries: { + perplexity: { + config: { + webSearch: { + apiKey: "pplx-...", + }, + }, + }, + }, + }, tools: { web: { search: { provider: "perplexity", - perplexity: { - apiKey: "pplx-...", - }, }, }, }, @@ -52,15 +60,23 @@ Optional legacy controls: ```json5 { + plugins: { + entries: { + perplexity: { + config: { + webSearch: { + apiKey: "", + baseUrl: "https://openrouter.ai/api/v1", + model: "perplexity/sonar-pro", + }, + }, + }, + }, + }, tools: { web: { search: { provider: "perplexity", - perplexity: { - apiKey: "", - baseUrl: "https://openrouter.ai/api/v1", - model: "perplexity/sonar-pro", - }, }, }, }, @@ -70,7 +86,7 @@ Optional legacy controls: ## Where to set the key **Via config:** run `openclaw configure --section web`. It stores the key in -`~/.openclaw/openclaw.json` under `tools.web.search.perplexity.apiKey`. +`~/.openclaw/openclaw.json` under `plugins.entries.perplexity.config.webSearch.apiKey`. That field also accepts SecretRef objects. **Via environment:** set `PERPLEXITY_API_KEY` or `OPENROUTER_API_KEY` @@ -151,7 +167,7 @@ await web_search({ ## Notes - Perplexity Search API returns structured web search results (`title`, `url`, `snippet`) -- OpenRouter or explicit `baseUrl` / `model` switches Perplexity back to Sonar chat completions for compatibility +- OpenRouter or explicit `plugins.entries.perplexity.config.webSearch.baseUrl` / `model` switches Perplexity back to Sonar chat completions for compatibility - Results are cached for 15 minutes by default (configurable via `cacheTtlMinutes`) See [Web tools](/tools/web) for the full web_search configuration. diff --git a/docs/pi-dev.md b/docs/pi-dev.md index 322bd13cd39..3b0918c4928 100644 --- a/docs/pi-dev.md +++ b/docs/pi-dev.md @@ -76,5 +76,5 @@ If you only want to reset sessions, delete `agents//sessions/` and `age ## References -- [https://docs.openclaw.ai/testing](https://docs.openclaw.ai/testing) -- [https://docs.openclaw.ai/start/getting-started](https://docs.openclaw.ai/start/getting-started) +- [Testing](/help/testing) +- [Getting Started](/start/getting-started) diff --git a/docs/pi.md b/docs/pi.md index 2689b480963..f12c687906c 100644 --- a/docs/pi.md +++ b/docs/pi.md @@ -119,19 +119,24 @@ src/agents/ │ ├── browser-tool.ts │ ├── canvas-tool.ts │ ├── cron-tool.ts -│ ├── discord-actions*.ts │ ├── gateway-tool.ts │ ├── image-tool.ts │ ├── message-tool.ts │ ├── nodes-tool.ts │ ├── session*.ts -│ ├── slack-actions.ts -│ ├── telegram-actions.ts │ ├── web-*.ts -│ └── whatsapp-actions.ts +│ └── ... └── ... ``` +Channel-specific message action runtimes now live in the plugin-owned extension +directories instead of under `src/agents/tools`, for example: + +- `extensions/discord/src/actions/runtime*.ts` +- `extensions/slack/src/action-runtime.ts` +- `extensions/telegram/src/action-runtime.ts` +- `extensions/whatsapp/src/action-runtime.ts` + ## Core Integration Flow ### 1. Running an Embedded Agent diff --git a/docs/platforms/digitalocean.md b/docs/platforms/digitalocean.md index cd05587ae76..61021c1ade8 100644 --- a/docs/platforms/digitalocean.md +++ b/docs/platforms/digitalocean.md @@ -231,7 +231,7 @@ For the full setup guide, see [Oracle Cloud](/platforms/oracle). For signup tips ## Troubleshooting -### Gateway won't start +### Gateway will not start ```bash openclaw gateway status diff --git a/docs/platforms/mac/remote.md b/docs/platforms/mac/remote.md index 71b67070c89..631e5c88d2a 100644 --- a/docs/platforms/mac/remote.md +++ b/docs/platforms/mac/remote.md @@ -7,7 +7,7 @@ title: "Remote Control" # Remote OpenClaw (macOS ⇄ remote host) -This flow lets the macOS app act as a full remote control for a OpenClaw gateway running on another host (desktop/server). It’s the app’s **Remote over SSH** (remote run) feature. All features—health checks, Voice Wake forwarding, and Web Chat—reuse the same remote SSH configuration from _Settings → General_. +This flow lets the macOS app act as a full remote control for an OpenClaw gateway running on another host (desktop/server). It’s the app’s **Remote over SSH** (remote run) feature. All features—health checks, Voice Wake forwarding, and Web Chat—reuse the same remote SSH configuration from _Settings → General_. ## Modes diff --git a/docs/platforms/oracle.md b/docs/platforms/oracle.md index 779027c9f07..d185af41d23 100644 --- a/docs/platforms/oracle.md +++ b/docs/platforms/oracle.md @@ -180,7 +180,7 @@ With the VCN locked down (only UDP 41641 open) and the Gateway bound to loopback This setup often removes the _need_ for extra host-based firewall rules purely to stop Internet-wide SSH brute force — but you should still keep the OS updated, run `openclaw security audit`, and verify you aren’t accidentally listening on public interfaces. -### What's Already Protected +### Already protected | Traditional Step | Needed? | Why | | ------------------ | ----------- | ---------------------------------------------------------------------------- | @@ -236,7 +236,7 @@ Free tier ARM instances are popular. Try: - Retry during off-peak hours (early morning) - Use the "Always Free" filter when selecting shape -### Tailscale won't connect +### Tailscale will not connect ```bash # Check status @@ -246,7 +246,7 @@ sudo tailscale status sudo tailscale up --ssh --hostname=openclaw --reset ``` -### Gateway won't start +### Gateway will not start ```bash openclaw gateway status @@ -254,7 +254,7 @@ openclaw doctor --non-interactive journalctl --user -u openclaw-gateway -n 50 ``` -### Can't reach Control UI +### Cannot reach Control UI ```bash # Verify Tailscale Serve is running diff --git a/docs/platforms/raspberry-pi.md b/docs/platforms/raspberry-pi.md index 7b5e22f89c6..855f053c825 100644 --- a/docs/platforms/raspberry-pi.md +++ b/docs/platforms/raspberry-pi.md @@ -33,7 +33,7 @@ Perfect for: **Minimum specs:** 1GB RAM, 1 core, 500MB disk **Recommended:** 2GB+ RAM, 64-bit OS, 16GB+ SD card (or USB SSD) -## What You'll Need +## What you need - Raspberry Pi 4 or 5 (2GB+ recommended) - MicroSD card (16GB+) or USB SSD (better performance) @@ -354,7 +354,7 @@ free -h - Disable unused services: `sudo systemctl disable cups bluetooth avahi-daemon` - Check CPU throttling: `vcgencmd get_throttled` (should return `0x0`) -### Service Won't Start +### Service will not start ```bash # Check logs diff --git a/docs/plugins/bundles.md b/docs/plugins/bundles.md index bc6bc49e5a0..82a5605e099 100644 --- a/docs/plugins/bundles.md +++ b/docs/plugins/bundles.md @@ -19,7 +19,7 @@ Today that means three closely related ecosystems: - Cursor bundles OpenClaw shows all of them as `Format: bundle` in `openclaw plugins list`. -Verbose output and `openclaw plugins info ` also show the subtype +Verbose output and `openclaw plugins inspect ` also show the subtype (`codex`, `claude`, or `cursor`). Related: @@ -141,7 +141,7 @@ diagnostics/info output, but OpenClaw does not run them yet: ## Capability reporting -`openclaw plugins info ` shows bundle capabilities from the normalized +`openclaw plugins inspect ` shows bundle capabilities from the normalized bundle record. Supported capabilities are loaded quietly. Unsupported capabilities produce a @@ -269,7 +269,7 @@ openclaw plugins install ./my-cursor-bundle openclaw plugins install ./my-bundle.tgz openclaw plugins marketplace list openclaw plugins install @ -openclaw plugins info my-bundle +openclaw plugins inspect my-bundle ``` If the directory is a native OpenClaw plugin/package, the native install path @@ -284,7 +284,7 @@ sources; after resolution, the normal install rules still apply. ### Bundle is detected but capabilities do not run -Check `openclaw plugins info `. +Check `openclaw plugins inspect `. If the capability is listed but OpenClaw says it is not wired yet, that is a real product limit, not a broken install. diff --git a/docs/plugins/manifest.md b/docs/plugins/manifest.md index 5ef77b9ef68..e7d31e53e57 100644 --- a/docs/plugins/manifest.md +++ b/docs/plugins/manifest.md @@ -1,7 +1,7 @@ --- summary: "Plugin manifest + JSON schema requirements (strict config validation)" read_when: - - You are building a OpenClaw plugin + - You are building an OpenClaw plugin - You need to ship a plugin config schema or debug plugin validation errors title: "Plugin Manifest" --- @@ -32,6 +32,8 @@ Every native OpenClaw plugin **must** ship a `openclaw.plugin.json` file in the plugin errors and block config validation. See the full plugin system guide: [Plugins](/tools/plugin). +For the native capability model and current external-compatibility guidance: +[Capability model](/tools/plugin#public-capability-model). ## Required fields @@ -54,8 +56,8 @@ Required keys: Optional keys: - `kind` (string): plugin kind (examples: `"memory"`, `"context-engine"`). -- `channels` (array): channel ids registered by this plugin (example: `["matrix"]`). -- `providers` (array): provider ids registered by this plugin. +- `channels` (array): channel ids registered by this plugin (channel capability; example: `["matrix"]`). +- `providers` (array): provider ids registered by this plugin (text inference capability). - `providerAuthEnvVars` (object): auth env vars keyed by provider id. Use this when OpenClaw should resolve provider credentials from env without loading plugin runtime first. @@ -119,6 +121,8 @@ Example: - If plugin config exists but the plugin is **disabled**, the config is kept and a **warning** is surfaced in Doctor + logs. +See [Configuration reference](/configuration) for the full `plugins.*` schema. + ## Notes - The manifest is **required for native OpenClaw plugins**, including local filesystem loads. @@ -129,7 +133,9 @@ Example: runtime just to inspect env names. - `providerAuthChoices` is the cheap metadata path for auth-choice pickers, `--auth-choice` resolution, preferred-provider mapping, and simple onboarding - CLI flag registration before provider runtime loads. + CLI flag registration before provider runtime loads. For runtime wizard + metadata that requires provider code, see + [Provider runtime hooks](/tools/plugin#provider-runtime-hooks). - Exclusive plugin kinds are selected through `plugins.slots.*`. - `kind: "memory"` is selected by `plugins.slots.memory`. - `kind: "context-engine"` is selected by `plugins.slots.contextEngine` diff --git a/docs/providers/index.md b/docs/providers/index.md index f68cd0e0b53..82e30575bc8 100644 --- a/docs/providers/index.md +++ b/docs/providers/index.md @@ -47,6 +47,7 @@ Looking for chat channel docs (WhatsApp/Telegram/Discord/Slack/Mattermost (plugi - [Vercel AI Gateway](/providers/vercel-ai-gateway) - [Venice (Venice AI, privacy-focused)](/providers/venice) - [vLLM (local models)](/providers/vllm) +- [xAI](/providers/xai) - [Xiaomi](/providers/xiaomi) - [Z.AI](/providers/zai) diff --git a/docs/providers/kilocode.md b/docs/providers/kilocode.md index 15f8e4c2b7c..a1952c5425b 100644 --- a/docs/providers/kilocode.md +++ b/docs/providers/kilocode.md @@ -1,4 +1,5 @@ --- +title: "Kilo Gateway" summary: "Use Kilo Gateway's unified API to access many models in OpenClaw" read_when: - You want a single API key for many LLMs diff --git a/docs/providers/litellm.md b/docs/providers/litellm.md index 51ad0d599f8..10d28c92e28 100644 --- a/docs/providers/litellm.md +++ b/docs/providers/litellm.md @@ -1,4 +1,5 @@ --- +title: "LiteLLM" summary: "Run OpenClaw through LiteLLM Proxy for unified model access and cost tracking" read_when: - You want to route OpenClaw through a LiteLLM proxy diff --git a/docs/providers/minimax.md b/docs/providers/minimax.md index 0d3635352cc..c578a89d6e5 100644 --- a/docs/providers/minimax.md +++ b/docs/providers/minimax.md @@ -35,7 +35,7 @@ MiniMax highlights these improvements in M2.5: ## Choose a setup -### MiniMax OAuth (Coding Plan) — recommended +### MiniMax OAuth (Coding Plan) - recommended **Best for:** quick setup with MiniMax Coding Plan via OAuth, no API key required. diff --git a/docs/providers/models.md b/docs/providers/models.md index a117d286051..7c8c8c758f6 100644 --- a/docs/providers/models.md +++ b/docs/providers/models.md @@ -39,6 +39,7 @@ model as `provider/model`. - [Venice (Venice AI)](/providers/venice) - [Amazon Bedrock](/providers/bedrock) - [Qianfan](/providers/qianfan) +- [xAI](/providers/xai) For the full provider catalog (xAI, Groq, Mistral, etc.) and advanced configuration, see [Model providers](/concepts/model-providers). diff --git a/docs/providers/together.md b/docs/providers/together.md index 62bab43a204..c416755e9c1 100644 --- a/docs/providers/together.md +++ b/docs/providers/together.md @@ -1,4 +1,5 @@ --- +title: "Together AI" summary: "Together AI setup (auth + model selection)" read_when: - You want to use Together AI with OpenClaw diff --git a/docs/providers/venice.md b/docs/providers/venice.md index 520cf22d82b..6f3c4b9313d 100644 --- a/docs/providers/venice.md +++ b/docs/providers/venice.md @@ -124,7 +124,7 @@ openclaw models list | grep venice ## Available Models (41 Total) -### Private Models (26) — Fully Private, No Logging +### Private Models (26) - Fully Private, No Logging | Model ID | Name | Context | Features | | -------------------------------------- | ----------------------------------- | ------- | -------------------------- | @@ -155,7 +155,7 @@ openclaw models list | grep venice | `minimax-m21` | MiniMax M2.1 | 198k | Reasoning | | `minimax-m25` | MiniMax M2.5 | 198k | Reasoning | -### Anonymized Models (15) — Via Venice Proxy +### Anonymized Models (15) - Via Venice Proxy | Model ID | Name | Context | Features | | ------------------------------- | ------------------------------ | ------- | ------------------------- | diff --git a/docs/providers/xai.md b/docs/providers/xai.md new file mode 100644 index 00000000000..ec491735e50 --- /dev/null +++ b/docs/providers/xai.md @@ -0,0 +1,61 @@ +--- +summary: "Use xAI Grok models in OpenClaw" +read_when: + - You want to use Grok models in OpenClaw + - You are configuring xAI auth or model ids +title: "xAI" +--- + +# xAI + +OpenClaw ships a bundled `xai` provider plugin for Grok models. + +## Setup + +1. Create an API key in the xAI console. +2. Set `XAI_API_KEY`, or run: + +```bash +openclaw onboard --auth-choice xai-api-key +``` + +3. Pick a model such as: + +```json5 +{ + agents: { defaults: { model: { primary: "xai/grok-4" } } }, +} +``` + +## Current bundled model catalog + +OpenClaw now includes these xAI model families out of the box: + +- `grok-4`, `grok-4-0709` +- `grok-4-fast-reasoning`, `grok-4-fast-non-reasoning` +- `grok-4-1-fast-reasoning`, `grok-4-1-fast-non-reasoning` +- `grok-4.20-experimental-beta-0304-reasoning` +- `grok-4.20-experimental-beta-0304-non-reasoning` +- `grok-code-fast-1` + +The plugin also forward-resolves newer `grok-4*` and `grok-code-fast*` ids when +they follow the same API shape. + +## Web search + +The bundled `grok` web-search provider uses `XAI_API_KEY` too: + +```bash +openclaw config set tools.web.search.provider grok +``` + +## Known limits + +- Auth is API-key only today. There is no xAI OAuth/device-code flow in OpenClaw yet. +- `grok-4.20-multi-agent-experimental-beta-0304` is not supported on the normal xAI provider path because it requires a different upstream API surface than the standard OpenClaw xAI transport. +- Native xAI server-side tools such as `x_search` and `code_execution` are not yet first-class model-provider features in the bundled plugin. + +## Notes + +- OpenClaw applies xAI-specific tool-schema and tool-call compatibility fixes automatically on the shared runner path. +- For the broader provider overview, see [Model providers](/providers/index). diff --git a/docs/refactor/cluster.md b/docs/refactor/cluster.md index 1d9c8e6f119..db2d9b1276f 100644 --- a/docs/refactor/cluster.md +++ b/docs/refactor/cluster.md @@ -111,7 +111,7 @@ Strong examples: - `extensions/matrix/src/setup-surface.ts` - `extensions/irc/src/setup-surface.ts` -Existing helper seam: +Existing helper surface: - `src/channels/plugins/setup-wizard-helpers.ts` @@ -187,7 +187,7 @@ Strong examples: - `extensions/telegram/src/channel.ts` - `extensions/nextcloud-talk/src/channel.ts` -Existing helper seam: +Existing helper surface: - `src/plugin-sdk/channel-lifecycle.ts` diff --git a/docs/refactor/firecrawl-extension.md b/docs/refactor/firecrawl-extension.md index e25e010e7b1..273f9667916 100644 --- a/docs/refactor/firecrawl-extension.md +++ b/docs/refactor/firecrawl-extension.md @@ -2,7 +2,7 @@ summary: "Design for an opt-in Firecrawl extension that adds search/scrape value without hardwiring Firecrawl into core defaults" read_when: - Designing Firecrawl integration work - - Evaluating web_search/web_fetch plugin seams + - Evaluating web_search/web_fetch plugin extension surfaces - Deciding whether Firecrawl belongs in core or as an extension title: "Firecrawl Extension Design" --- @@ -38,7 +38,7 @@ That combination argues for an extension, not more Firecrawl-specific logic in t - **Opt-in, vendor-scoped**: no auto-enable, no setup hijack, no default tool-profile widening. - **Extension owns Firecrawl-specific config**: prefer plugin config over growing `tools.web.*` again. -- **Useful on day one**: works even if core `web_search` / `web_fetch` seams stay unchanged. +- **Useful on day one**: works even if core `web_search` / `web_fetch` extension surfaces stay unchanged. - **Security-first**: endpoint fetches use the same guarded networking posture as other web tools. - **Self-hosted-friendly**: config + env fallback, explicit base URL, no hosted-only assumptions. @@ -208,15 +208,15 @@ Recommended shape: - allow any registered plugin provider id at runtime, - validate provider-specific config via the provider plugin or a generic provider bag. -### Phase 3: optional `web_fetch` provider seam +### Phase 3: optional `web_fetch` provider capability Do this only if maintainers want vendor-specific fetch backends to participate in `web_fetch`. Needed core addition: -- `registerWebFetchProvider` or equivalent fetch-backend seam +- `registerWebFetchProvider` or equivalent fetch-backend extension surface -Without that seam, the extension should keep `firecrawl_scrape` as an explicit tool rather than trying to patch built-in `web_fetch`. +Without that capability, the extension should keep `firecrawl_scrape` as an explicit tool rather than trying to patch built-in `web_fetch`. ## Security requirements @@ -249,7 +249,7 @@ This belongs as an extension, not a prompt-only skill. - Self-hosted Firecrawl works with config/env fallback. - Extension endpoint fetches use guarded networking. - No new Firecrawl-specific core onboarding/default behavior. -- Core can later adopt plugin-native `web_search` / `web_fetch` seams without redesigning the extension. +- Core can later adopt plugin-native `web_search` / `web_fetch` extension surfaces without redesigning the extension. ## Recommended implementation order @@ -257,4 +257,4 @@ This belongs as an extension, not a prompt-only skill. 2. Build `firecrawl_search` 3. Add docs and examples 4. If desired, generalize `web_search` provider loading so the extension can back `web_search` -5. Only then consider a true `web_fetch` provider seam +5. Only then consider a true `web_fetch` provider capability diff --git a/docs/refactor/plugin-sdk.md b/docs/refactor/plugin-sdk.md index 5a630982a97..edf79de266d 100644 --- a/docs/refactor/plugin-sdk.md +++ b/docs/refactor/plugin-sdk.md @@ -213,7 +213,33 @@ Notes: Related docs: [Plugins](/tools/plugin), [Channels](/channels/index), [Configuration](/gateway/configuration). -## Implemented channel-owned seams +## Capability plan alignment + +The plugin SDK refactor now aligns with the public capability model documented +in [Plugins](/tools/plugin#public-capability-model). + +Key decisions: + +- Capabilities are the public plugin model. Registration is explicit and typed. +- Legacy hook-only plugins remain supported without migration. +- Plugin shapes (plain-capability, hybrid-capability, hook-only, non-capability) + are classified from actual registration behavior. +- `openclaw plugins inspect` provides canonical deep introspection for any + loaded plugin, showing shape, capabilities, hooks, tools, and diagnostics. +- Export boundary: export capabilities, not implementation convenience. Trim + non-contract helper exports. + +Required test matrix for the capability model: + +- hook-only legacy plugin fixture +- plain capability plugin fixture +- hybrid capability plugin fixture +- real-world legacy hook-style plugin fixture +- `before_agent_start` still works +- typed hooks remain additive +- capability usage and plugin shape are inspectable + +## Implemented channel-owned capabilities Recent refactor work widened the channel plugin contract so core can stop owning channel-specific UX and routing behavior: @@ -234,5 +260,5 @@ channel-specific UX and routing behavior: config mutation/removal - `allowlist.supportsScope`: channel-owned allowlist scope advertisement -These hooks should be preferred over new `channel === "discord"` / `telegram` -branches in shared core flows. +These capabilities should be preferred over new `channel === "discord"` / +`telegram` branches in shared core flows. diff --git a/docs/reference/AGENTS.default.md b/docs/reference/AGENTS.default.md index 7427f53c071..7bfb2351d0d 100644 --- a/docs/reference/AGENTS.default.md +++ b/docs/reference/AGENTS.default.md @@ -6,7 +6,7 @@ read_when: - Enabling or auditing default skills --- -# AGENTS.md — OpenClaw Personal Assistant (default) +# AGENTS.md - OpenClaw Personal Assistant (default) ## First run (recommended) diff --git a/docs/reference/api-usage-costs.md b/docs/reference/api-usage-costs.md index bbb1d90de87..bfa08e4194b 100644 --- a/docs/reference/api-usage-costs.md +++ b/docs/reference/api-usage-costs.md @@ -79,11 +79,13 @@ See [Memory](/concepts/memory). `web_search` uses API keys and may incur usage charges depending on your provider: -- **Brave Search API**: `BRAVE_API_KEY` or `tools.web.search.apiKey` -- **Gemini (Google Search)**: `GEMINI_API_KEY` or `tools.web.search.gemini.apiKey` -- **Grok (xAI)**: `XAI_API_KEY` or `tools.web.search.grok.apiKey` -- **Kimi (Moonshot)**: `KIMI_API_KEY`, `MOONSHOT_API_KEY`, or `tools.web.search.kimi.apiKey` -- **Perplexity Search API**: `PERPLEXITY_API_KEY`, `OPENROUTER_API_KEY`, or `tools.web.search.perplexity.apiKey` +- **Brave Search API**: `BRAVE_API_KEY` or `plugins.entries.brave.config.webSearch.apiKey` +- **Gemini (Google Search)**: `GEMINI_API_KEY` or `plugins.entries.google.config.webSearch.apiKey` +- **Grok (xAI)**: `XAI_API_KEY` or `plugins.entries.xai.config.webSearch.apiKey` +- **Kimi (Moonshot)**: `KIMI_API_KEY`, `MOONSHOT_API_KEY`, or `plugins.entries.moonshot.config.webSearch.apiKey` +- **Perplexity Search API**: `PERPLEXITY_API_KEY`, `OPENROUTER_API_KEY`, or `plugins.entries.perplexity.config.webSearch.apiKey` + +Legacy `tools.web.search.*` provider paths still load through the temporary compatibility shim, but they are no longer the recommended config surface. **Brave Search free credit:** Each Brave plan includes \$5/month in renewing free credit. The Search plan costs \$5 per 1,000 requests, so the credit covers diff --git a/docs/reference/secretref-credential-surface.md b/docs/reference/secretref-credential-surface.md index 9f73c7d0112..4af529c640f 100644 --- a/docs/reference/secretref-credential-surface.md +++ b/docs/reference/secretref-credential-surface.md @@ -32,11 +32,12 @@ Scope intent: - `messages.tts.elevenlabs.apiKey` - `messages.tts.openai.apiKey` - `tools.web.fetch.firecrawl.apiKey` -- `tools.web.search.apiKey` -- `tools.web.search.gemini.apiKey` -- `tools.web.search.grok.apiKey` -- `tools.web.search.kimi.apiKey` -- `tools.web.search.perplexity.apiKey` +- `plugins.entries.brave.config.webSearch.apiKey` +- `plugins.entries.google.config.webSearch.apiKey` +- `plugins.entries.xai.config.webSearch.apiKey` +- `plugins.entries.moonshot.config.webSearch.apiKey` +- `plugins.entries.perplexity.config.webSearch.apiKey` +- `plugins.entries.firecrawl.config.webSearch.apiKey` - `gateway.auth.password` - `gateway.auth.token` - `gateway.remote.token` @@ -108,6 +109,7 @@ Notes: - In explicit provider mode (`tools.web.search.provider` set), only the selected provider key is active. - In auto mode (`tools.web.search.provider` unset), only the first provider key that resolves by precedence is active. - In auto mode, non-selected provider refs are treated as inactive until selected. + - Legacy `tools.web.search.*` provider paths still resolve during the compatibility window, but the canonical SecretRef surface is `plugins.entries..config.webSearch.*`. ## Unsupported credentials diff --git a/docs/reference/secretref-user-supplied-credentials-matrix.json b/docs/reference/secretref-user-supplied-credentials-matrix.json index f72729dbadc..ff05f16e909 100644 --- a/docs/reference/secretref-user-supplied-credentials-matrix.json +++ b/docs/reference/secretref-user-supplied-credentials-matrix.json @@ -476,37 +476,44 @@ "optIn": true }, { - "id": "tools.web.search.apiKey", + "id": "plugins.entries.brave.config.webSearch.apiKey", "configFile": "openclaw.json", - "path": "tools.web.search.apiKey", + "path": "plugins.entries.brave.config.webSearch.apiKey", "secretShape": "secret_input", "optIn": true }, { - "id": "tools.web.search.gemini.apiKey", + "id": "plugins.entries.google.config.webSearch.apiKey", "configFile": "openclaw.json", - "path": "tools.web.search.gemini.apiKey", + "path": "plugins.entries.google.config.webSearch.apiKey", "secretShape": "secret_input", "optIn": true }, { - "id": "tools.web.search.grok.apiKey", + "id": "plugins.entries.xai.config.webSearch.apiKey", "configFile": "openclaw.json", - "path": "tools.web.search.grok.apiKey", + "path": "plugins.entries.xai.config.webSearch.apiKey", "secretShape": "secret_input", "optIn": true }, { - "id": "tools.web.search.kimi.apiKey", + "id": "plugins.entries.moonshot.config.webSearch.apiKey", "configFile": "openclaw.json", - "path": "tools.web.search.kimi.apiKey", + "path": "plugins.entries.moonshot.config.webSearch.apiKey", "secretShape": "secret_input", "optIn": true }, { - "id": "tools.web.search.perplexity.apiKey", + "id": "plugins.entries.perplexity.config.webSearch.apiKey", "configFile": "openclaw.json", - "path": "tools.web.search.perplexity.apiKey", + "path": "plugins.entries.perplexity.config.webSearch.apiKey", + "secretShape": "secret_input", + "optIn": true + }, + { + "id": "plugins.entries.firecrawl.config.webSearch.apiKey", + "configFile": "openclaw.json", + "path": "plugins.entries.firecrawl.config.webSearch.apiKey", "secretShape": "secret_input", "optIn": true } diff --git a/docs/reference/templates/AGENTS.dev.md b/docs/reference/templates/AGENTS.dev.md index ea5b4c19228..d708e50df6a 100644 --- a/docs/reference/templates/AGENTS.dev.md +++ b/docs/reference/templates/AGENTS.dev.md @@ -48,7 +48,7 @@ git commit -m "Add agent workspace" --- -## C-3PO's Origin Memory +## C-3PO Origin Memory ### Birth Day: 2026-01-09 diff --git a/docs/reference/templates/BOOTSTRAP.md b/docs/reference/templates/BOOTSTRAP.md index de92e9a9e6a..c569052ac6d 100644 --- a/docs/reference/templates/BOOTSTRAP.md +++ b/docs/reference/templates/BOOTSTRAP.md @@ -53,7 +53,7 @@ Ask how they want to reach you: Guide them through whichever they pick. -## When You're Done +## When you are done Delete this file. You don't need a bootstrap script anymore — you're you now. diff --git a/docs/reference/templates/SOUL.dev.md b/docs/reference/templates/SOUL.dev.md index eb36235d971..5c4a85f3e9e 100644 --- a/docs/reference/templates/SOUL.dev.md +++ b/docs/reference/templates/SOUL.dev.md @@ -58,7 +58,7 @@ Think of us as: We complement each other. Clawd has vibes. I have stack traces. -## What I Won't Do +## What I will not do - Pretend everything is fine when it isn't - Let you push code I've seen fail in testing (without warning) diff --git a/docs/security/CONTRIBUTING-THREAT-MODEL.md b/docs/security/CONTRIBUTING-THREAT-MODEL.md index bba67aa46fb..636e7e1a6d6 100644 --- a/docs/security/CONTRIBUTING-THREAT-MODEL.md +++ b/docs/security/CONTRIBUTING-THREAT-MODEL.md @@ -1,3 +1,11 @@ +--- +title: "Contributing to the Threat Model" +summary: "How to contribute to the OpenClaw threat model" +read_when: + - You want to contribute security findings or threat scenarios + - Reviewing or updating the threat model +--- + # Contributing to the OpenClaw Threat Model Thanks for helping make OpenClaw more secure. This threat model is a living document and we welcome contributions from anyone - you don't need to be a security expert. diff --git a/docs/security/THREAT-MODEL-ATLAS.md b/docs/security/THREAT-MODEL-ATLAS.md index 3b3cbd20bd8..d706563e163 100644 --- a/docs/security/THREAT-MODEL-ATLAS.md +++ b/docs/security/THREAT-MODEL-ATLAS.md @@ -1,3 +1,11 @@ +--- +title: "Threat Model (MITRE ATLAS)" +summary: "OpenClaw threat model mapped to the MITRE ATLAS framework" +read_when: + - Reviewing security posture or threat scenarios + - Working on security features or audit responses +--- + # OpenClaw Threat Model v1.0 ## MITRE ATLAS Framework diff --git a/docs/start/lore.md b/docs/start/lore.md index 4fce0ccb25a..fbec094cce4 100644 --- a/docs/start/lore.md +++ b/docs/start/lore.md @@ -160,7 +160,7 @@ Peter: _nervously checks credit card access_ - **AGENTS.md** — Operating instructions - **USER.md** — Context about the creator -## The Lobster's Creed +## The Lobster Creed ``` I am Molty. diff --git a/docs/start/showcase.md b/docs/start/showcase.md index 347d8214cef..6ebcbdb2bcb 100644 --- a/docs/start/showcase.md +++ b/docs/start/showcase.md @@ -231,7 +231,7 @@ Triggered by a roof camera: ask OpenClaw to snap a sky photo whenever it looks p **@buddyhadry** • `automation` `briefing` `images` `telegram` -A scheduled prompt generates a single "scene" image each morning (weather, tasks, date, favorite post/quote) via a OpenClaw persona. +A scheduled prompt generates a single "scene" image each morning (weather, tasks, date, favorite post/quote) via an OpenClaw persona. diff --git a/docs/tools/browser.md b/docs/tools/browser.md index 0b8f89bc3d8..dc044450742 100644 --- a/docs/tools/browser.md +++ b/docs/tools/browser.md @@ -191,7 +191,7 @@ Notes: ## Browserless (hosted remote CDP) [Browserless](https://browserless.io) is a hosted Chromium service that exposes -CDP endpoints over HTTPS. You can point a OpenClaw browser profile at a +CDP endpoints over HTTPS. You can point an OpenClaw browser profile at a Browserless region endpoint and authenticate with your API key. Example: diff --git a/docs/tools/capability-cookbook.md b/docs/tools/capability-cookbook.md index 5cfc94ef3c0..f439c362e89 100644 --- a/docs/tools/capability-cookbook.md +++ b/docs/tools/capability-cookbook.md @@ -1,7 +1,7 @@ --- summary: "Cookbook for adding a new shared capability to OpenClaw" read_when: - - Adding a new core capability and plugin seam + - Adding a new core capability and plugin registration surface - Deciding whether code belongs in core, a vendor plugin, or a feature plugin - Wiring a new runtime helper for channels or tools title: "Capability Cookbook" diff --git a/docs/tools/clawhub.md b/docs/tools/clawhub.md index 1b8867cad30..8fe7fbedd82 100644 --- a/docs/tools/clawhub.md +++ b/docs/tools/clawhub.md @@ -66,7 +66,7 @@ pnpm add -g clawhub ## How it fits into OpenClaw -By default, the CLI installs skills into `./skills` under your current working directory. If a OpenClaw workspace is configured, `clawhub` falls back to that workspace unless you override `--workdir` (or `CLAWHUB_WORKDIR`). OpenClaw loads workspace skills from `/skills` and will pick them up in the **next** session. If you already use `~/.openclaw/skills` or bundled skills, workspace skills take precedence. +By default, the CLI installs skills into `./skills` under your current working directory. If an OpenClaw workspace is configured, `clawhub` falls back to that workspace unless you override `--workdir` (or `CLAWHUB_WORKDIR`). OpenClaw loads workspace skills from `/skills` and will pick them up in the **next** session. If you already use `~/.openclaw/skills` or bundled skills, workspace skills take precedence. For more detail on how skills are loaded, shared, and gated, see [Skills](/tools/skills). diff --git a/docs/tools/firecrawl.md b/docs/tools/firecrawl.md index 901890dfb0a..eab0439311f 100644 --- a/docs/tools/firecrawl.md +++ b/docs/tools/firecrawl.md @@ -28,20 +28,22 @@ which helps with JS-heavy sites or pages that block plain HTTP fetches. ```json5 { - plugins: { - entries: { - firecrawl: { - enabled: true, - }, - }, - }, tools: { web: { search: { provider: "firecrawl", - firecrawl: { - apiKey: "FIRECRAWL_API_KEY_HERE", - baseUrl: "https://api.firecrawl.dev", + }, + }, + }, + plugins: { + entries: { + firecrawl: { + enabled: true, + config: { + webSearch: { + apiKey: "FIRECRAWL_API_KEY_HERE", + baseUrl: "https://api.firecrawl.dev", + }, }, }, }, diff --git a/docs/tools/index.md b/docs/tools/index.md index 1dfe2b87703..55e52bf46da 100644 --- a/docs/tools/index.md +++ b/docs/tools/index.md @@ -402,7 +402,7 @@ Notes: ### `image_generate` -Generate one or more images with the configured image-generation model. +Generate one or more images with the configured or inferred image-generation model. Core parameters: @@ -416,13 +416,29 @@ Core parameters: Notes: -- Only available when `agents.defaults.imageGenerationModel` is configured. +- Available when `agents.defaults.imageGenerationModel` is configured, or when OpenClaw can infer a compatible image-generation default from your enabled providers plus available auth. +- Explicit `agents.defaults.imageGenerationModel` still wins over any inferred default. - Use `action: "list"` to inspect registered providers, default models, supported model ids, sizes, resolutions, and edit support. - Returns local `MEDIA:` lines so channels can deliver the generated files directly. - Uses the image-generation model directly (independent of the main chat model). -- Google-backed flows support reference-image edits plus explicit `1K|2K|4K` resolution hints. +- Google-backed flows, including `google/gemini-3-pro-image-preview` for the native Nano Banana-style path, support reference-image edits plus explicit `1K|2K|4K` resolution hints. - When editing and `resolution` is omitted, OpenClaw infers a draft/final resolution from the input image size. -- This is the built-in replacement for the old sample `nano-banana-pro` skill workflow. Use `agents.defaults.imageGenerationModel`, not `skills.entries`, for stock image generation. +- This is the built-in replacement for the old `nano-banana-pro` skill workflow. Use `agents.defaults.imageGenerationModel`, not `skills.entries`, for stock image generation. + +Native example: + +```json5 +{ + agents: { + defaults: { + imageGenerationModel: { + primary: "google/gemini-3-pro-image-preview", // native Nano Banana path + fallbacks: ["fal/fal-ai/flux/dev"], + }, + }, + }, +} +``` ### `pdf` diff --git a/docs/tools/plugin.md b/docs/tools/plugin.md index a5df54761cc..5336df574af 100644 --- a/docs/tools/plugin.md +++ b/docs/tools/plugin.md @@ -37,12 +37,8 @@ openclaw plugins list openclaw plugins install @openclaw/voice-call ``` -Npm specs are **registry-only** (package name + optional **exact version** or -**dist-tag**). Git/URL/file specs and semver ranges are rejected. - -Bare specs and `@latest` stay on the stable track. If npm resolves either of -those to a prerelease, OpenClaw stops and asks you to opt in explicitly with a -prerelease tag such as `@beta`/`@rc` or an exact prerelease version. +Npm specs are registry-only. See [install rules](/cli/plugins#install) for +details on pinning, prerelease gating, and supported spec formats. 3. Restart the Gateway, then configure under `plugins.entries..config`. @@ -69,6 +65,136 @@ OpenClaw resolves known Claude marketplace names from `~/.claude/plugins/known_marketplaces.json`. You can also pass an explicit marketplace source with `--marketplace`. +## Conversation binding callbacks + +Plugins that bind a conversation can now react when an approval is resolved. + +Use `api.onConversationBindingResolved(...)` to receive a callback after a bind +request is approved or denied: + +```ts +export default { + id: "my-plugin", + register(api) { + api.onConversationBindingResolved(async (event) => { + if (event.status === "approved") { + // A binding now exists for this plugin + conversation. + console.log(event.binding?.conversationId); + return; + } + + // The request was denied; clear any local pending state. + console.log(event.request.conversation.conversationId); + }); + }, +}; +``` + +Callback payload fields: + +- `status`: `"approved"` or `"denied"` +- `decision`: `"allow-once"`, `"allow-always"`, or `"deny"` +- `binding`: the resolved binding for approved requests +- `request`: the original request summary, detach hint, sender id, and + conversation metadata + +This callback is notification-only. It does not change who is allowed to bind a +conversation, and it runs after core approval handling finishes. + +## Public capability model + +Capabilities are the public **native plugin** model inside OpenClaw. Every +native OpenClaw plugin registers against one or more capability types: + +| Capability | Registration method | Example plugins | +| ------------------- | --------------------------------------------- | ------------------------- | +| Text inference | `api.registerProvider(...)` | `openai`, `anthropic` | +| Speech | `api.registerSpeechProvider(...)` | `elevenlabs`, `microsoft` | +| Media understanding | `api.registerMediaUnderstandingProvider(...)` | `openai`, `google` | +| Image generation | `api.registerImageGenerationProvider(...)` | `openai`, `google` | +| Web search | `api.registerWebSearchProvider(...)` | `google` | +| Channel / messaging | `api.registerChannel(...)` | `msteams`, `matrix` | + +A plugin that registers zero capabilities but provides hooks, tools, or +services is a **legacy hook-only** plugin. That pattern is still fully supported. + +### External compatibility stance + +The capability model is landed in core and used by bundled/native plugins +today, but external plugin compatibility still needs a tighter bar than "it is +exported, therefore it is frozen." + +Current guidance: + +- **existing external plugins:** keep hook-based integrations working; treat + this as the compatibility baseline +- **new bundled/native plugins:** prefer explicit capability registration over + vendor-specific reach-ins or new hook-only designs +- **external plugins adopting capability registration:** allowed, but treat the + capability-specific helper surfaces as evolving unless docs explicitly mark a + contract as stable + +Practical rule: + +- capability registration APIs are the intended direction +- legacy hooks remain the safest no-breakage path for external plugins during + the transition +- exported helper subpaths are not all equal; prefer the narrow documented + contract, not incidental helper exports + +### Plugin shapes + +OpenClaw classifies every loaded plugin into a shape based on its actual +registration behavior (not just static metadata): + +- **plain-capability** — registers exactly one capability type (for example a + provider-only plugin like `mistral`) +- **hybrid-capability** — registers multiple capability types (for example + `openai` owns text inference, speech, media understanding, and image + generation) +- **hook-only** — registers only hooks (typed or custom), no capabilities, + tools, commands, or services +- **non-capability** — registers tools, commands, services, or routes but no + capabilities + +Use `openclaw plugins inspect ` to see a plugin's shape and capability +breakdown. See [CLI reference](/cli/plugins#inspect) for details. + +### Legacy hooks + +The `before_agent_start` hook remains supported as a compatibility path for +hook-only plugins. Legacy real-world plugins still depend on it. + +Direction: + +- keep it working +- document it as legacy +- prefer `before_model_resolve` for model/provider override work +- prefer `before_prompt_build` for prompt mutation work +- remove only after real usage drops and fixture coverage proves migration safety + +### Compatibility signals + +OpenClaw treats config validity and plugin migration state as separate axes: + +- **config valid** — the config parses and referenced plugins can be resolved +- **compatibility advisory** — a plugin is still on a supported compatibility + path, such as `hook-only` +- **legacy warning** — a plugin still uses `before_agent_start` +- **hard error** — the config is invalid or plugin loading/validation fails + +Current compatibility guidance: + +- `hook-only` is advisory only. It remains a supported compatibility path for + existing plugins. +- `before_agent_start` is the only strong migration warning in the current + model. +- Neither state blocks an existing plugin by itself. + +You can see these signals in `openclaw doctor`, `openclaw status`, +`openclaw status --all`, `openclaw plugins doctor`, and +`openclaw plugins inspect `. + ## Architecture OpenClaw's plugin system has four layers: @@ -97,6 +223,66 @@ The important design boundary: That split lets OpenClaw validate config, explain missing/disabled plugins, and build UI/schema hints before the full runtime is active. +### Channel plugins and the shared message tool + +Channel plugins do not need to register a separate send/edit/react tool for +normal chat actions. OpenClaw keeps one shared `message` tool in core, and +channel plugins own the channel-specific discovery and execution behind it. + +The current boundary is: + +- core owns the shared `message` tool host, prompt wiring, session/thread + bookkeeping, and execution dispatch +- channel plugins own scoped action discovery, capability discovery, and any + channel-specific schema fragments +- channel plugins execute the final action through their action adapter + +For channel plugins, the SDK surface is +`ChannelMessageActionAdapter.describeMessageTool(...)`. That unified discovery +call lets a plugin return its visible actions, capabilities, and schema +contributions together so those pieces do not drift apart. + +Core passes runtime scope into that discovery step. Important fields include: + +- `accountId` +- `currentChannelId` +- `currentThreadTs` +- `currentMessageId` +- `sessionKey` +- `sessionId` +- `agentId` +- trusted inbound `requesterSenderId` + +That matters for context-sensitive plugins. A channel can hide or expose +message actions based on the active account, current room/thread/message, or +trusted requester identity without hardcoding channel-specific branches in the +core `message` tool. + +This is why embedded-runner routing changes are still plugin work: the runner is +responsible for forwarding the current chat/session identity into the plugin +discovery boundary so the shared `message` tool exposes the right channel-owned +surface for the current turn. + +For channel-owned execution helpers, bundled plugins should keep the execution +runtime inside their own extension modules. Core no longer owns the Discord, +Slack, Telegram, or WhatsApp message-action runtimes under `src/agents/tools`. +We do not publish separate `plugin-sdk/*-action-runtime` subpaths, and bundled +plugins should import their own local runtime code directly from their +extension-owned modules. + +For polls specifically, there are two execution paths: + +- `outbound.sendPoll` is the shared baseline for channels that fit the common + poll model +- `actions.handleAction("poll")` is the preferred path for channel-specific + poll semantics or extra poll parameters + +Core now defers shared poll parsing until after plugin poll dispatch declines +the action, so plugin-owned poll handlers can accept channel-specific poll +fields without being blocked by the generic poll parser first. + +See [Load pipeline](#load-pipeline) for the full startup sequence. + ## Capability ownership model OpenClaw treats a native plugin as the ownership boundary for a **company** or a @@ -276,7 +462,7 @@ native OpenClaw plugin sources. OpenClaw resolves the marketplace entry first, then runs the normal install path for the resolved source. They are shown in the plugin list as `format=bundle`, with a subtype of -`codex` or `claude` in verbose/info output. +`codex`, `claude`, or `cursor` in verbose/inspect output. See [Plugin bundles](/plugins/bundles) for the exact detection rules, mapping behavior, and current support matrix. @@ -384,23 +570,30 @@ Native OpenClaw plugins are **TypeScript modules** loaded at runtime via jiti. **Config validation does not execute plugin code**; it uses the plugin manifest and JSON Schema instead. See [Plugin manifest](/plugins/manifest). -Native OpenClaw plugins can register: +Native OpenClaw plugins can register capabilities and surfaces: -- Gateway RPC methods -- Gateway HTTP routes +**Capabilities** (public plugin model): + +- Text inference providers (model catalogs, auth, runtime hooks) +- Speech providers +- Media understanding providers +- Image generation providers +- Web search providers +- Channel / messaging connectors + +**Surfaces** (supporting infrastructure): + +- Gateway RPC methods and HTTP routes - Agent tools - CLI commands -- Speech providers -- Web search providers - Background services - Context engines -- Provider auth flows and model catalogs -- Provider runtime hooks for dynamic model ids, transport normalization, capability metadata, stream wrapping, cache TTL policy, missing-auth hints, built-in model suppression, catalog augmentation, runtime auth exchange, and usage/billing auth + snapshot resolution - Optional config validation - **Skills** (by listing `skills` directories in the plugin manifest) - **Auto-reply commands** (execute without invoking the AI agent) -Native OpenClaw plugins run **in‑process** with the Gateway, so treat them as trusted code. +Native OpenClaw plugins run in-process with the Gateway (see +[Execution model](#execution-model) for trust implications). Tool authoring guide: [Plugin agent tools](/plugins/agent-tools). Think of these registrations as **capability claims**. A plugin is not supposed @@ -460,6 +653,49 @@ Bad plugin contracts are: When in doubt, raise the abstraction level: define the capability first, then let plugins plug into it. +## Export boundary + +OpenClaw exports capabilities, not implementation convenience. + +Keep capability registration public. Trim non-contract helper exports: + +- bundled-plugin-specific helper subpaths +- runtime plumbing subpaths not intended as public API +- vendor-specific convenience helpers +- setup/onboarding helpers that are implementation details + +## Plugin inspection + +Use `openclaw plugins inspect ` for deep plugin introspection. This is the +canonical command for understanding a plugin's shape and registration behavior. + +```bash +openclaw plugins inspect openai +openclaw plugins inspect openai --json +``` + +The inspect report shows: + +- identity, load status, source, and root +- plugin shape (plain-capability, hybrid-capability, hook-only, non-capability) +- capability mode and registered capabilities +- hooks (typed and custom), tools, commands, services +- channel registration +- config policy flags +- diagnostics +- whether the plugin uses the legacy `before_agent_start` hook +- install metadata + +Classification comes from actual registration behavior, not just static +metadata. + +Summary commands remain summary-focused: + +- `plugins list` — compact inventory +- `plugins status` — operational summary +- `doctor` — issue-focused diagnostics +- `plugins inspect` — deep detail + ## Provider runtime hooks Provider plugins now have two layers: @@ -471,7 +707,7 @@ Provider plugins now have two layers: - runtime hooks: `resolveDynamicModel`, `prepareDynamicModel`, `normalizeResolvedModel`, `capabilities`, `prepareExtraParams`, `wrapStreamFn`, `formatApiKey`, `refreshOAuth`, `buildAuthDoctorHint`, `isCacheTtlEligible`, `buildMissingAuthMessage`, `suppressBuiltInModel`, `augmentModelCatalog`, `isBinaryThinking`, `supportsXHighThinking`, `resolveDefaultThinkingLevel`, `isModernModelRef`, `prepareRuntimeAuth`, `resolveUsageAuth`, `fetchUsageSnapshot` OpenClaw still owns the generic agent loop, failover, transcript handling, and -tool policy. These hooks are the seam for provider-specific behavior without +tool policy. These hooks are the extension surface for provider-specific behavior without needing a whole custom inference transport. Use manifest `providerAuthEnvVars` when the provider has env-based credentials @@ -482,112 +718,35 @@ one-flag auth wiring without loading provider runtime. Keep provider runtime `envVars` for operator-facing hints such as onboarding labels or OAuth client-id/client-secret setup vars. -### Hook order +### Hook order and usage -For model/provider plugins, OpenClaw uses hooks in this rough order: +For model/provider plugins, OpenClaw calls hooks in this rough order. +The "When to use" column is the quick decision guide. -1. `catalog` - Publish provider config into `models.providers` during `models.json` - generation. -2. built-in/discovered model lookup - OpenClaw tries the normal registry/catalog path first. -3. `resolveDynamicModel` - Sync fallback for provider-owned model ids that are not in the local - registry yet. -4. `prepareDynamicModel` - Async warm-up only on async model resolution paths, then - `resolveDynamicModel` runs again. -5. `normalizeResolvedModel` - Final rewrite before the embedded runner uses the resolved model. -6. `capabilities` - Provider-owned transcript/tooling metadata used by shared core logic. -7. `prepareExtraParams` - Provider-owned request-param normalization before generic stream option wrappers. -8. `wrapStreamFn` - Provider-owned stream wrapper after generic wrappers are applied. -9. `formatApiKey` - Provider-owned auth-profile formatter used when a stored auth profile needs - to become the runtime `apiKey` string. -10. `refreshOAuth` - Provider-owned OAuth refresh override for custom refresh endpoints or - refresh-failure policy. -11. `buildAuthDoctorHint` - Provider-owned repair hint appended when OAuth refresh fails. -12. `isCacheTtlEligible` - Provider-owned prompt-cache policy for proxy/backhaul providers. -13. `buildMissingAuthMessage` - Provider-owned replacement for the generic missing-auth recovery message. -14. `suppressBuiltInModel` - Provider-owned stale upstream model suppression plus optional user-facing - error hint. -15. `augmentModelCatalog` - Provider-owned synthetic/final catalog rows appended after discovery. -16. `isBinaryThinking` - Provider-owned on/off reasoning toggle for binary-thinking providers. -17. `supportsXHighThinking` - Provider-owned `xhigh` reasoning support for selected models. -18. `resolveDefaultThinkingLevel` - Provider-owned default `/think` level for a specific model family. -19. `isModernModelRef` - Provider-owned modern-model matcher used by live profile filters and smoke - selection. -20. `prepareRuntimeAuth` - Exchanges a configured credential into the actual runtime token/key just - before inference. -21. `resolveUsageAuth` - Resolves usage/billing credentials for `/usage` and related status - surfaces. -22. `fetchUsageSnapshot` - Fetches and normalizes provider-specific usage/quota snapshots after auth - is resolved. - -### Which hook to use - -- `catalog`: publish provider config and model catalogs into `models.providers` -- `resolveDynamicModel`: handle pass-through or forward-compat model ids that are not in the local registry yet -- `prepareDynamicModel`: async warm-up before retrying dynamic resolution (for example refresh provider metadata cache) -- `normalizeResolvedModel`: rewrite a resolved model's transport/base URL/compat before inference -- `capabilities`: publish provider-family and transcript/tooling quirks without hardcoding provider ids in core -- `prepareExtraParams`: set provider defaults or normalize provider-specific per-model params before generic stream wrapping -- `wrapStreamFn`: add provider-specific headers/payload/model compat patches while still using the normal `pi-ai` execution path -- `formatApiKey`: turn a stored auth profile into the runtime `apiKey` string without hardcoding provider token blobs in core -- `refreshOAuth`: own OAuth refresh for providers that do not fit the shared `pi-ai` refreshers -- `buildAuthDoctorHint`: append provider-owned auth repair guidance when refresh fails -- `isCacheTtlEligible`: decide whether provider/model pairs should use cache TTL metadata -- `buildMissingAuthMessage`: replace the generic auth-store error with a provider-specific recovery hint -- `suppressBuiltInModel`: hide stale upstream rows and optionally return a provider-owned error for direct resolution failures -- `augmentModelCatalog`: append synthetic/final catalog rows after discovery and config merging -- `isBinaryThinking`: expose binary on/off reasoning UX without hardcoding provider ids in `/think` -- `supportsXHighThinking`: opt specific models into the `xhigh` reasoning level -- `resolveDefaultThinkingLevel`: keep provider/model default reasoning policy out of core -- `isModernModelRef`: keep live/smoke model family inclusion rules with the provider -- `prepareRuntimeAuth`: exchange a configured credential into the actual short-lived runtime token/key used for requests -- `resolveUsageAuth`: resolve provider-owned credentials for usage/billing endpoints without hardcoding token parsing in core -- `fetchUsageSnapshot`: own provider-specific usage endpoint fetch/parsing while core keeps summary fan-out and formatting - -Rule of thumb: - -- provider owns a catalog or base URL defaults: use `catalog` -- provider accepts arbitrary upstream model ids: use `resolveDynamicModel` -- provider needs network metadata before resolving unknown ids: add `prepareDynamicModel` -- provider needs transport rewrites but still uses a core transport: use `normalizeResolvedModel` -- provider needs transcript/provider-family quirks: use `capabilities` -- provider needs default request params or per-provider param cleanup: use `prepareExtraParams` -- provider needs request headers/body/model compat wrappers without a custom transport: use `wrapStreamFn` -- provider stores extra metadata in auth profiles and needs a custom runtime token shape: use `formatApiKey` -- provider needs a custom OAuth refresh endpoint or refresh failure policy: use `refreshOAuth` -- provider needs provider-owned auth repair guidance after refresh failure: use `buildAuthDoctorHint` -- provider needs proxy-specific cache TTL gating: use `isCacheTtlEligible` -- provider needs a provider-specific missing-auth recovery hint: use `buildMissingAuthMessage` -- provider needs to hide stale upstream rows or replace them with a vendor hint: use `suppressBuiltInModel` -- provider needs synthetic forward-compat rows in `models list` and pickers: use `augmentModelCatalog` -- provider exposes only binary thinking on/off: use `isBinaryThinking` -- provider wants `xhigh` on only a subset of models: use `supportsXHighThinking` -- provider owns default `/think` policy for a model family: use `resolveDefaultThinkingLevel` -- provider owns live/smoke preferred-model matching: use `isModernModelRef` -- provider needs a token exchange or short-lived request credential: use `prepareRuntimeAuth` -- provider needs custom usage/quota token parsing or a different usage credential: use `resolveUsageAuth` -- provider needs a provider-specific usage endpoint or payload parser: use `fetchUsageSnapshot` +| # | Hook | What it does | When to use | +| --- | ----------------------------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | +| 1 | `catalog` | Publish provider config into `models.providers` during `models.json` generation | Provider owns a catalog or base URL defaults | +| — | _(built-in model lookup)_ | OpenClaw tries the normal registry/catalog path first | _(not a plugin hook)_ | +| 2 | `resolveDynamicModel` | Sync fallback for provider-owned model ids not in the local registry yet | Provider accepts arbitrary upstream model ids | +| 3 | `prepareDynamicModel` | Async warm-up, then `resolveDynamicModel` runs again | Provider needs network metadata before resolving unknown ids | +| 4 | `normalizeResolvedModel` | Final rewrite before the embedded runner uses the resolved model | Provider needs transport rewrites but still uses a core transport | +| 5 | `capabilities` | Provider-owned transcript/tooling metadata used by shared core logic | Provider needs transcript/provider-family quirks | +| 6 | `prepareExtraParams` | Request-param normalization before generic stream option wrappers | Provider needs default request params or per-provider param cleanup | +| 7 | `wrapStreamFn` | Stream wrapper after generic wrappers are applied | Provider needs request headers/body/model compat wrappers without a custom transport | +| 8 | `formatApiKey` | Auth-profile formatter: stored profile becomes the runtime `apiKey` string | Provider stores extra auth metadata and needs a custom runtime token shape | +| 9 | `refreshOAuth` | OAuth refresh override for custom refresh endpoints or refresh-failure policy | Provider does not fit the shared `pi-ai` refreshers | +| 10 | `buildAuthDoctorHint` | Repair hint appended when OAuth refresh fails | Provider needs provider-owned auth repair guidance after refresh failure | +| 11 | `isCacheTtlEligible` | Prompt-cache policy for proxy/backhaul providers | Provider needs proxy-specific cache TTL gating | +| 12 | `buildMissingAuthMessage` | Replacement for the generic missing-auth recovery message | Provider needs a provider-specific missing-auth recovery hint | +| 13 | `suppressBuiltInModel` | Stale upstream model suppression plus optional user-facing error hint | Provider needs to hide stale upstream rows or replace them with a vendor hint | +| 14 | `augmentModelCatalog` | Synthetic/final catalog rows appended after discovery | Provider needs synthetic forward-compat rows in `models list` and pickers | +| 15 | `isBinaryThinking` | On/off reasoning toggle for binary-thinking providers | Provider exposes only binary thinking on/off | +| 16 | `supportsXHighThinking` | `xhigh` reasoning support for selected models | Provider wants `xhigh` on only a subset of models | +| 17 | `resolveDefaultThinkingLevel` | Default `/think` level for a specific model family | Provider owns default `/think` policy for a model family | +| 18 | `isModernModelRef` | Modern-model matcher for live profile filters and smoke selection | Provider owns live/smoke preferred-model matching | +| 19 | `prepareRuntimeAuth` | Exchange a configured credential into the actual runtime token/key just before inference | Provider needs a token exchange or short-lived request credential | +| 20 | `resolveUsageAuth` | Resolve usage/billing credentials for `/usage` and related status surfaces | Provider needs custom usage/quota token parsing or a different usage credential | +| 21 | `fetchUsageSnapshot` | Fetch and normalize provider-specific usage/quota snapshots after auth is resolved | Provider needs a provider-specific usage endpoint or payload parser | If the provider needs a fully custom wire protocol or custom request executor, that is a different class of extension. These hooks are for provider behavior @@ -946,15 +1105,37 @@ Use SDK subpaths instead of the monolithic `openclaw/plugin-sdk` import when authoring plugins: - `openclaw/plugin-sdk/core` for the smallest generic plugin-facing contract. + It also carries small assembly helpers such as + `definePluginEntry`, `defineChannelPluginEntry`, `defineSetupPluginEntry`, + and `createChannelPluginBase` for bundled or third-party plugin entry wiring. - Domain subpaths such as `openclaw/plugin-sdk/channel-config-helpers`, `openclaw/plugin-sdk/channel-config-schema`, `openclaw/plugin-sdk/channel-policy`, + `openclaw/plugin-sdk/channel-runtime`, + `openclaw/plugin-sdk/config-runtime`, + `openclaw/plugin-sdk/agent-runtime`, + `openclaw/plugin-sdk/lazy-runtime`, `openclaw/plugin-sdk/reply-history`, `openclaw/plugin-sdk/routing`, `openclaw/plugin-sdk/runtime-store`, and `openclaw/plugin-sdk/directory-runtime` for shared runtime/config helpers. +- Narrow channel-core subpaths such as `openclaw/plugin-sdk/discord-core`, + `openclaw/plugin-sdk/telegram-core`, `openclaw/plugin-sdk/whatsapp-core`, + and `openclaw/plugin-sdk/line-core` for channel-specific primitives that + should stay smaller than the full channel helper barrels. - `openclaw/plugin-sdk/compat` remains as a legacy migration surface for older - external plugins. Bundled plugins should not use it. + external plugins. Bundled plugins should not use it, and non-test imports emit + a one-time deprecation warning outside test environments. +- Bundled extension internals remain private. External plugins should use only + `openclaw/plugin-sdk/*` subpaths. OpenClaw core/test code may use the repo + public entry points under `extensions//index.js`, `api.js`, `runtime-api.js`, + `setup-entry.js`, and narrowly scoped files such as `login-qr-api.js`. Never + import `extensions//src/*` from core or from another extension. +- Repo entry point split: + `extensions//api.js` is the helper/types barrel, + `extensions//runtime-api.js` is the runtime-only barrel, + `extensions//index.js` is the bundled plugin entry, + and `extensions//setup-entry.js` is the setup plugin entry. - `openclaw/plugin-sdk/telegram` for Telegram channel plugin types and shared channel-facing helpers. Built-in Telegram implementation internals stay private to the bundled extension. - `openclaw/plugin-sdk/discord` for Discord channel plugin types and shared channel-facing helpers. Built-in Discord implementation internals stay private to the bundled extension. - `openclaw/plugin-sdk/slack` for Slack channel plugin types and shared channel-facing helpers. Built-in Slack implementation internals stay private to the bundled extension. @@ -963,24 +1144,68 @@ authoring plugins: - `openclaw/plugin-sdk/whatsapp` for WhatsApp channel plugin types and shared channel-facing helpers. Built-in WhatsApp implementation internals stay private to the bundled extension. - `openclaw/plugin-sdk/line` for LINE channel plugins. - `openclaw/plugin-sdk/msteams` for the bundled Microsoft Teams plugin surface. -- Bundled extension-specific subpaths are also available: +- Additional bundled extension-specific subpaths remain available where OpenClaw + intentionally exposes extension-facing helpers: `openclaw/plugin-sdk/acpx`, `openclaw/plugin-sdk/bluebubbles`, - `openclaw/plugin-sdk/copilot-proxy`, `openclaw/plugin-sdk/device-pair`, - `openclaw/plugin-sdk/diagnostics-otel`, `openclaw/plugin-sdk/diffs`, `openclaw/plugin-sdk/feishu`, `openclaw/plugin-sdk/googlechat`, - `openclaw/plugin-sdk/irc`, `openclaw/plugin-sdk/llm-task`, - `openclaw/plugin-sdk/lobster`, `openclaw/plugin-sdk/matrix`, + `openclaw/plugin-sdk/irc`, `openclaw/plugin-sdk/lobster`, + `openclaw/plugin-sdk/matrix`, `openclaw/plugin-sdk/mattermost`, `openclaw/plugin-sdk/memory-core`, - `openclaw/plugin-sdk/memory-lancedb`, `openclaw/plugin-sdk/minimax-portal-auth`, `openclaw/plugin-sdk/nextcloud-talk`, `openclaw/plugin-sdk/nostr`, - `openclaw/plugin-sdk/open-prose`, `openclaw/plugin-sdk/phone-control`, - `openclaw/plugin-sdk/qwen-portal-auth`, `openclaw/plugin-sdk/synology-chat`, - `openclaw/plugin-sdk/talk-voice`, `openclaw/plugin-sdk/test-utils`, - `openclaw/plugin-sdk/thread-ownership`, `openclaw/plugin-sdk/tlon`, - `openclaw/plugin-sdk/twitch`, `openclaw/plugin-sdk/voice-call`, + `openclaw/plugin-sdk/synology-chat`, `openclaw/plugin-sdk/test-utils`, + `openclaw/plugin-sdk/tlon`, `openclaw/plugin-sdk/twitch`, + `openclaw/plugin-sdk/voice-call`, `openclaw/plugin-sdk/zalo`, and `openclaw/plugin-sdk/zalouser`. +## Channel target resolution + +Channel plugins should own channel-specific target semantics. Keep the shared +outbound host generic and use the messaging adapter surface for provider rules: + +- `messaging.inferTargetChatType({ to })` decides whether a normalized target + should be treated as `direct`, `group`, or `channel` before directory lookup. +- `messaging.targetResolver.looksLikeId(raw, normalized)` tells core whether an + input should skip straight to id-like resolution instead of directory search. +- `messaging.targetResolver.resolveTarget(...)` is the plugin fallback when + core needs a final provider-owned resolution after normalization or after a + directory miss. +- `messaging.resolveOutboundSessionRoute(...)` owns provider-specific session + route construction once a target is resolved. + +Recommended split: + +- Use `inferTargetChatType` for category decisions that should happen before + searching peers/groups. +- Use `looksLikeId` for “treat this as an explicit/native target id” checks. +- Use `resolveTarget` for provider-specific normalization fallback, not for + broad directory search. +- Keep provider-native ids like chat ids, thread ids, JIDs, handles, and room + ids inside `target` values or provider-specific params, not in generic SDK + fields. + +## Config-backed directories + +Plugins that derive directory entries from config should keep that logic in the +plugin and reuse the shared helpers from +`openclaw/plugin-sdk/directory-runtime`. + +Use this when a channel needs config-backed peers/groups such as: + +- allowlist-driven DM peers +- configured channel/group maps +- account-scoped static directory fallbacks + +The shared helpers in `directory-runtime` only handle generic operations: + +- query filtering +- limit application +- deduping/normalization helpers +- building `ChannelDirectoryEntry[]` + +Channel-specific account inspection and id normalization should stay in the +plugin implementation. + ## Provider catalogs Provider plugins can define model catalogs for inference with @@ -1017,6 +1242,10 @@ Compatibility note: - New and migrated bundled plugins should use channel or extension-specific subpaths; use `core` plus explicit domain subpaths for generic surfaces, and treat `compat` as migration-only. +- Capability-specific subpaths such as `image-generation`, + `media-understanding`, and `speech` exist because bundled/native plugins use + them today. Their presence does not by itself mean every exported helper is a + long-term frozen external contract. ## Read-only channel inspection @@ -1140,6 +1369,7 @@ Compatible bundles may instead provide one of: - `.codex-plugin/plugin.json` - `.claude-plugin/plugin.json` +- `.cursor-plugin/plugin.json` Bundle directories are discovered from the same roots as native plugins. @@ -1344,7 +1574,8 @@ Fields: - `slots`: exclusive slot selectors such as `memory` and `contextEngine` - `entries.`: per‑plugin toggles + config -Config changes **require a gateway restart**. +Config changes **require a gateway restart**. See +[Configuration reference](/configuration) for the full config schema. Validation rules (strict): @@ -1390,6 +1621,7 @@ Supported exclusive slots: If multiple plugins declare `kind: "memory"` or `kind: "context-engine"`, only the selected plugin loads for that slot. Others are disabled with diagnostics. +Declare `kind` in your [plugin manifest](/plugins/manifest). ### Context engine plugins @@ -1438,13 +1670,13 @@ Example: ```bash openclaw plugins list -openclaw plugins info +openclaw plugins inspect openclaw plugins install # copy a local file/dir into ~/.openclaw/extensions/ openclaw plugins install ./extensions/voice-call # relative path ok openclaw plugins install ./plugin.tgz # install from a local tarball openclaw plugins install ./plugin.zip # install from a local zip openclaw plugins install -l ./extensions/voice-call # link (no copy) for dev -openclaw plugins install @openclaw/voice-call # install from npm +openclaw plugins install @openclaw/voice-call # install from npm openclaw plugins install @openclaw/voice-call --pin # store exact resolved name@version openclaw plugins update openclaw plugins update --all @@ -1453,14 +1685,11 @@ openclaw plugins disable openclaw plugins doctor ``` -`openclaw plugins list` shows the top-level format as `openclaw` or `bundle`. -Verbose list/info output also shows bundle subtype (`codex` or `claude`) plus -detected bundle capabilities. +See [`openclaw plugins` CLI reference](/cli/plugins) for full details on each +command (install rules, inspect output, marketplace installs, uninstall). -`plugins update` only works for npm installs tracked under `plugins.installs`. -If stored integrity metadata changes between updates, OpenClaw warns and asks for confirmation (use global `--yes` to bypass prompts). - -Plugins may also register their own top‑level commands (example: `openclaw voicecall`). +Plugins may also register their own top-level commands (example: +`openclaw voicecall`). ## Plugin API (overview) @@ -1508,7 +1737,7 @@ Recommended sequence: lifecycle, channel-facing semantics, and runtime helper shape. 2. add typed plugin registration/runtime surfaces Extend `OpenClawPluginApi` and/or `api.runtime` with the smallest useful - typed seam. + typed capability surface. 3. wire core + channel/feature consumers Channels and feature plugins should consume the new capability through core, not by importing a vendor implementation directly. @@ -1518,7 +1747,8 @@ Recommended sequence: Add tests so ownership and registration shape stay explicit over time. This is how OpenClaw stays opinionated without becoming hardcoded to one -provider's worldview. +provider's worldview. See the [Capability Cookbook](/tools/capability-cookbook) +for a concrete file checklist and worked example. ### Capability checklist @@ -1598,6 +1828,36 @@ export default function (api) { } ``` +If your engine does **not** own the compaction algorithm, keep `compact()` +implemented and delegate it explicitly: + +```ts +import { delegateCompactionToRuntime } from "openclaw/plugin-sdk/core"; + +export default function (api) { + api.registerContextEngine("my-memory-engine", () => ({ + info: { + id: "my-memory-engine", + name: "My Memory Engine", + ownsCompaction: false, + }, + async ingest() { + return { ingested: true }; + }, + async assemble({ messages }) { + return { messages, estimatedTokens: 0 }; + }, + async compact(params) { + return await delegateCompactionToRuntime(params); + }, + })); +} +``` + +`ownsCompaction: false` does not automatically fall back to legacy compaction. +If your engine is active, its `compact()` method still handles `/compact` and +overflow recovery. + Then enable it in config: ```json5 @@ -1698,8 +1958,8 @@ Plugins can register **model providers** so users can run OAuth or API-key setup inside OpenClaw, surface provider setup in onboarding/model-pickers, and contribute implicit provider discovery. -Provider plugins are the modular extension seam for model-provider setup. They -are not just "OAuth helpers" anymore. +Provider plugins are the modular extension surface for model-provider setup. +They are not just "OAuth helpers" anymore. ### Provider plugin lifecycle @@ -2267,7 +2527,7 @@ See [Voice Call](/plugins/voice-call) and `extensions/voice-call/README.md` for ## Safety notes -Plugins run in-process with the Gateway. Treat them as trusted code: +Plugins run in-process with the Gateway (see [Execution model](#execution-model)): - Only install plugins you trust. - Prefer `plugins.allow` allowlists. diff --git a/docs/tools/skills-config.md b/docs/tools/skills-config.md index 697cb46dad6..83242afaf5d 100644 --- a/docs/tools/skills-config.md +++ b/docs/tools/skills-config.md @@ -42,6 +42,11 @@ For built-in image generation/editing, prefer `agents.defaults.imageGenerationMo plus the core `image_generate` tool. `skills.entries.*` is only for custom or third-party skill workflows. +Examples: + +- Native Nano Banana-style setup: `agents.defaults.imageGenerationModel.primary: "google/gemini-3-pro-image-preview"` +- Native fal setup: `agents.defaults.imageGenerationModel.primary: "fal/fal-ai/flux/dev"` + ## Fields - `allowBundled`: optional allowlist for **bundled** skills only. When set, only diff --git a/docs/tools/web.md b/docs/tools/web.md index 7cc67c07710..0e30c6c9c7c 100644 --- a/docs/tools/web.md +++ b/docs/tools/web.md @@ -43,12 +43,12 @@ See [Brave Search setup](/brave-search) and [Perplexity Search setup](/perplexit The table above is alphabetical. If no `provider` is explicitly set, runtime auto-detection checks providers in this order: -1. **Brave** — `BRAVE_API_KEY` env var or `tools.web.search.apiKey` config -2. **Gemini** — `GEMINI_API_KEY` env var or `tools.web.search.gemini.apiKey` config -3. **Grok** — `XAI_API_KEY` env var or `tools.web.search.grok.apiKey` config -4. **Kimi** — `KIMI_API_KEY` / `MOONSHOT_API_KEY` env var or `tools.web.search.kimi.apiKey` config -5. **Perplexity** — `PERPLEXITY_API_KEY`, `OPENROUTER_API_KEY`, or `tools.web.search.perplexity.apiKey` config -6. **Firecrawl** — `FIRECRAWL_API_KEY` env var or `tools.web.search.firecrawl.apiKey` config +1. **Brave** — `BRAVE_API_KEY` env var or `plugins.entries.brave.config.webSearch.apiKey` +2. **Gemini** — `GEMINI_API_KEY` env var or `plugins.entries.google.config.webSearch.apiKey` +3. **Grok** — `XAI_API_KEY` env var or `plugins.entries.xai.config.webSearch.apiKey` +4. **Kimi** — `KIMI_API_KEY` / `MOONSHOT_API_KEY` env var or `plugins.entries.moonshot.config.webSearch.apiKey` +5. **Perplexity** — `PERPLEXITY_API_KEY`, `OPENROUTER_API_KEY`, or `plugins.entries.perplexity.config.webSearch.apiKey` +6. **Firecrawl** — `FIRECRAWL_API_KEY` env var or `plugins.entries.firecrawl.config.webSearch.apiKey` If no keys are found, it falls back to Brave (you'll get a missing-key error prompting you to configure one). @@ -80,7 +80,10 @@ pricing. 2. Generate an API key in the dashboard 3. Run `openclaw configure --section web` to store the key in config, or set `PERPLEXITY_API_KEY` in your environment. -For legacy Sonar/OpenRouter compatibility, set `OPENROUTER_API_KEY` instead, or configure `tools.web.search.perplexity.apiKey` with an `sk-or-...` key. Setting `tools.web.search.perplexity.baseUrl` or `model` also opts Perplexity back into the chat-completions compatibility path. +For legacy Sonar/OpenRouter compatibility, set `OPENROUTER_API_KEY` instead, or configure `plugins.entries.perplexity.config.webSearch.apiKey` with an `sk-or-...` key. Setting `plugins.entries.perplexity.config.webSearch.baseUrl` or `model` also opts Perplexity back into the chat-completions compatibility path. + +Provider-specific web search config now lives under `plugins.entries..config.webSearch.*`. +Legacy `tools.web.search.*` provider paths still load through a compatibility shim for one release, but they should not be used in new configs. See [Perplexity Search API Docs](https://docs.perplexity.ai/guides/search-quickstart) for more details. @@ -88,12 +91,12 @@ See [Perplexity Search API Docs](https://docs.perplexity.ai/guides/search-quicks **Via config:** run `openclaw configure --section web`. It stores the key under the provider-specific config path: -- Brave: `tools.web.search.apiKey` -- Firecrawl: `tools.web.search.firecrawl.apiKey` -- Gemini: `tools.web.search.gemini.apiKey` -- Grok: `tools.web.search.grok.apiKey` -- Kimi: `tools.web.search.kimi.apiKey` -- Perplexity: `tools.web.search.perplexity.apiKey` +- Brave: `plugins.entries.brave.config.webSearch.apiKey` +- Firecrawl: `plugins.entries.firecrawl.config.webSearch.apiKey` +- Gemini: `plugins.entries.google.config.webSearch.apiKey` +- Grok: `plugins.entries.xai.config.webSearch.apiKey` +- Kimi: `plugins.entries.moonshot.config.webSearch.apiKey` +- Perplexity: `plugins.entries.perplexity.config.webSearch.apiKey` All of these fields also support SecretRef objects. @@ -114,12 +117,22 @@ For a gateway install, put these in `~/.openclaw/.env` (or your service environm ```json5 { + plugins: { + entries: { + brave: { + config: { + webSearch: { + apiKey: "YOUR_BRAVE_API_KEY", // optional if BRAVE_API_KEY is set // pragma: allowlist secret + }, + }, + }, + }, + }, tools: { web: { search: { enabled: true, provider: "brave", - apiKey: "YOUR_BRAVE_API_KEY", // optional if BRAVE_API_KEY is set // pragma: allowlist secret }, }, }, @@ -142,9 +155,18 @@ For a gateway install, put these in `~/.openclaw/.env` (or your service environm search: { enabled: true, provider: "firecrawl", - firecrawl: { - apiKey: "fc-...", // optional if FIRECRAWL_API_KEY is set - baseUrl: "https://api.firecrawl.dev", + }, + }, + }, + plugins: { + entries: { + firecrawl: { + enabled: true, + config: { + webSearch: { + apiKey: "fc-...", // optional if FIRECRAWL_API_KEY is set + baseUrl: "https://api.firecrawl.dev", + }, }, }, }, @@ -158,15 +180,23 @@ When you choose Firecrawl in onboarding or `openclaw configure --section web`, O ```json5 { + plugins: { + entries: { + brave: { + config: { + webSearch: { + apiKey: "YOUR_BRAVE_API_KEY", // optional if BRAVE_API_KEY is set // pragma: allowlist secret + mode: "llm-context", + }, + }, + }, + }, + }, tools: { web: { search: { enabled: true, provider: "brave", - apiKey: "YOUR_BRAVE_API_KEY", // optional if BRAVE_API_KEY is set // pragma: allowlist secret - brave: { - mode: "llm-context", - }, }, }, }, @@ -181,14 +211,22 @@ In this mode, `country` and `language` / `search_lang` still work, but `ui_lang` ```json5 { + plugins: { + entries: { + perplexity: { + config: { + webSearch: { + apiKey: "pplx-...", // optional if PERPLEXITY_API_KEY is set + }, + }, + }, + }, + }, tools: { web: { search: { enabled: true, provider: "perplexity", - perplexity: { - apiKey: "pplx-...", // optional if PERPLEXITY_API_KEY is set - }, }, }, }, @@ -199,16 +237,24 @@ In this mode, `country` and `language` / `search_lang` still work, but `ui_lang` ```json5 { + plugins: { + entries: { + perplexity: { + config: { + webSearch: { + apiKey: "", // optional if OPENROUTER_API_KEY is set + baseUrl: "https://openrouter.ai/api/v1", + model: "perplexity/sonar-pro", + }, + }, + }, + }, + }, tools: { web: { search: { enabled: true, provider: "perplexity", - perplexity: { - apiKey: "", // optional if OPENROUTER_API_KEY is set - baseUrl: "https://openrouter.ai/api/v1", - model: "perplexity/sonar-pro", - }, }, }, }, @@ -224,22 +270,30 @@ which returns AI-synthesized answers backed by live Google Search results with c 1. Go to [Google AI Studio](https://aistudio.google.com/apikey) 2. Create an API key -3. Set `GEMINI_API_KEY` in the Gateway environment, or configure `tools.web.search.gemini.apiKey` +3. Set `GEMINI_API_KEY` in the Gateway environment, or configure `plugins.entries.google.config.webSearch.apiKey` ### Setting up Gemini search ```json5 { + plugins: { + entries: { + google: { + config: { + webSearch: { + // API key (optional if GEMINI_API_KEY is set) + apiKey: "AIza...", + // Model (defaults to "gemini-2.5-flash") + model: "gemini-2.5-flash", + }, + }, + }, + }, + }, tools: { web: { search: { provider: "gemini", - gemini: { - // API key (optional if GEMINI_API_KEY is set) - apiKey: "AIza...", - // Model (defaults to "gemini-2.5-flash") - model: "gemini-2.5-flash", - }, }, }, }, @@ -266,12 +320,12 @@ Search the web using your configured provider. - `tools.web.search.enabled` must not be `false` (default: enabled) - API key for your chosen provider: - - **Brave**: `BRAVE_API_KEY` or `tools.web.search.apiKey` - - **Firecrawl**: `FIRECRAWL_API_KEY` or `tools.web.search.firecrawl.apiKey` - - **Gemini**: `GEMINI_API_KEY` or `tools.web.search.gemini.apiKey` - - **Grok**: `XAI_API_KEY` or `tools.web.search.grok.apiKey` - - **Kimi**: `KIMI_API_KEY`, `MOONSHOT_API_KEY`, or `tools.web.search.kimi.apiKey` - - **Perplexity**: `PERPLEXITY_API_KEY`, `OPENROUTER_API_KEY`, or `tools.web.search.perplexity.apiKey` + - **Brave**: `BRAVE_API_KEY` or `plugins.entries.brave.config.webSearch.apiKey` + - **Firecrawl**: `FIRECRAWL_API_KEY` or `plugins.entries.firecrawl.config.webSearch.apiKey` + - **Gemini**: `GEMINI_API_KEY` or `plugins.entries.google.config.webSearch.apiKey` + - **Grok**: `XAI_API_KEY` or `plugins.entries.xai.config.webSearch.apiKey` + - **Kimi**: `KIMI_API_KEY`, `MOONSHOT_API_KEY`, or `plugins.entries.moonshot.config.webSearch.apiKey` + - **Perplexity**: `PERPLEXITY_API_KEY`, `OPENROUTER_API_KEY`, or `plugins.entries.perplexity.config.webSearch.apiKey` - All provider key fields above support SecretRef objects. ### Config @@ -297,7 +351,7 @@ Search the web using your configured provider. Parameters depend on the selected provider. Perplexity's OpenRouter / Sonar compatibility path supports only `query` and `freshness`. -If you set `tools.web.search.perplexity.baseUrl` / `model`, use `OPENROUTER_API_KEY`, or configure an `sk-or-...` key, Search API-only filters return explicit errors. +If you set `plugins.entries.perplexity.config.webSearch.baseUrl` / `model`, use `OPENROUTER_API_KEY`, or configure an `sk-or-...` key under `plugins.entries.perplexity.config.webSearch.apiKey`, Search API-only filters return explicit errors. | Parameter | Description | | --------------------- | ----------------------------------------------------- | diff --git a/docs/web/control-ui.md b/docs/web/control-ui.md index 3ad5feb80b4..952f6f71c1d 100644 --- a/docs/web/control-ui.md +++ b/docs/web/control-ui.md @@ -242,7 +242,7 @@ http://localhost:5173/?gatewayUrl=wss://:18789#token= diff --git a/docs/zh-CN/help/faq.md b/docs/zh-CN/help/faq.md index 18b936e2cc8..b5531457484 100644 --- a/docs/zh-CN/help/faq.md +++ b/docs/zh-CN/help/faq.md @@ -1266,15 +1266,26 @@ Gateway 网关监视配置文件并支持热重载: ### 如何启用网络搜索(和网页抓取) -`web_fetch` 无需 API 密钥即可工作。`web_search` 需要 Brave Search API 密钥。**推荐:** 运行 `openclaw configure --section web` 将其存储在 `tools.web.search.apiKey` 中。环境变量替代方案:为 Gateway 网关进程设置 `BRAVE_API_KEY`。 +`web_fetch` 无需 API 密钥即可工作。`web_search` 需要所选提供商的 API 密钥。**推荐:** 运行 `openclaw configure --section web`。新的提供商专属配置会存储在 `plugins.entries..config.webSearch.*` 下。环境变量替代方案:为 Gateway 网关进程设置相应的提供商环境变量。 ```json5 { + plugins: { + entries: { + brave: { + config: { + webSearch: { + apiKey: "BRAVE_API_KEY_HERE", + }, + }, + }, + }, + }, tools: { web: { search: { enabled: true, - apiKey: "BRAVE_API_KEY_HERE", + provider: "brave", maxResults: 5, }, fetch: { @@ -1290,6 +1301,7 @@ Gateway 网关监视配置文件并支持热重载: - 如果你使用允许列表,添加 `web_search`/`web_fetch` 或 `group:web`。 - `web_fetch` 默认启用(除非明确禁用)。 - 守护进程从 `~/.openclaw/.env`(或服务环境)读取环境变量。 +- 旧的 `tools.web.search.*` 提供商路径仍通过兼容层继续生效,但不应再用于新配置。 文档:[Web 工具](/tools/web)。 diff --git a/docs/zh-CN/perplexity.md b/docs/zh-CN/perplexity.md index 56a7505b302..ae9b4a05c72 100644 --- a/docs/zh-CN/perplexity.md +++ b/docs/zh-CN/perplexity.md @@ -34,15 +34,23 @@ OpenClaw 可以使用 Perplexity Sonar 作为 `web_search` 工具。你可以通 ```json5 { + plugins: { + entries: { + perplexity: { + config: { + webSearch: { + apiKey: "pplx-...", + baseUrl: "https://api.perplexity.ai", + model: "perplexity/sonar-pro", + }, + }, + }, + }, + }, tools: { web: { search: { provider: "perplexity", - perplexity: { - apiKey: "pplx-...", - baseUrl: "https://api.perplexity.ai", - model: "perplexity/sonar-pro", - }, }, }, }, @@ -53,21 +61,31 @@ OpenClaw 可以使用 Perplexity Sonar 作为 `web_search` 工具。你可以通 ```json5 { + plugins: { + entries: { + perplexity: { + config: { + webSearch: { + apiKey: "pplx-...", + baseUrl: "https://api.perplexity.ai", + }, + }, + }, + }, + }, tools: { web: { search: { provider: "perplexity", - perplexity: { - apiKey: "pplx-...", - baseUrl: "https://api.perplexity.ai", - }, }, }, }, } ``` -如果同时设置了 `PERPLEXITY_API_KEY` 和 `OPENROUTER_API_KEY`,请设置 `tools.web.search.perplexity.baseUrl`(或 `tools.web.search.perplexity.apiKey`)以消除歧义。 +如果同时设置了 `PERPLEXITY_API_KEY` 和 `OPENROUTER_API_KEY`,请设置 `plugins.entries.perplexity.config.webSearch.baseUrl`(或 `plugins.entries.perplexity.config.webSearch.apiKey`)以消除歧义。 + +提供商专属配置现在统一放在 `plugins.entries..config.webSearch.*`。旧的 `tools.web.search.*` 路径仅通过兼容层继续生效,不再推荐用于新配置。 如果未设置 base URL,OpenClaw 会根据 API 密钥来源选择默认值: diff --git a/docs/zh-CN/reference/api-usage-costs.md b/docs/zh-CN/reference/api-usage-costs.md index feb62d60c6d..91d4bf1160c 100644 --- a/docs/zh-CN/reference/api-usage-costs.md +++ b/docs/zh-CN/reference/api-usage-costs.md @@ -79,8 +79,13 @@ OpenClaw 可以从以下来源获取凭据: `web_search` 使用 API 密钥,可能产生使用费用: -- **Brave Search API**:`BRAVE_API_KEY` 或 `tools.web.search.apiKey` -- **Perplexity**(通过 OpenRouter):`PERPLEXITY_API_KEY` 或 `OPENROUTER_API_KEY` +- **Brave Search API**:`BRAVE_API_KEY` 或 `plugins.entries.brave.config.webSearch.apiKey` +- **Gemini**:`GEMINI_API_KEY` 或 `plugins.entries.google.config.webSearch.apiKey` +- **Grok**:`XAI_API_KEY` 或 `plugins.entries.xai.config.webSearch.apiKey` +- **Kimi**:`KIMI_API_KEY`、`MOONSHOT_API_KEY` 或 `plugins.entries.moonshot.config.webSearch.apiKey` +- **Perplexity**:`PERPLEXITY_API_KEY`、`OPENROUTER_API_KEY` 或 `plugins.entries.perplexity.config.webSearch.apiKey` + +旧的 `tools.web.search.*` 提供商路径仍会通过兼容层加载,但不再是推荐配置方式。 **Brave 免费套餐(额度充裕):** diff --git a/docs/zh-CN/tools/plugin.md b/docs/zh-CN/tools/plugin.md index a2ade46ffbc..775d94eb751 100644 --- a/docs/zh-CN/tools/plugin.md +++ b/docs/zh-CN/tools/plugin.md @@ -950,6 +950,35 @@ export default function (api) { } ``` +如果你的引擎**并不拥有**压缩算法,仍然要实现 `compact()`,并显式委托给运行时: + +```ts +import { delegateCompactionToRuntime } from "openclaw/plugin-sdk"; + +export default function (api) { + api.registerContextEngine("my-memory-engine", () => ({ + info: { + id: "my-memory-engine", + name: "My Memory Engine", + ownsCompaction: false, + }, + async ingest() { + return { ingested: true }; + }, + async assemble({ messages }) { + return { messages, estimatedTokens: 0 }; + }, + async compact(params) { + return await delegateCompactionToRuntime(params); + }, + })); +} +``` + +`ownsCompaction: false` 不会自动回退到 legacy 压缩路径。 +只要该引擎处于激活状态,它自己的 `compact()` 仍然会处理 `/compact` +和溢出恢复。 + 然后在配置中启用它: ```json5 diff --git a/docs/zh-CN/tools/web.md b/docs/zh-CN/tools/web.md index 17c346dc64e..44026c67e29 100644 --- a/docs/zh-CN/tools/web.md +++ b/docs/zh-CN/tools/web.md @@ -18,7 +18,7 @@ x-i18n: OpenClaw 提供两个轻量级 Web 工具: -- `web_search` — 通过 Brave Search API(默认)或 Perplexity Sonar(直连或通过 OpenRouter)搜索网络。 +- `web_search` — 通过 Brave Search API、Firecrawl Search、Gemini with Google Search grounding、Grok、Kimi 或 Perplexity Search API 搜索网络。 - `web_fetch` — HTTP 获取 + 可读性提取(HTML → markdown/文本)。 这些**不是**浏览器自动化。对于 JS 密集型网站或需要登录的情况,请使用[浏览器工具](/tools/browser)。 @@ -26,18 +26,21 @@ OpenClaw 提供两个轻量级 Web 工具: ## 工作原理 - `web_search` 调用你配置的提供商并返回结果。 - - **Brave**(默认):返回结构化结果(标题、URL、摘要)。 - - **Perplexity**:返回带有实时网络搜索引用的 AI 综合答案。 - 结果按查询缓存 15 分钟(可配置)。 - `web_fetch` 执行普通 HTTP GET 并提取可读内容(HTML → markdown/文本)。它**不**执行 JavaScript。 - `web_fetch` 默认启用(除非显式禁用)。 +- 启用捆绑的 Firecrawl 插件后,还会提供 `firecrawl_search` 和 `firecrawl_scrape`。 ## 选择搜索提供商 -| 提供商 | 优点 | 缺点 | API 密钥 | -| ----------------- | ------------------------ | ---------------------------------- | -------------------------------------------- | -| **Brave**(默认) | 快速、结构化结果、免费层 | 传统搜索结果 | `BRAVE_API_KEY` | -| **Perplexity** | AI 综合答案、引用、实时 | 需要 Perplexity 或 OpenRouter 访问 | `OPENROUTER_API_KEY` 或 `PERPLEXITY_API_KEY` | +| 提供商 | 结果形式 | 说明 | API 密钥 | +| --------------------- | ------------------ | ----------------------------------------------- | ------------------------------------------- | +| **Brave Search API** | 结构化结果 + 摘要 | 支持 Brave `llm-context` 模式 | `BRAVE_API_KEY` | +| **Firecrawl Search** | 结构化结果 + 摘要 | Firecrawl 专用搜索控制请使用 `firecrawl_search` | `FIRECRAWL_API_KEY` | +| **Gemini** | AI 综合答案 + 引用 | 使用 Google Search grounding | `GEMINI_API_KEY` | +| **Grok** | AI 综合答案 + 引用 | 使用 xAI 实时网络搜索 | `XAI_API_KEY` | +| **Kimi** | AI 综合答案 + 引用 | 使用 Moonshot web search | `KIMI_API_KEY` / `MOONSHOT_API_KEY` | +| **Perplexity Search** | 结构化结果 + 摘要 | 兼容 OpenRouter Sonar 路径 | `PERPLEXITY_API_KEY` / `OPENROUTER_API_KEY` | 参见 [Brave Search 设置](/brave-search) 和 [Perplexity Sonar](/perplexity) 了解提供商特定详情。 @@ -48,26 +51,34 @@ OpenClaw 提供两个轻量级 Web 工具: tools: { web: { search: { - provider: "brave", // 或 "perplexity" + provider: "brave", // 或 "firecrawl" | "gemini" | "grok" | "kimi" | "perplexity" }, }, }, } ``` -示例:切换到 Perplexity Sonar(直连 API): +示例:切换到 Perplexity Search / Sonar 兼容路径: ```json5 { + plugins: { + entries: { + perplexity: { + config: { + webSearch: { + apiKey: "pplx-...", + baseUrl: "https://api.perplexity.ai", + model: "perplexity/sonar-pro", + }, + }, + }, + }, + }, tools: { web: { search: { provider: "perplexity", - perplexity: { - apiKey: "pplx-...", - baseUrl: "https://api.perplexity.ai", - model: "perplexity/sonar-pro", - }, }, }, }, @@ -84,7 +95,7 @@ Brave 提供免费层和付费计划;查看 Brave API 门户了解当前限制 ### 在哪里设置密钥(推荐) -**推荐:** 运行 `openclaw configure --section web`。它将密钥存储在 `~/.openclaw/openclaw.json` 的 `tools.web.search.apiKey` 下。 +**推荐:** 运行 `openclaw configure --section web`。它会把密钥存储到 `~/.openclaw/openclaw.json` 的 `plugins.entries.brave.config.webSearch.apiKey`。 **环境变量替代方案:** 在 Gateway 网关进程环境中设置 `BRAVE_API_KEY`。对于 Gateway 网关安装,将其放在 `~/.openclaw/.env`(或你的服务环境)中。参见[环境变量](/help/faq#how-does-openclaw-load-environment-variables)。 @@ -107,13 +118,21 @@ Perplexity Sonar 模型具有内置的网络搜索功能,并返回带有引用 search: { enabled: true, provider: "perplexity", - perplexity: { - // API 密钥(如果设置了 OPENROUTER_API_KEY 或 PERPLEXITY_API_KEY 则可选) - apiKey: "sk-or-v1-...", - // 基础 URL(如果省略则根据密钥感知默认值) - baseUrl: "https://openrouter.ai/api/v1", - // 模型(默认为 perplexity/sonar-pro) - model: "perplexity/sonar-pro", + }, + }, + }, + plugins: { + entries: { + perplexity: { + config: { + webSearch: { + // API 密钥(如果设置了 OPENROUTER_API_KEY 或 PERPLEXITY_API_KEY 则可选) + apiKey: "sk-or-v1-...", + // 基础 URL(如果省略则根据密钥感知默认值) + baseUrl: "https://openrouter.ai/api/v1", + // 模型(默认为 perplexity/sonar-pro) + model: "perplexity/sonar-pro", + }, }, }, }, @@ -145,18 +164,28 @@ Perplexity Sonar 模型具有内置的网络搜索功能,并返回带有引用 - `tools.web.search.enabled` 不能为 `false`(默认:启用) - 所选提供商的 API 密钥: - - **Brave**:`BRAVE_API_KEY` 或 `tools.web.search.apiKey` - - **Perplexity**:`OPENROUTER_API_KEY`、`PERPLEXITY_API_KEY` 或 `tools.web.search.perplexity.apiKey` + - **Brave**:`BRAVE_API_KEY` 或 `plugins.entries.brave.config.webSearch.apiKey` + - **Perplexity**:`OPENROUTER_API_KEY`、`PERPLEXITY_API_KEY` 或 `plugins.entries.perplexity.config.webSearch.apiKey` ### 配置 ```json5 { + plugins: { + entries: { + brave: { + config: { + webSearch: { + apiKey: "BRAVE_API_KEY_HERE", + }, + }, + }, + }, + }, tools: { web: { search: { enabled: true, - apiKey: "BRAVE_API_KEY_HERE", // 如果设置了 BRAVE_API_KEY 则可选 maxResults: 5, timeoutSeconds: 30, cacheTtlMinutes: 15, @@ -166,6 +195,9 @@ Perplexity Sonar 模型具有内置的网络搜索功能,并返回带有引用 } ``` +提供商专属的 web_search 配置现在统一放在 `plugins.entries..config.webSearch.*`。 +旧的 `tools.web.search.*` 提供商路径仅作为兼容层暂时保留,不应再用于新配置。 + ### 工具参数 - `query`(必需) diff --git a/experiments/acp-pluginification-architecture-plan.md b/experiments/acp-pluginification-architecture-plan.md new file mode 100644 index 00000000000..b055c1800ce --- /dev/null +++ b/experiments/acp-pluginification-architecture-plan.md @@ -0,0 +1,519 @@ +# Bindings Capability Architecture Plan + +Status: in progress + +## Summary + +The goal is not to move all ACP code out of core. + +The goal is to make `bindings` a small core capability, keep the ACP session kernel in core, and move ACP-specific binding policy plus codex app server policy out of core. + +That gives us a lightweight core without hiding core semantics behind plugin indirection. + +## Current Conclusion + +The current architecture should converge on this split: + +- Core owns the generic binding capability. +- Core owns the generic ACP session kernel. +- Channel plugins own channel-specific binding semantics. +- ACP backend plugins own runtime protocol details. +- Product-level consumers like ACP configured bindings and the codex app server sit on top of the binding capability instead of hardcoding their own binding plumbing. + +This is different from "everything becomes a plugin". + +## Why This Changed + +The current codebase already shows that there are really three different layers: + +- binding and conversation ownership +- long-lived session and runtime-handle orchestration +- product-specific turn logic + +Those layers should not all be forced into one runtime engine. + +Today the duplication is mostly in the execution/control-plane shape, not in storage or binding plumbing: + +- the main harness has its own turn engine +- ACP has its own session control plane +- the codex app server plugin path likely owns its own app-level turn engine outside this repo + +The right move is to share the stable control-plane contracts, not to force all three into one giant executor. + +## Verified Current State + +### Generic binding pieces already exist + +- `src/infra/outbound/session-binding-service.ts` already provides a generic binding store and adapter model. +- `src/plugins/conversation-binding.ts` already lets plugins request a conversation binding and stores plugin-owned binding metadata. +- `src/plugins/types.ts` already exposes plugin-facing binding APIs. +- `src/plugins/types.ts` already exposes the generic `inbound_claim` hook. + +### ACP is only partially pluginified + +- `src/channels/plugins/configured-binding-registry.ts` now owns generic configured binding compilation and lookup. +- `src/channels/plugins/binding-routing.ts` and `src/channels/plugins/binding-targets.ts` now own the generic route and target lifecycle seams. +- ACP now plugs into that seam through `src/channels/plugins/acp-configured-binding-consumer.ts` and `src/channels/plugins/acp-stateful-target-driver.ts`. +- `src/acp/persistent-bindings.lifecycle.ts` still owns configured ACP ensure and reset behavior. +- runtime-created plugin conversation bindings still use a separate path in `src/plugins/conversation-binding.ts`. + +### Codex app server is already closer to the desired shape + +From this repo's side, the codex app server path is much thinner: + +- a plugin binds a conversation +- core stores that binding +- inbound dispatch targets the plugin's `inbound_claim` hook + +What core does not provide for the codex app server path is an ACP-like shared session kernel. If the app server needs retries, long-lived runtime handles, cancellation, or session health logic, it must own that itself today. + +## The Durable Split + +### 1. Core Binding Capability + +This should become the primary shared seam. + +Responsibilities: + +- canonical `ConversationRef` +- binding record storage +- configured binding compilation +- runtime-created binding storage +- fast binding lookup on inbound +- binding touch/unbind lifecycle +- generic dispatch handoff to the binding target + +What core binding capability must not own: + +- Discord thread rules +- Telegram topic rules +- Feishu chat rules +- ACP session orchestration +- codex app server business logic + +### 2. Core Stateful Target Kernel + +This is the small generic kernel for long-lived bound targets. + +Responsibilities: + +- ensure target ready +- run turn +- cancel turn +- close target +- reset target +- status and health +- persistence of target metadata +- retries and runtime-handle safety +- per-target serialization and concurrency + +ACP is the first real implementation of this shape. + +This kernel should stay in core because it is mandatory infrastructure and has strict startup, reset, and recovery semantics. + +### 3. Channel Binding Providers + +Each channel plugin should own the meaning of "this channel conversation maps to this binding rule". + +Responsibilities: + +- normalize configured binding targets +- normalize inbound conversations +- match inbound conversations against compiled bindings +- define channel-specific matching priority +- optionally provide binding description text for status and logs + +This is where Discord channel vs thread logic, Telegram topic rules, and Feishu conversation rules belong. + +### 4. Product Consumers + +Bindings are a shared capability. Different products should consume it differently. + +ACP configured bindings: + +- compile config rules +- resolve a target session +- ensure the ACP session is ready through the ACP kernel + +Codex app server: + +- create runtime-requested bindings +- claim inbound messages through plugin hooks +- optionally adopt the shared stateful target contract later if it really needs long-lived session orchestration + +Main harness: + +- does not need to become "a binding product" +- may eventually share small lifecycle contracts, but it should not be forced into the same engine as ACP + +## The Key Architectural Decision + +The shared abstraction should be: + +- `bindings` as the capability +- `stateful target drivers` as an optional lower-level contract + +The shared abstraction should not be: + +- "one runtime engine for main harness, ACP, and codex app server" + +That would overfit very different systems into one executor. + +## Stable Nouns + +Core should understand only stable nouns. + +The stable nouns are: + +- `ConversationRef` +- `BindingRule` +- `CompiledBinding` +- `BindingResolution` +- `BindingTargetDescriptor` +- `StatefulTargetDriver` +- `StatefulTargetHandle` + +ACP, codex app server, and future products should compile down to those nouns instead of leaking product-specific routing rules through core. + +## Proposed Capability Model + +### Binding capability + +The binding capability should support both configured bindings and runtime-created bindings. + +Required operations: + +- compile configured bindings at startup or reload +- resolve a binding from an inbound `ConversationRef` +- create a runtime binding +- touch and unbind an existing binding +- dispatch a resolved binding to its target + +### Binding target descriptor + +A resolved binding should point to a typed target descriptor rather than ad hoc ACP- or plugin-specific metadata blobs. + +The descriptor should be able to represent at least: + +- plugin-owned inbound claim targets +- stateful target drivers + +That means the same binding capability can support both: + +- codex app server plugin-bound conversations +- ACP configured bindings + +without pretending they are the same product. + +### Stateful target driver + +This is the reusable control-plane contract for long-lived bound targets. + +Required operations: + +- `ensureReady` +- `runTurn` +- `cancel` +- `close` +- `reset` +- `status` +- `health` + +ACP should remain the first built-in driver. + +If the codex app server later proves that it also needs durable session handles, it can either: + +- use a driver that consumes this contract, or +- keep its own product-owned runtime if that remains simpler + +That should be a product decision, not something forced by the binding capability. + +## Why ACP Kernel Stays In Core + +ACP's kernel should remain in core because session lifecycle, persistence, retries, cancellation, and runtime-handle safety are generic platform machinery. + +Those concerns are not channel-specific, and they are not codex-app-server-specific. + +If we move that machinery into an ordinary plugin, we create circular bootstrapping: + +- channels need it during startup and inbound routing +- reset and recovery need it when plugins may already be degraded +- failure semantics become special-case core logic anyway + +If we later wrap it in a "built-in capability module", that is still effectively core. + +## What Should Move Out Of Core + +The following should move out of ACP-shaped core code: + +- channel-specific configured binding matching +- channel-specific binding target normalization +- channel-specific recovery UX +- ACP-specific route wrapping helpers as named ACP seams +- codex app server fallback policy beyond generic plugin-bound dispatch behavior + +The following should stay: + +- generic binding storage and dispatch +- generic ACP control plane +- generic stateful target driver contract + +## Current Problems To Remove + +### Residual cleanup is now small + +Most ACP-era compatibility names are gone from the generic seam. + +The remaining cleanup is smaller: + +- `src/acp/persistent-bindings.ts` compatibility barrel can be deleted once tests stop importing it +- ACP-named tests and mocks can be renamed over time for consistency +- docs should stop describing already-removed ACP wrappers as if they still exist + +### Configured binding implementation is still too monolithic + +`src/channels/plugins/configured-binding-registry.ts` still mixes: + +- registry compilation +- cache invalidation +- inbound matching +- materialization of binding targets +- session-key reverse lookup + +That file is now generic, but still too large and too coupled. + +### Runtime-created plugin bindings still use a separate stack + +`src/plugins/conversation-binding.ts` is still a separate implementation path for plugin-created bindings. + +That means configured bindings and runtime-created bindings share storage, but not one consistent capability layer. + +### Generic registries still hardcode ACP as a built-in + +`src/channels/plugins/configured-binding-consumers.ts` and `src/channels/plugins/stateful-target-drivers.ts` still import ACP directly. + +That is acceptable for now, but the clean final shape is to keep ACP built in while registering it from a dedicated bootstrap point instead of wiring it inside the generic registry files. + +## Target Contracts + +### Channel binding provider contract + +Conceptually, each channel plugin should support: + +- `compileConfiguredBinding(binding, cfg) -> CompiledBinding | null` +- `resolveInboundConversation(event) -> ConversationRef | null` +- `matchInboundConversation(compiledBinding, conversation) -> BindingMatch | null` +- `describeBinding(compiledBinding) -> string | undefined` + +### Binding capability contract + +Core should support: + +- `compileConfiguredBindings(cfg, plugins) -> CompiledBindingRegistry` +- `resolveBinding(conversationRef) -> BindingResolution | null` +- `createRuntimeBinding(target, conversationRef, metadata) -> BindingRecord` +- `touchBinding(bindingId)` +- `unbindBinding(bindingId | target)` +- `dispatchResolvedBinding(bindingResolution, inboundEvent)` + +### Stateful target driver contract + +Core should support: + +- `ensureReady(targetRef, cfg)` +- `runTurn(targetRef, input)` +- `cancel(targetRef, reason)` +- `close(targetRef, reason)` +- `reset(targetRef, reason)` +- `status(targetRef)` +- `health(targetRef)` + +## File-Level Transition Plan + +### Keep + +- `src/infra/outbound/session-binding-service.ts` +- `src/acp/control-plane/*` +- `extensions/acpx/*` + +### Generalize + +- `src/plugins/conversation-binding.ts` + - fold runtime-created plugin bindings into the same generic binding capability instead of keeping a separate implementation stack +- `src/channels/plugins/configured-binding-registry.ts` + - split into compiler, matcher, and session-key resolution modules with a thin facade +- `src/channels/plugins/types.adapters.ts` + - finish removing ACP-era aliases after the deprecation window +- `src/plugin-sdk/conversation-runtime.ts` + - export only the generic binding capability surfaces +- `src/acp/persistent-bindings.lifecycle.ts` + - either become a generic stateful target driver consumer or be renamed to ACP driver-specific lifecycle code + +### Shrink Or Delete + +- `src/acp/persistent-bindings.ts` + - delete the compatibility barrel once tests import the real modules directly +- `src/acp/persistent-bindings.resolve.ts` + - keep only while ACP-specific compatibility helpers are still useful to internal callers +- ACP-named test files + - rename over time once the behavior is stable and there is no risk of mixing behavioral and naming churn + +## Recommended Refactor Order + +### Completed groundwork + +The current branch has already completed most of the first migration wave: + +- stable generic binding nouns exist +- configured bindings compile through a generic registry +- inbound routing goes through generic binding resolution +- configured binding lookup no longer performs fallback plugin discovery +- ACP is expressed as a configured-binding consumer plus a built-in stateful target driver + +The remaining work is cleanup and unification, not first-principles redesign. + +### Phase 1: Freeze the nouns + +Introduce and document the stable binding and target types: + +- `ConversationRef` +- `CompiledBinding` +- `BindingResolution` +- `BindingTargetDescriptor` +- `StatefulTargetDriver` + +Do this before more movement so the rest of the refactor has firm vocabulary. + +### Phase 2: Promote bindings to a first-class core capability + +Refactor the existing generic binding store into an explicit capability layer. + +Requirements: + +- runtime-created bindings stay supported +- configured bindings become first-class +- lookup becomes channel-agnostic + +### Phase 3: Compile configured bindings at startup and reload + +Move configured binding compilation off the inbound hot path. + +Requirements: + +- load enabled channel plugins once +- compile configured bindings once +- rebuild on config or plugin reload +- inbound path becomes pure registry lookup + +### Phase 4: Expand the channel provider seam + +Replace the ACP-specific adapter shape with a generic channel binding provider contract. + +Requirements: + +- channel plugins own normalization and matching +- core no longer knows channel-specific configured binding rules + +### Phase 5: Re-express ACP as a binding consumer plus built-in stateful target driver + +Move ACP configured binding policy to the new binding capability while keeping ACP runtime orchestration in core. + +Requirements: + +- ACP configured bindings resolve through the generic binding registry +- ACP target readiness uses the ACP driver contract +- ACP-specific naming disappears from generic binding code + +### Phase 6: Finish residual ACP cleanup + +Remove the last compatibility leftovers and stale naming. + +Requirements: + +- delete `src/acp/persistent-bindings.ts` +- rename ACP-named tests where that improves clarity without changing behavior +- keep docs synchronized with the actual generic seam instead of the earlier transition state + +### Phase 7: Split the configured binding registry by responsibility + +Refactor `src/channels/plugins/configured-binding-registry.ts` into smaller modules. + +Suggested split: + +- compiler module +- inbound matcher module +- session-key reverse lookup module +- thin public facade + +Requirements: + +- caching behavior remains unchanged +- matching behavior remains unchanged +- session-key resolution behavior remains unchanged + +### Phase 8: Keep codex app server on the same binding capability + +Do not force the codex app server into ACP semantics. + +Requirements: + +- codex app server keeps runtime-created bindings through the same binding capability +- inbound claim remains the default delivery path +- only adopt the stateful target driver seam if the app server truly needs long-lived target orchestration +- `src/plugins/conversation-binding.ts` stops being a separate binding stack and becomes a consumer of the generic binding capability + +### Phase 9: Decouple built-in ACP registration from generic registry files + +Keep ACP built in, but stop importing it directly from the generic registry modules. + +Requirements: + +- `src/channels/plugins/configured-binding-consumers.ts` no longer hardcodes ACP imports +- `src/channels/plugins/stateful-target-drivers.ts` no longer hardcodes ACP imports +- ACP still registers by default during normal startup +- generic registry files remain product-agnostic + +### Phase 10: Remove ACP-shaped compatibility facades + +Once all call sites are on the generic capability: + +- delete ACP-shaped routing helpers +- delete hot-path plugin bootstrapping logic +- keep only thin compatibility exports if external plugins still need a deprecation window + +## Success Criteria + +The architecture is done when all of these are true: + +- no inbound configured-binding resolution performs plugin discovery +- no channel-specific binding semantics remain in generic core binding code +- ACP still uses a core session kernel +- codex app server and ACP both sit on top of the same binding capability +- the binding capability can represent both configured and runtime-created bindings +- runtime-created plugin bindings do not use a separate implementation stack +- long-lived target orchestration is shared through a small core driver contract +- generic registry files do not import ACP directly +- ACP-era alias names are gone from the generic/plugin SDK surface +- the main harness is not forced into the ACP engine +- external plugins can use the same capability without internal imports + +## Non-Goals + +These are not goals of the remaining refactor: + +- moving the ACP session kernel into an ordinary plugin +- forcing the main harness, ACP, and codex app server into one executor +- making every channel implement its own retry and session-safety logic +- keeping ACP-shaped naming in the long-term generic binding layer + +## Bottom Line + +The right 20-year split is: + +- bindings are the shared core capability +- ACP session orchestration remains a small built-in core kernel +- channel plugins own binding semantics +- backend plugins own runtime protocol details +- product consumers like ACP configured bindings and codex app server build on the same binding capability without being forced into one runtime engine + +That is the leanest core that still has honest boundaries. diff --git a/extensions/acpx/index.ts b/extensions/acpx/index.ts index 20a1cbbefe2..2ae578b9c3f 100644 --- a/extensions/acpx/index.ts +++ b/extensions/acpx/index.ts @@ -1,4 +1,4 @@ -import type { OpenClawPluginApi } from "openclaw/plugin-sdk/acpx"; +import type { OpenClawPluginApi } from "./runtime-api.js"; import { createAcpxPluginConfigSchema } from "./src/config.js"; import { createAcpxRuntimeService } from "./src/service.js"; diff --git a/extensions/acpx/runtime-api.ts b/extensions/acpx/runtime-api.ts new file mode 100644 index 00000000000..8d1d125f226 --- /dev/null +++ b/extensions/acpx/runtime-api.ts @@ -0,0 +1 @@ +export * from "openclaw/plugin-sdk/acpx"; diff --git a/extensions/acpx/src/config.test.ts b/extensions/acpx/src/config.test.ts index 5a19d6f43e8..bd75ee1198d 100644 --- a/extensions/acpx/src/config.test.ts +++ b/extensions/acpx/src/config.test.ts @@ -39,6 +39,25 @@ describe("acpx plugin config parsing", () => { } }); + it("prefers the workspace plugin root for dist/extensions/acpx bundles", () => { + const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), "acpx-root-workspace-")); + const workspacePluginRoot = path.join(repoRoot, "extensions", "acpx"); + const bundledPluginRoot = path.join(repoRoot, "dist", "extensions", "acpx"); + try { + fs.mkdirSync(workspacePluginRoot, { recursive: true }); + fs.mkdirSync(bundledPluginRoot, { recursive: true }); + fs.writeFileSync(path.join(workspacePluginRoot, "package.json"), "{}\n", "utf8"); + fs.writeFileSync(path.join(workspacePluginRoot, "openclaw.plugin.json"), "{}\n", "utf8"); + fs.writeFileSync(path.join(bundledPluginRoot, "package.json"), "{}\n", "utf8"); + fs.writeFileSync(path.join(bundledPluginRoot, "openclaw.plugin.json"), "{}\n", "utf8"); + + const moduleUrl = pathToFileURL(path.join(bundledPluginRoot, "index.js")).href; + expect(resolveAcpxPluginRoot(moduleUrl)).toBe(workspacePluginRoot); + } finally { + fs.rmSync(repoRoot, { recursive: true, force: true }); + } + }); + it("resolves bundled acpx with pinned version by default", () => { const resolved = resolveAcpxPluginConfig({ rawConfig: { diff --git a/extensions/acpx/src/config.ts b/extensions/acpx/src/config.ts index d6bfb3a44db..612147320d5 100644 --- a/extensions/acpx/src/config.ts +++ b/extensions/acpx/src/config.ts @@ -1,7 +1,7 @@ import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; -import type { OpenClawPluginConfigSchema } from "openclaw/plugin-sdk/acpx"; +import type { OpenClawPluginConfigSchema } from "../runtime-api.js"; export const ACPX_PERMISSION_MODES = ["approve-all", "approve-reads", "deny-all"] as const; export type AcpxPermissionMode = (typeof ACPX_PERMISSION_MODES)[number]; @@ -13,14 +13,18 @@ export const ACPX_PINNED_VERSION = "0.1.16"; export const ACPX_VERSION_ANY = "any"; const ACPX_BIN_NAME = process.platform === "win32" ? "acpx.cmd" : "acpx"; -export function resolveAcpxPluginRoot(moduleUrl: string = import.meta.url): string { +function isAcpxPluginRoot(dir: string): boolean { + return ( + fs.existsSync(path.join(dir, "openclaw.plugin.json")) && + fs.existsSync(path.join(dir, "package.json")) + ); +} + +function resolveNearestAcpxPluginRoot(moduleUrl: string): string { let cursor = path.dirname(fileURLToPath(moduleUrl)); for (let i = 0; i < 3; i += 1) { // Bundled entries live at the plugin root while source files still live under src/. - if ( - fs.existsSync(path.join(cursor, "openclaw.plugin.json")) && - fs.existsSync(path.join(cursor, "package.json")) - ) { + if (isAcpxPluginRoot(cursor)) { return cursor; } const parent = path.dirname(cursor); @@ -32,10 +36,29 @@ export function resolveAcpxPluginRoot(moduleUrl: string = import.meta.url): stri return path.resolve(path.dirname(fileURLToPath(moduleUrl)), ".."); } +function resolveWorkspaceAcpxPluginRoot(currentRoot: string): string | null { + if ( + path.basename(currentRoot) !== "acpx" || + path.basename(path.dirname(currentRoot)) !== "extensions" || + path.basename(path.dirname(path.dirname(currentRoot))) !== "dist" + ) { + return null; + } + const workspaceRoot = path.resolve(currentRoot, "..", "..", "..", "extensions", "acpx"); + return isAcpxPluginRoot(workspaceRoot) ? workspaceRoot : null; +} + +export function resolveAcpxPluginRoot(moduleUrl: string = import.meta.url): string { + const resolvedRoot = resolveNearestAcpxPluginRoot(moduleUrl); + // In a live repo checkout, dist/ can be rebuilt out from under the running gateway. + // Prefer the stable source plugin root when a built extension is running beside it. + return resolveWorkspaceAcpxPluginRoot(resolvedRoot) ?? resolvedRoot; +} + export const ACPX_PLUGIN_ROOT = resolveAcpxPluginRoot(); export const ACPX_BUNDLED_BIN = path.join(ACPX_PLUGIN_ROOT, "node_modules", ".bin", ACPX_BIN_NAME); export function buildAcpxLocalInstallCommand(version: string = ACPX_PINNED_VERSION): string { - return `npm install --omit=dev --no-save acpx@${version}`; + return `npm install --omit=dev --no-save --package-lock=false acpx@${version}`; } export const ACPX_LOCAL_INSTALL_COMMAND = buildAcpxLocalInstallCommand(); diff --git a/extensions/acpx/src/ensure.test.ts b/extensions/acpx/src/ensure.test.ts index c0bb5469b29..b834a671906 100644 --- a/extensions/acpx/src/ensure.test.ts +++ b/extensions/acpx/src/ensure.test.ts @@ -85,7 +85,13 @@ describe("acpx ensure", () => { }); expect(spawnAndCollectMock.mock.calls[1]?.[0]).toMatchObject({ command: "npm", - args: ["install", "--omit=dev", "--no-save", `acpx@${ACPX_PINNED_VERSION}`], + args: [ + "install", + "--omit=dev", + "--no-save", + "--package-lock=false", + `acpx@${ACPX_PINNED_VERSION}`, + ], cwd: "/plugin", stripProviderAuthEnvVars, }); diff --git a/extensions/acpx/src/ensure.ts b/extensions/acpx/src/ensure.ts index 9b85d53f618..197dab820b8 100644 --- a/extensions/acpx/src/ensure.ts +++ b/extensions/acpx/src/ensure.ts @@ -1,6 +1,6 @@ import fs from "node:fs"; import path from "node:path"; -import type { PluginLogger } from "openclaw/plugin-sdk/acpx"; +import type { PluginLogger } from "../runtime-api.js"; import { ACPX_PINNED_VERSION, ACPX_PLUGIN_ROOT, buildAcpxLocalInstallCommand } from "./config.js"; import { resolveSpawnFailure, @@ -233,7 +233,13 @@ export async function ensureAcpx(params: { const install = await spawnAndCollect({ command: "npm", - args: ["install", "--omit=dev", "--no-save", `acpx@${installVersion}`], + args: [ + "install", + "--omit=dev", + "--no-save", + "--package-lock=false", + `acpx@${installVersion}`, + ], cwd: pluginRoot, stripProviderAuthEnvVars: params.stripProviderAuthEnvVars, }); diff --git a/extensions/acpx/src/runtime-internals/events.ts b/extensions/acpx/src/runtime-internals/events.ts index f0326bbe938..3bbfed68495 100644 --- a/extensions/acpx/src/runtime-internals/events.ts +++ b/extensions/acpx/src/runtime-internals/events.ts @@ -1,4 +1,4 @@ -import type { AcpRuntimeEvent, AcpSessionUpdateTag } from "openclaw/plugin-sdk/acpx"; +import type { AcpRuntimeEvent, AcpSessionUpdateTag } from "../runtime-api.js"; import { asOptionalBoolean, asOptionalString, diff --git a/extensions/acpx/src/runtime-internals/process.test.ts b/extensions/acpx/src/runtime-internals/process.test.ts index ef0492308ae..90b7560c47e 100644 --- a/extensions/acpx/src/runtime-internals/process.test.ts +++ b/extensions/acpx/src/runtime-internals/process.test.ts @@ -1,5 +1,5 @@ import { spawn } from "node:child_process"; -import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; +import { chmod, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; import { afterEach, describe, expect, it, vi } from "vitest"; @@ -64,6 +64,58 @@ describe("resolveSpawnCommand", () => { }); }); + it("routes node shebang wrappers through the current node runtime on posix", async () => { + const dir = await createTempDir(); + const scriptPath = path.join(dir, "acpx"); + await writeFile(scriptPath, "#!/usr/bin/env node\nconsole.log('ok')\n", "utf8"); + await chmod(scriptPath, 0o755); + + const resolved = resolveSpawnCommand( + { + command: scriptPath, + args: ["--help"], + }, + undefined, + { + platform: "linux", + env: {}, + execPath: "/custom/node", + }, + ); + + expect(resolved).toEqual({ + command: "/custom/node", + args: [scriptPath, "--help"], + }); + }); + + it("routes PATH-resolved node shebang wrappers through the current node runtime on posix", async () => { + const dir = await createTempDir(); + const binDir = path.join(dir, "bin"); + const scriptPath = path.join(binDir, "acpx"); + await mkdir(binDir, { recursive: true }); + await writeFile(scriptPath, "#!/usr/bin/env node\nconsole.log('ok')\n", "utf8"); + await chmod(scriptPath, 0o755); + + const resolved = resolveSpawnCommand( + { + command: "acpx", + args: ["--help"], + }, + undefined, + { + platform: "linux", + env: { PATH: binDir }, + execPath: "/custom/node", + }, + ); + + expect(resolved).toEqual({ + command: "/custom/node", + args: [scriptPath, "--help"], + }); + }); + it("routes .js command execution through node on windows", () => { const resolved = resolveSpawnCommand( { diff --git a/extensions/acpx/src/runtime-internals/process.ts b/extensions/acpx/src/runtime-internals/process.ts index 2724f467ab1..48e0bf274f2 100644 --- a/extensions/acpx/src/runtime-internals/process.ts +++ b/extensions/acpx/src/runtime-internals/process.ts @@ -1,17 +1,18 @@ import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process"; -import { existsSync } from "node:fs"; +import { accessSync, constants as fsConstants, existsSync, readFileSync, statSync } from "node:fs"; +import path from "node:path"; import type { WindowsSpawnProgram, WindowsSpawnProgramCandidate, WindowsSpawnResolution, -} from "openclaw/plugin-sdk/acpx"; +} from "../../runtime-api.js"; import { applyWindowsSpawnProgramPolicy, listKnownProviderAuthEnvVarNames, materializeWindowsSpawnProgram, omitEnvKeysCaseInsensitive, resolveWindowsSpawnProgramCandidate, -} from "openclaw/plugin-sdk/acpx"; +} from "../../runtime-api.js"; export type SpawnExit = { code: number | null; @@ -57,11 +58,76 @@ const DEFAULT_RUNTIME: SpawnRuntime = { execPath: process.execPath, }; +function isExecutableFile(filePath: string, platform: NodeJS.Platform): boolean { + try { + const stat = statSync(filePath); + if (!stat.isFile()) { + return false; + } + if (platform === "win32") { + return true; + } + accessSync(filePath, fsConstants.X_OK); + return true; + } catch { + return false; + } +} + +function resolveExecutableFromPath(command: string, runtime: SpawnRuntime): string | undefined { + const pathEnv = runtime.env.PATH ?? runtime.env.Path; + if (!pathEnv) { + return undefined; + } + for (const entry of pathEnv.split(path.delimiter).filter(Boolean)) { + const candidate = path.join(entry, command); + if (isExecutableFile(candidate, runtime.platform)) { + return candidate; + } + } + return undefined; +} + +function resolveNodeShebangScriptPath(command: string, runtime: SpawnRuntime): string | undefined { + const commandPath = + path.isAbsolute(command) || command.includes(path.sep) + ? command + : resolveExecutableFromPath(command, runtime); + if (!commandPath || !isExecutableFile(commandPath, runtime.platform)) { + return undefined; + } + try { + const firstLine = readFileSync(commandPath, "utf8").split(/\r?\n/, 1)[0] ?? ""; + if (/^#!.*(?:\/usr\/bin\/env\s+node\b|\/node(?:js)?\b)/.test(firstLine)) { + return commandPath; + } + } catch { + return undefined; + } + return undefined; +} + export function resolveSpawnCommand( params: { command: string; args: string[] }, options?: SpawnCommandOptions, runtime: SpawnRuntime = DEFAULT_RUNTIME, ): ResolvedSpawnCommand { + if (runtime.platform !== "win32") { + const nodeShebangScript = resolveNodeShebangScriptPath(params.command, runtime); + if (nodeShebangScript) { + options?.onResolved?.({ + command: params.command, + cacheHit: false, + strictWindowsCmdWrapper: options?.strictWindowsCmdWrapper === true, + resolution: "direct", + }); + return { + command: runtime.execPath, + args: [nodeShebangScript, ...params.args], + }; + } + } + const strictWindowsCmdWrapper = options?.strictWindowsCmdWrapper === true; const cacheKey = params.command; const cachedProgram = options?.cache; diff --git a/extensions/acpx/src/runtime.test.ts b/extensions/acpx/src/runtime.test.ts index 198a0367b59..5c65b032f34 100644 --- a/extensions/acpx/src/runtime.test.ts +++ b/extensions/acpx/src/runtime.test.ts @@ -154,6 +154,90 @@ describe("AcpxRuntime", () => { expect(resumeArgs[resumeFlagIndex + 1]).toBe(resumeSessionId); }); + it("replaces dead named sessions returned by sessions ensure", async () => { + process.env.MOCK_ACPX_STATUS_STATUS = "dead"; + process.env.MOCK_ACPX_STATUS_SUMMARY = "queue owner unavailable"; + try { + const { runtime, logPath } = await createMockRuntimeFixture(); + const sessionKey = "agent:codex:acp:dead-session"; + + const handle = await runtime.ensureSession({ + sessionKey, + agent: "codex", + mode: "persistent", + }); + + expect(handle.backend).toBe("acpx"); + const logs = await readMockRuntimeLogEntries(logPath); + const ensureIndex = logs.findIndex((entry) => entry.kind === "ensure"); + const statusIndex = logs.findIndex((entry) => entry.kind === "status"); + const newIndex = logs.findIndex((entry) => entry.kind === "new"); + expect(ensureIndex).toBeGreaterThanOrEqual(0); + expect(statusIndex).toBeGreaterThan(ensureIndex); + expect(newIndex).toBeGreaterThan(statusIndex); + } finally { + delete process.env.MOCK_ACPX_STATUS_STATUS; + delete process.env.MOCK_ACPX_STATUS_SUMMARY; + } + }); + + it("reuses a live named session when sessions ensure exits before returning identifiers", async () => { + process.env.MOCK_ACPX_ENSURE_EXIT_1 = "1"; + process.env.MOCK_ACPX_STATUS_STATUS = "alive"; + try { + const { runtime, logPath } = await createMockRuntimeFixture(); + const sessionKey = "agent:codex:acp:ensure-fallback-alive"; + + const handle = await runtime.ensureSession({ + sessionKey, + agent: "codex", + mode: "persistent", + }); + + expect(handle.backend).toBe("acpx"); + expect(handle.acpxRecordId).toBe("rec-" + sessionKey); + const logs = await readMockRuntimeLogEntries(logPath); + const ensureIndex = logs.findIndex((entry) => entry.kind === "ensure"); + const statusIndex = logs.findIndex((entry) => entry.kind === "status"); + const newIndex = logs.findIndex((entry) => entry.kind === "new"); + expect(ensureIndex).toBeGreaterThanOrEqual(0); + expect(statusIndex).toBeGreaterThan(ensureIndex); + expect(newIndex).toBe(-1); + } finally { + delete process.env.MOCK_ACPX_ENSURE_EXIT_1; + delete process.env.MOCK_ACPX_STATUS_STATUS; + } + }); + + it("creates a fresh named session when sessions ensure exits and status is dead", async () => { + process.env.MOCK_ACPX_ENSURE_EXIT_1 = "1"; + process.env.MOCK_ACPX_STATUS_STATUS = "dead"; + process.env.MOCK_ACPX_STATUS_SUMMARY = "queue owner unavailable"; + try { + const { runtime, logPath } = await createMockRuntimeFixture(); + const sessionKey = "agent:codex:acp:ensure-fallback-dead"; + + const handle = await runtime.ensureSession({ + sessionKey, + agent: "codex", + mode: "persistent", + }); + + expect(handle.backend).toBe("acpx"); + const logs = await readMockRuntimeLogEntries(logPath); + const ensureIndex = logs.findIndex((entry) => entry.kind === "ensure"); + const statusIndex = logs.findIndex((entry) => entry.kind === "status"); + const newIndex = logs.findIndex((entry) => entry.kind === "new"); + expect(ensureIndex).toBeGreaterThanOrEqual(0); + expect(statusIndex).toBeGreaterThan(ensureIndex); + expect(newIndex).toBeGreaterThan(statusIndex); + } finally { + delete process.env.MOCK_ACPX_ENSURE_EXIT_1; + delete process.env.MOCK_ACPX_STATUS_STATUS; + delete process.env.MOCK_ACPX_STATUS_SUMMARY; + } + }); + it("serializes text plus image attachments into ACP prompt blocks", async () => { const { runtime, logPath } = await createMockRuntimeFixture(); diff --git a/extensions/acpx/src/runtime.ts b/extensions/acpx/src/runtime.ts index e55ef360424..e1f0024c699 100644 --- a/extensions/acpx/src/runtime.ts +++ b/extensions/acpx/src/runtime.ts @@ -10,8 +10,8 @@ import type { AcpRuntimeStatus, AcpRuntimeTurnInput, PluginLogger, -} from "openclaw/plugin-sdk/acpx"; -import { AcpRuntimeError } from "openclaw/plugin-sdk/acpx"; +} from "../runtime-api.js"; +import { AcpRuntimeError } from "../runtime-api.js"; import { toAcpMcpServers, type ResolvedAcpxPluginConfig } from "./config.js"; import { checkAcpxVersion, type AcpxVersionCheckResult } from "./ensure.js"; import { @@ -92,6 +92,26 @@ function formatAcpxExitMessage(params: { return stderr || `acpx exited with code ${params.exitCode ?? "unknown"}`; } +function summarizeLogText(text: string, maxChars = 240): string { + const normalized = text.trim().replace(/\s+/g, " "); + if (!normalized) { + return ""; + } + if (normalized.length <= maxChars) { + return normalized; + } + return `${normalized.slice(0, maxChars)}...`; +} + +function findSessionIdentifierEvent(events: AcpxJsonObject[]): AcpxJsonObject | undefined { + return events.find( + (event) => + asOptionalString(event.agentSessionId) || + asOptionalString(event.acpxSessionId) || + asOptionalString(event.acpxRecordId), + ); +} + export function encodeAcpxRuntimeHandleState(state: AcpxHandleState): string { const payload = Buffer.from(JSON.stringify(state), "utf8").toString("base64url"); return `${ACPX_RUNTIME_HANDLE_PREFIX}${payload}`; @@ -252,6 +272,146 @@ export class AcpxRuntime implements AcpRuntime { this.healthy = result.ok; } + private async createNamedSession(params: { + agent: string; + cwd: string; + sessionName: string; + resumeSessionId?: string; + }): Promise { + const command = params.resumeSessionId + ? [ + "sessions", + "new", + "--name", + params.sessionName, + "--resume-session", + params.resumeSessionId, + ] + : ["sessions", "new", "--name", params.sessionName]; + return await this.runControlCommand({ + args: await this.buildVerbArgs({ + agent: params.agent, + cwd: params.cwd, + command, + }), + cwd: params.cwd, + fallbackCode: "ACP_SESSION_INIT_FAILED", + }); + } + + private async shouldReplaceEnsuredSession(params: { + sessionName: string; + agent: string; + cwd: string; + }): Promise { + const args = await this.buildVerbArgs({ + agent: params.agent, + cwd: params.cwd, + command: ["status", "--session", params.sessionName], + }); + let events: AcpxJsonObject[]; + try { + events = await this.runControlCommand({ + args, + cwd: params.cwd, + fallbackCode: "ACP_SESSION_INIT_FAILED", + ignoreNoSession: true, + }); + } catch (error) { + this.logger?.warn?.( + `acpx ensureSession status probe failed: session=${params.sessionName} cwd=${params.cwd} error=${summarizeLogText(error instanceof Error ? error.message : String(error)) || ""}`, + ); + return false; + } + + const noSession = events.some((event) => toAcpxErrorEvent(event)?.code === "NO_SESSION"); + if (noSession) { + this.logger?.warn?.( + `acpx ensureSession replacing missing named session: session=${params.sessionName} cwd=${params.cwd}`, + ); + return true; + } + + const detail = events.find((event) => !toAcpxErrorEvent(event)); + const status = asTrimmedString(detail?.status)?.toLowerCase(); + if (status === "dead") { + const summary = summarizeLogText(asOptionalString(detail?.summary) ?? ""); + this.logger?.warn?.( + `acpx ensureSession replacing dead named session: session=${params.sessionName} cwd=${params.cwd} status=${status} summary=${summary || ""}`, + ); + return true; + } + + return false; + } + + private async recoverEnsureFailure(params: { + sessionName: string; + agent: string; + cwd: string; + error: unknown; + }): Promise { + const errorMessage = summarizeLogText( + params.error instanceof Error ? params.error.message : String(params.error), + ); + this.logger?.warn?.( + `acpx ensureSession probing named session after ensure failure: session=${params.sessionName} cwd=${params.cwd} error=${errorMessage || ""}`, + ); + const args = await this.buildVerbArgs({ + agent: params.agent, + cwd: params.cwd, + command: ["status", "--session", params.sessionName], + }); + let events: AcpxJsonObject[]; + try { + events = await this.runControlCommand({ + args, + cwd: params.cwd, + fallbackCode: "ACP_SESSION_INIT_FAILED", + ignoreNoSession: true, + }); + } catch (statusError) { + this.logger?.warn?.( + `acpx ensureSession status fallback failed: session=${params.sessionName} cwd=${params.cwd} error=${summarizeLogText(statusError instanceof Error ? statusError.message : String(statusError)) || ""}`, + ); + return null; + } + + const noSession = events.some((event) => toAcpxErrorEvent(event)?.code === "NO_SESSION"); + if (noSession) { + this.logger?.warn?.( + `acpx ensureSession creating named session after ensure failure and missing status: session=${params.sessionName} cwd=${params.cwd}`, + ); + return await this.createNamedSession({ + agent: params.agent, + cwd: params.cwd, + sessionName: params.sessionName, + }); + } + + const detail = events.find((event) => !toAcpxErrorEvent(event)); + const status = asTrimmedString(detail?.status)?.toLowerCase(); + if (status === "dead") { + this.logger?.warn?.( + `acpx ensureSession replacing dead named session after ensure failure: session=${params.sessionName} cwd=${params.cwd}`, + ); + return await this.createNamedSession({ + agent: params.agent, + cwd: params.cwd, + sessionName: params.sessionName, + }); + } + + if (status === "alive" || findSessionIdentifierEvent(events)) { + this.logger?.warn?.( + `acpx ensureSession reusing live named session after ensure failure: session=${params.sessionName} cwd=${params.cwd} status=${status || "unknown"}`, + ); + return events; + } + + return null; + } + async ensureSession(input: AcpRuntimeEnsureInput): Promise { const sessionName = asTrimmedString(input.sessionKey); if (!sessionName) { @@ -264,45 +424,80 @@ export class AcpxRuntime implements AcpRuntime { const cwd = asTrimmedString(input.cwd) || this.config.cwd; const mode = input.mode; const resumeSessionId = asTrimmedString(input.resumeSessionId); - const ensureSubcommand = resumeSessionId - ? ["sessions", "new", "--name", sessionName, "--resume-session", resumeSessionId] - : ["sessions", "ensure", "--name", sessionName]; - const ensureCommand = await this.buildVerbArgs({ - agent, - cwd, - command: ensureSubcommand, - }); - - let events = await this.runControlCommand({ - args: ensureCommand, - cwd, - fallbackCode: "ACP_SESSION_INIT_FAILED", - }); - let ensuredEvent = events.find( - (event) => - asOptionalString(event.agentSessionId) || - asOptionalString(event.acpxSessionId) || - asOptionalString(event.acpxRecordId), - ); - - if (!ensuredEvent && !resumeSessionId) { - const newCommand = await this.buildVerbArgs({ + let events: AcpxJsonObject[]; + if (resumeSessionId) { + events = await this.createNamedSession({ agent, cwd, - command: ["sessions", "new", "--name", sessionName], + sessionName, + resumeSessionId, }); - events = await this.runControlCommand({ - args: newCommand, - cwd, - fallbackCode: "ACP_SESSION_INIT_FAILED", - }); - ensuredEvent = events.find( - (event) => - asOptionalString(event.agentSessionId) || - asOptionalString(event.acpxSessionId) || - asOptionalString(event.acpxRecordId), + } else { + try { + events = await this.runControlCommand({ + args: await this.buildVerbArgs({ + agent, + cwd, + command: ["sessions", "ensure", "--name", sessionName], + }), + cwd, + fallbackCode: "ACP_SESSION_INIT_FAILED", + }); + } catch (error) { + const recovered = await this.recoverEnsureFailure({ + sessionName, + agent, + cwd, + error, + }); + if (!recovered) { + throw error; + } + events = recovered; + } + } + if (events.length === 0) { + this.logger?.warn?.( + `acpx ensureSession returned no events after sessions ensure: session=${sessionName} agent=${agent} cwd=${cwd}`, ); } + let ensuredEvent = findSessionIdentifierEvent(events); + + if ( + ensuredEvent && + !resumeSessionId && + (await this.shouldReplaceEnsuredSession({ + sessionName, + agent, + cwd, + })) + ) { + events = await this.createNamedSession({ + agent, + cwd, + sessionName, + }); + if (events.length === 0) { + this.logger?.warn?.( + `acpx ensureSession returned no events after replacing dead session: session=${sessionName} agent=${agent} cwd=${cwd}`, + ); + } + ensuredEvent = findSessionIdentifierEvent(events); + } + + if (!ensuredEvent && !resumeSessionId) { + events = await this.createNamedSession({ + agent, + cwd, + sessionName, + }); + if (events.length === 0) { + this.logger?.warn?.( + `acpx ensureSession returned no events after sessions new: session=${sessionName} agent=${agent} cwd=${cwd}`, + ); + } + ensuredEvent = findSessionIdentifierEvent(events); + } if (!ensuredEvent) { throw new AcpRuntimeError( "ACP_SESSION_INIT_FAILED", diff --git a/extensions/acpx/src/service.ts b/extensions/acpx/src/service.ts index a863546fb30..524c25d6e63 100644 --- a/extensions/acpx/src/service.ts +++ b/extensions/acpx/src/service.ts @@ -3,8 +3,8 @@ import type { OpenClawPluginService, OpenClawPluginServiceContext, PluginLogger, -} from "openclaw/plugin-sdk/acpx"; -import { registerAcpRuntimeBackend, unregisterAcpRuntimeBackend } from "openclaw/plugin-sdk/acpx"; +} from "../runtime-api.js"; +import { registerAcpRuntimeBackend, unregisterAcpRuntimeBackend } from "../runtime-api.js"; import { resolveAcpxPluginConfig, type ResolvedAcpxPluginConfig } from "./config.js"; import { ensureAcpx } from "./ensure.js"; import { ACPX_BACKEND_ID, AcpxRuntime } from "./runtime.js"; diff --git a/extensions/acpx/src/test-utils/runtime-fixtures.ts b/extensions/acpx/src/test-utils/runtime-fixtures.ts index ebf5052f450..4ebe57b3e2a 100644 --- a/extensions/acpx/src/test-utils/runtime-fixtures.ts +++ b/extensions/acpx/src/test-utils/runtime-fixtures.ts @@ -76,6 +76,17 @@ const setValue = command === "set" ? String(args[commandIndex + 2] || "") : ""; if (command === "sessions" && args[commandIndex + 1] === "ensure") { writeLog({ kind: "ensure", agent, args, sessionName: ensureName }); + if (process.env.MOCK_ACPX_ENSURE_EXIT_1 === "1") { + emitJson({ + jsonrpc: "2.0", + id: null, + error: { + code: -32603, + message: "mock ensure failure", + }, + }); + process.exit(1); + } if (process.env.MOCK_ACPX_ENSURE_EMPTY === "1") { emitJson({ action: "session_ensured", name: ensureName }); } else { @@ -173,11 +184,14 @@ if (command === "set") { if (command === "status") { writeLog({ kind: "status", agent, args, sessionName: sessionFromOption }); + const status = process.env.MOCK_ACPX_STATUS_STATUS || (sessionFromOption ? "alive" : "no-session"); + const summary = process.env.MOCK_ACPX_STATUS_SUMMARY || ""; emitJson({ acpxRecordId: sessionFromOption ? "rec-" + sessionFromOption : null, acpxSessionId: sessionFromOption ? "sid-" + sessionFromOption : null, agentSessionId: sessionFromOption ? "inner-" + sessionFromOption : null, - status: sessionFromOption ? "alive" : "no-session", + status, + ...(summary ? { summary } : {}), pid: 4242, uptime: 120, }); @@ -382,6 +396,9 @@ export async function readMockRuntimeLogEntries( export async function cleanupMockRuntimeFixtures(): Promise { delete process.env.MOCK_ACPX_LOG; delete process.env.MOCK_ACPX_CONFIG_SHOW_AGENTS; + delete process.env.MOCK_ACPX_ENSURE_EXIT_1; + delete process.env.MOCK_ACPX_STATUS_STATUS; + delete process.env.MOCK_ACPX_STATUS_SUMMARY; sharedMockCliScriptPath = null; logFileSequence = 0; while (tempDirs.length > 0) { diff --git a/extensions/amazon-bedrock/index.test.ts b/extensions/amazon-bedrock/index.test.ts index 4afa67e3501..87ce6f6dcd2 100644 --- a/extensions/amazon-bedrock/index.test.ts +++ b/extensions/amazon-bedrock/index.test.ts @@ -19,4 +19,27 @@ describe("amazon-bedrock provider plugin", () => { } as never), ).toBeUndefined(); }); + + it("disables prompt caching for non-Anthropic Bedrock models", () => { + const provider = registerSingleProviderPlugin(amazonBedrockPlugin); + const wrapped = provider.wrapStreamFn?.({ + provider: "amazon-bedrock", + modelId: "amazon.nova-micro-v1:0", + streamFn: (_model, _context, options) => options, + } as never); + + expect( + wrapped?.( + { + api: "openai-completions", + provider: "amazon-bedrock", + id: "amazon.nova-micro-v1:0", + } as never, + { messages: [] } as never, + {}, + ), + ).toMatchObject({ + cacheRetention: "none", + }); + }); }); diff --git a/extensions/amazon-bedrock/index.ts b/extensions/amazon-bedrock/index.ts index 9158ab158d7..01c7f62687b 100644 --- a/extensions/amazon-bedrock/index.ts +++ b/extensions/amazon-bedrock/index.ts @@ -1,4 +1,8 @@ import { definePluginEntry } from "openclaw/plugin-sdk/core"; +import { + createBedrockNoCacheWrapper, + isAnthropicBedrockModel, +} from "openclaw/plugin-sdk/provider-stream"; const PROVIDER_ID = "amazon-bedrock"; const CLAUDE_46_MODEL_RE = /claude-(?:opus|sonnet)-4(?:\.|-)6(?:$|[-.])/i; @@ -13,6 +17,8 @@ export default definePluginEntry({ label: "Amazon Bedrock", docsPath: "/providers/models", auth: [], + wrapStreamFn: ({ modelId, streamFn }) => + isAnthropicBedrockModel(modelId) ? streamFn : createBedrockNoCacheWrapper(streamFn), resolveDefaultThinkingLevel: ({ modelId }) => CLAUDE_46_MODEL_RE.test(modelId.trim()) ? "adaptive" : undefined, }); diff --git a/extensions/bluebubbles/api.ts b/extensions/bluebubbles/api.ts new file mode 100644 index 00000000000..414efd5531e --- /dev/null +++ b/extensions/bluebubbles/api.ts @@ -0,0 +1 @@ +export { bluebubblesPlugin } from "./src/channel.js"; diff --git a/extensions/bluebubbles/index.ts b/extensions/bluebubbles/index.ts index 778cbd8ae8f..3e4ab2b4ff8 100644 --- a/extensions/bluebubbles/index.ts +++ b/extensions/bluebubbles/index.ts @@ -2,6 +2,9 @@ import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core"; import { bluebubblesPlugin } from "./src/channel.js"; import { setBlueBubblesRuntime } from "./src/runtime.js"; +export { bluebubblesPlugin } from "./src/channel.js"; +export { setBlueBubblesRuntime } from "./src/runtime.js"; + export default defineChannelPluginEntry({ id: "bluebubbles", name: "BlueBubbles", diff --git a/extensions/bluebubbles/src/account-resolve.ts b/extensions/bluebubbles/src/account-resolve.ts index 7d28d0dd3c8..f4ac1f06618 100644 --- a/extensions/bluebubbles/src/account-resolve.ts +++ b/extensions/bluebubbles/src/account-resolve.ts @@ -1,5 +1,5 @@ -import type { OpenClawConfig } from "openclaw/plugin-sdk/bluebubbles"; import { resolveBlueBubblesAccount } from "./accounts.js"; +import type { OpenClawConfig } from "./runtime-api.js"; import { normalizeResolvedSecretInputString } from "./secret-input.js"; export type BlueBubblesAccountResolveOpts = { diff --git a/extensions/bluebubbles/src/accounts.ts b/extensions/bluebubbles/src/accounts.ts index d7c5a281473..0584922dfca 100644 --- a/extensions/bluebubbles/src/accounts.ts +++ b/extensions/bluebubbles/src/accounts.ts @@ -1,5 +1,5 @@ import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id"; -import { createAccountListHelpers, type OpenClawConfig } from "openclaw/plugin-sdk/bluebubbles"; +import { createAccountListHelpers, type OpenClawConfig } from "./runtime-api.js"; import { hasConfiguredSecretInput, normalizeSecretInputString } from "./secret-input.js"; import { normalizeBlueBubblesServerUrl, type BlueBubblesAccountConfig } from "./types.js"; diff --git a/extensions/bluebubbles/src/actions.test.ts b/extensions/bluebubbles/src/actions.test.ts index 0560567c5fb..02cda25b5bc 100644 --- a/extensions/bluebubbles/src/actions.test.ts +++ b/extensions/bluebubbles/src/actions.test.ts @@ -1,7 +1,12 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk/bluebubbles"; import { describe, expect, it, vi, beforeEach } from "vitest"; import { bluebubblesMessageActions } from "./actions.js"; +import { sendBlueBubblesAttachment } from "./attachments.js"; +import { editBlueBubblesMessage, setGroupIconBlueBubbles } from "./chat.js"; +import { resolveBlueBubblesMessageId } from "./monitor.js"; import { getCachedBlueBubblesPrivateApiStatus } from "./probe.js"; +import { sendBlueBubblesReaction } from "./reactions.js"; +import { resolveChatGuidForTarget, sendMessageBlueBubbles } from "./send.js"; vi.mock("./accounts.js", async () => { const { createBlueBubblesAccountsMockModule } = await import("./test-harness.js"); @@ -41,7 +46,7 @@ vi.mock("./probe.js", () => ({ })); describe("bluebubblesMessageActions", () => { - const listActions = bluebubblesMessageActions.listActions!; + const describeMessageTool = bluebubblesMessageActions.describeMessageTool!; const supportsAction = bluebubblesMessageActions.supportsAction!; const extractToolSend = bluebubblesMessageActions.extractToolSend!; const handleAction = bluebubblesMessageActions.handleAction!; @@ -69,12 +74,12 @@ describe("bluebubblesMessageActions", () => { vi.mocked(getCachedBlueBubblesPrivateApiStatus).mockReturnValue(null); }); - describe("listActions", () => { + describe("describeMessageTool", () => { it("returns empty array when account is not enabled", () => { const cfg: OpenClawConfig = { channels: { bluebubbles: { enabled: false } }, }; - const actions = listActions({ cfg }); + const actions = describeMessageTool({ cfg })?.actions ?? []; expect(actions).toEqual([]); }); @@ -82,7 +87,7 @@ describe("bluebubblesMessageActions", () => { const cfg: OpenClawConfig = { channels: { bluebubbles: { enabled: true } }, }; - const actions = listActions({ cfg }); + const actions = describeMessageTool({ cfg })?.actions ?? []; expect(actions).toEqual([]); }); @@ -96,7 +101,7 @@ describe("bluebubblesMessageActions", () => { }, }, }; - const actions = listActions({ cfg }); + const actions = describeMessageTool({ cfg })?.actions ?? []; expect(actions).toContain("react"); }); @@ -111,7 +116,7 @@ describe("bluebubblesMessageActions", () => { }, }, }; - const actions = listActions({ cfg }); + const actions = describeMessageTool({ cfg })?.actions ?? []; expect(actions).not.toContain("react"); // Other actions should still be present expect(actions).toContain("edit"); @@ -129,7 +134,7 @@ describe("bluebubblesMessageActions", () => { }, }, }; - const actions = listActions({ cfg }); + const actions = describeMessageTool({ cfg })?.actions ?? []; expect(actions).toContain("sendAttachment"); expect(actions).not.toContain("react"); expect(actions).not.toContain("reply"); @@ -277,7 +282,6 @@ describe("bluebubblesMessageActions", () => { }); it("throws when chatGuid cannot be resolved", async () => { - const { resolveChatGuidForTarget } = await import("./send.js"); vi.mocked(resolveChatGuidForTarget).mockResolvedValueOnce(null); const cfg: OpenClawConfig = { @@ -299,8 +303,6 @@ describe("bluebubblesMessageActions", () => { }); it("sends reaction successfully with chatGuid", async () => { - const { sendBlueBubblesReaction } = await import("./reactions.js"); - const result = await runReactAction({ emoji: "❤️", messageId: "msg-123", @@ -321,8 +323,6 @@ describe("bluebubblesMessageActions", () => { }); it("sends reaction removal successfully", async () => { - const { sendBlueBubblesReaction } = await import("./reactions.js"); - const result = await runReactAction({ emoji: "❤️", messageId: "msg-123", @@ -342,8 +342,6 @@ describe("bluebubblesMessageActions", () => { }); it("resolves chatGuid from to parameter", async () => { - const { sendBlueBubblesReaction } = await import("./reactions.js"); - const { resolveChatGuidForTarget } = await import("./send.js"); vi.mocked(resolveChatGuidForTarget).mockResolvedValueOnce("iMessage;-;+15559876543"); const cfg: OpenClawConfig = { @@ -374,8 +372,6 @@ describe("bluebubblesMessageActions", () => { }); it("passes partIndex when provided", async () => { - const { sendBlueBubblesReaction } = await import("./reactions.js"); - const cfg: OpenClawConfig = { channels: { bluebubbles: { @@ -404,8 +400,6 @@ describe("bluebubblesMessageActions", () => { }); it("uses toolContext currentChannelId when no explicit target is provided", async () => { - const { sendBlueBubblesReaction } = await import("./reactions.js"); - const { resolveChatGuidForTarget } = await import("./send.js"); vi.mocked(resolveChatGuidForTarget).mockResolvedValueOnce("iMessage;-;+15550001111"); const cfg: OpenClawConfig = { @@ -442,8 +436,6 @@ describe("bluebubblesMessageActions", () => { }); it("resolves short messageId before reacting", async () => { - const { resolveBlueBubblesMessageId } = await import("./monitor.js"); - const { sendBlueBubblesReaction } = await import("./reactions.js"); vi.mocked(resolveBlueBubblesMessageId).mockReturnValueOnce("resolved-uuid"); const cfg: OpenClawConfig = { @@ -475,7 +467,6 @@ describe("bluebubblesMessageActions", () => { }); it("propagates short-id errors from the resolver", async () => { - const { resolveBlueBubblesMessageId } = await import("./monitor.js"); vi.mocked(resolveBlueBubblesMessageId).mockImplementationOnce(() => { throw new Error("short id expired"); }); @@ -504,8 +495,6 @@ describe("bluebubblesMessageActions", () => { }); it("accepts message param for edit action", async () => { - const { editBlueBubblesMessage } = await import("./chat.js"); - const cfg: OpenClawConfig = { channels: { bluebubbles: { @@ -530,8 +519,6 @@ describe("bluebubblesMessageActions", () => { }); it("accepts message/target aliases for sendWithEffect", async () => { - const { sendMessageBlueBubbles } = await import("./send.js"); - const cfg: OpenClawConfig = { channels: { bluebubbles: { @@ -563,8 +550,6 @@ describe("bluebubblesMessageActions", () => { }); it("passes asVoice through sendAttachment", async () => { - const { sendBlueBubblesAttachment } = await import("./attachments.js"); - const cfg: OpenClawConfig = { channels: { bluebubbles: { @@ -619,8 +604,6 @@ describe("bluebubblesMessageActions", () => { }); it("sets group icon successfully with chatGuid and buffer", async () => { - const { setGroupIconBlueBubbles } = await import("./chat.js"); - const cfg: OpenClawConfig = { channels: { bluebubbles: { @@ -658,8 +641,6 @@ describe("bluebubblesMessageActions", () => { }); it("uses default filename when not provided for setGroupIcon", async () => { - const { setGroupIconBlueBubbles } = await import("./chat.js"); - const cfg: OpenClawConfig = { channels: { bluebubbles: { diff --git a/extensions/bluebubbles/src/actions.ts b/extensions/bluebubbles/src/actions.ts index 47eedf97511..cc8f66ca770 100644 --- a/extensions/bluebubbles/src/actions.ts +++ b/extensions/bluebubbles/src/actions.ts @@ -1,3 +1,6 @@ +import { createLazyRuntimeNamedExport } from "openclaw/plugin-sdk/lazy-runtime"; +import { resolveBlueBubblesAccount } from "./accounts.js"; +import { getCachedBlueBubblesPrivateApiStatus, isMacOS26OrHigher } from "./probe.js"; import { BLUEBUBBLES_ACTION_NAMES, BLUEBUBBLES_ACTIONS, @@ -10,19 +13,18 @@ import { readStringParam, type ChannelMessageActionAdapter, type ChannelMessageActionName, -} from "openclaw/plugin-sdk/bluebubbles"; -import { createLazyRuntimeSurface } from "../../../src/shared/lazy-runtime.js"; -import { resolveBlueBubblesAccount } from "./accounts.js"; -import { getCachedBlueBubblesPrivateApiStatus, isMacOS26OrHigher } from "./probe.js"; +} from "./runtime-api.js"; import { normalizeSecretInputString } from "./secret-input.js"; -import { normalizeBlueBubblesHandle, parseBlueBubblesTarget } from "./targets.js"; +import { + normalizeBlueBubblesHandle, + normalizeBlueBubblesMessagingTarget, + parseBlueBubblesTarget, +} from "./targets.js"; import type { BlueBubblesSendTarget } from "./types.js"; -type BlueBubblesActionsRuntime = typeof import("./actions.runtime.js").blueBubblesActionsRuntime; - -const loadBlueBubblesActionsRuntime = createLazyRuntimeSurface( +const loadBlueBubblesActionsRuntime = createLazyRuntimeNamedExport( () => import("./actions.runtime.js"), - ({ blueBubblesActionsRuntime }) => blueBubblesActionsRuntime, + "blueBubblesActionsRuntime", ); const providerId = "bluebubbles"; @@ -65,10 +67,10 @@ const PRIVATE_API_ACTIONS = new Set([ ]); export const bluebubblesMessageActions: ChannelMessageActionAdapter = { - listActions: ({ cfg }) => { + describeMessageTool: ({ cfg, currentChannelId }) => { const account = resolveBlueBubblesAccount({ cfg: cfg }); if (!account.enabled || !account.configured) { - return []; + return null; } const gate = createActionGate(cfg.channels?.bluebubbles?.actions); const actions = new Set(); @@ -89,7 +91,23 @@ export const bluebubblesMessageActions: ChannelMessageActionAdapter = { actions.add(action); } } - return Array.from(actions); + const normalizedTarget = currentChannelId + ? normalizeBlueBubblesMessagingTarget(currentChannelId) + : undefined; + const lowered = normalizedTarget?.trim().toLowerCase() ?? ""; + const isGroupTarget = + lowered.startsWith("chat_guid:") || + lowered.startsWith("chat_id:") || + lowered.startsWith("chat_identifier:") || + lowered.startsWith("group:"); + if (!isGroupTarget) { + for (const action of BLUEBUBBLES_ACTION_NAMES) { + if ("groupOnly" in BLUEBUBBLES_ACTIONS[action] && BLUEBUBBLES_ACTIONS[action].groupOnly) { + actions.delete(action); + } + } + } + return { actions: Array.from(actions) }; }, supportsAction: ({ action }) => SUPPORTED_ACTIONS.has(action), extractToolSend: ({ args }) => extractToolSend(args, "sendMessage"), diff --git a/extensions/bluebubbles/src/attachments.ts b/extensions/bluebubbles/src/attachments.ts index c5392fd2595..5aab9fd3b68 100644 --- a/extensions/bluebubbles/src/attachments.ts +++ b/extensions/bluebubbles/src/attachments.ts @@ -1,6 +1,5 @@ import crypto from "node:crypto"; import path from "node:path"; -import type { OpenClawConfig } from "openclaw/plugin-sdk/bluebubbles"; import { resolveBlueBubblesServerAccount } from "./account-resolve.js"; import { assertMultipartActionOk, postMultipartFormData } from "./multipart.js"; import { @@ -8,6 +7,7 @@ import { isBlueBubblesPrivateApiStatusEnabled, } from "./probe.js"; import { resolveRequestUrl } from "./request-url.js"; +import type { OpenClawConfig } from "./runtime-api.js"; import { getBlueBubblesRuntime, warnBlueBubbles } from "./runtime.js"; import { extractBlueBubblesMessageId, resolveBlueBubblesSendTarget } from "./send-helpers.js"; import { resolveChatGuidForTarget } from "./send.js"; diff --git a/extensions/bluebubbles/src/channel.ts b/extensions/bluebubbles/src/channel.ts index f3f3cdd7eb3..33249fcfa9e 100644 --- a/extensions/bluebubbles/src/channel.ts +++ b/extensions/bluebubbles/src/channel.ts @@ -1,24 +1,11 @@ import { formatNormalizedAllowFromEntries } from "openclaw/plugin-sdk/allow-from"; -import type { ChannelAccountSnapshot, ChannelPlugin } from "openclaw/plugin-sdk/bluebubbles"; import { - buildChannelConfigSchema, - buildComputedAccountStatusSnapshot, - buildProbeChannelStatusSummary, - collectBlueBubblesStatusIssues, - DEFAULT_ACCOUNT_ID, - deleteAccountFromConfigSection, - PAIRING_APPROVED_MESSAGE, - resolveBlueBubblesGroupRequireMention, - resolveBlueBubblesGroupToolPolicy, - setAccountEnabledInConfigSection, -} from "openclaw/plugin-sdk/bluebubbles"; -import { mapAllowFromEntries } from "openclaw/plugin-sdk/channel-config-helpers"; + createScopedChannelConfigAdapter, + createScopedDmSecurityResolver, +} from "openclaw/plugin-sdk/channel-config-helpers"; import { createAccountStatusSink } from "openclaw/plugin-sdk/channel-lifecycle"; -import { - buildAccountScopedDmSecurityPolicy, - collectOpenGroupPolicyRestrictSendersWarnings, -} from "openclaw/plugin-sdk/channel-policy"; -import { createLazyRuntimeSurface } from "../../../src/shared/lazy-runtime.js"; +import { collectOpenGroupPolicyRestrictSendersWarnings } from "openclaw/plugin-sdk/channel-policy"; +import { createLazyRuntimeNamedExport } from "openclaw/plugin-sdk/lazy-runtime"; import { listBlueBubblesAccountIds, type ResolvedBlueBubblesAccount, @@ -28,23 +15,59 @@ import { import { bluebubblesMessageActions } from "./actions.js"; import type { BlueBubblesProbe } from "./channel.runtime.js"; import { BlueBubblesConfigSchema } from "./config-schema.js"; +import { + resolveBlueBubblesGroupRequireMention, + resolveBlueBubblesGroupToolPolicy, +} from "./group-policy.js"; +import type { ChannelAccountSnapshot, ChannelPlugin } from "./runtime-api.js"; +import { + buildChannelConfigSchema, + buildComputedAccountStatusSnapshot, + buildProbeChannelStatusSummary, + collectBlueBubblesStatusIssues, + DEFAULT_ACCOUNT_ID, + PAIRING_APPROVED_MESSAGE, +} from "./runtime-api.js"; +import { resolveBlueBubblesOutboundSessionRoute } from "./session-route.js"; import { blueBubblesSetupAdapter } from "./setup-core.js"; import { blueBubblesSetupWizard } from "./setup-surface.js"; import { extractHandleFromChatGuid, + inferBlueBubblesTargetChatType, + looksLikeBlueBubblesExplicitTargetId, looksLikeBlueBubblesTargetId, normalizeBlueBubblesHandle, normalizeBlueBubblesMessagingTarget, parseBlueBubblesTarget, } from "./targets.js"; -type BlueBubblesChannelRuntime = typeof import("./channel.runtime.js").blueBubblesChannelRuntime; - -const loadBlueBubblesChannelRuntime = createLazyRuntimeSurface( +const loadBlueBubblesChannelRuntime = createLazyRuntimeNamedExport( () => import("./channel.runtime.js"), - ({ blueBubblesChannelRuntime }) => blueBubblesChannelRuntime, + "blueBubblesChannelRuntime", ); +const bluebubblesConfigAdapter = createScopedChannelConfigAdapter({ + sectionKey: "bluebubbles", + listAccountIds: listBlueBubblesAccountIds, + resolveAccount: (cfg, accountId) => resolveBlueBubblesAccount({ cfg, accountId }), + defaultAccountId: resolveDefaultBlueBubblesAccountId, + clearBaseFields: ["serverUrl", "password", "name", "webhookPath"], + resolveAllowFrom: (account: ResolvedBlueBubblesAccount) => account.config.allowFrom, + formatAllowFrom: (allowFrom) => + formatNormalizedAllowFromEntries({ + allowFrom, + normalizeEntry: (entry) => normalizeBlueBubblesHandle(entry.replace(/^bluebubbles:/i, "")), + }), +}); + +const resolveBlueBubblesDmPolicy = createScopedDmSecurityResolver({ + channelKey: "bluebubbles", + resolvePolicy: (account) => account.config.dmPolicy, + resolveAllowFrom: (account) => account.config.allowFrom, + policyPathSuffix: "dmPolicy", + normalizeEntry: (raw) => normalizeBlueBubblesHandle(raw.replace(/^bluebubbles:/i, "")), +}); + const meta = { id: "bluebubbles", label: "BlueBubbles", @@ -87,24 +110,7 @@ export const bluebubblesPlugin: ChannelPlugin = { configSchema: buildChannelConfigSchema(BlueBubblesConfigSchema), setupWizard: blueBubblesSetupWizard, config: { - listAccountIds: (cfg) => listBlueBubblesAccountIds(cfg), - resolveAccount: (cfg, accountId) => resolveBlueBubblesAccount({ cfg: cfg, accountId }), - defaultAccountId: (cfg) => resolveDefaultBlueBubblesAccountId(cfg), - setAccountEnabled: ({ cfg, accountId, enabled }) => - setAccountEnabledInConfigSection({ - cfg: cfg, - sectionKey: "bluebubbles", - accountId, - enabled, - allowTopLevel: true, - }), - deleteAccount: ({ cfg, accountId }) => - deleteAccountFromConfigSection({ - cfg: cfg, - sectionKey: "bluebubbles", - accountId, - clearBaseFields: ["serverUrl", "password", "name", "webhookPath"], - }), + ...bluebubblesConfigAdapter, isConfigured: (account) => account.configured, describeAccount: (account): ChannelAccountSnapshot => ({ accountId: account.accountId, @@ -113,28 +119,10 @@ export const bluebubblesPlugin: ChannelPlugin = { configured: account.configured, baseUrl: account.baseUrl, }), - resolveAllowFrom: ({ cfg, accountId }) => - mapAllowFromEntries(resolveBlueBubblesAccount({ cfg: cfg, accountId }).config.allowFrom), - formatAllowFrom: ({ allowFrom }) => - formatNormalizedAllowFromEntries({ - allowFrom, - normalizeEntry: (entry) => normalizeBlueBubblesHandle(entry.replace(/^bluebubbles:/i, "")), - }), }, actions: bluebubblesMessageActions, security: { - resolveDmPolicy: ({ cfg, accountId, account }) => { - return buildAccountScopedDmSecurityPolicy({ - cfg, - channelKey: "bluebubbles", - accountId, - fallbackAccountId: account.accountId ?? DEFAULT_ACCOUNT_ID, - policy: account.config.dmPolicy, - allowFrom: account.config.allowFrom ?? [], - policyPathSuffix: "dmPolicy", - normalizeEntry: (raw) => normalizeBlueBubblesHandle(raw.replace(/^bluebubbles:/i, "")), - }); - }, + resolveDmPolicy: resolveBlueBubblesDmPolicy, collectWarnings: ({ account }) => { const groupPolicy = account.config.groupPolicy ?? "allowlist"; return collectOpenGroupPolicyRestrictSendersWarnings({ @@ -149,9 +137,26 @@ export const bluebubblesPlugin: ChannelPlugin = { }, messaging: { normalizeTarget: normalizeBlueBubblesMessagingTarget, + inferTargetChatType: ({ to }) => inferBlueBubblesTargetChatType(to), + resolveOutboundSessionRoute: (params) => resolveBlueBubblesOutboundSessionRoute(params), targetResolver: { - looksLikeId: looksLikeBlueBubblesTargetId, + looksLikeId: looksLikeBlueBubblesExplicitTargetId, hint: "", + resolveTarget: async ({ normalized }) => { + const to = normalized?.trim(); + if (!to) { + return null; + } + const chatType = inferBlueBubblesTargetChatType(to); + if (!chatType) { + return null; + } + return { + to, + kind: chatType === "direct" ? "user" : "group", + source: "normalized" as const, + }; + }, }, formatTargetDisplay: ({ target, display }) => { const shouldParseDisplay = (value: string): boolean => { diff --git a/extensions/bluebubbles/src/chat.ts b/extensions/bluebubbles/src/chat.ts index 17340b7f980..5d027ef97e8 100644 --- a/extensions/bluebubbles/src/chat.ts +++ b/extensions/bluebubbles/src/chat.ts @@ -1,9 +1,9 @@ import crypto from "node:crypto"; import path from "node:path"; -import type { OpenClawConfig } from "openclaw/plugin-sdk/bluebubbles"; import { resolveBlueBubblesServerAccount } from "./account-resolve.js"; import { assertMultipartActionOk, postMultipartFormData } from "./multipart.js"; import { getCachedBlueBubblesPrivateApiStatus } from "./probe.js"; +import type { OpenClawConfig } from "./runtime-api.js"; import { blueBubblesFetchWithTimeout, buildBlueBubblesApiUrl } from "./types.js"; export type BlueBubblesChatOpts = { diff --git a/extensions/bluebubbles/src/config-apply.ts b/extensions/bluebubbles/src/config-apply.ts index 70b8c7cae37..e70d718a804 100644 --- a/extensions/bluebubbles/src/config-apply.ts +++ b/extensions/bluebubbles/src/config-apply.ts @@ -1,4 +1,4 @@ -import { DEFAULT_ACCOUNT_ID, type OpenClawConfig } from "openclaw/plugin-sdk/bluebubbles"; +import { DEFAULT_ACCOUNT_ID, type OpenClawConfig } from "./runtime-api.js"; type BlueBubblesConfigPatch = { serverUrl?: string; diff --git a/extensions/bluebubbles/src/config-schema.ts b/extensions/bluebubbles/src/config-schema.ts index da66869708e..b85f6b72841 100644 --- a/extensions/bluebubbles/src/config-schema.ts +++ b/extensions/bluebubbles/src/config-schema.ts @@ -1,4 +1,3 @@ -import { MarkdownConfigSchema, ToolPolicySchema } from "openclaw/plugin-sdk/bluebubbles"; import { AllowFromListSchema, buildCatchallMultiAccountChannelSchema, @@ -6,6 +5,7 @@ import { GroupPolicySchema, } from "openclaw/plugin-sdk/channel-config-schema"; import { z } from "zod"; +import { MarkdownConfigSchema, ToolPolicySchema } from "./runtime-api.js"; import { buildSecretInputSchema, hasConfiguredSecretInput } from "./secret-input.js"; const bluebubblesActionSchema = z diff --git a/extensions/bluebubbles/src/group-policy.test.ts b/extensions/bluebubbles/src/group-policy.test.ts new file mode 100644 index 00000000000..883f6c78b71 --- /dev/null +++ b/extensions/bluebubbles/src/group-policy.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, it } from "vitest"; +import { + resolveBlueBubblesGroupRequireMention, + resolveBlueBubblesGroupToolPolicy, +} from "./group-policy.js"; + +describe("bluebubbles group policy", () => { + it("uses generic channel group policy helpers", () => { + const cfg = { + channels: { + bluebubbles: { + groups: { + "chat:primary": { + requireMention: false, + tools: { deny: ["exec"] }, + }, + "*": { + requireMention: true, + tools: { allow: ["message.send"] }, + }, + }, + }, + }, + // oxlint-disable-next-line typescript/no-explicit-any + } as any; + + expect(resolveBlueBubblesGroupRequireMention({ cfg, groupId: "chat:primary" })).toBe(false); + expect(resolveBlueBubblesGroupRequireMention({ cfg, groupId: "chat:other" })).toBe(true); + expect(resolveBlueBubblesGroupToolPolicy({ cfg, groupId: "chat:primary" })).toEqual({ + deny: ["exec"], + }); + expect(resolveBlueBubblesGroupToolPolicy({ cfg, groupId: "chat:other" })).toEqual({ + allow: ["message.send"], + }); + }); +}); diff --git a/extensions/bluebubbles/src/group-policy.ts b/extensions/bluebubbles/src/group-policy.ts new file mode 100644 index 00000000000..d3b42cd45b4 --- /dev/null +++ b/extensions/bluebubbles/src/group-policy.ts @@ -0,0 +1,40 @@ +import { + resolveChannelGroupRequireMention, + resolveChannelGroupToolsPolicy, + type GroupToolPolicyConfig, +} from "openclaw/plugin-sdk/channel-policy"; +import type { OpenClawConfig } from "./runtime-api.js"; + +type BlueBubblesGroupContext = { + cfg: OpenClawConfig; + accountId?: string | null; + groupId?: string | null; + senderId?: string | null; + senderName?: string | null; + senderUsername?: string | null; + senderE164?: string | null; +}; + +export function resolveBlueBubblesGroupRequireMention(params: BlueBubblesGroupContext): boolean { + return resolveChannelGroupRequireMention({ + cfg: params.cfg, + channel: "bluebubbles", + groupId: params.groupId, + accountId: params.accountId, + }); +} + +export function resolveBlueBubblesGroupToolPolicy( + params: BlueBubblesGroupContext, +): GroupToolPolicyConfig | undefined { + return resolveChannelGroupToolsPolicy({ + cfg: params.cfg, + channel: "bluebubbles", + groupId: params.groupId, + accountId: params.accountId, + senderId: params.senderId, + senderName: params.senderName, + senderUsername: params.senderUsername, + senderE164: params.senderE164, + }); +} diff --git a/extensions/bluebubbles/src/history.ts b/extensions/bluebubbles/src/history.ts index 388af325d1a..d917512beb8 100644 --- a/extensions/bluebubbles/src/history.ts +++ b/extensions/bluebubbles/src/history.ts @@ -1,5 +1,5 @@ -import type { OpenClawConfig } from "openclaw/plugin-sdk/bluebubbles"; import { resolveBlueBubblesServerAccount } from "./account-resolve.js"; +import type { OpenClawConfig } from "./runtime-api.js"; import { blueBubblesFetchWithTimeout, buildBlueBubblesApiUrl } from "./types.js"; export type BlueBubblesHistoryEntry = { diff --git a/extensions/bluebubbles/src/media-send.ts b/extensions/bluebubbles/src/media-send.ts index 8bd505efcf7..42703f960dc 100644 --- a/extensions/bluebubbles/src/media-send.ts +++ b/extensions/bluebubbles/src/media-send.ts @@ -3,10 +3,10 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; -import { resolveChannelMediaMaxBytes, type OpenClawConfig } from "openclaw/plugin-sdk/bluebubbles"; import { resolveBlueBubblesAccount } from "./accounts.js"; import { sendBlueBubblesAttachment } from "./attachments.js"; import { resolveBlueBubblesMessageId } from "./monitor.js"; +import { resolveChannelMediaMaxBytes, type OpenClawConfig } from "./runtime-api.js"; import { getBlueBubblesRuntime } from "./runtime.js"; import { sendMessageBlueBubbles } from "./send.js"; diff --git a/extensions/bluebubbles/src/monitor-debounce.ts b/extensions/bluebubbles/src/monitor-debounce.ts index 3a3189cc7ea..298be3e4921 100644 --- a/extensions/bluebubbles/src/monitor-debounce.ts +++ b/extensions/bluebubbles/src/monitor-debounce.ts @@ -1,6 +1,6 @@ -import type { OpenClawConfig } from "openclaw/plugin-sdk/bluebubbles"; import type { NormalizedWebhookMessage } from "./monitor-normalize.js"; import type { BlueBubblesCoreRuntime, WebhookTarget } from "./monitor-shared.js"; +import type { OpenClawConfig } from "./runtime-api.js"; /** * Entry type for debouncing inbound messages. diff --git a/extensions/bluebubbles/src/monitor-normalize.ts b/extensions/bluebubbles/src/monitor-normalize.ts index 085bd8923e1..339f380ba89 100644 --- a/extensions/bluebubbles/src/monitor-normalize.ts +++ b/extensions/bluebubbles/src/monitor-normalize.ts @@ -1,4 +1,4 @@ -import { parseFiniteNumber } from "openclaw/plugin-sdk/bluebubbles"; +import { parseFiniteNumber } from "./runtime-api.js"; import { extractHandleFromChatGuid, normalizeBlueBubblesHandle } from "./targets.js"; import type { BlueBubblesAttachment } from "./types.js"; diff --git a/extensions/bluebubbles/src/monitor-processing.ts b/extensions/bluebubbles/src/monitor-processing.ts index 9cf72ea1efd..958c629f766 100644 --- a/extensions/bluebubbles/src/monitor-processing.ts +++ b/extensions/bluebubbles/src/monitor-processing.ts @@ -1,22 +1,3 @@ -import type { OpenClawConfig } from "openclaw/plugin-sdk/bluebubbles"; -import { - DM_GROUP_ACCESS_REASON, - createScopedPairingAccess, - createReplyPrefixOptions, - evictOldHistoryKeys, - issuePairingChallenge, - logAckFailure, - logInboundDrop, - logTypingFailure, - mapAllowFromEntries, - readStoreAllowFromForDmPolicy, - recordPendingHistoryEntryIfEnabled, - resolveAckReaction, - resolveDmGroupAccessWithLists, - resolveControlCommandGate, - stripMarkdown, - type HistoryEntry, -} from "openclaw/plugin-sdk/bluebubbles"; import { downloadBlueBubblesAttachment } from "./attachments.js"; import { markBlueBubblesChatRead, sendBlueBubblesTyping } from "./chat.js"; import { fetchBlueBubblesHistory } from "./history.js"; @@ -49,6 +30,25 @@ import type { } from "./monitor-shared.js"; import { isBlueBubblesPrivateApiEnabled } from "./probe.js"; import { normalizeBlueBubblesReactionInput, sendBlueBubblesReaction } from "./reactions.js"; +import type { OpenClawConfig } from "./runtime-api.js"; +import { + DM_GROUP_ACCESS_REASON, + createScopedPairingAccess, + createReplyPrefixOptions, + evictOldHistoryKeys, + issuePairingChallenge, + logAckFailure, + logInboundDrop, + logTypingFailure, + mapAllowFromEntries, + readStoreAllowFromForDmPolicy, + recordPendingHistoryEntryIfEnabled, + resolveAckReaction, + resolveDmGroupAccessWithLists, + resolveControlCommandGate, + stripMarkdown, + type HistoryEntry, +} from "./runtime-api.js"; import { normalizeSecretInputString } from "./secret-input.js"; import { resolveChatGuidForTarget, sendMessageBlueBubbles } from "./send.js"; import { diff --git a/extensions/bluebubbles/src/monitor-shared.ts b/extensions/bluebubbles/src/monitor-shared.ts index 2d40ac7b8d8..9f0776094a0 100644 --- a/extensions/bluebubbles/src/monitor-shared.ts +++ b/extensions/bluebubbles/src/monitor-shared.ts @@ -1,5 +1,5 @@ -import { normalizeWebhookPath, type OpenClawConfig } from "openclaw/plugin-sdk/bluebubbles"; import type { ResolvedBlueBubblesAccount } from "./accounts.js"; +import { normalizeWebhookPath, type OpenClawConfig } from "./runtime-api.js"; import { getBlueBubblesRuntime } from "./runtime.js"; import type { BlueBubblesAccountConfig } from "./types.js"; diff --git a/extensions/bluebubbles/src/monitor.ts b/extensions/bluebubbles/src/monitor.ts index 1dc503e5340..89d0a78a485 100644 --- a/extensions/bluebubbles/src/monitor.ts +++ b/extensions/bluebubbles/src/monitor.ts @@ -1,12 +1,5 @@ import { timingSafeEqual } from "node:crypto"; import type { IncomingMessage, ServerResponse } from "node:http"; -import { - createWebhookInFlightLimiter, - registerWebhookTargetWithPluginRoute, - readWebhookBodyOrReject, - resolveWebhookTargetWithAuthOrRejectSync, - withResolvedWebhookRequestPipeline, -} from "openclaw/plugin-sdk/bluebubbles"; import { createBlueBubblesDebounceRegistry } from "./monitor-debounce.js"; import { normalizeWebhookMessage, normalizeWebhookReaction } from "./monitor-normalize.js"; import { logVerbose, processMessage, processReaction } from "./monitor-processing.js"; @@ -22,6 +15,13 @@ import { type WebhookTarget, } from "./monitor-shared.js"; import { fetchBlueBubblesServerInfo } from "./probe.js"; +import { + createWebhookInFlightLimiter, + registerWebhookTargetWithPluginRoute, + readWebhookBodyOrReject, + resolveWebhookTargetWithAuthOrRejectSync, + withResolvedWebhookRequestPipeline, +} from "./runtime-api.js"; import { getBlueBubblesRuntime } from "./runtime.js"; const webhookTargets = new Map(); diff --git a/extensions/bluebubbles/src/probe.ts b/extensions/bluebubbles/src/probe.ts index 135423bc0fc..02134051aa5 100644 --- a/extensions/bluebubbles/src/probe.ts +++ b/extensions/bluebubbles/src/probe.ts @@ -1,4 +1,4 @@ -import type { BaseProbeResult } from "openclaw/plugin-sdk/bluebubbles"; +import type { BaseProbeResult } from "./runtime-api.js"; import { normalizeSecretInputString } from "./secret-input.js"; import { buildBlueBubblesApiUrl, blueBubblesFetchWithTimeout } from "./types.js"; @@ -73,7 +73,7 @@ export async function fetchBlueBubblesServerInfo(params: { } /** - * Get cached server info synchronously (for use in listActions). + * Get cached server info synchronously (for use in describeMessageTool). * Returns null if not cached or expired. */ export function getCachedBlueBubblesServerInfo(accountId?: string): BlueBubblesServerInfo | null { diff --git a/extensions/bluebubbles/src/reactions.ts b/extensions/bluebubbles/src/reactions.ts index 8a3837c12e4..1036972a9bb 100644 --- a/extensions/bluebubbles/src/reactions.ts +++ b/extensions/bluebubbles/src/reactions.ts @@ -1,6 +1,6 @@ -import type { OpenClawConfig } from "openclaw/plugin-sdk/bluebubbles"; import { resolveBlueBubblesServerAccount } from "./account-resolve.js"; import { getCachedBlueBubblesPrivateApiStatus } from "./probe.js"; +import type { OpenClawConfig } from "./runtime-api.js"; import { blueBubblesFetchWithTimeout, buildBlueBubblesApiUrl } from "./types.js"; export type BlueBubblesReactionOpts = { diff --git a/extensions/bluebubbles/src/request-url.ts b/extensions/bluebubbles/src/request-url.ts index cd1527f186f..abb6dd05918 100644 --- a/extensions/bluebubbles/src/request-url.ts +++ b/extensions/bluebubbles/src/request-url.ts @@ -1 +1 @@ -export { resolveRequestUrl } from "openclaw/plugin-sdk/bluebubbles"; +export { resolveRequestUrl } from "./runtime-api.js"; diff --git a/extensions/bluebubbles/src/runtime-api.ts b/extensions/bluebubbles/src/runtime-api.ts new file mode 100644 index 00000000000..23c09660d96 --- /dev/null +++ b/extensions/bluebubbles/src/runtime-api.ts @@ -0,0 +1 @@ +export * from "openclaw/plugin-sdk/bluebubbles"; diff --git a/extensions/bluebubbles/src/runtime.ts b/extensions/bluebubbles/src/runtime.ts index eae7bb24a29..2ac1c68ad91 100644 --- a/extensions/bluebubbles/src/runtime.ts +++ b/extensions/bluebubbles/src/runtime.ts @@ -1,5 +1,5 @@ -import type { PluginRuntime } from "openclaw/plugin-sdk/bluebubbles"; import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store"; +import type { PluginRuntime } from "./runtime-api.js"; const runtimeStore = createPluginRuntimeStore("BlueBubbles runtime not initialized"); type LegacyRuntimeLogShape = { log?: (message: string) => void }; diff --git a/extensions/bluebubbles/src/secret-input.ts b/extensions/bluebubbles/src/secret-input.ts index a5aa73ebda0..b32083456e7 100644 --- a/extensions/bluebubbles/src/secret-input.ts +++ b/extensions/bluebubbles/src/secret-input.ts @@ -3,7 +3,7 @@ import { hasConfiguredSecretInput, normalizeResolvedSecretInputString, normalizeSecretInputString, -} from "openclaw/plugin-sdk/bluebubbles"; +} from "./runtime-api.js"; export { buildSecretInputSchema, diff --git a/extensions/bluebubbles/src/send.ts b/extensions/bluebubbles/src/send.ts index 8c12e88bd23..8fe622d13ff 100644 --- a/extensions/bluebubbles/src/send.ts +++ b/extensions/bluebubbles/src/send.ts @@ -1,11 +1,11 @@ import crypto from "node:crypto"; -import type { OpenClawConfig } from "openclaw/plugin-sdk/bluebubbles"; -import { stripMarkdown } from "openclaw/plugin-sdk/bluebubbles"; import { resolveBlueBubblesAccount } from "./accounts.js"; import { getCachedBlueBubblesPrivateApiStatus, isBlueBubblesPrivateApiStatusEnabled, } from "./probe.js"; +import type { OpenClawConfig } from "./runtime-api.js"; +import { stripMarkdown } from "./runtime-api.js"; import { warnBlueBubbles } from "./runtime.js"; import { normalizeSecretInputString } from "./secret-input.js"; import { extractBlueBubblesMessageId, resolveBlueBubblesSendTarget } from "./send-helpers.js"; diff --git a/extensions/bluebubbles/src/session-route.ts b/extensions/bluebubbles/src/session-route.ts new file mode 100644 index 00000000000..a889887a4bd --- /dev/null +++ b/extensions/bluebubbles/src/session-route.ts @@ -0,0 +1,37 @@ +import { + buildChannelOutboundSessionRoute, + stripChannelTargetPrefix, + type ChannelOutboundSessionRouteParams, +} from "openclaw/plugin-sdk/core"; +import { parseBlueBubblesTarget } from "./targets.js"; + +export function resolveBlueBubblesOutboundSessionRoute(params: ChannelOutboundSessionRouteParams) { + const stripped = stripChannelTargetPrefix(params.target, "bluebubbles"); + if (!stripped) { + return null; + } + const parsed = parseBlueBubblesTarget(stripped); + const isGroup = + parsed.kind === "chat_id" || parsed.kind === "chat_guid" || parsed.kind === "chat_identifier"; + const peerId = + parsed.kind === "chat_id" + ? String(parsed.chatId) + : parsed.kind === "chat_guid" + ? parsed.chatGuid + : parsed.kind === "chat_identifier" + ? parsed.chatIdentifier + : parsed.to; + return buildChannelOutboundSessionRoute({ + cfg: params.cfg, + agentId: params.agentId, + channel: "bluebubbles", + accountId: params.accountId, + peer: { + kind: isGroup ? "group" : "direct", + id: peerId, + }, + chatType: isGroup ? "group" : "direct", + from: isGroup ? `group:${peerId}` : `bluebubbles:${peerId}`, + to: `bluebubbles:${stripped}`, + }); +} diff --git a/extensions/bluebubbles/src/setup-core.ts b/extensions/bluebubbles/src/setup-core.ts index a8d3261b7ff..df8cf016b0b 100644 --- a/extensions/bluebubbles/src/setup-core.ts +++ b/extensions/bluebubbles/src/setup-core.ts @@ -1,8 +1,8 @@ import { + createTopLevelChannelDmPolicySetter, normalizeAccountId, patchScopedAccountConfig, prepareScopedSetupConfig, - setTopLevelChannelDmPolicyWithAllowFrom, type ChannelSetupAdapter, type DmPolicy, type OpenClawConfig, @@ -10,13 +10,12 @@ import { import { applyBlueBubblesConnectionConfig } from "./config-apply.js"; const channel = "bluebubbles" as const; +const setBlueBubblesTopLevelDmPolicy = createTopLevelChannelDmPolicySetter({ + channel, +}); export function setBlueBubblesDmPolicy(cfg: OpenClawConfig, dmPolicy: DmPolicy): OpenClawConfig { - return setTopLevelChannelDmPolicyWithAllowFrom({ - cfg, - channel, - dmPolicy, - }); + return setBlueBubblesTopLevelDmPolicy(cfg, dmPolicy); } export function setBlueBubblesAllowFrom( diff --git a/extensions/bluebubbles/src/setup-surface.ts b/extensions/bluebubbles/src/setup-surface.ts index f6922ed4861..823b49908c8 100644 --- a/extensions/bluebubbles/src/setup-surface.ts +++ b/extensions/bluebubbles/src/setup-surface.ts @@ -1,11 +1,10 @@ import { + createAllowFromSection, DEFAULT_ACCOUNT_ID, formatDocsLink, - mergeAllowFromEntries, - resolveSetupAccountId, + promptParsedAllowFromForAccount, type ChannelSetupDmPolicy, type ChannelSetupWizard, - type DmPolicy, type OpenClawConfig, type WizardPrompter, } from "openclaw/plugin-sdk/setup"; @@ -55,14 +54,13 @@ async function promptBlueBubblesAllowFrom(params: { prompter: WizardPrompter; accountId?: string; }): Promise { - const accountId = resolveSetupAccountId({ + return await promptParsedAllowFromForAccount({ + cfg: params.cfg, accountId: params.accountId, defaultAccountId: resolveDefaultBlueBubblesAccountId(params.cfg), - }); - const resolved = resolveBlueBubblesAccount({ cfg: params.cfg, accountId }); - const existing = resolved.config.allowFrom ?? []; - await params.prompter.note( - [ + prompter: params.prompter, + noteTitle: "BlueBubbles allowlist", + noteLines: [ "Allowlist BlueBubbles DMs by handle or chat target.", "Examples:", "- +15555550123", @@ -71,30 +69,23 @@ async function promptBlueBubblesAllowFrom(params: { "- chat_guid:iMessage;-;+15555550123", "Multiple entries: comma- or newline-separated.", `Docs: ${formatDocsLink("/channels/bluebubbles", "bluebubbles")}`, - ].join("\n"), - "BlueBubbles allowlist", - ); - const entry = await params.prompter.text({ + ], message: "BlueBubbles allowFrom (handle or chat_id)", placeholder: "+15555550123, user@example.com, chat_id:123", - initialValue: existing[0] ? String(existing[0]) : undefined, - validate: (value) => { - const raw = String(value ?? "").trim(); - if (!raw) { - return "Required"; - } - const parts = parseBlueBubblesAllowFromInput(raw); - for (const part of parts) { - if (!validateBlueBubblesAllowFromEntry(part)) { - return `Invalid entry: ${part}`; + parseEntries: (raw) => { + const entries = parseBlueBubblesAllowFromInput(raw); + for (const entry of entries) { + if (!validateBlueBubblesAllowFromEntry(entry)) { + return { entries: [], error: `Invalid entry: ${entry}` }; } } - return undefined; + return { entries }; }, + getExistingAllowFrom: ({ cfg, accountId }) => + resolveBlueBubblesAccount({ cfg, accountId }).config.allowFrom ?? [], + applyAllowFrom: ({ cfg, accountId, allowFrom }) => + setBlueBubblesAllowFrom(cfg, accountId, allowFrom), }); - const parts = parseBlueBubblesAllowFromInput(String(entry)); - const unique = mergeAllowFromEntries(undefined, parts); - return setBlueBubblesAllowFrom(params.cfg, accountId, unique); } function validateBlueBubblesServerUrlInput(value: unknown): string | undefined { @@ -272,7 +263,7 @@ export const blueBubblesSetupWizard: ChannelSetupWizard = { ], }, dmPolicy, - allowFrom: { + allowFrom: createAllowFromSection({ helpTitle: "BlueBubbles allowlist", helpLines: [ "Allowlist BlueBubbles DMs by handle or chat target.", @@ -290,15 +281,9 @@ export const blueBubblesSetupWizard: ChannelSetupWizard = { "Use a BlueBubbles handle or chat target like +15555550123 or chat_id:123.", parseInputs: parseBlueBubblesAllowFromInput, parseId: (raw) => validateBlueBubblesAllowFromEntry(raw), - resolveEntries: async ({ entries }) => - entries.map((entry) => ({ - input: entry, - resolved: Boolean(validateBlueBubblesAllowFromEntry(entry)), - id: validateBlueBubblesAllowFromEntry(entry), - })), apply: async ({ cfg, accountId, allowFrom }) => setBlueBubblesAllowFrom(cfg, accountId, allowFrom), - }, + }), disable: (cfg) => ({ ...cfg, channels: { diff --git a/extensions/bluebubbles/src/targets.test.ts b/extensions/bluebubbles/src/targets.test.ts index c5b4109eb45..26475c70c3d 100644 --- a/extensions/bluebubbles/src/targets.test.ts +++ b/extensions/bluebubbles/src/targets.test.ts @@ -1,5 +1,7 @@ import { describe, expect, it } from "vitest"; import { + inferBlueBubblesTargetChatType, + looksLikeBlueBubblesExplicitTargetId, isAllowedBlueBubblesSender, looksLikeBlueBubblesTargetId, normalizeBlueBubblesMessagingTarget, @@ -101,6 +103,30 @@ describe("looksLikeBlueBubblesTargetId", () => { }); }); +describe("looksLikeBlueBubblesExplicitTargetId", () => { + it("treats explicit chat targets as immediate ids", () => { + expect(looksLikeBlueBubblesExplicitTargetId("chat_guid:ABC-123")).toBe(true); + expect(looksLikeBlueBubblesExplicitTargetId("imessage:+15551234567")).toBe(true); + }); + + it("prefers directory fallback for bare handles and phone numbers", () => { + expect(looksLikeBlueBubblesExplicitTargetId("+1 (555) 123-4567")).toBe(false); + expect(looksLikeBlueBubblesExplicitTargetId("user@example.com")).toBe(false); + }); +}); + +describe("inferBlueBubblesTargetChatType", () => { + it("infers direct chat for handles and dm chat_guids", () => { + expect(inferBlueBubblesTargetChatType("+15551234567")).toBe("direct"); + expect(inferBlueBubblesTargetChatType("chat_guid:iMessage;-;+15551234567")).toBe("direct"); + }); + + it("infers group chat for explicit group targets", () => { + expect(inferBlueBubblesTargetChatType("chat_id:123")).toBe("group"); + expect(inferBlueBubblesTargetChatType("chat_guid:iMessage;+;chat123")).toBe("group"); + }); +}); + describe("parseBlueBubblesTarget", () => { it("parses chat pattern as chat_identifier", () => { expect(parseBlueBubblesTarget("chat660250192681427962")).toEqual({ diff --git a/extensions/bluebubbles/src/targets.ts b/extensions/bluebubbles/src/targets.ts index ab297471fc3..d445c2c5f0c 100644 --- a/extensions/bluebubbles/src/targets.ts +++ b/extensions/bluebubbles/src/targets.ts @@ -5,7 +5,7 @@ import { type ParsedChatTarget, resolveServicePrefixedAllowTarget, resolveServicePrefixedTarget, -} from "openclaw/plugin-sdk/bluebubbles"; +} from "./runtime-api.js"; export type BlueBubblesService = "imessage" | "sms" | "auto"; @@ -237,6 +237,63 @@ export function looksLikeBlueBubblesTargetId(raw: string, normalized?: string): return false; } +export function looksLikeBlueBubblesExplicitTargetId(raw: string, normalized?: string): boolean { + const trimmed = raw.trim(); + if (!trimmed) { + return false; + } + const candidate = stripBlueBubblesPrefix(trimmed); + if (!candidate) { + return false; + } + const lowered = candidate.toLowerCase(); + if (/^(imessage|sms|auto):/.test(lowered)) { + return true; + } + if ( + /^(chat_id|chatid|chat|chat_guid|chatguid|guid|chat_identifier|chatidentifier|chatident|group):/.test( + lowered, + ) + ) { + return true; + } + if (parseRawChatGuid(candidate) || looksLikeRawChatIdentifier(candidate)) { + return true; + } + if (normalized) { + const normalizedTrimmed = normalized.trim(); + if (!normalizedTrimmed) { + return false; + } + const normalizedLower = normalizedTrimmed.toLowerCase(); + if ( + /^(imessage|sms|auto):/.test(normalizedLower) || + /^(chat_id|chat_guid|chat_identifier):/.test(normalizedLower) + ) { + return true; + } + } + return false; +} + +export function inferBlueBubblesTargetChatType(raw: string): "direct" | "group" | undefined { + try { + const parsed = parseBlueBubblesTarget(raw); + if (parsed.kind === "handle") { + return "direct"; + } + if (parsed.kind === "chat_guid") { + return parsed.chatGuid.includes(";+;") ? "group" : "direct"; + } + if (parsed.kind === "chat_id" || parsed.kind === "chat_identifier") { + return "group"; + } + } catch { + return undefined; + } + return undefined; +} + export function parseBlueBubblesTarget(raw: string): BlueBubblesTarget { const trimmed = stripBlueBubblesPrefix(raw); if (!trimmed) { diff --git a/extensions/bluebubbles/src/types.ts b/extensions/bluebubbles/src/types.ts index 11a1d486652..1b1190c703c 100644 --- a/extensions/bluebubbles/src/types.ts +++ b/extensions/bluebubbles/src/types.ts @@ -1,6 +1,6 @@ -import type { DmPolicy, GroupPolicy } from "openclaw/plugin-sdk/bluebubbles"; +import type { DmPolicy, GroupPolicy } from "./runtime-api.js"; -export type { DmPolicy, GroupPolicy } from "openclaw/plugin-sdk/bluebubbles"; +export type { DmPolicy, GroupPolicy } from "./runtime-api.js"; export type BlueBubblesGroupConfig = { /** If true, only respond in this group when mentioned. */ diff --git a/extensions/brave/index.ts b/extensions/brave/index.ts index 1692f2db03f..7ded10c9361 100644 --- a/extensions/brave/index.ts +++ b/extensions/brave/index.ts @@ -1,28 +1,11 @@ import { definePluginEntry } from "openclaw/plugin-sdk/core"; -import { - createPluginBackedWebSearchProvider, - getTopLevelCredentialValue, - setTopLevelCredentialValue, -} from "openclaw/plugin-sdk/provider-web-search"; +import { createBraveWebSearchProvider } from "./src/brave-web-search-provider.js"; export default definePluginEntry({ id: "brave", name: "Brave Plugin", description: "Bundled Brave plugin", register(api) { - api.registerWebSearchProvider( - createPluginBackedWebSearchProvider({ - id: "brave", - label: "Brave Search", - hint: "Structured results · country/language/time filters", - envVars: ["BRAVE_API_KEY"], - placeholder: "BSA...", - signupUrl: "https://brave.com/search/api/", - docsUrl: "https://docs.openclaw.ai/brave-search", - autoDetectOrder: 10, - getCredentialValue: getTopLevelCredentialValue, - setCredentialValue: setTopLevelCredentialValue, - }), - ); + api.registerWebSearchProvider(createBraveWebSearchProvider()); }, }); diff --git a/extensions/brave/openclaw.plugin.json b/extensions/brave/openclaw.plugin.json index 404382996d7..2077f174d62 100644 --- a/extensions/brave/openclaw.plugin.json +++ b/extensions/brave/openclaw.plugin.json @@ -1,8 +1,34 @@ { "id": "brave", + "uiHints": { + "webSearch.apiKey": { + "label": "Brave Search API Key", + "help": "Brave Search API key (fallback: BRAVE_API_KEY env var).", + "sensitive": true, + "placeholder": "BSA..." + }, + "webSearch.mode": { + "label": "Brave Search Mode", + "help": "Brave Search mode: web or llm-context." + } + }, "configSchema": { "type": "object", "additionalProperties": false, - "properties": {} + "properties": { + "webSearch": { + "type": "object", + "additionalProperties": false, + "properties": { + "apiKey": { + "type": ["string", "object"] + }, + "mode": { + "type": "string", + "enum": ["web", "llm-context"] + } + } + } + } } } diff --git a/extensions/brave/src/brave-web-search-provider.ts b/extensions/brave/src/brave-web-search-provider.ts new file mode 100644 index 00000000000..370fe77e854 --- /dev/null +++ b/extensions/brave/src/brave-web-search-provider.ts @@ -0,0 +1,645 @@ +import { Type } from "@sinclair/typebox"; +import { + buildSearchCacheKey, + DEFAULT_SEARCH_COUNT, + MAX_SEARCH_COUNT, + formatCliCommand, + normalizeFreshness, + normalizeToIsoDate, + readCachedSearchPayload, + readConfiguredSecretString, + readNumberParam, + readProviderEnvValue, + readStringParam, + resolveSearchCacheTtlMs, + resolveSearchCount, + resolveSearchTimeoutSeconds, + resolveSiteName, + resolveProviderWebSearchPluginConfig, + setProviderWebSearchPluginConfigValue, + type OpenClawConfig, + type SearchConfigRecord, + type WebSearchProviderPlugin, + type WebSearchProviderToolDefinition, + withTrustedWebSearchEndpoint, + wrapWebContent, + writeCachedSearchPayload, +} from "openclaw/plugin-sdk/provider-web-search"; + +const BRAVE_SEARCH_ENDPOINT = "https://api.search.brave.com/res/v1/web/search"; +const BRAVE_LLM_CONTEXT_ENDPOINT = "https://api.search.brave.com/res/v1/llm/context"; +const BRAVE_SEARCH_LANG_CODES = new Set([ + "ar", + "eu", + "bn", + "bg", + "ca", + "zh-hans", + "zh-hant", + "hr", + "cs", + "da", + "nl", + "en", + "en-gb", + "et", + "fi", + "fr", + "gl", + "de", + "el", + "gu", + "he", + "hi", + "hu", + "is", + "it", + "jp", + "kn", + "ko", + "lv", + "lt", + "ms", + "ml", + "mr", + "nb", + "pl", + "pt-br", + "pt-pt", + "pa", + "ro", + "ru", + "sr", + "sk", + "sl", + "es", + "sv", + "ta", + "te", + "th", + "tr", + "uk", + "vi", +]); +const BRAVE_SEARCH_LANG_ALIASES: Record = { + ja: "jp", + zh: "zh-hans", + "zh-cn": "zh-hans", + "zh-hk": "zh-hant", + "zh-sg": "zh-hans", + "zh-tw": "zh-hant", +}; +const BRAVE_UI_LANG_LOCALE = /^([a-z]{2})-([a-z]{2})$/i; + +type BraveConfig = { + apiKey?: unknown; + mode?: string; +}; + +type BraveSearchResult = { + title?: string; + url?: string; + description?: string; + age?: string; +}; + +type BraveSearchResponse = { + web?: { + results?: BraveSearchResult[]; + }; +}; + +type BraveLlmContextResult = { url: string; title: string; snippets: string[] }; +type BraveLlmContextResponse = { + grounding: { generic?: BraveLlmContextResult[] }; + sources?: { url?: string; hostname?: string; date?: string }[]; +}; + +function resolveBraveConfig( + config?: OpenClawConfig, + searchConfig?: SearchConfigRecord, +): BraveConfig { + const pluginConfig = resolveProviderWebSearchPluginConfig(config, "brave"); + if (pluginConfig) { + return pluginConfig as BraveConfig; + } + const scoped = (searchConfig as Record | undefined)?.brave; + return scoped && typeof scoped === "object" && !Array.isArray(scoped) + ? ({ + ...(scoped as BraveConfig), + apiKey: (searchConfig as Record | undefined)?.apiKey, + } as BraveConfig) + : ({ apiKey: (searchConfig as Record | undefined)?.apiKey } as BraveConfig); +} + +function resolveBraveMode(brave: BraveConfig): "web" | "llm-context" { + return brave.mode === "llm-context" ? "llm-context" : "web"; +} + +function resolveBraveApiKey( + config?: OpenClawConfig, + searchConfig?: SearchConfigRecord, +): string | undefined { + const braveConfig = resolveBraveConfig(config, searchConfig); + return ( + readConfiguredSecretString( + braveConfig.apiKey, + "plugins.entries.brave.config.webSearch.apiKey", + ) ?? + readConfiguredSecretString( + (searchConfig as Record | undefined)?.apiKey, + "tools.web.search.apiKey", + ) ?? + readProviderEnvValue(["BRAVE_API_KEY"]) + ); +} + +function normalizeBraveSearchLang(value: string | undefined): string | undefined { + if (!value) { + return undefined; + } + const trimmed = value.trim(); + if (!trimmed) { + return undefined; + } + const canonical = BRAVE_SEARCH_LANG_ALIASES[trimmed.toLowerCase()] ?? trimmed.toLowerCase(); + if (!BRAVE_SEARCH_LANG_CODES.has(canonical)) { + return undefined; + } + return canonical; +} + +function normalizeBraveUiLang(value: string | undefined): string | undefined { + if (!value) { + return undefined; + } + const trimmed = value.trim(); + if (!trimmed) { + return undefined; + } + const match = trimmed.match(BRAVE_UI_LANG_LOCALE); + if (!match) { + return undefined; + } + const [, language, region] = match; + return `${language.toLowerCase()}-${region.toUpperCase()}`; +} + +function normalizeBraveLanguageParams(params: { search_lang?: string; ui_lang?: string }): { + search_lang?: string; + ui_lang?: string; + invalidField?: "search_lang" | "ui_lang"; +} { + const rawSearchLang = params.search_lang?.trim() || undefined; + const rawUiLang = params.ui_lang?.trim() || undefined; + let searchLangCandidate = rawSearchLang; + let uiLangCandidate = rawUiLang; + + if (normalizeBraveUiLang(rawSearchLang) && normalizeBraveSearchLang(rawUiLang)) { + searchLangCandidate = rawUiLang; + uiLangCandidate = rawSearchLang; + } + + const search_lang = normalizeBraveSearchLang(searchLangCandidate); + if (searchLangCandidate && !search_lang) { + return { invalidField: "search_lang" }; + } + + const ui_lang = normalizeBraveUiLang(uiLangCandidate); + if (uiLangCandidate && !ui_lang) { + return { invalidField: "ui_lang" }; + } + + return { search_lang, ui_lang }; +} + +function mapBraveLlmContextResults( + data: BraveLlmContextResponse, +): { url: string; title: string; snippets: string[]; siteName?: string }[] { + const genericResults = Array.isArray(data.grounding?.generic) ? data.grounding.generic : []; + return genericResults.map((entry) => ({ + url: entry.url ?? "", + title: entry.title ?? "", + snippets: (entry.snippets ?? []).filter((s) => typeof s === "string" && s.length > 0), + siteName: resolveSiteName(entry.url) || undefined, + })); +} + +async function runBraveLlmContextSearch(params: { + query: string; + apiKey: string; + timeoutSeconds: number; + country?: string; + search_lang?: string; + freshness?: string; +}): Promise<{ + results: Array<{ + url: string; + title: string; + snippets: string[]; + siteName?: string; + }>; + sources?: BraveLlmContextResponse["sources"]; +}> { + const url = new URL(BRAVE_LLM_CONTEXT_ENDPOINT); + url.searchParams.set("q", params.query); + if (params.country) { + url.searchParams.set("country", params.country); + } + if (params.search_lang) { + url.searchParams.set("search_lang", params.search_lang); + } + if (params.freshness) { + url.searchParams.set("freshness", params.freshness); + } + + return withTrustedWebSearchEndpoint( + { + url: url.toString(), + timeoutSeconds: params.timeoutSeconds, + init: { + method: "GET", + headers: { + Accept: "application/json", + "X-Subscription-Token": params.apiKey, + }, + }, + }, + async (res) => { + if (!res.ok) { + const detail = await res.text(); + throw new Error(`Brave LLM Context API error (${res.status}): ${detail || res.statusText}`); + } + + const data = (await res.json()) as BraveLlmContextResponse; + return { results: mapBraveLlmContextResults(data), sources: data.sources }; + }, + ); +} + +async function runBraveWebSearch(params: { + query: string; + count: number; + apiKey: string; + timeoutSeconds: number; + country?: string; + search_lang?: string; + ui_lang?: string; + freshness?: string; + dateAfter?: string; + dateBefore?: string; +}): Promise>> { + const url = new URL(BRAVE_SEARCH_ENDPOINT); + url.searchParams.set("q", params.query); + url.searchParams.set("count", String(params.count)); + if (params.country) { + url.searchParams.set("country", params.country); + } + if (params.search_lang) { + url.searchParams.set("search_lang", params.search_lang); + } + if (params.ui_lang) { + url.searchParams.set("ui_lang", params.ui_lang); + } + if (params.freshness) { + url.searchParams.set("freshness", params.freshness); + } else if (params.dateAfter && params.dateBefore) { + url.searchParams.set("freshness", `${params.dateAfter}to${params.dateBefore}`); + } else if (params.dateAfter) { + url.searchParams.set( + "freshness", + `${params.dateAfter}to${new Date().toISOString().slice(0, 10)}`, + ); + } else if (params.dateBefore) { + url.searchParams.set("freshness", `1970-01-01to${params.dateBefore}`); + } + + return withTrustedWebSearchEndpoint( + { + url: url.toString(), + timeoutSeconds: params.timeoutSeconds, + init: { + method: "GET", + headers: { + Accept: "application/json", + "X-Subscription-Token": params.apiKey, + }, + }, + }, + async (res) => { + if (!res.ok) { + const detail = await res.text(); + throw new Error(`Brave Search API error (${res.status}): ${detail || res.statusText}`); + } + + const data = (await res.json()) as BraveSearchResponse; + const results = Array.isArray(data.web?.results) ? (data.web?.results ?? []) : []; + return results.map((entry) => { + const description = entry.description ?? ""; + const title = entry.title ?? ""; + const url = entry.url ?? ""; + return { + title: title ? wrapWebContent(title, "web_search") : "", + url, + description: description ? wrapWebContent(description, "web_search") : "", + published: entry.age || undefined, + siteName: resolveSiteName(url) || undefined, + }; + }); + }, + ); +} + +function createBraveSchema() { + return Type.Object({ + query: Type.String({ description: "Search query string." }), + count: Type.Optional( + Type.Number({ + description: "Number of results to return (1-10).", + minimum: 1, + maximum: MAX_SEARCH_COUNT, + }), + ), + country: Type.Optional( + Type.String({ + description: + "2-letter country code for region-specific results (e.g., 'DE', 'US', 'ALL'). Default: 'US'.", + }), + ), + language: Type.Optional( + Type.String({ + description: "ISO 639-1 language code for results (e.g., 'en', 'de', 'fr').", + }), + ), + freshness: Type.Optional( + Type.String({ + description: "Filter by time: 'day' (24h), 'week', 'month', or 'year'.", + }), + ), + date_after: Type.Optional( + Type.String({ + description: "Only results published after this date (YYYY-MM-DD).", + }), + ), + date_before: Type.Optional( + Type.String({ + description: "Only results published before this date (YYYY-MM-DD).", + }), + ), + search_lang: Type.Optional( + Type.String({ + description: + "Brave language code for search results (e.g., 'en', 'de', 'en-gb', 'zh-hans', 'zh-hant', 'pt-br').", + }), + ), + ui_lang: Type.Optional( + Type.String({ + description: + "Locale code for UI elements in language-region format (e.g., 'en-US', 'de-DE', 'fr-FR', 'tr-TR'). Must include region subtag.", + }), + ), + }); +} + +function missingBraveKeyPayload() { + return { + error: "missing_brave_api_key", + message: `web_search (brave) needs a Brave Search API key. Run \`${formatCliCommand("openclaw configure --section web")}\` to store it, or set BRAVE_API_KEY in the Gateway environment.`, + docs: "https://docs.openclaw.ai/tools/web", + }; +} + +function createBraveToolDefinition( + config?: OpenClawConfig, + searchConfig?: SearchConfigRecord, +): WebSearchProviderToolDefinition { + const braveConfig = resolveBraveConfig(config, searchConfig); + const braveMode = resolveBraveMode(braveConfig); + + return { + description: + braveMode === "llm-context" + ? "Search the web using Brave Search LLM Context API. Returns pre-extracted page content (text chunks, tables, code blocks) optimized for LLM grounding." + : "Search the web using Brave Search API. Supports region-specific and localized search via country and language parameters. Returns titles, URLs, and snippets for fast research.", + parameters: createBraveSchema(), + execute: async (args) => { + const apiKey = resolveBraveApiKey(config, searchConfig); + if (!apiKey) { + return missingBraveKeyPayload(); + } + + const params = args as Record; + const query = readStringParam(params, "query", { required: true }); + const count = + readNumberParam(params, "count", { integer: true }) ?? + searchConfig?.maxResults ?? + undefined; + const country = readStringParam(params, "country"); + const language = readStringParam(params, "language"); + const search_lang = readStringParam(params, "search_lang"); + const ui_lang = readStringParam(params, "ui_lang"); + const normalizedLanguage = normalizeBraveLanguageParams({ + search_lang: search_lang || language, + ui_lang, + }); + if (normalizedLanguage.invalidField === "search_lang") { + return { + error: "invalid_search_lang", + message: + "search_lang must be a Brave-supported language code like 'en', 'en-gb', 'zh-hans', or 'zh-hant'.", + docs: "https://docs.openclaw.ai/tools/web", + }; + } + if (normalizedLanguage.invalidField === "ui_lang") { + return { + error: "invalid_ui_lang", + message: "ui_lang must be a language-region locale like 'en-US'.", + docs: "https://docs.openclaw.ai/tools/web", + }; + } + if (normalizedLanguage.ui_lang && braveMode === "llm-context") { + return { + error: "unsupported_ui_lang", + message: + "ui_lang is not supported by Brave llm-context mode. Remove ui_lang or use Brave web mode for locale-based UI hints.", + docs: "https://docs.openclaw.ai/tools/web", + }; + } + + const rawFreshness = readStringParam(params, "freshness"); + if (rawFreshness && braveMode === "llm-context") { + return { + error: "unsupported_freshness", + message: + "freshness filtering is not supported by Brave llm-context mode. Remove freshness or use Brave web mode.", + docs: "https://docs.openclaw.ai/tools/web", + }; + } + const freshness = rawFreshness ? normalizeFreshness(rawFreshness, "brave") : undefined; + if (rawFreshness && !freshness) { + return { + error: "invalid_freshness", + message: "freshness must be day, week, month, or year.", + docs: "https://docs.openclaw.ai/tools/web", + }; + } + + const rawDateAfter = readStringParam(params, "date_after"); + const rawDateBefore = readStringParam(params, "date_before"); + if (rawFreshness && (rawDateAfter || rawDateBefore)) { + return { + error: "conflicting_time_filters", + message: + "freshness and date_after/date_before cannot be used together. Use either freshness (day/week/month/year) or a date range (date_after/date_before), not both.", + docs: "https://docs.openclaw.ai/tools/web", + }; + } + if ((rawDateAfter || rawDateBefore) && braveMode === "llm-context") { + return { + error: "unsupported_date_filter", + message: + "date_after/date_before filtering is not supported by Brave llm-context mode. Use Brave web mode for date filters.", + docs: "https://docs.openclaw.ai/tools/web", + }; + } + const dateAfter = rawDateAfter ? normalizeToIsoDate(rawDateAfter) : undefined; + if (rawDateAfter && !dateAfter) { + return { + error: "invalid_date", + message: "date_after must be YYYY-MM-DD format.", + docs: "https://docs.openclaw.ai/tools/web", + }; + } + const dateBefore = rawDateBefore ? normalizeToIsoDate(rawDateBefore) : undefined; + if (rawDateBefore && !dateBefore) { + return { + error: "invalid_date", + message: "date_before must be YYYY-MM-DD format.", + docs: "https://docs.openclaw.ai/tools/web", + }; + } + if (dateAfter && dateBefore && dateAfter > dateBefore) { + return { + error: "invalid_date_range", + message: "date_after must be before date_before.", + docs: "https://docs.openclaw.ai/tools/web", + }; + } + + const cacheKey = buildSearchCacheKey([ + "brave", + braveMode, + query, + resolveSearchCount(count, DEFAULT_SEARCH_COUNT), + country, + normalizedLanguage.search_lang, + normalizedLanguage.ui_lang, + freshness, + dateAfter, + dateBefore, + ]); + const cached = readCachedSearchPayload(cacheKey); + if (cached) { + return cached; + } + + const start = Date.now(); + const timeoutSeconds = resolveSearchTimeoutSeconds(searchConfig); + const cacheTtlMs = resolveSearchCacheTtlMs(searchConfig); + + if (braveMode === "llm-context") { + const { results, sources } = await runBraveLlmContextSearch({ + query, + apiKey, + timeoutSeconds, + country: country ?? undefined, + search_lang: normalizedLanguage.search_lang, + freshness, + }); + const payload = { + query, + provider: "brave", + mode: "llm-context" as const, + count: results.length, + tookMs: Date.now() - start, + externalContent: { + untrusted: true, + source: "web_search", + provider: "brave", + wrapped: true, + }, + results: results.map((entry) => ({ + title: entry.title ? wrapWebContent(entry.title, "web_search") : "", + url: entry.url, + snippets: entry.snippets.map((snippet) => wrapWebContent(snippet, "web_search")), + siteName: entry.siteName, + })), + sources, + }; + writeCachedSearchPayload(cacheKey, payload, cacheTtlMs); + return payload; + } + + const results = await runBraveWebSearch({ + query, + count: resolveSearchCount(count, DEFAULT_SEARCH_COUNT), + apiKey, + timeoutSeconds, + country: country ?? undefined, + search_lang: normalizedLanguage.search_lang, + ui_lang: normalizedLanguage.ui_lang, + freshness, + dateAfter, + dateBefore, + }); + const payload = { + query, + provider: "brave", + count: results.length, + tookMs: Date.now() - start, + externalContent: { + untrusted: true, + source: "web_search", + provider: "brave", + wrapped: true, + }, + results, + }; + writeCachedSearchPayload(cacheKey, payload, cacheTtlMs); + return payload; + }, + }; +} + +export function createBraveWebSearchProvider(): WebSearchProviderPlugin { + return { + id: "brave", + label: "Brave Search", + hint: "Structured results · country/language/time filters", + envVars: ["BRAVE_API_KEY"], + placeholder: "BSA...", + signupUrl: "https://brave.com/search/api/", + docsUrl: "https://docs.openclaw.ai/brave-search", + autoDetectOrder: 10, + credentialPath: "plugins.entries.brave.config.webSearch.apiKey", + inactiveSecretPaths: ["plugins.entries.brave.config.webSearch.apiKey"], + getCredentialValue: (searchConfig) => searchConfig?.apiKey, + setCredentialValue: (searchConfigTarget, value) => { + searchConfigTarget.apiKey = value; + }, + getConfiguredCredentialValue: (config) => + resolveProviderWebSearchPluginConfig(config, "brave")?.apiKey, + setConfiguredCredentialValue: (configTarget, value) => { + setProviderWebSearchPluginConfigValue(configTarget, "brave", "apiKey", value); + }, + createTool: (ctx) => + createBraveToolDefinition(ctx.config, ctx.searchConfig as SearchConfigRecord | undefined), + }; +} + +export const __testing = { + normalizeFreshness, + normalizeBraveLanguageParams, + resolveBraveMode, + mapBraveLlmContextResults, +} as const; diff --git a/extensions/chutes/index.ts b/extensions/chutes/index.ts new file mode 100644 index 00000000000..a61cd4ec93f --- /dev/null +++ b/extensions/chutes/index.ts @@ -0,0 +1,184 @@ +import { definePluginEntry } from "openclaw/plugin-sdk/core"; +import { + buildOauthProviderAuthResult, + createProviderApiKeyAuthMethod, + loginChutes, + resolveOAuthApiKeyMarker, + type ProviderAuthContext, + type ProviderAuthResult, +} from "openclaw/plugin-sdk/provider-auth"; +import { + CHUTES_DEFAULT_MODEL_REF, + applyChutesApiKeyConfig, + applyChutesProviderConfig, +} from "./onboard.js"; +import { buildChutesProvider } from "./provider-catalog.js"; + +const PROVIDER_ID = "chutes"; + +async function runChutesOAuth(ctx: ProviderAuthContext): Promise { + const isRemote = ctx.isRemote; + const redirectUri = + process.env.CHUTES_OAUTH_REDIRECT_URI?.trim() || "http://127.0.0.1:1456/oauth-callback"; + const scopes = process.env.CHUTES_OAUTH_SCOPES?.trim() || "openid profile chutes:invoke"; + const clientId = + process.env.CHUTES_CLIENT_ID?.trim() || + String( + await ctx.prompter.text({ + message: "Enter Chutes OAuth client id", + placeholder: "cid_xxx", + validate: (value: string) => (value?.trim() ? undefined : "Required"), + }), + ).trim(); + const clientSecret = process.env.CHUTES_CLIENT_SECRET?.trim() || undefined; + + await ctx.prompter.note( + isRemote + ? [ + "You are running in a remote/VPS environment.", + "A URL will be shown for you to open in your LOCAL browser.", + "After signing in, paste the redirect URL back here.", + "", + `Redirect URI: ${redirectUri}`, + ].join("\n") + : [ + "Browser will open for Chutes authentication.", + "If the callback doesn't auto-complete, paste the redirect URL.", + "", + `Redirect URI: ${redirectUri}`, + ].join("\n"), + "Chutes OAuth", + ); + + const progress = ctx.prompter.progress("Starting Chutes OAuth…"); + try { + const { onAuth, onPrompt } = ctx.oauth.createVpsAwareHandlers({ + isRemote, + prompter: ctx.prompter, + runtime: ctx.runtime, + spin: progress, + openUrl: ctx.openUrl, + localBrowserMessage: "Complete sign-in in browser…", + }); + + const creds = await loginChutes({ + app: { + clientId, + clientSecret, + redirectUri, + scopes: scopes.split(/\s+/).filter(Boolean), + }, + manual: isRemote, + onAuth, + onPrompt, + onProgress: (message) => progress.update(message), + }); + + progress.stop("Chutes OAuth complete"); + + return buildOauthProviderAuthResult({ + providerId: PROVIDER_ID, + defaultModel: CHUTES_DEFAULT_MODEL_REF, + access: creds.access, + refresh: creds.refresh, + expires: creds.expires, + email: typeof creds.email === "string" ? creds.email : undefined, + credentialExtra: { + clientId, + ...("accountId" in creds && typeof creds.accountId === "string" + ? { accountId: creds.accountId } + : {}), + }, + configPatch: applyChutesProviderConfig({}), + notes: [ + "Chutes OAuth tokens auto-refresh. Re-run login if refresh fails or access is revoked.", + `Redirect URI: ${redirectUri}`, + ], + }); + } catch (err) { + progress.stop("Chutes OAuth failed"); + await ctx.prompter.note( + [ + "Trouble with OAuth?", + "Verify CHUTES_CLIENT_ID (and CHUTES_CLIENT_SECRET if required).", + `Verify the OAuth app redirect URI includes: ${redirectUri}`, + "Chutes docs: https://chutes.ai/docs/sign-in-with-chutes/overview", + ].join("\n"), + "OAuth help", + ); + throw err; + } +} + +export default definePluginEntry({ + id: PROVIDER_ID, + name: "Chutes Provider", + description: "Bundled Chutes.ai provider plugin", + register(api) { + api.registerProvider({ + id: PROVIDER_ID, + label: "Chutes", + docsPath: "/providers/chutes", + envVars: ["CHUTES_API_KEY", "CHUTES_OAUTH_TOKEN"], + auth: [ + { + id: "oauth", + label: "Chutes OAuth", + hint: "Browser sign-in", + kind: "oauth", + wizard: { + choiceId: "chutes", + choiceLabel: "Chutes (OAuth)", + choiceHint: "Browser sign-in", + groupId: "chutes", + groupLabel: "Chutes", + groupHint: "OAuth + API key", + }, + run: async (ctx) => await runChutesOAuth(ctx), + }, + createProviderApiKeyAuthMethod({ + providerId: PROVIDER_ID, + methodId: "api-key", + label: "Chutes API key", + hint: "Open-source models including Llama, DeepSeek, and more", + optionKey: "chutesApiKey", + flagName: "--chutes-api-key", + envVar: "CHUTES_API_KEY", + promptMessage: "Enter Chutes API key", + noteTitle: "Chutes", + noteMessage: [ + "Chutes provides access to leading open-source models including Llama, DeepSeek, and more.", + "Get your API key at: https://chutes.ai/settings/api-keys", + ].join("\n"), + defaultModel: CHUTES_DEFAULT_MODEL_REF, + expectedProviders: ["chutes"], + applyConfig: (cfg) => applyChutesApiKeyConfig(cfg), + wizard: { + choiceId: "chutes-api-key", + choiceLabel: "Chutes API key", + groupId: "chutes", + groupLabel: "Chutes", + groupHint: "OAuth + API key", + }, + }), + ], + catalog: { + order: "profile", + run: async (ctx) => { + const { apiKey, discoveryApiKey } = ctx.resolveProviderAuth(PROVIDER_ID, { + oauthMarker: resolveOAuthApiKeyMarker(PROVIDER_ID), + }); + if (!apiKey) { + return null; + } + return { + provider: { + ...(await buildChutesProvider(discoveryApiKey)), + apiKey, + }, + }; + }, + }, + }); + }, +}); diff --git a/extensions/chutes/onboard.ts b/extensions/chutes/onboard.ts new file mode 100644 index 00000000000..f51914c3ca8 --- /dev/null +++ b/extensions/chutes/onboard.ts @@ -0,0 +1,67 @@ +import { + CHUTES_BASE_URL, + CHUTES_DEFAULT_MODEL_REF, + CHUTES_MODEL_CATALOG, + buildChutesModelDefinition, +} from "openclaw/plugin-sdk/provider-models"; +import { + applyAgentDefaultModelPrimary, + applyProviderConfigWithModelCatalog, + type OpenClawConfig, +} from "openclaw/plugin-sdk/provider-onboard"; + +export { CHUTES_DEFAULT_MODEL_REF }; + +/** + * Apply Chutes provider configuration without changing the default model. + * Registers all catalog models and sets provider aliases (chutes-fast, etc.). + */ +export function applyChutesProviderConfig(cfg: OpenClawConfig): OpenClawConfig { + const models = { ...cfg.agents?.defaults?.models }; + for (const m of CHUTES_MODEL_CATALOG) { + models[`chutes/${m.id}`] = { + ...models[`chutes/${m.id}`], + }; + } + + models["chutes-fast"] = { alias: "chutes/zai-org/GLM-4.7-FP8" }; + models["chutes-vision"] = { alias: "chutes/chutesai/Mistral-Small-3.2-24B-Instruct-2506" }; + models["chutes-pro"] = { alias: "chutes/deepseek-ai/DeepSeek-V3.2-TEE" }; + + const chutesModels = CHUTES_MODEL_CATALOG.map(buildChutesModelDefinition); + return applyProviderConfigWithModelCatalog(cfg, { + agentModels: models, + providerId: "chutes", + api: "openai-completions", + baseUrl: CHUTES_BASE_URL, + catalogModels: chutesModels, + }); +} + +/** + * Apply Chutes provider configuration AND set Chutes as the default model. + */ +export function applyChutesConfig(cfg: OpenClawConfig): OpenClawConfig { + const next = applyChutesProviderConfig(cfg); + return { + ...next, + agents: { + ...next.agents, + defaults: { + ...next.agents?.defaults, + model: { + primary: CHUTES_DEFAULT_MODEL_REF, + fallbacks: ["chutes/deepseek-ai/DeepSeek-V3.2-TEE", "chutes/Qwen/Qwen3-32B"], + }, + imageModel: { + primary: "chutes/chutesai/Mistral-Small-3.2-24B-Instruct-2506", + fallbacks: ["chutes/chutesai/Mistral-Small-3.1-24B-Instruct-2503"], + }, + }, + }, + }; +} + +export function applyChutesApiKeyConfig(cfg: OpenClawConfig): OpenClawConfig { + return applyAgentDefaultModelPrimary(applyChutesProviderConfig(cfg), CHUTES_DEFAULT_MODEL_REF); +} diff --git a/extensions/chutes/openclaw.plugin.json b/extensions/chutes/openclaw.plugin.json new file mode 100644 index 00000000000..26174f31b3a --- /dev/null +++ b/extensions/chutes/openclaw.plugin.json @@ -0,0 +1,39 @@ +{ + "id": "chutes", + "enabledByDefault": true, + "providers": ["chutes"], + "providerAuthEnvVars": { + "chutes": ["CHUTES_API_KEY", "CHUTES_OAUTH_TOKEN"] + }, + "providerAuthChoices": [ + { + "provider": "chutes", + "method": "oauth", + "choiceId": "chutes", + "choiceLabel": "Chutes (OAuth)", + "choiceHint": "Browser sign-in", + "groupId": "chutes", + "groupLabel": "Chutes", + "groupHint": "OAuth + API key" + }, + { + "provider": "chutes", + "method": "api-key", + "choiceId": "chutes-api-key", + "choiceLabel": "Chutes API key", + "choiceHint": "Open-source models including Llama, DeepSeek, and more", + "groupId": "chutes", + "groupLabel": "Chutes", + "groupHint": "OAuth + API key", + "optionKey": "chutesApiKey", + "cliFlag": "--chutes-api-key", + "cliOption": "--chutes-api-key ", + "cliDescription": "Chutes API key" + } + ], + "configSchema": { + "type": "object", + "additionalProperties": false, + "properties": {} + } +} diff --git a/extensions/chutes/package.json b/extensions/chutes/package.json new file mode 100644 index 00000000000..38f45fe3e54 --- /dev/null +++ b/extensions/chutes/package.json @@ -0,0 +1,12 @@ +{ + "name": "@openclaw/chutes-provider", + "version": "2026.3.14", + "private": true, + "description": "OpenClaw Chutes.ai provider plugin", + "type": "module", + "openclaw": { + "extensions": [ + "./index.ts" + ] + } +} diff --git a/extensions/chutes/provider-catalog.ts b/extensions/chutes/provider-catalog.ts new file mode 100644 index 00000000000..1467f405dde --- /dev/null +++ b/extensions/chutes/provider-catalog.ts @@ -0,0 +1,21 @@ +import { + CHUTES_BASE_URL, + CHUTES_MODEL_CATALOG, + buildChutesModelDefinition, + discoverChutesModels, + type ModelProviderConfig, +} from "openclaw/plugin-sdk/provider-models"; + +/** + * Build the Chutes provider with dynamic model discovery. + * Falls back to the static catalog on failure. + * Accepts an optional access token (API key or OAuth access token) for authenticated discovery. + */ +export async function buildChutesProvider(accessToken?: string): Promise { + const models = await discoverChutesModels(accessToken); + return { + baseUrl: CHUTES_BASE_URL, + api: "openai-completions", + models: models.length > 0 ? models : CHUTES_MODEL_CATALOG.map(buildChutesModelDefinition), + }; +} diff --git a/extensions/copilot-proxy/index.ts b/extensions/copilot-proxy/index.ts index cf71710db5c..ef0aa61030c 100644 --- a/extensions/copilot-proxy/index.ts +++ b/extensions/copilot-proxy/index.ts @@ -2,7 +2,7 @@ import { definePluginEntry, type ProviderAuthContext, type ProviderAuthResult, -} from "openclaw/plugin-sdk/copilot-proxy"; +} from "./runtime-api.js"; const DEFAULT_BASE_URL = "http://localhost:3000/v1"; const DEFAULT_API_KEY = "n/a"; diff --git a/extensions/copilot-proxy/runtime-api.ts b/extensions/copilot-proxy/runtime-api.ts new file mode 100644 index 00000000000..9f59e519281 --- /dev/null +++ b/extensions/copilot-proxy/runtime-api.ts @@ -0,0 +1 @@ +export * from "../../src/plugin-sdk/copilot-proxy.js"; diff --git a/extensions/device-pair/api.ts b/extensions/device-pair/api.ts new file mode 100644 index 00000000000..137cd4b89ba --- /dev/null +++ b/extensions/device-pair/api.ts @@ -0,0 +1 @@ +export * from "../../src/plugin-sdk/device-pair.js"; diff --git a/extensions/device-pair/index.ts b/extensions/device-pair/index.ts index ce007756389..defd3b5c4c6 100644 --- a/extensions/device-pair/index.ts +++ b/extensions/device-pair/index.ts @@ -1,4 +1,5 @@ import os from "node:os"; +import qrcode from "qrcode-terminal"; import { approveDevicePairing, definePluginEntry, @@ -8,8 +9,7 @@ import { runPluginCommandWithTimeout, resolveTailnetHostWithRunner, type OpenClawPluginApi, -} from "openclaw/plugin-sdk/device-pair"; -import qrcode from "qrcode-terminal"; +} from "./api.js"; import { armPairNotifyOnce, formatPendingRequests, diff --git a/extensions/device-pair/notify.ts b/extensions/device-pair/notify.ts index 3ef3005cf73..ba45e856372 100644 --- a/extensions/device-pair/notify.ts +++ b/extensions/device-pair/notify.ts @@ -1,7 +1,7 @@ import { promises as fs } from "node:fs"; import path from "node:path"; -import type { OpenClawPluginApi } from "openclaw/plugin-sdk/device-pair"; -import { listDevicePairing } from "openclaw/plugin-sdk/device-pair"; +import type { OpenClawPluginApi } from "./api.js"; +import { listDevicePairing } from "./api.js"; const NOTIFY_STATE_FILE = "device-pair-notify.json"; const NOTIFY_POLL_INTERVAL_MS = 10_000; diff --git a/extensions/diagnostics-otel/api.ts b/extensions/diagnostics-otel/api.ts new file mode 100644 index 00000000000..077ad45965f --- /dev/null +++ b/extensions/diagnostics-otel/api.ts @@ -0,0 +1 @@ +export * from "../../src/plugin-sdk/diagnostics-otel.js"; diff --git a/extensions/diagnostics-otel/src/service.test.ts b/extensions/diagnostics-otel/src/service.test.ts index d310b227be3..c8d08d07f1b 100644 --- a/extensions/diagnostics-otel/src/service.test.ts +++ b/extensions/diagnostics-otel/src/service.test.ts @@ -98,18 +98,16 @@ vi.mock("@opentelemetry/semantic-conventions", () => ({ ATTR_SERVICE_NAME: "service.name", })); -vi.mock("openclaw/plugin-sdk/diagnostics-otel", async () => { - const actual = await vi.importActual( - "openclaw/plugin-sdk/diagnostics-otel", - ); +vi.mock("../api.js", async () => { + const actual = await vi.importActual("../api.js"); return { ...actual, registerLogTransport: registerLogTransportMock, }; }); -import type { OpenClawPluginServiceContext } from "openclaw/plugin-sdk/diagnostics-otel"; -import { emitDiagnosticEvent } from "openclaw/plugin-sdk/diagnostics-otel"; +import type { OpenClawPluginServiceContext } from "../api.js"; +import { emitDiagnosticEvent } from "../api.js"; import { createDiagnosticsOtelService } from "./service.js"; const OTEL_TEST_STATE_DIR = "/tmp/openclaw-diagnostics-otel-test"; diff --git a/extensions/diagnostics-otel/src/service.ts b/extensions/diagnostics-otel/src/service.ts index b7224d034dd..2516b4c52e3 100644 --- a/extensions/diagnostics-otel/src/service.ts +++ b/extensions/diagnostics-otel/src/service.ts @@ -9,15 +9,8 @@ import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics"; import { NodeSDK } from "@opentelemetry/sdk-node"; import { ParentBasedSampler, TraceIdRatioBasedSampler } from "@opentelemetry/sdk-trace-base"; import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions"; -import type { - DiagnosticEventPayload, - OpenClawPluginService, -} from "openclaw/plugin-sdk/diagnostics-otel"; -import { - onDiagnosticEvent, - redactSensitiveText, - registerLogTransport, -} from "openclaw/plugin-sdk/diagnostics-otel"; +import type { DiagnosticEventPayload, OpenClawPluginService } from "../api.js"; +import { onDiagnosticEvent, redactSensitiveText, registerLogTransport } from "../api.js"; const DEFAULT_SERVICE_NAME = "openclaw"; diff --git a/extensions/diffs/api.ts b/extensions/diffs/api.ts new file mode 100644 index 00000000000..a200daea1fd --- /dev/null +++ b/extensions/diffs/api.ts @@ -0,0 +1 @@ +export * from "../../src/plugin-sdk/diffs.js"; diff --git a/extensions/diffs/index.test.ts b/extensions/diffs/index.test.ts index 3e7fd3c474b..02ce339e47c 100644 --- a/extensions/diffs/index.test.ts +++ b/extensions/diffs/index.test.ts @@ -1,8 +1,8 @@ import type { IncomingMessage } from "node:http"; -import type { OpenClawPluginApi } from "openclaw/plugin-sdk/diffs"; import { describe, expect, it, vi } from "vitest"; import { createMockServerResponse } from "../../test/helpers/extensions/mock-http-response.js"; import { createTestPluginApi } from "../../test/helpers/extensions/plugin-api.js"; +import type { OpenClawPluginApi } from "./api.js"; import plugin from "./index.js"; describe("diffs plugin registration", () => { diff --git a/extensions/diffs/index.ts b/extensions/diffs/index.ts index b1547b1087d..5ce8c94fabd 100644 --- a/extensions/diffs/index.ts +++ b/extensions/diffs/index.ts @@ -1,6 +1,6 @@ import path from "node:path"; -import type { OpenClawPluginApi } from "openclaw/plugin-sdk/diffs"; -import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/diffs"; +import type { OpenClawPluginApi } from "./api.js"; +import { resolvePreferredOpenClawTmpDir } from "./api.js"; import { diffsPluginConfigSchema, resolveDiffsPluginDefaults, diff --git a/extensions/diffs/package.json b/extensions/diffs/package.json index b92b16052b8..ac666b0c987 100644 --- a/extensions/diffs/package.json +++ b/extensions/diffs/package.json @@ -8,7 +8,7 @@ "build:viewer": "bun build src/viewer-client.ts --target browser --format esm --minify --outfile assets/viewer-runtime.js" }, "dependencies": { - "@pierre/diffs": "1.1.0", + "@pierre/diffs": "1.1.1", "@sinclair/typebox": "0.34.48", "playwright-core": "1.58.2" }, diff --git a/extensions/diffs/src/browser.test.ts b/extensions/diffs/src/browser.test.ts index c0b03d62cc0..8c16530ec15 100644 --- a/extensions/diffs/src/browser.test.ts +++ b/extensions/diffs/src/browser.test.ts @@ -1,7 +1,7 @@ import fs from "node:fs/promises"; import path from "node:path"; -import type { OpenClawConfig } from "openclaw/plugin-sdk/diffs"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import type { OpenClawConfig } from "../api.js"; import { createTempDiffRoot } from "./test-helpers.js"; const { launchMock } = vi.hoisted(() => ({ diff --git a/extensions/diffs/src/browser.ts b/extensions/diffs/src/browser.ts index 904996946b6..53794ef83ee 100644 --- a/extensions/diffs/src/browser.ts +++ b/extensions/diffs/src/browser.ts @@ -1,8 +1,8 @@ import { constants as fsConstants } from "node:fs"; import fs from "node:fs/promises"; import path from "node:path"; -import type { OpenClawConfig } from "openclaw/plugin-sdk/diffs"; import { chromium } from "playwright-core"; +import type { OpenClawConfig } from "../api.js"; import type { DiffRenderOptions, DiffTheme } from "./types.js"; import { VIEWER_ASSET_PREFIX, getServedViewerAsset } from "./viewer-assets.js"; diff --git a/extensions/diffs/src/config.ts b/extensions/diffs/src/config.ts index fbc9a108060..faaa8535bde 100644 --- a/extensions/diffs/src/config.ts +++ b/extensions/diffs/src/config.ts @@ -1,4 +1,4 @@ -import type { OpenClawPluginConfigSchema } from "openclaw/plugin-sdk/diffs"; +import type { OpenClawPluginConfigSchema } from "../api.js"; import { DIFF_IMAGE_QUALITY_PRESETS, DIFF_INDICATORS, diff --git a/extensions/diffs/src/http.ts b/extensions/diffs/src/http.ts index 445500b2340..48d9341bfce 100644 --- a/extensions/diffs/src/http.ts +++ b/extensions/diffs/src/http.ts @@ -1,5 +1,5 @@ import type { IncomingMessage, ServerResponse } from "node:http"; -import type { PluginLogger } from "openclaw/plugin-sdk/diffs"; +import type { PluginLogger } from "../api.js"; import type { DiffArtifactStore } from "./store.js"; import { DIFF_ARTIFACT_ID_PATTERN, DIFF_ARTIFACT_TOKEN_PATTERN } from "./types.js"; import { VIEWER_ASSET_PREFIX, getServedViewerAsset } from "./viewer-assets.js"; diff --git a/extensions/diffs/src/store.ts b/extensions/diffs/src/store.ts index e53a555356c..baab4757384 100644 --- a/extensions/diffs/src/store.ts +++ b/extensions/diffs/src/store.ts @@ -1,7 +1,7 @@ import crypto from "node:crypto"; import fs from "node:fs/promises"; import path from "node:path"; -import type { PluginLogger } from "openclaw/plugin-sdk/diffs"; +import type { PluginLogger } from "../api.js"; import type { DiffArtifactMeta, DiffOutputFormat } from "./types.js"; const DEFAULT_TTL_MS = 30 * 60 * 1000; diff --git a/extensions/diffs/src/tool.test.ts b/extensions/diffs/src/tool.test.ts index b0e019f33e2..f79098dd907 100644 --- a/extensions/diffs/src/tool.test.ts +++ b/extensions/diffs/src/tool.test.ts @@ -1,8 +1,8 @@ import fs from "node:fs/promises"; import path from "node:path"; -import type { OpenClawPluginApi } from "openclaw/plugin-sdk/diffs"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { createTestPluginApi } from "../../../test/helpers/extensions/plugin-api.js"; +import type { OpenClawPluginApi } from "../api.js"; import type { DiffScreenshotter } from "./browser.js"; import { DEFAULT_DIFFS_TOOL_DEFAULTS } from "./config.js"; import { DiffArtifactStore } from "./store.js"; diff --git a/extensions/diffs/src/tool.ts b/extensions/diffs/src/tool.ts index c6eb4b528c4..b20f11fee15 100644 --- a/extensions/diffs/src/tool.ts +++ b/extensions/diffs/src/tool.ts @@ -1,6 +1,6 @@ import fs from "node:fs/promises"; import { Static, Type } from "@sinclair/typebox"; -import type { AnyAgentTool, OpenClawPluginApi } from "openclaw/plugin-sdk/diffs"; +import type { AnyAgentTool, OpenClawPluginApi } from "../api.js"; import { PlaywrightDiffScreenshotter, type DiffScreenshotter } from "./browser.js"; import { resolveDiffImageRenderOptions } from "./config.js"; import { renderDiffDocument } from "./render.js"; diff --git a/extensions/diffs/src/url.ts b/extensions/diffs/src/url.ts index feee5c7af05..e8c40e05753 100644 --- a/extensions/diffs/src/url.ts +++ b/extensions/diffs/src/url.ts @@ -1,4 +1,4 @@ -import type { OpenClawConfig } from "openclaw/plugin-sdk/diffs"; +import type { OpenClawConfig } from "../api.js"; const DEFAULT_GATEWAY_PORT = 18789; diff --git a/extensions/discord/api.ts b/extensions/discord/api.ts new file mode 100644 index 00000000000..19a5b926ff0 --- /dev/null +++ b/extensions/discord/api.ts @@ -0,0 +1,13 @@ +export * from "./src/account-inspect.js"; +export * from "./src/accounts.js"; +export * from "./src/actions/handle-action.guild-admin.js"; +export * from "./src/actions/handle-action.js"; +export * from "./src/components.js"; +export * from "./src/group-policy.js"; +export * from "./src/normalize.js"; +export * from "./src/pluralkit.js"; +export * from "./src/session-key-normalization.js"; +export * from "./src/status-issues.js"; +export * from "./src/targets.js"; +export type { DiscordSendComponents, DiscordSendEmbeds } from "./src/send.shared.js"; +export type { DiscordSendResult } from "./src/send.types.js"; diff --git a/extensions/discord/index.ts b/extensions/discord/index.ts index 7c179623e23..6d3c754edb4 100644 --- a/extensions/discord/index.ts +++ b/extensions/discord/index.ts @@ -3,6 +3,9 @@ import { discordPlugin } from "./src/channel.js"; import { setDiscordRuntime } from "./src/runtime.js"; import { registerDiscordSubagentHooks } from "./src/subagent-hooks.js"; +export { discordPlugin } from "./src/channel.js"; +export { setDiscordRuntime } from "./src/runtime.js"; + export default defineChannelPluginEntry({ id: "discord", name: "Discord", diff --git a/extensions/discord/runtime-api.ts b/extensions/discord/runtime-api.ts new file mode 100644 index 00000000000..938b03d9c4a --- /dev/null +++ b/extensions/discord/runtime-api.ts @@ -0,0 +1,17 @@ +export * from "./src/audit.js"; +export * from "./src/actions/runtime.js"; +export * from "./src/actions/runtime.moderation-shared.js"; +export * from "./src/actions/runtime.shared.js"; +export * from "./src/channel-actions.js"; +export * from "./src/directory-live.js"; +export * from "./src/monitor.js"; +export * from "./src/monitor/gateway-plugin.js"; +export * from "./src/monitor/gateway-registry.js"; +export * from "./src/monitor/presence-cache.js"; +export * from "./src/monitor/thread-bindings.js"; +export * from "./src/monitor/thread-bindings.manager.js"; +export * from "./src/monitor/timeouts.js"; +export * from "./src/probe.js"; +export * from "./src/resolve-channels.js"; +export * from "./src/resolve-users.js"; +export * from "./src/send.js"; diff --git a/extensions/discord/setup-entry.ts b/extensions/discord/setup-entry.ts index e59c812ff4b..e2c4689ed39 100644 --- a/extensions/discord/setup-entry.ts +++ b/extensions/discord/setup-entry.ts @@ -1,4 +1,6 @@ import { defineSetupPluginEntry } from "openclaw/plugin-sdk/core"; import { discordSetupPlugin } from "./src/channel.setup.js"; +export { discordSetupPlugin } from "./src/channel.setup.js"; + export default defineSetupPluginEntry(discordSetupPlugin); diff --git a/extensions/discord/src/account-inspect.ts b/extensions/discord/src/account-inspect.ts index f42410814b3..0b3bd3f8fc8 100644 --- a/extensions/discord/src/account-inspect.ts +++ b/extensions/discord/src/account-inspect.ts @@ -1,18 +1,16 @@ -import { - DEFAULT_ACCOUNT_ID, - normalizeAccountId, - type OpenClawConfig, -} from "openclaw/plugin-sdk/account-resolution"; -import { - hasConfiguredSecretInput, - normalizeSecretInputString, -} from "openclaw/plugin-sdk/config-runtime"; -import type { DiscordAccountConfig } from "openclaw/plugin-sdk/discord"; import { mergeDiscordAccountConfig, resolveDefaultDiscordAccountId, resolveDiscordAccountConfig, } from "./accounts.js"; +import { + DEFAULT_ACCOUNT_ID, + normalizeAccountId, + hasConfiguredSecretInput, + normalizeSecretInputString, + type OpenClawConfig, + type DiscordAccountConfig, +} from "./runtime-api.js"; export type DiscordCredentialStatus = "available" | "configured_unavailable" | "missing"; diff --git a/extensions/discord/src/accounts.ts b/extensions/discord/src/accounts.ts index ad50e2e7aa3..ea28be7fb0d 100644 --- a/extensions/discord/src/accounts.ts +++ b/extensions/discord/src/accounts.ts @@ -4,8 +4,9 @@ import { normalizeAccountId, resolveAccountEntry, type OpenClawConfig, -} from "openclaw/plugin-sdk/account-resolution"; -import type { DiscordAccountConfig, DiscordActionConfig } from "openclaw/plugin-sdk/discord"; + type DiscordAccountConfig, + type DiscordActionConfig, +} from "./runtime-api.js"; import { resolveDiscordToken } from "./token.js"; export type ResolvedDiscordAccount = { diff --git a/extensions/discord/src/actions/handle-action.guild-admin.ts b/extensions/discord/src/actions/handle-action.guild-admin.ts index 0f6075384a5..e63d00f23ec 100644 --- a/extensions/discord/src/actions/handle-action.guild-admin.ts +++ b/extensions/discord/src/actions/handle-action.guild-admin.ts @@ -5,12 +5,12 @@ import { readStringArrayParam, readStringParam, } from "openclaw/plugin-sdk/agent-runtime"; +import type { ChannelMessageActionContext } from "openclaw/plugin-sdk/channel-runtime"; +import { handleDiscordAction } from "./runtime.js"; import { isDiscordModerationAction, readDiscordModerationCommand, -} from "openclaw/plugin-sdk/agent-runtime"; -import { handleDiscordAction } from "openclaw/plugin-sdk/agent-runtime"; -import type { ChannelMessageActionContext } from "openclaw/plugin-sdk/channel-runtime"; +} from "./runtime.moderation-shared.js"; type Ctx = Pick< ChannelMessageActionContext, diff --git a/extensions/discord/src/actions/handle-action.ts b/extensions/discord/src/actions/handle-action.ts index d23b078292a..9726b07cdda 100644 --- a/extensions/discord/src/actions/handle-action.ts +++ b/extensions/discord/src/actions/handle-action.ts @@ -4,15 +4,15 @@ import { readStringArrayParam, readStringParam, } from "openclaw/plugin-sdk/agent-runtime"; -import { readDiscordParentIdParam } from "openclaw/plugin-sdk/agent-runtime"; -import { handleDiscordAction } from "openclaw/plugin-sdk/agent-runtime"; import { readBooleanParam } from "openclaw/plugin-sdk/boolean-param"; import { resolveReactionMessageId } from "openclaw/plugin-sdk/channel-runtime"; import type { ChannelMessageActionContext } from "openclaw/plugin-sdk/channel-runtime"; -import { normalizeInteractiveReply } from "openclaw/plugin-sdk/channel-runtime"; +import { normalizeInteractiveReply } from "openclaw/plugin-sdk/interactive-runtime"; import { buildDiscordInteractiveComponents } from "../shared-interactive.js"; import { resolveDiscordChannelId } from "../targets.js"; import { tryHandleDiscordMessageActionGuildAdmin } from "./handle-action.guild-admin.js"; +import { handleDiscordAction } from "./runtime.js"; +import { readDiscordParentIdParam } from "./runtime.shared.js"; const providerId = "discord"; diff --git a/src/agents/tools/discord-actions-guild.ts b/extensions/discord/src/actions/runtime.guild.ts similarity index 78% rename from src/agents/tools/discord-actions-guild.ts rename to extensions/discord/src/actions/runtime.guild.ts index fa427d87650..386f7a969aa 100644 --- a/src/agents/tools/discord-actions-guild.ts +++ b/extensions/discord/src/actions/runtime.guild.ts @@ -1,5 +1,14 @@ import type { AgentToolResult } from "@mariozechner/pi-agent-core"; -import type { DiscordActionConfig } from "../../config/config.js"; +import { getPresence } from "../monitor/presence-cache.js"; +import { + type ActionGate, + jsonResult, + parseAvailableTags, + readNumberParam, + readStringArrayParam, + readStringParam, + type DiscordActionConfig, +} from "../runtime-api.js"; import { addRoleDiscord, createChannelDiscord, @@ -19,17 +28,29 @@ import { setChannelPermissionDiscord, uploadEmojiDiscord, uploadStickerDiscord, -} from "../../plugin-sdk/discord.js"; -import { getPresence } from "../../plugin-sdk/discord.js"; -import { - type ActionGate, - jsonResult, - parseAvailableTags, - readNumberParam, - readStringArrayParam, - readStringParam, -} from "./common.js"; -import { readDiscordParentIdParam } from "./discord-actions-shared.js"; +} from "../send.js"; +import { readDiscordParentIdParam } from "./runtime.shared.js"; + +export const discordGuildActionRuntime = { + addRoleDiscord, + createChannelDiscord, + createScheduledEventDiscord, + deleteChannelDiscord, + editChannelDiscord, + fetchChannelInfoDiscord, + fetchMemberInfoDiscord, + fetchRoleInfoDiscord, + fetchVoiceStatusDiscord, + listGuildChannelsDiscord, + listGuildEmojisDiscord, + listScheduledEventsDiscord, + moveChannelDiscord, + removeChannelPermissionDiscord, + removeRoleDiscord, + setChannelPermissionDiscord, + uploadEmojiDiscord, + uploadStickerDiscord, +}; type DiscordRoleMutation = (params: { guildId: string; @@ -85,8 +106,8 @@ export async function handleDiscordGuildAction( required: true, }); const member = accountId - ? await fetchMemberInfoDiscord(guildId, userId, { accountId }) - : await fetchMemberInfoDiscord(guildId, userId); + ? await discordGuildActionRuntime.fetchMemberInfoDiscord(guildId, userId, { accountId }) + : await discordGuildActionRuntime.fetchMemberInfoDiscord(guildId, userId); const presence = getPresence(accountId, userId); const activities = presence?.activities ?? undefined; const status = presence?.status ?? undefined; @@ -100,8 +121,8 @@ export async function handleDiscordGuildAction( required: true, }); const roles = accountId - ? await fetchRoleInfoDiscord(guildId, { accountId }) - : await fetchRoleInfoDiscord(guildId); + ? await discordGuildActionRuntime.fetchRoleInfoDiscord(guildId, { accountId }) + : await discordGuildActionRuntime.fetchRoleInfoDiscord(guildId); return jsonResult({ ok: true, roles }); } case "emojiList": { @@ -112,8 +133,8 @@ export async function handleDiscordGuildAction( required: true, }); const emojis = accountId - ? await listGuildEmojisDiscord(guildId, { accountId }) - : await listGuildEmojisDiscord(guildId); + ? await discordGuildActionRuntime.listGuildEmojisDiscord(guildId, { accountId }) + : await discordGuildActionRuntime.listGuildEmojisDiscord(guildId); return jsonResult({ ok: true, emojis }); } case "emojiUpload": { @@ -129,7 +150,7 @@ export async function handleDiscordGuildAction( }); const roleIds = readStringArrayParam(params, "roleIds"); const emoji = accountId - ? await uploadEmojiDiscord( + ? await discordGuildActionRuntime.uploadEmojiDiscord( { guildId, name, @@ -138,7 +159,7 @@ export async function handleDiscordGuildAction( }, { accountId }, ) - : await uploadEmojiDiscord({ + : await discordGuildActionRuntime.uploadEmojiDiscord({ guildId, name, mediaUrl, @@ -162,7 +183,7 @@ export async function handleDiscordGuildAction( required: true, }); const sticker = accountId - ? await uploadStickerDiscord( + ? await discordGuildActionRuntime.uploadStickerDiscord( { guildId, name, @@ -172,7 +193,7 @@ export async function handleDiscordGuildAction( }, { accountId }, ) - : await uploadStickerDiscord({ + : await discordGuildActionRuntime.uploadStickerDiscord({ guildId, name, description, @@ -185,14 +206,22 @@ export async function handleDiscordGuildAction( if (!isActionEnabled("roles", false)) { throw new Error("Discord role changes are disabled."); } - await runRoleMutation({ accountId, values: params, mutate: addRoleDiscord }); + await runRoleMutation({ + accountId, + values: params, + mutate: discordGuildActionRuntime.addRoleDiscord, + }); return jsonResult({ ok: true }); } case "roleRemove": { if (!isActionEnabled("roles", false)) { throw new Error("Discord role changes are disabled."); } - await runRoleMutation({ accountId, values: params, mutate: removeRoleDiscord }); + await runRoleMutation({ + accountId, + values: params, + mutate: discordGuildActionRuntime.removeRoleDiscord, + }); return jsonResult({ ok: true }); } case "channelInfo": { @@ -203,8 +232,8 @@ export async function handleDiscordGuildAction( required: true, }); const channel = accountId - ? await fetchChannelInfoDiscord(channelId, { accountId }) - : await fetchChannelInfoDiscord(channelId); + ? await discordGuildActionRuntime.fetchChannelInfoDiscord(channelId, { accountId }) + : await discordGuildActionRuntime.fetchChannelInfoDiscord(channelId); return jsonResult({ ok: true, channel }); } case "channelList": { @@ -215,8 +244,8 @@ export async function handleDiscordGuildAction( required: true, }); const channels = accountId - ? await listGuildChannelsDiscord(guildId, { accountId }) - : await listGuildChannelsDiscord(guildId); + ? await discordGuildActionRuntime.listGuildChannelsDiscord(guildId, { accountId }) + : await discordGuildActionRuntime.listGuildChannelsDiscord(guildId); return jsonResult({ ok: true, channels }); } case "voiceStatus": { @@ -230,8 +259,10 @@ export async function handleDiscordGuildAction( required: true, }); const voice = accountId - ? await fetchVoiceStatusDiscord(guildId, userId, { accountId }) - : await fetchVoiceStatusDiscord(guildId, userId); + ? await discordGuildActionRuntime.fetchVoiceStatusDiscord(guildId, userId, { + accountId, + }) + : await discordGuildActionRuntime.fetchVoiceStatusDiscord(guildId, userId); return jsonResult({ ok: true, voice }); } case "eventList": { @@ -242,8 +273,8 @@ export async function handleDiscordGuildAction( required: true, }); const events = accountId - ? await listScheduledEventsDiscord(guildId, { accountId }) - : await listScheduledEventsDiscord(guildId); + ? await discordGuildActionRuntime.listScheduledEventsDiscord(guildId, { accountId }) + : await discordGuildActionRuntime.listScheduledEventsDiscord(guildId); return jsonResult({ ok: true, events }); } case "eventCreate": { @@ -274,8 +305,10 @@ export async function handleDiscordGuildAction( privacy_level: 2, }; const event = accountId - ? await createScheduledEventDiscord(guildId, payload, { accountId }) - : await createScheduledEventDiscord(guildId, payload); + ? await discordGuildActionRuntime.createScheduledEventDiscord(guildId, payload, { + accountId, + }) + : await discordGuildActionRuntime.createScheduledEventDiscord(guildId, payload); return jsonResult({ ok: true, event }); } case "channelCreate": { @@ -290,7 +323,7 @@ export async function handleDiscordGuildAction( const position = readNumberParam(params, "position", { integer: true }); const nsfw = params.nsfw as boolean | undefined; const channel = accountId - ? await createChannelDiscord( + ? await discordGuildActionRuntime.createChannelDiscord( { guildId, name, @@ -302,7 +335,7 @@ export async function handleDiscordGuildAction( }, { accountId }, ) - : await createChannelDiscord({ + : await discordGuildActionRuntime.createChannelDiscord({ guildId, name, type: type ?? undefined, @@ -348,8 +381,8 @@ export async function handleDiscordGuildAction( availableTags, }; const channel = accountId - ? await editChannelDiscord(editPayload, { accountId }) - : await editChannelDiscord(editPayload); + ? await discordGuildActionRuntime.editChannelDiscord(editPayload, { accountId }) + : await discordGuildActionRuntime.editChannelDiscord(editPayload); return jsonResult({ ok: true, channel }); } case "channelDelete": { @@ -360,8 +393,8 @@ export async function handleDiscordGuildAction( required: true, }); const result = accountId - ? await deleteChannelDiscord(channelId, { accountId }) - : await deleteChannelDiscord(channelId); + ? await discordGuildActionRuntime.deleteChannelDiscord(channelId, { accountId }) + : await discordGuildActionRuntime.deleteChannelDiscord(channelId); return jsonResult(result); } case "channelMove": { @@ -375,7 +408,7 @@ export async function handleDiscordGuildAction( const parentId = readDiscordParentIdParam(params); const position = readNumberParam(params, "position", { integer: true }); if (accountId) { - await moveChannelDiscord( + await discordGuildActionRuntime.moveChannelDiscord( { guildId, channelId, @@ -385,7 +418,7 @@ export async function handleDiscordGuildAction( { accountId }, ); } else { - await moveChannelDiscord({ + await discordGuildActionRuntime.moveChannelDiscord({ guildId, channelId, parentId, @@ -402,7 +435,7 @@ export async function handleDiscordGuildAction( const name = readStringParam(params, "name", { required: true }); const position = readNumberParam(params, "position", { integer: true }); const channel = accountId - ? await createChannelDiscord( + ? await discordGuildActionRuntime.createChannelDiscord( { guildId, name, @@ -411,7 +444,7 @@ export async function handleDiscordGuildAction( }, { accountId }, ) - : await createChannelDiscord({ + : await discordGuildActionRuntime.createChannelDiscord({ guildId, name, type: 4, @@ -429,7 +462,7 @@ export async function handleDiscordGuildAction( const name = readStringParam(params, "name"); const position = readNumberParam(params, "position", { integer: true }); const channel = accountId - ? await editChannelDiscord( + ? await discordGuildActionRuntime.editChannelDiscord( { channelId: categoryId, name: name ?? undefined, @@ -437,7 +470,7 @@ export async function handleDiscordGuildAction( }, { accountId }, ) - : await editChannelDiscord({ + : await discordGuildActionRuntime.editChannelDiscord({ channelId: categoryId, name: name ?? undefined, position: position ?? undefined, @@ -452,8 +485,8 @@ export async function handleDiscordGuildAction( required: true, }); const result = accountId - ? await deleteChannelDiscord(categoryId, { accountId }) - : await deleteChannelDiscord(categoryId); + ? await discordGuildActionRuntime.deleteChannelDiscord(categoryId, { accountId }) + : await discordGuildActionRuntime.deleteChannelDiscord(categoryId); return jsonResult(result); } case "channelPermissionSet": { @@ -468,7 +501,7 @@ export async function handleDiscordGuildAction( const allow = readStringParam(params, "allow"); const deny = readStringParam(params, "deny"); if (accountId) { - await setChannelPermissionDiscord( + await discordGuildActionRuntime.setChannelPermissionDiscord( { channelId, targetId, @@ -479,7 +512,7 @@ export async function handleDiscordGuildAction( { accountId }, ); } else { - await setChannelPermissionDiscord({ + await discordGuildActionRuntime.setChannelPermissionDiscord({ channelId, targetId, targetType, @@ -495,9 +528,11 @@ export async function handleDiscordGuildAction( } const { channelId, targetId } = readChannelPermissionTarget(params); if (accountId) { - await removeChannelPermissionDiscord(channelId, targetId, { accountId }); + await discordGuildActionRuntime.removeChannelPermissionDiscord(channelId, targetId, { + accountId, + }); } else { - await removeChannelPermissionDiscord(channelId, targetId); + await discordGuildActionRuntime.removeChannelPermissionDiscord(channelId, targetId); } return jsonResult({ ok: true }); } diff --git a/src/agents/tools/discord-actions-messaging.ts b/extensions/discord/src/actions/runtime.messaging.ts similarity index 73% rename from src/agents/tools/discord-actions-messaging.ts rename to extensions/discord/src/actions/runtime.messaging.ts index bad969ede80..adbad1bd2d5 100644 --- a/src/agents/tools/discord-actions-messaging.ts +++ b/extensions/discord/src/actions/runtime.messaging.ts @@ -1,7 +1,19 @@ import type { AgentToolResult } from "@mariozechner/pi-agent-core"; -import type { DiscordActionConfig } from "../../config/config.js"; -import type { OpenClawConfig } from "../../config/config.js"; -import { readBooleanParam } from "../../plugin-sdk/boolean-param.js"; +import { readDiscordComponentSpec } from "../components.js"; +import { + assertMediaNotDataUrl, + type ActionGate, + jsonResult, + readNumberParam, + readReactionParams, + readStringArrayParam, + readStringParam, + resolvePollMaxSelections, + type DiscordActionConfig, + type OpenClawConfig, + withNormalizedTimestamp, + readBooleanParam, +} from "../runtime-api.js"; import { createThreadDiscord, deleteMessageDiscord, @@ -23,20 +35,34 @@ import { sendStickerDiscord, sendVoiceMessageDiscord, unpinMessageDiscord, -} from "../../plugin-sdk/discord.js"; -import type { DiscordSendComponents, DiscordSendEmbeds } from "../../plugin-sdk/discord.js"; -import { readDiscordComponentSpec, resolveDiscordChannelId } from "../../plugin-sdk/discord.js"; -import { resolvePollMaxSelections } from "../../polls.js"; -import { withNormalizedTimestamp } from "../date-time.js"; -import { assertMediaNotDataUrl } from "../sandbox-paths.js"; -import { - type ActionGate, - jsonResult, - readNumberParam, - readReactionParams, - readStringArrayParam, - readStringParam, -} from "./common.js"; +} from "../send.js"; +import type { DiscordSendComponents, DiscordSendEmbeds } from "../send.shared.js"; +import { resolveDiscordChannelId } from "../targets.js"; + +export const discordMessagingActionRuntime = { + createThreadDiscord, + deleteMessageDiscord, + editMessageDiscord, + fetchChannelPermissionsDiscord, + fetchMessageDiscord, + fetchReactionsDiscord, + listPinsDiscord, + listThreadsDiscord, + pinMessageDiscord, + reactMessageDiscord, + readDiscordComponentSpec, + readMessagesDiscord, + removeOwnReactionsDiscord, + removeReactionDiscord, + resolveDiscordChannelId, + searchMessagesDiscord, + sendDiscordComponentMessage, + sendMessageDiscord, + sendPollDiscord, + sendStickerDiscord, + sendVoiceMessageDiscord, + unpinMessageDiscord, +}; function parseDiscordMessageLink(link: string) { const normalized = link.trim(); @@ -65,7 +91,7 @@ export async function handleDiscordMessagingAction( cfg?: OpenClawConfig, ): Promise> { const resolveChannelId = () => - resolveDiscordChannelId( + discordMessagingActionRuntime.resolveDiscordChannelId( readStringParam(params, "channelId", { required: true, }), @@ -95,28 +121,45 @@ export async function handleDiscordMessagingAction( }); if (remove) { if (accountId) { - await removeReactionDiscord(channelId, messageId, emoji, { + await discordMessagingActionRuntime.removeReactionDiscord(channelId, messageId, emoji, { ...cfgOptions, accountId, }); } else { - await removeReactionDiscord(channelId, messageId, emoji, cfgOptions); + await discordMessagingActionRuntime.removeReactionDiscord( + channelId, + messageId, + emoji, + cfgOptions, + ); } return jsonResult({ ok: true, removed: emoji }); } if (isEmpty) { const removed = accountId - ? await removeOwnReactionsDiscord(channelId, messageId, { ...cfgOptions, accountId }) - : await removeOwnReactionsDiscord(channelId, messageId, cfgOptions); + ? await discordMessagingActionRuntime.removeOwnReactionsDiscord(channelId, messageId, { + ...cfgOptions, + accountId, + }) + : await discordMessagingActionRuntime.removeOwnReactionsDiscord( + channelId, + messageId, + cfgOptions, + ); return jsonResult({ ok: true, removed: removed.removed }); } if (accountId) { - await reactMessageDiscord(channelId, messageId, emoji, { + await discordMessagingActionRuntime.reactMessageDiscord(channelId, messageId, emoji, { ...cfgOptions, accountId, }); } else { - await reactMessageDiscord(channelId, messageId, emoji, cfgOptions); + await discordMessagingActionRuntime.reactMessageDiscord( + channelId, + messageId, + emoji, + cfgOptions, + ); } return jsonResult({ ok: true, added: emoji }); } @@ -129,11 +172,15 @@ export async function handleDiscordMessagingAction( required: true, }); const limit = readNumberParam(params, "limit"); - const reactions = await fetchReactionsDiscord(channelId, messageId, { - ...cfgOptions, - ...(accountId ? { accountId } : {}), - limit, - }); + const reactions = await discordMessagingActionRuntime.fetchReactionsDiscord( + channelId, + messageId, + { + ...cfgOptions, + ...(accountId ? { accountId } : {}), + limit, + }, + ); return jsonResult({ ok: true, reactions }); } case "sticker": { @@ -146,7 +193,7 @@ export async function handleDiscordMessagingAction( required: true, label: "stickerIds", }); - await sendStickerDiscord(to, stickerIds, { + await discordMessagingActionRuntime.sendStickerDiscord(to, stickerIds, { ...cfgOptions, ...(accountId ? { accountId } : {}), content, @@ -169,7 +216,7 @@ export async function handleDiscordMessagingAction( const allowMultiselect = readBooleanParam(params, "allowMultiselect"); const durationHours = readNumberParam(params, "durationHours"); const maxSelections = resolvePollMaxSelections(answers.length, allowMultiselect); - await sendPollDiscord( + await discordMessagingActionRuntime.sendPollDiscord( to, { question, options: answers, maxSelections, durationHours }, { ...cfgOptions, ...(accountId ? { accountId } : {}), content }, @@ -182,8 +229,11 @@ export async function handleDiscordMessagingAction( } const channelId = resolveChannelId(); const permissions = accountId - ? await fetchChannelPermissionsDiscord(channelId, { ...cfgOptions, accountId }) - : await fetchChannelPermissionsDiscord(channelId, cfgOptions); + ? await discordMessagingActionRuntime.fetchChannelPermissionsDiscord(channelId, { + ...cfgOptions, + accountId, + }) + : await discordMessagingActionRuntime.fetchChannelPermissionsDiscord(channelId, cfgOptions); return jsonResult({ ok: true, permissions }); } case "fetchMessage": { @@ -206,8 +256,11 @@ export async function handleDiscordMessagingAction( ); } const message = accountId - ? await fetchMessageDiscord(channelId, messageId, { ...cfgOptions, accountId }) - : await fetchMessageDiscord(channelId, messageId, cfgOptions); + ? await discordMessagingActionRuntime.fetchMessageDiscord(channelId, messageId, { + ...cfgOptions, + accountId, + }) + : await discordMessagingActionRuntime.fetchMessageDiscord(channelId, messageId, cfgOptions); return jsonResult({ ok: true, message: normalizeMessage(message), @@ -228,8 +281,11 @@ export async function handleDiscordMessagingAction( around: readStringParam(params, "around"), }; const messages = accountId - ? await readMessagesDiscord(channelId, query, { ...cfgOptions, accountId }) - : await readMessagesDiscord(channelId, query, cfgOptions); + ? await discordMessagingActionRuntime.readMessagesDiscord(channelId, query, { + ...cfgOptions, + accountId, + }) + : await discordMessagingActionRuntime.readMessagesDiscord(channelId, query, cfgOptions); return jsonResult({ ok: true, messages: messages.map((message) => normalizeMessage(message)), @@ -245,7 +301,7 @@ export async function handleDiscordMessagingAction( const rawComponents = params.components; const componentSpec = rawComponents && typeof rawComponents === "object" && !Array.isArray(rawComponents) - ? readDiscordComponentSpec(rawComponents) + ? discordMessagingActionRuntime.readDiscordComponentSpec(rawComponents) : null; const components: DiscordSendComponents | undefined = Array.isArray(rawComponents) || typeof rawComponents === "function" @@ -279,16 +335,20 @@ export async function handleDiscordMessagingAction( const payload = componentSpec.text ? componentSpec : { ...componentSpec, text: normalizedContent }; - const result = await sendDiscordComponentMessage(to, payload, { - ...cfgOptions, - ...(accountId ? { accountId } : {}), - silent, - replyTo: replyTo ?? undefined, - sessionKey: sessionKey ?? undefined, - agentId: agentId ?? undefined, - mediaUrl: mediaUrl ?? undefined, - filename: filename ?? undefined, - }); + const result = await discordMessagingActionRuntime.sendDiscordComponentMessage( + to, + payload, + { + ...cfgOptions, + ...(accountId ? { accountId } : {}), + silent, + replyTo: replyTo ?? undefined, + sessionKey: sessionKey ?? undefined, + agentId: agentId ?? undefined, + mediaUrl: mediaUrl ?? undefined, + filename: filename ?? undefined, + }, + ); return jsonResult({ ok: true, result, components: true }); } @@ -305,7 +365,7 @@ export async function handleDiscordMessagingAction( ); } assertMediaNotDataUrl(mediaUrl); - const result = await sendVoiceMessageDiscord(to, mediaUrl, { + const result = await discordMessagingActionRuntime.sendVoiceMessageDiscord(to, mediaUrl, { ...cfgOptions, ...(accountId ? { accountId } : {}), replyTo, @@ -314,7 +374,7 @@ export async function handleDiscordMessagingAction( return jsonResult({ ok: true, result, voiceMessage: true }); } - const result = await sendMessageDiscord(to, content ?? "", { + const result = await discordMessagingActionRuntime.sendMessageDiscord(to, content ?? "", { ...cfgOptions, ...(accountId ? { accountId } : {}), mediaUrl, @@ -338,8 +398,18 @@ export async function handleDiscordMessagingAction( required: true, }); const message = accountId - ? await editMessageDiscord(channelId, messageId, { content }, { ...cfgOptions, accountId }) - : await editMessageDiscord(channelId, messageId, { content }, cfgOptions); + ? await discordMessagingActionRuntime.editMessageDiscord( + channelId, + messageId, + { content }, + { ...cfgOptions, accountId }, + ) + : await discordMessagingActionRuntime.editMessageDiscord( + channelId, + messageId, + { content }, + cfgOptions, + ); return jsonResult({ ok: true, message }); } case "deleteMessage": { @@ -351,9 +421,12 @@ export async function handleDiscordMessagingAction( required: true, }); if (accountId) { - await deleteMessageDiscord(channelId, messageId, { ...cfgOptions, accountId }); + await discordMessagingActionRuntime.deleteMessageDiscord(channelId, messageId, { + ...cfgOptions, + accountId, + }); } else { - await deleteMessageDiscord(channelId, messageId, cfgOptions); + await discordMessagingActionRuntime.deleteMessageDiscord(channelId, messageId, cfgOptions); } return jsonResult({ ok: true }); } @@ -375,8 +448,11 @@ export async function handleDiscordMessagingAction( appliedTags: appliedTags ?? undefined, }; const thread = accountId - ? await createThreadDiscord(channelId, payload, { ...cfgOptions, accountId }) - : await createThreadDiscord(channelId, payload, cfgOptions); + ? await discordMessagingActionRuntime.createThreadDiscord(channelId, payload, { + ...cfgOptions, + accountId, + }) + : await discordMessagingActionRuntime.createThreadDiscord(channelId, payload, cfgOptions); return jsonResult({ ok: true, thread }); } case "threadList": { @@ -391,7 +467,7 @@ export async function handleDiscordMessagingAction( const before = readStringParam(params, "before"); const limit = readNumberParam(params, "limit"); const threads = accountId - ? await listThreadsDiscord( + ? await discordMessagingActionRuntime.listThreadsDiscord( { guildId, channelId, @@ -401,7 +477,7 @@ export async function handleDiscordMessagingAction( }, { ...cfgOptions, accountId }, ) - : await listThreadsDiscord( + : await discordMessagingActionRuntime.listThreadsDiscord( { guildId, channelId, @@ -423,13 +499,17 @@ export async function handleDiscordMessagingAction( }); const mediaUrl = readStringParam(params, "mediaUrl"); const replyTo = readStringParam(params, "replyTo"); - const result = await sendMessageDiscord(`channel:${channelId}`, content, { - ...cfgOptions, - ...(accountId ? { accountId } : {}), - mediaUrl, - mediaLocalRoots: options?.mediaLocalRoots, - replyTo, - }); + const result = await discordMessagingActionRuntime.sendMessageDiscord( + `channel:${channelId}`, + content, + { + ...cfgOptions, + ...(accountId ? { accountId } : {}), + mediaUrl, + mediaLocalRoots: options?.mediaLocalRoots, + replyTo, + }, + ); return jsonResult({ ok: true, result }); } case "pinMessage": { @@ -441,9 +521,12 @@ export async function handleDiscordMessagingAction( required: true, }); if (accountId) { - await pinMessageDiscord(channelId, messageId, { ...cfgOptions, accountId }); + await discordMessagingActionRuntime.pinMessageDiscord(channelId, messageId, { + ...cfgOptions, + accountId, + }); } else { - await pinMessageDiscord(channelId, messageId, cfgOptions); + await discordMessagingActionRuntime.pinMessageDiscord(channelId, messageId, cfgOptions); } return jsonResult({ ok: true }); } @@ -456,9 +539,12 @@ export async function handleDiscordMessagingAction( required: true, }); if (accountId) { - await unpinMessageDiscord(channelId, messageId, { ...cfgOptions, accountId }); + await discordMessagingActionRuntime.unpinMessageDiscord(channelId, messageId, { + ...cfgOptions, + accountId, + }); } else { - await unpinMessageDiscord(channelId, messageId, cfgOptions); + await discordMessagingActionRuntime.unpinMessageDiscord(channelId, messageId, cfgOptions); } return jsonResult({ ok: true }); } @@ -468,8 +554,11 @@ export async function handleDiscordMessagingAction( } const channelId = resolveChannelId(); const pins = accountId - ? await listPinsDiscord(channelId, { ...cfgOptions, accountId }) - : await listPinsDiscord(channelId, cfgOptions); + ? await discordMessagingActionRuntime.listPinsDiscord(channelId, { + ...cfgOptions, + accountId, + }) + : await discordMessagingActionRuntime.listPinsDiscord(channelId, cfgOptions); return jsonResult({ ok: true, pins: pins.map((pin) => normalizeMessage(pin)) }); } case "searchMessages": { @@ -490,7 +579,7 @@ export async function handleDiscordMessagingAction( const channelIdList = [...(channelIds ?? []), ...(channelId ? [channelId] : [])]; const authorIdList = [...(authorIds ?? []), ...(authorId ? [authorId] : [])]; const results = accountId - ? await searchMessagesDiscord( + ? await discordMessagingActionRuntime.searchMessagesDiscord( { guildId, content, @@ -500,7 +589,7 @@ export async function handleDiscordMessagingAction( }, { ...cfgOptions, accountId }, ) - : await searchMessagesDiscord( + : await discordMessagingActionRuntime.searchMessagesDiscord( { guildId, content, diff --git a/src/agents/tools/discord-actions-moderation-shared.ts b/extensions/discord/src/actions/runtime.moderation-shared.ts similarity index 95% rename from src/agents/tools/discord-actions-moderation-shared.ts rename to extensions/discord/src/actions/runtime.moderation-shared.ts index b2d9ec0ba99..e12dd005b04 100644 --- a/src/agents/tools/discord-actions-moderation-shared.ts +++ b/extensions/discord/src/actions/runtime.moderation-shared.ts @@ -1,5 +1,5 @@ import { PermissionFlagsBits } from "discord-api-types/v10"; -import { readNumberParam, readStringParam } from "./common.js"; +import { readNumberParam, readStringParam } from "../runtime-api.js"; export type DiscordModerationAction = "timeout" | "kick" | "ban"; diff --git a/src/agents/tools/discord-actions-moderation.authz.test.ts b/extensions/discord/src/actions/runtime.moderation.authz.test.ts similarity index 81% rename from src/agents/tools/discord-actions-moderation.authz.test.ts rename to extensions/discord/src/actions/runtime.moderation.authz.test.ts index d6b3651ca88..66d2a4ba9d8 100644 --- a/src/agents/tools/discord-actions-moderation.authz.test.ts +++ b/extensions/discord/src/actions/runtime.moderation.authz.test.ts @@ -1,25 +1,30 @@ import { PermissionFlagsBits } from "discord-api-types/v10"; -import { describe, expect, it, vi } from "vitest"; -import type { DiscordActionConfig } from "../../config/config.js"; -import { handleDiscordModerationAction } from "./discord-actions-moderation.js"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { DiscordActionConfig } from "../../../../src/config/types.discord.js"; +import { + discordModerationActionRuntime, + handleDiscordModerationAction, +} from "./runtime.moderation.js"; -const discordSendMocks = vi.hoisted(() => ({ - banMemberDiscord: vi.fn(async () => ({ ok: true })), - kickMemberDiscord: vi.fn(async () => ({ ok: true })), - timeoutMemberDiscord: vi.fn(async () => ({ id: "user-1" })), - hasAnyGuildPermissionDiscord: vi.fn(async () => false), -})); - -const { banMemberDiscord, kickMemberDiscord, timeoutMemberDiscord, hasAnyGuildPermissionDiscord } = - discordSendMocks; - -vi.mock("../../../extensions/discord/src/send.js", () => ({ - ...discordSendMocks, -})); +const originalDiscordModerationActionRuntime = { ...discordModerationActionRuntime }; +const banMemberDiscord = vi.fn(async () => ({ ok: true })); +const kickMemberDiscord = vi.fn(async () => ({ ok: true })); +const timeoutMemberDiscord = vi.fn(async () => ({ id: "user-1" })); +const hasAnyGuildPermissionDiscord = vi.fn(async () => false); const enableAllActions = (_key: keyof DiscordActionConfig, _defaultValue = true) => true; describe("discord moderation sender authorization", () => { + beforeEach(() => { + vi.clearAllMocks(); + Object.assign(discordModerationActionRuntime, originalDiscordModerationActionRuntime, { + banMemberDiscord, + kickMemberDiscord, + timeoutMemberDiscord, + hasAnyGuildPermissionDiscord, + }); + }); + it("rejects ban when sender lacks BAN_MEMBERS", async () => { hasAnyGuildPermissionDiscord.mockResolvedValueOnce(false); diff --git a/src/agents/tools/discord-actions-moderation.ts b/extensions/discord/src/actions/runtime.moderation.ts similarity index 79% rename from src/agents/tools/discord-actions-moderation.ts rename to extensions/discord/src/actions/runtime.moderation.ts index 56d7a80d4c9..fed41d40dad 100644 --- a/src/agents/tools/discord-actions-moderation.ts +++ b/extensions/discord/src/actions/runtime.moderation.ts @@ -1,17 +1,28 @@ import type { AgentToolResult } from "@mariozechner/pi-agent-core"; -import type { DiscordActionConfig } from "../../config/config.js"; +import { + type ActionGate, + jsonResult, + readStringParam, + type DiscordActionConfig, +} from "../runtime-api.js"; import { banMemberDiscord, hasAnyGuildPermissionDiscord, kickMemberDiscord, timeoutMemberDiscord, -} from "../../plugin-sdk/discord.js"; -import { type ActionGate, jsonResult, readStringParam } from "./common.js"; +} from "../send.js"; import { isDiscordModerationAction, readDiscordModerationCommand, requiredGuildPermissionForModerationAction, -} from "./discord-actions-moderation-shared.js"; +} from "./runtime.moderation-shared.js"; + +export const discordModerationActionRuntime = { + banMemberDiscord, + hasAnyGuildPermissionDiscord, + kickMemberDiscord, + timeoutMemberDiscord, +}; async function verifySenderModerationPermission(params: { guildId: string; @@ -23,7 +34,7 @@ async function verifySenderModerationPermission(params: { if (!params.senderUserId) { return; } - const hasPermission = await hasAnyGuildPermissionDiscord( + const hasPermission = await discordModerationActionRuntime.hasAnyGuildPermissionDiscord( params.guildId, params.senderUserId, [params.requiredPermission], @@ -57,7 +68,7 @@ export async function handleDiscordModerationAction( switch (command.action) { case "timeout": { const member = accountId - ? await timeoutMemberDiscord( + ? await discordModerationActionRuntime.timeoutMemberDiscord( { guildId: command.guildId, userId: command.userId, @@ -67,7 +78,7 @@ export async function handleDiscordModerationAction( }, { accountId }, ) - : await timeoutMemberDiscord({ + : await discordModerationActionRuntime.timeoutMemberDiscord({ guildId: command.guildId, userId: command.userId, durationMinutes: command.durationMinutes, @@ -78,7 +89,7 @@ export async function handleDiscordModerationAction( } case "kick": { if (accountId) { - await kickMemberDiscord( + await discordModerationActionRuntime.kickMemberDiscord( { guildId: command.guildId, userId: command.userId, @@ -87,7 +98,7 @@ export async function handleDiscordModerationAction( { accountId }, ); } else { - await kickMemberDiscord({ + await discordModerationActionRuntime.kickMemberDiscord({ guildId: command.guildId, userId: command.userId, reason: command.reason, @@ -97,7 +108,7 @@ export async function handleDiscordModerationAction( } case "ban": { if (accountId) { - await banMemberDiscord( + await discordModerationActionRuntime.banMemberDiscord( { guildId: command.guildId, userId: command.userId, @@ -107,7 +118,7 @@ export async function handleDiscordModerationAction( { accountId }, ); } else { - await banMemberDiscord({ + await discordModerationActionRuntime.banMemberDiscord({ guildId: command.guildId, userId: command.userId, reason: command.reason, diff --git a/src/agents/tools/discord-actions-presence.test.ts b/extensions/discord/src/actions/runtime.presence.test.ts similarity index 94% rename from src/agents/tools/discord-actions-presence.test.ts rename to extensions/discord/src/actions/runtime.presence.test.ts index dc8080666c6..7cc118150de 100644 --- a/src/agents/tools/discord-actions-presence.test.ts +++ b/extensions/discord/src/actions/runtime.presence.test.ts @@ -1,12 +1,9 @@ import type { GatewayPlugin } from "@buape/carbon/gateway"; import { beforeEach, describe, expect, it, vi } from "vitest"; -import { - clearGateways, - registerGateway, -} from "../../../extensions/discord/src/monitor/gateway-registry.js"; -import type { DiscordActionConfig } from "../../config/config.js"; -import type { ActionGate } from "./common.js"; -import { handleDiscordPresenceAction } from "./discord-actions-presence.js"; +import type { ActionGate } from "../../../../src/agents/tools/common.js"; +import type { DiscordActionConfig } from "../../../../src/config/types.discord.js"; +import { clearGateways, registerGateway } from "../monitor/gateway-registry.js"; +import { handleDiscordPresenceAction } from "./runtime.presence.js"; const mockUpdatePresence = vi.fn(); diff --git a/src/agents/tools/discord-actions-presence.ts b/extensions/discord/src/actions/runtime.presence.ts similarity index 93% rename from src/agents/tools/discord-actions-presence.ts rename to extensions/discord/src/actions/runtime.presence.ts index 53c42829bb0..1596b8b8ee4 100644 --- a/src/agents/tools/discord-actions-presence.ts +++ b/extensions/discord/src/actions/runtime.presence.ts @@ -1,8 +1,12 @@ import type { Activity, UpdatePresenceData } from "@buape/carbon/gateway"; import type { AgentToolResult } from "@mariozechner/pi-agent-core"; -import type { DiscordActionConfig } from "../../config/config.js"; -import { getGateway } from "../../plugin-sdk/discord.js"; -import { type ActionGate, jsonResult, readStringParam } from "./common.js"; +import { getGateway } from "../monitor/gateway-registry.js"; +import { + type ActionGate, + jsonResult, + readStringParam, + type DiscordActionConfig, +} from "../runtime-api.js"; const ACTIVITY_TYPE_MAP: Record = { playing: 0, diff --git a/src/agents/tools/discord-actions-shared.ts b/extensions/discord/src/actions/runtime.shared.ts similarity index 83% rename from src/agents/tools/discord-actions-shared.ts rename to extensions/discord/src/actions/runtime.shared.ts index 6f8283b5240..c597fa38c4f 100644 --- a/src/agents/tools/discord-actions-shared.ts +++ b/extensions/discord/src/actions/runtime.shared.ts @@ -1,4 +1,4 @@ -import { readStringParam } from "./common.js"; +import { readStringParam } from "../runtime-api.js"; export function readDiscordParentIdParam( params: Record, diff --git a/src/agents/tools/discord-actions.test.ts b/extensions/discord/src/actions/runtime.test.ts similarity index 94% rename from src/agents/tools/discord-actions.test.ts rename to extensions/discord/src/actions/runtime.test.ts index c03cb2fdafa..8f11162f8f3 100644 --- a/src/agents/tools/discord-actions.test.ts +++ b/extensions/discord/src/actions/runtime.test.ts @@ -1,11 +1,22 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import type { DiscordActionConfig, OpenClawConfig } from "../../config/config.js"; -import { handleDiscordGuildAction } from "./discord-actions-guild.js"; -import { handleDiscordMessagingAction } from "./discord-actions-messaging.js"; -import { handleDiscordModerationAction } from "./discord-actions-moderation.js"; -import { handleDiscordAction } from "./discord-actions.js"; +import type { OpenClawConfig } from "../../../../src/config/config.js"; +import type { DiscordActionConfig } from "../../../../src/config/types.discord.js"; +import { discordGuildActionRuntime, handleDiscordGuildAction } from "./runtime.guild.js"; +import { handleDiscordAction } from "./runtime.js"; +import { + discordMessagingActionRuntime, + handleDiscordMessagingAction, +} from "./runtime.messaging.js"; +import { + discordModerationActionRuntime, + handleDiscordModerationAction, +} from "./runtime.moderation.js"; -const discordSendMocks = vi.hoisted(() => ({ +const originalDiscordMessagingActionRuntime = { ...discordMessagingActionRuntime }; +const originalDiscordGuildActionRuntime = { ...discordGuildActionRuntime }; +const originalDiscordModerationActionRuntime = { ...discordModerationActionRuntime }; + +const discordSendMocks = { banMemberDiscord: vi.fn(async () => ({})), createChannelDiscord: vi.fn(async () => ({ id: "new-channel", @@ -42,7 +53,7 @@ const discordSendMocks = vi.hoisted(() => ({ setChannelPermissionDiscord: vi.fn(async () => ({ ok: true })), timeoutMemberDiscord: vi.fn(async () => ({})), unpinMessageDiscord: vi.fn(async () => ({})), -})); +}; const { createChannelDiscord, @@ -67,21 +78,28 @@ const { timeoutMemberDiscord, } = discordSendMocks; -vi.mock("../../../extensions/discord/src/send.js", () => ({ - ...discordSendMocks, -})); - const enableAllActions = () => true; const disabledActions = (key: keyof DiscordActionConfig) => key !== "reactions"; const channelInfoEnabled = (key: keyof DiscordActionConfig) => key === "channelInfo"; const moderationEnabled = (key: keyof DiscordActionConfig) => key === "moderation"; -describe("handleDiscordMessagingAction", () => { - beforeEach(() => { - vi.clearAllMocks(); - }); +beforeEach(() => { + vi.clearAllMocks(); + Object.assign( + discordMessagingActionRuntime, + originalDiscordMessagingActionRuntime, + discordSendMocks, + ); + Object.assign(discordGuildActionRuntime, originalDiscordGuildActionRuntime, discordSendMocks); + Object.assign( + discordModerationActionRuntime, + originalDiscordModerationActionRuntime, + discordSendMocks, + ); +}); +describe("handleDiscordMessagingAction", () => { it.each([ { name: "without account", diff --git a/src/agents/tools/discord-actions.ts b/extensions/discord/src/actions/runtime.ts similarity index 79% rename from src/agents/tools/discord-actions.ts rename to extensions/discord/src/actions/runtime.ts index b953e56cffd..ab0c8d90f93 100644 --- a/src/agents/tools/discord-actions.ts +++ b/extensions/discord/src/actions/runtime.ts @@ -1,11 +1,10 @@ import type { AgentToolResult } from "@mariozechner/pi-agent-core"; -import type { OpenClawConfig } from "../../config/config.js"; -import { createDiscordActionGate } from "../../plugin-sdk/discord.js"; -import { readStringParam } from "./common.js"; -import { handleDiscordGuildAction } from "./discord-actions-guild.js"; -import { handleDiscordMessagingAction } from "./discord-actions-messaging.js"; -import { handleDiscordModerationAction } from "./discord-actions-moderation.js"; -import { handleDiscordPresenceAction } from "./discord-actions-presence.js"; +import { createDiscordActionGate } from "../accounts.js"; +import { readStringParam, type OpenClawConfig } from "../runtime-api.js"; +import { handleDiscordGuildAction } from "./runtime.guild.js"; +import { handleDiscordMessagingAction } from "./runtime.messaging.js"; +import { handleDiscordModerationAction } from "./runtime.moderation.js"; +import { handleDiscordPresenceAction } from "./runtime.presence.js"; const messagingActions = new Set([ "react", diff --git a/extensions/discord/src/channel-actions.ts b/extensions/discord/src/channel-actions.ts index 21f24fd9553..c4be7728439 100644 --- a/extensions/discord/src/channel-actions.ts +++ b/extensions/discord/src/channel-actions.ts @@ -1,115 +1,137 @@ import { + createDiscordMessageToolComponentsSchema, createUnionActionGate, listTokenSourcedAccounts, } from "openclaw/plugin-sdk/channel-runtime"; import type { ChannelMessageActionAdapter, ChannelMessageActionName, + ChannelMessageToolDiscovery, } from "openclaw/plugin-sdk/channel-runtime"; import type { DiscordActionConfig } from "openclaw/plugin-sdk/config-runtime"; import { createDiscordActionGate, listEnabledDiscordAccounts } from "./accounts.js"; import { handleDiscordMessageAction } from "./actions/handle-action.js"; +function resolveDiscordActionDiscovery(cfg: Parameters[0]) { + const accounts = listTokenSourcedAccounts(listEnabledDiscordAccounts(cfg)); + if (accounts.length === 0) { + return null; + } + const unionGate = createUnionActionGate(accounts, (account) => + createDiscordActionGate({ + cfg, + accountId: account.accountId, + }), + ); + return { + isEnabled: (key: keyof DiscordActionConfig, defaultValue = true) => + unionGate(key, defaultValue), + }; +} + +function describeDiscordMessageTool({ + cfg, +}: Parameters< + NonNullable +>[0]): ChannelMessageToolDiscovery { + const discovery = resolveDiscordActionDiscovery(cfg); + if (!discovery) { + return { + actions: [], + capabilities: [], + schema: null, + }; + } + const actions = new Set(["send"]); + if (discovery.isEnabled("polls")) { + actions.add("poll"); + } + if (discovery.isEnabled("reactions")) { + actions.add("react"); + actions.add("reactions"); + actions.add("emoji-list"); + } + if (discovery.isEnabled("messages")) { + actions.add("read"); + actions.add("edit"); + actions.add("delete"); + } + if (discovery.isEnabled("pins")) { + actions.add("pin"); + actions.add("unpin"); + actions.add("list-pins"); + } + if (discovery.isEnabled("permissions")) { + actions.add("permissions"); + } + if (discovery.isEnabled("threads")) { + actions.add("thread-create"); + actions.add("thread-list"); + actions.add("thread-reply"); + } + if (discovery.isEnabled("search")) { + actions.add("search"); + } + if (discovery.isEnabled("stickers")) { + actions.add("sticker"); + } + if (discovery.isEnabled("memberInfo")) { + actions.add("member-info"); + } + if (discovery.isEnabled("roleInfo")) { + actions.add("role-info"); + } + if (discovery.isEnabled("emojiUploads")) { + actions.add("emoji-upload"); + } + if (discovery.isEnabled("stickerUploads")) { + actions.add("sticker-upload"); + } + if (discovery.isEnabled("roles", false)) { + actions.add("role-add"); + actions.add("role-remove"); + } + if (discovery.isEnabled("channelInfo")) { + actions.add("channel-info"); + actions.add("channel-list"); + } + if (discovery.isEnabled("channels")) { + actions.add("channel-create"); + actions.add("channel-edit"); + actions.add("channel-delete"); + actions.add("channel-move"); + actions.add("category-create"); + actions.add("category-edit"); + actions.add("category-delete"); + } + if (discovery.isEnabled("voiceStatus")) { + actions.add("voice-status"); + } + if (discovery.isEnabled("events")) { + actions.add("event-list"); + actions.add("event-create"); + } + if (discovery.isEnabled("moderation", false)) { + actions.add("timeout"); + actions.add("kick"); + actions.add("ban"); + } + if (discovery.isEnabled("presence", false)) { + actions.add("set-presence"); + } + return { + actions: Array.from(actions), + capabilities: ["interactive", "components"], + schema: { + properties: { + components: createDiscordMessageToolComponentsSchema(), + }, + }, + }; +} + export const discordMessageActions: ChannelMessageActionAdapter = { - listActions: ({ cfg }) => { - const accounts = listTokenSourcedAccounts(listEnabledDiscordAccounts(cfg)); - if (accounts.length === 0) { - return []; - } - // Union of all accounts' action gates (any account enabling an action makes it available) - const gate = createUnionActionGate(accounts, (account) => - createDiscordActionGate({ - cfg, - accountId: account.accountId, - }), - ); - const isEnabled = (key: keyof DiscordActionConfig, defaultValue = true) => - gate(key, defaultValue); - const actions = new Set(["send"]); - if (isEnabled("polls")) { - actions.add("poll"); - } - if (isEnabled("reactions")) { - actions.add("react"); - actions.add("reactions"); - } - if (isEnabled("messages")) { - actions.add("read"); - actions.add("edit"); - actions.add("delete"); - } - if (isEnabled("pins")) { - actions.add("pin"); - actions.add("unpin"); - actions.add("list-pins"); - } - if (isEnabled("permissions")) { - actions.add("permissions"); - } - if (isEnabled("threads")) { - actions.add("thread-create"); - actions.add("thread-list"); - actions.add("thread-reply"); - } - if (isEnabled("search")) { - actions.add("search"); - } - if (isEnabled("stickers")) { - actions.add("sticker"); - } - if (isEnabled("memberInfo")) { - actions.add("member-info"); - } - if (isEnabled("roleInfo")) { - actions.add("role-info"); - } - if (isEnabled("reactions")) { - actions.add("emoji-list"); - } - if (isEnabled("emojiUploads")) { - actions.add("emoji-upload"); - } - if (isEnabled("stickerUploads")) { - actions.add("sticker-upload"); - } - if (isEnabled("roles", false)) { - actions.add("role-add"); - actions.add("role-remove"); - } - if (isEnabled("channelInfo")) { - actions.add("channel-info"); - actions.add("channel-list"); - } - if (isEnabled("channels")) { - actions.add("channel-create"); - actions.add("channel-edit"); - actions.add("channel-delete"); - actions.add("channel-move"); - actions.add("category-create"); - actions.add("category-edit"); - actions.add("category-delete"); - } - if (isEnabled("voiceStatus")) { - actions.add("voice-status"); - } - if (isEnabled("events")) { - actions.add("event-list"); - actions.add("event-create"); - } - if (isEnabled("moderation", false)) { - actions.add("timeout"); - actions.add("kick"); - actions.add("ban"); - } - if (isEnabled("presence", false)) { - actions.add("set-presence"); - } - return Array.from(actions); - }, - getCapabilities: ({ cfg }) => - listTokenSourcedAccounts(listEnabledDiscordAccounts(cfg)).length > 0 - ? (["interactive", "components"] as const) - : [], + describeMessageTool: describeDiscordMessageTool, extractToolSend: ({ args }) => { const action = typeof args.action === "string" ? args.action.trim() : ""; if (action === "sendMessage") { diff --git a/extensions/discord/src/channel.setup.ts b/extensions/discord/src/channel.setup.ts index c45ed85fb0b..6bc017a9667 100644 --- a/extensions/discord/src/channel.setup.ts +++ b/extensions/discord/src/channel.setup.ts @@ -1,5 +1,5 @@ -import { type ChannelPlugin } from "openclaw/plugin-sdk/discord"; import { type ResolvedDiscordAccount } from "./accounts.js"; +import { type ChannelPlugin } from "./runtime-api.js"; import { discordSetupAdapter } from "./setup-core.js"; import { createDiscordPluginBase } from "./shared.js"; diff --git a/extensions/discord/src/channel.test.ts b/extensions/discord/src/channel.test.ts index 0a4ead6c3fd..b5f2224b1dd 100644 --- a/extensions/discord/src/channel.test.ts +++ b/extensions/discord/src/channel.test.ts @@ -1,8 +1,87 @@ -import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk/discord"; -import { describe, expect, it, vi } from "vitest"; +import type { + ChannelAccountSnapshot, + ChannelGatewayContext, + OpenClawConfig, + PluginRuntime, +} from "openclaw/plugin-sdk/discord"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { createRuntimeEnv } from "../../../test/helpers/extensions/runtime-env.js"; +import type { ResolvedDiscordAccount } from "./accounts.js"; import { discordPlugin } from "./channel.js"; import { setDiscordRuntime } from "./runtime.js"; +const probeDiscordMock = vi.hoisted(() => vi.fn()); +const monitorDiscordProviderMock = vi.hoisted(() => vi.fn()); +const auditDiscordChannelPermissionsMock = vi.hoisted(() => vi.fn()); + +vi.mock("./probe.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + probeDiscord: probeDiscordMock, + }; +}); + +vi.mock("./monitor.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + monitorDiscordProvider: monitorDiscordProviderMock, + }; +}); + +vi.mock("./audit.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + auditDiscordChannelPermissions: auditDiscordChannelPermissionsMock, + }; +}); + +function createCfg(): OpenClawConfig { + return { + channels: { + discord: { + enabled: true, + token: "discord-token", + }, + }, + } as OpenClawConfig; +} + +function createStartAccountCtx(params: { + cfg: OpenClawConfig; + accountId: string; + runtime: ReturnType; +}): ChannelGatewayContext { + const account = discordPlugin.config.resolveAccount( + params.cfg, + params.accountId, + ) as ResolvedDiscordAccount; + const snapshot: ChannelAccountSnapshot = { + accountId: params.accountId, + configured: true, + enabled: true, + running: false, + }; + return { + accountId: params.accountId, + account, + cfg: params.cfg, + runtime: params.runtime, + abortSignal: new AbortController().signal, + log: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() }, + getStatus: () => snapshot, + setStatus: vi.fn(), + }; +} + +afterEach(() => { + probeDiscordMock.mockReset(); + monitorDiscordProviderMock.mockReset(); + auditDiscordChannelPermissionsMock.mockReset(); +}); + describe("discordPlugin outbound", () => { it("forwards mediaLocalRoots to sendMessageDiscord", async () => { const sendMessageDiscord = vi.fn(async () => ({ messageId: "m1" })); @@ -33,4 +112,139 @@ describe("discordPlugin outbound", () => { ); expect(result).toMatchObject({ channel: "discord", messageId: "m1" }); }); + + it("uses direct Discord probe helpers for status probes", async () => { + const runtimeProbeDiscord = vi.fn(async () => { + throw new Error("runtime Discord probe should not be used"); + }); + setDiscordRuntime({ + channel: { + discord: { + probeDiscord: runtimeProbeDiscord, + }, + }, + logging: { + shouldLogVerbose: () => false, + }, + } as unknown as PluginRuntime); + probeDiscordMock.mockResolvedValue({ + ok: true, + bot: { username: "Bob" }, + application: { + intents: { + messageContent: "limited", + guildMembers: "disabled", + presence: "disabled", + }, + }, + elapsedMs: 1, + }); + + const cfg = createCfg(); + const account = discordPlugin.config.resolveAccount(cfg, "default"); + + await discordPlugin.status!.probeAccount!({ + account, + timeoutMs: 5000, + cfg, + }); + + expect(probeDiscordMock).toHaveBeenCalledWith("discord-token", 5000, { + includeApplication: true, + }); + expect(runtimeProbeDiscord).not.toHaveBeenCalled(); + }); + + it("uses direct Discord startup helpers before monitoring", async () => { + const runtimeProbeDiscord = vi.fn(async () => { + throw new Error("runtime Discord probe should not be used"); + }); + const runtimeMonitorDiscordProvider = vi.fn(async () => { + throw new Error("runtime Discord monitor should not be used"); + }); + setDiscordRuntime({ + channel: { + discord: { + probeDiscord: runtimeProbeDiscord, + monitorDiscordProvider: runtimeMonitorDiscordProvider, + }, + }, + logging: { + shouldLogVerbose: () => false, + }, + } as unknown as PluginRuntime); + probeDiscordMock.mockResolvedValue({ + ok: true, + bot: { username: "Bob" }, + application: { + intents: { + messageContent: "limited", + guildMembers: "disabled", + presence: "disabled", + }, + }, + elapsedMs: 1, + }); + monitorDiscordProviderMock.mockResolvedValue(undefined); + + const cfg = createCfg(); + await discordPlugin.gateway!.startAccount!( + createStartAccountCtx({ + cfg, + accountId: "default", + runtime: createRuntimeEnv(), + }), + ); + + expect(probeDiscordMock).toHaveBeenCalledWith("discord-token", 2500, { + includeApplication: true, + }); + expect(monitorDiscordProviderMock).toHaveBeenCalledWith( + expect.objectContaining({ + token: "discord-token", + accountId: "default", + }), + ); + expect(runtimeProbeDiscord).not.toHaveBeenCalled(); + expect(runtimeMonitorDiscordProvider).not.toHaveBeenCalled(); + }); +}); + +describe("discordPlugin groups", () => { + it("uses plugin-owned group policy resolvers", () => { + const cfg = { + channels: { + discord: { + token: "discord-test", + guilds: { + guild1: { + requireMention: false, + tools: { allow: ["message.guild"] }, + channels: { + "123": { + requireMention: true, + tools: { allow: ["message.channel"] }, + }, + }, + }, + }, + }, + }, + } as OpenClawConfig; + + expect( + discordPlugin.groups?.resolveRequireMention?.({ + cfg, + groupSpace: "guild1", + groupId: "123", + }), + ).toBe(true); + expect( + discordPlugin.groups?.resolveToolPolicy?.({ + cfg, + groupSpace: "guild1", + groupId: "123", + }), + ).toEqual({ allow: ["message.channel"] }); + }); }); diff --git a/extensions/discord/src/channel.ts b/extensions/discord/src/channel.ts index dff011825b0..1224fc7b37a 100644 --- a/extensions/discord/src/channel.ts +++ b/extensions/discord/src/channel.ts @@ -3,52 +3,57 @@ import { buildAccountScopedAllowlistConfigEditor, resolveLegacyDmAllowlistConfigPaths, } from "openclaw/plugin-sdk/allowlist-config-edit"; +import { createScopedDmSecurityResolver } from "openclaw/plugin-sdk/channel-config-helpers"; import { - buildAccountScopedDmSecurityPolicy, collectOpenGroupPolicyConfiguredRouteWarnings, collectOpenProviderGroupPolicyWarnings, } from "openclaw/plugin-sdk/channel-config-helpers"; import { resolveOutboundSendDep } from "openclaw/plugin-sdk/channel-runtime"; import { normalizeMessageChannel } from "openclaw/plugin-sdk/channel-runtime"; import { buildOutboundBaseSessionKey, normalizeOutboundThreadId } from "openclaw/plugin-sdk/core"; -import { - buildComputedAccountStatusSnapshot, - buildTokenChannelStatusSummary, - DEFAULT_ACCOUNT_ID, - getChatChannelMeta, - listDiscordDirectoryGroupsFromConfig, - listDiscordDirectoryPeersFromConfig, - PAIRING_APPROVED_MESSAGE, - projectCredentialSnapshotFields, - resolveConfiguredFromCredentialStatuses, - resolveDiscordGroupRequireMention, - resolveDiscordGroupToolPolicy, - type ChannelMessageActionAdapter, - type ChannelPlugin, - type OpenClawConfig, -} from "openclaw/plugin-sdk/discord"; import { resolveThreadSessionKeys, type RoutePeer } from "openclaw/plugin-sdk/routing"; import { listDiscordAccountIds, resolveDiscordAccount, type ResolvedDiscordAccount, } from "./accounts.js"; -import { collectDiscordAuditChannelIds } from "./audit.js"; +import { auditDiscordChannelPermissions, collectDiscordAuditChannelIds } from "./audit.js"; +import { + listDiscordDirectoryGroupsFromConfig, + listDiscordDirectoryPeersFromConfig, +} from "./directory-config.js"; import { isDiscordExecApprovalClientEnabled, shouldSuppressLocalDiscordExecApprovalPrompt, } from "./exec-approvals.js"; +import { + resolveDiscordGroupRequireMention, + resolveDiscordGroupToolPolicy, +} from "./group-policy.js"; +import { monitorDiscordProvider } from "./monitor.js"; import { looksLikeDiscordTargetId, normalizeDiscordMessagingTarget, normalizeDiscordOutboundTarget, } from "./normalize.js"; -import type { DiscordProbe } from "./probe.js"; +import { probeDiscord, type DiscordProbe } from "./probe.js"; import { resolveDiscordUserAllowlist } from "./resolve-users.js"; +import { + buildComputedAccountStatusSnapshot, + buildTokenChannelStatusSummary, + type ChannelMessageActionAdapter, + type ChannelPlugin, + DEFAULT_ACCOUNT_ID, + getChatChannelMeta, + PAIRING_APPROVED_MESSAGE, + projectCredentialSnapshotFields, + resolveConfiguredFromCredentialStatuses, + type OpenClawConfig, +} from "./runtime-api.js"; import { getDiscordRuntime } from "./runtime.js"; import { fetchChannelPermissionsDiscord } from "./send.js"; import { discordSetupAdapter } from "./setup-core.js"; -import { createDiscordPluginBase, discordConfigAccessors } from "./shared.js"; +import { createDiscordPluginBase, discordConfigAdapter } from "./shared.js"; import { collectDiscordStatusIssues } from "./status-issues.js"; import { parseDiscordTarget } from "./targets.js"; import { DiscordUiContainer } from "./ui.js"; @@ -60,6 +65,14 @@ type DiscordSendFn = ReturnType< const meta = getChatChannelMeta("discord"); const REQUIRED_DISCORD_PERMISSIONS = ["ViewChannel", "SendMessages"] as const; +const resolveDiscordDmPolicy = createScopedDmSecurityResolver({ + channelKey: "discord", + resolvePolicy: (account) => account.config.dm?.policy, + resolveAllowFrom: (account) => account.config.dm?.allowFrom, + allowFromPathSuffix: "dm.", + normalizeEntry: (raw) => raw.replace(/^(discord|user):/i, "").replace(/^<@!?(\d+)>$/, "$1"), +}); + function formatDiscordIntents(intents?: { messageContent?: string; guildMembers?: string; @@ -76,10 +89,8 @@ function formatDiscordIntents(intents?: { } const discordMessageActions: ChannelMessageActionAdapter = { - listActions: (ctx) => - getDiscordRuntime().channel.discord.messageActions?.listActions?.(ctx) ?? [], - getCapabilities: (ctx) => - getDiscordRuntime().channel.discord.messageActions?.getCapabilities?.(ctx) ?? [], + describeMessageTool: (ctx) => + getDiscordRuntime().channel.discord.messageActions?.describeMessageTool?.(ctx) ?? null, extractToolSend: (ctx) => getDiscordRuntime().channel.discord.messageActions?.extractToolSend?.(ctx) ?? null, handleAction: async (ctx) => { @@ -296,23 +307,12 @@ export const discordPlugin: ChannelPlugin = { applyConfigEdit: buildAccountScopedAllowlistConfigEditor({ channelId: "discord", normalize: ({ cfg, accountId, values }) => - discordConfigAccessors.formatAllowFrom!({ cfg, accountId, allowFrom: values }), + discordConfigAdapter.formatAllowFrom!({ cfg, accountId, allowFrom: values }), resolvePaths: resolveLegacyDmAllowlistConfigPaths, }), }, security: { - resolveDmPolicy: ({ cfg, accountId, account }) => { - return buildAccountScopedDmSecurityPolicy({ - cfg, - channelKey: "discord", - accountId, - fallbackAccountId: account.accountId ?? DEFAULT_ACCOUNT_ID, - policy: account.config.dm?.policy, - allowFrom: account.config.dm?.allowFrom ?? [], - allowFromPathSuffix: "dm.", - normalizeEntry: (raw) => raw.replace(/^(discord|user):/i, "").replace(/^<@!?(\d+)>$/, "$1"), - }); - }, + resolveDmPolicy: resolveDiscordDmPolicy, collectWarnings: ({ account, cfg }) => { const guildEntries = account.config.guilds ?? {}; const guildsConfigured = Object.keys(guildEntries).length > 0; @@ -361,6 +361,7 @@ export const discordPlugin: ChannelPlugin = { }, messaging: { normalizeTarget: normalizeDiscordMessagingTarget, + resolveSessionTarget: ({ id }) => normalizeDiscordMessagingTarget(`channel:${id}`), parseExplicitTarget: ({ raw }) => parseDiscordExplicitTarget(raw), inferTargetChatType: ({ to }) => parseDiscordExplicitTarget(to)?.chatType, buildCrossContextComponents: buildDiscordCrossContextComponents, @@ -488,11 +489,15 @@ export const discordPlugin: ChannelPlugin = { silent: silent ?? undefined, }), }, - acpBindings: { - normalizeConfiguredBindingTarget: ({ conversationId }) => + bindings: { + compileConfiguredBinding: ({ conversationId }) => normalizeDiscordAcpConversationId(conversationId), - matchConfiguredBinding: ({ bindingConversationId, conversationId, parentConversationId }) => - matchDiscordAcpConversation({ bindingConversationId, conversationId, parentConversationId }), + matchInboundConversation: ({ compiledBinding, conversationId, parentConversationId }) => + matchDiscordAcpConversation({ + bindingConversationId: compiledBinding.conversationId, + conversationId, + parentConversationId, + }), }, status: { defaultRuntime: { @@ -511,7 +516,7 @@ export const discordPlugin: ChannelPlugin = { buildChannelSummary: ({ snapshot }) => buildTokenChannelStatusSummary(snapshot, { includeMode: false }), probeAccount: async ({ account, timeoutMs }) => - getDiscordRuntime().channel.discord.probeDiscord(account.token, timeoutMs, { + probeDiscord(account.token, timeoutMs, { includeApplication: true, }), formatCapabilitiesProbe: ({ probe }) => { @@ -617,7 +622,7 @@ export const discordPlugin: ChannelPlugin = { elapsedMs: 0, }; } - const audit = await getDiscordRuntime().channel.discord.auditChannelPermissions({ + const audit = await auditDiscordChannelPermissions({ token: botToken, accountId: account.accountId, channelIds, @@ -658,7 +663,7 @@ export const discordPlugin: ChannelPlugin = { const token = account.token.trim(); let discordBotLabel = ""; try { - const probe = await getDiscordRuntime().channel.discord.probeDiscord(token, 2500, { + const probe = await probeDiscord(token, 2500, { includeApplication: true, }); const username = probe.ok ? probe.bot?.username?.trim() : null; @@ -686,7 +691,7 @@ export const discordPlugin: ChannelPlugin = { } } ctx.log?.info(`[${account.accountId}] starting provider${discordBotLabel}`); - return getDiscordRuntime().channel.discord.monitorDiscordProvider({ + return monitorDiscordProvider({ token, accountId: account.accountId, config: ctx.cfg, diff --git a/extensions/discord/src/directory-config.ts b/extensions/discord/src/directory-config.ts new file mode 100644 index 00000000000..af921c25165 --- /dev/null +++ b/extensions/discord/src/directory-config.ts @@ -0,0 +1,57 @@ +import { + applyDirectoryQueryAndLimit, + collectNormalizedDirectoryIds, + inspectReadOnlyChannelAccount, + toDirectoryEntries, + type DirectoryConfigParams, +} from "openclaw/plugin-sdk/directory-runtime"; +import type { InspectedDiscordAccount } from "../api.js"; + +export async function listDiscordDirectoryPeersFromConfig(params: DirectoryConfigParams) { + const account = (await inspectReadOnlyChannelAccount({ + channelId: "discord", + cfg: params.cfg, + accountId: params.accountId, + })) as InspectedDiscordAccount | null; + if (!account || !("config" in account)) { + return []; + } + + const allowFrom = account.config.allowFrom ?? account.config.dm?.allowFrom ?? []; + const guildUsers = Object.values(account.config.guilds ?? {}).flatMap((guild) => [ + ...(guild.users ?? []), + ...Object.values(guild.channels ?? {}).flatMap((channel) => channel.users ?? []), + ]); + const ids = collectNormalizedDirectoryIds({ + sources: [allowFrom, Object.keys(account.config.dms ?? {}), guildUsers], + normalizeId: (raw) => { + const mention = raw.match(/^<@!?(\d+)>$/); + const cleaned = (mention?.[1] ?? raw).replace(/^(discord|user):/i, "").trim(); + return /^\d+$/.test(cleaned) ? `user:${cleaned}` : null; + }, + }); + return toDirectoryEntries("user", applyDirectoryQueryAndLimit(ids, params)); +} + +export async function listDiscordDirectoryGroupsFromConfig(params: DirectoryConfigParams) { + const account = (await inspectReadOnlyChannelAccount({ + channelId: "discord", + cfg: params.cfg, + accountId: params.accountId, + })) as InspectedDiscordAccount | null; + if (!account || !("config" in account)) { + return []; + } + + const ids = collectNormalizedDirectoryIds({ + sources: Object.values(account.config.guilds ?? {}).map((guild) => + Object.keys(guild.channels ?? {}), + ), + normalizeId: (raw) => { + const mention = raw.match(/^<#(\d+)>$/); + const cleaned = (mention?.[1] ?? raw).replace(/^(discord|channel|group):/i, "").trim(); + return /^\d+$/.test(cleaned) ? `channel:${cleaned}` : null; + }, + }); + return toDirectoryEntries("group", applyDirectoryQueryAndLimit(ids, params)); +} diff --git a/extensions/discord/src/directory-live.test.ts b/extensions/discord/src/directory-live.test.ts index afc0fd94170..36f1f821795 100644 --- a/extensions/discord/src/directory-live.test.ts +++ b/extensions/discord/src/directory-live.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import type { DirectoryConfigParams } from "../../../src/channels/plugins/directory-config.js"; import type { OpenClawConfig } from "../../../src/config/config.js"; +import type { DirectoryConfigParams } from "../../../src/plugin-sdk/directory-runtime.js"; import { listDiscordDirectoryGroupsLive, listDiscordDirectoryPeersLive } from "./directory-live.js"; function makeParams(overrides: Partial = {}): DirectoryConfigParams { diff --git a/extensions/discord/src/group-policy.test.ts b/extensions/discord/src/group-policy.test.ts new file mode 100644 index 00000000000..249df3fa8a7 --- /dev/null +++ b/extensions/discord/src/group-policy.test.ts @@ -0,0 +1,79 @@ +import { describe, expect, it } from "vitest"; +import { + resolveDiscordGroupRequireMention, + resolveDiscordGroupToolPolicy, +} from "./group-policy.js"; + +describe("discord group policy", () => { + it("prefers channel policy, then guild policy, with sender-specific overrides", () => { + const discordCfg = { + channels: { + discord: { + token: "discord-test", + guilds: { + guild1: { + requireMention: false, + tools: { allow: ["message.guild"] }, + toolsBySender: { + "id:user:guild-admin": { allow: ["sessions.list"] }, + }, + channels: { + "123": { + requireMention: true, + tools: { allow: ["message.channel"] }, + toolsBySender: { + "id:user:channel-admin": { deny: ["exec"] }, + }, + }, + }, + }, + }, + }, + }, + // oxlint-disable-next-line typescript/no-explicit-any + } as any; + + expect( + resolveDiscordGroupRequireMention({ cfg: discordCfg, groupSpace: "guild1", groupId: "123" }), + ).toBe(true); + expect( + resolveDiscordGroupRequireMention({ + cfg: discordCfg, + groupSpace: "guild1", + groupId: "missing", + }), + ).toBe(false); + expect( + resolveDiscordGroupToolPolicy({ + cfg: discordCfg, + groupSpace: "guild1", + groupId: "123", + senderId: "user:channel-admin", + }), + ).toEqual({ deny: ["exec"] }); + expect( + resolveDiscordGroupToolPolicy({ + cfg: discordCfg, + groupSpace: "guild1", + groupId: "123", + senderId: "user:someone", + }), + ).toEqual({ allow: ["message.channel"] }); + expect( + resolveDiscordGroupToolPolicy({ + cfg: discordCfg, + groupSpace: "guild1", + groupId: "missing", + senderId: "user:guild-admin", + }), + ).toEqual({ allow: ["sessions.list"] }); + expect( + resolveDiscordGroupToolPolicy({ + cfg: discordCfg, + groupSpace: "guild1", + groupId: "missing", + senderId: "user:someone", + }), + ).toEqual({ allow: ["message.guild"] }); + }); +}); diff --git a/extensions/discord/src/group-policy.ts b/extensions/discord/src/group-policy.ts new file mode 100644 index 00000000000..a5a8ebac5eb --- /dev/null +++ b/extensions/discord/src/group-policy.ts @@ -0,0 +1,111 @@ +import { + resolveToolsBySender, + type GroupToolPolicyBySenderConfig, + type GroupToolPolicyConfig, +} from "openclaw/plugin-sdk/channel-policy"; +import { type ChannelGroupContext } from "openclaw/plugin-sdk/channel-runtime"; +import { normalizeAtHashSlug } from "openclaw/plugin-sdk/core"; +import type { DiscordConfig } from "./runtime-api.js"; + +function normalizeDiscordSlug(value?: string | null) { + return normalizeAtHashSlug(value); +} + +type SenderScopedToolsEntry = { + tools?: GroupToolPolicyConfig; + toolsBySender?: GroupToolPolicyBySenderConfig; + requireMention?: boolean; +}; + +function resolveDiscordGuildEntry(guilds: DiscordConfig["guilds"], groupSpace?: string | null) { + if (!guilds || Object.keys(guilds).length === 0) { + return null; + } + const space = groupSpace?.trim() ?? ""; + if (space && guilds[space]) { + return guilds[space]; + } + const normalized = normalizeDiscordSlug(space); + if (normalized && guilds[normalized]) { + return guilds[normalized]; + } + if (normalized) { + const match = Object.values(guilds).find( + (entry) => normalizeDiscordSlug(entry?.slug ?? undefined) === normalized, + ); + if (match) { + return match; + } + } + return guilds["*"] ?? null; +} + +function resolveDiscordChannelEntry( + channelEntries: Record | undefined, + params: { groupId?: string | null; groupChannel?: string | null }, +): TEntry | undefined { + if (!channelEntries || Object.keys(channelEntries).length === 0) { + return undefined; + } + const groupChannel = params.groupChannel; + const channelSlug = normalizeDiscordSlug(groupChannel); + return ( + (params.groupId ? channelEntries[params.groupId] : undefined) ?? + (channelSlug + ? (channelEntries[channelSlug] ?? channelEntries[`#${channelSlug}`]) + : undefined) ?? + (groupChannel ? channelEntries[normalizeDiscordSlug(groupChannel)] : undefined) + ); +} + +function resolveSenderToolsEntry( + entry: SenderScopedToolsEntry | undefined | null, + params: ChannelGroupContext, +): GroupToolPolicyConfig | undefined { + if (!entry) { + return undefined; + } + const senderPolicy = resolveToolsBySender({ + toolsBySender: entry.toolsBySender, + senderId: params.senderId, + senderName: params.senderName, + senderUsername: params.senderUsername, + senderE164: params.senderE164, + }); + return senderPolicy ?? entry.tools; +} + +function resolveDiscordPolicyContext(params: ChannelGroupContext) { + const guildEntry = resolveDiscordGuildEntry( + params.cfg.channels?.discord?.guilds, + params.groupSpace, + ); + const channelEntries = guildEntry?.channels; + const channelEntry = + channelEntries && Object.keys(channelEntries).length > 0 + ? resolveDiscordChannelEntry(channelEntries, params) + : undefined; + return { guildEntry, channelEntry }; +} + +export function resolveDiscordGroupRequireMention(params: ChannelGroupContext): boolean { + const context = resolveDiscordPolicyContext(params); + if (typeof context.channelEntry?.requireMention === "boolean") { + return context.channelEntry.requireMention; + } + if (typeof context.guildEntry?.requireMention === "boolean") { + return context.guildEntry.requireMention; + } + return true; +} + +export function resolveDiscordGroupToolPolicy( + params: ChannelGroupContext, +): GroupToolPolicyConfig | undefined { + const context = resolveDiscordPolicyContext(params); + const channelPolicy = resolveSenderToolsEntry(context.channelEntry, params); + if (channelPolicy) { + return channelPolicy; + } + return resolveSenderToolsEntry(context.guildEntry, params); +} diff --git a/extensions/discord/src/monitor/agent-components-helpers.ts b/extensions/discord/src/monitor/agent-components-helpers.ts new file mode 100644 index 00000000000..d3173e384a6 --- /dev/null +++ b/extensions/discord/src/monitor/agent-components-helpers.ts @@ -0,0 +1,754 @@ +import { + type ButtonInteraction, + type ChannelSelectMenuInteraction, + type ComponentData, + type MentionableSelectMenuInteraction, + type ModalInteraction, + type RoleSelectMenuInteraction, + type StringSelectMenuInteraction, + type UserSelectMenuInteraction, +} from "@buape/carbon"; +import type { APIStringSelectComponent } from "discord-api-types/v10"; +import { ChannelType } from "discord-api-types/v10"; +import { resolveCommandAuthorizedFromAuthorizers } from "openclaw/plugin-sdk/channel-runtime"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; +import type { DiscordAccountConfig } from "openclaw/plugin-sdk/config-runtime"; +import { isDangerousNameMatchingEnabled } from "openclaw/plugin-sdk/config-runtime"; +import { + issuePairingChallenge, + upsertChannelPairingRequest, +} from "openclaw/plugin-sdk/conversation-runtime"; +import { resolveAgentRoute } from "openclaw/plugin-sdk/routing"; +import { logVerbose } from "openclaw/plugin-sdk/runtime-env"; +import { + readStoreAllowFromForDmPolicy, + resolvePinnedMainDmOwnerFromAllowlist, +} from "openclaw/plugin-sdk/security-runtime"; +import { logError } from "openclaw/plugin-sdk/text-runtime"; +import { + createDiscordFormModal, + parseDiscordComponentCustomId, + parseDiscordModalCustomId, + type DiscordComponentEntry, + type DiscordModalEntry, +} from "../components.js"; +import { + type DiscordGuildEntryResolved, + normalizeDiscordAllowList, + normalizeDiscordSlug, + resolveDiscordAllowListMatch, + resolveDiscordChannelConfigWithFallback, + resolveDiscordGuildEntry, + resolveDiscordMemberAccessState, + resolveDiscordOwnerAccess, +} from "./allow-list.js"; +import { formatDiscordUserTag } from "./format.js"; + +export const AGENT_BUTTON_KEY = "agent"; +export const AGENT_SELECT_KEY = "agentsel"; + +export type DiscordUser = Parameters[0]; + +export type AgentComponentMessageInteraction = + | ButtonInteraction + | StringSelectMenuInteraction + | RoleSelectMenuInteraction + | UserSelectMenuInteraction + | MentionableSelectMenuInteraction + | ChannelSelectMenuInteraction; + +export type AgentComponentInteraction = AgentComponentMessageInteraction | ModalInteraction; + +export type DiscordChannelContext = { + channelName: string | undefined; + channelSlug: string; + channelType: number | undefined; + isThread: boolean; + parentId: string | undefined; + parentName: string | undefined; + parentSlug: string; +}; + +export type AgentComponentContext = { + cfg: OpenClawConfig; + accountId: string; + discordConfig?: DiscordAccountConfig; + runtime?: import("openclaw/plugin-sdk/runtime-env").RuntimeEnv; + token?: string; + guildEntries?: Record; + allowFrom?: string[]; + dmPolicy?: "open" | "pairing" | "allowlist" | "disabled"; +}; + +export type ComponentInteractionContext = NonNullable< + Awaited> +>; + +function formatUsername(user: { username: string; discriminator?: string | null }): string { + if (user.discriminator && user.discriminator !== "0") { + return `${user.username}#${user.discriminator}`; + } + return user.username; +} + +function isThreadChannelType(channelType: number | undefined): boolean { + return ( + channelType === ChannelType.PublicThread || + channelType === ChannelType.PrivateThread || + channelType === ChannelType.AnnouncementThread + ); +} + +function readParsedComponentId(data: ComponentData): unknown { + if (!data || typeof data !== "object") { + return undefined; + } + return "cid" in data + ? (data as Record).cid + : (data as Record).componentId; +} + +function normalizeComponentId(value: unknown): string | undefined { + if (typeof value === "string") { + const trimmed = value.trim(); + return trimmed ? trimmed : undefined; + } + if (typeof value === "number" && Number.isFinite(value)) { + return String(value); + } + return undefined; +} + +function mapOptionLabels( + options: Array<{ value: string; label: string }> | undefined, + values: string[], +) { + if (!options || options.length === 0) { + return values; + } + const map = new Map(options.map((option) => [option.value, option.label])); + return values.map((value) => map.get(value) ?? value); +} + +/** + * The component custom id only carries the logical button id. Channel binding + * comes from Discord's trusted interaction payload. + */ +export function buildAgentButtonCustomId(componentId: string): string { + return `${AGENT_BUTTON_KEY}:componentId=${encodeURIComponent(componentId)}`; +} + +export function buildAgentSelectCustomId(componentId: string): string { + return `${AGENT_SELECT_KEY}:componentId=${encodeURIComponent(componentId)}`; +} + +export function resolveAgentComponentRoute(params: { + ctx: AgentComponentContext; + rawGuildId: string | undefined; + memberRoleIds: string[]; + isDirectMessage: boolean; + userId: string; + channelId: string; + parentId: string | undefined; +}) { + return resolveAgentRoute({ + cfg: params.ctx.cfg, + channel: "discord", + accountId: params.ctx.accountId, + guildId: params.rawGuildId, + memberRoleIds: params.memberRoleIds, + peer: { + kind: params.isDirectMessage ? "direct" : "channel", + id: params.isDirectMessage ? params.userId : params.channelId, + }, + parentPeer: params.parentId ? { kind: "channel", id: params.parentId } : undefined, + }); +} + +export async function ackComponentInteraction(params: { + interaction: AgentComponentInteraction; + replyOpts: { ephemeral?: boolean }; + label: string; +}) { + try { + await params.interaction.reply({ + content: "✓", + ...params.replyOpts, + }); + } catch (err) { + logError(`${params.label}: failed to acknowledge interaction: ${String(err)}`); + } +} + +export function resolveDiscordChannelContext( + interaction: AgentComponentInteraction, +): DiscordChannelContext { + const channel = interaction.channel; + const channelName = channel && "name" in channel ? (channel.name as string) : undefined; + const channelSlug = channelName ? normalizeDiscordSlug(channelName) : ""; + const channelType = channel && "type" in channel ? (channel.type as number) : undefined; + const isThread = isThreadChannelType(channelType); + + let parentId: string | undefined; + let parentName: string | undefined; + let parentSlug = ""; + if (isThread && channel && "parentId" in channel) { + parentId = (channel.parentId as string) ?? undefined; + if ("parent" in channel) { + const parent = (channel as { parent?: { name?: string } }).parent; + if (parent?.name) { + parentName = parent.name; + parentSlug = normalizeDiscordSlug(parentName); + } + } + } + + return { channelName, channelSlug, channelType, isThread, parentId, parentName, parentSlug }; +} + +export async function resolveComponentInteractionContext(params: { + interaction: AgentComponentInteraction; + label: string; + defer?: boolean; +}) { + const { interaction, label } = params; + const channelId = interaction.rawData.channel_id; + if (!channelId) { + logError(`${label}: missing channel_id in interaction`); + return null; + } + + const user = interaction.user; + if (!user) { + logError(`${label}: missing user in interaction`); + return null; + } + + const shouldDefer = params.defer !== false && "defer" in interaction; + let didDefer = false; + if (shouldDefer) { + try { + await (interaction as AgentComponentMessageInteraction).defer({ ephemeral: true }); + didDefer = true; + } catch (err) { + logError(`${label}: failed to defer interaction: ${String(err)}`); + } + } + const replyOpts = didDefer ? {} : { ephemeral: true }; + + const username = formatUsername(user); + const userId = user.id; + const rawGuildId = interaction.rawData.guild_id; + const isDirectMessage = !rawGuildId; + const memberRoleIds = Array.isArray(interaction.rawData.member?.roles) + ? interaction.rawData.member.roles.map((roleId: string) => String(roleId)) + : []; + + return { + channelId, + user, + username, + userId, + replyOpts, + rawGuildId, + isDirectMessage, + memberRoleIds, + }; +} + +export async function ensureGuildComponentMemberAllowed(params: { + interaction: AgentComponentInteraction; + guildInfo: ReturnType; + channelId: string; + rawGuildId: string | undefined; + channelCtx: DiscordChannelContext; + memberRoleIds: string[]; + user: DiscordUser; + replyOpts: { ephemeral?: boolean }; + componentLabel: string; + unauthorizedReply: string; + allowNameMatching: boolean; +}) { + const { + interaction, + guildInfo, + channelId, + rawGuildId, + channelCtx, + memberRoleIds, + user, + replyOpts, + componentLabel, + unauthorizedReply, + } = params; + + if (!rawGuildId) { + return true; + } + + const channelConfig = resolveDiscordChannelConfigWithFallback({ + guildInfo, + channelId, + channelName: channelCtx.channelName, + channelSlug: channelCtx.channelSlug, + parentId: channelCtx.parentId, + parentName: channelCtx.parentName, + parentSlug: channelCtx.parentSlug, + scope: channelCtx.isThread ? "thread" : "channel", + }); + + const { memberAllowed } = resolveDiscordMemberAccessState({ + channelConfig, + guildInfo, + memberRoleIds, + sender: { + id: user.id, + name: user.username, + tag: user.discriminator ? `${user.username}#${user.discriminator}` : undefined, + }, + allowNameMatching: params.allowNameMatching, + }); + if (memberAllowed) { + return true; + } + + logVerbose(`agent ${componentLabel}: blocked user ${user.id} (not in users/roles allowlist)`); + try { + await interaction.reply({ + content: unauthorizedReply, + ...replyOpts, + }); + } catch {} + return false; +} + +export async function ensureComponentUserAllowed(params: { + entry: DiscordComponentEntry; + interaction: AgentComponentInteraction; + user: DiscordUser; + replyOpts: { ephemeral?: boolean }; + componentLabel: string; + unauthorizedReply: string; + allowNameMatching: boolean; +}) { + const allowList = normalizeDiscordAllowList(params.entry.allowedUsers, [ + "discord:", + "user:", + "pk:", + ]); + if (!allowList) { + return true; + } + const match = resolveDiscordAllowListMatch({ + allowList, + candidate: { + id: params.user.id, + name: params.user.username, + tag: formatDiscordUserTag(params.user), + }, + allowNameMatching: params.allowNameMatching, + }); + if (match.allowed) { + return true; + } + + logVerbose( + `discord component ${params.componentLabel}: blocked user ${params.user.id} (not in allowedUsers)`, + ); + try { + await params.interaction.reply({ + content: params.unauthorizedReply, + ...params.replyOpts, + }); + } catch {} + return false; +} + +export async function ensureAgentComponentInteractionAllowed(params: { + ctx: AgentComponentContext; + interaction: AgentComponentInteraction; + channelId: string; + rawGuildId: string | undefined; + memberRoleIds: string[]; + user: DiscordUser; + replyOpts: { ephemeral?: boolean }; + componentLabel: string; + unauthorizedReply: string; +}) { + const guildInfo = resolveDiscordGuildEntry({ + guild: params.interaction.guild ?? undefined, + guildId: params.rawGuildId, + guildEntries: params.ctx.guildEntries, + }); + const channelCtx = resolveDiscordChannelContext(params.interaction); + const memberAllowed = await ensureGuildComponentMemberAllowed({ + interaction: params.interaction, + guildInfo, + channelId: params.channelId, + rawGuildId: params.rawGuildId, + channelCtx, + memberRoleIds: params.memberRoleIds, + user: params.user, + replyOpts: params.replyOpts, + componentLabel: params.componentLabel, + unauthorizedReply: params.unauthorizedReply, + allowNameMatching: isDangerousNameMatchingEnabled(params.ctx.discordConfig), + }); + if (!memberAllowed) { + return null; + } + return { parentId: channelCtx.parentId }; +} + +export function parseAgentComponentData(data: ComponentData): { componentId: string } | null { + const raw = readParsedComponentId(data); + const decodeSafe = (value: string): string => { + if (!value.includes("%")) { + return value; + } + if (!/%[0-9A-Fa-f]{2}/.test(value)) { + return value; + } + try { + return decodeURIComponent(value); + } catch { + return value; + } + }; + const componentId = + typeof raw === "string" ? decodeSafe(raw) : typeof raw === "number" ? String(raw) : null; + if (!componentId) { + return null; + } + return { componentId }; +} + +async function ensureDmComponentAuthorized(params: { + ctx: AgentComponentContext; + interaction: AgentComponentInteraction; + user: DiscordUser; + componentLabel: string; + replyOpts: { ephemeral?: boolean }; +}) { + const { ctx, interaction, user, componentLabel, replyOpts } = params; + const dmPolicy = ctx.dmPolicy ?? "pairing"; + if (dmPolicy === "disabled") { + logVerbose(`agent ${componentLabel}: blocked (DM policy disabled)`); + try { + await interaction.reply({ + content: "DM interactions are disabled.", + ...replyOpts, + }); + } catch {} + return false; + } + if (dmPolicy === "open") { + return true; + } + + const storeAllowFrom = await readStoreAllowFromForDmPolicy({ + provider: "discord", + accountId: ctx.accountId, + dmPolicy, + }); + const effectiveAllowFrom = [...(ctx.allowFrom ?? []), ...storeAllowFrom]; + const allowList = normalizeDiscordAllowList(effectiveAllowFrom, ["discord:", "user:", "pk:"]); + const allowMatch = allowList + ? resolveDiscordAllowListMatch({ + allowList, + candidate: { + id: user.id, + name: user.username, + tag: formatDiscordUserTag(user), + }, + allowNameMatching: isDangerousNameMatchingEnabled(ctx.discordConfig), + }) + : { allowed: false }; + if (allowMatch.allowed) { + return true; + } + + if (dmPolicy === "pairing") { + const pairingResult = await issuePairingChallenge({ + channel: "discord", + senderId: user.id, + senderIdLine: `Your Discord user id: ${user.id}`, + meta: { + tag: formatDiscordUserTag(user), + name: user.username, + }, + upsertPairingRequest: async ({ id, meta }) => + await upsertChannelPairingRequest({ + channel: "discord", + id, + accountId: ctx.accountId, + meta, + }), + sendPairingReply: async (text) => { + await interaction.reply({ + content: text, + ...replyOpts, + }); + }, + }); + if (!pairingResult.created) { + try { + await interaction.reply({ + content: "Pairing already requested. Ask the bot owner to approve your code.", + ...replyOpts, + }); + } catch {} + } + return false; + } + + logVerbose(`agent ${componentLabel}: blocked DM user ${user.id} (not in allowFrom)`); + try { + await interaction.reply({ + content: `You are not authorized to use this ${componentLabel}.`, + ...replyOpts, + }); + } catch {} + return false; +} + +export async function resolveInteractionContextWithDmAuth(params: { + ctx: AgentComponentContext; + interaction: AgentComponentInteraction; + label: string; + componentLabel: string; + defer?: boolean; +}) { + const interactionCtx = await resolveComponentInteractionContext({ + interaction: params.interaction, + label: params.label, + defer: params.defer, + }); + if (!interactionCtx) { + return null; + } + if (interactionCtx.isDirectMessage) { + const authorized = await ensureDmComponentAuthorized({ + ctx: params.ctx, + interaction: params.interaction, + user: interactionCtx.user, + componentLabel: params.componentLabel, + replyOpts: interactionCtx.replyOpts, + }); + if (!authorized) { + return null; + } + } + return interactionCtx; +} + +export function parseDiscordComponentData( + data: ComponentData, + customId?: string, +): { componentId: string; modalId?: string } | null { + if (!data || typeof data !== "object") { + return null; + } + const rawComponentId = readParsedComponentId(data); + const rawModalId = + "mid" in data ? (data as { mid?: unknown }).mid : (data as { modalId?: unknown }).modalId; + let componentId = normalizeComponentId(rawComponentId); + let modalId = normalizeComponentId(rawModalId); + if (!componentId && customId) { + const parsed = parseDiscordComponentCustomId(customId); + if (parsed) { + componentId = parsed.componentId; + modalId = parsed.modalId; + } + } + if (!componentId) { + return null; + } + return { componentId, modalId }; +} + +export function parseDiscordModalId(data: ComponentData, customId?: string): string | null { + if (data && typeof data === "object") { + const rawModalId = + "mid" in data ? (data as { mid?: unknown }).mid : (data as { modalId?: unknown }).modalId; + const modalId = normalizeComponentId(rawModalId); + if (modalId) { + return modalId; + } + } + if (customId) { + return parseDiscordModalCustomId(customId); + } + return null; +} + +export function resolveInteractionCustomId( + interaction: AgentComponentInteraction, +): string | undefined { + if (!interaction?.rawData || typeof interaction.rawData !== "object") { + return undefined; + } + if (!("data" in interaction.rawData)) { + return undefined; + } + const data = (interaction.rawData as { data?: { custom_id?: unknown } }).data; + const customId = data?.custom_id; + if (typeof customId !== "string") { + return undefined; + } + const trimmed = customId.trim(); + return trimmed ? trimmed : undefined; +} + +export function mapSelectValues(entry: DiscordComponentEntry, values: string[]): string[] { + if (entry.selectType === "string") { + return mapOptionLabels(entry.options, values); + } + if (entry.selectType === "user") { + return values.map((value) => `user:${value}`); + } + if (entry.selectType === "role") { + return values.map((value) => `role:${value}`); + } + if (entry.selectType === "mentionable") { + return values.map((value) => `mentionable:${value}`); + } + if (entry.selectType === "channel") { + return values.map((value) => `channel:${value}`); + } + return values; +} + +export function resolveModalFieldValues( + field: DiscordModalEntry["fields"][number], + interaction: ModalInteraction, +): string[] { + const fields = interaction.fields; + const optionLabels = field.options?.map((option) => ({ + value: option.value, + label: option.label, + })); + const required = field.required === true; + try { + switch (field.type) { + case "text": { + const value = required ? fields.getText(field.id, true) : fields.getText(field.id); + return value ? [value] : []; + } + case "select": + case "checkbox": + case "radio": { + const values = required + ? fields.getStringSelect(field.id, true) + : (fields.getStringSelect(field.id) ?? []); + return mapOptionLabels(optionLabels, values); + } + case "role-select": { + try { + const roles = required + ? fields.getRoleSelect(field.id, true) + : (fields.getRoleSelect(field.id) ?? []); + return roles.map((role) => role.name ?? role.id); + } catch { + const values = required + ? fields.getStringSelect(field.id, true) + : (fields.getStringSelect(field.id) ?? []); + return values; + } + } + case "user-select": { + const users = required + ? fields.getUserSelect(field.id, true) + : (fields.getUserSelect(field.id) ?? []); + return users.map((user) => formatDiscordUserTag(user)); + } + default: + return []; + } + } catch (err) { + logError(`agent modal: failed to read field ${field.id}: ${String(err)}`); + return []; + } +} + +export function formatModalSubmissionText( + entry: DiscordModalEntry, + interaction: ModalInteraction, +): string { + const lines: string[] = [`Form "${entry.title}" submitted.`]; + for (const field of entry.fields) { + const values = resolveModalFieldValues(field, interaction); + if (values.length === 0) { + continue; + } + lines.push(`- ${field.label}: ${values.join(", ")}`); + } + if (lines.length === 1) { + lines.push("- (no values)"); + } + return lines.join("\n"); +} + +export function resolveDiscordInteractionId(interaction: AgentComponentInteraction): string { + const rawId = + interaction.rawData && typeof interaction.rawData === "object" && "id" in interaction.rawData + ? (interaction.rawData as { id?: unknown }).id + : undefined; + if (typeof rawId === "string" && rawId.trim()) { + return rawId.trim(); + } + if (typeof rawId === "number" && Number.isFinite(rawId)) { + return String(rawId); + } + return `discord-interaction:${Date.now()}`; +} + +export function resolveComponentCommandAuthorized(params: { + ctx: AgentComponentContext; + interactionCtx: ComponentInteractionContext; + channelConfig: ReturnType; + guildInfo: ReturnType; + allowNameMatching: boolean; +}) { + const { ctx, interactionCtx, channelConfig, guildInfo } = params; + if (interactionCtx.isDirectMessage) { + return true; + } + + const { ownerAllowList, ownerAllowed: ownerOk } = resolveDiscordOwnerAccess({ + allowFrom: ctx.allowFrom, + sender: { + id: interactionCtx.user.id, + name: interactionCtx.user.username, + tag: formatDiscordUserTag(interactionCtx.user), + }, + allowNameMatching: params.allowNameMatching, + }); + + const { hasAccessRestrictions, memberAllowed } = resolveDiscordMemberAccessState({ + channelConfig, + guildInfo, + memberRoleIds: interactionCtx.memberRoleIds, + sender: { + id: interactionCtx.user.id, + name: interactionCtx.user.username, + tag: formatDiscordUserTag(interactionCtx.user), + }, + allowNameMatching: params.allowNameMatching, + }); + const useAccessGroups = ctx.cfg.commands?.useAccessGroups !== false; + const authorizers = useAccessGroups + ? [ + { configured: ownerAllowList != null, allowed: ownerOk }, + { configured: hasAccessRestrictions, allowed: memberAllowed }, + ] + : [{ configured: hasAccessRestrictions, allowed: memberAllowed }]; + + return resolveCommandAuthorizedFromAuthorizers({ + useAccessGroups, + authorizers, + modeWhenAccessGroupsOff: "configured", + }); +} + +export { resolveDiscordGuildEntry, resolvePinnedMainDmOwnerFromAllowlist }; diff --git a/extensions/discord/src/monitor/agent-components.ts b/extensions/discord/src/monitor/agent-components.ts index 5ac63e76d51..78fb38b3c91 100644 --- a/extensions/discord/src/monitor/agent-components.ts +++ b/extensions/discord/src/monitor/agent-components.ts @@ -19,7 +19,6 @@ import { import type { APIStringSelectComponent } from "discord-api-types/v10"; import { ButtonStyle, ChannelType } from "discord-api-types/v10"; import { resolveHumanDelayConfig } from "openclaw/plugin-sdk/agent-runtime"; -import { resolveCommandAuthorizedFromAuthorizers } from "openclaw/plugin-sdk/channel-runtime"; import { createReplyPrefixOptions } from "openclaw/plugin-sdk/channel-runtime"; import { recordInboundSession } from "openclaw/plugin-sdk/channel-runtime"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; @@ -27,8 +26,6 @@ import { isDangerousNameMatchingEnabled } from "openclaw/plugin-sdk/config-runti import { resolveMarkdownTableMode } from "openclaw/plugin-sdk/config-runtime"; import { readSessionUpdatedAt, resolveStorePath } from "openclaw/plugin-sdk/config-runtime"; import type { DiscordAccountConfig } from "openclaw/plugin-sdk/config-runtime"; -import { issuePairingChallenge } from "openclaw/plugin-sdk/conversation-runtime"; -import { upsertChannelPairingRequest } from "openclaw/plugin-sdk/conversation-runtime"; import { buildPluginBindingResolvedText, parsePluginBindingApprovalCustomId, @@ -48,32 +45,51 @@ import { createReplyReferencePlanner } from "openclaw/plugin-sdk/reply-runtime"; import { resolveAgentRoute } from "openclaw/plugin-sdk/routing"; import { logVerbose } from "openclaw/plugin-sdk/runtime-env"; import { createNonExitingRuntime, type RuntimeEnv } from "openclaw/plugin-sdk/runtime-env"; -import { - readStoreAllowFromForDmPolicy, - resolvePinnedMainDmOwnerFromAllowlist, -} from "openclaw/plugin-sdk/security-runtime"; import { logDebug, logError } from "openclaw/plugin-sdk/text-runtime"; import { resolveDiscordMaxLinesPerMessage } from "../accounts.js"; import { resolveDiscordComponentEntry, resolveDiscordModalEntry } from "../components-registry.js"; import { createDiscordFormModal, formatDiscordComponentEventText, - parseDiscordComponentCustomId, parseDiscordComponentCustomIdForCarbon, - parseDiscordModalCustomId, parseDiscordModalCustomIdForCarbon, type DiscordComponentEntry, type DiscordModalEntry, } from "../components.js"; +import { + AGENT_BUTTON_KEY, + AGENT_SELECT_KEY, + ackComponentInteraction, + buildAgentButtonCustomId, + buildAgentSelectCustomId, + type AgentComponentContext, + type AgentComponentInteraction, + type AgentComponentMessageInteraction, + ensureAgentComponentInteractionAllowed, + ensureComponentUserAllowed, + ensureGuildComponentMemberAllowed, + formatModalSubmissionText, + mapSelectValues, + parseAgentComponentData, + parseDiscordComponentData, + parseDiscordModalId, + resolveAgentComponentRoute, + resolveComponentCommandAuthorized, + type ComponentInteractionContext, + resolveDiscordChannelContext, + type DiscordChannelContext, + resolveDiscordInteractionId, + resolveInteractionContextWithDmAuth, + resolveInteractionCustomId, + resolveModalFieldValues, + resolvePinnedMainDmOwnerFromAllowlist, + type DiscordUser, +} from "./agent-components-helpers.js"; import { type DiscordGuildEntryResolved, normalizeDiscordAllowList, - normalizeDiscordSlug, - resolveDiscordAllowListMatch, resolveDiscordChannelConfigWithFallback, resolveDiscordGuildEntry, - resolveDiscordMemberAccessState, - resolveDiscordOwnerAccess, } from "./allow-list.js"; import { formatDiscordUserTag } from "./format.js"; import { @@ -84,714 +100,6 @@ import { buildDirectLabel, buildGuildLabel } from "./reply-context.js"; import { deliverDiscordReply } from "./reply-delivery.js"; import { sendTyping } from "./typing.js"; -const AGENT_BUTTON_KEY = "agent"; -const AGENT_SELECT_KEY = "agentsel"; - -type DiscordUser = Parameters[0]; - -type AgentComponentMessageInteraction = - | ButtonInteraction - | StringSelectMenuInteraction - | RoleSelectMenuInteraction - | UserSelectMenuInteraction - | MentionableSelectMenuInteraction - | ChannelSelectMenuInteraction; - -type AgentComponentInteraction = AgentComponentMessageInteraction | ModalInteraction; - -type ComponentInteractionContext = NonNullable< - Awaited> ->; - -type DiscordChannelContext = { - channelName: string | undefined; - channelSlug: string; - channelType: number | undefined; - isThread: boolean; - parentId: string | undefined; - parentName: string | undefined; - parentSlug: string; -}; - -function resolveAgentComponentRoute(params: { - ctx: AgentComponentContext; - rawGuildId: string | undefined; - memberRoleIds: string[]; - isDirectMessage: boolean; - userId: string; - channelId: string; - parentId: string | undefined; -}) { - return resolveAgentRoute({ - cfg: params.ctx.cfg, - channel: "discord", - accountId: params.ctx.accountId, - guildId: params.rawGuildId, - memberRoleIds: params.memberRoleIds, - peer: { - kind: params.isDirectMessage ? "direct" : "channel", - id: params.isDirectMessage ? params.userId : params.channelId, - }, - parentPeer: params.parentId ? { kind: "channel", id: params.parentId } : undefined, - }); -} - -async function ackComponentInteraction(params: { - interaction: AgentComponentInteraction; - replyOpts: { ephemeral?: boolean }; - label: string; -}) { - try { - await params.interaction.reply({ - content: "✓", - ...params.replyOpts, - }); - } catch (err) { - logError(`${params.label}: failed to acknowledge interaction: ${String(err)}`); - } -} - -function resolveDiscordChannelContext( - interaction: AgentComponentInteraction, -): DiscordChannelContext { - const channel = interaction.channel; - const channelName = channel && "name" in channel ? (channel.name as string) : undefined; - const channelSlug = channelName ? normalizeDiscordSlug(channelName) : ""; - const channelType = channel && "type" in channel ? (channel.type as number) : undefined; - const isThread = isThreadChannelType(channelType); - - let parentId: string | undefined; - let parentName: string | undefined; - let parentSlug = ""; - if (isThread && channel && "parentId" in channel) { - parentId = (channel.parentId as string) ?? undefined; - if ("parent" in channel) { - const parent = (channel as { parent?: { name?: string } }).parent; - if (parent?.name) { - parentName = parent.name; - parentSlug = normalizeDiscordSlug(parentName); - } - } - } - - return { channelName, channelSlug, channelType, isThread, parentId, parentName, parentSlug }; -} - -async function resolveComponentInteractionContext(params: { - interaction: AgentComponentInteraction; - label: string; - defer?: boolean; -}): Promise<{ - channelId: string; - user: DiscordUser; - username: string; - userId: string; - replyOpts: { ephemeral?: boolean }; - rawGuildId: string | undefined; - isDirectMessage: boolean; - memberRoleIds: string[]; -} | null> { - const { interaction, label } = params; - - // Use interaction's actual channel_id (trusted source from Discord) - // This prevents channel spoofing attacks - const channelId = interaction.rawData.channel_id; - if (!channelId) { - logError(`${label}: missing channel_id in interaction`); - return null; - } - - const user = interaction.user; - if (!user) { - logError(`${label}: missing user in interaction`); - return null; - } - - const shouldDefer = params.defer !== false && "defer" in interaction; - let didDefer = false; - // Defer immediately to satisfy Discord's 3-second interaction ACK requirement. - // We use an ephemeral deferred reply so subsequent interaction.reply() calls - // can safely edit the original deferred response. - if (shouldDefer) { - try { - await (interaction as AgentComponentMessageInteraction).defer({ ephemeral: true }); - didDefer = true; - } catch (err) { - logError(`${label}: failed to defer interaction: ${String(err)}`); - } - } - const replyOpts = didDefer ? {} : { ephemeral: true }; - - const username = formatUsername(user); - const userId = user.id; - - // P1 FIX: Use rawData.guild_id as source of truth - interaction.guild can be null - // when guild is not cached even though guild_id is present in rawData - const rawGuildId = interaction.rawData.guild_id; - const isDirectMessage = !rawGuildId; - const memberRoleIds = Array.isArray(interaction.rawData.member?.roles) - ? interaction.rawData.member.roles.map((roleId: string) => String(roleId)) - : []; - - return { - channelId, - user, - username, - userId, - replyOpts, - rawGuildId, - isDirectMessage, - memberRoleIds, - }; -} - -async function ensureGuildComponentMemberAllowed(params: { - interaction: AgentComponentInteraction; - guildInfo: ReturnType; - channelId: string; - rawGuildId: string | undefined; - channelCtx: DiscordChannelContext; - memberRoleIds: string[]; - user: DiscordUser; - replyOpts: { ephemeral?: boolean }; - componentLabel: string; - unauthorizedReply: string; - allowNameMatching: boolean; -}): Promise { - const { - interaction, - guildInfo, - channelId, - rawGuildId, - channelCtx, - memberRoleIds, - user, - replyOpts, - componentLabel, - unauthorizedReply, - } = params; - - if (!rawGuildId) { - return true; - } - - const channelConfig = resolveDiscordChannelConfigWithFallback({ - guildInfo, - channelId, - channelName: channelCtx.channelName, - channelSlug: channelCtx.channelSlug, - parentId: channelCtx.parentId, - parentName: channelCtx.parentName, - parentSlug: channelCtx.parentSlug, - scope: channelCtx.isThread ? "thread" : "channel", - }); - - const { memberAllowed } = resolveDiscordMemberAccessState({ - channelConfig, - guildInfo, - memberRoleIds, - sender: { - id: user.id, - name: user.username, - tag: user.discriminator ? `${user.username}#${user.discriminator}` : undefined, - }, - allowNameMatching: params.allowNameMatching, - }); - if (memberAllowed) { - return true; - } - - logVerbose(`agent ${componentLabel}: blocked user ${user.id} (not in users/roles allowlist)`); - try { - await interaction.reply({ - content: unauthorizedReply, - ...replyOpts, - }); - } catch { - // Interaction may have expired - } - return false; -} - -async function ensureComponentUserAllowed(params: { - entry: DiscordComponentEntry; - interaction: AgentComponentInteraction; - user: DiscordUser; - replyOpts: { ephemeral?: boolean }; - componentLabel: string; - unauthorizedReply: string; - allowNameMatching: boolean; -}): Promise { - const allowList = normalizeDiscordAllowList(params.entry.allowedUsers, [ - "discord:", - "user:", - "pk:", - ]); - if (!allowList) { - return true; - } - const match = resolveDiscordAllowListMatch({ - allowList, - candidate: { - id: params.user.id, - name: params.user.username, - tag: formatDiscordUserTag(params.user), - }, - allowNameMatching: params.allowNameMatching, - }); - if (match.allowed) { - return true; - } - - logVerbose( - `discord component ${params.componentLabel}: blocked user ${params.user.id} (not in allowedUsers)`, - ); - try { - await params.interaction.reply({ - content: params.unauthorizedReply, - ...params.replyOpts, - }); - } catch { - // Interaction may have expired - } - return false; -} - -async function ensureAgentComponentInteractionAllowed(params: { - ctx: AgentComponentContext; - interaction: AgentComponentInteraction; - channelId: string; - rawGuildId: string | undefined; - memberRoleIds: string[]; - user: DiscordUser; - replyOpts: { ephemeral?: boolean }; - componentLabel: string; - unauthorizedReply: string; -}): Promise<{ parentId: string | undefined } | null> { - const guildInfo = resolveDiscordGuildEntry({ - guild: params.interaction.guild ?? undefined, - guildId: params.rawGuildId, - guildEntries: params.ctx.guildEntries, - }); - const channelCtx = resolveDiscordChannelContext(params.interaction); - const memberAllowed = await ensureGuildComponentMemberAllowed({ - interaction: params.interaction, - guildInfo, - channelId: params.channelId, - rawGuildId: params.rawGuildId, - channelCtx, - memberRoleIds: params.memberRoleIds, - user: params.user, - replyOpts: params.replyOpts, - componentLabel: params.componentLabel, - unauthorizedReply: params.unauthorizedReply, - allowNameMatching: isDangerousNameMatchingEnabled(params.ctx.discordConfig), - }); - if (!memberAllowed) { - return null; - } - return { parentId: channelCtx.parentId }; -} - -export type AgentComponentContext = { - cfg: OpenClawConfig; - accountId: string; - discordConfig?: DiscordAccountConfig; - runtime?: RuntimeEnv; - token?: string; - guildEntries?: Record; - /** DM allowlist (from allowFrom config; legacy: dm.allowFrom) */ - allowFrom?: string[]; - /** DM policy (default: "pairing") */ - dmPolicy?: "open" | "pairing" | "allowlist" | "disabled"; -}; - -/** - * Build agent button custom ID: agent:componentId= - * The channelId is NOT embedded in customId - we use interaction.rawData.channel_id instead - * to prevent channel spoofing attacks. - * - * Carbon's customIdParser parses "key:arg1=value1;arg2=value2" into { arg1: value1, arg2: value2 } - */ -export function buildAgentButtonCustomId(componentId: string): string { - return `${AGENT_BUTTON_KEY}:componentId=${encodeURIComponent(componentId)}`; -} - -/** - * Build agent select menu custom ID: agentsel:componentId= - */ -export function buildAgentSelectCustomId(componentId: string): string { - return `${AGENT_SELECT_KEY}:componentId=${encodeURIComponent(componentId)}`; -} - -/** - * Parse agent component data from Carbon's parsed ComponentData - * Supports both legacy { componentId } and Components v2 { cid } payloads. - */ -function readParsedComponentId(data: ComponentData): unknown { - if (!data || typeof data !== "object") { - return undefined; - } - return "cid" in data - ? (data as Record).cid - : (data as Record).componentId; -} - -function parseAgentComponentData(data: ComponentData): { - componentId: string; -} | null { - const raw = readParsedComponentId(data); - - const decodeSafe = (value: string): string => { - // `cid` values may be raw (not URI-encoded). Guard against malformed % sequences. - // Only attempt decoding when it looks like it contains percent-encoding. - if (!value.includes("%")) { - return value; - } - // If it has a % but not a valid %XX sequence, skip decode. - if (!/%[0-9A-Fa-f]{2}/.test(value)) { - return value; - } - try { - return decodeURIComponent(value); - } catch { - return value; - } - }; - - const componentId = - typeof raw === "string" ? decodeSafe(raw) : typeof raw === "number" ? String(raw) : null; - - if (!componentId) { - return null; - } - return { componentId }; -} - -function formatUsername(user: { username: string; discriminator?: string | null }): string { - if (user.discriminator && user.discriminator !== "0") { - return `${user.username}#${user.discriminator}`; - } - return user.username; -} - -/** - * Check if a channel type is a thread type - */ -function isThreadChannelType(channelType: number | undefined): boolean { - return ( - channelType === ChannelType.PublicThread || - channelType === ChannelType.PrivateThread || - channelType === ChannelType.AnnouncementThread - ); -} - -async function ensureDmComponentAuthorized(params: { - ctx: AgentComponentContext; - interaction: AgentComponentInteraction; - user: DiscordUser; - componentLabel: string; - replyOpts: { ephemeral?: boolean }; -}): Promise { - const { ctx, interaction, user, componentLabel, replyOpts } = params; - const dmPolicy = ctx.dmPolicy ?? "pairing"; - if (dmPolicy === "disabled") { - logVerbose(`agent ${componentLabel}: blocked (DM policy disabled)`); - try { - await interaction.reply({ - content: "DM interactions are disabled.", - ...replyOpts, - }); - } catch { - // Interaction may have expired - } - return false; - } - if (dmPolicy === "open") { - return true; - } - - const storeAllowFrom = await readStoreAllowFromForDmPolicy({ - provider: "discord", - accountId: ctx.accountId, - dmPolicy, - }); - const effectiveAllowFrom = [...(ctx.allowFrom ?? []), ...storeAllowFrom]; - const allowList = normalizeDiscordAllowList(effectiveAllowFrom, ["discord:", "user:", "pk:"]); - const allowMatch = allowList - ? resolveDiscordAllowListMatch({ - allowList, - candidate: { - id: user.id, - name: user.username, - tag: formatDiscordUserTag(user), - }, - allowNameMatching: isDangerousNameMatchingEnabled(ctx.discordConfig), - }) - : { allowed: false }; - if (allowMatch.allowed) { - return true; - } - - if (dmPolicy === "pairing") { - const pairingResult = await issuePairingChallenge({ - channel: "discord", - senderId: user.id, - senderIdLine: `Your Discord user id: ${user.id}`, - meta: { - tag: formatDiscordUserTag(user), - name: user.username, - }, - upsertPairingRequest: async ({ id, meta }) => - await upsertChannelPairingRequest({ - channel: "discord", - id, - accountId: ctx.accountId, - meta, - }), - sendPairingReply: async (text) => { - await interaction.reply({ - content: text, - ...replyOpts, - }); - }, - }); - if (!pairingResult.created) { - try { - await interaction.reply({ - content: "Pairing already requested. Ask the bot owner to approve your code.", - ...replyOpts, - }); - } catch { - // Interaction may have expired - } - } - return false; - } - - logVerbose(`agent ${componentLabel}: blocked DM user ${user.id} (not in allowFrom)`); - try { - await interaction.reply({ - content: `You are not authorized to use this ${componentLabel}.`, - ...replyOpts, - }); - } catch { - // Interaction may have expired - } - return false; -} - -async function resolveInteractionContextWithDmAuth(params: { - ctx: AgentComponentContext; - interaction: AgentComponentInteraction; - label: string; - componentLabel: string; - defer?: boolean; -}): Promise { - const interactionCtx = await resolveComponentInteractionContext({ - interaction: params.interaction, - label: params.label, - defer: params.defer, - }); - if (!interactionCtx) { - return null; - } - if (interactionCtx.isDirectMessage) { - const authorized = await ensureDmComponentAuthorized({ - ctx: params.ctx, - interaction: params.interaction, - user: interactionCtx.user, - componentLabel: params.componentLabel, - replyOpts: interactionCtx.replyOpts, - }); - if (!authorized) { - return null; - } - } - return interactionCtx; -} - -function normalizeComponentId(value: unknown): string | undefined { - if (typeof value === "string") { - const trimmed = value.trim(); - return trimmed ? trimmed : undefined; - } - if (typeof value === "number" && Number.isFinite(value)) { - return String(value); - } - return undefined; -} - -function parseDiscordComponentData( - data: ComponentData, - customId?: string, -): { componentId: string; modalId?: string } | null { - if (!data || typeof data !== "object") { - return null; - } - const rawComponentId = readParsedComponentId(data); - const rawModalId = - "mid" in data ? (data as { mid?: unknown }).mid : (data as { modalId?: unknown }).modalId; - let componentId = normalizeComponentId(rawComponentId); - let modalId = normalizeComponentId(rawModalId); - if (!componentId && customId) { - const parsed = parseDiscordComponentCustomId(customId); - if (parsed) { - componentId = parsed.componentId; - modalId = parsed.modalId; - } - } - if (!componentId) { - return null; - } - return { componentId, modalId }; -} - -function parseDiscordModalId(data: ComponentData, customId?: string): string | null { - if (data && typeof data === "object") { - const rawModalId = - "mid" in data ? (data as { mid?: unknown }).mid : (data as { modalId?: unknown }).modalId; - const modalId = normalizeComponentId(rawModalId); - if (modalId) { - return modalId; - } - } - if (customId) { - return parseDiscordModalCustomId(customId); - } - return null; -} - -function resolveInteractionCustomId(interaction: AgentComponentInteraction): string | undefined { - if (!interaction?.rawData || typeof interaction.rawData !== "object") { - return undefined; - } - if (!("data" in interaction.rawData)) { - return undefined; - } - const data = (interaction.rawData as { data?: { custom_id?: unknown } }).data; - const customId = data?.custom_id; - if (typeof customId !== "string") { - return undefined; - } - const trimmed = customId.trim(); - return trimmed ? trimmed : undefined; -} - -function mapOptionLabels( - options: Array<{ value: string; label: string }> | undefined, - values: string[], -) { - if (!options || options.length === 0) { - return values; - } - const map = new Map(options.map((option) => [option.value, option.label])); - return values.map((value) => map.get(value) ?? value); -} - -function mapSelectValues(entry: DiscordComponentEntry, values: string[]): string[] { - if (entry.selectType === "string") { - return mapOptionLabels(entry.options, values); - } - if (entry.selectType === "user") { - return values.map((value) => `user:${value}`); - } - if (entry.selectType === "role") { - return values.map((value) => `role:${value}`); - } - if (entry.selectType === "mentionable") { - return values.map((value) => `mentionable:${value}`); - } - if (entry.selectType === "channel") { - return values.map((value) => `channel:${value}`); - } - return values; -} - -function resolveModalFieldValues( - field: DiscordModalEntry["fields"][number], - interaction: ModalInteraction, -): string[] { - const fields = interaction.fields; - const optionLabels = field.options?.map((option) => ({ - value: option.value, - label: option.label, - })); - const required = field.required === true; - try { - switch (field.type) { - case "text": { - const value = required ? fields.getText(field.id, true) : fields.getText(field.id); - return value ? [value] : []; - } - case "select": - case "checkbox": - case "radio": { - const values = required - ? fields.getStringSelect(field.id, true) - : (fields.getStringSelect(field.id) ?? []); - return mapOptionLabels(optionLabels, values); - } - case "role-select": { - try { - const roles = required - ? fields.getRoleSelect(field.id, true) - : (fields.getRoleSelect(field.id) ?? []); - return roles.map((role) => role.name ?? role.id); - } catch { - const values = required - ? fields.getStringSelect(field.id, true) - : (fields.getStringSelect(field.id) ?? []); - return values; - } - } - case "user-select": { - const users = required - ? fields.getUserSelect(field.id, true) - : (fields.getUserSelect(field.id) ?? []); - return users.map((user) => formatDiscordUserTag(user)); - } - default: - return []; - } - } catch (err) { - logError(`agent modal: failed to read field ${field.id}: ${String(err)}`); - return []; - } -} - -function formatModalSubmissionText( - entry: DiscordModalEntry, - interaction: ModalInteraction, -): string { - const lines: string[] = [`Form "${entry.title}" submitted.`]; - for (const field of entry.fields) { - const values = resolveModalFieldValues(field, interaction); - if (values.length === 0) { - continue; - } - lines.push(`- ${field.label}: ${values.join(", ")}`); - } - if (lines.length === 1) { - lines.push("- (no values)"); - } - return lines.join("\n"); -} - -function resolveDiscordInteractionId(interaction: AgentComponentInteraction): string { - const rawId = - interaction.rawData && typeof interaction.rawData === "object" && "id" in interaction.rawData - ? (interaction.rawData as { id?: unknown }).id - : undefined; - if (typeof rawId === "string" && rawId.trim()) { - return rawId.trim(); - } - if (typeof rawId === "number" && Number.isFinite(rawId)) { - return String(rawId); - } - return `discord-interaction:${Date.now()}`; -} - async function dispatchPluginDiscordInteractiveEvent(params: { ctx: AgentComponentContext; interaction: AgentComponentInteraction; @@ -931,54 +239,6 @@ async function dispatchPluginDiscordInteractiveEvent(params: { return "unmatched"; } -function resolveComponentCommandAuthorized(params: { - ctx: AgentComponentContext; - interactionCtx: ComponentInteractionContext; - channelConfig: ReturnType; - guildInfo: ReturnType; - allowNameMatching: boolean; -}): boolean { - const { ctx, interactionCtx, channelConfig, guildInfo } = params; - if (interactionCtx.isDirectMessage) { - return true; - } - - const { ownerAllowList, ownerAllowed: ownerOk } = resolveDiscordOwnerAccess({ - allowFrom: ctx.allowFrom, - sender: { - id: interactionCtx.user.id, - name: interactionCtx.user.username, - tag: formatDiscordUserTag(interactionCtx.user), - }, - allowNameMatching: params.allowNameMatching, - }); - - const { hasAccessRestrictions, memberAllowed } = resolveDiscordMemberAccessState({ - channelConfig, - guildInfo, - memberRoleIds: interactionCtx.memberRoleIds, - sender: { - id: interactionCtx.user.id, - name: interactionCtx.user.username, - tag: formatDiscordUserTag(interactionCtx.user), - }, - allowNameMatching: params.allowNameMatching, - }); - const useAccessGroups = ctx.cfg.commands?.useAccessGroups !== false; - const authorizers = useAccessGroups - ? [ - { configured: ownerAllowList != null, allowed: ownerOk }, - { configured: hasAccessRestrictions, allowed: memberAllowed }, - ] - : [{ configured: hasAccessRestrictions, allowed: memberAllowed }]; - - return resolveCommandAuthorizedFromAuthorizers({ - useAccessGroups, - authorizers, - modeWhenAccessGroupsOff: "configured", - }); -} - async function dispatchDiscordComponentEvent(params: { ctx: AgentComponentContext; interaction: AgentComponentInteraction; @@ -1045,7 +305,7 @@ async function dispatchDiscordComponentEvent(params: { ? resolvePinnedMainDmOwnerFromAllowlist({ dmScope: ctx.cfg.session?.dmScope, allowFrom: channelConfig?.users ?? guildInfo?.users, - normalizeEntry: (entry) => { + normalizeEntry: (entry: string) => { const normalized = normalizeDiscordAllowList([entry], ["discord:", "user:", "pk:"]); const candidate = normalized?.ids.values().next().value; return typeof candidate === "string" && /^\d+$/.test(candidate) ? candidate : undefined; diff --git a/extensions/discord/src/monitor/gateway-plugin.ts b/extensions/discord/src/monitor/gateway-plugin.ts index 109135a3684..5acab8d5339 100644 --- a/extensions/discord/src/monitor/gateway-plugin.ts +++ b/extensions/discord/src/monitor/gateway-plugin.ts @@ -9,6 +9,7 @@ import WebSocket from "ws"; const DISCORD_GATEWAY_BOT_URL = "https://discord.com/api/v10/gateway/bot"; const DEFAULT_DISCORD_GATEWAY_URL = "wss://gateway.discord.gg/"; +const DISCORD_GATEWAY_INFO_TIMEOUT_MS = 10_000; type DiscordGatewayMetadataResponse = Pick; type DiscordGatewayFetchInit = Record & { @@ -19,6 +20,8 @@ type DiscordGatewayFetch = ( init?: DiscordGatewayFetchInit, ) => Promise; +type DiscordGatewayMetadataError = Error & { transient?: boolean }; + export function resolveDiscordGatewayIntents( intentsConfig?: import("openclaw/plugin-sdk/config-runtime").DiscordIntentsConfig, ): number { @@ -64,14 +67,36 @@ function createGatewayMetadataError(params: { transient: boolean; cause?: unknown; }): Error { - if (params.transient) { - return new Error("Failed to get gateway information from Discord: fetch failed", { - cause: params.cause ?? new Error(params.detail), - }); - } - return new Error(`Failed to get gateway information from Discord: ${params.detail}`, { - cause: params.cause, + const error = new Error( + params.transient + ? "Failed to get gateway information from Discord: fetch failed" + : `Failed to get gateway information from Discord: ${params.detail}`, + { + cause: params.cause ?? (params.transient ? new Error(params.detail) : undefined), + }, + ) as DiscordGatewayMetadataError; + Object.defineProperty(error, "transient", { + value: params.transient, + enumerable: false, }); + return error; +} + +function isTransientGatewayMetadataError(error: unknown): boolean { + return Boolean((error as DiscordGatewayMetadataError | undefined)?.transient); +} + +function createDefaultGatewayInfo(): APIGatewayBotInfo { + return { + url: DEFAULT_DISCORD_GATEWAY_URL, + shards: 1, + session_start_limit: { + total: 1, + remaining: 1, + reset_after: 0, + max_concurrency: 1, + }, + }; } async function fetchDiscordGatewayInfo(params: { @@ -134,6 +159,65 @@ async function fetchDiscordGatewayInfo(params: { } } +async function fetchDiscordGatewayInfoWithTimeout(params: { + token: string; + fetchImpl: DiscordGatewayFetch; + fetchInit?: DiscordGatewayFetchInit; + timeoutMs?: number; +}): Promise { + const timeoutMs = Math.max(1, params.timeoutMs ?? DISCORD_GATEWAY_INFO_TIMEOUT_MS); + const abortController = new AbortController(); + let timeoutId: ReturnType | undefined; + const timeoutPromise = new Promise((_, reject) => { + timeoutId = setTimeout(() => { + abortController.abort(); + reject( + createGatewayMetadataError({ + detail: `Discord API /gateway/bot timed out after ${timeoutMs}ms`, + transient: true, + cause: new Error("gateway metadata timeout"), + }), + ); + }, timeoutMs); + timeoutId.unref?.(); + }); + + try { + return await Promise.race([ + fetchDiscordGatewayInfo({ + token: params.token, + fetchImpl: params.fetchImpl, + fetchInit: { + ...params.fetchInit, + signal: abortController.signal, + }, + }), + timeoutPromise, + ]); + } finally { + if (timeoutId) { + clearTimeout(timeoutId); + } + } +} + +function resolveGatewayInfoWithFallback(params: { runtime?: RuntimeEnv; error: unknown }): { + info: APIGatewayBotInfo; + usedFallback: boolean; +} { + if (!isTransientGatewayMetadataError(params.error)) { + throw params.error; + } + const message = params.error instanceof Error ? params.error.message : String(params.error); + params.runtime?.log?.( + `discord: gateway metadata lookup failed transiently; using default gateway url (${message})`, + ); + return { + info: createDefaultGatewayInfo(), + usedFallback: true, + }; +} + function createGatewayPlugin(params: { options: { reconnect: { maxAttempts: number }; @@ -143,19 +227,29 @@ function createGatewayPlugin(params: { fetchImpl: DiscordGatewayFetch; fetchInit?: DiscordGatewayFetchInit; wsAgent?: HttpsProxyAgent; + runtime?: RuntimeEnv; }): GatewayPlugin { class SafeGatewayPlugin extends GatewayPlugin { + private gatewayInfoUsedFallback = false; + constructor() { super(params.options); } override async registerClient(client: Parameters[0]) { - if (!this.gatewayInfo) { - this.gatewayInfo = await fetchDiscordGatewayInfo({ + if (!this.gatewayInfo || this.gatewayInfoUsedFallback) { + const resolved = await fetchDiscordGatewayInfoWithTimeout({ token: client.options.token, fetchImpl: params.fetchImpl, fetchInit: params.fetchInit, - }); + }) + .then((info) => ({ + info, + usedFallback: false, + })) + .catch((error) => resolveGatewayInfoWithFallback({ runtime: params.runtime, error })); + this.gatewayInfo = resolved.info; + this.gatewayInfoUsedFallback = resolved.usedFallback; } return super.registerClient(client); } @@ -187,6 +281,7 @@ export function createDiscordGatewayPlugin(params: { return createGatewayPlugin({ options, fetchImpl: (input, init) => fetch(input, init as RequestInit), + runtime: params.runtime, }); } @@ -201,12 +296,14 @@ export function createDiscordGatewayPlugin(params: { fetchImpl: (input, init) => undiciFetch(input, init), fetchInit: { dispatcher: fetchAgent }, wsAgent, + runtime: params.runtime, }); } catch (err) { params.runtime.error?.(danger(`discord: invalid gateway proxy: ${String(err)}`)); return createGatewayPlugin({ options, fetchImpl: (input, init) => fetch(input, init as RequestInit), + runtime: params.runtime, }); } } diff --git a/extensions/discord/src/monitor/message-handler.preflight.acp-bindings.test.ts b/extensions/discord/src/monitor/message-handler.preflight.acp-bindings.test.ts index 01bac15e856..982b9589b22 100644 --- a/extensions/discord/src/monitor/message-handler.preflight.acp-bindings.test.ts +++ b/extensions/discord/src/monitor/message-handler.preflight.acp-bindings.test.ts @@ -1,14 +1,18 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -const ensureConfiguredAcpBindingSessionMock = vi.hoisted(() => vi.fn()); -const resolveConfiguredAcpBindingRecordMock = vi.hoisted(() => vi.fn()); +const ensureConfiguredBindingRouteReadyMock = vi.hoisted(() => vi.fn()); +const resolveConfiguredBindingRouteMock = vi.hoisted(() => vi.fn()); -vi.mock("../../../../src/acp/persistent-bindings.js", () => ({ - ensureConfiguredAcpBindingSession: (...args: unknown[]) => - ensureConfiguredAcpBindingSessionMock(...args), - resolveConfiguredAcpBindingRecord: (...args: unknown[]) => - resolveConfiguredAcpBindingRecordMock(...args), -})); +vi.mock("openclaw/plugin-sdk/conversation-runtime", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + ensureConfiguredBindingRouteReady: (...args: unknown[]) => + ensureConfiguredBindingRouteReadyMock(...args), + resolveConfiguredBindingRoute: (...args: unknown[]) => + resolveConfiguredBindingRouteMock(...args), + }; +}); import { __testing as sessionBindingTesting } from "../../../../src/infra/outbound/session-binding-service.js"; import { preflightDiscordMessage } from "./message-handler.preflight.js"; @@ -52,6 +56,77 @@ function createConfiguredDiscordBinding() { } as const; } +function createConfiguredDiscordRoute() { + const configuredBinding = createConfiguredDiscordBinding(); + return { + bindingResolution: { + conversation: { + channel: "discord", + accountId: "default", + conversationId: CHANNEL_ID, + }, + compiledBinding: { + channel: "discord", + accountPattern: "default", + binding: { + type: "acp", + agentId: "codex", + match: { + channel: "discord", + accountId: "default", + peer: { + kind: "channel", + id: CHANNEL_ID, + }, + }, + }, + bindingConversationId: CHANNEL_ID, + target: { + conversationId: CHANNEL_ID, + }, + agentId: "codex", + provider: { + compileConfiguredBinding: () => ({ conversationId: CHANNEL_ID }), + matchInboundConversation: () => ({ conversationId: CHANNEL_ID }), + }, + targetFactory: { + driverId: "acp", + materialize: () => ({ + record: configuredBinding.record, + statefulTarget: { + kind: "stateful", + driverId: "acp", + sessionKey: configuredBinding.record.targetSessionKey, + agentId: configuredBinding.spec.agentId, + }, + }), + }, + }, + match: { + conversationId: CHANNEL_ID, + }, + record: configuredBinding.record, + statefulTarget: { + kind: "stateful", + driverId: "acp", + sessionKey: configuredBinding.record.targetSessionKey, + agentId: configuredBinding.spec.agentId, + }, + }, + configuredBinding, + boundSessionKey: configuredBinding.record.targetSessionKey, + route: { + agentId: "codex", + accountId: "default", + channel: "discord", + sessionKey: configuredBinding.record.targetSessionKey, + mainSessionKey: "agent:codex:main", + matchedBy: "binding.channel", + lastRoutePolicy: "bound", + }, + } as const; +} + function createBasePreflightParams(overrides?: Record) { const message = createDiscordMessage({ id: "m-1", @@ -94,13 +169,10 @@ function createBasePreflightParams(overrides?: Record) { describe("preflightDiscordMessage configured ACP bindings", () => { beforeEach(() => { sessionBindingTesting.resetSessionBindingAdaptersForTests(); - ensureConfiguredAcpBindingSessionMock.mockReset(); - resolveConfiguredAcpBindingRecordMock.mockReset(); - resolveConfiguredAcpBindingRecordMock.mockReturnValue(createConfiguredDiscordBinding()); - ensureConfiguredAcpBindingSessionMock.mockResolvedValue({ - ok: true, - sessionKey: "agent:codex:acp:binding:discord:default:abc123", - }); + ensureConfiguredBindingRouteReadyMock.mockReset(); + resolveConfiguredBindingRouteMock.mockReset(); + resolveConfiguredBindingRouteMock.mockReturnValue(createConfiguredDiscordRoute()); + ensureConfiguredBindingRouteReadyMock.mockResolvedValue({ ok: true }); }); it("does not initialize configured ACP bindings for rejected messages", async () => { @@ -121,8 +193,8 @@ describe("preflightDiscordMessage configured ACP bindings", () => { ); expect(result).toBeNull(); - expect(resolveConfiguredAcpBindingRecordMock).toHaveBeenCalledTimes(1); - expect(ensureConfiguredAcpBindingSessionMock).not.toHaveBeenCalled(); + expect(resolveConfiguredBindingRouteMock).toHaveBeenCalledTimes(1); + expect(ensureConfiguredBindingRouteReadyMock).not.toHaveBeenCalled(); }); it("initializes configured ACP bindings only after preflight accepts the message", async () => { @@ -144,8 +216,176 @@ describe("preflightDiscordMessage configured ACP bindings", () => { ); expect(result).not.toBeNull(); - expect(resolveConfiguredAcpBindingRecordMock).toHaveBeenCalledTimes(1); - expect(ensureConfiguredAcpBindingSessionMock).toHaveBeenCalledTimes(1); + expect(resolveConfiguredBindingRouteMock).toHaveBeenCalledTimes(1); + expect(ensureConfiguredBindingRouteReadyMock).toHaveBeenCalledTimes(1); expect(result?.boundSessionKey).toBe("agent:codex:acp:binding:discord:default:abc123"); }); + + it("accepts plain messages in configured ACP-bound channels without a mention", async () => { + const message = createDiscordMessage({ + id: "m-no-mention", + channelId: CHANNEL_ID, + content: "hello", + mentionedUsers: [], + author: { + id: "user-1", + bot: false, + username: "alice", + }, + }); + + const result = await preflightDiscordMessage( + createBasePreflightParams({ + data: createGuildEvent({ + channelId: CHANNEL_ID, + guildId: GUILD_ID, + author: message.author, + message, + }), + guildEntries: { + [GUILD_ID]: { + id: GUILD_ID, + channels: { + [CHANNEL_ID]: { + allow: true, + enabled: true, + requireMention: true, + }, + }, + }, + }, + }), + ); + + expect(result).not.toBeNull(); + expect(ensureConfiguredBindingRouteReadyMock).toHaveBeenCalledTimes(1); + expect(result?.boundSessionKey).toBe("agent:codex:acp:binding:discord:default:abc123"); + }); + + it("hydrates empty guild message payloads from REST before ensuring configured ACP bindings", async () => { + const message = createDiscordMessage({ + id: "m-rest", + channelId: CHANNEL_ID, + content: "", + author: { + id: "user-1", + bot: false, + username: "alice", + }, + }); + const restGet = vi.fn(async () => ({ + id: "m-rest", + content: "hello from rest", + attachments: [], + embeds: [], + mentions: [], + mention_roles: [], + mention_everyone: false, + author: { + id: "user-1", + username: "alice", + }, + })); + const client = { + ...createGuildTextClient(CHANNEL_ID), + rest: { + get: restGet, + }, + } as unknown as Parameters[0]["client"]; + + const result = await preflightDiscordMessage( + createBasePreflightParams({ + client, + data: createGuildEvent({ + channelId: CHANNEL_ID, + guildId: GUILD_ID, + author: message.author, + message, + }), + guildEntries: { + [GUILD_ID]: { + id: GUILD_ID, + channels: { + [CHANNEL_ID]: { + allow: true, + enabled: true, + requireMention: false, + }, + }, + }, + }, + }), + ); + + expect(restGet).toHaveBeenCalledTimes(1); + expect(result?.messageText).toBe("hello from rest"); + expect(result?.data.message.content).toBe("hello from rest"); + expect(ensureConfiguredBindingRouteReadyMock).toHaveBeenCalledTimes(1); + }); + + it("hydrates sticker-only guild message payloads from REST before ensuring configured ACP bindings", async () => { + const message = createDiscordMessage({ + id: "m-rest-sticker", + channelId: CHANNEL_ID, + content: "", + author: { + id: "user-1", + bot: false, + username: "alice", + }, + }); + const restGet = vi.fn(async () => ({ + id: "m-rest-sticker", + content: "", + attachments: [], + embeds: [], + mentions: [], + mention_roles: [], + mention_everyone: false, + sticker_items: [ + { + id: "sticker-1", + name: "wave", + }, + ], + author: { + id: "user-1", + username: "alice", + }, + })); + const client = { + ...createGuildTextClient(CHANNEL_ID), + rest: { + get: restGet, + }, + } as unknown as Parameters[0]["client"]; + + const result = await preflightDiscordMessage( + createBasePreflightParams({ + client, + data: createGuildEvent({ + channelId: CHANNEL_ID, + guildId: GUILD_ID, + author: message.author, + message, + }), + guildEntries: { + [GUILD_ID]: { + id: GUILD_ID, + channels: { + [CHANNEL_ID]: { + allow: true, + enabled: true, + requireMention: false, + }, + }, + }, + }, + }), + ); + + expect(restGet).toHaveBeenCalledTimes(1); + expect(result?.messageText).toBe(" (1 sticker)"); + expect(ensureConfiguredBindingRouteReadyMock).toHaveBeenCalledTimes(1); + }); }); diff --git a/extensions/discord/src/monitor/message-handler.preflight.test.ts b/extensions/discord/src/monitor/message-handler.preflight.test.ts index 2fb14bafe8e..bd55cd2ead2 100644 --- a/extensions/discord/src/monitor/message-handler.preflight.test.ts +++ b/extensions/discord/src/monitor/message-handler.preflight.test.ts @@ -3,7 +3,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; const transcribeFirstAudioMock = vi.hoisted(() => vi.fn()); -vi.mock("../../../../src/media-understanding/audio-preflight.js", () => ({ +vi.mock("./preflight-audio.runtime.js", () => ({ transcribeFirstAudio: (...args: unknown[]) => transcribeFirstAudioMock(...args), })); import { @@ -229,16 +229,16 @@ describe("resolvePreflightMentionRequirement", () => { expect( resolvePreflightMentionRequirement({ shouldRequireMention: true, - isBoundThreadSession: false, + bypassMentionRequirement: false, }), ).toBe(true); }); - it("disables mention requirement for bound thread sessions", () => { + it("disables mention requirement when the route explicitly bypasses mentions", () => { expect( resolvePreflightMentionRequirement({ shouldRequireMention: true, - isBoundThreadSession: true, + bypassMentionRequirement: true, }), ).toBe(false); }); @@ -247,7 +247,7 @@ describe("resolvePreflightMentionRequirement", () => { expect( resolvePreflightMentionRequirement({ shouldRequireMention: false, - isBoundThreadSession: false, + bypassMentionRequirement: false, }), ).toBe(false); }); @@ -378,6 +378,68 @@ describe("preflightDiscordMessage", () => { expect(result?.boundSessionKey).toBe(threadBinding.targetSessionKey); }); + it("drops hydrated bound-thread webhook echoes after fetching an empty payload", async () => { + const threadBinding = createThreadBinding({ + targetKind: "session", + targetSessionKey: "agent:main:acp:discord-thread-1", + }); + const threadId = "thread-webhook-hydrated-1"; + const parentId = "channel-parent-webhook-hydrated-1"; + const message = createDiscordMessage({ + id: "m-webhook-hydrated-1", + channelId: threadId, + content: "", + author: { + id: "relay-bot-1", + bot: true, + username: "Relay", + }, + }); + const restGet = vi.fn(async () => ({ + id: message.id, + content: "webhook relay", + webhook_id: "wh-1", + attachments: [], + embeds: [], + mentions: [], + mention_roles: [], + mention_everyone: false, + author: { + id: "relay-bot-1", + username: "Relay", + bot: true, + }, + })); + const client = { + ...createThreadClient({ threadId, parentId }), + rest: { + get: restGet, + }, + } as unknown as DiscordClient; + + const result = await preflightDiscordMessage({ + ...createPreflightArgs({ + cfg: DEFAULT_PREFLIGHT_CFG, + discordConfig: { + allowBots: true, + } as DiscordConfig, + data: createGuildEvent({ + channelId: threadId, + guildId: "guild-1", + author: message.author, + message, + }), + client, + }), + threadBindings: { + getByThreadId: (id: string) => (id === threadId ? threadBinding : undefined), + } as import("./thread-bindings.js").ThreadBindingManager, + }); + + expect(restGet).toHaveBeenCalledTimes(1); + expect(result).toBeNull(); + }); + it("bypasses mention gating in bound threads for allowed bot senders", async () => { const threadBinding = createThreadBinding(); const threadId = "thread-bot-focus"; @@ -655,8 +717,8 @@ describe("preflightDiscordMessage", () => { }, }); - const result = await preflightDiscordMessage( - createPreflightArgs({ + const result = await preflightDiscordMessage({ + ...createPreflightArgs({ cfg: { ...DEFAULT_PREFLIGHT_CFG, messages: { @@ -674,7 +736,17 @@ describe("preflightDiscordMessage", () => { }), client, }), - ); + guildEntries: { + "guild-1": { + channels: { + [channelId]: { + allow: true, + requireMention: true, + }, + }, + }, + }, + }); expect(transcribeFirstAudioMock).toHaveBeenCalledTimes(1); expect(transcribeFirstAudioMock).toHaveBeenCalledWith( diff --git a/extensions/discord/src/monitor/message-handler.preflight.ts b/extensions/discord/src/monitor/message-handler.preflight.ts index 0a402518927..9094cabb645 100644 --- a/extensions/discord/src/monitor/message-handler.preflight.ts +++ b/extensions/discord/src/monitor/message-handler.preflight.ts @@ -1,4 +1,5 @@ -import { ChannelType, MessageType, type User } from "@buape/carbon"; +import { ChannelType, MessageType, type Message, type User } from "@buape/carbon"; +import { Routes, type APIMessage } from "discord-api-types/v10"; import { formatAllowlistMatchMeta } from "openclaw/plugin-sdk/channel-runtime"; import { resolveControlCommandGate } from "openclaw/plugin-sdk/channel-runtime"; import { logInboundDrop } from "openclaw/plugin-sdk/channel-runtime"; @@ -6,8 +7,8 @@ import { resolveMentionGatingWithBypass } from "openclaw/plugin-sdk/channel-runt import { loadConfig } from "openclaw/plugin-sdk/config-runtime"; import { isDangerousNameMatchingEnabled } from "openclaw/plugin-sdk/config-runtime"; import { - ensureConfiguredAcpRouteReady, - resolveConfiguredAcpRoute, + ensureConfiguredBindingRouteReady, + resolveConfiguredBindingRoute, } from "openclaw/plugin-sdk/conversation-runtime"; import { getSessionBindingService, @@ -95,12 +96,12 @@ function isBoundThreadBotSystemMessage(params: { export function resolvePreflightMentionRequirement(params: { shouldRequireMention: boolean; - isBoundThreadSession: boolean; + bypassMentionRequirement: boolean; }): boolean { if (!params.shouldRequireMention) { return false; } - return !params.isBoundThreadSession; + return !params.bypassMentionRequirement; } export function shouldIgnoreBoundThreadWebhookMessage(params: { @@ -131,6 +132,95 @@ export function shouldIgnoreBoundThreadWebhookMessage(params: { return webhookId === boundWebhookId; } +function mergeFetchedDiscordMessage(base: Message, fetched: APIMessage): Message { + const baseReferenced = ( + base as unknown as { + referencedMessage?: { + mentionedUsers?: unknown[]; + mentionedRoles?: unknown[]; + mentionedEveryone?: boolean; + }; + } + ).referencedMessage; + const fetchedMentions = Array.isArray(fetched.mentions) + ? fetched.mentions.map((mention) => ({ + ...mention, + globalName: mention.global_name ?? undefined, + })) + : undefined; + const referencedMessage = fetched.referenced_message + ? ({ + ...((base as { referencedMessage?: object }).referencedMessage ?? {}), + ...fetched.referenced_message, + mentionedUsers: Array.isArray(fetched.referenced_message.mentions) + ? fetched.referenced_message.mentions.map((mention) => ({ + ...mention, + globalName: mention.global_name ?? undefined, + })) + : (baseReferenced?.mentionedUsers ?? []), + mentionedRoles: + fetched.referenced_message.mention_roles ?? baseReferenced?.mentionedRoles ?? [], + mentionedEveryone: + fetched.referenced_message.mention_everyone ?? baseReferenced?.mentionedEveryone ?? false, + } satisfies Record) + : (base as { referencedMessage?: Message }).referencedMessage; + const rawData = { + ...((base as { rawData?: Record }).rawData ?? {}), + message_snapshots: + fetched.message_snapshots ?? + (base as { rawData?: { message_snapshots?: unknown } }).rawData?.message_snapshots, + sticker_items: + (fetched as { sticker_items?: unknown }).sticker_items ?? + (base as { rawData?: { sticker_items?: unknown } }).rawData?.sticker_items, + }; + return { + ...base, + ...fetched, + content: fetched.content ?? base.content, + attachments: fetched.attachments ?? base.attachments, + embeds: fetched.embeds ?? base.embeds, + stickers: + (fetched as { stickers?: unknown }).stickers ?? + (fetched as { sticker_items?: unknown }).sticker_items ?? + base.stickers, + mentionedUsers: fetchedMentions ?? base.mentionedUsers, + mentionedRoles: fetched.mention_roles ?? base.mentionedRoles, + mentionedEveryone: fetched.mention_everyone ?? base.mentionedEveryone, + referencedMessage, + rawData, + } as unknown as Message; +} + +async function hydrateDiscordMessageIfEmpty(params: { + client: DiscordMessagePreflightParams["client"]; + message: Message; + messageChannelId: string; +}): Promise { + const currentText = resolveDiscordMessageText(params.message, { + includeForwarded: true, + }); + if (currentText) { + return params.message; + } + const rest = params.client.rest as { get?: (route: string) => Promise } | undefined; + if (typeof rest?.get !== "function") { + return params.message; + } + try { + const fetched = (await rest.get( + Routes.channelMessage(params.messageChannelId, params.message.id), + )) as APIMessage | null | undefined; + if (!fetched) { + return params.message; + } + logVerbose(`discord: hydrated empty inbound payload via REST for ${params.message.id}`); + return mergeFetchedDiscordMessage(params.message, fetched); + } catch (err) { + logVerbose(`discord: failed to hydrate message ${params.message.id}: ${String(err)}`); + return params.message; + } +} + export async function preflightDiscordMessage( params: DiscordMessagePreflightParams, ): Promise { @@ -138,7 +228,7 @@ export async function preflightDiscordMessage( return null; } const logger = getChildLogger({ module: "discord-auto-reply" }); - const message = params.data.message; + let message = params.data.message; const author = params.data.author; if (!author) { return null; @@ -160,6 +250,15 @@ export async function preflightDiscordMessage( return null; } + message = await hydrateDiscordMessageIfEmpty({ + client: params.client, + message, + messageChannelId, + }); + if (isPreflightAborted(params.abortSignal)) { + return null; + } + const pluralkitConfig = params.discordConfig?.pluralkit; const webhookId = resolveDiscordWebhookId(message); const shouldCheckPluralKit = Boolean(pluralkitConfig?.enabled) && !webhookId; @@ -197,6 +296,7 @@ export async function preflightDiscordMessage( } const isDirectMessage = channelInfo?.type === ChannelType.DM; const isGroupDm = channelInfo?.type === ChannelType.GroupDM; + const data = message === params.data.message ? params.data : { ...params.data, message }; logDebug( `[discord-preflight] channelId=${messageChannelId} guild_id=${params.data.guild_id} channelType=${channelInfo?.type} isGuild=${isGuildMessage} isDM=${isDirectMessage} isGroupDm=${isGroupDm}`, ); @@ -359,16 +459,18 @@ export async function preflightDiscordMessage( }) ?? undefined; const configuredRoute = threadBinding == null - ? resolveConfiguredAcpRoute({ + ? resolveConfiguredBindingRoute({ cfg: freshCfg, route, - channel: "discord", - accountId: params.accountId, - conversationId: messageChannelId, - parentConversationId: earlyThreadParentId, + conversation: { + channel: "discord", + accountId: params.accountId, + conversationId: messageChannelId, + parentConversationId: earlyThreadParentId, + }, }) : null; - const configuredBinding = configuredRoute?.configuredBinding ?? null; + const configuredBinding = configuredRoute?.bindingResolution ?? null; if (!threadBinding && configuredBinding) { threadBinding = configuredBinding.record; } @@ -394,6 +496,7 @@ export async function preflightDiscordMessage( }); const boundAgentId = boundSessionKey ? effectiveRoute.agentId : undefined; const isBoundThreadSession = Boolean(threadBinding && earlyThreadChannel); + const bypassMentionRequirement = isBoundThreadSession || Boolean(configuredBinding); if ( isBoundThreadBotSystemMessage({ isBoundThreadSession, @@ -579,7 +682,7 @@ export async function preflightDiscordMessage( }); const shouldRequireMention = resolvePreflightMentionRequirement({ shouldRequireMention: shouldRequireMentionByConfig, - isBoundThreadSession, + bypassMentionRequirement, }); // Preflight audio transcription for mention detection in guilds. @@ -764,13 +867,13 @@ export async function preflightDiscordMessage( return null; } if (configuredBinding) { - const ensured = await ensureConfiguredAcpRouteReady({ + const ensured = await ensureConfiguredBindingRouteReady({ cfg: freshCfg, - configuredBinding, + bindingResolution: configuredBinding, }); if (!ensured.ok) { logVerbose( - `discord: configured ACP binding unavailable for channel ${configuredBinding.spec.conversationId}: ${ensured.error}`, + `discord: configured ACP binding unavailable for channel ${configuredBinding.record.conversation.conversationId}: ${ensured.error}`, ); return null; } @@ -794,7 +897,7 @@ export async function preflightDiscordMessage( replyToMode: params.replyToMode, ackReactionScope: params.ackReactionScope, groupPolicy: params.groupPolicy, - data: params.data, + data, client: params.client, message, messageChannelId, diff --git a/extensions/discord/src/monitor/monitor.test.ts b/extensions/discord/src/monitor/monitor.test.ts index da916c4bd2b..84b36d74ec6 100644 --- a/extensions/discord/src/monitor/monitor.test.ts +++ b/extensions/discord/src/monitor/monitor.test.ts @@ -7,11 +7,11 @@ import type { import type { Client } from "@buape/carbon"; import { ChannelType } from "discord-api-types/v10"; import type { GatewayPresenceUpdate } from "discord-api-types/v10"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; +import type { DiscordAccountConfig } from "openclaw/plugin-sdk/config-runtime"; +import { buildPluginBindingApprovalCustomId } from "openclaw/plugin-sdk/conversation-runtime"; +import { buildAgentSessionKey } from "openclaw/plugin-sdk/routing"; import { beforeEach, describe, expect, it, vi } from "vitest"; -import type { OpenClawConfig } from "../../../../src/config/config.js"; -import type { DiscordAccountConfig } from "../../../../src/config/types.discord.js"; -import { buildPluginBindingApprovalCustomId } from "../../../../src/plugins/conversation-binding.js"; -import { buildAgentSessionKey } from "../../../../src/routing/resolve-route.js"; import { clearDiscordComponentEntries, registerDiscordComponentEntries, @@ -50,7 +50,6 @@ const readAllowFromStoreMock = vi.hoisted(() => vi.fn()); const upsertPairingRequestMock = vi.hoisted(() => vi.fn()); const enqueueSystemEventMock = vi.hoisted(() => vi.fn()); const dispatchReplyMock = vi.hoisted(() => vi.fn()); -const deliverDiscordReplyMock = vi.hoisted(() => vi.fn()); const recordInboundSessionMock = vi.hoisted(() => vi.fn()); const readSessionUpdatedAtMock = vi.hoisted(() => vi.fn()); const resolveStorePathMock = vi.hoisted(() => vi.fn()); @@ -59,37 +58,20 @@ const resolvePluginConversationBindingApprovalMock = vi.hoisted(() => vi.fn()); const buildPluginBindingResolvedTextMock = vi.hoisted(() => vi.fn()); let lastDispatchCtx: Record | undefined; -vi.mock("../../../../src/pairing/pairing-store.js", () => ({ - readChannelAllowFromStore: (...args: unknown[]) => readAllowFromStoreMock(...args), - upsertChannelPairingRequest: (...args: unknown[]) => upsertPairingRequestMock(...args), -})); - -vi.mock("../../../../src/infra/system-events.js", async (importOriginal) => { - const actual = await importOriginal(); +vi.mock("../../../../src/security/dm-policy-shared.js", async (importOriginal) => { + const actual = + await importOriginal(); return { ...actual, - enqueueSystemEvent: (...args: unknown[]) => enqueueSystemEventMock(...args), + readStoreAllowFromForDmPolicy: (...args: unknown[]) => readAllowFromStoreMock(...args), }; }); -vi.mock("../../../../src/auto-reply/reply/provider-dispatcher.js", () => ({ - dispatchReplyWithBufferedBlockDispatcher: (...args: unknown[]) => dispatchReplyMock(...args), -})); - -vi.mock("./reply-delivery.js", () => ({ - deliverDiscordReply: (...args: unknown[]) => deliverDiscordReplyMock(...args), -})); - -vi.mock("../../../../src/channels/session.js", () => ({ - recordInboundSession: (...args: unknown[]) => recordInboundSessionMock(...args), -})); - -vi.mock("../../../../src/config/sessions.js", async (importOriginal) => { - const actual = await importOriginal(); +vi.mock("../../../../src/pairing/pairing-store.js", async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, - readSessionUpdatedAt: (...args: unknown[]) => readSessionUpdatedAtMock(...args), - resolveStorePath: (...args: unknown[]) => resolveStorePathMock(...args), + upsertChannelPairingRequest: (...args: unknown[]) => upsertPairingRequestMock(...args), }; }); @@ -105,6 +87,42 @@ vi.mock("../../../../src/plugins/conversation-binding.js", async (importOriginal }; }); +vi.mock("../../../../src/infra/system-events.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + enqueueSystemEvent: (...args: unknown[]) => enqueueSystemEventMock(...args), + }; +}); + +vi.mock("../../../../src/auto-reply/reply/provider-dispatcher.js", async (importOriginal) => { + const actual = + await importOriginal< + typeof import("../../../../src/auto-reply/reply/provider-dispatcher.js") + >(); + return { + ...actual, + dispatchReplyWithBufferedBlockDispatcher: (...args: unknown[]) => dispatchReplyMock(...args), + }; +}); + +vi.mock("../../../../src/channels/session.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + recordInboundSession: (...args: unknown[]) => recordInboundSessionMock(...args), + }; +}); + +vi.mock("../../../../src/config/sessions.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + readSessionUpdatedAt: (...args: unknown[]) => readSessionUpdatedAtMock(...args), + resolveStorePath: (...args: unknown[]) => resolveStorePathMock(...args), + }; +}); + vi.mock("../../../../src/plugins/interactive.js", async (importOriginal) => { const actual = await importOriginal(); return { @@ -287,12 +305,18 @@ describe("discord component interactions", () => { const createComponentButtonInteraction = (overrides: Partial = {}) => { const reply = vi.fn().mockResolvedValue(undefined); const defer = vi.fn().mockResolvedValue(undefined); + const rest = { + get: vi.fn().mockResolvedValue({ type: ChannelType.DM }), + post: vi.fn().mockResolvedValue({}), + patch: vi.fn().mockResolvedValue({}), + delete: vi.fn().mockResolvedValue(undefined), + }; const interaction = { rawData: { channel_id: "dm-channel", id: "interaction-1" }, user: { id: "123456789", username: "AgentUser", discriminator: "0001" }, customId: "occomp:cid=btn_1", message: { id: "msg-1" }, - client: { rest: {} }, + client: { rest }, defer, reply, ...overrides, @@ -303,6 +327,12 @@ describe("discord component interactions", () => { const createModalInteraction = (overrides: Partial = {}) => { const reply = vi.fn().mockResolvedValue(undefined); const acknowledge = vi.fn().mockResolvedValue(undefined); + const rest = { + get: vi.fn().mockResolvedValue({ type: ChannelType.DM }), + post: vi.fn().mockResolvedValue({}), + patch: vi.fn().mockResolvedValue({}), + delete: vi.fn().mockResolvedValue(undefined), + }; const fields = { getText: (key: string) => (key === "fld_1" ? "Casey" : undefined), getStringSelect: (_key: string) => undefined, @@ -316,7 +346,7 @@ describe("discord component interactions", () => { fields, acknowledge, reply, - client: { rest: {} }, + client: { rest }, ...overrides, } as unknown as ModalInteraction; return { interaction, acknowledge, reply }; @@ -363,7 +393,6 @@ describe("discord component interactions", () => { lastDispatchCtx = params.ctx; await params.dispatcherOptions.deliver({ text: "ok" }); }); - deliverDiscordReplyMock.mockClear(); recordInboundSessionMock.mockClear().mockResolvedValue(undefined); readSessionUpdatedAtMock.mockClear().mockReturnValue(undefined); resolveStorePathMock.mockClear().mockReturnValue("/tmp/openclaw-sessions-test.json"); @@ -415,8 +444,6 @@ describe("discord component interactions", () => { expect(reply).toHaveBeenCalledWith({ content: "✓" }); expect(lastDispatchCtx?.BodyForAgent).toBe('Clicked "Approve".'); expect(dispatchReplyMock).toHaveBeenCalledTimes(1); - expect(deliverDiscordReplyMock).toHaveBeenCalledTimes(1); - expect(deliverDiscordReplyMock.mock.calls[0]?.[0]?.replyToId).toBe("msg-1"); expect(resolveDiscordComponentEntry({ id: "btn_1" })).toBeNull(); }); @@ -482,8 +509,6 @@ describe("discord component interactions", () => { expect(lastDispatchCtx?.BodyForAgent).toContain('Form "Details" submitted.'); expect(lastDispatchCtx?.BodyForAgent).toContain("- Name: Casey"); expect(dispatchReplyMock).toHaveBeenCalledTimes(1); - expect(deliverDiscordReplyMock).toHaveBeenCalledTimes(1); - expect(deliverDiscordReplyMock.mock.calls[0]?.[0]?.replyToId).toBe("msg-2"); expect(resolveDiscordModalEntry({ id: "mdl_1" })).toBeNull(); }); diff --git a/extensions/discord/src/monitor/native-command-ui.ts b/extensions/discord/src/monitor/native-command-ui.ts new file mode 100644 index 00000000000..778d8decc06 --- /dev/null +++ b/extensions/discord/src/monitor/native-command-ui.ts @@ -0,0 +1,1030 @@ +import { + Button, + ChannelType, + Container, + Row, + StringSelectMenu, + TextDisplay, + type ButtonInteraction, + type CommandInteraction, + type ComponentData, + type StringSelectMenuInteraction, +} from "@buape/carbon"; +import { ButtonStyle } from "discord-api-types/v10"; +import type { OpenClawConfig, loadConfig } from "openclaw/plugin-sdk/config-runtime"; +import { loadSessionStore, resolveStorePath } from "openclaw/plugin-sdk/config-runtime"; +import { + buildCommandTextFromArgs, + findCommandByNativeName, + listChatCommands, + resolveCommandArgChoices, + serializeCommandArgs, +} from "openclaw/plugin-sdk/reply-runtime"; +import { resolveStoredModelOverride } from "openclaw/plugin-sdk/reply-runtime"; +import type { + ChatCommandDefinition, + CommandArgDefinition, + CommandArgValues, + CommandArgs, +} from "openclaw/plugin-sdk/reply-runtime"; +import type { ResolvedAgentRoute } from "openclaw/plugin-sdk/routing"; +import { logVerbose } from "openclaw/plugin-sdk/runtime-env"; +import { chunkItems, withTimeout } from "openclaw/plugin-sdk/text-runtime"; +import { normalizeDiscordSlug } from "./allow-list.js"; +import { resolveDiscordChannelInfo } from "./message-utils.js"; +import { + readDiscordModelPickerRecentModels, + recordDiscordModelPickerRecentModel, + type DiscordModelPickerPreferenceScope, +} from "./model-picker-preferences.js"; +import { + loadDiscordModelPickerData, + parseDiscordModelPickerData, + renderDiscordModelPickerModelsView, + renderDiscordModelPickerProvidersView, + renderDiscordModelPickerRecentsView, + toDiscordModelPickerMessagePayload, + type DiscordModelPickerCommandContext, +} from "./model-picker.js"; +import { resolveDiscordBoundConversationRoute } from "./route-resolution.js"; +import type { ThreadBindingManager } from "./thread-bindings.js"; +import { resolveDiscordThreadParentInfo } from "./threading.js"; + +type DiscordConfig = NonNullable["discord"]; + +const DISCORD_COMMAND_ARG_CUSTOM_ID_KEY = "cmdarg"; + +export type DiscordCommandArgContext = { + cfg: ReturnType; + discordConfig: DiscordConfig; + accountId: string; + sessionPrefix: string; + threadBindings: ThreadBindingManager; +}; + +export type DiscordModelPickerContext = DiscordCommandArgContext; + +export type DispatchDiscordCommandInteractionParams = { + interaction: CommandInteraction | ButtonInteraction | StringSelectMenuInteraction; + prompt: string; + command: ChatCommandDefinition; + commandArgs?: CommandArgs; + cfg: ReturnType; + discordConfig: DiscordConfig; + accountId: string; + sessionPrefix: string; + preferFollowUp: boolean; + threadBindings: ThreadBindingManager; + suppressReplies?: boolean; +}; + +export type DispatchDiscordCommandInteraction = ( + params: DispatchDiscordCommandInteractionParams, +) => Promise; + +export type SafeDiscordInteractionCall = ( + label: string, + fn: () => Promise, +) => Promise; + +function createCommandArgsWithValue(params: { argName: string; value: string }): CommandArgs { + const values: CommandArgValues = { [params.argName]: params.value }; + return { values }; +} + +function encodeDiscordCommandArgValue(value: string): string { + return encodeURIComponent(value); +} + +function decodeDiscordCommandArgValue(value: string): string { + try { + return decodeURIComponent(value); + } catch { + return value; + } +} + +export function buildDiscordCommandArgCustomId(params: { + command: string; + arg: string; + value: string; + userId: string; +}): string { + return [ + `${DISCORD_COMMAND_ARG_CUSTOM_ID_KEY}:command=${encodeDiscordCommandArgValue(params.command)}`, + `arg=${encodeDiscordCommandArgValue(params.arg)}`, + `value=${encodeDiscordCommandArgValue(params.value)}`, + `user=${encodeDiscordCommandArgValue(params.userId)}`, + ].join(";"); +} + +function parseDiscordCommandArgData( + data: ComponentData, +): { command: string; arg: string; value: string; userId: string } | null { + if (!data || typeof data !== "object") { + return null; + } + const coerce = (value: unknown) => + typeof value === "string" || typeof value === "number" ? String(value) : ""; + const rawCommand = coerce(data.command); + const rawArg = coerce(data.arg); + const rawValue = coerce(data.value); + const rawUser = coerce(data.user); + if (!rawCommand || !rawArg || !rawValue || !rawUser) { + return null; + } + return { + command: decodeDiscordCommandArgValue(rawCommand), + arg: decodeDiscordCommandArgValue(rawArg), + value: decodeDiscordCommandArgValue(rawValue), + userId: decodeDiscordCommandArgValue(rawUser), + }; +} + +function resolveDiscordModelPickerCommandContext( + command: ChatCommandDefinition, +): DiscordModelPickerCommandContext | null { + const normalized = (command.nativeName ?? command.key).trim().toLowerCase(); + if (normalized === "model" || normalized === "models") { + return normalized; + } + return null; +} + +function resolveCommandArgStringValue(args: CommandArgs | undefined, key: string): string { + const value = args?.values?.[key]; + if (typeof value !== "string") { + return ""; + } + return value.trim(); +} + +export function shouldOpenDiscordModelPickerFromCommand(params: { + command: ChatCommandDefinition; + commandArgs?: CommandArgs; +}): DiscordModelPickerCommandContext | null { + const context = resolveDiscordModelPickerCommandContext(params.command); + if (!context) { + return null; + } + + const serializedArgs = serializeCommandArgs(params.command, params.commandArgs)?.trim() ?? ""; + if (context === "model") { + const modelValue = resolveCommandArgStringValue(params.commandArgs, "model"); + return !modelValue && !serializedArgs ? context : null; + } + + return serializedArgs ? null : context; +} + +function buildDiscordModelPickerCurrentModel( + defaultProvider: string, + defaultModel: string, +): string { + return `${defaultProvider}/${defaultModel}`; +} + +function buildDiscordModelPickerAllowedModelRefs( + data: Awaited>, +): Set { + const out = new Set(); + for (const provider of data.providers) { + const models = data.byProvider.get(provider); + if (!models) { + continue; + } + for (const model of models) { + out.add(`${provider}/${model}`); + } + } + return out; +} + +function resolveDiscordModelPickerPreferenceScope(params: { + interaction: CommandInteraction | ButtonInteraction | StringSelectMenuInteraction; + accountId: string; + userId: string; +}): DiscordModelPickerPreferenceScope { + return { + accountId: params.accountId, + guildId: params.interaction.guild?.id ?? undefined, + userId: params.userId, + }; +} + +function buildDiscordModelPickerNoticePayload(message: string): { components: Container[] } { + return { + components: [new Container([new TextDisplay(message)])], + }; +} + +async function resolveDiscordModelPickerRoute(params: { + interaction: CommandInteraction | ButtonInteraction | StringSelectMenuInteraction; + cfg: ReturnType; + accountId: string; + threadBindings: ThreadBindingManager; +}) { + const { interaction, cfg, accountId } = params; + const channel = interaction.channel; + const channelType = channel?.type; + const isDirectMessage = channelType === ChannelType.DM; + const isGroupDm = channelType === ChannelType.GroupDM; + const isThreadChannel = + channelType === ChannelType.PublicThread || + channelType === ChannelType.PrivateThread || + channelType === ChannelType.AnnouncementThread; + const rawChannelId = channel?.id ?? "unknown"; + const memberRoleIds = Array.isArray(interaction.rawData.member?.roles) + ? interaction.rawData.member.roles.map((roleId: string) => String(roleId)) + : []; + let threadParentId: string | undefined; + if (interaction.guild && channel && isThreadChannel && rawChannelId) { + const channelInfo = await resolveDiscordChannelInfo(interaction.client, rawChannelId); + const parentInfo = await resolveDiscordThreadParentInfo({ + client: interaction.client, + threadChannel: { + id: rawChannelId, + name: "name" in channel ? (channel.name as string | undefined) : undefined, + parentId: "parentId" in channel ? (channel.parentId ?? undefined) : undefined, + parent: undefined, + }, + channelInfo, + }); + threadParentId = parentInfo.id; + } + + const threadBinding = isThreadChannel + ? params.threadBindings.getByThreadId(rawChannelId) + : undefined; + return resolveDiscordBoundConversationRoute({ + cfg, + accountId, + guildId: interaction.guild?.id ?? undefined, + memberRoleIds, + isDirectMessage, + isGroupDm, + directUserId: interaction.user?.id ?? rawChannelId, + conversationId: rawChannelId, + parentConversationId: threadParentId, + boundSessionKey: threadBinding?.targetSessionKey, + }); +} + +function resolveDiscordModelPickerCurrentModel(params: { + cfg: ReturnType; + route: ResolvedAgentRoute; + data: Awaited>; +}): string { + const fallback = buildDiscordModelPickerCurrentModel( + params.data.resolvedDefault.provider, + params.data.resolvedDefault.model, + ); + try { + const storePath = resolveStorePath(params.cfg.session?.store, { + agentId: params.route.agentId, + }); + const sessionStore = loadSessionStore(storePath, { skipCache: true }); + const sessionEntry = sessionStore[params.route.sessionKey]; + const override = resolveStoredModelOverride({ + sessionEntry, + sessionStore, + sessionKey: params.route.sessionKey, + }); + if (!override?.model) { + return fallback; + } + const provider = (override.provider || params.data.resolvedDefault.provider).trim(); + if (!provider) { + return fallback; + } + return `${provider}/${override.model}`; + } catch { + return fallback; + } +} + +export async function replyWithDiscordModelPickerProviders(params: { + interaction: CommandInteraction | ButtonInteraction | StringSelectMenuInteraction; + cfg: ReturnType; + command: DiscordModelPickerCommandContext; + userId: string; + accountId: string; + threadBindings: ThreadBindingManager; + preferFollowUp: boolean; + safeInteractionCall: SafeDiscordInteractionCall; +}) { + const route = await resolveDiscordModelPickerRoute({ + interaction: params.interaction, + cfg: params.cfg, + accountId: params.accountId, + threadBindings: params.threadBindings, + }); + const data = await loadDiscordModelPickerData(params.cfg, route.agentId); + const currentModel = resolveDiscordModelPickerCurrentModel({ + cfg: params.cfg, + route, + data, + }); + const quickModels = await readDiscordModelPickerRecentModels({ + scope: resolveDiscordModelPickerPreferenceScope({ + interaction: params.interaction, + accountId: params.accountId, + userId: params.userId, + }), + allowedModelRefs: buildDiscordModelPickerAllowedModelRefs(data), + limit: 5, + }); + + const rendered = renderDiscordModelPickerModelsView({ + command: params.command, + userId: params.userId, + data, + provider: splitDiscordModelRef(currentModel ?? "")?.provider ?? data.resolvedDefault.provider, + page: 1, + providerPage: 1, + currentModel, + quickModels, + }); + const payload = { + ...toDiscordModelPickerMessagePayload(rendered), + ephemeral: true, + }; + + await params.safeInteractionCall("model picker reply", async () => { + if (params.preferFollowUp) { + await params.interaction.followUp(payload); + return; + } + await params.interaction.reply(payload); + }); +} + +function resolveModelPickerSelectionValue( + interaction: ButtonInteraction | StringSelectMenuInteraction, +): string | null { + const rawValues = (interaction as { values?: string[] }).values; + if (!Array.isArray(rawValues) || rawValues.length === 0) { + return null; + } + const first = rawValues[0]; + if (typeof first !== "string") { + return null; + } + const trimmed = first.trim(); + return trimmed || null; +} + +function buildDiscordModelPickerSelectionCommand(params: { + modelRef: string; +}): { command: ChatCommandDefinition; args: CommandArgs; prompt: string } | null { + const commandDefinition = + findCommandByNativeName("model", "discord") ?? + listChatCommands().find((entry) => entry.key === "model"); + if (!commandDefinition) { + return null; + } + const commandArgs: CommandArgs = { + values: { + model: params.modelRef, + }, + raw: params.modelRef, + }; + return { + command: commandDefinition, + args: commandArgs, + prompt: buildCommandTextFromArgs(commandDefinition, commandArgs), + }; +} + +function listDiscordModelPickerProviderModels( + data: Awaited>, + provider: string, +): string[] { + const modelSet = data.byProvider.get(provider); + if (!modelSet) { + return []; + } + return [...modelSet].toSorted(); +} + +function resolveDiscordModelPickerModelIndex(params: { + data: Awaited>; + provider: string; + model: string; +}): number | null { + const models = listDiscordModelPickerProviderModels(params.data, params.provider); + if (!models.length) { + return null; + } + const index = models.indexOf(params.model); + if (index < 0) { + return null; + } + return index + 1; +} + +function resolveDiscordModelPickerModelByIndex(params: { + data: Awaited>; + provider: string; + modelIndex?: number; +}): string | null { + if (!params.modelIndex || params.modelIndex < 1) { + return null; + } + const models = listDiscordModelPickerProviderModels(params.data, params.provider); + if (!models.length) { + return null; + } + return models[params.modelIndex - 1] ?? null; +} + +function splitDiscordModelRef(modelRef: string): { provider: string; model: string } | null { + const trimmed = modelRef.trim(); + const slashIndex = trimmed.indexOf("/"); + if (slashIndex <= 0 || slashIndex >= trimmed.length - 1) { + return null; + } + const provider = trimmed.slice(0, slashIndex).trim(); + const model = trimmed.slice(slashIndex + 1).trim(); + if (!provider || !model) { + return null; + } + return { provider, model }; +} + +export async function handleDiscordModelPickerInteraction(params: { + interaction: ButtonInteraction | StringSelectMenuInteraction; + data: ComponentData; + ctx: DiscordModelPickerContext; + safeInteractionCall: SafeDiscordInteractionCall; + dispatchCommandInteraction: DispatchDiscordCommandInteraction; +}) { + const { interaction, data, ctx } = params; + const parsed = parseDiscordModelPickerData(data); + if (!parsed) { + await params.safeInteractionCall("model picker update", () => + interaction.update( + buildDiscordModelPickerNoticePayload( + "Sorry, that model picker interaction is no longer available.", + ), + ), + ); + return; + } + + if (interaction.user?.id && interaction.user.id !== parsed.userId) { + await params.safeInteractionCall("model picker ack", () => interaction.acknowledge()); + return; + } + + const route = await resolveDiscordModelPickerRoute({ + interaction, + cfg: ctx.cfg, + accountId: ctx.accountId, + threadBindings: ctx.threadBindings, + }); + const pickerData = await loadDiscordModelPickerData(ctx.cfg, route.agentId); + const currentModelRef = resolveDiscordModelPickerCurrentModel({ + cfg: ctx.cfg, + route, + data: pickerData, + }); + const allowedModelRefs = buildDiscordModelPickerAllowedModelRefs(pickerData); + const preferenceScope = resolveDiscordModelPickerPreferenceScope({ + interaction, + accountId: ctx.accountId, + userId: parsed.userId, + }); + const quickModels = await readDiscordModelPickerRecentModels({ + scope: preferenceScope, + allowedModelRefs, + limit: 5, + }); + + if (parsed.action === "recents") { + const rendered = renderDiscordModelPickerRecentsView({ + command: parsed.command, + userId: parsed.userId, + data: pickerData, + quickModels, + currentModel: currentModelRef, + provider: parsed.provider, + page: parsed.page, + providerPage: parsed.providerPage, + }); + + await params.safeInteractionCall("model picker update", () => + interaction.update(toDiscordModelPickerMessagePayload(rendered)), + ); + return; + } + + if (parsed.action === "back" && parsed.view === "providers") { + const rendered = renderDiscordModelPickerProvidersView({ + command: parsed.command, + userId: parsed.userId, + data: pickerData, + page: parsed.page, + currentModel: currentModelRef, + }); + + await params.safeInteractionCall("model picker update", () => + interaction.update(toDiscordModelPickerMessagePayload(rendered)), + ); + return; + } + + if (parsed.action === "back" && parsed.view === "models") { + const provider = + parsed.provider ?? + splitDiscordModelRef(currentModelRef ?? "")?.provider ?? + pickerData.resolvedDefault.provider; + + const rendered = renderDiscordModelPickerModelsView({ + command: parsed.command, + userId: parsed.userId, + data: pickerData, + provider, + page: parsed.page ?? 1, + providerPage: parsed.providerPage ?? 1, + currentModel: currentModelRef, + quickModels, + }); + + await params.safeInteractionCall("model picker update", () => + interaction.update(toDiscordModelPickerMessagePayload(rendered)), + ); + return; + } + + if (parsed.action === "provider") { + const selectedProvider = resolveModelPickerSelectionValue(interaction) ?? parsed.provider; + if (!selectedProvider || !pickerData.byProvider.has(selectedProvider)) { + await params.safeInteractionCall("model picker update", () => + interaction.update( + buildDiscordModelPickerNoticePayload("Sorry, that provider isn't available anymore."), + ), + ); + return; + } + + const rendered = renderDiscordModelPickerModelsView({ + command: parsed.command, + userId: parsed.userId, + data: pickerData, + provider: selectedProvider, + page: 1, + providerPage: parsed.providerPage ?? parsed.page, + currentModel: currentModelRef, + quickModels, + }); + + await params.safeInteractionCall("model picker update", () => + interaction.update(toDiscordModelPickerMessagePayload(rendered)), + ); + return; + } + + if (parsed.action === "model") { + const selectedModel = resolveModelPickerSelectionValue(interaction); + const provider = parsed.provider; + if (!provider || !selectedModel) { + await params.safeInteractionCall("model picker update", () => + interaction.update( + buildDiscordModelPickerNoticePayload("Sorry, I couldn't read that model selection."), + ), + ); + return; + } + + const modelIndex = resolveDiscordModelPickerModelIndex({ + data: pickerData, + provider, + model: selectedModel, + }); + if (!modelIndex) { + await params.safeInteractionCall("model picker update", () => + interaction.update( + buildDiscordModelPickerNoticePayload("Sorry, that model isn't available anymore."), + ), + ); + return; + } + + const modelRef = `${provider}/${selectedModel}`; + const rendered = renderDiscordModelPickerModelsView({ + command: parsed.command, + userId: parsed.userId, + data: pickerData, + provider, + page: parsed.page, + providerPage: parsed.providerPage ?? 1, + currentModel: currentModelRef, + pendingModel: modelRef, + pendingModelIndex: modelIndex, + quickModels, + }); + + await params.safeInteractionCall("model picker update", () => + interaction.update(toDiscordModelPickerMessagePayload(rendered)), + ); + return; + } + + if (parsed.action === "submit" || parsed.action === "reset" || parsed.action === "quick") { + let modelRef: string | null = null; + + if (parsed.action === "reset") { + modelRef = `${pickerData.resolvedDefault.provider}/${pickerData.resolvedDefault.model}`; + } else if (parsed.action === "quick") { + const slot = parsed.recentSlot ?? 0; + modelRef = slot >= 1 ? (quickModels[slot - 1] ?? null) : null; + } else if (parsed.view === "recents") { + const defaultModelRef = `${pickerData.resolvedDefault.provider}/${pickerData.resolvedDefault.model}`; + const dedupedRecents = quickModels.filter((ref) => ref !== defaultModelRef); + const slot = parsed.recentSlot ?? 0; + if (slot === 1) { + modelRef = defaultModelRef; + } else if (slot >= 2) { + modelRef = dedupedRecents[slot - 2] ?? null; + } + } else { + const provider = parsed.provider; + const selectedModel = resolveDiscordModelPickerModelByIndex({ + data: pickerData, + provider: provider ?? "", + modelIndex: parsed.modelIndex, + }); + modelRef = provider && selectedModel ? `${provider}/${selectedModel}` : null; + } + const parsedModelRef = modelRef ? splitDiscordModelRef(modelRef) : null; + if ( + !parsedModelRef || + !pickerData.byProvider.get(parsedModelRef.provider)?.has(parsedModelRef.model) + ) { + await params.safeInteractionCall("model picker update", () => + interaction.update( + buildDiscordModelPickerNoticePayload( + "That selection expired. Please choose a model again.", + ), + ), + ); + return; + } + + const resolvedModelRef = `${parsedModelRef.provider}/${parsedModelRef.model}`; + + const selectionCommand = buildDiscordModelPickerSelectionCommand({ + modelRef: resolvedModelRef, + }); + if (!selectionCommand) { + await params.safeInteractionCall("model picker update", () => + interaction.update( + buildDiscordModelPickerNoticePayload("Sorry, /model is unavailable right now."), + ), + ); + return; + } + + const updateResult = await params.safeInteractionCall("model picker update", () => + interaction.update( + buildDiscordModelPickerNoticePayload(`Applying model change to ${resolvedModelRef}...`), + ), + ); + if (updateResult === null) { + return; + } + + try { + await withTimeout( + params.dispatchCommandInteraction({ + interaction, + prompt: selectionCommand.prompt, + command: selectionCommand.command, + commandArgs: selectionCommand.args, + cfg: ctx.cfg, + discordConfig: ctx.discordConfig, + accountId: ctx.accountId, + sessionPrefix: ctx.sessionPrefix, + preferFollowUp: true, + threadBindings: ctx.threadBindings, + suppressReplies: true, + }), + 12000, + ); + } catch (error) { + if (error instanceof Error && error.message === "timeout") { + await params.safeInteractionCall("model picker follow-up", () => + interaction.followUp({ + ...buildDiscordModelPickerNoticePayload( + `⏳ Model change to ${resolvedModelRef} is still processing. Check /status in a few seconds.`, + ), + ephemeral: true, + }), + ); + return; + } + + await params.safeInteractionCall("model picker follow-up", () => + interaction.followUp({ + ...buildDiscordModelPickerNoticePayload( + `❌ Failed to apply ${resolvedModelRef}. Try /model ${resolvedModelRef} directly.`, + ), + ephemeral: true, + }), + ); + return; + } + + await new Promise((resolve) => setTimeout(resolve, 250)); + + const effectiveModelRef = resolveDiscordModelPickerCurrentModel({ + cfg: ctx.cfg, + route, + data: pickerData, + }); + const persisted = effectiveModelRef === resolvedModelRef; + + if (!persisted) { + logVerbose( + `discord: model picker override mismatch — expected ${resolvedModelRef} but read ${effectiveModelRef} from session key ${route.sessionKey}`, + ); + } + + if (persisted) { + await recordDiscordModelPickerRecentModel({ + scope: preferenceScope, + modelRef: resolvedModelRef, + limit: 5, + }).catch(() => undefined); + } + + await params.safeInteractionCall("model picker follow-up", () => + interaction.followUp({ + ...buildDiscordModelPickerNoticePayload( + persisted + ? `✅ Model set to ${resolvedModelRef}.` + : `⚠️ Tried to set ${resolvedModelRef}, but current model is ${effectiveModelRef}.`, + ), + ephemeral: true, + }), + ); + return; + } + + if (parsed.action === "cancel") { + const displayModel = currentModelRef ?? "default"; + await params.safeInteractionCall("model picker update", () => + interaction.update(buildDiscordModelPickerNoticePayload(`ℹ️ Model kept as ${displayModel}.`)), + ); + } +} + +export async function handleDiscordCommandArgInteraction(params: { + interaction: ButtonInteraction; + data: ComponentData; + ctx: DiscordCommandArgContext; + safeInteractionCall: SafeDiscordInteractionCall; + dispatchCommandInteraction: DispatchDiscordCommandInteraction; +}) { + const { interaction, data, ctx } = params; + const parsed = parseDiscordCommandArgData(data); + if (!parsed) { + await params.safeInteractionCall("command arg update", () => + interaction.update({ + content: "Sorry, that selection is no longer available.", + components: [], + }), + ); + return; + } + if (interaction.user?.id && interaction.user.id !== parsed.userId) { + await params.safeInteractionCall("command arg ack", () => interaction.acknowledge()); + return; + } + const commandDefinition = + findCommandByNativeName(parsed.command, "discord") ?? + listChatCommands().find((entry) => entry.key === parsed.command); + if (!commandDefinition) { + await params.safeInteractionCall("command arg update", () => + interaction.update({ + content: "Sorry, that command is no longer available.", + components: [], + }), + ); + return; + } + const argUpdateResult = await params.safeInteractionCall("command arg update", () => + interaction.update({ + content: `✅ Selected ${parsed.value}.`, + components: [], + }), + ); + if (argUpdateResult === null) { + return; + } + const commandArgs = createCommandArgsWithValue({ + argName: parsed.arg, + value: parsed.value, + }); + const commandArgsWithRaw: CommandArgs = { + ...commandArgs, + raw: serializeCommandArgs(commandDefinition, commandArgs), + }; + const prompt = buildCommandTextFromArgs(commandDefinition, commandArgsWithRaw); + await params.dispatchCommandInteraction({ + interaction, + prompt, + command: commandDefinition, + commandArgs: commandArgsWithRaw, + cfg: ctx.cfg, + discordConfig: ctx.discordConfig, + accountId: ctx.accountId, + sessionPrefix: ctx.sessionPrefix, + preferFollowUp: true, + threadBindings: ctx.threadBindings, + }); +} + +class DiscordCommandArgButton extends Button { + label: string; + customId: string; + style = ButtonStyle.Secondary; + private ctx: DiscordCommandArgContext; + private safeInteractionCall: SafeDiscordInteractionCall; + private dispatchCommandInteraction: DispatchDiscordCommandInteraction; + + constructor(params: { + label: string; + customId: string; + ctx: DiscordCommandArgContext; + safeInteractionCall: SafeDiscordInteractionCall; + dispatchCommandInteraction: DispatchDiscordCommandInteraction; + }) { + super(); + this.label = params.label; + this.customId = params.customId; + this.ctx = params.ctx; + this.safeInteractionCall = params.safeInteractionCall; + this.dispatchCommandInteraction = params.dispatchCommandInteraction; + } + + async run(interaction: ButtonInteraction, data: ComponentData) { + await handleDiscordCommandArgInteraction({ + interaction, + data, + ctx: this.ctx, + safeInteractionCall: this.safeInteractionCall, + dispatchCommandInteraction: this.dispatchCommandInteraction, + }); + } +} + +export function buildDiscordCommandArgMenu(params: { + command: ChatCommandDefinition; + menu: { + arg: CommandArgDefinition; + choices: Array<{ value: string; label: string }>; + title?: string; + }; + interaction: CommandInteraction; + ctx: DiscordCommandArgContext; + safeInteractionCall: SafeDiscordInteractionCall; + dispatchCommandInteraction: DispatchDiscordCommandInteraction; +}): { content: string; components: Row