Compare commits

..

179 Commits

Author SHA1 Message Date
Mason Huang
80f65fce4a CI: add back narrow paths-ignore for markdown/LICENSE only 2026-04-17 10:24:53 +08:00
Mason Huang
9fc5f061e2 CI: restore daily schedule, remove paths-ignore from CodeQL PR trigger 2026-04-17 10:21:29 +08:00
Mason Huang
9b055ee2a3 CI: add paths-ignore to CodeQL pull_request trigger 2026-04-17 10:11:46 +08:00
Mason Huang
457b2ee175 CI: add pull_request trigger to CodeQL workflow, change schedule to weekly 2026-04-17 10:07:56 +08:00
Peter Steinberger
7e9ff0f86e test: reuse system-run temp fixtures 2026-04-17 02:49:37 +01:00
Peter Steinberger
12a59b0a18 test: trim hotspot wait overhead 2026-04-17 02:47:09 +01:00
Gustavo Madeira Santana
baf11b83d7 Check: avoid duplicate boundary prep
Rely on the lint wrapper to prepare extension package-boundary artifacts during pnpm check instead of invoking the same prep script again at the end.

Add a script regression so the duplicate check path does not return.
2026-04-16 21:37:08 -04:00
Peter Steinberger
3a59eddd07 test: reduce hotspot fixture overhead 2026-04-17 02:37:00 +01:00
Val Alexander
2cfb660a9b feat(ui): overhaul settings and slash command UX (#67819) thanks @BunsDev
Co-authored-by: Val Alexander <68980965+BunsDev@users.noreply.github.com>
2026-04-16 20:29:11 -05:00
Gustavo Madeira Santana
42805d26cf QA Matrix: exit cleanly on failure
Make the Matrix QA CLI single-shot exit contract symmetric: artifact-backed failures now print the preserved error, flush stdio, and exit with code 1 instead of waiting on Matrix native handles.

Keep an opt-out for direct test harnesses with OPENCLAW_QA_MATRIX_DISABLE_FORCE_EXIT.
2026-04-16 21:24:59 -04:00
Gustavo Madeira Santana
7e659e168b QA Matrix: isolate scenario coverage
Add the Matrix subagent-thread scenario and route it through the contract runner while preserving the current missing-hook failure as an explicit scenario result.

Give E2EE scenarios isolated rooms and storage keys so lifecycle tests do not reuse stale encrypted state across scenarios.
2026-04-16 21:24:59 -04:00
Gustavo Madeira Santana
94081d8863 Matrix: refresh crypto bootstrap state
Refresh published cross-signing keys before bootstrap imports secret-storage keys, add sync-filter plumbing for QA E2EE clients, and document the remaining upstream key-backup cache noise without suppressing SDK logs.
2026-04-16 21:24:59 -04:00
Gustavo Madeira Santana
bb7e9823a8 QA Lab: add provider registry
Move mock and live provider behavior behind provider-owned definitions so suite, manual, Matrix, and transport lanes share defaults, auth staging, model config, and standalone server startup.

Add AIMock as a first-class local provider mode while keeping mock-openai as the scenario-aware deterministic lane.
2026-04-16 21:24:59 -04:00
Gustavo Madeira Santana
4acab55db8 Matrix: add plugin changelog 2026-04-16 21:24:58 -04:00
Peter Steinberger
f4853115a9 test: trim more hotspot overhead 2026-04-17 02:20:02 +01:00
Peter Steinberger
6ba8626c25 test: trim remaining hotspot tests 2026-04-17 02:07:26 +01:00
Peter Steinberger
dbc8179f31 test: narrow hotspot mocks 2026-04-17 01:53:16 +01:00
Peter Steinberger
cd330f5f98 test: isolate gemini embedding request helpers 2026-04-17 01:46:47 +01:00
Peter Steinberger
fd48dfa68f test: trim memory and mcp hotspots 2026-04-17 01:39:39 +01:00
Peter Steinberger
2e08c77582 test: slim provider registry mocks 2026-04-17 01:29:12 +01:00
Peter Steinberger
a2753e2d9f fix: keep Opus 4.7 effort separate from adaptive thinking 2026-04-17 01:26:11 +01:00
Peter Steinberger
c73a6d2f68 feat: support xhigh for Claude Opus 4.7 2026-04-17 01:26:11 +01:00
Peter Steinberger
272536015f test: slim runtime hotspot mocks 2026-04-17 01:15:31 +01:00
Peter Steinberger
59b98334f6 test: narrow hotspot boundaries 2026-04-17 01:10:48 +01:00
Peter Steinberger
0dc4c4076c chore: bump version to 2026.4.16 2026-04-17 00:45:04 +01:00
Peter Steinberger
26db52ed69 build: restore qa lab updater sidecar 2026-04-17 00:44:35 +01:00
Peter Steinberger
0c5bdbde89 chore: update mac appcast for 2026.4.15 2026-04-17 00:39:15 +01:00
Peter Steinberger
5c1c52f870 build: bump protobufjs override 2026-04-17 00:22:58 +01:00
Peter Steinberger
8507935d3a test: reuse system run plan fixtures 2026-04-17 00:20:06 +01:00
Peter Steinberger
992ff81ae1 docs: consolidate 2026.4.15 changelog 2026-04-17 00:17:24 +01:00
B.K.
6878c19449 fix(onboard): preserve existing gateway auth token during re-onboard (#67821)
Merged via squash.

Prepared head SHA: e602f8f4ab
Co-authored-by: BKF-Gitty <263413630+BKF-Gitty@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-04-16 19:11:12 -04:00
Peter Steinberger
f8bac822b6 test: align reset prompt expectation 2026-04-17 00:05:12 +01:00
Peter Steinberger
ed04d38bec build: restore qa channel updater sidecar 2026-04-17 00:00:41 +01:00
Peter Steinberger
ce1be0f43d test: speed gemini embedding tests 2026-04-17 00:00:41 +01:00
Tak Hoffman
81818df1b4 fix(startup): prioritize bootstrap on fresh sessions 2026-04-16 17:53:07 -05:00
Peter Steinberger
b21540fabc ci: include update sidecars in docker build profile 2026-04-16 23:40:47 +01:00
Peter Steinberger
350aa6343a build: bump basic-ftp override 2026-04-16 23:28:56 +01:00
Peter Steinberger
b2cae7f12a test: trim duplicate memory hotspot coverage 2026-04-16 23:15:38 +01:00
Peter Steinberger
a98754d504 refactor(agents): clarify prompt cache compatibility gates 2026-04-16 14:59:20 -07:00
Peter Steinberger
d59604b15e test: speed up hotspot boundaries 2026-04-16 22:55:30 +01:00
Peter Steinberger
041266a669 chore: prepare 2026.4.15 release 2026-04-16 22:45:32 +01:00
Peter Steinberger
4d2854a2b0 test: tighten hotspot boundaries 2026-04-16 22:40:06 +01:00
Josh Lehman
80e78f7b90 docs: unify duplicated 2026.4.15-beta.1 changelog block (#67827)
* docs: unify duplicated 2026.4.15-beta.1 changelog block

* docs: remove beta.2 duplicates from beta.1 changelog

* docs: restore preserved beta.1 changelog entries
2026-04-16 14:35:54 -07:00
Onur
fc137ec5e3 CI: fix live docker vite temp overlay 2026-04-16 23:30:37 +02:00
Peter Steinberger
63e53fbf2e test: trim duplicate hotspot coverage 2026-04-16 22:19:32 +01:00
Onur
98c681e033 CI: mount writable Docker cache homes (#67825) 2026-04-16 23:16:48 +02:00
Peter Steinberger
678b019467 test: stabilize config and plugin scanner tests 2026-04-16 22:10:36 +01:00
Josh Lehman
dafc71c913 Update contributor details for Josh Lehman (#67824) 2026-04-16 14:05:56 -07:00
Onur
3ae5d95bfd CI: fix live Docker auth mounts (#67812)
* CI: fix live Docker auth mounts

* CI: harden live Docker auth mounts
2026-04-16 23:00:11 +02:00
Vincent Koc
012b577e84 fix(ci): guard qa matrix fault proxy fetch 2026-04-16 13:59:07 -07:00
Peter Steinberger
8a37bb4ed6 perf: speed up security audit test imports 2026-04-16 21:54:13 +01:00
Peter Steinberger
cd45f53b4e test: stabilize parallels npm update smoke 2026-04-16 21:52:10 +01:00
Vincent Koc
c9103c2e47 fix(ci): prepare plugin sdk dts before lint 2026-04-16 13:50:23 -07:00
Vincent Koc
f835da1667 fix(ci): trim slow task and gateway paths 2026-04-16 13:34:34 -07:00
Gustavo Madeira Santana
56a9fd4b34 QA Matrix: capture full runner output 2026-04-16 16:18:54 -04:00
Gustavo Madeira Santana
988447ca24 QA Matrix: expand contract coverage 2026-04-16 16:18:54 -04:00
Gustavo Madeira Santana
0f7c40e508 Matrix: expose E2EE QA verification hooks 2026-04-16 16:18:54 -04:00
Gustavo Madeira Santana
21d500a65f test: expose bundled plugin QA test APIs 2026-04-16 16:18:54 -04:00
Gustavo Madeira Santana
5bb180061a Check: run type and lint earlier 2026-04-16 16:18:54 -04:00
Peter Steinberger
372c0051ba test: speed up slow import-boundary tests 2026-04-16 21:14:17 +01:00
Devin Robison
8b7d76bfbb fix(compaction): stop retaining credential-like values (#67801) 2026-04-16 14:04:45 -06:00
Peter Steinberger
894e728fd0 test: relax Parallels install smoke timeout 2026-04-16 12:54:13 -07:00
Peter Steinberger
5262757f9a fix: handle Codex HTML challenge errors (#67704) (thanks @chris-yyau) 2026-04-16 12:47:12 -07:00
Chris Yau
59caf03d67 Avoid rescanning HTML challenge pages during error formatting
The HTML challenge fix already keeps standalone CDN block pages out of the DNS transport path. This follow-up caches the HTML classification so status-prefixed non-HTML failures do not pay for the same scan twice and the control flow stays simpler.

Constraint: Keep behavior identical for both status-prefixed HTML pages and standalone HTML challenge pages
Rejected: Inline the helper into the status branch only | would duplicate the standalone HTML branch logic
Confidence: high
Scope-risk: narrow
Directive: If this formatter grows more branches, keep a single HTML classification result and reuse it through the decision tree
Tested: oxfmt --check src/shared/assistant-error-format.ts
Tested: node scripts/test-projects.mjs src/agents/pi-embedded-helpers.formatassistanterrortext.test.ts src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts
2026-04-16 12:47:12 -07:00
Chris Yau
36dd58ac2a Prevent Codex HTML challenge pages from looking like DNS failures
Cloudflare challenge pages from chatgpt.com/backend-api can arrive as raw HTML without an HTTP status prefix. The transport sanitizer scanned for generic "dns" substrings before HTML detection, so these pages could surface as DNS lookup failures instead of the existing HTML/CDN block message.

Constraint: Must preserve DNS transport classification for real ENOTFOUND/getaddrinfo failures
Rejected: Treat every bare HTML document as an upstream HTML error | too broad for arbitrary model text/errors
Confidence: high
Scope-risk: narrow
Directive: Keep standalone HTML challenge detection ahead of generic transport keyword matching so CDN block pages do not regress into DNS copy
Tested: oxfmt --check on changed files; targeted node --import tsx verification for standalone Cloudflare HTML classification and DNS control case
Not-tested: Full Vitest shard run in this environment
2026-04-16 12:47:12 -07:00
Onur
51606e9889 CI: fix release-check caller permissions (#67787)
* CI: fix release-check caller permissions

* CI: fix scheduled live and e2e checks

* CI: tighten release workflow permissions

* CI: restore release workflow caller permissions

* Actions: harden release check inputs
2026-04-16 21:41:21 +02:00
Vincent Koc
781b1de921 fix(ci): cap core shard checkout stalls 2026-04-16 12:35:38 -07:00
Vincent Koc
2285429aa2 test(vitest): cut unit-ui startup overhead 2026-04-16 12:16:21 -07:00
Peter Steinberger
29427fefc7 ci: make mlx audio manifest patch writable 2026-04-16 20:12:18 +01:00
Peter Steinberger
15c7f478da docs: update plugin sdk api baseline 2026-04-16 19:58:08 +01:00
Peter Steinberger
006a8aeb8c ci: register blacksmith macos runner labels 2026-04-16 19:58:08 +01:00
Peter Steinberger
ad9da24317 test: keep web search config imports stable 2026-04-16 19:58:08 +01:00
Peter Steinberger
c635efd233 chore: prepare 2026.4.15-beta.2 release 2026-04-16 19:58:08 +01:00
Josh Lehman
a327b6750d fix: stabilize context engine prompt cache touches (#67767)
* fix: stabilize context engine prompt cache touches

* fix(changelog): document context-engine prompt cache touch stabilization
2026-04-16 11:53:42 -07:00
Vincent Koc
ac717a92e8 test(gateway): avoid mapped hook provenance event race 2026-04-16 11:35:14 -07:00
Vincent Koc
c2db918c60 fix(ci): silence mlx-audio-swift README warnings 2026-04-16 11:27:32 -07:00
Vincent Koc
42d100c390 fix(ci): move macOS jobs to blacksmith 2026-04-16 11:18:50 -07:00
zqchris
82e349a48a memory: strip inbound metadata envelopes from user messages in session corpus (#66548)
Merged via squash.

Prepared head SHA: 98562b2a84
Co-authored-by: zqchris <4436110+zqchris@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
2026-04-16 11:15:44 -07:00
Vincent Koc
00d21d1b23 fix(ci): retry stalled core shard checkout 2026-04-16 11:04:16 -07:00
Onur
900e291f31 CI: expand native release validation coverage (#67144)
* Actions: grant reusable release checks actions read

* Actions: use read-all for reusable release checks

* CI: add native cross-OS release checks

* CI: wire Discord smoke secrets for cross-OS checks

* CI: fix native cross-OS installer compatibility

* CI: skip empty pnpm cache saves in matrix jobs

* CI: honor workflow runner override envs

* CI: finish native cross-OS update checks

* CI: fix native cross-OS workflow regressions

* Installer: capture Windows npm stderr safely

* CI: harden cross-OS release checks

* CI: resolve reusable workflow harness ref

* CI: stabilize cross-OS dev update lanes

* CI: tighten release-check workflow semantics

* CI: repoint repaired git CLI on POSIX

* CI: repair native dev-update shell handoff

* CI: preserve real updater semantics

* CI: harden supported release-check refs

* CI: harden release-check refs and fresh mode

* CI: skip dev-update for immutable tag refs

* CI: repair fresh installer release checks

* CI: fix native release check installer lanes

* CI: install release checks from candidate artifacts

* CI: use Windows cmd shims in release checks

* Installer: run Windows npm shim via PowerShell

* CI: pin dev update verification to candidate sha

* CI: pin reusable harness and published installers

* CI: isolate Windows dev-update PATH validation

* CI: align Windows dev-update bootstrap validation

* CI: avoid Windows installer gateway flake

* CI: run cross-OS release checks via TypeScript

* CI: bootstrap tsx for release-check workflow

* CI: fix native release-check follow-ups

* CI: tighten dev-update release checks

* CI: peel annotated workflow refs

* CI: harden native release checks

* CI: fix release-check verifier drift

* CI: fix release-check workflow drift

* CI: fix release-check ref resolution

* CI: harden Windows release-check gateway startup

* CI: fix release-check fallback validation

* CI: harden cross-os release checks

* CI: pin dev-update release checks to candidate SHA

* CI: resolve remote dev target refs

* CI: detect cloned dev-update checkouts

* CI: harden Windows release-check launcher

* Windows: harden task fallback and runner overrides

* Release checks: preserve Windows PATH and baseline version reads

* CI: add release validation live lanes

* CI: expand live and e2e release coverage

* CI: add branch dispatch for live and e2e checks
2026-04-16 19:58:19 +02:00
Daniel Salmerón Amselem
687ede50a5 fix(agents): add prompt cache compatibility opt-out
Add compat.supportsPromptCacheKey for OpenAI Responses prompt_cache_key handling, update generated config baseline, changelog, and A2UI dependency-layout test compatibility.
2026-04-16 10:48:51 -07:00
Viz
f624b1d246 fix(security): 7 P1 hardening fixes — scan-paths, windows-acl, audit-extra (#67003)
* test(security): add coverage tests before security fixes

- scan-paths.ts: 100% line coverage (new test file, previously zero)
- windows-acl.ts: 100% line coverage (SID bypass, whoami throw, no-user null return)
- external-content.ts: 99% (line 248 defensive overlap guard, unreachable)
- skill-scanner.ts: 93% (lines 293-294/330/571 are defensive guards for
  future extensibility, unreachable with current rules/patterns)

200+ tests covering TOCTOU paths, cache invalidation, forced-file escapes,
dir-entry-cache hit, SID world-bypass, diacritic-strip fallback,
fullwidth homoglyph markers, and more.

* fix(security): 5 security hardening fixes in src/security/

scan-paths: default requireRealpath to false (safe). All production callers
already pass requireRealpath: true; default callers are now secure.

windows-acl: block world-equivalent SIDs (S-1-1-0 Everyone etc.) from being
added to trusted set via USERSID env var.

windows-acl: log resolveCurrentUserSid failures instead of bare catch{}.

audit-extra: wrap JSON.parse in readPluginManifestExtensions with try-catch.
Malformed package.json returns [] instead of crashing the audit.

audit-extra: depth guard in listWorkspaceSkillMarkdownFiles to prevent
resource exhaustion from deep symlink cycles.

audit-extra: 2s timeout on fs.realpath in collectWorkspaceSkillSymlinkEscapeFindings
to protect against hanging on slow/network filesystems.

audit-extra: warn about phantom entries in plugins.allow that don't match
any installed plugin (pre-approval exploitation vector).

media-understanding/types: add allowPrivateNetwork to transport overrides
(duplicate of PR #66967, required for tsgo to pass here).

* fix(security): address security review findings in audit-extra.async.ts

Issue 1 — Symlink escape audit bypass on realpath timeout:
When realpathWithTimeout returns null (timeout or failure), the previous code
called 'continue', silently skipping the escape check. An attacker with a
symlink to a slow/network filesystem could hang realpath to prevent escape
detection. Now treats unverifiable symlinks as potential escapes and includes
them in the finding.

Issue 2 — Malformed package.json hides extension entrypoints from deep scan:
readPluginManifestExtensions previously swallowed JSON.parse errors and
returned [], which a malicious plugin could exploit by crafting a malformed
package.json to hide its openclaw.extensions entrypoints from the deep code
scanner. Now re-throws the parse error (with cause) so the caller in
collectPluginsCodeSafetyFindings can surface a warn finding and alert the
user, while still scanning the plugin directory via getCodeSafetySummary.

* fix(security): address PR review findings (P1 + P2)

P1 — BFS realpath in listWorkspaceSkillMarkdownFiles lacks timeout:
Extract realpathWithTimeout to module scope so the BFS dequeue loop
uses the same 2 s guard as the outer escape-detection callers. Previously
only the per-workspace and per-skill-file realpaths had the timeout;
a hanging NFS/SMB directory entry inside the BFS could still block
indefinitely.

P1 (acknowledged limitation) — Promise.race leaves the underlying
fs.realpath call running after timeout. fs.realpath cannot be cancelled
once submitted to libuv. Callers are sequential (one await at a time),
so at most one worker thread is occupied; the OS will eventually time
out the stuck call. This is documented in the module-level JSDoc.

P2 — Phantom allowlist check incorrectly flags bundled plugin IDs:
listChannelPlugins() returns bundled channel plugin IDs (telegram,
discord, browser, etc.) that are never in stateDir/extensions.
Add bundledPluginIds exclusion so the phantom-entry finding is scoped
to user-installed extension IDs only.

P2 — Rename MAX_SYMLINK_DEPTH / depthGuard to MAX_TOTAL_DIR_VISITS /
totalDirVisits to accurately reflect that the guard caps total BFS
iterations (2_000 * 20 = 40_000), not per-path symlink depth.

* fix(security): clean up realpathWithTimeout timer and add regression tests

- Clear the timer handle when fs.realpath resolves before the deadline,
  preventing timer accumulation during large audit runs with many files.
- Add .unref() on the timer so it cannot hold the process alive while
  waiting on a potentially hanging NFS/SMB path.

Regression tests added for three audit-extra.async security fixes:
- manifest parse error: malformed plugin package.json surfaces
  plugins.code_safety.manifest_parse_error (audit-extra.async.test.ts)
- phantom allowlist with bundled exclusion: bundled channel plugin IDs
  are excluded from plugins.allow_phantom_entries warnings; non-installed
  non-bundled IDs are correctly reported (audit-plugins-phantom.test.ts)
- unverifiable realpath escape: fs.realpath failure / timeout produces a
  skills.workspace.symlink_escape finding with 'realpath timed out' in
  the detail (audit-workspace-skill-escape.test.ts)

* chore(security): add TODO for structured logger in windows-acl resolveCurrentUserSid

console.warn is acceptable short-term but may be noisy on constrained
Windows hosts; note the follow-up in-code so it is not lost.

* chore: drop unrelated formatting churn from security PR

Restores extensions/memory-lancedb/config.ts and
src/agents/pi-embedded-helpers/errors.ts to their origin/main state.
These were line-wrap-only formatting changes with no relation to the
security fixes in this branch.

* fix(security): address Codex P2 review findings

1. Normalize plugins.allow entries through normalizePluginId before
   phantom-entry filtering so that bundled plugin aliases and legacy IDs
   are correctly excluded. Without this, valid allow entries that resolve
   via alias normalization could generate false-positive phantom warnings.

2. Surface a skills.workspace.scan_truncated warn finding when the BFS
   visit cap (MAX_TOTAL_DIR_VISITS) is hit mid-traversal. Previously the
   scanner silently returned partial results, allowing escaped SKILL.md
   symlinks in the unvisited tree to go undetected.

   listWorkspaceSkillMarkdownFiles now returns {skillFilePaths, truncated}
   and collectWorkspaceSkillSymlinkEscapeFindings emits the new finding
   when truncated is true.

Regression test added for the truncation path using a mocked readdir
that fills the queue past the cap (40 001 fake entries) and a mocked
realpath for zero-I/O iteration speed.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-04-16 13:40:05 -04:00
Peter Steinberger
1d27e0ef08 test: keep discord channel actions on public test SDK 2026-04-16 10:28:22 -07:00
Peter Steinberger
b31d243c57 fix: stabilize skills prompt ordering (#64198) (thanks @Bartok9) 2026-04-16 10:28:22 -07:00
Bartok
c4488d5ef5 fix: pin localeCompare to 'en' locale for cross-environment stability
Addresses review feedback: localeCompare without a fixed locale uses the
runtime default, which varies across servers. Pinning 'en' ensures
byte-identical prompts for cache stability. Applied at all three sort
points in workspace.ts.
2026-04-16 10:28:22 -07:00
Bartok Moltbot
a4b94f77b9 fix(skills): sort available_skills alphabetically for prompt cache stability
Sort the merged skill entries by name before rendering into the
available_skills prompt block. Previously the order depended on
Map insertion order which varies with skills.load.extraDirs config,
causing identical deployments to produce different prompts and bypass
LLM prompt caching.

Two sort points added:
1. loadSkillEntries — canonical ordering at the source
2. resolveWorkspaceSkillPromptState — ensures prompt stability even
   when callers pass pre-built entry arrays

Fixes #64167
2026-04-16 10:28:22 -07:00
Omar Shahine
77d9fd693f fix(bluebubbles): restore inbound image attachments and accept updated-message events (#67510)
* fix(bluebubbles): restore inbound image attachments and accept updated-message events

Four interconnected fixes for BlueBubbles inbound media:

1. Strip bundled-undici dispatcher from non-SSRF fetch path so attachment
   downloads no longer silently fail on Node 22+ (#64105, #61861)

2. Accept updated-message webhook events that carry attachments instead of
   filtering them as non-reaction events (#65430)

3. Include eventType in the persistent GUID dedup key so updated-message
   follow-ups are not rejected as duplicates of the original new-message (#52277)

4. Retry attachment fetch from BB API (2s delay) when the initial webhook
   arrives with an empty attachments array — image-only messages and
   updated-message events only (#67437)

Closes #64105, closes #61861, closes #65430.

* fix(bluebubbles): resolve review findings — SSRF policy, reuse extractAttachments, add tests

- F1 (BLOCKER): pass undefined instead of {} for SSRF policy when
  allowPrivateNetwork is false, so localhost BB servers are not blocked.
- F2 (IMPORTANT): reuse exported extractAttachments() from monitor-normalize
  instead of duplicating field extraction logic.
- F3 (IMPORTANT): simplify asRecord(asRecord(payload)?.data) to
  asRecord(payload.data) since payload is already Record<string, unknown>.
- F4 (NIT): bind retryMessageId before the guard to eliminate non-null assertion.
- F5 (IMPORTANT): add 4 tests for fetchBlueBubblesMessageAttachments covering
  success, non-ok HTTP, empty data, and guid-less entries.
- Add CHANGELOG entry for the user-facing fix.

* fix(ci): update raw-fetch allowlist line number after dispatcher strip

* fix(bluebubbles): resolve PR review findings (#67510)

- monitor-processing: move attachment retry into the !rawBody guard so
  image-only new-message events that arrive with empty attachments and
  empty text are recovered via a BB API refetch before being dropped.
  The existing retry block at the end of processMessageAfterDedupe was
  unreachable for this case because the !rawBody early-return fired
  first. (Greptile)
- monitor: derive isAttachmentUpdate from the normalized message shape
  instead of raw payload.data.attachments so updated-message webhooks
  with attachments under wrapper formats (payload.message, JSON-string
  payloads) are correctly routed through for processing instead of
  silently filtered. (Codex)
- types: use bundled-undici fetch when init.dispatcher is present so
  the SSRF guard's DNS-pinning dispatcher is preserved when this
  function is called as fetchImpl from guarded callers (e.g. the
  attachment download path via fetchRemoteMedia). Falls back to
  globalThis.fetch when no dispatcher is present so tests that stub
  globalThis.fetch keep working. (Codex)
- attachments: blueBubblesPolicy returns undefined for the non-private
  case (matching monitor-processing's helper) so sendBlueBubblesAttachment
  stops routing localhost BB through the SSRF guard. (Greptile)
- scripts/check-no-raw-channel-fetch: bump the types.ts allowlist line
  to match the restructured non-SSRF branch.

* fix(bluebubbles): move attachment retry before rawBody guard, fix stale log

Move the attachment retry block (2s BB API refetch for empty attachments)
before the !rawBody early-return guard. Previously, image-only messages
with text='' and attachments=[] would be dropped by the !rawBody check
before the retry could fire, making fix #4 dead code for its primary
use-case. Now the retry runs first and recomputes the placeholder from
resolved attachments so rawBody becomes non-empty when media is found.

Also fix stale log message that still said 'without reaction' after the
filter was expanded to pass through attachment updates.

* fix(bluebubbles): revert undici import, restore dispatcher-strip approach

Revert the @claude bot's undici import in types.ts — it introduced a
direct 'undici' dependency that is not declared in the BB extension's
package.json and would break isolated plugin installs. Restore the
original dispatcher-strip approach which is correct: the SSRF guard
already completed validation upstream before calling this function as
fetchImpl, so stripping the dispatcher does not weaken security.

* fix(bluebubbles): remove dead empty-body recovery block in !rawBody guard

The empty-body attachment-recovery block added in the earlier PR revision
is now redundant because the main retry block was moved above the rawBody
computation in 0d7d1c4208. Worse, that leftover block reassigned the
(now-const) placeholder variable, throwing `TypeError: Assignment to
constant variable` at runtime for image-only messages — breaking the very
recovery path it was meant to protect (flagged by Codex on 4bfc2777).

Remove the dead block; the up-front retry already handles the image-only
case by recovering attachments before the rawBody computation, so once we
reach the !rawBody guard with an empty body it is genuinely empty and
should drop as before.

* fix(ci): update raw-fetch allowlist line after dispatcher-strip revert

279dba17d2 reverted types.ts back to the dispatcher-strip approach,
which put the `fetch(url, ...)` call at line 189 instead of line 198.
Bump the allowlist entry to match so `lint:tmp:no-raw-channel-fetch`
stops failing check-additional.

* test(pdf-tool): update stale opus-4-6 constant to opus-4-7

`628b454eff feat: default Anthropic to Opus 4.7` bumped the bundled
anthropic image default to `claude-opus-4-7` but missed updating the
`ANTHROPIC_PDF_MODEL` constant in pdf-tool.model-config.test.ts. The
tests now fail on any PR that runs the `checks-node-agentic-agents-plugins`
shard because the resolver returns 4-7 while the test asserts 4-6.

Bump the constant to 4-7 to match the bundled default.

---------

Co-authored-by: Lobster <10343873+omarshahine@users.noreply.github.com>
2026-04-16 10:04:20 -07:00
Peter Steinberger
6429fa0a7f test: keep prompt cache PR gate green 2026-04-16 09:41:26 -07:00
Ted Li
eb10803691 fix(prompt): keep inbound chat ids out of system prefix 2026-04-16 09:41:26 -07:00
Peter Steinberger
1183832d4f fix: pin codex resume sandbox override 2026-04-16 17:31:41 +01:00
Peter Steinberger
d842ec4179 fix: tighten delivery mirror dedupe (#67185) (thanks @andyylin) 2026-04-16 09:20:01 -07:00
Andy
e95efa4373 fix(sessions): dedupe redundant delivery mirrors 2026-04-16 09:20:01 -07:00
Peter Steinberger
86f108401b fix: share agent harness runtime activation (#67474) 2026-04-16 09:06:45 -07:00
duqaXxX
f4bbd0122a test(plugins): remove useless spread in startup config fixture 2026-04-16 09:06:45 -07:00
duqaXxX
69ba924b53 fix(codex): activate harness plugin for forced runtime 2026-04-16 09:06:45 -07:00
Ayaan Zaidi
16c608e393 fix: harden cron announce NO_REPLY suppression (#65016) (thanks @BKF-Gitty) 2026-04-16 21:36:43 +05:30
Peter Steinberger
1d41ef724a test: isolate tts provider auto-selection env 2026-04-16 17:05:36 +01:00
Peter Steinberger
892baf2e81 test: align PDF tool expectations with Opus 4.7 2026-04-16 08:56:56 -07:00
Peter Steinberger
99dfc1b616 docs: add changelog for Codex Desktop user agents (#64666) (thanks @cyrusaf) 2026-04-16 08:56:56 -07:00
Cyrus Forbes
728295c046 Codex: parse Desktop app-server user agents 2026-04-16 08:56:56 -07:00
Gustavo Madeira Santana
d74533c718 Docs: remove QA Matrix changelog entry 2026-04-16 11:42:33 -04:00
Peter Steinberger
461d0050d9 fix: keep codex resume runs non-interactive (#67666) (thanks @plgonzalezrx8) 2026-04-16 08:41:57 -07:00
Pedro Gonzalez
4c66978591 security(codex): restore sandbox protections for resumed CLI sessions 2026-04-16 08:41:57 -07:00
Peter Steinberger
1a98090bf3 test: harden Parallels update smoke 2026-04-16 08:19:30 -07:00
Peter Steinberger
628b454eff feat: default Anthropic to Opus 4.7 2026-04-16 16:12:06 +01:00
Ayaan Zaidi
75c551e89e fix: harden node-host shell payload mutability checks 2026-04-16 20:34:17 +05:30
tmimmanuel
29919bb6e4 fix: land node-host approval binding for native binaries (#66731) (thanks @tmimmanuel)
* fix(node-host): allow absolute-path native binaries through approval binder

* test(node-host): cover binary binder edge cases

* test(node-host): use stable native binary fixture

* fix(ci): restore fail-closed race handling

* refactor(node-host): distill approval binding regressions

* fix(node-host): fail closed on unknown shell payload headers

* fix: land node-host approval binding for native binaries (#66731) (thanks @tmimmanuel)

* fix: keep relative shell binary payloads fail-closed (#66731) (thanks @tmimmanuel)

* fix: keep shell binary bypass on stable paths only (#66731) (thanks @tmimmanuel)

* fix: fail closed on symlinked shell binary targets (#66731) (thanks @tmimmanuel)

---------

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
2026-04-16 20:30:09 +05:30
Mason Huang
69d25f5f16 CI: add daily schedule to CodeQL workflow (#67645)
* CI: add weekly schedule to CodeQL workflow

* CI: add daily schedule to CodeQL workflow and pin third-party actions
2026-04-16 21:27:45 +08:00
Chunyue Wang
8c11210fe5 fix(gateway): capture config hash after plugin auto-enable to prevent restart loop (#67557)
Merged via squash.

Prepared head SHA: 07250958a7
Co-authored-by: openperf <80630709+openperf@users.noreply.github.com>
Co-authored-by: openperf <80630709+openperf@users.noreply.github.com>
Reviewed-by: @openperf
2026-04-16 21:18:24 +08:00
stain lu
c3c7a9953f fix: repair sanitized replay tool results before send (#67620) (thanks @stainlu)
* fix(agents): preserve native Anthropic tool IDs for hybrid providers

Fixes #66892

MiniMax and other hybrid providers use api.minimaxi.com/anthropic
(modelApi: anthropic-messages), which generates and expects native
Anthropic tool_call_ids in toolu_* format. The hybrid replay policy
(buildHybridAnthropicOrOpenAIReplayPolicy) applied strict
sanitization that stripped underscores from these IDs, causing
MiniMax to reject them with error 2013.

The native Anthropic provider already preserved these IDs via
preserveNativeAnthropicToolUseIds (added in 4613f121ad). This
commit enables the same flag for the hybrid anthropic-messages
branch, so toolu_* IDs pass through unsanitized while other
synthetic IDs still get strict cleanup.

* fix(agents): repair sanitized replay tool results before send

* fix: repair sanitized replay tool results before send (#67620) (thanks @stainlu)

* fix: preserve aborted-span tool results during replay sanitize (#67620) (thanks @stainlu)

---------

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
2026-04-16 18:38:57 +05:30
Ayaan Zaidi
de129a6530 fix: restrict HTML timeout short-circuit to transient statuses 2026-04-16 18:33:35 +05:30
Ayaan Zaidi
3525273930 fix: keep TUI watchdog bound to active run (#67401) (thanks @xantorres) 2026-04-16 18:31:56 +05:30
Xan Torres
d7f489f85e Gateway/skills: dedupe skills prefix-match + drop dead fallback on log 2026-04-16 18:31:56 +05:30
Xan Torres
b555214c96 Extensions/lmstudio: back off inference preload after consecutive failures 2026-04-16 18:31:56 +05:30
Xan Torres
f44ab20d4d TUI/streaming: add watchdog that resets the activity indicator after delta silence 2026-04-16 18:31:56 +05:30
Xan Torres
36ed36768c Agents/tool-loop: enable unknown-tool stream guard by default 2026-04-16 18:31:56 +05:30
Xan Torres
b23d59a522 Gateway/skills: invalidate session skills snapshot on config write 2026-04-16 18:31:56 +05:30
stain lu
e588e904a7 fix: classify HTML provider error pages correctly (#67642) (thanks @stainlu)
* fix(agents): classify Cloudflare/CDN HTML error pages as transport failures

Fixes #67517

When a provider endpoint returns an HTML error page (e.g. Cloudflare
502/503/520-524), the pattern-based message classifiers would scan
the HTML body and misinterpret embedded text like "Rate limit
exceeded" as a structured rate_limit API error. This caused
incorrect failover behavior (profile rotation instead of clean
retry/fallback) and left the TUI stuck.

Two fixes:
1. classifyFailoverSignal now short-circuits on HTML responses
   before running pattern matchers, returning "timeout" (transport
   failure) so retry/fallback handles them correctly.
2. classifyProviderRuntimeFailureKind now detects HTML errors at
   any status (not just 403), returning "upstream_html" for
   non-403 statuses with a clear user-facing message about
   CDN/gateway errors.

Adds regression tests covering Cloudflare 502/503 HTML with
embedded rate-limit text, 403 HTML (still classified as auth),
and JSON rate-limit responses (still classified correctly).

* fix: preserve auth and proxy HTML classification

* fix: classify HTML provider error pages correctly (#67642) (thanks @stainlu)

---------

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
2026-04-16 18:19:53 +05:30
Mason Huang
55f05df77e fix(skills): remove unused model-usage import (#67641)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-16 19:56:34 +08:00
Nimrod Gutman
e485f24301 docs(changelog): credit codex fix superseded PRs 2026-04-16 14:33:19 +03:00
Nimrod Gutman
90801ba400 fix(openai-codex): normalize stale transport metadata in resolution and discovery (#67635)
Merged via squash.

Supersedes:
- #66969 by @saamuelng601-pixel
- #67159 by @hclsys

Co-authored-by: saamuelng601-pixel <274746699+saamuelng601-pixel@users.noreply.github.com>
Co-authored-by: hclsys <7755017+hclsys@users.noreply.github.com>
2026-04-16 14:30:05 +03:00
Mason Huang
f697b01747 CI: pin Docker-related GitHub Actions (#67632)
* CI: pin Docker-related GitHub Actions

* CI: pin docker build-push action
2026-04-16 19:23:03 +08:00
Mason Huang
44a6e50fcc Android: modernize WebView and discovery API usage (#67627)
* Android: modernize WebView and discovery API usage

* Android: narrow discovery API cleanup scope
2026-04-16 19:22:15 +08:00
Mason Huang
fbccc18e74 fix(deps): bump hono to 4.12.14 and @hono/node-server to 1.19.14 (GHSA-458j-xx4x-4375) (#67613) 2026-04-16 18:23:53 +08:00
Mason Huang
2c2dc00fb4 fix(deps): bump dompurify to 3.4.0 (#67614) 2026-04-16 18:18:55 +08:00
Mason Huang
01b7516a95 CI: add explicit permissions to all workflow jobs (fixes code-scanning #40-#57) (#67612) 2026-04-16 18:18:35 +08:00
stain lu
6ea3cddf0d fix: register bundled TTS providers and route overrides correctly (#62846) (thanks @stainlu)
* fix(microsoft,elevenlabs): add enabledByDefault so speech providers register at runtime

* fix(tts): route generic directive tokens to the explicitly declared provider

Addresses the P2 Codex review on #62846 that flagged auto-enabling
ElevenLabs as a product regression for MiniMax users. Both providers
claim the generic `speed` token, and parseTtsDirectives walked
providers in autoSelectOrder with first-match-wins, so inputs like
`[[tts:provider=minimax speed=1.2]]` silently routed speed to
providerOverrides.elevenlabs once elevenlabs participated in every
parse pass.

The parser now pre-scans for `provider=` (honoring legacy last-wins
semantics) and routes generic tokens with the declared provider tried
first, falling back to autoSelectOrder when it doesn't handle the key.
Token order inside the directive no longer matters: `speed=1.2` before
or after `provider=minimax` both resolve to MiniMax.

Adds a regression test suite covering the exact ElevenLabs/MiniMax
speed collision plus fallback, mixed-token, last-wins, and
allowProvider-disabled cases. parseTtsDirectives had no prior test
coverage.

* fix(tts): prefer active provider for generic directives

* fix: register bundled TTS providers safely (#62846) (thanks @stainlu)

* fix: use exported TTS SDK seam (#62846) (thanks @stainlu)

---------

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
2026-04-16 15:26:38 +05:30
stain lu
ecfaf64526 fix: align host tilde paths with OS home (#62804) (thanks @stainlu)
* fix(tools): expand tilde in host edit/write paths (non-workspace mode)

* test: use it.runIf for visible skip when tmpdir is not under home

* fix(tools): address Codex P2 review on tilde host edit/write

Responds to two P2 findings from chatgpt-codex-connector on #62804:

1. Tests never ran in CI. The it.runIf(tmpdirUnderHome) guard always
   skipped on Linux runners where os.tmpdir() is /tmp, outside $HOME, so
   the regression tests reported green without executing. Tmpdirs now use
   the test-isolated HOME (process.env.HOME from test/test-env.ts) so
   tests run in every environment and match what expandHomePrefix
   resolves, keeping them hermetic.

2. Edit recovery path resolution was inconsistent. resolveEditPath
   inlined os.homedir() for tilde expansion, bypassing OPENCLAW_HOME,
   while the write/edit operations use expandHomePrefix. Under a custom
   OPENCLAW_HOME, wrapEditToolWithRecovery's readback targeted a
   different file than the edit actually touched, so successful edits
   could be reported as failures. resolveEditPath now uses the same
   expandHomePrefix helper.

* test(tools): verify tilde expansion honors OPENCLAW_HOME override

The prior tests covered tilde expansion but only under the default test
home, which matches os.homedir(). That passed whether the production code
used expandHomePrefix() or inlined os.homedir() — the behaviors only
diverge when OPENCLAW_HOME is set to a path outside $HOME.

Adds four tests that set OPENCLAW_HOME to a temp dir explicitly outside
$HOME and verify that write/mkdir/read/access tilde operations resolve
against OPENCLAW_HOME, not os.homedir(). These would fail if
pi-tools.read.ts or pi-tools.host-edit.ts reverted to os.homedir(),
directly covering the Codex P2 feedback about OPENCLAW_HOME consistency.

Uses the same env snapshot/restore pattern as test/helpers/temp-home.ts.

* Agents: resolve host tilde paths against OS home

* fix: align host tilde paths with OS home (#62804) (thanks @stainlu)

* fix: keep the changelog entry in the active block (#62804) (thanks @stainlu)

---------

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
2026-04-16 14:37:55 +05:30
Gustavo Madeira Santana
05cac5b980 QA: fix private dist freshness check 2026-04-16 03:44:40 -04:00
Gustavo Madeira Santana
4db162db7f QA: split lab runtime and extend Matrix coverage (#67430)
Merged via squash.

Prepared head SHA: 790418b93b
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-04-16 03:08:39 -04:00
Gustavo Madeira Santana
8ecb6bbb12 QA: accept nodejs as Node runtime 2026-04-16 02:39:52 -04:00
Gustavo Madeira Santana
51b5d16faf CI: cap parity gate concurrency 2026-04-16 02:27:44 -04:00
Barron Roth
bf59917cd1 fix: add Google Gemini TTS provider (#67515) (thanks @barronlroth)
* Add Google Gemini TTS provider

* Remove committed planning artifact

* Explain Google media provider type shape

* google: distill Gemini TTS provider

* fix: add Google Gemini TTS provider (#67515) (thanks @barronlroth)

* fix: honor cfg-backed Google TTS selection (#67515) (thanks @barronlroth)

* fix: narrow Google TTS directive aliases (#67515) (thanks @barronlroth)

---------

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
2026-04-16 11:54:35 +05:30
Tak Hoffman
b10ae0bf13 fix(docs): add active memory speed recommendations 2026-04-16 01:11:08 -05:00
Subash Natarajan
6f5459364a fix: restore Ollama chat model IDs (#67457) (thanks @suboss87)
* fix(ollama): strip provider prefix from model ID in chat requests

buildOllamaChatRequest passed params.modelId directly to the Ollama API
without stripping the "ollama/" provider prefix. The embedding provider
already handles this (normalizeEmbeddingModel at line 100), but the chat
stream path did not. When setup writes the primary model as
"ollama/<model>" or the model ID flows through without normalization,
the Ollama API rejects it with a 404.

Closes #67435

* ollama: guard chat fetch and streamline tests

* fix: restore Ollama chat model IDs (#67457) (thanks @suboss87)

* fix: preserve Ollama default chat fallback (#67457) (thanks @suboss87)

---------

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
2026-04-16 11:15:37 +05:30
OwenYWT
b878d50e0e fix(whatsapp): write creds.json atomically (#63577)
Merged via squash.

Prepared head SHA: 70ef5b379f
Co-authored-by: OwenYWT <103073962+OwenYWT@users.noreply.github.com>
Co-authored-by: mcaxtr <7562095+mcaxtr@users.noreply.github.com>
Reviewed-by: @mcaxtr
2026-04-16 02:44:46 -03:00
Omar Shahine
4af7641350 BlueBubbles/catchup: per-message retry cap for wedged messages (#66870) (#67426)
Merged via squash.

Prepared head SHA: 39e3cf1df5
Co-authored-by: omarshahine <10343873+omarshahine@users.noreply.github.com>
Co-authored-by: omarshahine <10343873+omarshahine@users.noreply.github.com>
Reviewed-by: @omarshahine
2026-04-15 22:23:27 -07:00
Neerav Makwana
405c63fb32 fix: flush creds queue before reconnect socket open (#67464) (thanks @neeravmakwana)
* WhatsApp: flush creds queue before reconnect socket open

* fix: flush creds queue before reconnect socket open (#67464) (thanks @neeravmakwana)

---------

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
2026-04-16 10:16:00 +05:30
OfflynAI
78df859e15 fix: strip standalone <function> tool call tags from visible text (#67318) (thanks @joelnishanth)
* fix: strip standalone <function> tool call tags from visible text (#67093)

Models like Gemma emit tool calls as standalone <function> blocks with
nested <parameter> XML instead of wrapping them in <tool_call>. The
existing stripToolCallXmlTags only recognized tool_call, tool_result,
function_call, function_calls, and tool_calls — so bare <function> and
</function> tags leaked through to the user as raw syntax on Discord
and other channels.

Add "function" to TOOL_CALL_TAG_NAMES and extend the payload detection
for <function> tags to check XML payloads (not just JSON), matching the
same behavior already applied to <tool_call>. Other tag types keep the
more conservative JSON-only check to avoid stripping prose examples.

Made-with: Cursor

* Text: harden standalone <function> stripping

* fix: strip standalone <function> tool call tags from visible text (#67318) (thanks @joelnishanth)

---------

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
2026-04-16 09:53:35 +05:30
Ayaan Zaidi
898fd0482a fix(agents): preserve cli session metadata before transcript persist (#67490) 2026-04-16 09:30:31 +05:30
Ayaan Zaidi
c1817c62e3 docs(changelog): move cli transcript entry 2026-04-16 09:30:31 +05:30
Ayaan Zaidi
3a3fae0eac fix(agents): normalize cli transcript api field 2026-04-16 09:30:31 +05:30
Ayaan Zaidi
6c343f1f58 docs(changelog): note cli transcript persistence 2026-04-16 09:30:31 +05:30
Ayaan Zaidi
b8ef507cc0 fix(agents): persist cli transcript turns 2026-04-16 09:30:31 +05:30
Peter Steinberger
c56b56e514 fix(msteams): harden security-sensitive flows (#65841)
* fix(msteams): validate participant graph params

* fix(msteams): restore media fetch ip guard

* fix(msteams): open delegated auth urls without shell
2026-04-15 22:30:23 -05:00
Ziy
053c5b05c1 [Dashboard] Fix exec approval modal overflow for long command content (#67082)
Merged via squash.

Prepared head SHA: 5d14e78381
Co-authored-by: Ziy1-Tan <49604965+Ziy1-Tan@users.noreply.github.com>
Co-authored-by: hxy91819 <8814856+hxy91819@users.noreply.github.com>
Reviewed-by: @hxy91819
2026-04-16 11:28:20 +08:00
Gustavo Madeira Santana
7fd57717a9 Docs: remove QA changelog entry 2026-04-15 22:02:36 -04:00
Gustavo Madeira Santana
d5933af80b QA: fix private runtime source loading (#67428)
Merged via squash.

Prepared head SHA: b8bf2b6be6
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-04-15 21:59:16 -04:00
Omar Shahine
489404d75e docs(gateway): correct protocol.md schema path, hello-ok example, auth precedence, and add client constants table (#67372)
Co-authored-by: Omar Shahine <10343873+omarshahine@users.noreply.github.com>
2026-04-15 17:41:32 -07:00
Gustavo Madeira Santana
4ffa6218c4 CI: pin Node 22 runners to 22.18.0 2026-04-15 20:33:12 -04:00
Omar Shahine
f2fdb9d125 models.authStatus: normalize provider ids + tighten env-backed escape hatch (#67253)
Fix false-positive "missing" alerts on the Model Auth status card:
- Normalize provider ids before expectsOAuth membership check (alias mismatch)
- Apply env-backed escape hatch to auth.profiles loop (not just models.providers)
- Check actual env var resolution for SecretRef apiKeys

Co-authored-by: Omar Shahine <10343873+omarshahine@users.noreply.github.com>
2026-04-15 15:29:26 -07:00
Vincent Koc
7694a926c4 Update CHANGELOG.md 2026-04-15 23:06:42 +01:00
Peter Steinberger
045ea7bf6a test(parallels): clean up npm update guard jobs 2026-04-15 22:43:28 +01:00
Gustavo Madeira Santana
b2974da33a Plugins: prefer scanDir override paths 2026-04-15 17:24:31 -04:00
mjamiv
8c392f0019 fix(dreaming): default storage.mode to "separate" so phase blocks stop polluting daily memory files (#66412)
Merged via squash.

Prepared head SHA: 4b1c8ac4ec
Co-authored-by: mjamiv <142179942+mjamiv@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
2026-04-15 13:49:18 -07:00
Josh Lehman
a1b01f0281 fix(memory-core): skip dreaming transcript ingestion via session store (#67315)
Merged via squash.

Prepared head SHA: 87c09b2a75
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
2026-04-15 13:09:07 -07:00
Josh Lehman
5dcf526a43 fix: dedupe replayed exec.finished node events (#67281)
* docs: add async exec duplicate completion investigation

Add an internal refactor note tracing the node exec completion to system event to heartbeat to transcript path for duplicate async exec injections. Document the most likely gateway-side gap as missing idempotency for replayed exec.finished events, and note why plain outbound delivery retry is a weaker fit for duplicate user turns.

Regeneration-Prompt: |
  Investigate a live duplicate async exec completion that appeared as two identical user turns in an OpenClaw session. Trace the completion path from exec producers into enqueueSystemEvent, heartbeat wake scheduling, prompt assembly, and embedded transcript persistence. Decide whether duplicate wake handling, outbound delivery retry, or duplicate completion event ingestion is the more likely cause, cite the exact code locations, and capture the smallest plausible fix seam without making runtime changes.

* fix: dedupe replayed exec finished node events

Add a narrow idempotency guard in the gateway node-event handler for repeated exec.finished events with the same canonical session key and runId. This blocks replayed async exec completions from being enqueued and heartbeated twice into the parent session. Also only request a heartbeat when the system event was actually queued, and add a regression test for duplicate runId injection.

Regeneration-Prompt: |
  Prevent duplicate async exec completion events from being injected twice into the parent session. Keep the scope tight around the highest-confidence path: node exec.finished events entering gateway server-node-events and becoming system-event-driven heartbeat prompts. Add a small idempotency guard keyed by canonical session plus exec runId, avoid broader delivery or retry changes unless needed, and add regression coverage that fails if the same exec.finished replay is enqueued and woken twice.

* fix: note exec finished replay dedupe
2026-04-15 13:06:18 -07:00
Peter Steinberger
a5dafa27b6 fix(release): allow legacy qa sidecar verification 2026-04-15 20:49:30 +01:00
dallylee
bd7418d4e9 fix(agents): classify connection-mismatch replay errors as replay-invalid (#66475)
Merged via squash.

Prepared head SHA: 97738583de
Co-authored-by: dallylee <132358482+dallylee@users.noreply.github.com>
Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com>
Reviewed-by: @altaywtf
2026-04-15 22:49:11 +03:00
Peter Steinberger
943cb47274 fix(qa): use exported runner sdk seam 2026-04-15 20:26:12 +01:00
Peter Steinberger
4caa882476 test: harden gateway live docker flake handling 2026-04-15 20:13:28 +01:00
Devin Robison
52ef42302e fix: tighten trusted tool media passthrough (#67303)
* fix: tighten trusted tool media passthrough

* changelog: tighten trusted tool media passthrough (#67303)

* address review: thread rawToolName into emitToolResultOutput and keep plugin-tool media passthrough

- Pass rawToolName through emitToolResultOutput params so the emit and
  collect calls no longer reference an out-of-scope identifier
  (ReferenceError on any verbose tool-output path).
- Widen builtinToolNames to all effective tool raw names for this run
  (core + bundled/trusted plugin tools), so plugin tools on the trusted
  media list still receive local MEDIA: passthrough. Admission-time
  client-tool conflict check keeps using the core-only set so unrelated
  plugin names do not spuriously reject client definitions; MEDIA
  passthrough is still gated by the raw-name set, so a client tool that
  normalize-collides with a plugin name cannot inherit its media trust.
- Add unit coverage for bundled-plugin raw-name passthrough and for
  case-variant plugin-name collisions.

* drop redundant String() casts flagged by oxlint no-useless-cast

The names from effectiveTools, client tool function names, and the
existingToolNames iterable are already typed as string, so wrapping them
in String(...) adds nothing and trips oxlint's no-useless-cast rule.
2026-04-15 13:12:44 -06:00
Bartok9
4de56b18ba fix(dreaming): use ingestion date for dayBucket instead of file date (#67091)
Merged via squash.

Prepared head SHA: 2df44e4d50
Co-authored-by: Bartok9 <259807879+Bartok9@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
2026-04-15 11:56:10 -07:00
Peter Steinberger
a177d8d454 build: refresh release baselines 2026-04-15 19:41:32 +01:00
Peter Steinberger
23dca0a089 test: fix upstream type drift 2026-04-15 19:31:10 +01:00
Peter Steinberger
4efd3c3d74 test: harden beta release gates 2026-04-15 19:28:49 +01:00
Peter Steinberger
41699cfc2d build: fix bundled channel smoke layout 2026-04-15 19:28:49 +01:00
Peter Steinberger
0a57279309 docs: update beta changelog 2026-04-15 19:28:49 +01:00
Pavan Kumar Gondhi
1470de5d3e fix(webchat): reject remote-host file:// URLs in media embedding path [AI-assisted] (#67293)
* fix: address issue

* fix: address PR review feedback

* fix: address PR review feedback

* docs: add changelog entry for PR merge
2026-04-15 23:58:01 +05:30
Gustavo Madeira Santana
84185cb3eb docs: clean up clawtributors generator 2026-04-15 14:20:32 -04:00
hcl
be7f4a2342 fix(terminal): tolerate undefined path in formatDocsLink (#67076, #67074) (#67086)
formatDocsLink called path.trim() unconditionally. The typed contract
says 'docsPath: string' (required on ChannelMeta), but a handful of
channel plugins and catalog rows leave it unset at runtime, so
onboarding flows that call formatChannelSelectionLine(entry.meta, ...)
hit a TypeError on the first meta without a docsPath:

  TypeError: Cannot read properties of undefined (reading 'trim')

Symptom: 'openclaw onboard --install-daemon' and the 'Select channel
(QuickStart)' -> 'Skip for now' path both crash on 2026.4.12 and
2026.4.14.

Fix: widen formatDocsLink's path parameter to 'string | undefined |
null' and fall back to the docs root when path is missing. The single
call site that guards with 'if (params.docsPath)' stays fine; the
unguarded channel-selection path now degrades gracefully.

Fixes #67076
Fixes #67074
2026-04-15 23:40:52 +05:30
Gustavo Madeira Santana
2bfd808a83 fix(matrix): skip pairing-store reads for room auth (#67325)
Merged via squash.

Prepared head SHA: 121ff3b38c
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-04-15 14:08:43 -04:00
Tak Hoffman
4f00b76925 fix(context-window): Tighten context limits and bound memory excerpts (#67277)
* Tighten context limits and bound memory excerpts

* Align startup context defaults in config docs

* Align qmd memory_get bounds with shared limits

* Preserve qmd partial memory reads

* Fix shared memory read type import

* Add changelog entry for context bounds
2026-04-15 13:06:02 -05:00
Peter Steinberger
89d2c145df test: harden gateway live docker test assertions 2026-04-15 18:47:40 +01:00
Gustavo Madeira Santana
4dfcc030ae fix(release): ignore leaf test filenames 2026-04-15 13:38:38 -04:00
Peter Steinberger
893d0635b6 test(parallels): harden smoke harness progress and gateway startup 2026-04-15 18:33:05 +01:00
Pavan Kumar Gondhi
6e58f1f9f5 fix(gateway): enforce localRoots containment on webchat audio embedding path [AI-assisted] (#67298)
* fix: address issue

* fix: address review feedback

* fix: address PR review feedback

* docs: add changelog entry for PR merge
2026-04-15 22:54:06 +05:30
Gustavo Madeira Santana
7c6f2c0a5a Build: prune packaged runtime test cargo (#67275)
Merged via squash.

Prepared head SHA: 403f8e5749
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-04-15 13:18:03 -04:00
Pavan Kumar Gondhi
f8705f512b fix(matrix): block DM pairing-store entries from authorizing room control commands [AI-assisted] (#67294)
* fix: address issue

* fix: address review feedback

* docs: add changelog entry for PR merge
2026-04-15 22:45:14 +05:30
744 changed files with 39018 additions and 10106 deletions

View File

@@ -22,7 +22,7 @@ Use this skill for Parallels guest workflows and smoke interpretation. Do not lo
- Windows: `90m`
- aggregate npm-update wrapper: `150m`
If a lane hits the cap, stop there, inspect the newest `/tmp/openclaw-parallels-*` run directory and phase log, then fix or rerun the smallest affected lane. Do not keep waiting on a capped lane.
- Actual OpenClaw npm install/update phases are a stricter budget than whole lanes: they should finish within 5 minutes. If a phase named `install-main`, `install-latest`, `install-baseline`, `install-baseline-package`, `update-dev`, or same-guest `openclaw update` exceeds 300s, treat it as a failure/harness bug and start diagnosis from that phase log. Do not wait for a longer lane cap.
- Actual OpenClaw npm install/update phases are a stricter budget than whole lanes: install phases should finish within 7 minutes, and update phases should finish within 5 minutes. If a phase named `install-main`, `install-latest`, `install-baseline`, or `install-baseline-package` exceeds 420s, or a phase named `update-dev` / same-guest `openclaw update` exceeds 300s, treat it as a failure/harness bug and start diagnosis from that phase log. Do not wait for a longer lane cap.
- For a full OS matrix, prefer running independent guest-family lanes in parallel when host capacity allows:
- `timeout --foreground 75m pnpm test:parallels:macos -- --json`
- `timeout --foreground 90m pnpm test:parallels:windows -- --json`

View File

@@ -11,6 +11,8 @@ self-hosted-runner:
- blacksmith-16vcpu-windows-2025
- blacksmith-32vcpu-windows-2025
- blacksmith-16vcpu-ubuntu-2404-arm
- blacksmith-6vcpu-macos-latest
- blacksmith-12vcpu-macos-latest
# Ignore patterns for known issues
paths:

View File

@@ -20,6 +20,8 @@ jobs:
# Preflight: establish routing truth and job matrices once, then let real
# work fan out from a single source of truth.
preflight:
permissions:
contents: read
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
runs-on: blacksmith-16vcpu-ubuntu-2404
timeout-minutes: 20
@@ -227,7 +229,7 @@ jobs:
check_name: "checks-node-compat-node22",
runtime: "node",
task: "compat-node22",
node_version: "22.x",
node_version: "22.18.0",
cache_key_suffix: "node22",
},
]
@@ -298,6 +300,8 @@ jobs:
# Run the fast security/SCM checks in parallel with scope detection so the
# main Node jobs do not have to wait for Python/pre-commit setup.
security-fast:
permissions:
contents: read
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
runs-on: blacksmith-16vcpu-ubuntu-2404
timeout-minutes: 20
@@ -396,6 +400,8 @@ jobs:
# Keep this overlapping with the fast correctness lanes so green PRs get heavy
# test/build feedback sooner instead of waiting behind a full `check` pass.
build-artifacts:
permissions:
contents: read
needs: [preflight]
if: needs.preflight.outputs.run_build_artifacts == 'true'
runs-on: blacksmith-16vcpu-ubuntu-2404
@@ -449,6 +455,8 @@ jobs:
retention-days: 1
checks-fast-core:
permissions:
contents: read
name: ${{ matrix.check_name }}
needs: [preflight]
if: needs.preflight.outputs.run_checks_fast == 'true'
@@ -493,6 +501,8 @@ jobs:
esac
checks-node-extensions-shard:
permissions:
contents: read
name: ${{ matrix.check_name }}
needs: [preflight]
if: needs.preflight.outputs.run_checks_fast == 'true'
@@ -520,6 +530,8 @@ jobs:
run: pnpm test:extensions:batch -- "$OPENCLAW_EXTENSION_BATCH"
checks-node-extensions:
permissions:
contents: read
name: checks-node-extensions
needs: [preflight, checks-node-extensions-shard]
if: always() && needs.preflight.outputs.run_checks_fast == 'true'
@@ -536,6 +548,8 @@ jobs:
fi
checks:
permissions:
contents: read
name: ${{ matrix.check_name }}
needs: [preflight, build-artifacts]
if: always() && needs.preflight.outputs.run_checks == 'true' && needs.build-artifacts.result == 'success'
@@ -622,6 +636,8 @@ jobs:
esac
checks-node-core-test-shard:
permissions:
contents: read
name: ${{ matrix.check_name }}
needs: [preflight, build-artifacts]
if: always() && needs.preflight.outputs.run_checks == 'true' && needs.build-artifacts.result == 'success'
@@ -632,10 +648,51 @@ jobs:
matrix: ${{ fromJson(needs.preflight.outputs.checks_node_core_test_matrix) }}
steps:
- name: Checkout
uses: actions/checkout@v6
with:
persist-credentials: false
submodules: false
shell: bash
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_SHA: ${{ github.sha }}
CHECKOUT_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
workdir="$GITHUB_WORKSPACE"
auth_header="$(printf 'x-access-token:%s' "$CHECKOUT_TOKEN" | base64 | tr -d '\n')"
reset_checkout_dir() {
mkdir -p "$workdir"
find "$workdir" -mindepth 1 -maxdepth 1 -exec rm -rf {} +
}
checkout_attempt() {
local attempt="$1"
reset_checkout_dir
git init "$workdir" >/dev/null
git config --global --add safe.directory "$workdir"
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}"
git -C "$workdir" config gc.auto 0
timeout --signal=TERM 30s git -C "$workdir" \
-c protocol.version=2 \
-c "http.https://github.com/.extraheader=AUTHORIZATION: basic ${auth_header}" \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target"
git -C "$workdir" checkout --force --detach "$CHECKOUT_SHA"
echo "checkout attempt ${attempt}/2 succeeded"
}
for attempt in 1 2; do
if checkout_attempt "$attempt"; then
exit 0
fi
echo "checkout attempt ${attempt}/2 failed"
sleep $((attempt * 5))
done
echo "checkout failed after 2 attempts" >&2
exit 1
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
@@ -711,6 +768,8 @@ jobs:
EOF
checks-node-core-test:
permissions:
contents: read
name: checks-node-core
needs: [preflight, checks-node-core-test-shard]
if: always() && needs.preflight.outputs.run_checks == 'true'
@@ -727,6 +786,8 @@ jobs:
fi
extension-fast:
permissions:
contents: read
name: "extension-fast"
needs: [preflight]
if: needs.preflight.outputs.run_extension_fast == 'true'
@@ -755,6 +816,8 @@ jobs:
# Types, lint, and format check.
check:
permissions:
contents: read
name: "check"
needs: [preflight]
if: always() && needs.preflight.outputs.run_check == 'true'
@@ -782,6 +845,8 @@ jobs:
run: pnpm build:strict-smoke
check-additional:
permissions:
contents: read
name: "check-additional"
needs: [preflight]
if: always() && needs.preflight.outputs.run_check_additional == 'true'
@@ -989,6 +1054,8 @@ jobs:
exit "$failures"
build-smoke:
permissions:
contents: read
name: "build-smoke"
needs: [preflight, build-artifacts]
if: always() && needs.preflight.outputs.run_build_smoke == 'true' && (github.event_name != 'push' || needs.build-artifacts.result == 'success')
@@ -1043,6 +1110,8 @@ jobs:
# Validate docs (format, lint, broken links) only when docs files changed.
check-docs:
permissions:
contents: read
needs: [preflight]
if: needs.preflight.outputs.run_check_docs == 'true'
runs-on: blacksmith-16vcpu-ubuntu-2404
@@ -1064,6 +1133,8 @@ jobs:
run: pnpm check:docs
skills-python:
permissions:
contents: read
needs: [preflight]
if: needs.preflight.outputs.run_skills_python_job == 'true'
runs-on: blacksmith-16vcpu-ubuntu-2404
@@ -1092,6 +1163,8 @@ jobs:
run: python -m pytest -q skills
checks-windows:
permissions:
contents: read
name: ${{ matrix.check_name }}
needs: [preflight, build-artifacts]
if: always() && needs.preflight.outputs.run_checks_windows == 'true' && needs.build-artifacts.result == 'success'
@@ -1207,10 +1280,12 @@ jobs:
esac
macos-node:
permissions:
contents: read
name: ${{ matrix.check_name }}
needs: [preflight, build-artifacts]
if: always() && needs.preflight.outputs.run_macos_node == 'true' && needs.build-artifacts.result == 'success'
runs-on: macos-latest
runs-on: blacksmith-6vcpu-macos-latest
timeout-minutes: 20
strategy:
fail-fast: false
@@ -1239,6 +1314,30 @@ jobs:
name: canvas-a2ui-bundle
path: src/canvas-host/a2ui/
- name: Patch mlx-audio-swift manifest
run: |
set -euo pipefail
swift package resolve --package-path apps/macos >/dev/null
chmod u+w apps/macos/.build/checkouts/mlx-audio-swift/Package.swift
python <<'PY'
from pathlib import Path
path = Path("apps/macos/.build/checkouts/mlx-audio-swift/Package.swift")
text = path.read_text()
if "Models/Qwen3/README.md" in text:
print("mlx-audio-swift README excludes already present")
raise SystemExit(0)
needle = ' path: "Sources/MLXAudioTTS"\n'
replacement = """ path: \"Sources/MLXAudioTTS\",\n exclude: [\n \"Models/Llama/README.md\",\n \"Models/Marvis/README.md\",\n \"Models/PocketTTS/README.md\",\n \"Models/Qwen3/README.md\",\n \"Models/Soprano/README.md\",\n ]\n"""
if needle not in text:
raise SystemExit("Could not find MLXAudioTTS target path in mlx-audio-swift Package.swift")
path.write_text(text.replace(needle, replacement, 1))
print(f"Patched {path}")
PY
- name: TS tests (macOS)
env:
NODE_OPTIONS: --max-old-space-size=4096
@@ -1260,10 +1359,12 @@ jobs:
esac
macos-swift:
permissions:
contents: read
name: "macos-swift"
needs: [preflight]
if: needs.preflight.outputs.run_macos_swift == 'true'
runs-on: macos-latest
runs-on: blacksmith-12vcpu-macos-latest
timeout-minutes: 20
steps:
- name: Checkout
@@ -1272,11 +1373,6 @@ jobs:
persist-credentials: false
submodules: false
- name: Select Xcode 26.1
run: |
sudo xcode-select -s /Applications/Xcode_26.1.app
xcodebuild -version
- name: Install XcodeGen / SwiftLint / SwiftFormat
run: brew install xcodegen swiftlint swiftformat
@@ -1288,6 +1384,30 @@ jobs:
restore-keys: |
${{ runner.os }}-swiftpm-
- name: Patch mlx-audio-swift manifest
run: |
set -euo pipefail
swift package resolve --package-path apps/macos >/dev/null
chmod u+w apps/macos/.build/checkouts/mlx-audio-swift/Package.swift
python <<'PY'
from pathlib import Path
path = Path("apps/macos/.build/checkouts/mlx-audio-swift/Package.swift")
text = path.read_text()
if "Models/Qwen3/README.md" in text:
print("mlx-audio-swift README excludes already present")
raise SystemExit(0)
needle = ' path: "Sources/MLXAudioTTS"\n'
replacement = """ path: \"Sources/MLXAudioTTS\",\n exclude: [\n \"Models/Llama/README.md\",\n \"Models/Marvis/README.md\",\n \"Models/PocketTTS/README.md\",\n \"Models/Qwen3/README.md\",\n \"Models/Soprano/README.md\",\n ]\n"""
if needle not in text:
raise SystemExit("Could not find MLXAudioTTS target path in mlx-audio-swift Package.swift")
path.write_text(text.replace(needle, replacement, 1))
print(f"Patched {path}")
PY
- name: Show toolchain
run: |
sw_vers
@@ -1324,6 +1444,8 @@ jobs:
exit 1
android:
permissions:
contents: read
name: ${{ matrix.check_name }}
needs: [preflight]
if: needs.preflight.outputs.run_android_job == 'true'

View File

@@ -1,7 +1,15 @@
name: CodeQL
on:
pull_request:
branches: [main]
paths-ignore:
- "**/*.md"
- "**/*.mdx"
- "LICENSE"
workflow_dispatch:
schedule:
- cron: "0 6 * * *"
concurrency:
group: codeql-${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}
@@ -70,7 +78,7 @@ jobs:
config_file: ""
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
submodules: false
@@ -83,13 +91,13 @@ jobs:
- name: Setup Python
if: matrix.needs_python
uses: actions/setup-python@v6
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: "3.12"
- name: Setup Java
if: matrix.needs_java
uses: actions/setup-java@v5
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
with:
distribution: temurin
java-version: "21"
@@ -103,7 +111,7 @@ jobs:
swift --version
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
uses: github/codeql-action/init@b25d0ebf40e5b63ee81e1bd6e5d2a12b7c2aeb61 # v4
with:
languages: ${{ matrix.language }}
queries: security-and-quality
@@ -111,7 +119,7 @@ jobs:
- name: Autobuild
if: matrix.needs_autobuild
uses: github/codeql-action/autobuild@v4
uses: github/codeql-action/autobuild@b25d0ebf40e5b63ee81e1bd6e5d2a12b7c2aeb61 # v4
- name: Build Android for CodeQL
if: matrix.language == 'java-kotlin'
@@ -132,6 +140,6 @@ jobs:
CODE_SIGNING_ALLOWED=NO
- name: Analyze
uses: github/codeql-action/analyze@v4
uses: github/codeql-action/analyze@b25d0ebf40e5b63ee81e1bd6e5d2a12b7c2aeb61 # v4
with:
category: "/language:${{ matrix.language }}"

View File

@@ -83,10 +83,10 @@ jobs:
fetch-depth: 0
- name: Set up Docker Builder
uses: docker/setup-buildx-action@v4
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
- name: Login to GitHub Container Registry
uses: docker/login-action@v4
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.repository_owner }}
@@ -153,7 +153,7 @@ jobs:
- name: Build and push amd64 image
id: build
# WARNING: KEEP THE OFFICIAL DOCKER ACTION HERE; DO NOT SWITCH THIS BACK TO BLACKSMITH BLINDLY.
uses: docker/build-push-action@v6
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
with:
context: .
platforms: linux/amd64
@@ -167,7 +167,7 @@ jobs:
- name: Build and push amd64 slim image
id: build-slim
# WARNING: KEEP THE OFFICIAL DOCKER ACTION HERE; DO NOT SWITCH THIS BACK TO BLACKSMITH BLINDLY.
uses: docker/build-push-action@v6
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
with:
context: .
platforms: linux/amd64
@@ -200,10 +200,10 @@ jobs:
fetch-depth: 0
- name: Set up Docker Builder
uses: docker/setup-buildx-action@v4
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
- name: Login to GitHub Container Registry
uses: docker/login-action@v4
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.repository_owner }}
@@ -270,7 +270,7 @@ jobs:
- name: Build and push arm64 image
id: build
# WARNING: KEEP THE OFFICIAL DOCKER ACTION HERE; DO NOT SWITCH THIS BACK TO BLACKSMITH BLINDLY.
uses: docker/build-push-action@v6
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
with:
context: .
platforms: linux/arm64
@@ -284,7 +284,7 @@ jobs:
- name: Build and push arm64 slim image
id: build-slim
# WARNING: KEEP THE OFFICIAL DOCKER ACTION HERE; DO NOT SWITCH THIS BACK TO BLACKSMITH BLINDLY.
uses: docker/build-push-action@v6
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
with:
context: .
platforms: linux/arm64
@@ -314,7 +314,7 @@ jobs:
fetch-depth: 0
- name: Login to GitHub Container Registry
uses: docker/login-action@v4
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.repository_owner }}

View File

@@ -25,7 +25,7 @@ jobs:
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
node-version: "22.18.0"
- name: Clone publish repo
env:

View File

@@ -7,6 +7,9 @@ on:
types: [opened, reopened, synchronize, ready_for_review, converted_to_draft]
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' }}
@@ -92,12 +95,12 @@ jobs:
uses: actions/checkout@v6
- name: Set up Docker Builder
uses: docker/setup-buildx-action@v4
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
# Blacksmith can fall back to the local docker driver, which rejects gha
# cache export/import. Keep smoke builds driver-agnostic.
- name: Build root Dockerfile smoke image
uses: useblacksmith/build-push-action@v2
uses: useblacksmith/build-push-action@cbd1f60d194a98cb3be5523b15134501eaf0fbf3 # v2
with:
context: .
file: ./Dockerfile
@@ -116,7 +119,7 @@ jobs:
# runtime deps declared by the plugin and that matrix discovery stays
# healthy in the final runtime image.
- name: Build extension Dockerfile smoke image
uses: useblacksmith/build-push-action@v2
uses: useblacksmith/build-push-action@cbd1f60d194a98cb3be5523b15134501eaf0fbf3 # v2
with:
context: .
file: ./Dockerfile
@@ -174,7 +177,7 @@ jobs:
'
- name: Build installer smoke image
uses: useblacksmith/build-push-action@v2
uses: useblacksmith/build-push-action@cbd1f60d194a98cb3be5523b15134501eaf0fbf3 # v2
with:
context: ./scripts/docker
file: ./scripts/docker/install-sh-smoke/Dockerfile
@@ -185,7 +188,7 @@ jobs:
- name: Build installer non-root image
if: github.event_name != 'pull_request'
uses: useblacksmith/build-push-action@v2
uses: useblacksmith/build-push-action@cbd1f60d194a98cb3be5523b15134501eaf0fbf3 # v2
with:
context: ./scripts/docker
file: ./scripts/docker/install-sh-nonroot/Dockerfile

View File

@@ -1,12 +1,67 @@
name: OpenClaw Cross-OS Release Checks (Reusable)
on:
workflow_dispatch:
inputs:
ref:
description: Public OpenClaw ref to validate (tag, branch, or full commit SHA)
required: true
default: main
type: string
workflow_ref:
description: Optional openclaw/openclaw ref that provides the reusable workflow harness
required: false
default: ""
type: string
provider:
description: Provider lane to use for onboarding and the end-to-end turn
required: true
default: openai
type: choice
options:
- openai
- anthropic
- minimax
mode:
description: Which release-check lanes to run
required: true
default: both
type: choice
options:
- fresh
- upgrade
- both
previous_version:
description: Optional baseline version for installer/dev-update and packaged upgrade
required: false
default: ""
type: string
ubuntu_runner:
description: Optional Linux runner label override
required: false
default: ""
type: string
windows_runner:
description: Optional Windows runner label override
required: false
default: ""
type: string
macos_runner:
description: Optional macOS runner label override
required: false
default: ""
type: string
workflow_call:
inputs:
ref:
description: Public OpenClaw ref to validate (tag, branch, or full commit SHA)
required: true
type: string
workflow_ref:
description: Optional openclaw/openclaw ref that provides the reusable workflow harness
required: false
default: ""
type: string
provider:
description: Provider lane to use for onboarding and the end-to-end turn
required: true
@@ -42,6 +97,14 @@ on:
required: false
MINIMAX_API_KEY:
required: false
OPENCLAW_DISCORD_SMOKE_BOT_TOKEN:
required: false
OPENCLAW_DISCORD_SMOKE_GUILD_ID:
required: false
OPENCLAW_DISCORD_SMOKE_CHANNEL_ID:
required: false
permissions: read-all
concurrency:
group: openclaw-cross-os-release-checks-${{ inputs.ref }}-${{ inputs.provider }}-${{ inputs.mode }}
@@ -52,12 +115,11 @@ env:
NODE_VERSION: "24.x"
PNPM_VERSION: "10.32.1"
OPENCLAW_REPOSITORY: openclaw/openclaw
TSX_VERSION: "4.21.0"
jobs:
prepare:
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
baseline_file_name: ${{ steps.baseline_metadata.outputs.file_name }}
baseline_spec: ${{ steps.baseline.outputs.value }}
@@ -65,6 +127,7 @@ jobs:
candidate_version: ${{ steps.candidate_metadata.outputs.version }}
matrix: ${{ steps.matrix.outputs.value }}
source_sha: ${{ steps.candidate_metadata.outputs.source_sha }}
workflow_ref: ${{ steps.workflow_ref.outputs.value }}
steps:
- name: Validate provider secret availability
env:
@@ -90,9 +153,109 @@ jobs:
;;
esac
- name: Checkout caller release workflow repo
- name: Resolve workflow ref
id: workflow_ref
env:
INPUT_WORKFLOW_REF: ${{ inputs.workflow_ref }}
CALLER_REPOSITORY: ${{ github.repository }}
CURRENT_SHA: ${{ github.sha }}
WORKFLOW_CONTEXT_REF: ${{ github.workflow_ref }}
WORKFLOW_REPOSITORY: ${{ env.OPENCLAW_REPOSITORY }}
run: |
set -euo pipefail
resolve_unique_remote_ref() {
local remote_url="$1"
shift
local -a refs=("$@")
local -a matches=()
local ref=""
for ref in "${refs[@]}"; do
[[ -n "${ref}" ]] || continue
mapfile -t matches < <(
git ls-remote "${remote_url}" "${ref}" | awk '{print $1}' | awk '!seen[$0]++'
)
if [[ "${#matches[@]}" -eq 0 ]]; then
continue
fi
if [[ "${#matches[@]}" -ne 1 ]]; then
return 2
fi
printf '%s\n' "${matches[0]}"
return 0
done
return 1
}
if [[ -n "${INPUT_WORKFLOW_REF}" ]]; then
TARGET_REF="${INPUT_WORKFLOW_REF}"
elif [[ "${CALLER_REPOSITORY}" == "${WORKFLOW_REPOSITORY}" ]]; then
TARGET_REF="${CURRENT_SHA}"
elif [[ "${WORKFLOW_CONTEXT_REF}" == "${WORKFLOW_REPOSITORY}/"* ]] && [[ "${WORKFLOW_CONTEXT_REF}" == *"@"* ]]; then
TARGET_REF="${WORKFLOW_CONTEXT_REF##*@}"
else
echo "Failed to infer workflow ref from github.workflow_ref=${WORKFLOW_CONTEXT_REF}" >&2
exit 1
fi
if [[ "${TARGET_REF}" =~ ^[0-9a-fA-F]{40}$ ]]; then
echo "value=${TARGET_REF}" >> "$GITHUB_OUTPUT"
exit 0
fi
REMOTE_URL="https://github.com/${WORKFLOW_REPOSITORY}.git"
if [[ "${TARGET_REF}" == refs/* ]]; then
if [[ "${TARGET_REF}" == refs/tags/* ]]; then
mapfile -t MATCHES < <(
resolve_unique_remote_ref "${REMOTE_URL}" "${TARGET_REF}^{}" "${TARGET_REF}" || true
)
else
mapfile -t MATCHES < <(resolve_unique_remote_ref "${REMOTE_URL}" "${TARGET_REF}" || true)
fi
else
mapfile -t BRANCH_MATCHES < <(
resolve_unique_remote_ref "${REMOTE_URL}" "refs/heads/${TARGET_REF}" || true
)
mapfile -t TAG_MATCHES < <(
resolve_unique_remote_ref "${REMOTE_URL}" "refs/tags/${TARGET_REF}^{}" "refs/tags/${TARGET_REF}" || true
)
MATCH_COUNT=$(( ${#BRANCH_MATCHES[@]} + ${#TAG_MATCHES[@]} ))
if [[ "${MATCH_COUNT}" -eq 1 ]]; then
if [[ "${#BRANCH_MATCHES[@]}" -eq 1 ]]; then
MATCHES=("${BRANCH_MATCHES[0]}")
else
MATCHES=("${TAG_MATCHES[0]}")
fi
elif [[ "${MATCH_COUNT}" -eq 0 ]]; then
MATCHES=()
else
echo "Workflow ref resolved ambiguously: ${TARGET_REF}" >&2
exit 1
fi
fi
case "${#MATCHES[@]}" in
1)
echo "value=${MATCHES[0]}" >> "$GITHUB_OUTPUT"
;;
0)
echo "Failed to resolve workflow ref: ${TARGET_REF}" >&2
exit 1
;;
*)
echo "Workflow ref resolved ambiguously: ${TARGET_REF}" >&2
exit 1
;;
esac
- name: Checkout workflow repo
uses: actions/checkout@v6
with:
repository: ${{ env.OPENCLAW_REPOSITORY }}
ref: ${{ steps.workflow_ref.outputs.value }}
path: workflow
fetch-depth: 1
persist-credentials: false
@@ -123,7 +286,7 @@ jobs:
env:
OUTPUT_DIR: ${{ runner.temp }}/openclaw-cross-os-release-checks/prepare
run: |
node --disable-warning=ExperimentalWarning scripts/openclaw-cross-os-release-checks.ts \
pnpm dlx "tsx@${TSX_VERSION}" workflow/scripts/openclaw-cross-os-release-checks.ts \
--prepare-only \
--source-dir source \
--output-dir "${OUTPUT_DIR}"
@@ -198,6 +361,7 @@ jobs:
- name: Resolve runner matrix
id: matrix
env:
INPUT_REF: ${{ inputs.ref }}
INPUT_MODE: ${{ inputs.mode }}
INPUT_UBUNTU_RUNNER: ${{ inputs.ubuntu_runner }}
INPUT_WINDOWS_RUNNER: ${{ inputs.windows_runner }}
@@ -206,53 +370,30 @@ jobs:
VAR_WINDOWS_RUNNER: ${{ vars.OPENCLAW_RELEASE_CHECKS_WINDOWS_RUNNER }}
VAR_MACOS_RUNNER: ${{ vars.OPENCLAW_RELEASE_CHECKS_MACOS_RUNNER }}
run: |
node <<'NODE' >>"$GITHUB_OUTPUT"
const pick = (...values) => values.find((value) => typeof value === "string" && value.trim().length > 0)?.trim();
const lanes = (process.env.INPUT_MODE ?? "both") === "both" ? ["fresh", "upgrade"] : [process.env.INPUT_MODE ?? "both"];
const runners = [
{
os_id: "ubuntu",
display_name: "Linux",
runner: pick(process.env.INPUT_UBUNTU_RUNNER, process.env.VAR_UBUNTU_RUNNER, "ubuntu-latest"),
artifact_name: "linux",
},
{
os_id: "windows",
display_name: "Windows",
runner: pick(
process.env.INPUT_WINDOWS_RUNNER,
process.env.VAR_WINDOWS_RUNNER,
"blacksmith-32vcpu-windows-2025",
),
artifact_name: "windows",
},
{
os_id: "macos",
display_name: "macOS",
runner: pick(process.env.INPUT_MACOS_RUNNER, process.env.VAR_MACOS_RUNNER, "macos-latest-xlarge"),
artifact_name: "macos",
},
];
const matrix = {
include: runners.flatMap((runner) => lanes.map((lane) => ({ ...runner, lane }))),
};
process.stdout.write(`value=${JSON.stringify(matrix)}\n`);
NODE
MATRIX_JSON="$(pnpm dlx "tsx@${TSX_VERSION}" workflow/scripts/openclaw-cross-os-release-checks.ts \
--resolve-matrix \
--ref "${INPUT_REF}" \
--mode "${INPUT_MODE}" \
--ubuntu-runner "${INPUT_UBUNTU_RUNNER}" \
--windows-runner "${INPUT_WINDOWS_RUNNER}" \
--macos-runner "${INPUT_MACOS_RUNNER}")"
echo "value=${MATRIX_JSON}" >> "$GITHUB_OUTPUT"
cross_os_release_checks:
name: "${{ matrix.display_name }} / ${{ matrix.lane }}"
name: "${{ matrix.display_name }} / ${{ matrix.suite_label }}"
needs: prepare
permissions:
contents: read
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.prepare.outputs.matrix) }}
runs-on: ${{ matrix.runner }}
timeout-minutes: 120
steps:
- name: Checkout caller release workflow repo
- name: Checkout workflow repo
uses: actions/checkout@v6
with:
repository: ${{ env.OPENCLAW_REPOSITORY }}
ref: ${{ needs.prepare.outputs.workflow_ref }}
path: workflow
fetch-depth: 1
persist-credentials: false
@@ -274,7 +415,7 @@ jobs:
path: ${{ runner.temp }}/openclaw-cross-os-release-checks/candidate
- name: Download baseline artifact
if: ${{ matrix.lane == 'upgrade' }}
if: ${{ matrix.suite == 'packaged-upgrade' }}
uses: actions/download-artifact@v8
with:
name: openclaw-cross-os-release-checks-baseline-${{ github.run_id }}
@@ -286,24 +427,35 @@ jobs:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
MINIMAX_API_KEY: ${{ secrets.MINIMAX_API_KEY }}
OPENCLAW_DISCORD_SMOKE_BOT_TOKEN: ${{ secrets.OPENCLAW_DISCORD_SMOKE_BOT_TOKEN }}
OPENCLAW_DISCORD_SMOKE_GUILD_ID: ${{ secrets.OPENCLAW_DISCORD_SMOKE_GUILD_ID }}
OPENCLAW_DISCORD_SMOKE_CHANNEL_ID: ${{ secrets.OPENCLAW_DISCORD_SMOKE_CHANNEL_ID }}
OPENCLAW_RELEASE_CHECK_OS: ${{ matrix.os_id }}
OPENCLAW_RELEASE_CHECK_RUNNER: ${{ matrix.runner }}
run: |
node --disable-warning=ExperimentalWarning scripts/openclaw-cross-os-release-checks.ts \
DISCORD_ARGS=()
if [[ -n "${OPENCLAW_DISCORD_SMOKE_BOT_TOKEN}" ]] && [[ -n "${OPENCLAW_DISCORD_SMOKE_GUILD_ID}" ]] && [[ -n "${OPENCLAW_DISCORD_SMOKE_CHANNEL_ID}" ]]; then
DISCORD_ARGS+=(--run-discord-roundtrip true)
fi
pnpm dlx "tsx@${TSX_VERSION}" workflow/scripts/openclaw-cross-os-release-checks.ts \
--candidate-tgz "$RUNNER_TEMP/openclaw-cross-os-release-checks/candidate/${{ needs.prepare.outputs.candidate_file_name }}" \
--candidate-version "${{ needs.prepare.outputs.candidate_version }}" \
--source-sha "${{ needs.prepare.outputs.source_sha }}" \
--baseline-spec "${{ needs.prepare.outputs.baseline_spec }}" \
--previous-version "${{ inputs.previous_version }}" \
--baseline-tgz "$RUNNER_TEMP/openclaw-cross-os-release-checks/baseline/${{ needs.prepare.outputs.baseline_file_name }}" \
--provider "${{ inputs.provider }}" \
--mode "${{ matrix.lane }}" \
--output-dir "$RUNNER_TEMP/openclaw-cross-os-release-checks/${{ matrix.artifact_name }}-${{ matrix.lane }}"
--suite "${{ matrix.suite }}" \
--ref "${{ inputs.ref }}" \
"${DISCORD_ARGS[@]}" \
--output-dir "$RUNNER_TEMP/openclaw-cross-os-release-checks/${{ matrix.artifact_name }}-${{ matrix.suite }}"
- name: Summarize release checks
if: always()
shell: bash
env:
SUMMARY_PATH: ${{ runner.temp }}/openclaw-cross-os-release-checks/${{ matrix.artifact_name }}-${{ matrix.lane }}/summary.md
SUMMARY_PATH: ${{ runner.temp }}/openclaw-cross-os-release-checks/${{ matrix.artifact_name }}-${{ matrix.suite }}/summary.md
run: |
if [[ -f "${SUMMARY_PATH}" ]]; then
cat "${SUMMARY_PATH}" >> "$GITHUB_STEP_SUMMARY"
@@ -315,6 +467,6 @@ jobs:
if: always()
uses: actions/upload-artifact@v7
with:
name: openclaw-cross-os-release-checks-${{ matrix.artifact_name }}-${{ matrix.lane }}-${{ github.run_id }}
path: ${{ runner.temp }}/openclaw-cross-os-release-checks/${{ matrix.artifact_name }}-${{ matrix.lane }}
name: openclaw-cross-os-release-checks-${{ matrix.artifact_name }}-${{ matrix.suite }}-${{ github.run_id }}
path: ${{ runner.temp }}/openclaw-cross-os-release-checks/${{ matrix.artifact_name }}-${{ matrix.suite }}
if-no-files-found: error

View File

@@ -0,0 +1,572 @@
name: OpenClaw Live And E2E Checks (Reusable)
on:
workflow_dispatch:
inputs:
ref:
description: Ref, tag, or SHA to validate
required: true
default: main
type: string
include_repo_e2e:
description: Whether to run pnpm test:e2e plus repo-specific extra E2E lanes
required: false
default: true
type: boolean
include_release_path_suites:
description: Whether to run the Docker release-path suites
required: false
default: true
type: boolean
include_openwebui:
description: Whether to run the Open WebUI Docker smoke
required: false
default: true
type: boolean
include_live_suites:
description: Whether to run live-provider coverage
required: false
default: true
type: boolean
workflow_call:
inputs:
ref:
description: Ref, tag, or SHA to validate
required: true
type: string
include_repo_e2e:
description: Whether to run pnpm test:e2e
required: false
default: false
type: boolean
include_release_path_suites:
description: Whether to run the Docker release-path suites
required: false
default: false
type: boolean
include_openwebui:
description: Whether to run the Open WebUI Docker smoke
required: false
default: true
type: boolean
include_live_suites:
description: Whether to run live-provider coverage
required: false
default: true
type: boolean
secrets:
OPENAI_API_KEY:
required: false
OPENAI_BASE_URL:
required: false
ANTHROPIC_API_KEY:
required: false
ANTHROPIC_API_KEY_OLD:
required: false
ANTHROPIC_API_TOKEN:
required: false
BYTEPLUS_API_KEY:
required: false
CEREBRAS_API_KEY:
required: false
DASHSCOPE_API_KEY:
required: false
GROQ_API_KEY:
required: false
KIMI_API_KEY:
required: false
MODELSTUDIO_API_KEY:
required: false
MOONSHOT_API_KEY:
required: false
MISTRAL_API_KEY:
required: false
MINIMAX_API_KEY:
required: false
OPENCODE_API_KEY:
required: false
OPENCODE_ZEN_API_KEY:
required: false
OPENCLAW_LIVE_BROWSER_CDP_URL:
required: false
OPENCLAW_LIVE_SETUP_TOKEN:
required: false
OPENCLAW_LIVE_SETUP_TOKEN_MODEL:
required: false
OPENCLAW_LIVE_SETUP_TOKEN_PROFILE:
required: false
OPENCLAW_LIVE_SETUP_TOKEN_VALUE:
required: false
GEMINI_API_KEY:
required: false
GOOGLE_API_KEY:
required: false
OPENROUTER_API_KEY:
required: false
QWEN_API_KEY:
required: false
FAL_KEY:
required: false
RUNWAY_API_KEY:
required: false
DEEPGRAM_API_KEY:
required: false
TOGETHER_API_KEY:
required: false
VYDRA_API_KEY:
required: false
XAI_API_KEY:
required: false
ZAI_API_KEY:
required: false
Z_AI_API_KEY:
required: false
BYTEPLUS_ACCESS_KEY_ID:
required: false
BYTEPLUS_SECRET_ACCESS_KEY:
required: false
CLAUDE_CODE_OAUTH_TOKEN:
required: false
OPENCLAW_CODEX_AUTH_JSON:
required: false
OPENCLAW_CODEX_CONFIG_TOML:
required: false
OPENCLAW_CLAUDE_JSON:
required: false
OPENCLAW_CLAUDE_CREDENTIALS_JSON:
required: false
OPENCLAW_CLAUDE_SETTINGS_JSON:
required: false
OPENCLAW_CLAUDE_SETTINGS_LOCAL_JSON:
required: false
OPENCLAW_GEMINI_SETTINGS_JSON:
required: false
permissions:
contents: read
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
NODE_VERSION: "24.x"
PNPM_VERSION: "10.32.1"
jobs:
validate_release_live_cache:
if: inputs.include_live_suites
runs-on: blacksmith-32vcpu-ubuntu-2404
timeout-minutes: 60
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
OPENCLAW_LIVE_CACHE_TEST: "1"
OPENCLAW_LIVE_TEST: "1"
steps:
- name: Checkout selected ref
uses: actions/checkout@v6
with:
ref: ${{ inputs.ref }}
fetch-depth: 0
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "true"
use-sticky-disk: "false"
- name: Validate live cache credentials
run: |
set -euo pipefail
if [[ -z "${OPENAI_API_KEY:-}" ]]; then
echo "Missing OPENAI_API_KEY secret for live-cache validation." >&2
exit 1
fi
if [[ -z "${ANTHROPIC_API_KEY:-}" ]]; then
echo "Missing ANTHROPIC_API_KEY secret for live-cache validation." >&2
exit 1
fi
- name: Verify live prompt cache floors
run: pnpm test:live:cache
validate_repo_e2e:
if: inputs.include_repo_e2e
runs-on: blacksmith-32vcpu-ubuntu-2404
timeout-minutes: 90
env:
OPENCLAW_VITEST_MAX_WORKERS: "2"
steps:
- name: Checkout selected ref
uses: actions/checkout@v6
with:
ref: ${{ inputs.ref }}
fetch-depth: 0
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "true"
use-sticky-disk: "false"
- name: Build dist for repo E2E
run: pnpm build
- name: Run repo E2E suite
run: pnpm test:e2e
validate_special_e2e:
if: inputs.include_repo_e2e || inputs.include_live_suites
runs-on: blacksmith-32vcpu-ubuntu-2404
timeout-minutes: ${{ matrix.timeout_minutes }}
strategy:
fail-fast: false
matrix:
include:
- suite_id: openshell-e2e
label: OpenShell repo E2E
command: pnpm test:e2e:openshell
timeout_minutes: 120
requires_repo_e2e: true
requires_live_suites: false
- suite_id: openai-ws-stream-live-e2e
label: OpenAI WebSocket live E2E
command: pnpm test:e2e -- src/agents/openai-ws-stream.e2e.test.ts
timeout_minutes: 90
requires_repo_e2e: false
requires_live_suites: true
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENCLAW_E2E_WORKERS: "1"
OPENCLAW_VITEST_MAX_WORKERS: "1"
steps:
- name: Checkout selected ref
uses: actions/checkout@v6
with:
ref: ${{ inputs.ref }}
fetch-depth: 0
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "true"
use-sticky-disk: "false"
- name: Build dist for special E2E
if: |
(inputs.include_repo_e2e && matrix.requires_repo_e2e) ||
(inputs.include_live_suites && matrix.requires_live_suites)
run: pnpm build
- name: Configure suite-specific env
shell: bash
run: |
set -euo pipefail
case "${{ matrix.suite_id }}" in
openai-ws-stream-live-e2e)
echo "OPENAI_LIVE_TEST=1" >> "$GITHUB_ENV"
echo "OPENCLAW_LIVE_TEST=1" >> "$GITHUB_ENV"
;;
esac
- name: Validate suite credentials
shell: bash
run: |
set -euo pipefail
case "${{ matrix.suite_id }}" in
openai-ws-stream-live-e2e)
[[ -n "${OPENAI_API_KEY:-}" ]] || {
echo "OPENAI_API_KEY is required for the OpenAI WebSocket live E2E suite." >&2
exit 1
}
;;
esac
- name: Run ${{ matrix.label }}
if: |
(inputs.include_repo_e2e && matrix.requires_repo_e2e) ||
(inputs.include_live_suites && matrix.requires_live_suites)
run: ${{ matrix.command }}
validate_docker_e2e:
if: inputs.include_release_path_suites || inputs.include_openwebui
runs-on: blacksmith-32vcpu-ubuntu-2404
timeout-minutes: ${{ matrix.timeout_minutes }}
strategy:
fail-fast: false
matrix:
include:
- suite_id: docker-onboard
label: Onboarding Docker E2E
command: pnpm test:docker:onboard
timeout_minutes: 60
release_path: true
openwebui_only: false
- suite_id: docker-gateway-network
label: Gateway Network Docker E2E
command: pnpm test:docker:gateway-network
timeout_minutes: 60
release_path: true
openwebui_only: false
- suite_id: docker-mcp-channels
label: MCP Channels Docker E2E
command: pnpm test:docker:mcp-channels
timeout_minutes: 60
release_path: true
openwebui_only: false
- suite_id: docker-plugins
label: Plugins Docker E2E
command: pnpm test:docker:plugins
timeout_minutes: 75
release_path: true
openwebui_only: false
- suite_id: docker-doctor-switch
label: Doctor Install Switch Docker E2E
command: pnpm test:docker:doctor-switch
timeout_minutes: 60
release_path: true
openwebui_only: false
- suite_id: docker-qr
label: QR Import Docker E2E
command: pnpm test:docker:qr
timeout_minutes: 60
release_path: true
openwebui_only: false
- suite_id: docker-install-e2e
label: Installer Docker E2E
command: pnpm test:install:e2e
timeout_minutes: 120
release_path: true
openwebui_only: false
- suite_id: docker-openwebui
label: Open WebUI Docker E2E
command: pnpm test:docker:openwebui
timeout_minutes: 75
release_path: false
openwebui_only: true
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
ANTHROPIC_API_TOKEN: ${{ secrets.ANTHROPIC_API_TOKEN }}
ANTHROPIC_API_KEY_OLD: ${{ secrets.ANTHROPIC_API_KEY_OLD }}
BYTEPLUS_API_KEY: ${{ secrets.BYTEPLUS_API_KEY }}
CEREBRAS_API_KEY: ${{ secrets.CEREBRAS_API_KEY }}
DASHSCOPE_API_KEY: ${{ secrets.DASHSCOPE_API_KEY }}
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
KIMI_API_KEY: ${{ secrets.KIMI_API_KEY }}
MODELSTUDIO_API_KEY: ${{ secrets.MODELSTUDIO_API_KEY }}
MOONSHOT_API_KEY: ${{ secrets.MOONSHOT_API_KEY }}
MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }}
MINIMAX_API_KEY: ${{ secrets.MINIMAX_API_KEY }}
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
OPENCODE_ZEN_API_KEY: ${{ secrets.OPENCODE_ZEN_API_KEY }}
OPENCLAW_LIVE_BROWSER_CDP_URL: ${{ secrets.OPENCLAW_LIVE_BROWSER_CDP_URL }}
OPENCLAW_LIVE_SETUP_TOKEN: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN }}
OPENCLAW_LIVE_SETUP_TOKEN_MODEL: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN_MODEL }}
OPENCLAW_LIVE_SETUP_TOKEN_PROFILE: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN_PROFILE }}
OPENCLAW_LIVE_SETUP_TOKEN_VALUE: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN_VALUE }}
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
QWEN_API_KEY: ${{ secrets.QWEN_API_KEY }}
FAL_KEY: ${{ secrets.FAL_KEY }}
RUNWAY_API_KEY: ${{ secrets.RUNWAY_API_KEY }}
DEEPGRAM_API_KEY: ${{ secrets.DEEPGRAM_API_KEY }}
TOGETHER_API_KEY: ${{ secrets.TOGETHER_API_KEY }}
VYDRA_API_KEY: ${{ secrets.VYDRA_API_KEY }}
XAI_API_KEY: ${{ secrets.XAI_API_KEY }}
ZAI_API_KEY: ${{ secrets.ZAI_API_KEY }}
Z_AI_API_KEY: ${{ secrets.Z_AI_API_KEY }}
BYTEPLUS_ACCESS_KEY_ID: ${{ secrets.BYTEPLUS_ACCESS_KEY_ID }}
BYTEPLUS_SECRET_ACCESS_KEY: ${{ secrets.BYTEPLUS_SECRET_ACCESS_KEY }}
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
OPENCLAW_CODEX_AUTH_JSON: ${{ secrets.OPENCLAW_CODEX_AUTH_JSON }}
OPENCLAW_CODEX_CONFIG_TOML: ${{ secrets.OPENCLAW_CODEX_CONFIG_TOML }}
OPENCLAW_CLAUDE_JSON: ${{ secrets.OPENCLAW_CLAUDE_JSON }}
OPENCLAW_CLAUDE_CREDENTIALS_JSON: ${{ secrets.OPENCLAW_CLAUDE_CREDENTIALS_JSON }}
OPENCLAW_CLAUDE_SETTINGS_JSON: ${{ secrets.OPENCLAW_CLAUDE_SETTINGS_JSON }}
OPENCLAW_CLAUDE_SETTINGS_LOCAL_JSON: ${{ secrets.OPENCLAW_CLAUDE_SETTINGS_LOCAL_JSON }}
OPENCLAW_GEMINI_SETTINGS_JSON: ${{ secrets.OPENCLAW_GEMINI_SETTINGS_JSON }}
steps:
- name: Checkout selected ref
uses: actions/checkout@v6
with:
ref: ${{ inputs.ref }}
fetch-depth: 0
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "true"
use-sticky-disk: "false"
- name: Hydrate live auth/profile inputs
run: bash scripts/ci-hydrate-live-auth.sh
- name: Configure suite-specific env
shell: bash
run: |
set -euo pipefail
case "${{ matrix.suite_id }}" in
docker-install-e2e)
echo "OPENCLAW_E2E_MODELS=both" >> "$GITHUB_ENV"
;;
esac
- name: Validate suite credentials
shell: bash
run: |
set -euo pipefail
case "${{ matrix.suite_id }}" in
docker-install-e2e)
[[ -n "${OPENAI_API_KEY:-}" ]] || {
echo "OPENAI_API_KEY is required for installer Docker E2E." >&2
exit 1
}
if [[ -z "${ANTHROPIC_API_TOKEN:-}" && -z "${ANTHROPIC_API_KEY:-}" ]]; then
echo "ANTHROPIC_API_TOKEN or ANTHROPIC_API_KEY is required for installer Docker E2E." >&2
exit 1
fi
;;
docker-openwebui)
[[ -n "${OPENAI_API_KEY:-}" ]] || {
echo "OPENAI_API_KEY is required for the Open WebUI Docker smoke." >&2
exit 1
}
;;
esac
- name: Run ${{ matrix.label }}
if: |
(inputs.include_release_path_suites && matrix.release_path) ||
(inputs.include_openwebui && matrix.openwebui_only)
run: ${{ matrix.command }}
validate_live_provider_suites:
if: inputs.include_live_suites
runs-on: blacksmith-32vcpu-ubuntu-2404
timeout-minutes: ${{ matrix.timeout_minutes }}
strategy:
fail-fast: false
matrix:
include:
- suite_id: live-all
label: pnpm test:live
command: pnpm test:live
timeout_minutes: 180
profile_env_only: false
- suite_id: live-models-docker
label: Docker live models
command: pnpm test:docker:live-models
timeout_minutes: 120
profile_env_only: false
- suite_id: live-gateway-docker
label: Docker live gateway
command: pnpm test:docker:live-gateway
timeout_minutes: 120
profile_env_only: false
- suite_id: live-cli-backend-docker
label: Docker live CLI backend
command: pnpm test:docker:live-cli-backend
timeout_minutes: 120
profile_env_only: false
- suite_id: live-acp-bind-docker
label: Docker live ACP bind
command: pnpm test:docker:live-acp-bind
timeout_minutes: 120
profile_env_only: false
- suite_id: live-codex-harness-docker
label: Docker live Codex harness
command: pnpm test:docker:live-codex-harness
timeout_minutes: 120
profile_env_only: false
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
ANTHROPIC_API_TOKEN: ${{ secrets.ANTHROPIC_API_TOKEN }}
ANTHROPIC_API_KEY_OLD: ${{ secrets.ANTHROPIC_API_KEY_OLD }}
BYTEPLUS_API_KEY: ${{ secrets.BYTEPLUS_API_KEY }}
CEREBRAS_API_KEY: ${{ secrets.CEREBRAS_API_KEY }}
DASHSCOPE_API_KEY: ${{ secrets.DASHSCOPE_API_KEY }}
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
KIMI_API_KEY: ${{ secrets.KIMI_API_KEY }}
MODELSTUDIO_API_KEY: ${{ secrets.MODELSTUDIO_API_KEY }}
MOONSHOT_API_KEY: ${{ secrets.MOONSHOT_API_KEY }}
MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }}
MINIMAX_API_KEY: ${{ secrets.MINIMAX_API_KEY }}
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
OPENCODE_ZEN_API_KEY: ${{ secrets.OPENCODE_ZEN_API_KEY }}
OPENCLAW_LIVE_BROWSER_CDP_URL: ${{ secrets.OPENCLAW_LIVE_BROWSER_CDP_URL }}
OPENCLAW_LIVE_SETUP_TOKEN: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN }}
OPENCLAW_LIVE_SETUP_TOKEN_MODEL: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN_MODEL }}
OPENCLAW_LIVE_SETUP_TOKEN_PROFILE: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN_PROFILE }}
OPENCLAW_LIVE_SETUP_TOKEN_VALUE: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN_VALUE }}
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
QWEN_API_KEY: ${{ secrets.QWEN_API_KEY }}
FAL_KEY: ${{ secrets.FAL_KEY }}
RUNWAY_API_KEY: ${{ secrets.RUNWAY_API_KEY }}
DEEPGRAM_API_KEY: ${{ secrets.DEEPGRAM_API_KEY }}
TOGETHER_API_KEY: ${{ secrets.TOGETHER_API_KEY }}
VYDRA_API_KEY: ${{ secrets.VYDRA_API_KEY }}
XAI_API_KEY: ${{ secrets.XAI_API_KEY }}
ZAI_API_KEY: ${{ secrets.ZAI_API_KEY }}
Z_AI_API_KEY: ${{ secrets.Z_AI_API_KEY }}
BYTEPLUS_ACCESS_KEY_ID: ${{ secrets.BYTEPLUS_ACCESS_KEY_ID }}
BYTEPLUS_SECRET_ACCESS_KEY: ${{ secrets.BYTEPLUS_SECRET_ACCESS_KEY }}
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
OPENCLAW_CODEX_AUTH_JSON: ${{ secrets.OPENCLAW_CODEX_AUTH_JSON }}
OPENCLAW_CODEX_CONFIG_TOML: ${{ secrets.OPENCLAW_CODEX_CONFIG_TOML }}
OPENCLAW_CLAUDE_JSON: ${{ secrets.OPENCLAW_CLAUDE_JSON }}
OPENCLAW_CLAUDE_CREDENTIALS_JSON: ${{ secrets.OPENCLAW_CLAUDE_CREDENTIALS_JSON }}
OPENCLAW_CLAUDE_SETTINGS_JSON: ${{ secrets.OPENCLAW_CLAUDE_SETTINGS_JSON }}
OPENCLAW_CLAUDE_SETTINGS_LOCAL_JSON: ${{ secrets.OPENCLAW_CLAUDE_SETTINGS_LOCAL_JSON }}
OPENCLAW_GEMINI_SETTINGS_JSON: ${{ secrets.OPENCLAW_GEMINI_SETTINGS_JSON }}
OPENCLAW_LIVE_VIDEO_GENERATION_SKIP_PROVIDERS: ""
OPENCLAW_LIVE_VYDRA_VIDEO: "1"
OPENCLAW_VITEST_MAX_WORKERS: "2"
steps:
- name: Checkout selected ref
uses: actions/checkout@v6
with:
ref: ${{ inputs.ref }}
fetch-depth: 0
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "true"
use-sticky-disk: "false"
- name: Hydrate live auth/profile inputs
run: bash scripts/ci-hydrate-live-auth.sh
- name: Configure suite-specific env
shell: bash
run: |
set -euo pipefail
if [[ "${{ matrix.profile_env_only }}" == "true" ]]; then
echo "OPENCLAW_DOCKER_PROFILE_ENV_ONLY=1" >> "$GITHUB_ENV"
fi
case "${{ matrix.suite_id }}" in
live-cli-backend-docker)
echo "OPENCLAW_LIVE_CLI_BACKEND_MODEL=codex-cli/gpt-5.4" >> "$GITHUB_ENV"
;;
live-acp-bind-docker)
echo "OPENCLAW_LIVE_ACP_BIND_AGENTS=claude,codex,gemini" >> "$GITHUB_ENV"
;;
esac
- name: Run ${{ matrix.label }}
run: ${{ matrix.command }}

View File

@@ -7,6 +7,24 @@ on:
description: Existing release tag or current full 40-character main commit SHA to validate (for example v2026.4.12 or 0123456789abcdef0123456789abcdef01234567)
required: true
type: string
provider:
description: Provider lane for cross-OS onboarding and the end-to-end agent turn
required: false
default: openai
type: choice
options:
- openai
- anthropic
- minimax
mode:
description: Which cross-OS release lanes to run
required: false
default: both
type: choice
options:
- fresh
- upgrade
- both
concurrency:
group: openclaw-release-checks-${{ inputs.ref }}
@@ -14,18 +32,18 @@ concurrency:
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
NODE_VERSION: "24.x"
PNPM_VERSION: "10.32.1"
jobs:
# THIS WORKFLOW EXISTS SO RELEASE-TIME LIVE CHECKS CAN RUN WITHOUT BLOCKING npm PUBLISH.
# PUT THE SLOWER, EXTERNAL, OR SOMETIMES-FLAKY RELEASE CHECKS HERE INSTEAD OF
# RECOUPLING THEM TO openclaw-npm-release.yml.
validate_release_live_cache:
resolve_target:
runs-on: blacksmith-32vcpu-ubuntu-2404
timeout-minutes: 60
timeout-minutes: 30
permissions:
contents: read
outputs:
ref: ${{ steps.inputs.outputs.ref }}
sha: ${{ steps.ref.outputs.sha }}
provider: ${{ steps.inputs.outputs.provider }}
mode: ${{ steps.inputs.outputs.mode }}
steps:
- name: Require main workflow ref for release checks
env:
@@ -73,48 +91,56 @@ jobs:
git merge-base --is-ancestor HEAD origin/main
fi
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "true"
use-sticky-disk: "false"
- name: Validate live cache credentials
- name: Capture selected inputs
id: inputs
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
RELEASE_REF_INPUT: ${{ inputs.ref }}
RELEASE_PROVIDER_INPUT: ${{ inputs.provider }}
RELEASE_MODE_INPUT: ${{ inputs.mode }}
run: |
set -euo pipefail
if [[ -z "${OPENAI_API_KEY}" ]]; then
echo "Missing OPENAI_API_KEY secret for release checks." >&2
exit 1
fi
if [[ -z "${ANTHROPIC_API_KEY}" ]]; then
echo "Missing ANTHROPIC_API_KEY secret for release checks." >&2
exit 1
fi
# KEEP RELEASE-TIME LIVE COVERAGE HERE SO OPERATORS CAN RUN IT ON DEMAND
# WITHOUT MAKING THE PUBLISH PATH WAIT FOR A SLOW OR FLAKY EXTERNAL CHECK.
- name: Verify live prompt cache floors
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENCLAW_LIVE_CACHE_TEST: "1"
OPENCLAW_LIVE_TEST: "1"
run: pnpm test:live:cache
{
printf 'ref=%s\n' "$RELEASE_REF_INPUT"
printf 'provider=%s\n' "$RELEASE_PROVIDER_INPUT"
printf 'mode=%s\n' "$RELEASE_MODE_INPUT"
} >> "$GITHUB_OUTPUT"
- name: Summarize validated ref
env:
RELEASE_REF: ${{ inputs.ref }}
RELEASE_SHA: ${{ steps.ref.outputs.sha }}
RELEASE_PROVIDER: ${{ inputs.provider }}
RELEASE_MODE: ${{ inputs.mode }}
run: |
{
echo "## Release checks"
echo
echo "- Requested ref: \`${RELEASE_REF}\`"
echo "- Validated SHA: \`${RELEASE_SHA}\`"
echo "- Check: \`pnpm test:live:cache\`"
echo "- Cross-OS provider: \`${RELEASE_PROVIDER}\`"
echo "- Cross-OS mode: \`${RELEASE_MODE}\`"
echo "- This run will execute cross-OS release validation plus the non-Parallels Docker/live/openwebui coverage from the CI migration plan."
} >> "$GITHUB_STEP_SUMMARY"
cross_os_release_checks:
needs: [resolve_target]
permissions: read-all
uses: ./.github/workflows/openclaw-cross-os-release-checks-reusable.yml
with:
ref: ${{ needs.resolve_target.outputs.ref }}
provider: ${{ needs.resolve_target.outputs.provider }}
mode: ${{ needs.resolve_target.outputs.mode }}
secrets: inherit
live_and_e2e_release_checks:
needs: [resolve_target]
permissions:
contents: read
uses: ./.github/workflows/openclaw-live-and-e2e-checks-reusable.yml
with:
ref: ${{ needs.resolve_target.outputs.ref }}
include_repo_e2e: true
include_release_path_suites: true
include_openwebui: true
include_live_suites: true
secrets: inherit

View File

@@ -0,0 +1,29 @@
name: OpenClaw Scheduled Live And E2E Checks
on:
schedule:
- cron: "23 4 * * *"
workflow_dispatch:
permissions:
contents: read
concurrency:
group: openclaw-scheduled-live-checks-${{ github.ref }}
cancel-in-progress: false
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
jobs:
live_and_openwebui_checks:
permissions:
contents: read
uses: ./.github/workflows/openclaw-live-and-e2e-checks-reusable.yml
with:
ref: ${{ github.sha }}
include_repo_e2e: true
include_release_path_suites: false
include_openwebui: true
include_live_suites: true
secrets: inherit

View File

@@ -33,6 +33,13 @@ jobs:
# meaningful verdict without touching a real API. If any of these
# leak into the job env, fail hard instead of silently running
# against a live provider and burning real budget.
#
# The parity pack has 11 isolated scenario workers. Letting qa suite
# fan out to its default "all scenarios at once" mode on smaller CI
# VMs makes the short strict-agentic scenarios flaky, especially the
# approval-turn followthrough gate that expects a fast post-approval
# read within a 30s agent.wait timeout.
QA_PARITY_CONCURRENCY: "2"
OPENAI_API_KEY: ""
ANTHROPIC_API_KEY: ""
OPENCLAW_LIVE_OPENAI_KEY: ""
@@ -49,7 +56,7 @@ jobs:
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "22.14.0"
node-version: "22.18.0"
cache: "pnpm"
- name: Install dependencies
@@ -60,6 +67,7 @@ jobs:
pnpm openclaw qa suite \
--provider-mode mock-openai \
--parity-pack agentic \
--concurrency "${QA_PARITY_CONCURRENCY}" \
--model openai/gpt-5.4 \
--alt-model openai/gpt-5.4-alt \
--output-dir .artifacts/qa-e2e/gpt54
@@ -69,6 +77,7 @@ jobs:
pnpm openclaw qa suite \
--provider-mode mock-openai \
--parity-pack agentic \
--concurrency "${QA_PARITY_CONCURRENCY}" \
--model anthropic/claude-opus-4-6 \
--alt-model anthropic/claude-sonnet-4-6 \
--output-dir .artifacts/qa-e2e/opus46

View File

@@ -14,6 +14,9 @@ on:
- Dockerfile.sandbox-common
- scripts/sandbox-common-setup.sh
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
@@ -32,7 +35,7 @@ jobs:
submodules: false
- name: Set up Docker Builder
uses: docker/setup-buildx-action@v4
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
- name: Build minimal sandbox base (USER sandbox)
shell: bash

View File

@@ -6,6 +6,9 @@ on:
branches: [main]
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

View File

@@ -4,20 +4,16 @@ Docs: https://docs.openclaw.ai
## Unreleased
### Changes
### Fixes
- Matrix/commands: keep DM pairing-store approvals out of room control-command authorization so DM-paired-only senders can no longer run owner-style commands in Matrix rooms without explicit room sender authorization.
- Docker/build: verify `@matrix-org/matrix-sdk-crypto-nodejs` native bindings with `find` under `node_modules` instead of a hardcoded `.pnpm/...` path so pnpm v10+ virtual-store layouts no longer fail the image build. (#67143) thanks @ly85206559.
- Matrix/E2EE: keep startup bootstrap conservative for passwordless token-auth bots, still attempt the guarded repair pass without requiring `channels.matrix.password`, and document the remaining password-UIA limitation. (#66228) Thanks @SARAMALI15792.
- Cron/announce delivery: suppress mixed-content isolated cron announce replies that end with `NO_REPLY` so trailing silent sentinels no longer leak summary text to the target channel. (#65004) thanks @neo1027144-creator.
- Plugins/bundled channels: partition bundled channel lazy caches by active bundled root so `OPENCLAW_BUNDLED_PLUGINS_DIR` flips stop reusing stale plugin, setup, secrets, and runtime state. (#67200) Thanks @gumadeiras.
- Onboarding/non-interactive: preserve existing gateway auth tokens during re-onboard so active local gateway clients are not disconnected by an implicit token rotation. (#67821) Thanks @BKF-Gitty.
## 2026.4.15-beta.1
## 2026.4.15
### Changes
- Anthropic/models: default Anthropic selections, `opus` aliases, Claude CLI defaults, and bundled image understanding to Claude Opus 4.7.
- Google/TTS: add Gemini text-to-speech support to the bundled `google` plugin, including provider registration, voice selection, WAV reply output, PCM telephony output, and setup/docs guidance. (#67515) Thanks @barronlroth.
- Control UI/Overview: add a Model Auth status card showing OAuth token health and provider rate-limit pressure at a glance, with attention callouts when OAuth tokens are expiring or expired. Backed by a new `models.authStatus` gateway method that strips credentials and caches for 60s. (#66211) Thanks @omarshahine.
- Memory/LanceDB: add cloud storage support to `memory-lancedb` so durable memory indexes can run on remote object storage instead of local disk only. (#63502) Thanks @rugvedS07.
- GitHub Copilot/memory search: add a GitHub Copilot embedding provider for memory search, and expose a dedicated Copilot embedding host helper so plugins can reuse the transport while honoring remote overrides, token refresh, and safer payload validation. (#61718) Thanks @feiskyer and @vincentkoc.
@@ -28,6 +24,49 @@ Docs: https://docs.openclaw.ai
### Fixes
- Gateway/tools: anchor trusted local `MEDIA:` tool-result passthrough on the exact raw name of this run's registered built-in tools, and reject client tool definitions whose names normalize-collide with a built-in or with another client tool in the same request (`400 invalid_request_error` on both JSON and SSE paths), so a client-supplied tool named like a built-in can no longer inherit its local-media trust. (#67303)
- Agents/replay recovery: classify the provider wording `401 input item ID does not belong to this connection` as replay-invalid, so users get the existing `/new` session reset guidance instead of a raw 401-style failure. (#66475) Thanks @dallylee.
- Gateway/webchat: enforce localRoots containment on webchat audio embedding path [AI-assisted]. (#67298) Thanks @pgondhi987.
- Matrix/pairing: block DM pairing-store entries from authorizing room control commands [AI-assisted]. (#67294) Thanks @pgondhi987.
- Docker/build: verify `@matrix-org/matrix-sdk-crypto-nodejs` native bindings with `find` under `node_modules` instead of a hardcoded `.pnpm/...` path so pnpm v10+ virtual-store layouts no longer fail the image build. (#67143) thanks @ly85206559.
- Matrix/E2EE: keep startup bootstrap conservative for passwordless token-auth bots, still attempt the guarded repair pass without requiring `channels.matrix.password`, and document the remaining password-UIA limitation. (#66228) Thanks @SARAMALI15792.
- Cron/announce delivery: suppress mixed-content isolated cron announce replies that end with `NO_REPLY` so trailing silent sentinels no longer leak summary text to the target channel. (#65004) thanks @neo1027144-creator.
- Plugins/bundled channels: partition bundled channel lazy caches by active bundled root so `OPENCLAW_BUNDLED_PLUGINS_DIR` flips stop reusing stale plugin, setup, secrets, and runtime state. (#67200) Thanks @gumadeiras.
- Packaging/plugins: prune common test/spec cargo from bundled plugin runtime dependencies and fail npm release validation if packaged test cargo reappears, keeping published tarballs leaner without plugin-specific special cases. (#67275) thanks @gumadeiras.
- Agents/context + Memory: trim default startup/skills prompt budgets, cap `memory_get` excerpts by default with explicit continuation metadata, and keep QMD reads aligned with the same bounded excerpt contract so long sessions pull less context by default without losing deterministic follow-up reads.
- Matrix/commands: skip DM pairing-store reads on room traffic now that room control-command authorization ignores pairing-store entries, keeping the room path narrower without changing room auth behavior. (#67325) Thanks @gumadeiras.
- Memory-core/dreaming: skip dreaming narrative transcripts from session-store metadata before bootstrap records land so dream diary prompt/prose lines do not pollute session ingestion. (#67315) thanks @jalehman.
- Agents/local models: clarify low-context preflight hints for self-hosted models, point config-backed caps at the relevant OpenClaw setting, and stop suggesting larger models when `agents.defaults.contextTokens` is the real limit. (#66236) Thanks @ImLukeF.
- Dreaming/memory-core: change the default `dreaming.storage.mode` from `inline` to `separate` so Dreaming phase blocks (`## Light Sleep`, `## REM Sleep`) land in `memory/dreaming/{phase}/YYYY-MM-DD.md` instead of being injected into `memory/YYYY-MM-DD.md`. Daily memory files no longer get dominated by structured candidate output, and the daily-ingestion scanner that already strips dream marker blocks no longer has to compete with hundreds of phase-block lines on every run. Operators who want the previous behavior can opt in by setting `plugins.entries.memory-core.config.dreaming.storage.mode: "inline"`. (#66412) Thanks @mjamiv.
- Control UI/Overview: fix false-positive "missing" alerts on the Model Auth status card for aliased providers, env-backed OAuth with auth.profiles, and unresolvable env SecretRefs. (#67253) Thanks @omarshahine.
- Dashboard: constrain exec approval modal overflow on desktop so long command content no longer pushes action buttons out of view. (#67082) Thanks @Ziy1-Tan.
- Agents/CLI transcripts: persist successful CLI-backed turns into the OpenClaw session transcript so google-gemini-cli replies appear in session history and the Control UI again. (#67490) Thanks @obviyus.
- Discord/tool-call text: strip standalone Gemma-style `<function>...</function>` tool-call payloads from visible assistant text without truncating prose examples or trailing replies. (#67318) Thanks @joelnishanth.
- WhatsApp/web-session: drain the pending per-auth creds save queue before reopening sockets so reconnect-time auth bootstrap no longer races in-flight `creds.json` writes and falsely restores from backup. (#67464) Thanks @neeravmakwana.
- BlueBubbles/catchup: add a per-message retry ceiling (`catchup.maxFailureRetries`, default 10) so a persistently-failing message with a malformed payload no longer wedges the catchup cursor forever. After N consecutive `processMessage` failures against the same GUID, catchup logs a WARN, skips that message on subsequent sweeps, and lets the cursor advance past it. Transient failures still retry from the same point as before. Also fixes a lost-update race in the persistent dedupe file lock that silently dropped inbound GUIDs on concurrent writes, a dedupe file naming migration gap on version upgrade, and a balloon-event bypass that let catchup replay debouncer-coalesced events as standalone messages. (#67426, #66870) Thanks @omarshahine.
- Ollama/chat: strip the `ollama/` provider prefix from Ollama chat request model ids so configured refs like `ollama/qwen3:14b-q8_0` stop 404ing against the Ollama API. (#67457) Thanks @suboss87.
- Agents/tools: resolve non-workspace host tilde paths against the OS home directory and keep edit recovery aligned with that same path target, so `~/...` host edit/write operations stop failing or reading back the wrong file when `OPENCLAW_HOME` differs. (#62804) Thanks @stainlu.
- Speech/TTS: auto-enable the bundled Microsoft and ElevenLabs speech providers, and route generic TTS directive tokens through the explicit or active provider first so overrides like `[[tts:speed=1.2]]` stop silently landing on the wrong provider. (#62846) Thanks @stainlu.
- OpenAI Codex/models: normalize stale native transport metadata in both runtime resolution and discovery/listing so legacy `openai-codex` rows with missing `api` or `https://chatgpt.com/backend-api/v1` self-heal to the canonical Codex transport instead of routing requests through broken HTML/Cloudflare paths, combining the original fixes proposed in #66969 (saamuelng601-pixel) and #67159 (hclsys). (#67635)
- Agents/failover: treat HTML provider error pages as upstream transport failures for CDN-style 5xx responses without misclassifying embedded body text as API rate limits, while still preserving auth remediation for HTML 401/403 pages and proxy remediation for HTML 407 pages. (#67642) Thanks @stainlu.
- Gateway/skills: bump the cached skills-snapshot version whenever a config write touches `skills.*` (for example `skills.allowBundled`, `skills.entries.<id>.enabled`, or `skills.profile`). Existing agent sessions persist a `skillsSnapshot` in `sessions.json` that reuses the skill list frozen at session creation; without this invalidation, removing a bundled skill from the allowlist left the old snapshot live and the model kept calling the disabled tool, producing `Tool <name> not found` loops that ran until the embedded-run timeout. (#67401) Thanks @xantorres.
- Agents/tool-loop: enable the unknown-tool stream guard by default. Previously `resolveUnknownToolGuardThreshold` returned `undefined` unless `tools.loopDetection.enabled` was explicitly set to `true`, which left the protection off in the default configuration. A hallucinated or removed tool (for example `himalaya` after it was dropped from `skills.allowBundled`) would then loop "Tool X not found" attempts until the full embedded-run timeout. The guard has no false-positive surface because it only triggers on tools that are objectively not registered in the run, so it now stays on regardless of `tools.loopDetection.enabled` and still accepts `tools.loopDetection.unknownToolThreshold` as a per-run override (default 10). (#67401) Thanks @xantorres.
- TUI/streaming: add a client-side streaming watchdog to `tui-event-handlers` so the `streaming · Xm Ys` activity indicator resets to `idle` after 30s of delta silence on the active run. Guards against lost or late `state: "final"` chat events (WS reconnects, gateway restarts, etc.) leaving the TUI stuck on `streaming` indefinitely; a new system log line surfaces the reset so users know to send a new message to resync. The window is configurable via the new `streamingWatchdogMs` context option (set to `0` to disable), and the handler now exposes a `dispose()` that clears the pending timer on shutdown. (#67401) Thanks @xantorres.
- Extensions/lmstudio: add exponential backoff to the inference-preload wrapper so an LM Studio model-load failure (for example the built-in memory guardrail rejecting a load because the swap is saturated) no longer produces a WARN line every ~2s for every chat request. The wrapper now records consecutive preload failures per `(baseUrl, modelKey, contextLength)` tuple with a 5s → 10s → 20s → … → 5min cooldown and skips the preload step entirely while a cooldown is active, letting chat requests proceed directly to the stream (the model is often already loaded via the LM Studio UI). The combined `preload failed` log line now reports consecutive-failure count and remaining cooldown so operators can act on the real issue instead of drowning in repeated warnings. (#67401) Thanks @xantorres.
- Agents/replay: re-run tool/result pairing after strict replay tool-call ID sanitization on outbound requests so Anthropic-compatible providers like MiniMax no longer receive malformed orphan tool-result IDs such as `...toolresult1` during compaction and retry flows. (#67620) Thanks @stainlu.
- Gateway/startup: fix spurious SIGUSR1 restart loop on Linux/systemd when plugin auto-enable is the only startup config write; the config hash guard was not captured for that write path, causing chokidar to treat each boot write as an external change and trigger a reload → restart cycle that corrupts manifest.db after repeated cycles. Fixes #67436. (#67557) thanks @openperf
- Codex/harness: auto-enable the Codex plugin when `codex` is selected as an embedded agent harness runtime, including forced default, per-agent, and `OPENCLAW_AGENT_RUNTIME` paths. (#67474) Thanks @duqaXxX.
- OpenAI Codex/CLI: keep resumed `codex exec resume` runs on the safe non-interactive path without reintroducing the removed dangerous bypass flag by passing the supported `--skip-git-repo-check` resume arg plus Codex's native `sandbox_mode="workspace-write"` config override. (#67666) Thanks @plgonzalezrx8.
- Codex/app-server: parse Desktop-originated app-server user agents such as `Codex Desktop/0.118.0`, keeping the version gate working when the Codex CLI inherits a multi-word originator. (#64666) Thanks @cyrusaf.
- Cron/announce delivery: keep isolated announce `NO_REPLY` stripping case-insensitive across direct and text delivery, preserve structured media-only sends when a caption strips silent, and derive main-session awareness from the cleaned payloads so silent captions no longer leak stale `NO_REPLY` text. (#65016) Thanks @BKF-Gitty.
- Sessions/Codex: skip redundant `delivery-mirror` transcript appends only when the latest assistant message has the same visible text, preventing duplicate visible replies on Codex-backed turns without suppressing repeated answers across turns. (#67185) Thanks @andyylin.
- Auto-reply/prompt-cache: keep volatile inbound chat IDs out of the stable system prompt so task-scoped adapters can reuse prompt caches across runs, while preserving conversation metadata for the user turn and media-only messages. (#65071) Thanks @MonkeyLeeT.
- BlueBubbles/inbound: restore inbound image attachment downloads on Node 22+ by stripping incompatible bundled-undici dispatchers from the non-SSRF fetch path, accept `updated-message` webhooks carrying attachments, use event-type-aware dedup keys so attachment follow-ups are not rejected as duplicates, and retry attachment fetch from the BB API when the initial webhook arrives with an empty array. (#64105, #61861, #65430, #67510) Thanks @omarshahine.
- Agents/skills: sort prompt-facing `available_skills` entries by skill name after merging sources so `skills.load.extraDirs` order no longer changes prompt-cache prefixes. (#64198) Thanks @Bartok9.
- Agents/OpenAI Responses: add `models.providers.*.models.*.compat.supportsPromptCacheKey` so OpenAI-compatible proxies that forward `prompt_cache_key` can keep prompt caching enabled while incompatible endpoints can still force stripping. (#67427) Thanks @damselem.
- Agents/context engines: keep loop-hook and final `afterTurn` prompt-cache touch metadata aligned with the current assistant turn so cache-aware context engines retain accurate cache TTL state during tool loops. (#67767) thanks @jalehman.
- Memory/dreaming: strip AI-facing inbound metadata envelopes from session-corpus user turns before normalization so REM topic extraction sees the user's actual message text, including array-shaped split envelopes. (#66548) Thanks @zqchris.
- Agents/errors: detect standalone Cloudflare/CDN HTML challenge pages before transport DNS classification so provider block pages no longer appear as local DNS lookup failures. (#67704) Thanks @chris-yyau.
- Security/approvals: redact secrets in exec approval prompts so inline approval review can no longer leak credential material in rendered prompt content. (#61077, #64790)
- CLI/configure: re-read the persisted config hash after writes so config updates stop failing with stale-hash races. (#64188, #66528)
- CLI/update: prune stale packaged `dist` chunks after npm upgrades and keep downgrade/verify inventory checks compat-safe so global upgrades stop failing on stale chunk imports. (#66959) Thanks @obviyus.
@@ -59,7 +98,7 @@ Docs: https://docs.openclaw.ai
- Telegram/documents: sanitize binary reply context and ZIP-like archive extraction so `.epub` and `.mobi` uploads can no longer leak raw binary into prompt context through reply metadata or archive-to-`text/plain` coercion. (#66877) Thanks @martinfrancois.
- Telegram/native commands: restore plugin-registry-backed auto defaults for native commands and native skills so Telegram slash commands keep registering when `commands.native` and `commands.nativeSkills` stay on `auto`. (#66843) Thanks @kashevk0.
- OpenRouter/Qwen3: parse `reasoning_details` stream deltas as thinking content without skipping same-chunk tool calls, so Qwen3 replies no longer fail empty on OpenRouter and mixed reasoning/tool-call chunks still execute normally. (#66905) Thanks @bladin.
- fix(bluebubbles): replay missed webhook messages after gateway restart via a persistent per-account cursor and `/api/v1/message/query?after=<ts>` pass, so messages delivered while the gateway was down no longer disappear. Uses the existing `processMessage` path and is deduped by #66816's inbound GUID cache. (#66857, #66721) Thanks @omarshahine.
- BlueBubbles/catchup: replay missed webhook messages after gateway restart via a persistent per-account cursor and `/api/v1/message/query?after=<ts>` pass, so messages delivered while the gateway was down no longer disappear. Uses the existing `processMessage` path and is deduped by #66816's inbound GUID cache. (#66857, #66721) Thanks @omarshahine.
- Telegram/native commands: keep Telegram command-sync cache process-local so gateway restarts re-register the menu instead of trusting stale on-disk sync state after Telegram cleared commands out-of-band. (#66730) Thanks @nightq.
- Audio/self-hosted STT: restore `models.providers.*.request.allowPrivateNetwork` for audio transcription so private or LAN speech-to-text endpoints stop tripping SSRF blocks after the v2026.4.14 regression. (#66692) Thanks @jhsmith409.
- Auto-reply/media: allow workspace-rooted absolute media paths in auto-reply send flows so valid local media references no longer fail path validation. (#66689)
@@ -71,6 +110,9 @@ Docs: https://docs.openclaw.ai
- Control UI/chat: keep optimistic user message cards visible during active sends by deferring same-session history reloads until the active run ends, including aborted and errored runs. (#66997) Thanks @scotthuang and @vincentkoc.
- Media/Slack: allow host-local CSV and Markdown uploads only when the fallback buffer actually decodes as text, so real plain-text files work without letting opaque non-text blobs renamed to `.csv` or `.md` slip past the host-read guard. (#67047) Thanks @Unayung.
- Ollama/onboarding: split setup into `Cloud + Local`, `Cloud only`, and `Local only`, support direct `OLLAMA_API_KEY` cloud setup without a local daemon, and keep Ollama web search on the local-host path. (#67005) Thanks @obviyus.
- Webchat/security: reject remote-host `file://` URLs in the media embedding path. (#67293) Thanks @pgondhi987.
- Dreaming/memory-core: use the ingestion day, not the source file day, for daily recall dedupe so repeat sweeps of the same daily note can increment `dailyCount` across days instead of stalling at `1`. (#67091) Thanks @Bartok9.
- Node-host/tools.exec: let approval binding distinguish known native binaries from mutable shell payload files, while still fail-closing unknown or racy file probes so absolute-path node-host commands like `/usr/bin/whoami` no longer get rejected as unsafe interpreter/runtime commands. (#66731) Thanks @tmimmanuel.
## 2026.4.14
@@ -150,6 +192,7 @@ Docs: https://docs.openclaw.ai
- Media/store: honor configured agent media limits when saving generated media and persisting outbound reply media, so the store no longer hard-stops those flows at 5 MB before the configured limit applies. (#66229) Thanks @neeravmakwana and @vincentkoc.
- Plugins/setup-entry: preserve separate setup-entry secrets exports when loading bundled setup-runtime channels, so setup-mode flows keep the channel secret contract for split plugin + secrets entrypoints. (#66261) Thanks @hxy91819.
- CLI/update: prune stale packaged `dist` chunks after npm upgrades, verify installed package inventory, and keep downgrade/update verification working across older releases. (#66959) Thanks @obviyus.
- Gateway/exec events: dedupe replayed `exec.finished` node events by canonical session key plus `runId` so duplicate async completion replays no longer inject duplicate completion turns into the parent session transcript. (#67281) thanks @jalehman.
## 2026.4.12
@@ -415,6 +458,7 @@ Docs: https://docs.openclaw.ai
- iMessage: retry transient `watch.subscribe` startup failures before tearing down the monitor, so brief local transport stalls do not immediately bounce the channel. (#65393) Thanks @vincentkoc.
- Status/session_status: move shared session status text into a neutral internal status module and keep the tool importing a local runtime shim, so built `session_status` no longer depends on reply command internals or a bundler-opaque runtime import. (#65807) Thanks @dutifulbob.
- QQBot/security: replace raw `fetch()` in the image-size probe with SSRF-guarded `fetchRemoteMedia`, fix `resolveRepoRoot()` to walk up to `.git` instead of hardcoding two parent levels, and refresh the raw-fetch allowlist to match the corrected scan. (#63495) Thanks @dims.
- WhatsApp/web: rewrite queued `creds.json` updates atomically so interrupted saves do not leave truncated login state behind. (#63577) thanks @OwenYWT
## 2026.4.9

View File

@@ -59,7 +59,7 @@ Welcome to the lobster tank! 🦞
- **Jonathan Taylor** - ACP subsystem, Gateway features/bugs, Gog/Mog/Sog CLI's, SEDMAT
- GitHub [@visionik](https://github.com/visionik) · X: [@visionik](https://x.com/visionik)
- **Josh Lehman** - Compaction, Tlon/Urbit subsystem
- **Josh Lehman** - Compaction, Context Engine
- GitHub [@jalehman](https://github.com/jalehman) · X: [@jlehman\_](https://x.com/jlehman_)
- **Radek Sienkiewicz** - Docs, Control UI

712
README.md
View File

@@ -19,16 +19,19 @@
</p>
**OpenClaw** is a _personal AI assistant_ you run on your own devices.
It answers you on the channels you already use (WhatsApp, Telegram, Slack, Discord, Google Chat, Signal, iMessage, BlueBubbles, IRC, Microsoft Teams, Matrix, Feishu, LINE, Mattermost, Nextcloud Talk, Nostr, Synology Chat, Tlon, Twitch, Zalo, Zalo Personal, WeChat, QQ, WebChat). It can speak and listen on macOS/iOS/Android, and can render a live Canvas you control. The Gateway is just the control plane — the product is the assistant.
It answers you on the channels you already use. It can speak and listen on macOS/iOS/Android, and can render a live Canvas you control. The Gateway is just the control plane — the product is the assistant.
If you want a personal, single-user assistant that feels local, fast, and always-on, this is it.
Supported channels include: WhatsApp, Telegram, Slack, Discord, Google Chat, Signal, iMessage, BlueBubbles, IRC, Microsoft Teams, Matrix, Feishu, LINE, Mattermost, Nextcloud Talk, Nostr, Synology Chat, Tlon, Twitch, Zalo, Zalo Personal, WeChat, QQ, WebChat.
[Website](https://openclaw.ai) · [Docs](https://docs.openclaw.ai) · [Vision](VISION.md) · [DeepWiki](https://deepwiki.com/openclaw/openclaw) · [Getting Started](https://docs.openclaw.ai/start/getting-started) · [Updating](https://docs.openclaw.ai/install/updating) · [Showcase](https://docs.openclaw.ai/start/showcase) · [FAQ](https://docs.openclaw.ai/help/faq) · [Onboarding](https://docs.openclaw.ai/start/wizard) · [Nix](https://github.com/openclaw/nix-openclaw) · [Docker](https://docs.openclaw.ai/install/docker) · [Discord](https://discord.gg/clawd)
New install? Start here: [Getting started](https://docs.openclaw.ai/start/getting-started)
Preferred setup: run `openclaw onboard` in your terminal.
OpenClaw Onboard guides you step by step through setting up the gateway, workspace, channels, and skills. It is the recommended CLI setup path and works on **macOS, Linux, and Windows (via WSL2; strongly recommended)**.
Works with npm, pnpm, or bun.
New install? Start here: [Getting started](https://docs.openclaw.ai/start/getting-started)
## Sponsors
@@ -91,11 +94,6 @@ New install? Start here: [Getting started](https://docs.openclaw.ai/start/gettin
Model note: while many providers and models are supported, prefer a current flagship model from the provider you trust and already use. See [Onboarding](https://docs.openclaw.ai/start/onboarding).
## Models (selection + auth)
- Models config + CLI: [Models](https://docs.openclaw.ai/concepts/models)
- Auth profile rotation (OAuth vs API keys) + fallbacks: [Model failover](https://docs.openclaw.ai/concepts/model-failover)
## Install (recommended)
Runtime: **Node 24 (recommended) or Node 22.16+**.
@@ -129,34 +127,7 @@ openclaw agent --message "Ship checklist" --thinking high
Upgrading? [Updating guide](https://docs.openclaw.ai/install/updating) (and run `openclaw doctor`).
## Development channels
- **stable**: tagged releases (`vYYYY.M.D` or `vYYYY.M.D-<patch>`), npm dist-tag `latest`.
- **beta**: prerelease tags (`vYYYY.M.D-beta.N`), npm dist-tag `beta` (macOS app may be missing).
- **dev**: moving head of `main`, npm dist-tag `dev` (when published).
Switch channels (git + npm): `openclaw update --channel stable|beta|dev`.
Details: [Development channels](https://docs.openclaw.ai/install/development-channels).
## From source (development)
Prefer `pnpm` for builds from source. Bun is optional for running TypeScript directly.
```bash
git clone https://github.com/openclaw/openclaw.git
cd openclaw
pnpm install
pnpm ui:build # auto-installs UI deps on first run
pnpm build
pnpm openclaw onboard --install-daemon
# Dev loop (auto-reload on source/config changes)
pnpm gateway:watch
```
Note: `pnpm openclaw ...` runs TypeScript directly (via `tsx`). `pnpm build` produces `dist/` for running via Node / the packaged `openclaw` binary.
Models config + CLI: [Models](https://docs.openclaw.ai/concepts/models). Auth profile rotation + fallbacks: [Model failover](https://docs.openclaw.ai/concepts/model-failover).
## Security defaults (DM access)
@@ -183,152 +154,30 @@ Run `openclaw doctor` to surface risky/misconfigured DM policies.
- **[Companion apps](https://docs.openclaw.ai/platforms/macos)** — macOS menu bar app + iOS/Android [nodes](https://docs.openclaw.ai/nodes).
- **[Onboarding](https://docs.openclaw.ai/start/wizard) + [skills](https://docs.openclaw.ai/tools/skills)** — onboarding-driven setup with bundled/managed/workspace skills.
## Star History
## Security model (important)
[![Star History Chart](https://api.star-history.com/svg?repos=openclaw/openclaw&type=date&legend=top-left)](https://www.star-history.com/#openclaw/openclaw&type=date&legend=top-left)
- Default: tools run on the host for the `main` session, so the agent has full access when it is just you.
- Group/channel safety: set `agents.defaults.sandbox.mode: "non-main"` to run non-`main` sessions inside per-session Docker sandboxes.
- Typical sandbox default: allow `bash`, `process`, `read`, `write`, `edit`, `sessions_list`, `sessions_history`, `sessions_send`, `sessions_spawn`; deny `browser`, `canvas`, `nodes`, `cron`, `discord`, `gateway`.
- Before exposing anything remotely, read [Security](https://docs.openclaw.ai/gateway/security), [Docker sandboxing](https://docs.openclaw.ai/install/docker), and [Configuration](https://docs.openclaw.ai/gateway/configuration).
## Everything we built so far
## Operator quick refs
### Core platform
- Chat commands: `/status`, `/new`, `/reset`, `/compact`, `/think <level>`, `/verbose on|off`, `/trace on|off`, `/usage off|tokens|full`, `/restart`, `/activation mention|always`
- Session tools: `sessions_list`, `sessions_history`, `sessions_send`
- Skills registry: [ClawHub](https://clawhub.com)
- Architecture overview: [Architecture](https://docs.openclaw.ai/concepts/architecture)
- [Gateway WS control plane](https://docs.openclaw.ai/gateway) with sessions, presence, config, cron, webhooks, [Control UI](https://docs.openclaw.ai/web), and [Canvas host](https://docs.openclaw.ai/platforms/mac/canvas#canvas-a2ui).
- [CLI surface](https://docs.openclaw.ai/tools/agent-send): gateway, agent, send, [onboarding](https://docs.openclaw.ai/start/wizard), and [doctor](https://docs.openclaw.ai/gateway/doctor).
- [Pi agent runtime](https://docs.openclaw.ai/concepts/agent) in RPC mode with tool streaming and block streaming.
- [Session model](https://docs.openclaw.ai/concepts/session): `main` for direct chats, group isolation, activation modes, queue modes, reply-back. Group rules: [Groups](https://docs.openclaw.ai/channels/groups).
- [Media pipeline](https://docs.openclaw.ai/nodes/images): images/audio/video, transcription hooks, size caps, temp file lifecycle. Audio details: [Audio](https://docs.openclaw.ai/nodes/audio).
## Docs by goal
### Channels
- [Channels](https://docs.openclaw.ai/channels): [WhatsApp](https://docs.openclaw.ai/channels/whatsapp) (Baileys), [Telegram](https://docs.openclaw.ai/channels/telegram) (grammY), [Slack](https://docs.openclaw.ai/channels/slack) (Bolt), [Discord](https://docs.openclaw.ai/channels/discord) (discord.js), [Google Chat](https://docs.openclaw.ai/channels/googlechat) (Chat API), [Signal](https://docs.openclaw.ai/channels/signal) (signal-cli), [BlueBubbles](https://docs.openclaw.ai/channels/bluebubbles) (iMessage, recommended), [iMessage](https://docs.openclaw.ai/channels/imessage) (legacy imsg), [IRC](https://docs.openclaw.ai/channels/irc), [Microsoft Teams](https://docs.openclaw.ai/channels/msteams), [Matrix](https://docs.openclaw.ai/channels/matrix), [Feishu](https://docs.openclaw.ai/channels/feishu), [LINE](https://docs.openclaw.ai/channels/line), [Mattermost](https://docs.openclaw.ai/channels/mattermost), [Nextcloud Talk](https://docs.openclaw.ai/channels/nextcloud-talk), [Nostr](https://docs.openclaw.ai/channels/nostr), [Synology Chat](https://docs.openclaw.ai/channels/synology-chat), [Tlon](https://docs.openclaw.ai/channels/tlon), [Twitch](https://docs.openclaw.ai/channels/twitch), [Zalo](https://docs.openclaw.ai/channels/zalo), [Zalo Personal](https://docs.openclaw.ai/channels/zalouser), WeChat (`@tencent-weixin/openclaw-weixin`), [QQ](https://docs.openclaw.ai/channels/qqbot), [WebChat](https://docs.openclaw.ai/web/webchat).
- [Group routing](https://docs.openclaw.ai/channels/group-messages): mention gating, reply tags, per-channel chunking and routing. Channel rules: [Channels](https://docs.openclaw.ai/channels).
### Apps + nodes
- [macOS app](https://docs.openclaw.ai/platforms/macos): menu bar control plane, [Voice Wake](https://docs.openclaw.ai/nodes/voicewake)/PTT, [Talk Mode](https://docs.openclaw.ai/nodes/talk) overlay, [WebChat](https://docs.openclaw.ai/web/webchat), debug tools, [remote gateway](https://docs.openclaw.ai/gateway/remote) control.
- [iOS node](https://docs.openclaw.ai/platforms/ios): [Canvas](https://docs.openclaw.ai/platforms/mac/canvas), [Voice Wake](https://docs.openclaw.ai/nodes/voicewake), [Talk Mode](https://docs.openclaw.ai/nodes/talk), camera, screen recording, Bonjour + device pairing.
- [Android node](https://docs.openclaw.ai/platforms/android): Connect tab (setup code/manual), chat sessions, voice tab, [Canvas](https://docs.openclaw.ai/platforms/mac/canvas), camera/screen recording, and Android device commands (notifications/location/SMS/photos/contacts/calendar/motion/app update).
- [macOS node mode](https://docs.openclaw.ai/nodes): system.run/notify + canvas/camera exposure.
### Tools + automation
- [Browser control](https://docs.openclaw.ai/tools/browser): dedicated openclaw Chrome/Chromium, snapshots, actions, uploads, profiles.
- [Canvas](https://docs.openclaw.ai/platforms/mac/canvas): [A2UI](https://docs.openclaw.ai/platforms/mac/canvas#canvas-a2ui) push/reset, eval, snapshot.
- [Nodes](https://docs.openclaw.ai/nodes): camera snap/clip, screen record, [location.get](https://docs.openclaw.ai/nodes/location-command), notifications.
- [Cron + wakeups](https://docs.openclaw.ai/automation/cron-jobs); [webhooks](https://docs.openclaw.ai/automation/webhook); [Gmail Pub/Sub](https://docs.openclaw.ai/automation/gmail-pubsub).
- [Skills platform](https://docs.openclaw.ai/tools/skills): bundled, managed, and workspace skills with install gating + UI.
### Runtime + safety
- [Channel routing](https://docs.openclaw.ai/channels/channel-routing), [retry policy](https://docs.openclaw.ai/concepts/retry), and [streaming/chunking](https://docs.openclaw.ai/concepts/streaming).
- [Presence](https://docs.openclaw.ai/concepts/presence), [typing indicators](https://docs.openclaw.ai/concepts/typing-indicators), and [usage tracking](https://docs.openclaw.ai/concepts/usage-tracking).
- [Models](https://docs.openclaw.ai/concepts/models), [model failover](https://docs.openclaw.ai/concepts/model-failover), and [session pruning](https://docs.openclaw.ai/concepts/session-pruning).
- [Security](https://docs.openclaw.ai/gateway/security) and [troubleshooting](https://docs.openclaw.ai/channels/troubleshooting).
### Ops + packaging
- [Control UI](https://docs.openclaw.ai/web) + [WebChat](https://docs.openclaw.ai/web/webchat) served directly from the Gateway.
- [Tailscale Serve/Funnel](https://docs.openclaw.ai/gateway/tailscale) or [SSH tunnels](https://docs.openclaw.ai/gateway/remote) with token/password auth.
- [Nix mode](https://docs.openclaw.ai/install/nix) for declarative config; [Docker](https://docs.openclaw.ai/install/docker)-based installs.
- [Doctor](https://docs.openclaw.ai/gateway/doctor) migrations, [logging](https://docs.openclaw.ai/logging).
## How it works (short)
```
WhatsApp / Telegram / Slack / Discord / Google Chat / Signal / iMessage / BlueBubbles / IRC / Microsoft Teams / Matrix / Feishu / LINE / Mattermost / Nextcloud Talk / Nostr / Synology Chat / Tlon / Twitch / Zalo / Zalo Personal / WeChat / QQ / WebChat
┌───────────────────────────────┐
│ Gateway │
│ (control plane) │
│ ws://127.0.0.1:18789 │
└──────────────┬────────────────┘
├─ Pi agent (RPC)
├─ CLI (openclaw …)
├─ WebChat UI
├─ macOS app
└─ iOS / Android nodes
```
## Key subsystems
- **[Gateway WebSocket network](https://docs.openclaw.ai/concepts/architecture)** — single WS control plane for clients, tools, and events (plus ops: [Gateway runbook](https://docs.openclaw.ai/gateway)).
- **[Tailscale exposure](https://docs.openclaw.ai/gateway/tailscale)** — Serve/Funnel for the Gateway dashboard + WS (remote access: [Remote](https://docs.openclaw.ai/gateway/remote)).
- **[Browser control](https://docs.openclaw.ai/tools/browser)** — openclawmanaged Chrome/Chromium with CDP control.
- **[Canvas + A2UI](https://docs.openclaw.ai/platforms/mac/canvas)** — agentdriven visual workspace (A2UI host: [Canvas/A2UI](https://docs.openclaw.ai/platforms/mac/canvas#canvas-a2ui)).
- **[Voice Wake](https://docs.openclaw.ai/nodes/voicewake) + [Talk Mode](https://docs.openclaw.ai/nodes/talk)** — wake words on macOS/iOS plus continuous voice on Android.
- **[Nodes](https://docs.openclaw.ai/nodes)** — Canvas, camera snap/clip, screen record, `location.get`, notifications, plus macOSonly `system.run`/`system.notify`.
## Tailscale access (Gateway dashboard)
OpenClaw can auto-configure Tailscale **Serve** (tailnet-only) or **Funnel** (public) while the Gateway stays bound to loopback. Configure `gateway.tailscale.mode`:
- `off`: no Tailscale automation (default).
- `serve`: tailnet-only HTTPS via `tailscale serve` (uses Tailscale identity headers by default).
- `funnel`: public HTTPS via `tailscale funnel` (requires shared password auth).
Notes:
- `gateway.bind` must stay `loopback` when Serve/Funnel is enabled (OpenClaw enforces this).
- Serve can be forced to require a password by setting `gateway.auth.mode: "password"` or `gateway.auth.allowTailscale: false`.
- Funnel refuses to start unless `gateway.auth.mode: "password"` is set.
- Optional: `gateway.tailscale.resetOnExit` to undo Serve/Funnel on shutdown.
Details: [Tailscale guide](https://docs.openclaw.ai/gateway/tailscale) · [Web surfaces](https://docs.openclaw.ai/web)
## Remote Gateway (Linux is great)
Its perfectly fine to run the Gateway on a small Linux instance. Clients (macOS app, CLI, WebChat) can connect over **Tailscale Serve/Funnel** or **SSH tunnels**, and you can still pair device nodes (macOS/iOS/Android) to execute devicelocal actions when needed.
- **Gateway host** runs the exec tool and channel connections by default.
- **Device nodes** run devicelocal actions (`system.run`, camera, screen recording, notifications) via `node.invoke`.
In short: exec runs where the Gateway lives; device actions run where the device lives.
Details: [Remote access](https://docs.openclaw.ai/gateway/remote) · [Nodes](https://docs.openclaw.ai/nodes) · [Security](https://docs.openclaw.ai/gateway/security)
## macOS permissions via the Gateway protocol
The macOS app can run in **node mode** and advertises its capabilities + permission map over the Gateway WebSocket (`node.list` / `node.describe`). Clients can then execute local actions via `node.invoke`:
- `system.run` runs a local command and returns stdout/stderr/exit code; set `needsScreenRecording: true` to require screen-recording permission (otherwise youll get `PERMISSION_MISSING`).
- `system.notify` posts a user notification and fails if notifications are denied.
- `canvas.*`, `camera.*`, `screen.record`, and `location.get` are also routed via `node.invoke` and follow TCC permission status.
Elevated bash (host permissions) is separate from macOS TCC:
- Use `/elevated on|off` to toggle persession elevated access when enabled + allowlisted.
- Gateway persists the persession toggle via `sessions.patch` (WS method) alongside `thinkingLevel`, `verboseLevel`, `model`, `sendPolicy`, and `groupActivation`.
Details: [Nodes](https://docs.openclaw.ai/nodes) · [macOS app](https://docs.openclaw.ai/platforms/macos) · [Gateway protocol](https://docs.openclaw.ai/concepts/architecture)
## Agent to Agent (sessions\_\* tools)
- Use these to coordinate work across sessions without jumping between chat surfaces.
- `sessions_list` — discover active sessions (agents) and their metadata.
- `sessions_history` — fetch transcript logs for a session.
- `sessions_send` — message another session; optional replyback pingpong + announce step (`REPLY_SKIP`, `ANNOUNCE_SKIP`).
Details: [Session tools](https://docs.openclaw.ai/concepts/session-tool)
## Skills registry (ClawHub)
ClawHub is a minimal skill registry. With ClawHub enabled, the agent can search for skills automatically and pull in new ones as needed.
[ClawHub](https://clawhub.com)
## Chat commands
Send these in WhatsApp/Telegram/Slack/Google Chat/Microsoft Teams/WebChat (group commands are owner-only):
- `/status` — compact session status (model + tokens, cost when available)
- `/new` or `/reset` — reset the session
- `/compact` — compact session context (summary)
- `/think <level>` — off|minimal|low|medium|high|xhigh (GPT-5.2 + Codex models only)
- `/verbose on|off`
- `/trace on|off` — plugin trace/debug lines only
- `/usage off|tokens|full` — per-response usage footer
- `/restart` — restart the gateway (owner-only in groups)
- `/activation mention|always` — group activation toggle (groups only)
- New here: [Getting started](https://docs.openclaw.ai/start/getting-started), [Onboarding](https://docs.openclaw.ai/start/wizard), [Updating](https://docs.openclaw.ai/install/updating)
- Channel setup: [Channels index](https://docs.openclaw.ai/channels), [WhatsApp](https://docs.openclaw.ai/channels/whatsapp), [Telegram](https://docs.openclaw.ai/channels/telegram), [Discord](https://docs.openclaw.ai/channels/discord), [Slack](https://docs.openclaw.ai/channels/slack)
- Apps + nodes: [macOS](https://docs.openclaw.ai/platforms/macos), [iOS](https://docs.openclaw.ai/platforms/ios), [Android](https://docs.openclaw.ai/platforms/android), [Nodes](https://docs.openclaw.ai/nodes)
- Config + security: [Configuration](https://docs.openclaw.ai/gateway/configuration), [Security](https://docs.openclaw.ai/gateway/security), [Docker sandboxing](https://docs.openclaw.ai/install/docker)
- Remote + web: [Gateway](https://docs.openclaw.ai/gateway), [Remote access](https://docs.openclaw.ai/gateway/remote), [Tailscale](https://docs.openclaw.ai/gateway/tailscale), [Web surfaces](https://docs.openclaw.ai/web)
- Tools + automation: [Tools](https://docs.openclaw.ai/tools), [Skills](https://docs.openclaw.ai/tools/skills), [Cron jobs](https://docs.openclaw.ai/automation/cron-jobs), [Webhooks](https://docs.openclaw.ai/automation/webhook), [Gmail Pub/Sub](https://docs.openclaw.ai/automation/gmail-pubsub)
- Internals: [Architecture](https://docs.openclaw.ai/concepts/architecture), [Agent](https://docs.openclaw.ai/concepts/agent), [Session model](https://docs.openclaw.ai/concepts/session), [Gateway protocol](https://docs.openclaw.ai/reference/rpc)
- Troubleshooting: [Channel troubleshooting](https://docs.openclaw.ai/channels/troubleshooting), [Logging](https://docs.openclaw.ai/logging), [Docs home](https://docs.openclaw.ai)
## Apps (optional)
@@ -359,6 +208,35 @@ Runbook: [iOS connect](https://docs.openclaw.ai/platforms/ios).
- Exposes Connect/Chat/Voice tabs plus Canvas, Camera, Screen capture, and Android device command families.
- Runbook: [Android connect](https://docs.openclaw.ai/platforms/android).
## From source (development)
Prefer `pnpm` for builds from source. Bun is optional for running TypeScript directly.
```bash
git clone https://github.com/openclaw/openclaw.git
cd openclaw
pnpm install
pnpm ui:build # auto-installs UI deps on first run
pnpm build
pnpm openclaw onboard --install-daemon
# Dev loop (auto-reload on source/config changes)
pnpm gateway:watch
```
Note: `pnpm openclaw ...` runs TypeScript directly (via `tsx`). `pnpm build` produces `dist/` for running via Node / the packaged `openclaw` binary.
## Development channels
- **stable**: tagged releases (`vYYYY.M.D` or `vYYYY.M.D-<patch>`), npm dist-tag `latest`.
- **beta**: prerelease tags (`vYYYY.M.D-beta.N`), npm dist-tag `beta` (macOS app may be missing).
- **dev**: moving head of `main`, npm dist-tag `dev` (when published).
Switch channels (git + npm): `openclaw update --channel stable|beta|dev`.
Details: [Development channels](https://docs.openclaw.ai/install/development-channels).
## Agent workspace + skills
- Workspace root: `~/.openclaw/workspace` (configurable via `agents.defaults.workspace`).
@@ -379,179 +257,9 @@ Minimal `~/.openclaw/openclaw.json` (model + defaults):
[Full configuration reference (all keys + examples).](https://docs.openclaw.ai/gateway/configuration)
## Security model (important)
## Star History
- **Default:** tools run on the host for the **main** session, so the agent has full access when its just you.
- **Group/channel safety:** set `agents.defaults.sandbox.mode: "non-main"` to run **nonmain sessions** (groups/channels) inside persession Docker sandboxes; bash then runs in Docker for those sessions.
- **Sandbox defaults:** allowlist `bash`, `process`, `read`, `write`, `edit`, `sessions_list`, `sessions_history`, `sessions_send`, `sessions_spawn`; denylist `browser`, `canvas`, `nodes`, `cron`, `discord`, `gateway`.
Details: [Security guide](https://docs.openclaw.ai/gateway/security) · [Docker + sandboxing](https://docs.openclaw.ai/install/docker) · [Sandbox config](https://docs.openclaw.ai/gateway/configuration)
### [WhatsApp](https://docs.openclaw.ai/channels/whatsapp)
- Link the device: `pnpm openclaw channels login` (stores creds in `~/.openclaw/credentials`).
- Allowlist who can talk to the assistant via `channels.whatsapp.allowFrom`.
- If `channels.whatsapp.groups` is set, it becomes a group allowlist; include `"*"` to allow all.
### [Telegram](https://docs.openclaw.ai/channels/telegram)
- Set `TELEGRAM_BOT_TOKEN` or `channels.telegram.botToken` (env wins).
- Optional: set `channels.telegram.groups` (with `channels.telegram.groups."*".requireMention`); when set, it is a group allowlist (include `"*"` to allow all). Also `channels.telegram.allowFrom` or `channels.telegram.webhookUrl` + `channels.telegram.webhookSecret` as needed.
```json5
{
channels: {
telegram: {
botToken: "123456:ABCDEF",
},
},
}
```
### [Slack](https://docs.openclaw.ai/channels/slack)
- Set `SLACK_BOT_TOKEN` + `SLACK_APP_TOKEN` (or `channels.slack.botToken` + `channels.slack.appToken`).
### [Discord](https://docs.openclaw.ai/channels/discord)
- Set `DISCORD_BOT_TOKEN` or `channels.discord.token`.
- Optional: set `commands.native`, `commands.text`, or `commands.useAccessGroups`, plus `channels.discord.allowFrom`, `channels.discord.guilds`, or `channels.discord.mediaMaxMb` as needed.
```json5
{
channels: {
discord: {
token: "1234abcd",
},
},
}
```
### [Signal](https://docs.openclaw.ai/channels/signal)
- Requires `signal-cli` and a `channels.signal` config section.
### [BlueBubbles (iMessage)](https://docs.openclaw.ai/channels/bluebubbles)
- **Recommended** iMessage integration.
- Configure `channels.bluebubbles.serverUrl` + `channels.bluebubbles.password` and a webhook (`channels.bluebubbles.webhookPath`).
- The BlueBubbles server runs on macOS; the Gateway can run on macOS or elsewhere.
### [iMessage (legacy)](https://docs.openclaw.ai/channels/imessage)
- Legacy macOS-only integration via `imsg` (Messages must be signed in).
- If `channels.imessage.groups` is set, it becomes a group allowlist; include `"*"` to allow all.
### [Microsoft Teams](https://docs.openclaw.ai/channels/msteams)
- Configure a Teams app + Bot Framework, then add a `msteams` config section.
- Allowlist who can talk via `msteams.allowFrom`; group access via `msteams.groupAllowFrom` or `msteams.groupPolicy: "open"`.
### [QQ](https://docs.openclaw.ai/channels/qqbot)
- Go to the QQ Open Platform and scan the QR code with your phone QQ to register / log in.
- Click **Create Bot** to create a new QQ bot. Find **AppID** and **AppSecret** on the bot's settings page and copy them.
```json
{
"channels": {
"qqbot": {
"enabled": true,
"appId": "YOUR_APP_ID",
"clientSecret": "YOUR_APP_SECRET"
}
}
}
```
### WeChat
- Official Tencent plugin via [`@tencent-weixin/openclaw-weixin`](https://www.npmjs.com/package/@tencent-weixin/openclaw-weixin) (iLink Bot API). Private chats only; v2.x requires OpenClaw `>=2026.3.22`.
- Install: `openclaw plugins install "@tencent-weixin/openclaw-weixin"`, then `openclaw channels login --channel openclaw-weixin` to scan the QR code.
- Requires the WeChat ClawBot plugin (WeChat > Me > Settings > Plugins); gradual rollout by Tencent.
### [WebChat](https://docs.openclaw.ai/web/webchat)
- Uses the Gateway WebSocket; no separate WebChat port/config.
Browser control (optional):
```json5
{
browser: {
enabled: true,
color: "#FF4500",
},
}
```
## Docs
Use these when youre past the onboarding flow and want the deeper reference.
- [Start with the docs index for navigation and “whats where.”](https://docs.openclaw.ai)
- [Read the architecture overview for the gateway + protocol model.](https://docs.openclaw.ai/concepts/architecture)
- [Use the full configuration reference when you need every key and example.](https://docs.openclaw.ai/gateway/configuration)
- [Run the Gateway by the book with the operational runbook.](https://docs.openclaw.ai/gateway)
- [Learn how the Control UI/Web surfaces work and how to expose them safely.](https://docs.openclaw.ai/web)
- [Understand remote access over SSH tunnels or tailnets.](https://docs.openclaw.ai/gateway/remote)
- [Follow OpenClaw Onboard for a guided setup.](https://docs.openclaw.ai/start/wizard)
- [Wire external triggers via the webhook surface.](https://docs.openclaw.ai/automation/webhook)
- [Set up Gmail Pub/Sub triggers.](https://docs.openclaw.ai/automation/gmail-pubsub)
- [Learn the macOS menu bar companion details.](https://docs.openclaw.ai/platforms/mac/menu-bar)
- [Platform guides: Windows (WSL2)](https://docs.openclaw.ai/platforms/windows), [Linux](https://docs.openclaw.ai/platforms/linux), [macOS](https://docs.openclaw.ai/platforms/macos), [iOS](https://docs.openclaw.ai/platforms/ios), [Android](https://docs.openclaw.ai/platforms/android)
- [Debug common failures with the troubleshooting guide.](https://docs.openclaw.ai/channels/troubleshooting)
- [Review security guidance before exposing anything.](https://docs.openclaw.ai/gateway/security)
## Advanced docs (discovery + control)
- [Discovery + transports](https://docs.openclaw.ai/gateway/discovery)
- [Bonjour/mDNS](https://docs.openclaw.ai/gateway/bonjour)
- [Gateway pairing](https://docs.openclaw.ai/gateway/pairing)
- [Remote gateway README](https://docs.openclaw.ai/gateway/remote-gateway-readme)
- [Control UI](https://docs.openclaw.ai/web/control-ui)
- [Dashboard](https://docs.openclaw.ai/web/dashboard)
## Operations & troubleshooting
- [Health checks](https://docs.openclaw.ai/gateway/health)
- [Gateway lock](https://docs.openclaw.ai/gateway/gateway-lock)
- [Background process](https://docs.openclaw.ai/gateway/background-process)
- [Browser troubleshooting (Linux)](https://docs.openclaw.ai/tools/browser-linux-troubleshooting)
- [Logging](https://docs.openclaw.ai/logging)
## Deep dives
- [Agent loop](https://docs.openclaw.ai/concepts/agent-loop)
- [Presence](https://docs.openclaw.ai/concepts/presence)
- [TypeBox schemas](https://docs.openclaw.ai/concepts/typebox)
- [RPC adapters](https://docs.openclaw.ai/reference/rpc)
- [Queue](https://docs.openclaw.ai/concepts/queue)
## Workspace & skills
- [Skills config](https://docs.openclaw.ai/tools/skills-config)
- [Default AGENTS](https://docs.openclaw.ai/reference/AGENTS.default)
- [Templates: AGENTS](https://docs.openclaw.ai/reference/templates/AGENTS)
- [Templates: BOOTSTRAP](https://docs.openclaw.ai/reference/templates/BOOTSTRAP)
- [Templates: IDENTITY](https://docs.openclaw.ai/reference/templates/IDENTITY)
- [Templates: SOUL](https://docs.openclaw.ai/reference/templates/SOUL)
- [Templates: TOOLS](https://docs.openclaw.ai/reference/templates/TOOLS)
- [Templates: USER](https://docs.openclaw.ai/reference/templates/USER)
## Platform internals
- [macOS dev setup](https://docs.openclaw.ai/platforms/mac/dev-setup)
- [macOS menu bar](https://docs.openclaw.ai/platforms/mac/menu-bar)
- [macOS voice wake](https://docs.openclaw.ai/platforms/mac/voicewake)
- [iOS node](https://docs.openclaw.ai/platforms/ios)
- [Android node](https://docs.openclaw.ai/platforms/android)
- [Windows (WSL2)](https://docs.openclaw.ai/platforms/windows)
- [Linux app](https://docs.openclaw.ai/platforms/linux)
## Email hooks (Gmail)
- [docs.openclaw.ai/gmail-pubsub](https://docs.openclaw.ai/automation/gmail-pubsub)
[![Star History Chart](https://api.star-history.com/svg?repos=openclaw/openclaw&type=date&legend=top-left)](https://www.star-history.com/#openclaw/openclaw&type=date&legend=top-left)
## Molty
@@ -570,63 +278,257 @@ AI/vibe-coded PRs welcome! 🤖
Special thanks to [Mario Zechner](https://mariozechner.at/) for his support and for
[pi-mono](https://github.com/badlogic/pi-mono).
Special thanks to Adam Doppelt for lobster.bot.
Special thanks to Adam Doppelt for the lobster.bot domain.
Thanks to all clawtributors:
<p align="left">
<a href="https://github.com/steipete"><img src="https://avatars.githubusercontent.com/u/58493?v=4&s=48" width="48" height="48" alt="steipete" title="steipete"/></a> <a href="https://github.com/vincentkoc"><img src="https://avatars.githubusercontent.com/u/25068?v=4&s=48" width="48" height="48" alt="vincentkoc" title="vincentkoc"/></a> <a href="https://github.com/vignesh07"><img src="https://avatars.githubusercontent.com/u/1436853?v=4&s=48" width="48" height="48" alt="vignesh07" title="vignesh07"/></a> <a href="https://github.com/obviyus"><img src="https://avatars.githubusercontent.com/u/22031114?v=4&s=48" width="48" height="48" alt="obviyus" title="obviyus"/></a> <a href="https://github.com/mbelinky"><img src="https://avatars.githubusercontent.com/u/132747814?v=4&s=48" width="48" height="48" alt="Mariano Belinky" title="Mariano Belinky"/></a> <a href="https://github.com/sebslight"><img src="https://avatars.githubusercontent.com/u/19554889?v=4&s=48" width="48" height="48" alt="sebslight" title="sebslight"/></a> <a href="https://github.com/gumadeiras"><img src="https://avatars.githubusercontent.com/u/5599352?v=4&s=48" width="48" height="48" alt="gumadeiras" title="gumadeiras"/></a> <a href="https://github.com/Takhoffman"><img src="https://avatars.githubusercontent.com/u/781889?v=4&s=48" width="48" height="48" alt="Takhoffman" title="Takhoffman"/></a> <a href="https://github.com/thewilloftheshadow"><img src="https://avatars.githubusercontent.com/u/35580099?v=4&s=48" width="48" height="48" alt="thewilloftheshadow" title="thewilloftheshadow"/></a> <a href="https://github.com/cpojer"><img src="https://avatars.githubusercontent.com/u/13352?v=4&s=48" width="48" height="48" alt="cpojer" title="cpojer"/></a>
<a href="https://github.com/tyler6204"><img src="https://avatars.githubusercontent.com/u/64381258?v=4&s=48" width="48" height="48" alt="tyler6204" title="tyler6204"/></a> <a href="https://github.com/joshp123"><img src="https://avatars.githubusercontent.com/u/1497361?v=4&s=48" width="48" height="48" alt="joshp123" title="joshp123"/></a> <a href="https://github.com/Glucksberg"><img src="https://avatars.githubusercontent.com/u/80581902?v=4&s=48" width="48" height="48" alt="Glucksberg" title="Glucksberg"/></a> <a href="https://github.com/mcaxtr"><img src="https://avatars.githubusercontent.com/u/7562095?v=4&s=48" width="48" height="48" alt="mcaxtr" title="mcaxtr"/></a> <a href="https://github.com/quotentiroler"><img src="https://avatars.githubusercontent.com/u/40643627?v=4&s=48" width="48" height="48" alt="quotentiroler" title="quotentiroler"/></a> <a href="https://github.com/osolmaz"><img src="https://avatars.githubusercontent.com/u/2453968?v=4&s=48" width="48" height="48" alt="osolmaz" title="osolmaz"/></a> <a href="https://github.com/Sid-Qin"><img src="https://avatars.githubusercontent.com/u/201593046?v=4&s=48" width="48" height="48" alt="Sid-Qin" title="Sid-Qin"/></a> <a href="https://github.com/joshavant"><img src="https://avatars.githubusercontent.com/u/830519?v=4&s=48" width="48" height="48" alt="joshavant" title="joshavant"/></a> <a href="https://github.com/shakkernerd"><img src="https://avatars.githubusercontent.com/u/165377636?v=4&s=48" width="48" height="48" alt="shakkernerd" title="shakkernerd"/></a> <a href="https://github.com/bmendonca3"><img src="https://avatars.githubusercontent.com/u/208517100?v=4&s=48" width="48" height="48" alt="bmendonca3" title="bmendonca3"/></a>
<a href="https://github.com/mukhtharcm"><img src="https://avatars.githubusercontent.com/u/56378562?v=4&s=48" width="48" height="48" alt="mukhtharcm" title="mukhtharcm"/></a> <a href="https://github.com/zerone0x"><img src="https://avatars.githubusercontent.com/u/39543393?v=4&s=48" width="48" height="48" alt="zerone0x" title="zerone0x"/></a> <a href="https://github.com/mcinteerj"><img src="https://avatars.githubusercontent.com/u/3613653?v=4&s=48" width="48" height="48" alt="mcinteerj" title="mcinteerj"/></a> <a href="https://github.com/ngutman"><img src="https://avatars.githubusercontent.com/u/1540134?v=4&s=48" width="48" height="48" alt="ngutman" title="ngutman"/></a> <a href="https://github.com/lailoo"><img src="https://avatars.githubusercontent.com/u/20536249?v=4&s=48" width="48" height="48" alt="lailoo" title="lailoo"/></a> <a href="https://github.com/arosstale"><img src="https://avatars.githubusercontent.com/u/117890364?v=4&s=48" width="48" height="48" alt="arosstale" title="arosstale"/></a> <a href="https://github.com/rodrigouroz"><img src="https://avatars.githubusercontent.com/u/384037?v=4&s=48" width="48" height="48" alt="rodrigouroz" title="rodrigouroz"/></a> <a href="https://github.com/robbyczgw-cla"><img src="https://avatars.githubusercontent.com/u/239660374?v=4&s=48" width="48" height="48" alt="robbyczgw-cla" title="robbyczgw-cla"/></a> <a href="https://github.com/0xRaini"><img src="https://avatars.githubusercontent.com/u/190923101?v=4&s=48" width="48" height="48" alt="Elonito" title="Elonito"/></a> <a href="https://github.com/Clawborn"><img src="https://avatars.githubusercontent.com/u/261310391?v=4&s=48" width="48" height="48" alt="Clawborn" title="Clawborn"/></a>
<a href="https://github.com/yinghaosang"><img src="https://avatars.githubusercontent.com/u/261132136?v=4&s=48" width="48" height="48" alt="yinghaosang" title="yinghaosang"/></a> <a href="https://github.com/BunsDev"><img src="https://avatars.githubusercontent.com/u/68980965?v=4&s=48" width="48" height="48" alt="BunsDev" title="BunsDev"/></a> <a href="https://github.com/christianklotz"><img src="https://avatars.githubusercontent.com/u/69443?v=4&s=48" width="48" height="48" alt="christianklotz" title="christianklotz"/></a> <a href="https://github.com/echoVic"><img src="https://avatars.githubusercontent.com/u/16428813?v=4&s=48" width="48" height="48" alt="echoVic" title="echoVic"/></a> <a href="https://github.com/coygeek"><img src="https://avatars.githubusercontent.com/u/65363919?v=4&s=48" width="48" height="48" alt="coygeek" title="coygeek"/></a> <a href="https://github.com/roshanasingh4"><img src="https://avatars.githubusercontent.com/u/88576930?v=4&s=48" width="48" height="48" alt="roshanasingh4" title="roshanasingh4"/></a> <a href="https://github.com/mneves75"><img src="https://avatars.githubusercontent.com/u/2423436?v=4&s=48" width="48" height="48" alt="mneves75" title="mneves75"/></a> <a href="https://github.com/joaohlisboa"><img src="https://avatars.githubusercontent.com/u/8200873?v=4&s=48" width="48" height="48" alt="joaohlisboa" title="joaohlisboa"/></a> <a href="https://github.com/bohdanpodvirnyi"><img src="https://avatars.githubusercontent.com/u/31819391?v=4&s=48" width="48" height="48" alt="bohdanpodvirnyi" title="bohdanpodvirnyi"/></a> <a href="https://github.com/Nachx639"><img src="https://avatars.githubusercontent.com/u/71144023?v=4&s=48" width="48" height="48" alt="nachx639" title="nachx639"/></a>
<a href="https://github.com/onutc"><img src="https://avatars.githubusercontent.com/u/152018508?v=4&s=48" width="48" height="48" alt="onutc" title="onutc"/></a> <a href="https://github.com/VeriteIgiraneza"><img src="https://avatars.githubusercontent.com/u/69280208?v=4&s=48" width="48" height="48" alt="Verite Igiraneza" title="Verite Igiraneza"/></a> <a href="https://github.com/widingmarcus-cyber"><img src="https://avatars.githubusercontent.com/u/245375637?v=4&s=48" width="48" height="48" alt="widingmarcus-cyber" title="widingmarcus-cyber"/></a> <a href="https://github.com/akramcodez"><img src="https://avatars.githubusercontent.com/u/179671552?v=4&s=48" width="48" height="48" alt="akramcodez" title="akramcodez"/></a> <a href="https://github.com/aether-ai-agent"><img src="https://avatars.githubusercontent.com/u/261339948?v=4&s=48" width="48" height="48" alt="aether-ai-agent" title="aether-ai-agent"/></a> <a href="https://github.com/bjesuiter"><img src="https://avatars.githubusercontent.com/u/2365676?v=4&s=48" width="48" height="48" alt="bjesuiter" title="bjesuiter"/></a> <a href="https://github.com/MaudeBot"><img src="https://avatars.githubusercontent.com/u/255777700?v=4&s=48" width="48" height="48" alt="MaudeBot" title="MaudeBot"/></a> <a href="https://github.com/YuriNachos"><img src="https://avatars.githubusercontent.com/u/19365375?v=4&s=48" width="48" height="48" alt="YuriNachos" title="YuriNachos"/></a> <a href="https://github.com/chilu18"><img src="https://avatars.githubusercontent.com/u/7957943?v=4&s=48" width="48" height="48" alt="chilu18" title="chilu18"/></a> <a href="https://github.com/byungsker"><img src="https://avatars.githubusercontent.com/u/72309817?v=4&s=48" width="48" height="48" alt="byungsker" title="byungsker"/></a>
<a href="https://github.com/dbhurley"><img src="https://avatars.githubusercontent.com/u/5251425?v=4&s=48" width="48" height="48" alt="dbhurley" title="dbhurley"/></a> <a href="https://github.com/JayMishra-source"><img src="https://avatars.githubusercontent.com/u/82963117?v=4&s=48" width="48" height="48" alt="JayMishra-source" title="JayMishra-source"/></a> <a href="https://github.com/iHildy"><img src="https://avatars.githubusercontent.com/u/25069719?v=4&s=48" width="48" height="48" alt="iHildy" title="iHildy"/></a> <a href="https://github.com/mudrii"><img src="https://avatars.githubusercontent.com/u/220262?v=4&s=48" width="48" height="48" alt="mudrii" title="mudrii"/></a> <a href="https://github.com/dlauer"><img src="https://avatars.githubusercontent.com/u/757041?v=4&s=48" width="48" height="48" alt="dlauer" title="dlauer"/></a> <a href="https://github.com/Solvely-Colin"><img src="https://avatars.githubusercontent.com/u/211764741?v=4&s=48" width="48" height="48" alt="Solvely-Colin" title="Solvely-Colin"/></a> <a href="https://github.com/czekaj"><img src="https://avatars.githubusercontent.com/u/1464539?v=4&s=48" width="48" height="48" alt="czekaj" title="czekaj"/></a> <a href="https://github.com/advaitpaliwal"><img src="https://avatars.githubusercontent.com/u/66044327?v=4&s=48" width="48" height="48" alt="advaitpaliwal" title="advaitpaliwal"/></a> <a href="https://github.com/lc0rp"><img src="https://avatars.githubusercontent.com/u/2609441?v=4&s=48" width="48" height="48" alt="lc0rp" title="lc0rp"/></a> <a href="https://github.com/grp06"><img src="https://avatars.githubusercontent.com/u/1573959?v=4&s=48" width="48" height="48" alt="grp06" title="grp06"/></a>
<a href="https://github.com/HenryLoenwind"><img src="https://avatars.githubusercontent.com/u/1485873?v=4&s=48" width="48" height="48" alt="HenryLoenwind" title="HenryLoenwind"/></a> <a href="https://github.com/azade-c"><img src="https://avatars.githubusercontent.com/u/252790079?v=4&s=48" width="48" height="48" alt="azade-c" title="azade-c"/></a> <a href="https://github.com/Lukavyi"><img src="https://avatars.githubusercontent.com/u/1013690?v=4&s=48" width="48" height="48" alt="Lukavyi" title="Lukavyi"/></a> <a href="https://github.com/vrknetha"><img src="https://avatars.githubusercontent.com/u/20596261?v=4&s=48" width="48" height="48" alt="vrknetha" title="vrknetha"/></a> <a href="https://github.com/brandonwise"><img src="https://avatars.githubusercontent.com/u/21148772?v=4&s=48" width="48" height="48" alt="brandonwise" title="brandonwise"/></a> <a href="https://github.com/conroywhitney"><img src="https://avatars.githubusercontent.com/u/249891?v=4&s=48" width="48" height="48" alt="conroywhitney" title="conroywhitney"/></a> <a href="https://github.com/tobiasbischoff"><img src="https://avatars.githubusercontent.com/u/711564?v=4&s=48" width="48" height="48" alt="Tobias Bischoff" title="Tobias Bischoff"/></a> <a href="https://github.com/davidrudduck"><img src="https://avatars.githubusercontent.com/u/47308254?v=4&s=48" width="48" height="48" alt="davidrudduck" title="davidrudduck"/></a> <a href="https://github.com/xinhuagu"><img src="https://avatars.githubusercontent.com/u/562450?v=4&s=48" width="48" height="48" alt="xinhuagu" title="xinhuagu"/></a> <a href="https://github.com/jaydenfyi"><img src="https://avatars.githubusercontent.com/u/213395523?v=4&s=48" width="48" height="48" alt="jaydenfyi" title="jaydenfyi"/></a>
<a href="https://github.com/petter-b"><img src="https://avatars.githubusercontent.com/u/62076402?v=4&s=48" width="48" height="48" alt="petter-b" title="petter-b"/></a> <a href="https://github.com/heyhudson"><img src="https://avatars.githubusercontent.com/u/258693705?v=4&s=48" width="48" height="48" alt="heyhudson" title="heyhudson"/></a> <a href="https://github.com/MatthieuBizien"><img src="https://avatars.githubusercontent.com/u/173090?v=4&s=48" width="48" height="48" alt="MatthieuBizien" title="MatthieuBizien"/></a> <a href="https://github.com/huntharo"><img src="https://avatars.githubusercontent.com/u/5617868?v=4&s=48" width="48" height="48" alt="huntharo" title="huntharo"/></a> <a href="https://github.com/omair445"><img src="https://avatars.githubusercontent.com/u/32237905?v=4&s=48" width="48" height="48" alt="omair445" title="omair445"/></a> <a href="https://github.com/adam91holt"><img src="https://avatars.githubusercontent.com/u/9592417?v=4&s=48" width="48" height="48" alt="adam91holt" title="adam91holt"/></a> <a href="https://github.com/adhitShet"><img src="https://avatars.githubusercontent.com/u/131381638?v=4&s=48" width="48" height="48" alt="adhitShet" title="adhitShet"/></a> <a href="https://github.com/smartprogrammer93"><img src="https://avatars.githubusercontent.com/u/33181301?v=4&s=48" width="48" height="48" alt="smartprogrammer93" title="smartprogrammer93"/></a> <a href="https://github.com/radek-paclt"><img src="https://avatars.githubusercontent.com/u/50451445?v=4&s=48" width="48" height="48" alt="radek-paclt" title="radek-paclt"/></a> <a href="https://github.com/frankekn"><img src="https://avatars.githubusercontent.com/u/4488090?v=4&s=48" width="48" height="48" alt="frankekn" title="frankekn"/></a>
<a href="https://github.com/bradleypriest"><img src="https://avatars.githubusercontent.com/u/167215?v=4&s=48" width="48" height="48" alt="bradleypriest" title="bradleypriest"/></a> <a href="https://github.com/rahthakor"><img src="https://avatars.githubusercontent.com/u/8470553?v=4&s=48" width="48" height="48" alt="rahthakor" title="rahthakor"/></a> <a href="https://github.com/shadril238"><img src="https://avatars.githubusercontent.com/u/63901551?v=4&s=48" width="48" height="48" alt="shadril238" title="shadril238"/></a> <a href="https://github.com/VACInc"><img src="https://avatars.githubusercontent.com/u/3279061?v=4&s=48" width="48" height="48" alt="VACInc" title="VACInc"/></a> <a href="https://github.com/juanpablodlc"><img src="https://avatars.githubusercontent.com/u/92012363?v=4&s=48" width="48" height="48" alt="juanpablodlc" title="juanpablodlc"/></a> <a href="https://github.com/jonisjongithub"><img src="https://avatars.githubusercontent.com/u/86072337?v=4&s=48" width="48" height="48" alt="jonisjongithub" title="jonisjongithub"/></a> <a href="https://github.com/magimetal"><img src="https://avatars.githubusercontent.com/u/36491250?v=4&s=48" width="48" height="48" alt="magimetal" title="magimetal"/></a> <a href="https://github.com/stakeswky"><img src="https://avatars.githubusercontent.com/u/64798754?v=4&s=48" width="48" height="48" alt="stakeswky" title="stakeswky"/></a> <a href="https://github.com/AbhisekBasu1"><img src="https://avatars.githubusercontent.com/u/40645221?v=4&s=48" width="48" height="48" alt="abhisekbasu1" title="abhisekbasu1"/></a> <a href="https://github.com/MisterGuy420"><img src="https://avatars.githubusercontent.com/u/255743668?v=4&s=48" width="48" height="48" alt="MisterGuy420" title="MisterGuy420"/></a>
<a href="https://github.com/hsrvc"><img src="https://avatars.githubusercontent.com/u/129702169?v=4&s=48" width="48" height="48" alt="hsrvc" title="hsrvc"/></a> <a href="https://github.com/nabbilkhan"><img src="https://avatars.githubusercontent.com/u/203121263?v=4&s=48" width="48" height="48" alt="nabbilkhan" title="nabbilkhan"/></a> <a href="https://github.com/aldoeliacim"><img src="https://avatars.githubusercontent.com/u/17973757?v=4&s=48" width="48" height="48" alt="aldoeliacim" title="aldoeliacim"/></a> <a href="https://github.com/jamesgroat"><img src="https://avatars.githubusercontent.com/u/2634024?v=4&s=48" width="48" height="48" alt="jamesgroat" title="jamesgroat"/></a> <a href="https://github.com/orlyjamie"><img src="https://avatars.githubusercontent.com/u/6668807?v=4&s=48" width="48" height="48" alt="orlyjamie" title="orlyjamie"/></a> <a href="https://github.com/Elarwei001"><img src="https://avatars.githubusercontent.com/u/168552401?v=4&s=48" width="48" height="48" alt="Elarwei001" title="Elarwei001"/></a> <a href="https://github.com/rubyrunsstuff"><img src="https://avatars.githubusercontent.com/u/246602379?v=4&s=48" width="48" height="48" alt="rubyrunsstuff" title="rubyrunsstuff"/></a> <a href="https://github.com/Phineas1500"><img src="https://avatars.githubusercontent.com/u/41450967?v=4&s=48" width="48" height="48" alt="Phineas1500" title="Phineas1500"/></a> <a href="https://github.com/meaningfool"><img src="https://avatars.githubusercontent.com/u/2862331?v=4&s=48" width="48" height="48" alt="meaningfool" title="meaningfool"/></a> <a href="https://github.com/sfo2001"><img src="https://avatars.githubusercontent.com/u/103369858?v=4&s=48" width="48" height="48" alt="sfo2001" title="sfo2001"/></a>
<a href="https://github.com/Marvae"><img src="https://avatars.githubusercontent.com/u/11957602?v=4&s=48" width="48" height="48" alt="Marvae" title="Marvae"/></a> <a href="https://github.com/liuy"><img src="https://avatars.githubusercontent.com/u/1192888?v=4&s=48" width="48" height="48" alt="liuy" title="liuy"/></a> <a href="https://github.com/shtse8"><img src="https://avatars.githubusercontent.com/u/8020099?v=4&s=48" width="48" height="48" alt="shtse8" title="shtse8"/></a> <a href="https://github.com/thebenignhacker"><img src="https://avatars.githubusercontent.com/u/32418586?v=4&s=48" width="48" height="48" alt="thebenignhacker" title="thebenignhacker"/></a> <a href="https://github.com/carrotRakko"><img src="https://avatars.githubusercontent.com/u/24588751?v=4&s=48" width="48" height="48" alt="carrotRakko" title="carrotRakko"/></a> <a href="https://github.com/ranausmanai"><img src="https://avatars.githubusercontent.com/u/257128159?v=4&s=48" width="48" height="48" alt="ranausmanai" title="ranausmanai"/></a> <a href="https://github.com/kevinWangSheng"><img src="https://avatars.githubusercontent.com/u/118158941?v=4&s=48" width="48" height="48" alt="kevinWangSheng" title="kevinWangSheng"/></a> <a href="https://github.com/gregmousseau"><img src="https://avatars.githubusercontent.com/u/5036458?v=4&s=48" width="48" height="48" alt="gregmousseau" title="gregmousseau"/></a> <a href="https://github.com/rrenamed"><img src="https://avatars.githubusercontent.com/u/87486610?v=4&s=48" width="48" height="48" alt="rrenamed" title="rrenamed"/></a> <a href="https://github.com/akoscz"><img src="https://avatars.githubusercontent.com/u/1360047?v=4&s=48" width="48" height="48" alt="akoscz" title="akoscz"/></a>
<a href="https://github.com/jarvis-medmatic"><img src="https://avatars.githubusercontent.com/u/252428873?v=4&s=48" width="48" height="48" alt="jarvis-medmatic" title="jarvis-medmatic"/></a> <a href="https://github.com/danielz1z"><img src="https://avatars.githubusercontent.com/u/235270390?v=4&s=48" width="48" height="48" alt="danielz1z" title="danielz1z"/></a> <a href="https://github.com/pandego"><img src="https://avatars.githubusercontent.com/u/7780875?v=4&s=48" width="48" height="48" alt="pandego" title="pandego"/></a> <a href="https://github.com/xadenryan"><img src="https://avatars.githubusercontent.com/u/165437834?v=4&s=48" width="48" height="48" alt="xadenryan" title="xadenryan"/></a> <a href="https://github.com/NicholasSpisak"><img src="https://avatars.githubusercontent.com/u/129075147?v=4&s=48" width="48" height="48" alt="NicholasSpisak" title="NicholasSpisak"/></a> <a href="https://github.com/graysurf"><img src="https://avatars.githubusercontent.com/u/10785178?v=4&s=48" width="48" height="48" alt="graysurf" title="graysurf"/></a> <a href="https://github.com/gupsammy"><img src="https://avatars.githubusercontent.com/u/20296019?v=4&s=48" width="48" height="48" alt="gupsammy" title="gupsammy"/></a> <a href="https://github.com/nyanjou"><img src="https://avatars.githubusercontent.com/u/258645604?v=4&s=48" width="48" height="48" alt="nyanjou" title="nyanjou"/></a> <a href="https://github.com/sibbl"><img src="https://avatars.githubusercontent.com/u/866535?v=4&s=48" width="48" height="48" alt="sibbl" title="sibbl"/></a> <a href="https://github.com/gejifeng"><img src="https://avatars.githubusercontent.com/u/17561857?v=4&s=48" width="48" height="48" alt="gejifeng" title="gejifeng"/></a>
<a href="https://github.com/ide-rea"><img src="https://avatars.githubusercontent.com/u/30512600?v=4&s=48" width="48" height="48" alt="ide-rea" title="ide-rea"/></a> <a href="https://github.com/leszekszpunar"><img src="https://avatars.githubusercontent.com/u/13106764?v=4&s=48" width="48" height="48" alt="leszekszpunar" title="leszekszpunar"/></a> <a href="https://github.com/Yida-Dev"><img src="https://avatars.githubusercontent.com/u/92713555?v=4&s=48" width="48" height="48" alt="Yida-Dev" title="Yida-Dev"/></a> <a href="https://github.com/AI-Reviewer-QS"><img src="https://avatars.githubusercontent.com/u/255312808?v=4&s=48" width="48" height="48" alt="AI-Reviewer-QS" title="AI-Reviewer-QS"/></a> <a href="https://github.com/SocialNerd42069"><img src="https://avatars.githubusercontent.com/u/118244303?v=4&s=48" width="48" height="48" alt="SocialNerd42069" title="SocialNerd42069"/></a> <a href="https://github.com/maxsumrall"><img src="https://avatars.githubusercontent.com/u/628843?v=4&s=48" width="48" height="48" alt="maxsumrall" title="maxsumrall"/></a> <a href="https://github.com/hougangdev"><img src="https://avatars.githubusercontent.com/u/105773686?v=4&s=48" width="48" height="48" alt="hougangdev" title="hougangdev"/></a> <a href="https://github.com/Minidoracat"><img src="https://avatars.githubusercontent.com/u/11269639?v=4&s=48" width="48" height="48" alt="Minidoracat" title="Minidoracat"/></a> <a href="https://github.com/AnonO6"><img src="https://avatars.githubusercontent.com/u/124311066?v=4&s=48" width="48" height="48" alt="AnonO6" title="AnonO6"/></a> <a href="https://github.com/sreekaransrinath"><img src="https://avatars.githubusercontent.com/u/50989977?v=4&s=48" width="48" height="48" alt="sreekaransrinath" title="sreekaransrinath"/></a>
<a href="https://github.com/YuzuruS"><img src="https://avatars.githubusercontent.com/u/1485195?v=4&s=48" width="48" height="48" alt="YuzuruS" title="YuzuruS"/></a> <a href="https://github.com/riccardogiorato"><img src="https://avatars.githubusercontent.com/u/4527364?v=4&s=48" width="48" height="48" alt="riccardogiorato" title="riccardogiorato"/></a> <a href="https://github.com/Bridgerz"><img src="https://avatars.githubusercontent.com/u/24499532?v=4&s=48" width="48" height="48" alt="Bridgerz" title="Bridgerz"/></a> <a href="https://github.com/Mrseenz"><img src="https://avatars.githubusercontent.com/u/101962919?v=4&s=48" width="48" height="48" alt="Mrseenz" title="Mrseenz"/></a> <a href="https://github.com/buddyh"><img src="https://avatars.githubusercontent.com/u/31752869?v=4&s=48" width="48" height="48" alt="buddyh" title="buddyh"/></a> <a href="https://github.com/omniwired"><img src="https://avatars.githubusercontent.com/u/322761?v=4&s=48" width="48" height="48" alt="Eng. Juan Combetto" title="Eng. Juan Combetto"/></a> <a href="https://github.com/peschee"><img src="https://avatars.githubusercontent.com/u/63866?v=4&s=48" width="48" height="48" alt="peschee" title="peschee"/></a> <a href="https://github.com/cash-echo-bot"><img src="https://avatars.githubusercontent.com/u/252747386?v=4&s=48" width="48" height="48" alt="cash-echo-bot" title="cash-echo-bot"/></a> <a href="https://github.com/jalehman"><img src="https://avatars.githubusercontent.com/u/550978?v=4&s=48" width="48" height="48" alt="jalehman" title="jalehman"/></a> <a href="https://github.com/zknicker"><img src="https://avatars.githubusercontent.com/u/1164085?v=4&s=48" width="48" height="48" alt="zknicker" title="zknicker"/></a>
<a href="https://github.com/buerbaumer"><img src="https://avatars.githubusercontent.com/u/44548809?v=4&s=48" width="48" height="48" alt="Harald Buerbaumer" title="Harald Buerbaumer"/></a> <a href="https://github.com/taw0002"><img src="https://avatars.githubusercontent.com/u/42811278?v=4&s=48" width="48" height="48" alt="taw0002" title="taw0002"/></a> <a href="https://github.com/scald"><img src="https://avatars.githubusercontent.com/u/1215913?v=4&s=48" width="48" height="48" alt="scald" title="scald"/></a> <a href="https://github.com/openperf"><img src="https://avatars.githubusercontent.com/u/80630709?v=4&s=48" width="48" height="48" alt="openperf" title="openperf"/></a> <a href="https://github.com/BUGKillerKing"><img src="https://avatars.githubusercontent.com/u/117326392?v=4&s=48" width="48" height="48" alt="BUGKillerKing" title="BUGKillerKing"/></a> <a href="https://github.com/Oceanswave"><img src="https://avatars.githubusercontent.com/u/760674?v=4&s=48" width="48" height="48" alt="Oceanswave" title="Oceanswave"/></a> <a href="https://github.com/patelhiren"><img src="https://avatars.githubusercontent.com/u/172098?v=4&s=48" width="48" height="48" alt="Hiren Patel" title="Hiren Patel"/></a> <a href="https://github.com/kiranjd"><img src="https://avatars.githubusercontent.com/u/25822851?v=4&s=48" width="48" height="48" alt="kiranjd" title="kiranjd"/></a> <a href="https://github.com/antons"><img src="https://avatars.githubusercontent.com/u/129705?v=4&s=48" width="48" height="48" alt="antons" title="antons"/></a> <a href="https://github.com/dan-dr"><img src="https://avatars.githubusercontent.com/u/6669808?v=4&s=48" width="48" height="48" alt="dan-dr" title="dan-dr"/></a>
<a href="https://github.com/jadilson12"><img src="https://avatars.githubusercontent.com/u/36805474?v=4&s=48" width="48" height="48" alt="jadilson12" title="jadilson12"/></a> <a href="https://github.com/sumleo"><img src="https://avatars.githubusercontent.com/u/29517764?v=4&s=48" width="48" height="48" alt="sumleo" title="sumleo"/></a> <a href="https://github.com/Whoaa512"><img src="https://avatars.githubusercontent.com/u/1581943?v=4&s=48" width="48" height="48" alt="Whoaa512" title="Whoaa512"/></a> <a href="https://github.com/luijoc"><img src="https://avatars.githubusercontent.com/u/96428056?v=4&s=48" width="48" height="48" alt="luijoc" title="luijoc"/></a> <a href="https://github.com/niceysam"><img src="https://avatars.githubusercontent.com/u/256747835?v=4&s=48" width="48" height="48" alt="niceysam" title="niceysam"/></a> <a href="https://github.com/JustYannicc"><img src="https://avatars.githubusercontent.com/u/52761674?v=4&s=48" width="48" height="48" alt="JustYannicc" title="JustYannicc"/></a> <a href="https://github.com/emanuelst"><img src="https://avatars.githubusercontent.com/u/9994339?v=4&s=48" width="48" height="48" alt="emanuelst" title="emanuelst"/></a> <a href="https://github.com/TsekaLuk"><img src="https://avatars.githubusercontent.com/u/79151285?v=4&s=48" width="48" height="48" alt="TsekaLuk" title="TsekaLuk"/></a> <a href="https://github.com/JustasMonkev"><img src="https://avatars.githubusercontent.com/u/59362982?v=4&s=48" width="48" height="48" alt="JustasM" title="JustasM"/></a> <a href="https://github.com/loiie45e"><img src="https://avatars.githubusercontent.com/u/15420100?v=4&s=48" width="48" height="48" alt="loiie45e" title="loiie45e"/></a>
<a href="https://github.com/davidguttman"><img src="https://avatars.githubusercontent.com/u/431696?v=4&s=48" width="48" height="48" alt="davidguttman" title="davidguttman"/></a> <a href="https://github.com/natefikru"><img src="https://avatars.githubusercontent.com/u/10344644?v=4&s=48" width="48" height="48" alt="natefikru" title="natefikru"/></a> <a href="https://github.com/dougvk"><img src="https://avatars.githubusercontent.com/u/401660?v=4&s=48" width="48" height="48" alt="dougvk" title="dougvk"/></a> <a href="https://github.com/koala73"><img src="https://avatars.githubusercontent.com/u/996596?v=4&s=48" width="48" height="48" alt="koala73" title="koala73"/></a> <a href="https://github.com/mkbehr"><img src="https://avatars.githubusercontent.com/u/1285?v=4&s=48" width="48" height="48" alt="mkbehr" title="mkbehr"/></a> <a href="https://github.com/zats"><img src="https://avatars.githubusercontent.com/u/2688806?v=4&s=48" width="48" height="48" alt="zats" title="zats"/></a> <a href="https://github.com/simonemacario"><img src="https://avatars.githubusercontent.com/u/2116609?v=4&s=48" width="48" height="48" alt="Simone Macario" title="Simone Macario"/></a> <a href="https://github.com/openclaw-bot"><img src="https://avatars.githubusercontent.com/u/258178069?v=4&s=48" width="48" height="48" alt="openclaw-bot" title="openclaw-bot"/></a> <a href="https://github.com/ENCHIGO"><img src="https://avatars.githubusercontent.com/u/38551565?v=4&s=48" width="48" height="48" alt="ENCHIGO" title="ENCHIGO"/></a> <a href="https://github.com/mteam88"><img src="https://avatars.githubusercontent.com/u/84196639?v=4&s=48" width="48" height="48" alt="mteam88" title="mteam88"/></a>
<a href="https://github.com/Blakeshannon"><img src="https://avatars.githubusercontent.com/u/257822860?v=4&s=48" width="48" height="48" alt="Blakeshannon" title="Blakeshannon"/></a> <a href="https://github.com/gabriel-trigo"><img src="https://avatars.githubusercontent.com/u/38991125?v=4&s=48" width="48" height="48" alt="gabriel-trigo" title="gabriel-trigo"/></a> <a href="https://github.com/neist"><img src="https://avatars.githubusercontent.com/u/1029724?v=4&s=48" width="48" height="48" alt="neist" title="neist"/></a> <a href="https://github.com/pejmanjohn"><img src="https://avatars.githubusercontent.com/u/481729?v=4&s=48" width="48" height="48" alt="pejmanjohn" title="pejmanjohn"/></a> <a href="https://github.com/durenzidu"><img src="https://avatars.githubusercontent.com/u/38130340?v=4&s=48" width="48" height="48" alt="durenzidu" title="durenzidu"/></a> <a href="https://github.com/Ryan-Haines"><img src="https://avatars.githubusercontent.com/u/1855752?v=4&s=48" width="48" height="48" alt="Ryan Haines" title="Ryan Haines"/></a> <a href="https://github.com/hclsys"><img src="https://avatars.githubusercontent.com/u/7755017?v=4&s=48" width="48" height="48" alt="hcl" title="hcl"/></a> <a href="https://github.com/xuhao1"><img src="https://avatars.githubusercontent.com/u/5087930?v=4&s=48" width="48" height="48" alt="XuHao" title="XuHao"/></a> <a href="https://github.com/benithors"><img src="https://avatars.githubusercontent.com/u/20652882?v=4&s=48" width="48" height="48" alt="benithors" title="benithors"/></a> <a href="https://github.com/bitfoundry-ai"><img src="https://avatars.githubusercontent.com/u/239082898?v=4&s=48" width="48" height="48" alt="bitfoundry-ai" title="bitfoundry-ai"/></a>
<a href="https://github.com/HeMuling"><img src="https://avatars.githubusercontent.com/u/74801533?v=4&s=48" width="48" height="48" alt="HeMuling" title="HeMuling"/></a> <a href="https://github.com/markmusson"><img src="https://avatars.githubusercontent.com/u/4801649?v=4&s=48" width="48" height="48" alt="markmusson" title="markmusson"/></a> <a href="https://github.com/ameno-"><img src="https://avatars.githubusercontent.com/u/2416135?v=4&s=48" width="48" height="48" alt="ameno-" title="ameno-"/></a> <a href="https://github.com/battman21"><img src="https://avatars.githubusercontent.com/u/2656916?v=4&s=48" width="48" height="48" alt="battman21" title="battman21"/></a> <a href="https://github.com/BinHPdev"><img src="https://avatars.githubusercontent.com/u/219093083?v=4&s=48" width="48" height="48" alt="BinHPdev" title="BinHPdev"/></a> <a href="https://github.com/dguido"><img src="https://avatars.githubusercontent.com/u/294844?v=4&s=48" width="48" height="48" alt="dguido" title="dguido"/></a> <a href="https://github.com/evalexpr"><img src="https://avatars.githubusercontent.com/u/23485511?v=4&s=48" width="48" height="48" alt="evalexpr" title="evalexpr"/></a> <a href="https://github.com/guirguispierre"><img src="https://avatars.githubusercontent.com/u/22091706?v=4&s=48" width="48" height="48" alt="guirguispierre" title="guirguispierre"/></a> <a href="https://github.com/henrino3"><img src="https://avatars.githubusercontent.com/u/4260288?v=4&s=48" width="48" height="48" alt="henrino3" title="henrino3"/></a> <a href="https://github.com/joeykrug"><img src="https://avatars.githubusercontent.com/u/5925937?v=4&s=48" width="48" height="48" alt="joeykrug" title="joeykrug"/></a>
<a href="https://github.com/loganprit"><img src="https://avatars.githubusercontent.com/u/72722788?v=4&s=48" width="48" height="48" alt="loganprit" title="loganprit"/></a> <a href="https://github.com/odysseus0"><img src="https://avatars.githubusercontent.com/u/8635094?v=4&s=48" width="48" height="48" alt="odysseus0" title="odysseus0"/></a> <a href="https://github.com/dbachelder"><img src="https://avatars.githubusercontent.com/u/325706?v=4&s=48" width="48" height="48" alt="dbachelder" title="dbachelder"/></a> <a href="https://github.com/divanoli"><img src="https://avatars.githubusercontent.com/u/12023205?v=4&s=48" width="48" height="48" alt="Divanoli Mydeen Pitchai" title="Divanoli Mydeen Pitchai"/></a> <a href="https://github.com/liuxiaopai-ai"><img src="https://avatars.githubusercontent.com/u/73659136?v=4&s=48" width="48" height="48" alt="liuxiaopai-ai" title="liuxiaopai-ai"/></a> <a href="https://github.com/theSamPadilla"><img src="https://avatars.githubusercontent.com/u/35386211?v=4&s=48" width="48" height="48" alt="Sam Padilla" title="Sam Padilla"/></a> <a href="https://github.com/pvtclawn"><img src="https://avatars.githubusercontent.com/u/258811507?v=4&s=48" width="48" height="48" alt="pvtclawn" title="pvtclawn"/></a> <a href="https://github.com/seheepeak"><img src="https://avatars.githubusercontent.com/u/134766597?v=4&s=48" width="48" height="48" alt="seheepeak" title="seheepeak"/></a> <a href="https://github.com/TSavo"><img src="https://avatars.githubusercontent.com/u/877990?v=4&s=48" width="48" height="48" alt="TSavo" title="TSavo"/></a> <a href="https://github.com/nachoiacovino"><img src="https://avatars.githubusercontent.com/u/50103937?v=4&s=48" width="48" height="48" alt="nachoiacovino" title="nachoiacovino"/></a>
<a href="https://github.com/misterdas"><img src="https://avatars.githubusercontent.com/u/170702047?v=4&s=48" width="48" height="48" alt="misterdas" title="misterdas"/></a> <a href="https://github.com/xzq-xu"><img src="https://avatars.githubusercontent.com/u/53989315?v=4&s=48" width="48" height="48" alt="LeftX" title="LeftX"/></a> <a href="https://github.com/badlogic"><img src="https://avatars.githubusercontent.com/u/514052?v=4&s=48" width="48" height="48" alt="badlogic" title="badlogic"/></a> <a href="https://github.com/Shuai-DaiDai"><img src="https://avatars.githubusercontent.com/u/134567396?v=4&s=48" width="48" height="48" alt="Shuai-DaiDai" title="Shuai-DaiDai"/></a> <a href="https://github.com/mousberg"><img src="https://avatars.githubusercontent.com/u/57605064?v=4&s=48" width="48" height="48" alt="mousberg" title="mousberg"/></a> <a href="https://github.com/harhogefoo"><img src="https://avatars.githubusercontent.com/u/11906529?v=4&s=48" width="48" height="48" alt="Masataka Shinohara" title="Masataka Shinohara"/></a> <a href="https://github.com/BillChirico"><img src="https://avatars.githubusercontent.com/u/13951316?v=4&s=48" width="48" height="48" alt="BillChirico" title="BillChirico"/></a> <a href="https://github.com/lewiswigmore"><img src="https://avatars.githubusercontent.com/u/58551848?v=4&s=48" width="48" height="48" alt="Lewis" title="Lewis"/></a> <a href="https://github.com/solstead"><img src="https://avatars.githubusercontent.com/u/168413654?v=4&s=48" width="48" height="48" alt="solstead" title="solstead"/></a> <a href="https://github.com/julianengel"><img src="https://avatars.githubusercontent.com/u/10634231?v=4&s=48" width="48" height="48" alt="julianengel" title="julianengel"/></a>
<a href="https://github.com/dantelex"><img src="https://avatars.githubusercontent.com/u/631543?v=4&s=48" width="48" height="48" alt="dantelex" title="dantelex"/></a> <a href="https://github.com/sahilsatralkar"><img src="https://avatars.githubusercontent.com/u/62758655?v=4&s=48" width="48" height="48" alt="sahilsatralkar" title="sahilsatralkar"/></a> <a href="https://github.com/kkarimi"><img src="https://avatars.githubusercontent.com/u/875218?v=4&s=48" width="48" height="48" alt="kkarimi" title="kkarimi"/></a> <a href="https://github.com/mahmoudashraf93"><img src="https://avatars.githubusercontent.com/u/9130129?v=4&s=48" width="48" height="48" alt="mahmoudashraf93" title="mahmoudashraf93"/></a> <a href="https://github.com/pkrmf"><img src="https://avatars.githubusercontent.com/u/1714267?v=4&s=48" width="48" height="48" alt="pkrmf" title="pkrmf"/></a> <a href="https://github.com/ryan-crabbe"><img src="https://avatars.githubusercontent.com/u/128659760?v=4&s=48" width="48" height="48" alt="ryan-crabbe" title="ryan-crabbe"/></a> <a href="https://github.com/miloudbelarebia"><img src="https://avatars.githubusercontent.com/u/136994453?v=4&s=48" width="48" height="48" alt="miloudbelarebia" title="miloudbelarebia"/></a> <a href="https://github.com/Mellowambience"><img src="https://avatars.githubusercontent.com/u/40958792?v=4&s=48" width="48" height="48" alt="Mars" title="Mars"/></a> <a href="https://github.com/El-Fitz"><img src="https://avatars.githubusercontent.com/u/8971906?v=4&s=48" width="48" height="48" alt="El-Fitz" title="El-Fitz"/></a> <a href="https://github.com/mcrolly"><img src="https://avatars.githubusercontent.com/u/60803337?v=4&s=48" width="48" height="48" alt="McRolly NWANGWU" title="McRolly NWANGWU"/></a>
<a href="https://github.com/carlulsoe"><img src="https://avatars.githubusercontent.com/u/34673973?v=4&s=48" width="48" height="48" alt="carlulsoe" title="carlulsoe"/></a> <a href="https://github.com/Dithilli"><img src="https://avatars.githubusercontent.com/u/41286037?v=4&s=48" width="48" height="48" alt="Dithilli" title="Dithilli"/></a> <a href="https://github.com/emonty"><img src="https://avatars.githubusercontent.com/u/95156?v=4&s=48" width="48" height="48" alt="emonty" title="emonty"/></a> <a href="https://github.com/fal3"><img src="https://avatars.githubusercontent.com/u/6484295?v=4&s=48" width="48" height="48" alt="fal3" title="fal3"/></a> <a href="https://github.com/mitschabaude-bot"><img src="https://avatars.githubusercontent.com/u/247582884?v=4&s=48" width="48" height="48" alt="mitschabaude-bot" title="mitschabaude-bot"/></a> <a href="https://github.com/benostein"><img src="https://avatars.githubusercontent.com/u/31802821?v=4&s=48" width="48" height="48" alt="benostein" title="benostein"/></a> <a href="https://github.com/PeterShanxin"><img src="https://avatars.githubusercontent.com/u/128674037?v=4&s=48" width="48" height="48" alt="LI SHANXIN" title="LI SHANXIN"/></a> <a href="https://github.com/magendary"><img src="https://avatars.githubusercontent.com/u/30611068?v=4&s=48" width="48" height="48" alt="magendary" title="magendary"/></a> <a href="https://github.com/mahanandhi"><img src="https://avatars.githubusercontent.com/u/46371575?v=4&s=48" width="48" height="48" alt="mahanandhi" title="mahanandhi"/></a> <a href="https://github.com/CashWilliams"><img src="https://avatars.githubusercontent.com/u/613573?v=4&s=48" width="48" height="48" alt="CashWilliams" title="CashWilliams"/></a>
<a href="https://github.com/j2h4u"><img src="https://avatars.githubusercontent.com/u/39818683?v=4&s=48" width="48" height="48" alt="j2h4u" title="j2h4u"/></a> <a href="https://github.com/bsormagec"><img src="https://avatars.githubusercontent.com/u/965219?v=4&s=48" width="48" height="48" alt="bsormagec" title="bsormagec"/></a> <a href="https://github.com/jessy2027"><img src="https://avatars.githubusercontent.com/u/89694096?v=4&s=48" width="48" height="48" alt="Jessy LANGE" title="Jessy LANGE"/></a> <a href="https://github.com/aerolalit"><img src="https://avatars.githubusercontent.com/u/17166039?v=4&s=48" width="48" height="48" alt="Lalit Singh" title="Lalit Singh"/></a> <a href="https://github.com/hyf0-agent"><img src="https://avatars.githubusercontent.com/u/258783736?v=4&s=48" width="48" height="48" alt="hyf0-agent" title="hyf0-agent"/></a> <a href="https://github.com/andranik-sahakyan"><img src="https://avatars.githubusercontent.com/u/8908029?v=4&s=48" width="48" height="48" alt="andranik-sahakyan" title="andranik-sahakyan"/></a> <a href="https://github.com/unisone"><img src="https://avatars.githubusercontent.com/u/32521398?v=4&s=48" width="48" height="48" alt="unisone" title="unisone"/></a> <a href="https://github.com/jeann2013"><img src="https://avatars.githubusercontent.com/u/3299025?v=4&s=48" width="48" height="48" alt="jeann2013" title="jeann2013"/></a> <a href="https://github.com/jogelin"><img src="https://avatars.githubusercontent.com/u/954509?v=4&s=48" width="48" height="48" alt="jogelin" title="jogelin"/></a> <a href="https://github.com/rmorse"><img src="https://avatars.githubusercontent.com/u/853547?v=4&s=48" width="48" height="48" alt="rmorse" title="rmorse"/></a>
<a href="https://github.com/scz2011"><img src="https://avatars.githubusercontent.com/u/9337506?v=4&s=48" width="48" height="48" alt="scz2011" title="scz2011"/></a> <a href="https://github.com/wes-davis"><img src="https://avatars.githubusercontent.com/u/16506720?v=4&s=48" width="48" height="48" alt="wes-davis" title="wes-davis"/></a> <a href="https://github.com/popomore"><img src="https://avatars.githubusercontent.com/u/360661?v=4&s=48" width="48" height="48" alt="popomore" title="popomore"/></a> <a href="https://github.com/cathrynlavery"><img src="https://avatars.githubusercontent.com/u/50469282?v=4&s=48" width="48" height="48" alt="cathrynlavery" title="cathrynlavery"/></a> <a href="https://github.com/Iamadig"><img src="https://avatars.githubusercontent.com/u/102129234?v=4&s=48" width="48" height="48" alt="iamadig" title="iamadig"/></a> <a href="https://github.com/vsabavat"><img src="https://avatars.githubusercontent.com/u/50385532?v=4&s=48" width="48" height="48" alt="Vasanth Rao Naik Sabavat" title="Vasanth Rao Naik Sabavat"/></a> <a href="https://github.com/jscaldwell55"><img src="https://avatars.githubusercontent.com/u/111952840?v=4&s=48" width="48" height="48" alt="Jay Caldwell" title="Jay Caldwell"/></a> <a href="https://github.com/gut-puncture"><img src="https://avatars.githubusercontent.com/u/75851986?v=4&s=48" width="48" height="48" alt="Shailesh" title="Shailesh"/></a> <a href="https://github.com/KirillShchetinin"><img src="https://avatars.githubusercontent.com/u/13061871?v=4&s=48" width="48" height="48" alt="Kirill Shchetynin" title="Kirill Shchetynin"/></a> <a href="https://github.com/ruypang"><img src="https://avatars.githubusercontent.com/u/46941315?v=4&s=48" width="48" height="48" alt="ruypang" title="ruypang"/></a>
<a href="https://github.com/mitchmcalister"><img src="https://avatars.githubusercontent.com/u/209334?v=4&s=48" width="48" height="48" alt="mitchmcalister" title="mitchmcalister"/></a> <a href="https://github.com/pvoo"><img src="https://avatars.githubusercontent.com/u/20116814?v=4&s=48" width="48" height="48" alt="Paul van Oorschot" title="Paul van Oorschot"/></a> <a href="https://github.com/guxu11"><img src="https://avatars.githubusercontent.com/u/53551744?v=4&s=48" width="48" height="48" alt="Xu Gu" title="Xu Gu"/></a> <a href="https://github.com/lml2468"><img src="https://avatars.githubusercontent.com/u/39320777?v=4&s=48" width="48" height="48" alt="Menglin Li" title="Menglin Li"/></a> <a href="https://github.com/artuskg"><img src="https://avatars.githubusercontent.com/u/11966157?v=4&s=48" width="48" height="48" alt="artuskg" title="artuskg"/></a> <a href="https://github.com/jackheuberger"><img src="https://avatars.githubusercontent.com/u/7830838?v=4&s=48" width="48" height="48" alt="jackheuberger" title="jackheuberger"/></a> <a href="https://github.com/imfing"><img src="https://avatars.githubusercontent.com/u/5097752?v=4&s=48" width="48" height="48" alt="imfing" title="imfing"/></a> <a href="https://github.com/superman32432432"><img src="https://avatars.githubusercontent.com/u/7228420?v=4&s=48" width="48" height="48" alt="superman32432432" title="superman32432432"/></a> <a href="https://github.com/Syhids"><img src="https://avatars.githubusercontent.com/u/671202?v=4&s=48" width="48" height="48" alt="Syhids" title="Syhids"/></a> <a href="https://github.com/Zitzak"><img src="https://avatars.githubusercontent.com/u/43185740?v=4&s=48" width="48" height="48" alt="Marvin" title="Marvin"/></a>
<a href="https://github.com/DrCrinkle"><img src="https://avatars.githubusercontent.com/u/62564740?v=4&s=48" width="48" height="48" alt="Taylor Asplund" title="Taylor Asplund"/></a> <a href="https://github.com/dakshaymehta"><img src="https://avatars.githubusercontent.com/u/50276213?v=4&s=48" width="48" height="48" alt="dakshaymehta" title="dakshaymehta"/></a> <a href="https://github.com/stefangalescu"><img src="https://avatars.githubusercontent.com/u/52995748?v=4&s=48" width="48" height="48" alt="Stefan Galescu" title="Stefan Galescu"/></a> <a href="https://github.com/lploc94"><img src="https://avatars.githubusercontent.com/u/28453843?v=4&s=48" width="48" height="48" alt="lploc94" title="lploc94"/></a> <a href="https://github.com/WalterSumbon"><img src="https://avatars.githubusercontent.com/u/45062253?v=4&s=48" width="48" height="48" alt="WalterSumbon" title="WalterSumbon"/></a> <a href="https://github.com/krizpoon"><img src="https://avatars.githubusercontent.com/u/1977532?v=4&s=48" width="48" height="48" alt="krizpoon" title="krizpoon"/></a> <a href="https://github.com/EnzeD"><img src="https://avatars.githubusercontent.com/u/9866900?v=4&s=48" width="48" height="48" alt="EnzeD" title="EnzeD"/></a> <a href="https://github.com/Evizero"><img src="https://avatars.githubusercontent.com/u/10854026?v=4&s=48" width="48" height="48" alt="Evizero" title="Evizero"/></a> <a href="https://github.com/Grynn"><img src="https://avatars.githubusercontent.com/u/212880?v=4&s=48" width="48" height="48" alt="Grynn" title="Grynn"/></a> <a href="https://github.com/hydro13"><img src="https://avatars.githubusercontent.com/u/6640526?v=4&s=48" width="48" height="48" alt="hydro13" title="hydro13"/></a>
<a href="https://github.com/jverdi"><img src="https://avatars.githubusercontent.com/u/345050?v=4&s=48" width="48" height="48" alt="jverdi" title="jverdi"/></a> <a href="https://github.com/kentaro"><img src="https://avatars.githubusercontent.com/u/3458?v=4&s=48" width="48" height="48" alt="kentaro" title="kentaro"/></a> <a href="https://github.com/kunalk16"><img src="https://avatars.githubusercontent.com/u/5303824?v=4&s=48" width="48" height="48" alt="kunalk16" title="kunalk16"/></a> <a href="https://github.com/longmaba"><img src="https://avatars.githubusercontent.com/u/9361500?v=4&s=48" width="48" height="48" alt="longmaba" title="longmaba"/></a> <a href="https://github.com/mjrussell"><img src="https://avatars.githubusercontent.com/u/1641895?v=4&s=48" width="48" height="48" alt="mjrussell" title="mjrussell"/></a> <a href="https://github.com/optimikelabs"><img src="https://avatars.githubusercontent.com/u/31423109?v=4&s=48" width="48" height="48" alt="optimikelabs" title="optimikelabs"/></a> <a href="https://github.com/oswalpalash"><img src="https://avatars.githubusercontent.com/u/6431196?v=4&s=48" width="48" height="48" alt="oswalpalash" title="oswalpalash"/></a> <a href="https://github.com/RamiNoodle733"><img src="https://avatars.githubusercontent.com/u/117773986?v=4&s=48" width="48" height="48" alt="RamiNoodle733" title="RamiNoodle733"/></a> <a href="https://github.com/sauerdaniel"><img src="https://avatars.githubusercontent.com/u/81422812?v=4&s=48" width="48" height="48" alt="sauerdaniel" title="sauerdaniel"/></a> <a href="https://github.com/SleuthCo"><img src="https://avatars.githubusercontent.com/u/259695222?v=4&s=48" width="48" height="48" alt="SleuthCo" title="SleuthCo"/></a>
<a href="https://github.com/TaKO8Ki"><img src="https://avatars.githubusercontent.com/u/41065217?v=4&s=48" width="48" height="48" alt="TaKO8Ki" title="TaKO8Ki"/></a> <a href="https://github.com/travisp"><img src="https://avatars.githubusercontent.com/u/165698?v=4&s=48" width="48" height="48" alt="travisp" title="travisp"/></a> <a href="https://github.com/rodbland2021"><img src="https://avatars.githubusercontent.com/u/86267410?v=4&s=48" width="48" height="48" alt="rodbland2021" title="rodbland2021"/></a> <a href="https://github.com/fagemx"><img src="https://avatars.githubusercontent.com/u/117356295?v=4&s=48" width="48" height="48" alt="fagemx" title="fagemx"/></a> <a href="https://github.com/BigUncle"><img src="https://avatars.githubusercontent.com/u/9360607?v=4&s=48" width="48" height="48" alt="BigUncle" title="BigUncle"/></a> <a href="https://github.com/pycckuu"><img src="https://avatars.githubusercontent.com/u/1489583?v=4&s=48" width="48" height="48" alt="Igor Markelov" title="Igor Markelov"/></a> <a href="https://github.com/zhoulongchao77"><img src="https://avatars.githubusercontent.com/u/65058500?v=4&s=48" width="48" height="48" alt="zhoulc777" title="zhoulc777"/></a> <a href="https://github.com/connorshea"><img src="https://avatars.githubusercontent.com/u/2977353?v=4&s=48" width="48" height="48" alt="connorshea" title="connorshea"/></a> <a href="https://github.com/paceyw"><img src="https://avatars.githubusercontent.com/u/44923937?v=4&s=48" width="48" height="48" alt="TIHU" title="TIHU"/></a> <a href="https://github.com/tonydehnke"><img src="https://avatars.githubusercontent.com/u/36720180?v=4&s=48" width="48" height="48" alt="Tony Dehnke" title="Tony Dehnke"/></a>
<a href="https://github.com/pablohrcarvalho"><img src="https://avatars.githubusercontent.com/u/66948122?v=4&s=48" width="48" height="48" alt="pablohrcarvalho" title="pablohrcarvalho"/></a> <a href="https://github.com/bonald"><img src="https://avatars.githubusercontent.com/u/12394874?v=4&s=48" width="48" height="48" alt="bonald" title="bonald"/></a> <a href="https://github.com/rhuanssauro"><img src="https://avatars.githubusercontent.com/u/164682191?v=4&s=48" width="48" height="48" alt="rhuanssauro" title="rhuanssauro"/></a> <a href="https://github.com/CommanderCrowCode"><img src="https://avatars.githubusercontent.com/u/72845369?v=4&s=48" width="48" height="48" alt="Tanwa Arpornthip" title="Tanwa Arpornthip"/></a> <a href="https://github.com/webvijayi"><img src="https://avatars.githubusercontent.com/u/49924855?v=4&s=48" width="48" height="48" alt="webvijayi" title="webvijayi"/></a> <a href="https://github.com/tomron87"><img src="https://avatars.githubusercontent.com/u/126325152?v=4&s=48" width="48" height="48" alt="Tom Ron" title="Tom Ron"/></a> <a href="https://github.com/ozbillwang"><img src="https://avatars.githubusercontent.com/u/8954908?v=4&s=48" width="48" height="48" alt="ozbillwang" title="ozbillwang"/></a> <a href="https://github.com/Patrick-Barletta"><img src="https://avatars.githubusercontent.com/u/67929313?v=4&s=48" width="48" height="48" alt="Patrick Barletta" title="Patrick Barletta"/></a> <a href="https://github.com/ianderrington"><img src="https://avatars.githubusercontent.com/u/76016868?v=4&s=48" width="48" height="48" alt="Ian Derrington" title="Ian Derrington"/></a> <a href="https://github.com/austinm911"><img src="https://avatars.githubusercontent.com/u/31991302?v=4&s=48" width="48" height="48" alt="austinm911" title="austinm911"/></a>
<a href="https://github.com/Ayush10"><img src="https://avatars.githubusercontent.com/u/7945279?v=4&s=48" width="48" height="48" alt="Ayush10" title="Ayush10"/></a> <a href="https://github.com/boris721"><img src="https://avatars.githubusercontent.com/u/257853888?v=4&s=48" width="48" height="48" alt="boris721" title="boris721"/></a> <a href="https://github.com/damoahdominic"><img src="https://avatars.githubusercontent.com/u/4623434?v=4&s=48" width="48" height="48" alt="damoahdominic" title="damoahdominic"/></a> <a href="https://github.com/doodlewind"><img src="https://avatars.githubusercontent.com/u/7312949?v=4&s=48" width="48" height="48" alt="doodlewind" title="doodlewind"/></a> <a href="https://github.com/ikari-pl"><img src="https://avatars.githubusercontent.com/u/811702?v=4&s=48" width="48" height="48" alt="ikari-pl" title="ikari-pl"/></a> <a href="https://github.com/philipp-spiess"><img src="https://avatars.githubusercontent.com/u/458591?v=4&s=48" width="48" height="48" alt="philipp-spiess" title="philipp-spiess"/></a> <a href="https://github.com/shayan919293"><img src="https://avatars.githubusercontent.com/u/60409704?v=4&s=48" width="48" height="48" alt="shayan919293" title="shayan919293"/></a> <a href="https://github.com/Harrington-bot"><img src="https://avatars.githubusercontent.com/u/261410808?v=4&s=48" width="48" height="48" alt="Harrington-bot" title="Harrington-bot"/></a> <a href="https://github.com/nonggialiang"><img src="https://avatars.githubusercontent.com/u/14367839?v=4&s=48" width="48" height="48" alt="nonggia.liang" title="nonggia.liang"/></a> <a href="https://github.com/TinyTb"><img src="https://avatars.githubusercontent.com/u/5957298?v=4&s=48" width="48" height="48" alt="Michael Lee" title="Michael Lee"/></a>
<a href="https://github.com/OscarMinjarez"><img src="https://avatars.githubusercontent.com/u/86080038?v=4&s=48" width="48" height="48" alt="OscarMinjarez" title="OscarMinjarez"/></a> <a href="https://github.com/claude"><img src="https://avatars.githubusercontent.com/u/81847?v=4&s=48" width="48" height="48" alt="claude" title="claude"/></a> <a href="https://github.com/Alg0rix"><img src="https://avatars.githubusercontent.com/u/53804949?v=4&s=48" width="48" height="48" alt="Alg0rix" title="Alg0rix"/></a> <a href="https://github.com/L-U-C-K-Y"><img src="https://avatars.githubusercontent.com/u/14868134?v=4&s=48" width="48" height="48" alt="Lucky" title="Lucky"/></a> <a href="https://github.com/Kepler2024"><img src="https://avatars.githubusercontent.com/u/166882517?v=4&s=48" width="48" height="48" alt="Harry Cui Kepler" title="Harry Cui Kepler"/></a> <a href="https://github.com/h0tp-ftw"><img src="https://avatars.githubusercontent.com/u/141889580?v=4&s=48" width="48" height="48" alt="h0tp-ftw" title="h0tp-ftw"/></a> <a href="https://github.com/Youyou972"><img src="https://avatars.githubusercontent.com/u/50808411?v=4&s=48" width="48" height="48" alt="Youyou972" title="Youyou972"/></a> <a href="https://github.com/dominicnunez"><img src="https://avatars.githubusercontent.com/u/43616264?v=4&s=48" width="48" height="48" alt="Dominic" title="Dominic"/></a> <a href="https://github.com/danielwanwx"><img src="https://avatars.githubusercontent.com/u/144515713?v=4&s=48" width="48" height="48" alt="danielwanwx" title="danielwanwx"/></a> <a href="https://github.com/0xJonHoldsCrypto"><img src="https://avatars.githubusercontent.com/u/81202085?v=4&s=48" width="48" height="48" alt="0xJonHoldsCrypto" title="0xJonHoldsCrypto"/></a>
<a href="https://github.com/akyourowngames"><img src="https://avatars.githubusercontent.com/u/123736861?v=4&s=48" width="48" height="48" alt="akyourowngames" title="akyourowngames"/></a> <a href="https://github.com/apps/clawdinator"><img src="https://avatars.githubusercontent.com/in/2607181?v=4&s=48" width="48" height="48" alt="clawdinator[bot]" title="clawdinator[bot]"/></a> <a href="https://github.com/erikpr1994"><img src="https://avatars.githubusercontent.com/u/6299331?v=4&s=48" width="48" height="48" alt="erikpr1994" title="erikpr1994"/></a> <a href="https://github.com/thesash"><img src="https://avatars.githubusercontent.com/u/1166151?v=4&s=48" width="48" height="48" alt="thesash" title="thesash"/></a> <a href="https://github.com/thesomewhatyou"><img src="https://avatars.githubusercontent.com/u/162917831?v=4&s=48" width="48" height="48" alt="thesomewhatyou" title="thesomewhatyou"/></a> <a href="https://github.com/dashed"><img src="https://avatars.githubusercontent.com/u/139499?v=4&s=48" width="48" height="48" alt="dashed" title="dashed"/></a> <a href="https://github.com/minupla"><img src="https://avatars.githubusercontent.com/u/42547246?v=4&s=48" width="48" height="48" alt="Dale Babiy" title="Dale Babiy"/></a> <a href="https://github.com/Diaspar4u"><img src="https://avatars.githubusercontent.com/u/3605840?v=4&s=48" width="48" height="48" alt="Diaspar4u" title="Diaspar4u"/></a> <a href="https://github.com/brianleach"><img src="https://avatars.githubusercontent.com/u/1900805?v=4&s=48" width="48" height="48" alt="brianleach" title="brianleach"/></a> <a href="https://github.com/codexGW"><img src="https://avatars.githubusercontent.com/u/9350182?v=4&s=48" width="48" height="48" alt="codexGW" title="codexGW"/></a>
<a href="https://github.com/dirbalak"><img src="https://avatars.githubusercontent.com/u/30323349?v=4&s=48" width="48" height="48" alt="dirbalak" title="dirbalak"/></a> <a href="https://github.com/Iranb"><img src="https://avatars.githubusercontent.com/u/49674669?v=4&s=48" width="48" height="48" alt="Iranb" title="Iranb"/></a> <a href="https://github.com/rdev"><img src="https://avatars.githubusercontent.com/u/8418866?v=4&s=48" width="48" height="48" alt="Max" title="Max"/></a> <a href="https://github.com/papago2355"><img src="https://avatars.githubusercontent.com/u/68721273?v=4&s=48" width="48" height="48" alt="TideFinder" title="TideFinder"/></a> <a href="https://github.com/cdorsey"><img src="https://avatars.githubusercontent.com/u/12650570?v=4&s=48" width="48" height="48" alt="Chase Dorsey" title="Chase Dorsey"/></a> <a href="https://github.com/Joly0"><img src="https://avatars.githubusercontent.com/u/13993216?v=4&s=48" width="48" height="48" alt="Joly0" title="Joly0"/></a> <a href="https://github.com/adityashaw2"><img src="https://avatars.githubusercontent.com/u/41204444?v=4&s=48" width="48" height="48" alt="adityashaw2" title="adityashaw2"/></a> <a href="https://github.com/tumf"><img src="https://avatars.githubusercontent.com/u/69994?v=4&s=48" width="48" height="48" alt="tumf" title="tumf"/></a> <a href="https://github.com/slonce70"><img src="https://avatars.githubusercontent.com/u/130596182?v=4&s=48" width="48" height="48" alt="slonce70" title="slonce70"/></a> <a href="https://github.com/alexgleason"><img src="https://avatars.githubusercontent.com/u/3639540?v=4&s=48" width="48" height="48" alt="alexgleason" title="alexgleason"/></a>
<a href="https://github.com/theonejvo"><img src="https://avatars.githubusercontent.com/u/125909656?v=4&s=48" width="48" height="48" alt="theonejvo" title="theonejvo"/></a> <a href="https://github.com/adao-max"><img src="https://avatars.githubusercontent.com/u/153898832?v=4&s=48" width="48" height="48" alt="Skyler Miao" title="Skyler Miao"/></a> <a href="https://github.com/jlowin"><img src="https://avatars.githubusercontent.com/u/153965?v=4&s=48" width="48" height="48" alt="Jeremiah Lowin" title="Jeremiah Lowin"/></a> <a href="https://github.com/peetzweg"><img src="https://avatars.githubusercontent.com/u/839848?v=4&s=48" width="48" height="48" alt="peetzweg/" title="peetzweg/"/></a> <a href="https://github.com/chrisrodz"><img src="https://avatars.githubusercontent.com/u/2967620?v=4&s=48" width="48" height="48" alt="chrisrodz" title="chrisrodz"/></a> <a href="https://github.com/ghsmc"><img src="https://avatars.githubusercontent.com/u/68118719?v=4&s=48" width="48" height="48" alt="ghsmc" title="ghsmc"/></a> <a href="https://github.com/ibrahimq21"><img src="https://avatars.githubusercontent.com/u/8392472?v=4&s=48" width="48" height="48" alt="ibrahimq21" title="ibrahimq21"/></a> <a href="https://github.com/irtiq7"><img src="https://avatars.githubusercontent.com/u/3823029?v=4&s=48" width="48" height="48" alt="irtiq7" title="irtiq7"/></a> <a href="https://github.com/jdrhyne"><img src="https://avatars.githubusercontent.com/u/7828464?v=4&s=48" width="48" height="48" alt="Jonathan D. Rhyne (DJ-D)" title="Jonathan D. Rhyne (DJ-D)"/></a> <a href="https://github.com/kelvinCB"><img src="https://avatars.githubusercontent.com/u/50544379?v=4&s=48" width="48" height="48" alt="kelvinCB" title="kelvinCB"/></a>
<a href="https://github.com/mitsuhiko"><img src="https://avatars.githubusercontent.com/u/7396?v=4&s=48" width="48" height="48" alt="mitsuhiko" title="mitsuhiko"/></a> <a href="https://github.com/rybnikov"><img src="https://avatars.githubusercontent.com/u/7761808?v=4&s=48" width="48" height="48" alt="rybnikov" title="rybnikov"/></a> <a href="https://github.com/santiagomed"><img src="https://avatars.githubusercontent.com/u/30184543?v=4&s=48" width="48" height="48" alt="santiagomed" title="santiagomed"/></a> <a href="https://github.com/suminhthanh"><img src="https://avatars.githubusercontent.com/u/2907636?v=4&s=48" width="48" height="48" alt="suminhthanh" title="suminhthanh"/></a> <a href="https://github.com/svkozak"><img src="https://avatars.githubusercontent.com/u/31941359?v=4&s=48" width="48" height="48" alt="svkozak" title="svkozak"/></a> <a href="https://github.com/kaizen403"><img src="https://avatars.githubusercontent.com/u/134706404?v=4&s=48" width="48" height="48" alt="kaizen403" title="kaizen403"/></a> <a href="https://github.com/sleontenko"><img src="https://avatars.githubusercontent.com/u/7135949?v=4&s=48" width="48" height="48" alt="sleontenko" title="sleontenko"/></a> <a href="https://github.com/nk1tz"><img src="https://avatars.githubusercontent.com/u/12980165?v=4&s=48" width="48" height="48" alt="Nate" title="Nate"/></a> <a href="https://github.com/CornBrother0x"><img src="https://avatars.githubusercontent.com/u/101160087?v=4&s=48" width="48" height="48" alt="CornBrother0x" title="CornBrother0x"/></a> <a href="https://github.com/DukeDeSouth"><img src="https://avatars.githubusercontent.com/u/51200688?v=4&s=48" width="48" height="48" alt="DukeDeSouth" title="DukeDeSouth"/></a>
<a href="https://github.com/crimeacs"><img src="https://avatars.githubusercontent.com/u/35071559?v=4&s=48" width="48" height="48" alt="crimeacs" title="crimeacs"/></a> <a href="https://github.com/liebertar"><img src="https://avatars.githubusercontent.com/u/99405438?v=4&s=48" width="48" height="48" alt="Cklee" title="Cklee"/></a> <a href="https://github.com/garnetlyx"><img src="https://avatars.githubusercontent.com/u/12513503?v=4&s=48" width="48" height="48" alt="Garnet Liu" title="Garnet Liu"/></a> <a href="https://github.com/Bermudarat"><img src="https://avatars.githubusercontent.com/u/10937319?v=4&s=48" width="48" height="48" alt="neverland" title="neverland"/></a> <a href="https://github.com/ryancontent"><img src="https://avatars.githubusercontent.com/u/39743613?v=4&s=48" width="48" height="48" alt="ryan" title="ryan"/></a> <a href="https://github.com/sircrumpet"><img src="https://avatars.githubusercontent.com/u/4436535?v=4&s=48" width="48" height="48" alt="sircrumpet" title="sircrumpet"/></a> <a href="https://github.com/AdeboyeDN"><img src="https://avatars.githubusercontent.com/u/65312338?v=4&s=48" width="48" height="48" alt="AdeboyeDN" title="AdeboyeDN"/></a> <a href="https://github.com/neooriginal"><img src="https://avatars.githubusercontent.com/u/54811660?v=4&s=48" width="48" height="48" alt="Neo" title="Neo"/></a> <a href="https://github.com/asklee-klawd"><img src="https://avatars.githubusercontent.com/u/105007315?v=4&s=48" width="48" height="48" alt="asklee-klawd" title="asklee-klawd"/></a> <a href="https://github.com/benediktjohannes"><img src="https://avatars.githubusercontent.com/u/253604130?v=4&s=48" width="48" height="48" alt="benediktjohannes" title="benediktjohannes"/></a>
<a href="https://github.com/zhangzhefang-github"><img src="https://avatars.githubusercontent.com/u/34058239?v=4&s=48" width="48" height="48" alt="张哲芳" title="张哲芳"/></a> <a href="https://github.com/constansino"><img src="https://avatars.githubusercontent.com/u/65108260?v=4&s=48" width="48" height="48" alt="constansino" title="constansino"/></a> <a href="https://github.com/yuting0624"><img src="https://avatars.githubusercontent.com/u/32728916?v=4&s=48" width="48" height="48" alt="Yuting Lin" title="Yuting Lin"/></a> <a href="https://github.com/joelnishanth"><img src="https://avatars.githubusercontent.com/u/140015627?v=4&s=48" width="48" height="48" alt="OfflynAI" title="OfflynAI"/></a> <a href="https://github.com/18-RAJAT"><img src="https://avatars.githubusercontent.com/u/78920780?v=4&s=48" width="48" height="48" alt="Rajat Joshi" title="Rajat Joshi"/></a> <a href="https://github.com/pahdo"><img src="https://avatars.githubusercontent.com/u/12799392?v=4&s=48" width="48" height="48" alt="Daniel Zou" title="Daniel Zou"/></a> <a href="https://github.com/manikv12"><img src="https://avatars.githubusercontent.com/u/49544491?v=4&s=48" width="48" height="48" alt="Manik Vahsith" title="Manik Vahsith"/></a> <a href="https://github.com/ProspectOre"><img src="https://avatars.githubusercontent.com/u/54486432?v=4&s=48" width="48" height="48" alt="ProspectOre" title="ProspectOre"/></a> <a href="https://github.com/detecti1"><img src="https://avatars.githubusercontent.com/u/1622461?v=4&s=48" width="48" height="48" alt="Lilo" title="Lilo"/></a> <a href="https://github.com/24601"><img src="https://avatars.githubusercontent.com/u/1157207?v=4&s=48" width="48" height="48" alt="24601" title="24601"/></a>
<a href="https://github.com/awkoy"><img src="https://avatars.githubusercontent.com/u/13995636?v=4&s=48" width="48" height="48" alt="awkoy" title="awkoy"/></a> <a href="https://github.com/dawondyifraw"><img src="https://avatars.githubusercontent.com/u/9797257?v=4&s=48" width="48" height="48" alt="dawondyifraw" title="dawondyifraw"/></a> <a href="https://github.com/apps/google-labs-jules"><img src="https://avatars.githubusercontent.com/in/842251?v=4&s=48" width="48" height="48" alt="google-labs-jules[bot]" title="google-labs-jules[bot]"/></a> <a href="https://github.com/hyojin"><img src="https://avatars.githubusercontent.com/u/3413183?v=4&s=48" width="48" height="48" alt="hyojin" title="hyojin"/></a> <a href="https://github.com/Kansodata"><img src="https://avatars.githubusercontent.com/u/225288021?v=4&s=48" width="48" height="48" alt="Kansodata" title="Kansodata"/></a> <a href="https://github.com/natedenh"><img src="https://avatars.githubusercontent.com/u/13399956?v=4&s=48" width="48" height="48" alt="natedenh" title="natedenh"/></a> <a href="https://github.com/pi0"><img src="https://avatars.githubusercontent.com/u/5158436?v=4&s=48" width="48" height="48" alt="pi0" title="pi0"/></a> <a href="https://github.com/dddabtc"><img src="https://avatars.githubusercontent.com/u/104875499?v=4&s=48" width="48" height="48" alt="dddabtc" title="dddabtc"/></a> <a href="https://github.com/AkashKobal"><img src="https://avatars.githubusercontent.com/u/98216083?v=4&s=48" width="48" height="48" alt="AkashKobal" title="AkashKobal"/></a> <a href="https://github.com/wu-tian807"><img src="https://avatars.githubusercontent.com/u/61640083?v=4&s=48" width="48" height="48" alt="wu-tian807" title="wu-tian807"/></a>
<a href="https://github.com/kyleok"><img src="https://avatars.githubusercontent.com/u/58307870?v=4&s=48" width="48" height="48" alt="Ganghyun Kim" title="Ganghyun Kim"/></a> <a href="https://github.com/sbking"><img src="https://avatars.githubusercontent.com/u/3913213?v=4&s=48" width="48" height="48" alt="Stephen Brian King" title="Stephen Brian King"/></a> <a href="https://github.com/tosh-hamburg"><img src="https://avatars.githubusercontent.com/u/58424326?v=4&s=48" width="48" height="48" alt="tosh-hamburg" title="tosh-hamburg"/></a> <a href="https://github.com/John-Rood"><img src="https://avatars.githubusercontent.com/u/62669593?v=4&s=48" width="48" height="48" alt="John Rood" title="John Rood"/></a> <a href="https://github.com/divisonofficer"><img src="https://avatars.githubusercontent.com/u/41609506?v=4&s=48" width="48" height="48" alt="JINNYEONG KIM" title="JINNYEONG KIM"/></a> <a href="https://github.com/dinakars777"><img src="https://avatars.githubusercontent.com/u/250428393?v=4&s=48" width="48" height="48" alt="Dinakar Sarbada" title="Dinakar Sarbada"/></a> <a href="https://github.com/aj47"><img src="https://avatars.githubusercontent.com/u/8023513?v=4&s=48" width="48" height="48" alt="aj47" title="aj47"/></a> <a href="https://github.com/Protocol-zero-0"><img src="https://avatars.githubusercontent.com/u/257158451?v=4&s=48" width="48" height="48" alt="Protocol Zero" title="Protocol Zero"/></a> <a href="https://github.com/Limitless2023"><img src="https://avatars.githubusercontent.com/u/127183162?v=4&s=48" width="48" height="48" alt="Limitless" title="Limitless"/></a> <a href="https://github.com/cheeeee"><img src="https://avatars.githubusercontent.com/u/21245729?v=4&s=48" width="48" height="48" alt="Mykyta Bozhenko" title="Mykyta Bozhenko"/></a>
<a href="https://github.com/nicholascyh"><img src="https://avatars.githubusercontent.com/u/188132635?v=4&s=48" width="48" height="48" alt="Nicholas" title="Nicholas"/></a> <a href="https://github.com/shivamraut101"><img src="https://avatars.githubusercontent.com/u/110457469?v=4&s=48" width="48" height="48" alt="Shivam Kumar Raut" title="Shivam Kumar Raut"/></a> <a href="https://github.com/andreesg"><img src="https://avatars.githubusercontent.com/u/810322?v=4&s=48" width="48" height="48" alt="andreesg" title="andreesg"/></a> <a href="https://github.com/fwhite13"><img src="https://avatars.githubusercontent.com/u/173006051?v=4&s=48" width="48" height="48" alt="Fred White" title="Fred White"/></a> <a href="https://github.com/Anandesh-Sharma"><img src="https://avatars.githubusercontent.com/u/30695364?v=4&s=48" width="48" height="48" alt="Anandesh-Sharma" title="Anandesh-Sharma"/></a> <a href="https://github.com/ysqander"><img src="https://avatars.githubusercontent.com/u/80843820?v=4&s=48" width="48" height="48" alt="ysqander" title="ysqander"/></a> <a href="https://github.com/ezhikkk"><img src="https://avatars.githubusercontent.com/u/105670095?v=4&s=48" width="48" height="48" alt="ezhikkk" title="ezhikkk"/></a> <a href="https://github.com/andreabadesso"><img src="https://avatars.githubusercontent.com/u/3586068?v=4&s=48" width="48" height="48" alt="andreabadesso" title="andreabadesso"/></a> <a href="https://github.com/BinaryMuse"><img src="https://avatars.githubusercontent.com/u/189606?v=4&s=48" width="48" height="48" alt="BinaryMuse" title="BinaryMuse"/></a> <a href="https://github.com/cordx56"><img src="https://avatars.githubusercontent.com/u/23298744?v=4&s=48" width="48" height="48" alt="cordx56" title="cordx56"/></a>
<a href="https://github.com/DevSecTim"><img src="https://avatars.githubusercontent.com/u/2226767?v=4&s=48" width="48" height="48" alt="DevSecTim" title="DevSecTim"/></a> <a href="https://github.com/edincampara"><img src="https://avatars.githubusercontent.com/u/142477787?v=4&s=48" width="48" height="48" alt="edincampara" title="edincampara"/></a> <a href="https://github.com/fcatuhe"><img src="https://avatars.githubusercontent.com/u/17382215?v=4&s=48" width="48" height="48" alt="fcatuhe" title="fcatuhe"/></a> <a href="https://github.com/gildo"><img src="https://avatars.githubusercontent.com/u/133645?v=4&s=48" width="48" height="48" alt="gildo" title="gildo"/></a> <a href="https://github.com/itsjaydesu"><img src="https://avatars.githubusercontent.com/u/220390?v=4&s=48" width="48" height="48" alt="itsjaydesu" title="itsjaydesu"/></a> <a href="https://github.com/ivanrvpereira"><img src="https://avatars.githubusercontent.com/u/183991?v=4&s=48" width="48" height="48" alt="ivanrvpereira" title="ivanrvpereira"/></a> <a href="https://github.com/loeclos"><img src="https://avatars.githubusercontent.com/u/116607327?v=4&s=48" width="48" height="48" alt="loeclos" title="loeclos"/></a> <a href="https://github.com/MarvinCui"><img src="https://avatars.githubusercontent.com/u/130876763?v=4&s=48" width="48" height="48" alt="MarvinCui" title="MarvinCui"/></a> <a href="https://github.com/p6l-richard"><img src="https://avatars.githubusercontent.com/u/18185649?v=4&s=48" width="48" height="48" alt="p6l-richard" title="p6l-richard"/></a> <a href="https://github.com/thejhinvirtuoso"><img src="https://avatars.githubusercontent.com/u/258521837?v=4&s=48" width="48" height="48" alt="thejhinvirtuoso" title="thejhinvirtuoso"/></a>
<a href="https://github.com/yudshj"><img src="https://avatars.githubusercontent.com/u/16971372?v=4&s=48" width="48" height="48" alt="yudshj" title="yudshj"/></a> <a href="https://github.com/Wangnov"><img src="https://avatars.githubusercontent.com/u/48670012?v=4&s=48" width="48" height="48" alt="Wangnov" title="Wangnov"/></a> <a href="https://github.com/JonathanWorks"><img src="https://avatars.githubusercontent.com/u/124476234?v=4&s=48" width="48" height="48" alt="Jonathan Works" title="Jonathan Works"/></a> <a href="https://github.com/yassine20011"><img src="https://avatars.githubusercontent.com/u/59234686?v=4&s=48" width="48" height="48" alt="Yassine Amjad" title="Yassine Amjad"/></a> <a href="https://github.com/djangonavarro220"><img src="https://avatars.githubusercontent.com/u/251162586?v=4&s=48" width="48" height="48" alt="Django Navarro" title="Django Navarro"/></a> <a href="https://github.com/hirefrank"><img src="https://avatars.githubusercontent.com/u/183158?v=4&s=48" width="48" height="48" alt="Frank Harris" title="Frank Harris"/></a> <a href="https://github.com/kennyklee"><img src="https://avatars.githubusercontent.com/u/1432489?v=4&s=48" width="48" height="48" alt="Kenny Lee" title="Kenny Lee"/></a> <a href="https://github.com/ThomsenDrake"><img src="https://avatars.githubusercontent.com/u/120344051?v=4&s=48" width="48" height="48" alt="Drake Thomsen" title="Drake Thomsen"/></a> <a href="https://github.com/wangai-studio"><img src="https://avatars.githubusercontent.com/u/256938352?v=4&s=48" width="48" height="48" alt="wangai-studio" title="wangai-studio"/></a> <a href="https://github.com/AytuncYildizli"><img src="https://avatars.githubusercontent.com/u/47717026?v=4&s=48" width="48" height="48" alt="AytuncYildizli" title="AytuncYildizli"/></a>
<a href="https://github.com/KnHack"><img src="https://avatars.githubusercontent.com/u/2346724?v=4&s=48" width="48" height="48" alt="Charlie Niño" title="Charlie Niño"/></a> <a href="https://github.com/17jmumford"><img src="https://avatars.githubusercontent.com/u/36290330?v=4&s=48" width="48" height="48" alt="Jeremy Mumford" title="Jeremy Mumford"/></a> <a href="https://github.com/Yeom-JinHo"><img src="https://avatars.githubusercontent.com/u/81306489?v=4&s=48" width="48" height="48" alt="Yeom-JinHo" title="Yeom-JinHo"/></a> <a href="https://github.com/robaxelsen"><img src="https://avatars.githubusercontent.com/u/13132899?v=4&s=48" width="48" height="48" alt="Rob Axelsen" title="Rob Axelsen"/></a> <a href="https://github.com/junjunjunbong"><img src="https://avatars.githubusercontent.com/u/153147718?v=4&s=48" width="48" height="48" alt="junwon" title="junwon"/></a> <a href="https://github.com/prathamdby"><img src="https://avatars.githubusercontent.com/u/134331217?v=4&s=48" width="48" height="48" alt="Pratham Dubey" title="Pratham Dubey"/></a> <a href="https://github.com/amitbiswal007"><img src="https://avatars.githubusercontent.com/u/108086198?v=4&s=48" width="48" height="48" alt="amitbiswal007" title="amitbiswal007"/></a> <a href="https://github.com/Slats24"><img src="https://avatars.githubusercontent.com/u/42514321?v=4&s=48" width="48" height="48" alt="Slats" title="Slats"/></a> <a href="https://github.com/orenyomtov"><img src="https://avatars.githubusercontent.com/u/168856?v=4&s=48" width="48" height="48" alt="Oren" title="Oren"/></a> <a href="https://github.com/parkertoddbrooks"><img src="https://avatars.githubusercontent.com/u/585456?v=4&s=48" width="48" height="48" alt="Parker Todd Brooks" title="Parker Todd Brooks"/></a>
<a href="https://github.com/mattqdev"><img src="https://avatars.githubusercontent.com/u/115874885?v=4&s=48" width="48" height="48" alt="MattQ" title="MattQ"/></a> <a href="https://github.com/Milofax"><img src="https://avatars.githubusercontent.com/u/2537423?v=4&s=48" width="48" height="48" alt="Milofax" title="Milofax"/></a> <a href="https://github.com/stevebot-alive"><img src="https://avatars.githubusercontent.com/u/261149299?v=4&s=48" width="48" height="48" alt="Steve (OpenClaw)" title="Steve (OpenClaw)"/></a> <a href="https://github.com/ZetiMente"><img src="https://avatars.githubusercontent.com/u/76985631?v=4&s=48" width="48" height="48" alt="Matthew" title="Matthew"/></a> <a href="https://github.com/Cassius0924"><img src="https://avatars.githubusercontent.com/u/62874592?v=4&s=48" width="48" height="48" alt="Cassius0924" title="Cassius0924"/></a> <a href="https://github.com/0xbrak"><img src="https://avatars.githubusercontent.com/u/181251288?v=4&s=48" width="48" height="48" alt="0xbrak" title="0xbrak"/></a> <a href="https://github.com/8BlT"><img src="https://avatars.githubusercontent.com/u/162764392?v=4&s=48" width="48" height="48" alt="8BlT" title="8BlT"/></a> <a href="https://github.com/Abdul535"><img src="https://avatars.githubusercontent.com/u/54276938?v=4&s=48" width="48" height="48" alt="Abdul535" title="Abdul535"/></a> <a href="https://github.com/abhaymundhara"><img src="https://avatars.githubusercontent.com/u/62872231?v=4&s=48" width="48" height="48" alt="abhaymundhara" title="abhaymundhara"/></a> <a href="https://github.com/aduk059"><img src="https://avatars.githubusercontent.com/u/257603478?v=4&s=48" width="48" height="48" alt="aduk059" title="aduk059"/></a>
<a href="https://github.com/afurm"><img src="https://avatars.githubusercontent.com/u/6375192?v=4&s=48" width="48" height="48" alt="afurm" title="afurm"/></a> <a href="https://github.com/aisling404"><img src="https://avatars.githubusercontent.com/u/211950534?v=4&s=48" width="48" height="48" alt="aisling404" title="aisling404"/></a> <a href="https://github.com/akari-musubi"><img src="https://avatars.githubusercontent.com/u/259925157?v=4&s=48" width="48" height="48" alt="akari-musubi" title="akari-musubi"/></a> <a href="https://github.com/albertlieyingadrian"><img src="https://avatars.githubusercontent.com/u/12984659?v=4&s=48" width="48" height="48" alt="albertlieyingadrian" title="albertlieyingadrian"/></a> <a href="https://github.com/Alex-Alaniz"><img src="https://avatars.githubusercontent.com/u/88956822?v=4&s=48" width="48" height="48" alt="Alex-Alaniz" title="Alex-Alaniz"/></a> <a href="https://github.com/ali-aljufairi"><img src="https://avatars.githubusercontent.com/u/85583841?v=4&s=48" width="48" height="48" alt="ali-aljufairi" title="ali-aljufairi"/></a> <a href="https://github.com/altaywtf"><img src="https://avatars.githubusercontent.com/u/9790196?v=4&s=48" width="48" height="48" alt="altaywtf" title="altaywtf"/></a> <a href="https://github.com/araa47"><img src="https://avatars.githubusercontent.com/u/22760261?v=4&s=48" width="48" height="48" alt="araa47" title="araa47"/></a> <a href="https://github.com/Asleep123"><img src="https://avatars.githubusercontent.com/u/122379135?v=4&s=48" width="48" height="48" alt="Asleep123" title="Asleep123"/></a> <a href="https://github.com/avacadobanana352"><img src="https://avatars.githubusercontent.com/u/263496834?v=4&s=48" width="48" height="48" alt="avacadobanana352" title="avacadobanana352"/></a>
<a href="https://github.com/barronlroth"><img src="https://avatars.githubusercontent.com/u/5567884?v=4&s=48" width="48" height="48" alt="barronlroth" title="barronlroth"/></a> <a href="https://github.com/bennewton999"><img src="https://avatars.githubusercontent.com/u/458991?v=4&s=48" width="48" height="48" alt="bennewton999" title="bennewton999"/></a> <a href="https://github.com/bguidolim"><img src="https://avatars.githubusercontent.com/u/987360?v=4&s=48" width="48" height="48" alt="bguidolim" title="bguidolim"/></a> <a href="https://github.com/bigwest60"><img src="https://avatars.githubusercontent.com/u/12373979?v=4&s=48" width="48" height="48" alt="bigwest60" title="bigwest60"/></a> <a href="https://github.com/caelum0x"><img src="https://avatars.githubusercontent.com/u/130079063?v=4&s=48" width="48" height="48" alt="caelum0x" title="caelum0x"/></a> <a href="https://github.com/championswimmer"><img src="https://avatars.githubusercontent.com/u/1327050?v=4&s=48" width="48" height="48" alt="championswimmer" title="championswimmer"/></a> <a href="https://github.com/dutifulbob"><img src="https://avatars.githubusercontent.com/u/261991368?v=4&s=48" width="48" height="48" alt="dutifulbob" title="dutifulbob"/></a> <a href="https://github.com/eternauta1337"><img src="https://avatars.githubusercontent.com/u/550409?v=4&s=48" width="48" height="48" alt="eternauta1337" title="eternauta1337"/></a> <a href="https://github.com/foeken"><img src="https://avatars.githubusercontent.com/u/13864?v=4&s=48" width="48" height="48" alt="foeken" title="foeken"/></a> <a href="https://github.com/gittb"><img src="https://avatars.githubusercontent.com/u/8284364?v=4&s=48" width="48" height="48" alt="gittb" title="gittb"/></a>
<a href="https://github.com/HeimdallStrategy"><img src="https://avatars.githubusercontent.com/u/223014405?v=4&s=48" width="48" height="48" alt="HeimdallStrategy" title="HeimdallStrategy"/></a> <a href="https://github.com/junsuwhy"><img src="https://avatars.githubusercontent.com/u/4645498?v=4&s=48" width="48" height="48" alt="junsuwhy" title="junsuwhy"/></a> <a href="https://github.com/knocte"><img src="https://avatars.githubusercontent.com/u/331303?v=4&s=48" width="48" height="48" alt="knocte" title="knocte"/></a> <a href="https://github.com/MackDing"><img src="https://avatars.githubusercontent.com/u/19878893?v=4&s=48" width="48" height="48" alt="MackDing" title="MackDing"/></a> <a href="https://github.com/nobrainer-tech"><img src="https://avatars.githubusercontent.com/u/445466?v=4&s=48" width="48" height="48" alt="nobrainer-tech" title="nobrainer-tech"/></a> <a href="https://github.com/Noctivoro"><img src="https://avatars.githubusercontent.com/u/183974570?v=4&s=48" width="48" height="48" alt="Noctivoro" title="Noctivoro"/></a> <a href="https://github.com/Raikan10"><img src="https://avatars.githubusercontent.com/u/20675476?v=4&s=48" width="48" height="48" alt="Raikan10" title="Raikan10"/></a> <a href="https://github.com/Swader"><img src="https://avatars.githubusercontent.com/u/1430603?v=4&s=48" width="48" height="48" alt="Swader" title="Swader"/></a> <a href="https://github.com/algal"><img src="https://avatars.githubusercontent.com/u/264412?v=4&s=48" width="48" height="48" alt="Alexis Gallagher" title="Alexis Gallagher"/></a> <a href="https://github.com/alexstyl"><img src="https://avatars.githubusercontent.com/u/1665273?v=4&s=48" width="48" height="48" alt="alexstyl" title="alexstyl"/></a> <a href="https://github.com/ethanpalm"><img src="https://avatars.githubusercontent.com/u/56270045?v=4&s=48" width="48" height="48" alt="Ethan Palm" title="Ethan Palm"/></a>
<a href="https://github.com/yingchunbai"><img src="https://avatars.githubusercontent.com/u/33477283?v=4&s=48" width="48" height="48" alt="yingchunbai" title="yingchunbai"/></a> <a href="https://github.com/joshrad-dev"><img src="https://avatars.githubusercontent.com/u/62785552?v=4&s=48" width="48" height="48" alt="joshrad-dev" title="joshrad-dev"/></a> <a href="https://github.com/danballance"><img src="https://avatars.githubusercontent.com/u/13839912?v=4&s=48" width="48" height="48" alt="Dan Ballance" title="Dan Ballance"/></a> <a href="https://github.com/GHesericsu"><img src="https://avatars.githubusercontent.com/u/60202455?v=4&s=48" width="48" height="48" alt="Eric Su" title="Eric Su"/></a> <a href="https://github.com/kimitaka"><img src="https://avatars.githubusercontent.com/u/167225?v=4&s=48" width="48" height="48" alt="Kimitaka Watanabe" title="Kimitaka Watanabe"/></a> <a href="https://github.com/itsjling"><img src="https://avatars.githubusercontent.com/u/2521993?v=4&s=48" width="48" height="48" alt="Justin Ling" title="Justin Ling"/></a> <a href="https://github.com/lutr0"><img src="https://avatars.githubusercontent.com/u/76906369?v=4&s=48" width="48" height="48" alt="lutr0" title="lutr0"/></a> <a href="https://github.com/RayBB"><img src="https://avatars.githubusercontent.com/u/921217?v=4&s=48" width="48" height="48" alt="Raymond Berger" title="Raymond Berger"/></a> <a href="https://github.com/atalovesyou"><img src="https://avatars.githubusercontent.com/u/3534502?v=4&s=48" width="48" height="48" alt="atalovesyou" title="atalovesyou"/></a> <a href="https://github.com/jayhickey"><img src="https://avatars.githubusercontent.com/u/1676460?v=4&s=48" width="48" height="48" alt="jayhickey" title="jayhickey"/></a>
<a href="https://github.com/jonasjancarik"><img src="https://avatars.githubusercontent.com/u/2459191?v=4&s=48" width="48" height="48" alt="jonasjancarik" title="jonasjancarik"/></a> <a href="https://github.com/latitudeki5223"><img src="https://avatars.githubusercontent.com/u/119656367?v=4&s=48" width="48" height="48" alt="latitudeki5223" title="latitudeki5223"/></a> <a href="https://github.com/minghinmatthewlam"><img src="https://avatars.githubusercontent.com/u/14224566?v=4&s=48" width="48" height="48" alt="minghinmatthewlam" title="minghinmatthewlam"/></a> <a href="https://github.com/rafaelreis-r"><img src="https://avatars.githubusercontent.com/u/57492577?v=4&s=48" width="48" height="48" alt="rafaelreis-r" title="rafaelreis-r"/></a> <a href="https://github.com/ratulsarna"><img src="https://avatars.githubusercontent.com/u/105903728?v=4&s=48" width="48" height="48" alt="ratulsarna" title="ratulsarna"/></a> <a href="https://github.com/timkrase"><img src="https://avatars.githubusercontent.com/u/38947626?v=4&s=48" width="48" height="48" alt="timkrase" title="timkrase"/></a> <a href="https://github.com/efe-buken"><img src="https://avatars.githubusercontent.com/u/262546946?v=4&s=48" width="48" height="48" alt="efe-buken" title="efe-buken"/></a> <a href="https://github.com/manmal"><img src="https://avatars.githubusercontent.com/u/142797?v=4&s=48" width="48" height="48" alt="manmal" title="manmal"/></a> <a href="https://github.com/easternbloc"><img src="https://avatars.githubusercontent.com/u/92585?v=4&s=48" width="48" height="48" alt="easternbloc" title="easternbloc"/></a> <a href="https://github.com/ManuelHettich"><img src="https://avatars.githubusercontent.com/u/17690367?v=4&s=48" width="48" height="48" alt="manuelhettich" title="manuelhettich"/></a>
<a href="https://github.com/sktbrd"><img src="https://avatars.githubusercontent.com/u/116202536?v=4&s=48" width="48" height="48" alt="sktbrd" title="sktbrd"/></a> <a href="https://github.com/larlyssa"><img src="https://avatars.githubusercontent.com/u/13128869?v=4&s=48" width="48" height="48" alt="larlyssa" title="larlyssa"/></a> <a href="https://github.com/Mind-Dragon"><img src="https://avatars.githubusercontent.com/u/262945885?v=4&s=48" width="48" height="48" alt="Mind-Dragon" title="Mind-Dragon"/></a> <a href="https://github.com/pcty-nextgen-service-account"><img src="https://avatars.githubusercontent.com/u/112553441?v=4&s=48" width="48" height="48" alt="pcty-nextgen-service-account" title="pcty-nextgen-service-account"/></a> <a href="https://github.com/tmchow"><img src="https://avatars.githubusercontent.com/u/517103?v=4&s=48" width="48" height="48" alt="tmchow" title="tmchow"/></a> <a href="https://github.com/uli-will-code"><img src="https://avatars.githubusercontent.com/u/49715419?v=4&s=48" width="48" height="48" alt="uli-will-code" title="uli-will-code"/></a> <a href="https://github.com/mgratch"><img src="https://avatars.githubusercontent.com/u/2238658?v=4&s=48" width="48" height="48" alt="Marc Gratch" title="Marc Gratch"/></a> <a href="https://github.com/JackyWay"><img src="https://avatars.githubusercontent.com/u/53031570?v=4&s=48" width="48" height="48" alt="JackyWay" title="JackyWay"/></a> <a href="https://github.com/aaronveklabs"><img src="https://avatars.githubusercontent.com/u/225997828?v=4&s=48" width="48" height="48" alt="aaronveklabs" title="aaronveklabs"/></a> <a href="https://github.com/CJWTRUST"><img src="https://avatars.githubusercontent.com/u/235565898?v=4&s=48" width="48" height="48" alt="CJWTRUST" title="CJWTRUST"/></a>
<a href="https://github.com/erik-agens"><img src="https://avatars.githubusercontent.com/u/80908960?v=4&s=48" width="48" height="48" alt="erik-agens" title="erik-agens"/></a> <a href="https://github.com/odnxe"><img src="https://avatars.githubusercontent.com/u/403141?v=4&s=48" width="48" height="48" alt="odnxe" title="odnxe"/></a> <a href="https://github.com/T5-AndyML"><img src="https://avatars.githubusercontent.com/u/22801233?v=4&s=48" width="48" height="48" alt="T5-AndyML" title="T5-AndyML"/></a> <a href="https://github.com/j1philli"><img src="https://avatars.githubusercontent.com/u/3744255?v=4&s=48" width="48" height="48" alt="Josh Phillips" title="Josh Phillips"/></a> <a href="https://github.com/mujiannan"><img src="https://avatars.githubusercontent.com/u/46643837?v=4&s=48" width="48" height="48" alt="mujiannan" title="mujiannan"/></a> <a href="https://github.com/marcodd23"><img src="https://avatars.githubusercontent.com/u/3519682?v=4&s=48" width="48" height="48" alt="Marco Di Dionisio" title="Marco Di Dionisio"/></a> <a href="https://github.com/RandyVentures"><img src="https://avatars.githubusercontent.com/u/149904821?v=4&s=48" width="48" height="48" alt="Randy Torres" title="Randy Torres"/></a> <a href="https://github.com/afern247"><img src="https://avatars.githubusercontent.com/u/34192856?v=4&s=48" width="48" height="48" alt="afern247" title="afern247"/></a> <a href="https://github.com/0oAstro"><img src="https://avatars.githubusercontent.com/u/79555780?v=4&s=48" width="48" height="48" alt="0oAstro" title="0oAstro"/></a> <a href="https://github.com/alexanderatallah"><img src="https://avatars.githubusercontent.com/u/1011391?v=4&s=48" width="48" height="48" alt="alexanderatallah" title="alexanderatallah"/></a>
<a href="https://github.com/testingabc321"><img src="https://avatars.githubusercontent.com/u/8577388?v=4&s=48" width="48" height="48" alt="testingabc321" title="testingabc321"/></a> <a href="https://github.com/humanwritten"><img src="https://avatars.githubusercontent.com/u/206531610?v=4&s=48" width="48" height="48" alt="humanwritten" title="humanwritten"/></a> <a href="https://github.com/aaronn"><img src="https://avatars.githubusercontent.com/u/1653630?v=4&s=48" width="48" height="48" alt="aaronn" title="aaronn"/></a> <a href="https://github.com/Alphonse-arianee"><img src="https://avatars.githubusercontent.com/u/254457365?v=4&s=48" width="48" height="48" alt="Alphonse-arianee" title="Alphonse-arianee"/></a> <a href="https://github.com/gtsifrikas"><img src="https://avatars.githubusercontent.com/u/8904378?v=4&s=48" width="48" height="48" alt="gtsifrikas" title="gtsifrikas"/></a> <a href="https://github.com/hrdwdmrbl"><img src="https://avatars.githubusercontent.com/u/554881?v=4&s=48" width="48" height="48" alt="hrdwdmrbl" title="hrdwdmrbl"/></a> <a href="https://github.com/hugobarauna"><img src="https://avatars.githubusercontent.com/u/2719?v=4&s=48" width="48" height="48" alt="hugobarauna" title="hugobarauna"/></a> <a href="https://github.com/jiulingyun"><img src="https://avatars.githubusercontent.com/u/126459548?v=4&s=48" width="48" height="48" alt="jiulingyun" title="jiulingyun"/></a> <a href="https://github.com/kitze"><img src="https://avatars.githubusercontent.com/u/1160594?v=4&s=48" width="48" height="48" alt="kitze" title="kitze"/></a> <a href="https://github.com/loukotal"><img src="https://avatars.githubusercontent.com/u/18210858?v=4&s=48" width="48" height="48" alt="loukotal" title="loukotal"/></a>
<a href="https://github.com/MSch"><img src="https://avatars.githubusercontent.com/u/7475?v=4&s=48" width="48" height="48" alt="MSch" title="MSch"/></a> <a href="https://github.com/odrobnik"><img src="https://avatars.githubusercontent.com/u/333270?v=4&s=48" width="48" height="48" alt="odrobnik" title="odrobnik"/></a> <a href="https://github.com/reeltimeapps"><img src="https://avatars.githubusercontent.com/u/637338?v=4&s=48" width="48" height="48" alt="reeltimeapps" title="reeltimeapps"/></a> <a href="https://github.com/rhjoh"><img src="https://avatars.githubusercontent.com/u/105699450?v=4&s=48" width="48" height="48" alt="rhjoh" title="rhjoh"/></a> <a href="https://github.com/ronak-guliani"><img src="https://avatars.githubusercontent.com/u/23518228?v=4&s=48" width="48" height="48" alt="ronak-guliani" title="ronak-guliani"/></a> <a href="https://github.com/snopoke"><img src="https://avatars.githubusercontent.com/u/249606?v=4&s=48" width="48" height="48" alt="snopoke" title="snopoke"/></a>
</p>
<!-- clawtributors:start -->
[![steipete](https://avatars.githubusercontent.com/u/58493?v=4&s=48)](https://github.com/steipete) [![vincentkoc](https://avatars.githubusercontent.com/u/25068?v=4&s=48)](https://github.com/vincentkoc) [![Takhoffman](https://avatars.githubusercontent.com/u/781889?v=4&s=48)](https://github.com/Takhoffman) [![obviyus](https://avatars.githubusercontent.com/u/22031114?v=4&s=48)](https://github.com/obviyus) [![gumadeiras](https://avatars.githubusercontent.com/u/5599352?v=4&s=48)](https://github.com/gumadeiras) [![Mariano Belinky](https://avatars.githubusercontent.com/u/132747814?v=4&s=48)](https://github.com/mbelinky) [![vignesh07](https://avatars.githubusercontent.com/u/1436853?v=4&s=48)](https://github.com/vignesh07) [![joshavant](https://avatars.githubusercontent.com/u/830519?v=4&s=48)](https://github.com/joshavant) [![scoootscooob](https://avatars.githubusercontent.com/u/167050519?v=4&s=48)](https://github.com/scoootscooob) [![jacobtomlinson](https://avatars.githubusercontent.com/u/1610850?v=4&s=48)](https://github.com/jacobtomlinson)
[![shakkernerd](https://avatars.githubusercontent.com/u/165377636?v=4&s=48)](https://github.com/shakkernerd) [![sebslight](https://avatars.githubusercontent.com/u/19554889?v=4&s=48)](https://github.com/sebslight) [![tyler6204](https://avatars.githubusercontent.com/u/64381258?v=4&s=48)](https://github.com/tyler6204) [![ngutman](https://avatars.githubusercontent.com/u/1540134?v=4&s=48)](https://github.com/ngutman) [![thewilloftheshadow](https://avatars.githubusercontent.com/u/35580099?v=4&s=48)](https://github.com/thewilloftheshadow) [![Sid-Qin](https://avatars.githubusercontent.com/u/201593046?v=4&s=48)](https://github.com/Sid-Qin) [![mcaxtr](https://avatars.githubusercontent.com/u/7562095?v=4&s=48)](https://github.com/mcaxtr) [![eleqtrizit](https://avatars.githubusercontent.com/u/31522568?v=4&s=48)](https://github.com/eleqtrizit) [![BunsDev](https://avatars.githubusercontent.com/u/68980965?v=4&s=48)](https://github.com/BunsDev) [![cpojer](https://avatars.githubusercontent.com/u/13352?v=4&s=48)](https://github.com/cpojer)
[![Glucksberg](https://avatars.githubusercontent.com/u/80581902?v=4&s=48)](https://github.com/Glucksberg) [![osolmaz](https://avatars.githubusercontent.com/u/2453968?v=4&s=48)](https://github.com/osolmaz) [![bmendonca3](https://avatars.githubusercontent.com/u/208517100?v=4&s=48)](https://github.com/bmendonca3) [![jalehman](https://avatars.githubusercontent.com/u/550978?v=4&s=48)](https://github.com/jalehman) [![huntharo](https://avatars.githubusercontent.com/u/5617868?v=4&s=48)](https://github.com/huntharo) [![neeravmakwana](https://avatars.githubusercontent.com/u/261249544?v=4&s=48)](https://github.com/neeravmakwana) [![openperf](https://avatars.githubusercontent.com/u/80630709?v=4&s=48)](https://github.com/openperf) [![joshp123](https://avatars.githubusercontent.com/u/1497361?v=4&s=48)](https://github.com/joshp123) [![pgondhi987](https://avatars.githubusercontent.com/u/270720687?v=4&s=48)](https://github.com/pgondhi987) [![altaywtf](https://avatars.githubusercontent.com/u/9790196?v=4&s=48)](https://github.com/altaywtf)
[![quotentiroler](https://avatars.githubusercontent.com/u/40643627?v=4&s=48)](https://github.com/quotentiroler) [![liuxiaopai-ai](https://avatars.githubusercontent.com/u/73659136?v=4&s=48)](https://github.com/liuxiaopai-ai) [![rodrigouroz](https://avatars.githubusercontent.com/u/384037?v=4&s=48)](https://github.com/rodrigouroz) [![frankekn](https://avatars.githubusercontent.com/u/4488090?v=4&s=48)](https://github.com/frankekn) [![drobison00](https://avatars.githubusercontent.com/u/5256797?v=4&s=48)](https://github.com/drobison00) [![zerone0x](https://avatars.githubusercontent.com/u/39543393?v=4&s=48)](https://github.com/zerone0x) [![onutc](https://avatars.githubusercontent.com/u/152018508?v=4&s=48)](https://github.com/onutc) [![ademczuk](https://avatars.githubusercontent.com/u/5212682?v=4&s=48)](https://github.com/ademczuk) [![ImLukeF](https://avatars.githubusercontent.com/u/92253590?v=4&s=48)](https://github.com/ImLukeF) [![hydro13](https://avatars.githubusercontent.com/u/6640526?v=4&s=48)](https://github.com/hydro13)
[![hxy91819](https://avatars.githubusercontent.com/u/8814856?v=4&s=48)](https://github.com/hxy91819) [![coygeek](https://avatars.githubusercontent.com/u/65363919?v=4&s=48)](https://github.com/coygeek) [![dutifulbob](https://avatars.githubusercontent.com/u/261991368?v=4&s=48)](https://github.com/dutifulbob) [![sliverp](https://avatars.githubusercontent.com/u/38134380?v=4&s=48)](https://github.com/sliverp) [![Elonito](https://avatars.githubusercontent.com/u/190923101?v=4&s=48)](https://github.com/0xRaini) [![robbyczgw-cla](https://avatars.githubusercontent.com/u/239660374?v=4&s=48)](https://github.com/robbyczgw-cla) [![joelnishanth](https://avatars.githubusercontent.com/u/140015627?v=4&s=48)](https://github.com/joelnishanth) [![echoVic](https://avatars.githubusercontent.com/u/16428813?v=4&s=48)](https://github.com/echoVic) [![sallyom](https://avatars.githubusercontent.com/u/11166065?v=4&s=48)](https://github.com/sallyom) [![yinghaosang](https://avatars.githubusercontent.com/u/261132136?v=4&s=48)](https://github.com/yinghaosang)
[![BradGroux](https://avatars.githubusercontent.com/u/3053586?v=4&s=48)](https://github.com/BradGroux) [![christianklotz](https://avatars.githubusercontent.com/u/69443?v=4&s=48)](https://github.com/christianklotz) [![odysseus0](https://avatars.githubusercontent.com/u/8635094?v=4&s=48)](https://github.com/odysseus0) [![hclsys](https://avatars.githubusercontent.com/u/7755017?v=4&s=48)](https://github.com/hclsys) [![byungsker](https://avatars.githubusercontent.com/u/72309817?v=4&s=48)](https://github.com/byungsker) [![pashpashpash](https://avatars.githubusercontent.com/u/20898225?v=4&s=48)](https://github.com/pashpashpash) [![stakeswky](https://avatars.githubusercontent.com/u/64798754?v=4&s=48)](https://github.com/stakeswky) [![github-actions[bot]](https://avatars.githubusercontent.com/in/15368?v=4&s=48)](https://github.com/apps/github-actions) [![xinhuagu](https://avatars.githubusercontent.com/u/562450?v=4&s=48)](https://github.com/xinhuagu) [![MonkeyLeeT](https://avatars.githubusercontent.com/u/6754057?v=4&s=48)](https://github.com/MonkeyLeeT)
[![100yenadmin](https://avatars.githubusercontent.com/u/239388517?v=4&s=48)](https://github.com/100yenadmin) [![mcinteerj](https://avatars.githubusercontent.com/u/3613653?v=4&s=48)](https://github.com/mcinteerj) [![samzong](https://avatars.githubusercontent.com/u/13782141?v=4&s=48)](https://github.com/samzong) [![chilu18](https://avatars.githubusercontent.com/u/7957943?v=4&s=48)](https://github.com/chilu18) [![darkamenosa](https://avatars.githubusercontent.com/u/6668014?v=4&s=48)](https://github.com/darkamenosa) [![widingmarcus-cyber](https://avatars.githubusercontent.com/u/245375637?v=4&s=48)](https://github.com/widingmarcus-cyber) [![cgdusek](https://avatars.githubusercontent.com/u/38732970?v=4&s=48)](https://github.com/cgdusek) [![Lukavyi](https://avatars.githubusercontent.com/u/1013690?v=4&s=48)](https://github.com/Lukavyi) [![davidrudduck](https://avatars.githubusercontent.com/u/47308254?v=4&s=48)](https://github.com/davidrudduck) [![VACInc](https://avatars.githubusercontent.com/u/3279061?v=4&s=48)](https://github.com/VACInc)
[![MoerAI](https://avatars.githubusercontent.com/u/26067127?v=4&s=48)](https://github.com/MoerAI) [![velvet-shark](https://avatars.githubusercontent.com/u/126378?v=4&s=48)](https://github.com/velvet-shark) [![HenryLoenwind](https://avatars.githubusercontent.com/u/1485873?v=4&s=48)](https://github.com/HenryLoenwind) [![omarshahine](https://avatars.githubusercontent.com/u/10343873?v=4&s=48)](https://github.com/omarshahine) [![bohdanpodvirnyi](https://avatars.githubusercontent.com/u/31819391?v=4&s=48)](https://github.com/bohdanpodvirnyi) [![Verite Igiraneza](https://avatars.githubusercontent.com/u/69280208?v=4&s=48)](https://github.com/VeriteIgiraneza) [![akramcodez](https://avatars.githubusercontent.com/u/179671552?v=4&s=48)](https://github.com/akramcodez) [![Kaneki-x](https://avatars.githubusercontent.com/u/6857108?v=4&s=48)](https://github.com/Kaneki-x) [![aether-ai-agent](https://avatars.githubusercontent.com/u/261339948?v=4&s=48)](https://github.com/aether-ai-agent) [![joaohlisboa](https://avatars.githubusercontent.com/u/8200873?v=4&s=48)](https://github.com/joaohlisboa)
[![MaudeBot](https://avatars.githubusercontent.com/u/255777700?v=4&s=48)](https://github.com/MaudeBot) [![davidguttman](https://avatars.githubusercontent.com/u/431696?v=4&s=48)](https://github.com/davidguttman) [![justinhuangcode](https://avatars.githubusercontent.com/u/252443740?v=4&s=48)](https://github.com/justinhuangcode) [![lml2468](https://avatars.githubusercontent.com/u/39320777?v=4&s=48)](https://github.com/lml2468) [![wirjo](https://avatars.githubusercontent.com/u/165846?v=4&s=48)](https://github.com/wirjo) [![iHildy](https://avatars.githubusercontent.com/u/25069719?v=4&s=48)](https://github.com/iHildy) [![mudrii](https://avatars.githubusercontent.com/u/220262?v=4&s=48)](https://github.com/mudrii) [![advaitpaliwal](https://avatars.githubusercontent.com/u/66044327?v=4&s=48)](https://github.com/advaitpaliwal) [![czekaj](https://avatars.githubusercontent.com/u/1464539?v=4&s=48)](https://github.com/czekaj) [![dlauer](https://avatars.githubusercontent.com/u/757041?v=4&s=48)](https://github.com/dlauer)
[![Solvely-Colin](https://avatars.githubusercontent.com/u/211764741?v=4&s=48)](https://github.com/Solvely-Colin) [![feiskyer](https://avatars.githubusercontent.com/u/676637?v=4&s=48)](https://github.com/feiskyer) [![brandonwise](https://avatars.githubusercontent.com/u/21148772?v=4&s=48)](https://github.com/brandonwise) [![conroywhitney](https://avatars.githubusercontent.com/u/249891?v=4&s=48)](https://github.com/conroywhitney) [![mneves75](https://avatars.githubusercontent.com/u/2423436?v=4&s=48)](https://github.com/mneves75) [![jaydenfyi](https://avatars.githubusercontent.com/u/213395523?v=4&s=48)](https://github.com/jaydenfyi) [![davemorin](https://avatars.githubusercontent.com/u/78139?v=4&s=48)](https://github.com/davemorin) [![joeykrug](https://avatars.githubusercontent.com/u/5925937?v=4&s=48)](https://github.com/joeykrug) [![kevinWangSheng](https://avatars.githubusercontent.com/u/118158941?v=4&s=48)](https://github.com/kevinWangSheng) [![pejmanjohn](https://avatars.githubusercontent.com/u/481729?v=4&s=48)](https://github.com/pejmanjohn)
[![Lanfei](https://avatars.githubusercontent.com/u/2156642?v=4&s=48)](https://github.com/Lanfei) [![liuy](https://avatars.githubusercontent.com/u/1192888?v=4&s=48)](https://github.com/liuy) [![lc0rp](https://avatars.githubusercontent.com/u/2609441?v=4&s=48)](https://github.com/lc0rp) [![teconomix](https://avatars.githubusercontent.com/u/6959299?v=4&s=48)](https://github.com/teconomix) [![omair445](https://avatars.githubusercontent.com/u/32237905?v=4&s=48)](https://github.com/omair445) [![dorukardahan](https://avatars.githubusercontent.com/u/35905596?v=4&s=48)](https://github.com/dorukardahan) [![mmaps](https://avatars.githubusercontent.com/u/3399869?v=4&s=48)](https://github.com/mmaps) [![Tobias Bischoff](https://avatars.githubusercontent.com/u/711564?v=4&s=48)](https://github.com/tobiasbischoff) [![adhitShet](https://avatars.githubusercontent.com/u/131381638?v=4&s=48)](https://github.com/adhitShet) [![pandego](https://avatars.githubusercontent.com/u/7780875?v=4&s=48)](https://github.com/pandego)
[![bradleypriest](https://avatars.githubusercontent.com/u/167215?v=4&s=48)](https://github.com/bradleypriest) [![bjesuiter](https://avatars.githubusercontent.com/u/2365676?v=4&s=48)](https://github.com/bjesuiter) [![grp06](https://avatars.githubusercontent.com/u/1573959?v=4&s=48)](https://github.com/grp06) [![shadril238](https://avatars.githubusercontent.com/u/63901551?v=4&s=48)](https://github.com/shadril238) [![kesku](https://avatars.githubusercontent.com/u/62210496?v=4&s=48)](https://github.com/kesku) [![YuriNachos](https://avatars.githubusercontent.com/u/19365375?v=4&s=48)](https://github.com/YuriNachos) [![vrknetha](https://avatars.githubusercontent.com/u/20596261?v=4&s=48)](https://github.com/vrknetha) [![smartprogrammer93](https://avatars.githubusercontent.com/u/33181301?v=4&s=48)](https://github.com/smartprogrammer93) [![nachx639](https://avatars.githubusercontent.com/u/71144023?v=4&s=48)](https://github.com/Nachx639) [![jnMetaCode](https://avatars.githubusercontent.com/u/12096460?v=4&s=48)](https://github.com/jnMetaCode)
[![Phineas1500](https://avatars.githubusercontent.com/u/41450967?v=4&s=48)](https://github.com/Phineas1500) [![dingn42](https://avatars.githubusercontent.com/u/17723822?v=4&s=48)](https://github.com/dingn42) [![geekhuashan](https://avatars.githubusercontent.com/u/47098938?v=4&s=48)](https://github.com/geekhuashan) [![Nanako0129](https://avatars.githubusercontent.com/u/44753291?v=4&s=48)](https://github.com/Nanako0129) [![AytuncYildizli](https://avatars.githubusercontent.com/u/47717026?v=4&s=48)](https://github.com/AytuncYildizli) [![BruceMacD](https://avatars.githubusercontent.com/u/5853428?v=4&s=48)](https://github.com/BruceMacD) [![jjjojoj](https://avatars.githubusercontent.com/u/88077783?v=4&s=48)](https://github.com/jjjojoj) [![mvanhorn](https://avatars.githubusercontent.com/u/455140?v=4&s=48)](https://github.com/mvanhorn) [![bugkill3r](https://avatars.githubusercontent.com/u/2924124?v=4&s=48)](https://github.com/bugkill3r) [![rahthakor](https://avatars.githubusercontent.com/u/8470553?v=4&s=48)](https://github.com/rahthakor)
[![GodsBoy](https://avatars.githubusercontent.com/u/5792287?v=4&s=48)](https://github.com/GodsBoy) [![SARAMALI15792](https://avatars.githubusercontent.com/u/140950904?v=4&s=48)](https://github.com/SARAMALI15792) [![Radek Paclt](https://avatars.githubusercontent.com/u/50451445?v=4&s=48)](https://github.com/radek-paclt) [![Elarwei001](https://avatars.githubusercontent.com/u/168552401?v=4&s=48)](https://github.com/Elarwei001) [![ingyukoh](https://avatars.githubusercontent.com/u/6015960?v=4&s=48)](https://github.com/ingyukoh) [![SnowSky1](https://avatars.githubusercontent.com/u/126348592?v=4&s=48)](https://github.com/SnowSky1) [![lewiswigmore](https://avatars.githubusercontent.com/u/58551848?v=4&s=48)](https://github.com/lewiswigmore) [![Hiroshi Tanaka](https://avatars.githubusercontent.com/u/145330217?v=4&s=48)](https://github.com/solavrc) [![aldoeliacim](https://avatars.githubusercontent.com/u/17973757?v=4&s=48)](https://github.com/aldoeliacim) [![Jakub Rusz](https://avatars.githubusercontent.com/u/55534579?v=4&s=48)](https://github.com/jrusz)
[![Tony Dehnke](https://avatars.githubusercontent.com/u/36720180?v=4&s=48)](https://github.com/tonydehnke) [![roshanasingh4](https://avatars.githubusercontent.com/u/88576930?v=4&s=48)](https://github.com/roshanasingh4) [![zssggle-rgb](https://avatars.githubusercontent.com/u/226775494?v=4&s=48)](https://github.com/zssggle-rgb) [![adam91holt](https://avatars.githubusercontent.com/u/9592417?v=4&s=48)](https://github.com/adam91holt) [![graysurf](https://avatars.githubusercontent.com/u/10785178?v=4&s=48)](https://github.com/graysurf) [![xadenryan](https://avatars.githubusercontent.com/u/165437834?v=4&s=48)](https://github.com/xadenryan) [![sfo2001](https://avatars.githubusercontent.com/u/103369858?v=4&s=48)](https://github.com/sfo2001) [![Jamieson O'Reilly](https://avatars.githubusercontent.com/u/6668807?v=4&s=48)](https://github.com/orlyjamie) [![hsrvc](https://avatars.githubusercontent.com/u/129702169?v=4&s=48)](https://github.com/hsrvc) [![tomsun28](https://avatars.githubusercontent.com/u/24788200?v=4&s=48)](https://github.com/tomsun28)
[![BillChirico](https://avatars.githubusercontent.com/u/13951316?v=4&s=48)](https://github.com/BillChirico) [![carrotRakko](https://avatars.githubusercontent.com/u/24588751?v=4&s=48)](https://github.com/carrotRakko) [![ranausmanai](https://avatars.githubusercontent.com/u/257128159?v=4&s=48)](https://github.com/ranausmanai) [![arkyu2077](https://avatars.githubusercontent.com/u/42494191?v=4&s=48)](https://github.com/arkyu2077) [![hoyyeva](https://avatars.githubusercontent.com/u/63033505?v=4&s=48)](https://github.com/hoyyeva) [![luoyanglang](https://avatars.githubusercontent.com/u/238804951?v=4&s=48)](https://github.com/luoyanglang) [![sibbl](https://avatars.githubusercontent.com/u/866535?v=4&s=48)](https://github.com/sibbl) [![gregmousseau](https://avatars.githubusercontent.com/u/5036458?v=4&s=48)](https://github.com/gregmousseau) [![sahilsatralkar](https://avatars.githubusercontent.com/u/62758655?v=4&s=48)](https://github.com/sahilsatralkar) [![akoscz](https://avatars.githubusercontent.com/u/1360047?v=4&s=48)](https://github.com/akoscz)
[![rrenamed](https://avatars.githubusercontent.com/u/87486610?v=4&s=48)](https://github.com/rrenamed) [![YuzuruS](https://avatars.githubusercontent.com/u/1485195?v=4&s=48)](https://github.com/YuzuruS) [![Hongwei Ma](https://avatars.githubusercontent.com/u/11957602?v=4&s=48)](https://github.com/Marvae) [![mitchmcalister](https://avatars.githubusercontent.com/u/209334?v=4&s=48)](https://github.com/mitchmcalister) [![juanpablodlc](https://avatars.githubusercontent.com/u/92012363?v=4&s=48)](https://github.com/juanpablodlc) [![shtse8](https://avatars.githubusercontent.com/u/8020099?v=4&s=48)](https://github.com/shtse8) [![thebenignhacker](https://avatars.githubusercontent.com/u/32418586?v=4&s=48)](https://github.com/thebenignhacker) [![nimbleenigma](https://avatars.githubusercontent.com/u/129692390?v=4&s=48)](https://github.com/nimbleenigma) [![Linux2010](https://avatars.githubusercontent.com/u/35169750?v=4&s=48)](https://github.com/Linux2010) [![shichangs](https://avatars.githubusercontent.com/u/46870204?v=4&s=48)](https://github.com/shichangs)
[![efe-arv](https://avatars.githubusercontent.com/u/259833796?v=4&s=48)](https://github.com/efe-arv) [![Hsiao A](https://avatars.githubusercontent.com/u/70124331?v=4&s=48)](https://github.com/hsiaoa) [![nabbilkhan](https://avatars.githubusercontent.com/u/203121263?v=4&s=48)](https://github.com/nabbilkhan) [![ayanesakura](https://avatars.githubusercontent.com/u/40628300?v=4&s=48)](https://github.com/ayanesakura) [![lupuletic](https://avatars.githubusercontent.com/u/105351510?v=4&s=48)](https://github.com/lupuletic) [![polooooo](https://avatars.githubusercontent.com/u/50262693?v=4&s=48)](https://github.com/polooooo) [![xaeon2026](https://avatars.githubusercontent.com/u/264572156?v=4&s=48)](https://github.com/xaeon2026) [![shrey150](https://avatars.githubusercontent.com/u/3813908?v=4&s=48)](https://github.com/shrey150) [![taw0002](https://avatars.githubusercontent.com/u/42811278?v=4&s=48)](https://github.com/taw0002) [![dinakars777](https://avatars.githubusercontent.com/u/250428393?v=4&s=48)](https://github.com/dinakars777)
[![giulio-leone](https://avatars.githubusercontent.com/u/6887247?v=4&s=48)](https://github.com/giulio-leone) [![nyanjou](https://avatars.githubusercontent.com/u/258645604?v=4&s=48)](https://github.com/nyanjou) [![meaningfool](https://avatars.githubusercontent.com/u/2862331?v=4&s=48)](https://github.com/meaningfool) [![kunalk16](https://avatars.githubusercontent.com/u/5303824?v=4&s=48)](https://github.com/kunalk16) [![ide-rea](https://avatars.githubusercontent.com/u/30512600?v=4&s=48)](https://github.com/ide-rea) [![Jonathan Jing](https://avatars.githubusercontent.com/u/17068507?v=4&s=48)](https://github.com/JonathanJing) [![yelog](https://avatars.githubusercontent.com/u/14227866?v=4&s=48)](https://github.com/yelog) [![markmusson](https://avatars.githubusercontent.com/u/4801649?v=4&s=48)](https://github.com/markmusson) [![kiranvk-2011](https://avatars.githubusercontent.com/u/91108465?v=4&s=48)](https://github.com/kiranvk-2011) [![Sathvik Veerapaneni](https://avatars.githubusercontent.com/u/98241593?v=4&s=48)](https://github.com/Sathvik-Chowdary-Veerapaneni)
[![rogerdigital](https://avatars.githubusercontent.com/u/13251150?v=4&s=48)](https://github.com/rogerdigital) [![artwalker](https://avatars.githubusercontent.com/u/44759507?v=4&s=48)](https://github.com/artwalker) [![azade-c](https://avatars.githubusercontent.com/u/252790079?v=4&s=48)](https://github.com/azade-c) [![chinar-amrutkar](https://avatars.githubusercontent.com/u/22189135?v=4&s=48)](https://github.com/chinar-amrutkar) [![maxsumrall](https://avatars.githubusercontent.com/u/628843?v=4&s=48)](https://github.com/maxsumrall) [![Minidoracat](https://avatars.githubusercontent.com/u/11269639?v=4&s=48)](https://github.com/Minidoracat) [![unisone](https://avatars.githubusercontent.com/u/32521398?v=4&s=48)](https://github.com/unisone) [![ly85206559](https://avatars.githubusercontent.com/u/12526624?v=4&s=48)](https://github.com/ly85206559) [![Sam Padilla](https://avatars.githubusercontent.com/u/35386211?v=4&s=48)](https://github.com/theSamPadilla) [![AnonO6](https://avatars.githubusercontent.com/u/124311066?v=4&s=48)](https://github.com/AnonO6)
[![afurm](https://avatars.githubusercontent.com/u/6375192?v=4&s=48)](https://github.com/afurm) [![황재원](https://avatars.githubusercontent.com/u/91544407?v=4&s=48)](https://github.com/jwchmodx) [![Leszek Szpunar](https://avatars.githubusercontent.com/u/13106764?v=4&s=48)](https://github.com/leszekszpunar) [![Mrseenz](https://avatars.githubusercontent.com/u/101962919?v=4&s=48)](https://github.com/Mrseenz) [![Yida-Dev](https://avatars.githubusercontent.com/u/92713555?v=4&s=48)](https://github.com/Yida-Dev) [![kesor](https://avatars.githubusercontent.com/u/7056?v=4&s=48)](https://github.com/kesor) [![mazhe-nerd](https://avatars.githubusercontent.com/u/106217973?v=4&s=48)](https://github.com/mazhe-nerd) [![Harald Buerbaumer](https://avatars.githubusercontent.com/u/44548809?v=4&s=48)](https://github.com/buerbaumer) [![magimetal](https://avatars.githubusercontent.com/u/36491250?v=4&s=48)](https://github.com/magimetal) [![Hiren Patel](https://avatars.githubusercontent.com/u/172098?v=4&s=48)](https://github.com/patelhiren)
[![BinHPdev](https://avatars.githubusercontent.com/u/219093083?v=4&s=48)](https://github.com/BinHPdev) [![RyanLee-Dev](https://avatars.githubusercontent.com/u/33855278?v=4&s=48)](https://github.com/RyanLee-Dev) [![cathrynlavery](https://avatars.githubusercontent.com/u/50469282?v=4&s=48)](https://github.com/cathrynlavery) [![al3mart](https://avatars.githubusercontent.com/u/11448715?v=4&s=48)](https://github.com/al3mart) [![JustYannicc](https://avatars.githubusercontent.com/u/52761674?v=4&s=48)](https://github.com/JustYannicc) [![abhisekbasu1](https://avatars.githubusercontent.com/u/40645221?v=4&s=48)](https://github.com/AbhisekBasu1) [![dbhurley](https://avatars.githubusercontent.com/u/5251425?v=4&s=48)](https://github.com/dbhurley) [![Kris Wu](https://avatars.githubusercontent.com/u/32388289?v=4&s=48)](https://github.com/mpz4life) [![tmimmanuel](https://avatars.githubusercontent.com/u/14046872?v=4&s=48)](https://github.com/tmimmanuel) [![JustasM](https://avatars.githubusercontent.com/u/59362982?v=4&s=48)](https://github.com/JustasMonkev)
[![Simantak Dabhade](https://avatars.githubusercontent.com/u/67303107?v=4&s=48)](https://github.com/simantak-dabhade) [![NicholasSpisak](https://avatars.githubusercontent.com/u/129075147?v=4&s=48)](https://github.com/NicholasSpisak) [![natefikru](https://avatars.githubusercontent.com/u/10344644?v=4&s=48)](https://github.com/natefikru) [![dunamismax](https://avatars.githubusercontent.com/u/65822992?v=4&s=48)](https://github.com/dunamismax) [![Simone Macario](https://avatars.githubusercontent.com/u/2116609?v=4&s=48)](https://github.com/simonemacario) [![ENCHIGO](https://avatars.githubusercontent.com/u/38551565?v=4&s=48)](https://github.com/ENCHIGO) [![xingsy97](https://avatars.githubusercontent.com/u/87063252?v=4&s=48)](https://github.com/xingsy97) [![emonty](https://avatars.githubusercontent.com/u/95156?v=4&s=48)](https://github.com/emonty) [![jadilson12](https://avatars.githubusercontent.com/u/36805474?v=4&s=48)](https://github.com/jadilson12) [![Yi-Cheng Wang](https://avatars.githubusercontent.com/u/80525895?v=4&s=48)](https://github.com/kirisame-wang)
[![Mathias Nagler](https://avatars.githubusercontent.com/u/9951231?v=4&s=48)](https://github.com/mathiasnagler) [![Sean McLellan](https://avatars.githubusercontent.com/u/760674?v=4&s=48)](https://github.com/Oceanswave) [![gumclaw](https://avatars.githubusercontent.com/u/265388744?v=4&s=48)](https://github.com/gumclaw) [![RichardCao](https://avatars.githubusercontent.com/u/4612401?v=4&s=48)](https://github.com/RichardCao) [![MKV21](https://avatars.githubusercontent.com/u/4974411?v=4&s=48)](https://github.com/MKV21) [![petter-b](https://avatars.githubusercontent.com/u/62076402?v=4&s=48)](https://github.com/petter-b) [![CodeForgeNet](https://avatars.githubusercontent.com/u/166907114?v=4&s=48)](https://github.com/CodeForgeNet) [![Johnson Shi](https://avatars.githubusercontent.com/u/13926417?v=4&s=48)](https://github.com/johnsonshi) [![durenzidu](https://avatars.githubusercontent.com/u/38130340?v=4&s=48)](https://github.com/durenzidu) [![dougvk](https://avatars.githubusercontent.com/u/401660?v=4&s=48)](https://github.com/dougvk)
[![Whoaa512](https://avatars.githubusercontent.com/u/1581943?v=4&s=48)](https://github.com/Whoaa512) [![zimeg](https://avatars.githubusercontent.com/u/18134219?v=4&s=48)](https://github.com/zimeg) [![Tseka Luk](https://avatars.githubusercontent.com/u/79151285?v=4&s=48)](https://github.com/TsekaLuk) [![Ryan Haines](https://avatars.githubusercontent.com/u/1855752?v=4&s=48)](https://github.com/Ryan-Haines) [![ufhy](https://avatars.githubusercontent.com/u/41638541?v=4&s=48)](https://github.com/uf-hy) [![Daan van der Plas](https://avatars.githubusercontent.com/u/93204684?v=4&s=48)](https://github.com/Daanvdplas) [![bittoby](https://avatars.githubusercontent.com/u/218712309?v=4&s=48)](https://github.com/bittoby) [![XuHao](https://avatars.githubusercontent.com/u/5087930?v=4&s=48)](https://github.com/xuhao1) [![Lucenx9](https://avatars.githubusercontent.com/u/185146821?v=4&s=48)](https://github.com/Lucenx9) [![HeMuling](https://avatars.githubusercontent.com/u/74801533?v=4&s=48)](https://github.com/HeMuling)
[![AaronLuo00](https://avatars.githubusercontent.com/u/112882500?v=4&s=48)](https://github.com/AaronLuo00) [![YUJIE2002](https://avatars.githubusercontent.com/u/123847463?v=4&s=48)](https://github.com/YUJIE2002) [![DhruvBhatia0](https://avatars.githubusercontent.com/u/69252327?v=4&s=48)](https://github.com/DhruvBhatia0) [![Divanoli Mydeen Pitchai](https://avatars.githubusercontent.com/u/12023205?v=4&s=48)](https://github.com/divanoli) [![Bronko](https://avatars.githubusercontent.com/u/2217509?v=4&s=48)](https://github.com/derbronko) [![rubyrunsstuff](https://avatars.githubusercontent.com/u/246602379?v=4&s=48)](https://github.com/rubyrunsstuff) [![rabsef-bicrym](https://avatars.githubusercontent.com/u/52549148?v=4&s=48)](https://github.com/rabsef-bicrym) [![IVY-AI-gif](https://avatars.githubusercontent.com/u/62232838?v=4&s=48)](https://github.com/IVY-AI-gif) [![pvtclawn](https://avatars.githubusercontent.com/u/258811507?v=4&s=48)](https://github.com/pvtclawn) [![stephenschoettler](https://avatars.githubusercontent.com/u/7587303?v=4&s=48)](https://github.com/stephenschoettler)
[![Dale Babiy](https://avatars.githubusercontent.com/u/42547246?v=4&s=48)](https://github.com/minupla) [![LeftX](https://avatars.githubusercontent.com/u/53989315?v=4&s=48)](https://github.com/xzq-xu) [![David Gelberg](https://avatars.githubusercontent.com/u/57605064?v=4&s=48)](https://github.com/mousberg) [![Engr. Arif Ahmed Joy](https://avatars.githubusercontent.com/u/4543396?v=4&s=48)](https://github.com/arifahmedjoy) [![Masataka Shinohara](https://avatars.githubusercontent.com/u/11906529?v=4&s=48)](https://github.com/harhogefoo) [![2233admin](https://avatars.githubusercontent.com/u/57929895?v=4&s=48)](https://github.com/2233admin) [![ameno-](https://avatars.githubusercontent.com/u/2416135?v=4&s=48)](https://github.com/ameno-) [![battman21](https://avatars.githubusercontent.com/u/2656916?v=4&s=48)](https://github.com/battman21) [![bcherny](https://avatars.githubusercontent.com/u/1761758?v=4&s=48)](https://github.com/bcherny) [![bobashopcashier](https://avatars.githubusercontent.com/u/77253505?v=4&s=48)](https://github.com/bobashopcashier)
[![dguido](https://avatars.githubusercontent.com/u/294844?v=4&s=48)](https://github.com/dguido) [![druide67](https://avatars.githubusercontent.com/u/212749853?v=4&s=48)](https://github.com/druide67) [![guirguispierre](https://avatars.githubusercontent.com/u/22091706?v=4&s=48)](https://github.com/guirguispierre) [![jzakirov](https://avatars.githubusercontent.com/u/15848838?v=4&s=48)](https://github.com/jzakirov) [![loganprit](https://avatars.githubusercontent.com/u/72722788?v=4&s=48)](https://github.com/loganprit) [![martinfrancois](https://avatars.githubusercontent.com/u/14319020?v=4&s=48)](https://github.com/martinfrancois) [![neo1027144-creator](https://avatars.githubusercontent.com/u/267440006?v=4&s=48)](https://github.com/neo1027144-creator) [![RealKai42](https://avatars.githubusercontent.com/u/44634134?v=4&s=48)](https://github.com/RealKai42) [![schumilin](https://avatars.githubusercontent.com/u/2003498?v=4&s=48)](https://github.com/schumilin) [![shuofengzhang](https://avatars.githubusercontent.com/u/24763026?v=4&s=48)](https://github.com/shuofengzhang)
[![solstead](https://avatars.githubusercontent.com/u/168413654?v=4&s=48)](https://github.com/solstead) [![hengm3467](https://avatars.githubusercontent.com/u/100685635?v=4&s=48)](https://github.com/hengm3467) [![chziyue](https://avatars.githubusercontent.com/u/62380760?v=4&s=48)](https://github.com/chziyue) [![James L. Cowan Jr.](https://avatars.githubusercontent.com/u/112015792?v=4&s=48)](https://github.com/jameslcowan) [![scifantastic](https://avatars.githubusercontent.com/u/150712374?v=4&s=48)](https://github.com/scifantastic) [![ryan-crabbe](https://avatars.githubusercontent.com/u/128659760?v=4&s=48)](https://github.com/ryan-crabbe) [![alexfilatov](https://avatars.githubusercontent.com/u/138589?v=4&s=48)](https://github.com/alexfilatov) [![Luckymingxuan](https://avatars.githubusercontent.com/u/159552597?v=4&s=48)](https://github.com/Luckymingxuan) [![HollyChou](https://avatars.githubusercontent.com/u/128659251?v=4&s=48)](https://github.com/Hollychou924) [![badlogic](https://avatars.githubusercontent.com/u/514052?v=4&s=48)](https://github.com/badlogic)
[![Daniel Hnyk](https://avatars.githubusercontent.com/u/2741256?v=4&s=48)](https://github.com/hnykda) [![dan bachelder](https://avatars.githubusercontent.com/u/325706?v=4&s=48)](https://github.com/dbachelder) [![heavenlost](https://avatars.githubusercontent.com/u/70937055?v=4&s=48)](https://github.com/heavenlost) [![shad0wca7](https://avatars.githubusercontent.com/u/9969843?v=4&s=48)](https://github.com/shad0wca7) [![Jared](https://avatars.githubusercontent.com/u/37019497?v=4&s=48)](https://github.com/jared596) [![kiranjd](https://avatars.githubusercontent.com/u/25822851?v=4&s=48)](https://github.com/kiranjd) [![Mars](https://avatars.githubusercontent.com/u/40958792?v=4&s=48)](https://github.com/Mellowambience) [![Kim](https://avatars.githubusercontent.com/u/150593189?v=4&s=48)](https://github.com/KimGLee) [![seheepeak](https://avatars.githubusercontent.com/u/134766597?v=4&s=48)](https://github.com/seheepeak) [![tsavo](https://avatars.githubusercontent.com/u/877990?v=4&s=48)](https://github.com/TSavo)
[![McRolly NWANGWU](https://avatars.githubusercontent.com/u/60803337?v=4&s=48)](https://github.com/mcrolly) [![dashed](https://avatars.githubusercontent.com/u/139499?v=4&s=48)](https://github.com/dashed) [![Shuai-DaiDai](https://avatars.githubusercontent.com/u/134567396?v=4&s=48)](https://github.com/Shuai-DaiDai) [![Subash Natarajan](https://avatars.githubusercontent.com/u/11032439?v=4&s=48)](https://github.com/suboss87) [![emanuelst](https://avatars.githubusercontent.com/u/9994339?v=4&s=48)](https://github.com/emanuelst) [![magendary](https://avatars.githubusercontent.com/u/30611068?v=4&s=48)](https://github.com/magendary) [![LI SHANXIN](https://avatars.githubusercontent.com/u/128674037?v=4&s=48)](https://github.com/PeterShanxin) [![j2h4u](https://avatars.githubusercontent.com/u/39818683?v=4&s=48)](https://github.com/j2h4u) [![bsormagec](https://avatars.githubusercontent.com/u/965219?v=4&s=48)](https://github.com/bsormagec) [![mjamiv](https://avatars.githubusercontent.com/u/142179942?v=4&s=48)](https://github.com/mjamiv)
[![Lalit Singh](https://avatars.githubusercontent.com/u/17166039?v=4&s=48)](https://github.com/aerolalit) [![Jessy LANGE](https://avatars.githubusercontent.com/u/89694096?v=4&s=48)](https://github.com/jessy2027) [![buddyh](https://avatars.githubusercontent.com/u/31752869?v=4&s=48)](https://github.com/buddyh) [![Aaron Zhu](https://avatars.githubusercontent.com/u/139607425?v=4&s=48)](https://github.com/aaron-he-zhu) [![F_ool](https://avatars.githubusercontent.com/u/112874572?v=4&s=48)](https://github.com/hhhhao28) [![Ben Stein](https://avatars.githubusercontent.com/u/31802821?v=4&s=48)](https://github.com/benostein) [![Lyle](https://avatars.githubusercontent.com/u/31182860?v=4&s=48)](https://github.com/LyleLiu666) [![Ping](https://avatars.githubusercontent.com/u/5123601?v=4&s=48)](https://github.com/pingren) [![popomore](https://avatars.githubusercontent.com/u/360661?v=4&s=48)](https://github.com/popomore) [![Dithilli](https://avatars.githubusercontent.com/u/41286037?v=4&s=48)](https://github.com/Dithilli)
[![fal3](https://avatars.githubusercontent.com/u/6484295?v=4&s=48)](https://github.com/fal3) [![mkbehr](https://avatars.githubusercontent.com/u/1285?v=4&s=48)](https://github.com/mkbehr) [![mteam88](https://avatars.githubusercontent.com/u/84196639?v=4&s=48)](https://github.com/mteam88) [![gupsammy](https://avatars.githubusercontent.com/u/20296019?v=4&s=48)](https://github.com/gupsammy) [![Shailesh](https://avatars.githubusercontent.com/u/75851986?v=4&s=48)](https://github.com/gut-puncture) [![Garnet Liu](https://avatars.githubusercontent.com/u/12513503?v=4&s=48)](https://github.com/garnetlyx) [![Thorfinn](https://avatars.githubusercontent.com/u/136994453?v=4&s=48)](https://github.com/miloudbelarebia) [![Protocol-zero-0](https://avatars.githubusercontent.com/u/257158451?v=4&s=48)](https://github.com/Protocol-zero-0) [![Paul van Oorschot](https://avatars.githubusercontent.com/u/20116814?v=4&s=48)](https://github.com/pvoo) [![Patrick Yingxi Pan](https://avatars.githubusercontent.com/u/5210631?v=4&s=48)](https://github.com/patrick-yingxi-pan)
[![Ptah.ai](https://avatars.githubusercontent.com/u/11701?v=4&s=48)](https://github.com/ptahdunbar) [![정우용](https://avatars.githubusercontent.com/u/71975659?v=4&s=48)](https://github.com/keepitmello) [![artuskg](https://avatars.githubusercontent.com/u/11966157?v=4&s=48)](https://github.com/artuskg) [![Anandesh-Sharma](https://avatars.githubusercontent.com/u/30695364?v=4&s=48)](https://github.com/Anandesh-Sharma) [![zidongdesign](https://avatars.githubusercontent.com/u/81469543?v=4&s=48)](https://github.com/zidongdesign) [![innocent-children](https://avatars.githubusercontent.com/u/55626758?v=4&s=48)](https://github.com/Innocent-children) [![El-Fitz](https://avatars.githubusercontent.com/u/8971906?v=4&s=48)](https://github.com/El-Fitz) [![arthurbr11](https://avatars.githubusercontent.com/u/99079981?v=4&s=48)](https://github.com/arthurbr11) [![jackheuberger](https://avatars.githubusercontent.com/u/7830838?v=4&s=48)](https://github.com/jackheuberger) [![Sergiusz](https://avatars.githubusercontent.com/u/6172067?v=4&s=48)](https://github.com/serkonyc)
[![Xu Gu](https://avatars.githubusercontent.com/u/53551744?v=4&s=48)](https://github.com/guxu11) [![hyojin](https://avatars.githubusercontent.com/u/3413183?v=4&s=48)](https://github.com/hyojin) [![jeann2013](https://avatars.githubusercontent.com/u/3299025?v=4&s=48)](https://github.com/jeann2013) [![jogelin](https://avatars.githubusercontent.com/u/954509?v=4&s=48)](https://github.com/jogelin) [![rmorse](https://avatars.githubusercontent.com/u/853547?v=4&s=48)](https://github.com/rmorse) [![scz2011](https://avatars.githubusercontent.com/u/9337506?v=4&s=48)](https://github.com/scz2011) [![Andyliu](https://avatars.githubusercontent.com/u/2377291?v=4&s=48)](https://github.com/andyliu) [![benithors](https://avatars.githubusercontent.com/u/20652882?v=4&s=48)](https://github.com/benithors) [![xiwuqi](https://avatars.githubusercontent.com/u/64734786?v=4&s=48)](https://github.com/xiwuqi) [![Alvin](https://avatars.githubusercontent.com/u/48358093?v=4&s=48)](https://github.com/TigerInYourDream)
[![AARON AGENT](https://avatars.githubusercontent.com/u/78432083?v=4&s=48)](https://github.com/aaronagent) [![Derek YU](https://avatars.githubusercontent.com/u/154693526?v=4&s=48)](https://github.com/TonyDerek-dot) [![Marvin](https://avatars.githubusercontent.com/u/43185740?v=4&s=48)](https://github.com/Zitzak) [![Andrew Jeon](https://avatars.githubusercontent.com/u/46941315?v=4&s=48)](https://github.com/ruypang) [![stain lu](https://avatars.githubusercontent.com/u/109842185?v=4&s=48)](https://github.com/stainlu) [![OpenCils](https://avatars.githubusercontent.com/u/114985039?v=4&s=48)](https://github.com/OpenCils) [![Stefan Galescu](https://avatars.githubusercontent.com/u/52995748?v=4&s=48)](https://github.com/stefangalescu) [![SP](https://avatars.githubusercontent.com/u/8068616?v=4&s=48)](https://github.com/sp-hk2ldn) [![Michael Flanagan](https://avatars.githubusercontent.com/u/39276573?v=4&s=48)](https://github.com/MikeORed) [![Gracie Gould](https://avatars.githubusercontent.com/u/66045258?v=4&s=48)](https://github.com/graciegould)
[![cash-echo-bot](https://avatars.githubusercontent.com/u/252747386?v=4&s=48)](https://github.com/cash-echo-bot) [![visionik](https://avatars.githubusercontent.com/u/52174?v=4&s=48)](https://github.com/visionik) [![WalterSumbon](https://avatars.githubusercontent.com/u/45062253?v=4&s=48)](https://github.com/WalterSumbon) [![huangcj](https://avatars.githubusercontent.com/u/43933609?v=4&s=48)](https://github.com/SubtleSpark) [![krizpoon](https://avatars.githubusercontent.com/u/1977532?v=4&s=48)](https://github.com/krizpoon) [![rodbland2021](https://avatars.githubusercontent.com/u/86267410?v=4&s=48)](https://github.com/rodbland2021) [![Thomas M](https://avatars.githubusercontent.com/u/44269971?v=4&s=48)](https://github.com/thomasxm) [![sar618](https://avatars.githubusercontent.com/u/214745104?v=4&s=48)](https://github.com/sar618) [![fagemx](https://avatars.githubusercontent.com/u/117356295?v=4&s=48)](https://github.com/fagemx) [![daymade](https://avatars.githubusercontent.com/u/4291901?v=4&s=48)](https://github.com/daymade)
[![Tyson Cung](https://avatars.githubusercontent.com/u/45380903?v=4&s=48)](https://github.com/tysoncung) [![Igor Markelov](https://avatars.githubusercontent.com/u/1489583?v=4&s=48)](https://github.com/pycckuu) [![Eng. Juan Combetto](https://avatars.githubusercontent.com/u/322761?v=4&s=48)](https://github.com/omniwired) [![connorshea](https://avatars.githubusercontent.com/u/2977353?v=4&s=48)](https://github.com/connorshea) [![bonald](https://avatars.githubusercontent.com/u/12394874?v=4&s=48)](https://github.com/bonald) [![Keenan](https://avatars.githubusercontent.com/u/85285887?v=4&s=48)](https://github.com/BeeSting50) [![nachoiacovino](https://avatars.githubusercontent.com/u/50103937?v=4&s=48)](https://github.com/nachoiacovino) [![zhumengzhu](https://avatars.githubusercontent.com/u/4508623?v=4&s=48)](https://github.com/zhumengzhu) [![Amine Harch el korane](https://avatars.githubusercontent.com/u/95189778?v=4&s=48)](https://github.com/Vitalcheffe) [![zhoulc777](https://avatars.githubusercontent.com/u/65058500?v=4&s=48)](https://github.com/zhoulongchao77)
[![Alex Navarro](https://avatars.githubusercontent.com/u/78754189?v=4&s=48)](https://github.com/navarrotech) [![Tanwa Arpornthip](https://avatars.githubusercontent.com/u/72845369?v=4&s=48)](https://github.com/CommanderCrowCode) [![TIHU](https://avatars.githubusercontent.com/u/44923937?v=4&s=48)](https://github.com/paceyw) [![Aftabbs](https://avatars.githubusercontent.com/u/112916888?v=4&s=48)](https://github.com/Aftabbs) [![Alex-Alaniz](https://avatars.githubusercontent.com/u/88956822?v=4&s=48)](https://github.com/Alex-Alaniz) [![jarvis-medmatic](https://avatars.githubusercontent.com/u/252428873?v=4&s=48)](https://github.com/jarvis-medmatic) [![Tom Ron](https://avatars.githubusercontent.com/u/126325152?v=4&s=48)](https://github.com/tomron87) [![day253](https://avatars.githubusercontent.com/u/9634619?v=4&s=48)](https://github.com/day253) [![Jaaneek](https://avatars.githubusercontent.com/u/25470423?v=4&s=48)](https://github.com/Jaaneek) [![Justin Song](https://avatars.githubusercontent.com/u/32268203?v=4&s=48)](https://github.com/AnCoSONG)
[![ziomancer](https://avatars.githubusercontent.com/u/262232137?v=4&s=48)](https://github.com/ziomancer) [![shayan919293](https://avatars.githubusercontent.com/u/60409704?v=4&s=48)](https://github.com/shayan919293) [![Edward](https://avatars.githubusercontent.com/u/53964601?v=4&s=48)](https://github.com/edwluo) [![Roger Chien](https://avatars.githubusercontent.com/u/20276663?v=4&s=48)](https://github.com/rjchien728) [![Michael Lee](https://avatars.githubusercontent.com/u/5957298?v=4&s=48)](https://github.com/TinyTb) [![Tomáš Dinh](https://avatars.githubusercontent.com/u/82420070?v=4&s=48)](https://github.com/No898) [![Ian Derrington](https://avatars.githubusercontent.com/u/76016868?v=4&s=48)](https://github.com/ianderrington) [![Lucky](https://avatars.githubusercontent.com/u/14868134?v=4&s=48)](https://github.com/L-U-C-K-Y) [![peschee](https://avatars.githubusercontent.com/u/63866?v=4&s=48)](https://github.com/peschee) [![Harry Cui Kepler](https://avatars.githubusercontent.com/u/166882517?v=4&s=48)](https://github.com/Kepler2024)
[![julianengel](https://avatars.githubusercontent.com/u/10634231?v=4&s=48)](https://github.com/julianengel) [![markfietje](https://avatars.githubusercontent.com/u/4325889?v=4&s=48)](https://github.com/markfietje) [![Dakshay Mehta](https://avatars.githubusercontent.com/u/50276213?v=4&s=48)](https://github.com/dakshaymehta) [![TheRipper](https://avatars.githubusercontent.com/u/144421782?v=4&s=48)](https://github.com/DavidNitZ) [![Dominic](https://avatars.githubusercontent.com/u/43616264?v=4&s=48)](https://github.com/dominicnunez) [![danielwanwx](https://avatars.githubusercontent.com/u/144515713?v=4&s=48)](https://github.com/danielwanwx) [![Seungwoo hong](https://avatars.githubusercontent.com/u/1100974?v=4&s=48)](https://github.com/hongsw) [![Youyou972](https://avatars.githubusercontent.com/u/50808411?v=4&s=48)](https://github.com/Youyou972) [![boris721](https://avatars.githubusercontent.com/u/257853888?v=4&s=48)](https://github.com/boris721) [![damoahdominic](https://avatars.githubusercontent.com/u/4623434?v=4&s=48)](https://github.com/damoahdominic)
[![dan-dr](https://avatars.githubusercontent.com/u/6669808?v=4&s=48)](https://github.com/dan-dr) [![doodlewind](https://avatars.githubusercontent.com/u/7312949?v=4&s=48)](https://github.com/doodlewind) [![kkarimi](https://avatars.githubusercontent.com/u/875218?v=4&s=48)](https://github.com/kkarimi) [![brokemac79](https://avatars.githubusercontent.com/u/255583030?v=4&s=48)](https://github.com/brokemac79) [![ozbillwang](https://avatars.githubusercontent.com/u/8954908?v=4&s=48)](https://github.com/ozbillwang) [![Ravish Gupta](https://avatars.githubusercontent.com/u/1249023?v=4&s=48)](https://github.com/ravyg) [![Jason Hargrove](https://avatars.githubusercontent.com/u/285708?v=4&s=48)](https://github.com/jasonhargrove) [![BrianWang1990](https://avatars.githubusercontent.com/u/20699847?v=4&s=48)](https://github.com/BrianWang1990) [![Joshua McKiddy](https://avatars.githubusercontent.com/u/43189238?v=4&s=48)](https://github.com/hackersifu) [![Fologan](https://avatars.githubusercontent.com/u/164580328?v=4&s=48)](https://github.com/Fologan)
[![Anonymous Amit](https://avatars.githubusercontent.com/u/134582556?v=4&s=48)](https://github.com/AnonAmit) [![v1p0r](https://avatars.githubusercontent.com/u/25909990?v=4&s=48)](https://github.com/v1p0r) [![Ajay Elika](https://avatars.githubusercontent.com/u/73169130?v=4&s=48)](https://github.com/ajay99511) [![Iranb](https://avatars.githubusercontent.com/u/49674669?v=4&s=48)](https://github.com/Iranb) [![Yonatan](https://avatars.githubusercontent.com/u/10474956?v=4&s=48)](https://github.com/yhyatt) [![codexGW](https://avatars.githubusercontent.com/u/9350182?v=4&s=48)](https://github.com/codexGW) [![Shaun Tsai](https://avatars.githubusercontent.com/u/13811075?v=4&s=48)](https://github.com/ShaunTsai) [![TideFinder](https://avatars.githubusercontent.com/u/68721273?v=4&s=48)](https://github.com/papago2355) [![Chase Dorsey](https://avatars.githubusercontent.com/u/12650570?v=4&s=48)](https://github.com/cdorsey) [![tda](https://avatars.githubusercontent.com/u/95275462?v=4&s=48)](https://github.com/tda1017)
[![0xJonHoldsCrypto](https://avatars.githubusercontent.com/u/81202085?v=4&s=48)](https://github.com/0xJonHoldsCrypto) [![akyourowngames](https://avatars.githubusercontent.com/u/123736861?v=4&s=48)](https://github.com/akyourowngames) [![clawdinator[bot]](https://avatars.githubusercontent.com/in/2607181?v=4&s=48)](https://github.com/apps/clawdinator) [![koala73](https://avatars.githubusercontent.com/u/996596?v=4&s=48)](https://github.com/koala73) [![sircrumpet](https://avatars.githubusercontent.com/u/4436535?v=4&s=48)](https://github.com/sircrumpet) [![thesomewhatyou](https://avatars.githubusercontent.com/u/162917831?v=4&s=48)](https://github.com/thesomewhatyou) [![zats](https://avatars.githubusercontent.com/u/2688806?v=4&s=48)](https://github.com/zats) [![Accunza](https://avatars.githubusercontent.com/u/12242811?v=4&s=48)](https://github.com/duqaXxX) [![Joly0](https://avatars.githubusercontent.com/u/13993216?v=4&s=48)](https://github.com/Joly0) [![Hanna](https://avatars.githubusercontent.com/u/4538260?v=4&s=48)](https://github.com/hannasdev)
[![Jeremiah Lowin](https://avatars.githubusercontent.com/u/153965?v=4&s=48)](https://github.com/jlowin) [![peetzweg/](https://avatars.githubusercontent.com/u/839848?v=4&s=48)](https://github.com/peetzweg) [![Skyler Miao](https://avatars.githubusercontent.com/u/153898832?v=4&s=48)](https://github.com/adao-max) [![tumf](https://avatars.githubusercontent.com/u/69994?v=4&s=48)](https://github.com/tumf) [![Hiago Silva](https://avatars.githubusercontent.com/u/97215740?v=4&s=48)](https://github.com/Huntterxx) [![Nate](https://avatars.githubusercontent.com/u/12980165?v=4&s=48)](https://github.com/nk1tz) [![lidamao633](https://avatars.githubusercontent.com/u/94925404?v=4&s=48)](https://github.com/lidamao633) [![Cklee](https://avatars.githubusercontent.com/u/99405438?v=4&s=48)](https://github.com/liebertar) [![CornBrother0x](https://avatars.githubusercontent.com/u/101160087?v=4&s=48)](https://github.com/CornBrother0x) [![DukeDeSouth](https://avatars.githubusercontent.com/u/51200688?v=4&s=48)](https://github.com/DukeDeSouth)
[![Sahan](https://avatars.githubusercontent.com/u/57447079?v=4&s=48)](https://github.com/sahancava) [![CashWilliams](https://avatars.githubusercontent.com/u/613573?v=4&s=48)](https://github.com/CashWilliams) [![Felix Lu](https://avatars.githubusercontent.com/u/58391009?v=4&s=48)](https://github.com/lumpinif) [![AdeboyeDN](https://avatars.githubusercontent.com/u/65312338?v=4&s=48)](https://github.com/AdeboyeDN) [![Rohan Santhosh Kumar](https://avatars.githubusercontent.com/u/181558744?v=4&s=48)](https://github.com/Rohan5commit) [![Srinivas Pavan](https://avatars.githubusercontent.com/u/34889400?v=4&s=48)](https://github.com/srinivaspavan9) [![h0tp](https://avatars.githubusercontent.com/u/141889580?v=4&s=48)](https://github.com/h0tp-ftw) [![Neo](https://avatars.githubusercontent.com/u/54811660?v=4&s=48)](https://github.com/neooriginal) [![Tianworld](https://avatars.githubusercontent.com/u/40754565?v=4&s=48)](https://github.com/Tianworld) [![neverland](https://avatars.githubusercontent.com/u/10937319?v=4&s=48)](https://github.com/Bermudarat)
[![asklee-klawd](https://avatars.githubusercontent.com/u/105007315?v=4&s=48)](https://github.com/asklee-klawd) [![Yuting Lin](https://avatars.githubusercontent.com/u/32728916?v=4&s=48)](https://github.com/yuting0624) [![constansino](https://avatars.githubusercontent.com/u/65108260?v=4&s=48)](https://github.com/constansino) [![ghsmc](https://avatars.githubusercontent.com/u/68118719?v=4&s=48)](https://github.com/ghsmc) [![ibrahimq21](https://avatars.githubusercontent.com/u/8392472?v=4&s=48)](https://github.com/ibrahimq21) [![irtiq7](https://avatars.githubusercontent.com/u/3823029?v=4&s=48)](https://github.com/irtiq7) [![kelvinCB](https://avatars.githubusercontent.com/u/50544379?v=4&s=48)](https://github.com/kelvinCB) [![mitsuhiko](https://avatars.githubusercontent.com/u/7396?v=4&s=48)](https://github.com/mitsuhiko) [![nohat](https://avatars.githubusercontent.com/u/838027?v=4&s=48)](https://github.com/nohat) [![santiagomed](https://avatars.githubusercontent.com/u/30184543?v=4&s=48)](https://github.com/santiagomed)
[![suminhthanh](https://avatars.githubusercontent.com/u/2907636?v=4&s=48)](https://github.com/suminhthanh) [![svkozak](https://avatars.githubusercontent.com/u/31941359?v=4&s=48)](https://github.com/svkozak) [![张哲芳](https://avatars.githubusercontent.com/u/34058239?v=4&s=48)](https://github.com/zhangzhefang-github) [![Ho Lim](https://avatars.githubusercontent.com/u/166576253?v=4&s=48)](https://github.com/HOYALIM) [![Toven](https://avatars.githubusercontent.com/u/69218856?v=4&s=48)](https://github.com/ping-Toven) [![R. Desmond](https://avatars.githubusercontent.com/u/134018026?v=4&s=48)](https://github.com/0-CYBERDYNE-SYSTEMS-0) [![游乐场](https://avatars.githubusercontent.com/u/79438767?v=4&s=48)](https://github.com/ylc0919) [![Reed](https://avatars.githubusercontent.com/u/129141816?v=4&s=48)](https://github.com/reed1898) [![Aditya Chaudhary](https://avatars.githubusercontent.com/u/55331140?v=4&s=48)](https://github.com/ItsAditya-xyz) [![Sam](https://avatars.githubusercontent.com/u/14844597?v=4&s=48)](https://github.com/samrusani)
[![Andy](https://avatars.githubusercontent.com/u/91510251?v=4&s=48)](https://github.com/andyk-ms) [![Rajat Joshi](https://avatars.githubusercontent.com/u/78920780?v=4&s=48)](https://github.com/18-RAJAT) [![cyb1278588254](https://avatars.githubusercontent.com/u/48212932?v=4&s=48)](https://github.com/cyb1278588254) [![Zoher Ghadyali](https://avatars.githubusercontent.com/u/34316555?v=4&s=48)](https://github.com/zoherghadyali) [![Manik Vahsith](https://avatars.githubusercontent.com/u/49544491?v=4&s=48)](https://github.com/manikv12) [![tarouca](https://avatars.githubusercontent.com/u/36767065?v=4&s=48)](https://github.com/manueltarouca) [![MrBrain](https://avatars.githubusercontent.com/u/176294248?v=4&s=48)](https://github.com/GaosCode) [![Daniel Zou](https://avatars.githubusercontent.com/u/12799392?v=4&s=48)](https://github.com/pahdo) [![Lilo](https://avatars.githubusercontent.com/u/1622461?v=4&s=48)](https://github.com/detecti1) [![Jason](https://avatars.githubusercontent.com/u/101583541?v=4&s=48)](https://github.com/JasonOA888)
[![SUMUKH](https://avatars.githubusercontent.com/u/130692934?v=4&s=48)](https://github.com/sumukhj1219) [![Bakhtier Sizhaev](https://avatars.githubusercontent.com/u/108124494?v=4&s=48)](https://github.com/bakhtiersizhaev) [![Ganghyun Kim](https://avatars.githubusercontent.com/u/58307870?v=4&s=48)](https://github.com/kyleok) [![AkashKobal](https://avatars.githubusercontent.com/u/98216083?v=4&s=48)](https://github.com/AkashKobal) [![Brian](https://avatars.githubusercontent.com/u/95547369?v=4&s=48)](https://github.com/zhuisDEV) [![wu-tian807](https://avatars.githubusercontent.com/u/61640083?v=4&s=48)](https://github.com/wu-tian807) [![Vasanth Rao Naik Sabavat](https://avatars.githubusercontent.com/u/50385532?v=4&s=48)](https://github.com/vsabavat) [![Kinfey](https://avatars.githubusercontent.com/u/93169410?v=4&s=48)](https://github.com/kinfey) [![Artemii](https://avatars.githubusercontent.com/u/35071559?v=4&s=48)](https://github.com/crimeacs) [![VibhorGautam](https://avatars.githubusercontent.com/u/55019395?v=4&s=48)](https://github.com/VibhorGautam)
[![John Rood](https://avatars.githubusercontent.com/u/62669593?v=4&s=48)](https://github.com/John-Rood) [![velamints2](https://avatars.githubusercontent.com/u/93711796?v=4&s=48)](https://github.com/velamints2) [![Benji Peng](https://avatars.githubusercontent.com/u/11394934?v=4&s=48)](https://github.com/benjipeng) [![JINNYEONG KIM](https://avatars.githubusercontent.com/u/41609506?v=4&s=48)](https://github.com/divisonofficer) [![Rahul kumar Pal](https://avatars.githubusercontent.com/u/151990777?v=4&s=48)](https://github.com/Rahulkumar070) [![Rockcent](https://avatars.githubusercontent.com/u/128210877?v=4&s=48)](https://github.com/rockcent) [![Limitless](https://avatars.githubusercontent.com/u/127183162?v=4&s=48)](https://github.com/Limitless2023) [![24601](https://avatars.githubusercontent.com/u/1157207?v=4&s=48)](https://github.com/24601) [![awkoy](https://avatars.githubusercontent.com/u/13995636?v=4&s=48)](https://github.com/awkoy) [![dawondyifraw](https://avatars.githubusercontent.com/u/9797257?v=4&s=48)](https://github.com/dawondyifraw)
[![google-labs-jules[bot]](https://avatars.githubusercontent.com/in/842251?v=4&s=48)](https://github.com/apps/google-labs-jules) [![henrino3](https://avatars.githubusercontent.com/u/4260288?v=4&s=48)](https://github.com/henrino3) [![Kansodata](https://avatars.githubusercontent.com/u/225288021?v=4&s=48)](https://github.com/Kansodata) [![kaonash](https://avatars.githubusercontent.com/u/7535663?v=4&s=48)](https://github.com/kaonash) [![p6l-richard](https://avatars.githubusercontent.com/u/18185649?v=4&s=48)](https://github.com/p6l-richard) [![pi0](https://avatars.githubusercontent.com/u/5158436?v=4&s=48)](https://github.com/pi0) [![skainguyen1412](https://avatars.githubusercontent.com/u/14249881?v=4&s=48)](https://github.com/skainguyen1412) [![Starhappysh](https://avatars.githubusercontent.com/u/221244539?v=4&s=48)](https://github.com/Starhappysh) [![xdanger](https://avatars.githubusercontent.com/u/7087?v=4&s=48)](https://github.com/xdanger) [![Penchan](https://avatars.githubusercontent.com/u/5032148?v=4&s=48)](https://github.com/p3nchan)
[![scald](https://avatars.githubusercontent.com/u/1215913?v=4&s=48)](https://github.com/scald) [![Serhii](https://avatars.githubusercontent.com/u/151471784?v=4&s=48)](https://github.com/kashevk0) [![a](https://avatars.githubusercontent.com/u/33371662?v=4&s=48)](https://github.com/Yuandiaodiaodiao) [![Doğu Abaris](https://avatars.githubusercontent.com/u/135986694?v=4&s=48)](https://github.com/doguabaris) [![ysqander](https://avatars.githubusercontent.com/u/80843820?v=4&s=48)](https://github.com/ysqander) [![andranik-sahakyan](https://avatars.githubusercontent.com/u/8908029?v=4&s=48)](https://github.com/andranik-sahakyan) [![Wangnov](https://avatars.githubusercontent.com/u/48670012?v=4&s=48)](https://github.com/Wangnov) [![Austin](https://avatars.githubusercontent.com/u/112558420?v=4&s=48)](https://github.com/rixau) [![lisitan](https://avatars.githubusercontent.com/u/50470712?v=4&s=48)](https://github.com/lisitan) [![Rishi Vhavle](https://avatars.githubusercontent.com/u/134706404?v=4&s=48)](https://github.com/kaizen403)
[![Frank Harris](https://avatars.githubusercontent.com/u/183158?v=4&s=48)](https://github.com/hirefrank) [![Kenny Lee](https://avatars.githubusercontent.com/u/1432489?v=4&s=48)](https://github.com/kennyklee) [![Alice Losasso](https://avatars.githubusercontent.com/u/104875499?v=4&s=48)](https://github.com/dddabtc) [![edincampara](https://avatars.githubusercontent.com/u/142477787?v=4&s=48)](https://github.com/edincampara) [![Felix Hellström](https://avatars.githubusercontent.com/u/30758862?v=4&s=48)](https://github.com/fellanH) [![Varun Chopra](https://avatars.githubusercontent.com/u/113368492?v=4&s=48)](https://github.com/VarunChopra11) [![wangai-studio](https://avatars.githubusercontent.com/u/256938352?v=4&s=48)](https://github.com/wangai-studio) [![sleontenko](https://avatars.githubusercontent.com/u/7135949?v=4&s=48)](https://github.com/sleontenko) [![Yassine Amjad](https://avatars.githubusercontent.com/u/59234686?v=4&s=48)](https://github.com/yassine20011) [![Anton Eicher](https://avatars.githubusercontent.com/u/54324760?v=4&s=48)](https://github.com/ant1eicher)
[![Drake Thomsen](https://avatars.githubusercontent.com/u/120344051?v=4&s=48)](https://github.com/ThomsenDrake) [![Hinata Kaga (samon)](https://avatars.githubusercontent.com/u/61647657?v=4&s=48)](https://github.com/kakuteki) [![andreabadesso](https://avatars.githubusercontent.com/u/3586068?v=4&s=48)](https://github.com/andreabadesso) [![chenxin-yan](https://avatars.githubusercontent.com/u/71162231?v=4&s=48)](https://github.com/chenxin-yan) [![cordx56](https://avatars.githubusercontent.com/u/23298744?v=4&s=48)](https://github.com/cordx56) [![dvrshil](https://avatars.githubusercontent.com/u/81693876?v=4&s=48)](https://github.com/dvrshil) [![MarvinCui](https://avatars.githubusercontent.com/u/130876763?v=4&s=48)](https://github.com/MarvinCui) [![Yeom-JinHo](https://avatars.githubusercontent.com/u/81306489?v=4&s=48)](https://github.com/Yeom-JinHo) [![Jeremy Mumford](https://avatars.githubusercontent.com/u/36290330?v=4&s=48)](https://github.com/17jmumford) [![Charlie Niño](https://avatars.githubusercontent.com/u/2346724?v=4&s=48)](https://github.com/KnHack)
[![Sharoon Sharif](https://avatars.githubusercontent.com/u/150296639?v=4&s=48)](https://github.com/SharoonSharif) [![Oren](https://avatars.githubusercontent.com/u/168856?v=4&s=48)](https://github.com/orenyomtov) [![MattQ](https://avatars.githubusercontent.com/u/115874885?v=4&s=48)](https://github.com/mattqdev) [![Parker Todd Brooks](https://avatars.githubusercontent.com/u/585456?v=4&s=48)](https://github.com/parkertoddbrooks) [![Yufeng He](https://avatars.githubusercontent.com/u/40085740?v=4&s=48)](https://github.com/he-yufeng) [![Milofax](https://avatars.githubusercontent.com/u/2537423?v=4&s=48)](https://github.com/Milofax) [![Steve (OpenClaw)](https://avatars.githubusercontent.com/u/261149299?v=4&s=48)](https://github.com/stevebot-alive) [![zhoulf1006](https://avatars.githubusercontent.com/u/35586967?v=4&s=48)](https://github.com/zhoulf1006) [![Jonatan](https://avatars.githubusercontent.com/u/19454127?v=4&s=48)](https://github.com/jrrcdev) [![Sebastian B Otaegui](https://avatars.githubusercontent.com/u/91633?v=4&s=48)](https://github.com/feniix)
[![Matthew](https://avatars.githubusercontent.com/u/76985631?v=4&s=48)](https://github.com/ZetiMente) [![ABFS Tech](https://avatars.githubusercontent.com/u/82096803?v=4&s=48)](https://github.com/QuantDeveloperUSA) [![alexstyl](https://avatars.githubusercontent.com/u/1665273?v=4&s=48)](https://github.com/alexstyl) [![Ethan Palm](https://avatars.githubusercontent.com/u/56270045?v=4&s=48)](https://github.com/ethanpalm) [![Qkal](https://avatars.githubusercontent.com/u/77361240?v=4&s=48)](https://github.com/qkal) [![cygaar](https://avatars.githubusercontent.com/u/97691933?v=4&s=48)](https://github.com/cygaar) [![Umut CAN](https://avatars.githubusercontent.com/u/78921017?v=4&s=48)](https://github.com/U-C4N) [![Jakob](https://avatars.githubusercontent.com/u/38699060?v=4&s=48)](https://github.com/jakobdylanc) [![antons](https://avatars.githubusercontent.com/u/129705?v=4&s=48)](https://github.com/antons) [![austinm911](https://avatars.githubusercontent.com/u/31991302?v=4&s=48)](https://github.com/austinm911)
[![mahmoudashraf93](https://avatars.githubusercontent.com/u/9130129?v=4&s=48)](https://github.com/mahmoudashraf93) [![philipp-spiess](https://avatars.githubusercontent.com/u/458591?v=4&s=48)](https://github.com/philipp-spiess) [![pkrmf](https://avatars.githubusercontent.com/u/1714267?v=4&s=48)](https://github.com/pkrmf) [![joshrad-dev](https://avatars.githubusercontent.com/u/62785552?v=4&s=48)](https://github.com/joshrad-dev) [![factnest365-ops](https://avatars.githubusercontent.com/u/236534360?v=4&s=48)](https://github.com/factnest365-ops) [![yingchunbai](https://avatars.githubusercontent.com/u/33477283?v=4&s=48)](https://github.com/yingchunbai) [![AJ (@techfren)](https://avatars.githubusercontent.com/u/8023513?v=4&s=48)](https://github.com/aj47) [![Marchel Fahrezi](https://avatars.githubusercontent.com/u/53804949?v=4&s=48)](https://github.com/Alg0rix) [![futhgar](https://avatars.githubusercontent.com/u/51002668?v=4&s=48)](https://github.com/futhgar) [![Zhang](https://avatars.githubusercontent.com/u/56248212?v=4&s=48)](https://github.com/YonganZhang)
[![Rémi](https://avatars.githubusercontent.com/u/1299873?v=4&s=48)](https://github.com/remusao) [![Dan Ballance](https://avatars.githubusercontent.com/u/13839912?v=4&s=48)](https://github.com/danballance) [![Eric Su](https://avatars.githubusercontent.com/u/60202455?v=4&s=48)](https://github.com/GHesericsu) [![Kimitaka Watanabe](https://avatars.githubusercontent.com/u/167225?v=4&s=48)](https://github.com/kimitaka) [![Justin Ling](https://avatars.githubusercontent.com/u/2521993?v=4&s=48)](https://github.com/itsjling) [![Raymond Berger](https://avatars.githubusercontent.com/u/921217?v=4&s=48)](https://github.com/RayBB) [![lutr0](https://avatars.githubusercontent.com/u/76906369?v=4&s=48)](https://github.com/lutr0) [![claude](https://avatars.githubusercontent.com/u/81847?v=4&s=48)](https://github.com/claude) [![AngryBird](https://avatars.githubusercontent.com/u/48046333?v=4&s=48)](https://github.com/angrybirddd) [![Fabian Williams](https://avatars.githubusercontent.com/u/92543063?v=4&s=48)](https://github.com/fabianwilliams)
[![0x4C33](https://avatars.githubusercontent.com/u/60883781?v=4&s=48)](https://github.com/haoruilee) [![8BlT](https://avatars.githubusercontent.com/u/162764392?v=4&s=48)](https://github.com/8BlT) [![atalovesyou](https://avatars.githubusercontent.com/u/3534502?v=4&s=48)](https://github.com/atalovesyou) [![erikpr1994](https://avatars.githubusercontent.com/u/6299331?v=4&s=48)](https://github.com/erikpr1994) [![jonasjancarik](https://avatars.githubusercontent.com/u/2459191?v=4&s=48)](https://github.com/jonasjancarik) [![longmaba](https://avatars.githubusercontent.com/u/9361500?v=4&s=48)](https://github.com/longmaba) [![mitschabaude-bot](https://avatars.githubusercontent.com/u/247582884?v=4&s=48)](https://github.com/mitschabaude-bot) [![thesash](https://avatars.githubusercontent.com/u/1166151?v=4&s=48)](https://github.com/thesash) [![Max](https://avatars.githubusercontent.com/u/8418866?v=4&s=48)](https://github.com/rdev) [![easternbloc](https://avatars.githubusercontent.com/u/92585?v=4&s=48)](https://github.com/easternbloc)
[![chrisrodz](https://avatars.githubusercontent.com/u/2967620?v=4&s=48)](https://github.com/chrisrodz) [![gabriel-trigo](https://avatars.githubusercontent.com/u/38991125?v=4&s=48)](https://github.com/gabriel-trigo) [![manmal](https://avatars.githubusercontent.com/u/142797?v=4&s=48)](https://github.com/manmal) [![neist](https://avatars.githubusercontent.com/u/1029724?v=4&s=48)](https://github.com/neist) [![wes-davis](https://avatars.githubusercontent.com/u/16506720?v=4&s=48)](https://github.com/wes-davis) [![manuelhettich](https://avatars.githubusercontent.com/u/17690367?v=4&s=48)](https://github.com/ManuelHettich) [![sktbrd](https://avatars.githubusercontent.com/u/116202536?v=4&s=48)](https://github.com/sktbrd) [![larlyssa](https://avatars.githubusercontent.com/u/13128869?v=4&s=48)](https://github.com/larlyssa) [![pcty-nextgen-service-account](https://avatars.githubusercontent.com/u/112553441?v=4&s=48)](https://github.com/pcty-nextgen-service-account) [![Syhids](https://avatars.githubusercontent.com/u/671202?v=4&s=48)](https://github.com/Syhids)
[![tmchow](https://avatars.githubusercontent.com/u/517103?v=4&s=48)](https://github.com/tmchow) [![Marc Gratch](https://avatars.githubusercontent.com/u/2238658?v=4&s=48)](https://github.com/mgratch) [![xtao](https://avatars.githubusercontent.com/u/1050163?v=4&s=48)](https://github.com/xtao) [![JackyWay](https://avatars.githubusercontent.com/u/53031570?v=4&s=48)](https://github.com/JackyWay) [![Josh Phillips](https://avatars.githubusercontent.com/u/3744255?v=4&s=48)](https://github.com/j1philli) [![T5-AndyML](https://avatars.githubusercontent.com/u/22801233?v=4&s=48)](https://github.com/T5-AndyML) [![huohua-dev](https://avatars.githubusercontent.com/u/258873123?v=4&s=48)](https://github.com/huohua-dev) [![imfing](https://avatars.githubusercontent.com/u/5097752?v=4&s=48)](https://github.com/imfing) [![Randy Torres](https://avatars.githubusercontent.com/u/149904821?v=4&s=48)](https://github.com/RandyVentures) [![Marco Di Dionisio](https://avatars.githubusercontent.com/u/3519682?v=4&s=48)](https://github.com/marcodd23)
[![iamadig](https://avatars.githubusercontent.com/u/102129234?v=4&s=48)](https://github.com/Iamadig) [![humanwritten](https://avatars.githubusercontent.com/u/206531610?v=4&s=48)](https://github.com/humanwritten) [![Rob Axelsen](https://avatars.githubusercontent.com/u/13132899?v=4&s=48)](https://github.com/robaxelsen) [![Pratham Dubey](https://avatars.githubusercontent.com/u/134331217?v=4&s=48)](https://github.com/prathamdby) [![0oAstro](https://avatars.githubusercontent.com/u/79555780?v=4&s=48)](https://github.com/0oAstro) [![aaronn](https://avatars.githubusercontent.com/u/1653630?v=4&s=48)](https://github.com/aaronn) [![Arturo](https://avatars.githubusercontent.com/u/34192856?v=4&s=48)](https://github.com/afern247) [![Asleep123](https://avatars.githubusercontent.com/u/122379135?v=4&s=48)](https://github.com/Asleep123) [![dantelex](https://avatars.githubusercontent.com/u/631543?v=4&s=48)](https://github.com/dantelex) [![fcatuhe](https://avatars.githubusercontent.com/u/17382215?v=4&s=48)](https://github.com/fcatuhe)
[![gtsifrikas](https://avatars.githubusercontent.com/u/8904378?v=4&s=48)](https://github.com/gtsifrikas) [![hrdwdmrbl](https://avatars.githubusercontent.com/u/554881?v=4&s=48)](https://github.com/hrdwdmrbl) [![hugobarauna](https://avatars.githubusercontent.com/u/2719?v=4&s=48)](https://github.com/hugobarauna) [![jayhickey](https://avatars.githubusercontent.com/u/1676460?v=4&s=48)](https://github.com/jayhickey) [![jiulingyun](https://avatars.githubusercontent.com/u/126459548?v=4&s=48)](https://github.com/jiulingyun) [![Jonathan D. Rhyne (DJ-D)](https://avatars.githubusercontent.com/u/7828464?v=4&s=48)](https://github.com/jdrhyne) [![jverdi](https://avatars.githubusercontent.com/u/345050?v=4&s=48)](https://github.com/jverdi) [![kitze](https://avatars.githubusercontent.com/u/1160594?v=4&s=48)](https://github.com/kitze) [![loukotal](https://avatars.githubusercontent.com/u/18210858?v=4&s=48)](https://github.com/loukotal) [![minghinmatthewlam](https://avatars.githubusercontent.com/u/14224566?v=4&s=48)](https://github.com/minghinmatthewlam)
[![MSch](https://avatars.githubusercontent.com/u/7475?v=4&s=48)](https://github.com/MSch) [![odrobnik](https://avatars.githubusercontent.com/u/333270?v=4&s=48)](https://github.com/odrobnik) [![oswalpalash](https://avatars.githubusercontent.com/u/6431196?v=4&s=48)](https://github.com/oswalpalash) [![ratulsarna](https://avatars.githubusercontent.com/u/105903728?v=4&s=48)](https://github.com/ratulsarna) [![reeltimeapps](https://avatars.githubusercontent.com/u/637338?v=4&s=48)](https://github.com/reeltimeapps) [![snopoke](https://avatars.githubusercontent.com/u/249606?v=4&s=48)](https://github.com/snopoke) [![sreekaransrinath](https://avatars.githubusercontent.com/u/50989977?v=4&s=48)](https://github.com/sreekaransrinath) [![timkrase](https://avatars.githubusercontent.com/u/38947626?v=4&s=48)](https://github.com/timkrase)
<!-- clawtributors:end -->
<!-- clawtributors:hidden:start
default-avatar-cache: hidden from the rendered wall because these users still use GitHub's default avatar
13otkmdr
aaronveklabs
adityashaw2
ai-reviewer-qs
alexyyyander
alphonse-arianee
amitbiswal007
bbblending
bbddbb1
bitfoundry-ai
bugkillerking
carlulsoe
charzhou
cheeeee
dalomeve
danielz1z
diaspar4u
dirbalak
djangonavarro220
dobbylorenzbot
drcrinkle
drickon
eddertalmor
eengad
efe-buken
eric-fr4
eronfan
evandance
extrasmall0
ezhikkk
fuller-stack-dev
fwhite13
gambletan
gejifeng
harrington-bot
heimdallstrategy
heyhudson
hougangdev
jamesgroat
jamtujest
jaymishra-source
joe2643
joetomasone
jonathanworks
jonisjongithub
jscaldwell55
julbarth
junjunjunbong
kirillshchetinin
kyohwang
lailoo
latitudeki5223
lawrence3699
liaosvcaf
livingghost
luijoc
lukeboyett
lurebat
mahanandhi
maple778
martingarramon
matthew19990919
moktamd
moltbot886
mujiannan
mukhtharcm
mylszd
natedenh
nicholascyh
nickhood1984
nico-hoff
nikus-pan
nonggialiang
oliviareid-svg
openclaw-bot
pablohrcarvalho
patrick-barletta
pinghuachiu
private-peter
prospectore
rafaelreis-r
rexl2018
rexlunae
rhjoh
ronak-guliani
ryancontent
ryanngit
rybnikov
sandpile
sbking
shivamraut101
shuicici
slats24
slepybear
sline
socialnerd42069
solodmd
sudie-codes
sumleo
superman32432432
ted-developer
tempeste
theonejvo
tosh-hamburg
uli-will-code
w-sss
whiskyboy
wittam-01
xieyongliang
yassinebkr
yuna78
yuweuii
yxjsxy
zijiess
clawtributors:hidden:end -->

View File

@@ -2,6 +2,122 @@
<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" version="2.0">
<channel>
<title>OpenClaw</title>
<item>
<title>2026.4.15</title>
<pubDate>Thu, 16 Apr 2026 23:33:29 +0000</pubDate>
<link>https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml</link>
<sparkle:version>2026041590</sparkle:version>
<sparkle:shortVersionString>2026.4.15</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
<description><![CDATA[<h2>OpenClaw 2026.4.15</h2>
<h3>Changes</h3>
<ul>
<li>Anthropic/models: default Anthropic selections, <code>opus</code> aliases, Claude CLI defaults, and bundled image understanding to Claude Opus 4.7.</li>
<li>Google/TTS: add Gemini text-to-speech support to the bundled <code>google</code> plugin, including provider registration, voice selection, WAV reply output, PCM telephony output, and setup/docs guidance. (#67515) Thanks @barronlroth.</li>
<li>Control UI/Overview: add a Model Auth status card showing OAuth token health and provider rate-limit pressure at a glance, with attention callouts when OAuth tokens are expiring or expired. Backed by a new <code>models.authStatus</code> gateway method that strips credentials and caches for 60s. (#66211) Thanks @omarshahine.</li>
<li>Memory/LanceDB: add cloud storage support to <code>memory-lancedb</code> so durable memory indexes can run on remote object storage instead of local disk only. (#63502) Thanks @rugvedS07.</li>
<li>GitHub Copilot/memory search: add a GitHub Copilot embedding provider for memory search, and expose a dedicated Copilot embedding host helper so plugins can reuse the transport while honoring remote overrides, token refresh, and safer payload validation. (#61718) Thanks @feiskyer and @vincentkoc.</li>
<li>Agents/local models: add experimental <code>agents.defaults.experimental.localModelLean: true</code> to drop heavyweight default tools like <code>browser</code>, <code>cron</code>, and <code>message</code>, reducing prompt size for weaker local-model setups without changing the normal path. (#66495) Thanks @ImLukeF.</li>
<li>Packaging/plugins: localize bundled plugin runtime deps to their owning extensions, trim the published docs payload, and tighten install/package-manager guardrails so published builds stay leaner and core stops carrying extension-owned runtime baggage. (#67099) Thanks @vincentkoc.</li>
<li>QA/Matrix: split Matrix live QA into a source-linked <code>qa-matrix</code> runner and keep repo-private <code>qa-*</code> surfaces out of packaged and published builds. (#66723) Thanks @gumadeiras.</li>
<li>Docs/showcase: add a scannable hero, complete section jump links, and a responsive video grid for community examples. (#48493) Thanks @jchopard69.</li>
</ul>
<h3>Fixes</h3>
<ul>
<li>Gateway/tools: anchor trusted local <code>MEDIA:</code> tool-result passthrough on the exact raw name of this run's registered built-in tools, and reject client tool definitions whose names normalize-collide with a built-in or with another client tool in the same request (<code>400 invalid_request_error</code> on both JSON and SSE paths), so a client-supplied tool named like a built-in can no longer inherit its local-media trust. (#67303)</li>
<li>Agents/replay recovery: classify the provider wording <code>401 input item ID does not belong to this connection</code> as replay-invalid, so users get the existing <code>/new</code> session reset guidance instead of a raw 401-style failure. (#66475) Thanks @dallylee.</li>
<li>Gateway/webchat: enforce localRoots containment on webchat audio embedding path [AI-assisted]. (#67298) Thanks @pgondhi987.</li>
<li>Matrix/pairing: block DM pairing-store entries from authorizing room control commands [AI-assisted]. (#67294) Thanks @pgondhi987.</li>
<li>Docker/build: verify <code>@matrix-org/matrix-sdk-crypto-nodejs</code> native bindings with <code>find</code> under <code>node_modules</code> instead of a hardcoded <code>.pnpm/...</code> path so pnpm v10+ virtual-store layouts no longer fail the image build. (#67143) thanks @ly85206559.</li>
<li>Matrix/E2EE: keep startup bootstrap conservative for passwordless token-auth bots, still attempt the guarded repair pass without requiring <code>channels.matrix.password</code>, and document the remaining password-UIA limitation. (#66228) Thanks @SARAMALI15792.</li>
<li>Cron/announce delivery: suppress mixed-content isolated cron announce replies that end with <code>NO_REPLY</code> so trailing silent sentinels no longer leak summary text to the target channel. (#65004) thanks @neo1027144-creator.</li>
<li>Plugins/bundled channels: partition bundled channel lazy caches by active bundled root so <code>OPENCLAW_BUNDLED_PLUGINS_DIR</code> flips stop reusing stale plugin, setup, secrets, and runtime state. (#67200) Thanks @gumadeiras.</li>
<li>Packaging/plugins: prune common test/spec cargo from bundled plugin runtime dependencies and fail npm release validation if packaged test cargo reappears, keeping published tarballs leaner without plugin-specific special cases. (#67275) thanks @gumadeiras.</li>
<li>Agents/context + Memory: trim default startup/skills prompt budgets, cap <code>memory_get</code> excerpts by default with explicit continuation metadata, and keep QMD reads aligned with the same bounded excerpt contract so long sessions pull less context by default without losing deterministic follow-up reads.</li>
<li>Matrix/commands: skip DM pairing-store reads on room traffic now that room control-command authorization ignores pairing-store entries, keeping the room path narrower without changing room auth behavior. (#67325) Thanks @gumadeiras.</li>
<li>Memory-core/dreaming: skip dreaming narrative transcripts from session-store metadata before bootstrap records land so dream diary prompt/prose lines do not pollute session ingestion. (#67315) thanks @jalehman.</li>
<li>Agents/local models: clarify low-context preflight hints for self-hosted models, point config-backed caps at the relevant OpenClaw setting, and stop suggesting larger models when <code>agents.defaults.contextTokens</code> is the real limit. (#66236) Thanks @ImLukeF.</li>
<li>Dreaming/memory-core: change the default <code>dreaming.storage.mode</code> from <code>inline</code> to <code>separate</code> so Dreaming phase blocks (<code>## Light Sleep</code>, <code>## REM Sleep</code>) land in <code>memory/dreaming/{phase}/YYYY-MM-DD.md</code> instead of being injected into <code>memory/YYYY-MM-DD.md</code>. Daily memory files no longer get dominated by structured candidate output, and the daily-ingestion scanner that already strips dream marker blocks no longer has to compete with hundreds of phase-block lines on every run. Operators who want the previous behavior can opt in by setting <code>plugins.entries.memory-core.config.dreaming.storage.mode: "inline"</code>. (#66412) Thanks @mjamiv.</li>
<li>Control UI/Overview: fix false-positive "missing" alerts on the Model Auth status card for aliased providers, env-backed OAuth with auth.profiles, and unresolvable env SecretRefs. (#67253) Thanks @omarshahine.</li>
<li>Dashboard: constrain exec approval modal overflow on desktop so long command content no longer pushes action buttons out of view. (#67082) Thanks @Ziy1-Tan.</li>
<li>Agents/CLI transcripts: persist successful CLI-backed turns into the OpenClaw session transcript so google-gemini-cli replies appear in session history and the Control UI again. (#67490) Thanks @obviyus.</li>
<li>Discord/tool-call text: strip standalone Gemma-style <code><function>...</function></code> tool-call payloads from visible assistant text without truncating prose examples or trailing replies. (#67318) Thanks @joelnishanth.</li>
<li>WhatsApp/web-session: drain the pending per-auth creds save queue before reopening sockets so reconnect-time auth bootstrap no longer races in-flight <code>creds.json</code> writes and falsely restores from backup. (#67464) Thanks @neeravmakwana.</li>
<li>BlueBubbles/catchup: add a per-message retry ceiling (<code>catchup.maxFailureRetries</code>, default 10) so a persistently-failing message with a malformed payload no longer wedges the catchup cursor forever. After N consecutive <code>processMessage</code> failures against the same GUID, catchup logs a WARN, skips that message on subsequent sweeps, and lets the cursor advance past it. Transient failures still retry from the same point as before. Also fixes a lost-update race in the persistent dedupe file lock that silently dropped inbound GUIDs on concurrent writes, a dedupe file naming migration gap on version upgrade, and a balloon-event bypass that let catchup replay debouncer-coalesced events as standalone messages. (#67426, #66870) Thanks @omarshahine.</li>
<li>Ollama/chat: strip the <code>ollama/</code> provider prefix from Ollama chat request model ids so configured refs like <code>ollama/qwen3:14b-q8_0</code> stop 404ing against the Ollama API. (#67457) Thanks @suboss87.</li>
<li>Agents/tools: resolve non-workspace host tilde paths against the OS home directory and keep edit recovery aligned with that same path target, so <code>~/...</code> host edit/write operations stop failing or reading back the wrong file when <code>OPENCLAW_HOME</code> differs. (#62804) Thanks @stainlu.</li>
<li>Speech/TTS: auto-enable the bundled Microsoft and ElevenLabs speech providers, and route generic TTS directive tokens through the explicit or active provider first so overrides like <code>[[tts:speed=1.2]]</code> stop silently landing on the wrong provider. (#62846) Thanks @stainlu.</li>
<li>OpenAI Codex/models: normalize stale native transport metadata in both runtime resolution and discovery/listing so legacy <code>openai-codex</code> rows with missing <code>api</code> or <code>https://chatgpt.com/backend-api/v1</code> self-heal to the canonical Codex transport instead of routing requests through broken HTML/Cloudflare paths, combining the original fixes proposed in #66969 (saamuelng601-pixel) and #67159 (hclsys). (#67635)</li>
<li>Agents/failover: treat HTML provider error pages as upstream transport failures for CDN-style 5xx responses without misclassifying embedded body text as API rate limits, while still preserving auth remediation for HTML 401/403 pages and proxy remediation for HTML 407 pages. (#67642) Thanks @stainlu.</li>
<li>Gateway/skills: bump the cached skills-snapshot version whenever a config write touches <code>skills.*</code> (for example <code>skills.allowBundled</code>, <code>skills.entries.<id>.enabled</code>, or <code>skills.profile</code>). Existing agent sessions persist a <code>skillsSnapshot</code> in <code>sessions.json</code> that reuses the skill list frozen at session creation; without this invalidation, removing a bundled skill from the allowlist left the old snapshot live and the model kept calling the disabled tool, producing <code>Tool <name> not found</code> loops that ran until the embedded-run timeout. (#67401) Thanks @xantorres.</li>
<li>Agents/tool-loop: enable the unknown-tool stream guard by default. Previously <code>resolveUnknownToolGuardThreshold</code> returned <code>undefined</code> unless <code>tools.loopDetection.enabled</code> was explicitly set to <code>true</code>, which left the protection off in the default configuration. A hallucinated or removed tool (for example <code>himalaya</code> after it was dropped from <code>skills.allowBundled</code>) would then loop "Tool X not found" attempts until the full embedded-run timeout. The guard has no false-positive surface because it only triggers on tools that are objectively not registered in the run, so it now stays on regardless of <code>tools.loopDetection.enabled</code> and still accepts <code>tools.loopDetection.unknownToolThreshold</code> as a per-run override (default 10). (#67401) Thanks @xantorres.</li>
<li>TUI/streaming: add a client-side streaming watchdog to <code>tui-event-handlers</code> so the <code>streaming · Xm Ys</code> activity indicator resets to <code>idle</code> after 30s of delta silence on the active run. Guards against lost or late <code>state: "final"</code> chat events (WS reconnects, gateway restarts, etc.) leaving the TUI stuck on <code>streaming</code> indefinitely; a new system log line surfaces the reset so users know to send a new message to resync. The window is configurable via the new <code>streamingWatchdogMs</code> context option (set to <code>0</code> to disable), and the handler now exposes a <code>dispose()</code> that clears the pending timer on shutdown. (#67401) Thanks @xantorres.</li>
<li>Extensions/lmstudio: add exponential backoff to the inference-preload wrapper so an LM Studio model-load failure (for example the built-in memory guardrail rejecting a load because the swap is saturated) no longer produces a WARN line every ~2s for every chat request. The wrapper now records consecutive preload failures per <code>(baseUrl, modelKey, contextLength)</code> tuple with a 5s → 10s → 20s → … → 5min cooldown and skips the preload step entirely while a cooldown is active, letting chat requests proceed directly to the stream (the model is often already loaded via the LM Studio UI). The combined <code>preload failed</code> log line now reports consecutive-failure count and remaining cooldown so operators can act on the real issue instead of drowning in repeated warnings. (#67401) Thanks @xantorres.</li>
<li>Agents/replay: re-run tool/result pairing after strict replay tool-call ID sanitization on outbound requests so Anthropic-compatible providers like MiniMax no longer receive malformed orphan tool-result IDs such as <code>...toolresult1</code> during compaction and retry flows. (#67620) Thanks @stainlu.</li>
<li>Gateway/startup: fix spurious SIGUSR1 restart loop on Linux/systemd when plugin auto-enable is the only startup config write; the config hash guard was not captured for that write path, causing chokidar to treat each boot write as an external change and trigger a reload → restart cycle that corrupts manifest.db after repeated cycles. Fixes #67436. (#67557) thanks @openperf</li>
<li>Codex/harness: auto-enable the Codex plugin when <code>codex</code> is selected as an embedded agent harness runtime, including forced default, per-agent, and <code>OPENCLAW_AGENT_RUNTIME</code> paths. (#67474) Thanks @duqaXxX.</li>
<li>OpenAI Codex/CLI: keep resumed <code>codex exec resume</code> runs on the safe non-interactive path without reintroducing the removed dangerous bypass flag by passing the supported <code>--skip-git-repo-check</code> resume arg plus Codex's native <code>sandbox_mode="workspace-write"</code> config override. (#67666) Thanks @plgonzalezrx8.</li>
<li>Codex/app-server: parse Desktop-originated app-server user agents such as <code>Codex Desktop/0.118.0</code>, keeping the version gate working when the Codex CLI inherits a multi-word originator. (#64666) Thanks @cyrusaf.</li>
<li>Cron/announce delivery: keep isolated announce <code>NO_REPLY</code> stripping case-insensitive across direct and text delivery, preserve structured media-only sends when a caption strips silent, and derive main-session awareness from the cleaned payloads so silent captions no longer leak stale <code>NO_REPLY</code> text. (#65016) Thanks @BKF-Gitty.</li>
<li>Sessions/Codex: skip redundant <code>delivery-mirror</code> transcript appends only when the latest assistant message has the same visible text, preventing duplicate visible replies on Codex-backed turns without suppressing repeated answers across turns. (#67185) Thanks @andyylin.</li>
<li>Auto-reply/prompt-cache: keep volatile inbound chat IDs out of the stable system prompt so task-scoped adapters can reuse prompt caches across runs, while preserving conversation metadata for the user turn and media-only messages. (#65071) Thanks @MonkeyLeeT.</li>
<li>BlueBubbles/inbound: restore inbound image attachment downloads on Node 22+ by stripping incompatible bundled-undici dispatchers from the non-SSRF fetch path, accept <code>updated-message</code> webhooks carrying attachments, use event-type-aware dedup keys so attachment follow-ups are not rejected as duplicates, and retry attachment fetch from the BB API when the initial webhook arrives with an empty array. (#64105, #61861, #65430, #67510) Thanks @omarshahine.</li>
<li>Agents/skills: sort prompt-facing <code>available_skills</code> entries by skill name after merging sources so <code>skills.load.extraDirs</code> order no longer changes prompt-cache prefixes. (#64198) Thanks @Bartok9.</li>
<li>Agents/OpenAI Responses: add <code>models.providers.*.models.*.compat.supportsPromptCacheKey</code> so OpenAI-compatible proxies that forward <code>prompt_cache_key</code> can keep prompt caching enabled while incompatible endpoints can still force stripping. (#67427) Thanks @damselem.</li>
<li>Agents/context engines: keep loop-hook and final <code>afterTurn</code> prompt-cache touch metadata aligned with the current assistant turn so cache-aware context engines retain accurate cache TTL state during tool loops. (#67767) thanks @jalehman.</li>
<li>Memory/dreaming: strip AI-facing inbound metadata envelopes from session-corpus user turns before normalization so REM topic extraction sees the user's actual message text, including array-shaped split envelopes. (#66548) Thanks @zqchris.</li>
<li>Agents/errors: detect standalone Cloudflare/CDN HTML challenge pages before transport DNS classification so provider block pages no longer appear as local DNS lookup failures. (#67704) Thanks @chris-yyau.</li>
<li>Security/approvals: redact secrets in exec approval prompts so inline approval review can no longer leak credential material in rendered prompt content. (#61077, #64790)</li>
<li>CLI/configure: re-read the persisted config hash after writes so config updates stop failing with stale-hash races. (#64188, #66528)</li>
<li>CLI/update: prune stale packaged <code>dist</code> chunks after npm upgrades and keep downgrade/verify inventory checks compat-safe so global upgrades stop failing on stale chunk imports. (#66959) Thanks @obviyus.</li>
<li>Onboarding/CLI: fix channel-selection crashes on globally installed CLI setups during onboarding. (#66736)</li>
<li>Video generation/live tests: bound provider polling for live video smoke, default to the fast non-FAL text-to-video path, and use a one-second lobster prompt so release validation no longer waits indefinitely on slow provider queues.</li>
<li>Memory-core/QMD <code>memory_get</code>: reject reads of arbitrary workspace markdown paths and only allow canonical memory files (<code>MEMORY.md</code>, <code>memory.md</code>, <code>DREAMS.md</code>, <code>dreams.md</code>, <code>memory/**</code>) plus exact paths of active indexed QMD workspace documents, so the QMD memory backend can no longer be used as a generic workspace-file read shim that bypasses <code>read</code> tool-policy denials. (#66026) Thanks @eleqtrizit.</li>
<li>Cron/agents: forward embedded-run tool policy and internal event params into the attempt layer so <code>--tools</code> allowlists, cron-owned message-tool suppression, explicit message targeting, and command-path internal events all take effect at runtime again. (#62675) Thanks @hexsprite.</li>
<li>Setup/providers: guard preferred-provider lookup during setup so malformed plugin metadata with a missing provider id no longer crashes the wizard with <code>Cannot read properties of undefined (reading 'trim')</code>. (#66649) Thanks @Tianworld.</li>
<li>Matrix/security: normalize sandboxed profile avatar params, preserve <code>mxc://</code> avatar URLs, and surface gmail watcher stop failures during reload. (#64701) Thanks @slepybear.</li>
<li>Telegram/documents: drop leaked binary caption bytes from inbound Telegram text handling so document uploads like <code>.mobi</code> or <code>.epub</code> no longer explode prompt token counts. (#66663) Thanks @joelnishanth.</li>
<li>Gateway/auth: resolve the active gateway bearer per-request on the HTTP server and the HTTP upgrade handler via <code>getResolvedAuth()</code>, mirroring the WebSocket path, so a secret rotated through <code>secrets.reload</code> or config hot-reload stops authenticating on <code>/v1/*</code>, <code>/tools/invoke</code>, plugin HTTP routes, and the canvas upgrade path immediately instead of remaining valid on HTTP until gateway restart. (#66651) Thanks @mmaps.</li>
<li>Agents/compaction: cap the compaction reserve-token floor to the model context window so small-context local models (e.g. Ollama with 16K tokens) no longer trigger context-overflow errors or infinite compaction loops on every prompt. (#65671) Thanks @openperf.</li>
<li>Agents/OpenAI Responses: classify the exact <code>Unknown error (no error details in response)</code> transport failure as failover reason <code>unknown</code> so assistant/model fallback still runs for that no-details failure path. (#65254) Thanks @OpenCodeEngineer.</li>
<li>Models/probe: surface invalid-model probe failures as <code>format</code> instead of <code>unknown</code> in <code>models list --probe</code>, and lock the invalid-model fallback path in with regression coverage. (#50028) Thanks @xiwuqi.</li>
<li>Agents/failover: classify OpenAI-compatible <code>finish_reason: network_error</code> stream failures as timeout so model fallback retries continue instead of stopping with an unknown failover reason. (#61784) thanks @lawrence3699.</li>
<li>Onboarding/channels: normalize channel setup metadata before discovery and validation so malformed or mixed-shape channel plugin metadata no longer breaks setup and onboarding channel lists. (#66706) Thanks @darkamenosa.</li>
<li>Slack/native commands: fix option menus for slash commands such as <code>/verbose</code> when Slack renders native buttons by giving each button a unique action ID while still routing them through the shared <code>openclaw_cmdarg*</code> listener. Thanks @Wangmerlyn.</li>
<li>Feishu/webhook: harden the webhook transport and card-action replay guards to fail closed on missing <code>encryptKey</code> and blank callback tokens — refuse to start the webhook transport without an <code>encryptKey</code>, reject unsigned requests when no key is present instead of accepting them, and drop blank card-action tokens before the dedupe claim and dispatcher. Defense-in-depth over the already-closed monitor-account layer. (#66707) Thanks @eleqtrizit.</li>
<li>Agents/workspace files: route <code>agents.files.get</code>, <code>agents.files.set</code>, and workspace listing through the shared <code>fs-safe</code> helpers (<code>openFileWithinRoot</code>/<code>readFileWithinRoot</code>/<code>writeFileWithinRoot</code>), reject symlink aliases for allowlisted agent files, and have <code>fs-safe</code> resolve opened-file real paths from the file descriptor before falling back to path-based <code>realpath</code> so a symlink swap between <code>open</code> and <code>realpath</code> can no longer redirect the validated path off the intended inode. (#66636) Thanks @eleqtrizit.</li>
<li>Gateway/MCP loopback: switch the <code>/mcp</code> bearer comparison from plain <code>!==</code> to constant-time <code>safeEqualSecret</code> (matching the convention every other auth surface in the codebase uses), and reject non-loopback browser-origin requests via <code>checkBrowserOrigin</code> before the auth gate runs. Loopback origins (<code>127.0.0.1:*</code>, <code>localhost:*</code>, same-origin) still go through, including the <code>localhost</code>↔<code>127.0.0.1</code> host mismatch that browsers flag as <code>Sec-Fetch-Site: cross-site</code>. (#66665) Thanks @eleqtrizit.</li>
<li>Auto-reply/billing: classify pure billing cooldown fallback summaries from structured fallback reasons so users see billing guidance instead of the generic failure reply. (#66363) Thanks @Rohan5commit.</li>
<li>Agents/fallback: preserve the original prompt body on model fallback retries with session history so the retrying model keeps the active task instead of only seeing a generic continue message. (#66029) Thanks @WuKongAI-CMU.</li>
<li>Reply/secrets: resolve active reply channel/account SecretRefs before reply-run message-action discovery so channel token SecretRefs (for example Discord) do not degrade into discovery-time unresolved-secret failures. (#66796) Thanks @joshavant.</li>
<li>Agents/Anthropic: ignore non-positive Anthropic Messages token overrides and fail locally when no positive token budget remains, so invalid <code>max_tokens</code> values no longer reach the provider API. (#66664) thanks @jalehman</li>
<li>Agents/context engines: preserve prompt-only token counts, not full request totals, when deferred maintenance reuses after-turn runtime context so background compaction bookkeeping matches the active prompt window. (#66820) thanks @jalehman.</li>
<li>BlueBubbles/inbound: add a persistent file-backed GUID dedupe so MessagePoller webhook replays after BB Server restart or reconnect no longer cause the agent to re-reply to already-handled messages. (#19176, #12053, #66816) Thanks @omarshahine.</li>
<li>Secrets/plugins/status: align SecretRef inspect-vs-strict handling across plugin preload, read-only status/agents surfaces, and runtime auth paths so unresolved refs no longer crash read-only CLI flows while runtime-required non-env refs stay strict. (#66818) Thanks @joshavant.</li>
<li>Memory/dreaming: stop ordinary transcripts that merely quote the dream-diary prompt from being classified as internal dreaming runs and silently dropped from session recall ingestion. (#66852) Thanks @gumadeiras.</li>
<li>Telegram/documents: sanitize binary reply context and ZIP-like archive extraction so <code>.epub</code> and <code>.mobi</code> uploads can no longer leak raw binary into prompt context through reply metadata or archive-to-<code>text/plain</code> coercion. (#66877) Thanks @martinfrancois.</li>
<li>Telegram/native commands: restore plugin-registry-backed auto defaults for native commands and native skills so Telegram slash commands keep registering when <code>commands.native</code> and <code>commands.nativeSkills</code> stay on <code>auto</code>. (#66843) Thanks @kashevk0.</li>
<li>OpenRouter/Qwen3: parse <code>reasoning_details</code> stream deltas as thinking content without skipping same-chunk tool calls, so Qwen3 replies no longer fail empty on OpenRouter and mixed reasoning/tool-call chunks still execute normally. (#66905) Thanks @bladin.</li>
<li>BlueBubbles/catchup: replay missed webhook messages after gateway restart via a persistent per-account cursor and <code>/api/v1/message/query?after=<ts></code> pass, so messages delivered while the gateway was down no longer disappear. Uses the existing <code>processMessage</code> path and is deduped by #66816's inbound GUID cache. (#66857, #66721) Thanks @omarshahine.</li>
<li>Telegram/native commands: keep Telegram command-sync cache process-local so gateway restarts re-register the menu instead of trusting stale on-disk sync state after Telegram cleared commands out-of-band. (#66730) Thanks @nightq.</li>
<li>Audio/self-hosted STT: restore <code>models.providers.*.request.allowPrivateNetwork</code> for audio transcription so private or LAN speech-to-text endpoints stop tripping SSRF blocks after the v2026.4.14 regression. (#66692) Thanks @jhsmith409.</li>
<li>Auto-reply/media: allow workspace-rooted absolute media paths in auto-reply send flows so valid local media references no longer fail path validation. (#66689)</li>
<li>WhatsApp/Baileys media upload: harden encrypted upload handling so large outbound media sends avoid buffer spikes and reliability regressions. (#65966) Thanks @frankekn.</li>
<li>QQBot/cron: guard against undefined <code>event.content</code> in <code>parseFaceTags</code> and <code>filterInternalMarkers</code> so cron-triggered agent turns with no content payload no longer crash with <code>TypeError: Cannot read properties of undefined (reading 'startsWith')</code>. (#66302) Thanks @xinmotlanthua.</li>
<li>CLI/plugins: stop <code>--dangerously-force-unsafe-install</code> plugin installs from falling back to hook-pack installs after security scan failures, while still preserving non-security fallback behavior for real hook packs. (#58909) Thanks @hxy91819.</li>
<li>Claude CLI/sessions: classify <code>No conversation found with session ID</code> as <code>session_expired</code> so expired CLI-backed conversations clear the stale binding and recover on the next turn. (#65028) thanks @Ivan-Fn.</li>
<li>Context Engine: gracefully fall back to the legacy engine when a third-party context engine plugin fails at resolution time (unregistered id, factory throw, or contract violation), preventing a full gateway outage on every channel. (#66930) Thanks @openperf.</li>
<li>Control UI/chat: keep optimistic user message cards visible during active sends by deferring same-session history reloads until the active run ends, including aborted and errored runs. (#66997) Thanks @scotthuang and @vincentkoc.</li>
<li>Media/Slack: allow host-local CSV and Markdown uploads only when the fallback buffer actually decodes as text, so real plain-text files work without letting opaque non-text blobs renamed to <code>.csv</code> or <code>.md</code> slip past the host-read guard. (#67047) Thanks @Unayung.</li>
<li>Ollama/onboarding: split setup into <code>Cloud + Local</code>, <code>Cloud only</code>, and <code>Local only</code>, support direct <code>OLLAMA_API_KEY</code> cloud setup without a local daemon, and keep Ollama web search on the local-host path. (#67005) Thanks @obviyus.</li>
<li>Webchat/security: reject remote-host <code>file://</code> URLs in the media embedding path. (#67293) Thanks @pgondhi987.</li>
<li>Dreaming/memory-core: use the ingestion day, not the source file day, for daily recall dedupe so repeat sweeps of the same daily note can increment <code>dailyCount</code> across days instead of stalling at <code>1</code>. (#67091) Thanks @Bartok9.</li>
<li>Node-host/tools.exec: let approval binding distinguish known native binaries from mutable shell payload files, while still fail-closing unknown or racy file probes so absolute-path node-host commands like <code>/usr/bin/whoami</code> no longer get rejected as unsafe interpreter/runtime commands. (#66731) Thanks @tmimmanuel.</li>
</ul>
<p><a href="https://github.com/openclaw/openclaw/blob/main/CHANGELOG.md">View full changelog</a></p>
]]></description>
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.4.15/OpenClaw-2026.4.15.zip" length="47501638" type="application/octet-stream" sparkle:edSignature="JUG3cicpJqCQDvp7VYoN6qBuN4Kn4s0+QQFjlMR69OZlwViLdiStPIHa+1vpuoR4miYhJc9knSDVCFzSfQuYCQ=="/>
</item>
<item>
<title>2026.4.14</title>
<pubDate>Tue, 14 Apr 2026 14:08:09 +0000</pubDate>

View File

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

View File

@@ -12,6 +12,7 @@ import java.io.IOException
import java.net.InetSocketAddress
import java.nio.ByteBuffer
import java.nio.charset.CodingErrorAction
import java.time.Duration
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.Executor
import java.util.concurrent.Executors
@@ -132,38 +133,38 @@ class GatewayDiscovery(
object : NsdManager.ResolveListener {
override fun onResolveFailed(serviceInfo: NsdServiceInfo, errorCode: Int) {}
override fun onServiceResolved(resolved: NsdServiceInfo) {
val host = resolved.host?.hostAddress ?: return
val port = resolved.port
if (port <= 0) return
override fun onServiceResolved(resolved: NsdServiceInfo) {
val host = resolved.host?.hostAddress ?: return
val port = resolved.port
if (port <= 0) return
val rawServiceName = resolved.serviceName
val serviceName = BonjourEscapes.decode(rawServiceName)
val displayName = BonjourEscapes.decode(txt(resolved, "displayName") ?: serviceName)
val lanHost = txt(resolved, "lanHost")
val tailnetDns = txt(resolved, "tailnetDns")
val gatewayPort = txtInt(resolved, "gatewayPort")
val canvasPort = txtInt(resolved, "canvasPort")
val tlsEnabled = txtBool(resolved, "gatewayTls")
val tlsFingerprint = txt(resolved, "gatewayTlsSha256")
val id = stableId(serviceName, "local.")
localById[id] =
GatewayEndpoint(
stableId = id,
name = displayName,
host = host,
port = port,
lanHost = lanHost,
tailnetDns = tailnetDns,
gatewayPort = gatewayPort,
canvasPort = canvasPort,
tlsEnabled = tlsEnabled,
tlsFingerprintSha256 = tlsFingerprint,
)
publish()
}
},
)
val rawServiceName = resolved.serviceName
val serviceName = BonjourEscapes.decode(rawServiceName)
val displayName = BonjourEscapes.decode(txt(resolved, "displayName") ?: serviceName)
val lanHost = txt(resolved, "lanHost")
val tailnetDns = txt(resolved, "tailnetDns")
val gatewayPort = txtInt(resolved, "gatewayPort")
val canvasPort = txtInt(resolved, "canvasPort")
val tlsEnabled = txtBool(resolved, "gatewayTls")
val tlsFingerprint = txt(resolved, "gatewayTlsSha256")
val id = stableId(serviceName, "local.")
localById[id] =
GatewayEndpoint(
stableId = id,
name = displayName,
host = host,
port = port,
lanHost = lanHost,
tailnetDns = tailnetDns,
gatewayPort = gatewayPort,
canvasPort = canvasPort,
tlsEnabled = tlsEnabled,
tlsFingerprintSha256 = tlsFingerprint,
)
publish()
}
},
)
}
private fun publish() {
@@ -350,7 +351,7 @@ class GatewayDiscovery(
}
private fun records(msg: Message?, section: Int): List<Record> {
return msg?.getSectionArray(section)?.toList() ?: emptyList()
return msg?.getSection(section).orEmpty()
}
private fun keyName(raw: String): String {
@@ -426,14 +427,14 @@ class GatewayDiscovery(
try {
SimpleResolver().apply {
setAddress(InetSocketAddress(addr, 53))
setTimeout(3)
setTimeout(Duration.ofSeconds(3))
}
} catch (_: Throwable) {
null
}
}
if (resolvers.isEmpty()) return null
ExtendedResolver(resolvers.toTypedArray()).apply { setTimeout(3) }
ExtendedResolver(resolvers.toTypedArray()).apply { setTimeout(Duration.ofSeconds(3)) }
} catch (_: Throwable) {
null
}

View File

@@ -56,10 +56,10 @@ fun CanvasScreen(viewModel: MainViewModel, visible: Boolean, modifier: Modifier
settings.builtInZoomControls = false
settings.displayZoomControls = false
settings.setSupportZoom(false)
// targetSdk 33+ ignores Force Dark APIs, so only opt out through the supported
// algorithmic darkening flag when this WebView implementation exposes it.
if (WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING)) {
WebSettingsCompat.setAlgorithmicDarkeningAllowed(settings, false)
} else {
disableForceDarkIfSupported(settings)
}
if (isDebuggable) {
Log.d("OpenClawWebView", "userAgent: ${settings.userAgentString}")
@@ -157,12 +157,6 @@ fun CanvasScreen(viewModel: MainViewModel, visible: Boolean, modifier: Modifier
)
}
private fun disableForceDarkIfSupported(settings: WebSettings) {
if (!WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) return
@Suppress("DEPRECATION")
WebSettingsCompat.setForceDark(settings, WebSettingsCompat.FORCE_DARK_OFF)
}
internal class CanvasA2UIActionBridge(
private val isTrustedPage: () -> Boolean,
private val onMessage: (String) -> Unit,

View File

@@ -1,5 +1,9 @@
# OpenClaw iOS Changelog
## 2026.4.16 - 2026-04-17
Maintenance update for the current OpenClaw release.
## 2026.4.15 - 2026-04-15
Maintenance update for the current OpenClaw beta release.

View File

@@ -2,8 +2,8 @@
// Source of truth: apps/ios/version.json
// Generated by scripts/ios-sync-versioning.ts.
OPENCLAW_IOS_VERSION = 2026.4.15
OPENCLAW_MARKETING_VERSION = 2026.4.15
OPENCLAW_IOS_VERSION = 2026.4.16
OPENCLAW_MARKETING_VERSION = 2026.4.16
OPENCLAW_BUILD_VERSION = 1
#include? "../build/Version.xcconfig"

View File

@@ -1 +1 @@
Maintenance update for the current OpenClaw beta release.
Maintenance update for the current OpenClaw release.

View File

@@ -1,3 +1,3 @@
{
"version": "2026.4.15"
"version": "2026.4.16"
}

View File

@@ -15,9 +15,9 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>2026.4.15-beta.1</string>
<string>2026.4.16</string>
<key>CFBundleVersion</key>
<string>2026041501</string>
<string>2026041690</string>
<key>CFBundleIconFile</key>
<string>OpenClaw</string>
<key>CFBundleURLTypes</key>

View File

@@ -1,4 +1,4 @@
900c26a9b060f1dfa712abfba877bd3bf9c7b0c9f2294faf9834038283ec24b6 config-baseline.json
d956a1d60f776bba712cb04374a4f5657cad95bb088b536c5e3e4e29d4a21328 config-baseline.core.json
ef83a06633fc001b5b2535566939186ecb49d05cd1a90b40e54cc58d3e6e44e3 config-baseline.channel.json
3c87ac2fc4c234348eb88812d1904724d7492890498f101d953bc761da8fdead config-baseline.json
eeed6fe659078632d9f95b3350b27103b4aba282d050ff38d3b0953a456d242d config-baseline.core.json
99bb34fcf83ba6bb50a3fc11f170bd379bee5728b0938707fc39ebd7638e12eb config-baseline.channel.json
5f5d4e850df6e9854a85b5d008236854ce185c707fdbb566efcf00f8c08b36e3 config-baseline.plugin.json

View File

@@ -1,2 +1,2 @@
73091009a0a45c72eded8003fdf9cf4c10e9470c4a055592a98ea00d55cd45d1 plugin-sdk-api-baseline.json
9c9d59ffc0b3b6677794cb8fd5afd0208dbc9f3cd1ad59b30ee627f6f6352929 plugin-sdk-api-baseline.jsonl
9683f324fae8f455f2b64d7e152a77009941e4c7558521bca2510d8bcf573af9 plugin-sdk-api-baseline.json
097bf226e4e857e9296d0851852a2963c6263d176c4c470452d9a8efd36988e5 plugin-sdk-api-baseline.jsonl

View File

@@ -116,6 +116,91 @@ What this means:
- `config.promptStyle: "balanced"` uses the default general-purpose prompt style for `recent` mode
- active memory still runs only on eligible interactive persistent chat sessions
## Speed recommendations
The simplest setup is to leave `config.model` unset and let Active Memory use
the same model you already use for normal replies. That is the safest default
because it follows your existing provider, auth, and model preferences.
If you want Active Memory to feel faster, use a dedicated inference model
instead of borrowing the main chat model.
Example fast-provider setup:
```json5
models: {
providers: {
cerebras: {
baseUrl: "https://api.cerebras.ai/v1",
apiKey: "${CEREBRAS_API_KEY}",
api: "openai-completions",
models: [{ id: "gpt-oss-120b", name: "GPT OSS 120B (Cerebras)" }],
},
},
},
plugins: {
entries: {
"active-memory": {
enabled: true,
config: {
model: "cerebras/gpt-oss-120b",
},
},
},
}
```
Fast-model options worth considering:
- `cerebras/gpt-oss-120b` for a fast dedicated recall model with a narrow tool surface
- your normal session model, by leaving `config.model` unset
- a low-latency fallback model such as `google/gemini-3-flash` when you want a separate recall model without changing your primary chat model
Why Cerebras is a strong speed-oriented option for Active Memory:
- the Active Memory tool surface is narrow: it only calls `memory_search` and `memory_get`
- recall quality matters, but latency matters more than for the main answer path
- a dedicated fast provider avoids tying memory recall latency to your primary chat provider
If you do not want a separate speed-optimized model, leave `config.model` unset
and let Active Memory inherit the current session model.
### Cerebras setup
Add a provider entry like this:
```json5
models: {
providers: {
cerebras: {
baseUrl: "https://api.cerebras.ai/v1",
apiKey: "${CEREBRAS_API_KEY}",
api: "openai-completions",
models: [{ id: "gpt-oss-120b", name: "GPT OSS 120B (Cerebras)" }],
},
},
}
```
Then point Active Memory at it:
```json5
plugins: {
entries: {
"active-memory": {
enabled: true,
config: {
model: "cerebras/gpt-oss-120b",
},
},
},
}
```
Caveat:
- make sure the Cerebras API key actually has model access for the model you choose, because `/v1/models` visibility alone does not guarantee `chat/completions` access
## How to see it
Active memory injects a hidden untrusted prompt prefix for the model. It does

View File

@@ -62,7 +62,10 @@ That lane provisions a disposable Tuwunel homeserver in Docker, registers
temporary driver, SUT, and observer users, creates one private room, then runs
the real Matrix plugin inside a QA gateway child. The live transport lane keeps
the child config scoped to the transport under test, so Matrix runs without
`qa-channel` in the child config.
`qa-channel` in the child config. It writes the structured report artifacts and
a combined stdout/stderr log into the selected Matrix QA output directory. To
capture the outer `scripts/run-node.mjs` build/launcher output too, set
`OPENCLAW_RUN_NODE_OUTPUT_LOG=<path>` to a repo-local log file.
For a transport-real Telegram smoke lane, run:
@@ -148,6 +151,22 @@ The baseline list should stay broad enough to cover:
- repo-reading and docs-reading
- one small build task such as Lobster Invaders
## Provider mock lanes
`qa suite` has two local provider mock lanes:
- `mock-openai` is the scenario-aware OpenClaw mock. It remains the default
deterministic mock lane for repo-backed QA and parity gates.
- `aimock` starts an AIMock-backed provider server for experimental protocol,
fixture, record/replay, and chaos coverage. It is additive and does not
replace the `mock-openai` scenario dispatcher.
Provider-lane implementation lives under `extensions/qa-lab/src/providers/`.
Each provider owns its defaults, local server startup, gateway model config,
auth-profile staging needs, and live/mock capability flags. Shared suite and
gateway code should route through the provider registry instead of branching on
provider names.
## Transport adapters
`qa-lab` owns a generic transport seam for markdown QA scenarios.

View File

@@ -177,6 +177,19 @@ and the effective agent skill allowlist when `agents.defaults.skills` or
This keeps the base prompt small while still enabling targeted skill usage.
The skills list budget is owned by the skills subsystem:
- Global default: `skills.limits.maxSkillsPromptChars`
- Per-agent override: `agents.list[].skillsLimits.maxSkillsPromptChars`
Generic bounded runtime excerpts use a different surface:
- `agents.defaults.contextLimits.*`
- `agents.list[].contextLimits.*`
That split keeps skills sizing separate from runtime read/injection sizing such
as `memory_get`, live tool results, and post-compaction AGENTS.md refreshes.
## Documentation
When available, the system prompt includes a **Documentation** section that points to the

View File

@@ -221,7 +221,7 @@ The bundled OpenAI plugin also registers a default for `codex-cli`:
- `command: "codex"`
- `args: ["exec","--json","--color","never","--sandbox","workspace-write","--skip-git-repo-check"]`
- `resumeArgs: ["exec","resume","{sessionId}","--color","never","--sandbox","workspace-write","--skip-git-repo-check"]`
- `resumeArgs: ["exec","resume","{sessionId}","-c","sandbox_mode=\"workspace-write\"","--skip-git-repo-check"]`
- `output: "jsonl"`
- `resumeOutput: "text"`
- `modelArg: "--model"`

View File

@@ -988,6 +988,142 @@ Default: `"once"`.
}
```
### Context budget ownership map
OpenClaw has multiple high-volume prompt/context budgets, and they are
intentionally split by subsystem instead of all flowing through one generic
knob.
- `agents.defaults.bootstrapMaxChars` /
`agents.defaults.bootstrapTotalMaxChars`:
normal workspace bootstrap injection.
- `agents.defaults.startupContext.*`:
one-shot `/new` and `/reset` startup prelude, including recent daily
`memory/*.md` files.
- `skills.limits.*`:
the compact skills list injected into the system prompt.
- `agents.defaults.contextLimits.*`:
bounded runtime excerpts and injected runtime-owned blocks.
- `memory.qmd.limits.*`:
indexed memory-search snippet and injection sizing.
Use the matching per-agent override only when one agent needs a different
budget:
- `agents.list[].skillsLimits.maxSkillsPromptChars`
- `agents.list[].contextLimits.*`
#### `agents.defaults.startupContext`
Controls the first-turn startup prelude injected on bare `/new` and `/reset`
runs.
```json5
{
agents: {
defaults: {
startupContext: {
enabled: true,
applyOn: ["new", "reset"],
dailyMemoryDays: 2,
maxFileBytes: 16384,
maxFileChars: 1200,
maxTotalChars: 2800,
},
},
},
}
```
#### `agents.defaults.contextLimits`
Shared defaults for bounded runtime context surfaces.
```json5
{
agents: {
defaults: {
contextLimits: {
memoryGetMaxChars: 12000,
memoryGetDefaultLines: 120,
toolResultMaxChars: 16000,
postCompactionMaxChars: 1800,
},
},
},
}
```
- `memoryGetMaxChars`: default `memory_get` excerpt cap before truncation
metadata and continuation notice are added.
- `memoryGetDefaultLines`: default `memory_get` line window when `lines` is
omitted.
- `toolResultMaxChars`: live tool-result cap used for persisted results and
overflow recovery.
- `postCompactionMaxChars`: AGENTS.md excerpt cap used during post-compaction
refresh injection.
#### `agents.list[].contextLimits`
Per-agent override for the shared `contextLimits` knobs. Omitted fields inherit
from `agents.defaults.contextLimits`.
```json5
{
agents: {
defaults: {
contextLimits: {
memoryGetMaxChars: 12000,
toolResultMaxChars: 16000,
},
},
list: [
{
id: "tiny-local",
contextLimits: {
memoryGetMaxChars: 6000,
toolResultMaxChars: 8000,
},
},
],
},
}
```
#### `skills.limits.maxSkillsPromptChars`
Global cap for the compact skills list injected into the system prompt. This
does not affect reading `SKILL.md` files on demand.
```json5
{
skills: {
limits: {
maxSkillsPromptChars: 18000,
},
},
}
```
#### `agents.list[].skillsLimits.maxSkillsPromptChars`
Per-agent override for the skills prompt budget.
```json5
{
agents: {
list: [
{
id: "tiny-local",
skillsLimits: {
maxSkillsPromptChars: 6000,
},
},
],
},
}
```
### `agents.defaults.imageMaxDimensionPx`
Max pixel size for the longest image side in transcript/tool image blocks before provider calls.

View File

@@ -73,10 +73,24 @@ Gateway → Client:
"type": "res",
"id": "…",
"ok": true,
"payload": { "type": "hello-ok", "protocol": 3, "policy": { "tickIntervalMs": 15000 } }
"payload": {
"type": "hello-ok",
"protocol": 3,
"server": { "version": "…", "connId": "…" },
"features": { "methods": ["…"], "events": ["…"] },
"snapshot": { "…": "…" },
"policy": {
"maxPayload": 26214400,
"maxBufferedBytes": 52428800,
"tickIntervalMs": 15000
}
}
}
```
`server`, `features`, `snapshot`, and `policy` are all required by the schema
(`src/gateway/protocol/schema/frames.ts`). `auth` and `canvasHostUrl` are optional.
When a device token is issued, `hello-ok` also includes:
```json
@@ -492,13 +506,36 @@ implemented in `src/gateway/server-methods/*.ts`.
## Versioning
- `PROTOCOL_VERSION` lives in `src/gateway/protocol/schema.ts`.
- `PROTOCOL_VERSION` lives in `src/gateway/protocol/schema/protocol-schemas.ts`.
- Clients send `minProtocol` + `maxProtocol`; the server rejects mismatches.
- Schemas + models are generated from TypeBox definitions:
- `pnpm protocol:gen`
- `pnpm protocol:gen:swift`
- `pnpm protocol:check`
### Client constants
The reference client in `src/gateway/client.ts` uses these defaults. Values are
stable across protocol v3 and are the expected baseline for third-party clients.
| Constant | Default | Source |
| ----------------------------------------- | ----------------------------------------------------- | ---------------------------------------------------------- |
| `PROTOCOL_VERSION` | `3` | `src/gateway/protocol/schema/protocol-schemas.ts` |
| Request timeout (per RPC) | `30_000` ms | `src/gateway/client.ts` (`requestTimeoutMs`) |
| Preauth / connect-challenge timeout | `10_000` ms | `src/gateway/handshake-timeouts.ts` (clamp `250``10_000`) |
| Initial reconnect backoff | `1_000` ms | `src/gateway/client.ts` (`backoffMs`) |
| Max reconnect backoff | `30_000` ms | `src/gateway/client.ts` (`scheduleReconnect`) |
| Fast-retry clamp after device-token close | `250` ms | `src/gateway/client.ts` |
| Force-stop grace before `terminate()` | `250` ms | `FORCE_STOP_TERMINATE_GRACE_MS` |
| `stopAndWait()` default timeout | `1_000` ms | `STOP_AND_WAIT_TIMEOUT_MS` |
| Default tick interval (pre `hello-ok`) | `30_000` ms | `src/gateway/client.ts` |
| Tick-timeout close | code `4000` when silence exceeds `tickIntervalMs * 2` | `src/gateway/client.ts` |
| `MAX_PAYLOAD_BYTES` | `25 * 1024 * 1024` (25 MB) | `src/gateway/server-constants.ts` |
The server advertises the effective `policy.tickIntervalMs`, `policy.maxPayload`,
and `policy.maxBufferedBytes` in `hello-ok`; clients should honor those values
rather than the pre-handshake defaults.
## Auth
- Shared-secret gateway auth uses `connect.params.auth.token` or
@@ -518,8 +555,18 @@ implemented in `src/gateway/server-methods/*.ts`.
approved scope set for that token. This preserves read/probe/status access
that was already granted and avoids silently collapsing reconnects to a
narrower implicit admin-only scope.
- Normal connect auth precedence is explicit shared token/password first, then
explicit `deviceToken`, then stored per-device token, then bootstrap token.
- Client-side connect auth assembly (`selectConnectAuth` in
`src/gateway/client.ts`):
- `auth.password` is orthogonal and is always forwarded when set.
- `auth.token` is populated in priority order: explicit shared token first,
then an explicit `deviceToken`, then a stored per-device token (keyed by
`deviceId` + `role`).
- `auth.bootstrapToken` is sent only when none of the above resolved an
`auth.token`. A shared token or any resolved device token suppresses it.
- Auto-promotion of a stored device token on the one-shot
`AUTH_TOKEN_MISMATCH` retry is gated to **trusted endpoints only**
loopback, or `wss://` with a pinned `tlsFingerprint`. Public `wss://`
without pinning does not qualify.
- Additional `hello-ok.auth.deviceTokens` entries are bootstrap handoff tokens.
Persist them only when the connect used bootstrap auth on a trusted transport
such as `wss://` or loopback/local pairing.

View File

@@ -52,6 +52,10 @@ These commands sit beside the main test suites when you need QA-lab realism:
gateway workers, up to 64 workers or the selected scenario count. Use
`--concurrency <count>` to tune the worker count, or `--concurrency 1` for
the older serial lane.
- Supports provider modes `live-frontier`, `mock-openai`, and `aimock`.
`aimock` starts a local AIMock-backed provider server for experimental
fixture and protocol-mock coverage without replacing the scenario-aware
`mock-openai` lane.
- `pnpm openclaw qa suite --runner multipass`
- Runs the same QA suite inside a disposable Multipass Linux VM.
- Keeps the same scenario-selection behavior as `qa suite` on the host.
@@ -65,6 +69,9 @@ These commands sit beside the main test suites when you need QA-lab realism:
`.artifacts/qa-e2e/...`.
- `pnpm qa:lab:up`
- Starts the Docker-backed QA site for operator-style QA work.
- `pnpm openclaw qa aimock`
- Starts only the local AIMock provider server for direct protocol smoke
testing.
- `pnpm openclaw qa matrix`
- Runs the Matrix live QA lane against a disposable Docker-backed Tuwunel homeserver.
- This QA host is repo/dev-only today. Packaged OpenClaw installs do not ship
@@ -74,7 +81,7 @@ These commands sit beside the main test suites when you need QA-lab realism:
- Provisions three temporary Matrix users (`driver`, `sut`, `observer`) plus one private room, then starts a QA gateway child with the real Matrix plugin as the SUT transport.
- Uses the pinned stable Tuwunel image `ghcr.io/matrix-construct/tuwunel:v1.5.1` by default. Override with `OPENCLAW_QA_MATRIX_TUWUNEL_IMAGE` when you need to test a different image.
- Matrix does not expose shared credential-source flags because the lane provisions disposable users locally.
- Writes a Matrix QA report, summary, and observed-events artifact under `.artifacts/qa-e2e/...`.
- Writes a Matrix QA report, summary, observed-events artifact, and combined stdout/stderr output log under `.artifacts/qa-e2e/...`.
- `pnpm openclaw qa telegram`
- Runs the Telegram live QA lane against a real private group using the driver and SUT bot tokens from env.
- Requires `OPENCLAW_QA_TELEGRAM_GROUP_ID`, `OPENCLAW_QA_TELEGRAM_DRIVER_BOT_TOKEN`, and `OPENCLAW_QA_TELEGRAM_SUT_BOT_TOKEN`. The group id must be the numeric Telegram chat id.

View File

@@ -1,6 +1,6 @@
---
title: "Google (Gemini)"
summary: "Google Gemini setup (API key + OAuth, image generation, media understanding, web search)"
summary: "Google Gemini setup (API key + OAuth, image generation, media understanding, TTS, web search)"
read_when:
- You want to use Google Gemini models with OpenClaw
- You need the API key or OAuth auth flow
@@ -9,7 +9,7 @@ read_when:
# Google (Gemini)
The Google plugin provides access to Gemini models through Google AI Studio, plus
image generation, media understanding (image/audio/video), and web search via
image generation, media understanding (image/audio/video), text-to-speech, and web search via
Gemini Grounding.
- Provider: `google`
@@ -133,6 +133,7 @@ Choose your preferred auth method and follow the setup steps.
| Chat completions | Yes |
| Image generation | Yes |
| Music generation | Yes |
| Text-to-speech | Yes |
| Image understanding | Yes |
| Audio transcription | Yes |
| Video understanding | Yes |
@@ -233,6 +234,50 @@ To use Google as the default music provider:
See [Music Generation](/tools/music-generation) for shared tool parameters, provider selection, and failover behavior.
</Note>
## Text-to-speech
The bundled `google` speech provider uses the Gemini API TTS path with
`gemini-3.1-flash-tts-preview`.
- Default voice: `Kore`
- Auth: `messages.tts.providers.google.apiKey`, `models.providers.google.apiKey`, `GEMINI_API_KEY`, or `GOOGLE_API_KEY`
- Output: WAV for regular TTS attachments, PCM for Talk/telephony
- Native voice-note output: not supported on this Gemini API path because the API returns PCM rather than Opus
To use Google as the default TTS provider:
```json5
{
messages: {
tts: {
auto: "always",
provider: "google",
providers: {
google: {
model: "gemini-3.1-flash-tts-preview",
voiceName: "Kore",
},
},
},
},
}
```
Gemini API TTS accepts expressive square-bracket audio tags in the text, such as
`[whispers]` or `[laughs]`. To keep tags out of the visible chat reply while
sending them to TTS, put them inside a `[[tts:text]]...[[/tts:text]]` block:
```text
Here is the clean reply text.
[[tts:text]][whispers] Here is the spoken version.[[/tts:text]]
```
<Note>
A Google Cloud Console API key restricted to the Gemini API is valid for this
provider. This is not the separate Cloud Text-to-Speech API path.
</Note>
## Advanced configuration
<AccordionGroup>

View File

@@ -0,0 +1,122 @@
# Async Exec Duplicate Completion Investigation
## Scope
- Session: `agent:main:telegram:group:-1003774691294:topic:1`
- Symptom: the same async exec completion for session/run `keen-nexus` was recorded twice in LCM as user turns.
- Goal: identify whether this is most likely duplicate session injection or plain outbound delivery retry.
## Conclusion
Most likely this is **duplicate session injection**, not a pure outbound delivery retry.
The strongest gateway-side gap is in the **node exec completion path**:
1. A node-side exec finish emits `exec.finished` with the full `runId`.
2. Gateway `server-node-events` converts that into a system event and requests a heartbeat.
3. The heartbeat run injects the drained system event block into the agent prompt.
4. The embedded runner persists that prompt as a new user turn in the session transcript.
If the same `exec.finished` reaches the gateway twice for the same `runId` for any reason (replay, reconnect duplicate, upstream resend, duplicated producer), OpenClaw currently has **no idempotency check keyed by `runId`/`contextKey`** on this path. The second copy will become a second user message with the same content.
## Exact Code Path
### 1. Producer: node exec completion event
- `src/node-host/invoke.ts:340-360`
- `sendExecFinishedEvent(...)` emits `node.event` with event `exec.finished`.
- Payload includes `sessionKey` and full `runId`.
### 2. Gateway event ingestion
- `src/gateway/server-node-events.ts:574-640`
- Handles `exec.finished`.
- Builds text:
- `Exec finished (node=..., id=<runId>, code ...)`
- Enqueues it via:
- `enqueueSystemEvent(text, { sessionKey, contextKey: runId ? \`exec:${runId}\` : "exec", trusted: false })`
- Immediately requests a wake:
- `requestHeartbeatNow(scopedHeartbeatWakeOptions(sessionKey, { reason: "exec-event" }))`
### 3. System event dedupe weakness
- `src/infra/system-events.ts:90-115`
- `enqueueSystemEvent(...)` only suppresses **consecutive duplicate text**:
- `if (entry.lastText === cleaned) return false`
- It stores `contextKey`, but does **not** use `contextKey` for idempotency.
- After drain, duplicate suppression resets.
This means a replayed `exec.finished` with the same `runId` can be accepted again later, even though the code already had a stable idempotency candidate (`exec:<runId>`).
### 4. Wake handling is not the primary duplicator
- `src/infra/heartbeat-wake.ts:79-117`
- Wakes are coalesced by `(agentId, sessionKey)`.
- Duplicate wake requests for the same target collapse to one pending wake entry.
This makes **duplicate wake handling alone** a weaker explanation than duplicate event ingestion.
### 5. Heartbeat consumes the event and turns it into prompt input
- `src/infra/heartbeat-runner.ts:535-574`
- Preflight peeks pending system events and classifies exec-event runs.
- `src/auto-reply/reply/session-system-events.ts:86-90`
- `drainFormattedSystemEvents(...)` drains the queue for the session.
- `src/auto-reply/reply/get-reply-run.ts:400-427`
- The drained system event block is prepended into the agent prompt body.
### 6. Transcript injection point
- `src/agents/pi-embedded-runner/run/attempt.ts:2000-2017`
- `activeSession.prompt(effectivePrompt)` submits the full prompt to the embedded PI session.
- That is the point where the completion-derived prompt becomes a persisted user turn.
So once the same system event is rebuilt into the prompt twice, duplicate LCM user messages are expected.
## Why plain outbound delivery retry is less likely
There is a real outbound failure path in the heartbeat runner:
- `src/infra/heartbeat-runner.ts:1194-1242`
- The reply is generated first.
- Outbound delivery happens later via `deliverOutboundPayloads(...)`.
- Failure there returns `{ status: "failed" }`.
However, for the same system event queue entry, this alone is **not sufficient** to explain the duplicate user turns:
- `src/auto-reply/reply/session-system-events.ts:86-90`
- The system event queue is already drained before outbound delivery.
So a channel send retry by itself would not recreate the exact same queued event. It could explain missing/failed external delivery, but not by itself a second identical session user message.
## Secondary, lower-confidence possibility
There is a full-run retry loop in the agent runner:
- `src/auto-reply/reply/agent-runner-execution.ts:741-1473`
- Certain transient failures can retry the whole run and resubmit the same `commandBody`.
That can duplicate a persisted user prompt **within the same reply execution** if the prompt was already appended before the retry condition triggered.
I rank this lower than duplicate `exec.finished` ingestion because:
- the observed gap was around 51 seconds, which looks more like a second wake/turn than an in-process retry;
- the report already mentions repeated message send failures, which points more toward a separate later turn than an immediate model/runtime retry.
## Root Cause Hypothesis
Highest-confidence hypothesis:
- The `keen-nexus` completion came through the **node exec event path**.
- The same `exec.finished` was delivered to `server-node-events` twice.
- Gateway accepted both because `enqueueSystemEvent(...)` does not dedupe by `contextKey` / `runId`.
- Each accepted event triggered a heartbeat and was injected as a user turn into the PI transcript.
## Proposed Tiny Surgical Fix
If a fix is wanted, the smallest high-value change is:
- make exec/system-event idempotency honor `contextKey` for a short horizon, at least for exact `(sessionKey, contextKey, text)` repeats;
- or add a dedicated dedupe in `server-node-events` for `exec.finished` keyed by `(sessionKey, runId, event kind)`.
That would directly block replayed `exec.finished` duplicates before they become session turns.

View File

@@ -16,9 +16,12 @@ OpenAI-style models average ~4 characters per token for English text.
OpenClaw assembles its own system prompt on every run. It includes:
- Tool list + short descriptions
- Skills list (only metadata; instructions are loaded on demand with `read`)
- Skills list (only metadata; instructions are loaded on demand with `read`).
The compact skills block is bounded by `skills.limits.maxSkillsPromptChars`,
with optional per-agent override at
`agents.list[].skillsLimits.maxSkillsPromptChars`.
- Self-update instructions
- Workspace + bootstrap files (`AGENTS.md`, `SOUL.md`, `TOOLS.md`, `IDENTITY.md`, `USER.md`, `HEARTBEAT.md`, `BOOTSTRAP.md` when new, plus `MEMORY.md` when present or `memory.md` as a lowercase fallback). Large files are truncated by `agents.defaults.bootstrapMaxChars` (default: 20000), and total bootstrap injection is capped by `agents.defaults.bootstrapTotalMaxChars` (default: 150000). `memory/*.md` daily files are not part of the normal bootstrap prompt; they remain on-demand via memory tools on ordinary turns, but bare `/new` and `/reset` can prepend a one-shot startup-context block with recent daily memory for that first turn. That startup prelude is controlled by `agents.defaults.startupContext`.
- Workspace + bootstrap files (`AGENTS.md`, `SOUL.md`, `TOOLS.md`, `IDENTITY.md`, `USER.md`, `HEARTBEAT.md`, `BOOTSTRAP.md` when new, plus `MEMORY.md` when present or `memory.md` as a lowercase fallback). Large files are truncated by `agents.defaults.bootstrapMaxChars` (default: 12000), and total bootstrap injection is capped by `agents.defaults.bootstrapTotalMaxChars` (default: 60000). `memory/*.md` daily files are not part of the normal bootstrap prompt; they remain on-demand via memory tools on ordinary turns, but bare `/new` and `/reset` can prepend a one-shot startup-context block with recent daily memory for that first turn. That startup prelude is controlled by `agents.defaults.startupContext`.
- Time (UTC + user timezone)
- Reply tags + heartbeat behavior
- Runtime metadata (host/OS/model/thinking)
@@ -36,6 +39,18 @@ Everything the model receives counts toward the context limit:
- Compaction summaries and pruning artifacts
- Provider wrappers or safety headers (not visible, but still counted)
Some runtime-heavy surfaces have their own explicit caps:
- `agents.defaults.contextLimits.memoryGetMaxChars`
- `agents.defaults.contextLimits.memoryGetDefaultLines`
- `agents.defaults.contextLimits.toolResultMaxChars`
- `agents.defaults.contextLimits.postCompactionMaxChars`
Per-agent overrides live under `agents.list[].contextLimits`. These knobs are
for bounded runtime excerpts and injected runtime-owned blocks. They are
separate from bootstrap limits, startup-context limits, and skills prompt
limits.
For images, OpenClaw downscales transcript/tool image payloads before provider calls.
Use `agents.defaults.imageMaxDimensionPx` (default: `1200`) to tune this:

View File

@@ -15,12 +15,14 @@ title: "Thinking Levels"
- low → “think hard”
- medium → “think harder”
- high → “ultrathink” (max budget)
- xhigh → “ultrathink+” (GPT-5.2 + Codex models only)
- adaptive → provider-managed adaptive reasoning budget (supported for Anthropic Claude 4.6 model family)
- xhigh → “ultrathink+” (GPT-5.2 + Codex models and Anthropic Claude Opus 4.7 effort)
- adaptive → provider-managed adaptive thinking (supported for Anthropic Claude 4.6 and Opus 4.7)
- `x-high`, `x_high`, `extra-high`, `extra high`, and `extra_high` map to `xhigh`.
- `highest`, `max` map to `high`.
- Provider notes:
- Anthropic Claude 4.6 models default to `adaptive` when no explicit thinking level is set.
- Anthropic Claude Opus 4.7 does not default to adaptive thinking. Its API effort default remains provider-owned unless you explicitly set a thinking level.
- Anthropic Claude Opus 4.7 maps `/think xhigh` to adaptive thinking plus `output_config.effort: "xhigh"`, because `/think` is a thinking directive and `xhigh` is the Opus 4.7 effort setting.
- MiniMax (`minimax/*`) on the Anthropic-compatible streaming path defaults to `thinking: { type: "disabled" }` unless you explicitly set thinking in model params or request params. This avoids leaked `reasoning_content` deltas from MiniMax's non-native Anthropic stream format.
- Z.AI (`zai/*`) only supports binary thinking (`on`/`off`). Any non-`off` level is treated as `on` (mapped to `low`).
- Moonshot (`moonshot/*`) maps `/think off` to `thinking: { type: "disabled" }` and any non-`off` level to `thinking: { type: "enabled" }`. When thinking is enabled, Moonshot only accepts `tool_choice` `auto|none`; OpenClaw normalizes incompatible values to `auto`.
@@ -31,7 +33,7 @@ title: "Thinking Levels"
2. Session override (set by sending a directive-only message).
3. Per-agent default (`agents.list[].thinkingDefault` in config).
4. Global default (`agents.defaults.thinkingDefault` in config).
5. Fallback: `adaptive` for Anthropic Claude 4.6 models, `low` for other reasoning-capable models, `off` otherwise.
5. Fallback: `adaptive` for Anthropic Claude 4.6 models, `off` for Anthropic Claude Opus 4.7 unless explicitly configured, `low` for other reasoning-capable models, `off` otherwise.
## Setting a session default
@@ -104,8 +106,9 @@ title: "Thinking Levels"
- The web chat thinking selector mirrors the session's stored level from the inbound session store/config when the page loads.
- Picking another level writes the session override immediately via `sessions.patch`; it does not wait for the next send and it is not a one-shot `thinkingOnce` override.
- The first option is always `Default (<resolved level>)`, where the resolved default comes from the active session model: `adaptive` for Claude 4.6 on Anthropic/Bedrock, `low` for other reasoning-capable models, `off` otherwise.
- The first option is always `Default (<resolved level>)`, where the resolved default comes from the active session model: `adaptive` for Claude 4.6 on Anthropic, `off` for Anthropic Claude Opus 4.7 unless configured, `low` for other reasoning-capable models, `off` otherwise.
- The picker stays provider-aware:
- most providers show `off | minimal | low | medium | high | adaptive`
- Anthropic Claude Opus 4.7 shows `off | minimal | low | medium | high | xhigh | adaptive`
- Z.AI shows binary `off | on`
- `/think:<level>` still works and updates the same stored session level, so chat directives and the picker stay in sync.

View File

@@ -9,12 +9,13 @@ title: "Text-to-Speech"
# Text-to-speech (TTS)
OpenClaw can convert outbound replies into audio using ElevenLabs, Microsoft, MiniMax, or OpenAI.
OpenClaw can convert outbound replies into audio using ElevenLabs, Google Gemini, Microsoft, MiniMax, or OpenAI.
It works anywhere OpenClaw can send audio.
## Supported services
- **ElevenLabs** (primary or fallback provider)
- **Google Gemini** (primary or fallback provider; uses Gemini API TTS)
- **Microsoft** (primary or fallback provider; current bundled implementation uses `node-edge-tts`)
- **MiniMax** (primary or fallback provider; uses the T2A v2 API)
- **OpenAI** (primary or fallback provider; also used for summaries)
@@ -34,9 +35,10 @@ or ElevenLabs.
## Optional keys
If you want OpenAI, ElevenLabs, or MiniMax:
If you want OpenAI, ElevenLabs, Google Gemini, or MiniMax:
- `ELEVENLABS_API_KEY` (or `XI_API_KEY`)
- `GEMINI_API_KEY` (or `GOOGLE_API_KEY`)
- `MINIMAX_API_KEY`
- `OPENAI_API_KEY`
@@ -170,6 +172,32 @@ Full schema is in [Gateway configuration](/gateway/configuration).
}
```
### Google Gemini primary
```json5
{
messages: {
tts: {
auto: "always",
provider: "google",
providers: {
google: {
apiKey: "gemini_api_key",
model: "gemini-3.1-flash-tts-preview",
voiceName: "Kore",
},
},
},
},
}
```
Google Gemini TTS uses the Gemini API key path. A Google Cloud Console API key
restricted to the Gemini API is valid here, and it is the same style of key used
by the bundled Google image-generation provider. Resolution order is
`messages.tts.providers.google.apiKey` -> `models.providers.google.apiKey` ->
`GEMINI_API_KEY` -> `GOOGLE_API_KEY`.
### Disable Microsoft speech
```json5
@@ -238,7 +266,7 @@ Then run:
- `tagged` only sends audio when the reply includes `[[tts:key=value]]` directives or a `[[tts:text]]...[[/tts:text]]` block.
- `enabled`: legacy toggle (doctor migrates this to `auto`).
- `mode`: `"final"` (default) or `"all"` (includes tool/block replies).
- `provider`: speech provider id such as `"elevenlabs"`, `"microsoft"`, `"minimax"`, or `"openai"` (fallback is automatic).
- `provider`: speech provider id such as `"elevenlabs"`, `"google"`, `"microsoft"`, `"minimax"`, or `"openai"` (fallback is automatic).
- If `provider` is **unset**, OpenClaw uses the first configured speech provider in registry auto-select order.
- Legacy `provider: "edge"` still works and is normalized to `microsoft`.
- `summaryModel`: optional cheap model for auto-summary; defaults to `agents.defaults.model.primary`.
@@ -250,7 +278,7 @@ Then run:
- `maxTextLength`: hard cap for TTS input (chars). `/tts audio` fails if exceeded.
- `timeoutMs`: request timeout (ms).
- `prefsPath`: override the local prefs JSON path (provider/limit/summary).
- `apiKey` values fall back to env vars (`ELEVENLABS_API_KEY`/`XI_API_KEY`, `MINIMAX_API_KEY`, `OPENAI_API_KEY`).
- `apiKey` values fall back to env vars (`ELEVENLABS_API_KEY`/`XI_API_KEY`, `GEMINI_API_KEY`/`GOOGLE_API_KEY`, `MINIMAX_API_KEY`, `OPENAI_API_KEY`).
- `providers.elevenlabs.baseUrl`: override ElevenLabs API base URL.
- `providers.openai.baseUrl`: override the OpenAI TTS endpoint.
- Resolution order: `messages.tts.providers.openai.baseUrl` -> `OPENAI_TTS_BASE_URL` -> `https://api.openai.com/v1`
@@ -268,6 +296,10 @@ Then run:
- `providers.minimax.speed`: playback speed `0.5..2.0` (default 1.0).
- `providers.minimax.vol`: volume `(0, 10]` (default 1.0; must be greater than 0).
- `providers.minimax.pitch`: pitch shift `-12..12` (default 0).
- `providers.google.model`: Gemini TTS model (default `gemini-3.1-flash-tts-preview`).
- `providers.google.voiceName`: Gemini prebuilt voice name (default `Kore`; `voice` is also accepted).
- `providers.google.baseUrl`: override the Gemini API base URL. Only `https://generativelanguage.googleapis.com` is accepted.
- If `messages.tts.providers.google.apiKey` is omitted, TTS can reuse `models.providers.google.apiKey` before env fallback.
- `providers.microsoft.enabled`: allow Microsoft speech usage (default `true`; no API key).
- `providers.microsoft.voice`: Microsoft neural voice name (e.g. `en-US-MichelleNeural`).
- `providers.microsoft.lang`: language code (e.g. `en-US`).
@@ -302,9 +334,9 @@ Here you go.
Available directive keys (when enabled):
- `provider` (registered speech provider id, for example `openai`, `elevenlabs`, `minimax`, or `microsoft`; requires `allowProvider: true`)
- `voice` (OpenAI voice) or `voiceId` (ElevenLabs / MiniMax)
- `model` (OpenAI TTS model, ElevenLabs model id, or MiniMax model)
- `provider` (registered speech provider id, for example `openai`, `elevenlabs`, `google`, `minimax`, or `microsoft`; requires `allowProvider: true`)
- `voice` (OpenAI voice), `voiceName` / `voice_name` / `google_voice` (Google voice), or `voiceId` (ElevenLabs / MiniMax)
- `model` (OpenAI TTS model, ElevenLabs model id, or MiniMax model) or `google_model` (Google TTS model)
- `stability`, `similarityBoost`, `style`, `speed`, `useSpeakerBoost`
- `vol` / `volume` (MiniMax volume, 0-10)
- `pitch` (MiniMax pitch, -12 to 12)
@@ -364,6 +396,7 @@ These override `messages.tts.*` for that host.
- **Other channels**: MP3 (`mp3_44100_128` from ElevenLabs, `mp3` from OpenAI).
- 44.1kHz / 128kbps is the default balance for speech clarity.
- **MiniMax**: MP3 (`speech-2.8-hd` model, 32kHz sample rate). Voice-note format not natively supported; use OpenAI or ElevenLabs for guaranteed Opus voice messages.
- **Google Gemini**: Gemini API TTS returns raw 24kHz PCM. OpenClaw wraps it as WAV for audio attachments and returns PCM directly for Talk/telephony. Native Opus voice-note format is not supported by this path.
- **Microsoft**: uses `microsoft.outputFormat` (default `audio-24khz-48kbitrate-mono-mp3`).
- The bundled transport accepts an `outputFormat`, but not all formats are available from the service.
- Output format values follow Microsoft Speech output formats (including Ogg/WebM Opus).

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/acpx",
"version": "2026.4.15-beta.1",
"version": "2026.4.16",
"description": "OpenClaw ACP runtime backend",
"type": "module",
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/alibaba-provider",
"version": "2026.4.15-beta.1",
"version": "2026.4.16",
"private": true,
"description": "OpenClaw Alibaba Model Studio video provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/amazon-bedrock-mantle-provider",
"version": "2026.4.15-beta.1",
"version": "2026.4.16",
"private": true,
"description": "OpenClaw Amazon Bedrock Mantle (OpenAI-compatible) provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/amazon-bedrock-provider",
"version": "2026.4.15-beta.1",
"version": "2026.4.16",
"private": true,
"description": "OpenClaw Amazon Bedrock provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/anthropic-vertex-provider",
"version": "2026.4.15-beta.1",
"version": "2026.4.16",
"private": true,
"description": "OpenClaw Anthropic Vertex provider plugin",
"type": "module",

View File

@@ -101,11 +101,11 @@ describe("anthropic cli migration", () => {
agents: {
defaults: {
model: {
primary: "anthropic/claude-sonnet-4-6",
primary: "anthropic/claude-opus-4-7",
fallbacks: ["anthropic/claude-opus-4-6", "openai/gpt-5.2"],
},
models: {
"anthropic/claude-sonnet-4-6": { alias: "Sonnet" },
"anthropic/claude-opus-4-7": { alias: "Opus" },
"anthropic/claude-opus-4-6": { alias: "Opus" },
"openai/gpt-5.2": {},
},
@@ -114,16 +114,17 @@ describe("anthropic cli migration", () => {
});
expect(result.profiles).toEqual([]);
expect(result.defaultModel).toBe("claude-cli/claude-sonnet-4-6");
expect(result.defaultModel).toBe("claude-cli/claude-opus-4-7");
expect(result.configPatch).toEqual({
agents: {
defaults: {
model: {
primary: "claude-cli/claude-sonnet-4-6",
primary: "claude-cli/claude-opus-4-7",
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-7": { alias: "Opus" },
"claude-cli/claude-sonnet-4-6": {},
"claude-cli/claude-opus-4-6": { alias: "Opus" },
"claude-cli/claude-opus-4-5": {},
"claude-cli/claude-sonnet-4-5": {},
@@ -147,12 +148,13 @@ describe("anthropic cli migration", () => {
},
});
expect(result.defaultModel).toBe("claude-cli/claude-sonnet-4-6");
expect(result.defaultModel).toBe("claude-cli/claude-opus-4-7");
expect(result.configPatch).toEqual({
agents: {
defaults: {
models: {
"openai/gpt-5.2": {},
"claude-cli/claude-opus-4-7": {},
"claude-cli/claude-sonnet-4-6": {},
"claude-cli/claude-opus-4-6": {},
"claude-cli/claude-opus-4-5": {},
@@ -168,9 +170,9 @@ describe("anthropic cli migration", () => {
const result = buildAnthropicCliMigrationResult({
agents: {
defaults: {
model: { primary: "claude-cli/claude-sonnet-4-6" },
model: { primary: "claude-cli/claude-opus-4-7" },
models: {
"claude-cli/claude-sonnet-4-6": {},
"claude-cli/claude-opus-4-7": {},
},
},
},
@@ -180,6 +182,7 @@ describe("anthropic cli migration", () => {
agents: {
defaults: {
models: {
"claude-cli/claude-opus-4-7": {},
"claude-cli/claude-sonnet-4-6": {},
"claude-cli/claude-opus-4-6": {},
"claude-cli/claude-opus-4-5": {},
@@ -217,11 +220,11 @@ describe("anthropic cli migration", () => {
agents: {
defaults: {
model: {
primary: "anthropic/claude-sonnet-4-6",
primary: "anthropic/claude-opus-4-7",
fallbacks: ["anthropic/claude-opus-4-6", "openai/gpt-5.2"],
},
models: {
"anthropic/claude-sonnet-4-6": { alias: "Sonnet" },
"anthropic/claude-opus-4-7": { alias: "Opus" },
"anthropic/claude-opus-4-6": { alias: "Opus" },
"openai/gpt-5.2": {},
},
@@ -297,11 +300,11 @@ describe("anthropic cli migration", () => {
agents: {
defaults: {
model: {
primary: "anthropic/claude-sonnet-4-6",
primary: "anthropic/claude-opus-4-7",
fallbacks: ["anthropic/claude-opus-4-6", "openai/gpt-5.2"],
},
models: {
"anthropic/claude-sonnet-4-6": { alias: "Sonnet" },
"anthropic/claude-opus-4-7": { alias: "Opus" },
"anthropic/claude-opus-4-6": { alias: "Opus" },
"openai/gpt-5.2": {},
},
@@ -315,11 +318,11 @@ describe("anthropic cli migration", () => {
agents: {
defaults: {
model: {
primary: "claude-cli/claude-sonnet-4-6",
primary: "claude-cli/claude-opus-4-7",
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-7": { alias: "Opus" },
"claude-cli/claude-opus-4-6": { alias: "Opus" },
"openai/gpt-5.2": {},
},

View File

@@ -2,9 +2,10 @@ import type { CliBackendConfig } from "openclaw/plugin-sdk/cli-backend";
import { normalizeOptionalLowercaseString } from "openclaw/plugin-sdk/text-runtime";
export const CLAUDE_CLI_BACKEND_ID = "claude-cli";
export const CLAUDE_CLI_DEFAULT_MODEL_REF = `${CLAUDE_CLI_BACKEND_ID}/claude-sonnet-4-6`;
export const CLAUDE_CLI_DEFAULT_MODEL_REF = `${CLAUDE_CLI_BACKEND_ID}/claude-opus-4-7`;
export const CLAUDE_CLI_DEFAULT_ALLOWLIST_REFS = [
CLAUDE_CLI_DEFAULT_MODEL_REF,
`${CLAUDE_CLI_BACKEND_ID}/claude-sonnet-4-6`,
`${CLAUDE_CLI_BACKEND_ID}/claude-opus-4-6`,
`${CLAUDE_CLI_BACKEND_ID}/claude-opus-4-5`,
`${CLAUDE_CLI_BACKEND_ID}/claude-sonnet-4-5`,
@@ -13,9 +14,11 @@ export const CLAUDE_CLI_DEFAULT_ALLOWLIST_REFS = [
export const CLAUDE_CLI_MODEL_ALIASES: Record<string, string> = {
opus: "opus",
"opus-4.7": "opus",
"opus-4.6": "opus",
"opus-4.5": "opus",
"opus-4": "opus",
"claude-opus-4-7": "opus",
"claude-opus-4-6": "opus",
"claude-opus-4-5": "opus",
"claude-opus-4": "opus",

View File

@@ -87,7 +87,7 @@ function resolveAnthropicPrimaryModelRef(raw?: string): string | null {
}
const aliasKey = normalizeLowercaseStringOrEmpty(trimmed);
if (aliasKey === "opus") {
return "anthropic/claude-opus-4-6";
return "anthropic/claude-opus-4-7";
}
if (aliasKey === "sonnet") {
return "anthropic/claude-sonnet-4-6";

View File

@@ -1,3 +1,7 @@
import type {
ProviderResolveDynamicModelContext,
ProviderRuntimeModel,
} from "openclaw/plugin-sdk/plugin-entry";
import { capturePluginRegistration } from "openclaw/plugin-sdk/testing";
import { describe, expect, it, vi } from "vitest";
import { registerSingleProviderPlugin } from "../../test/helpers/plugins/plugin-registration.js";
@@ -18,6 +22,19 @@ vi.mock("./cli-auth-seam.js", () => {
import anthropicPlugin from "./index.js";
function createModelRegistry(models: ProviderRuntimeModel[]) {
return {
find(providerId: string, modelId: string) {
return (
models.find(
(model) =>
model.provider === providerId && model.id.toLowerCase() === modelId.toLowerCase(),
) ?? null
);
},
};
}
describe("anthropic provider replay hooks", () => {
it("registers the claude-cli backend", async () => {
const captured = capturePluginRegistration({ register: anthropicPlugin.register });
@@ -129,9 +146,9 @@ describe("anthropic provider replay hooks", () => {
},
agents: {
defaults: {
model: { primary: "claude-cli/claude-sonnet-4-6" },
model: { primary: "claude-cli/claude-opus-4-7" },
models: {
"claude-cli/claude-sonnet-4-6": {},
"claude-cli/claude-opus-4-7": {},
},
},
},
@@ -142,6 +159,7 @@ describe("anthropic provider replay hooks", () => {
every: "1h",
});
expect(next?.agents?.defaults?.models).toMatchObject({
"claude-cli/claude-opus-4-7": {},
"claude-cli/claude-sonnet-4-6": {},
"claude-cli/claude-opus-4-6": {},
"claude-cli/claude-opus-4-5": {},
@@ -150,6 +168,58 @@ describe("anthropic provider replay hooks", () => {
});
});
it("resolves explicit claude-opus-4-7 refs from the 4.6 template family", async () => {
const provider = await registerSingleProviderPlugin(anthropicPlugin);
const resolved = provider.resolveDynamicModel?.({
provider: "anthropic",
modelId: "claude-opus-4-7",
modelRegistry: createModelRegistry([
{
id: "claude-opus-4-6",
name: "Claude Opus 4.6",
provider: "anthropic",
api: "anthropic-messages",
reasoning: true,
input: ["text", "image"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 200_000,
maxTokens: 32_000,
} as ProviderRuntimeModel,
]),
} as ProviderResolveDynamicModelContext);
expect(resolved).toMatchObject({
provider: "anthropic",
id: "claude-opus-4-7",
api: "anthropic-messages",
reasoning: true,
});
expect(
provider.resolveDefaultThinkingLevel?.({
provider: "anthropic",
modelId: "claude-opus-4-7",
} as never),
).toBe("off");
expect(
provider.resolveDefaultThinkingLevel?.({
provider: "anthropic",
modelId: "claude-opus-4-6",
} as never),
).toBe("adaptive");
expect(
provider.supportsXHighThinking?.({
provider: "anthropic",
modelId: "claude-opus-4-7",
} as never),
).toBe(true);
expect(
provider.supportsXHighThinking?.({
provider: "anthropic",
modelId: "claude-opus-4-6",
} as never),
).toBe(false);
});
it("resolves claude-cli synthetic oauth auth", async () => {
readClaudeCliCredentialsForRuntimeMock.mockReset();
readClaudeCliCredentialsForRuntimeMock.mockReturnValue({

View File

@@ -7,7 +7,7 @@ import {
export const anthropicMediaUnderstandingProvider: MediaUnderstandingProvider = {
id: "anthropic",
capabilities: ["image"],
defaultModels: { image: "claude-opus-4-6" },
defaultModels: { image: "claude-opus-4-7" },
autoPriority: { image: 20 },
nativeDocumentInputs: ["pdf"],
describeImage: describeImageWithModel,

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/anthropic-provider",
"version": "2026.4.15-beta.1",
"version": "2026.4.16",
"private": true,
"description": "OpenClaw Anthropic provider plugin",
"type": "module",

View File

@@ -38,14 +38,23 @@ import { buildAnthropicReplayPolicy } from "./replay-policy.js";
import { wrapAnthropicProviderStream } from "./stream-wrappers.js";
const PROVIDER_ID = "anthropic";
const DEFAULT_ANTHROPIC_MODEL = "anthropic/claude-sonnet-4-6";
const DEFAULT_ANTHROPIC_MODEL = "anthropic/claude-opus-4-7";
const ANTHROPIC_OPUS_47_MODEL_ID = "claude-opus-4-7";
const ANTHROPIC_OPUS_47_DOT_MODEL_ID = "claude-opus-4.7";
const ANTHROPIC_OPUS_46_MODEL_ID = "claude-opus-4-6";
const ANTHROPIC_OPUS_46_DOT_MODEL_ID = "claude-opus-4.6";
const ANTHROPIC_OPUS_47_TEMPLATE_MODEL_IDS = [
ANTHROPIC_OPUS_46_MODEL_ID,
ANTHROPIC_OPUS_46_DOT_MODEL_ID,
"claude-opus-4-5",
"claude-opus-4.5",
] as const;
const ANTHROPIC_OPUS_TEMPLATE_MODEL_IDS = ["claude-opus-4-5", "claude-opus-4.5"] as const;
const ANTHROPIC_SONNET_46_MODEL_ID = "claude-sonnet-4-6";
const ANTHROPIC_SONNET_46_DOT_MODEL_ID = "claude-sonnet-4.6";
const ANTHROPIC_SONNET_TEMPLATE_MODEL_IDS = ["claude-sonnet-4-5", "claude-sonnet-4.5"] as const;
const ANTHROPIC_MODERN_MODEL_PREFIXES = [
"claude-opus-4-7",
"claude-opus-4-6",
"claude-sonnet-4-6",
"claude-opus-4-5",
@@ -221,6 +230,14 @@ function resolveAnthropicForwardCompatModel(
ctx: ProviderResolveDynamicModelContext,
): ProviderRuntimeModel | undefined {
return (
resolveAnthropic46ForwardCompatModel({
ctx,
dashModelId: ANTHROPIC_OPUS_47_MODEL_ID,
dotModelId: ANTHROPIC_OPUS_47_DOT_MODEL_ID,
dashTemplateId: ANTHROPIC_OPUS_46_MODEL_ID,
dotTemplateId: ANTHROPIC_OPUS_46_DOT_MODEL_ID,
fallbackTemplateIds: ANTHROPIC_OPUS_47_TEMPLATE_MODEL_IDS,
}) ??
resolveAnthropic46ForwardCompatModel({
ctx,
dashModelId: ANTHROPIC_OPUS_46_MODEL_ID,
@@ -250,6 +267,14 @@ function shouldUseAnthropicAdaptiveThinkingDefault(modelId: string): boolean {
);
}
function isAnthropicOpus47Model(modelId: string): boolean {
const lowerModelId = normalizeLowercaseStringOrEmpty(modelId);
return (
lowerModelId.startsWith(ANTHROPIC_OPUS_47_MODEL_ID) ||
lowerModelId.startsWith(ANTHROPIC_OPUS_47_DOT_MODEL_ID)
);
}
function matchesAnthropicModernModel(modelId: string): boolean {
const lower = normalizeLowercaseStringOrEmpty(modelId);
return ANTHROPIC_MODERN_MODEL_PREFIXES.some((prefix) => lower.startsWith(prefix));
@@ -372,7 +397,7 @@ async function runAnthropicCliMigrationNonInteractive(ctx: {
export function registerAnthropicPlugin(api: OpenClawPluginApi): void {
const providerId = "anthropic";
const defaultAnthropicModel = "anthropic/claude-sonnet-4-6";
const defaultAnthropicModel = DEFAULT_ANTHROPIC_MODEL;
api.registerCliBackend(buildAnthropicCliBackend());
api.registerProvider({
id: providerId,
@@ -462,11 +487,14 @@ export function registerAnthropicPlugin(api: OpenClawPluginApi): void {
buildReplayPolicy: buildAnthropicReplayPolicy,
isModernModelRef: ({ modelId }) => matchesAnthropicModernModel(modelId),
resolveReasoningOutputMode: () => "native",
supportsXHighThinking: ({ modelId }) => isAnthropicOpus47Model(modelId),
wrapStreamFn: wrapAnthropicProviderStream,
resolveDefaultThinkingLevel: ({ modelId }) =>
matchesAnthropicModernModel(modelId) && shouldUseAnthropicAdaptiveThinkingDefault(modelId)
? "adaptive"
: undefined,
isAnthropicOpus47Model(modelId)
? "off"
: matchesAnthropicModernModel(modelId) && shouldUseAnthropicAdaptiveThinkingDefault(modelId)
? "adaptive"
: undefined,
resolveUsageAuth: async (ctx) => await ctx.resolveOAuthToken(),
fetchUsageSnapshot: async (ctx) =>
await fetchClaudeUsage(ctx.token, ctx.timeoutMs, ctx.fetchFn),

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/arcee-provider",
"version": "2026.4.15-beta.1",
"version": "2026.4.16",
"private": true,
"description": "OpenClaw Arcee provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/bluebubbles",
"version": "2026.4.15-beta.1",
"version": "2026.4.16",
"description": "OpenClaw BlueBubbles channel plugin",
"type": "module",
"devDependencies": {
@@ -8,7 +8,7 @@
"openclaw": "workspace:*"
},
"peerDependencies": {
"openclaw": ">=2026.4.15-beta.1"
"openclaw": ">=2026.4.16"
},
"peerDependenciesMeta": {
"openclaw": {
@@ -43,10 +43,10 @@
"minHostVersion": ">=2026.4.10"
},
"compat": {
"pluginApi": ">=2026.4.15-beta.1"
"pluginApi": ">=2026.4.16"
},
"build": {
"openclawVersion": "2026.4.15-beta.1"
"openclawVersion": "2026.4.16"
},
"release": {
"publishToClawHub": true,

View File

@@ -1,6 +1,10 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import "./test-mocks.js";
import { downloadBlueBubblesAttachment, sendBlueBubblesAttachment } from "./attachments.js";
import {
downloadBlueBubblesAttachment,
fetchBlueBubblesMessageAttachments,
sendBlueBubblesAttachment,
} from "./attachments.js";
import { fetchBlueBubblesServerInfo, getCachedBlueBubblesPrivateApiStatus } from "./probe.js";
import type { PluginRuntime } from "./runtime-api.js";
import { setBlueBubblesRuntime } from "./runtime.js";
@@ -769,3 +773,86 @@ describe("sendBlueBubblesAttachment", () => {
).rejects.toThrow("chatGuid not found");
});
});
describe("fetchBlueBubblesMessageAttachments", () => {
beforeEach(() => {
mockFetch.mockReset();
});
it("returns attachments from the BB API response", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: () =>
Promise.resolve({
data: {
attachments: [
{
guid: "att-1",
mimeType: "image/jpeg",
transferName: "photo.jpg",
totalBytes: 1024,
},
{
guid: "att-2",
mime_type: "image/png",
transfer_name: "screenshot.png",
total_bytes: 2048,
},
],
},
}),
});
const result = await fetchBlueBubblesMessageAttachments("msg-guid", {
baseUrl: "http://localhost:1234",
password: "test",
});
expect(result).toHaveLength(2);
expect(result[0].guid).toBe("att-1");
expect(result[0].mimeType).toBe("image/jpeg");
expect(result[1].guid).toBe("att-2");
expect(result[1].mimeType).toBe("image/png");
});
it("returns empty array on non-ok HTTP response", async () => {
mockFetch.mockResolvedValueOnce({
ok: false,
status: 404,
});
const result = await fetchBlueBubblesMessageAttachments("msg-guid", {
baseUrl: "http://localhost:1234",
password: "test",
});
expect(result).toEqual([]);
});
it("returns empty array when data has no attachments", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({ data: {} }),
});
const result = await fetchBlueBubblesMessageAttachments("msg-guid", {
baseUrl: "http://localhost:1234",
password: "test",
});
expect(result).toEqual([]);
});
it("includes entries without a guid (downstream download handles filtering)", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: () =>
Promise.resolve({
data: {
attachments: [{ mimeType: "image/jpeg" }, { guid: "att-valid", mimeType: "image/png" }],
},
}),
});
const result = await fetchBlueBubblesMessageAttachments("msg-guid", {
baseUrl: "http://localhost:1234",
password: "test",
});
expect(result).toHaveLength(2);
expect(result[0].guid).toBeUndefined();
expect(result[1].guid).toBe("att-valid");
});
});

View File

@@ -8,6 +8,7 @@ import {
normalizeOptionalString,
} from "openclaw/plugin-sdk/text-runtime";
import { resolveBlueBubblesServerAccount } from "./account-resolve.js";
import { extractAttachments } from "./monitor-normalize.js";
import { assertMultipartActionOk, postMultipartFormData } from "./multipart.js";
import {
fetchBlueBubblesServerInfo,
@@ -26,8 +27,12 @@ import {
type SsrFPolicy,
} from "./types.js";
function blueBubblesPolicy(allowPrivateNetwork: boolean | undefined): SsrFPolicy {
return allowPrivateNetwork ? { allowPrivateNetwork: true } : {};
function blueBubblesPolicy(allowPrivateNetwork: boolean | undefined): SsrFPolicy | undefined {
// Pass `undefined` (not `{}`) for the non-private case so the non-SSRF fallback path
// is used. An empty `{}` policy routes through the SSRF guard, which blocks the
// localhost BB deployments that are the most common self-hosted setup. The opt-in
// private-network branch keeps the explicit policy. (#64105, #67510)
return allowPrivateNetwork ? { allowPrivateNetwork: true } : undefined;
}
export type BlueBubblesAttachmentOpts = {
@@ -95,6 +100,51 @@ function readMediaFetchErrorCode(error: unknown): MediaFetchErrorCode | undefine
: undefined;
}
/**
* Fetch attachment metadata for a message from the BlueBubbles API.
*
* BlueBubbles sometimes fires the `new-message` webhook before attachment
* indexing is complete, so `attachments` arrives as `[]`. This function
* GETs the message by GUID and returns whatever attachments the server
* has indexed by now. (#65430, #67437)
*/
export async function fetchBlueBubblesMessageAttachments(
messageGuid: string,
opts: {
baseUrl: string;
password: string;
timeoutMs?: number;
allowPrivateNetwork?: boolean;
},
): Promise<BlueBubblesAttachment[]> {
const url = buildBlueBubblesApiUrl({
baseUrl: opts.baseUrl,
path: `/api/v1/message/${encodeURIComponent(messageGuid)}`,
password: opts.password,
});
// Pass undefined (not {}) when private network is not opted-in so the
// non-SSRF fallback path is used — an empty {} triggers the SSRF-guarded
// path which blocks localhost BB servers by default. (#64105)
const policy: SsrFPolicy | undefined = opts.allowPrivateNetwork
? { allowPrivateNetwork: true }
: undefined;
const response = await blueBubblesFetchWithTimeout(
url,
{ method: "GET" },
opts.timeoutMs,
policy,
);
if (!response.ok) {
return [];
}
const json = (await response.json()) as Record<string, unknown>;
const data = json.data as Record<string, unknown> | undefined;
if (!data) {
return [];
}
return extractAttachments(data);
}
export async function downloadBlueBubblesAttachment(
attachment: BlueBubblesAttachment,
opts: BlueBubblesAttachmentOpts & { maxBytes?: number } = {},

View File

@@ -428,9 +428,13 @@ describe("runBlueBubblesCatchup", () => {
// Cursor is held just before the bad message's timestamp so the next
// sweep retries it (and re-queries ok1 which dedupe will drop).
expect(summary?.failed).toBe(1);
expect(summary?.givenUp).toBe(0);
expect(summary?.cursorAfter).toBe(7 * 60 * 1000 - 1);
const cursorAfter = await loadBlueBubblesCatchupCursor("test-account");
expect(cursorAfter?.lastSeenMs).toBe(7 * 60 * 1000 - 1);
// Retry counter is persisted so subsequent sweeps know how close we
// are to the give-up ceiling.
expect(cursorAfter?.failureRetries?.bad).toBe(1);
});
it("clamps held cursor to previous cursor when failure ts is below it", async () => {
@@ -606,6 +610,494 @@ describe("runBlueBubblesCatchup", () => {
});
});
describe("runBlueBubblesCatchup — per-message retry cap", () => {
let stateDir: string;
beforeEach(() => {
stateDir = makeStateDir();
});
afterEach(() => {
clearStateDir(stateDir);
vi.restoreAllMocks();
});
it("increments retry counter on each consecutive failure and holds cursor", async () => {
// Three sweeps, all fail on the same GUID. Counter accumulates and
// cursor stays pinned below the failing message so every sweep
// retries it. maxFailureRetries: 5 so we don't give up inside this
// test.
const now1 = 10 * 60 * 1000;
const now2 = now1 + 60 * 1000;
const now3 = now2 + 60 * 1000;
await saveBlueBubblesCatchupCursor("test-account", 5 * 60 * 1000);
const target = makeTarget({
account: {
accountId: "test-account",
enabled: true,
configured: true,
baseUrl: "http://127.0.0.1:1234",
config: {
serverUrl: "http://127.0.0.1:1234",
password: "x",
network: { dangerouslyAllowPrivateNetwork: true },
catchup: { maxFailureRetries: 5 },
} as unknown as WebhookTarget["account"]["config"],
},
});
const fetchMessages = async () => ({
resolved: true,
messages: [makeBbMessage({ guid: "wedge", dateCreated: 7 * 60 * 1000 })],
});
const processMessageFn = async () => {
throw new Error("boom");
};
const s1 = await runBlueBubblesCatchup(target, {
now: () => now1,
fetchMessages,
processMessageFn,
});
const s2 = await runBlueBubblesCatchup(target, {
now: () => now2,
fetchMessages,
processMessageFn,
});
const s3 = await runBlueBubblesCatchup(target, {
now: () => now3,
fetchMessages,
processMessageFn,
});
expect(s1?.failed).toBe(1);
expect(s1?.givenUp).toBe(0);
expect(s2?.givenUp).toBe(0);
expect(s3?.givenUp).toBe(0);
const cursor = await loadBlueBubblesCatchupCursor("test-account");
expect(cursor?.failureRetries?.wedge).toBe(3);
// Cursor still held just below the wedge message's timestamp.
expect(cursor?.lastSeenMs).toBe(7 * 60 * 1000 - 1);
});
it("gives up on the Nth consecutive failure and records count >= max", async () => {
const now = 10 * 60 * 1000;
await saveBlueBubblesCatchupCursor("test-account", 5 * 60 * 1000);
// Pre-seed a cursor with retries at the one-before-give-up threshold
// so a single run trips the ceiling. This mirrors what would happen
// after many runs through the incremental-retry path above.
await saveBlueBubblesCatchupCursor("test-account", 5 * 60 * 1000, { wedge: 2 });
const warnings: string[] = [];
const target = makeTarget({
account: {
accountId: "test-account",
enabled: true,
configured: true,
baseUrl: "http://127.0.0.1:1234",
config: {
serverUrl: "http://127.0.0.1:1234",
password: "x",
network: { dangerouslyAllowPrivateNetwork: true },
catchup: { maxFailureRetries: 3 },
} as unknown as WebhookTarget["account"]["config"],
},
});
const summary = await runBlueBubblesCatchup(target, {
now: () => now,
fetchMessages: async () => ({
resolved: true,
messages: [makeBbMessage({ guid: "wedge", dateCreated: 7 * 60 * 1000 })],
}),
processMessageFn: async () => {
throw new Error("malformed");
},
error: (m) => warnings.push(m),
});
expect(summary?.failed).toBe(1);
expect(summary?.givenUp).toBe(1);
// Give-up no longer holds the cursor: it advances to nowMs so the
// wedge message falls out of the next query window entirely.
expect(summary?.cursorAfter).toBe(now);
const persisted = await loadBlueBubblesCatchupCursor("test-account");
expect(persisted?.lastSeenMs).toBe(now);
// Counter is persisted at the give-up value so a later sweep that
// still sees the message (e.g., because a different GUID is holding
// the cursor) will recognize the GUID as given up and skip it.
expect(persisted?.failureRetries?.wedge).toBe(3);
// Distinct WARN log line fired on the give-up transition.
const giveUpWarnings = warnings.filter((w) => w.includes("giving up on guid="));
expect(giveUpWarnings).toHaveLength(1);
expect(giveUpWarnings[0]).toContain("guid=wedge");
expect(giveUpWarnings[0]).toContain("3 consecutive failures");
});
it("skips an already-given-up GUID without re-attempting processMessage", async () => {
// Setup: the cursor file was written with wedge already at the
// give-up threshold from a prior run. On this run, the cursor is
// held by a different, still-retrying GUID (`held`), so wedge's
// timestamp falls back into the query window. Catchup must skip
// wedge without invoking processMessage on it.
const now = 10 * 60 * 1000;
await saveBlueBubblesCatchupCursor("test-account", 5 * 60 * 1000, { wedge: 3 });
const attempted: string[] = [];
const target = makeTarget({
account: {
accountId: "test-account",
enabled: true,
configured: true,
baseUrl: "http://127.0.0.1:1234",
config: {
serverUrl: "http://127.0.0.1:1234",
password: "x",
network: { dangerouslyAllowPrivateNetwork: true },
catchup: { maxFailureRetries: 3 },
} as unknown as WebhookTarget["account"]["config"],
},
});
const summary = await runBlueBubblesCatchup(target, {
now: () => now,
fetchMessages: async () => ({
resolved: true,
messages: [
makeBbMessage({ guid: "held", dateCreated: 6 * 60 * 1000 }),
makeBbMessage({ guid: "wedge", dateCreated: 7 * 60 * 1000 }),
],
}),
processMessageFn: async (m) => {
attempted.push(m.messageId ?? "?");
if (m.messageId === "held") {
throw new Error("transient");
}
},
});
// processMessage never runs for wedge.
expect(attempted).toEqual(["held"]);
expect(summary?.skippedGivenUp).toBe(1);
expect(summary?.failed).toBe(1);
expect(summary?.givenUp).toBe(0);
// Cursor held at `held` so held keeps retrying next sweep.
expect(summary?.cursorAfter).toBe(6 * 60 * 1000 - 1);
const cursor = await loadBlueBubblesCatchupCursor("test-account");
// Both entries preserved: held at count 1 (still retrying),
// wedge at count 3 (given up, sticky).
expect(cursor?.failureRetries?.held).toBe(1);
expect(cursor?.failureRetries?.wedge).toBe(3);
});
it("clears the retry counter on successful processing", async () => {
// GUID recovered after a transient failure. The counter must drop
// so the next failure starts fresh (not carrying forward stale
// retry history).
const now = 10 * 60 * 1000;
await saveBlueBubblesCatchupCursor("test-account", 5 * 60 * 1000, { flaky: 4 });
const summary = await runBlueBubblesCatchup(makeTarget(), {
now: () => now,
fetchMessages: async () => ({
resolved: true,
messages: [makeBbMessage({ guid: "flaky", dateCreated: 6 * 60 * 1000 })],
}),
processMessageFn: async () => {
/* succeeds */
},
});
expect(summary?.replayed).toBe(1);
const cursor = await loadBlueBubblesCatchupCursor("test-account");
expect(cursor?.failureRetries?.flaky).toBeUndefined();
// When the map is empty, the field itself is omitted from the file.
expect(cursor?.failureRetries).toBeUndefined();
expect(cursor?.lastSeenMs).toBe(now);
});
it("resolves 'earlier retry + later give-up' by holding cursor at earlier and skipping later", async () => {
// This is the key scenario issue #66870 exists to solve. GUID A at
// t=6min is still retrying (count=1). GUID B at t=7min has been
// failing for many runs and crosses the ceiling on this run. The
// wrong answer is "advance cursor past B to t=7min" — that would
// lose A. The right answer is "hold cursor below A, record B as
// given-up, skip B on sight next run".
const now = 10 * 60 * 1000;
await saveBlueBubblesCatchupCursor("test-account", 5 * 60 * 1000, { giveUpHere: 2 });
const target = makeTarget({
account: {
accountId: "test-account",
enabled: true,
configured: true,
baseUrl: "http://127.0.0.1:1234",
config: {
serverUrl: "http://127.0.0.1:1234",
password: "x",
network: { dangerouslyAllowPrivateNetwork: true },
catchup: { maxFailureRetries: 3 },
} as unknown as WebhookTarget["account"]["config"],
},
});
const summary = await runBlueBubblesCatchup(target, {
now: () => now,
fetchMessages: async () => ({
resolved: true,
messages: [
makeBbMessage({ guid: "retryEarlier", dateCreated: 6 * 60 * 1000 }),
makeBbMessage({ guid: "giveUpHere", dateCreated: 7 * 60 * 1000 }),
],
}),
processMessageFn: async () => {
throw new Error("failing");
},
});
expect(summary?.failed).toBe(2);
expect(summary?.givenUp).toBe(1);
// Cursor held at (earlier message ts - 1) so retryEarlier keeps retrying.
expect(summary?.cursorAfter).toBe(6 * 60 * 1000 - 1);
const cursor = await loadBlueBubblesCatchupCursor("test-account");
expect(cursor?.failureRetries?.retryEarlier).toBe(1);
// Give-up counter preserved at or above the threshold.
expect(cursor?.failureRetries?.giveUpHere).toBe(3);
});
it("uses the default retry cap when maxFailureRetries is omitted from config", async () => {
// Boot-strap: record 9 failures, then a 10th should trigger give-up
// at the default threshold. We pre-seed the counter at 9 so this
// single-run test doesn't need to iterate the whole sequence.
const now = 10 * 60 * 1000;
await saveBlueBubblesCatchupCursor("test-account", 5 * 60 * 1000, { wedge: 9 });
const warnings: string[] = [];
const summary = await runBlueBubblesCatchup(makeTarget(), {
now: () => now,
fetchMessages: async () => ({
resolved: true,
messages: [makeBbMessage({ guid: "wedge", dateCreated: 6 * 60 * 1000 })],
}),
processMessageFn: async () => {
throw new Error("boom");
},
error: (m) => warnings.push(m),
});
expect(summary?.givenUp).toBe(1);
expect(warnings.some((w) => w.includes("giving up on guid=wedge"))).toBe(true);
expect(warnings.some((w) => w.includes("10 consecutive failures"))).toBe(true);
});
it("clamps maxFailureRetries to >= 1 when configured to zero or negative", async () => {
// With clamp floor of 1, the first failure already meets count >= 1
// so catchup gives up immediately on first attempt.
const now = 10 * 60 * 1000;
await saveBlueBubblesCatchupCursor("test-account", 5 * 60 * 1000);
const summary = await runBlueBubblesCatchup(
makeTarget({
account: {
accountId: "test-account",
enabled: true,
configured: true,
baseUrl: "http://127.0.0.1:1234",
config: {
serverUrl: "http://127.0.0.1:1234",
password: "x",
network: { dangerouslyAllowPrivateNetwork: true },
catchup: { maxFailureRetries: 0 },
} as unknown as WebhookTarget["account"]["config"],
},
}),
{
now: () => now,
fetchMessages: async () => ({
resolved: true,
messages: [makeBbMessage({ guid: "wedge", dateCreated: 6 * 60 * 1000 })],
}),
processMessageFn: async () => {
throw new Error("boom");
},
},
);
expect(summary?.givenUp).toBe(1);
expect(summary?.cursorAfter).toBe(now);
});
it("loads cleanly from a legacy cursor file without a failureRetries field", async () => {
// Older cursor files (written before this field existed) must still
// parse. Round-trip: save without the field (legacy path), then
// run catchup and confirm a normal sweep proceeds.
await saveBlueBubblesCatchupCursor("test-account", 5 * 60 * 1000);
const loaded = await loadBlueBubblesCatchupCursor("test-account");
expect(loaded?.lastSeenMs).toBe(5 * 60 * 1000);
expect(loaded?.failureRetries).toBeUndefined();
const summary = await runBlueBubblesCatchup(makeTarget(), {
now: () => 10 * 60 * 1000,
fetchMessages: async () => ({
resolved: true,
messages: [makeBbMessage({ guid: "ok", dateCreated: 6 * 60 * 1000 })],
}),
processMessageFn: async () => {},
});
expect(summary?.replayed).toBe(1);
});
it("drops retry entries for GUIDs that are no longer in the query window", async () => {
// A stale entry carried in the cursor file (e.g., from an older
// run whose cursor has since advanced past its timestamp) should
// NOT be carried forward if the GUID does not appear in the
// current fetch. Otherwise the map grows without bound over time.
const now = 10 * 60 * 1000;
await saveBlueBubblesCatchupCursor("test-account", 5 * 60 * 1000, {
staleGuid: 2,
alsoStale: 5,
});
const summary = await runBlueBubblesCatchup(makeTarget(), {
now: () => now,
fetchMessages: async () => ({
resolved: true,
// Fetch returns entirely different GUIDs from the stored map.
messages: [makeBbMessage({ guid: "fresh", dateCreated: 6 * 60 * 1000 })],
}),
processMessageFn: async () => {},
});
expect(summary?.replayed).toBe(1);
const cursor = await loadBlueBubblesCatchupCursor("test-account");
// Both stale entries dropped; no new entries since the fresh message
// succeeded.
expect(cursor?.failureRetries).toBeUndefined();
});
it("preserves stickiness when a given-up GUID reappears and fails again", async () => {
// Setup: cursor advanced, but held by a newer still-retrying GUID
// `held`. The wedge GUID is already given up from a prior run and
// still appears because `held` is holding the cursor below it.
// Catchup must continue to skip wedge on sight across many runs
// without ever calling processMessage on it.
const now = 10 * 60 * 1000;
await saveBlueBubblesCatchupCursor("test-account", 5 * 60 * 1000, {
wedge: 10,
held: 1,
});
const attempted: string[] = [];
const target = makeTarget({
account: {
accountId: "test-account",
enabled: true,
configured: true,
baseUrl: "http://127.0.0.1:1234",
config: {
serverUrl: "http://127.0.0.1:1234",
password: "x",
network: { dangerouslyAllowPrivateNetwork: true },
catchup: { maxFailureRetries: 5 },
} as unknown as WebhookTarget["account"]["config"],
},
});
const fetchMessages = async () => ({
resolved: true,
messages: [
makeBbMessage({ guid: "held", dateCreated: 6 * 60 * 1000 }),
makeBbMessage({ guid: "wedge", dateCreated: 7 * 60 * 1000 }),
],
});
const processMessageFn = async () => {
throw new Error("still broken");
};
for (let i = 0; i < 3; i++) {
await runBlueBubblesCatchup(target, {
now: () => now + i,
fetchMessages,
processMessageFn: async (m) => {
attempted.push(m.messageId ?? "?");
return processMessageFn();
},
});
}
// wedge is NEVER attempted despite reappearing every sweep.
expect(attempted.filter((g) => g === "wedge")).toHaveLength(0);
// held is attempted every sweep.
expect(attempted.filter((g) => g === "held")).toHaveLength(3);
});
it("summary.skippedGivenUp counter is zero on a clean run", async () => {
const summary = await runBlueBubblesCatchup(makeTarget(), {
now: () => 10_000,
fetchMessages: async () => ({ resolved: true, messages: [] }),
processMessageFn: async () => {},
});
expect(summary?.skippedGivenUp).toBe(0);
expect(summary?.givenUp).toBe(0);
});
});
describe("saveBlueBubblesCatchupCursor + loadBlueBubblesCatchupCursor — retry map", () => {
let stateDir: string;
beforeEach(() => {
stateDir = makeStateDir();
});
afterEach(() => {
clearStateDir(stateDir);
});
it("round-trips an empty retry map by omitting the field from the persisted shape", async () => {
await saveBlueBubblesCatchupCursor("acct", 100, {});
const loaded = await loadBlueBubblesCatchupCursor("acct");
expect(loaded?.lastSeenMs).toBe(100);
expect(loaded?.failureRetries).toBeUndefined();
});
it("round-trips a populated retry map", async () => {
await saveBlueBubblesCatchupCursor("acct", 100, { a: 1, b: 9 });
const loaded = await loadBlueBubblesCatchupCursor("acct");
expect(loaded?.failureRetries).toEqual({ a: 1, b: 9 });
});
it("filters malformed retry entries during load (zero, negative, non-numeric)", async () => {
// Use the public save to produce the on-disk file, then overwrite
// its contents with a hand-crafted payload to exercise the loader's
// sanitization independently of what the saver would emit.
await saveBlueBubblesCatchupCursor("acct", 100);
const stateRoot = process.env.OPENCLAW_STATE_DIR;
if (!stateRoot) {
throw new Error("OPENCLAW_STATE_DIR must be set by the test harness");
}
const dir = path.join(stateRoot, "bluebubbles", "catchup");
const files = fs.readdirSync(dir);
expect(files).toHaveLength(1);
const firstFile = files[0];
if (!firstFile) {
throw new Error("expected a cursor file to exist after save");
}
const badCursor = {
lastSeenMs: 100,
updatedAt: 0,
failureRetries: {
good: 3,
zero: 0,
negative: -1,
notANumber: "oops",
infinite: Number.POSITIVE_INFINITY,
nan: Number.NaN,
},
};
fs.writeFileSync(path.join(dir, firstFile), JSON.stringify(badCursor));
const loaded = await loadBlueBubblesCatchupCursor("acct");
expect(loaded?.lastSeenMs).toBe(100);
expect(loaded?.failureRetries).toEqual({ good: 3 });
});
});
describe("fetchBlueBubblesMessagesSince", () => {
it("returns resolved:false when the network call throws", async () => {
// Point at a port nothing is listening on so fetch fails fast.

View File

@@ -4,6 +4,7 @@ import { readJsonFileWithFallback, writeJsonFileAtomically } from "openclaw/plug
import { resolveStateDir } from "openclaw/plugin-sdk/state-paths";
import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/temp-path";
import { resolveBlueBubblesServerAccount } from "./account-resolve.js";
import { warmupBlueBubblesInboundDedupe } from "./inbound-dedupe.js";
import { asRecord, normalizeWebhookMessage } from "./monitor-normalize.js";
import { processMessage } from "./monitor-processing.js";
import type { WebhookTarget } from "./monitor-shared.js";
@@ -21,6 +22,14 @@ const MAX_MAX_AGE_MINUTES = 12 * 60;
const DEFAULT_PER_RUN_LIMIT = 50;
const MAX_PER_RUN_LIMIT = 500;
const DEFAULT_FIRST_RUN_LOOKBACK_MINUTES = 30;
const DEFAULT_MAX_FAILURE_RETRIES = 10;
const MAX_MAX_FAILURE_RETRIES = 1_000;
// Defense-in-depth bound: a runaway retry map (e.g., a storm of unique
// failing GUIDs) should not balloon the cursor file unboundedly. When the
// map exceeds this size, we keep only the highest-count entries (the ones
// closest to being given up) and drop the rest. Realistic backlogs stay
// well under this; the bound exists to cap pathological growth.
const MAX_FAILURE_RETRY_MAP_SIZE = 5_000;
const FETCH_TIMEOUT_MS = 15_000;
export type BlueBubblesCatchupConfig = {
@@ -28,6 +37,13 @@ export type BlueBubblesCatchupConfig = {
maxAgeMinutes?: number;
perRunLimit?: number;
firstRunLookbackMinutes?: number;
/**
* Per-message retry ceiling. After this many consecutive failed
* `processMessage` attempts against the same GUID, catchup logs a WARN
* and force-advances the cursor past the wedged message instead of
* holding it indefinitely. Defaults to 10. Clamped to [1, 1000].
*/
maxFailureRetries?: number;
};
export type BlueBubblesCatchupSummary = {
@@ -35,7 +51,21 @@ export type BlueBubblesCatchupSummary = {
replayed: number;
skippedFromMe: number;
skippedPreCursor: number;
/**
* Messages whose GUID was already recorded as "given up" from a previous
* run (count >= `maxFailureRetries`). These are skipped without calling
* `processMessage` again. Lets the cursor continue advancing past the
* wedged message on the next sweep while avoiding another failed attempt.
*/
skippedGivenUp: number;
failed: number;
/**
* Messages that crossed the `maxFailureRetries` ceiling ON THIS RUN.
* Each transition triggers a WARN log line. Already-given-up messages
* in subsequent runs count under `skippedGivenUp`, not here. Lets
* operators distinguish fresh give-up events from steady-state skips.
*/
givenUp: number;
cursorBefore: number | null;
cursorAfter: number;
windowStartMs: number;
@@ -43,7 +73,24 @@ export type BlueBubblesCatchupSummary = {
fetchedCount: number;
};
export type BlueBubblesCatchupCursor = { lastSeenMs: number; updatedAt: number };
export type BlueBubblesCatchupCursor = {
lastSeenMs: number;
updatedAt: number;
/**
* Per-GUID failure counter, preserved across runs. Two states:
* - `1 <= count < maxFailureRetries`: the GUID is still retrying and
* continues to hold the cursor back.
* - `count >= maxFailureRetries`: catchup has "given up" on the GUID.
* The message is skipped on sight (no `processMessage` attempt) and
* the GUID no longer holds the cursor. The entry stays in the map
* until the cursor naturally advances past the message's timestamp
* (at which point the message stops appearing in queries entirely).
*
* A successful `processMessage` removes the entry. Optional on the
* persisted shape so older cursor files without this field load cleanly.
*/
failureRetries?: Record<string, number>;
};
function resolveStateDirFromEnv(env: NodeJS.ProcessEnv = process.env): string {
// Explicit OPENCLAW_STATE_DIR overrides take precedence (including
@@ -81,6 +128,26 @@ function resolveCursorFilePath(accountId: string): string {
);
}
function sanitizeFailureRetriesInput(raw: unknown): Record<string, number> {
// Older cursor files don't carry this field; also guard against
// hand-edited JSON or future shape drift. Drop any entry whose count is
// not a finite positive integer so downstream arithmetic stays sound.
if (!raw || typeof raw !== "object") {
return {};
}
const out: Record<string, number> = {};
for (const [guid, count] of Object.entries(raw as Record<string, unknown>)) {
if (!guid || typeof guid !== "string") {
continue;
}
if (typeof count !== "number" || !Number.isFinite(count) || count <= 0) {
continue;
}
out[guid] = Math.floor(count);
}
return out;
}
export async function loadBlueBubblesCatchupCursor(
accountId: string,
): Promise<BlueBubblesCatchupCursor | null> {
@@ -92,18 +159,66 @@ export async function loadBlueBubblesCatchupCursor(
if (typeof value.lastSeenMs !== "number" || !Number.isFinite(value.lastSeenMs)) {
return null;
}
return value;
const failureRetries = sanitizeFailureRetriesInput(value.failureRetries);
const hasRetries = Object.keys(failureRetries).length > 0;
// Keep the shape consistent with what the writer emits: only carry the
// `failureRetries` key when there's something to retry. Old cursor files
// without the field continue to round-trip to the same shape.
return {
lastSeenMs: value.lastSeenMs,
updatedAt: typeof value.updatedAt === "number" ? value.updatedAt : 0,
...(hasRetries ? { failureRetries } : {}),
};
}
export async function saveBlueBubblesCatchupCursor(
accountId: string,
lastSeenMs: number,
failureRetries?: Record<string, number>,
): Promise<void> {
const filePath = resolveCursorFilePath(accountId);
const cursor: BlueBubblesCatchupCursor = { lastSeenMs, updatedAt: Date.now() };
const sanitized = sanitizeFailureRetriesInput(failureRetries);
const hasRetries = Object.keys(sanitized).length > 0;
const cursor: BlueBubblesCatchupCursor = {
lastSeenMs,
updatedAt: Date.now(),
// Only emit the field when non-empty so unrelated cursor writes from
// the happy path don't bloat the cursor file with `"failureRetries": {}`.
...(hasRetries ? { failureRetries: sanitized } : {}),
};
await writeJsonFileAtomically(filePath, cursor);
}
/**
* Bound the retry map so a pathological storm of unique failing GUIDs
* cannot grow the cursor file without limit. Keeps the `maxSize` entries
* with the highest counts (closest to give-up) when over the bound.
*
* The map is already scoped to "currently failing, still-retrying" GUIDs
* and prunes on every run (entries not observed in the fetched window are
* dropped), so this is a defense-in-depth cap, not the primary pruning
* mechanism.
*/
function capFailureRetriesMap(
map: Record<string, number>,
maxSize: number,
): Record<string, number> {
const entries = Object.entries(map);
if (entries.length <= maxSize) {
return map;
}
// Sort by count desc; stable tiebreak on guid string so the retained set
// is deterministic across runs (important for cursor-file diffing during
// debugging).
entries.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
const capped: Record<string, number> = {};
for (let i = 0; i < maxSize; i++) {
const [guid, count] = entries[i];
capped[guid] = count;
}
return capped;
}
type FetchOpts = {
baseUrl: string;
password: string;
@@ -180,10 +295,15 @@ function clampCatchupConfig(raw?: BlueBubblesCatchupConfig) {
Math.max(raw?.firstRunLookbackMinutes ?? DEFAULT_FIRST_RUN_LOOKBACK_MINUTES, 1),
MAX_MAX_AGE_MINUTES,
);
const maxFailureRetries = Math.min(
Math.max(Math.floor(raw?.maxFailureRetries ?? DEFAULT_MAX_FAILURE_RETRIES), 1),
MAX_MAX_FAILURE_RETRIES,
);
return {
maxAgeMs: maxAgeMinutes * 60_000,
perRunLimit,
firstRunLookbackMs: firstRunLookbackMinutes * 60_000,
maxFailureRetries,
};
}
@@ -247,10 +367,11 @@ async function runBlueBubblesCatchupInner(
const procFn = deps.processMessageFn ?? processMessage;
const accountId = target.account.accountId;
const { maxAgeMs, perRunLimit, firstRunLookbackMs } = clampCatchupConfig(raw);
const { maxAgeMs, perRunLimit, firstRunLookbackMs, maxFailureRetries } = clampCatchupConfig(raw);
const nowMs = now();
const existing = await loadBlueBubblesCatchupCursor(accountId).catch(() => null);
const cursorBefore = existing?.lastSeenMs ?? null;
const prevRetries = existing?.failureRetries ?? {};
// Catchup runs once per gateway startup (called from monitor.ts after
// webhook target registration). We deliberately do NOT short-circuit on
@@ -295,6 +416,15 @@ async function runBlueBubblesCatchupInner(
return null;
}
// Ensure legacy→hashed dedupe file migration runs and the on-disk store
// is warm before we replay. Without this, an upgrade from a version that
// used the old `${safe}.json` naming to the current `${safe}__${hash}.json`
// would start with an empty dedupe cache and re-dispatch every message in
// the catchup window — producing duplicate replies.
await warmupBlueBubblesInboundDedupe(accountId).catch((err) => {
error?.(`[${accountId}] BlueBubbles catchup: dedupe warmup failed: ${String(err)}`);
});
const { resolved, messages } = await fetchFn(windowStartMs, perRunLimit, {
baseUrl,
password,
@@ -306,7 +436,9 @@ async function runBlueBubblesCatchupInner(
replayed: 0,
skippedFromMe: 0,
skippedPreCursor: 0,
skippedGivenUp: 0,
failed: 0,
givenUp: 0,
cursorBefore,
cursorAfter: nowMs,
windowStartMs,
@@ -320,18 +452,31 @@ async function runBlueBubblesCatchupInner(
return summary;
}
// Track the earliest timestamp where `processMessage` threw so we never
// advance the cursor past a retryable failure. Normalize failures (the
// record didn't yield a usable NormalizedWebhookMessage) are treated as
// permanent skips and do NOT block cursor advance — those payloads are
// unlikely to ever normalize on retry, and blocking on them would wedge
// catchup forever.
// Track the earliest timestamp where `processMessage` threw *and* the
// failing message has not yet crossed the per-GUID retry ceiling, so we
// never advance the cursor past a retryable failure. Normalize failures
// (the record didn't yield a usable NormalizedWebhookMessage) are
// treated as permanent skips and do NOT block cursor advance — those
// payloads are unlikely to ever normalize on retry, and blocking on
// them would wedge catchup forever. Given-up messages (count >= max)
// also do NOT contribute here; see `skippedGivenUp` below.
let earliestProcessFailureTs: number | null = null;
// Track the latest fetched message timestamp regardless of fate, so a
// truncated query (fetchedCount === perRunLimit) can advance the cursor
// exactly to the page boundary. Without this, the unfetched tail past
// the cap is permanently unreachable.
let latestFetchedTs = windowStartMs;
// Next-run retry map. Built from scratch each run so entries for GUIDs
// that didn't appear in this fetch are dropped (the cursor has
// advanced past them and they will never be queried again). Entries we
// do carry forward encode two states via the stored count:
// - `1 <= count < maxFailureRetries`: still-retrying, holds cursor.
// - `count >= maxFailureRetries`: given-up, skipped on sight without
// another `processMessage` attempt. Preserving the count is what
// keeps the give-up state sticky across runs when an earlier
// still-retrying failure is holding the cursor and the given-up
// message keeps reappearing in the query window.
const nextRetries: Record<string, number> = {};
for (const rec of messages) {
// Defense in depth: the server-side `after:` filter should already
@@ -353,6 +498,30 @@ async function runBlueBubblesCatchupInner(
continue;
}
// Skip tapback/reaction/balloon events. These carry an
// `associatedMessageGuid` pointing at the parent text message and
// have a different `guid` of their own. The live webhook path handles
// balloons via the debouncer, which coalesces them with their parent.
// Without debouncing here, replaying a balloon would dispatch it as a
// standalone message — producing a duplicate reply to the parent.
//
// Guard: only skip when `associatedMessageType` is set (tapbacks and
// reactions — e.g., "like", 2000) OR `balloonBundleId` is set (URL
// previews, stickers). iMessage threaded replies use a separate
// `threadOriginatorGuid` field and do NOT set either of these, so
// they pass through for correct catchup replay.
const assocGuid =
typeof rec.associatedMessageGuid === "string"
? rec.associatedMessageGuid.trim()
: typeof rec.associated_message_guid === "string"
? rec.associated_message_guid.trim()
: "";
const assocType = rec.associatedMessageType ?? rec.associated_message_type;
const balloonId = typeof rec.balloonBundleId === "string" ? rec.balloonBundleId.trim() : "";
if (assocGuid && (assocType != null || balloonId)) {
continue;
}
const normalized = normalizeWebhookMessage({ type: "new-message", data: rec });
if (!normalized) {
summary.failed++;
@@ -363,15 +532,62 @@ async function runBlueBubblesCatchupInner(
continue;
}
// Prefer the normalized messageId (what the dedupe cache uses) so the
// retry counter and downstream dedupe key agree on identity. Fall
// back to the raw BB `guid` only when normalization didn't supply one.
const retryKey = normalized.messageId ?? (typeof rec.guid === "string" ? rec.guid : "");
// Already-given-up GUIDs are skipped without another `processMessage`
// attempt. This is what lets catchup make forward progress through an
// earlier, still-retrying failure while not burning cycles re-running
// a permanently broken message every sweep.
const prevCount = retryKey ? (prevRetries[retryKey] ?? 0) : 0;
if (retryKey && prevCount >= maxFailureRetries) {
summary.skippedGivenUp++;
// Preserve the count so give-up stickiness survives this run.
nextRetries[retryKey] = prevCount;
continue;
}
try {
await procFn(normalized, target);
summary.replayed++;
// Success clears any accumulated retries for this GUID. Since we
// build `nextRetries` from scratch rather than mutating
// `prevRetries`, simply NOT copying the entry is the clear. (We
// still need this branch so readers understand the lifecycle.)
} catch (err) {
summary.failed++;
if (ts > 0 && (earliestProcessFailureTs === null || ts < earliestProcessFailureTs)) {
earliestProcessFailureTs = ts;
const nextCount = prevCount + 1;
if (retryKey && nextCount >= maxFailureRetries) {
// Crossing the ceiling this run: log WARN once and record the
// give-up in the persisted map. Don't contribute to
// `earliestProcessFailureTs` — we're intentionally letting the
// cursor advance past this GUID on the next sweep.
summary.givenUp++;
nextRetries[retryKey] = nextCount;
error?.(
`[${accountId}] BlueBubbles catchup: giving up on guid=${retryKey} ` +
`after ${nextCount} consecutive failures; future sweeps will skip ` +
`this message. timestamp=${ts}: ${String(err)}`,
);
} else {
// Still retrying: count this failure and hold the cursor so the
// next sweep retries the same window. (retryKey may be empty in
// the unusual case where neither normalizer nor raw payload
// carried a GUID — in that case we hold the cursor but cannot
// increment a counter, matching pre-retry-cap behavior.)
if (retryKey) {
nextRetries[retryKey] = nextCount;
}
if (ts > 0 && (earliestProcessFailureTs === null || ts < earliestProcessFailureTs)) {
earliestProcessFailureTs = ts;
}
error?.(
`[${accountId}] BlueBubbles catchup: processMessage failed (retry ` +
`${nextCount}/${maxFailureRetries}): ${String(err)}`,
);
}
error?.(`[${accountId}] BlueBubbles catchup: processMessage failed: ${String(err)}`);
}
}
@@ -381,10 +597,17 @@ async function runBlueBubblesCatchupInner(
// this sweep finished (avoiding stuck rescans of a message with
// `dateCreated > nowMs` from minor clock skew between BB host and
// gateway host).
// - On retryable failure (any `processMessage` throw): hold the cursor
// just before the earliest failed timestamp so the next run retries
// from there. The inbound-dedupe cache from #66230 keeps successfully
// replayed messages from being re-processed.
// - On retryable failure (any still-retrying `processMessage` throw,
// where the GUID has NOT crossed `maxFailureRetries`): hold the
// cursor just before the earliest still-retrying failed timestamp so
// the next run retries from there. The inbound-dedupe cache from
// #66230 keeps successfully replayed messages from being re-processed.
// - On give-up (failures that crossed `maxFailureRetries`): the GUID
// is recorded in the persisted retry map with `count >= max` and
// skipped on sight in subsequent runs (without another processMessage
// attempt). Give-up GUIDs intentionally do NOT hold the cursor, so
// the cursor can advance past them naturally — this is what unwedges
// catchup from a permanently malformed message (issue #66870).
// - On truncation (fetched === perRunLimit): advance only to the latest
// fetched timestamp so the next run picks up from the page boundary.
// Otherwise the unfetched tail past the cap (which can be substantial
@@ -400,14 +623,18 @@ async function runBlueBubblesCatchupInner(
nextCursorMs = Math.min(Math.max(latestFetchedTs, cursorBefore ?? windowStartMs), nowMs);
}
summary.cursorAfter = nextCursorMs;
await saveBlueBubblesCatchupCursor(accountId, nextCursorMs).catch((err) => {
// Cap the retry map before writing — defense in depth against a storm
// of unique failing GUIDs ballooning the cursor file.
const retriesToPersist = capFailureRetriesMap(nextRetries, MAX_FAILURE_RETRY_MAP_SIZE);
await saveBlueBubblesCatchupCursor(accountId, nextCursorMs, retriesToPersist).catch((err) => {
error?.(`[${accountId}] BlueBubbles catchup: cursor save failed: ${String(err)}`);
});
log?.(
`[${accountId}] BlueBubbles catchup: replayed=${summary.replayed} ` +
`skipped_fromMe=${summary.skippedFromMe} skipped_preCursor=${summary.skippedPreCursor} ` +
`failed=${summary.failed} fetched=${summary.fetchedCount} ` +
`skipped_givenUp=${summary.skippedGivenUp} failed=${summary.failed} ` +
`given_up=${summary.givenUp} fetched=${summary.fetchedCount} ` +
`window_ms=${nowMs - windowStartMs}`,
);

View File

@@ -50,6 +50,14 @@ const bluebubblesCatchupSchema = z
perRunLimit: z.number().int().positive().optional(),
/** First-run lookback used when no cursor has been persisted yet. Clamped to [1, 720]. */
firstRunLookbackMinutes: z.number().int().positive().optional(),
/**
* Consecutive-failure ceiling per message GUID. After this many failed
* processMessage attempts against the same GUID, catchup logs a WARN
* and skips the message on subsequent sweeps (letting the cursor
* advance past a permanently malformed payload). Defaults to 10.
* Clamped to [1, 1000].
*/
maxFailureRetries: z.number().int().positive().optional(),
})
.strict()
.optional();

View File

@@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it } from "vitest";
import {
_resetBlueBubblesInboundDedupForTest,
claimBlueBubblesInboundMessage,
resolveBlueBubblesInboundDedupeKey,
} from "./inbound-dedupe.js";
async function claimAndFinalize(guid: string | undefined, accountId: string): Promise<string> {
@@ -56,3 +57,38 @@ describe("claimBlueBubblesInboundMessage", () => {
expect(await claimAndFinalize("g1", "acc")).toBe("claimed");
});
});
describe("resolveBlueBubblesInboundDedupeKey", () => {
it("returns messageId for new-message events", () => {
expect(resolveBlueBubblesInboundDedupeKey({ messageId: "msg-1" })).toBe("msg-1");
});
it("returns associatedMessageGuid for balloon events", () => {
expect(
resolveBlueBubblesInboundDedupeKey({
messageId: "balloon-1",
balloonBundleId: "com.apple.messages.URLBalloonProvider",
associatedMessageGuid: "msg-1",
}),
).toBe("msg-1");
});
it("suffixes key with :updated for updated-message events", () => {
expect(
resolveBlueBubblesInboundDedupeKey({ messageId: "msg-1", eventType: "updated-message" }),
).toBe("msg-1:updated");
});
it("updated-message and new-message for same GUID produce distinct keys", () => {
const newKey = resolveBlueBubblesInboundDedupeKey({ messageId: "msg-1" });
const updatedKey = resolveBlueBubblesInboundDedupeKey({
messageId: "msg-1",
eventType: "updated-message",
});
expect(newKey).not.toBe(updatedKey);
});
it("returns undefined when messageId is missing", () => {
expect(resolveBlueBubblesInboundDedupeKey({})).toBeUndefined();
});
});

View File

@@ -1,4 +1,5 @@
import { createHash } from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import { type ClaimableDedupe, createClaimableDedupe } from "openclaw/plugin-sdk/persistent-dedupe";
import { resolveStateDir } from "openclaw/plugin-sdk/state-paths";
@@ -33,6 +34,11 @@ function resolveStateDirFromEnv(env: NodeJS.ProcessEnv = process.env): string {
return resolveStateDir(env);
}
function resolveLegacyNamespaceFilePath(namespace: string): string {
const safe = namespace.replace(/[^a-zA-Z0-9_-]/g, "_") || "global";
return path.join(resolveStateDirFromEnv(), "bluebubbles", "inbound-dedupe", `${safe}.json`);
}
function resolveNamespaceFilePath(namespace: string): string {
// Keep a readable prefix for operator debugging, but suffix with a short
// hash of the raw namespace so account IDs that only differ by
@@ -40,12 +46,42 @@ function resolveNamespaceFilePath(namespace: string): string {
// onto the same file.
const safePrefix = namespace.replace(/[^a-zA-Z0-9_-]/g, "_") || "ns";
const hash = createHash("sha256").update(namespace, "utf8").digest("hex").slice(0, 12);
return path.join(
resolveStateDirFromEnv(),
"bluebubbles",
"inbound-dedupe",
`${safePrefix}__${hash}.json`,
);
const dir = path.join(resolveStateDirFromEnv(), "bluebubbles", "inbound-dedupe");
const newPath = path.join(dir, `${safePrefix}__${hash}.json`);
// One-time migration: earlier beta shipped `${safe}.json` (no hash).
// Rename so the upgrade preserves existing dedupe entries instead of
// starting from an empty file and replaying already-handled messages.
migrateLegacyDedupeFile(namespace, newPath);
return newPath;
}
const migratedNamespaces = new Set<string>();
function migrateLegacyDedupeFile(namespace: string, newPath: string): void {
if (migratedNamespaces.has(namespace)) {
return;
}
migratedNamespaces.add(namespace);
try {
const legacyPath = resolveLegacyNamespaceFilePath(namespace);
if (legacyPath === newPath) {
return;
}
if (!fs.existsSync(legacyPath)) {
return;
}
if (!fs.existsSync(newPath)) {
fs.renameSync(legacyPath, newPath);
} else {
// Both exist: new file is authoritative; remove the stale legacy.
fs.unlinkSync(legacyPath);
}
} catch {
// Best-effort migration; a missed rename is strictly less harmful
// than crashing the module load path.
}
}
function buildPersistentImpl(): ClaimableDedupe {
@@ -100,15 +136,27 @@ function sanitizeGuid(guid: string | undefined | null): string | null {
export function resolveBlueBubblesInboundDedupeKey(
message: Pick<
NormalizedWebhookMessage,
"messageId" | "balloonBundleId" | "associatedMessageGuid"
"messageId" | "balloonBundleId" | "associatedMessageGuid" | "eventType"
>,
): string | undefined {
const balloonBundleId = message.balloonBundleId?.trim();
const associatedMessageGuid = message.associatedMessageGuid?.trim();
let base: string | undefined;
if (balloonBundleId && associatedMessageGuid) {
return associatedMessageGuid;
base = associatedMessageGuid;
} else {
base = message.messageId?.trim() || undefined;
}
return message.messageId?.trim() || undefined;
if (!base) {
return undefined;
}
// `updated-message` events get a distinct key so they are not rejected as
// duplicates of the already-committed `new-message` for the same GUID.
// This lets attachment-carrying follow-up webhooks through. (#65430, #52277)
if (message.eventType === "updated-message") {
return `${base}:updated`;
}
return base;
}
export type InboundDedupeClaim =
@@ -162,6 +210,18 @@ export async function claimBlueBubblesInboundMessage(params: {
};
}
/**
* Ensure the legacy→hashed dedupe file migration runs and the on-disk
* store is warmed into memory for the given account. Call before any
* catchup replay so already-handled GUIDs are recognized even when the
* file-naming convention changed between versions.
*/
export async function warmupBlueBubblesInboundDedupe(accountId: string): Promise<void> {
// Trigger the migration side-effect inside resolveNamespaceFilePath.
resolveNamespaceFilePath(accountId);
await impl.warmup(accountId);
}
/**
* Reset inbound dedupe state between tests. Installs an in-memory-only
* implementation so tests do not hit disk, avoiding file-lock timing issues

View File

@@ -34,7 +34,7 @@ function readNumberLike(record: Record<string, unknown> | null, key: string): nu
return parseFiniteNumber(record[key]);
}
function extractAttachments(message: Record<string, unknown>): BlueBubblesAttachment[] {
export function extractAttachments(message: Record<string, unknown>): BlueBubblesAttachment[] {
const raw = message["attachments"];
if (!Array.isArray(raw)) {
return [];
@@ -477,6 +477,8 @@ export type NormalizedWebhookMessage = {
replyToId?: string;
replyToBody?: string;
replyToSender?: string;
/** Webhook event type preserved for dedup key differentiation. */
eventType?: string;
};
export type NormalizedWebhookReaction = {
@@ -687,6 +689,7 @@ function extractMessagePayload(payload: Record<string, unknown>): Record<string,
export function normalizeWebhookMessage(
payload: Record<string, unknown>,
options?: { eventType?: string },
): NormalizedWebhookMessage | null {
const message = extractMessagePayload(payload);
if (!message) {
@@ -774,6 +777,7 @@ export function normalizeWebhookMessage(
replyToId: replyMetadata.replyToId,
replyToBody: replyMetadata.replyToBody,
replyToSender: replyMetadata.replyToSender,
eventType: options?.eventType,
};
}

View File

@@ -9,7 +9,10 @@ import {
normalizeOptionalLowercaseString,
normalizeOptionalString,
} from "openclaw/plugin-sdk/text-runtime";
import { downloadBlueBubblesAttachment } from "./attachments.js";
import {
downloadBlueBubblesAttachment,
fetchBlueBubblesMessageAttachments,
} from "./attachments.js";
import { markBlueBubblesChatRead, sendBlueBubblesTyping } from "./chat.js";
import { resolveBlueBubblesConversationRoute } from "./conversation-route.js";
import { fetchBlueBubblesHistory } from "./history.js";
@@ -692,8 +695,52 @@ async function processMessageAfterDedupe(
const isGroup = typeof groupFlag === "boolean" ? groupFlag : message.isGroup;
const text = message.text.trim();
const attachments = message.attachments ?? [];
const placeholder = buildMessagePlaceholder(message);
let attachments = message.attachments ?? [];
const baseUrl = normalizeSecretInputString(account.config.serverUrl);
const password = normalizeSecretInputString(account.config.password);
// BlueBubbles may fire the webhook before attachment indexing is complete,
// so the initial `attachments` array can be empty for messages that actually
// have media. When the message text is empty (image-only) or this is an
// `updated-message` event, wait briefly and re-fetch from the BB API as a
// fallback for cases where BB doesn't send a follow-up webhook. (#65430, #67437)
// This must run before the !rawBody guard below, otherwise image-only messages
// with empty attachments are dropped before the retry can fire.
const retryMessageId = message.messageId?.trim();
const shouldRetryAttachments =
attachments.length === 0 &&
retryMessageId &&
baseUrl &&
password &&
(text.length === 0 || message.eventType === "updated-message");
if (shouldRetryAttachments) {
try {
await new Promise<void>((resolve) => setTimeout(resolve, 2_000));
const fetched = await fetchBlueBubblesMessageAttachments(retryMessageId, {
baseUrl,
password,
timeoutMs: 10_000,
allowPrivateNetwork: isPrivateNetworkOptInEnabled(account.config),
});
if (fetched.length > 0) {
logVerbose(
core,
runtime,
`attachment retry found ${fetched.length} attachment(s) for msgId=${message.messageId}`,
);
attachments = fetched;
}
} catch (err) {
logVerbose(
core,
runtime,
`attachment retry failed for msgId=${message.messageId}: ${String(err)}`,
);
}
}
// Recompute placeholder from resolved attachments (may have been updated by retry).
const placeholder = buildMessagePlaceholder({ ...message, attachments });
// Check if text is a tapback pattern (e.g., 'Loved "hello"') and transform to emoji format
// For tapbacks, we'll append [[reply_to:N]] at the end; for regular messages, prepend it
const tapbackContext = resolveTapbackContext(message);
@@ -1019,9 +1066,6 @@ async function processMessageAfterDedupe(
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({

View File

@@ -249,11 +249,22 @@ export async function handleBlueBubblesWebhookRequest(
return true;
}
const reaction = normalizeWebhookReaction(payload);
// Normalize the webhook message early so the attachment-update detection
// below sees attachments under any supported wrapper format (`payload.data`,
// `payload.message`, `payload.data.message`, JSON-string payloads), not just
// raw `payload.data.attachments`. (#65430, #67510)
const message = reaction ? null : normalizeWebhookMessage(payload, { eventType });
// BlueBubbles fires `updated-message` when attachments are indexed after the
// initial `new-message` (which may arrive with attachments: []). Let those
// through so the agent can ingest the image. (#65430)
const isAttachmentUpdate =
eventType === "updated-message" && (message?.attachments?.length ?? 0) > 0;
if (
(eventType === "updated-message" ||
eventType === "message-reaction" ||
eventType === "reaction") &&
!reaction
!reaction &&
!isAttachmentUpdate
) {
res.statusCode = 200;
res.end("ok");
@@ -261,12 +272,11 @@ export async function handleBlueBubblesWebhookRequest(
logVerbose(
firstTarget.core,
firstTarget.runtime,
`webhook ignored ${eventType || "event"} without reaction`,
`webhook ignored ${eventType || "event"} (no reaction or attachment update)`,
);
}
return true;
}
const message = reaction ? null : normalizeWebhookMessage(payload);
if (!message && !reaction) {
res.statusCode = 400;
res.end("invalid payload");

View File

@@ -175,10 +175,18 @@ export async function blueBubblesFetchWithTimeout(
await release();
}
}
// Strip `dispatcher` from init — the SSRF guard may have attached a bundled-undici
// dispatcher that is incompatible with Node 22+'s built-in undici backing globalThis.fetch().
// Passing it through causes a silent TypeError (invalid onRequestStart method).
// The SSRF validation already completed upstream in fetchWithSsrFGuard before calling
// this function as fetchImpl, so stripping the dispatcher does not weaken security. (#64105)
const { dispatcher: _dispatcher, ...safeInit } = (init ?? {}) as RequestInit & {
dispatcher?: unknown;
};
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
return await fetch(url, { ...init, signal: controller.signal });
return await fetch(url, { ...safeInit, signal: controller.signal });
} finally {
clearTimeout(timer);
}

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/brave-plugin",
"version": "2026.4.15-beta.1",
"version": "2026.4.16",
"private": true,
"description": "OpenClaw Brave plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/browser-plugin",
"version": "2026.4.15-beta.1",
"version": "2026.4.16",
"private": true,
"description": "OpenClaw browser tool plugin",
"type": "module",

View File

@@ -3,6 +3,6 @@ export const DEFAULT_BROWSER_EVALUATE_ENABLED = true;
export const DEFAULT_OPENCLAW_BROWSER_COLOR = "#FF4500";
export const DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME = "openclaw";
export const DEFAULT_BROWSER_DEFAULT_PROFILE_NAME = "openclaw";
export const DEFAULT_AI_SNAPSHOT_MAX_CHARS = 80_000;
export const DEFAULT_AI_SNAPSHOT_EFFICIENT_MAX_CHARS = 10_000;
export const DEFAULT_AI_SNAPSHOT_MAX_CHARS = 40_000;
export const DEFAULT_AI_SNAPSHOT_EFFICIENT_MAX_CHARS = 8_000;
export const DEFAULT_AI_SNAPSHOT_EFFICIENT_DEPTH = 6;

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/byteplus-provider",
"version": "2026.4.15-beta.1",
"version": "2026.4.16",
"private": true,
"description": "OpenClaw BytePlus provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/chutes-provider",
"version": "2026.4.15-beta.1",
"version": "2026.4.16",
"private": true,
"description": "OpenClaw Chutes.ai provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/cloudflare-ai-gateway-provider",
"version": "2026.4.15-beta.1",
"version": "2026.4.16",
"private": true,
"description": "OpenClaw Cloudflare AI Gateway provider plugin",
"type": "module",

View File

@@ -3,6 +3,9 @@
"name": "Codex",
"description": "Codex app-server harness and Codex-managed GPT model catalog.",
"providers": ["codex"],
"activation": {
"onAgentHarnesses": ["codex"]
},
"commandAliases": [
{
"name": "codex",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/codex",
"version": "2026.4.15-beta.1",
"version": "2026.4.16",
"description": "OpenClaw Codex harness and model provider plugin",
"type": "module",
"dependencies": {

View File

@@ -200,10 +200,14 @@ describe("CodexAppServerClient", () => {
expect(process.unref).toHaveBeenCalledTimes(1);
});
it("reads the Codex version from the app-server user agent", () => {
expect(readCodexVersionFromUserAgent("Codex Desktop/0.118.0")).toBe("0.118.0");
expect(readCodexVersionFromUserAgent("openclaw/0.118.0 (macOS; test)")).toBe("0.118.0");
expect(readCodexVersionFromUserAgent("codex_cli_rs/0.118.1-dev (linux; test)")).toBe(
"0.118.1-dev",
);
expect(readCodexVersionFromUserAgent("Codex Desktop/not-a-version")).toBeUndefined();
expect(readCodexVersionFromUserAgent("Codex Desktop/0.118")).toBeUndefined();
expect(readCodexVersionFromUserAgent("openclaw/0.118.0abc")).toBeUndefined();
expect(readCodexVersionFromUserAgent("missing-version")).toBeUndefined();
});

View File

@@ -373,8 +373,11 @@ function assertSupportedCodexAppServerVersion(response: CodexInitializeResponse)
export function readCodexVersionFromUserAgent(userAgent: string | undefined): string | undefined {
// Codex returns `<originator>/<codex-version> ...`; the originator can be
// OpenClaw or an env override, so only the slash-delimited version is stable.
const match = userAgent?.match(/^[^/\s]+\/(\d+\.\d+\.\d+(?:[-+][^\s()]*)?)/);
// OpenClaw, Codex Desktop, or an env override, so only the slash-delimited
// version in the leading product field is stable.
const match = userAgent?.match(
/^[^/]+\/(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?)(?:[\s(]|$)/,
);
return match?.[1];
}

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/comfy-provider",
"version": "2026.4.15-beta.1",
"version": "2026.4.16",
"private": true,
"description": "OpenClaw ComfyUI provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/copilot-proxy",
"version": "2026.4.15-beta.1",
"version": "2026.4.16",
"private": true,
"description": "OpenClaw Copilot Proxy provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/deepgram-provider",
"version": "2026.4.15-beta.1",
"version": "2026.4.16",
"private": true,
"description": "OpenClaw Deepgram media-understanding provider",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/deepseek-provider",
"version": "2026.4.15-beta.1",
"version": "2026.4.16",
"private": true,
"description": "OpenClaw DeepSeek provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/diagnostics-otel",
"version": "2026.4.15-beta.1",
"version": "2026.4.16",
"description": "OpenClaw diagnostics OpenTelemetry exporter",
"type": "module",
"dependencies": {
@@ -24,10 +24,10 @@
"./index.ts"
],
"compat": {
"pluginApi": ">=2026.4.15-beta.1"
"pluginApi": ">=2026.4.16"
},
"build": {
"openclawVersion": "2026.4.15-beta.1"
"openclawVersion": "2026.4.16"
},
"release": {
"publishToClawHub": true,

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/diffs",
"version": "2026.4.15-beta.1",
"version": "2026.4.16",
"private": true,
"description": "OpenClaw diff viewer plugin",
"type": "module",

View File

@@ -0,0 +1,6 @@
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
import { inspectDiscordAccount } from "./src/account-inspect.js";
export function inspectDiscordReadOnlyAccount(cfg: OpenClawConfig, accountId?: string | null) {
return inspectDiscordAccount({ cfg, accountId });
}

View File

@@ -22,6 +22,10 @@ export default defineBundledChannelEntry({
specifier: "./runtime-api.js",
exportName: "setDiscordRuntime",
},
accountInspect: {
specifier: "./account-inspect-api.js",
exportName: "inspectDiscordReadOnlyAccount",
},
registerFull(api) {
api.on("subagent_spawning", async (event) => {
const { handleDiscordSubagentSpawning } = await loadDiscordSubagentHooksModule();

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/discord",
"version": "2026.4.15-beta.1",
"version": "2026.4.16",
"description": "OpenClaw Discord channel plugin",
"type": "module",
"dependencies": {
@@ -16,7 +16,7 @@
"openclaw": "workspace:*"
},
"peerDependencies": {
"openclaw": ">=2026.4.15-beta.1"
"openclaw": ">=2026.4.16"
},
"peerDependenciesMeta": {
"openclaw": {
@@ -52,10 +52,10 @@
"minHostVersion": ">=2026.4.10"
},
"compat": {
"pluginApi": ">=2026.4.15-beta.1"
"pluginApi": ">=2026.4.16"
},
"build": {
"openclawVersion": "2026.4.15-beta.1"
"openclawVersion": "2026.4.16"
},
"bundle": {
"stageRuntimeDependencies": true

View File

@@ -0,0 +1 @@
export { collectDiscordSecurityAuditFindings } from "./src/security-audit.js";

View File

@@ -1,6 +1,7 @@
import { Type } from "@sinclair/typebox";
import type { ChannelMessageActionContext } from "openclaw/plugin-sdk/channel-contract";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
import { withEnv } from "openclaw/plugin-sdk/testing";
import { describe, expect, it, vi } from "vitest";
const handleDiscordMessageActionMock = vi.hoisted(() =>
@@ -15,20 +16,22 @@ const { discordMessageActions } = await import("./channel-actions.js");
describe("discordMessageActions", () => {
it("returns no tool actions when no token-sourced Discord accounts are enabled", () => {
const discovery = discordMessageActions.describeMessageTool?.({
cfg: {
channels: {
discord: {
enabled: true,
withEnv({ DISCORD_BOT_TOKEN: undefined }, () => {
const discovery = discordMessageActions.describeMessageTool?.({
cfg: {
channels: {
discord: {
enabled: true,
},
},
},
} as OpenClawConfig,
});
} as OpenClawConfig,
});
expect(discovery).toEqual({
actions: [],
capabilities: [],
schema: null,
expect(discovery).toEqual({
actions: [],
capabilities: [],
schema: null,
});
});
});

View File

@@ -1,15 +1,19 @@
import { coerceNativeSetting, normalizeAllowFromList } from "openclaw/plugin-sdk/channel-policy";
import { readChannelAllowFromStore } from "openclaw/plugin-sdk/conversation-runtime";
import { isDangerousNameMatchingEnabled } from "openclaw/plugin-sdk/dangerous-name-runtime";
import {
isDangerousNameMatchingEnabled,
resolveNativeCommandsEnabled,
resolveNativeSkillsEnabled,
} from "openclaw/plugin-sdk/config-runtime";
import { readChannelAllowFromStore } from "openclaw/plugin-sdk/conversation-runtime";
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
} from "openclaw/plugin-sdk/native-command-config-runtime";
import type { ResolvedDiscordAccount } from "./accounts.js";
import type { OpenClawConfig } from "./runtime-api.js";
import { isDiscordMutableAllowEntry } from "./security-doctor.js";
function normalizeOptionalString(value: string | null | undefined): string | undefined {
const normalized = value?.trim();
return normalized ? normalized : undefined;
}
function addDiscordNameBasedEntries(params: {
target: Set<string>;
values: unknown;

View File

@@ -5,9 +5,14 @@ import path from "node:path";
import type { Readable } from "node:stream";
import { ChannelType, type Client, ReadyListener } from "@buape/carbon";
import type { VoicePlugin } from "@buape/carbon/voice";
import { resolveAgentDir } from "openclaw/plugin-sdk/agent-runtime";
import { agentCommandFromIngress } from "openclaw/plugin-sdk/agent-runtime";
import { resolveTtsConfig, type ResolvedTtsConfig } from "openclaw/plugin-sdk/agent-runtime";
import {
agentCommandFromIngress,
getTtsProvider,
resolveAgentDir,
resolveTtsConfig,
resolveTtsPrefsPath,
type ResolvedTtsConfig,
} from "openclaw/plugin-sdk/agent-runtime";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
import type { DiscordAccountConfig, TtsConfig } from "openclaw/plugin-sdk/config-runtime";
import { resolveAgentRoute } from "openclaw/plugin-sdk/routing";
@@ -809,6 +814,7 @@ export class DiscordVoiceManager {
const directive = parseTtsDirectives(replyText, ttsConfig.modelOverrides, {
cfg: ttsCfg,
providerConfigs: ttsConfig.providerConfigs,
preferredProviderId: getTtsProvider(ttsConfig, resolveTtsPrefsPath(ttsConfig)),
});
const rawSpeakText = directive.overrides.ttsText ?? directive.cleanedText.trim();
const speakText = sanitizeVoiceReplyTextForSpeech(rawSpeakText, speaker.label);

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/duckduckgo-plugin",
"version": "2026.4.15-beta.1",
"version": "2026.4.16",
"private": true,
"description": "OpenClaw DuckDuckGo plugin",
"type": "module",

View File

@@ -1,5 +1,6 @@
{
"id": "elevenlabs",
"enabledByDefault": true,
"contracts": {
"speechProviders": ["elevenlabs"]
},

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/elevenlabs-speech",
"version": "2026.4.15-beta.1",
"version": "2026.4.16",
"private": true,
"description": "OpenClaw ElevenLabs speech plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/exa-plugin",
"version": "2026.4.15-beta.1",
"version": "2026.4.16",
"private": true,
"description": "OpenClaw Exa plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/fal-provider",
"version": "2026.4.15-beta.1",
"version": "2026.4.16",
"private": true,
"description": "OpenClaw fal provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/feishu",
"version": "2026.4.15-beta.1",
"version": "2026.4.16",
"description": "OpenClaw Feishu/Lark channel plugin (community maintained by @m1heng)",
"type": "module",
"dependencies": {
@@ -13,7 +13,7 @@
"openclaw": "workspace:*"
},
"peerDependencies": {
"openclaw": ">=2026.4.15-beta.1"
"openclaw": ">=2026.4.16"
},
"peerDependenciesMeta": {
"openclaw": {
@@ -44,10 +44,10 @@
"minHostVersion": ">=2026.4.10"
},
"compat": {
"pluginApi": ">=2026.4.15-beta.1"
"pluginApi": ">=2026.4.16"
},
"build": {
"openclawVersion": "2026.4.15-beta.1"
"openclawVersion": "2026.4.16"
},
"bundle": {
"stageRuntimeDependencies": true

View File

@@ -1 +1,81 @@
export { collectFeishuSecurityAuditFindings } from "./src/security-audit.js";
import type { OpenClawConfig } from "./runtime-api.js";
function asRecord(value: unknown): Record<string, unknown> | undefined {
return value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: undefined;
}
function hasNonEmptyString(value: unknown): boolean {
return typeof value === "string" && value.trim().length > 0;
}
function hasConfiguredSecretInput(value: unknown): boolean {
if (hasNonEmptyString(value)) {
return true;
}
const record = asRecord(value);
return (
Boolean(record) &&
hasNonEmptyString(record?.source) &&
hasNonEmptyString(record?.provider) &&
hasNonEmptyString(record?.id)
);
}
function isFeishuDocToolEnabled(cfg: OpenClawConfig): boolean {
const channels = asRecord(cfg.channels);
const feishu = asRecord(channels?.feishu);
if (!feishu || feishu.enabled === false) {
return false;
}
const baseTools = asRecord(feishu.tools);
const baseDocEnabled = baseTools?.doc !== false;
const baseAppId = hasNonEmptyString(feishu.appId);
const baseAppSecret = hasConfiguredSecretInput(feishu.appSecret);
const baseConfigured = baseAppId && baseAppSecret;
const accounts = asRecord(feishu.accounts);
if (!accounts || Object.keys(accounts).length === 0) {
return baseDocEnabled && baseConfigured;
}
for (const accountValue of Object.values(accounts)) {
const account = asRecord(accountValue) ?? {};
if (account.enabled === false) {
continue;
}
const accountTools = asRecord(account.tools);
const effectiveTools = accountTools ?? baseTools;
const docEnabled = effectiveTools?.doc !== false;
if (!docEnabled) {
continue;
}
const accountConfigured =
(hasNonEmptyString(account.appId) || baseAppId) &&
(hasConfiguredSecretInput(account.appSecret) || baseAppSecret);
if (accountConfigured) {
return true;
}
}
return false;
}
export function collectFeishuSecurityAuditFindings(params: { cfg: OpenClawConfig }) {
if (!isFeishuDocToolEnabled(params.cfg)) {
return [];
}
return [
{
checkId: "channels.feishu.doc_owner_open_id",
severity: "warn" as const,
title: "Feishu doc create can grant requester permissions",
detail:
'channels.feishu tools include "doc"; feishu_doc action "create" can grant document access to the trusted requesting Feishu user.',
remediation:
"Disable channels.feishu.tools.doc when not needed, and restrict tool access for untrusted prompts.",
},
];
}

View File

@@ -1,5 +1,5 @@
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
import { hasConfiguredSecretInput } from "openclaw/plugin-sdk/setup";
import { hasConfiguredSecretInput } from "openclaw/plugin-sdk/secret-input";
import type { OpenClawConfig } from "../runtime-api.js";
import { asRecord, hasNonEmptyString } from "./comment-shared.js";
function isFeishuDocToolEnabled(cfg: OpenClawConfig): boolean {

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/firecrawl-plugin",
"version": "2026.4.15-beta.1",
"version": "2026.4.16",
"private": true,
"description": "OpenClaw Firecrawl plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/fireworks-provider",
"version": "2026.4.15-beta.1",
"version": "2026.4.16",
"private": true,
"description": "OpenClaw Fireworks provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/github-copilot-provider",
"version": "2026.4.15-beta.1",
"version": "2026.4.16",
"private": true,
"description": "OpenClaw GitHub Copilot provider plugin",
"type": "module",

View File

@@ -5,18 +5,19 @@ import { buildGoogleGeminiCliBackend } from "./cli-backend.js";
import { registerGoogleGeminiCliProvider } from "./gemini-cli-provider.js";
import { buildGoogleMusicGenerationProvider } from "./music-generation-provider.js";
import { registerGoogleProvider } from "./provider-registration.js";
import { buildGoogleSpeechProvider } from "./speech-provider.js";
import { createGeminiWebSearchProvider } from "./src/gemini-web-search-provider.js";
import { buildGoogleVideoGenerationProvider } from "./video-generation-provider.js";
let googleImageGenerationProviderPromise: Promise<ImageGenerationProvider> | null = null;
let googleMediaUnderstandingProviderPromise: Promise<MediaUnderstandingProvider> | null = null;
type GoogleMediaUnderstandingProvider = MediaUnderstandingProvider & {
describeImage: NonNullable<MediaUnderstandingProvider["describeImage"]>;
describeImages: NonNullable<MediaUnderstandingProvider["describeImages"]>;
transcribeAudio: NonNullable<MediaUnderstandingProvider["transcribeAudio"]>;
describeVideo: NonNullable<MediaUnderstandingProvider["describeVideo"]>;
};
type GoogleMediaUnderstandingProvider = Required<
Pick<
MediaUnderstandingProvider,
"describeImage" | "describeImages" | "transcribeAudio" | "describeVideo"
>
>;
async function loadGoogleImageGenerationProvider(): Promise<ImageGenerationProvider> {
if (!googleImageGenerationProviderPromise) {
@@ -113,6 +114,7 @@ export default definePluginEntry({
api.registerImageGenerationProvider(createLazyGoogleImageGenerationProvider());
api.registerMediaUnderstandingProvider(createLazyGoogleMediaUnderstandingProvider());
api.registerMusicGenerationProvider(buildGoogleMusicGenerationProvider());
api.registerSpeechProvider(buildGoogleSpeechProvider());
api.registerVideoGenerationProvider(buildGoogleVideoGenerationProvider());
api.registerWebSearchProvider(createGeminiWebSearchProvider());
},

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