Compare commits

..

1 Commits

Author SHA1 Message Date
Tyler Yust
793ad7d557 Fix BlueBubbles command output chunking 2026-03-25 23:49:11 -07:00
1042 changed files with 18779 additions and 31841 deletions

View File

@@ -17,11 +17,6 @@ Use this skill for Parallels guest workflows and smoke interpretation. Do not lo
- Per-phase logs land under `/tmp/openclaw-parallels-*`.
- Do not run local and gateway agent turns in parallel on the same fresh workspace or session.
- For `prlctl exec`, pass the VM name before `--current-user` (`prlctl exec "$VM" --current-user ...`), not the other way around.
- If the workflow installs OpenClaw from a repo checkout instead of the site installer/npm release, finish by installing a real guest CLI shim and verifying it in a fresh guest shell. `pnpm openclaw ...` inside the repo is not enough for handoff parity.
- On macOS guests, prefer a user-global install plus a stable PATH-visible shim:
- install with `NPM_CONFIG_PREFIX="$HOME/.npm-global" npm install -g .`
- make sure `~/.local/bin/openclaw` exists or `~/.npm-global/bin` is on PATH
- verify from a brand-new guest shell with `which openclaw` and `openclaw --version`
## npm install then update

View File

@@ -9,8 +9,6 @@ body:
value: |
Thanks for filing this report. Keep every answer concise, reproducible, and grounded in observed evidence.
Do not speculate or infer beyond the evidence. If a narrative section cannot be answered from the available evidence, respond with exactly `NOT_ENOUGH_INFO`.
If this is a plugin beta-release blocker, rename the issue title to `Beta blocker: <plugin-name> - <summary>` and apply the `beta-blocker` label after filing.
- type: dropdown
id: bug_type
attributes:
@@ -22,19 +20,6 @@ body:
- Behavior bug (incorrect output/state without crash)
validations:
required: true
- type: dropdown
id: beta_blocker
attributes:
label: Beta release blocker
description: >
Choose `Yes` only if this blocks plugin compatibility during the current beta release window.
Selecting `Yes` does not apply the label automatically. You must also rename the issue title
to `Beta blocker: <plugin-name> - <summary>` for the automation to apply the `beta-blocker` label.
options:
- "No"
- "Yes"
validations:
required: true
- type: textarea
id: summary
attributes:

4
.github/labeler.yml vendored
View File

@@ -221,6 +221,10 @@
- changed-files:
- any-glob-to-any-file:
- "extensions/open-prose/**"
"extensions: qwen-portal-auth":
- changed-files:
- any-glob-to-any-file:
- "extensions/qwen-portal-auth/**"
"extensions: device-pair":
- changed-files:
- any-glob-to-any-file:

View File

@@ -2,8 +2,6 @@
Describe the problem and fix in 25 bullets:
If this PR fixes a plugin beta-release blocker, title it `fix(<plugin-id>): beta blocker - <summary>` and link the matching `Beta blocker: <plugin-name> - <summary>` issue labeled `beta-blocker`. Contributors cannot label PRs, so the title is the PR-side signal for maintainers and automation.
- Problem:
- Why it matters:
- What changed:
@@ -65,18 +63,6 @@ For bug fixes or regressions, name the smallest reliable test coverage that shou
List user-visible changes (including defaults/config).
If none, write `None`.
## Diagram (if applicable)
For UI changes or non-trivial logic flows, include a small ASCII diagram reviewers can scan quickly. Otherwise write `N/A`.
```text
Before:
[user action] -> [old state]
After:
[user action] -> [new state] -> [result]
```
## Security Impact (required)
- New permissions/capabilities? (`Yes/No`)
@@ -141,6 +127,12 @@ If a bot review conversation is addressed by this PR, resolve that conversation
- Migration needed? (`Yes/No`)
- If yes, exact upgrade steps:
## Failure Recovery (if this breaks)
- How to disable/revert this change quickly:
- Files/config to restore:
- Known bad symptoms reviewers should watch for:
## Risks and Mitigations
List only real risks for this PR. Add/remove entries as needed. If none, write `None`.

View File

@@ -2,6 +2,8 @@ name: Docker Release
on:
push:
branches:
- main
tags:
- "v*"
paths-ignore:

View File

@@ -2,9 +2,9 @@ name: Labeler
on:
pull_request_target: # zizmor: ignore[dangerous-triggers] maintainer-owned triage workflow; no untrusted checkout or PR code execution
types: [opened, synchronize, reopened, edited]
types: [opened, synchronize, reopened]
issues:
types: [opened, edited]
types: [opened]
workflow_dispatch:
inputs:
max_prs:
@@ -209,59 +209,6 @@ jobs:
// labels: [trustedLabel],
// });
// }
- name: Apply beta-blocker title label
uses: actions/github-script@v8
with:
github-token: ${{ steps.app-token.outputs.token || steps.app-token-fallback.outputs.token }}
script: |
const pullRequest = context.payload.pull_request;
if (!pullRequest) {
return;
}
const labelName = "beta-blocker";
const matchesBetaBlocker = /\bbeta blocker\b/i.test(pullRequest.title ?? "");
try {
await github.rest.issues.getLabel({
owner: context.repo.owner,
repo: context.repo.repo,
name: labelName,
});
} catch (error) {
if (error?.status !== 404) {
throw error;
}
core.info(`Skipping ${labelName} labeling because the label does not exist in the repository.`);
return;
}
const currentLabels = await github.paginate(github.rest.issues.listLabelsOnIssue, {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pullRequest.number,
per_page: 100,
});
const hasLabel = currentLabels.some((label) => label.name === labelName);
if (matchesBetaBlocker && !hasLabel) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pullRequest.number,
labels: [labelName],
});
return;
}
if (!matchesBetaBlocker && hasLabel) {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pullRequest.number,
name: labelName,
});
}
- name: Apply too-many-prs label
uses: actions/github-script@v8
with:
@@ -472,7 +419,6 @@ jobs:
const maxCount = processAll ? Number.POSITIVE_INFINITY : Math.max(1, maxPrs);
const sizeLabels = ["size: XS", "size: S", "size: M", "size: L", "size: XL"];
const betaBlockerLabel = "beta-blocker";
const labelColor = "b76e79";
// const trustedLabel = "trusted-contributor";
// const experiencedLabel = "experienced-contributor";
@@ -503,22 +449,6 @@ jobs:
}
}
async function hasBetaBlockerLabel() {
try {
await github.rest.issues.getLabel({
owner,
repo,
name: betaBlockerLabel,
});
return true;
} catch (error) {
if (error?.status !== 404) {
throw error;
}
return false;
}
}
async function resolveContributorLabel(login) {
if (contributorCache.has(login)) {
return contributorCache.get(login);
@@ -650,37 +580,7 @@ jobs:
labelNames.add(label);
}
async function applyBetaBlockerTitleLabel(pullRequest, labelNames) {
const matchesBetaBlocker = /\bbeta blocker\b/i.test(pullRequest.title ?? "");
if (matchesBetaBlocker) {
if (!labelNames.has(betaBlockerLabel)) {
await github.rest.issues.addLabels({
owner,
repo,
issue_number: pullRequest.number,
labels: [betaBlockerLabel],
});
labelNames.add(betaBlockerLabel);
}
return;
}
if (!labelNames.has(betaBlockerLabel)) {
return;
}
await github.rest.issues.removeLabel({
owner,
repo,
issue_number: pullRequest.number,
name: betaBlockerLabel,
});
labelNames.delete(betaBlockerLabel);
}
await ensureSizeLabels();
const betaBlockerLabelExists = await hasBetaBlockerLabel();
let page = 1;
let processed = 0;
@@ -718,9 +618,6 @@ jobs:
await applySizeLabel(pullRequest, currentLabels, labelNames);
await applyContributorLabel(pullRequest, labelNames);
if (betaBlockerLabelExists) {
await applyBetaBlockerTitleLabel(pullRequest, labelNames);
}
processed += 1;
}
@@ -822,56 +719,3 @@ jobs:
// labels: [trustedLabel],
// });
// }
- name: Apply beta-blocker title label
uses: actions/github-script@v8
with:
github-token: ${{ steps.app-token.outputs.token || steps.app-token-fallback.outputs.token }}
script: |
const issue = context.payload.issue;
if (!issue || issue.pull_request) {
return;
}
const labelName = "beta-blocker";
const matchesBetaBlocker = /^beta blocker:/i.test(issue.title ?? "");
try {
await github.rest.issues.getLabel({
owner: context.repo.owner,
repo: context.repo.repo,
name: labelName,
});
} catch (error) {
if (error?.status !== 404) {
throw error;
}
core.info(`Skipping ${labelName} labeling because the label does not exist in the repository.`);
return;
}
const currentLabels = await github.paginate(github.rest.issues.listLabelsOnIssue, {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
per_page: 100,
});
const hasLabel = currentLabels.some((label) => label.name === labelName);
if (matchesBetaBlocker && !hasLabel) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
labels: [labelName],
});
return;
}
if (!matchesBetaBlocker && hasLabel) {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
name: labelName,
});
}

View File

@@ -6,61 +6,36 @@ Docs: https://docs.openclaw.ai
### Breaking
- Providers/Qwen: remove the deprecated `qwen-portal-auth` OAuth integration for `portal.qwen.ai`; migrate to Model Studio with `openclaw onboard --auth-choice modelstudio-api-key`. (#52709) Thanks @pomelo-nwu.
### Changes
- MiniMax: add image generation provider for `image-01` model, supporting generate and image-to-image editing with aspect ratio control. (#54487) Thanks @liyuan97.
- Slack/tool actions: add an explicit `upload-file` Slack action that routes file uploads through the existing Slack upload transport, with optional filename/title/comment overrides for channels and DMs.
- Plugins/Matrix TTS: send auto-TTS replies as native Matrix voice bubbles instead of generic audio attachments. (#37080) thanks @Matthew19990919.
- Memory/plugins: move the pre-compaction memory flush plan behind the active memory plugin contract so `memory-core` owns flush prompts and target-path policy instead of hardcoded core logic.
- MiniMax: trim model catalog to M2.7 only, removing legacy M2, M2.1, M2.5, and VL-01 models. (#54487) Thanks @liyuan97.
- CLI: add `openclaw config schema` to print the generated JSON schema for `openclaw.json`. (#54523) Thanks @kvokka.
- Plugins/runtime: expose `runHeartbeatOnce` in the plugin runtime `system` namespace so plugins can trigger a single heartbeat cycle with an explicit delivery target override (e.g. `heartbeat: { target: "last" }`). (#40299) Thanks @loveyana.
- Agents/compaction: preserve the post-compaction AGENTS refresh on stale-usage preflight compaction for both immediate replies and queued followups. (#49479) Thanks @jared596.
- Agents/compaction: surface safeguard-specific cancel reasons and relabel benign manual `/compact` no-op cases as skipped instead of failed. (#51072) Thanks @afurm.
- Plugins/CLI backends: move bundled Claude CLI, Codex CLI, and Gemini CLI inference defaults onto the plugin surface, add bundled Gemini CLI backend support, and replace `gateway run --claude-cli-logs` with generic `--cli-backend-logs` while keeping the old flag as a compatibility alias.
- Plugins/startup: auto-load bundled provider and CLI-backend plugins from explicit config refs, so bundled Claude CLI, Codex CLI, and Gemini CLI message-provider setups no longer need manual `plugins.allow` entries.
- Agents/compaction: preserve the post-compaction AGENTS refresh on stale-usage preflight compaction for both immediate replies and queued followups. (#49479) Thanks @jared596.
- CLI: add `openclaw config schema` to print the generated JSON schema for `openclaw.json`. (#54523) Thanks @kvokka.
### Fixes
- WhatsApp: fix infinite echo loop in self-chat DM mode where the bot's own outbound replies were re-processed as new inbound user messages. (#54570) Thanks @joelnishanth
- OpenAI Codex/image tools: register Codex for media understanding and route image prompts through Codex instructions so image analysis no longer fails on missing provider registration or missing `instructions`. (#54829) Thanks @neeravmakwana.
- Agents/image tool: restore the generic image-runtime fallback when no provider-specific media-understanding provider is registered, so image analysis works again for providers like `openrouter` and `minimax-portal`. (#54858) Thanks @MonkeyLeeT.
- Telegram: deliver verbose tool summaries inside forum topic sessions again, so threaded topic chats now match DM verbose behavior. (#43236) Thanks @frankbuild.
- BlueBubbles/CLI agents: restore inbound prompt image refs for CLI routed turns, reapply embedded runner image size guardrails, and cover both CLI image transport paths with regression tests. (#51373)
- BlueBubbles/groups: optionally enrich unnamed participant lists with local macOS Contacts names after group gating passes, so group member context can show names instead of only raw phone numbers.
- Discord/reconnect: drain stale gateway sockets, clear cached resume state before forced fresh reconnects, and fail closed when old sockets refuse to die so Discord recovery stops looping on poisoned resume state. (#54697) Thanks @ngutman.
- iMessage: stop leaking inline `[[reply_to:...]]` tags into delivered text by sending `reply_to` as RPC metadata and stripping stray directive tags from outbound messages. (#39512) Thanks @mvanhorn.
- CLI/plugins: make routed commands use the same auto-enabled bundled-channel snapshot as gateway startup, so configured bundled channels like Slack load without requiring a prior config rewrite. (#54809) Thanks @neeravmakwana.
- CLI/message send: write manual `openclaw message send` deliveries into the resolved agent session transcript again by always threading the default CLI agent through outbound mirroring. (#54187) Thanks @KevInTheCloud5617.
- CLI/onboarding: show the Kimi Code API key option again in the Moonshot setup menu so the interactive picker includes all Kimi setup paths together. Fixes #54412 Thanks @sparkyrider
- Agents/status: use provider-aware context window lookup for fresh Anthropic 4.6 model overrides so `/status` shows the correct 1.0m window instead of an underreported shared-cache minimum. (#54796) Thanks @neeravmakwana.
- Agents/errors: surface provider quota/reset details when available, but keep HTML/Cloudflare rate-limit pages on the generic fallback so raw error pages are not shown to users. (#54512) Thanks @bugkill3r.
- Agents/embedded replies: surface mid-turn 429 and overload failures when embedded runs end without a user-visible reply, while preserving successful media-only replies that still use legacy `mediaUrl`. (#50930) Thanks @infichen.
- WhatsApp/allowFrom: show a specific allowFrom policy error for valid blocked targets instead of the misleading `<E.164|group JID>` format hint. Thanks @mcaxtr.
- Agents/cooldowns: scope rate-limit cooldowns per model so one 429 no longer blocks every model on the same auth profile, replace the exponential 1 min -> 1 h escalation with a stepped 30 s / 1 min / 5 min ladder, and surface a user-facing countdown message when all models are rate-limited. (#49834) Thanks @kiranvk-2011.
- Agents/embedded transport errors: distinguish common network failures like connection refused, DNS lookup failure, and interrupted sockets from true timeouts in embedded-run user messaging and lifecycle diagnostics. (#51419) Thanks @scoootscooob.
- Telegram/pairing: ignore self-authored DM `message` updates so bot-pinned status cards and similar service updates do not trigger bogus pairing requests or re-enter inbound dispatch. (#54530) thanks @huntharo
- Mattermost/replies: keep pairing replies, slash-command fallback replies, and model-picker messages on the resolved config path so `exec:` SecretRef bot tokens work across all outbound reply branches. (#48347) thanks @mathiasnagler.
- Microsoft Teams/config: accept the existing `welcomeCard`, `groupWelcomeCard`, `promptStarters`, and feedback/reflection keys in strict config validation so already-supported Teams runtime settings stop failing schema checks. (#54679) Thanks @gumclaw.
- Plugins/SDK: thread `moduleUrl` through plugin-sdk alias resolution so user-installed plugins outside the openclaw directory (e.g. `~/.openclaw/extensions/`) correctly resolve `openclaw/plugin-sdk/*` subpath imports, and gate `plugin-sdk:check-exports` in `release:check`. (#54283) Thanks @xieyongliang.
- Config/web fetch: allow the documented `tools.web.fetch.maxResponseBytes` setting in runtime schema validation so valid configs no longer fail with unrecognized-key errors. (#53401) Thanks @erhhung.
- Message tool/buttons: keep the shared `buttons` schema optional in merged tool definitions so plain `action=send` calls stop failing validation when no buttons are provided. (#54418) Thanks @adzendo.
- Agents/openai-compatible tool calls: deduplicate repeated tool call ids across live assistant messages and replayed history so OpenAI-compatible backends no longer reject duplicate `tool_call_id` values with HTTP 400. (#40996) Thanks @xaeon2026.
- Models/openai-completions: default non-native OpenAI-compatible providers to omit tool-definition `strict` fields unless users explicitly opt back in, so tool calling keeps working on providers that reject that option. (#45497) Thanks @sahancava.
- Plugins/context engines: retry strict legacy `assemble()` calls without the new `prompt` field when older engines reject it, preserving prompt-aware retrieval compatibility for pre-prompt plugins. (#50848) thanks @danhdoan.
- CLI/update status: explicitly say `up to date` when the local version already matches npm latest, while keeping the availability logic unchanged. (#51409) Thanks @dongzhenye.
- Daemon/Linux: stop flagging non-gateway systemd services as duplicate gateways just because their unit files mention OpenClaw, reducing false-positive doctor/log noise. (#45328) Thanks @gregretkowski.
- Feishu: close WebSocket connections on monitor stop/abort so ghost connections no longer persist, preventing duplicate event processing and resource leaks across restart cycles. (#52844) Thanks @schumilin.
- Feishu: use the original message `create_time` instead of `Date.now()` for inbound timestamps so offline-retried messages carry the correct authoring time, preventing mis-targeted agent actions on stale instructions. (#52809) Thanks @schumilin.
- Agents/sandbox: honor `tools.sandbox.tools.alsoAllow`, let explicit sandbox re-allows remove matching built-in default-deny tools, and keep sandbox explain/error guidance aligned with the effective sandbox tool policy. (#54492) Thanks @ngutman.
- Agents/sandbox: make blocked-tool guidance glob-aware again, redact/sanitize session-specific explain hints for safer copy-paste, and avoid leaking control-character session keys in those hints. (#54684) Thanks @ngutman.
- Feishu: close WebSocket connections on monitor stop/abort so ghost connections no longer persist, preventing duplicate event processing and resource leaks across restart cycles. (#52844) Thanks @schumilin.
- Feishu: use the original message `create_time` instead of `Date.now()` for inbound timestamps so offline-retried messages carry the correct authoring time, preventing mis-targeted agent actions on stale instructions. (#52809) Thanks @schumilin.
- Daemon/Linux: stop flagging non-gateway systemd services as duplicate gateways just because their unit files mention OpenClaw, reducing false-positive doctor/log noise. (#45328) Thanks @gregretkowski.
- Plugins/SDK: thread `moduleUrl` through plugin-sdk alias resolution so user-installed plugins outside the openclaw directory (e.g. `~/.openclaw/extensions/`) correctly resolve `openclaw/plugin-sdk/*` subpath imports, and gate `plugin-sdk:check-exports` in `release:check`. (#54283) Thanks @xieyongliang.
- Telegram/pairing: ignore self-authored DM `message` updates so bot-pinned status cards and similar service updates do not trigger bogus pairing requests or re-enter inbound dispatch. (#54530) thanks @huntharo
- iMessage: stop leaking inline `[[reply_to:...]]` tags into delivered text by sending `reply_to` as RPC metadata and stripping stray directive tags from outbound messages. (#39512) Thanks @mvanhorn.
- Agents/embedded replies: surface mid-turn 429 and overload failures when embedded runs end without a user-visible reply, while preserving successful media-only replies that still use legacy `mediaUrl`. (#50930) Thanks @infichen.
- Agents/image tool: restore the generic image-runtime fallback when no provider-specific media-understanding provider is registered, so image analysis works again for providers like `openrouter` and `minimax-portal`. (#54858) Thanks @MonkeyLeeT.
- Agents/compaction: trigger timeout recovery compaction before retrying high-context LLM timeouts so embedded runs stop repeating oversized requests. (#46417) thanks @joeykrug.
- Agents/compaction: reconcile `sessions.json.compactionCount` after a late embedded auto-compaction success so persisted session counts catch up once the handler reports completion. (#45493) Thanks @jackal092927.
- Agents/failover: classify Codex accountId token extraction failures as auth errors so model fallback continues to the next configured candidate. (#55206) Thanks @cosmicnet.
- Talk/macOS: stop direct system-voice failures from replaying system speech, use app-locale fallback for shared watchdog timing, and add regression coverage for the macOS fallback route and language-aware timeout policy. (#53511) thanks @hongsw.
- Discord/gateway cleanup: keep late Carbon reconnect-exhausted errors suppressed through startup/dispose cleanup so Discord monitor shutdown no longer crashes on late gateway close events. (#55373) Thanks @Takhoffman.
- Microsoft Teams/config: accept the existing `welcomeCard`, `groupWelcomeCard`, `promptStarters`, and feedback/reflection keys in strict config validation so already-supported Teams runtime settings stop failing schema checks. (#54679) Thanks @gumclaw.
- CLI/plugins: make routed commands use the same auto-enabled bundled-channel snapshot as gateway startup, so configured bundled channels like Slack load without requiring a prior config rewrite. (#54809) Thanks @neeravmakwana.
- Agents/status: use provider-aware context window lookup for fresh Anthropic 4.6 model overrides so `/status` shows the correct 1.0m window instead of an underreported shared-cache minimum. (#54796) Thanks @neeravmakwana.
- CLI/message send: write manual `openclaw message send` deliveries into the resolved agent session transcript again by always threading the default CLI agent through outbound mirroring. (#54187) Thanks @KevInTheCloud5617.
- CLI/onboarding: show the Kimi Code API key option again in the Moonshot setup menu so the interactive picker includes all Kimi setup paths together. Fixes #54412 Thanks @sparkyrider
- WhatsApp: fix infinite echo loop in self-chat DM mode where the bot's own outbound replies were re-processed as new inbound user messages. (#54570) Thanks @joelnishanth
## 2026.3.24
@@ -104,6 +79,7 @@ Docs: https://docs.openclaw.ai
- Telegram/outbound errors: preserve actionable 403 membership/block/kick details and treat `bot not a member` as a permanent delivery failure so Telegram sends stop retrying doomed chats. (#53635) Thanks @w-sss.
- Telegram/photos: preflight Telegram photo dimension and aspect-ratio rules, and fall back to document sends when image metadata is invalid or unavailable so photo uploads stop failing with `PHOTO_INVALID_DIMENSIONS`. (#52545) Thanks @hnshah.
- Slack/runtime defaults: trim Slack DM reply overhead, restore Codex auto transport, and tighten Slack/web-search runtime defaults around DM preview threading, cache scoping, warning dedupe, and explicit web-search opt-in. (#53957) Thanks @vincentkoc.
- WhatsApp/allowFrom: show a specific allowFrom policy error for valid blocked targets instead of the misleading `<E.164|group JID>` format hint. Thanks @mcaxtr.
## 2026.3.24-beta.2
@@ -183,6 +159,9 @@ Docs: https://docs.openclaw.ai
- Discord/config types: add missing `autoArchiveDuration` to `DiscordGuildChannelConfig` so TypeScript config definitions match the existing schema and runtime support. (#43427) Thanks @davidguttman.
- Docs/IRC: fix five `json55` code-fence typos in the IRC channel examples so Mintlify applies JSON5 syntax highlighting correctly. (#50842) Thanks @Hollychou924.
- Discord/commands: trim overlong slash-command descriptions to Discord's 100-character limit and map rejected deploy indexes from Discord validation payloads back to command names/descriptions, so deploys stop failing on long descriptions and startup logs identify the rejected commands. (#54118) thanks @huntharo
- Agents/cooldowns: scope rate-limit cooldowns per model so one 429 no longer blocks every model on the same auth profile, replace the exponential 1 min → 1 h escalation with a stepped 30 s / 1 min / 5 min ladder, and surface a user-facing countdown message when all models are rate-limited. (#49834) Thanks @kiranvk-2011.
- Config/web fetch: allow the documented `tools.web.fetch.maxResponseBytes` setting in runtime schema validation so valid configs no longer fail with unrecognized-key errors. (#53401) Thanks @erhhung.
- Message tool/buttons: keep the shared `buttons` schema optional in merged tool definitions so plain `action=send` calls stop failing validation when no buttons are provided. (#54418) Thanks @adzendo.
## 2026.3.23
@@ -432,16 +411,6 @@ Docs: https://docs.openclaw.ai
- Security/network: harden explicit-proxy SSRF pinning by translating target-hop transport hints onto HTTPS proxy tunnels and failing closed for plain HTTP guarded fetches that cannot preserve pinned DNS.
- Security/Synology Chat: require explicit per-account webhook paths for multi-account setups by default, reject duplicate exact webhook paths fail-closed, and keep inherited-path behavior behind an explicit dangerous opt-in so shared routes can no longer collapse DM policy contexts across accounts. Thanks @tdjackey for reporting.
- Browser/remote CDP: honor strict browser SSRF policy during remote CDP reachability and `/json/version` discovery checks, redact sensitive `cdpUrl` tokens from status output, and warn when remote CDP targets private/internal hosts.
- Telegram/replies: set `allow_sending_without_reply` on reply-targeted sends and media-error notices so deleted parent messages no longer drop otherwise valid replies. (#52524) Thanks @moltbot886.
- Gateway/status: resolve env-backed `gateway.auth.*` SecretRefs before read-only probe auth checks so status no longer reports false probe failures when auth is configured through SecretRef. (#52513) Thanks @CodeForgeNet.
- Agents/exec: return plain-text failed tool output for timeouts and other non-success exec outcomes so models no longer parrot raw JSON error payloads back to users. (#52508) Thanks @martingarramon.
- CLI/startup: lazy-load channel add and root help startup paths to trim avoidable RSS and help latency on constrained hosts. (#46784) Thanks @vincentkoc.
- CLI/onboarding: import static provider definitions directly for onboarding model/config helpers so those paths no longer pull provider discovery just for built-in defaults. (#47467) Thanks @vincentkoc.
- CLI/configure: clarify fresh-setup memory-search warnings so they say semantic recall needs at least one embedding provider, and scope the initial model allowlist picker to the provider selected in configure. Thanks @vincentkoc.
- CLI/auth choice: lazy-load plugin/provider fallback resolution so mapped auth choices stay on the static path and only unknown choices pay the heavy provider load. (#47495) Thanks @vincentkoc.
- CLI: avoid loading provider discovery during startup model normalization. (#46522) Thanks @ItsAditya-xyz and @vincentkoc.
- CLI/status: keep `status --json` stdout clean by skipping plugin compatibility scans that were not rendered in the JSON payload. (#52449) Thanks @cgdusek.
- Agents/Telegram: avoid rebuilding the full model catalog on ordinary inbound replies so Telegram message handling no longer pays multi-second core startup latency before reply generation. Thanks @vincentkoc.
- Media/security: bound remote-media error-body snippets with the same streaming caps and idle timeouts as successful downloads, so malicious HTTP error responses cannot force unbounded buffering before OpenClaw throws.
- Gateway/auth: ignore spoofed loopback hops in trusted forwarding chains and block device approvals that request scopes above the caller session. (#46800) Thanks @vincentkoc.
- 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.
@@ -473,6 +442,8 @@ Docs: https://docs.openclaw.ai
- macOS/canvas actions: keep unattended local agent actions on trusted in-app canvas surfaces only, and stop exposing the deep-link fallback key to arbitrary page scripts. (#46790) Thanks @vincentkoc.
- Agents/compaction: extend the enclosing run deadline once while compaction is actively in flight, and abort the underlying SDK compaction on timeout/cancel so large-session compactions stop freezing mid-run. (#46889) Thanks @asyncjason.
- Gateway/Telegram shutdown: abort stalled Telegram polling fetches on shutdown, clean up per-cycle abort listeners, and keep the in-process watchdog ahead of supervisor stop timeouts so SIGTERM no longer leaves zombie gateways behind. (#51242) Thanks @juliabush.
- Agents/openai-compatible tool calls: deduplicate repeated tool call ids across live assistant messages and replayed history so OpenAI-compatible backends no longer reject duplicate `tool_call_id` values with HTTP 400. (#40996) Thanks @xaeon2026.
- Models/openai-completions: default non-native OpenAI-compatible providers to omit tool-definition `strict` fields unless users explicitly opt back in, so tool calling keeps working on providers that reject that option. (#45497) Thanks @sahancava.
- Telegram/setup: warn when setup leaves DMs on pairing without an allowlist, and show valid account-scoped remediation commands. (#50710) Thanks @ernestodeoliveira.
- Doctor/Telegram: replace the fresh-install empty group-allowlist false positive with first-run guidance that explains DM pairing approval and the next group setup steps, so new Telegram installs get actionable setup help instead of a broken-config warning. Thanks @vincentkoc.
- Doctor/extensions: keep Matrix DM `allowFrom` repairs on the canonical `dm.allowFrom` path and stop treating Zalouser group sender gating as if it fell back to `allowFrom`, so doctor warnings and `--fix` stay aligned with runtime access control. Thanks @vincentkoc.
@@ -525,6 +496,8 @@ Docs: https://docs.openclaw.ai
- Exec: harden host env override handling across gateway and node (#51207) Thanks @gladiator9797 and @joshavant.
- Voice Call: enforce spoken-output contract and fix stream TTS silence regression (#51500) Thanks @joshavant.
- xAI/models: rename the bundled Grok 4.20 catalog entries to the GA IDs and normalize saved deprecated beta IDs at runtime so existing configs and sessions keep resolving. (#50772) thanks @Jaaneek
- Plugins/Matrix TTS: send auto-TTS replies as native Matrix voice bubbles instead of generic audio attachments. (#37080) thanks @Matthew19990919.
- Plugins/Matrix TTS: send auto-TTS replies as native Matrix voice bubbles instead of generic audio attachments. (#37080) thanks @Matthew19990919.
- Agents/bootstrap warnings: move bootstrap truncation warnings out of the system prompt and into the per-turn prompt body so prompt-cache reuse stays stable when truncation warnings appear or disappear. (#48753) Thanks @scoootscooob and @obviyus.
- 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.
- ACP/configured bindings: reinitialize configured ACP sessions that are stuck in `error` state instead of reusing the failed runtime.
@@ -544,6 +517,9 @@ Docs: https://docs.openclaw.ai
- Telegram/routing: fail loud when `message send` targets an unknown non-default Telegram `accountId`, instead of silently falling back to the channel-level bot token and sending through the wrong bot. (#50853) Thanks @hclsys.
- Web search: align onboarding, configure, and finalize with plugin-owned provider contracts, including disabled-provider recovery, config-aware credential hooks, and runtime-visible summaries. (#50935) Thanks @gumadeiras.
- Agents/replay: sanitize malformed assistant tool-call replay blocks before provider replay so follow-up Anthropic requests do not inherit the downstream `replace` crash. (#50005) Thanks @jalehman.
- Plugins/context engines: retry strict legacy `assemble()` calls without the new `prompt` field when older engines reject it, preserving prompt-aware retrieval compatibility for pre-prompt plugins. (#50848) thanks @danhdoan.
- CLI/update status: explicitly say `up to date` when the local version already matches npm latest, while keeping the availability logic unchanged. (#51409) Thanks @dongzhenye.
- Agents/embedded transport errors: distinguish common network failures like connection refused, DNS lookup failure, and interrupted sockets from true timeouts in embedded-run user messaging and lifecycle diagnostics. (#51419) Thanks @scoootscooob.
- Discord/startup logging: report client initialization while the gateway is still connecting instead of claiming Discord is logged in before readiness is reached. (#51425) Thanks @scoootscoob.
- Agents/compaction safeguard: preserve split-turn context and preserved recent turns when capped retry fallback reuses the last successful summary. (#27727) thanks @Pandadadadazxf.
- Agents/memory flush: keep transcript-hash dedup active across memory-flush fallback retries so a write-then-throw flush attempt cannot append duplicate `MEMORY.md` entries before the fallback cycle completes. (#34222) Thanks @lml2468.
@@ -582,6 +558,8 @@ Docs: https://docs.openclaw.ai
### Fixes
- Agents/edit tool: accept common path/text alias spellings, show current file contents on exact-match failures, and avoid false edit failures after successful writes. (#52516) thanks @mbelinky.
- Agents/compaction: reconcile `sessions.json.compactionCount` after a late embedded auto-compaction success so persisted session counts catch up once the handler reports completion. (#45493) Thanks @jackal092927.
- Mattermost/replies: keep pairing replies, slash-command fallback replies, and model-picker messages on the resolved config path so `exec:` SecretRef bot tokens work across all outbound reply branches. (#48347) thanks @mathiasnagler.
## 2026.3.13

View File

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

View File

@@ -1,8 +1,8 @@
// Shared iOS version defaults.
// Generated overrides live in build/Version.xcconfig (git-ignored).
OPENCLAW_GATEWAY_VERSION = 2026.3.25
OPENCLAW_MARKETING_VERSION = 2026.3.25
OPENCLAW_BUILD_VERSION = 202603250
OPENCLAW_GATEWAY_VERSION = 2026.3.24
OPENCLAW_MARKETING_VERSION = 2026.3.24
OPENCLAW_BUILD_VERSION = 2026032490
#include? "../build/Version.xcconfig"

View File

@@ -15,9 +15,9 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>2026.3.25</string>
<string>2026.3.24</string>
<key>CFBundleVersion</key>
<string>202603250</string>
<string>2026032490</string>
<key>CFBundleIconFile</key>
<string>OpenClaw</string>
<key>CFBundleURLTypes</key>

View File

@@ -8,11 +8,6 @@ import Speech
actor TalkModeRuntime {
static let shared = TalkModeRuntime()
enum PlaybackPlan: Equatable {
case elevenLabsThenSystemVoice(apiKey: String, voiceId: String)
case systemVoiceOnly
}
private let logger = Logger(subsystem: "ai.openclaw", category: "talk.runtime")
private let ttsLogger = Logger(subsystem: "ai.openclaw", category: "talk.tts")
private static let defaultModelIdFallback = "eleven_v3"
@@ -456,22 +451,17 @@ actor TalkModeRuntime {
private func playAssistant(text: String) async {
guard let input = await self.preparePlaybackInput(text: text) else { return }
switch Self.playbackPlan(apiKey: input.apiKey, voiceId: input.voiceId) {
case let .elevenLabsThenSystemVoice(apiKey, voiceId):
do {
do {
if let apiKey = input.apiKey, !apiKey.isEmpty, let voiceId = input.voiceId {
try await self.playElevenLabs(input: input, apiKey: apiKey, voiceId: voiceId)
} catch {
self.ttsLogger
.error(
"talk TTS failed: \(error.localizedDescription, privacy: .public); " +
"falling back to system voice")
do {
try await self.playSystemVoice(input: input)
} catch {
self.ttsLogger.error("talk system voice failed: \(error.localizedDescription, privacy: .public)")
}
} else {
try await self.playSystemVoice(input: input)
}
case .systemVoiceOnly:
} catch {
self.ttsLogger
.error(
"talk TTS failed: \(error.localizedDescription, privacy: .public); " +
"falling back to system voice")
do {
try await self.playSystemVoice(input: input)
} catch {
@@ -485,13 +475,6 @@ actor TalkModeRuntime {
}
}
static func playbackPlan(apiKey: String?, voiceId: String?) -> PlaybackPlan {
guard let apiKey, !apiKey.isEmpty, let voiceId else {
return .systemVoiceOnly
}
return .elevenLabsThenSystemVoice(apiKey: apiKey, voiceId: voiceId)
}
private struct TalkPlaybackInput {
let generation: Int
let cleanedText: String
@@ -681,12 +664,9 @@ actor TalkModeRuntime {
await MainActor.run { TalkModeController.shared.updatePhase(.speaking) }
self.phase = .speaking
await TalkSystemSpeechSynthesizer.shared.stop()
// Use app locale as fallback when no explicit language is set (e.g. system voice without ElevenLabs directive).
let appLocale = await MainActor.run { AppStateStore.shared.voiceWakeLocaleID }
let ttsLanguage = input.language ?? appLocale
try await TalkSystemSpeechSynthesizer.shared.speak(
text: input.cleanedText,
language: ttsLanguage)
language: input.language)
self.ttsLogger.info("talk system voice done")
}

View File

@@ -11,13 +11,4 @@ struct TalkModeRuntimeSpeechTests {
#expect(request.shouldReportPartialResults)
#expect(request.taskHint == .dictation)
}
@Test func `playback plan falls back only from elevenlabs`() {
#expect(
TalkModeRuntime.playbackPlan(apiKey: "key", voiceId: "voice")
== .elevenLabsThenSystemVoice(apiKey: "key", voiceId: "voice"))
#expect(TalkModeRuntime.playbackPlan(apiKey: nil, voiceId: "voice") == .systemVoiceOnly)
#expect(TalkModeRuntime.playbackPlan(apiKey: "key", voiceId: nil) == .systemVoiceOnly)
#expect(TalkModeRuntime.playbackPlan(apiKey: "", voiceId: "voice") == .systemVoiceOnly)
}
}

View File

@@ -51,11 +51,11 @@ public final class TalkSystemSpeechSynthesizer: NSObject {
}
self.currentUtterance = utterance
let watchdogTimeout = Self.watchdogTimeoutSeconds(text: trimmed, language: language ?? utterance.voice?.language)
let estimatedSeconds = max(3.0, min(180.0, Double(trimmed.count) * 0.08))
self.watchdog?.cancel()
self.watchdog = Task { @MainActor [weak self] in
guard let self else { return }
try? await Task.sleep(nanoseconds: UInt64(watchdogTimeout * 1_000_000_000))
try? await Task.sleep(nanoseconds: UInt64(estimatedSeconds * 1_000_000_000))
if Task.isCancelled { return }
guard self.currentToken == token else { return }
if self.synth.isSpeaking {
@@ -63,7 +63,7 @@ public final class TalkSystemSpeechSynthesizer: NSObject {
}
self.finishCurrent(
with: NSError(domain: "TalkSystemSpeechSynthesizer", code: 408, userInfo: [
NSLocalizedDescriptionKey: "system TTS timed out after \(watchdogTimeout)s",
NSLocalizedDescriptionKey: "system TTS timed out after \(estimatedSeconds)s",
]))
}
@@ -83,37 +83,6 @@ public final class TalkSystemSpeechSynthesizer: NSObject {
}
}
static func watchdogTimeoutSeconds(text: String, language: String?) -> Double {
// Estimate speech duration per language, then apply 3x safety margin.
// The watchdog is a hang guard normal completion relies on didFinish.
//
// Speech rates based on Pellegrino et al. (2019) syllable-per-second data,
// adjusted for TTS synthesis (slower than natural speech):
// https://www.science.org/doi/10.1126/sciadv.aaw2594
// Japanese: 7.84 SPS -> ~0.20s/char (mixed kana/kanji avg ~1.5 mora/char)
// Korean: 5.96 SPS -> ~0.25s/char (1 char = 1 syllable)
// Chinese: 5.18 SPS -> ~0.28s/char (1 char = 1 syllable)
// English: 6.19 SPS -> ~0.08s/char (avg ~5 chars/syllable)
let normalizedLanguage = language?.lowercased() ?? "en"
let perCharSeconds: Double
let minSeconds: Double
if normalizedLanguage.hasPrefix("ko") {
perCharSeconds = 0.25
minSeconds = 10.0
} else if normalizedLanguage.hasPrefix("zh") {
perCharSeconds = 0.28
minSeconds = 10.0
} else if normalizedLanguage.hasPrefix("ja") {
perCharSeconds = 0.20
minSeconds = 10.0
} else {
perCharSeconds = 0.08
minSeconds = 3.0
}
let estimatedSeconds = max(minSeconds, min(300.0, Double(text.count) * perCharSeconds))
return estimatedSeconds * 3.0
}
private func matchesCurrentUtterance(_ utteranceID: ObjectIdentifier) -> Bool {
guard let currentUtterance = self.currentUtterance else { return false }
return ObjectIdentifier(currentUtterance) == utteranceID

View File

@@ -1,44 +0,0 @@
import XCTest
@testable import OpenClawKit
final class TalkSystemSpeechSynthesizerTests: XCTestCase {
func testWatchdogTimeoutDefaultsToLatinProfile() {
let timeout = TalkSystemSpeechSynthesizer.watchdogTimeoutSeconds(
text: String(repeating: "a", count: 100),
language: nil)
XCTAssertEqual(timeout, 24.0, accuracy: 0.001)
}
func testWatchdogTimeoutUsesKoreanProfile() {
let timeout = TalkSystemSpeechSynthesizer.watchdogTimeoutSeconds(
text: String(repeating: "", count: 100),
language: "ko-KR")
XCTAssertEqual(timeout, 75.0, accuracy: 0.001)
}
func testWatchdogTimeoutUsesChineseProfile() {
let timeout = TalkSystemSpeechSynthesizer.watchdogTimeoutSeconds(
text: String(repeating: "", count: 100),
language: "zh-CN")
XCTAssertEqual(timeout, 84.0, accuracy: 0.001)
}
func testWatchdogTimeoutUsesJapaneseProfile() {
let timeout = TalkSystemSpeechSynthesizer.watchdogTimeoutSeconds(
text: String(repeating: "", count: 100),
language: "ja-JP")
XCTAssertEqual(timeout, 60.0, accuracy: 0.001)
}
func testWatchdogTimeoutClampsVeryLongUtterances() {
let timeout = TalkSystemSpeechSynthesizer.watchdogTimeoutSeconds(
text: String(repeating: "a", count: 10_000),
language: "en-US")
XCTAssertEqual(timeout, 900.0, accuracy: 0.001)
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
{"generatedBy":"scripts/generate-config-doc-baseline.ts","recordType":"meta","totalPaths":5531}
{"generatedBy":"scripts/generate-config-doc-baseline.ts","recordType":"meta","totalPaths":5639}
{"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}
@@ -746,7 +746,6 @@
{"recordType":"path","path":"channels.bluebubbles.accounts.*.dmHistoryLimit","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.bluebubbles.accounts.*.dmPolicy","kind":"channel","type":"string","required":false,"enumValues":["pairing","allowlist","open","disabled"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.bluebubbles.accounts.*.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.bluebubbles.accounts.*.enrichGroupParticipantsFromContacts","kind":"channel","type":"boolean","required":true,"defaultValue":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.bluebubbles.accounts.*.groupAllowFrom","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.bluebubbles.accounts.*.groupAllowFrom.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.bluebubbles.accounts.*.groupPolicy","kind":"channel","type":"string","required":false,"enumValues":["open","disabled","allowlist"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
@@ -796,7 +795,6 @@
{"recordType":"path","path":"channels.bluebubbles.dmHistoryLimit","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.bluebubbles.dmPolicy","kind":"channel","type":"string","required":false,"enumValues":["pairing","allowlist","open","disabled"],"deprecated":false,"sensitive":false,"tags":["access","channels","network"],"label":"BlueBubbles DM Policy","help":"Direct message access control (\"pairing\" recommended). \"open\" requires channels.bluebubbles.allowFrom=[\"*\"].","hasChildren":false}
{"recordType":"path","path":"channels.bluebubbles.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.bluebubbles.enrichGroupParticipantsFromContacts","kind":"channel","type":"boolean","required":true,"defaultValue":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.bluebubbles.groupAllowFrom","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.bluebubbles.groupAllowFrom.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.bluebubbles.groupPolicy","kind":"channel","type":"string","required":false,"enumValues":["open","disabled","allowlist"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
@@ -1029,8 +1027,47 @@
{"recordType":"path","path":"channels.discord.accounts.*.voice.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.auto","kind":"channel","type":"string","required":false,"enumValues":["off","always","inbound","tagged"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.edge","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.edge.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.edge.lang","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.edge.outputFormat","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.edge.pitch","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.edge.proxy","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.edge.rate","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.edge.saveSubtitles","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.edge.timeoutMs","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.edge.voice","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.edge.volume","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.elevenlabs","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.elevenlabs.apiKey","kind":"channel","type":["object","string"],"required":false,"deprecated":false,"sensitive":true,"tags":["auth","channels","media","network","security"],"hasChildren":true}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.elevenlabs.apiKey.id","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.elevenlabs.apiKey.provider","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.elevenlabs.apiKey.source","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.elevenlabs.applyTextNormalization","kind":"channel","type":"string","required":false,"enumValues":["auto","on","off"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.elevenlabs.baseUrl","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.elevenlabs.languageCode","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.elevenlabs.modelId","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.elevenlabs.seed","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.elevenlabs.voiceId","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.elevenlabs.voiceSettings","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.elevenlabs.voiceSettings.similarityBoost","kind":"channel","type":"number","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.elevenlabs.voiceSettings.speed","kind":"channel","type":"number","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.elevenlabs.voiceSettings.stability","kind":"channel","type":"number","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.elevenlabs.voiceSettings.style","kind":"channel","type":"number","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.elevenlabs.voiceSettings.useSpeakerBoost","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.maxTextLength","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.microsoft","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.microsoft.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.microsoft.lang","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.microsoft.outputFormat","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.microsoft.pitch","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.microsoft.proxy","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.microsoft.rate","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.microsoft.saveSubtitles","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.microsoft.timeoutMs","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.microsoft.voice","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.microsoft.volume","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.mode","kind":"channel","type":"string","required":false,"enumValues":["final","all"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.modelOverrides","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.modelOverrides.allowModelId","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
@@ -1041,16 +1078,18 @@
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.modelOverrides.allowVoice","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.modelOverrides.allowVoiceSettings","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.modelOverrides.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.openai","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.openai.apiKey","kind":"channel","type":["object","string"],"required":false,"deprecated":false,"sensitive":true,"tags":["auth","channels","media","network","security"],"hasChildren":true}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.openai.apiKey.id","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.openai.apiKey.provider","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.openai.apiKey.source","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.openai.baseUrl","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.openai.instructions","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.openai.model","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.openai.speed","kind":"channel","type":"number","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.openai.voice","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.prefsPath","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.provider","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.providers","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.providers.*","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.providers.*.*","kind":"channel","type":["array","boolean","null","number","object","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.providers.*.*.*","kind":"channel","type":[],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.providers.*.apiKey","kind":"channel","type":["object","string"],"required":false,"deprecated":false,"sensitive":true,"tags":["auth","channels","media","network","security"],"hasChildren":true}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.providers.*.apiKey.id","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.providers.*.apiKey.provider","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.providers.*.apiKey.source","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.summaryModel","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.timeoutMs","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.ackReaction","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
@@ -1255,8 +1294,47 @@
{"recordType":"path","path":"channels.discord.voice.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"Discord Voice Enabled","help":"Enable Discord voice channel conversations (default: true). Omit channels.discord.voice to keep voice support disabled for the account.","hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["channels","media","network"],"label":"Discord Voice Text-to-Speech","help":"Optional TTS overrides for Discord voice playback (merged with messages.tts).","hasChildren":true}
{"recordType":"path","path":"channels.discord.voice.tts.auto","kind":"channel","type":"string","required":false,"enumValues":["off","always","inbound","tagged"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.edge","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.voice.tts.edge.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.edge.lang","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.edge.outputFormat","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.edge.pitch","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.edge.proxy","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.edge.rate","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.edge.saveSubtitles","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.edge.timeoutMs","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.edge.voice","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.edge.volume","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.elevenlabs","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.voice.tts.elevenlabs.apiKey","kind":"channel","type":["object","string"],"required":false,"deprecated":false,"sensitive":true,"tags":["auth","channels","media","network","security"],"hasChildren":true}
{"recordType":"path","path":"channels.discord.voice.tts.elevenlabs.apiKey.id","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.elevenlabs.apiKey.provider","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.elevenlabs.apiKey.source","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.elevenlabs.applyTextNormalization","kind":"channel","type":"string","required":false,"enumValues":["auto","on","off"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.elevenlabs.baseUrl","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.elevenlabs.languageCode","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.elevenlabs.modelId","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.elevenlabs.seed","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.elevenlabs.voiceId","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.elevenlabs.voiceSettings","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.voice.tts.elevenlabs.voiceSettings.similarityBoost","kind":"channel","type":"number","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.elevenlabs.voiceSettings.speed","kind":"channel","type":"number","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.elevenlabs.voiceSettings.stability","kind":"channel","type":"number","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.elevenlabs.voiceSettings.style","kind":"channel","type":"number","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.elevenlabs.voiceSettings.useSpeakerBoost","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.maxTextLength","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.microsoft","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.voice.tts.microsoft.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.microsoft.lang","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.microsoft.outputFormat","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.microsoft.pitch","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.microsoft.proxy","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.microsoft.rate","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.microsoft.saveSubtitles","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.microsoft.timeoutMs","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.microsoft.voice","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.microsoft.volume","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.mode","kind":"channel","type":"string","required":false,"enumValues":["final","all"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.modelOverrides","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.voice.tts.modelOverrides.allowModelId","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
@@ -1267,16 +1345,18 @@
{"recordType":"path","path":"channels.discord.voice.tts.modelOverrides.allowVoice","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.modelOverrides.allowVoiceSettings","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.modelOverrides.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.openai","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.voice.tts.openai.apiKey","kind":"channel","type":["object","string"],"required":false,"deprecated":false,"sensitive":true,"tags":["auth","channels","media","network","security"],"hasChildren":true}
{"recordType":"path","path":"channels.discord.voice.tts.openai.apiKey.id","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.openai.apiKey.provider","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.openai.apiKey.source","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.openai.baseUrl","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.openai.instructions","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.openai.model","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.openai.speed","kind":"channel","type":"number","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.openai.voice","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.prefsPath","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.provider","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.providers","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.voice.tts.providers.*","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.voice.tts.providers.*.*","kind":"channel","type":["array","boolean","null","number","object","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.voice.tts.providers.*.*.*","kind":"channel","type":[],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.providers.*.apiKey","kind":"channel","type":["object","string"],"required":false,"deprecated":false,"sensitive":true,"tags":["auth","channels","media","network","security"],"hasChildren":true}
{"recordType":"path","path":"channels.discord.voice.tts.providers.*.apiKey.id","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.providers.*.apiKey.provider","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.providers.*.apiKey.source","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.summaryModel","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.timeoutMs","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.feishu","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"Feishu","help":"飞书/Lark enterprise messaging with doc/wiki/drive tools.","hasChildren":true}
@@ -2011,7 +2091,6 @@
{"recordType":"path","path":"channels.mattermost.accounts.*.actions.reactions","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.mattermost.accounts.*.allowFrom","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.mattermost.accounts.*.allowFrom.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.mattermost.accounts.*.allowPrivateNetwork","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.mattermost.accounts.*.baseUrl","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.mattermost.accounts.*.blockStreaming","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.mattermost.accounts.*.blockStreamingCoalesce","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
@@ -2060,7 +2139,6 @@
{"recordType":"path","path":"channels.mattermost.actions.reactions","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.mattermost.allowFrom","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.mattermost.allowFrom.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.mattermost.allowPrivateNetwork","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.mattermost.baseUrl","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"Mattermost Base URL","help":"Base URL for your Mattermost server (e.g., https://chat.example.com).","hasChildren":false}
{"recordType":"path","path":"channels.mattermost.blockStreaming","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.mattermost.blockStreamingCoalesce","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
@@ -2206,7 +2284,6 @@
{"recordType":"path","path":"channels.nextcloud-talk.accounts.*","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.nextcloud-talk.accounts.*.allowFrom","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.nextcloud-talk.accounts.*.allowFrom.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.nextcloud-talk.accounts.*.allowPrivateNetwork","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.nextcloud-talk.accounts.*.apiPassword","kind":"channel","type":["object","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.nextcloud-talk.accounts.*.apiPassword.id","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.nextcloud-talk.accounts.*.apiPassword.provider","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
@@ -2263,7 +2340,6 @@
{"recordType":"path","path":"channels.nextcloud-talk.accounts.*.webhookPublicUrl","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.nextcloud-talk.allowFrom","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.nextcloud-talk.allowFrom.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.nextcloud-talk.allowPrivateNetwork","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.nextcloud-talk.apiPassword","kind":"channel","type":["object","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.nextcloud-talk.apiPassword.id","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.nextcloud-talk.apiPassword.provider","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
@@ -3864,8 +3940,47 @@
{"recordType":"path","path":"messages.suppressToolErrors","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Suppress Tool Error Warnings","help":"When true, suppress ⚠️ tool-error warnings from being shown to the user. The agent already sees errors in context and can retry. Default: false.","hasChildren":false}
{"recordType":"path","path":"messages.tts","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["media"],"label":"Message Text-to-Speech","help":"Text-to-speech policy for reading agent replies aloud on supported voice or audio surfaces. Keep disabled unless voice playback is part of your operator/user workflow.","hasChildren":true}
{"recordType":"path","path":"messages.tts.auto","kind":"core","type":"string","required":false,"enumValues":["off","always","inbound","tagged"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.edge","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"messages.tts.edge.enabled","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.edge.lang","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.edge.outputFormat","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.edge.pitch","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.edge.proxy","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.edge.rate","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.edge.saveSubtitles","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.edge.timeoutMs","kind":"core","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.edge.voice","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.edge.volume","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.elevenlabs","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"messages.tts.elevenlabs.apiKey","kind":"core","type":["object","string"],"required":false,"deprecated":false,"sensitive":true,"tags":["auth","media","security"],"hasChildren":true}
{"recordType":"path","path":"messages.tts.elevenlabs.apiKey.id","kind":"core","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.elevenlabs.apiKey.provider","kind":"core","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.elevenlabs.apiKey.source","kind":"core","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.elevenlabs.applyTextNormalization","kind":"core","type":"string","required":false,"enumValues":["auto","on","off"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.elevenlabs.baseUrl","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.elevenlabs.languageCode","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.elevenlabs.modelId","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.elevenlabs.seed","kind":"core","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.elevenlabs.voiceId","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.elevenlabs.voiceSettings","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"messages.tts.elevenlabs.voiceSettings.similarityBoost","kind":"core","type":"number","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.elevenlabs.voiceSettings.speed","kind":"core","type":"number","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.elevenlabs.voiceSettings.stability","kind":"core","type":"number","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.elevenlabs.voiceSettings.style","kind":"core","type":"number","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.elevenlabs.voiceSettings.useSpeakerBoost","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.enabled","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.maxTextLength","kind":"core","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.microsoft","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"messages.tts.microsoft.enabled","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.microsoft.lang","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.microsoft.outputFormat","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.microsoft.pitch","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.microsoft.proxy","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.microsoft.rate","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.microsoft.saveSubtitles","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.microsoft.timeoutMs","kind":"core","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.microsoft.voice","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.microsoft.volume","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.mode","kind":"core","type":"string","required":false,"enumValues":["final","all"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.modelOverrides","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"messages.tts.modelOverrides.allowModelId","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
@@ -3876,16 +3991,18 @@
{"recordType":"path","path":"messages.tts.modelOverrides.allowVoice","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.modelOverrides.allowVoiceSettings","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.modelOverrides.enabled","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.openai","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"messages.tts.openai.apiKey","kind":"core","type":["object","string"],"required":false,"deprecated":false,"sensitive":true,"tags":["auth","media","security"],"hasChildren":true}
{"recordType":"path","path":"messages.tts.openai.apiKey.id","kind":"core","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.openai.apiKey.provider","kind":"core","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.openai.apiKey.source","kind":"core","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.openai.baseUrl","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.openai.instructions","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.openai.model","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.openai.speed","kind":"core","type":"number","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.openai.voice","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.prefsPath","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.provider","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.providers","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["media"],"label":"TTS Provider Settings","help":"Provider-specific TTS settings keyed by speech provider id. Use this instead of bundled provider-specific top-level keys so speech plugins stay decoupled from core config schema.","hasChildren":true}
{"recordType":"path","path":"messages.tts.providers.*","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["media"],"label":"TTS Provider Config","help":"Provider-specific TTS configuration for one speech provider id. Keep fields scoped to the plugin that owns that provider.","hasChildren":true}
{"recordType":"path","path":"messages.tts.providers.*.*","kind":"core","type":["array","boolean","null","number","object","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"messages.tts.providers.*.*.*","kind":"core","type":[],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.providers.*.apiKey","kind":"core","type":["object","string"],"required":false,"deprecated":false,"sensitive":true,"tags":["auth","media","security"],"label":"TTS Provider API Key","help":"Provider API key used by that speech provider when its plugin requires authenticated TTS access.","hasChildren":true}
{"recordType":"path","path":"messages.tts.providers.*.apiKey.id","kind":"core","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.providers.*.apiKey.provider","kind":"core","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.providers.*.apiKey.source","kind":"core","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.summaryModel","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.timeoutMs","kind":"core","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"meta","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Metadata","help":"Metadata fields automatically maintained by OpenClaw to record write/version history for this config file. Keep these values system-managed and avoid manual edits unless debugging migration history.","hasChildren":true}
@@ -4036,15 +4153,6 @@
{"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.browser","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/browser-plugin","help":"OpenClaw browser tool plugin (plugin: browser)","hasChildren":true}
{"recordType":"path","path":"plugins.entries.browser.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/browser-plugin Config","help":"Plugin-defined config payload for browser.","hasChildren":false}
{"recordType":"path","path":"plugins.entries.browser.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/browser-plugin","hasChildren":false}
{"recordType":"path","path":"plugins.entries.browser.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.browser.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.browser.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.browser.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.browser.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"plugins.entries.browser.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}
@@ -4384,15 +4492,6 @@
{"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-foundry","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/microsoft-foundry","help":"OpenClaw Microsoft Foundry provider plugin (plugin: microsoft-foundry)","hasChildren":true}
{"recordType":"path","path":"plugins.entries.microsoft-foundry.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/microsoft-foundry Config","help":"Plugin-defined config payload for microsoft-foundry.","hasChildren":false}
{"recordType":"path","path":"plugins.entries.microsoft-foundry.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/microsoft-foundry","hasChildren":false}
{"recordType":"path","path":"plugins.entries.microsoft-foundry.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-foundry.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-foundry.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-foundry.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-foundry.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"plugins.entries.microsoft-foundry.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.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}
@@ -4583,6 +4682,15 @@
{"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}

View File

@@ -5,6 +5,33 @@
"category": "legacy",
"entrypoint": "index",
"exports": [
{
"declaration": "export function buildFalImageGenerationProvider(): ImageGenerationProvider;",
"exportName": "buildFalImageGenerationProvider",
"kind": "function",
"source": {
"line": 190,
"path": "extensions/fal/image-generation-provider.ts"
}
},
{
"declaration": "export function buildGoogleImageGenerationProvider(): ImageGenerationProvider;",
"exportName": "buildGoogleImageGenerationProvider",
"kind": "function",
"source": {
"line": 98,
"path": "extensions/google/image-generation-provider.ts"
}
},
{
"declaration": "export function buildOpenAIImageGenerationProvider(): ImageGenerationProvider;",
"exportName": "buildOpenAIImageGenerationProvider",
"kind": "function",
"source": {
"line": 22,
"path": "extensions/openai/image-generation-provider.ts"
}
},
{
"declaration": "export function delegateCompactionToRuntime(params: { sessionId: string; sessionKey?: string | undefined; sessionFile: string; tokenBudget?: number | undefined; force?: boolean | undefined; currentTokenCount?: number | undefined; compactionTarget?: \"budget\" | ... 1 more ... | undefined; customInstructions?: string | undefined; runtimeContext?: ContextEngineRuntimeContext | undefined; }): Promise<...>;",
"exportName": "delegateCompactionToRuntime",
@@ -159,7 +186,7 @@
}
},
{
"declaration": "export type ChannelMessageActionName = \"send\" | \"broadcast\" | \"poll\" | \"poll-vote\" | \"react\" | \"reactions\" | \"read\" | \"edit\" | \"unsend\" | \"reply\" | \"sendWithEffect\" | \"renameGroup\" | \"setGroupIcon\" | \"addParticipant\" | \"removeParticipant\" | \"leaveGroup\" | \"sendAttachment\" | \"delete\" | \"pin\" | \"unpin\" | \"list-pins\" | \"permissions\" | \"thread-create\" | \"thread-list\" | \"thread-reply\" | \"search\" | \"sticker\" | \"sticker-search\" | \"member-info\" | \"role-info\" | \"emoji-list\" | \"emoji-upload\" | \"sticker-upload\" | \"role-add\" | \"role-remove\" | \"channel-info\" | \"channel-list\" | \"channel-create\" | \"channel-edit\" | \"channel-delete\" | \"channel-move\" | \"category-create\" | \"category-edit\" | \"category-delete\" | \"topic-create\" | \"topic-edit\" | \"voice-status\" | \"event-list\" | \"event-create\" | \"timeout\" | \"kick\" | \"ban\" | \"set-profile\" | \"set-presence\" | \"download-file\" | \"upload-file\";",
"declaration": "export type ChannelMessageActionName = \"send\" | \"broadcast\" | \"poll\" | \"poll-vote\" | \"react\" | \"reactions\" | \"read\" | \"edit\" | \"unsend\" | \"reply\" | \"sendWithEffect\" | \"renameGroup\" | \"setGroupIcon\" | \"addParticipant\" | \"removeParticipant\" | \"leaveGroup\" | \"sendAttachment\" | \"delete\" | \"pin\" | \"unpin\" | \"list-pins\" | \"permissions\" | \"thread-create\" | \"thread-list\" | \"thread-reply\" | \"search\" | \"sticker\" | \"sticker-search\" | \"member-info\" | \"role-info\" | \"emoji-list\" | \"emoji-upload\" | \"sticker-upload\" | \"role-add\" | \"role-remove\" | \"channel-info\" | \"channel-list\" | \"channel-create\" | \"channel-edit\" | \"channel-delete\" | \"channel-move\" | \"category-create\" | \"category-edit\" | \"category-delete\" | \"topic-create\" | \"topic-edit\" | \"voice-status\" | \"event-list\" | \"event-create\" | \"timeout\" | \"kick\" | \"ban\" | \"set-profile\" | \"set-presence\" | \"download-file\";",
"exportName": "ChannelMessageActionName",
"kind": "type",
"source": {
@@ -230,24 +257,6 @@
"path": "src/config/types.openclaw.ts"
}
},
{
"declaration": "export type CliBackendConfig = CliBackendConfig;",
"exportName": "CliBackendConfig",
"kind": "type",
"source": {
"line": 47,
"path": "src/config/types.agent-defaults.ts"
}
},
{
"declaration": "export type CliBackendPlugin = CliBackendPlugin;",
"exportName": "CliBackendPlugin",
"kind": "type",
"source": {
"line": 1346,
"path": "src/plugins/types.ts"
}
},
{
"declaration": "export type CompiledConfiguredBinding = CompiledConfiguredBinding;",
"exportName": "CompiledConfiguredBinding",
@@ -388,7 +397,7 @@
"exportName": "MediaUnderstandingProviderPlugin",
"kind": "type",
"source": {
"line": 969,
"line": 951,
"path": "src/plugins/types.ts"
}
},
@@ -406,7 +415,7 @@
"exportName": "OpenClawPluginApi",
"kind": "type",
"source": {
"line": 1390,
"line": 1314,
"path": "src/plugins/types.ts"
}
},
@@ -415,7 +424,7 @@
"exportName": "OpenClawPluginConfigSchema",
"kind": "type",
"source": {
"line": 95,
"line": 88,
"path": "src/plugins/types.ts"
}
},
@@ -424,7 +433,7 @@
"exportName": "PluginLogger",
"kind": "type",
"source": {
"line": 66,
"line": 59,
"path": "src/plugins/types.ts"
}
},
@@ -442,7 +451,7 @@
"exportName": "ProviderAuthContext",
"kind": "type",
"source": {
"line": 166,
"line": 155,
"path": "src/plugins/types.ts"
}
},
@@ -451,7 +460,7 @@
"exportName": "ProviderAuthResult",
"kind": "type",
"source": {
"line": 151,
"line": 140,
"path": "src/plugins/types.ts"
}
},
@@ -460,7 +469,7 @@
"exportName": "ProviderRuntimeModel",
"kind": "type",
"source": {
"line": 306,
"line": 295,
"path": "src/plugins/types.ts"
}
},
@@ -514,7 +523,7 @@
"exportName": "SpeechProviderPlugin",
"kind": "type",
"source": {
"line": 944,
"line": 933,
"path": "src/plugins/types.ts"
}
},
@@ -896,7 +905,7 @@
"exportName": "createMessageToolButtonsSchema",
"kind": "function",
"source": {
"line": 12,
"line": 11,
"path": "src/plugin-sdk/channel-actions.ts"
}
},
@@ -905,7 +914,7 @@
"exportName": "createMessageToolCardSchema",
"kind": "function",
"source": {
"line": 30,
"line": 29,
"path": "src/plugin-sdk/channel-actions.ts"
}
},
@@ -927,15 +936,6 @@
"path": "src/channels/plugins/actions/shared.ts"
}
},
{
"declaration": "export function optionalStringEnum<T extends readonly string[]>(values: T, options?: StringEnumOptions<T>): TOptional<TUnsafe<T[number]>>;",
"exportName": "optionalStringEnum",
"kind": "function",
"source": {
"line": 31,
"path": "src/agents/schema/typebox.ts"
}
},
{
"declaration": "export function resolveReactionMessageId(params: { args: Record<string, unknown>; toolContext?: ReactionToolContext | undefined; }): string | number | undefined;",
"exportName": "resolveReactionMessageId",
@@ -944,15 +944,6 @@
"line": 7,
"path": "src/channels/plugins/actions/reaction-message-id.ts"
}
},
{
"declaration": "export function stringEnum<T extends readonly string[]>(values: T, options?: StringEnumOptions<T>): TUnsafe<T[number]>;",
"exportName": "stringEnum",
"kind": "function",
"source": {
"line": 15,
"path": "src/agents/schema/typebox.ts"
}
}
],
"importSpecifier": "openclaw/plugin-sdk/channel-actions",
@@ -1121,7 +1112,7 @@
}
},
{
"declaration": "export type ChannelMessageActionName = \"send\" | \"broadcast\" | \"poll\" | \"poll-vote\" | \"react\" | \"reactions\" | \"read\" | \"edit\" | \"unsend\" | \"reply\" | \"sendWithEffect\" | \"renameGroup\" | \"setGroupIcon\" | \"addParticipant\" | \"removeParticipant\" | \"leaveGroup\" | \"sendAttachment\" | \"delete\" | \"pin\" | \"unpin\" | \"list-pins\" | \"permissions\" | \"thread-create\" | \"thread-list\" | \"thread-reply\" | \"search\" | \"sticker\" | \"sticker-search\" | \"member-info\" | \"role-info\" | \"emoji-list\" | \"emoji-upload\" | \"sticker-upload\" | \"role-add\" | \"role-remove\" | \"channel-info\" | \"channel-list\" | \"channel-create\" | \"channel-edit\" | \"channel-delete\" | \"channel-move\" | \"category-create\" | \"category-edit\" | \"category-delete\" | \"topic-create\" | \"topic-edit\" | \"voice-status\" | \"event-list\" | \"event-create\" | \"timeout\" | \"kick\" | \"ban\" | \"set-profile\" | \"set-presence\" | \"download-file\" | \"upload-file\";",
"declaration": "export type ChannelMessageActionName = \"send\" | \"broadcast\" | \"poll\" | \"poll-vote\" | \"react\" | \"reactions\" | \"read\" | \"edit\" | \"unsend\" | \"reply\" | \"sendWithEffect\" | \"renameGroup\" | \"setGroupIcon\" | \"addParticipant\" | \"removeParticipant\" | \"leaveGroup\" | \"sendAttachment\" | \"delete\" | \"pin\" | \"unpin\" | \"list-pins\" | \"permissions\" | \"thread-create\" | \"thread-list\" | \"thread-reply\" | \"search\" | \"sticker\" | \"sticker-search\" | \"member-info\" | \"role-info\" | \"emoji-list\" | \"emoji-upload\" | \"sticker-upload\" | \"role-add\" | \"role-remove\" | \"channel-info\" | \"channel-list\" | \"channel-create\" | \"channel-edit\" | \"channel-delete\" | \"channel-move\" | \"category-create\" | \"category-edit\" | \"category-delete\" | \"topic-create\" | \"topic-edit\" | \"voice-status\" | \"event-list\" | \"event-create\" | \"timeout\" | \"kick\" | \"ban\" | \"set-profile\" | \"set-presence\" | \"download-file\";",
"exportName": "ChannelMessageActionName",
"kind": "type",
"source": {
@@ -1505,7 +1496,7 @@
}
},
{
"declaration": "export const CHANNEL_MESSAGE_ACTION_NAMES: readonly [\"send\", \"broadcast\", \"poll\", \"poll-vote\", \"react\", \"reactions\", \"read\", \"edit\", \"unsend\", \"reply\", \"sendWithEffect\", \"renameGroup\", \"setGroupIcon\", \"addParticipant\", \"removeParticipant\", \"leaveGroup\", \"sendAttachment\", \"delete\", \"pin\", \"unpin\", \"list-pins\", \"permissions\", \"thread-create\", \"thread-list\", \"thread-reply\", \"search\", \"sticker\", \"sticker-search\", \"member-info\", \"role-info\", \"emoji-list\", \"emoji-upload\", \"sticker-upload\", \"role-add\", \"role-remove\", \"channel-info\", \"channel-list\", \"channel-create\", \"channel-edit\", \"channel-delete\", \"channel-move\", \"category-create\", \"category-edit\", \"category-delete\", \"topic-create\", \"topic-edit\", \"voice-status\", \"event-list\", \"event-create\", \"timeout\", \"kick\", \"ban\", \"set-profile\", \"set-presence\", \"set-profile\", \"download-file\", \"upload-file\"];",
"declaration": "export const CHANNEL_MESSAGE_ACTION_NAMES: readonly [\"send\", \"broadcast\", \"poll\", \"poll-vote\", \"react\", \"reactions\", \"read\", \"edit\", \"unsend\", \"reply\", \"sendWithEffect\", \"renameGroup\", \"setGroupIcon\", \"addParticipant\", \"removeParticipant\", \"leaveGroup\", \"sendAttachment\", \"delete\", \"pin\", \"unpin\", \"list-pins\", \"permissions\", \"thread-create\", \"thread-list\", \"thread-reply\", \"search\", \"sticker\", \"sticker-search\", \"member-info\", \"role-info\", \"emoji-list\", \"emoji-upload\", \"sticker-upload\", \"role-add\", \"role-remove\", \"channel-info\", \"channel-list\", \"channel-create\", \"channel-edit\", \"channel-delete\", \"channel-move\", \"category-create\", \"category-edit\", \"category-delete\", \"topic-create\", \"topic-edit\", \"voice-status\", \"event-list\", \"event-create\", \"timeout\", \"kick\", \"ban\", \"set-profile\", \"set-presence\", \"set-profile\", \"download-file\"];",
"exportName": "CHANNEL_MESSAGE_ACTION_NAMES",
"kind": "const",
"source": {
@@ -1901,7 +1892,7 @@
}
},
{
"declaration": "export type ChannelMessageActionName = \"send\" | \"broadcast\" | \"poll\" | \"poll-vote\" | \"react\" | \"reactions\" | \"read\" | \"edit\" | \"unsend\" | \"reply\" | \"sendWithEffect\" | \"renameGroup\" | \"setGroupIcon\" | \"addParticipant\" | \"removeParticipant\" | \"leaveGroup\" | \"sendAttachment\" | \"delete\" | \"pin\" | \"unpin\" | \"list-pins\" | \"permissions\" | \"thread-create\" | \"thread-list\" | \"thread-reply\" | \"search\" | \"sticker\" | \"sticker-search\" | \"member-info\" | \"role-info\" | \"emoji-list\" | \"emoji-upload\" | \"sticker-upload\" | \"role-add\" | \"role-remove\" | \"channel-info\" | \"channel-list\" | \"channel-create\" | \"channel-edit\" | \"channel-delete\" | \"channel-move\" | \"category-create\" | \"category-edit\" | \"category-delete\" | \"topic-create\" | \"topic-edit\" | \"voice-status\" | \"event-list\" | \"event-create\" | \"timeout\" | \"kick\" | \"ban\" | \"set-profile\" | \"set-presence\" | \"download-file\" | \"upload-file\";",
"declaration": "export type ChannelMessageActionName = \"send\" | \"broadcast\" | \"poll\" | \"poll-vote\" | \"react\" | \"reactions\" | \"read\" | \"edit\" | \"unsend\" | \"reply\" | \"sendWithEffect\" | \"renameGroup\" | \"setGroupIcon\" | \"addParticipant\" | \"removeParticipant\" | \"leaveGroup\" | \"sendAttachment\" | \"delete\" | \"pin\" | \"unpin\" | \"list-pins\" | \"permissions\" | \"thread-create\" | \"thread-list\" | \"thread-reply\" | \"search\" | \"sticker\" | \"sticker-search\" | \"member-info\" | \"role-info\" | \"emoji-list\" | \"emoji-upload\" | \"sticker-upload\" | \"role-add\" | \"role-remove\" | \"channel-info\" | \"channel-list\" | \"channel-create\" | \"channel-edit\" | \"channel-delete\" | \"channel-move\" | \"category-create\" | \"category-edit\" | \"category-delete\" | \"topic-create\" | \"topic-edit\" | \"voice-status\" | \"event-list\" | \"event-create\" | \"timeout\" | \"kick\" | \"ban\" | \"set-profile\" | \"set-presence\" | \"download-file\";",
"exportName": "ChannelMessageActionName",
"kind": "type",
"source": {
@@ -2386,7 +2377,7 @@
"exportName": "buildCommandsPaginationKeyboard",
"kind": "function",
"source": {
"line": 175,
"line": 196,
"path": "src/auto-reply/reply/commands-info.ts"
}
},
@@ -3018,7 +3009,7 @@
"exportName": "buildChannelOutboundSessionRoute",
"kind": "function",
"source": {
"line": 161,
"line": 163,
"path": "src/plugin-sdk/core.ts"
}
},
@@ -3054,7 +3045,7 @@
"exportName": "createChannelPluginBase",
"kind": "function",
"source": {
"line": 435,
"line": 437,
"path": "src/plugin-sdk/core.ts"
}
},
@@ -3063,7 +3054,7 @@
"exportName": "createChatChannelPlugin",
"kind": "function",
"source": {
"line": 412,
"line": 414,
"path": "src/plugin-sdk/core.ts"
}
},
@@ -3072,7 +3063,7 @@
"exportName": "defineChannelPluginEntry",
"kind": "function",
"source": {
"line": 244,
"line": 246,
"path": "src/plugin-sdk/core.ts"
}
},
@@ -3081,7 +3072,7 @@
"exportName": "definePluginEntry",
"kind": "function",
"source": {
"line": 90,
"line": 88,
"path": "src/plugin-sdk/plugin-entry.ts"
}
},
@@ -3090,7 +3081,7 @@
"exportName": "defineSetupPluginEntry",
"kind": "function",
"source": {
"line": 275,
"line": 277,
"path": "src/plugin-sdk/core.ts"
}
},
@@ -3288,7 +3279,7 @@
"exportName": "stripChannelTargetPrefix",
"kind": "function",
"source": {
"line": 141,
"line": 143,
"path": "src/plugin-sdk/core.ts"
}
},
@@ -3297,7 +3288,7 @@
"exportName": "stripTargetKindPrefix",
"kind": "function",
"source": {
"line": 153,
"line": 155,
"path": "src/plugin-sdk/core.ts"
}
},
@@ -3369,7 +3360,7 @@
"exportName": "ChannelOutboundSessionRouteParams",
"kind": "type",
"source": {
"line": 136,
"line": 138,
"path": "src/plugin-sdk/core.ts"
}
},
@@ -3405,7 +3396,7 @@
"exportName": "MediaUnderstandingProviderPlugin",
"kind": "type",
"source": {
"line": 969,
"line": 951,
"path": "src/plugins/types.ts"
}
},
@@ -3423,7 +3414,7 @@
"exportName": "OpenClawPluginApi",
"kind": "type",
"source": {
"line": 1390,
"line": 1314,
"path": "src/plugins/types.ts"
}
},
@@ -3432,7 +3423,7 @@
"exportName": "OpenClawPluginCommandDefinition",
"kind": "type",
"source": {
"line": 1108,
"line": 1068,
"path": "src/plugins/types.ts"
}
},
@@ -3441,7 +3432,7 @@
"exportName": "OpenClawPluginConfigSchema",
"kind": "type",
"source": {
"line": 95,
"line": 88,
"path": "src/plugins/types.ts"
}
},
@@ -3450,7 +3441,7 @@
"exportName": "OpenClawPluginDefinition",
"kind": "type",
"source": {
"line": 1372,
"line": 1296,
"path": "src/plugins/types.ts"
}
},
@@ -3459,7 +3450,7 @@
"exportName": "OpenClawPluginService",
"kind": "type",
"source": {
"line": 1339,
"line": 1285,
"path": "src/plugins/types.ts"
}
},
@@ -3468,7 +3459,7 @@
"exportName": "OpenClawPluginServiceContext",
"kind": "type",
"source": {
"line": 1331,
"line": 1277,
"path": "src/plugins/types.ts"
}
},
@@ -3477,7 +3468,7 @@
"exportName": "OpenClawPluginToolContext",
"kind": "type",
"source": {
"line": 110,
"line": 103,
"path": "src/plugins/types.ts"
}
},
@@ -3486,7 +3477,7 @@
"exportName": "OpenClawPluginToolFactory",
"kind": "type",
"source": {
"line": 131,
"line": 120,
"path": "src/plugins/types.ts"
}
},
@@ -3495,7 +3486,7 @@
"exportName": "PluginCommandContext",
"kind": "type",
"source": {
"line": 984,
"line": 966,
"path": "src/plugins/types.ts"
}
},
@@ -3504,7 +3495,7 @@
"exportName": "PluginInteractiveTelegramHandlerContext",
"kind": "type",
"source": {
"line": 1145,
"line": 1097,
"path": "src/plugins/types.ts"
}
},
@@ -3513,7 +3504,7 @@
"exportName": "PluginLogger",
"kind": "type",
"source": {
"line": 66,
"line": 59,
"path": "src/plugins/types.ts"
}
},
@@ -3531,7 +3522,7 @@
"exportName": "ProviderAugmentModelCatalogContext",
"kind": "type",
"source": {
"line": 582,
"line": 571,
"path": "src/plugins/types.ts"
}
},
@@ -3540,7 +3531,7 @@
"exportName": "ProviderAuthContext",
"kind": "type",
"source": {
"line": 166,
"line": 155,
"path": "src/plugins/types.ts"
}
},
@@ -3549,7 +3540,7 @@
"exportName": "ProviderAuthDoctorHintContext",
"kind": "type",
"source": {
"line": 457,
"line": 446,
"path": "src/plugins/types.ts"
}
},
@@ -3558,7 +3549,7 @@
"exportName": "ProviderAuthMethod",
"kind": "type",
"source": {
"line": 244,
"line": 233,
"path": "src/plugins/types.ts"
}
},
@@ -3567,7 +3558,7 @@
"exportName": "ProviderAuthMethodNonInteractiveContext",
"kind": "type",
"source": {
"line": 228,
"line": 217,
"path": "src/plugins/types.ts"
}
},
@@ -3576,7 +3567,7 @@
"exportName": "ProviderAuthResult",
"kind": "type",
"source": {
"line": 151,
"line": 140,
"path": "src/plugins/types.ts"
}
},
@@ -3585,7 +3576,7 @@
"exportName": "ProviderBuildMissingAuthMessageContext",
"kind": "type",
"source": {
"line": 510,
"line": 499,
"path": "src/plugins/types.ts"
}
},
@@ -3594,7 +3585,7 @@
"exportName": "ProviderBuiltInModelSuppressionContext",
"kind": "type",
"source": {
"line": 526,
"line": 515,
"path": "src/plugins/types.ts"
}
},
@@ -3603,7 +3594,7 @@
"exportName": "ProviderBuiltInModelSuppressionResult",
"kind": "type",
"source": {
"line": 535,
"line": 524,
"path": "src/plugins/types.ts"
}
},
@@ -3612,7 +3603,7 @@
"exportName": "ProviderCacheTtlEligibilityContext",
"kind": "type",
"source": {
"line": 498,
"line": 487,
"path": "src/plugins/types.ts"
}
},
@@ -3621,7 +3612,7 @@
"exportName": "ProviderCatalogContext",
"kind": "type",
"source": {
"line": 265,
"line": 254,
"path": "src/plugins/types.ts"
}
},
@@ -3630,7 +3621,7 @@
"exportName": "ProviderCatalogResult",
"kind": "type",
"source": {
"line": 288,
"line": 277,
"path": "src/plugins/types.ts"
}
},
@@ -3639,7 +3630,7 @@
"exportName": "ProviderDefaultThinkingPolicyContext",
"kind": "type",
"source": {
"line": 559,
"line": 548,
"path": "src/plugins/types.ts"
}
},
@@ -3648,7 +3639,7 @@
"exportName": "ProviderDiscoveryContext",
"kind": "type",
"source": {
"line": 598,
"line": 587,
"path": "src/plugins/types.ts"
}
},
@@ -3657,7 +3648,7 @@
"exportName": "ProviderFetchUsageSnapshotContext",
"kind": "type",
"source": {
"line": 438,
"line": 427,
"path": "src/plugins/types.ts"
}
},
@@ -3666,7 +3657,7 @@
"exportName": "ProviderModernModelPolicyContext",
"kind": "type",
"source": {
"line": 569,
"line": 558,
"path": "src/plugins/types.ts"
}
},
@@ -3675,7 +3666,7 @@
"exportName": "ProviderNormalizeResolvedModelContext",
"kind": "type",
"source": {
"line": 349,
"line": 338,
"path": "src/plugins/types.ts"
}
},
@@ -3684,7 +3675,7 @@
"exportName": "ProviderPreparedRuntimeAuth",
"kind": "type",
"source": {
"line": 385,
"line": 374,
"path": "src/plugins/types.ts"
}
},
@@ -3693,7 +3684,7 @@
"exportName": "ProviderPrepareDynamicModelContext",
"kind": "type",
"source": {
"line": 340,
"line": 329,
"path": "src/plugins/types.ts"
}
},
@@ -3702,7 +3693,7 @@
"exportName": "ProviderPrepareExtraParamsContext",
"kind": "type",
"source": {
"line": 471,
"line": 460,
"path": "src/plugins/types.ts"
}
},
@@ -3711,7 +3702,7 @@
"exportName": "ProviderPrepareRuntimeAuthContext",
"kind": "type",
"source": {
"line": 364,
"line": 353,
"path": "src/plugins/types.ts"
}
},
@@ -3720,7 +3711,7 @@
"exportName": "ProviderResolvedUsageAuth",
"kind": "type",
"source": {
"line": 425,
"line": 414,
"path": "src/plugins/types.ts"
}
},
@@ -3729,7 +3720,7 @@
"exportName": "ProviderResolveDynamicModelContext",
"kind": "type",
"source": {
"line": 323,
"line": 312,
"path": "src/plugins/types.ts"
}
},
@@ -3738,7 +3729,7 @@
"exportName": "ProviderResolveUsageAuthContext",
"kind": "type",
"source": {
"line": 406,
"line": 395,
"path": "src/plugins/types.ts"
}
},
@@ -3747,7 +3738,7 @@
"exportName": "ProviderRuntimeModel",
"kind": "type",
"source": {
"line": 306,
"line": 295,
"path": "src/plugins/types.ts"
}
},
@@ -3756,7 +3747,7 @@
"exportName": "ProviderThinkingPolicyContext",
"kind": "type",
"source": {
"line": 547,
"line": 536,
"path": "src/plugins/types.ts"
}
},
@@ -3774,7 +3765,7 @@
"exportName": "ProviderWrapStreamFnContext",
"kind": "type",
"source": {
"line": 488,
"line": 477,
"path": "src/plugins/types.ts"
}
},
@@ -3819,7 +3810,7 @@
"exportName": "SpeechProviderPlugin",
"kind": "type",
"source": {
"line": 944,
"line": 933,
"path": "src/plugins/types.ts"
}
},
@@ -3884,7 +3875,7 @@
"exportName": "definePluginEntry",
"kind": "function",
"source": {
"line": 90,
"line": 88,
"path": "src/plugin-sdk/plugin-entry.ts"
}
},
@@ -3911,7 +3902,7 @@
"exportName": "MediaUnderstandingProviderPlugin",
"kind": "type",
"source": {
"line": 969,
"line": 951,
"path": "src/plugins/types.ts"
}
},
@@ -3929,7 +3920,7 @@
"exportName": "OpenClawPluginApi",
"kind": "type",
"source": {
"line": 1390,
"line": 1314,
"path": "src/plugins/types.ts"
}
},
@@ -3938,7 +3929,7 @@
"exportName": "OpenClawPluginCommandDefinition",
"kind": "type",
"source": {
"line": 1108,
"line": 1068,
"path": "src/plugins/types.ts"
}
},
@@ -3947,7 +3938,7 @@
"exportName": "OpenClawPluginConfigSchema",
"kind": "type",
"source": {
"line": 95,
"line": 88,
"path": "src/plugins/types.ts"
}
},
@@ -3956,7 +3947,7 @@
"exportName": "OpenClawPluginDefinition",
"kind": "type",
"source": {
"line": 1372,
"line": 1296,
"path": "src/plugins/types.ts"
}
},
@@ -3965,7 +3956,7 @@
"exportName": "OpenClawPluginService",
"kind": "type",
"source": {
"line": 1339,
"line": 1285,
"path": "src/plugins/types.ts"
}
},
@@ -3974,25 +3965,7 @@
"exportName": "OpenClawPluginServiceContext",
"kind": "type",
"source": {
"line": 1331,
"path": "src/plugins/types.ts"
}
},
{
"declaration": "export type OpenClawPluginToolContext = OpenClawPluginToolContext;",
"exportName": "OpenClawPluginToolContext",
"kind": "type",
"source": {
"line": 110,
"path": "src/plugins/types.ts"
}
},
{
"declaration": "export type OpenClawPluginToolFactory = OpenClawPluginToolFactory;",
"exportName": "OpenClawPluginToolFactory",
"kind": "type",
"source": {
"line": 131,
"line": 1277,
"path": "src/plugins/types.ts"
}
},
@@ -4001,7 +3974,7 @@
"exportName": "PluginCommandContext",
"kind": "type",
"source": {
"line": 984,
"line": 966,
"path": "src/plugins/types.ts"
}
},
@@ -4010,7 +3983,7 @@
"exportName": "PluginInteractiveTelegramHandlerContext",
"kind": "type",
"source": {
"line": 1145,
"line": 1097,
"path": "src/plugins/types.ts"
}
},
@@ -4019,7 +3992,7 @@
"exportName": "PluginLogger",
"kind": "type",
"source": {
"line": 66,
"line": 59,
"path": "src/plugins/types.ts"
}
},
@@ -4028,7 +4001,7 @@
"exportName": "ProviderAugmentModelCatalogContext",
"kind": "type",
"source": {
"line": 582,
"line": 571,
"path": "src/plugins/types.ts"
}
},
@@ -4037,7 +4010,7 @@
"exportName": "ProviderAuthContext",
"kind": "type",
"source": {
"line": 166,
"line": 155,
"path": "src/plugins/types.ts"
}
},
@@ -4046,7 +4019,7 @@
"exportName": "ProviderAuthDoctorHintContext",
"kind": "type",
"source": {
"line": 457,
"line": 446,
"path": "src/plugins/types.ts"
}
},
@@ -4055,7 +4028,7 @@
"exportName": "ProviderAuthMethod",
"kind": "type",
"source": {
"line": 244,
"line": 233,
"path": "src/plugins/types.ts"
}
},
@@ -4064,7 +4037,7 @@
"exportName": "ProviderAuthMethodNonInteractiveContext",
"kind": "type",
"source": {
"line": 228,
"line": 217,
"path": "src/plugins/types.ts"
}
},
@@ -4073,7 +4046,7 @@
"exportName": "ProviderAuthResult",
"kind": "type",
"source": {
"line": 151,
"line": 140,
"path": "src/plugins/types.ts"
}
},
@@ -4082,7 +4055,7 @@
"exportName": "ProviderBuildMissingAuthMessageContext",
"kind": "type",
"source": {
"line": 510,
"line": 499,
"path": "src/plugins/types.ts"
}
},
@@ -4091,7 +4064,7 @@
"exportName": "ProviderBuiltInModelSuppressionContext",
"kind": "type",
"source": {
"line": 526,
"line": 515,
"path": "src/plugins/types.ts"
}
},
@@ -4100,7 +4073,7 @@
"exportName": "ProviderBuiltInModelSuppressionResult",
"kind": "type",
"source": {
"line": 535,
"line": 524,
"path": "src/plugins/types.ts"
}
},
@@ -4109,7 +4082,7 @@
"exportName": "ProviderCacheTtlEligibilityContext",
"kind": "type",
"source": {
"line": 498,
"line": 487,
"path": "src/plugins/types.ts"
}
},
@@ -4118,7 +4091,7 @@
"exportName": "ProviderCatalogContext",
"kind": "type",
"source": {
"line": 265,
"line": 254,
"path": "src/plugins/types.ts"
}
},
@@ -4127,7 +4100,7 @@
"exportName": "ProviderCatalogResult",
"kind": "type",
"source": {
"line": 288,
"line": 277,
"path": "src/plugins/types.ts"
}
},
@@ -4136,7 +4109,7 @@
"exportName": "ProviderDefaultThinkingPolicyContext",
"kind": "type",
"source": {
"line": 559,
"line": 548,
"path": "src/plugins/types.ts"
}
},
@@ -4145,7 +4118,7 @@
"exportName": "ProviderDiscoveryContext",
"kind": "type",
"source": {
"line": 598,
"line": 587,
"path": "src/plugins/types.ts"
}
},
@@ -4154,7 +4127,7 @@
"exportName": "ProviderFetchUsageSnapshotContext",
"kind": "type",
"source": {
"line": 438,
"line": 427,
"path": "src/plugins/types.ts"
}
},
@@ -4163,7 +4136,7 @@
"exportName": "ProviderModernModelPolicyContext",
"kind": "type",
"source": {
"line": 569,
"line": 558,
"path": "src/plugins/types.ts"
}
},
@@ -4172,7 +4145,7 @@
"exportName": "ProviderNormalizeResolvedModelContext",
"kind": "type",
"source": {
"line": 349,
"line": 338,
"path": "src/plugins/types.ts"
}
},
@@ -4181,7 +4154,7 @@
"exportName": "ProviderPreparedRuntimeAuth",
"kind": "type",
"source": {
"line": 385,
"line": 374,
"path": "src/plugins/types.ts"
}
},
@@ -4190,7 +4163,7 @@
"exportName": "ProviderPrepareDynamicModelContext",
"kind": "type",
"source": {
"line": 340,
"line": 329,
"path": "src/plugins/types.ts"
}
},
@@ -4199,7 +4172,7 @@
"exportName": "ProviderPrepareExtraParamsContext",
"kind": "type",
"source": {
"line": 471,
"line": 460,
"path": "src/plugins/types.ts"
}
},
@@ -4208,7 +4181,7 @@
"exportName": "ProviderPrepareRuntimeAuthContext",
"kind": "type",
"source": {
"line": 364,
"line": 353,
"path": "src/plugins/types.ts"
}
},
@@ -4217,7 +4190,7 @@
"exportName": "ProviderResolvedUsageAuth",
"kind": "type",
"source": {
"line": 425,
"line": 414,
"path": "src/plugins/types.ts"
}
},
@@ -4226,7 +4199,7 @@
"exportName": "ProviderResolveDynamicModelContext",
"kind": "type",
"source": {
"line": 323,
"line": 312,
"path": "src/plugins/types.ts"
}
},
@@ -4235,7 +4208,7 @@
"exportName": "ProviderResolveUsageAuthContext",
"kind": "type",
"source": {
"line": 406,
"line": 395,
"path": "src/plugins/types.ts"
}
},
@@ -4244,7 +4217,7 @@
"exportName": "ProviderRuntimeModel",
"kind": "type",
"source": {
"line": 306,
"line": 295,
"path": "src/plugins/types.ts"
}
},
@@ -4253,7 +4226,7 @@
"exportName": "ProviderThinkingPolicyContext",
"kind": "type",
"source": {
"line": 547,
"line": 536,
"path": "src/plugins/types.ts"
}
},
@@ -4262,7 +4235,7 @@
"exportName": "ProviderWrapStreamFnContext",
"kind": "type",
"source": {
"line": 488,
"line": 477,
"path": "src/plugins/types.ts"
}
},
@@ -4271,7 +4244,7 @@
"exportName": "SpeechProviderPlugin",
"kind": "type",
"source": {
"line": 944,
"line": 933,
"path": "src/plugins/types.ts"
}
}
@@ -5115,15 +5088,6 @@
"path": "src/infra/http-body.ts"
}
},
{
"declaration": "export function resolveRequestClientIp(req?: IncomingMessage | undefined, trustedProxies?: string[] | undefined, allowRealIpFallback?: boolean): string | undefined;",
"exportName": "resolveRequestClientIp",
"kind": "function",
"source": {
"line": 186,
"path": "src/gateway/net.ts"
}
},
{
"declaration": "export function resolveSingleWebhookTarget<T>(targets: readonly T[], isMatch: (target: T) => boolean): WebhookTargetMatchResult<T>;",
"exportName": "resolveSingleWebhookTarget",

View File

@@ -1,4 +1,7 @@
{"category":"legacy","entrypoint":"index","importSpecifier":"openclaw/plugin-sdk","recordType":"module","sourceLine":1,"sourcePath":"src/plugin-sdk/index.ts"}
{"declaration":"export function buildFalImageGenerationProvider(): ImageGenerationProvider;","entrypoint":"index","exportName":"buildFalImageGenerationProvider","importSpecifier":"openclaw/plugin-sdk","kind":"function","recordType":"export","sourceLine":190,"sourcePath":"extensions/fal/image-generation-provider.ts"}
{"declaration":"export function buildGoogleImageGenerationProvider(): ImageGenerationProvider;","entrypoint":"index","exportName":"buildGoogleImageGenerationProvider","importSpecifier":"openclaw/plugin-sdk","kind":"function","recordType":"export","sourceLine":98,"sourcePath":"extensions/google/image-generation-provider.ts"}
{"declaration":"export function buildOpenAIImageGenerationProvider(): ImageGenerationProvider;","entrypoint":"index","exportName":"buildOpenAIImageGenerationProvider","importSpecifier":"openclaw/plugin-sdk","kind":"function","recordType":"export","sourceLine":22,"sourcePath":"extensions/openai/image-generation-provider.ts"}
{"declaration":"export function delegateCompactionToRuntime(params: { sessionId: string; sessionKey?: string | undefined; sessionFile: string; tokenBudget?: number | undefined; force?: boolean | undefined; currentTokenCount?: number | undefined; compactionTarget?: \"budget\" | ... 1 more ... | undefined; customInstructions?: string | undefined; runtimeContext?: ContextEngineRuntimeContext | undefined; }): Promise<...>;","entrypoint":"index","exportName":"delegateCompactionToRuntime","importSpecifier":"openclaw/plugin-sdk","kind":"function","recordType":"export","sourceLine":16,"sourcePath":"src/context-engine/delegate.ts"}
{"declaration":"export function emptyPluginConfigSchema(): OpenClawPluginConfigSchema;","entrypoint":"index","exportName":"emptyPluginConfigSchema","importSpecifier":"openclaw/plugin-sdk","kind":"function","recordType":"export","sourceLine":13,"sourcePath":"src/plugins/config-schema.ts"}
{"declaration":"export function onDiagnosticEvent(listener: (evt: DiagnosticEventPayload) => void): () => void;","entrypoint":"index","exportName":"onDiagnosticEvent","importSpecifier":"openclaw/plugin-sdk","kind":"function","recordType":"export","sourceLine":229,"sourcePath":"src/infra/diagnostic-events.ts"}
@@ -16,7 +19,7 @@
{"declaration":"export type ChannelId = ChannelId;","entrypoint":"index","exportName":"ChannelId","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":13,"sourcePath":"src/channels/plugins/types.core.ts"}
{"declaration":"export type ChannelMessageActionAdapter = ChannelMessageActionAdapter;","entrypoint":"index","exportName":"ChannelMessageActionAdapter","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":516,"sourcePath":"src/channels/plugins/types.core.ts"}
{"declaration":"export type ChannelMessageActionContext = ChannelMessageActionContext;","entrypoint":"index","exportName":"ChannelMessageActionContext","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":482,"sourcePath":"src/channels/plugins/types.core.ts"}
{"declaration":"export type ChannelMessageActionName = \"send\" | \"broadcast\" | \"poll\" | \"poll-vote\" | \"react\" | \"reactions\" | \"read\" | \"edit\" | \"unsend\" | \"reply\" | \"sendWithEffect\" | \"renameGroup\" | \"setGroupIcon\" | \"addParticipant\" | \"removeParticipant\" | \"leaveGroup\" | \"sendAttachment\" | \"delete\" | \"pin\" | \"unpin\" | \"list-pins\" | \"permissions\" | \"thread-create\" | \"thread-list\" | \"thread-reply\" | \"search\" | \"sticker\" | \"sticker-search\" | \"member-info\" | \"role-info\" | \"emoji-list\" | \"emoji-upload\" | \"sticker-upload\" | \"role-add\" | \"role-remove\" | \"channel-info\" | \"channel-list\" | \"channel-create\" | \"channel-edit\" | \"channel-delete\" | \"channel-move\" | \"category-create\" | \"category-edit\" | \"category-delete\" | \"topic-create\" | \"topic-edit\" | \"voice-status\" | \"event-list\" | \"event-create\" | \"timeout\" | \"kick\" | \"ban\" | \"set-profile\" | \"set-presence\" | \"download-file\" | \"upload-file\";","entrypoint":"index","exportName":"ChannelMessageActionName","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":6,"sourcePath":"src/channels/plugins/types.ts"}
{"declaration":"export type ChannelMessageActionName = \"send\" | \"broadcast\" | \"poll\" | \"poll-vote\" | \"react\" | \"reactions\" | \"read\" | \"edit\" | \"unsend\" | \"reply\" | \"sendWithEffect\" | \"renameGroup\" | \"setGroupIcon\" | \"addParticipant\" | \"removeParticipant\" | \"leaveGroup\" | \"sendAttachment\" | \"delete\" | \"pin\" | \"unpin\" | \"list-pins\" | \"permissions\" | \"thread-create\" | \"thread-list\" | \"thread-reply\" | \"search\" | \"sticker\" | \"sticker-search\" | \"member-info\" | \"role-info\" | \"emoji-list\" | \"emoji-upload\" | \"sticker-upload\" | \"role-add\" | \"role-remove\" | \"channel-info\" | \"channel-list\" | \"channel-create\" | \"channel-edit\" | \"channel-delete\" | \"channel-move\" | \"category-create\" | \"category-edit\" | \"category-delete\" | \"topic-create\" | \"topic-edit\" | \"voice-status\" | \"event-list\" | \"event-create\" | \"timeout\" | \"kick\" | \"ban\" | \"set-profile\" | \"set-presence\" | \"download-file\";","entrypoint":"index","exportName":"ChannelMessageActionName","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":6,"sourcePath":"src/channels/plugins/types.ts"}
{"declaration":"export type ChannelPlugin = ChannelPlugin<ResolvedAccount, Probe, Audit>;","entrypoint":"index","exportName":"ChannelPlugin","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":55,"sourcePath":"src/channels/plugins/types.plugin.ts"}
{"declaration":"export type ChannelSetupAdapter = ChannelSetupAdapter;","entrypoint":"index","exportName":"ChannelSetupAdapter","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":56,"sourcePath":"src/channels/plugins/types.adapters.ts"}
{"declaration":"export type ChannelSetupInput = ChannelSetupInput;","entrypoint":"index","exportName":"ChannelSetupInput","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":63,"sourcePath":"src/channels/plugins/types.core.ts"}
@@ -24,8 +27,6 @@
{"declaration":"export type ChannelSetupWizardAllowFromEntry = ChannelSetupWizardAllowFromEntry;","entrypoint":"index","exportName":"ChannelSetupWizardAllowFromEntry","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":154,"sourcePath":"src/channels/plugins/setup-wizard.ts"}
{"declaration":"export type ChannelStatusIssue = ChannelStatusIssue;","entrypoint":"index","exportName":"ChannelStatusIssue","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":100,"sourcePath":"src/channels/plugins/types.core.ts"}
{"declaration":"export type OpenClawConfig = OpenClawConfig;","entrypoint":"index","exportName":"ClawdbotConfig","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":32,"sourcePath":"src/config/types.openclaw.ts"}
{"declaration":"export type CliBackendConfig = CliBackendConfig;","entrypoint":"index","exportName":"CliBackendConfig","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":47,"sourcePath":"src/config/types.agent-defaults.ts"}
{"declaration":"export type CliBackendPlugin = CliBackendPlugin;","entrypoint":"index","exportName":"CliBackendPlugin","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":1346,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type CompiledConfiguredBinding = CompiledConfiguredBinding;","entrypoint":"index","exportName":"CompiledConfiguredBinding","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":38,"sourcePath":"src/channels/plugins/binding-types.ts"}
{"declaration":"export type ConfiguredBindingConversation = ConversationRef;","entrypoint":"index","exportName":"ConfiguredBindingConversation","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":13,"sourcePath":"src/channels/plugins/binding-types.ts"}
{"declaration":"export type ConfiguredBindingResolution = ConfiguredBindingResolution;","entrypoint":"index","exportName":"ConfiguredBindingResolution","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":49,"sourcePath":"src/channels/plugins/binding-types.ts"}
@@ -41,21 +42,21 @@
{"declaration":"export type ImageGenerationResolution = ImageGenerationResolution;","entrypoint":"index","exportName":"ImageGenerationResolution","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":12,"sourcePath":"src/image-generation/types.ts"}
{"declaration":"export type ImageGenerationResult = ImageGenerationResult;","entrypoint":"index","exportName":"ImageGenerationResult","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":36,"sourcePath":"src/image-generation/types.ts"}
{"declaration":"export type ImageGenerationSourceImage = ImageGenerationSourceImage;","entrypoint":"index","exportName":"ImageGenerationSourceImage","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":14,"sourcePath":"src/image-generation/types.ts"}
{"declaration":"export type MediaUnderstandingProviderPlugin = MediaUnderstandingProvider;","entrypoint":"index","exportName":"MediaUnderstandingProviderPlugin","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":969,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type MediaUnderstandingProviderPlugin = MediaUnderstandingProvider;","entrypoint":"index","exportName":"MediaUnderstandingProviderPlugin","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":951,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawConfig = OpenClawConfig;","entrypoint":"index","exportName":"OpenClawConfig","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":32,"sourcePath":"src/config/types.openclaw.ts"}
{"declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"index","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":1390,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginConfigSchema = OpenClawPluginConfigSchema;","entrypoint":"index","exportName":"OpenClawPluginConfigSchema","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":95,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type PluginLogger = PluginLogger;","entrypoint":"index","exportName":"PluginLogger","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":66,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"index","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":1314,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginConfigSchema = OpenClawPluginConfigSchema;","entrypoint":"index","exportName":"OpenClawPluginConfigSchema","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":88,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type PluginLogger = PluginLogger;","entrypoint":"index","exportName":"PluginLogger","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":59,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type PluginRuntime = PluginRuntime;","entrypoint":"index","exportName":"PluginRuntime","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":54,"sourcePath":"src/plugins/runtime/types.ts"}
{"declaration":"export type ProviderAuthContext = ProviderAuthContext;","entrypoint":"index","exportName":"ProviderAuthContext","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":166,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthResult = ProviderAuthResult;","entrypoint":"index","exportName":"ProviderAuthResult","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":151,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderRuntimeModel = ProviderRuntimeModel;","entrypoint":"index","exportName":"ProviderRuntimeModel","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":306,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthContext = ProviderAuthContext;","entrypoint":"index","exportName":"ProviderAuthContext","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":155,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthResult = ProviderAuthResult;","entrypoint":"index","exportName":"ProviderAuthResult","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":140,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderRuntimeModel = ProviderRuntimeModel;","entrypoint":"index","exportName":"ProviderRuntimeModel","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":295,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ReplyPayload = ReplyPayload;","entrypoint":"index","exportName":"ReplyPayload","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":76,"sourcePath":"src/auto-reply/types.ts"}
{"declaration":"export type RuntimeEnv = RuntimeEnv;","entrypoint":"index","exportName":"RuntimeEnv","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":4,"sourcePath":"src/runtime.ts"}
{"declaration":"export type RuntimeLogger = RuntimeLogger;","entrypoint":"index","exportName":"RuntimeLogger","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":7,"sourcePath":"src/plugins/runtime/types-core.ts"}
{"declaration":"export type SecretInput = SecretInput;","entrypoint":"index","exportName":"SecretInput","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":16,"sourcePath":"src/config/types.secrets.ts"}
{"declaration":"export type SecretRef = SecretRef;","entrypoint":"index","exportName":"SecretRef","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":10,"sourcePath":"src/config/types.secrets.ts"}
{"declaration":"export type SpeechProviderPlugin = SpeechProviderPlugin;","entrypoint":"index","exportName":"SpeechProviderPlugin","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":944,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type SpeechProviderPlugin = SpeechProviderPlugin;","entrypoint":"index","exportName":"SpeechProviderPlugin","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":933,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type StatefulBindingTargetDescriptor = StatefulBindingTargetDescriptor;","entrypoint":"index","exportName":"StatefulBindingTargetDescriptor","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":17,"sourcePath":"src/channels/plugins/binding-types.ts"}
{"declaration":"export type StatefulBindingTargetDriver = StatefulBindingTargetDriver;","entrypoint":"index","exportName":"StatefulBindingTargetDriver","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":15,"sourcePath":"src/channels/plugins/stateful-target-drivers.ts"}
{"declaration":"export type StatefulBindingTargetReadyResult = StatefulBindingTargetReadyResult;","entrypoint":"index","exportName":"StatefulBindingTargetReadyResult","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":7,"sourcePath":"src/channels/plugins/stateful-target-drivers.ts"}
@@ -97,13 +98,11 @@
{"declaration":"export type BasicAllowlistResolutionEntry = BasicAllowlistResolutionEntry;","entrypoint":"allow-from","exportName":"BasicAllowlistResolutionEntry","importSpecifier":"openclaw/plugin-sdk/allow-from","kind":"type","recordType":"export","sourceLine":129,"sourcePath":"src/plugin-sdk/allow-from.ts"}
{"declaration":"export type CompiledAllowlist = CompiledAllowlist;","entrypoint":"allow-from","exportName":"CompiledAllowlist","importSpecifier":"openclaw/plugin-sdk/allow-from","kind":"type","recordType":"export","sourceLine":19,"sourcePath":"src/channels/allowlist-match.ts"}
{"category":"channel","entrypoint":"channel-actions","importSpecifier":"openclaw/plugin-sdk/channel-actions","recordType":"module","sourceLine":1,"sourcePath":"src/plugin-sdk/channel-actions.ts"}
{"declaration":"export function createMessageToolButtonsSchema(): TSchema;","entrypoint":"channel-actions","exportName":"createMessageToolButtonsSchema","importSpecifier":"openclaw/plugin-sdk/channel-actions","kind":"function","recordType":"export","sourceLine":12,"sourcePath":"src/plugin-sdk/channel-actions.ts"}
{"declaration":"export function createMessageToolCardSchema(): TSchema;","entrypoint":"channel-actions","exportName":"createMessageToolCardSchema","importSpecifier":"openclaw/plugin-sdk/channel-actions","kind":"function","recordType":"export","sourceLine":30,"sourcePath":"src/plugin-sdk/channel-actions.ts"}
{"declaration":"export function createMessageToolButtonsSchema(): TSchema;","entrypoint":"channel-actions","exportName":"createMessageToolButtonsSchema","importSpecifier":"openclaw/plugin-sdk/channel-actions","kind":"function","recordType":"export","sourceLine":11,"sourcePath":"src/plugin-sdk/channel-actions.ts"}
{"declaration":"export function createMessageToolCardSchema(): TSchema;","entrypoint":"channel-actions","exportName":"createMessageToolCardSchema","importSpecifier":"openclaw/plugin-sdk/channel-actions","kind":"function","recordType":"export","sourceLine":29,"sourcePath":"src/plugin-sdk/channel-actions.ts"}
{"declaration":"export function createUnionActionGate<TAccount, TKey extends string>(accounts: readonly TAccount[], createGate: (account: TAccount) => OptionalDefaultGate<TKey>): OptionalDefaultGate<TKey>;","entrypoint":"channel-actions","exportName":"createUnionActionGate","importSpecifier":"openclaw/plugin-sdk/channel-actions","kind":"function","recordType":"export","sourceLine":13,"sourcePath":"src/channels/plugins/actions/shared.ts"}
{"declaration":"export function listTokenSourcedAccounts<TAccount extends TokenSourcedAccount>(accounts: readonly TAccount[]): TAccount[];","entrypoint":"channel-actions","exportName":"listTokenSourcedAccounts","importSpecifier":"openclaw/plugin-sdk/channel-actions","kind":"function","recordType":"export","sourceLine":7,"sourcePath":"src/channels/plugins/actions/shared.ts"}
{"declaration":"export function optionalStringEnum<T extends readonly string[]>(values: T, options?: StringEnumOptions<T>): TOptional<TUnsafe<T[number]>>;","entrypoint":"channel-actions","exportName":"optionalStringEnum","importSpecifier":"openclaw/plugin-sdk/channel-actions","kind":"function","recordType":"export","sourceLine":31,"sourcePath":"src/agents/schema/typebox.ts"}
{"declaration":"export function resolveReactionMessageId(params: { args: Record<string, unknown>; toolContext?: ReactionToolContext | undefined; }): string | number | undefined;","entrypoint":"channel-actions","exportName":"resolveReactionMessageId","importSpecifier":"openclaw/plugin-sdk/channel-actions","kind":"function","recordType":"export","sourceLine":7,"sourcePath":"src/channels/plugins/actions/reaction-message-id.ts"}
{"declaration":"export function stringEnum<T extends readonly string[]>(values: T, options?: StringEnumOptions<T>): TUnsafe<T[number]>;","entrypoint":"channel-actions","exportName":"stringEnum","importSpecifier":"openclaw/plugin-sdk/channel-actions","kind":"function","recordType":"export","sourceLine":15,"sourcePath":"src/agents/schema/typebox.ts"}
{"category":"channel","entrypoint":"channel-config-schema","importSpecifier":"openclaw/plugin-sdk/channel-config-schema","recordType":"module","sourceLine":1,"sourcePath":"src/plugin-sdk/channel-config-schema.ts"}
{"declaration":"export function buildCatchallMultiAccountChannelSchema<T extends ExtendableZodObject>(accountSchema: T): T;","entrypoint":"channel-config-schema","exportName":"buildCatchallMultiAccountChannelSchema","importSpecifier":"openclaw/plugin-sdk/channel-config-schema","kind":"function","recordType":"export","sourceLine":26,"sourcePath":"src/channels/plugins/config-schema.ts"}
{"declaration":"export function buildChannelConfigSchema(schema: ZodType<unknown, unknown, $ZodTypeInternals<unknown, unknown>>): ChannelConfigSchema;","entrypoint":"channel-config-schema","exportName":"buildChannelConfigSchema","importSpecifier":"openclaw/plugin-sdk/channel-config-schema","kind":"function","recordType":"export","sourceLine":35,"sourcePath":"src/channels/plugins/config-schema.ts"}
@@ -122,7 +121,7 @@
{"declaration":"export type ChannelMessageActionAdapter = ChannelMessageActionAdapter;","entrypoint":"channel-contract","exportName":"ChannelMessageActionAdapter","importSpecifier":"openclaw/plugin-sdk/channel-contract","kind":"type","recordType":"export","sourceLine":516,"sourcePath":"src/channels/plugins/types.core.ts"}
{"declaration":"export type ChannelMessageActionContext = ChannelMessageActionContext;","entrypoint":"channel-contract","exportName":"ChannelMessageActionContext","importSpecifier":"openclaw/plugin-sdk/channel-contract","kind":"type","recordType":"export","sourceLine":482,"sourcePath":"src/channels/plugins/types.core.ts"}
{"declaration":"export type ChannelMessageActionDiscoveryContext = ChannelMessageActionDiscoveryContext;","entrypoint":"channel-contract","exportName":"ChannelMessageActionDiscoveryContext","importSpecifier":"openclaw/plugin-sdk/channel-contract","kind":"type","recordType":"export","sourceLine":31,"sourcePath":"src/channels/plugins/types.core.ts"}
{"declaration":"export type ChannelMessageActionName = \"send\" | \"broadcast\" | \"poll\" | \"poll-vote\" | \"react\" | \"reactions\" | \"read\" | \"edit\" | \"unsend\" | \"reply\" | \"sendWithEffect\" | \"renameGroup\" | \"setGroupIcon\" | \"addParticipant\" | \"removeParticipant\" | \"leaveGroup\" | \"sendAttachment\" | \"delete\" | \"pin\" | \"unpin\" | \"list-pins\" | \"permissions\" | \"thread-create\" | \"thread-list\" | \"thread-reply\" | \"search\" | \"sticker\" | \"sticker-search\" | \"member-info\" | \"role-info\" | \"emoji-list\" | \"emoji-upload\" | \"sticker-upload\" | \"role-add\" | \"role-remove\" | \"channel-info\" | \"channel-list\" | \"channel-create\" | \"channel-edit\" | \"channel-delete\" | \"channel-move\" | \"category-create\" | \"category-edit\" | \"category-delete\" | \"topic-create\" | \"topic-edit\" | \"voice-status\" | \"event-list\" | \"event-create\" | \"timeout\" | \"kick\" | \"ban\" | \"set-profile\" | \"set-presence\" | \"download-file\" | \"upload-file\";","entrypoint":"channel-contract","exportName":"ChannelMessageActionName","importSpecifier":"openclaw/plugin-sdk/channel-contract","kind":"type","recordType":"export","sourceLine":6,"sourcePath":"src/channels/plugins/types.ts"}
{"declaration":"export type ChannelMessageActionName = \"send\" | \"broadcast\" | \"poll\" | \"poll-vote\" | \"react\" | \"reactions\" | \"read\" | \"edit\" | \"unsend\" | \"reply\" | \"sendWithEffect\" | \"renameGroup\" | \"setGroupIcon\" | \"addParticipant\" | \"removeParticipant\" | \"leaveGroup\" | \"sendAttachment\" | \"delete\" | \"pin\" | \"unpin\" | \"list-pins\" | \"permissions\" | \"thread-create\" | \"thread-list\" | \"thread-reply\" | \"search\" | \"sticker\" | \"sticker-search\" | \"member-info\" | \"role-info\" | \"emoji-list\" | \"emoji-upload\" | \"sticker-upload\" | \"role-add\" | \"role-remove\" | \"channel-info\" | \"channel-list\" | \"channel-create\" | \"channel-edit\" | \"channel-delete\" | \"channel-move\" | \"category-create\" | \"category-edit\" | \"category-delete\" | \"topic-create\" | \"topic-edit\" | \"voice-status\" | \"event-list\" | \"event-create\" | \"timeout\" | \"kick\" | \"ban\" | \"set-profile\" | \"set-presence\" | \"download-file\";","entrypoint":"channel-contract","exportName":"ChannelMessageActionName","importSpecifier":"openclaw/plugin-sdk/channel-contract","kind":"type","recordType":"export","sourceLine":6,"sourcePath":"src/channels/plugins/types.ts"}
{"declaration":"export type ChannelMessageToolDiscovery = ChannelMessageToolDiscovery;","entrypoint":"channel-contract","exportName":"ChannelMessageToolDiscovery","importSpecifier":"openclaw/plugin-sdk/channel-contract","kind":"type","recordType":"export","sourceLine":56,"sourcePath":"src/channels/plugins/types.core.ts"}
{"declaration":"export type ChannelMessageToolSchemaContribution = ChannelMessageToolSchemaContribution;","entrypoint":"channel-contract","exportName":"ChannelMessageToolSchemaContribution","importSpecifier":"openclaw/plugin-sdk/channel-contract","kind":"type","recordType":"export","sourceLine":51,"sourcePath":"src/channels/plugins/types.core.ts"}
{"declaration":"export type ChannelStatusIssue = ChannelStatusIssue;","entrypoint":"channel-contract","exportName":"ChannelStatusIssue","importSpecifier":"openclaw/plugin-sdk/channel-contract","kind":"type","recordType":"export","sourceLine":100,"sourcePath":"src/channels/plugins/types.core.ts"}
@@ -164,7 +163,7 @@
{"declaration":"export function resolvePollMaxSelections(optionCount: number, allowMultiselect: boolean | undefined): number;","entrypoint":"channel-runtime","exportName":"resolvePollMaxSelections","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"function","recordType":"export","sourceLine":29,"sourcePath":"src/polls.ts"}
{"declaration":"export function resolveWhatsAppHeartbeatRecipients(cfg: OpenClawConfig, opts?: HeartbeatRecipientsOpts): HeartbeatRecipientsResult;","entrypoint":"channel-runtime","exportName":"resolveWhatsAppHeartbeatRecipients","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"function","recordType":"export","sourceLine":48,"sourcePath":"src/channels/plugins/whatsapp-heartbeat.ts"}
{"declaration":"export function waitUntilAbort(signal?: AbortSignal | undefined, onAbort?: (() => void | Promise<void>) | undefined): Promise<void>;","entrypoint":"channel-runtime","exportName":"waitUntilAbort","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"function","recordType":"export","sourceLine":38,"sourcePath":"src/plugin-sdk/channel-lifecycle.ts"}
{"declaration":"export const CHANNEL_MESSAGE_ACTION_NAMES: readonly [\"send\", \"broadcast\", \"poll\", \"poll-vote\", \"react\", \"reactions\", \"read\", \"edit\", \"unsend\", \"reply\", \"sendWithEffect\", \"renameGroup\", \"setGroupIcon\", \"addParticipant\", \"removeParticipant\", \"leaveGroup\", \"sendAttachment\", \"delete\", \"pin\", \"unpin\", \"list-pins\", \"permissions\", \"thread-create\", \"thread-list\", \"thread-reply\", \"search\", \"sticker\", \"sticker-search\", \"member-info\", \"role-info\", \"emoji-list\", \"emoji-upload\", \"sticker-upload\", \"role-add\", \"role-remove\", \"channel-info\", \"channel-list\", \"channel-create\", \"channel-edit\", \"channel-delete\", \"channel-move\", \"category-create\", \"category-edit\", \"category-delete\", \"topic-create\", \"topic-edit\", \"voice-status\", \"event-list\", \"event-create\", \"timeout\", \"kick\", \"ban\", \"set-profile\", \"set-presence\", \"set-profile\", \"download-file\", \"upload-file\"];","entrypoint":"channel-runtime","exportName":"CHANNEL_MESSAGE_ACTION_NAMES","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"const","recordType":"export","sourceLine":1,"sourcePath":"src/channels/plugins/message-action-names.ts"}
{"declaration":"export const CHANNEL_MESSAGE_ACTION_NAMES: readonly [\"send\", \"broadcast\", \"poll\", \"poll-vote\", \"react\", \"reactions\", \"read\", \"edit\", \"unsend\", \"reply\", \"sendWithEffect\", \"renameGroup\", \"setGroupIcon\", \"addParticipant\", \"removeParticipant\", \"leaveGroup\", \"sendAttachment\", \"delete\", \"pin\", \"unpin\", \"list-pins\", \"permissions\", \"thread-create\", \"thread-list\", \"thread-reply\", \"search\", \"sticker\", \"sticker-search\", \"member-info\", \"role-info\", \"emoji-list\", \"emoji-upload\", \"sticker-upload\", \"role-add\", \"role-remove\", \"channel-info\", \"channel-list\", \"channel-create\", \"channel-edit\", \"channel-delete\", \"channel-move\", \"category-create\", \"category-edit\", \"category-delete\", \"topic-create\", \"topic-edit\", \"voice-status\", \"event-list\", \"event-create\", \"timeout\", \"kick\", \"ban\", \"set-profile\", \"set-presence\", \"set-profile\", \"download-file\"];","entrypoint":"channel-runtime","exportName":"CHANNEL_MESSAGE_ACTION_NAMES","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"const","recordType":"export","sourceLine":1,"sourcePath":"src/channels/plugins/message-action-names.ts"}
{"declaration":"export const CHANNEL_MESSAGE_CAPABILITIES: readonly [\"interactive\", \"buttons\", \"cards\", \"components\", \"blocks\"];","entrypoint":"channel-runtime","exportName":"CHANNEL_MESSAGE_CAPABILITIES","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"const","recordType":"export","sourceLine":1,"sourcePath":"src/channels/plugins/message-capabilities.ts"}
{"declaration":"export type BaseProbeResult = BaseProbeResult<TError>;","entrypoint":"channel-runtime","exportName":"BaseProbeResult","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":559,"sourcePath":"src/channels/plugins/types.core.ts"}
{"declaration":"export type BaseTokenResolution = BaseTokenResolution;","entrypoint":"channel-runtime","exportName":"BaseTokenResolution","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":565,"sourcePath":"src/channels/plugins/types.core.ts"}
@@ -208,7 +207,7 @@
{"declaration":"export type ChannelMessageActionAdapter = ChannelMessageActionAdapter;","entrypoint":"channel-runtime","exportName":"ChannelMessageActionAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":516,"sourcePath":"src/channels/plugins/types.core.ts"}
{"declaration":"export type ChannelMessageActionContext = ChannelMessageActionContext;","entrypoint":"channel-runtime","exportName":"ChannelMessageActionContext","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":482,"sourcePath":"src/channels/plugins/types.core.ts"}
{"declaration":"export type ChannelMessageActionDiscoveryContext = ChannelMessageActionDiscoveryContext;","entrypoint":"channel-runtime","exportName":"ChannelMessageActionDiscoveryContext","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":31,"sourcePath":"src/channels/plugins/types.core.ts"}
{"declaration":"export type ChannelMessageActionName = \"send\" | \"broadcast\" | \"poll\" | \"poll-vote\" | \"react\" | \"reactions\" | \"read\" | \"edit\" | \"unsend\" | \"reply\" | \"sendWithEffect\" | \"renameGroup\" | \"setGroupIcon\" | \"addParticipant\" | \"removeParticipant\" | \"leaveGroup\" | \"sendAttachment\" | \"delete\" | \"pin\" | \"unpin\" | \"list-pins\" | \"permissions\" | \"thread-create\" | \"thread-list\" | \"thread-reply\" | \"search\" | \"sticker\" | \"sticker-search\" | \"member-info\" | \"role-info\" | \"emoji-list\" | \"emoji-upload\" | \"sticker-upload\" | \"role-add\" | \"role-remove\" | \"channel-info\" | \"channel-list\" | \"channel-create\" | \"channel-edit\" | \"channel-delete\" | \"channel-move\" | \"category-create\" | \"category-edit\" | \"category-delete\" | \"topic-create\" | \"topic-edit\" | \"voice-status\" | \"event-list\" | \"event-create\" | \"timeout\" | \"kick\" | \"ban\" | \"set-profile\" | \"set-presence\" | \"download-file\" | \"upload-file\";","entrypoint":"channel-runtime","exportName":"ChannelMessageActionName","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":6,"sourcePath":"src/channels/plugins/types.ts"}
{"declaration":"export type ChannelMessageActionName = \"send\" | \"broadcast\" | \"poll\" | \"poll-vote\" | \"react\" | \"reactions\" | \"read\" | \"edit\" | \"unsend\" | \"reply\" | \"sendWithEffect\" | \"renameGroup\" | \"setGroupIcon\" | \"addParticipant\" | \"removeParticipant\" | \"leaveGroup\" | \"sendAttachment\" | \"delete\" | \"pin\" | \"unpin\" | \"list-pins\" | \"permissions\" | \"thread-create\" | \"thread-list\" | \"thread-reply\" | \"search\" | \"sticker\" | \"sticker-search\" | \"member-info\" | \"role-info\" | \"emoji-list\" | \"emoji-upload\" | \"sticker-upload\" | \"role-add\" | \"role-remove\" | \"channel-info\" | \"channel-list\" | \"channel-create\" | \"channel-edit\" | \"channel-delete\" | \"channel-move\" | \"category-create\" | \"category-edit\" | \"category-delete\" | \"topic-create\" | \"topic-edit\" | \"voice-status\" | \"event-list\" | \"event-create\" | \"timeout\" | \"kick\" | \"ban\" | \"set-profile\" | \"set-presence\" | \"download-file\";","entrypoint":"channel-runtime","exportName":"ChannelMessageActionName","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":6,"sourcePath":"src/channels/plugins/types.ts"}
{"declaration":"export type ChannelMessageCapability = \"interactive\" | \"buttons\" | \"cards\" | \"components\" | \"blocks\";","entrypoint":"channel-runtime","exportName":"ChannelMessageCapability","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":9,"sourcePath":"src/channels/plugins/message-capabilities.ts"}
{"declaration":"export type ChannelMessageToolDiscovery = ChannelMessageToolDiscovery;","entrypoint":"channel-runtime","exportName":"ChannelMessageToolDiscovery","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":56,"sourcePath":"src/channels/plugins/types.core.ts"}
{"declaration":"export type ChannelMessageToolSchemaContribution = ChannelMessageToolSchemaContribution;","entrypoint":"channel-runtime","exportName":"ChannelMessageToolSchemaContribution","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":51,"sourcePath":"src/channels/plugins/types.core.ts"}
@@ -261,7 +260,7 @@
{"category":"channel","entrypoint":"command-auth","importSpecifier":"openclaw/plugin-sdk/command-auth","recordType":"module","sourceLine":1,"sourcePath":"src/plugin-sdk/command-auth.ts"}
{"declaration":"export function buildCommandsMessage(cfg?: OpenClawConfig | undefined, skillCommands?: SkillCommandSpec[] | undefined, options?: CommandsMessageOptions | undefined): string;","entrypoint":"command-auth","exportName":"buildCommandsMessage","importSpecifier":"openclaw/plugin-sdk/command-auth","kind":"function","recordType":"export","sourceLine":1049,"sourcePath":"src/auto-reply/status.ts"}
{"declaration":"export function buildCommandsMessagePaginated(cfg?: OpenClawConfig | undefined, skillCommands?: SkillCommandSpec[] | undefined, options?: CommandsMessageOptions | undefined): CommandsMessageResult;","entrypoint":"command-auth","exportName":"buildCommandsMessagePaginated","importSpecifier":"openclaw/plugin-sdk/command-auth","kind":"function","recordType":"export","sourceLine":1058,"sourcePath":"src/auto-reply/status.ts"}
{"declaration":"export function buildCommandsPaginationKeyboard(currentPage: number, totalPages: number, agentId?: string | undefined): { text: string; callback_data: string; }[][];","entrypoint":"command-auth","exportName":"buildCommandsPaginationKeyboard","importSpecifier":"openclaw/plugin-sdk/command-auth","kind":"function","recordType":"export","sourceLine":175,"sourcePath":"src/auto-reply/reply/commands-info.ts"}
{"declaration":"export function buildCommandsPaginationKeyboard(currentPage: number, totalPages: number, agentId?: string | undefined): { text: string; callback_data: string; }[][];","entrypoint":"command-auth","exportName":"buildCommandsPaginationKeyboard","importSpecifier":"openclaw/plugin-sdk/command-auth","kind":"function","recordType":"export","sourceLine":196,"sourcePath":"src/auto-reply/reply/commands-info.ts"}
{"declaration":"export function buildCommandText(commandName: string, args?: string | undefined): string;","entrypoint":"command-auth","exportName":"buildCommandText","importSpecifier":"openclaw/plugin-sdk/command-auth","kind":"function","recordType":"export","sourceLine":199,"sourcePath":"src/auto-reply/commands-registry.ts"}
{"declaration":"export function buildCommandTextFromArgs(command: ChatCommandDefinition, args?: CommandArgs | undefined): string;","entrypoint":"command-auth","exportName":"buildCommandTextFromArgs","importSpecifier":"openclaw/plugin-sdk/command-auth","kind":"function","recordType":"export","sourceLine":291,"sourcePath":"src/auto-reply/commands-registry.ts"}
{"declaration":"export function buildHelpMessage(cfg?: OpenClawConfig | undefined): string;","entrypoint":"command-auth","exportName":"buildHelpMessage","importSpecifier":"openclaw/plugin-sdk/command-auth","kind":"function","recordType":"export","sourceLine":844,"sourcePath":"src/auto-reply/status.ts"}
@@ -331,15 +330,15 @@
{"declaration":"export function applyAccountNameToChannelSection(params: { cfg: OpenClawConfig; channelKey: string; accountId: string; name?: string | undefined; alwaysUseAccounts?: boolean | undefined; }): OpenClawConfig;","entrypoint":"core","exportName":"applyAccountNameToChannelSection","importSpecifier":"openclaw/plugin-sdk/core","kind":"function","recordType":"export","sourceLine":33,"sourcePath":"src/channels/plugins/setup-helpers.ts"}
{"declaration":"export function buildAgentSessionKey(params: { agentId: string; channel: string; accountId?: string | null | undefined; peer?: RoutePeer | null | undefined; dmScope?: \"main\" | \"per-peer\" | \"per-channel-peer\" | \"per-account-channel-peer\" | undefined; identityLinks?: Record<...> | undefined; }): string;","entrypoint":"core","exportName":"buildAgentSessionKey","importSpecifier":"openclaw/plugin-sdk/core","kind":"function","recordType":"export","sourceLine":91,"sourcePath":"src/routing/resolve-route.ts"}
{"declaration":"export function buildChannelConfigSchema(schema: ZodType<unknown, unknown, $ZodTypeInternals<unknown, unknown>>): ChannelConfigSchema;","entrypoint":"core","exportName":"buildChannelConfigSchema","importSpecifier":"openclaw/plugin-sdk/core","kind":"function","recordType":"export","sourceLine":35,"sourcePath":"src/channels/plugins/config-schema.ts"}
{"declaration":"export function buildChannelOutboundSessionRoute(params: { cfg: OpenClawConfig; agentId: string; channel: string; accountId?: string | null | undefined; peer: { kind: \"direct\" | \"group\" | \"channel\"; id: string; }; chatType: \"direct\" | \"group\" | \"channel\"; from: string; to: string; threadId?: string | ... 1 more ... | undefined; }): ChannelOutboundSessionRoute;","entrypoint":"core","exportName":"buildChannelOutboundSessionRoute","importSpecifier":"openclaw/plugin-sdk/core","kind":"function","recordType":"export","sourceLine":161,"sourcePath":"src/plugin-sdk/core.ts"}
{"declaration":"export function buildChannelOutboundSessionRoute(params: { cfg: OpenClawConfig; agentId: string; channel: string; accountId?: string | null | undefined; peer: { kind: \"direct\" | \"group\" | \"channel\"; id: string; }; chatType: \"direct\" | \"group\" | \"channel\"; from: string; to: string; threadId?: string | ... 1 more ... | undefined; }): ChannelOutboundSessionRoute;","entrypoint":"core","exportName":"buildChannelOutboundSessionRoute","importSpecifier":"openclaw/plugin-sdk/core","kind":"function","recordType":"export","sourceLine":163,"sourcePath":"src/plugin-sdk/core.ts"}
{"declaration":"export function channelTargetSchema(options?: { description?: string | undefined; } | undefined): TString;","entrypoint":"core","exportName":"channelTargetSchema","importSpecifier":"openclaw/plugin-sdk/core","kind":"function","recordType":"export","sourceLine":38,"sourcePath":"src/agents/schema/typebox.ts"}
{"declaration":"export function channelTargetsSchema(options?: { description?: string | undefined; } | undefined): TArray<TString>;","entrypoint":"core","exportName":"channelTargetsSchema","importSpecifier":"openclaw/plugin-sdk/core","kind":"function","recordType":"export","sourceLine":44,"sourcePath":"src/agents/schema/typebox.ts"}
{"declaration":"export function clearAccountEntryFields<TAccountEntry extends object>(params: { accounts?: Record<string, TAccountEntry> | undefined; accountId: string; fields: string[]; isValueSet?: ((value: unknown) => boolean) | undefined; markClearedOnFieldPresence?: boolean | undefined; }): { ...; };","entrypoint":"core","exportName":"clearAccountEntryFields","importSpecifier":"openclaw/plugin-sdk/core","kind":"function","recordType":"export","sourceLine":122,"sourcePath":"src/channels/plugins/config-helpers.ts"}
{"declaration":"export function createChannelPluginBase<TResolvedAccount>(params: CreateChannelPluginBaseOptions<TResolvedAccount>): CreatedChannelPluginBase<TResolvedAccount>;","entrypoint":"core","exportName":"createChannelPluginBase","importSpecifier":"openclaw/plugin-sdk/core","kind":"function","recordType":"export","sourceLine":435,"sourcePath":"src/plugin-sdk/core.ts"}
{"declaration":"export function createChatChannelPlugin<TResolvedAccount extends { accountId?: string | null; }, Probe = unknown, Audit = unknown>(params: { base: ChatChannelPluginBase<TResolvedAccount, Probe, Audit>; security?: ChannelSecurityAdapter<TResolvedAccount> | ChatChannelSecurityOptions<...> | undefined; pairing?: ChannelPairingAdapter | ... 1 more ... | undefined; threading?: ChannelThreadingAdapter | ... 1 more ... | undefined; outbound?: ChannelOutboundAdapter | ... 1 more ... | undefined; }): ChannelPlugin<...>;","entrypoint":"core","exportName":"createChatChannelPlugin","importSpecifier":"openclaw/plugin-sdk/core","kind":"function","recordType":"export","sourceLine":412,"sourcePath":"src/plugin-sdk/core.ts"}
{"declaration":"export function defineChannelPluginEntry<TPlugin>({ id, name, description, plugin, configSchema, setRuntime, registerFull, }: DefineChannelPluginEntryOptions<TPlugin>): DefinedPluginEntry;","entrypoint":"core","exportName":"defineChannelPluginEntry","importSpecifier":"openclaw/plugin-sdk/core","kind":"function","recordType":"export","sourceLine":244,"sourcePath":"src/plugin-sdk/core.ts"}
{"declaration":"export function definePluginEntry({ id, name, description, kind, configSchema, register, }: DefinePluginEntryOptions): DefinedPluginEntry;","entrypoint":"core","exportName":"definePluginEntry","importSpecifier":"openclaw/plugin-sdk/core","kind":"function","recordType":"export","sourceLine":90,"sourcePath":"src/plugin-sdk/plugin-entry.ts"}
{"declaration":"export function defineSetupPluginEntry<TPlugin>(plugin: TPlugin): { plugin: TPlugin; };","entrypoint":"core","exportName":"defineSetupPluginEntry","importSpecifier":"openclaw/plugin-sdk/core","kind":"function","recordType":"export","sourceLine":275,"sourcePath":"src/plugin-sdk/core.ts"}
{"declaration":"export function createChannelPluginBase<TResolvedAccount>(params: CreateChannelPluginBaseOptions<TResolvedAccount>): CreatedChannelPluginBase<TResolvedAccount>;","entrypoint":"core","exportName":"createChannelPluginBase","importSpecifier":"openclaw/plugin-sdk/core","kind":"function","recordType":"export","sourceLine":437,"sourcePath":"src/plugin-sdk/core.ts"}
{"declaration":"export function createChatChannelPlugin<TResolvedAccount extends { accountId?: string | null; }, Probe = unknown, Audit = unknown>(params: { base: ChatChannelPluginBase<TResolvedAccount, Probe, Audit>; security?: ChannelSecurityAdapter<TResolvedAccount> | ChatChannelSecurityOptions<...> | undefined; pairing?: ChannelPairingAdapter | ... 1 more ... | undefined; threading?: ChannelThreadingAdapter | ... 1 more ... | undefined; outbound?: ChannelOutboundAdapter | ... 1 more ... | undefined; }): ChannelPlugin<...>;","entrypoint":"core","exportName":"createChatChannelPlugin","importSpecifier":"openclaw/plugin-sdk/core","kind":"function","recordType":"export","sourceLine":414,"sourcePath":"src/plugin-sdk/core.ts"}
{"declaration":"export function defineChannelPluginEntry<TPlugin>({ id, name, description, plugin, configSchema, setRuntime, registerFull, }: DefineChannelPluginEntryOptions<TPlugin>): DefinedPluginEntry;","entrypoint":"core","exportName":"defineChannelPluginEntry","importSpecifier":"openclaw/plugin-sdk/core","kind":"function","recordType":"export","sourceLine":246,"sourcePath":"src/plugin-sdk/core.ts"}
{"declaration":"export function definePluginEntry({ id, name, description, kind, configSchema, register, }: DefinePluginEntryOptions): DefinedPluginEntry;","entrypoint":"core","exportName":"definePluginEntry","importSpecifier":"openclaw/plugin-sdk/core","kind":"function","recordType":"export","sourceLine":88,"sourcePath":"src/plugin-sdk/plugin-entry.ts"}
{"declaration":"export function defineSetupPluginEntry<TPlugin>(plugin: TPlugin): { plugin: TPlugin; };","entrypoint":"core","exportName":"defineSetupPluginEntry","importSpecifier":"openclaw/plugin-sdk/core","kind":"function","recordType":"export","sourceLine":277,"sourcePath":"src/plugin-sdk/core.ts"}
{"declaration":"export function delegateCompactionToRuntime(params: { sessionId: string; sessionKey?: string | undefined; sessionFile: string; tokenBudget?: number | undefined; force?: boolean | undefined; currentTokenCount?: number | undefined; compactionTarget?: \"budget\" | ... 1 more ... | undefined; customInstructions?: string | undefined; runtimeContext?: ContextEngineRuntimeContext | undefined; }): Promise<...>;","entrypoint":"core","exportName":"delegateCompactionToRuntime","importSpecifier":"openclaw/plugin-sdk/core","kind":"function","recordType":"export","sourceLine":16,"sourcePath":"src/context-engine/delegate.ts"}
{"declaration":"export function deleteAccountFromConfigSection(params: { cfg: OpenClawConfig; sectionKey: string; accountId: string; clearBaseFields?: string[] | undefined; }): OpenClawConfig;","entrypoint":"core","exportName":"deleteAccountFromConfigSection","importSpecifier":"openclaw/plugin-sdk/core","kind":"function","recordType":"export","sourceLine":60,"sourcePath":"src/channels/plugins/config-helpers.ts"}
{"declaration":"export function emptyPluginConfigSchema(): OpenClawPluginConfigSchema;","entrypoint":"core","exportName":"emptyPluginConfigSchema","importSpecifier":"openclaw/plugin-sdk/core","kind":"function","recordType":"export","sourceLine":13,"sourcePath":"src/plugins/config-schema.ts"}
@@ -361,8 +360,8 @@
{"declaration":"export function resolveThreadSessionKeys(params: { baseSessionKey: string; threadId?: string | null | undefined; parentSessionKey?: string | undefined; useSuffix?: boolean | undefined; normalizeThreadId?: ((threadId: string) => string) | undefined; }): { ...; };","entrypoint":"core","exportName":"resolveThreadSessionKeys","importSpecifier":"openclaw/plugin-sdk/core","kind":"function","recordType":"export","sourceLine":234,"sourcePath":"src/routing/session-key.ts"}
{"declaration":"export function setAccountEnabledInConfigSection(params: { cfg: OpenClawConfig; sectionKey: string; accountId: string; enabled: boolean; allowTopLevel?: boolean | undefined; }): OpenClawConfig;","entrypoint":"core","exportName":"setAccountEnabledInConfigSection","importSpecifier":"openclaw/plugin-sdk/core","kind":"function","recordType":"export","sourceLine":16,"sourcePath":"src/channels/plugins/config-helpers.ts"}
{"declaration":"export function stringEnum<T extends readonly string[]>(values: T, options?: StringEnumOptions<T>): TUnsafe<T[number]>;","entrypoint":"core","exportName":"stringEnum","importSpecifier":"openclaw/plugin-sdk/core","kind":"function","recordType":"export","sourceLine":15,"sourcePath":"src/agents/schema/typebox.ts"}
{"declaration":"export function stripChannelTargetPrefix(raw: string, ...providers: string[]): string;","entrypoint":"core","exportName":"stripChannelTargetPrefix","importSpecifier":"openclaw/plugin-sdk/core","kind":"function","recordType":"export","sourceLine":141,"sourcePath":"src/plugin-sdk/core.ts"}
{"declaration":"export function stripTargetKindPrefix(raw: string): string;","entrypoint":"core","exportName":"stripTargetKindPrefix","importSpecifier":"openclaw/plugin-sdk/core","kind":"function","recordType":"export","sourceLine":153,"sourcePath":"src/plugin-sdk/core.ts"}
{"declaration":"export function stripChannelTargetPrefix(raw: string, ...providers: string[]): string;","entrypoint":"core","exportName":"stripChannelTargetPrefix","importSpecifier":"openclaw/plugin-sdk/core","kind":"function","recordType":"export","sourceLine":143,"sourcePath":"src/plugin-sdk/core.ts"}
{"declaration":"export function stripTargetKindPrefix(raw: string): string;","entrypoint":"core","exportName":"stripTargetKindPrefix","importSpecifier":"openclaw/plugin-sdk/core","kind":"function","recordType":"export","sourceLine":155,"sourcePath":"src/plugin-sdk/core.ts"}
{"declaration":"export function tryReadSecretFileSync(filePath: string | undefined, label: string, options?: SecretFileReadOptions): string | undefined;","entrypoint":"core","exportName":"tryReadSecretFileSync","importSpecifier":"openclaw/plugin-sdk/core","kind":"function","recordType":"export","sourceLine":130,"sourcePath":"src/infra/secret-file.ts"}
{"declaration":"export const DEFAULT_ACCOUNT_ID: \"default\";","entrypoint":"core","exportName":"DEFAULT_ACCOUNT_ID","importSpecifier":"openclaw/plugin-sdk/core","kind":"const","recordType":"export","sourceLine":3,"sourcePath":"src/routing/account-id.ts"}
{"declaration":"export const DEFAULT_SECRET_FILE_MAX_BYTES: number;","entrypoint":"core","exportName":"DEFAULT_SECRET_FILE_MAX_BYTES","importSpecifier":"openclaw/plugin-sdk/core","kind":"const","recordType":"export","sourceLine":5,"sourcePath":"src/infra/secret-file.ts"}
@@ -370,107 +369,105 @@
{"declaration":"export type ChannelMessageActionContext = ChannelMessageActionContext;","entrypoint":"core","exportName":"ChannelMessageActionContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":482,"sourcePath":"src/channels/plugins/types.core.ts"}
{"declaration":"export type ChannelMessagingAdapter = ChannelMessagingAdapter;","entrypoint":"core","exportName":"ChannelMessagingAdapter","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":395,"sourcePath":"src/channels/plugins/types.core.ts"}
{"declaration":"export type ChannelOutboundSessionRoute = ChannelOutboundSessionRoute;","entrypoint":"core","exportName":"ChannelOutboundSessionRoute","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":309,"sourcePath":"src/channels/plugins/types.core.ts"}
{"declaration":"export type ChannelOutboundSessionRouteParams = { cfg: OpenClawConfig; agentId: string; accountId?: string | null; target: string; resolvedTarget?: { to: string; kind: import(\"src/channels/plugins/types.core\").ChannelDirectoryEntryKind | \"channel\"; display?: string; source: \"normalized\" | \"directory\"; }; replyToId?: string | null; threadId?: string | number | null;};","entrypoint":"core","exportName":"ChannelOutboundSessionRouteParams","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":136,"sourcePath":"src/plugin-sdk/core.ts"}
{"declaration":"export type ChannelOutboundSessionRouteParams = { cfg: OpenClawConfig; agentId: string; accountId?: string | null; target: string; resolvedTarget?: { to: string; kind: import(\"src/channels/plugins/types.core\").ChannelDirectoryEntryKind | \"channel\"; display?: string; source: \"normalized\" | \"directory\"; }; replyToId?: string | null; threadId?: string | number | null;};","entrypoint":"core","exportName":"ChannelOutboundSessionRouteParams","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":138,"sourcePath":"src/plugin-sdk/core.ts"}
{"declaration":"export type ChannelPlugin = ChannelPlugin<ResolvedAccount, Probe, Audit>;","entrypoint":"core","exportName":"ChannelPlugin","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":55,"sourcePath":"src/channels/plugins/types.plugin.ts"}
{"declaration":"export type GatewayBindUrlResult = GatewayBindUrlResult;","entrypoint":"core","exportName":"GatewayBindUrlResult","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1,"sourcePath":"src/shared/gateway-bind-url.ts"}
{"declaration":"export type GatewayRequestHandlerOptions = GatewayRequestHandlerOptions;","entrypoint":"core","exportName":"GatewayRequestHandlerOptions","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":112,"sourcePath":"src/gateway/server-methods/types.ts"}
{"declaration":"export type MediaUnderstandingProviderPlugin = MediaUnderstandingProvider;","entrypoint":"core","exportName":"MediaUnderstandingProviderPlugin","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":969,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type MediaUnderstandingProviderPlugin = MediaUnderstandingProvider;","entrypoint":"core","exportName":"MediaUnderstandingProviderPlugin","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":951,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawConfig = OpenClawConfig;","entrypoint":"core","exportName":"OpenClawConfig","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":32,"sourcePath":"src/config/types.openclaw.ts"}
{"declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"core","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1390,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginCommandDefinition = OpenClawPluginCommandDefinition;","entrypoint":"core","exportName":"OpenClawPluginCommandDefinition","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1108,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginConfigSchema = OpenClawPluginConfigSchema;","entrypoint":"core","exportName":"OpenClawPluginConfigSchema","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":95,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginDefinition = OpenClawPluginDefinition;","entrypoint":"core","exportName":"OpenClawPluginDefinition","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1372,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginService = OpenClawPluginService;","entrypoint":"core","exportName":"OpenClawPluginService","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1339,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginServiceContext = OpenClawPluginServiceContext;","entrypoint":"core","exportName":"OpenClawPluginServiceContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1331,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginToolContext = OpenClawPluginToolContext;","entrypoint":"core","exportName":"OpenClawPluginToolContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":110,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginToolFactory = OpenClawPluginToolFactory;","entrypoint":"core","exportName":"OpenClawPluginToolFactory","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":131,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type PluginCommandContext = PluginCommandContext;","entrypoint":"core","exportName":"PluginCommandContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":984,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type PluginInteractiveTelegramHandlerContext = PluginInteractiveTelegramHandlerContext;","entrypoint":"core","exportName":"PluginInteractiveTelegramHandlerContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1145,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type PluginLogger = PluginLogger;","entrypoint":"core","exportName":"PluginLogger","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":66,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"core","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1314,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginCommandDefinition = OpenClawPluginCommandDefinition;","entrypoint":"core","exportName":"OpenClawPluginCommandDefinition","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1068,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginConfigSchema = OpenClawPluginConfigSchema;","entrypoint":"core","exportName":"OpenClawPluginConfigSchema","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":88,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginDefinition = OpenClawPluginDefinition;","entrypoint":"core","exportName":"OpenClawPluginDefinition","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1296,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginService = OpenClawPluginService;","entrypoint":"core","exportName":"OpenClawPluginService","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1285,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginServiceContext = OpenClawPluginServiceContext;","entrypoint":"core","exportName":"OpenClawPluginServiceContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1277,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginToolContext = OpenClawPluginToolContext;","entrypoint":"core","exportName":"OpenClawPluginToolContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":103,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginToolFactory = OpenClawPluginToolFactory;","entrypoint":"core","exportName":"OpenClawPluginToolFactory","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":120,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type PluginCommandContext = PluginCommandContext;","entrypoint":"core","exportName":"PluginCommandContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":966,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type PluginInteractiveTelegramHandlerContext = PluginInteractiveTelegramHandlerContext;","entrypoint":"core","exportName":"PluginInteractiveTelegramHandlerContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1097,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type PluginLogger = PluginLogger;","entrypoint":"core","exportName":"PluginLogger","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":59,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type PluginRuntime = PluginRuntime;","entrypoint":"core","exportName":"PluginRuntime","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":54,"sourcePath":"src/plugins/runtime/types.ts"}
{"declaration":"export type ProviderAugmentModelCatalogContext = ProviderAugmentModelCatalogContext;","entrypoint":"core","exportName":"ProviderAugmentModelCatalogContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":582,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthContext = ProviderAuthContext;","entrypoint":"core","exportName":"ProviderAuthContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":166,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthDoctorHintContext = ProviderAuthDoctorHintContext;","entrypoint":"core","exportName":"ProviderAuthDoctorHintContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":457,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthMethod = ProviderAuthMethod;","entrypoint":"core","exportName":"ProviderAuthMethod","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":244,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthMethodNonInteractiveContext = ProviderAuthMethodNonInteractiveContext;","entrypoint":"core","exportName":"ProviderAuthMethodNonInteractiveContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":228,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthResult = ProviderAuthResult;","entrypoint":"core","exportName":"ProviderAuthResult","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":151,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderBuildMissingAuthMessageContext = ProviderBuildMissingAuthMessageContext;","entrypoint":"core","exportName":"ProviderBuildMissingAuthMessageContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":510,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderBuiltInModelSuppressionContext = ProviderBuiltInModelSuppressionContext;","entrypoint":"core","exportName":"ProviderBuiltInModelSuppressionContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":526,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderBuiltInModelSuppressionResult = ProviderBuiltInModelSuppressionResult;","entrypoint":"core","exportName":"ProviderBuiltInModelSuppressionResult","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":535,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderCacheTtlEligibilityContext = ProviderCacheTtlEligibilityContext;","entrypoint":"core","exportName":"ProviderCacheTtlEligibilityContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":498,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderCatalogContext = ProviderCatalogContext;","entrypoint":"core","exportName":"ProviderCatalogContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":265,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderCatalogResult = ProviderCatalogResult;","entrypoint":"core","exportName":"ProviderCatalogResult","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":288,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderDefaultThinkingPolicyContext = ProviderDefaultThinkingPolicyContext;","entrypoint":"core","exportName":"ProviderDefaultThinkingPolicyContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":559,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderDiscoveryContext = ProviderCatalogContext;","entrypoint":"core","exportName":"ProviderDiscoveryContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":598,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderFetchUsageSnapshotContext = ProviderFetchUsageSnapshotContext;","entrypoint":"core","exportName":"ProviderFetchUsageSnapshotContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":438,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderModernModelPolicyContext = ProviderModernModelPolicyContext;","entrypoint":"core","exportName":"ProviderModernModelPolicyContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":569,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderNormalizeResolvedModelContext = ProviderNormalizeResolvedModelContext;","entrypoint":"core","exportName":"ProviderNormalizeResolvedModelContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":349,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderPreparedRuntimeAuth = ProviderPreparedRuntimeAuth;","entrypoint":"core","exportName":"ProviderPreparedRuntimeAuth","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":385,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderPrepareDynamicModelContext = ProviderResolveDynamicModelContext;","entrypoint":"core","exportName":"ProviderPrepareDynamicModelContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":340,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderPrepareExtraParamsContext = ProviderPrepareExtraParamsContext;","entrypoint":"core","exportName":"ProviderPrepareExtraParamsContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":471,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderPrepareRuntimeAuthContext = ProviderPrepareRuntimeAuthContext;","entrypoint":"core","exportName":"ProviderPrepareRuntimeAuthContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":364,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderResolvedUsageAuth = ProviderResolvedUsageAuth;","entrypoint":"core","exportName":"ProviderResolvedUsageAuth","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":425,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderResolveDynamicModelContext = ProviderResolveDynamicModelContext;","entrypoint":"core","exportName":"ProviderResolveDynamicModelContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":323,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderResolveUsageAuthContext = ProviderResolveUsageAuthContext;","entrypoint":"core","exportName":"ProviderResolveUsageAuthContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":406,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderRuntimeModel = ProviderRuntimeModel;","entrypoint":"core","exportName":"ProviderRuntimeModel","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":306,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderThinkingPolicyContext = ProviderThinkingPolicyContext;","entrypoint":"core","exportName":"ProviderThinkingPolicyContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":547,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAugmentModelCatalogContext = ProviderAugmentModelCatalogContext;","entrypoint":"core","exportName":"ProviderAugmentModelCatalogContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":571,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthContext = ProviderAuthContext;","entrypoint":"core","exportName":"ProviderAuthContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":155,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthDoctorHintContext = ProviderAuthDoctorHintContext;","entrypoint":"core","exportName":"ProviderAuthDoctorHintContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":446,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthMethod = ProviderAuthMethod;","entrypoint":"core","exportName":"ProviderAuthMethod","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":233,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthMethodNonInteractiveContext = ProviderAuthMethodNonInteractiveContext;","entrypoint":"core","exportName":"ProviderAuthMethodNonInteractiveContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":217,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthResult = ProviderAuthResult;","entrypoint":"core","exportName":"ProviderAuthResult","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":140,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderBuildMissingAuthMessageContext = ProviderBuildMissingAuthMessageContext;","entrypoint":"core","exportName":"ProviderBuildMissingAuthMessageContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":499,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderBuiltInModelSuppressionContext = ProviderBuiltInModelSuppressionContext;","entrypoint":"core","exportName":"ProviderBuiltInModelSuppressionContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":515,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderBuiltInModelSuppressionResult = ProviderBuiltInModelSuppressionResult;","entrypoint":"core","exportName":"ProviderBuiltInModelSuppressionResult","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":524,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderCacheTtlEligibilityContext = ProviderCacheTtlEligibilityContext;","entrypoint":"core","exportName":"ProviderCacheTtlEligibilityContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":487,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderCatalogContext = ProviderCatalogContext;","entrypoint":"core","exportName":"ProviderCatalogContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":254,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderCatalogResult = ProviderCatalogResult;","entrypoint":"core","exportName":"ProviderCatalogResult","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":277,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderDefaultThinkingPolicyContext = ProviderDefaultThinkingPolicyContext;","entrypoint":"core","exportName":"ProviderDefaultThinkingPolicyContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":548,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderDiscoveryContext = ProviderCatalogContext;","entrypoint":"core","exportName":"ProviderDiscoveryContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":587,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderFetchUsageSnapshotContext = ProviderFetchUsageSnapshotContext;","entrypoint":"core","exportName":"ProviderFetchUsageSnapshotContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":427,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderModernModelPolicyContext = ProviderModernModelPolicyContext;","entrypoint":"core","exportName":"ProviderModernModelPolicyContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":558,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderNormalizeResolvedModelContext = ProviderNormalizeResolvedModelContext;","entrypoint":"core","exportName":"ProviderNormalizeResolvedModelContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":338,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderPreparedRuntimeAuth = ProviderPreparedRuntimeAuth;","entrypoint":"core","exportName":"ProviderPreparedRuntimeAuth","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":374,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderPrepareDynamicModelContext = ProviderResolveDynamicModelContext;","entrypoint":"core","exportName":"ProviderPrepareDynamicModelContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":329,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderPrepareExtraParamsContext = ProviderPrepareExtraParamsContext;","entrypoint":"core","exportName":"ProviderPrepareExtraParamsContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":460,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderPrepareRuntimeAuthContext = ProviderPrepareRuntimeAuthContext;","entrypoint":"core","exportName":"ProviderPrepareRuntimeAuthContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":353,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderResolvedUsageAuth = ProviderResolvedUsageAuth;","entrypoint":"core","exportName":"ProviderResolvedUsageAuth","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":414,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderResolveDynamicModelContext = ProviderResolveDynamicModelContext;","entrypoint":"core","exportName":"ProviderResolveDynamicModelContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":312,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderResolveUsageAuthContext = ProviderResolveUsageAuthContext;","entrypoint":"core","exportName":"ProviderResolveUsageAuthContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":395,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderRuntimeModel = ProviderRuntimeModel;","entrypoint":"core","exportName":"ProviderRuntimeModel","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":295,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderThinkingPolicyContext = ProviderThinkingPolicyContext;","entrypoint":"core","exportName":"ProviderThinkingPolicyContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":536,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderUsageSnapshot = ProviderUsageSnapshot;","entrypoint":"core","exportName":"ProviderUsageSnapshot","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":7,"sourcePath":"src/infra/provider-usage.types.ts"}
{"declaration":"export type ProviderWrapStreamFnContext = ProviderWrapStreamFnContext;","entrypoint":"core","exportName":"ProviderWrapStreamFnContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":488,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderWrapStreamFnContext = ProviderWrapStreamFnContext;","entrypoint":"core","exportName":"ProviderWrapStreamFnContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":477,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type RoutePeer = RoutePeer;","entrypoint":"core","exportName":"RoutePeer","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":21,"sourcePath":"src/routing/resolve-route.ts"}
{"declaration":"export type RoutePeerKind = ChatType;","entrypoint":"core","exportName":"RoutePeerKind","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":19,"sourcePath":"src/routing/resolve-route.ts"}
{"declaration":"export type SecretFileReadOptions = SecretFileReadOptions;","entrypoint":"core","exportName":"SecretFileReadOptions","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":7,"sourcePath":"src/infra/secret-file.ts"}
{"declaration":"export type SecretFileReadResult = SecretFileReadResult;","entrypoint":"core","exportName":"SecretFileReadResult","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":12,"sourcePath":"src/infra/secret-file.ts"}
{"declaration":"export type SpeechProviderPlugin = SpeechProviderPlugin;","entrypoint":"core","exportName":"SpeechProviderPlugin","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":944,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type SpeechProviderPlugin = SpeechProviderPlugin;","entrypoint":"core","exportName":"SpeechProviderPlugin","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":933,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type TailscaleStatusCommandResult = TailscaleStatusCommandResult;","entrypoint":"core","exportName":"TailscaleStatusCommandResult","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1,"sourcePath":"src/shared/tailscale-status.ts"}
{"declaration":"export type TailscaleStatusCommandRunner = TailscaleStatusCommandRunner;","entrypoint":"core","exportName":"TailscaleStatusCommandRunner","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":6,"sourcePath":"src/shared/tailscale-status.ts"}
{"declaration":"export type UsageProviderId = UsageProviderId;","entrypoint":"core","exportName":"UsageProviderId","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":20,"sourcePath":"src/infra/provider-usage.types.ts"}
{"declaration":"export type UsageWindow = UsageWindow;","entrypoint":"core","exportName":"UsageWindow","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1,"sourcePath":"src/infra/provider-usage.types.ts"}
{"declaration":"export class KeyedAsyncQueue","entrypoint":"core","exportName":"KeyedAsyncQueue","importSpecifier":"openclaw/plugin-sdk/core","kind":"class","recordType":"export","sourceLine":34,"sourcePath":"src/plugin-sdk/keyed-async-queue.ts"}
{"category":"core","entrypoint":"plugin-entry","importSpecifier":"openclaw/plugin-sdk/plugin-entry","recordType":"module","sourceLine":1,"sourcePath":"src/plugin-sdk/plugin-entry.ts"}
{"declaration":"export function definePluginEntry({ id, name, description, kind, configSchema, register, }: DefinePluginEntryOptions): DefinedPluginEntry;","entrypoint":"plugin-entry","exportName":"definePluginEntry","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"function","recordType":"export","sourceLine":90,"sourcePath":"src/plugin-sdk/plugin-entry.ts"}
{"declaration":"export function definePluginEntry({ id, name, description, kind, configSchema, register, }: DefinePluginEntryOptions): DefinedPluginEntry;","entrypoint":"plugin-entry","exportName":"definePluginEntry","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"function","recordType":"export","sourceLine":88,"sourcePath":"src/plugin-sdk/plugin-entry.ts"}
{"declaration":"export function emptyPluginConfigSchema(): OpenClawPluginConfigSchema;","entrypoint":"plugin-entry","exportName":"emptyPluginConfigSchema","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"function","recordType":"export","sourceLine":13,"sourcePath":"src/plugins/config-schema.ts"}
{"declaration":"export type AnyAgentTool = AnyAgentTool;","entrypoint":"plugin-entry","exportName":"AnyAgentTool","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":9,"sourcePath":"src/agents/tools/common.ts"}
{"declaration":"export type MediaUnderstandingProviderPlugin = MediaUnderstandingProvider;","entrypoint":"plugin-entry","exportName":"MediaUnderstandingProviderPlugin","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":969,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type MediaUnderstandingProviderPlugin = MediaUnderstandingProvider;","entrypoint":"plugin-entry","exportName":"MediaUnderstandingProviderPlugin","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":951,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawConfig = OpenClawConfig;","entrypoint":"plugin-entry","exportName":"OpenClawConfig","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":32,"sourcePath":"src/config/types.openclaw.ts"}
{"declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"plugin-entry","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":1390,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginCommandDefinition = OpenClawPluginCommandDefinition;","entrypoint":"plugin-entry","exportName":"OpenClawPluginCommandDefinition","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":1108,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginConfigSchema = OpenClawPluginConfigSchema;","entrypoint":"plugin-entry","exportName":"OpenClawPluginConfigSchema","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":95,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginDefinition = OpenClawPluginDefinition;","entrypoint":"plugin-entry","exportName":"OpenClawPluginDefinition","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":1372,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginService = OpenClawPluginService;","entrypoint":"plugin-entry","exportName":"OpenClawPluginService","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":1339,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginServiceContext = OpenClawPluginServiceContext;","entrypoint":"plugin-entry","exportName":"OpenClawPluginServiceContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":1331,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginToolContext = OpenClawPluginToolContext;","entrypoint":"plugin-entry","exportName":"OpenClawPluginToolContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":110,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginToolFactory = OpenClawPluginToolFactory;","entrypoint":"plugin-entry","exportName":"OpenClawPluginToolFactory","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":131,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type PluginCommandContext = PluginCommandContext;","entrypoint":"plugin-entry","exportName":"PluginCommandContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":984,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type PluginInteractiveTelegramHandlerContext = PluginInteractiveTelegramHandlerContext;","entrypoint":"plugin-entry","exportName":"PluginInteractiveTelegramHandlerContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":1145,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type PluginLogger = PluginLogger;","entrypoint":"plugin-entry","exportName":"PluginLogger","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":66,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAugmentModelCatalogContext = ProviderAugmentModelCatalogContext;","entrypoint":"plugin-entry","exportName":"ProviderAugmentModelCatalogContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":582,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthContext = ProviderAuthContext;","entrypoint":"plugin-entry","exportName":"ProviderAuthContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":166,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthDoctorHintContext = ProviderAuthDoctorHintContext;","entrypoint":"plugin-entry","exportName":"ProviderAuthDoctorHintContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":457,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthMethod = ProviderAuthMethod;","entrypoint":"plugin-entry","exportName":"ProviderAuthMethod","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":244,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthMethodNonInteractiveContext = ProviderAuthMethodNonInteractiveContext;","entrypoint":"plugin-entry","exportName":"ProviderAuthMethodNonInteractiveContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":228,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthResult = ProviderAuthResult;","entrypoint":"plugin-entry","exportName":"ProviderAuthResult","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":151,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderBuildMissingAuthMessageContext = ProviderBuildMissingAuthMessageContext;","entrypoint":"plugin-entry","exportName":"ProviderBuildMissingAuthMessageContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":510,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderBuiltInModelSuppressionContext = ProviderBuiltInModelSuppressionContext;","entrypoint":"plugin-entry","exportName":"ProviderBuiltInModelSuppressionContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":526,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderBuiltInModelSuppressionResult = ProviderBuiltInModelSuppressionResult;","entrypoint":"plugin-entry","exportName":"ProviderBuiltInModelSuppressionResult","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":535,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderCacheTtlEligibilityContext = ProviderCacheTtlEligibilityContext;","entrypoint":"plugin-entry","exportName":"ProviderCacheTtlEligibilityContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":498,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderCatalogContext = ProviderCatalogContext;","entrypoint":"plugin-entry","exportName":"ProviderCatalogContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":265,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderCatalogResult = ProviderCatalogResult;","entrypoint":"plugin-entry","exportName":"ProviderCatalogResult","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":288,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderDefaultThinkingPolicyContext = ProviderDefaultThinkingPolicyContext;","entrypoint":"plugin-entry","exportName":"ProviderDefaultThinkingPolicyContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":559,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderDiscoveryContext = ProviderCatalogContext;","entrypoint":"plugin-entry","exportName":"ProviderDiscoveryContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":598,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderFetchUsageSnapshotContext = ProviderFetchUsageSnapshotContext;","entrypoint":"plugin-entry","exportName":"ProviderFetchUsageSnapshotContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":438,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderModernModelPolicyContext = ProviderModernModelPolicyContext;","entrypoint":"plugin-entry","exportName":"ProviderModernModelPolicyContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":569,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderNormalizeResolvedModelContext = ProviderNormalizeResolvedModelContext;","entrypoint":"plugin-entry","exportName":"ProviderNormalizeResolvedModelContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":349,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderPreparedRuntimeAuth = ProviderPreparedRuntimeAuth;","entrypoint":"plugin-entry","exportName":"ProviderPreparedRuntimeAuth","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":385,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderPrepareDynamicModelContext = ProviderResolveDynamicModelContext;","entrypoint":"plugin-entry","exportName":"ProviderPrepareDynamicModelContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":340,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderPrepareExtraParamsContext = ProviderPrepareExtraParamsContext;","entrypoint":"plugin-entry","exportName":"ProviderPrepareExtraParamsContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":471,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderPrepareRuntimeAuthContext = ProviderPrepareRuntimeAuthContext;","entrypoint":"plugin-entry","exportName":"ProviderPrepareRuntimeAuthContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":364,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderResolvedUsageAuth = ProviderResolvedUsageAuth;","entrypoint":"plugin-entry","exportName":"ProviderResolvedUsageAuth","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":425,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderResolveDynamicModelContext = ProviderResolveDynamicModelContext;","entrypoint":"plugin-entry","exportName":"ProviderResolveDynamicModelContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":323,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderResolveUsageAuthContext = ProviderResolveUsageAuthContext;","entrypoint":"plugin-entry","exportName":"ProviderResolveUsageAuthContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":406,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderRuntimeModel = ProviderRuntimeModel;","entrypoint":"plugin-entry","exportName":"ProviderRuntimeModel","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":306,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderThinkingPolicyContext = ProviderThinkingPolicyContext;","entrypoint":"plugin-entry","exportName":"ProviderThinkingPolicyContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":547,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderWrapStreamFnContext = ProviderWrapStreamFnContext;","entrypoint":"plugin-entry","exportName":"ProviderWrapStreamFnContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":488,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type SpeechProviderPlugin = SpeechProviderPlugin;","entrypoint":"plugin-entry","exportName":"SpeechProviderPlugin","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":944,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"plugin-entry","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":1314,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginCommandDefinition = OpenClawPluginCommandDefinition;","entrypoint":"plugin-entry","exportName":"OpenClawPluginCommandDefinition","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":1068,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginConfigSchema = OpenClawPluginConfigSchema;","entrypoint":"plugin-entry","exportName":"OpenClawPluginConfigSchema","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":88,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginDefinition = OpenClawPluginDefinition;","entrypoint":"plugin-entry","exportName":"OpenClawPluginDefinition","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":1296,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginService = OpenClawPluginService;","entrypoint":"plugin-entry","exportName":"OpenClawPluginService","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":1285,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginServiceContext = OpenClawPluginServiceContext;","entrypoint":"plugin-entry","exportName":"OpenClawPluginServiceContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":1277,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type PluginCommandContext = PluginCommandContext;","entrypoint":"plugin-entry","exportName":"PluginCommandContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":966,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type PluginInteractiveTelegramHandlerContext = PluginInteractiveTelegramHandlerContext;","entrypoint":"plugin-entry","exportName":"PluginInteractiveTelegramHandlerContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":1097,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type PluginLogger = PluginLogger;","entrypoint":"plugin-entry","exportName":"PluginLogger","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":59,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAugmentModelCatalogContext = ProviderAugmentModelCatalogContext;","entrypoint":"plugin-entry","exportName":"ProviderAugmentModelCatalogContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":571,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthContext = ProviderAuthContext;","entrypoint":"plugin-entry","exportName":"ProviderAuthContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":155,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthDoctorHintContext = ProviderAuthDoctorHintContext;","entrypoint":"plugin-entry","exportName":"ProviderAuthDoctorHintContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":446,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthMethod = ProviderAuthMethod;","entrypoint":"plugin-entry","exportName":"ProviderAuthMethod","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":233,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthMethodNonInteractiveContext = ProviderAuthMethodNonInteractiveContext;","entrypoint":"plugin-entry","exportName":"ProviderAuthMethodNonInteractiveContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":217,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthResult = ProviderAuthResult;","entrypoint":"plugin-entry","exportName":"ProviderAuthResult","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":140,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderBuildMissingAuthMessageContext = ProviderBuildMissingAuthMessageContext;","entrypoint":"plugin-entry","exportName":"ProviderBuildMissingAuthMessageContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":499,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderBuiltInModelSuppressionContext = ProviderBuiltInModelSuppressionContext;","entrypoint":"plugin-entry","exportName":"ProviderBuiltInModelSuppressionContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":515,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderBuiltInModelSuppressionResult = ProviderBuiltInModelSuppressionResult;","entrypoint":"plugin-entry","exportName":"ProviderBuiltInModelSuppressionResult","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":524,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderCacheTtlEligibilityContext = ProviderCacheTtlEligibilityContext;","entrypoint":"plugin-entry","exportName":"ProviderCacheTtlEligibilityContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":487,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderCatalogContext = ProviderCatalogContext;","entrypoint":"plugin-entry","exportName":"ProviderCatalogContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":254,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderCatalogResult = ProviderCatalogResult;","entrypoint":"plugin-entry","exportName":"ProviderCatalogResult","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":277,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderDefaultThinkingPolicyContext = ProviderDefaultThinkingPolicyContext;","entrypoint":"plugin-entry","exportName":"ProviderDefaultThinkingPolicyContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":548,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderDiscoveryContext = ProviderCatalogContext;","entrypoint":"plugin-entry","exportName":"ProviderDiscoveryContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":587,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderFetchUsageSnapshotContext = ProviderFetchUsageSnapshotContext;","entrypoint":"plugin-entry","exportName":"ProviderFetchUsageSnapshotContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":427,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderModernModelPolicyContext = ProviderModernModelPolicyContext;","entrypoint":"plugin-entry","exportName":"ProviderModernModelPolicyContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":558,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderNormalizeResolvedModelContext = ProviderNormalizeResolvedModelContext;","entrypoint":"plugin-entry","exportName":"ProviderNormalizeResolvedModelContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":338,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderPreparedRuntimeAuth = ProviderPreparedRuntimeAuth;","entrypoint":"plugin-entry","exportName":"ProviderPreparedRuntimeAuth","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":374,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderPrepareDynamicModelContext = ProviderResolveDynamicModelContext;","entrypoint":"plugin-entry","exportName":"ProviderPrepareDynamicModelContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":329,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderPrepareExtraParamsContext = ProviderPrepareExtraParamsContext;","entrypoint":"plugin-entry","exportName":"ProviderPrepareExtraParamsContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":460,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderPrepareRuntimeAuthContext = ProviderPrepareRuntimeAuthContext;","entrypoint":"plugin-entry","exportName":"ProviderPrepareRuntimeAuthContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":353,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderResolvedUsageAuth = ProviderResolvedUsageAuth;","entrypoint":"plugin-entry","exportName":"ProviderResolvedUsageAuth","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":414,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderResolveDynamicModelContext = ProviderResolveDynamicModelContext;","entrypoint":"plugin-entry","exportName":"ProviderResolveDynamicModelContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":312,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderResolveUsageAuthContext = ProviderResolveUsageAuthContext;","entrypoint":"plugin-entry","exportName":"ProviderResolveUsageAuthContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":395,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderRuntimeModel = ProviderRuntimeModel;","entrypoint":"plugin-entry","exportName":"ProviderRuntimeModel","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":295,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderThinkingPolicyContext = ProviderThinkingPolicyContext;","entrypoint":"plugin-entry","exportName":"ProviderThinkingPolicyContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":536,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderWrapStreamFnContext = ProviderWrapStreamFnContext;","entrypoint":"plugin-entry","exportName":"ProviderWrapStreamFnContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":477,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type SpeechProviderPlugin = SpeechProviderPlugin;","entrypoint":"plugin-entry","exportName":"SpeechProviderPlugin","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":933,"sourcePath":"src/plugins/types.ts"}
{"category":"provider","entrypoint":"provider-onboard","importSpecifier":"openclaw/plugin-sdk/provider-onboard","recordType":"module","sourceLine":1,"sourcePath":"src/plugin-sdk/provider-onboard.ts"}
{"declaration":"export function applyAgentDefaultModelPrimary(cfg: OpenClawConfig, primary: string): OpenClawConfig;","entrypoint":"provider-onboard","exportName":"applyAgentDefaultModelPrimary","importSpecifier":"openclaw/plugin-sdk/provider-onboard","kind":"function","recordType":"export","sourceLine":76,"sourcePath":"src/plugins/provider-onboarding-config.ts"}
{"declaration":"export function applyCloudflareAiGatewayConfig(cfg: OpenClawConfig, params?: { accountId?: string | undefined; gatewayId?: string | undefined; } | undefined): OpenClawConfig;","entrypoint":"provider-onboard","exportName":"applyCloudflareAiGatewayConfig","importSpecifier":"openclaw/plugin-sdk/provider-onboard","kind":"function","recordType":"export","sourceLine":85,"sourcePath":"extensions/cloudflare-ai-gateway/onboard.ts"}
@@ -563,7 +560,6 @@
{"declaration":"export function registerWebhookTarget<T extends { path: string; }>(targetsByPath: Map<string, T[]>, target: T, opts?: RegisterWebhookTargetOptions<T> | undefined): RegisteredWebhookTarget<T>;","entrypoint":"webhook-ingress","exportName":"registerWebhookTarget","importSpecifier":"openclaw/plugin-sdk/webhook-ingress","kind":"function","recordType":"export","sourceLine":61,"sourcePath":"src/plugin-sdk/webhook-targets.ts"}
{"declaration":"export function registerWebhookTargetWithPluginRoute<T extends { path: string; }>(params: { targetsByPath: Map<string, T[]>; target: T; route: RegisterWebhookPluginRouteOptions; onLastPathTargetRemoved?: ((params: { ...; }) => void) | undefined; }): RegisteredWebhookTarget<...>;","entrypoint":"webhook-ingress","exportName":"registerWebhookTargetWithPluginRoute","importSpecifier":"openclaw/plugin-sdk/webhook-ingress","kind":"function","recordType":"export","sourceLine":30,"sourcePath":"src/plugin-sdk/webhook-targets.ts"}
{"declaration":"export function requestBodyErrorToText(code: RequestBodyLimitErrorCode): string;","entrypoint":"webhook-ingress","exportName":"requestBodyErrorToText","importSpecifier":"openclaw/plugin-sdk/webhook-ingress","kind":"function","recordType":"export","sourceLine":60,"sourcePath":"src/infra/http-body.ts"}
{"declaration":"export function resolveRequestClientIp(req?: IncomingMessage | undefined, trustedProxies?: string[] | undefined, allowRealIpFallback?: boolean): string | undefined;","entrypoint":"webhook-ingress","exportName":"resolveRequestClientIp","importSpecifier":"openclaw/plugin-sdk/webhook-ingress","kind":"function","recordType":"export","sourceLine":186,"sourcePath":"src/gateway/net.ts"}
{"declaration":"export function resolveSingleWebhookTarget<T>(targets: readonly T[], isMatch: (target: T) => boolean): WebhookTargetMatchResult<T>;","entrypoint":"webhook-ingress","exportName":"resolveSingleWebhookTarget","importSpecifier":"openclaw/plugin-sdk/webhook-ingress","kind":"function","recordType":"export","sourceLine":193,"sourcePath":"src/plugin-sdk/webhook-targets.ts"}
{"declaration":"export function resolveSingleWebhookTargetAsync<T>(targets: readonly T[], isMatch: (target: T) => Promise<boolean>): Promise<WebhookTargetMatchResult<T>>;","entrypoint":"webhook-ingress","exportName":"resolveSingleWebhookTargetAsync","importSpecifier":"openclaw/plugin-sdk/webhook-ingress","kind":"function","recordType":"export","sourceLine":212,"sourcePath":"src/plugin-sdk/webhook-targets.ts"}
{"declaration":"export function resolveWebhookPath(params: { webhookPath?: string | undefined; webhookUrl?: string | undefined; defaultPath?: string | null | undefined; }): string | null;","entrypoint":"webhook-ingress","exportName":"resolveWebhookPath","importSpecifier":"openclaw/plugin-sdk/webhook-ingress","kind":"function","recordType":"export","sourceLine":15,"sourcePath":"src/plugin-sdk/webhook-path.ts"}

View File

@@ -162,25 +162,6 @@ Groups:
- `channels.bluebubbles.groupPolicy = open | allowlist | disabled` (default: `allowlist`).
- `channels.bluebubbles.groupAllowFrom` controls who can trigger in groups when `allowlist` is set.
### Contact name enrichment (macOS, optional)
BlueBubbles group webhooks often only include raw participant addresses. If you want `GroupMembers` context to show local contact names instead, you can opt in to local Contacts enrichment on macOS:
- `channels.bluebubbles.enrichGroupParticipantsFromContacts = true` enables the lookup. Default: `false`.
- Lookups run only after group access, command authorization, and mention gating have allowed the message through.
- Only unnamed phone participants are enriched.
- Raw phone numbers remain as the fallback when no local match is found.
```json5
{
channels: {
bluebubbles: {
enrichGroupParticipantsFromContacts: true,
},
},
}
```
### Mention gating (groups)
BlueBubbles supports mention gating for group chats, matching iMessage/WhatsApp behavior:
@@ -319,7 +300,6 @@ Provider options:
- `channels.bluebubbles.allowFrom`: DM allowlist (handles, emails, E.164 numbers, `chat_id:*`, `chat_guid:*`).
- `channels.bluebubbles.groupPolicy`: `open | allowlist | disabled` (default: `allowlist`).
- `channels.bluebubbles.groupAllowFrom`: Group sender allowlist.
- `channels.bluebubbles.enrichGroupParticipantsFromContacts`: On macOS, optionally enrich unnamed group participants from local Contacts after gating passes. Default: `false`.
- `channels.bluebubbles.groups`: Per-group config (`requireMention`, etc.).
- `channels.bluebubbles.sendReadReceipts`: Send read receipts (default: `true`).
- `channels.bluebubbles.blockStreaming`: Enable block streaming (default: `false`; required for streaming replies).

View File

@@ -366,10 +366,6 @@ Group inbound payloads set:
- `WasMentioned` (mention gating result)
- Telegram forum topics also include `MessageThreadId` and `IsForum`.
Channel specific notes:
- BlueBubbles can optionally enrich unnamed macOS group participants from the local Contacts database before populating `GroupMembers`. This is off by default and only runs after normal group gating passes.
The agent system prompt includes a group intro on the first turn of a new group session. It reminds the model to respond like a human, avoid Markdown tables, and avoid typing literal `\n` sequences.
## iMessage specifics

View File

@@ -344,8 +344,6 @@ Available action groups in current Slack tooling:
| memberInfo | enabled |
| emojiList | enabled |
Current Slack message actions include `send`, `upload-file`, `download-file`, `read`, `edit`, `delete`, `pin`, `unpin`, `list-pins`, `member-info`, and `emoji-list`.
## Events and operational behavior
- Message edits/deletes/thread broadcasts are mapped into system events.

View File

@@ -55,8 +55,7 @@ Notes:
- `--reset`: reset dev config + credentials + sessions + workspace (requires `--dev`).
- `--force`: kill any existing listener on the selected port before starting.
- `--verbose`: verbose logs.
- `--cli-backend-logs`: only show CLI backend logs in the console (and enable stdout/stderr).
- `--claude-cli-logs`: deprecated alias for `--cli-backend-logs`.
- `--claude-cli-logs`: only show claude-cli logs in the console (and enable its stdout/stderr).
- `--ws-log <auto|full|compact>`: websocket log style (default `auto`).
- `--compact`: alias for `--ws-log compact`.
- `--raw-stream`: log raw model stream events to jsonl.

View File

@@ -780,8 +780,7 @@ Options:
- `--reset` (reset dev config + credentials + sessions + workspace)
- `--force` (kill existing listener on port)
- `--verbose`
- `--cli-backend-logs`
- `--claude-cli-logs` (deprecated alias)
- `--claude-cli-logs`
- `--ws-log <auto|full|compact>`
- `--compact` (alias for `--ws-log compact`)
- `--raw-stream`
@@ -872,13 +871,6 @@ Policy note: this is technical compatibility. Anthropic has blocked some
subscription usage outside Claude Code in the past; verify current Anthropic
terms before relying on setup-token in production.
Anthropic Claude CLI migration:
```bash
openclaw models auth login --provider anthropic --method cli --set-default
openclaw onboard --auth-choice anthropic-cli
```
### `models` (root)
`openclaw models` is an alias for `models status`.

View File

@@ -38,7 +38,7 @@ Notes:
- `models set <model-or-alias>` accepts `provider/model` or an alias.
- Model refs are parsed by splitting on the **first** `/`. If the model ID includes `/` (OpenRouter-style), include the provider prefix (example: `openrouter/moonshotai/kimi-k2`).
- If you omit the provider, OpenClaw treats the input as an alias or a model for the **default provider** (only works when there is no `/` in the model ID).
- `models status` may show `marker(<value>)` in auth output for non-secret placeholders (for example `OPENAI_API_KEY`, `secretref-managed`, `minimax-oauth`, `oauth:chutes`, `ollama-local`) instead of masking them as secrets.
- `models status` may show `marker(<value>)` in auth output for non-secret placeholders (for example `OPENAI_API_KEY`, `secretref-managed`, `minimax-oauth`, `qwen-oauth`, `ollama-local`) instead of masking them as secrets.
### `models status`
@@ -74,17 +74,8 @@ openclaw models auth paste-token
`models auth login` runs a provider plugins auth flow (OAuth/API key). Use
`openclaw plugins list` to see which providers are installed.
Examples:
```bash
openclaw models auth login --provider anthropic --method cli --set-default
openclaw models auth login --provider openai-codex --set-default
```
Notes:
- `login --provider anthropic --method cli --set-default` reuses a local Claude
CLI login and rewrites the main Anthropic default-model path to `claude-cli/...`.
- `setup-token` prompts for a setup-token value (generate it with `claude setup-token` on any machine).
- `paste-token` accepts a token string generated elsewhere or from automation.
- Anthropic policy note: setup-token support is technical compatibility. Anthropic has blocked some subscription usage outside Claude Code in the past, so verify current terms before using it broadly.

View File

@@ -56,9 +56,6 @@ When a session is **close to auto-compaction**, OpenClaw triggers a **silent,
agentic turn** that reminds the model to write durable memory **before** the
context is compacted. The default prompts explicitly say the model _may reply_,
but usually `NO_REPLY` is the correct response so the user never sees this turn.
The active memory plugin owns the prompt/path policy for that flush; the
default `memory-core` plugin writes to the canonical daily file under
`memory/YYYY-MM-DD.md`.
This is controlled by `agents.defaults.compaction.memoryFlush`:

View File

@@ -108,6 +108,7 @@ Current bundled examples:
- `byteplus`, `cloudflare-ai-gateway`, `huggingface`, `kimi-coding`,
`modelstudio`, `nvidia`, `qianfan`, `synthetic`, `together`, `venice`,
`vercel-ai-gateway`, and `volcengine`: plugin-owned catalogs only
- `qwen-portal`: plugin-owned catalog, OAuth login, and OAuth refresh
- `minimax` and `xiaomi`: plugin-owned catalogs plus usage auth/snapshot logic
The bundled `openai` plugin now owns both provider ids: `openai` and
@@ -347,6 +348,22 @@ Kimi Coding uses Moonshot AI's Anthropic-compatible endpoint:
}
```
### Qwen OAuth (free tier)
Qwen provides OAuth access to Qwen Coder + Vision via a device-code flow.
The bundled provider plugin is enabled by default, so just log in:
```bash
openclaw models auth login --provider qwen-portal --set-default
```
Model refs:
- `qwen-portal/coder-model`
- `qwen-portal/vision-model`
See [/providers/qwen](/providers/qwen) for setup details and notes.
### Volcano Engine (Doubao)
Volcano Engine (火山引擎) provides access to Doubao and other models in China.

View File

@@ -10,7 +10,7 @@ title: "OAuth"
# OAuth
OpenClaw supports “subscription auth” via OAuth for providers that offer it (notably **OpenAI Codex (ChatGPT OAuth)**). For Anthropic subscriptions, you can either use the **setup-token** flow or reuse a local **Claude CLI** login on the gateway host. Anthropic subscription use outside Claude Code has been restricted for some users in the past, so treat it as a user-choice risk and verify current Anthropic policy yourself. OpenAI Codex OAuth is explicitly supported for use in external tools like OpenClaw. This page explains:
OpenClaw supports “subscription auth” via OAuth for providers that offer it (notably **OpenAI Codex (ChatGPT OAuth)**). For Anthropic subscriptions, use the **setup-token** flow. Anthropic subscription use outside Claude Code has been restricted for some users in the past, so treat it as a user-choice risk and verify current Anthropic policy yourself. OpenAI Codex OAuth is explicitly supported for use in external tools like OpenClaw. This page explains:
For Anthropic in production, API key auth is the safer recommended path over subscription setup-token auth.
@@ -80,48 +80,19 @@ Verify:
openclaw models status
```
## Anthropic Claude CLI migration
If Claude CLI is already installed and signed in on the gateway host, you can
switch Anthropic model selection over to the local CLI backend:
```bash
openclaw models auth login --provider anthropic --method cli --set-default
```
Onboarding shortcut:
```bash
openclaw onboard --auth-choice anthropic-cli
```
This keeps existing Anthropic auth profiles for rollback, but rewrites the main
default-model path from `anthropic/...` to `claude-cli/...`.
## OAuth exchange (how login works)
OpenClaws interactive login flows are implemented in `@mariozechner/pi-ai` and wired into the wizards/commands.
### Anthropic setup-token / Claude CLI
### Anthropic setup-token
Flow shape:
Setup-token path:
1. run `claude setup-token`
2. paste the token into OpenClaw
3. store as a token auth profile (no refresh)
Claude CLI path:
1. sign in with `claude auth login` on the gateway host
2. run `openclaw models auth login --provider anthropic --method cli --set-default`
3. store no new auth profile; switch model selection to `claude-cli/...`
Wizard paths:
- `openclaw onboard` → auth choice `anthropic-cli`
- `openclaw onboard` → auth choice `setup-token` (Anthropic)
The wizard path is `openclaw onboard` → auth choice `setup-token` (Anthropic).
### OpenAI Codex (ChatGPT OAuth)

View File

@@ -52,10 +52,6 @@
]
},
"redirects": [
{
"source": "/providers/modelstudio",
"destination": "/providers/qwen_modelstudio"
},
{
"source": "/platforms/oracle",
"destination": "/install/oracle"
@@ -1198,6 +1194,7 @@
"providers/litellm",
"providers/minimax",
"providers/mistral",
"providers/modelstudio",
"providers/moonshot",
"providers/nvidia",
"providers/ollama",
@@ -1207,7 +1204,6 @@
"providers/openrouter",
"providers/perplexity-provider",
"providers/qianfan",
"providers/qwen_modelstudio",
"providers/qwen",
"providers/sglang",
"providers/synthetic",

View File

@@ -113,26 +113,6 @@ Optional ops scripts (systemd/Termux) are documented here:
> `claude setup-token` requires an interactive TTY.
## Anthropic: Claude CLI migration
If Claude CLI is already installed and signed in on the gateway host, you can
switch an existing Anthropic setup over to the CLI backend instead of pasting a
setup-token:
```bash
openclaw models auth login --provider anthropic --method cli --set-default
```
This keeps your existing Anthropic auth profiles for rollback, but changes the
default model selection to `claude-cli/...` and adds matching Claude CLI
allowlist entries under `agents.defaults.models`.
Onboarding shortcut:
```bash
openclaw onboard --auth-choice anthropic-cli
```
## Checking model auth status
```bash

View File

@@ -22,14 +22,13 @@ want “always works” text responses without relying on external APIs.
## Beginner-friendly quick start
You can use Claude Code CLI **without any config** (the bundled Anthropic plugin
registers a default backend):
You can use Claude Code CLI **without any config** (OpenClaw ships a built-in default):
```bash
openclaw agent --message "hi" --model claude-cli/opus-4.6
```
Codex CLI also works out of the box (via the bundled OpenAI plugin):
Codex CLI also works out of the box:
```bash
openclaw agent --message "hi" --model codex-cli/gpt-5.4
@@ -54,11 +53,6 @@ command path:
Thats it. No keys, no extra auth config needed beyond the CLI itself.
If you use a bundled CLI backend as the **primary message provider** on a
gateway host, OpenClaw now auto-loads the owning bundled plugin when your config
explicitly references that backend in a model ref or under
`agents.defaults.cliBackends`.
## Using it as a fallback
Add a CLI backend to your fallback list so it only runs when primary models fail:
@@ -186,9 +180,9 @@ Input modes:
- `input: "stdin"` sends the prompt via stdin.
- If the prompt is very long and `maxPromptArgChars` is set, stdin is used.
## Defaults (plugin-owned)
## Defaults (built-in)
The bundled Anthropic plugin registers a default for `claude-cli`:
OpenClaw ships a default for `claude-cli`:
- `command: "claude"`
- `args: ["-p", "--output-format", "json", "--permission-mode", "bypassPermissions"]`
@@ -199,38 +193,19 @@ The bundled Anthropic plugin registers a default for `claude-cli`:
- `systemPromptWhen: "first"`
- `sessionMode: "always"`
The bundled OpenAI plugin also registers a default for `codex-cli`:
OpenClaw also ships a default for `codex-cli`:
- `command: "codex"`
- `args: ["exec","--json","--color","never","--sandbox","workspace-write","--skip-git-repo-check"]`
- `resumeArgs: ["exec","resume","{sessionId}","--color","never","--sandbox","workspace-write","--skip-git-repo-check"]`
- `args: ["exec","--json","--color","never","--sandbox","read-only","--skip-git-repo-check"]`
- `resumeArgs: ["exec","resume","{sessionId}","--color","never","--sandbox","read-only","--skip-git-repo-check"]`
- `output: "jsonl"`
- `resumeOutput: "text"`
- `modelArg: "--model"`
- `imageArg: "--image"`
- `sessionMode: "existing"`
The bundled Google plugin also registers a default for `google-gemini-cli`:
- `command: "gemini"`
- `args: ["--prompt", "--output-format", "json"]`
- `resumeArgs: ["--resume", "{sessionId}", "--prompt", "--output-format", "json"]`
- `modelArg: "--model"`
- `sessionMode: "existing"`
- `sessionIdFields: ["session_id", "sessionId"]`
Override only if needed (common: absolute `command` path).
## Plugin-owned defaults
CLI backend defaults are now part of the plugin surface:
- Plugins register them with `api.registerCliBackend(...)`.
- The backend `id` becomes the provider prefix in model refs.
- User config in `agents.defaults.cliBackends.<id>` still overrides the plugin default.
- Backend-specific config cleanup stays plugin-owned through the optional
`normalizeConfig` hook.
## Limitations
- **No OpenClaw tools** (the CLI backend never receives tool calls). Some CLIs

View File

@@ -546,8 +546,8 @@ Quick answers plus deeper troubleshooting for real-world setups (local dev, VPS,
</Accordion>
<Accordion title="Can I use Claude Max subscription without an API key?">
Yes. You can either use a **setup-token** or reuse a local **Claude CLI**
login on the gateway host.
Yes. You can authenticate with a **setup-token**
instead of an API key. This is the subscription path.
Claude Pro/Max subscriptions **do not include an API key**, so this is the
technical path for subscription accounts. But this is your decision: Anthropic
@@ -572,12 +572,7 @@ Quick answers plus deeper troubleshooting for real-world setups (local dev, VPS,
</Accordion>
<Accordion title="Do you support Claude subscription auth (Claude Pro or Max)?">
Yes. You can either:
- use a **setup-token**
- reuse a local **Claude CLI** login on the gateway host with `openclaw models auth login --provider anthropic --method cli --set-default`
Setup-token is still supported. Claude CLI migration is simpler when the gateway host already runs Claude Code. See [Anthropic](/providers/anthropic) and [OAuth](/concepts/oauth).
Yes - via **setup-token**. OpenClaw no longer reuses Claude Code CLI OAuth tokens; use a setup-token or an Anthropic API key. Generate the token anywhere and paste it on the gateway host. See [Anthropic](/providers/anthropic) and [OAuth](/concepts/oauth).
Important: this is technical compatibility, not a policy guarantee. Anthropic
has blocked some subscription usage outside Claude Code in the past.

View File

@@ -23,7 +23,6 @@ This doc is a “how we test” guide:
Most days:
- Full gate (expected before push): `pnpm build && pnpm check && pnpm test`
- Faster local full-suite run on a roomy machine: `pnpm test:max`
When you touch tests or want extra confidence:
@@ -55,19 +54,11 @@ Think of the suites as “increasing realism” (and increasing flakiness/cost):
- Should be fast and stable
- Scheduler note:
- `pnpm test` now keeps a small checked-in behavioral manifest for true pool/isolation overrides and a separate timing snapshot for the slowest unit files.
- Extension-only local runs now also use a checked-in extensions timing snapshot plus a slightly coarser shared batch target on high-memory hosts, so the shared extensions lane avoids spawning an extra batch when two measured shared runs are enough.
- High-memory local extension shared batches also run with a slightly higher worker cap than before, which shortened the two remaining shared extension batches without changing the isolated extension lanes.
- High-memory local channel runs now reuse the checked-in channel timing snapshot to split the shared channels lane into a few measured batches instead of one long shared worker.
- High-memory local channel shared batches also run with a slightly lower worker cap than shared unit batches, which helped targeted channel reruns avoid CPU oversubscription once isolated channel lanes are already in flight.
- Targeted local channel reruns now start splitting shared channel work a bit earlier, which keeps medium-sized targeted reruns from leaving one oversized shared channel batch on the critical path.
- Targeted local unit reruns also split medium-sized shared unit selections into measured batches, which helps large focused reruns overlap instead of waiting behind one long shared unit lane.
- High-memory local multi-surface runs also use slightly coarser shared `unit-fast` batches so the mixed planner spends less time spinning up extra shared unit workers before the later surfaces can overlap.
- Shared unit, extension, channel, and gateway runs all stay on Vitest `forks`.
- The wrapper keeps measured fork-isolated exceptions and heavy singleton lanes explicit in `test/fixtures/test-parallel.behavior.json`.
- Shared unit coverage now defaults to `threads`, while the manifest keeps the measured fork-only exceptions and heavy singleton lanes explicit.
- The shared extension lane still defaults to `threads`; the wrapper keeps explicit fork-only exceptions in `test/fixtures/test-parallel.behavior.json` when a file cannot safely share a non-isolated worker.
- The channel suite (`vitest.channels.config.ts`) now also defaults to `threads`; the March 22, 2026 direct full-suite control run passed clean without channel-specific fork exceptions.
- The wrapper peels the heaviest measured files into dedicated lanes instead of relying on a growing hand-maintained exclusion list.
- For surface-only local runs, unit, extension, and channel shared lanes can overlap their isolated hotspots instead of waiting behind one serial prefix.
- For multi-surface local runs, the wrapper keeps the shared surface phases ordered, but batches inside the same shared phase now fan out together, deferred isolated work can overlap the next shared phase, and spare `unit-fast` headroom now starts that deferred work earlier instead of leaving those slots idle.
- Refresh the timing snapshots with `pnpm test:perf:update-timings` and `pnpm test:perf:update-timings:extensions` after major suite shape changes.
- Refresh the timing snapshot with `pnpm test:perf:update-timings` after major suite shape changes.
- Embedded runner note:
- When you change message-tool discovery inputs or compaction runtime context,
keep both levels of coverage.
@@ -81,16 +72,15 @@ Think of the suites as “increasing realism” (and increasing flakiness/cost):
sufficient substitute for those integration paths.
- Pool note:
- Base Vitest config still defaults to `forks`.
- Unit, channel, extension, and gateway wrapper lanes all default to `forks`.
- Unit wrapper lanes default to `threads`, with explicit manifest fork-only exceptions.
- Extension scoped config defaults to `threads`.
- Channel scoped config defaults to `threads`.
- Unit, channel, and extension configs default to `isolate: false` for faster file startup.
- `pnpm test` also passes `--isolate=false` at the wrapper level.
- Opt back into Vitest file isolation with `OPENCLAW_TEST_ISOLATE=1 pnpm test`.
- `OPENCLAW_TEST_NO_ISOLATE=0` or `OPENCLAW_TEST_NO_ISOLATE=false` also force isolated runs.
- Fast-local iteration note:
- `pnpm test:changed` runs the wrapper with `--changed origin/main`.
- `pnpm test:changed:max` keeps the same changed-file filter but uses the wrapper's aggressive local planner profile.
- `pnpm test:max` exposes that same planner profile for a full local run.
- On supported local Node versions, including Node 25, the normal profile can use top-level lane parallelism. `pnpm test:max` still pushes the planner harder when you want a more aggressive local run.
- The base Vitest config marks the wrapper manifests/config files as `forceRerunTriggers` so changed-mode reruns stay correct when scheduler inputs change.
- Vitest's filesystem module cache is now enabled by default for Node-side test reruns.
- Opt out with `OPENCLAW_VITEST_FS_MODULE_CACHE=0` or `OPENCLAW_VITEST_FS_MODULE_CACHE=false` if you suspect stale transform cache behavior.
@@ -305,19 +295,6 @@ OPENCLAW_LIVE_CLI_BACKEND=1 \
pnpm test:live src/gateway/gateway-cli-backend.live.test.ts
```
Docker recipe:
```bash
pnpm test:docker:live-cli-backend
```
Notes:
- The Docker runner lives at `scripts/test-live-cli-backend-docker.sh`.
- It runs the live CLI-backend smoke inside the repo Docker image as the non-root `node` user, because Claude CLI rejects `bypassPermissions` when invoked as root.
- For `claude-cli`, it installs the Linux `@anthropic-ai/claude-code` package into a cached writable prefix at `OPENCLAW_DOCKER_CLI_TOOLS_DIR` (default: `~/.cache/openclaw/docker-cli-tools`).
- It copies `~/.claude` into the container when available, but on machines where Claude auth is backed by `ANTHROPIC_API_KEY`, it also preserves `ANTHROPIC_API_KEY` / `ANTHROPIC_API_KEY_OLD` for the child Claude CLI via `OPENCLAW_LIVE_CLI_BACKEND_PRESERVE_ENV`.
### Recommended live recipes
Narrow, explicit allowlists are fastest and least flaky:
@@ -455,7 +432,6 @@ These Docker runners split into two buckets:
The live-model Docker runners also bind-mount only the needed CLI auth homes (or all supported ones when the run is not narrowed), then copy them into the container home before the run so external-CLI OAuth can refresh tokens without mutating the host auth store:
- Direct models: `pnpm test:docker:live-models` (script: `scripts/test-live-models-docker.sh`)
- CLI backend smoke: `pnpm test:docker:live-cli-backend` (script: `scripts/test-live-cli-backend-docker.sh`)
- Gateway + dev agent: `pnpm test:docker:live-gateway` (script: `scripts/test-live-gateway-models-docker.sh`)
- Open WebUI live smoke: `pnpm test:docker:openwebui` (script: `scripts/e2e/openwebui-docker.sh`)
- Onboarding wizard (TTY, full scaffolding): `pnpm test:docker:onboard` (script: `scripts/e2e/onboard-docker.sh`)
@@ -492,9 +468,8 @@ Useful env vars:
- `OPENCLAW_CONFIG_DIR=...` (default: `~/.openclaw`) mounted to `/home/node/.openclaw`
- `OPENCLAW_WORKSPACE_DIR=...` (default: `~/.openclaw/workspace`) mounted to `/home/node/.openclaw/workspace`
- `OPENCLAW_PROFILE_FILE=...` (default: `~/.profile`) mounted to `/home/node/.profile` and sourced before running tests
- `OPENCLAW_DOCKER_CLI_TOOLS_DIR=...` (default: `~/.cache/openclaw/docker-cli-tools`) mounted to `/home/node/.npm-global` for cached CLI installs inside Docker
- External CLI auth dirs under `$HOME` are mounted read-only under `/host-auth/...`, then copied into `/home/node/...` before tests start
- Default: mount all supported dirs (`.codex`, `.claude`, `.minimax`)
- Default: mount all supported dirs (`.codex`, `.claude`, `.qwen`, `.minimax`)
- Narrowed provider runs mount only the needed dirs inferred from `OPENCLAW_LIVE_PROVIDERS` / `OPENCLAW_LIVE_GATEWAY_PROVIDERS`
- Override manually with `OPENCLAW_DOCKER_AUTH_DIRS=all`, `OPENCLAW_DOCKER_AUTH_DIRS=none`, or a comma list like `OPENCLAW_DOCKER_AUTH_DIRS=.claude,.codex`
- `OPENCLAW_LIVE_GATEWAY_MODELS=...` / `OPENCLAW_LIVE_MODELS=...` to narrow the run

View File

@@ -27,15 +27,14 @@ This page covers the internal architecture of the OpenClaw plugin system.
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` |
| CLI inference backend | `api.registerCliBackend(...)` | `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` |
| 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.

View File

@@ -128,20 +128,19 @@ and provider plugins have dedicated guides linked above.
A single plugin can register any number of capabilities via the `api` object:
| Capability | Registration method | Detailed guide |
| --------------------- | --------------------------------------------- | ------------------------------------------------------------------------------- |
| Text inference (LLM) | `api.registerProvider(...)` | [Provider Plugins](/plugins/sdk-provider-plugins) |
| CLI inference backend | `api.registerCliBackend(...)` | [CLI Backends](/gateway/cli-backends) |
| Channel / messaging | `api.registerChannel(...)` | [Channel Plugins](/plugins/sdk-channel-plugins) |
| Speech (TTS/STT) | `api.registerSpeechProvider(...)` | [Provider Plugins](/plugins/sdk-provider-plugins#step-5-add-extra-capabilities) |
| Media understanding | `api.registerMediaUnderstandingProvider(...)` | [Provider Plugins](/plugins/sdk-provider-plugins#step-5-add-extra-capabilities) |
| Image generation | `api.registerImageGenerationProvider(...)` | [Provider Plugins](/plugins/sdk-provider-plugins#step-5-add-extra-capabilities) |
| Web search | `api.registerWebSearchProvider(...)` | [Provider Plugins](/plugins/sdk-provider-plugins#step-5-add-extra-capabilities) |
| Agent tools | `api.registerTool(...)` | Below |
| Custom commands | `api.registerCommand(...)` | [Entry Points](/plugins/sdk-entrypoints) |
| Event hooks | `api.registerHook(...)` | [Entry Points](/plugins/sdk-entrypoints) |
| HTTP routes | `api.registerHttpRoute(...)` | [Internals](/plugins/architecture#gateway-http-routes) |
| CLI subcommands | `api.registerCli(...)` | [Entry Points](/plugins/sdk-entrypoints) |
| Capability | Registration method | Detailed guide |
| -------------------- | --------------------------------------------- | ------------------------------------------------------------------------------- |
| Text inference (LLM) | `api.registerProvider(...)` | [Provider Plugins](/plugins/sdk-provider-plugins) |
| Channel / messaging | `api.registerChannel(...)` | [Channel Plugins](/plugins/sdk-channel-plugins) |
| Speech (TTS/STT) | `api.registerSpeechProvider(...)` | [Provider Plugins](/plugins/sdk-provider-plugins#step-5-add-extra-capabilities) |
| Media understanding | `api.registerMediaUnderstandingProvider(...)` | [Provider Plugins](/plugins/sdk-provider-plugins#step-5-add-extra-capabilities) |
| Image generation | `api.registerImageGenerationProvider(...)` | [Provider Plugins](/plugins/sdk-provider-plugins#step-5-add-extra-capabilities) |
| Web search | `api.registerWebSearchProvider(...)` | [Provider Plugins](/plugins/sdk-provider-plugins#step-5-add-extra-capabilities) |
| Agent tools | `api.registerTool(...)` | Below |
| Custom commands | `api.registerCommand(...)` | [Entry Points](/plugins/sdk-entrypoints) |
| Event hooks | `api.registerHook(...)` | [Entry Points](/plugins/sdk-entrypoints) |
| HTTP routes | `api.registerHttpRoute(...)` | [Internals](/plugins/architecture#gateway-http-routes) |
| CLI subcommands | `api.registerCli(...)` | [Entry Points](/plugins/sdk-entrypoints) |
For the full registration API, see [SDK Overview](/plugins/sdk-overview#registration-api).
@@ -225,15 +224,6 @@ internal imports — never import your own plugin through its SDK path.
<Check>Tests pass (`pnpm test -- extensions/my-plugin/`)</Check>
<Check>`pnpm check` passes (in-repo plugins)</Check>
## Beta Release Testing
1. Watch for GitHub release tags on [openclaw/openclaw](https://github.com/openclaw/openclaw/releases) and subscribe via `Watch` > `Releases`. Beta tags look like `v2026.3.N-beta.1`. You can also turn on notifications for the official OpenClaw X account [@openclaw](https://x.com/openclaw) for release announcements.
2. Test your plugin against the beta tag as soon as it appears. The window before stable is typically only a few hours.
3. Post in your plugin's thread in the `plugin-forum` Discord channel after testing with either `all good` or what broke. If you do not have a thread yet, create one.
4. If something breaks, open or update an issue titled `Beta blocker: <plugin-name> - <summary>` and apply the `beta-blocker` label. Put the issue link in your thread.
5. Open a PR to `main` titled `fix(<plugin-id>): beta blocker - <summary>` and link the issue in both the PR and your Discord thread. Contributors cannot label PRs, so the title is the PR-side signal for maintainers and automation. Blockers with a PR get merged; blockers without one might ship anyway. Maintainers watch these threads during beta testing.
6. Silence means green. If you miss the window, your fix likely lands in the next cycle.
## Next steps
<CardGroup cols={2}>

View File

@@ -78,7 +78,6 @@ Those belong in your plugin code and `package.json`.
"description": "OpenRouter provider plugin",
"version": "1.0.0",
"providers": ["openrouter"],
"cliBackends": ["openrouter-cli"],
"providerAuthEnvVars": {
"openrouter": ["OPENROUTER_API_KEY"]
},
@@ -126,7 +125,6 @@ Those belong in your plugin code and `package.json`.
| `kind` | No | `"memory"` \| `"context-engine"` | Declares an exclusive plugin kind used by `plugins.slots.*`. |
| `channels` | No | `string[]` | Channel ids owned by this plugin. Used for discovery and config validation. |
| `providers` | No | `string[]` | Provider ids owned by this plugin. |
| `cliBackends` | No | `string[]` | CLI inference backend ids owned by this plugin. Used for startup auto-activation from explicit config refs. |
| `providerAuthEnvVars` | No | `Record<string, string[]>` | Cheap provider-auth env metadata that OpenClaw can inspect without loading plugin code. |
| `providerAuthChoices` | No | `object[]` | Cheap auth-choice metadata for onboarding pickers, preferred-provider resolution, and simple CLI flag wiring. |
| `skills` | No | `string[]` | Skill directories to load, relative to the plugin root. |
@@ -236,8 +234,8 @@ See [Configuration reference](/gateway/configuration) for the full `plugins.*` s
- `kind: "memory"` is selected by `plugins.slots.memory`.
- `kind: "context-engine"` is selected by `plugins.slots.contextEngine`
(default: built-in `legacy`).
- `channels`, `providers`, `cliBackends`, and `skills` can be omitted when a
plugin does not need them.
- `channels`, `providers`, and `skills` can be omitted when a plugin does not
need them.
- If your plugin depends on native modules, document the build steps and any
package-manager allowlist requirements (for example, pnpm `allow-build-scripts`
- `pnpm rebuild <package>`).

View File

@@ -66,7 +66,6 @@ subpaths is in `scripts/lib/plugin-sdk-entrypoints.json`.
<Accordion title="Provider subpaths">
| Subpath | Key exports |
| --- | --- |
| `plugin-sdk/cli-backend` | CLI backend defaults + watchdog constants |
| `plugin-sdk/provider-auth` | `createProviderApiKeyAuthMethod`, `ensureApiKeyFromOptionEnvOrPrompt`, `upsertAuthProfile` |
| `plugin-sdk/provider-models` | `normalizeModelCompat` |
| `plugin-sdk/provider-catalog` | Catalog type re-exports |
@@ -115,7 +114,6 @@ methods:
| Method | What it registers |
| --------------------------------------------- | ------------------------------ |
| `api.registerProvider(...)` | Text inference (LLM) |
| `api.registerCliBackend(...)` | Local CLI inference backend |
| `api.registerChannel(...)` | Messaging channel |
| `api.registerSpeechProvider(...)` | Text-to-speech / STT synthesis |
| `api.registerMediaUnderstandingProvider(...)` | Image/audio/video analysis |
@@ -140,25 +138,12 @@ methods:
| `api.registerService(service)` | Background service |
| `api.registerInteractiveHandler(registration)` | Interactive handler |
### CLI backend registration
`api.registerCliBackend(...)` lets a plugin own the default config for a local
AI CLI backend such as `claude-cli` or `codex-cli`.
- The backend `id` becomes the provider prefix in model refs like `claude-cli/opus`.
- The backend `config` uses the same shape as `agents.defaults.cliBackends.<id>`.
- User config still wins. OpenClaw merges `agents.defaults.cliBackends.<id>` over the
plugin default before running the CLI.
- Use `normalizeConfig` when a backend needs compatibility rewrites after merge
(for example normalizing old flag shapes).
### Exclusive slots
| Method | What it registers |
| ------------------------------------------ | ------------------------------------- |
| `api.registerContextEngine(id, factory)` | Context engine (one active at a time) |
| `api.registerMemoryPromptSection(builder)` | Memory prompt section builder |
| `api.registerMemoryFlushPlan(resolver)` | Memory flush plan resolver |
### Events and lifecycle

View File

@@ -1,9 +1,8 @@
---
summary: "Use Anthropic Claude via API keys, setup-token, or Claude CLI in OpenClaw"
summary: "Use Anthropic Claude via API keys or setup-token in OpenClaw"
read_when:
- You want to use Anthropic models in OpenClaw
- You want setup-token instead of API keys
- You want to reuse Claude CLI subscription auth on the gateway host
title: "Anthropic"
---
@@ -27,7 +26,7 @@ openclaw onboard
openclaw onboard --anthropic-api-key "$ANTHROPIC_API_KEY"
```
### Claude CLI config snippet
### Config snippet
```json5
{
@@ -187,122 +186,7 @@ Note: Anthropic currently rejects `context-1m-*` beta requests when using
OAuth/subscription tokens (`sk-ant-oat-*`). OpenClaw automatically skips the
context1m beta header for OAuth auth and keeps the required OAuth betas.
## Option B: Claude CLI as the message provider
**Best for:** a single-user gateway host that already has Claude CLI installed
and signed in with a Claude subscription.
This path uses the local `claude` binary for model inference instead of calling
the Anthropic API directly. OpenClaw treats it as a **CLI backend provider**
with model refs like:
- `claude-cli/claude-sonnet-4-6`
- `claude-cli/claude-opus-4-6`
How it works:
1. OpenClaw launches `claude -p --output-format json ...` on the **gateway
host**.
2. The first turn sends `--session-id <uuid>`.
3. Follow-up turns reuse the stored Claude session via `--resume <sessionId>`.
4. Your chat messages still go through the normal OpenClaw message pipeline, but
the actual model reply is produced by Claude CLI.
### Requirements
- Claude CLI installed on the gateway host and available on PATH, or configured
with an absolute command path.
- Claude CLI already authenticated on that same host:
```bash
claude auth status
```
- OpenClaw auto-loads the bundled Anthropic plugin at gateway startup when your
config explicitly references `claude-cli/...` or `claude-cli` backend config.
### Config snippet
```json5
{
agents: {
defaults: {
model: {
primary: "claude-cli/claude-sonnet-4-6",
},
models: {
"claude-cli/claude-sonnet-4-6": {},
},
sandbox: { mode: "off" },
},
},
}
```
If the `claude` binary is not on the gateway host PATH:
```json5
{
agents: {
defaults: {
cliBackends: {
"claude-cli": {
command: "/opt/homebrew/bin/claude",
},
},
},
},
}
```
### What you get
- Claude subscription auth reused from the local CLI
- Normal OpenClaw message/session routing
- Claude CLI session continuity across turns
### Migrate from Anthropic auth to Claude CLI
If you currently use `anthropic/...` with a setup-token or API key and want to
switch the same gateway host to Claude CLI:
```bash
openclaw models auth login --provider anthropic --method cli --set-default
```
Or in onboarding:
```bash
openclaw onboard --auth-choice anthropic-cli
```
What this does:
- verifies Claude CLI is already signed in on the gateway host
- switches the default model to `claude-cli/...`
- rewrites Anthropic default-model fallbacks like `anthropic/claude-opus-4-6`
to `claude-cli/claude-opus-4-6`
- adds matching `claude-cli/...` entries to `agents.defaults.models`
What it does **not** do:
- delete your existing Anthropic auth profiles
- remove every old `anthropic/...` config reference outside the main default
model/allowlist path
That makes rollback simple: change the default model back to `anthropic/...` if
you need to.
### Important limits
- This is **not** the Anthropic API provider. It is the local CLI runtime.
- Tools are disabled on the OpenClaw side for CLI backend runs.
- Text in, text out. No OpenClaw streaming handoff.
- Best fit for a personal gateway host, not shared multi-user billing setups.
More details: [/gateway/cli-backends](/gateway/cli-backends)
## Option C: Claude setup-token
## Option B: Claude setup-token
**Best for:** using your Claude subscription.

View File

@@ -39,6 +39,7 @@ Looking for chat channel docs (WhatsApp/Telegram/Discord/Slack/Mattermost (plugi
- [LiteLLM (unified gateway)](/providers/litellm)
- [MiniMax](/providers/minimax)
- [Mistral](/providers/mistral)
- [Model Studio (Alibaba Cloud)](/providers/modelstudio)
- [Moonshot AI (Kimi + Kimi Coding)](/providers/moonshot)
- [NVIDIA](/providers/nvidia)
- [Ollama (cloud + local models)](/providers/ollama)
@@ -48,7 +49,7 @@ Looking for chat channel docs (WhatsApp/Telegram/Discord/Slack/Mattermost (plugi
- [OpenRouter](/providers/openrouter)
- [Perplexity (web search)](/providers/perplexity-provider)
- [Qianfan](/providers/qianfan)
- [Qwen / Model Studio (Alibaba Cloud)](/providers/qwen_modelstudio)
- [Qwen (OAuth)](/providers/qwen)
- [SGLang (local models)](/providers/sglang)
- [Synthetic](/providers/synthetic)
- [Together AI](/providers/together)

View File

@@ -0,0 +1,66 @@
---
title: "Model Studio"
summary: "Alibaba Cloud Model Studio setup (Coding Plan, dual region endpoints)"
read_when:
- You want to use Alibaba Cloud Model Studio with OpenClaw
- You need the API key env var for Model Studio
---
# Model Studio (Alibaba Cloud)
The Model Studio provider gives access to Alibaba Cloud Coding Plan models,
including Qwen and third-party models hosted on the platform.
- Provider: `modelstudio`
- Auth: `MODELSTUDIO_API_KEY`
- API: OpenAI-compatible
## Quick start
1. Set the API key:
```bash
openclaw onboard --auth-choice modelstudio-api-key
```
2. Set a default model:
```json5
{
agents: {
defaults: {
model: { primary: "modelstudio/qwen3.5-plus" },
},
},
}
```
## Region endpoints
Model Studio has two endpoints based on region:
| Region | Endpoint |
| ---------- | ------------------------------------ |
| China (CN) | `coding.dashscope.aliyuncs.com` |
| Global | `coding-intl.dashscope.aliyuncs.com` |
The provider auto-selects based on the auth choice (`modelstudio-api-key` for
global, `modelstudio-api-key-cn` for China). You can override with a custom
`baseUrl` in config.
## Available models
- **qwen3.5-plus** (default) - Qwen 3.5 Plus
- **qwen3-max** - Qwen 3 Max
- **qwen3-coder** series - Qwen coding models
- **GLM-5**, **GLM-4.7** - GLM models via Alibaba
- **Kimi K2.5** - Moonshot AI via Alibaba
- **MiniMax-M2.5** - MiniMax via Alibaba
Most models support image input. Context windows range from 200K to 1M tokens.
## Environment note
If the Gateway runs as a daemon (launchd/systemd), make sure
`MODELSTUDIO_API_KEY` is available to that process (for example, in
`~/.openclaw/.env` or via `env.shellEnv`).

View File

@@ -1,33 +1,53 @@
---
summary: "Use Qwen models via Alibaba Cloud Model Studio"
summary: "Use Qwen OAuth (free tier) in OpenClaw"
read_when:
- You want to use Qwen with OpenClaw
- You previously used Qwen OAuth
- You want free-tier OAuth access to Qwen Coder
title: "Qwen"
---
# Qwen
<Warning>
Qwen provides a free-tier OAuth flow for Qwen Coder and Qwen Vision models
(2,000 requests/day, subject to Qwen rate limits).
**Qwen OAuth has been removed.** The free-tier OAuth integration
(`qwen-portal`) that used `portal.qwen.ai` endpoints is no longer available.
See [Issue #49557](https://github.com/openclaw/openclaw/issues/49557) for
background.
</Warning>
## Recommended: Model Studio (Alibaba Cloud Coding Plan)
Use [Model Studio](/providers/modelstudio) for officially supported access to
Qwen models (Qwen 3.5 Plus, GLM-4.7, Kimi K2.5, MiniMax M2.5, and more).
## Enable the plugin
```bash
# Global endpoint
openclaw onboard --auth-choice modelstudio-api-key
# China endpoint
openclaw onboard --auth-choice modelstudio-api-key-cn
openclaw plugins enable qwen-portal-auth
```
See [Model Studio](/providers/modelstudio) for full setup details.
Restart the Gateway after enabling.
## Authenticate
```bash
openclaw models auth login --provider qwen-portal --set-default
```
This runs the Qwen device-code OAuth flow and writes a provider entry to your
`models.json` (plus a `qwen` alias for quick switching).
## Model IDs
- `qwen-portal/coder-model`
- `qwen-portal/vision-model`
Switch models with:
```bash
openclaw models set qwen-portal/coder-model
```
## Reuse Qwen Code CLI login
If you already logged in with the Qwen Code CLI, OpenClaw will sync credentials
from `~/.qwen/oauth_creds.json` when it loads the auth store. You still need a
`models.providers.qwen-portal` entry (use the login command above to create one).
## Notes
- Tokens auto-refresh; re-run the login command if refresh fails or access is revoked.
- Default base URL: `https://portal.qwen.ai/v1` (override with
`models.providers.qwen-portal.baseUrl` if Qwen provides a different endpoint).
- See [Model providers](/concepts/model-providers) for provider-wide rules.

View File

@@ -1,85 +0,0 @@
---
title: "Qwen / Model Studio"
summary: "Alibaba Cloud Model Studio setup (Standard pay-as-you-go and Coding Plan, dual region endpoints)"
read_when:
- You want to use Qwen (Alibaba Cloud Model Studio) with OpenClaw
- You need the API key env var for Model Studio
- You want to use the Standard (pay-as-you-go) or Coding Plan endpoint
---
# Qwen / Model Studio (Alibaba Cloud)
The Model Studio provider gives access to Alibaba Cloud models including Qwen
and third-party models hosted on the platform. Two billing plans are supported:
**Standard** (pay-as-you-go) and **Coding Plan** (subscription).
- Provider: `modelstudio`
- Auth: `MODELSTUDIO_API_KEY`
- API: OpenAI-compatible
## Quick start
### Standard (pay-as-you-go)
```bash
# China endpoint
openclaw onboard --auth-choice modelstudio-standard-api-key-cn
# Global/Intl endpoint
openclaw onboard --auth-choice modelstudio-standard-api-key
```
### Coding Plan (subscription)
```bash
# China endpoint
openclaw onboard --auth-choice modelstudio-api-key-cn
# Global/Intl endpoint
openclaw onboard --auth-choice modelstudio-api-key
```
After onboarding, set a default model:
```json5
{
agents: {
defaults: {
model: { primary: "modelstudio/qwen3.5-plus" },
},
},
}
```
## Plan types and endpoints
| Plan | Region | Auth choice | Endpoint |
| -------------------------- | ------ | --------------------------------- | ------------------------------------------------ |
| Standard (pay-as-you-go) | China | `modelstudio-standard-api-key-cn` | `dashscope.aliyuncs.com/compatible-mode/v1` |
| Standard (pay-as-you-go) | Global | `modelstudio-standard-api-key` | `dashscope-intl.aliyuncs.com/compatible-mode/v1` |
| Coding Plan (subscription) | China | `modelstudio-api-key-cn` | `coding.dashscope.aliyuncs.com/v1` |
| Coding Plan (subscription) | Global | `modelstudio-api-key` | `coding-intl.dashscope.aliyuncs.com/v1` |
The provider auto-selects the endpoint based on your auth choice. You can
override with a custom `baseUrl` in config.
## Get your API key
- **China**: [bailian.console.aliyun.com](https://bailian.console.aliyun.com/)
- **Global/Intl**: [modelstudio.console.alibabacloud.com](https://modelstudio.console.alibabacloud.com/)
## Available models
- **qwen3.5-plus** (default) — Qwen 3.5 Plus
- **qwen3-coder-plus**, **qwen3-coder-next** — Qwen coding models
- **GLM-5** — GLM models via Alibaba
- **Kimi K2.5** — Moonshot AI via Alibaba
- **MiniMax-M2.5** — MiniMax via Alibaba
Some models (qwen3.5-plus, kimi-k2.5) support image input. Context windows range from 200K to 1M tokens.
## Environment note
If the Gateway runs as a daemon (launchd/systemd), make sure
`MODELSTUDIO_API_KEY` is available to that process (for example, in
`~/.openclaw/.env` or via `env.shellEnv`).

View File

@@ -29,7 +29,8 @@ Scope intent:
- `agents.list[].memorySearch.remote.apiKey`
- `talk.apiKey`
- `talk.providers.*.apiKey`
- `messages.tts.providers.*.apiKey`
- `messages.tts.elevenlabs.apiKey`
- `messages.tts.openai.apiKey`
- `tools.web.fetch.firecrawl.apiKey`
- `plugins.entries.brave.config.webSearch.apiKey`
- `plugins.entries.google.config.webSearch.apiKey`
@@ -62,10 +63,12 @@ Scope intent:
- `channels.slack.accounts.*.signingSecret`
- `channels.discord.token`
- `channels.discord.pluralkit.token`
- `channels.discord.voice.tts.providers.*.apiKey`
- `channels.discord.voice.tts.elevenlabs.apiKey`
- `channels.discord.voice.tts.openai.apiKey`
- `channels.discord.accounts.*.token`
- `channels.discord.accounts.*.pluralkit.token`
- `channels.discord.accounts.*.voice.tts.providers.*.apiKey`
- `channels.discord.accounts.*.voice.tts.elevenlabs.apiKey`
- `channels.discord.accounts.*.voice.tts.openai.apiKey`
- `channels.irc.password`
- `channels.irc.nickserv.password`
- `channels.irc.accounts.*.password`

View File

@@ -80,9 +80,16 @@
"optIn": true
},
{
"id": "channels.discord.accounts.*.voice.tts.providers.*.apiKey",
"id": "channels.discord.accounts.*.voice.tts.elevenlabs.apiKey",
"configFile": "openclaw.json",
"path": "channels.discord.accounts.*.voice.tts.providers.*.apiKey",
"path": "channels.discord.accounts.*.voice.tts.elevenlabs.apiKey",
"secretShape": "secret_input",
"optIn": true
},
{
"id": "channels.discord.accounts.*.voice.tts.openai.apiKey",
"configFile": "openclaw.json",
"path": "channels.discord.accounts.*.voice.tts.openai.apiKey",
"secretShape": "secret_input",
"optIn": true
},
@@ -101,9 +108,16 @@
"optIn": true
},
{
"id": "channels.discord.voice.tts.providers.*.apiKey",
"id": "channels.discord.voice.tts.elevenlabs.apiKey",
"configFile": "openclaw.json",
"path": "channels.discord.voice.tts.providers.*.apiKey",
"path": "channels.discord.voice.tts.elevenlabs.apiKey",
"secretShape": "secret_input",
"optIn": true
},
{
"id": "channels.discord.voice.tts.openai.apiKey",
"configFile": "openclaw.json",
"path": "channels.discord.voice.tts.openai.apiKey",
"secretShape": "secret_input",
"optIn": true
},
@@ -406,9 +420,16 @@
"optIn": true
},
{
"id": "messages.tts.providers.*.apiKey",
"id": "messages.tts.elevenlabs.apiKey",
"configFile": "openclaw.json",
"path": "messages.tts.providers.*.apiKey",
"path": "messages.tts.elevenlabs.apiKey",
"secretShape": "secret_input",
"optIn": true
},
{
"id": "messages.tts.openai.apiKey",
"configFile": "openclaw.json",
"path": "messages.tts.openai.apiKey",
"secretShape": "secret_input",
"optIn": true
},

View File

@@ -31,7 +31,7 @@ For a high-level overview, see [Onboarding (CLI)](/start/wizard).
</Step>
<Step title="Model/Auth">
- **Anthropic API key**: uses `ANTHROPIC_API_KEY` if present or prompts for a key, then saves it for daemon use.
- **Anthropic Claude CLI**: on macOS onboarding checks Keychain item "Claude Code-credentials" (choose "Always Allow" so launchd starts don't block); on Linux/Windows it reuses `~/.claude/.credentials.json` if present and switches model selection to `claude-cli/...`.
- **Anthropic OAuth (Claude Code CLI)**: on macOS onboarding checks Keychain item "Claude Code-credentials" (choose "Always Allow" so launchd starts don't block); on Linux/Windows it reuses `~/.claude/.credentials.json` if present.
- **Anthropic token (paste setup-token)**: run `claude setup-token` on any machine, then paste the token (you can name it; blank = default).
- **OpenAI Code (Codex) subscription (Codex CLI)**: if `~/.codex/auth.json` exists, onboarding can reuse it.
- **OpenAI Code (Codex) subscription (OAuth)**: browser flow; paste the `code#state`.

View File

@@ -126,10 +126,7 @@ What you set:
<Accordion title="Anthropic API key">
Uses `ANTHROPIC_API_KEY` if present or prompts for a key, then saves it for daemon use.
</Accordion>
<Accordion title="Anthropic Claude CLI">
Reuses a local Claude CLI login on the gateway host and switches model
selection to `claude-cli/...`.
<Accordion title="Anthropic OAuth (Claude Code CLI)">
- macOS: checks Keychain item "Claude Code-credentials"
- Linux and Windows: reuses `~/.claude/.credentials.json` if present

View File

@@ -42,44 +42,6 @@ openclaw browser --browser-profile openclaw snapshot
If you get “Browser disabled”, enable it in config (see below) and restart the
Gateway.
## Plugin control
The default `browser` tool is now a bundled plugin that ships enabled by
default. That means you can disable or replace it without removing the rest of
OpenClaw's plugin system:
```json5
{
plugins: {
entries: {
browser: {
enabled: false,
},
},
},
}
```
Disable the bundled plugin before installing another plugin that provides the
same `browser` tool name. The default browser experience needs both:
- `plugins.entries.browser.enabled` not disabled
- `browser.enabled=true`
If you turn off only the plugin, the bundled browser CLI (`openclaw browser`),
gateway method (`browser.request`), agent tool, and default browser control
service all disappear together. Your `browser.*` config stays intact for a
replacement plugin to reuse.
The bundled browser plugin also owns the browser runtime implementation now.
Core keeps only shared Plugin SDK helpers plus compatibility re-exports for
older internal import paths. In practice, removing or replacing
`extensions/browser` removes the browser feature set instead of leaving a
second core-owned runtime behind.
Browser config changes still require a Gateway restart so the bundled plugin
can re-register its browser service with the new settings.
## Profiles: `openclaw` vs `user`
- `openclaw`: managed, isolated browser (no extension required).

View File

@@ -90,7 +90,7 @@ and the [Plugin SDK Overview](/plugins/sdk-overview).
`anthropic`, `byteplus`, `cloudflare-ai-gateway`, `github-copilot`, `google`,
`huggingface`, `kilocode`, `kimi-coding`, `minimax`, `mistral`, `modelstudio`,
`moonshot`, `nvidia`, `openai`, `opencode`, `opencode-go`, `openrouter`,
`qianfan`, `synthetic`, `together`, `venice`,
`qianfan`, `qwen-portal-auth`, `synthetic`, `together`, `venice`,
`vercel-ai-gateway`, `volcengine`, `xiaomi`, `zai`
</Accordion>
@@ -104,7 +104,6 @@ and the [Plugin SDK Overview](/plugins/sdk-overview).
</Accordion>
<Accordion title="Other">
- `browser` — bundled browser plugin for the browser tool, `openclaw browser` CLI, `browser.request` gateway method, browser runtime, and default browser control service (enabled by default; disable before replacing it)
- `copilot-proxy` — VS Code Copilot Proxy bridge (disabled by default)
</Accordion>
</AccordionGroup>

View File

@@ -109,6 +109,7 @@ x-i18n:
- `byteplus``cloudflare-ai-gateway``huggingface``kimi-coding`
`modelstudio``nvidia``qianfan``synthetic``together``venice`
`vercel-ai-gateway``volcengine`:仅插件接管的目录
- `qwen-portal`插件接管的目录、OAuth 登录和 OAuth 刷新
- `minimax``xiaomi`:插件接管的目录,以及使用量身份验证/快照逻辑
内置的 `openai` 插件现在接管两个提供商 ID`openai`
@@ -347,6 +348,22 @@ Kimi Coding 使用 Moonshot AI 的 Anthropic 兼容端点:
}
```
### Qwen OAuth免费层
Qwen 通过设备代码流程提供对 Qwen Coder + Vision 的 OAuth 访问。
内置提供商插件默认启用,因此只需登录:
```bash
openclaw models auth login --provider qwen-portal --set-default
```
模型引用:
- `qwen-portal/coder-model`
- `qwen-portal/vision-model`
设置详情和说明请参见 [/providers/qwen](/providers/qwen)。
### Volcano EngineDoubao
Volcano Engine火山引擎为中国用户提供对 Doubao 和其他模型的访问。

View File

@@ -1,36 +1,55 @@
---
read_when:
- 你想在 OpenClaw 中使用 Qwen
-之前使用过 Qwen OAuth
summary: 通过阿里云 Model Studio 使用 Qwen 模型
-想要免费层 OAuth 访问 Qwen Coder
summary: 在 OpenClaw 中使用 Qwen OAuth免费层
title: Qwen
x-i18n:
generated_at: "2026-03-23T00:00:00Z"
generated_at: "2026-02-03T07:53:34Z"
model: claude-opus-4-5
provider: pi
source_hash: ""
source_hash: 88b88e224e2fecbb1ca26e24fbccdbe25609be40b38335d0451343a5da53fdd4
source_path: providers/qwen.md
workflow: 15
---
# Qwen
<Warning>
Qwen 为 Qwen Coder 和 Qwen Vision 模型提供免费层 OAuth 流程(每天 2,000 次请求,受 Qwen 速率限制约束)。
**Qwen OAuth 已移除。** 使用 `portal.qwen.ai` 端点的免费层 OAuth 集成(`qwen-portal`)已不再可用。详情请参见 [Issue #49557](https://github.com/openclaw/openclaw/issues/49557)。
</Warning>
## 推荐方案Model Studio阿里云 Coding Plan
使用 [Model Studio](/providers/modelstudio) 获取官方支持的 Qwen 模型访问Qwen 3.5 Plus、GLM-4.7、Kimi K2.5、MiniMax M2.5 等)。
## 启用插件
```bash
# 全球端点
openclaw onboard --auth-choice modelstudio-api-key
# 中国端点
openclaw onboard --auth-choice modelstudio-api-key-cn
openclaw plugins enable qwen-portal-auth
```
完整设置详情请参见 [Model Studio](/providers/modelstudio)
启用后重启 Gateway 网关
## 认证
```bash
openclaw models auth login --provider qwen-portal --set-default
```
这会运行 Qwen 设备码 OAuth 流程并将提供商条目写入你的 `models.json`(加上一个 `qwen` 别名以便快速切换)。
## 模型 ID
- `qwen-portal/coder-model`
- `qwen-portal/vision-model`
切换模型:
```bash
openclaw models set qwen-portal/coder-model
```
## 复用 Qwen Code CLI 登录
如果你已经使用 Qwen Code CLI 登录OpenClaw 会在加载认证存储时从 `~/.qwen/oauth_creds.json` 同步凭证。你仍然需要一个 `models.providers.qwen-portal` 条目(使用上面的登录命令创建一个)。
## 注意
- 令牌自动刷新;如果刷新失败或访问被撤销,请重新运行登录命令。
- 默认基础 URL`https://portal.qwen.ai/v1`(如果 Qwen 提供不同的端点,使用 `models.providers.qwen-portal.baseUrl` 覆盖)。
- 参阅[模型提供商](/concepts/model-providers)了解提供商级别的规则。

View File

@@ -155,6 +155,7 @@ Bundle hook 支持仅限于常规 OpenClaw hook 目录格式(在声明的 hook
- OpenCode Zen provider 能力 — 以 `opencode` 形式捆绑(默认启用)
- OpenRouter provider 运行时 — 以 `openrouter` 形式捆绑(默认启用)
- Qianfan provider catalog — 以 `qianfan` 形式捆绑(默认启用)
- Qwen OAuthprovider 身份验证 + catalog— 以 `qwen-portal-auth` 形式捆绑(默认启用)
- Synthetic provider catalog — 以 `synthetic` 形式捆绑(默认启用)
- Together provider catalog — 以 `together` 形式捆绑(默认启用)
- Venice provider catalog — 以 `venice` 形式捆绑(默认启用)
@@ -496,7 +497,7 @@ api.registerHttpRoute({
`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/synology-chat`
`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`
@@ -612,6 +613,7 @@ OpenClaw 按以下顺序扫描:
- `openrouter`
- `phone-control`
- `qianfan`
- `qwen-portal-auth`
- `sglang`
- `synthetic`
- `talk-voice`

View File

@@ -1,112 +0,0 @@
import type { CliBackendPlugin, CliBackendConfig } from "openclaw/plugin-sdk/cli-backend";
import {
CLI_FRESH_WATCHDOG_DEFAULTS,
CLI_RESUME_WATCHDOG_DEFAULTS,
} from "openclaw/plugin-sdk/cli-backend";
const CLAUDE_MODEL_ALIASES: Record<string, string> = {
opus: "opus",
"opus-4.6": "opus",
"opus-4.5": "opus",
"opus-4": "opus",
"claude-opus-4-6": "opus",
"claude-opus-4-5": "opus",
"claude-opus-4": "opus",
sonnet: "sonnet",
"sonnet-4.6": "sonnet",
"sonnet-4.5": "sonnet",
"sonnet-4.1": "sonnet",
"sonnet-4.0": "sonnet",
"claude-sonnet-4-6": "sonnet",
"claude-sonnet-4-5": "sonnet",
"claude-sonnet-4-1": "sonnet",
"claude-sonnet-4-0": "sonnet",
haiku: "haiku",
"haiku-3.5": "haiku",
"claude-haiku-3-5": "haiku",
};
const CLAUDE_LEGACY_SKIP_PERMISSIONS_ARG = "--dangerously-skip-permissions";
const CLAUDE_PERMISSION_MODE_ARG = "--permission-mode";
const CLAUDE_BYPASS_PERMISSIONS_MODE = "bypassPermissions";
function normalizeClaudePermissionArgs(args?: string[]): string[] | undefined {
if (!args) {
return args;
}
const normalized: string[] = [];
let sawLegacySkip = false;
let hasPermissionMode = false;
for (let i = 0; i < args.length; i += 1) {
const arg = args[i];
if (arg === CLAUDE_LEGACY_SKIP_PERMISSIONS_ARG) {
sawLegacySkip = true;
continue;
}
if (arg === CLAUDE_PERMISSION_MODE_ARG) {
hasPermissionMode = true;
normalized.push(arg);
const maybeValue = args[i + 1];
if (typeof maybeValue === "string") {
normalized.push(maybeValue);
i += 1;
}
continue;
}
if (arg.startsWith(`${CLAUDE_PERMISSION_MODE_ARG}=`)) {
hasPermissionMode = true;
}
normalized.push(arg);
}
if (sawLegacySkip && !hasPermissionMode) {
normalized.push(CLAUDE_PERMISSION_MODE_ARG, CLAUDE_BYPASS_PERMISSIONS_MODE);
}
return normalized;
}
function normalizeClaudeBackendConfig(config: CliBackendConfig): CliBackendConfig {
return {
...config,
args: normalizeClaudePermissionArgs(config.args),
resumeArgs: normalizeClaudePermissionArgs(config.resumeArgs),
};
}
export function buildAnthropicCliBackend(): CliBackendPlugin {
return {
id: "claude-cli",
bundleMcp: true,
config: {
command: "claude",
args: ["-p", "--output-format", "json", "--permission-mode", "bypassPermissions"],
resumeArgs: [
"-p",
"--output-format",
"json",
"--permission-mode",
"bypassPermissions",
"--resume",
"{sessionId}",
],
output: "json",
input: "arg",
modelArg: "--model",
modelAliases: CLAUDE_MODEL_ALIASES,
sessionArg: "--session-id",
sessionMode: "always",
sessionIdFields: ["session_id", "sessionId", "conversation_id", "conversationId"],
systemPromptArg: "--append-system-prompt",
systemPromptMode: "append",
systemPromptWhen: "first",
clearEnv: ["ANTHROPIC_API_KEY", "ANTHROPIC_API_KEY_OLD"],
reliability: {
watchdog: {
fresh: { ...CLI_FRESH_WATCHDOG_DEFAULTS },
resume: { ...CLI_RESUME_WATCHDOG_DEFAULTS },
},
},
serialize: true,
},
normalizeConfig: normalizeClaudeBackendConfig,
};
}

View File

@@ -1,82 +0,0 @@
import { describe, expect, it, vi } from "vitest";
const readClaudeCliCredentialsCached = vi.hoisted(() => vi.fn());
vi.mock("openclaw/plugin-sdk/provider-auth", async (importActual) => {
const actual = await importActual<typeof import("openclaw/plugin-sdk/provider-auth")>();
return {
...actual,
readClaudeCliCredentialsCached,
};
});
const { buildAnthropicCliMigrationResult, hasClaudeCliAuth } = await import("./cli-migration.js");
describe("anthropic cli migration", () => {
it("detects local Claude CLI auth", () => {
readClaudeCliCredentialsCached.mockReturnValue({ type: "oauth" });
expect(hasClaudeCliAuth()).toBe(true);
});
it("rewrites anthropic defaults to claude-cli defaults", () => {
const result = buildAnthropicCliMigrationResult({
agents: {
defaults: {
model: {
primary: "anthropic/claude-sonnet-4-6",
fallbacks: ["anthropic/claude-opus-4-6", "openai/gpt-5.2"],
},
models: {
"anthropic/claude-sonnet-4-6": { alias: "Sonnet" },
"anthropic/claude-opus-4-6": { alias: "Opus" },
"openai/gpt-5.2": {},
},
},
},
});
expect(result.profiles).toEqual([]);
expect(result.defaultModel).toBe("claude-cli/claude-sonnet-4-6");
expect(result.configPatch).toEqual({
agents: {
defaults: {
model: {
primary: "claude-cli/claude-sonnet-4-6",
fallbacks: ["claude-cli/claude-opus-4-6", "openai/gpt-5.2"],
},
models: {
"claude-cli/claude-sonnet-4-6": { alias: "Sonnet" },
"claude-cli/claude-opus-4-6": { alias: "Opus" },
"openai/gpt-5.2": {},
},
},
},
});
});
it("adds a Claude CLI default when no anthropic default is present", () => {
const result = buildAnthropicCliMigrationResult({
agents: {
defaults: {
model: { primary: "openai/gpt-5.2" },
models: {
"openai/gpt-5.2": {},
},
},
},
});
expect(result.defaultModel).toBe("claude-cli/claude-sonnet-4-6");
expect(result.configPatch).toEqual({
agents: {
defaults: {
models: {
"openai/gpt-5.2": {},
"claude-cli/claude-sonnet-4-6": {},
},
},
},
});
});
});

View File

@@ -1,131 +0,0 @@
import type { OpenClawConfig, ProviderAuthResult } from "openclaw/plugin-sdk/provider-auth";
import { readClaudeCliCredentialsCached } from "openclaw/plugin-sdk/provider-auth";
const DEFAULT_CLAUDE_CLI_MODEL = "claude-cli/claude-sonnet-4-6";
type AgentDefaultsModel = NonNullable<NonNullable<OpenClawConfig["agents"]>["defaults"]>["model"];
type AgentDefaultsModels = NonNullable<NonNullable<OpenClawConfig["agents"]>["defaults"]>["models"];
function toClaudeCliModelRef(raw: string): string | null {
const trimmed = raw.trim();
if (!trimmed.toLowerCase().startsWith("anthropic/")) {
return null;
}
const modelId = trimmed.slice("anthropic/".length).trim();
if (!modelId.toLowerCase().startsWith("claude-")) {
return null;
}
return `claude-cli/${modelId}`;
}
function rewriteModelSelection(model: AgentDefaultsModel): {
value: AgentDefaultsModel;
primary?: string;
changed: boolean;
} {
if (typeof model === "string") {
const converted = toClaudeCliModelRef(model);
return converted
? { value: converted, primary: converted, changed: true }
: { value: model, changed: false };
}
if (!model || typeof model !== "object" || Array.isArray(model)) {
return { value: model, changed: false };
}
const current = model as Record<string, unknown>;
const next: Record<string, unknown> = { ...current };
let changed = false;
let primary: string | undefined;
if (typeof current.primary === "string") {
const converted = toClaudeCliModelRef(current.primary);
if (converted) {
next.primary = converted;
primary = converted;
changed = true;
}
}
const currentFallbacks = current.fallbacks;
if (Array.isArray(currentFallbacks)) {
const nextFallbacks = currentFallbacks.map((entry) =>
typeof entry === "string" ? (toClaudeCliModelRef(entry) ?? entry) : entry,
);
if (nextFallbacks.some((entry, index) => entry !== currentFallbacks[index])) {
next.fallbacks = nextFallbacks;
changed = true;
}
}
return {
value: changed ? next : model,
...(primary ? { primary } : {}),
changed,
};
}
function rewriteModelEntryMap(models: Record<string, unknown> | undefined): {
value: Record<string, unknown> | undefined;
migrated: string[];
} {
if (!models) {
return { value: models, migrated: [] };
}
const next = { ...models };
const migrated: string[] = [];
for (const [rawKey, value] of Object.entries(models)) {
const converted = toClaudeCliModelRef(rawKey);
if (!converted) {
continue;
}
if (!(converted in next)) {
next[converted] = value;
}
delete next[rawKey];
migrated.push(converted);
}
return {
value: migrated.length > 0 ? next : models,
migrated,
};
}
export function hasClaudeCliAuth(): boolean {
return Boolean(readClaudeCliCredentialsCached());
}
export function buildAnthropicCliMigrationResult(config: OpenClawConfig): ProviderAuthResult {
const defaults = config.agents?.defaults;
const rewrittenModel = rewriteModelSelection(defaults?.model);
const rewrittenModels = rewriteModelEntryMap(defaults?.models);
const existingModels = (rewrittenModels.value ??
defaults?.models ??
{}) as NonNullable<AgentDefaultsModels>;
const defaultModel = rewrittenModel.primary ?? DEFAULT_CLAUDE_CLI_MODEL;
return {
profiles: [],
configPatch: {
agents: {
defaults: {
...(rewrittenModel.changed ? { model: rewrittenModel.value } : {}),
models: {
...existingModels,
[defaultModel]: existingModels[defaultModel] ?? {},
} as NonNullable<AgentDefaultsModels>,
},
},
},
defaultModel,
notes: [
"Claude CLI auth detected; switched Anthropic model selection to the local Claude CLI backend.",
"Existing Anthropic auth profiles are kept for rollback.",
...(rewrittenModels.migrated.length > 0
? [`Migrated allowlist entries: ${rewrittenModels.migrated.join(", ")}.`]
: []),
],
};
}

View File

@@ -27,8 +27,6 @@ import {
} from "openclaw/plugin-sdk/provider-auth";
import { normalizeModelCompat } from "openclaw/plugin-sdk/provider-models";
import { fetchClaudeUsage } from "openclaw/plugin-sdk/provider-usage";
import { buildAnthropicCliBackend } from "./cli-backend.js";
import { buildAnthropicCliMigrationResult, hasClaudeCliAuth } from "./cli-migration.js";
import { anthropicMediaUnderstandingProvider } from "./media-understanding-provider.js";
const PROVIDER_ID = "anthropic";
@@ -313,65 +311,11 @@ async function runAnthropicSetupTokenNonInteractive(ctx: {
});
}
async function runAnthropicCliMigration(ctx: ProviderAuthContext): Promise<ProviderAuthResult> {
if (!hasClaudeCliAuth()) {
throw new Error(
[
"Claude CLI is not authenticated on this host.",
`Run ${formatCliCommand("claude auth login")} first, then re-run this setup.`,
].join("\n"),
);
}
return buildAnthropicCliMigrationResult(ctx.config);
}
async function runAnthropicCliMigrationNonInteractive(ctx: {
config: ProviderAuthContext["config"];
runtime: ProviderAuthContext["runtime"];
}): Promise<ProviderAuthContext["config"] | null> {
if (!hasClaudeCliAuth()) {
ctx.runtime.error(
[
'Auth choice "anthropic-cli" requires Claude CLI auth on this host.',
`Run ${formatCliCommand("claude auth login")} first.`,
].join("\n"),
);
ctx.runtime.exit(1);
return null;
}
const result = buildAnthropicCliMigrationResult(ctx.config);
const currentDefaults = ctx.config.agents?.defaults;
const currentModel = currentDefaults?.model;
const currentFallbacks =
currentModel && typeof currentModel === "object" && "fallbacks" in currentModel
? currentModel.fallbacks
: undefined;
return {
...ctx.config,
...result.configPatch,
agents: {
...ctx.config.agents,
...result.configPatch?.agents,
defaults: {
...currentDefaults,
...result.configPatch?.agents?.defaults,
model: {
...(Array.isArray(currentFallbacks) ? { fallbacks: currentFallbacks } : {}),
primary: result.defaultModel,
},
},
},
};
}
export default definePluginEntry({
id: PROVIDER_ID,
name: "Anthropic Provider",
description: "Bundled Anthropic provider plugin",
register(api) {
api.registerCliBackend(buildAnthropicCliBackend());
api.registerProvider({
id: PROVIDER_ID,
label: "Anthropic",
@@ -379,33 +323,6 @@ export default definePluginEntry({
envVars: ["ANTHROPIC_OAUTH_TOKEN", "ANTHROPIC_API_KEY"],
deprecatedProfileIds: [CLAUDE_CLI_PROFILE_ID],
auth: [
{
id: "cli",
label: "Claude CLI",
hint: "Reuse a local Claude CLI login and switch model selection to claude-cli/*",
kind: "custom",
wizard: {
choiceId: "anthropic-cli",
choiceLabel: "Anthropic Claude CLI",
choiceHint: "Reuse a local Claude CLI login on this host",
groupId: "anthropic",
groupLabel: "Anthropic",
groupHint: "Claude CLI + setup-token + API key",
modelAllowlist: {
allowedKeys: [...ANTHROPIC_OAUTH_ALLOWLIST].map((model) =>
model.replace(/^anthropic\//, "claude-cli/"),
),
initialSelections: ["claude-cli/claude-sonnet-4-6"],
message: "Claude CLI models",
},
},
run: async (ctx: ProviderAuthContext) => await runAnthropicCliMigration(ctx),
runNonInteractive: async (ctx) =>
await runAnthropicCliMigrationNonInteractive({
config: ctx.config,
runtime: ctx.runtime,
}),
},
{
id: "setup-token",
label: "setup-token (claude)",
@@ -417,7 +334,7 @@ export default definePluginEntry({
choiceHint: "Run `claude setup-token` elsewhere, then paste the token here",
groupId: "anthropic",
groupLabel: "Anthropic",
groupHint: "Claude CLI + setup-token + API key",
groupHint: "setup-token + API key",
modelAllowlist: {
allowedKeys: [...ANTHROPIC_OAUTH_ALLOWLIST],
initialSelections: ["anthropic/claude-sonnet-4-6"],
@@ -449,7 +366,7 @@ export default definePluginEntry({
choiceLabel: "Anthropic API key",
groupId: "anthropic",
groupLabel: "Anthropic",
groupHint: "Claude CLI + setup-token + API key",
groupHint: "setup-token + API key",
},
}),
],

View File

@@ -1,22 +1,10 @@
{
"id": "anthropic",
"providers": ["anthropic"],
"mediaUnderstandingProviders": ["anthropic"],
"cliBackends": ["claude-cli"],
"providerAuthEnvVars": {
"anthropic": ["ANTHROPIC_OAUTH_TOKEN", "ANTHROPIC_API_KEY"]
},
"providerAuthChoices": [
{
"provider": "anthropic",
"method": "cli",
"choiceId": "anthropic-cli",
"choiceLabel": "Anthropic Claude CLI",
"choiceHint": "Reuse a local Claude CLI login on this host",
"groupId": "anthropic",
"groupLabel": "Anthropic",
"groupHint": "Claude CLI + setup-token + API key"
},
{
"provider": "anthropic",
"method": "setup-token",
@@ -25,7 +13,7 @@
"choiceHint": "Run `claude setup-token` elsewhere, then paste the token here",
"groupId": "anthropic",
"groupLabel": "Anthropic",
"groupHint": "Claude CLI + setup-token + API key"
"groupHint": "setup-token + API key"
},
{
"provider": "anthropic",
@@ -34,7 +22,7 @@
"choiceLabel": "Anthropic API key",
"groupId": "anthropic",
"groupLabel": "Anthropic",
"groupHint": "Claude CLI + setup-token + API key",
"groupHint": "setup-token + API key",
"optionKey": "anthropicApiKey",
"cliFlag": "--anthropic-api-key",
"cliOption": "--anthropic-api-key <key>",

View File

@@ -1,8 +1,6 @@
import { isBlockedHostnameOrIp } from "openclaw/plugin-sdk/infra-runtime";
import { resolveBlueBubblesAccount } from "./accounts.js";
import type { OpenClawConfig } from "./runtime-api.js";
import { normalizeResolvedSecretInputString } from "./secret-input.js";
import { normalizeBlueBubblesServerUrl } from "./types.js";
export type BlueBubblesAccountResolveOpts = {
serverUrl?: string;
@@ -45,19 +43,10 @@ export function resolveBlueBubblesServerAccount(params: BlueBubblesAccountResolv
if (!password) {
throw new Error("BlueBubbles password is required");
}
let autoAllowPrivateNetwork = false;
try {
const hostname = new URL(normalizeBlueBubblesServerUrl(baseUrl)).hostname.trim();
autoAllowPrivateNetwork = Boolean(hostname) && isBlockedHostnameOrIp(hostname);
} catch {
autoAllowPrivateNetwork = false;
}
return {
baseUrl,
password,
accountId: account.accountId,
allowPrivateNetwork: account.config.allowPrivateNetwork === true || autoAllowPrivateNetwork,
allowPrivateNetwork: account.config.allowPrivateNetwork === true,
};
}

View File

@@ -161,12 +161,7 @@ export const bluebubblesMessageActions: ChannelMessageActionAdapter = {
throw new Error(`BlueBubbles ${action} requires serverUrl and password.`);
}
const resolved = await runtime.resolveChatGuidForTarget({
baseUrl,
password,
target,
allowPrivateNetwork: account.config.allowPrivateNetwork === true,
});
const resolved = await runtime.resolveChatGuidForTarget({ baseUrl, password, target });
if (!resolved) {
throw new Error(`BlueBubbles ${action} failed: chatGuid not found for target.`);
}

View File

@@ -1,6 +1,6 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import "./test-mocks.js";
import { downloadBlueBubblesAttachment, sendBlueBubblesAttachment } from "./attachments.js";
import "./test-mocks.js";
import { getCachedBlueBubblesPrivateApiStatus } from "./probe.js";
import type { PluginRuntime } from "./runtime-api.js";
import { setBlueBubblesRuntime } from "./runtime.js";
@@ -288,45 +288,30 @@ describe("downloadBlueBubblesAttachment", () => {
expect(fetchMediaArgs.ssrfPolicy).toEqual({ allowPrivateNetwork: true });
});
it("auto-enables private-network fetches for loopback serverUrl when allowPrivateNetwork is not set", async () => {
it("auto-allowlists serverUrl hostname when allowPrivateNetwork is not set", async () => {
mockSuccessfulAttachmentDownload();
const attachment: BlueBubblesAttachment = { guid: "att-no-ssrf" };
await downloadBlueBubblesAttachment(attachment, {
serverUrl: "http://localhost:1234",
password: "test",
cfg: { channels: { bluebubbles: {} } },
});
const fetchMediaArgs = fetchRemoteMediaMock.mock.calls[0][0] as Record<string, unknown>;
expect(fetchMediaArgs.ssrfPolicy).toEqual({ allowPrivateNetwork: true });
expect(fetchMediaArgs.ssrfPolicy).toEqual({ allowedHostnames: ["localhost"] });
});
it("auto-enables private-network fetches for private IP serverUrl when allowPrivateNetwork is not set", async () => {
it("auto-allowlists private IP serverUrl hostname when allowPrivateNetwork is not set", async () => {
mockSuccessfulAttachmentDownload();
const attachment: BlueBubblesAttachment = { guid: "att-private-ip" };
await downloadBlueBubblesAttachment(attachment, {
serverUrl: "http://192.168.1.5:1234",
password: "test",
cfg: { channels: { bluebubbles: {} } },
});
const fetchMediaArgs = fetchRemoteMediaMock.mock.calls[0][0] as Record<string, unknown>;
expect(fetchMediaArgs.ssrfPolicy).toEqual({ allowPrivateNetwork: true });
});
it("allowlists public serverUrl hostname when allowPrivateNetwork is not set", async () => {
mockSuccessfulAttachmentDownload();
const attachment: BlueBubblesAttachment = { guid: "att-public-host" };
await downloadBlueBubblesAttachment(attachment, {
serverUrl: "https://bluebubbles.example.com:1234",
password: "test",
});
const fetchMediaArgs = fetchRemoteMediaMock.mock.calls[0][0] as Record<string, unknown>;
expect(fetchMediaArgs.ssrfPolicy).toEqual({ allowedHostnames: ["bluebubbles.example.com"] });
expect(fetchMediaArgs.ssrfPolicy).toEqual({ allowedHostnames: ["192.168.1.5"] });
});
});

View File

@@ -16,13 +16,8 @@ import {
buildBlueBubblesApiUrl,
type BlueBubblesAttachment,
type BlueBubblesSendTarget,
type SsrFPolicy,
} from "./types.js";
function blueBubblesPolicy(allowPrivateNetwork: boolean | undefined): SsrFPolicy {
return allowPrivateNetwork ? { allowPrivateNetwork: true } : {};
}
export type BlueBubblesAttachmentOpts = {
serverUrl?: string;
password?: string;
@@ -160,7 +155,7 @@ export async function sendBlueBubblesAttachment(params: {
const fallbackName = wantsVoice ? "Audio Message" : "attachment";
filename = sanitizeFilename(filename, fallbackName);
contentType = contentType?.trim() || undefined;
const { baseUrl, password, accountId, allowPrivateNetwork } = resolveAccount(opts);
const { baseUrl, password, accountId } = resolveAccount(opts);
const privateApiStatus = getCachedBlueBubblesPrivateApiStatus(accountId);
const privateApiEnabled = isBlueBubblesPrivateApiStatusEnabled(privateApiStatus);
@@ -190,7 +185,6 @@ export async function sendBlueBubblesAttachment(params: {
password,
timeoutMs: opts.timeoutMs,
target,
allowPrivateNetwork,
});
if (!chatGuid) {
// For handle targets (phone numbers/emails), auto-create a new DM chat
@@ -200,7 +194,6 @@ export async function sendBlueBubblesAttachment(params: {
password,
address: target.address,
timeoutMs: opts.timeoutMs,
allowPrivateNetwork,
});
chatGuid = created.chatGuid;
// If we still don't have a chatGuid, try resolving again (chat was created server-side)
@@ -210,7 +203,6 @@ export async function sendBlueBubblesAttachment(params: {
password,
timeoutMs: opts.timeoutMs,
target,
allowPrivateNetwork,
});
}
}
@@ -289,7 +281,6 @@ export async function sendBlueBubblesAttachment(params: {
boundary,
parts,
timeoutMs: opts.timeoutMs ?? 60_000, // longer timeout for file uploads
ssrfPolicy: blueBubblesPolicy(allowPrivateNetwork),
});
await assertMultipartActionOk(res, "attachment send");

View File

@@ -196,7 +196,6 @@ export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount, BlueBu
baseUrl: account.baseUrl,
password: account.config.password ?? null,
timeoutMs,
allowPrivateNetwork: account.config.allowPrivateNetwork === true,
}),
resolveAccountSnapshot: ({ account, runtime, probe }) => {
const running = runtime?.running ?? false;

View File

@@ -1,16 +1,11 @@
import crypto from "node:crypto";
import path from "node:path";
import type { SsrFPolicy } from "openclaw/plugin-sdk/infra-runtime";
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";
function blueBubblesPolicy(allowPrivateNetwork: boolean): SsrFPolicy {
return allowPrivateNetwork ? { allowPrivateNetwork: true } : {};
}
export type BlueBubblesChatOpts = {
serverUrl?: string;
password?: string;
@@ -46,7 +41,7 @@ async function sendBlueBubblesChatEndpointRequest(params: {
if (!trimmed) {
return;
}
const { baseUrl, password, accountId, allowPrivateNetwork } = resolveAccount(params.opts);
const { baseUrl, password, accountId } = resolveAccount(params.opts);
if (getCachedBlueBubblesPrivateApiStatus(accountId) === false) {
return;
}
@@ -59,7 +54,6 @@ async function sendBlueBubblesChatEndpointRequest(params: {
url,
{ method: params.method },
params.opts.timeoutMs,
blueBubblesPolicy(allowPrivateNetwork),
);
await assertMultipartActionOk(res, params.action);
}
@@ -72,7 +66,7 @@ async function sendPrivateApiJsonRequest(params: {
method: "POST" | "PUT" | "DELETE";
payload?: unknown;
}): Promise<void> {
const { baseUrl, password, accountId, allowPrivateNetwork } = resolveAccount(params.opts);
const { baseUrl, password, accountId } = resolveAccount(params.opts);
assertPrivateApiEnabled(accountId, params.feature);
const url = buildBlueBubblesApiUrl({
baseUrl,
@@ -86,12 +80,7 @@ async function sendPrivateApiJsonRequest(params: {
request.body = JSON.stringify(params.payload);
}
const res = await blueBubblesFetchWithTimeout(
url,
request,
params.opts.timeoutMs,
blueBubblesPolicy(allowPrivateNetwork),
);
const res = await blueBubblesFetchWithTimeout(url, request, params.opts.timeoutMs);
await assertMultipartActionOk(res, params.action);
}
@@ -293,7 +282,7 @@ export async function setGroupIconBlueBubbles(
throw new Error("BlueBubbles setGroupIcon requires image buffer");
}
const { baseUrl, password, accountId, allowPrivateNetwork } = resolveAccount(opts);
const { baseUrl, password, accountId } = resolveAccount(opts);
assertPrivateApiEnabled(accountId, "setGroupIcon");
const url = buildBlueBubblesApiUrl({
baseUrl,
@@ -328,7 +317,6 @@ export async function setGroupIconBlueBubbles(
boundary,
parts,
timeoutMs: opts.timeoutMs ?? 60_000, // longer timeout for file uploads
ssrfPolicy: blueBubblesPolicy(allowPrivateNetwork),
});
await assertMultipartActionOk(res, "setGroupIcon");

View File

@@ -42,7 +42,6 @@ const bluebubblesAccountSchema = z
allowFrom: AllowFromListSchema,
groupAllowFrom: AllowFromListSchema,
groupPolicy: GroupPolicySchema.optional(),
enrichGroupParticipantsFromContacts: z.boolean().optional().default(true),
historyLimit: z.number().int().min(0).optional(),
dmHistoryLimit: z.number().int().min(0).optional(),
textChunkLimit: z.number().int().positive().optional(),

View File

@@ -83,13 +83,11 @@ export async function fetchBlueBubblesHistory(
let baseUrl: string;
let password: string;
let allowPrivateNetwork = false;
try {
({ baseUrl, password, allowPrivateNetwork } = resolveAccount(opts));
({ baseUrl, password } = resolveAccount(opts));
} catch {
return { entries: [], resolved: false };
}
const ssrfPolicy = allowPrivateNetwork ? { allowPrivateNetwork: true } : {};
// Try different common API patterns for fetching messages
const possiblePaths = [
@@ -105,7 +103,6 @@ export async function fetchBlueBubblesHistory(
url,
{ method: "GET" },
opts.timeoutMs ?? 10000,
ssrfPolicy,
);
if (!res.ok) {

View File

@@ -78,48 +78,6 @@ describe("normalizeWebhookMessage", () => {
expect(result).not.toBeNull();
expect(result?.senderId).toBe("+15551234567");
});
it("normalizes participant handles from the handles field", () => {
const result = normalizeWebhookMessage({
type: "new-message",
data: {
guid: "msg-handles-1",
text: "hello group",
isGroup: true,
isFromMe: false,
handle: { address: "+15550000000" },
chatGuid: "iMessage;+;chat123456",
handles: [
{ address: "+15551234567", displayName: "Alice" },
{ address: "+15557654321", displayName: "Bob" },
],
},
});
expect(result).not.toBeNull();
expect(result?.participants).toEqual([
{ id: "+15551234567", name: "Alice" },
{ id: "+15557654321", name: "Bob" },
]);
});
it("normalizes participant handles from the participantHandles field", () => {
const result = normalizeWebhookMessage({
type: "new-message",
data: {
guid: "msg-participant-handles-1",
text: "hello group",
isGroup: true,
isFromMe: false,
handle: { address: "+15550000000" },
chatGuid: "iMessage;+;chat123456",
participantHandles: [{ address: "+15551234567" }, "+15557654321"],
},
});
expect(result).not.toBeNull();
expect(result?.participants).toEqual([{ id: "+15551234567" }, { id: "+15557654321" }]);
});
});
describe("normalizeWebhookReaction", () => {

View File

@@ -189,25 +189,6 @@ function readFirstChatRecord(message: Record<string, unknown>): Record<string, u
return asRecord(first);
}
function readParticipantEntries(record: Record<string, unknown> | null): unknown[] | undefined {
if (!record) {
return undefined;
}
const participants = record["participants"];
if (Array.isArray(participants)) {
return participants;
}
const handles = record["handles"];
if (Array.isArray(handles)) {
return handles;
}
const participantHandles = record["participantHandles"];
if (Array.isArray(participantHandles)) {
return participantHandles;
}
return undefined;
}
function extractSenderInfo(message: Record<string, unknown>): {
senderId: string;
senderIdExplicit: boolean;
@@ -284,11 +265,16 @@ function extractChatContext(message: Record<string, unknown>): {
readString(chatFromList, "name") ??
undefined;
const participants =
readParticipantEntries(chat) ??
readParticipantEntries(message) ??
readParticipantEntries(chatFromList) ??
[];
const chatParticipants = chat ? chat["participants"] : undefined;
const messageParticipants = message["participants"];
const chatsParticipants = chatFromList ? chatFromList["participants"] : undefined;
const participants = Array.isArray(chatParticipants)
? chatParticipants
: Array.isArray(messageParticipants)
? messageParticipants
: Array.isArray(chatsParticipants)
? chatsParticipants
: [];
const participantsCount = participants.length;
const groupFromChatGuid = resolveGroupFlagFromChatGuid(chatGuid);
const explicitIsGroup =
@@ -350,14 +336,13 @@ function normalizeParticipantEntry(entry: unknown): BlueBubblesParticipant | nul
return { id: normalizedId, name };
}
export function normalizeParticipantList(raw: unknown): BlueBubblesParticipant[] {
const entries = Array.isArray(raw) ? raw : (readParticipantEntries(asRecord(raw)) ?? []);
if (entries.length === 0) {
function normalizeParticipantList(raw: unknown): BlueBubblesParticipant[] {
if (!Array.isArray(raw) || raw.length === 0) {
return [];
}
const seen = new Set<string>();
const output: BlueBubblesParticipant[] = [];
for (const entry of entries) {
for (const entry of raw) {
const normalized = normalizeParticipantEntry(entry);
if (!normalized?.id) {
continue;

View File

@@ -12,7 +12,6 @@ import {
formatGroupAllowlistEntry,
formatGroupMembers,
formatReplyTag,
normalizeParticipantList,
parseTapbackText,
resolveGroupFlagFromChatGuid,
resolveTapbackContext,
@@ -34,7 +33,6 @@ import type {
BlueBubblesRuntimeEnv,
WebhookTarget,
} from "./monitor-shared.js";
import { enrichBlueBubblesParticipantsWithContactNames } from "./participant-contact-names.js";
import { isBlueBubblesPrivateApiEnabled } from "./probe.js";
import { normalizeBlueBubblesReactionInput, sendBlueBubblesReaction } from "./reactions.js";
import type { OpenClawConfig } from "./runtime-api.js";
@@ -63,7 +61,6 @@ import {
isAllowedBlueBubblesSender,
normalizeBlueBubblesHandle,
} from "./targets.js";
import { blueBubblesFetchWithTimeout, buildBlueBubblesApiUrl } from "./types.js";
const DEFAULT_TEXT_LIMIT = 4000;
const invalidAckReactions = new Set<string>();
@@ -92,138 +89,30 @@ function trimOrUndefined(value?: string | null): string | undefined {
return trimmed ? trimmed : undefined;
}
function shouldPreferLogicalSectionChunking(
ctx: {
BodyForCommands?: string;
CommandBody?: string;
RawBody?: string;
Body?: string;
CommandAuthorized?: boolean;
},
kind: "tool" | "block" | "final",
): boolean {
if (kind === "tool") {
return true;
}
if (ctx.CommandAuthorized !== true) {
return false;
}
const body = (ctx.BodyForCommands ?? ctx.CommandBody ?? ctx.RawBody ?? ctx.Body ?? "").trim();
return body.startsWith("/") || body.startsWith("!");
}
function normalizeSnippet(value: string): string {
return stripMarkdown(value).replace(/\s+/g, " ").trim().toLowerCase();
}
type BlueBubblesChatRecord = Record<string, unknown>;
function blueBubblesPolicy(allowPrivateNetwork: boolean | undefined) {
return allowPrivateNetwork ? { allowPrivateNetwork: true } : undefined;
}
function extractBlueBubblesChatGuid(chat: BlueBubblesChatRecord): string | undefined {
const candidates = [chat.chatGuid, chat.guid, chat.chat_guid];
for (const candidate of candidates) {
if (typeof candidate === "string" && candidate.trim()) {
return candidate.trim();
}
}
return undefined;
}
function extractBlueBubblesChatId(chat: BlueBubblesChatRecord): number | undefined {
const candidates = [chat.chatId, chat.id, chat.chat_id];
for (const candidate of candidates) {
if (typeof candidate === "number" && Number.isFinite(candidate)) {
return candidate;
}
}
return undefined;
}
function extractChatIdentifierFromChatGuid(chatGuid: string): string | undefined {
const parts = chatGuid.split(";");
if (parts.length < 3) {
return undefined;
}
const identifier = parts[2]?.trim();
return identifier || undefined;
}
function extractBlueBubblesChatIdentifier(chat: BlueBubblesChatRecord): string | undefined {
const candidates = [chat.chatIdentifier, chat.chat_identifier, chat.identifier];
for (const candidate of candidates) {
if (typeof candidate === "string" && candidate.trim()) {
return candidate.trim();
}
}
const chatGuid = extractBlueBubblesChatGuid(chat);
return chatGuid ? extractChatIdentifierFromChatGuid(chatGuid) : undefined;
}
async function queryBlueBubblesChats(params: {
baseUrl: string;
password: string;
timeoutMs?: number;
offset: number;
limit: number;
allowPrivateNetwork?: boolean;
}): Promise<BlueBubblesChatRecord[]> {
const url = buildBlueBubblesApiUrl({
baseUrl: params.baseUrl,
path: "/api/v1/chat/query",
password: params.password,
});
const res = await blueBubblesFetchWithTimeout(
url,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
limit: params.limit,
offset: params.offset,
with: ["participants"],
}),
},
params.timeoutMs,
blueBubblesPolicy(params.allowPrivateNetwork),
);
if (!res.ok) {
return [];
}
const payload = (await res.json().catch(() => null)) as Record<string, unknown> | null;
const data = payload && typeof payload.data !== "undefined" ? (payload.data as unknown) : null;
return Array.isArray(data) ? (data as BlueBubblesChatRecord[]) : [];
}
async function fetchBlueBubblesParticipantsForInboundMessage(params: {
baseUrl: string;
password: string;
chatGuid?: string;
chatId?: number;
chatIdentifier?: string;
allowPrivateNetwork?: boolean;
}): Promise<import("./monitor-normalize.js").BlueBubblesParticipant[] | null> {
if (!params.chatGuid && params.chatId == null && !params.chatIdentifier) {
return null;
}
const limit = 500;
for (let offset = 0; offset < 5000; offset += limit) {
const chats = await queryBlueBubblesChats({
baseUrl: params.baseUrl,
password: params.password,
offset,
limit,
allowPrivateNetwork: params.allowPrivateNetwork,
});
if (chats.length === 0) {
return null;
}
for (const chat of chats) {
const chatGuid = extractBlueBubblesChatGuid(chat);
const chatId = extractBlueBubblesChatId(chat);
const chatIdentifier = extractBlueBubblesChatIdentifier(chat);
const matches =
(params.chatGuid && chatGuid === params.chatGuid) ||
(params.chatId != null && chatId === params.chatId) ||
(params.chatIdentifier &&
(chatIdentifier === params.chatIdentifier || chatGuid === params.chatIdentifier));
if (matches) {
return normalizeParticipantList(chat);
}
}
if (chats.length < limit) {
return null;
}
}
return null;
}
function isBlueBubblesSelfChatMessage(
message: NormalizedWebhookMessage,
isGroup: boolean,
@@ -914,47 +803,12 @@ export async function processMessage(
return;
}
const baseUrl = normalizeSecretInputString(account.config.serverUrl);
const password = normalizeSecretInputString(account.config.password);
if (isGroup && !message.participants?.length && baseUrl && password) {
try {
const fetchedParticipants = await fetchBlueBubblesParticipantsForInboundMessage({
baseUrl,
password,
chatGuid: message.chatGuid,
chatId: message.chatId,
chatIdentifier: message.chatIdentifier,
allowPrivateNetwork: account.config.allowPrivateNetwork === true,
});
if (fetchedParticipants?.length) {
message.participants = fetchedParticipants;
}
} catch (err) {
logVerbose(
core,
runtime,
`bluebubbles: participant fallback lookup failed chat=${peerId}: ${String(err)}`,
);
}
}
if (
isGroup &&
account.config.enrichGroupParticipantsFromContacts === true &&
message.participants?.length
) {
// BlueBubbles only gives us participant handles, so enrich phone numbers from local Contacts
// after access, command, and mention gating have already allowed the message through.
message.participants = await enrichBlueBubblesParticipantsWithContactNames(
message.participants,
);
}
// Cache allowed inbound messages so later replies can resolve sender/body without
// surfacing dropped content (allowlist/mention/command gating).
cacheInboundMessage();
const baseUrl = normalizeSecretInputString(account.config.serverUrl);
const password = normalizeSecretInputString(account.config.password);
const maxBytes =
account.config.mediaMaxMb && account.config.mediaMaxMb > 0
? account.config.mediaMaxMb * 1024 * 1024
@@ -1104,7 +958,6 @@ export async function processMessage(
baseUrl,
password,
target: resolveTarget,
allowPrivateNetwork: account.config.allowPrivateNetwork === true,
})) ?? undefined;
}
}
@@ -1506,8 +1359,14 @@ export async function processMessage(
const text = sanitizeReplyDirectiveText(
core.channel.text.convertMarkdownTables(payload.text ?? "", tableMode),
);
const chunks =
chunkMode === "newline"
const useLogicalSectionChunking =
chunkMode === "newline" && shouldPreferLogicalSectionChunking(ctxPayload, info.kind);
const chunks = useLogicalSectionChunking
? resolveTextChunksWithFallback(
text,
core.channel.text.chunkMarkdownTextWithMode(text, textLimit, "newline"),
)
: chunkMode === "newline"
? resolveTextChunksWithFallback(
text,
core.channel.text.chunkTextWithMode(text, textLimit, chunkMode),
@@ -1658,19 +1517,6 @@ export async function processReaction(
const peerId = reaction.isGroup
? (chatGuid ?? chatIdentifier ?? (chatId ? String(chatId) : "group"))
: reaction.senderId;
const requireMention =
reaction.isGroup &&
core.channel.groups.resolveRequireMention({
cfg: config,
channel: "bluebubbles",
groupId: peerId,
accountId: account.accountId,
});
if (requireMention) {
logVerbose(core, runtime, "bluebubbles: skipping group reaction (requireMention=true)");
return;
}
const route = core.channel.routing.resolveAgentRoute({
cfg: config,

View File

@@ -23,10 +23,6 @@ import {
setupWebhookTargetsForTest,
trackWebhookRegistrationForTest,
} from "./monitor.webhook.test-helpers.js";
import {
resetBlueBubblesParticipantContactNameCacheForTest,
setBlueBubblesParticipantContactDepsForTest,
} from "./participant-contact-names.js";
import type { OpenClawConfig, PluginRuntime } from "./runtime-api.js";
// Mock dependencies
@@ -108,7 +104,6 @@ const mockChunkTextWithMode = vi.fn((text: string) => (text ? [text] : []));
const mockChunkMarkdownTextWithMode = vi.fn((text: string) => (text ? [text] : []));
const mockResolveChunkMode = vi.fn(() => "length" as const);
const mockFetchBlueBubblesHistory = vi.mocked(fetchBlueBubblesHistory);
const mockFetch = vi.fn();
function createMockRuntime(): PluginRuntime {
return createBlueBubblesMonitorTestRuntime({
@@ -181,12 +176,6 @@ describe("BlueBubbles webhook monitor", () => {
}
beforeEach(() => {
vi.stubGlobal("fetch", mockFetch);
mockFetch.mockReset();
mockFetch.mockResolvedValue({
ok: true,
json: () => Promise.resolve({ data: [] }),
});
resetBlueBubblesMonitorTestState({
createRuntime: createMockRuntime,
fetchHistoryMock: mockFetchBlueBubblesHistory,
@@ -196,19 +185,13 @@ describe("BlueBubbles webhook monitor", () => {
hasControlCommandMock: mockHasControlCommand,
resolveCommandAuthorizedFromAuthorizersMock: mockResolveCommandAuthorizedFromAuthorizers,
buildMentionRegexesMock: mockBuildMentionRegexes,
extraReset: () => {
resetBlueBubblesSelfChatCache();
resetBlueBubblesParticipantContactNameCacheForTest();
setBlueBubblesParticipantContactDepsForTest();
},
extraReset: resetBlueBubblesSelfChatCache,
});
});
afterEach(() => {
unregister?.();
setBlueBubblesParticipantContactDepsForTest();
vi.useRealTimers();
vi.unstubAllGlobals();
});
describe("DM pairing behavior vs allowFrom", () => {
@@ -506,147 +489,6 @@ describe("BlueBubbles webhook monitor", () => {
expect(callArgs.ctx.GroupSubject).toBe("Family");
expect(callArgs.ctx.GroupMembers).toBe("Alice (+15551234567), Bob (+15557654321)");
});
it("does not enrich group participants when the config flag is disabled", async () => {
const resolvePhoneNames = vi.fn(async () => new Map([["5551234567", "Alice Contact"]]));
setupWebhookTarget({
account: createMockAccount({
enrichGroupParticipantsFromContacts: false,
}),
});
setBlueBubblesParticipantContactDepsForTest({
platform: "darwin",
resolvePhoneNames,
});
const payload = createTimestampedNewMessagePayloadForTest({
text: "hello bert",
isGroup: true,
chatGuid: "iMessage;+;chat123456",
chatName: "Family",
participants: [{ address: "+15551234567" }],
});
await dispatchWebhookPayload(payload);
expect(resolvePhoneNames).not.toHaveBeenCalled();
expect(getFirstDispatchCall().ctx.GroupMembers).toBe("+15551234567");
});
it("enriches unnamed phone participants from local contacts after gating passes", async () => {
const resolvePhoneNames = vi.fn(
async (phoneKeys: string[]) =>
new Map(
phoneKeys.map((phoneKey) => [
phoneKey,
phoneKey === "5551234567" ? "Alice Contact" : "Bob Contact",
]),
),
);
setupWebhookTarget({
account: createMockAccount({
enrichGroupParticipantsFromContacts: true,
}),
});
setBlueBubblesParticipantContactDepsForTest({
platform: "darwin",
resolvePhoneNames,
});
const payload = createTimestampedNewMessagePayloadForTest({
text: "hello bert",
isGroup: true,
chatGuid: "iMessage;+;chat123456",
chatName: "Family",
participants: [{ address: "+15551234567" }, { address: "+15557654321" }],
});
await dispatchWebhookPayload(payload);
expect(resolvePhoneNames).toHaveBeenCalledTimes(1);
const callArgs = getFirstDispatchCall();
expect(callArgs.ctx.GroupMembers).toBe(
"Alice Contact (+15551234567), Bob Contact (+15557654321)",
);
});
it("fetches missing group participants from the BlueBubbles API before contact enrichment", async () => {
const resolvePhoneNames = vi.fn(
async (phoneKeys: string[]) =>
new Map(
phoneKeys.map((phoneKey) => [
phoneKey,
phoneKey === "5551234567" ? "Alice Contact" : "Bob Contact",
]),
),
);
mockFetch.mockResolvedValueOnce({
ok: true,
json: () =>
Promise.resolve({
data: [
{
guid: "iMessage;+;chat123456",
participants: [{ address: "+15551234567" }, { address: "+15557654321" }],
},
],
}),
});
setupWebhookTarget({
account: createMockAccount({
enrichGroupParticipantsFromContacts: true,
}),
});
setBlueBubblesParticipantContactDepsForTest({
platform: "darwin",
resolvePhoneNames,
});
const payload = createTimestampedNewMessagePayloadForTest({
text: "hello bert",
isGroup: true,
chatGuid: "iMessage;+;chat123456",
chatName: "Family",
});
await dispatchWebhookPayload(payload);
expect(mockFetch).toHaveBeenCalledWith(
expect.stringContaining("/api/v1/chat/query"),
expect.objectContaining({ method: "POST" }),
);
expect(resolvePhoneNames).toHaveBeenCalledTimes(1);
expect(getFirstDispatchCall().ctx.GroupMembers).toBe(
"Alice Contact (+15551234567), Bob Contact (+15557654321)",
);
});
it("does not read local contacts before mention gating allows the message", async () => {
const resolvePhoneNames = vi.fn(async () => new Map([["5551234567", "Alice Contact"]]));
setupWebhookTarget({
account: createMockAccount({
enrichGroupParticipantsFromContacts: true,
}),
});
setBlueBubblesParticipantContactDepsForTest({
platform: "darwin",
resolvePhoneNames,
});
mockResolveRequireMention.mockReturnValueOnce(true);
const payload = createTimestampedNewMessagePayloadForTest({
text: "hello group",
isGroup: true,
chatGuid: "iMessage;+;chat123456",
chatName: "Family",
participants: [{ address: "+15551234567" }],
});
await dispatchWebhookPayload(payload);
expect(resolvePhoneNames).not.toHaveBeenCalled();
expect(mockDispatchReplyWithBufferedBlockDispatcher).not.toHaveBeenCalled();
});
});
describe("group sender identity in envelope", () => {
@@ -1303,6 +1145,174 @@ describe("BlueBubbles webhook monitor", () => {
});
});
describe("logical section chunking", () => {
it("keeps slash command replies grouped by blank-line sections", async () => {
const { sendMessageBlueBubbles } = await import("./send.js");
const sendMock = vi.mocked(sendMessageBlueBubbles);
sendMock.mockClear();
mockResolveCommandAuthorizedFromAuthorizers.mockReturnValue(true);
mockChunkTextWithMode.mockImplementation((text: string) =>
text
.split("\n")
.map((chunk) => chunk.trim())
.filter(Boolean),
);
mockChunkMarkdownTextWithMode.mockImplementation((text: string) =>
text
.split(/\n\s*\n/)
.map((chunk) => chunk.trim())
.filter(Boolean),
);
mockDispatchReplyWithBufferedBlockDispatcher.mockImplementationOnce(async (params) => {
params.ctx.CommandAuthorized = true;
params.ctx.CommandBody = "/subagents";
params.ctx.BodyForCommands = "/subagents";
await params.dispatcherOptions.deliver(
{
text: [
"active subagents:",
"1. alpha",
"2. beta",
"",
"recent subagents:",
"1. gamma",
"2. delta",
].join("\n"),
},
{ kind: "final" },
);
return EMPTY_DISPATCH_RESULT;
});
setupWebhookTarget({
account: createMockAccount({ chunkMode: "newline" }),
});
await dispatchWebhookPayload(
createTimestampedNewMessagePayloadForTest({
text: "/subagents",
chatGuid: "iMessage;-;+15551234567",
}),
);
expect(sendMock).toHaveBeenCalledTimes(2);
expect(sendMock.mock.calls.map((call) => call[1])).toEqual([
"active subagents:\n1. alpha\n2. beta",
"recent subagents:\n1. gamma\n2. delta",
]);
});
it("leaves normal assistant replies on the configured newline chunker", async () => {
const { sendMessageBlueBubbles } = await import("./send.js");
const sendMock = vi.mocked(sendMessageBlueBubbles);
sendMock.mockClear();
mockChunkTextWithMode.mockImplementation((text: string) =>
text
.split("\n")
.map((chunk) => chunk.trim())
.filter(Boolean),
);
mockChunkMarkdownTextWithMode.mockImplementation((text: string) =>
text
.split(/\n\s*\n/)
.map((chunk) => chunk.trim())
.filter(Boolean),
);
mockDispatchReplyWithBufferedBlockDispatcher.mockImplementationOnce(async (params) => {
await params.dispatcherOptions.deliver(
{
text: [
"active subagents:",
"1. alpha",
"2. beta",
"",
"recent subagents:",
"1. gamma",
"2. delta",
].join("\n"),
},
{ kind: "final" },
);
return EMPTY_DISPATCH_RESULT;
});
setupWebhookTarget({
account: createMockAccount({ chunkMode: "newline" }),
});
await dispatchWebhookPayload(
createTimestampedNewMessagePayloadForTest({
text: "hello there",
chatGuid: "iMessage;-;+15551234567",
}),
);
expect(sendMock.mock.calls.map((call) => call[1])).toEqual([
"active subagents:",
"1. alpha",
"2. beta",
"recent subagents:",
"1. gamma",
"2. delta",
]);
});
it("keeps tool summaries grouped by blank-line sections", async () => {
const { sendMessageBlueBubbles } = await import("./send.js");
const sendMock = vi.mocked(sendMessageBlueBubbles);
sendMock.mockClear();
mockChunkTextWithMode.mockImplementation((text: string) =>
text
.split("\n")
.map((chunk) => chunk.trim())
.filter(Boolean),
);
mockChunkMarkdownTextWithMode.mockImplementation((text: string) =>
text
.split(/\n\s*\n/)
.map((chunk) => chunk.trim())
.filter(Boolean),
);
mockDispatchReplyWithBufferedBlockDispatcher.mockImplementationOnce(async (params) => {
await params.dispatcherOptions.deliver(
{
text: [
"active subagents:",
"1. alpha",
"2. beta",
"",
"recent subagents:",
"1. gamma",
"2. delta",
].join("\n"),
},
{ kind: "tool" },
);
return EMPTY_DISPATCH_RESULT;
});
setupWebhookTarget({
account: createMockAccount({ chunkMode: "newline" }),
});
await dispatchWebhookPayload(
createTimestampedNewMessagePayloadForTest({
text: "status pls",
chatGuid: "iMessage;-;+15551234567",
}),
);
expect(sendMock).toHaveBeenCalledTimes(2);
expect(sendMock.mock.calls.map((call) => call[1])).toEqual([
"active subagents:\n1. alpha\n2. beta",
"recent subagents:\n1. gamma\n2. delta",
]);
});
});
describe("reaction events", () => {
it("drops DM reactions when dmPolicy=pairing and allowFrom is empty", async () => {
mockEnqueueSystemEvent.mockClear();
@@ -1318,28 +1328,6 @@ describe("BlueBubbles webhook monitor", () => {
expect(mockEnqueueSystemEvent).not.toHaveBeenCalled();
});
it("skips group reactions when requireMention=true", async () => {
mockEnqueueSystemEvent.mockClear();
mockResolveRequireMention.mockReturnValue(true);
setupWebhookTarget({
account: createMockAccount({
groupPolicy: "open",
}),
});
const payload = createTimestampedMessageReactionPayloadForTest({
isGroup: true,
chatGuid: "iMessage;+;chat123456",
associatedMessageType: 2000,
handle: { address: "+15559999999" },
});
await dispatchWebhookPayload(payload);
expect(mockEnqueueSystemEvent).not.toHaveBeenCalled();
});
it("enqueues system event for reaction added", async () => {
mockEnqueueSystemEvent.mockClear();
@@ -1357,31 +1345,6 @@ describe("BlueBubbles webhook monitor", () => {
);
});
it("enqueues group reactions when requireMention=false", async () => {
mockEnqueueSystemEvent.mockClear();
mockResolveRequireMention.mockReturnValue(false);
setupWebhookTarget({
account: createMockAccount({
groupPolicy: "open",
}),
});
const payload = createTimestampedMessageReactionPayloadForTest({
isGroup: true,
chatGuid: "iMessage;+;chat123456",
associatedMessageType: 2000,
handle: { address: "+15559999999" },
});
await dispatchWebhookPayload(payload);
expect(mockEnqueueSystemEvent).toHaveBeenCalledWith(
expect.stringContaining("reacted with ❤️ [[reply_to:"),
expect.any(Object),
);
});
it("enqueues system event for reaction removed", async () => {
mockEnqueueSystemEvent.mockClear();

View File

@@ -16,31 +16,18 @@ import {
} from "./monitor-shared.js";
import { fetchBlueBubblesServerInfo } from "./probe.js";
import {
WEBHOOK_RATE_LIMIT_DEFAULTS,
createFixedWindowRateLimiter,
createWebhookInFlightLimiter,
registerWebhookTargetWithPluginRoute,
readWebhookBodyOrReject,
resolveRequestClientIp,
resolveWebhookTargetWithAuthOrRejectSync,
withResolvedWebhookRequestPipeline,
} from "./runtime-api.js";
import { getBlueBubblesRuntime } from "./runtime.js";
const webhookTargets = new Map<string, WebhookTarget[]>();
const webhookRateLimiter = createFixedWindowRateLimiter({
windowMs: WEBHOOK_RATE_LIMIT_DEFAULTS.windowMs,
maxRequests: WEBHOOK_RATE_LIMIT_DEFAULTS.maxRequests,
maxTrackedKeys: WEBHOOK_RATE_LIMIT_DEFAULTS.maxTrackedKeys,
});
const webhookInFlightLimiter = createWebhookInFlightLimiter();
const debounceRegistry = createBlueBubblesDebounceRegistry({ processMessage });
export function clearBlueBubblesWebhookSecurityStateForTest(): void {
webhookRateLimiter.clear();
webhookInFlightLimiter.clear();
}
export function registerBlueBubblesWebhookTarget(target: WebhookTarget): () => void {
const registered = registerWebhookTargetWithPluginRoute({
targetsByPath: webhookTargets,
@@ -130,62 +117,18 @@ function safeEqualSecret(aRaw: string, bRaw: string): boolean {
return timingSafeEqual(bufA, bufB);
}
function collectTrustedProxies(targets: readonly WebhookTarget[]): string[] {
const proxies = new Set<string>();
for (const target of targets) {
for (const proxy of target.config.gateway?.trustedProxies ?? []) {
const normalized = proxy.trim();
if (normalized) {
proxies.add(normalized);
}
}
}
return [...proxies];
}
function resolveWebhookAllowRealIpFallback(targets: readonly WebhookTarget[]): boolean {
return targets.some((target) => target.config.gateway?.allowRealIpFallback === true);
}
function resolveWebhookClientIp(
req: IncomingMessage,
trustedProxies: readonly string[],
allowRealIpFallback: boolean,
): string {
if (!req.headers["x-forwarded-for"] && !(allowRealIpFallback && req.headers["x-real-ip"])) {
return req.socket.remoteAddress ?? "unknown";
}
// Mirror gateway client-IP trust rules so limiter buckets follow configured proxy hops.
return (
resolveRequestClientIp(req, [...trustedProxies], allowRealIpFallback) ??
req.socket.remoteAddress ??
"unknown"
);
}
export async function handleBlueBubblesWebhookRequest(
req: IncomingMessage,
res: ServerResponse,
): Promise<boolean> {
const requestUrl = new URL(req.url ?? "/", "http://localhost");
const normalizedPath = normalizeWebhookPath(requestUrl.pathname);
const pathTargets = webhookTargets.get(normalizedPath) ?? [];
const trustedProxies = collectTrustedProxies(pathTargets);
const allowRealIpFallback = resolveWebhookAllowRealIpFallback(pathTargets);
const clientIp = resolveWebhookClientIp(req, trustedProxies, allowRealIpFallback);
const rateLimitKey = `${normalizedPath}:${clientIp}`;
return await withResolvedWebhookRequestPipeline({
req,
res,
targetsByPath: webhookTargets,
allowMethods: ["POST"],
rateLimiter: webhookRateLimiter,
rateLimitKey,
inFlightLimiter: webhookInFlightLimiter,
inFlightKey: `${normalizedPath}:${clientIp}`,
handle: async ({ path, targets }) => {
const url = requestUrl;
const url = new URL(req.url ?? "/", "http://localhost");
const guidParam = url.searchParams.get("guid") ?? url.searchParams.get("password");
const headerToken =
req.headers["x-guid"] ??
@@ -332,7 +275,6 @@ export async function monitorBlueBubblesProvider(
password: account.config.password,
accountId: account.accountId,
timeoutMs: 5000,
allowPrivateNetwork: account.config.allowPrivateNetwork === true,
}).catch(() => null);
if (serverInfo?.os_version) {
runtime.log?.(`[${account.accountId}] BlueBubbles server macOS ${serverInfo.os_version}`);

View File

@@ -107,7 +107,6 @@ const mockChunkTextWithMode = vi.fn((text: string) => (text ? [text] : []));
const mockChunkMarkdownTextWithMode = vi.fn((text: string) => (text ? [text] : []));
const mockResolveChunkMode = vi.fn(() => "length" as const);
const mockFetchBlueBubblesHistory = vi.mocked(fetchBlueBubblesHistory);
const mockFetch = vi.fn();
const TEST_WEBHOOK_PASSWORD = "secret-token";
function createMockRuntime(): PluginRuntime {
@@ -143,12 +142,6 @@ describe("BlueBubbles webhook monitor", () => {
let unregister: () => void;
beforeEach(() => {
vi.stubGlobal("fetch", mockFetch);
mockFetch.mockReset();
mockFetch.mockResolvedValue({
ok: true,
json: () => Promise.resolve({ data: [] }),
});
resetBlueBubblesMonitorTestState({
createRuntime: createMockRuntime,
fetchHistoryMock: mockFetchBlueBubblesHistory,
@@ -163,7 +156,6 @@ describe("BlueBubbles webhook monitor", () => {
afterEach(() => {
unregister?.();
vi.unstubAllGlobals();
});
function setupWebhookTarget(params?: {
@@ -406,163 +398,6 @@ describe("BlueBubbles webhook monitor", () => {
);
});
it("rate limits repeated invalid password guesses from the same client", async () => {
setupWebhookTarget({
account: createMockAccount({
password: "99999999",
}),
});
let saw429 = false;
// Default webhook fixed-window budget is 120 requests/minute, so loop past it.
for (let i = 0; i < 130; i += 1) {
const candidate = String(i).padStart(8, "0");
const { res } = await dispatchWebhookPayloadForTest(
createPasswordQueryRequestParamsForTest({
password: candidate,
body: createTimestampedNewMessagePayloadForTest({
guid: `msg-${i}`,
text: `hello ${i}`,
}),
remoteAddress: "192.168.1.100",
}),
);
if (res.statusCode === 429) {
saw429 = true;
break;
}
expect(res.statusCode).toBe(401);
}
expect(saw429).toBe(true);
expect(mockDispatchReplyWithBufferedBlockDispatcher).not.toHaveBeenCalled();
});
it("keeps forwarded clients behind configured trusted proxies in separate auth buckets", async () => {
setupWebhookTarget({
account: createMockAccount({
password: "99999999",
}),
config: {
gateway: {
trustedProxies: ["10.0.0.0/8"],
},
} as OpenClawConfig,
});
let saw429 = false;
for (let i = 0; i < 130; i += 1) {
const candidate = String(i).padStart(8, "0");
const { res } = await dispatchWebhookPayloadForTest(
createPasswordQueryRequestParamsForTest({
password: candidate,
body: createTimestampedNewMessagePayloadForTest({
guid: `proxy-msg-${i}`,
text: `hello proxy ${i}`,
}),
remoteAddress: "10.0.0.5",
overrides: {
headers: {
host: "localhost",
"x-forwarded-for": "203.0.113.10",
},
},
}),
);
if (res.statusCode === 429) {
saw429 = true;
break;
}
expect(res.statusCode).toBe(401);
}
expect(saw429).toBe(true);
await expectWebhookRequestStatusForTest(
createPasswordQueryRequestParamsForTest({
password: "wrong-pass",
body: createTimestampedNewMessagePayloadForTest({
guid: "proxy-msg-other-client",
text: "hello other proxy client",
}),
remoteAddress: "10.0.0.5",
overrides: {
headers: {
host: "localhost",
"x-forwarded-for": "203.0.113.11",
},
},
}),
401,
);
});
it("keeps real-ip fallback clients behind trusted proxies in separate auth buckets", async () => {
setupWebhookTarget({
account: createMockAccount({
password: "99999999",
}),
config: {
gateway: {
trustedProxies: ["10.0.0.0/8"],
allowRealIpFallback: true,
},
} as OpenClawConfig,
});
let saw429 = false;
for (let i = 0; i < 130; i += 1) {
const candidate = String(i).padStart(8, "0");
const { res } = await dispatchWebhookPayloadForTest(
createPasswordQueryRequestParamsForTest({
password: candidate,
body: createTimestampedNewMessagePayloadForTest({
guid: `real-ip-msg-${i}`,
text: `hello real ip ${i}`,
}),
remoteAddress: "10.0.0.5",
overrides: {
headers: {
host: "localhost",
"x-real-ip": "203.0.113.10",
},
},
}),
);
if (res.statusCode === 429) {
saw429 = true;
break;
}
expect(res.statusCode).toBe(401);
}
expect(saw429).toBe(true);
await expectWebhookRequestStatusForTest(
createPasswordQueryRequestParamsForTest({
password: "wrong-pass",
body: createTimestampedNewMessagePayloadForTest({
guid: "real-ip-msg-other-client",
text: "hello other real ip client",
}),
remoteAddress: "10.0.0.5",
overrides: {
headers: {
host: "localhost",
"x-real-ip": "203.0.113.11",
},
},
}),
401,
);
});
it("rejects ambiguous routing when multiple targets match the same password", async () => {
const targetA = createProtectedWebhookTarget();
const targetB = createProtectedWebhookTarget();

View File

@@ -1,4 +1,3 @@
import type { SsrFPolicy } from "openclaw/plugin-sdk/infra-runtime";
import { blueBubblesFetchWithTimeout } from "./types.js";
export function concatUint8Arrays(parts: Uint8Array[]): Uint8Array {
@@ -17,7 +16,6 @@ export async function postMultipartFormData(params: {
boundary: string;
parts: Uint8Array[];
timeoutMs: number;
ssrfPolicy?: SsrFPolicy;
}): Promise<Response> {
const body = Buffer.from(concatUint8Arrays(params.parts));
return await blueBubblesFetchWithTimeout(
@@ -30,7 +28,6 @@ export async function postMultipartFormData(params: {
body,
},
params.timeoutMs,
params.ssrfPolicy,
);
}

View File

@@ -1,193 +0,0 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import {
enrichBlueBubblesParticipantsWithContactNames,
listBlueBubblesContactsDatabasesForTest,
queryBlueBubblesContactsDatabaseForTest,
resetBlueBubblesParticipantContactNameCacheForTest,
resolveBlueBubblesParticipantContactNamesFromMacOsContactsForTest,
} from "./participant-contact-names.js";
describe("enrichBlueBubblesParticipantsWithContactNames", () => {
beforeEach(() => {
resetBlueBubblesParticipantContactNameCacheForTest();
});
it("enriches unnamed phone participants and reuses cached names across formats", async () => {
const resolver = vi.fn(
async (phoneKeys: string[]) =>
new Map(
phoneKeys.map((phoneKey) => [
phoneKey,
phoneKey === "5551234567" ? "Alice Example" : "Bob Example",
]),
),
);
const first = await enrichBlueBubblesParticipantsWithContactNames(
[{ id: "+1 (555) 123-4567" }, { id: "+15557654321" }],
{
platform: "darwin",
now: () => 1_000,
resolvePhoneNames: resolver,
},
);
expect(first).toEqual([
{ id: "+1 (555) 123-4567", name: "Alice Example" },
{ id: "+15557654321", name: "Bob Example" },
]);
expect(resolver).toHaveBeenCalledTimes(1);
expect(resolver).toHaveBeenCalledWith(["5551234567", "5557654321"]);
const secondResolver = vi.fn(async () => new Map<string, string>());
const second = await enrichBlueBubblesParticipantsWithContactNames([{ id: "+15551234567" }], {
platform: "darwin",
now: () => 2_000,
resolvePhoneNames: secondResolver,
});
expect(second).toEqual([{ id: "+15551234567", name: "Alice Example" }]);
expect(secondResolver).not.toHaveBeenCalled();
});
it("retries negative cache entries after the short negative ttl expires", async () => {
const firstResolver = vi.fn(async () => new Map<string, string>());
const secondResolver = vi.fn(async () => new Map([["5551234567", "Alice Example"]]));
const first = await enrichBlueBubblesParticipantsWithContactNames([{ id: "+15551234567" }], {
platform: "darwin",
now: () => 1_000,
resolvePhoneNames: firstResolver,
});
const second = await enrichBlueBubblesParticipantsWithContactNames([{ id: "+15551234567" }], {
platform: "darwin",
now: () => 1_500,
resolvePhoneNames: secondResolver,
});
const third = await enrichBlueBubblesParticipantsWithContactNames([{ id: "+15551234567" }], {
platform: "darwin",
now: () => 1_000 + 6 * 60 * 1000,
resolvePhoneNames: secondResolver,
});
expect(first).toEqual([{ id: "+15551234567" }]);
expect(second).toEqual([{ id: "+15551234567" }]);
expect(third).toEqual([{ id: "+15551234567", name: "Alice Example" }]);
expect(firstResolver).toHaveBeenCalledTimes(1);
expect(secondResolver).toHaveBeenCalledTimes(1);
});
it("skips email addresses and keeps existing participant names", async () => {
const resolver = vi.fn(async () => new Map<string, string>());
const participants = await enrichBlueBubblesParticipantsWithContactNames(
[{ id: "alice@example.com" }, { id: "+15551234567", name: "Alice Existing" }],
{
platform: "darwin",
now: () => 1_000,
resolvePhoneNames: resolver,
},
);
expect(participants).toEqual([
{ id: "alice@example.com" },
{ id: "+15551234567", name: "Alice Existing" },
]);
expect(resolver).not.toHaveBeenCalled();
});
it("gracefully returns original participants when lookup fails", async () => {
const participants = [{ id: "+15551234567" }, { id: "+15557654321" }];
await expect(
enrichBlueBubblesParticipantsWithContactNames(participants, {
platform: "darwin",
now: () => 1_000,
resolvePhoneNames: vi.fn(async () => {
throw new Error("contacts unavailable");
}),
}),
).resolves.toBe(participants);
});
it("lists contacts databases from the current home directory", async () => {
const readdir = vi.fn(async () => ["source-a", "source-b"]);
const access = vi.fn(async (path: string) => {
if (!path.endsWith("source-a/AddressBook-v22.abcddb")) {
throw new Error("missing");
}
});
const databases = await listBlueBubblesContactsDatabasesForTest({
homeDir: "/Users/tester",
readdir,
access,
});
expect(readdir).toHaveBeenCalledWith(
"/Users/tester/Library/Application Support/AddressBook/Sources",
);
expect(databases).toEqual([
"/Users/tester/Library/Application Support/AddressBook/Sources/source-a/AddressBook-v22.abcddb",
]);
});
it("queries only the requested phone keys in sqlite", async () => {
const execFileAsync = vi.fn(async (_file: string, _args: string[], _options: unknown) => ({
stdout: "5551234567\tAlice Example\n5557654321\tBob Example\n",
stderr: "",
}));
const rows = await queryBlueBubblesContactsDatabaseForTest(
"/tmp/AddressBook-v22.abcddb",
["5551234567", "5557654321"],
{ execFileAsync },
);
expect(rows).toEqual([
{ phoneKey: "5551234567", name: "Alice Example" },
{ phoneKey: "5557654321", name: "Bob Example" },
]);
expect(execFileAsync).toHaveBeenCalledTimes(1);
const sql = execFileAsync.mock.calls[0]?.[1]?.[3];
expect(sql).toContain("WHERE digits IN ('5551234567', '5557654321')");
});
it("resolves names through the macOS contacts path across multiple databases", async () => {
const readdir = vi.fn(async () => ["source-a", "source-b"]);
const access = vi.fn(async () => undefined);
const execFileAsync = vi
.fn(async (_file: string, _args: string[], _options: unknown) => ({
stdout: "",
stderr: "",
}))
.mockResolvedValueOnce({ stdout: "5551234567\tAlice Example\n", stderr: "" })
.mockResolvedValueOnce({ stdout: "5557654321\tBob Example\n", stderr: "" });
const resolved = await resolveBlueBubblesParticipantContactNamesFromMacOsContactsForTest(
["5551234567", "5557654321"],
{
homeDir: "/Users/tester",
readdir,
access,
execFileAsync,
},
);
expect([...resolved.entries()]).toEqual([
["5551234567", "Alice Example"],
["5557654321", "Bob Example"],
]);
expect(execFileAsync).toHaveBeenCalledTimes(2);
});
it("skips contact lookup on non macOS hosts", async () => {
const participants = [{ id: "+15551234567" }];
const result = await enrichBlueBubblesParticipantsWithContactNames(participants, {
platform: "linux",
});
expect(result).toBe(participants);
});
});

View File

@@ -1,377 +0,0 @@
import { execFile, type ExecFileOptionsWithStringEncoding } from "node:child_process";
import { access, readdir } from "node:fs/promises";
import { join } from "node:path";
import { promisify } from "node:util";
import type { BlueBubblesParticipant } from "./monitor-normalize.js";
const execFileAsync = promisify(execFile) as ExecFileRunner;
const CONTACT_NAME_CACHE_TTL_MS = 60 * 60 * 1000;
const NEGATIVE_CONTACT_NAME_CACHE_TTL_MS = 5 * 60 * 1000;
const MAX_PARTICIPANT_CONTACT_NAME_CACHE_ENTRIES = 2048;
const SQLITE_MAX_BUFFER = 8 * 1024 * 1024;
const SQLITE_PHONE_DIGITS_SQL =
"REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(p.ZFULLNUMBER, ''), ' ', ''), '(', ''), ')', ''), '-', ''), '+', ''), '.', ''), '\n', ''), '\r', '')";
type ContactNameCacheEntry = {
name?: string;
expiresAt: number;
};
type ResolvePhoneNamesFn = (phoneKeys: string[]) => Promise<Map<string, string>>;
type ExecFileRunner = (
file: string,
args: string[],
options: ExecFileOptionsWithStringEncoding,
) => Promise<{ stdout: string; stderr: string }>;
type ReadDirRunner = (path: string) => Promise<string[]>;
type AccessRunner = (path: string) => Promise<unknown>;
type ParticipantContactNameDeps = {
platform?: NodeJS.Platform;
now?: () => number;
resolvePhoneNames?: ResolvePhoneNamesFn;
homeDir?: string;
readdir?: ReadDirRunner;
access?: AccessRunner;
execFileAsync?: ExecFileRunner;
};
type ResolvedParticipantContactNameDeps = {
platform: NodeJS.Platform;
now: () => number;
resolvePhoneNames?: ResolvePhoneNamesFn;
homeDir?: string;
readdir: ReadDirRunner;
access: AccessRunner;
execFileAsync: ExecFileRunner;
};
const participantContactNameCache = new Map<string, ContactNameCacheEntry>();
let participantContactNameDepsForTest: ParticipantContactNameDeps | undefined;
function normalizePhoneLookupKey(value: string): string | null {
const digits = value.replace(/\D/g, "");
if (!digits) {
return null;
}
const normalized = digits.length === 11 && digits.startsWith("1") ? digits.slice(1) : digits;
return normalized.length >= 7 ? normalized : null;
}
function uniqueNormalizedPhoneLookupKeys(phoneKeys: string[]): string[] {
const unique = new Set<string>();
for (const phoneKey of phoneKeys) {
const normalized = normalizePhoneLookupKey(phoneKey);
if (normalized) {
unique.add(normalized);
}
}
return [...unique];
}
function resolveParticipantPhoneLookupKey(participant: BlueBubblesParticipant): string | null {
if (participant.id.includes("@")) {
return null;
}
return normalizePhoneLookupKey(participant.id);
}
function trimParticipantContactNameCache(now: number): void {
for (const [phoneKey, entry] of participantContactNameCache) {
if (entry.expiresAt <= now) {
participantContactNameCache.delete(phoneKey);
}
}
while (participantContactNameCache.size > MAX_PARTICIPANT_CONTACT_NAME_CACHE_ENTRIES) {
const oldestPhoneKey = participantContactNameCache.keys().next().value;
if (!oldestPhoneKey) {
return;
}
participantContactNameCache.delete(oldestPhoneKey);
}
}
function readFreshCacheEntry(phoneKey: string, now: number): ContactNameCacheEntry | null {
const cached = participantContactNameCache.get(phoneKey);
if (!cached) {
return null;
}
if (cached.expiresAt <= now) {
participantContactNameCache.delete(phoneKey);
return null;
}
participantContactNameCache.delete(phoneKey);
participantContactNameCache.set(phoneKey, cached);
return cached;
}
function writeCacheEntry(phoneKey: string, name: string | undefined, now: number): void {
participantContactNameCache.delete(phoneKey);
participantContactNameCache.set(phoneKey, {
name,
expiresAt: now + (name ? CONTACT_NAME_CACHE_TTL_MS : NEGATIVE_CONTACT_NAME_CACHE_TTL_MS),
});
trimParticipantContactNameCache(now);
}
function buildAddressBookSourcesDir(homeDir?: string): string | null {
const trimmedHomeDir = homeDir?.trim();
if (!trimmedHomeDir) {
return null;
}
return join(trimmedHomeDir, "Library", "Application Support", "AddressBook", "Sources");
}
async function fileExists(
path: string,
deps: ResolvedParticipantContactNameDeps,
): Promise<boolean> {
try {
await deps.access(path);
return true;
} catch {
return false;
}
}
async function listContactsDatabases(deps: ResolvedParticipantContactNameDeps): Promise<string[]> {
const sourcesDir = buildAddressBookSourcesDir(deps.homeDir);
if (!sourcesDir) {
return [];
}
let entries: string[] = [];
try {
entries = await deps.readdir(sourcesDir);
} catch {
return [];
}
const databases: string[] = [];
for (const entry of entries) {
const dbPath = join(sourcesDir, entry, "AddressBook-v22.abcddb");
if (await fileExists(dbPath, deps)) {
databases.push(dbPath);
}
}
return databases;
}
function buildSqlitePhoneKeyList(phoneKeys: string[]): string {
return uniqueNormalizedPhoneLookupKeys(phoneKeys)
.map((phoneKey) => `'${phoneKey}'`)
.join(", ");
}
async function queryContactsDatabase(
dbPath: string,
phoneKeys: string[],
deps: ResolvedParticipantContactNameDeps,
): Promise<Array<{ phoneKey: string; name: string }>> {
const sqlitePhoneKeyList = buildSqlitePhoneKeyList(phoneKeys);
if (!sqlitePhoneKeyList) {
return [];
}
const sql = `
SELECT digits, name
FROM (
SELECT
${SQLITE_PHONE_DIGITS_SQL} AS digits,
TRIM(
CASE
WHEN TRIM(COALESCE(r.ZFIRSTNAME, '') || ' ' || COALESCE(r.ZLASTNAME, '')) != ''
THEN TRIM(COALESCE(r.ZFIRSTNAME, '') || ' ' || COALESCE(r.ZLASTNAME, ''))
ELSE COALESCE(r.ZORGANIZATION, '')
END
) AS name
FROM ZABCDRECORD r
JOIN ZABCDPHONENUMBER p ON p.ZOWNER = r.Z_PK
WHERE p.ZFULLNUMBER IS NOT NULL
)
WHERE digits IN (${sqlitePhoneKeyList})
AND name != '';
`;
const options: ExecFileOptionsWithStringEncoding = {
encoding: "utf8",
maxBuffer: SQLITE_MAX_BUFFER,
};
const { stdout } = await deps.execFileAsync(
"sqlite3",
["-separator", "\t", dbPath, sql],
options,
);
const rows: Array<{ phoneKey: string; name: string }> = [];
for (const line of stdout.split(/\r?\n/)) {
const trimmed = line.trim();
if (!trimmed) {
continue;
}
const [digitsRaw, ...nameParts] = trimmed.split("\t");
const phoneKey = normalizePhoneLookupKey(digitsRaw ?? "");
const name = nameParts.join("\t").trim();
if (!phoneKey || !name) {
continue;
}
rows.push({ phoneKey, name });
}
return rows;
}
async function resolvePhoneNamesFromMacOsContacts(
phoneKeys: string[],
deps: ResolvedParticipantContactNameDeps,
): Promise<Map<string, string>> {
const normalizedPhoneKeys = uniqueNormalizedPhoneLookupKeys(phoneKeys);
if (normalizedPhoneKeys.length === 0) {
return new Map();
}
const databases = await listContactsDatabases(deps);
if (databases.length === 0) {
return new Map();
}
const unresolved = new Set(normalizedPhoneKeys);
const resolved = new Map<string, string>();
for (const dbPath of databases) {
let rows: Array<{ phoneKey: string; name: string }> = [];
try {
rows = await queryContactsDatabase(dbPath, [...unresolved], deps);
} catch {
continue;
}
for (const row of rows) {
if (!unresolved.has(row.phoneKey) || resolved.has(row.phoneKey)) {
continue;
}
resolved.set(row.phoneKey, row.name);
unresolved.delete(row.phoneKey);
if (unresolved.size === 0) {
return resolved;
}
}
}
return resolved;
}
function resolveLookupDeps(deps?: ParticipantContactNameDeps): ResolvedParticipantContactNameDeps {
const merged = {
...participantContactNameDepsForTest,
...deps,
};
return {
platform: merged.platform ?? process.platform,
now: merged.now ?? (() => Date.now()),
resolvePhoneNames: merged.resolvePhoneNames,
homeDir: merged.homeDir ?? process.env.HOME,
readdir: merged.readdir ?? readdir,
access: merged.access ?? access,
execFileAsync: merged.execFileAsync ?? execFileAsync,
};
}
export async function enrichBlueBubblesParticipantsWithContactNames(
participants: BlueBubblesParticipant[] | undefined,
deps?: ParticipantContactNameDeps,
): Promise<BlueBubblesParticipant[]> {
if (!Array.isArray(participants) || participants.length === 0) {
return [];
}
const resolvedDeps = resolveLookupDeps(deps);
const lookup =
resolvedDeps.resolvePhoneNames ??
((phoneKeys: string[]) => resolvePhoneNamesFromMacOsContacts(phoneKeys, resolvedDeps));
const shouldAttemptLookup =
Boolean(resolvedDeps.resolvePhoneNames) || resolvedDeps.platform === "darwin";
if (!shouldAttemptLookup) {
return participants;
}
const nowMs = resolvedDeps.now();
trimParticipantContactNameCache(nowMs);
const pendingPhoneKeys = new Set<string>();
const cachedNames = new Map<string, string>();
for (const participant of participants) {
if (participant.name?.trim()) {
continue;
}
const phoneKey = resolveParticipantPhoneLookupKey(participant);
if (!phoneKey) {
continue;
}
const cached = readFreshCacheEntry(phoneKey, nowMs);
if (cached?.name) {
cachedNames.set(phoneKey, cached.name);
continue;
}
if (!cached) {
pendingPhoneKeys.add(phoneKey);
}
}
if (pendingPhoneKeys.size > 0) {
try {
const resolved = await lookup([...pendingPhoneKeys]);
for (const phoneKey of pendingPhoneKeys) {
const name = resolved.get(phoneKey)?.trim() || undefined;
writeCacheEntry(phoneKey, name, nowMs);
if (name) {
cachedNames.set(phoneKey, name);
}
}
} catch {
return participants;
}
}
let didChange = false;
const enriched = participants.map((participant) => {
if (participant.name?.trim()) {
return participant;
}
const phoneKey = resolveParticipantPhoneLookupKey(participant);
if (!phoneKey) {
return participant;
}
const name = cachedNames.get(phoneKey)?.trim();
if (!name) {
return participant;
}
didChange = true;
return { ...participant, name };
});
return didChange ? enriched : participants;
}
export async function listBlueBubblesContactsDatabasesForTest(
deps?: ParticipantContactNameDeps,
): Promise<string[]> {
return listContactsDatabases(resolveLookupDeps(deps));
}
export async function queryBlueBubblesContactsDatabaseForTest(
dbPath: string,
phoneKeys: string[],
deps?: ParticipantContactNameDeps,
): Promise<Array<{ phoneKey: string; name: string }>> {
return queryContactsDatabase(dbPath, phoneKeys, resolveLookupDeps(deps));
}
export async function resolveBlueBubblesParticipantContactNamesFromMacOsContactsForTest(
phoneKeys: string[],
deps?: ParticipantContactNameDeps,
): Promise<Map<string, string>> {
return resolvePhoneNamesFromMacOsContacts(phoneKeys, resolveLookupDeps(deps));
}
export function resetBlueBubblesParticipantContactNameCacheForTest(): void {
participantContactNameCache.clear();
}
export function setBlueBubblesParticipantContactDepsForTest(
deps?: ParticipantContactNameDeps,
): void {
participantContactNameDepsForTest = deps;
participantContactNameCache.clear();
}

View File

@@ -35,7 +35,6 @@ export async function fetchBlueBubblesServerInfo(params: {
password?: string | null;
accountId?: string;
timeoutMs?: number;
allowPrivateNetwork?: boolean;
}): Promise<BlueBubblesServerInfo | null> {
const baseUrl = normalizeSecretInputString(params.baseUrl);
const password = normalizeSecretInputString(params.password);
@@ -49,15 +48,9 @@ export async function fetchBlueBubblesServerInfo(params: {
return cached.info;
}
const ssrfPolicy = params.allowPrivateNetwork ? { allowPrivateNetwork: true } : {};
const url = buildBlueBubblesApiUrl({ baseUrl, path: "/api/v1/server/info", password });
try {
const res = await blueBubblesFetchWithTimeout(
url,
{ method: "GET" },
params.timeoutMs ?? 5000,
ssrfPolicy,
);
const res = await blueBubblesFetchWithTimeout(url, { method: "GET" }, params.timeoutMs ?? 5000);
if (!res.ok) {
return null;
}
@@ -145,7 +138,6 @@ export async function probeBlueBubbles(params: {
baseUrl?: string | null;
password?: string | null;
timeoutMs?: number;
allowPrivateNetwork?: boolean;
}): Promise<BlueBubblesProbe> {
const baseUrl = normalizeSecretInputString(params.baseUrl);
const password = normalizeSecretInputString(params.password);
@@ -155,15 +147,9 @@ export async function probeBlueBubbles(params: {
if (!password) {
return { ok: false, error: "password not configured" };
}
const probeSsrfPolicy = params.allowPrivateNetwork ? { allowPrivateNetwork: true } : {};
const url = buildBlueBubblesApiUrl({ baseUrl, path: "/api/v1/ping", password });
try {
const res = await blueBubblesFetchWithTimeout(
url,
{ method: "GET" },
params.timeoutMs,
probeSsrfPolicy,
);
const res = await blueBubblesFetchWithTimeout(url, { method: "GET" }, params.timeoutMs);
if (!res.ok) {
return { ok: false, status: res.status, error: `HTTP ${res.status}` };
}

View File

@@ -1,6 +1,5 @@
import { describe, expect, it, vi } from "vitest";
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
import { sendBlueBubblesReaction } from "./reactions.js";
import { installBlueBubblesFetchTestHooks } from "./test-harness.js";
vi.mock("./accounts.js", async () => {
const { createBlueBubblesAccountsMockModule } = await import("./test-harness.js");
@@ -8,16 +7,17 @@ vi.mock("./accounts.js", async () => {
});
const mockFetch = vi.fn();
const noopPrivateApiStatusMock = {
mockReturnValue: () => {},
};
installBlueBubblesFetchTestHooks({
mockFetch,
privateApiStatusMock: noopPrivateApiStatusMock,
});
describe("reactions", () => {
beforeEach(() => {
vi.stubGlobal("fetch", mockFetch);
mockFetch.mockReset();
});
afterEach(() => {
vi.unstubAllGlobals();
});
describe("sendBlueBubblesReaction", () => {
async function expectRemovedReaction(emoji: string, expectedReaction = "-love") {
mockFetch.mockResolvedValueOnce({

View File

@@ -149,7 +149,7 @@ export async function sendBlueBubblesReaction(params: {
throw new Error("BlueBubbles reaction requires messageGuid.");
}
const reaction = normalizeBlueBubblesReactionInput(params.emoji, params.remove);
const { baseUrl, password, accountId, allowPrivateNetwork } = resolveAccount(params.opts ?? {});
const { baseUrl, password, accountId } = resolveAccount(params.opts ?? {});
if (getCachedBlueBubblesPrivateApiStatus(accountId) === false) {
throw new Error(
"BlueBubbles reaction requires Private API, but it is disabled on the BlueBubbles server.",
@@ -166,7 +166,6 @@ export async function sendBlueBubblesReaction(params: {
reaction,
partIndex: typeof params.partIndex === "number" ? params.partIndex : 0,
};
const ssrfPolicy = allowPrivateNetwork ? { allowPrivateNetwork: true } : {};
const res = await blueBubblesFetchWithTimeout(
url,
{
@@ -175,7 +174,6 @@ export async function sendBlueBubblesReaction(params: {
body: JSON.stringify(payload),
},
params.opts?.timeoutMs,
ssrfPolicy,
);
if (!res.ok) {
const errorText = await res.text();

View File

@@ -6,15 +6,13 @@ import { clearBlueBubblesRuntime, setBlueBubblesRuntime } from "./runtime.js";
import { sendMessageBlueBubbles, resolveChatGuidForTarget, createChatForHandle } from "./send.js";
import {
BLUE_BUBBLES_PRIVATE_API_STATUS,
createBlueBubblesFetchGuardPassthroughInstaller,
installBlueBubblesFetchTestHooks,
mockBlueBubblesPrivateApiStatusOnce,
} from "./test-harness.js";
import { _setFetchGuardForTesting, type BlueBubblesSendTarget } from "./types.js";
import type { BlueBubblesSendTarget } from "./types.js";
const mockFetch = vi.fn();
const privateApiStatusMock = vi.mocked(getCachedBlueBubblesPrivateApiStatus);
const setFetchGuardPassthrough = createBlueBubblesFetchGuardPassthroughInstaller();
installBlueBubblesFetchTestHooks({
mockFetch,
@@ -63,12 +61,6 @@ function mockNewChatSendResponse(guid: string) {
});
}
function installSsrFPolicyCapture(policies: unknown[]) {
setFetchGuardPassthrough((policy) => {
policies.push(policy);
});
}
describe("send", () => {
describe("resolveChatGuidForTarget", () => {
const resolveHandleTargetGuid = async (data: Array<Record<string, unknown>>) => {
@@ -456,44 +448,6 @@ describe("send", () => {
expect(body.method).toBeUndefined();
});
it("auto-enables private-network fetches for loopback serverUrl when allowPrivateNetwork is not set", async () => {
const policies: unknown[] = [];
installSsrFPolicyCapture(policies);
mockResolvedHandleTarget();
mockSendResponse({ data: { guid: "msg-loopback" } });
try {
const result = await sendMessageBlueBubbles("+15551234567", "Hello world!", {
serverUrl: "http://localhost:1234",
password: "test",
});
expect(result.messageId).toBe("msg-loopback");
expect(policies).toEqual([{ allowPrivateNetwork: true }, { allowPrivateNetwork: true }]);
} finally {
_setFetchGuardForTesting(null);
}
});
it("auto-enables private-network fetches for private IP serverUrl when allowPrivateNetwork is not set", async () => {
const policies: unknown[] = [];
installSsrFPolicyCapture(policies);
mockResolvedHandleTarget();
mockSendResponse({ data: { guid: "msg-private-ip" } });
try {
const result = await sendMessageBlueBubbles("+15551234567", "Hello world!", {
serverUrl: "http://192.168.1.5:1234",
password: "test",
});
expect(result.messageId).toBe("msg-private-ip");
expect(policies).toEqual([{ allowPrivateNetwork: true }, { allowPrivateNetwork: true }]);
} finally {
_setFetchGuardForTesting(null);
}
});
it("strips markdown formatting from outbound messages", async () => {
mockResolvedHandleTarget();
mockSendResponse({ data: { guid: "msg-uuid-stripped" } });

View File

@@ -1,5 +1,5 @@
import crypto from "node:crypto";
import { resolveBlueBubblesServerAccount } from "./account-resolve.js";
import { resolveBlueBubblesAccount } from "./accounts.js";
import {
getCachedBlueBubblesPrivateApiStatus,
isBlueBubblesPrivateApiStatusEnabled,
@@ -7,19 +7,15 @@ import {
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";
import { extractHandleFromChatGuid, normalizeBlueBubblesHandle } from "./targets.js";
import {
blueBubblesFetchWithTimeout,
buildBlueBubblesApiUrl,
type BlueBubblesSendTarget,
type SsrFPolicy,
} from "./types.js";
function blueBubblesPolicy(allowPrivateNetwork: boolean | undefined): SsrFPolicy {
return allowPrivateNetwork ? { allowPrivateNetwork: true } : {};
}
export type BlueBubblesSendOpts = {
serverUrl?: string;
password?: string;
@@ -198,7 +194,6 @@ async function queryChats(params: {
timeoutMs?: number;
offset: number;
limit: number;
allowPrivateNetwork?: boolean;
}): Promise<BlueBubblesChatRecord[]> {
const url = buildBlueBubblesApiUrl({
baseUrl: params.baseUrl,
@@ -217,7 +212,6 @@ async function queryChats(params: {
}),
},
params.timeoutMs,
blueBubblesPolicy(params.allowPrivateNetwork),
);
if (!res.ok) {
return [];
@@ -232,7 +226,6 @@ export async function resolveChatGuidForTarget(params: {
password: string;
timeoutMs?: number;
target: BlueBubblesSendTarget;
allowPrivateNetwork?: boolean;
}): Promise<string | null> {
if (params.target.kind === "chat_guid") {
return params.target.chatGuid;
@@ -253,7 +246,6 @@ export async function resolveChatGuidForTarget(params: {
timeoutMs: params.timeoutMs,
offset,
limit,
allowPrivateNetwork: params.allowPrivateNetwork,
});
if (chats.length === 0) {
break;
@@ -333,7 +325,6 @@ export async function createChatForHandle(params: {
address: string;
message?: string;
timeoutMs?: number;
allowPrivateNetwork?: boolean;
}): Promise<{ chatGuid: string | null; messageId: string }> {
const url = buildBlueBubblesApiUrl({
baseUrl: params.baseUrl,
@@ -353,7 +344,6 @@ export async function createChatForHandle(params: {
body: JSON.stringify(payload),
},
params.timeoutMs,
blueBubblesPolicy(params.allowPrivateNetwork),
);
if (!res.ok) {
const errorText = await res.text();
@@ -417,7 +407,6 @@ async function createNewChatWithMessage(params: {
address: string;
message: string;
timeoutMs?: number;
allowPrivateNetwork?: boolean;
}): Promise<BlueBubblesSendResult> {
const result = await createChatForHandle({
baseUrl: params.baseUrl,
@@ -425,7 +414,6 @@ async function createNewChatWithMessage(params: {
address: params.address,
message: params.message,
timeoutMs: params.timeoutMs,
allowPrivateNetwork: params.allowPrivateNetwork,
});
return { messageId: result.messageId };
}
@@ -445,13 +433,23 @@ export async function sendMessageBlueBubbles(
throw new Error("BlueBubbles send requires text (message was empty after markdown removal)");
}
const { baseUrl, password, accountId, allowPrivateNetwork } = resolveBlueBubblesServerAccount({
const account = resolveBlueBubblesAccount({
cfg: opts.cfg ?? {},
accountId: opts.accountId,
serverUrl: opts.serverUrl,
password: opts.password,
});
const privateApiStatus = getCachedBlueBubblesPrivateApiStatus(accountId);
const baseUrl =
normalizeSecretInputString(opts.serverUrl) ||
normalizeSecretInputString(account.config.serverUrl);
const password =
normalizeSecretInputString(opts.password) ||
normalizeSecretInputString(account.config.password);
if (!baseUrl) {
throw new Error("BlueBubbles serverUrl is required");
}
if (!password) {
throw new Error("BlueBubbles password is required");
}
const privateApiStatus = getCachedBlueBubblesPrivateApiStatus(account.accountId);
const target = resolveBlueBubblesSendTarget(to);
const chatGuid = await resolveChatGuidForTarget({
@@ -459,7 +457,6 @@ export async function sendMessageBlueBubbles(
password,
timeoutMs: opts.timeoutMs,
target,
allowPrivateNetwork,
});
if (!chatGuid) {
// If target is a phone number/handle and no existing chat found,
@@ -471,7 +468,6 @@ export async function sendMessageBlueBubbles(
address: target.address,
message: strippedText,
timeoutMs: opts.timeoutMs,
allowPrivateNetwork,
});
}
throw new Error(
@@ -527,7 +523,6 @@ export async function sendMessageBlueBubbles(
body: JSON.stringify(payload),
},
opts.timeoutMs,
blueBubblesPolicy(allowPrivateNetwork),
);
if (!res.ok) {
const errorText = await res.text();

View File

@@ -239,37 +239,6 @@ describe("BlueBubblesConfigSchema", () => {
});
expect(parsed.success).toBe(true);
});
it("defaults enrichGroupParticipantsFromContacts to true", () => {
const parsed = BlueBubblesConfigSchema.safeParse({
serverUrl: "http://localhost:1234",
password: "secret", // pragma: allowlist secret
});
expect(parsed.success).toBe(true);
if (!parsed.success) {
return;
}
expect(parsed.data.enrichGroupParticipantsFromContacts).toBe(true);
});
it("defaults account enrichGroupParticipantsFromContacts to true", () => {
const parsed = BlueBubblesConfigSchema.safeParse({
accounts: {
work: {
serverUrl: "http://localhost:1234",
password: "secret", // pragma: allowlist secret
},
},
});
expect(parsed.success).toBe(true);
if (!parsed.success) {
return;
}
const accountConfig = (
parsed.data as { accounts?: { work?: { enrichGroupParticipantsFromContacts?: boolean } } }
).accounts?.work;
expect(accountConfig?.enrichGroupParticipantsFromContacts).toBe(true);
});
});
describe("bluebubbles group policy", () => {

View File

@@ -1,6 +1,5 @@
import type { Mock } from "vitest";
import { afterEach, beforeEach, vi } from "vitest";
import { _setFetchGuardForTesting } from "./types.js";
export const BLUE_BUBBLES_PRIVATE_API_STATUS = {
enabled: true,
@@ -68,12 +67,8 @@ export function installBlueBubblesFetchTestHooks(params: {
mockReturnValue: (value: boolean | null) => unknown;
};
}) {
const setFetchGuardPassthrough = createBlueBubblesFetchGuardPassthroughInstaller();
beforeEach(() => {
vi.stubGlobal("fetch", params.mockFetch);
// Replace the SSRF guard with a passthrough that delegates to the mocked global.fetch,
// wrapping the result in a real Response so callers can call .arrayBuffer() on it.
setFetchGuardPassthrough();
params.mockFetch.mockReset();
params.privateApiStatusMock.mockReset?.();
params.privateApiStatusMock.mockClear?.();
@@ -81,36 +76,6 @@ export function installBlueBubblesFetchTestHooks(params: {
});
afterEach(() => {
_setFetchGuardForTesting(null);
vi.unstubAllGlobals();
});
}
export function createBlueBubblesFetchGuardPassthroughInstaller() {
return (capturePolicy?: (policy: unknown) => void) => {
_setFetchGuardForTesting(async (params) => {
capturePolicy?.(params.policy);
const raw = await globalThis.fetch(params.url, params.init);
let body: ArrayBuffer;
if (typeof raw.arrayBuffer === "function") {
body = await raw.arrayBuffer();
} else {
const text =
typeof (raw as { text?: () => Promise<string> }).text === "function"
? await (raw as { text: () => Promise<string> }).text()
: typeof (raw as { json?: () => Promise<unknown> }).json === "function"
? JSON.stringify(await (raw as { json: () => Promise<unknown> }).json())
: "";
body = new TextEncoder().encode(text).buffer;
}
return {
response: new Response(body, {
status: (raw as { status?: number }).status ?? 200,
headers: (raw as { headers?: HeadersInit }).headers,
}),
release: async () => {},
finalUrl: params.url,
};
});
};
}

View File

@@ -1,7 +1,5 @@
import { fetchWithSsrFGuard, type SsrFPolicy } from "openclaw/plugin-sdk/infra-runtime";
import type { DmPolicy, GroupPolicy } from "openclaw/plugin-sdk/setup";
export type { SsrFPolicy } from "openclaw/plugin-sdk/infra-runtime";
export type { DmPolicy, GroupPolicy } from "openclaw/plugin-sdk/setup";
export type BlueBubblesGroupConfig = {
@@ -33,8 +31,6 @@ export type BlueBubblesAccountConfig = {
groupAllowFrom?: Array<string | number>;
/** Group message handling policy. */
groupPolicy?: GroupPolicy;
/** Enrich unnamed group participants with local macOS Contacts names after gating. Default: true. */
enrichGroupParticipantsFromContacts?: boolean;
/** Max group messages to keep as history context (0 disables). */
historyLimit?: number;
/** Max DM turns to keep as history context. */
@@ -130,43 +126,11 @@ export function buildBlueBubblesApiUrl(params: {
return url.toString();
}
// Overridable guard for testing; production code uses fetchWithSsrFGuard.
let _fetchGuard = fetchWithSsrFGuard;
/** @internal Replace the SSRF fetch guard in tests. */
export function _setFetchGuardForTesting(impl: typeof fetchWithSsrFGuard | null): void {
_fetchGuard = impl ?? fetchWithSsrFGuard;
}
export async function blueBubblesFetchWithTimeout(
url: string,
init: RequestInit,
timeoutMs = DEFAULT_TIMEOUT_MS,
ssrfPolicy?: SsrFPolicy,
): Promise<Response> {
if (ssrfPolicy !== undefined) {
// Use SSRF-guarded fetch; buffer the body so the dispatcher can be released
// before the caller reads the response (API responses are small JSON payloads).
const { response, release } = await _fetchGuard({
url,
init,
timeoutMs,
policy: ssrfPolicy,
auditContext: "bluebubbles-api",
});
// Null-body status codes per Fetch spec — Response constructor rejects a body for these.
const isNullBody =
response.status === 101 ||
response.status === 204 ||
response.status === 205 ||
response.status === 304;
try {
const bodyBytes = isNullBody ? null : await response.arrayBuffer();
return new Response(bodyBytes, { status: response.status, headers: response.headers });
} finally {
await release();
}
}
) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {

View File

@@ -1,90 +0,0 @@
import { describe, expect, it, vi } from "vitest";
import { createTestPluginApi } from "../../test/helpers/extensions/plugin-api.js";
import type { OpenClawPluginApi } from "./runtime-api.js";
const runtimeApiMocks = vi.hoisted(() => ({
createBrowserPluginService: vi.fn(() => ({ id: "browser-control", start: vi.fn() })),
createBrowserTool: vi.fn(() => ({
name: "browser",
description: "browser",
parameters: { type: "object", properties: {} },
execute: vi.fn(),
})),
handleBrowserGatewayRequest: vi.fn(),
registerBrowserCli: vi.fn(),
}));
vi.mock("./runtime-api.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("./runtime-api.js")>();
return {
...actual,
createBrowserPluginService: runtimeApiMocks.createBrowserPluginService,
createBrowserTool: runtimeApiMocks.createBrowserTool,
handleBrowserGatewayRequest: runtimeApiMocks.handleBrowserGatewayRequest,
registerBrowserCli: runtimeApiMocks.registerBrowserCli,
};
});
import browserPlugin from "./index.js";
function createApi() {
const registerCli = vi.fn();
const registerGatewayMethod = vi.fn();
const registerService = vi.fn();
const registerTool = vi.fn();
const api = createTestPluginApi({
id: "browser",
name: "Browser",
source: "test",
config: {},
runtime: {} as OpenClawPluginApi["runtime"],
registerCli,
registerGatewayMethod,
registerService,
registerTool,
}) as OpenClawPluginApi;
return { api, registerCli, registerGatewayMethod, registerService, registerTool };
}
describe("browser plugin", () => {
it("registers browser tool, cli, gateway method, and service ownership", () => {
const { api, registerCli, registerGatewayMethod, registerService, registerTool } = createApi();
browserPlugin.register(api);
expect(registerTool).toHaveBeenCalledTimes(1);
expect(registerCli).toHaveBeenCalledWith(expect.any(Function), { commands: ["browser"] });
expect(registerGatewayMethod).toHaveBeenCalledWith(
"browser.request",
runtimeApiMocks.handleBrowserGatewayRequest,
{ scope: "operator.write" },
);
expect(runtimeApiMocks.createBrowserPluginService).toHaveBeenCalledTimes(1);
expect(registerService).toHaveBeenCalledWith(
runtimeApiMocks.createBrowserPluginService.mock.results[0]?.value,
);
});
it("forwards per-session browser options into the tool factory", () => {
const { api, registerTool } = createApi();
browserPlugin.register(api);
const tool = registerTool.mock.calls[0]?.[0];
if (typeof tool !== "function") {
throw new Error("expected browser plugin to register a tool factory");
}
tool({
sessionKey: "agent:main:webchat:direct:123",
browser: {
sandboxBridgeUrl: "http://127.0.0.1:9999",
allowHostControl: true,
},
});
expect(runtimeApiMocks.createBrowserTool).toHaveBeenCalledWith({
sandboxBridgeUrl: "http://127.0.0.1:9999",
allowHostControl: true,
agentSessionKey: "agent:main:webchat:direct:123",
});
});
});

View File

@@ -1,28 +0,0 @@
import {
createBrowserPluginService,
createBrowserTool,
definePluginEntry,
handleBrowserGatewayRequest,
registerBrowserCli,
type OpenClawPluginToolContext,
type OpenClawPluginToolFactory,
} from "./runtime-api.js";
export default definePluginEntry({
id: "browser",
name: "Browser",
description: "Default browser tool plugin",
register(api) {
api.registerTool(((ctx: OpenClawPluginToolContext) =>
createBrowserTool({
sandboxBridgeUrl: ctx.browser?.sandboxBridgeUrl,
allowHostControl: ctx.browser?.allowHostControl,
agentSessionKey: ctx.sessionKey,
})) as OpenClawPluginToolFactory);
api.registerCli(({ program }) => registerBrowserCli(program), { commands: ["browser"] });
api.registerGatewayMethod("browser.request", handleBrowserGatewayRequest, {
scope: "operator.write",
});
api.registerService(createBrowserPluginService());
},
});

View File

@@ -1,9 +0,0 @@
{
"id": "browser",
"enabledByDefault": true,
"configSchema": {
"type": "object",
"additionalProperties": false,
"properties": {}
}
}

View File

@@ -1,12 +0,0 @@
{
"name": "@openclaw/browser-plugin",
"version": "2026.3.25",
"private": true,
"description": "OpenClaw browser tool plugin",
"type": "module",
"openclaw": {
"extensions": [
"./index.ts"
]
}
}

View File

@@ -1,10 +0,0 @@
export { createBrowserTool } from "./src/browser-tool.js";
export { registerBrowserCli } from "./src/cli/browser-cli.js";
export { createBrowserPluginService } from "./src/plugin-service.js";
export { handleBrowserGatewayRequest } from "./src/gateway/browser-request.js";
export {
definePluginEntry,
type OpenClawPluginApi,
type OpenClawPluginToolContext,
type OpenClawPluginToolFactory,
} from "openclaw/plugin-sdk/plugin-entry";

View File

@@ -1,87 +0,0 @@
export { startBrowserBridgeServer, stopBrowserBridgeServer } from "./browser/bridge-server.js";
export type { BrowserBridge } from "./browser/bridge-server.js";
export {
browserAct,
browserArmDialog,
browserArmFileChooser,
browserConsoleMessages,
browserNavigate,
browserPdfSave,
browserScreenshotAction,
} from "./browser/client-actions.js";
export {
browserCloseTab,
browserFocusTab,
browserOpenTab,
browserCreateProfile,
browserDeleteProfile,
browserProfiles,
browserResetProfile,
browserSnapshot,
browserStart,
browserStatus,
browserStop,
browserTabAction,
browserTabs,
} from "./browser/client.js";
export { runBrowserProxyCommand } from "./node-host/invoke-browser.js";
export type {
BrowserCreateProfileResult,
BrowserDeleteProfileResult,
BrowserResetProfileResult,
BrowserStatus,
BrowserTab,
BrowserTransport,
ProfileStatus,
SnapshotResult,
} from "./browser/client.js";
export type { BrowserExecutable } from "./browser/chrome.executables.js";
export type { ResolvedBrowserConfig, ResolvedBrowserProfile } from "./browser/config.js";
export { resolveBrowserConfig, resolveProfile } from "./browser/config.js";
export {
DEFAULT_AI_SNAPSHOT_MAX_CHARS,
DEFAULT_BROWSER_EVALUATE_ENABLED,
DEFAULT_OPENCLAW_BROWSER_COLOR,
DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME,
} from "./browser/constants.js";
export {
parseBrowserMajorVersion,
readBrowserVersion,
resolveGoogleChromeExecutableForPlatform,
} from "./browser/chrome.executables.js";
export { redactCdpUrl } from "./browser/cdp.helpers.js";
export { DEFAULT_UPLOAD_DIR, resolveExistingPathsWithinRoot } from "./browser/paths.js";
export { getBrowserProfileCapabilities } from "./browser/profile-capabilities.js";
export { applyBrowserProxyPaths, persistBrowserProxyFiles } from "./browser/proxy-files.js";
export {
isPersistentBrowserProfileMutation,
normalizeBrowserRequestPath,
resolveRequestedBrowserProfile,
} from "./browser/request-policy.js";
export {
closeTrackedBrowserTabsForSessions,
trackSessionBrowserTab,
untrackSessionBrowserTab,
} from "./browser/session-tab-registry.js";
export { ensureBrowserControlAuth, resolveBrowserControlAuth } from "./browser/control-auth.js";
export { movePathToTrash } from "./browser/trash.js";
export {
createBrowserControlContext,
getBrowserControlState,
startBrowserControlServiceFromConfig,
stopBrowserControlService,
} from "./control-service.js";
export { createBrowserRuntimeState, stopBrowserRuntime } from "./browser/runtime-lifecycle.js";
export { type BrowserServerState, createBrowserRouteContext } from "./browser/server-context.js";
export { registerBrowserRoutes } from "./browser/routes/index.js";
export { createBrowserRouteDispatcher } from "./browser/routes/dispatcher.js";
export type { BrowserRouteRegistrar } from "./browser/routes/types.js";
export {
installBrowserAuthMiddleware,
installBrowserCommonMiddleware,
} from "./browser/server-middleware.js";
export type { BrowserFormField } from "./browser/client-actions-core.js";
export {
normalizeBrowserFormField,
normalizeBrowserFormFieldValue,
} from "./browser/form-fields.js";

View File

@@ -1 +0,0 @@
export * from "../control-service.js";

View File

@@ -1 +0,0 @@
export * from "../plugin-enabled.js";

View File

@@ -1 +0,0 @@
export * from "../plugin-service.js";

View File

@@ -1 +0,0 @@
export * from "../server.js";

View File

@@ -1 +0,0 @@
export * from "openclaw/plugin-sdk/browser-support";

View File

@@ -1 +0,0 @@
export * from "../core-api.js";

View File

@@ -1 +0,0 @@
export * from "openclaw/plugin-sdk/browser-support";

View File

@@ -1 +0,0 @@
export * from "openclaw/plugin-sdk/browser-support";

View File

@@ -1 +0,0 @@
export * from "openclaw/plugin-sdk/browser-support";

View File

@@ -1,110 +0,0 @@
export {
DEFAULT_AI_SNAPSHOT_MAX_CHARS,
DEFAULT_UPLOAD_DIR,
applyBrowserProxyPaths,
browserAct,
browserArmDialog,
browserArmFileChooser,
browserCloseTab,
browserCreateProfile,
browserConsoleMessages,
browserDeleteProfile,
browserFocusTab,
browserNavigate,
browserOpenTab,
browserPdfSave,
browserProfiles,
browserResetProfile,
browserScreenshotAction,
browserSnapshot,
browserStart,
browserStatus,
browserStop,
browserTabAction,
browserTabs,
createBrowserControlContext,
createBrowserRouteDispatcher,
createBrowserRuntimeState,
createBrowserRouteContext,
ensureBrowserControlAuth,
getBrowserControlState,
getBrowserProfileCapabilities,
isPersistentBrowserProfileMutation,
installBrowserAuthMiddleware,
installBrowserCommonMiddleware,
normalizeBrowserFormField,
normalizeBrowserFormFieldValue,
normalizeBrowserRequestPath,
persistBrowserProxyFiles,
redactCdpUrl,
registerBrowserRoutes,
resolveBrowserConfig,
resolveBrowserControlAuth,
resolveExistingPathsWithinRoot,
resolveProfile,
resolveRequestedBrowserProfile,
startBrowserControlServiceFromConfig,
stopBrowserControlService,
stopBrowserRuntime,
trackSessionBrowserTab,
untrackSessionBrowserTab,
} from "./browser-runtime.js";
export type {
BrowserCreateProfileResult,
BrowserDeleteProfileResult,
BrowserFormField,
BrowserResetProfileResult,
BrowserRouteRegistrar,
BrowserServerState,
BrowserStatus,
BrowserTab,
BrowserTransport,
ProfileStatus,
SnapshotResult,
} from "./browser-runtime.js";
export {
callGatewayTool,
createSubsystemLogger,
danger,
defaultRuntime,
detectMime,
ErrorCodes,
errorShape,
formatCliCommand,
formatDocsLink,
formatHelpExamples,
addGatewayClientOptions,
callGatewayFromCli,
inheritOptionFromParent,
info,
imageResultFromFile,
isNodeCommandAllowed,
jsonResult,
listNodes,
loadConfig,
normalizePluginsConfig,
optionalStringEnum,
parseBooleanValue,
readStringParam,
respondUnavailableOnNodeInvokeError,
resolveEffectiveEnableState,
resolveNodeIdFromList,
resolveNodeCommandAllowlist,
runCommandWithRuntime,
selectDefaultNodeFromList,
safeParseJson,
shortenHomePath,
stringEnum,
theme,
withTimeout,
wrapExternalContent,
} from "openclaw/plugin-sdk/browser-support";
export type {
AnyAgentTool,
GatewayRequestHandlers,
GatewayRpcOpts,
NodeListNode,
NodeSession,
OpenClawConfig,
OpenClawPluginService,
} from "openclaw/plugin-sdk/browser-support";

View File

@@ -1 +0,0 @@
export * from "openclaw/plugin-sdk/browser-support";

View File

@@ -1,270 +0,0 @@
import crypto from "node:crypto";
import {
ErrorCodes,
applyBrowserProxyPaths,
createBrowserControlContext,
createBrowserRouteDispatcher,
errorShape,
isNodeCommandAllowed,
isPersistentBrowserProfileMutation,
loadConfig,
persistBrowserProxyFiles,
resolveNodeCommandAllowlist,
resolveRequestedBrowserProfile,
respondUnavailableOnNodeInvokeError,
safeParseJson,
startBrowserControlServiceFromConfig,
type GatewayRequestHandlers,
type NodeSession,
} from "../core-api.js";
type BrowserRequestParams = {
method?: string;
path?: string;
query?: Record<string, unknown>;
body?: unknown;
timeoutMs?: number;
};
type BrowserProxyFile = {
path: string;
base64: string;
mimeType?: string;
};
type BrowserProxyResult = {
result: unknown;
files?: BrowserProxyFile[];
};
function isBrowserNode(node: NodeSession) {
const caps = Array.isArray(node.caps) ? node.caps : [];
const commands = Array.isArray(node.commands) ? node.commands : [];
return caps.includes("browser") || commands.includes("browser.proxy");
}
function normalizeNodeKey(value: string) {
return value
.trim()
.toLowerCase()
.replace(/[^a-z0-9]+/g, "");
}
function resolveBrowserNode(nodes: NodeSession[], query: string): NodeSession | null {
const q = query.trim();
if (!q) {
return null;
}
const qNorm = normalizeNodeKey(q);
const matches = nodes.filter((node) => {
if (node.nodeId === q) {
return true;
}
if (typeof node.remoteIp === "string" && node.remoteIp === q) {
return true;
}
const name = typeof node.displayName === "string" ? node.displayName : "";
if (name && normalizeNodeKey(name) === qNorm) {
return true;
}
if (q.length >= 6 && node.nodeId.startsWith(q)) {
return true;
}
return false;
});
if (matches.length === 1) {
return matches[0] ?? null;
}
if (matches.length === 0) {
return null;
}
throw new Error(
`ambiguous node: ${q} (matches: ${matches
.map((node) => node.displayName || node.remoteIp || node.nodeId)
.join(", ")})`,
);
}
function resolveBrowserNodeTarget(params: {
cfg: ReturnType<typeof loadConfig>;
nodes: NodeSession[];
}): NodeSession | null {
const policy = params.cfg.gateway?.nodes?.browser;
const mode = policy?.mode ?? "auto";
if (mode === "off") {
return null;
}
const browserNodes = params.nodes.filter((node) => isBrowserNode(node));
if (browserNodes.length === 0) {
if (policy?.node?.trim()) {
throw new Error("No connected browser-capable nodes.");
}
return null;
}
const requested = policy?.node?.trim() || "";
if (requested) {
const resolved = resolveBrowserNode(browserNodes, requested);
if (!resolved) {
throw new Error(`Configured browser node not connected: ${requested}`);
}
return resolved;
}
if (mode === "manual") {
return null;
}
if (browserNodes.length === 1) {
return browserNodes[0] ?? null;
}
return null;
}
async function persistProxyFiles(files: BrowserProxyFile[] | undefined) {
return await persistBrowserProxyFiles(files);
}
function applyProxyPaths(result: unknown, mapping: Map<string, string>) {
applyBrowserProxyPaths(result, mapping);
}
export async function handleBrowserGatewayRequest({
params,
respond,
context,
}: Parameters<GatewayRequestHandlers["browser.request"]>[0]) {
const typed = params as BrowserRequestParams;
const methodRaw = typeof typed.method === "string" ? typed.method.trim().toUpperCase() : "";
const path = typeof typed.path === "string" ? typed.path.trim() : "";
const query = typed.query && typeof typed.query === "object" ? typed.query : undefined;
const body = typed.body;
const timeoutMs =
typeof typed.timeoutMs === "number" && Number.isFinite(typed.timeoutMs)
? Math.max(1, Math.floor(typed.timeoutMs))
: undefined;
if (!methodRaw || !path) {
respond(
false,
undefined,
errorShape(ErrorCodes.INVALID_REQUEST, "method and path are required"),
);
return;
}
if (methodRaw !== "GET" && methodRaw !== "POST" && methodRaw !== "DELETE") {
respond(
false,
undefined,
errorShape(ErrorCodes.INVALID_REQUEST, "method must be GET, POST, or DELETE"),
);
return;
}
if (isPersistentBrowserProfileMutation(methodRaw, path)) {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
"browser.request cannot mutate persistent browser profiles",
),
);
return;
}
const cfg = loadConfig();
let nodeTarget: NodeSession | null = null;
try {
nodeTarget = resolveBrowserNodeTarget({
cfg,
nodes: context.nodeRegistry.listConnected(),
});
} catch (err) {
respond(false, undefined, errorShape(ErrorCodes.UNAVAILABLE, String(err)));
return;
}
if (nodeTarget) {
const allowlist = resolveNodeCommandAllowlist(cfg, nodeTarget);
const allowed = isNodeCommandAllowed({
command: "browser.proxy",
declaredCommands: nodeTarget.commands,
allowlist,
});
if (!allowed.ok) {
const platform = nodeTarget.platform ?? "unknown";
const hint = `node command not allowed: ${allowed.reason} (platform: ${platform}, command: browser.proxy)`;
respond(
false,
undefined,
errorShape(ErrorCodes.INVALID_REQUEST, hint, {
details: { reason: allowed.reason, command: "browser.proxy" },
}),
);
return;
}
const proxyParams = {
method: methodRaw,
path,
query,
body,
timeoutMs,
profile: resolveRequestedBrowserProfile({ query, body }),
};
const res = await context.nodeRegistry.invoke({
nodeId: nodeTarget.nodeId,
command: "browser.proxy",
params: proxyParams,
timeoutMs,
idempotencyKey: crypto.randomUUID(),
});
if (!respondUnavailableOnNodeInvokeError(respond, res)) {
return;
}
const payload = res.payloadJSON ? safeParseJson(res.payloadJSON) : res.payload;
const proxy = payload && typeof payload === "object" ? (payload as BrowserProxyResult) : null;
if (!proxy || !("result" in proxy)) {
respond(false, undefined, errorShape(ErrorCodes.UNAVAILABLE, "browser proxy failed"));
return;
}
const mapping = await persistProxyFiles(proxy.files);
applyProxyPaths(proxy.result, mapping);
respond(true, proxy.result);
return;
}
const ready = await startBrowserControlServiceFromConfig();
if (!ready) {
respond(false, undefined, errorShape(ErrorCodes.UNAVAILABLE, "browser control is disabled"));
return;
}
let dispatcher;
try {
dispatcher = createBrowserRouteDispatcher(createBrowserControlContext());
} catch (err) {
respond(false, undefined, errorShape(ErrorCodes.UNAVAILABLE, String(err)));
return;
}
const result = await dispatcher.dispatch({
method: methodRaw,
path,
query,
body,
});
if (result.status >= 400) {
const message =
result.body && typeof result.body === "object" && "error" in result.body
? String((result.body as { error?: unknown }).error)
: `browser request failed (${result.status})`;
const code = result.status >= 500 ? ErrorCodes.UNAVAILABLE : ErrorCodes.INVALID_REQUEST;
respond(false, undefined, errorShape(code, message, { details: result.body }));
return;
}
respond(true, result.body);
}
export const browserHandlers: GatewayRequestHandlers = {
"browser.request": handleBrowserGatewayRequest,
};

View File

@@ -1 +0,0 @@
export * from "openclaw/plugin-sdk/browser-support";

View File

@@ -1 +0,0 @@
export * from "openclaw/plugin-sdk/browser-support";

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