Compare commits

..

172 Commits

Author SHA1 Message Date
Josh Lehman
53ac244ec6 Merge branch 'main' into codex/plugin-command-scope-auth 2026-03-26 16:15:50 -07:00
Peter Steinberger
d1d0887932 refactor: remove legacy browser bridge entrypoints 2026-03-26 23:11:17 +00:00
Josh Lehman
c94f10b915 Plugins: harden dynamic scope resolution
Catch dynamic gateway-scope resolver failures in the dispatcher, narrow
forwarded gateway scope strings with an explicit operator-scope guard, add
regression coverage for admin bypass and resolver-throw behavior, and
refresh bundled plugin metadata after main-branch drift.

Regeneration-Prompt: |
  Follow up on review feedback for the centralized plugin command auth
  change. Keep the scope tightly limited to the three review items:
  catch exceptions from `resolveRequiredGatewayScopes`, replace the raw
  `GatewayClientScopes` cast with explicit operator-scope narrowing, and
  add dispatcher-level tests for the `operator.admin` bypass plus the safe
  failure path when dynamic scope resolution throws.

  While landing that patch, the repo hook may report stale bundled plugin
  metadata generated files because main advanced. Regenerate those standard
  outputs with the repo generator so the branch is consistent enough to
  rebase, but do not chase unrelated CI or Discord test failures here.
2026-03-26 16:11:09 -07:00
Josh Lehman
70b43319ff Plugin SDK: refresh API baselines for auth context change
Update the generated Plugin SDK API baseline files after extending plugin
command types for centralized owner and gateway-scope authorization.

Regeneration-Prompt: |
  The prior commit intentionally changed exported plugin SDK types in
  `src/plugins/types.ts` by adding richer plugin command auth context and
  declarative command requirement fields. CI reported plugin SDK API drift,
  which means the generated baseline files under `docs/.generated/` no
  longer matched the exported surface.

  Regenerate only the plugin SDK API baseline artifacts with the repo's
  standard generator, verify `pnpm plugin-sdk:api:check` passes, and keep
  this follow-up scoped to those generated files. Do not fold in unrelated
  failing tests from untouched surfaces.
2026-03-26 16:11:09 -07:00
Peter Steinberger
4b40d4dfa8 perf: optimize cold import paths 2026-03-26 23:11:00 +00:00
Josh Lehman
487f752754 Plugins: centralize plugin command auth requirements
Move plugin command authorization toward the GHSA's long-term model by
preserving richer auth context, supporting declarative owner and gateway
scope requirements, and enforcing them in the shared dispatcher. Convert
`/pair approve` to use the centralized requirement path and add regression
coverage for dispatcher-level auth behavior.

Regeneration-Prompt: |
  This follow-up hardening is for the plugin command auth gap described in
  GHSA-9gwp-pxfh-w6r5. The immediate exploit path was already fixed by
  plumbing gateway scopes into the device-pair plugin and checking `/pair
  approve` inline, but the longer-term goal is to stop relying on lossy,
  plugin-specific auth checks.

  Preserve the existing plugin command flow and keep the change additive.
  Carry richer authorization context into plugin execution, including owner
  status and command surface, and let commands declare owner or internal
  gateway-scope requirements that the central dispatcher enforces. Internal
  callers should fail closed when required scopes are missing, with admin
  scope still satisfying narrower operator requirements, while non-internal
  chat surfaces should keep their current auth behavior.

  Because `/pair` mixes low-risk actions like `qr` and `status` with the
  privileged `approve` action, use a context-sensitive requirement instead
  of making the whole command require pairing scope. Add focused regression
  tests around dispatcher enforcement and update any command-context test
  helpers that now need the richer fields.
2026-03-26 16:10:38 -07:00
Peter Steinberger
15181b3a77 docs(anthropic): dedupe config heading 2026-03-26 23:08:26 +00:00
Peter Steinberger
5f2876911a fix: harden discord gateway cleanup (#55373) (thanks @Takhoffman) 2026-03-26 16:07:13 -07:00
Tak Hoffman
a79c9d50f7 fix(discord): guard gateway cleanup races 2026-03-26 16:07:13 -07:00
Peter Steinberger
f406b20e50 chore(docs): refresh generated baselines 2026-03-26 23:05:59 +00:00
Peter Steinberger
eef27001de docs: explain anthropic claude cli migration 2026-03-26 23:04:47 +00:00
Peter Steinberger
ebf5bd75f4 feat: add anthropic claude cli migration 2026-03-26 23:04:47 +00:00
Peter Steinberger
b96fccadb9 refactor: clean memory plugin host boundary 2026-03-26 23:02:24 +00:00
Peter Steinberger
556ce5cdda test(browser): fix CI after compat re-exports 2026-03-26 22:59:50 +00:00
Peter Steinberger
09c186d5f9 refactor: remove browser compat shadow tree 2026-03-26 22:53:37 +00:00
Peter Steinberger
d72115c9df refactor: genericize speech provider config surface 2026-03-26 22:48:57 +00:00
Peter Steinberger
83ca6fbfc6 refactor: finish browser compat untangle 2026-03-26 22:42:41 +00:00
Peter Steinberger
8ee809f3cc refactor: share plugin entry exports 2026-03-26 22:38:13 +00:00
Peter Steinberger
8df6134a1b refactor: share usage metrics timeslice walker 2026-03-26 22:38:13 +00:00
Peter Steinberger
ff47ad58fc refactor: share config path traversal helper 2026-03-26 22:38:13 +00:00
Peter Steinberger
5445bc68b9 refactor: share tts auto mode normalization 2026-03-26 22:38:13 +00:00
Seungwoo hong
138a92373b fix(talk): prevent double TTS playback when system voice times out (#53511)
Merged via squash.

Prepared head SHA: 864d556fa6
Co-authored-by: hongsw <1100974+hongsw@users.noreply.github.com>
Co-authored-by: grp06 <1573959+grp06@users.noreply.github.com>
Reviewed-by: @grp06
2026-03-26 15:37:40 -07:00
Peter Steinberger
0f5a77d058 refactor: route memory runtime through memory plugin 2026-03-26 22:30:47 +00:00
Peter Steinberger
2c6d099b01 refactor: plugin-own speech provider config 2026-03-26 22:28:24 +00:00
Peter Steinberger
8eeb7f0829 refactor: switch browser ownership to bundled plugin 2026-03-26 22:20:40 +00:00
Peter Steinberger
197510f693 refactor: add browser plugin runtime package 2026-03-26 22:20:39 +00:00
Peter Steinberger
1619090693 refactor: move memory plugin state into plugin host 2026-03-26 22:15:49 +00:00
Peter Steinberger
00aedb3414 refactor: split claude cli history import pipeline 2026-03-26 22:12:16 +00:00
Peter Steinberger
d0ce2d1044 refactor: split memory-core plugin helpers 2026-03-26 22:06:06 +00:00
Peter Steinberger
9dea807b28 test: share planner and sandbox test helpers 2026-03-26 22:03:18 +00:00
Peter Steinberger
672a24cbde fix: unify claude cli imported tool messages 2026-03-26 22:02:26 +00:00
Peter Steinberger
3d0050c306 refactor: add memory-core extension sources 2026-03-26 22:00:13 +00:00
Peter Steinberger
e955d574b2 refactor: move memory tooling into memory-core extension 2026-03-26 22:00:13 +00:00
Peter Steinberger
e0dfc776bb refactor: move memory flush ownership into memory plugin 2026-03-26 22:00:13 +00:00
Peter Steinberger
48a65f7749 refactor: simplify bundled plugin contracts 2026-03-26 21:55:41 +00:00
Peter Steinberger
8b42ad08e5 perf: speed up shared extension test batches 2026-03-26 21:51:25 +00:00
Peter Steinberger
29069bd250 refactor: share speech normalization helpers 2026-03-26 21:49:20 +00:00
Peter Steinberger
ce9dff1458 refactor: clean plugin capability boundaries 2026-03-26 21:41:49 +00:00
Tak Hoffman
d00dc5f46b fix(ci): repair discord and telegram follow-ups 2026-03-26 16:33:05 -05:00
Peter Steinberger
53f90af990 test: dedupe telegram polling session harness 2026-03-26 21:30:28 +00:00
Peter Steinberger
2a04053854 fix: skip cli backends in models auth warnings 2026-03-26 21:28:28 +00:00
Peter Steinberger
98ea8e244f fix: backfill claude cli chat history 2026-03-26 21:25:35 +00:00
Peter Steinberger
6aa9bec8d7 fix: export shared channel action enum helpers 2026-03-26 21:21:44 +00:00
Peter Steinberger
0c0f1e34cb refactor: split telegram polling and sdk surfaces 2026-03-26 21:13:16 +00:00
Tak Hoffman
0805078118 fix(ci): format discord provider follow-up 2026-03-26 16:05:25 -05:00
Tak Hoffman
aeee72426d fix(ci): restore discord provider test seams 2026-03-26 15:59:51 -05:00
Peter Steinberger
37894d0f1a test: dedupe discord provider proxy overrides 2026-03-26 20:43:02 +00:00
Lyle Hopkins
eb328a85e3 fix(agents): classify "Failed to extract accountId from token" as auth error for failover (#27055) (#55206)
Co-authored-by: Lyle Hopkins <55105+cosmicnet@users.noreply.github.com>
2026-03-26 23:42:38 +03:00
Tak Hoffman
53d3b8e92d fix(ci): clean up discord harness types 2026-03-26 15:39:20 -05:00
Tak Hoffman
a39e57a1bd fix(ci): repair discord harness regressions 2026-03-26 15:39:20 -05:00
Peter Steinberger
3f54076d37 refactor: dedupe cli runner session reuse 2026-03-26 20:35:19 +00:00
Peter Steinberger
61d29efc04 test: eliminate remaining clone seams 2026-03-26 20:28:36 +00:00
Peter Steinberger
5841e5fdf8 test: split cli agent command coverage 2026-03-26 20:25:20 +00:00
Peter Steinberger
12100719b8 fix: preserve cli sessions across model changes 2026-03-26 20:25:20 +00:00
Peter Steinberger
236e041ef9 test: share discord monitor fixtures 2026-03-26 20:12:21 +00:00
pkuGeo
e035a0d98c telegram: rebuild transport after stalled polling cycles 2026-03-26 13:11:15 -07:00
Peter Steinberger
663ba5a3cd perf: speed up test parallelism 2026-03-26 20:09:40 +00:00
Peter Steinberger
2fc017788c test: reduce remaining clone seams 2026-03-26 20:01:01 +00:00
Tak Hoffman
b20ae13c6b fix(ci): repair discord message handler tests 2026-03-26 14:49:06 -05:00
Peter Steinberger
be328e6cd1 test: dedupe extension channel fixtures 2026-03-26 19:47:27 +00:00
Peter Steinberger
e8f9d68bec test: share cli command and discord test helpers 2026-03-26 19:37:14 +00:00
Peter Steinberger
b48df79c0a test(gateway): strip MiniMax live scaffolding 2026-03-26 19:35:03 +00:00
Tak Hoffman
53f15afade fix(ci): repair discord regression tests 2026-03-26 14:23:57 -05:00
Peter Steinberger
ef381743d8 test: share cli and doctor test helpers 2026-03-26 19:16:43 +00:00
Peter Steinberger
ab4de18982 fix: auto-load bundled plugin capabilities from config refs 2026-03-26 19:15:56 +00:00
Peter Steinberger
8f1716ae5a refactor: share slack and telegram action helpers 2026-03-26 19:07:35 +00:00
Peter Steinberger
a1a9819be8 refactor: dedupe gateway session resolve visibility 2026-03-26 18:56:55 +00:00
Peter Steinberger
4069844795 refactor: share discord outbound session routing 2026-03-26 18:51:02 +00:00
Peter Steinberger
e774fe1286 refactor: share browser and sandbox helpers 2026-03-26 18:43:57 +00:00
Tak Hoffman
2b6375faf9 fix: keep spawned session owners in live events 2026-03-26 13:41:46 -05:00
Tak Hoffman
1062a048eb fix: expose spawned session owners in sessions list 2026-03-26 13:41:46 -05:00
Tak Hoffman
c041fcc04d fix: expose parent session keys in sessions list 2026-03-26 13:41:46 -05:00
Tak Hoffman
cb46b08efc fix: include dashboard children in owner filters 2026-03-26 13:41:46 -05:00
Tak Hoffman
c48a3e4fc9 ci: optimize windows test shard fanout (#55261)
* ci: reduce windows test shard fanout

* ci: tighten windows shard target

* ci: back off windows shard target

* ci: restore windows shard cap
2026-03-26 13:40:28 -05:00
Peter Steinberger
cca577a0cc refactor: share plugin setup helpers 2026-03-26 18:34:51 +00:00
Peter Steinberger
c98addeadd test: share auto-reply typing helpers 2026-03-26 18:27:13 +00:00
Peter Steinberger
1f740ff099 test: share cli and channel setup fixtures 2026-03-26 18:14:44 +00:00
Jacob Tomlinson
02cf12371f Gateway: require requester ownership for HTTP session kills (#55308) 2026-03-26 18:13:36 +00:00
Peter Steinberger
f29c1206cd test: dedupe extension channel fixtures 2026-03-26 17:59:05 +00:00
Peter Steinberger
48167a69b9 refactor: dedupe gateway and binding helpers 2026-03-26 17:49:19 +00:00
Jacob Tomlinson
1c45123231 Gateway: align HTTP session history scopes (#55285)
* Gateway: require scopes for HTTP session history

* Gateway: cover missing HTTP history scope header
2026-03-26 17:43:57 +00:00
Jacob Tomlinson
f8c9863078 bluebubbles: honor reaction mention gating (#55283) 2026-03-26 17:42:19 +00:00
Peter Steinberger
e7e4fbcab9 test: dedupe secrets and guardrail fixtures 2026-03-26 17:39:58 +00:00
Jacob Tomlinson
d3d8e316bd gateway: require pairing for backend scope upgrades (#55286) 2026-03-26 17:36:44 +00:00
Jacob Tomlinson
b5d785f1a5 Gateway: require caller scope for subagent session deletion (#55281) 2026-03-26 17:34:09 +00:00
Jacob Tomlinson
ec2dbcff9a fix: keep plugin HTTP runtime scopes least-privileged (#55284) 2026-03-26 17:28:30 +00:00
Tak Hoffman
21a679e567 fix(ci): refresh plugin sdk api baseline 2026-03-26 12:18:26 -05:00
Peter Steinberger
07c41301e3 style: normalize ui slash executor formatting 2026-03-26 17:09:21 +00:00
Peter Steinberger
d6f7de392c test: dedupe ui chat seams 2026-03-26 17:07:27 +00:00
Peter Steinberger
7bb95354c4 test: dedupe matrix setup seams 2026-03-26 17:04:23 +00:00
Peter Steinberger
c12623a857 test: share plugin auth and ui storage fixtures 2026-03-26 16:55:20 +00:00
Peter Steinberger
d748ea9361 docs: note guest openclaw shim in parallels skill 2026-03-26 16:49:52 +00:00
Peter Steinberger
f0991aab57 test: add docker cli-backend smoke 2026-03-26 16:49:52 +00:00
Peter Steinberger
e1f0a85128 refactor: share auto-reply reply helpers 2026-03-26 16:48:34 +00:00
Tak Hoffman
615fe4a06b fix: preserve reset cli session linkage 2026-03-26 11:46:47 -05:00
Tak Hoffman
22f9c19a39 fix: preserve reset acp session metadata 2026-03-26 11:46:47 -05:00
Tak Hoffman
74b0a948e3 fix: preserve reset channel identity 2026-03-26 11:46:47 -05:00
Tak Hoffman
cb0a752156 fix: preserve reset session behavior config 2026-03-26 11:46:47 -05:00
Peter Steinberger
99d052a203 perf: overlap isolated channel runs with shared lane 2026-03-26 16:45:08 +00:00
Peter Steinberger
d5acd7dee5 test: share ui reconnect and storage helpers 2026-03-26 16:41:51 +00:00
Peter Steinberger
03ea6953e0 test: share gateway authz and watchdog fixtures 2026-03-26 16:36:03 +00:00
Peter Steinberger
d9a7dcec4b test: share matrix migration fixtures 2026-03-26 16:25:23 +00:00
Jacob Tomlinson
c2c136ae95 telegram: throttle repeated webhook auth guesses (#55142)
* telegram: throttle repeated webhook auth guesses

* telegram: use per-listener webhook rate limits

* config: stabilize doc baseline ordering
2026-03-26 16:19:31 +00:00
Peter Steinberger
a92fbf7d40 test: dedupe remaining agent test seams 2026-03-26 16:14:45 +00:00
Peter Steinberger
880b2fb7fd perf: enable local channel planner parallelism on node 25 2026-03-26 16:06:09 +00:00
Peter Steinberger
bac603a63e test: share subagent and policy test fixtures 2026-03-26 16:04:34 +00:00
Tak Hoffman
22520a2058 fix: preserve reset spawn context 2026-03-26 10:57:42 -05:00
Tak Hoffman
8c6be29454 fix: preserve reset elevated level 2026-03-26 10:51:01 -05:00
Tak Hoffman
b04ec4bada ci: make docker release tag-driven 2026-03-26 10:47:01 -05:00
Peter Steinberger
4ed5895637 test: dedupe config compatibility fixtures 2026-03-26 15:45:14 +00:00
Tak Hoffman
6bdf5e5634 fix: preserve reset spawn depth 2026-03-26 10:42:12 -05:00
Peter Steinberger
c4048aea41 test: share msteams monitor and pi runner fixtures 2026-03-26 15:40:51 +00:00
Peter Steinberger
339cc33cf8 perf: speed up channel test runs 2026-03-26 15:40:01 +00:00
Tak Hoffman
06b4a0a1f2 test: improve test runner help text (#55227)
* test: improve test runner help text

* test: print extension help to stdout

* test: leave extension help passthrough alone

* test: parse timing update flags in one pass
2026-03-26 10:34:14 -05:00
Tak Hoffman
471da49c59 fix: preserve reset ownership metadata 2026-03-26 10:32:09 -05:00
Jacob Tomlinson
0b4d073374 synology-chat: throttle webhook token guesses (#55141)
* synology-chat: throttle webhook token guesses

* synology-chat: keep valid webhook traffic within configured limits

* docs: refresh generated config baseline

* synology-chat: enforce lockout after repeated token failures
2026-03-26 15:30:06 +00:00
Peter Steinberger
9bc3d33b53 test: dedupe web search provider fixtures 2026-03-26 15:26:11 +00:00
Tak Hoffman
df04ca7da3 fix: preserve metadata on voice session touches 2026-03-26 10:25:18 -05:00
Peter Steinberger
65a1afb9df test: share redact and approval fixtures 2026-03-26 15:23:12 +00:00
Peter Steinberger
5e78232bc5 test: share pi compaction fixtures 2026-03-26 15:19:32 +00:00
Tak Hoffman
d69ff3c022 fix(whatsapp): unwrap quoted wrapper messages 2026-03-26 10:16:33 -05:00
Peter Steinberger
f56a25a596 test: dedupe foundry auth fixtures 2026-03-26 15:14:03 +00:00
Peter Steinberger
a4a00aa1da feat: pluginize cli inference backends 2026-03-26 15:11:15 +00:00
Tak Hoffman
24dd7aec90 fix: prefer freshest duplicate store matches 2026-03-26 10:10:05 -05:00
Peter Steinberger
5f9f08394a refactor: share matrix and telegram dedupe helpers 2026-03-26 15:08:45 +00:00
Ayaan Zaidi
4b1c37a152 fix: avoid duplicate ACP Telegram finals (#55173)
* fix: avoid duplicate final ACP text on telegram

* fix: keep ACP final fallback for non-telegram blocks

* fix: count telegram ACP block replies as success

* fix: recover ACP final fallback after block failures

* fix: settle telegram ACP block delivery before fallback

* test: isolate ACP dispatch mocks under shared workers

* fix: prefer telegram provider for ACP visibility
2026-03-26 20:37:21 +05:30
Peter Steinberger
2ed11a375a refactor: share web media loader 2026-03-26 14:55:32 +00:00
Jacob Tomlinson
5e08ce36d5 fix(bluebubbles): throttle webhook auth guesses (#55133)
* fix(bluebubbles): throttle webhook auth guesses

* test(bluebubbles): isolate attachment ssrf config

* test(bluebubbles): hoist attachment mocks

* docs: refresh bluebubbles config baseline

* fix(bluebubbles): trust proxied webhook client IPs

* fix(bluebubbles): honor trusted proxy webhook IPs

* fix(bluebubbles): honor real-ip fallback for webhooks
2026-03-26 14:54:03 +00:00
Peter Steinberger
5c3e018492 refactor: dedupe msteams graph actions 2026-03-26 14:45:53 +00:00
Tak Hoffman
a4e5b23dc3 docs: update PR template review guidance 2026-03-26 09:36:36 -05:00
Tak Hoffman
9f0305420a docs: add beta blocker contributor guidance (#55199)
* docs: add beta blocker contributor guidance

* fix: tighten beta blocker labeling and flaky config test
2026-03-26 09:31:59 -05:00
Tak Hoffman
e403899cc1 test: fix portable stderr capture and env leakage (#55184) 2026-03-26 09:31:08 -05:00
Tak Hoffman
dd46c3d75b test(memory): initialize providers in lazy manager tests 2026-03-26 09:29:07 -05:00
Tyler Yust
2513a8d852 fix(bluebubbles): refactor sendMessageBlueBubbles to use resolveBlueBubblesServerAccount and enhance private network handling in tests 2026-03-26 07:21:48 -07:00
Jacob Tomlinson
81c45976db Feishu: reject legacy raw card command payloads (#55130)
* Feishu: reject legacy raw card callbacks

* Feishu: cover legacy text card payloads

* Docs: refresh config baseline

* CI: refresh PR checks

* Feishu: limit legacy card guard scope
2026-03-26 14:17:45 +00:00
Jacob Tomlinson
11ea1f6786 Google Chat: require stable group ids (#55131)
* Google Chat: require stable group ids

* Google Chat: fail closed on deprecated room keys
2026-03-26 14:15:51 +00:00
Jacob Tomlinson
464e2c10a5 ACP: sanitize terminal tool titles (#55137)
* ACP: sanitize terminal tool titles

Co-authored-by: nexrin <268879349+nexrin@users.noreply.github.com>

* Config: refresh config baseline and stabilize restart pid test

---------

Co-authored-by: nexrin <268879349+nexrin@users.noreply.github.com>
2026-03-26 14:12:24 +00:00
Peter Steinberger
883239a560 build: prepare 2026.3.25 unreleased 2026-03-26 13:57:45 +00:00
Shakker
e3660f265c docs: sync config baseline 2026-03-26 13:35:48 +00:00
Tak Hoffman
cc7f18d6c2 fix: replace stale canonical duplicate rows 2026-03-26 08:03:24 -05:00
Tak Hoffman
fde3871ee7 fix: prefer freshest duplicate row promotion 2026-03-26 07:54:43 -05:00
Tyler Yust
cc077ef1ef fix(bluebubbles): enable group participant enrichment by default, add fallback fetch and handle field aliases 2026-03-26 05:45:41 -07:00
Tak Hoffman
68c6abe32b docs: add beta release testing guidance 2026-03-26 07:34:08 -05:00
Tak Hoffman
b529d13477 test: fix bluebubbles attachment ssrf expectations 2026-03-26 07:25:14 -05:00
Saurabh Mishra
6fbe9dd935 fix: surface provider-specific rate limit error message (#54433) (#54512)
Merged via squash.

Prepared head SHA: 755cff833c
Co-authored-by: bugkill3r <2924124+bugkill3r@users.noreply.github.com>
Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com>
Reviewed-by: @altaywtf
2026-03-26 15:16:06 +03:00
Jacob Tomlinson
2383daf5c4 Matrix: gate verification notices on DM access (#55122) 2026-03-26 11:59:20 +00:00
Tyler Yust
e43600c9e5 fix(bluebubbles): auto-allow private network for local serverUrl and add allowPrivateNetwork to channel schema 2026-03-26 04:55:46 -07:00
Jacob Tomlinson
c5415a474b fix(msteams): align feedback invoke authorization (#55108)
* msteams: align feedback invoke authorization

* msteams: fix feedback allowlist regressions

* msteams: tighten feedback group authorization
2026-03-26 11:51:43 +00:00
Jacob Tomlinson
269282ac69 Telegram: enforce DM auth for callbacks (#55112) 2026-03-26 11:42:27 +00:00
Jacob Tomlinson
d9810811b6 fix(agents): enforce session_status guard after sessionId resolution (#55105)
* fix(agents): enforce visibility guard after sessionId resolution in session_status

When a sessionId (rather than an explicit agent key) is passed to the
session_status tool, the sessionId resolution block rewrites
requestedKeyRaw to an explicit "agent:..." key.  The subsequent
visibility guard check at line 375 tested
`!requestedKeyRaw.startsWith("agent:")`, which was now always false
after resolution — skipping the visibility check entirely.

This meant a sandboxed agent could bypass visibility restrictions by
providing a sessionId instead of an explicit session key.

Fix: use the original `isExplicitAgentKey` flag (captured before
resolution) instead of re-checking the dynamic requestedKeyRaw.
This ensures the visibility guard runs for sessionId inputs while
still skipping the redundant check for inputs that were already
validated at the earlier explicit-key check (lines 281-286).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: cover session status sessionId guard

* test: align parent sessionId guard coverage

---------

Co-authored-by: Kevin Sheng <shenghuikevin@github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:34:22 +00:00
Jacob Tomlinson
5e8cb22176 Feishu: validate webhook signatures before parsing (#55083)
* Feishu: validate webhook signatures before parsing

* Scripts: allow Feishu raw body guard callsite
2026-03-26 10:29:22 +00:00
Nimrod Gutman
a3b85e1583 fix(discord): force fresh gateway reconnects (#54697)
* fix(discord): force fresh gateway reconnects

* fix(discord): harden forced reconnect teardown

* fix(discord): retry after socket drain timeouts

* fix(discord): guard forced socket teardown

* fix(discord): stop cleanly during reconnect drain
2026-03-26 12:05:00 +02:00
Altay
8564480f3e chore: add lockfile entry for extensions/microsoft-foundry 2026-03-26 12:50:33 +03:00
Tyler Yust
4c85fd8569 BlueBubbles: enrich group participants with local Contacts names (#54984)
* BlueBubbles: enrich group participants with Contacts names

* BlueBubbles: gate contact enrichment behind opt in config
2026-03-26 18:38:37 +09:00
Jacob Tomlinson
f92c92515b fix(extensions): route fetch calls through fetchWithSsrFGuard (#53929)
* fix(extensions): route fetch calls through fetchWithSsrFGuard

Replace raw fetch() with fetchWithSsrFGuard in BlueBubbles, Mattermost,
Nextcloud Talk, and Thread Ownership extensions so outbound requests go
through the shared DNS-pinning and network-policy layer.

BlueBubbles: thread allowPrivateNetwork from account config through all
fetch call sites (send, chat, reactions, history, probe, attachments,
multipart). Add _setFetchGuardForTesting hook for test overrides.

Mattermost: add guardedFetchImpl wrapper in createMattermostClient that
buffers the response body before releasing the dispatcher. Handle
null-body status codes (204/304).

Nextcloud Talk: wrap both sendMessage and sendReaction with
fetchWithSsrFGuard and try/finally release.

Thread Ownership: add fetchWithSsrFGuard and ssrfPolicyFromAllowPrivateNetwork
to the plugin SDK surface; use allowPrivateNetwork:true for the
Docker-internal forwarder.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(extensions): improve null-body handling and test harness cleanup

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(bluebubbles): default to strict SSRF policy when allowPrivateNetwork is unset

Callers that omit allowPrivateNetwork previously got undefined policy,
which caused blueBubblesFetchWithTimeout to fall through to raw fetch
and bypass the SSRF guard entirely.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(bluebubbles): thread allowPrivateNetwork through action and monitor call sites

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(mattermost,nextcloud-talk): add allowPrivateNetwork config for self-hosted/LAN deployments

* fix: regenerate config docs baseline for new allowPrivateNetwork fields

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 02:04:54 -07:00
pomelo
dad68d319b Remove Qwen OAuth integration (qwen-portal-auth) (#52709)
* Remove Qwen OAuth integration (qwen-portal-auth)

Qwen OAuth via portal.qwen.ai is being deprecated by the Qwen team due
to traffic impact on their primary Qwen Code user base. Users should
migrate to the officially supported Model Studio (Alibaba Cloud Coding
Plan) provider instead.

Ref: https://github.com/openclaw/openclaw/issues/49557

- Delete extensions/qwen-portal-auth/ plugin entirely
- Remove qwen-portal from onboarding auth choices, provider aliases,
  auto-enable list, bundled plugin defaults, and pricing cache
- Remove Qwen CLI credential sync (external-cli-sync, cli-credentials)
- Remove QWEN_OAUTH_MARKER from model auth markers
- Update docs/providers/qwen.md to redirect to Model Studio
- Update model-providers docs (EN + zh-CN) to remove Qwen OAuth section
- Regenerate config and plugin-sdk baselines
- Update all affected tests

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* Clean up residual qwen-portal references after OAuth removal

* Add migration hint for deprecated qwen-portal OAuth provider

* fix: finish qwen oauth removal follow-up

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Frank Yang <frank.ekn@gmail.com>
2026-03-26 16:32:34 +08:00
Ayaan Zaidi
83e6c12f15 build: update plugin sdk api baseline 2026-03-26 13:52:52 +05:30
kevinlin-openai
432d5f863c fix: add slack upload-file action (#54987) (thanks @kevinlin-openai)
* feat(slack): add upload-file action

Co-authored-by: Codex <noreply@openai.com>

* fix(slack): guard upload-file routing

Co-authored-by: Codex <noreply@openai.com>

* fix(slack): tighten upload-file validation

---------

Co-authored-by: kevinlin-openai <kevin@dendron.so>
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
2026-03-26 13:37:58 +05:30
Ayaan Zaidi
78584413ec docs: refresh config baseline for microsoft foundry 2026-03-26 12:45:47 +05:30
Ayaan Zaidi
7ea17963b0 fix: wire microsoft foundry into contract registry 2026-03-26 12:43:03 +05:30
wenmeng zhou
143275687a Docs: rename modelstudio.md to qwen_modelstudio.md, add Standard API endpoints (#54407)
* Docs: rename modelstudio.md to qwen_modelstudio.md, add Standard API endpoints

* refine docs

* Docs: fix broken link in providers/index.md after modelstudio rename

* Docs: add redirect from /providers/modelstudio to /providers/qwen_modelstudio

* Docs: adjust the order in index.md

* docs: rename modelstudio to qwen_modelstudio, add Standard API endpoints (#54407) (thanks @wenmengzhou)

---------

Co-authored-by: George Zhang <georgezhangtj97@gmail.com>
2026-03-26 00:11:28 -07:00
Nyanako
d72cc7a380 fix: route codex responses over websocket and preserve tool warnings (#53702) (thanks @Nanako0129)
* fix: route codex responses over websocket and suppress gated core tool warnings

* fix: rebase codex websocket patch onto main

* fix: preserve explicit alsoAllow warnings (#53702) (thanks @Nanako0129)

---------

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
2026-03-26 12:28:17 +05:30
Tyler Yust
00e932a83c fix: restore inbound image embedding for CLI routed BlueBubbles turns (#51373)
* fix(cli): hydrate prompt image refs for inbound media

* Agents: harden CLI prompt image hydration (#51373)

* test: fix CLI prompt image hydration helper mocks
2026-03-26 15:47:44 +09:00
MetaX e|acc
a16dd967da feat: Add Microsoft Foundry provider with Entra ID authentication (#51973)
* Microsoft Foundry: add native provider

* Microsoft Foundry: tighten review fixes

* Microsoft Foundry: enable by default

* Microsoft Foundry: stabilize API routing
2026-03-26 01:33:14 -05:00
Ayaan Zaidi
06de515b6c fix(plugins): skip allowlist warning for config paths 2026-03-26 11:44:23 +05:30
sudie-codes
6329edfb8d msteams: add search message action (#54832)
* msteams: add pin/unpin, list-pins, and read message actions

Wire up Graph API endpoints for message read, pin, unpin, and list-pins
in the MS Teams extension, following the same patterns as edit/delete.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* msteams: address PR review comments for pin/unpin/read actions

- Handle 204 No Content in postGraphJson (Graph mutations may return empty body)
- Strip conversation:/user: prefixes in resolveConversationPath to avoid Graph 404s
- Remove dead variable in channel pin branch
- Rename unpin param from messageId to pinnedMessageId for semantic clarity
- Accept both pinnedMessageId and messageId in unpin action handler for compat

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* msteams: resolve user targets + add User-Agent to Graph helpers

- Resolve user:<aadId> targets to actual conversation IDs via conversation
  store before Graph API calls (fixes 404 for DM-context actions)
- Add User-Agent header to postGraphJson/deleteGraphRequest for consistency
  with fetchGraphJson after rebase onto main

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* msteams: resolve DM targets to Graph chat IDs + expose pin IDs

- Prefer cached graphChatId over Bot Framework conversation IDs for user
  targets; throw descriptive error when no Graph-compatible ID is available
- Add `id` field to list-pins rows so default formatters surface the pinned
  resource ID needed for the unpin flow

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* msteams: add react and reactions (list) message actions

* msteams: add search message action via Graph API

* msteams: fix search query injection, add ConsistencyLevel header, use manual query string

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 01:09:53 -05:00
sudie-codes
8c852d86f7 msteams: fetch thread history via Graph API for channel replies (#51643)
* msteams: fetch thread history via Graph API for channel replies

* msteams: address PR #51643 review feedback

- Wrap resolveTeamGroupId Graph call in try/catch, fall back to raw
  conversationTeamId when Team.ReadBasic.All permission is missing
- Remove dead fetchChatMessages function (exported but never called)
- Add JSDoc documenting oldest-50-replies Graph API limitation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* msteams: address thread history PR review comments

* msteams: only cache team group IDs on successful Graph lookup

Avoid caching raw conversationTeamId as a Graph team GUID when the
/teams/{id} lookup fails — the raw ID may be a Bot Framework conversation
key, not a valid GUID, causing silent thread-history failures for the
entire cache TTL.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 01:09:33 -05:00
George Zhang
6cbd2d36f8 Revert "feat: add video generation core infrastructure and extend image generation parameters (#53681)" (#54943)
This reverts commit 4cb8dde894.
2026-03-25 23:00:14 -07:00
OfflynAI
e45533d568 fix(whatsapp): drop fromMe echoes in self-chat DMs using outbound ID tracking (#54570)
Merged via squash.

Prepared head SHA: dad53caf39
Co-authored-by: joelnishanth <140015627+joelnishanth@users.noreply.github.com>
Co-authored-by: mcaxtr <7562095+mcaxtr@users.noreply.github.com>
Reviewed-by: @mcaxtr
2026-03-26 02:24:24 -03:00
Neerav Makwana
6fd9d2ff38 fix: support OpenAI Codex media understanding (#54829) (thanks @neeravmakwana)
* OpenAI: register Codex media understanding provider

* fix: route codex image prompts through system instructions

* fix: add changelog for codex image tool fix (#54829) (thanks @neeravmakwana)

* fix: remove any from provider registration tests (#54829) (thanks @neeravmakwana)

---------

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
2026-03-26 10:10:11 +05:30
Ted Li
76ff0d9298 fix: restore image-tool generic provider fallback (#54858) (thanks @MonkeyLeeT)
* Image tool: restore generic provider fallback

* Image tool: cover multi-image generic fallback

* test: tighten minimax-portal image fallback coverage

* fix: restore image-tool generic provider fallback (#54858) (thanks @MonkeyLeeT)

---------

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
2026-03-26 10:07:43 +05:30
Neerav Makwana
8efc6e001e fix: auto-enable configured channel plugins in routed CLI commands (#54809) (thanks @neeravmakwana)
* CLI: auto-enable configured channel plugins in routed commands

* fix: auto-enable configured channel plugins in routed CLI commands (#54809) (thanks @neeravmakwana)

---------

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
2026-03-26 10:06:16 +05:30
1075 changed files with 34075 additions and 20150 deletions

View File

@@ -17,6 +17,11 @@ 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,6 +9,8 @@ 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:
@@ -20,6 +22,19 @@ 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,10 +221,6 @@
- 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,6 +2,8 @@
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:
@@ -63,6 +65,18 @@ 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`)
@@ -127,12 +141,6 @@ 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,8 +2,6 @@ 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]
types: [opened, synchronize, reopened, edited]
issues:
types: [opened]
types: [opened, edited]
workflow_dispatch:
inputs:
max_prs:
@@ -209,6 +209,59 @@ 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:
@@ -419,6 +472,7 @@ 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";
@@ -449,6 +503,22 @@ 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);
@@ -580,7 +650,37 @@ 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;
@@ -618,6 +718,9 @@ jobs:
await applySizeLabel(pullRequest, currentLabels, labelNames);
await applyContributorLabel(pullRequest, labelNames);
if (betaBlockerLabelExists) {
await applyBetaBlockerTitleLabel(pullRequest, labelNames);
}
processed += 1;
}
@@ -719,3 +822,56 @@ 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

@@ -1,108 +0,0 @@
name: Thread Expansion Experiment
on:
pull_request:
types: [opened, reopened, synchronize, ready_for_review]
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.event_name == 'pull_request' && format('{0}-{1}', github.workflow, github.event.pull_request.number) || format('{0}-{1}', github.workflow, github.run_id) }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
jobs:
compare:
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
strategy:
fail-fast: false
matrix:
include:
- label: linux
runner: blacksmith-16vcpu-ubuntu-2404
mode: forks
- label: linux
runner: blacksmith-16vcpu-ubuntu-2404
mode: threads
- label: macos
runner: macos-latest
mode: forks
- label: macos
runner: macos-latest
mode: threads
name: compare (${{ matrix.label }}, ${{ matrix.mode }})
runs-on: ${{ matrix.runner }}
timeout-minutes: 20
env:
EXPERIMENT_MODE: ${{ matrix.mode }}
steps:
- name: Checkout
uses: actions/checkout@v6
with:
persist-credentials: false
submodules: false
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
install-bun: "false"
use-sticky-disk: "false"
- name: Show runtime info
run: |
node -v
pnpm -v
node -e 'const os=require("node:os"); console.log(JSON.stringify({ platform: process.platform, cpuCount: os.cpus().length, totalMemGiB: Math.round((os.totalmem() / 1024 / 1024 / 1024) * 10) / 10, loadavg: os.loadavg() }, null, 2));'
- name: Run thread expansion experiment
shell: bash
run: |
set -euo pipefail
case "$EXPERIMENT_MODE" in
forks)
export OPENCLAW_TEST_FORCE_FORKS=1
POOL="forks"
;;
threads)
export OPENCLAW_TEST_FORCE_THREADS=1
POOL="threads"
;;
*)
echo "Unsupported EXPERIMENT_MODE=$EXPERIMENT_MODE" >&2
exit 1
;;
esac
POLICY_FILES=(
src/commands/agents.test.ts
src/commands/text-format.test.ts
src/auto-reply/chunk.test.ts
)
EVIDENCE_FILES=(
src/commands/backup.test.ts
src/auto-reply/reply/commands-acp/install-hints.test.ts
src/agents/pi-extensions/compaction-safeguard.test.ts
)
echo "## ${RUNNER_OS} / ${EXPERIMENT_MODE}" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "- Wrapper policy sample files: \`${POLICY_FILES[*]}\`" >> "$GITHUB_STEP_SUMMARY"
echo "- Direct evidence sample files: \`${EVIDENCE_FILES[*]}\`" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "=== Wrapper policy sample (${EXPERIMENT_MODE}) ==="
SECONDS=0
OPENCLAW_TEST_SHOW_POOL_DECISION=1 node scripts/test-parallel.mjs "${POLICY_FILES[@]}"
wrapper_elapsed=$SECONDS
echo "- wrapper policy sample (${EXPERIMENT_MODE}): ${wrapper_elapsed}s" >> "$GITHUB_STEP_SUMMARY"
echo "=== Direct vitest evidence sample (${POOL}) ==="
SECONDS=0
pnpm vitest run --config vitest.config.ts "--pool=${POOL}" "${EVIDENCE_FILES[@]}"
direct_elapsed=$SECONDS
echo "- direct evidence sample (${POOL}): ${direct_elapsed}s" >> "$GITHUB_STEP_SUMMARY"

View File

@@ -6,32 +6,61 @@ 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.
- 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: surface safeguard-specific cancel reasons and relabel benign manual `/compact` no-op cases as skipped instead of failed. (#51072) Thanks @afurm.
- 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.
- 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.
### 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.
- 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
- 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.
- 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/compaction: trigger timeout recovery compaction before retrying high-context LLM timeouts so embedded runs stop repeating oversized requests. (#46417) thanks @joeykrug.
- 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.
- 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/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.
- 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.
## 2026.3.24
@@ -75,7 +104,6 @@ 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
@@ -155,9 +183,6 @@ 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
@@ -407,6 +432,16 @@ 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.
@@ -438,8 +473,6 @@ 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.
@@ -492,8 +525,6 @@ 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.
@@ -513,9 +544,6 @@ 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.
@@ -554,8 +582,6 @@ 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 = 2026032400
versionName = "2026.3.24"
versionCode = 2026032500
versionName = "2026.3.25"
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.24
OPENCLAW_MARKETING_VERSION = 2026.3.24
OPENCLAW_BUILD_VERSION = 2026032490
OPENCLAW_GATEWAY_VERSION = 2026.3.25
OPENCLAW_MARKETING_VERSION = 2026.3.25
OPENCLAW_BUILD_VERSION = 202603250
#include? "../build/Version.xcconfig"

View File

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

View File

@@ -8,6 +8,11 @@ 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"
@@ -451,17 +456,22 @@ actor TalkModeRuntime {
private func playAssistant(text: String) async {
guard let input = await self.preparePlaybackInput(text: text) else { return }
do {
if let apiKey = input.apiKey, !apiKey.isEmpty, let voiceId = input.voiceId {
switch Self.playbackPlan(apiKey: input.apiKey, voiceId: input.voiceId) {
case let .elevenLabsThenSystemVoice(apiKey, voiceId):
do {
try await self.playElevenLabs(input: input, apiKey: apiKey, voiceId: voiceId)
} else {
try await self.playSystemVoice(input: input)
} 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)")
}
}
} catch {
self.ttsLogger
.error(
"talk TTS failed: \(error.localizedDescription, privacy: .public); " +
"falling back to system voice")
case .systemVoiceOnly:
do {
try await self.playSystemVoice(input: input)
} catch {
@@ -475,6 +485,13 @@ 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
@@ -664,9 +681,12 @@ 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: input.language)
language: ttsLanguage)
self.ttsLogger.info("talk system voice done")
}

View File

@@ -11,4 +11,13 @@ 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 estimatedSeconds = max(3.0, min(180.0, Double(trimmed.count) * 0.08))
let watchdogTimeout = Self.watchdogTimeoutSeconds(text: trimmed, language: language ?? utterance.voice?.language)
self.watchdog?.cancel()
self.watchdog = Task { @MainActor [weak self] in
guard let self else { return }
try? await Task.sleep(nanoseconds: UInt64(estimatedSeconds * 1_000_000_000))
try? await Task.sleep(nanoseconds: UInt64(watchdogTimeout * 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 \(estimatedSeconds)s",
NSLocalizedDescriptionKey: "system TTS timed out after \(watchdogTimeout)s",
]))
}
@@ -83,6 +83,37 @@ 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

@@ -0,0 +1,44 @@
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":5639}
{"generatedBy":"scripts/generate-config-doc-baseline.ts","recordType":"meta","totalPaths":5531}
{"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,6 +746,7 @@
{"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}
@@ -795,6 +796,7 @@
{"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}
@@ -1027,47 +1029,8 @@
{"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}
@@ -1078,18 +1041,16 @@
{"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}
@@ -1294,47 +1255,8 @@
{"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}
@@ -1345,18 +1267,16 @@
{"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}
@@ -2091,6 +2011,7 @@
{"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}
@@ -2139,6 +2060,7 @@
{"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}
@@ -2284,6 +2206,7 @@
{"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}
@@ -2340,6 +2263,7 @@
{"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}
@@ -3940,47 +3864,8 @@
{"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}
@@ -3991,18 +3876,16 @@
{"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}
@@ -4153,6 +4036,15 @@
{"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}
@@ -4492,6 +4384,15 @@
{"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}
@@ -4682,15 +4583,6 @@
{"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,33 +5,6 @@
"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",
@@ -186,7 +159,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\";",
"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\";",
"exportName": "ChannelMessageActionName",
"kind": "type",
"source": {
@@ -257,6 +230,24 @@
"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",
@@ -352,7 +343,7 @@
"exportName": "ImageGenerationProvider",
"kind": "type",
"source": {
"line": 72,
"line": 66,
"path": "src/image-generation/types.ts"
}
},
@@ -379,7 +370,7 @@
"exportName": "ImageGenerationResult",
"kind": "type",
"source": {
"line": 42,
"line": 36,
"path": "src/image-generation/types.ts"
}
},
@@ -397,7 +388,7 @@
"exportName": "MediaUnderstandingProviderPlugin",
"kind": "type",
"source": {
"line": 952,
"line": 969,
"path": "src/plugins/types.ts"
}
},
@@ -415,7 +406,7 @@
"exportName": "OpenClawPluginApi",
"kind": "type",
"source": {
"line": 1316,
"line": 1390,
"path": "src/plugins/types.ts"
}
},
@@ -424,7 +415,7 @@
"exportName": "OpenClawPluginConfigSchema",
"kind": "type",
"source": {
"line": 89,
"line": 95,
"path": "src/plugins/types.ts"
}
},
@@ -433,7 +424,7 @@
"exportName": "PluginLogger",
"kind": "type",
"source": {
"line": 60,
"line": 66,
"path": "src/plugins/types.ts"
}
},
@@ -451,7 +442,7 @@
"exportName": "ProviderAuthContext",
"kind": "type",
"source": {
"line": 156,
"line": 166,
"path": "src/plugins/types.ts"
}
},
@@ -460,7 +451,7 @@
"exportName": "ProviderAuthResult",
"kind": "type",
"source": {
"line": 141,
"line": 151,
"path": "src/plugins/types.ts"
}
},
@@ -469,7 +460,7 @@
"exportName": "ProviderRuntimeModel",
"kind": "type",
"source": {
"line": 296,
"line": 306,
"path": "src/plugins/types.ts"
}
},
@@ -523,7 +514,7 @@
"exportName": "SpeechProviderPlugin",
"kind": "type",
"source": {
"line": 934,
"line": 944,
"path": "src/plugins/types.ts"
}
},
@@ -905,7 +896,7 @@
"exportName": "createMessageToolButtonsSchema",
"kind": "function",
"source": {
"line": 11,
"line": 12,
"path": "src/plugin-sdk/channel-actions.ts"
}
},
@@ -914,7 +905,7 @@
"exportName": "createMessageToolCardSchema",
"kind": "function",
"source": {
"line": 29,
"line": 30,
"path": "src/plugin-sdk/channel-actions.ts"
}
},
@@ -936,6 +927,15 @@
"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,6 +944,15 @@
"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",
@@ -1112,7 +1121,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\";",
"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\";",
"exportName": "ChannelMessageActionName",
"kind": "type",
"source": {
@@ -1496,7 +1505,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\"];",
"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\"];",
"exportName": "CHANNEL_MESSAGE_ACTION_NAMES",
"kind": "const",
"source": {
@@ -1892,7 +1901,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\";",
"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\";",
"exportName": "ChannelMessageActionName",
"kind": "type",
"source": {
@@ -2377,7 +2386,7 @@
"exportName": "buildCommandsPaginationKeyboard",
"kind": "function",
"source": {
"line": 196,
"line": 175,
"path": "src/auto-reply/reply/commands-info.ts"
}
},
@@ -3009,7 +3018,7 @@
"exportName": "buildChannelOutboundSessionRoute",
"kind": "function",
"source": {
"line": 163,
"line": 161,
"path": "src/plugin-sdk/core.ts"
}
},
@@ -3045,7 +3054,7 @@
"exportName": "createChannelPluginBase",
"kind": "function",
"source": {
"line": 437,
"line": 435,
"path": "src/plugin-sdk/core.ts"
}
},
@@ -3054,7 +3063,7 @@
"exportName": "createChatChannelPlugin",
"kind": "function",
"source": {
"line": 414,
"line": 412,
"path": "src/plugin-sdk/core.ts"
}
},
@@ -3063,7 +3072,7 @@
"exportName": "defineChannelPluginEntry",
"kind": "function",
"source": {
"line": 246,
"line": 244,
"path": "src/plugin-sdk/core.ts"
}
},
@@ -3072,7 +3081,7 @@
"exportName": "definePluginEntry",
"kind": "function",
"source": {
"line": 88,
"line": 90,
"path": "src/plugin-sdk/plugin-entry.ts"
}
},
@@ -3081,7 +3090,7 @@
"exportName": "defineSetupPluginEntry",
"kind": "function",
"source": {
"line": 277,
"line": 275,
"path": "src/plugin-sdk/core.ts"
}
},
@@ -3279,7 +3288,7 @@
"exportName": "stripChannelTargetPrefix",
"kind": "function",
"source": {
"line": 143,
"line": 141,
"path": "src/plugin-sdk/core.ts"
}
},
@@ -3288,7 +3297,7 @@
"exportName": "stripTargetKindPrefix",
"kind": "function",
"source": {
"line": 155,
"line": 153,
"path": "src/plugin-sdk/core.ts"
}
},
@@ -3360,7 +3369,7 @@
"exportName": "ChannelOutboundSessionRouteParams",
"kind": "type",
"source": {
"line": 138,
"line": 136,
"path": "src/plugin-sdk/core.ts"
}
},
@@ -3396,7 +3405,7 @@
"exportName": "MediaUnderstandingProviderPlugin",
"kind": "type",
"source": {
"line": 952,
"line": 969,
"path": "src/plugins/types.ts"
}
},
@@ -3414,7 +3423,7 @@
"exportName": "OpenClawPluginApi",
"kind": "type",
"source": {
"line": 1316,
"line": 1390,
"path": "src/plugins/types.ts"
}
},
@@ -3423,7 +3432,7 @@
"exportName": "OpenClawPluginCommandDefinition",
"kind": "type",
"source": {
"line": 1070,
"line": 1108,
"path": "src/plugins/types.ts"
}
},
@@ -3432,7 +3441,7 @@
"exportName": "OpenClawPluginConfigSchema",
"kind": "type",
"source": {
"line": 89,
"line": 95,
"path": "src/plugins/types.ts"
}
},
@@ -3441,7 +3450,7 @@
"exportName": "OpenClawPluginDefinition",
"kind": "type",
"source": {
"line": 1298,
"line": 1372,
"path": "src/plugins/types.ts"
}
},
@@ -3450,7 +3459,7 @@
"exportName": "OpenClawPluginService",
"kind": "type",
"source": {
"line": 1287,
"line": 1339,
"path": "src/plugins/types.ts"
}
},
@@ -3459,7 +3468,7 @@
"exportName": "OpenClawPluginServiceContext",
"kind": "type",
"source": {
"line": 1279,
"line": 1331,
"path": "src/plugins/types.ts"
}
},
@@ -3468,7 +3477,7 @@
"exportName": "OpenClawPluginToolContext",
"kind": "type",
"source": {
"line": 104,
"line": 110,
"path": "src/plugins/types.ts"
}
},
@@ -3477,7 +3486,7 @@
"exportName": "OpenClawPluginToolFactory",
"kind": "type",
"source": {
"line": 121,
"line": 131,
"path": "src/plugins/types.ts"
}
},
@@ -3486,7 +3495,7 @@
"exportName": "PluginCommandContext",
"kind": "type",
"source": {
"line": 968,
"line": 984,
"path": "src/plugins/types.ts"
}
},
@@ -3495,7 +3504,7 @@
"exportName": "PluginInteractiveTelegramHandlerContext",
"kind": "type",
"source": {
"line": 1099,
"line": 1145,
"path": "src/plugins/types.ts"
}
},
@@ -3504,7 +3513,7 @@
"exportName": "PluginLogger",
"kind": "type",
"source": {
"line": 60,
"line": 66,
"path": "src/plugins/types.ts"
}
},
@@ -3522,7 +3531,7 @@
"exportName": "ProviderAugmentModelCatalogContext",
"kind": "type",
"source": {
"line": 572,
"line": 582,
"path": "src/plugins/types.ts"
}
},
@@ -3531,7 +3540,7 @@
"exportName": "ProviderAuthContext",
"kind": "type",
"source": {
"line": 156,
"line": 166,
"path": "src/plugins/types.ts"
}
},
@@ -3540,7 +3549,7 @@
"exportName": "ProviderAuthDoctorHintContext",
"kind": "type",
"source": {
"line": 447,
"line": 457,
"path": "src/plugins/types.ts"
}
},
@@ -3549,7 +3558,7 @@
"exportName": "ProviderAuthMethod",
"kind": "type",
"source": {
"line": 234,
"line": 244,
"path": "src/plugins/types.ts"
}
},
@@ -3558,7 +3567,7 @@
"exportName": "ProviderAuthMethodNonInteractiveContext",
"kind": "type",
"source": {
"line": 218,
"line": 228,
"path": "src/plugins/types.ts"
}
},
@@ -3567,7 +3576,7 @@
"exportName": "ProviderAuthResult",
"kind": "type",
"source": {
"line": 141,
"line": 151,
"path": "src/plugins/types.ts"
}
},
@@ -3576,7 +3585,7 @@
"exportName": "ProviderBuildMissingAuthMessageContext",
"kind": "type",
"source": {
"line": 500,
"line": 510,
"path": "src/plugins/types.ts"
}
},
@@ -3585,7 +3594,7 @@
"exportName": "ProviderBuiltInModelSuppressionContext",
"kind": "type",
"source": {
"line": 516,
"line": 526,
"path": "src/plugins/types.ts"
}
},
@@ -3594,7 +3603,7 @@
"exportName": "ProviderBuiltInModelSuppressionResult",
"kind": "type",
"source": {
"line": 525,
"line": 535,
"path": "src/plugins/types.ts"
}
},
@@ -3603,7 +3612,7 @@
"exportName": "ProviderCacheTtlEligibilityContext",
"kind": "type",
"source": {
"line": 488,
"line": 498,
"path": "src/plugins/types.ts"
}
},
@@ -3612,7 +3621,7 @@
"exportName": "ProviderCatalogContext",
"kind": "type",
"source": {
"line": 255,
"line": 265,
"path": "src/plugins/types.ts"
}
},
@@ -3621,7 +3630,7 @@
"exportName": "ProviderCatalogResult",
"kind": "type",
"source": {
"line": 278,
"line": 288,
"path": "src/plugins/types.ts"
}
},
@@ -3630,7 +3639,7 @@
"exportName": "ProviderDefaultThinkingPolicyContext",
"kind": "type",
"source": {
"line": 549,
"line": 559,
"path": "src/plugins/types.ts"
}
},
@@ -3639,7 +3648,7 @@
"exportName": "ProviderDiscoveryContext",
"kind": "type",
"source": {
"line": 588,
"line": 598,
"path": "src/plugins/types.ts"
}
},
@@ -3648,7 +3657,7 @@
"exportName": "ProviderFetchUsageSnapshotContext",
"kind": "type",
"source": {
"line": 428,
"line": 438,
"path": "src/plugins/types.ts"
}
},
@@ -3657,7 +3666,7 @@
"exportName": "ProviderModernModelPolicyContext",
"kind": "type",
"source": {
"line": 559,
"line": 569,
"path": "src/plugins/types.ts"
}
},
@@ -3666,7 +3675,7 @@
"exportName": "ProviderNormalizeResolvedModelContext",
"kind": "type",
"source": {
"line": 339,
"line": 349,
"path": "src/plugins/types.ts"
}
},
@@ -3675,7 +3684,7 @@
"exportName": "ProviderPreparedRuntimeAuth",
"kind": "type",
"source": {
"line": 375,
"line": 385,
"path": "src/plugins/types.ts"
}
},
@@ -3684,7 +3693,7 @@
"exportName": "ProviderPrepareDynamicModelContext",
"kind": "type",
"source": {
"line": 330,
"line": 340,
"path": "src/plugins/types.ts"
}
},
@@ -3693,7 +3702,7 @@
"exportName": "ProviderPrepareExtraParamsContext",
"kind": "type",
"source": {
"line": 461,
"line": 471,
"path": "src/plugins/types.ts"
}
},
@@ -3702,7 +3711,7 @@
"exportName": "ProviderPrepareRuntimeAuthContext",
"kind": "type",
"source": {
"line": 354,
"line": 364,
"path": "src/plugins/types.ts"
}
},
@@ -3711,7 +3720,7 @@
"exportName": "ProviderResolvedUsageAuth",
"kind": "type",
"source": {
"line": 415,
"line": 425,
"path": "src/plugins/types.ts"
}
},
@@ -3720,7 +3729,7 @@
"exportName": "ProviderResolveDynamicModelContext",
"kind": "type",
"source": {
"line": 313,
"line": 323,
"path": "src/plugins/types.ts"
}
},
@@ -3729,7 +3738,7 @@
"exportName": "ProviderResolveUsageAuthContext",
"kind": "type",
"source": {
"line": 396,
"line": 406,
"path": "src/plugins/types.ts"
}
},
@@ -3738,7 +3747,7 @@
"exportName": "ProviderRuntimeModel",
"kind": "type",
"source": {
"line": 296,
"line": 306,
"path": "src/plugins/types.ts"
}
},
@@ -3747,7 +3756,7 @@
"exportName": "ProviderThinkingPolicyContext",
"kind": "type",
"source": {
"line": 537,
"line": 547,
"path": "src/plugins/types.ts"
}
},
@@ -3765,7 +3774,7 @@
"exportName": "ProviderWrapStreamFnContext",
"kind": "type",
"source": {
"line": 478,
"line": 488,
"path": "src/plugins/types.ts"
}
},
@@ -3810,7 +3819,7 @@
"exportName": "SpeechProviderPlugin",
"kind": "type",
"source": {
"line": 934,
"line": 944,
"path": "src/plugins/types.ts"
}
},
@@ -3875,7 +3884,7 @@
"exportName": "definePluginEntry",
"kind": "function",
"source": {
"line": 88,
"line": 90,
"path": "src/plugin-sdk/plugin-entry.ts"
}
},
@@ -3902,7 +3911,7 @@
"exportName": "MediaUnderstandingProviderPlugin",
"kind": "type",
"source": {
"line": 952,
"line": 969,
"path": "src/plugins/types.ts"
}
},
@@ -3920,7 +3929,7 @@
"exportName": "OpenClawPluginApi",
"kind": "type",
"source": {
"line": 1316,
"line": 1390,
"path": "src/plugins/types.ts"
}
},
@@ -3929,7 +3938,7 @@
"exportName": "OpenClawPluginCommandDefinition",
"kind": "type",
"source": {
"line": 1070,
"line": 1108,
"path": "src/plugins/types.ts"
}
},
@@ -3938,7 +3947,7 @@
"exportName": "OpenClawPluginConfigSchema",
"kind": "type",
"source": {
"line": 89,
"line": 95,
"path": "src/plugins/types.ts"
}
},
@@ -3947,7 +3956,7 @@
"exportName": "OpenClawPluginDefinition",
"kind": "type",
"source": {
"line": 1298,
"line": 1372,
"path": "src/plugins/types.ts"
}
},
@@ -3956,7 +3965,7 @@
"exportName": "OpenClawPluginService",
"kind": "type",
"source": {
"line": 1287,
"line": 1339,
"path": "src/plugins/types.ts"
}
},
@@ -3965,7 +3974,25 @@
"exportName": "OpenClawPluginServiceContext",
"kind": "type",
"source": {
"line": 1279,
"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,
"path": "src/plugins/types.ts"
}
},
@@ -3974,7 +4001,7 @@
"exportName": "PluginCommandContext",
"kind": "type",
"source": {
"line": 968,
"line": 984,
"path": "src/plugins/types.ts"
}
},
@@ -3983,7 +4010,7 @@
"exportName": "PluginInteractiveTelegramHandlerContext",
"kind": "type",
"source": {
"line": 1099,
"line": 1145,
"path": "src/plugins/types.ts"
}
},
@@ -3992,7 +4019,7 @@
"exportName": "PluginLogger",
"kind": "type",
"source": {
"line": 60,
"line": 66,
"path": "src/plugins/types.ts"
}
},
@@ -4001,7 +4028,7 @@
"exportName": "ProviderAugmentModelCatalogContext",
"kind": "type",
"source": {
"line": 572,
"line": 582,
"path": "src/plugins/types.ts"
}
},
@@ -4010,7 +4037,7 @@
"exportName": "ProviderAuthContext",
"kind": "type",
"source": {
"line": 156,
"line": 166,
"path": "src/plugins/types.ts"
}
},
@@ -4019,7 +4046,7 @@
"exportName": "ProviderAuthDoctorHintContext",
"kind": "type",
"source": {
"line": 447,
"line": 457,
"path": "src/plugins/types.ts"
}
},
@@ -4028,7 +4055,7 @@
"exportName": "ProviderAuthMethod",
"kind": "type",
"source": {
"line": 234,
"line": 244,
"path": "src/plugins/types.ts"
}
},
@@ -4037,7 +4064,7 @@
"exportName": "ProviderAuthMethodNonInteractiveContext",
"kind": "type",
"source": {
"line": 218,
"line": 228,
"path": "src/plugins/types.ts"
}
},
@@ -4046,7 +4073,7 @@
"exportName": "ProviderAuthResult",
"kind": "type",
"source": {
"line": 141,
"line": 151,
"path": "src/plugins/types.ts"
}
},
@@ -4055,7 +4082,7 @@
"exportName": "ProviderBuildMissingAuthMessageContext",
"kind": "type",
"source": {
"line": 500,
"line": 510,
"path": "src/plugins/types.ts"
}
},
@@ -4064,7 +4091,7 @@
"exportName": "ProviderBuiltInModelSuppressionContext",
"kind": "type",
"source": {
"line": 516,
"line": 526,
"path": "src/plugins/types.ts"
}
},
@@ -4073,7 +4100,7 @@
"exportName": "ProviderBuiltInModelSuppressionResult",
"kind": "type",
"source": {
"line": 525,
"line": 535,
"path": "src/plugins/types.ts"
}
},
@@ -4082,7 +4109,7 @@
"exportName": "ProviderCacheTtlEligibilityContext",
"kind": "type",
"source": {
"line": 488,
"line": 498,
"path": "src/plugins/types.ts"
}
},
@@ -4091,7 +4118,7 @@
"exportName": "ProviderCatalogContext",
"kind": "type",
"source": {
"line": 255,
"line": 265,
"path": "src/plugins/types.ts"
}
},
@@ -4100,7 +4127,7 @@
"exportName": "ProviderCatalogResult",
"kind": "type",
"source": {
"line": 278,
"line": 288,
"path": "src/plugins/types.ts"
}
},
@@ -4109,7 +4136,7 @@
"exportName": "ProviderDefaultThinkingPolicyContext",
"kind": "type",
"source": {
"line": 549,
"line": 559,
"path": "src/plugins/types.ts"
}
},
@@ -4118,7 +4145,7 @@
"exportName": "ProviderDiscoveryContext",
"kind": "type",
"source": {
"line": 588,
"line": 598,
"path": "src/plugins/types.ts"
}
},
@@ -4127,7 +4154,7 @@
"exportName": "ProviderFetchUsageSnapshotContext",
"kind": "type",
"source": {
"line": 428,
"line": 438,
"path": "src/plugins/types.ts"
}
},
@@ -4136,7 +4163,7 @@
"exportName": "ProviderModernModelPolicyContext",
"kind": "type",
"source": {
"line": 559,
"line": 569,
"path": "src/plugins/types.ts"
}
},
@@ -4145,7 +4172,7 @@
"exportName": "ProviderNormalizeResolvedModelContext",
"kind": "type",
"source": {
"line": 339,
"line": 349,
"path": "src/plugins/types.ts"
}
},
@@ -4154,7 +4181,7 @@
"exportName": "ProviderPreparedRuntimeAuth",
"kind": "type",
"source": {
"line": 375,
"line": 385,
"path": "src/plugins/types.ts"
}
},
@@ -4163,7 +4190,7 @@
"exportName": "ProviderPrepareDynamicModelContext",
"kind": "type",
"source": {
"line": 330,
"line": 340,
"path": "src/plugins/types.ts"
}
},
@@ -4172,7 +4199,7 @@
"exportName": "ProviderPrepareExtraParamsContext",
"kind": "type",
"source": {
"line": 461,
"line": 471,
"path": "src/plugins/types.ts"
}
},
@@ -4181,7 +4208,7 @@
"exportName": "ProviderPrepareRuntimeAuthContext",
"kind": "type",
"source": {
"line": 354,
"line": 364,
"path": "src/plugins/types.ts"
}
},
@@ -4190,7 +4217,7 @@
"exportName": "ProviderResolvedUsageAuth",
"kind": "type",
"source": {
"line": 415,
"line": 425,
"path": "src/plugins/types.ts"
}
},
@@ -4199,7 +4226,7 @@
"exportName": "ProviderResolveDynamicModelContext",
"kind": "type",
"source": {
"line": 313,
"line": 323,
"path": "src/plugins/types.ts"
}
},
@@ -4208,7 +4235,7 @@
"exportName": "ProviderResolveUsageAuthContext",
"kind": "type",
"source": {
"line": 396,
"line": 406,
"path": "src/plugins/types.ts"
}
},
@@ -4217,7 +4244,7 @@
"exportName": "ProviderRuntimeModel",
"kind": "type",
"source": {
"line": 296,
"line": 306,
"path": "src/plugins/types.ts"
}
},
@@ -4226,7 +4253,7 @@
"exportName": "ProviderThinkingPolicyContext",
"kind": "type",
"source": {
"line": 537,
"line": 547,
"path": "src/plugins/types.ts"
}
},
@@ -4235,7 +4262,7 @@
"exportName": "ProviderWrapStreamFnContext",
"kind": "type",
"source": {
"line": 478,
"line": 488,
"path": "src/plugins/types.ts"
}
},
@@ -4244,7 +4271,7 @@
"exportName": "SpeechProviderPlugin",
"kind": "type",
"source": {
"line": 934,
"line": 944,
"path": "src/plugins/types.ts"
}
}
@@ -5088,6 +5115,15 @@
"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,7 +1,4 @@
{"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"}
@@ -19,7 +16,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\";","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\" | \"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 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"}
@@ -27,6 +24,8 @@
{"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"}
@@ -37,26 +36,26 @@
{"declaration":"export type DiagnosticEventPayload = DiagnosticEventPayload;","entrypoint":"index","exportName":"DiagnosticEventPayload","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":150,"sourcePath":"src/infra/diagnostic-events.ts"}
{"declaration":"export type GeneratedImageAsset = GeneratedImageAsset;","entrypoint":"index","exportName":"GeneratedImageAsset","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":4,"sourcePath":"src/image-generation/types.ts"}
{"declaration":"export type HookEntry = HookEntry;","entrypoint":"index","exportName":"HookEntry","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":47,"sourcePath":"src/hooks/types.ts"}
{"declaration":"export type ImageGenerationProvider = ImageGenerationProvider;","entrypoint":"index","exportName":"ImageGenerationProvider","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":72,"sourcePath":"src/image-generation/types.ts"}
{"declaration":"export type ImageGenerationProvider = ImageGenerationProvider;","entrypoint":"index","exportName":"ImageGenerationProvider","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":66,"sourcePath":"src/image-generation/types.ts"}
{"declaration":"export type ImageGenerationRequest = ImageGenerationRequest;","entrypoint":"index","exportName":"ImageGenerationRequest","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":21,"sourcePath":"src/image-generation/types.ts"}
{"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":42,"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":952,"sourcePath":"src/plugins/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 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":1316,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginConfigSchema = OpenClawPluginConfigSchema;","entrypoint":"index","exportName":"OpenClawPluginConfigSchema","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":89,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type PluginLogger = PluginLogger;","entrypoint":"index","exportName":"PluginLogger","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":60,"sourcePath":"src/plugins/types.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 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":156,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthResult = ProviderAuthResult;","entrypoint":"index","exportName":"ProviderAuthResult","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":141,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderRuntimeModel = ProviderRuntimeModel;","entrypoint":"index","exportName":"ProviderRuntimeModel","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":296,"sourcePath":"src/plugins/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 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":934,"sourcePath":"src/plugins/types.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 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"}
@@ -98,11 +97,13 @@
{"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":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 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 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"}
@@ -121,7 +122,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\";","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\" | \"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 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"}
@@ -163,7 +164,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\"];","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\", \"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_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"}
@@ -207,7 +208,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\";","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\" | \"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 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"}
@@ -260,7 +261,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":196,"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":175,"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"}
@@ -330,15 +331,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":163,"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":161,"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":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 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 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"}
@@ -360,8 +361,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":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 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 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"}
@@ -369,105 +370,107 @@
{"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":138,"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":136,"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":952,"sourcePath":"src/plugins/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 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":1316,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginCommandDefinition = OpenClawPluginCommandDefinition;","entrypoint":"core","exportName":"OpenClawPluginCommandDefinition","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1070,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginConfigSchema = OpenClawPluginConfigSchema;","entrypoint":"core","exportName":"OpenClawPluginConfigSchema","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":89,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginDefinition = OpenClawPluginDefinition;","entrypoint":"core","exportName":"OpenClawPluginDefinition","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1298,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginService = OpenClawPluginService;","entrypoint":"core","exportName":"OpenClawPluginService","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1287,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginServiceContext = OpenClawPluginServiceContext;","entrypoint":"core","exportName":"OpenClawPluginServiceContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1279,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginToolContext = OpenClawPluginToolContext;","entrypoint":"core","exportName":"OpenClawPluginToolContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":104,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginToolFactory = OpenClawPluginToolFactory;","entrypoint":"core","exportName":"OpenClawPluginToolFactory","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":121,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type PluginCommandContext = PluginCommandContext;","entrypoint":"core","exportName":"PluginCommandContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":968,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type PluginInteractiveTelegramHandlerContext = PluginInteractiveTelegramHandlerContext;","entrypoint":"core","exportName":"PluginInteractiveTelegramHandlerContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1099,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type PluginLogger = PluginLogger;","entrypoint":"core","exportName":"PluginLogger","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":60,"sourcePath":"src/plugins/types.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 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":572,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthContext = ProviderAuthContext;","entrypoint":"core","exportName":"ProviderAuthContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":156,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthDoctorHintContext = ProviderAuthDoctorHintContext;","entrypoint":"core","exportName":"ProviderAuthDoctorHintContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":447,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthMethod = ProviderAuthMethod;","entrypoint":"core","exportName":"ProviderAuthMethod","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":234,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthMethodNonInteractiveContext = ProviderAuthMethodNonInteractiveContext;","entrypoint":"core","exportName":"ProviderAuthMethodNonInteractiveContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":218,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthResult = ProviderAuthResult;","entrypoint":"core","exportName":"ProviderAuthResult","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":141,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderBuildMissingAuthMessageContext = ProviderBuildMissingAuthMessageContext;","entrypoint":"core","exportName":"ProviderBuildMissingAuthMessageContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":500,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderBuiltInModelSuppressionContext = ProviderBuiltInModelSuppressionContext;","entrypoint":"core","exportName":"ProviderBuiltInModelSuppressionContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":516,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderBuiltInModelSuppressionResult = ProviderBuiltInModelSuppressionResult;","entrypoint":"core","exportName":"ProviderBuiltInModelSuppressionResult","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":525,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderCacheTtlEligibilityContext = ProviderCacheTtlEligibilityContext;","entrypoint":"core","exportName":"ProviderCacheTtlEligibilityContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":488,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderCatalogContext = ProviderCatalogContext;","entrypoint":"core","exportName":"ProviderCatalogContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":255,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderCatalogResult = ProviderCatalogResult;","entrypoint":"core","exportName":"ProviderCatalogResult","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":278,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderDefaultThinkingPolicyContext = ProviderDefaultThinkingPolicyContext;","entrypoint":"core","exportName":"ProviderDefaultThinkingPolicyContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":549,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderDiscoveryContext = ProviderCatalogContext;","entrypoint":"core","exportName":"ProviderDiscoveryContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":588,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderFetchUsageSnapshotContext = ProviderFetchUsageSnapshotContext;","entrypoint":"core","exportName":"ProviderFetchUsageSnapshotContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":428,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderModernModelPolicyContext = ProviderModernModelPolicyContext;","entrypoint":"core","exportName":"ProviderModernModelPolicyContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":559,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderNormalizeResolvedModelContext = ProviderNormalizeResolvedModelContext;","entrypoint":"core","exportName":"ProviderNormalizeResolvedModelContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":339,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderPreparedRuntimeAuth = ProviderPreparedRuntimeAuth;","entrypoint":"core","exportName":"ProviderPreparedRuntimeAuth","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":375,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderPrepareDynamicModelContext = ProviderResolveDynamicModelContext;","entrypoint":"core","exportName":"ProviderPrepareDynamicModelContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":330,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderPrepareExtraParamsContext = ProviderPrepareExtraParamsContext;","entrypoint":"core","exportName":"ProviderPrepareExtraParamsContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":461,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderPrepareRuntimeAuthContext = ProviderPrepareRuntimeAuthContext;","entrypoint":"core","exportName":"ProviderPrepareRuntimeAuthContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":354,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderResolvedUsageAuth = ProviderResolvedUsageAuth;","entrypoint":"core","exportName":"ProviderResolvedUsageAuth","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":415,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderResolveDynamicModelContext = ProviderResolveDynamicModelContext;","entrypoint":"core","exportName":"ProviderResolveDynamicModelContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":313,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderResolveUsageAuthContext = ProviderResolveUsageAuthContext;","entrypoint":"core","exportName":"ProviderResolveUsageAuthContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":396,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderRuntimeModel = ProviderRuntimeModel;","entrypoint":"core","exportName":"ProviderRuntimeModel","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":296,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderThinkingPolicyContext = ProviderThinkingPolicyContext;","entrypoint":"core","exportName":"ProviderThinkingPolicyContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":537,"sourcePath":"src/plugins/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 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":478,"sourcePath":"src/plugins/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 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":934,"sourcePath":"src/plugins/types.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 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":88,"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 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":952,"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":969,"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":1316,"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":1070,"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":89,"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":1298,"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":1287,"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":1279,"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":968,"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":1099,"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":60,"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":572,"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":156,"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":447,"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":234,"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":218,"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":141,"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":500,"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":516,"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":525,"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":488,"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":255,"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":278,"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":549,"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":588,"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":428,"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":559,"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":339,"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":375,"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":330,"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":461,"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":354,"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":415,"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":313,"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":396,"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":296,"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":537,"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":478,"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":934,"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":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"}
{"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"}
@@ -560,6 +563,7 @@
{"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,6 +162,25 @@ 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:
@@ -300,6 +319,7 @@ 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,6 +366,10 @@ 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,6 +344,8 @@ 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,7 +55,8 @@ Notes:
- `--reset`: reset dev config + credentials + sessions + workspace (requires `--dev`).
- `--force`: kill any existing listener on the selected port before starting.
- `--verbose`: verbose logs.
- `--claude-cli-logs`: only show claude-cli logs in the console (and enable its stdout/stderr).
- `--cli-backend-logs`: only show CLI backend logs in the console (and enable stdout/stderr).
- `--claude-cli-logs`: deprecated alias for `--cli-backend-logs`.
- `--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,7 +780,8 @@ Options:
- `--reset` (reset dev config + credentials + sessions + workspace)
- `--force` (kill existing listener on port)
- `--verbose`
- `--claude-cli-logs`
- `--cli-backend-logs`
- `--claude-cli-logs` (deprecated alias)
- `--ws-log <auto|full|compact>`
- `--compact` (alias for `--ws-log compact`)
- `--raw-stream`
@@ -871,6 +872,13 @@ 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`, `qwen-oauth`, `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`, `oauth:chutes`, `ollama-local`) instead of masking them as secrets.
### `models status`
@@ -74,8 +74,17 @@ 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,6 +56,9 @@ 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,7 +108,6 @@ 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
@@ -348,22 +347,6 @@ 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, 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:
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:
For Anthropic in production, API key auth is the safer recommended path over subscription setup-token auth.
@@ -80,19 +80,48 @@ 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
### Anthropic setup-token / Claude CLI
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)
The wizard path is `openclaw onboard` → auth choice `setup-token` (Anthropic).
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)
### OpenAI Codex (ChatGPT OAuth)

View File

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

View File

@@ -113,6 +113,26 @@ 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,13 +22,14 @@ want “always works” text responses without relying on external APIs.
## Beginner-friendly quick start
You can use Claude Code CLI **without any config** (OpenClaw ships a built-in default):
You can use Claude Code CLI **without any config** (the bundled Anthropic plugin
registers a default backend):
```bash
openclaw agent --message "hi" --model claude-cli/opus-4.6
```
Codex CLI also works out of the box:
Codex CLI also works out of the box (via the bundled OpenAI plugin):
```bash
openclaw agent --message "hi" --model codex-cli/gpt-5.4
@@ -53,6 +54,11 @@ 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:
@@ -180,9 +186,9 @@ Input modes:
- `input: "stdin"` sends the prompt via stdin.
- If the prompt is very long and `maxPromptArgChars` is set, stdin is used.
## Defaults (built-in)
## Defaults (plugin-owned)
OpenClaw ships a default for `claude-cli`:
The bundled Anthropic plugin registers a default for `claude-cli`:
- `command: "claude"`
- `args: ["-p", "--output-format", "json", "--permission-mode", "bypassPermissions"]`
@@ -193,19 +199,38 @@ OpenClaw ships a default for `claude-cli`:
- `systemPromptWhen: "first"`
- `sessionMode: "always"`
OpenClaw also ships a default for `codex-cli`:
The bundled OpenAI plugin also registers a default for `codex-cli`:
- `command: "codex"`
- `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"]`
- `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"]`
- `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 authenticate with a **setup-token**
instead of an API key. This is the subscription path.
Yes. You can either use a **setup-token** or reuse a local **Claude CLI**
login on the gateway host.
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,7 +572,12 @@ 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 - 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).
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).
Important: this is technical compatibility, not a policy guarantee. Anthropic
has blocked some subscription usage outside Claude Code in the past.

View File

@@ -23,6 +23,7 @@ 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:
@@ -54,11 +55,19 @@ 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.
- 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.
- 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`.
- The wrapper peels the heaviest measured files into dedicated lanes instead of relying on a growing hand-maintained exclusion list.
- Refresh the timing snapshot with `pnpm test:perf:update-timings` after major suite shape changes.
- 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.
- Embedded runner note:
- When you change message-tool discovery inputs or compaction runtime context,
keep both levels of coverage.
@@ -72,15 +81,16 @@ 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 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, extension, and gateway wrapper lanes all default to `forks`.
- 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.
@@ -295,6 +305,19 @@ 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:
@@ -432,6 +455,7 @@ 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`)
@@ -468,8 +492,9 @@ 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`, `.qwen`, `.minimax`)
- Default: mount all supported dirs (`.codex`, `.claude`, `.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,14 +27,15 @@ 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` |
| 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` |
| 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` |
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,19 +128,20 @@ 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) |
| 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) |
| 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) |
For the full registration API, see [SDK Overview](/plugins/sdk-overview#registration-api).
@@ -224,6 +225,15 @@ 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,6 +78,7 @@ 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"]
},
@@ -125,6 +126,7 @@ 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. |
@@ -234,8 +236,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`, and `skills` can be omitted when a plugin does not
need them.
- `channels`, `providers`, `cliBackends`, 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,6 +66,7 @@ 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 |
@@ -114,6 +115,7 @@ 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 |
@@ -138,12 +140,25 @@ 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,8 +1,9 @@
---
summary: "Use Anthropic Claude via API keys or setup-token in OpenClaw"
summary: "Use Anthropic Claude via API keys, setup-token, or Claude CLI 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"
---
@@ -26,7 +27,7 @@ openclaw onboard
openclaw onboard --anthropic-api-key "$ANTHROPIC_API_KEY"
```
### Config snippet
### Claude CLI config snippet
```json5
{
@@ -186,7 +187,122 @@ 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 setup-token
## 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
**Best for:** using your Claude subscription.

View File

@@ -39,7 +39,6 @@ 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)
@@ -49,7 +48,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 (OAuth)](/providers/qwen)
- [Qwen / Model Studio (Alibaba Cloud)](/providers/qwen_modelstudio)
- [SGLang (local models)](/providers/sglang)
- [Synthetic](/providers/synthetic)
- [Together AI](/providers/together)

View File

@@ -1,66 +0,0 @@
---
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,53 +1,33 @@
---
summary: "Use Qwen OAuth (free tier) in OpenClaw"
summary: "Use Qwen models via Alibaba Cloud Model Studio"
read_when:
- You want to use Qwen with OpenClaw
- You want free-tier OAuth access to Qwen Coder
- You previously used Qwen OAuth
title: "Qwen"
---
# Qwen
Qwen provides a free-tier OAuth flow for Qwen Coder and Qwen Vision models
(2,000 requests/day, subject to Qwen rate limits).
<Warning>
## Enable the plugin
**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).
```bash
openclaw plugins enable qwen-portal-auth
# Global endpoint
openclaw onboard --auth-choice modelstudio-api-key
# China endpoint
openclaw onboard --auth-choice modelstudio-api-key-cn
```
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.
See [Model Studio](/providers/modelstudio) for full setup details.

View File

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

View File

@@ -80,16 +80,9 @@
"optIn": true
},
{
"id": "channels.discord.accounts.*.voice.tts.elevenlabs.apiKey",
"id": "channels.discord.accounts.*.voice.tts.providers.*.apiKey",
"configFile": "openclaw.json",
"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",
"path": "channels.discord.accounts.*.voice.tts.providers.*.apiKey",
"secretShape": "secret_input",
"optIn": true
},
@@ -108,16 +101,9 @@
"optIn": true
},
{
"id": "channels.discord.voice.tts.elevenlabs.apiKey",
"id": "channels.discord.voice.tts.providers.*.apiKey",
"configFile": "openclaw.json",
"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",
"path": "channels.discord.voice.tts.providers.*.apiKey",
"secretShape": "secret_input",
"optIn": true
},
@@ -420,16 +406,9 @@
"optIn": true
},
{
"id": "messages.tts.elevenlabs.apiKey",
"id": "messages.tts.providers.*.apiKey",
"configFile": "openclaw.json",
"path": "messages.tts.elevenlabs.apiKey",
"secretShape": "secret_input",
"optIn": true
},
{
"id": "messages.tts.openai.apiKey",
"configFile": "openclaw.json",
"path": "messages.tts.openai.apiKey",
"path": "messages.tts.providers.*.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 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 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 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,7 +126,10 @@ 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 OAuth (Claude Code CLI)">
<Accordion title="Anthropic Claude CLI">
Reuses a local Claude CLI login on the gateway host and switches model
selection to `claude-cli/...`.
- macOS: checks Keychain item "Claude Code-credentials"
- Linux and Windows: reuses `~/.claude/.credentials.json` if present

View File

@@ -42,6 +42,44 @@ 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`, `qwen-portal-auth`, `synthetic`, `together`, `venice`,
`qianfan`, `synthetic`, `together`, `venice`,
`vercel-ai-gateway`, `volcengine`, `xiaomi`, `zai`
</Accordion>
@@ -104,6 +104,7 @@ 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,7 +109,6 @@ 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`
@@ -348,22 +347,6 @@ 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,55 +1,36 @@
---
read_when:
- 你想在 OpenClaw 中使用 Qwen
-想要免费层 OAuth 访问 Qwen Coder
summary: 在 OpenClaw 中使用 Qwen OAuth免费层
-之前使用过 Qwen OAuth
summary: 通过阿里云 Model Studio 使用 Qwen 模型
title: Qwen
x-i18n:
generated_at: "2026-02-03T07:53:34Z"
generated_at: "2026-03-23T00:00:00Z"
model: claude-opus-4-5
provider: pi
source_hash: 88b88e224e2fecbb1ca26e24fbccdbe25609be40b38335d0451343a5da53fdd4
source_hash: ""
source_path: providers/qwen.md
workflow: 15
---
# Qwen
Qwen 为 Qwen Coder 和 Qwen Vision 模型提供免费层 OAuth 流程(每天 2,000 次请求,受 Qwen 速率限制约束)。
<Warning>
## 启用插件
**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 plugins enable qwen-portal-auth
# 全球端点
openclaw onboard --auth-choice modelstudio-api-key
# 中国端点
openclaw onboard --auth-choice modelstudio-api-key-cn
```
启用后重启 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)了解提供商级别的规则。
完整设置详情请参见 [Model Studio](/providers/modelstudio)

View File

@@ -155,7 +155,6 @@ 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` 形式捆绑(默认启用)
@@ -497,7 +496,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/qwen-portal-auth``openclaw/plugin-sdk/synology-chat`
`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`
@@ -613,7 +612,6 @@ OpenClaw 按以下顺序扫描:
- `openrouter`
- `phone-control`
- `qianfan`
- `qwen-portal-auth`
- `sglang`
- `synthetic`
- `talk-voice`

View File

@@ -0,0 +1,112 @@
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

@@ -0,0 +1,82 @@
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

@@ -0,0 +1,131 @@
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,6 +27,8 @@ 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";
@@ -311,11 +313,65 @@ 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",
@@ -323,6 +379,33 @@ 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)",
@@ -334,7 +417,7 @@ export default definePluginEntry({
choiceHint: "Run `claude setup-token` elsewhere, then paste the token here",
groupId: "anthropic",
groupLabel: "Anthropic",
groupHint: "setup-token + API key",
groupHint: "Claude CLI + setup-token + API key",
modelAllowlist: {
allowedKeys: [...ANTHROPIC_OAUTH_ALLOWLIST],
initialSelections: ["anthropic/claude-sonnet-4-6"],
@@ -366,7 +449,7 @@ export default definePluginEntry({
choiceLabel: "Anthropic API key",
groupId: "anthropic",
groupLabel: "Anthropic",
groupHint: "setup-token + API key",
groupHint: "Claude CLI + setup-token + API key",
},
}),
],

View File

@@ -1,10 +1,22 @@
{
"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",
@@ -13,7 +25,7 @@
"choiceHint": "Run `claude setup-token` elsewhere, then paste the token here",
"groupId": "anthropic",
"groupLabel": "Anthropic",
"groupHint": "setup-token + API key"
"groupHint": "Claude CLI + setup-token + API key"
},
{
"provider": "anthropic",
@@ -22,7 +34,7 @@
"choiceLabel": "Anthropic API key",
"groupId": "anthropic",
"groupLabel": "Anthropic",
"groupHint": "setup-token + API key",
"groupHint": "Claude CLI + setup-token + API key",
"optionKey": "anthropicApiKey",
"cliFlag": "--anthropic-api-key",
"cliOption": "--anthropic-api-key <key>",

View File

@@ -1,6 +1,8 @@
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;
@@ -43,10 +45,19 @@ 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,
allowPrivateNetwork: account.config.allowPrivateNetwork === true || autoAllowPrivateNetwork,
};
}

View File

@@ -161,7 +161,12 @@ export const bluebubblesMessageActions: ChannelMessageActionAdapter = {
throw new Error(`BlueBubbles ${action} requires serverUrl and password.`);
}
const resolved = await runtime.resolveChatGuidForTarget({ baseUrl, password, target });
const resolved = await runtime.resolveChatGuidForTarget({
baseUrl,
password,
target,
allowPrivateNetwork: account.config.allowPrivateNetwork === true,
});
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 { downloadBlueBubblesAttachment, sendBlueBubblesAttachment } from "./attachments.js";
import "./test-mocks.js";
import { downloadBlueBubblesAttachment, sendBlueBubblesAttachment } from "./attachments.js";
import { getCachedBlueBubblesPrivateApiStatus } from "./probe.js";
import type { PluginRuntime } from "./runtime-api.js";
import { setBlueBubblesRuntime } from "./runtime.js";
@@ -288,30 +288,45 @@ describe("downloadBlueBubblesAttachment", () => {
expect(fetchMediaArgs.ssrfPolicy).toEqual({ allowPrivateNetwork: true });
});
it("auto-allowlists serverUrl hostname when allowPrivateNetwork is not set", async () => {
it("auto-enables private-network fetches for loopback serverUrl 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({ allowedHostnames: ["localhost"] });
expect(fetchMediaArgs.ssrfPolicy).toEqual({ allowPrivateNetwork: true });
});
it("auto-allowlists private IP serverUrl hostname when allowPrivateNetwork is not set", async () => {
it("auto-enables private-network fetches for private IP serverUrl 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({ allowedHostnames: ["192.168.1.5"] });
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"] });
});
});

View File

@@ -16,8 +16,13 @@ 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;
@@ -155,7 +160,7 @@ export async function sendBlueBubblesAttachment(params: {
const fallbackName = wantsVoice ? "Audio Message" : "attachment";
filename = sanitizeFilename(filename, fallbackName);
contentType = contentType?.trim() || undefined;
const { baseUrl, password, accountId } = resolveAccount(opts);
const { baseUrl, password, accountId, allowPrivateNetwork } = resolveAccount(opts);
const privateApiStatus = getCachedBlueBubblesPrivateApiStatus(accountId);
const privateApiEnabled = isBlueBubblesPrivateApiStatusEnabled(privateApiStatus);
@@ -185,6 +190,7 @@ 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
@@ -194,6 +200,7 @@ 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)
@@ -203,6 +210,7 @@ export async function sendBlueBubblesAttachment(params: {
password,
timeoutMs: opts.timeoutMs,
target,
allowPrivateNetwork,
});
}
}
@@ -281,6 +289,7 @@ 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,6 +196,7 @@ 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,11 +1,16 @@
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;
@@ -41,7 +46,7 @@ async function sendBlueBubblesChatEndpointRequest(params: {
if (!trimmed) {
return;
}
const { baseUrl, password, accountId } = resolveAccount(params.opts);
const { baseUrl, password, accountId, allowPrivateNetwork } = resolveAccount(params.opts);
if (getCachedBlueBubblesPrivateApiStatus(accountId) === false) {
return;
}
@@ -54,6 +59,7 @@ async function sendBlueBubblesChatEndpointRequest(params: {
url,
{ method: params.method },
params.opts.timeoutMs,
blueBubblesPolicy(allowPrivateNetwork),
);
await assertMultipartActionOk(res, params.action);
}
@@ -66,7 +72,7 @@ async function sendPrivateApiJsonRequest(params: {
method: "POST" | "PUT" | "DELETE";
payload?: unknown;
}): Promise<void> {
const { baseUrl, password, accountId } = resolveAccount(params.opts);
const { baseUrl, password, accountId, allowPrivateNetwork } = resolveAccount(params.opts);
assertPrivateApiEnabled(accountId, params.feature);
const url = buildBlueBubblesApiUrl({
baseUrl,
@@ -80,7 +86,12 @@ async function sendPrivateApiJsonRequest(params: {
request.body = JSON.stringify(params.payload);
}
const res = await blueBubblesFetchWithTimeout(url, request, params.opts.timeoutMs);
const res = await blueBubblesFetchWithTimeout(
url,
request,
params.opts.timeoutMs,
blueBubblesPolicy(allowPrivateNetwork),
);
await assertMultipartActionOk(res, params.action);
}
@@ -282,7 +293,7 @@ export async function setGroupIconBlueBubbles(
throw new Error("BlueBubbles setGroupIcon requires image buffer");
}
const { baseUrl, password, accountId } = resolveAccount(opts);
const { baseUrl, password, accountId, allowPrivateNetwork } = resolveAccount(opts);
assertPrivateApiEnabled(accountId, "setGroupIcon");
const url = buildBlueBubblesApiUrl({
baseUrl,
@@ -317,6 +328,7 @@ 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,6 +42,7 @@ 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,11 +83,13 @@ export async function fetchBlueBubblesHistory(
let baseUrl: string;
let password: string;
let allowPrivateNetwork = false;
try {
({ baseUrl, password } = resolveAccount(opts));
({ baseUrl, password, allowPrivateNetwork } = resolveAccount(opts));
} catch {
return { entries: [], resolved: false };
}
const ssrfPolicy = allowPrivateNetwork ? { allowPrivateNetwork: true } : {};
// Try different common API patterns for fetching messages
const possiblePaths = [
@@ -103,6 +105,7 @@ export async function fetchBlueBubblesHistory(
url,
{ method: "GET" },
opts.timeoutMs ?? 10000,
ssrfPolicy,
);
if (!res.ok) {

View File

@@ -78,6 +78,48 @@ 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,6 +189,25 @@ 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;
@@ -265,16 +284,11 @@ function extractChatContext(message: Record<string, unknown>): {
readString(chatFromList, "name") ??
undefined;
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 participants =
readParticipantEntries(chat) ??
readParticipantEntries(message) ??
readParticipantEntries(chatFromList) ??
[];
const participantsCount = participants.length;
const groupFromChatGuid = resolveGroupFlagFromChatGuid(chatGuid);
const explicitIsGroup =
@@ -336,13 +350,14 @@ function normalizeParticipantEntry(entry: unknown): BlueBubblesParticipant | nul
return { id: normalizedId, name };
}
function normalizeParticipantList(raw: unknown): BlueBubblesParticipant[] {
if (!Array.isArray(raw) || raw.length === 0) {
export function normalizeParticipantList(raw: unknown): BlueBubblesParticipant[] {
const entries = Array.isArray(raw) ? raw : (readParticipantEntries(asRecord(raw)) ?? []);
if (entries.length === 0) {
return [];
}
const seen = new Set<string>();
const output: BlueBubblesParticipant[] = [];
for (const entry of raw) {
for (const entry of entries) {
const normalized = normalizeParticipantEntry(entry);
if (!normalized?.id) {
continue;

View File

@@ -12,6 +12,7 @@ import {
formatGroupAllowlistEntry,
formatGroupMembers,
formatReplyTag,
normalizeParticipantList,
parseTapbackText,
resolveGroupFlagFromChatGuid,
resolveTapbackContext,
@@ -33,6 +34,7 @@ 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";
@@ -61,6 +63,7 @@ import {
isAllowedBlueBubblesSender,
normalizeBlueBubblesHandle,
} from "./targets.js";
import { blueBubblesFetchWithTimeout, buildBlueBubblesApiUrl } from "./types.js";
const DEFAULT_TEXT_LIMIT = 4000;
const invalidAckReactions = new Set<string>();
@@ -93,6 +96,134 @@ 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,
@@ -783,12 +914,47 @@ 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
@@ -938,6 +1104,7 @@ export async function processMessage(
baseUrl,
password,
target: resolveTarget,
allowPrivateNetwork: account.config.allowPrivateNetwork === true,
})) ?? undefined;
}
}
@@ -1491,6 +1658,19 @@ 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,6 +23,10 @@ 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
@@ -104,6 +108,7 @@ 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({
@@ -176,6 +181,12 @@ describe("BlueBubbles webhook monitor", () => {
}
beforeEach(() => {
vi.stubGlobal("fetch", mockFetch);
mockFetch.mockReset();
mockFetch.mockResolvedValue({
ok: true,
json: () => Promise.resolve({ data: [] }),
});
resetBlueBubblesMonitorTestState({
createRuntime: createMockRuntime,
fetchHistoryMock: mockFetchBlueBubblesHistory,
@@ -185,13 +196,19 @@ describe("BlueBubbles webhook monitor", () => {
hasControlCommandMock: mockHasControlCommand,
resolveCommandAuthorizedFromAuthorizersMock: mockResolveCommandAuthorizedFromAuthorizers,
buildMentionRegexesMock: mockBuildMentionRegexes,
extraReset: resetBlueBubblesSelfChatCache,
extraReset: () => {
resetBlueBubblesSelfChatCache();
resetBlueBubblesParticipantContactNameCacheForTest();
setBlueBubblesParticipantContactDepsForTest();
},
});
});
afterEach(() => {
unregister?.();
setBlueBubblesParticipantContactDepsForTest();
vi.useRealTimers();
vi.unstubAllGlobals();
});
describe("DM pairing behavior vs allowFrom", () => {
@@ -489,6 +506,147 @@ 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", () => {
@@ -1160,6 +1318,28 @@ 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();
@@ -1177,6 +1357,31 @@ 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,18 +16,31 @@ 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,
@@ -117,18 +130,62 @@ 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 = new URL(req.url ?? "/", "http://localhost");
const url = requestUrl;
const guidParam = url.searchParams.get("guid") ?? url.searchParams.get("password");
const headerToken =
req.headers["x-guid"] ??
@@ -275,6 +332,7 @@ 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,6 +107,7 @@ 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 {
@@ -142,6 +143,12 @@ 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,
@@ -156,6 +163,7 @@ describe("BlueBubbles webhook monitor", () => {
afterEach(() => {
unregister?.();
vi.unstubAllGlobals();
});
function setupWebhookTarget(params?: {
@@ -398,6 +406,163 @@ 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,3 +1,4 @@
import type { SsrFPolicy } from "openclaw/plugin-sdk/infra-runtime";
import { blueBubblesFetchWithTimeout } from "./types.js";
export function concatUint8Arrays(parts: Uint8Array[]): Uint8Array {
@@ -16,6 +17,7 @@ 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(
@@ -28,6 +30,7 @@ export async function postMultipartFormData(params: {
body,
},
params.timeoutMs,
params.ssrfPolicy,
);
}

View File

@@ -0,0 +1,193 @@
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

@@ -0,0 +1,377 @@
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,6 +35,7 @@ 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);
@@ -48,9 +49,15 @@ 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);
const res = await blueBubblesFetchWithTimeout(
url,
{ method: "GET" },
params.timeoutMs ?? 5000,
ssrfPolicy,
);
if (!res.ok) {
return null;
}
@@ -138,6 +145,7 @@ 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);
@@ -147,9 +155,15 @@ 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);
const res = await blueBubblesFetchWithTimeout(
url,
{ method: "GET" },
params.timeoutMs,
probeSsrfPolicy,
);
if (!res.ok) {
return { ok: false, status: res.status, error: `HTTP ${res.status}` };
}

View File

@@ -1,5 +1,6 @@
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
import { describe, expect, it, vi } 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");
@@ -7,17 +8,16 @@ 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 } = resolveAccount(params.opts ?? {});
const { baseUrl, password, accountId, allowPrivateNetwork } = resolveAccount(params.opts ?? {});
if (getCachedBlueBubblesPrivateApiStatus(accountId) === false) {
throw new Error(
"BlueBubbles reaction requires Private API, but it is disabled on the BlueBubbles server.",
@@ -166,6 +166,7 @@ export async function sendBlueBubblesReaction(params: {
reaction,
partIndex: typeof params.partIndex === "number" ? params.partIndex : 0,
};
const ssrfPolicy = allowPrivateNetwork ? { allowPrivateNetwork: true } : {};
const res = await blueBubblesFetchWithTimeout(
url,
{
@@ -174,6 +175,7 @@ export async function sendBlueBubblesReaction(params: {
body: JSON.stringify(payload),
},
params.opts?.timeoutMs,
ssrfPolicy,
);
if (!res.ok) {
const errorText = await res.text();

View File

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

View File

@@ -239,6 +239,37 @@ 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,5 +1,6 @@
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,
@@ -67,8 +68,12 @@ 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?.();
@@ -76,6 +81,36 @@ 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,5 +1,7 @@
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 = {
@@ -31,6 +33,8 @@ 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. */
@@ -126,11 +130,43 @@ 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

@@ -0,0 +1,90 @@
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

@@ -0,0 +1,28 @@
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

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

View File

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

View File

@@ -0,0 +1,10 @@
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

@@ -0,0 +1,87 @@
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,12 +1,18 @@
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
import { browserAct, browserConsoleMessages } from "../../browser/client-actions.js";
import { browserSnapshot, browserTabs } from "../../browser/client.js";
import { resolveBrowserConfig, resolveProfile } from "../../browser/config.js";
import { DEFAULT_AI_SNAPSHOT_MAX_CHARS } from "../../browser/constants.js";
import { getBrowserProfileCapabilities } from "../../browser/profile-capabilities.js";
import { loadConfig } from "../../config/config.js";
import { wrapExternalContent } from "../../security/external-content.js";
import { imageResultFromFile, jsonResult } from "./common.js";
import {
DEFAULT_AI_SNAPSHOT_MAX_CHARS,
browserAct,
browserConsoleMessages,
browserSnapshot,
browserTabs,
getBrowserProfileCapabilities,
imageResultFromFile,
jsonResult,
loadConfig,
resolveBrowserConfig,
resolveProfile,
wrapExternalContent,
} from "./core-api.js";
const browserToolActionDeps = {
browserAct,

View File

@@ -1,5 +1,5 @@
import { Type } from "@sinclair/typebox";
import { optionalStringEnum, stringEnum } from "../schema/typebox.js";
import { optionalStringEnum, stringEnum } from "./core-api.js";
const BROWSER_ACT_KINDS = [
"click",

View File

@@ -1,30 +1,4 @@
import crypto from "node:crypto";
import {
browserAct,
browserArmDialog,
browserArmFileChooser,
browserNavigate,
browserPdfSave,
browserScreenshotAction,
} from "../../browser/client-actions.js";
import {
browserCloseTab,
browserFocusTab,
browserOpenTab,
browserProfiles,
browserStart,
browserStatus,
browserStop,
} from "../../browser/client.js";
import { resolveBrowserConfig, resolveProfile } from "../../browser/config.js";
import { DEFAULT_UPLOAD_DIR, resolveExistingPathsWithinRoot } from "../../browser/paths.js";
import { getBrowserProfileCapabilities } from "../../browser/profile-capabilities.js";
import { applyBrowserProxyPaths, persistBrowserProxyFiles } from "../../browser/proxy-files.js";
import {
trackSessionBrowserTab,
untrackSessionBrowserTab,
} from "../../browser/session-tab-registry.js";
import { loadConfig } from "../../config/config.js";
import {
executeActAction,
executeConsoleAction,
@@ -32,14 +6,40 @@ import {
executeTabsAction,
} from "./browser-tool.actions.js";
import { BrowserToolSchema } from "./browser-tool.schema.js";
import { type AnyAgentTool, imageResultFromFile, jsonResult, readStringParam } from "./common.js";
import { callGatewayTool } from "./gateway.js";
import {
listNodes,
resolveNodeIdFromList,
selectDefaultNodeFromList,
type AnyAgentTool,
type NodeListNode,
} from "./nodes-utils.js";
DEFAULT_UPLOAD_DIR,
applyBrowserProxyPaths,
browserAct,
browserArmDialog,
browserArmFileChooser,
browserCloseTab,
browserFocusTab,
browserNavigate,
browserOpenTab,
browserPdfSave,
browserProfiles,
browserScreenshotAction,
browserStart,
browserStatus,
browserStop,
getBrowserProfileCapabilities,
imageResultFromFile,
jsonResult,
listNodes,
loadConfig,
persistBrowserProxyFiles,
readStringParam,
resolveBrowserConfig,
resolveExistingPathsWithinRoot,
resolveNodeIdFromList,
resolveProfile,
selectDefaultNodeFromList,
trackSessionBrowserTab,
untrackSessionBrowserTab,
} from "./core-api.js";
import { callGatewayTool } from "./core-api.js";
const browserToolDeps = {
browserAct,

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